Create new project design plans
This commit is contained in:
+129
@@ -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'],
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user