#!/usr/bin/env bash # Shared steps for the tools/geodata pipelines (sourced; runs inside the geotools image). : "${HG_DB_URL:?HG_DB_URL is required (tools/geodata/run sets it)}" GEODATA_LIB="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" export PGOPTIONS="-c client_min_messages=warning" log() { printf '\033[1;32m==>\033[0m %s\n' "$*"; } die() { printf '\033[1;31merror:\033[0m %s\n' "$*" >&2; exit 1; } psql_run() { psql "$HG_DB_URL" -v ON_ERROR_STOP=1 -q "$@"; } # fetch_arcgis # Copies an ArcGIS layer (paged ESRI JSON, 1,000 features a request) into staging. in EPSG:4326 and its # coded-value domains into staging.arcgis_domains. Fails if nothing comes back, so existing data is never replaced # by an empty download. HG_ARCGIS_PAGE_SIZE sets a smaller page for layers with very large polygons (ECCC's CPCAD # server cuts responses off at about 60 MB, e.g. a 1,000-area page of BC parks). # fetch_arcgis : pages through an ArcGIS layer into staging.
. # Returns non-zero on failure even when called inside an `if`. fetch_arcgis() { local dataset="$1" layer_url="$2" where="$3" table="$4" query fetched psql_run -c "create schema if not exists staging" || return 1 log "Code lists" python3 "$GEODATA_LIB/arcgis-domains.py" "$dataset" "$layer_url" | psql_run || return 1 log "Downloading features" query="$(python3 -c 'import sys, urllib.parse; print(urllib.parse.urlencode({"where": sys.argv[1], "outFields": "*", "outSR": "4326", "orderByFields": "OBJECTID", "resultRecordCount": sys.argv[2], "f": "json"}))' "$where" "${HG_ARCGIS_PAGE_SIZE:-1000}")" ogr2ogr -f PostgreSQL "PG:$HG_DB_URL" "ESRIJSON:$layer_url/query?$query" \ -nln "staging.$table" -overwrite -lco GEOMETRY_NAME=geom -lco FID=ogc_fid -lco SPATIAL_INDEX=NONE \ -nlt PROMOTE_TO_MULTI -t_srs EPSG:4326 --config OGR_TRUNCATE YES --config GDAL_HTTP_USERAGENT "HuntGrid geodata pipeline" || return 1 fetched="$(psql_run -tA -c "select count(*) from staging.$table")" log "Fetched $fetched features" [[ "$fetched" -gt 0 ]] || die "nothing fetched; refusing to replace existing data" } # fetch_arcgis_robust : like fetch_arcgis, for layers too big or busy for # GDAL's paging (PAD-US for the whole country, CPCAD): lib/arcgis-fetch.py fetches by object ID in chunks # (HG_ARCGIS_PAGE_SIZE, default 200), waits out rate limits, splits chunks the service cuts off and thins only single # features it can't send whole. Nothing is staged unless every feature arrives. fetch_arcgis_robust() { local dataset="$1" layer_url="$2" where="$3" table="$4" out fetched psql_run -c "create schema if not exists staging" || return 1 log "Code lists" python3 "$GEODATA_LIB/arcgis-domains.py" "$dataset" "$layer_url" | psql_run || return 1 log "Downloading features (by object ID)" mkdir -p "$BUILD_DIR" out="$BUILD_DIR/$table.geojsonl" rm -f "$out" python3 "$GEODATA_LIB/arcgis-fetch.py" "$layer_url" "$where" "$out" "${HG_ARCGIS_PAGE_SIZE:-200}" || return 1 ogr2ogr -f PostgreSQL "PG:$HG_DB_URL" "$out" -nln "staging.$table" -overwrite -lco GEOMETRY_NAME=geom -lco FID=ogc_fid \ -lco SPATIAL_INDEX=NONE -nlt PROMOTE_TO_MULTI -t_srs EPSG:4326 --config OGR_TRUNCATE YES || return 1 rm -f "$out" fetched="$(psql_run -tA -c "select count(*) from staging.$table")" log "Fetched $fetched features" [[ "$fetched" -gt 0 ]] || die "nothing fetched; refusing to replace existing data" } # Tile builds work in the container's own filesystem: tippecanoe's temporary SQLite database is very slow on Docker # Desktop bind mounts (Windows/macOS) and it saves I/O on Linux too. Finished files are copied into /data/tiles under a # temporary name and renamed into place, so Martin never sees a half-written file. BUILD_DIR="${HG_BUILD_DIR:-/tmp/hg-build}" # publish_tiles publish_tiles() { local name name="$(basename "$1")" mkdir -p /data/tiles cp "$1" "/data/tiles/.$name.part" mv -f "/data/tiles/.$name.part" "/data/tiles/$name" rm -f "$1" } # stamp_odbl : an OpenStreetMap-derived tileset carries the ODbL # notice, the licence URI, the 4.6 offer's address and the extract it came from in its own metadata (F20.15), which # Martin serves as TileJSON and `pmtiles extract` copies into offline packages. stamp_odbl() { local version version="$(psql_run -tA -c "select version from geo.dataset_imports where dataset_key = '$3' order by imported_at desc limit 1")" python3 "$GEODATA_LIB/pmtiles_meta.py" odbl "$1" "$2" "$version" } # upsert_layer : creates or refreshes the pipeline's catalog row (geo.layers). upsert_layer() { psql_run -v layer="$(cat "$1")" -f "$GEODATA_LIB/upsert-layer.sql" } # build_public_land_tiles [table]: the matching rows (geo.public_lands, or # geo.osm_public_lands for ODbL data) → data/tiles/.pmtiles # (layer public_lands, plus one label point per named area with its acreage in public_land_labels). Base Crown land # (attributes.land_base, cut into ~10 km pieces) carries base: true so styles don't outline every piece, and gets no # label point (one "Crown land" per piece). Martin serves it # as source ; restart Martin after the first build so it picks the file up. build_public_land_tiles() { local tileset="$1" filter="$2" attribution="$3" table="${4:-geo.public_lands}" log "Building tiles" mkdir -p "$BUILD_DIR" ogr2ogr -f GeoJSONSeq "$BUILD_DIR/$tileset.geojsonl" "PG:$HG_DB_URL" -overwrite -lco RS=NO -sql \ "select id, name, palette, public_access as access, hunting, manager_name as manager, designation, case when attributes->>'land_base' = 'true' then true end as base, geom from $table where $filter" # Labelling the polygons themselves repeats the name on every part in every tile. -r1 keeps every label point at # every zoom (tippecanoe thins points below the max zoom by default); the style picks which to show by size. ogr2ogr -f GeoJSONSeq "$BUILD_DIR/$tileset-labels.geojsonl" "PG:$HG_DB_URL" -overwrite -lco RS=NO -sql \ "select id, name, palette, round(coalesce((attributes->>'acres')::numeric, st_area(geom::geography) / 4046.86))::int as acres, st_pointonsurface((select d.geom from st_dump(geom) d order by st_area(d.geom) desc limit 1)) as geom from $table where ($filter) and name is not null and coalesce(attributes->>'land_base', '') <> 'true'" # tiles-build: zooms 4-6 lighter (roadmap 15.17c), 7+ as before. bash "$GEODATA_LIB/tiles-build" "$BUILD_DIR/$tileset.pmtiles" 4 14 -t "$BUILD_DIR" --quiet -P -n "HuntGrid $tileset" \ -A "$attribution" -r1 --use-attribute-for-id=id --drop-smallest-as-needed --detect-shared-borders \ -L "public_lands:$BUILD_DIR/$tileset.geojsonl" -L "public_land_labels:$BUILD_DIR/$tileset-labels.geojsonl" rm -f "$BUILD_DIR/$tileset.geojsonl" "$BUILD_DIR/$tileset-labels.geojsonl" if [[ "$table" == geo.osm_public_lands ]]; then stamp_odbl "$BUILD_DIR/$tileset.pmtiles" "$tileset" osm-protected; fi publish_tiles "$BUILD_DIR/$tileset.pmtiles" log "Done: data/tiles/$tileset.pmtiles ($(du -h "/data/tiles/$tileset.pmtiles" | cut -f1))" }