krz/omaha-metro-blotter
clone: git clone https://gitbay.org/krz/omaha-metro-blotter.git
main: fetch_boundaries.py · raw
1"""Refresh raw_data/boundaries.geojson, the map's geographic context.
2
3City limits and the county line are static reference geometry, not archive
4data, so this is a rare manual refresh rather than part of the daily pull.
5Douglas County publishes its own city limits and boundary; Sarpy publishes
6municipal boundaries under a different field name. Council Bluffs sits in
7Pottawattamie County, Iowa, which publishes neither, so it is unoutlined.
8"""
9
10import json
11import ssl
12import urllib.parse
13import urllib.request
14from pathlib import Path
15
16import certifi
17
18OUT = Path(__file__).parent / "raw_data" / "boundaries.geojson"
19CTX = ssl.create_default_context(cafile=certifi.where())
20# ~55 m; enough to keep the shapes honest without shipping full survey detail
21TOLERANCE = 0.0005
22
23LAYERS = [
24 {"kind": "county", "name": "Douglas County", "name_field": None,
25 "url": "https://services.arcgis.com/pDAi2YK0L0QxVJHj/arcgis/rest/services"
26 "/Douglas_County_Boundary/FeatureServer/0"},
27 {"kind": "city", "name_field": "town",
28 "url": "https://dcgis.org/server/rest/services/Hosted"
29 "/City_Limits_(source)_view/FeatureServer/0"},
30 {"kind": "city", "name_field": "NAME",
31 "url": "https://geodata.sarpy.gov/arcgis/rest/services/Cadastral"
32 "/LandRecordsDynamic/MapServer/39"},
33]
34
35
36def fetch(layer):
37 params = {"where": "1=1", "outFields": "*", "outSR": "4326",
38 "maxAllowableOffset": TOLERANCE, "f": "geojson"}
39 url = layer["url"] + "/query?" + urllib.parse.urlencode(params)
40 # Sarpy's MapServer 403s the default Python-urllib agent
41 req = urllib.request.Request(url, headers={"User-Agent": "omaha-metro-blotter/1.0"})
42 with urllib.request.urlopen(req, timeout=120, context=CTX) as r:
43 return json.load(r)
44
45
46def main():
47 features = []
48 for layer in LAYERS:
49 gj = fetch(layer)
50 for f in gj.get("features", []):
51 field = layer["name_field"]
52 name = (f.get("properties") or {}).get(field) if field else layer["name"]
53 features.append({
54 "type": "Feature",
55 "properties": {"name": name, "kind": layer["kind"]},
56 "geometry": f["geometry"],
57 })
58 print(f" {len(gj.get('features', []))} from {layer['url'].split('/')[2]}")
59
60 OUT.write_text(json.dumps(
61 {"type": "FeatureCollection", "features": features},
62 separators=(",", ":")))
63 verts = sum(len(r) for f in features
64 for poly in ([f["geometry"]["coordinates"]]
65 if f["geometry"]["type"] == "Polygon"
66 else f["geometry"]["coordinates"])
67 for r in poly)
68 print(f" wrote {OUT.relative_to(OUT.parent.parent)}: "
69 f"{len(features)} features, {verts} vertices, "
70 f"{OUT.stat().st_size / 1024:.0f} KiB")
71
72
73if __name__ == "__main__":
74 main()