PostGIS/OSM tutorial#

Introduction#

Luftdatenpumpe uses a PostGIS POINT to store the geolocation of LDI stations.

In the query expression examples displayed below, we use the geocoordinates POINT(9.1800132 48.7784485) for Stuttgart, Germany.

This coordinate has been returned by querying Nominatim for city==stuttgart.

Details#

See also:

Excerpts from the documentation#

4.2.2. When to use Geography Data type over Geometry data type If your data is global or covers a continental region, you may find that GEOGRAPHY allows you to build a system without having to worry about projection details. You store your data in longitude/latitude, and use the functions that have been defined on GEOGRAPHY.

https://postgis.net/docs/using_postgis_dbmanagement.html#PostGIS_GeographyVSGeometry

4.2.3.1. Do you calculate on the sphere or the spheroid? By default, all distance and area calculations are done on the spheroid.

https://postgis.net/docs/using_postgis_dbmanagement.html#idm1644

PostGIS query expressions#

Sort by distance#

-- Find specified number of nearest stations through sorting by distance,
-- display name and textual representation of coordinates.
SELECT name, ST_AsText(geopoint) AS geopoint
FROM ldi_stations
ORDER BY geopoint <-> 'POINT(9.18001 48.77844)'
LIMIT 5;
-- Same as above, but display name and GeoJson representation of coordinates.
SELECT name, ST_AsGeoJson(geopoint) AS geojson
FROM ldi_stations
ORDER BY geopoint <-> 'POINT(9.18001 48.77844)'
LIMIT 5;

Match within range#

-- Find all stations within specified range in meters while sorting by distance.
-- The computation uses a bounding box expanded from the specified geographic point,
-- by specifying a single distance with which the box will be expanded in all directions.
-- https://postgis.net/docs/ST_DWithin.html
-- https://postgis.net/docs/ST_Expand.html
SELECT name, ST_AsText(geopoint) AS geopoint
FROM ldi_stations
WHERE ST_DWithin(geopoint, 'POINT(9.18001 48.77844)', 3000)
ORDER BY geopoint <-> 'POINT(9.18001 48.77844)';

In order to save on repeating this POINT coordinates here, we can alias it into a virtual position field:

-- List all stations within 3000 meters around specified coordinates.
-- This is what OpenStreetMap/Nominatim thinks the center of Stuttgart is.
WITH stuttgart AS (
    SELECT ST_GeographyFromText('POINT(9.18001 48.77844)') AS position
)
SELECT name, ST_AsText(geopoint) AS geopoint
FROM ldi_stations, stuttgart
WHERE ST_DWithin(geopoint, stuttgart.position, 3000)
ORDER BY geopoint <-> stuttgart.position;

Mixing in OSM/Nominatim#

As these expressions use the constraint osm_city = 'Stuttgart', OSM data is involved. Centroid of OSM input coordinates

-- Compute center of city as the arithmetic mean of the input coordinates.
-- http://postgis.net/workshops/postgis-intro/geometry_returning.html
-- http://postgis.net/workshops/postgis-intro/advanced_geometry_construction.html
-- https://postgis.net/docs/ST_Collect.html
-- https://postgis.net/docs/ST_Centroid.html
SELECT ST_AsText(ST_Centroid(ST_Collect(geopoint::geometry))::geography) AS geopoint
FROM ldi_network
WHERE osm_city = 'Stuttgart';
-- Using the formula above, find specified number of nearest stations by city name.
-- https://stackoverflow.com/a/19859047
-- https://postgis.net/docs/ST_X.html
-- https://postgis.net/docs/ST_Y.html
WITH center_of AS (
    SELECT ST_Centroid(ST_Collect(geopoint::geometry))::geography AS position FROM ldi_network WHERE osm_city = 'Stuttgart'
)
SELECT name, ST_AsText(geopoint) AS geopoint
FROM ldi_stations, center_of
ORDER BY geopoint <-> center_of.position
LIMIT 5;
-- List all stations within 3000 meters around what LDI thinks the center of Stuttgart is.
WITH stuttgart AS (
    SELECT ST_Centroid(ST_Collect(geopoint::geometry))::geography AS position FROM ldi_network WHERE osm_city = 'Stuttgart'
)
SELECT name, ST_AsText(geopoint) AS geopoint
FROM ldi_stations, stuttgart
WHERE ST_DWithin(geopoint, stuttgart.position, 3000)
ORDER BY geopoint <-> stuttgart.position;

Accessing the OSM/Nominatim API#

By using the PostgreSQL extension pgsql-http, which is effectively a »HTTP client for PostgreSQL«, you can directly access the Nominatim HTTP API for asking for a geotext field from a specified city or other location by using the polygon_text=1 query parameter.

The geotext field yielded by the response of the API is in PostGIS-compatible POINT(lon lat) format already.

Print coordinate by asking for city=stuttgart, effectively roundtripping through HTTP and PostGIS:

-- https://github.com/pramsey/pgsql-http
-- https://wiki.openstreetmap.org/wiki/Nominatim
-- https://www.postgresql.org/docs/9.3/functions-json.html
-- TODO: Provide this as a native Grafana datasource and/or variable somehow?
CREATE EXTENSION http;

-- Nominatim request subselect.
WITH stuttgart AS (
    SELECT
        ST_GeographyFromText(content::json->0->>'geotext') AS position
    FROM
        http_get('https://nominatim.hiveeyes.org/search.php?format=jsonv2&addressdetails=1&polygon_text=1&city=stuttgart')
)

-- Print position as text.
-- You should do more sophisticated things here, see below.
SELECT ST_AsText(position) FROM stuttgart;

Match within range#

-- List all stations within 3000 meters around specified city.
-- The coordinates of the city is coming from OpenStreetMap/Nominatim.
WITH stuttgart AS (
    SELECT ST_GeographyFromText(content::json->0->>'geotext') AS position
    FROM http_get('https://nominatim.hiveeyes.org/search.php?format=jsonv2&addressdetails=1&polygon_text=1&city=stuttgart')
)
SELECT name, ST_AsText(geopoint) AS geopoint
FROM ldi_stations, stuttgart
WHERE ST_DWithin(geopoint, stuttgart.position, 3000)
ORDER BY geopoint <-> stuttgart.position;

Using the local database with OSM data#

Using the --reverse-geocode option, Luftdatenpumpe will query Nominatim on its own behalf, and will populate the PostGIS database with information from OSM.

This data can be queried conveniently without having to reach out to Nominatim using pgsql-http.

Todo

Show some example queries.