diff --git a/web/server.py b/web/server.py index d984ba0..6c48351 100644 --- a/web/server.py +++ b/web/server.py @@ -22,31 +22,24 @@ latest_path = "/data/latest" @app.route('/') def index(): """Main index page with script execution buttons""" - # Get available scripts and organize them - script_map = get_script_map() - - # Organize scripts by category - scripts_by_category = { - 'Download County Data': ['download-county-addresses', 'download-county-roads', 'download-county-paths'], - 'Download OSM Data': ['download-osm-roads', 'download-osm-addresses', 'download-osm-paths'], - 'Convert Data': ['convert-roads', 'convert-addresses', 'convert-paths'], - 'Diff Data': ['diff-roads', 'diff-paths', 'diff-addresses'], - 'Utilities': ['ls', 'make-new-latest'] - } + counties = get_counties() # Get list of files data_files = {} if os.path.exists(latest_path): - for countyFolder in os.listdir(latest_path): - files = os.listdir('/data/latest/'+countyFolder) - data_files[countyFolder] = files - # data_files.append(countyFolder) + try: + for countyFolder in os.listdir(latest_path): + folder_path = '/data/latest/' + countyFolder + if os.path.isdir(folder_path): + data_files[countyFolder] = os.listdir(folder_path) + except Exception: + pass + import json as _json return render_template('index.html', - script_map=script_map, - scripts_by_category=scripts_by_category, - data_files=data_files - ) + counties=counties, + counties_json=_json.dumps(counties), + data_files=data_files) @app.route('/map') def map_viewer(): @@ -54,64 +47,66 @@ def map_viewer(): return render_template('map.html') def get_script_map(): - """Get the map of available scripts and their commands""" - return { + """Build script map dynamically from county configuration.""" + counties = get_counties() + + county_dirs = ' '.join(f'$NEWDIR/{c["id"]}' for c in counties) + sm = { 'ls': 'ls -al /data', - 'make-new-latest': 'cd /data && NEWDIR=$(date +%y%m%d) && mkdir -p $NEWDIR/lake $NEWDIR/sumter && ln -sfn $NEWDIR latest', - # todo: make a clean-old-data script - 'download-county-paths': { - 'sumter': {'type': 'upload', 'dest': '/data/latest/sumter/paths.shp.zip'}, - # lake: not available - }, - 'download-county-addresses': { - # deliver files with standardized names - 'sumter': 'mkdir -p /data/latest/sumter && wget https://www.arcgis.com/sharing/rest/content/items/c75c5aac13a648968c5596b0665be28b/data -O /data/latest/sumter/addresses.shp.zip', - 'lake': 'mkdir -p /data/latest/lake && wget https://c.lakecountyfl.gov/ftp/GIS/GisDownloads/Shapefiles/Addresspoints.zip -O /data/latest/lake/addresses.shp.zip' - }, - 'download-county-roads': { - # deliver files with standardized names - 'sumter': 'mkdir -p /data/latest/sumter && wget https://www.arcgis.com/sharing/rest/content/items/9177e17c72d3433aa79630c7eda84add/data -O /data/latest/sumter/roads.shp.zip', - 'lake': 'mkdir -p /data/latest/lake && wget https://c.lakecountyfl.gov/ftp/GIS/GisDownloads/Shapefiles/Streets.zip -O /data/latest/lake/roads.shp.zip' - }, - # todo: integrate osm downloading and shapefile converting into diff-roads like addresses - 'download-osm-roads': { - 'lake': ['python', 'download-overpass.py', '--type', 'highways', 'Lake County', 'Florida', '/data/latest/lake/osm-roads.geojson'], - 'sumter': ['python', 'download-overpass.py', '--type', 'highways', 'Sumter County', 'Florida', '/data/latest/sumter/osm-roads.geojson'] - }, - 'download-osm-addresses': { - 'lake': ['python', 'download-overpass.py', '--type', 'addresses', 'Lake County', 'Florida', '/data/latest/lake/osm-addresses.geojson'], - 'sumter': ['python', 'download-overpass.py', '--type', 'addresses', 'Sumter County', 'Florida', '/data/latest/sumter/osm-addresses.geojson'] - }, - 'download-osm-paths': { - # todo: no lake county paths - #'lake': ['python', 'download-overpass.py', '--type', 'highways', 'Lake County', 'Florida', '/data/latest/lake/osm-roads.geojson'], - 'sumter': ['python', 'download-overpass.py', '--type', 'paths', 'Sumter County', 'Florida', '/data/latest/sumter/osm-paths.geojson'] - }, - # todo - 'convert-roads': { - 'sumter': ['python', 'shp-to-geojson.py', '/data/latest/sumter/roads.shp.zip', '/data/latest/sumter/county-roads.geojson'], - 'lake': ['python', 'shp-to-geojson.py', '/data/latest/lake/roads.shp.zip', '/data/latest/lake/county-roads.geojson'] - }, - 'convert-addresses': { - 'sumter': ['python', 'convert-addresses.py', '/data/latest/sumter/addresses.shp.zip', '/data/latest/sumter/county-addresses.geojson'], - 'lake': ['python', 'convert-addresses.py', '/data/latest/lake/addresses.shp.zip', '/data/latest/lake/county-addresses.geojson'], - }, - 'convert-paths': { - 'sumter': ['python', 'shp-to-geojson.py', '/data/latest/sumter/paths.shp.zip', '/data/latest/sumter/county-paths.geojson'], - }, - 'diff-roads': { - 'lake': ['python', 'diff-highways.py', '/data/latest/lake/osm-roads.geojson', '/data/latest/lake/county-roads.geojson', '--output', '/data/latest/lake/diff-roads.geojson'], - 'sumter': ['python', 'diff-highways.py', '/data/latest/sumter/osm-roads.geojson', '/data/latest/sumter/county-roads.geojson', '--output', '/data/latest/sumter/diff-roads.geojson'], - }, - 'diff-paths': { - 'sumter': ['python', 'diff-highways.py', '/data/latest/sumter/osm-paths.geojson', '/data/latest/sumter/county-paths.geojson', '--output', '/data/latest/sumter/diff-paths.geojson'], - }, - 'diff-addresses': { - 'lake': ['python', 'compare-addresses.py', '--local-file', '/data/latest/lake/county-addresses.geojson', '--osm-file', '/data/latest/lake/osm-addresses.geojson', '--output-dir', '/data/latest/lake'], - 'sumter': ['python', 'compare-addresses.py', '--local-file', '/data/latest/sumter/county-addresses.geojson', '--osm-file', '/data/latest/sumter/osm-addresses.geojson', '--output-dir', '/data/latest/sumter'], - }, + 'make-new-latest': f'cd /data && NEWDIR=$(date +%y%m%d) && mkdir -p {county_dirs} && ln -sfn $NEWDIR latest', } + dl_roads = {}; dl_addrs = {}; dl_paths = {} + osm_roads = {}; osm_addrs = {}; osm_paths = {} + conv_roads = {}; conv_addrs = {}; conv_paths = {} + diff_roads = {}; diff_addrs = {}; diff_paths = {} + + for c in counties: + cid = c['id'] + name = c['name'] + state = c['state'] + base = f'/data/latest/{cid}' + + if c.get('roads_url'): + dl_roads[cid] = f'mkdir -p {base} && wget {c["roads_url"]} -O {base}/roads.shp.zip' + if c.get('addresses_url'): + dl_addrs[cid] = f'mkdir -p {base} && wget {c["addresses_url"]} -O {base}/addresses.shp.zip' + pu = c.get('paths_url', '') + if pu == 'upload': + dl_paths[cid] = {'type': 'upload', 'dest': f'{base}/paths.shp.zip'} + elif pu: + dl_paths[cid] = f'mkdir -p {base} && wget {pu} -O {base}/paths.shp.zip' + + osm_roads[cid] = ['python', 'download-overpass.py', '--type', 'highways', name, state, f'{base}/osm-roads.geojson'] + osm_addrs[cid] = ['python', 'download-overpass.py', '--type', 'addresses', name, state, f'{base}/osm-addresses.geojson'] + if pu: + osm_paths[cid] = ['python', 'download-overpass.py', '--type', 'paths', name, state, f'{base}/osm-paths.geojson'] + + conv_roads[cid] = ['python', 'shp-to-geojson.py', f'{base}/roads.shp.zip', f'{base}/county-roads.geojson'] + conv_addrs[cid] = ['python', 'convert-addresses.py', f'{base}/addresses.shp.zip', f'{base}/county-addresses.geojson'] + if pu: + conv_paths[cid] = ['python', 'shp-to-geojson.py', f'{base}/paths.shp.zip', f'{base}/county-paths.geojson'] + + 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] + 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'] + + if dl_roads: sm['download-county-roads'] = dl_roads + if dl_addrs: sm['download-county-addresses'] = dl_addrs + if dl_paths: sm['download-county-paths'] = dl_paths + sm['download-osm-roads'] = osm_roads + sm['download-osm-addresses'] = osm_addrs + if osm_paths: sm['download-osm-paths'] = osm_paths + sm['convert-roads'] = conv_roads + sm['convert-addresses'] = conv_addrs + if conv_paths: sm['convert-paths'] = conv_paths + sm['diff-roads'] = diff_roads + sm['diff-addresses'] = diff_addrs + if diff_paths: sm['diff-paths'] = diff_paths + + return sm + @app.route('/api/run-script', methods=['POST']) def run_script(): """Execute a script in the background""" @@ -288,6 +283,76 @@ def serve_data(filename): """Serve GeoJSON files""" return send_from_directory('/data', filename) +# ── Counties config ──────────────────────────────────────────────────────────── + +COUNTIES_FILE = '/data/counties.yml' + +DEFAULT_COUNTIES = [ + { + 'id': 'lake', + 'name': 'Lake County', + 'state': 'Florida', + 'color': '#28a745', + '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': '', + }, + { + 'id': 'sumter', + 'name': 'Sumter County', + 'state': 'Florida', + 'color': '#17a2b8', + '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', + }, +] + +def _ensure_counties_file(): + if not os.path.exists(COUNTIES_FILE): + os.makedirs(os.path.dirname(COUNTIES_FILE), exist_ok=True) + with open(COUNTIES_FILE, 'w') as f: + yaml.dump({'counties': DEFAULT_COUNTIES}, f, + default_flow_style=False, allow_unicode=True) + +_ensure_counties_file() + +def get_counties(): + try: + with open(COUNTIES_FILE) as f: + data = yaml.safe_load(f) or {} + return data.get('counties', DEFAULT_COUNTIES) + except Exception: + return DEFAULT_COUNTIES + +@app.route('/counties') +def counties_page(): + return render_template('counties.html') + +@app.route('/api/counties', methods=['GET']) +def get_counties_api(): + try: + return jsonify({'counties': get_counties()}) + except Exception as e: + return jsonify({'error': str(e)}), 500 + +@app.route('/api/counties', methods=['POST']) +def save_counties(): + data = request.json + counties = data.get('counties', []) + for c in counties: + if not c.get('id', '').strip(): + return jsonify({'error': 'Each county must have a non-empty id'}), 400 + if not c.get('name', '').strip(): + return jsonify({'error': 'Each county must have a non-empty name'}), 400 + tmp = COUNTIES_FILE + '.tmp' + with open(tmp, 'w') as f: + yaml.dump({'counties': counties}, f, default_flow_style=False, allow_unicode=True) + os.replace(tmp, COUNTIES_FILE) + return jsonify({'success': True}) + +# ── Exceptions config ────────────────────────────────────────────────────────── + EXCEPTIONS_FILE = '/data/exceptions.yml' DEFAULT_EXCEPTIONS = [ diff --git a/web/static/map.js b/web/static/map.js index e130c15..091e0e3 100644 --- a/web/static/map.js +++ b/web/static/map.js @@ -1253,9 +1253,11 @@ document.addEventListener('DOMContentLoaded', function() { if (multiSelectMode) { button.style.background = '#007bff'; button.textContent = 'Multi-Select: ON'; + map.getContainer().classList.add('multi-select-active'); } else { button.style.background = '#6c757d'; button.textContent = 'Multi-Select: OFF'; + map.getContainer().classList.remove('multi-select-active'); // Clear selection when turning off multi-select mode clearSelection(); } diff --git a/web/templates/counties.html b/web/templates/counties.html new file mode 100644 index 0000000..922f329 --- /dev/null +++ b/web/templates/counties.html @@ -0,0 +1,348 @@ + + + + + + Counties — OSM Import Tools + + + +
+ +
+

OSM Import Tools

+
+ ← Back +
+ Counties + Street Exceptions + Open Map Viewer +
+ +
+ +
+ +
+ + + +
+
+ + + + diff --git a/web/templates/exceptions.html b/web/templates/exceptions.html index 43797e2..080fe36 100644 --- a/web/templates/exceptions.html +++ b/web/templates/exceptions.html @@ -190,6 +190,7 @@
← Back
+ Counties Street Exceptions Open Map Viewer diff --git a/web/templates/index.html b/web/templates/index.html index 42c24df..02c73d8 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -104,8 +104,7 @@ .county-tab:hover { color: #444; } - .county-tab.active.lake { color: #1a7a33; border-bottom-color: #28a745; background: white; } - .county-tab.active.sumter { color: #0d7a8f; border-bottom-color: #17a2b8; background: white; } + .county-tab.active { background: white; border-bottom-color: var(--county-color, #28a745); color: var(--county-color, #28a745); } /* Scrollable workflow area */ .workflows { @@ -189,8 +188,7 @@ .btn-run:hover { filter: brightness(0.88); } .btn-run:disabled { filter: brightness(0.5); cursor: not-allowed; } - .lake-active .btn-run { background: #28a745; } - .sumter-active .btn-run { background: #17a2b8; } + .left .btn-run { background: var(--active-color, #28a745); } /* Force redownload */ .force-label { @@ -249,8 +247,7 @@ font-weight: 500; } - .file-link.lake { background: #28a745; } - .file-link.sumter { background: #17a2b8; } + .file-link { background: var(--link-color, #888); } /* ── Right panel (console) ── */ .right { @@ -330,6 +327,7 @@
+ Counties Street Exceptions Open Map Viewer @@ -340,8 +338,11 @@
- - + {% for county in counties %} + + {% endfor %}
@@ -455,12 +456,15 @@
Downloaded files
@@ -482,51 +486,59 @@