OSM Quick 3D
OSM to native QGIS 2D/3D styled layers with flat-roof building massing • v0.20.4
1. Overview
OSM Quick 3D downloads OpenStreetMap data for a user-defined study area and produces native QGIS vector layers styled by function, with optional 3D building extrusion through QGIS's own 3D symbology engine. Unlike its sibling plugin osm_3d_model (which opens a browser-based Three.js viewer), OSM Quick 3D keeps everything inside QGIS as editable vector layers—suited for larger areas, analysis, printing, and manual editing. The plugin provides a dock-based, two-tab control panel with a Build tab for data acquisition and a Theme & Style tab for live retuning.
2. Theoretical Foundations
2.1 OpenStreetMap as Urban Data Infrastructure
Haklay and Weber (2008) established OSM as a viable volunteered geographic information (VGI) platform, demonstrating that user-generated street maps can match or exceed the completeness of authoritative datasets in well-mapped urban areas. Fan et al. (2014) specifically validated OSM building footprint quality, finding that in German urban areas, OSM building data achieved >80% completeness with positional accuracy comparable to cadastral surveys. The plugin leverages this by treating OSM as the primary data source for rapid urban morphology assessment, with the Overpass API serving as a query interface.
2.2 Level of Detail in 3D City Models
Kolbe (2009) formalised the CityGML LOD (Level of Detail) hierarchy for 3D city models: LOD1 comprises prismatic building blocks with flat roofs derived from footprints and height attributes, LOD2 adds roof structures, and LOD3 adds architectural details. OSM Quick 3D produces LOD1 models: flat-roofed extrusions from OSM building footprints with heights derived from OSM tags or typology-based defaults. Biljecki et al. (2017) demonstrated that LOD1 models generated from OSM without elevation data can achieve high accuracy when building height attributes are present, supporting the plugin's three-tier height fallback strategy.
3. Study Area Definition
Boundary Sources and Shapes
Two area sources: Visible map extent (canvas) or Selected features (active layer, dissolved via unaryUnion()). Five boundary shapes: Rectangle (raw bounding box), Rounded rectangle (corners at ~18% of short side via double-buffer technique: buffer(-corner, 12) then buffer(+corner, 12)), Circle (inscribed, radius = min(width, height) / 2, 64 segments), Hexagon (pointy-top orientation, 6 points at 60° intervals starting at −30°, inscribed), and Polygon (user-selected feature, dissolved if multiple).
The area is auto-projected to its local UTM zone via utm_epsg_for(): zone = \(\lfloor(\lambda + 180)/6\rfloor + 1\), clamped 1–60, EPSG = 32600 + zone (north) or 32700 + zone (south). Area is clamped to a configurable maximum in square kilometres (default 6 km2) by shrinking uniformly around the centroid.
4. OSM Data Download
Overpass API Pipeline with Multi-Mirror Fallback
The plugin constructs a single Overpass QL query requesting all features within the WGS84 bounding box: ways and relations with building, highway, waterway (river/stream/canal/drain/ditch/riverbank), leisure (park/garden/playground/pitch/nature_reserve/common/dog_park/golf_course), landuse (forest/grass/meadow/recreation_ground/cemetery/reservoir/basin/village_green/orchard/vineyard/farmland/allotments/greenfield), natural (wood/scrub/water/grassland/heath), amenity (parking/marketplace), and place=square; plus nodes for trees, bus stops (also public_transport=platform), benches, street lamps, and waste baskets. The query uses out body geom; for full geometry, with a 60-second timeout.
Multi-mirror fallback: Three endpoints are tried in order (overpass-api.de, overpass.kumi.systems, overpass.private.coffee) with a 10-second additional HTTP read timeout. HTTP 429 (rate-limited) triggers the next mirror. The first endpoint returning valid, non-empty JSON wins. Feedback messages distinguish first query from mirror fallback.
5. Data Layers
Twelve Layer Types
The plugin creates 12 in-memory vector layers (all Multi* geometry for features potentially split by clipping):
| Layer | Geometry | Key Attributes |
|---|---|---|
| OSM Buildings | MultiPolygon | osm_id, building, building_levels, height, footprint_m2, gfa_m2, name |
| OSM Roads | MultiLineString | osm_id, highway, width, name |
| OSM Bike lanes | MultiLineString | osm_id, highway, width, name |
| OSM Greens | MultiPolygon | osm_id, leisure, landuse, natural, amenity, highway, place, name |
| OSM Waterlines | MultiLineString | osm_id, waterway, width, name |
| OSM Water areas | MultiPolygon | osm_id, natural, waterway, landuse, name |
| OSM Trees | Point | osm_id, natural, height |
| OSM Bus stops | Point | osm_id, highway, name |
| OSM Benches | Point | osm_id, amenity |
| OSM Street lights | Point | osm_id, highway |
| OSM Trash bins | Point | osm_id, amenity |
| OSM Base (optional) | Polygon | Ground plinth extruded from -2 m below terrain to -0.15 m |
Each layer is clipped against the valid study boundary (geom.makeValid() if needed, then intersection(clip_boundary)). Multipolygon assembly handles OSM relations with outer/inner ring roles for buildings with courtyards. Geometry classification follows a priority order: tree nodes → water areas → parking lots → paved squares → buildings → roads (with cycleway deduplication) → waterways → greens.
6. Building Height Logic
Three-Tier Fallback with 57 Type-Specific Defaults
Height is derived per-building by _building_levels():
- Parse
building:levelsorlevelstag, rounded to integer, clamped to ≥1. Handles semicolons ("3;4") and unit suffixes ("m", "ft", etc.). - If no floor count: parse
heighttag in metres, divide by 3.0, round, clamp to ≥1. - Function-based default from a 57-entry dictionary keyed by OSM building type: apartments=4, residential=4, detached=2, house=2, bungalow=1, office=5, commercial=3, school=3, university=4, hospital=5, church/mosque/temple/synagogue/chapel=1 (cathedral=2), warehouse=1, garage=1, shed=1, unknown=3.
- Add
roof:levelsif present. Final: max(1, base + roof_levels).
For the 3D extrusion expression: coalesce(to_real("height"), to_int("building_levels") × 3, 9). The nine-metre default (3 floors at 3 m/floor) serves as the ultimate fallback when neither OSM height tag nor building:levels tag is present and the building type is unrecognised.
7. Building Colour Modes (8)
Function-Based and Height-Based Schemes
| Mode | 2D Renderer | Description |
|---|---|---|
| By function (OSM use) | QgsCategorizedSymbolRenderer | 6 categories: residential, commercial, industrial, civic, worship, other. Static palette per theme. |
| Graduated by height | Data-defined fill colour | 3 classification methods: Continuous (10-step color_rgb(scale_linear(...))), Discrete (5 equal-interval classes), Quantile (5 classes at 20/40/60/80th percentiles). |
| Soft tinted gray | Data-defined fill | Ramp: #e8e9e8 → #6e746f |
| Soft tinted warm | Data-defined fill | Ramp: #ede2d4 → #8a7460 |
| Soft teal | Data-defined fill | Ramp: #dbeae7 → #3c7c77 |
| Soft salmon | Data-defined fill | Ramp: #f1ded7 → #b9776a |
| Soft purple | Data-defined fill | Ramp: #e4dded → #6f5c86 |
| Soft sand | Data-defined fill | Ramp: #ece6d6 → #9a8a63 |
The height expression reads actual layer values: coalesce(to_real("height"), to_int("building_levels") × 3, 9.0). Break values and 5 interpolated colours are cached per layer ID for performance.
8. Map Themes (12)
Coordinated Colour Palettes
| Theme | Background | Character |
|---|---|---|
| Muted Planning | #ffffff | Professional muted european tones; default |
| Tokyo Cyber | #0a0b10 | Dark, neon purples/oranges/cyans on near-black |
| Editorial Paper | #fdfbf7 | Warm ivory paper with sepia-brown accents |
| Nordic Frost | #f5f7fa | Cool minimal blues on clean white |
| Monochrome Noir | #1e1e1e | High-contrast grayscale from near-white to near-black |
| Civic Atlas | #f7f8f2 | Clean survey tones: muted greens, blues, greys |
| Mediterranean Survey | #fbf4e6 | Warm sun/sea palette: terracotta, olive, turquoise |
| Night Print | #11161a | Dark cartographic: gold roads, dark greens, light buildings |
| Anime Cel | #dff3ff | Bright cartoon pastels: candy pink, lime, sky blue |
| Desert Dunes | #f6e7c8 | Warm arid: sand beige, rust orange, olive green |
| Pastel Candy | #fff5fb | Soft kawaii: lavender pinks, mint greens, baby blues |
| Vaporwave | #2a1a4a | Neon retro: deep purple base + hot pink/cyan/mint |
Each theme sets: background colour, 6 building function colours, building height ramp (low→high), major road colour (motorway/primary/secondary/tertiary), minor road colour (residential/service/foot/other), greens colour (park/forest/pitch/cemetery/water category), water colour, and tree colour. Theme changes are live: they update the canvas background, recolour all layers, and refresh the 3D view without re-downloading data.
9. Native QGIS 3D Extrusion
Rule-Based 3D Renderer with Phong Materials
Building 3D extrusion uses QgsRuleBased3DRenderer with per-function or per-height-class child rules:
- Function mode: 6 rules (residential, commercial, industrial, civic, worship, other ELSE) each filtering by OSM building tag via a SQL-like expression:
lower("building") IN ('apartments', 'residential', 'house', ...). - Ramp modes: 10 continuous-height rules or 5 discrete/quantile rules, each filtering by
EXTRUSION_EXPRESSIONrange. - Each rule has a
QgsPolygon3DSymbolwith: data-defined extrusion height (QgsProperty.fromExpression(extrusion_expression × scale)), back faces enabled, rendered facades = walls+roofs (value 3), edge rendering (colour #2b3436, width 0.15), altitude clamping to terrain. QgsPhongMaterialSettingswith per-rule colour, plus data-defined diffuse colour for graduated modes.
Ground base plinth: QgsPolygon3DSymbol extruded from -2.0 m below terrain to -0.15 m (prevents Z-fighting with terrain mesh). Theme-harmonised colour. Data-defined properties for height, extrusion depth, and offset.
Tree canopies: QgsPoint3DSymbol with Sphere shape (2.5 m radius). Transformed with QMatrix4x4().translate(0, 0, 2.5) so spheres rest on the surface. Phong material with theme tree colour.
Managed 3D canvas: Reuses a named Qgs3DMapCanvas ("OSM Quick 3D Scene") via iface.createNewMapCanvas3D(). Flat terrain with configurable tile resolution. Camera: distance = max(width, height, 100) × 1.35, pitch = 45°, zero bearing. Basemap clipping via QgsMapClippingRegion in ClipPainterOnly mode.
10. Tree Scattering Algorithm
Deterministic Procedural Tree Placement
Procedural trees are placed inside wooded green polygons (forest, wood, park, garden, etc.) using a deterministic seeded RNG:
| Green Type | Spacing (m2/tree) | Height Range (m) |
|---|---|---|
| Forest / Wood | 75 | 7–13 |
| Orchard | 110 | 4–7 |
| Park | 170 | 5–9 |
| Garden | 210 | 4–7 |
| Nature Reserve | 180 | 5–10 |
| Village Green | 220 | 4–8 |
| Recreation Ground | 230 | 4–8 |
| Cemetery | 260 | 5–9 |
| Scrub | 280 | 2.5–4.5 |
| Allotments | 320 | 3–5 |
| Grass | 400 | 3–6 |
| Meadow | 440 | 3–6 |
RNG: A Knuth MMIX linear congruential generator seeded from the polygon's bounding box coordinates, area, and green type code: seed = (bbox_minX × 73856093) XOR (bbox_minY × 19349663) XOR (area × 83492791) XOR (kind_code × 2654435761), masked to 32 bits. This ensures that re-running the same area produces identical tree placement every time.
Global cap: 500 total trees; per-polygon cap: 130. Target per polygon: min(130, remaining_cap, floor(polygon_area / spacing)). Rejection sampling: random points within the bounding box, accepted if geom_utm.contains(pt), up to 8× target attempts.
11. Expression-Based Styling
QGIS Expressions for Data-Driven Rendering
The plugin uses QGIS expression strings extensively, avoiding per-feature Python loops:
BUILDING_CLASS_EXPR: Maps 40+ OSM building tag values into 6 categories (residential, commercial, industrial, civic, worship, other) via nestedCASE WHEN ... THEN 'category' ... END.ROAD_CLASS_EXPR: Maps 18+ highway tag values into 8 hierarchy levels (major, primary, secondary, tertiary, residential, service, foot, other).ROAD_WIDTH_M_EXPR:coalesce(to_real("width"), CASE ...)with defaults from 24 m (motorway) down to 2.5 m (footway).ROAD_RENDER_WIDTH_EXPR: Clamps road width between 1.5 m and 30 m for rendering.GREEN_CLASS_EXPR: Maps leisure/landuse/natural/amenity/highway/place tags into 8 green categories.building_color_expression(): Returns either hex-literal CASE expressions (function/discrete modes) orcolor_rgb(scale_linear(...))expressions (continuous ramp).EXTRUSION_EXPRESSION:coalesce(to_real("height"), to_int("building_levels") × 3, 9)multiplied by height exaggeration factor.
All colour and width properties are applied as data-defined overrides (QgsProperty.fromExpression()) rather than fixed renderer values, enabling live retuning without rebuilding the renderer.
12. Theme & Style Tab
Post-Build Live Retuning
The Theme & Style tab operates on an existing OSM Quick 3D layer group. Controls: group selector, theme combo with preview swatches, six colour pickers (major roads, minor roads, greens, water, trees, scene background), building colour mode, classification method (continuous/discrete/quantile), height exaggeration (0.5–5.0x), building opacity (0–100%), tree canopy size, tree colour, 3D view controls (Open/Refresh/Focus/Close), per-layer 3D visibility checkboxes, scene resolution selector, and basemap clip region management.
The key design pattern: _apply_changes() freezes the canvas, applies all current state in one batch, unfreezes, and refreshes—avoiding intermediate redraws. The 3D view is refreshed via _refresh_3d_view() with delayed retry timers (600 ms, 1500 ms, 3000 ms). A scene info panel shows real-time stats: 3D canvas open/closed, visible layer count, basemap clip status, and resolution.
13. Caching System
SHA-256 Disk Cache with 7-Day TTL
Responses are cached as JSON files in %TEMP%/osm_quick_3d_cache/. The cache key is SHA-256 of the full Overpass query string (truncated to 40 hex characters). TTL: 604800 seconds (7 days). On cache miss: fetch from Overpass, write back. On cache hit: validate payload structure; if corrupt, discard and re-fetch. A "Clear cache" button reports files removed and bytes freed. The "Use OSM cache" checkbox allows users to bypass the cache for fresh data.
14. Parameters and Controls
Build Tab
| Control | Type | Range/Options | Default |
|---|---|---|---|
| Area source | Combo | Canvas / Selection | Canvas |
| Area shape | Combo | Rectangle / Rounded / Circle / Hexagon / Polygon | Rectangle |
| Max area | QDoubleSpinBox | 0.1 – 200 km2 | 6 km2 |
| Layer checkboxes | QCheckBox | Buildings / Roads / Water / Greens / Trees / Furniture / Labels | All on |
| 3D extrusion | QCheckBox | On / Off | Off |
| Height exaggeration | QDoubleSpinBox | 0.5–5.0x | 1.5x |
| Map theme | Combo | 12 themes | Muted Planning |
| Building colours | Combo | 8 modes | By function |
| Height classes | Combo | Continuous / Discrete / Quantile | Continuous |
| Ground base | QCheckBox | On / Off | Off |
| Save to GeoPackage | QCheckBox | On / Off | Off |
| Use OSM cache | QCheckBox | On / Off | On |
15. Workflow Guide
- Open the OSM Quick 3D dock from the toolbar icon.
- On the Build tab, optionally click Add OSM basemap for an XYZ reference tile layer (EPSG:3857).
- Set area source, shape, and max area limit. The area is clamped to the configured maximum.
- Choose which layers to download. Enable 3D extrusion, set theme and building colour mode as desired.
- Click Download & build 3D. The plugin fetches data via Overpass, processes geometries, applies 2D styling, and optionally builds 3D extrusion.
- Layers appear in a tree group named "OSM Quick 3D -- EPSG:xxxxx". A managed 3D Map View opens if enabled.
- Switch to the Theme & Style tab for live retuning of colours, themes, building opacity, tree size, and per-layer 3D visibility without re-downloading data.
- Optionally export to GeoPackage (with layer prefix) for persistence across sessions.
16. Technical Notes
- Multi-mirror Overpass: Three endpoints tried in order with 60-second query timeout + 10-second HTTP timeout. Cache: SHA-256 keyed JSON in temp directory, 7-day TTL.
- Tree scattering: Deterministic Knuth MMIX LCG seeded from polygon footprint coordinates. 500 total cap, 130 per-polygon cap.
- Expression-based 2D/3D: All colours, road widths, extrusion heights use QGIS expressions (
CASE,coalesce,scale_linear,color_rgb) viaQgsProperty.fromExpression(). - Cross-version compatibility: Extensive try/except fallback chains for scoped vs unscoped enum access,
QgsRuleBased3DRenderer.Ruleconstructor signatures (4 variants tried), and PyQt5/PyQt6 differences. - No external dependencies: Pure qgis.core / qgis.gui / qgis.PyQt. No .ui files—all UI built programmatically in
dock.py. - Geometry clipping robustness: All geometries transformed to WGS84 for Overpass, reprojected to UTM for clipping. Invalid geometries pass through
makeValid(). Features that fall entirely within the boundary after intersection failure are preserved.
17. Literature
- Haklay, M. & Weber, P. (2008). OpenStreetMap: User-generated street maps. IEEE Pervasive Computing, 7(4), 12–18. DOI: 10.1109/MPRV.2008.80
- Biljecki, F., Ledoux, H., & Stoter, J. (2017). Generating 3D city models without elevation data. Computers, Environment and Urban Systems, 64, 1–18. DOI: 10.1016/j.compenvurbsys.2017.01.001
- Kolbe, T. H. (2009). Representing and exchanging 3D city models with CityGML. In J. Lee & S. Zlatanova (Eds.), 3D Geo-Information Sciences (pp. 15–31). Springer. DOI: 10.1007/978-3-540-87395-2_2
- Fan, H., Zipf, A., Fu, Q., & Neis, P. (2014). Quality assessment for building footprints data on OpenStreetMap. International Journal of Geographical Information Science, 28(4), 700–719. DOI: 10.1080/13658816.2013.867495
- Goetz, M. (2013). Towards generating highly detailed 3D CityGML models from OpenStreetMap. International Journal of Geographical Information Science, 27(5), 845–865. DOI: 10.1080/13658816.2012.721552
- Goodchild, M. F. (2007). Citizens as sensors: The world of volunteered geography. GeoJournal, 69(4), 211–221. DOI: 10.1007/s10708-007-9111-y
- Mooney, P. & Minghini, M. (2017). A review of OpenStreetMap data. In G. Foody et al. (Eds.), Mapping and the Citizen Sensor (pp. 37–59). Ubiquity Press. DOI: 10.5334/bbf.c