Create new project design plans

This commit is contained in:
zyphlar
2026-08-28 14:23:26 -07:00
parent a7d6db8177
commit 7ce73f19c5
22 changed files with 2682 additions and 263 deletions
+129
View File
@@ -9,6 +9,7 @@ import subprocess
import os
import re
import secrets
import shlex
import shutil
import threading
import json
@@ -185,6 +186,117 @@ def map_viewer():
"""Map viewer page"""
return render_template('map.html')
# ── Reports ────────────────────────────────────────────────────────────────
@app.route('/reports')
def reports_page():
"""Reports index page"""
return render_template('reports.html', current_user=session.get('username'))
@app.route('/reports/city-boundaries')
def city_boundaries_report():
"""City name agreement report: county vs OSM addr:city, on a map"""
counties = get_counties()
available = []
pinned_cities = {}
for county in counties:
path = f'/data/{county["id"]}/city-comparison.geojson'
if os.path.isfile(path):
available.append(county['id'])
pinned_cities[county['id']] = county.get('pinned_cities') or []
return render_template('city-boundaries-report.html',
counties=counties,
available_counties=available,
pinned_cities=pinned_cities,
current_user=session.get('username'))
# ── map-checker: hand-traced boundary tool ──────────────────────────────────
#
# thevillages.com's Terms of Use prohibit automated access, so this only ever
# saves what a human draws by hand while looking at the official map
# themselves - nothing here fetches from thevillages.com. See map-checker/README.md.
MAP_CHECKER_DIR = '/data/map-checker'
MAP_CHECKER_BOUNDARY_FILE = os.path.join(MAP_CHECKER_DIR, 'boundary.geojson')
MAP_CHECKER_DRAFT_FILE = os.path.join(MAP_CHECKER_DIR, 'draft.json')
@app.route('/map-checker')
@require_auth
def map_checker_page():
"""Boundary tracer: hand-trace a polygon against a reference map, save as GeoJSON.
Requires login so tracing work is never done (and lost) as an anonymous session."""
return render_template('boundary-tracer.html',
counties=get_counties(),
current_user=session.get('username'))
@app.route('/api/map-checker/boundary', methods=['GET'])
@require_auth
def get_map_checker_boundary():
if not os.path.isfile(MAP_CHECKER_BOUNDARY_FILE):
return jsonify({'type': 'FeatureCollection', 'features': []})
with open(MAP_CHECKER_BOUNDARY_FILE) as f:
return jsonify(json.load(f))
@app.route('/api/map-checker/boundary', methods=['POST'])
@require_auth
def save_map_checker_boundary():
data = request.json or {}
if data.get('type') != 'FeatureCollection' or not isinstance(data.get('features'), list):
return jsonify({'error': 'Expected a GeoJSON FeatureCollection'}), 400
for feat in data['features']:
geom_type = ((feat.get('geometry') or {}).get('type'))
if geom_type not in ('Polygon', 'MultiPolygon'):
return jsonify({'error': f'Unsupported geometry type: {geom_type}'}), 400
os.makedirs(MAP_CHECKER_DIR, exist_ok=True)
tmp = MAP_CHECKER_BOUNDARY_FILE + '.tmp'
with open(tmp, 'w') as f:
json.dump(data, f)
os.replace(tmp, MAP_CHECKER_BOUNDARY_FILE)
return jsonify({'success': True, 'features': len(data['features'])})
def _is_latlng_list(val):
if not isinstance(val, list):
return False
for pt in val:
if not (isinstance(pt, list) and len(pt) == 2 and
all(isinstance(c, (int, float)) for c in pt)):
return False
return True
@app.route('/api/map-checker/draft', methods=['GET'])
@require_auth
def get_map_checker_draft():
if not os.path.isfile(MAP_CHECKER_DRAFT_FILE):
return jsonify({'polygons': [], 'inProgress': []})
with open(MAP_CHECKER_DRAFT_FILE) as f:
return jsonify(json.load(f))
@app.route('/api/map-checker/draft', methods=['POST'])
@require_auth
def save_map_checker_draft():
"""Autosave endpoint hit after every vertex/edit so in-progress tracing
survives a crashed tab or dropped connection, even before the user
explicitly finishes a polygon or clicks Save to server."""
data = request.json or {}
polygons = data.get('polygons')
in_progress = data.get('inProgress', [])
if not isinstance(polygons, list):
return jsonify({'error': 'Expected a polygons list'}), 400
for p in polygons:
if not (isinstance(p, dict) and isinstance(p.get('name'), str) and _is_latlng_list(p.get('latlngs'))):
return jsonify({'error': 'Malformed polygon entry'}), 400
if not _is_latlng_list(in_progress):
return jsonify({'error': 'Malformed inProgress point list'}), 400
os.makedirs(MAP_CHECKER_DIR, exist_ok=True)
tmp = MAP_CHECKER_DRAFT_FILE + '.tmp'
with open(tmp, 'w') as f:
json.dump({'polygons': polygons, 'inProgress': in_progress}, f)
os.replace(tmp, MAP_CHECKER_DRAFT_FILE)
return jsonify({'success': True})
def get_script_map():
"""Build script map dynamically from county configuration."""
counties = get_counties()
@@ -197,6 +309,7 @@ def get_script_map():
osm_roads = {}; osm_addrs = {}; osm_paths = {}
conv_roads = {}; conv_addrs = {}; conv_paths = {}
diff_roads = {}; diff_addrs = {}; diff_paths = {}
city_report = {}
for c in counties:
cid = c['id']
@@ -226,6 +339,7 @@ def get_script_map():
diff_roads[cid] = ['python', 'diff-highways.py', f'{base}/osm-roads.geojson', f'{base}/county-roads.geojson', '--output', f'{base}/diff-roads.geojson']
diff_addrs[cid] = ['python', 'compare-addresses.py', '--local-file', f'{base}/county-addresses.geojson', '--osm-file', f'{base}/osm-addresses.geojson', '--output-dir', base]
city_report[cid] = ['python', 'generate-city-report.py', '--existing-file', f'{base}/addresses-existing.geojson', '--output-dir', base]
if pu:
diff_paths[cid] = ['python', 'diff-highways.py', f'{base}/osm-paths.geojson', f'{base}/county-paths.geojson', '--output', f'{base}/diff-paths.geojson']
@@ -241,6 +355,19 @@ def get_script_map():
sm['diff-roads'] = diff_roads
sm['diff-addresses'] = diff_addrs
if diff_paths: sm['diff-paths'] = diff_paths
sm['generate-city-report'] = city_report
# Point-in-polygon check against the traced/imported boundary at
# /data/map-checker/boundary.geojson (see /map-checker and map-checker/README.md).
# Not per-county like the scripts above - one boundary can span multiple
# counties, so this checks all configured counties in a single run.
county_args = ' '.join(f'--county {shlex.quote(c["id"])}' for c in counties)
if county_args:
sm['boundary-check'] = (
f'python map-checker/check-boundary.py {county_args} '
f'--boundary /data/map-checker/boundary.geojson '
f'--output /data/map-checker/flagged.geojson'
)
return sm
@@ -548,6 +675,7 @@ DEFAULT_COUNTIES = [
'roads_url': 'https://www.arcgis.com/sharing/rest/content/items/9177e17c72d3433aa79630c7eda84add/data',
'addresses_url': 'https://www.arcgis.com/sharing/rest/content/items/c75c5aac13a648968c5596b0665be28b/data',
'paths_url': 'upload',
'pinned_cities': ['The Villages'],
},
{
'id': 'lake',
@@ -557,6 +685,7 @@ DEFAULT_COUNTIES = [
'roads_url': 'https://c.lakecountyfl.gov/ftp/GIS/GisDownloads/Shapefiles/Streets.zip',
'addresses_url': 'https://c.lakecountyfl.gov/ftp/GIS/GisDownloads/Shapefiles/Addresspoints.zip',
'paths_url': '',
'pinned_cities': ['The Villages'],
},
]