Skip to content

GeoJSON AOI Parser

HOT

Parse and normalize a GeoJSON area of interest, using PostGIS.

Publish Docs Publish Test Package version Downloads License


📖 Documentation: https://hotosm.github.io/geojson-aoi-parser/

🖥️ Source Code: https://github.com/hotosm/geojson-aoi-parser


Why do we need this?

  • We generally need an Area of Interest (AOI) specified for software to run on a geospatial area.
  • GeoJSON is a simple exchange format to communicate this AOI.
  • We only care about Polygon data types, but GeoJSON data can be quite variable, with many options for presenting data.
  • The goal of this package is to receive GeoJSON data in various forms, then produce a normalised output that can be used for further processing.

Priorities

  • Flexible data input: file path, file bytes, dict, string.
  • Flexible geometry input:
  • Polygon
  • MultiPolygons
  • GeometryCollection
  • Feature
  • FeatureCollection
  • KML (Polygons from each Placemark, keeping the name property)
  • Splits multipolygons into featcol of individual polygons.
  • Handle geometries nested inside GeometryCollection.
  • Remove any z-dimension coordinates.
  • Skip non-Polygon geometries, with a warning.
  • Warn user if CRS is provided, in a coordinate system other than EPSG:4326.
  • Optionally merge all polygons into a single polygon (merge=True).
  • Normalised output: FeatureCollection containing Polygon geoms.

All processing happens in PostGIS, so the only dependency is psycopg.

Merging Polygons

parse_aoi(db, raw_geojson, merge=True) returns a single Polygon:

  • Overlapping or touching polygons are unioned, and holes are removed.
  • Disjoint polygons are joined by their convex hull.

Other File Formats

Shapefile, GeoPackage, etc are not supported, as reading them requires GDAL. Convert them first, e.g. ogr2ogr -f GeoJSON aoi.geojson aoi.gpkg.

Capturing The Warnings

If the GeoJSON has an invalid CRS, coordinates seem off, or non-Polygon geometries are skipped, a warning will be raised.

To install:

pip install geojson-aoi-parser

Basic example:

from geojson_aoi import parse_aoi

polygon_geojson = {
    "type": "Polygon",
    "coordinates": [[[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]],
}

# Where 'db' is some upstream database connection.
feat_col = parse_aoi(db, polygon_geojson)

print(feat_col)
# {
#   'type': 'FeatureCollection',
#   'features': [
#       {
#           'type': 'Feature',
#           'geometry':
#               {
#                   'type': 'Polygon',
#                   'coordinates': [[[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]]
#                },
#           'properties': {}
#        }
#     ]
# }

Basic async example:

from geojson_aoi import parse_aoi_async

polygon_geojson = {
    "type": "Polygon",
    "coordinates": [[[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]],
}

# Where 'db' is some upstream database connection.
feat_col = await parse_aoi_async(db, polygon_geojson)

print(feat_col)
# {
#   'type': 'FeatureCollection',
#   'features': [
#       {
#           'type': 'Feature',
#           'geometry':
#               {
#                   'type': 'Polygon',
#                   'coordinates': [[[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]]
#                },
#           'properties': {}
#        }
#     ]
# }

To halt execution when a warning is raised and act on it:

try:
    featcol = parse_aoi(db, raw_geojson)
except UserWarning as warning:
    log.error(warning.message)
    msg = "Using a valid CRS is mandatory!"
    log.error(msg)
    raise HTTPException(HTTPStatus.BAD_REQUEST, detail=msg)

To record warnings, but allow execution to continue:

import warnings

with warnings.catch_warnings(record=True) as recorded_warnings:
    featcol = parse_aoi(db, raw_geojson)

if recorded_warnings:
    for warning in recorded_warnings:
        if isinstance(warning.message, UserWarning):
            # do stuff with warning
            logger.warning(f"A warning was encountered: {warning.message}")

Create a New DB Connection

  • If your app upstream already has a psycopg connection, this can be passed through.
  • If you require a new database connection, the connection parameters can be defined as DbConfig object variables:
from geojson_aoi import parse_aoi, DbConfig

db = DbConfig(
    dbname="db1",
    user="user1",
    password="pass1",
    host="localhost",
    port="5432",
)

featcol = parse_aoi(db.get_connection_string(), raw_geojson)
  • Or alternatively as variables from your system environment:
GEOJSON_AOI_DB_NAME=aoi
GEOJSON_AOI_DB_USER=aoi
GEOJSON_AOI_DB_PASSWORD=pass
GEOJSON_AOI_DB_HOST=localhost
GEOJSON_AOI_DB_PORT=5432

then

from geojson_aoi import parse_aoi, DbConfig

featcol = parse_aoi(DbConfig().get_connection_string(), raw_geojson)

((All credit for DbConfig goes to https://github.com/hotosm/pg-nearest-city/))

History

  • Initially I tried to write a pure-Python implementation of this, no dependencies.
  • I underestimated the amount of work that is! It could be possible to reverse engineer C++ Geos or georust/geos, but it's more hassle than it's worth.
  • As all of the target install candidates for this package use a db driver anyway, I thought it wisest (and most time efficient) to use the PostGIS Geos implementation (specifically for the unary_union and convex_hull algorithms).
  • An additional advantage is the potential to port this to PGLite when the PostGIS extension is available, meaning AOI processing easily in the browser.