Skip to main content

Example: 3D Building Map

3D map of lower Manhatten

You need at least osm2pgsql version 1.7.0 for this example.

For some buildings OSM data contains height information. Sometimes it is specified directly in the height tag, usually in meters. If not, we can have a look at the building:levels tag which sometimes contains the number of levels a building has. Assuming a level is on average 4 meters high, we can estimate the building height from that.

In a real world application you’d have to look into more details of those and other tags, the height is sometimes given in feet for instance. But for this demo this simplified setup should suffice.

To get some nice sample data, lets look at New York. You can use the Protomaps download service to get the extract. Zoom to New York and draw a polygon around the tip of Manhatten, add the name “newyork” and click on “Create Extract”. In a few seconds you’ll get a download button to download a file newyork.osm.pbf.

Create a database buildings and import the data:

osm2pgsql -d buildings -O flex -S 3dbuildings.lua newyork.osm.pbf

You can visualize this with QGIS by adding the buildings layer as usual, then choosing “2.5 D” as symbolization in the layers Symbology dialog. Choose the “height” attribute in the “Height” dropdown and play around with the rest of the settings.

Here is the configuration file you’ll need:

Download Lua config
local buildings = osm2pgsql.define_area_table('buildings', {
    -- Define an autoincrementing id column, QGIS likes a unique id on the table
    { column = 'id', sql_type = 'serial', create_only = true },
    { column = 'height', type = 'real', not_null = true },
    { column = 'geom', type = 'polygon', not_null = true },
})

-- calculate the height of the building from the tags
function calc_height(tags)
    -- if an explicit 'height' tag is available use that
    -- could be extended to also allow height in feet
    if tags.height then
        local h = tonumber(tags.height)
        if h and h > 0 and h < 900 then
            return h
        end
    end

    -- use 'building:levels' tag and assume each level is 4 meters high
    local levels = tags['building:levels']
    if levels then
        local l = tonumber(levels)
        if l and l > 0 and l < 200 then
            return l * 4
        end
    end

    -- fall back to default height if we can't find any useful tags
    return 12
end

function add_building(tags, geom)
    buildings:insert({
        height = calc_height(tags),
        geom = geom
    })
end

function osm2pgsql.process_way(object)
    if object.is_closed and object.tags.building then
        add_building(object.tags, object:as_polygon())
    end
end

function osm2pgsql.process_relation(object)
    if object.tags.type == 'multipolygon' and object.tags.building then
        geom = object:as_multipolygon()
        for g in geom:geometries() do
            add_building(object.tags, g)
        end
    end
end