diff --git a/web/server.py b/web/server.py index 21865ad..a9a8948 100644 --- a/web/server.py +++ b/web/server.py @@ -27,8 +27,8 @@ def index(): # 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-paths'], - 'Convert Data': ['convert-roads', 'convert-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'] } @@ -58,26 +58,29 @@ def get_script_map(): '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 [LAKE_URL_HERE] -O /data/latest/lake/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 [LAKE_URL_HERE] -O /data/latest/lake/roads.shp.zip' - }, - 'download-county-paths': { - # deliver files with standardized names - #'sumter': ['/data/latest/sumter/paths.shp.zip'], - #'lake': ['/data/latest/lake/paths.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'], @@ -88,6 +91,10 @@ def get_script_map(): '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', 'shp-to-geojson.py', '/data/latest/sumter/addresses.shp.zip', '/data/latest/sumter/county-addresses.geojson'], + 'lake': ['python', 'shp-to-geojson.py', '/data/latest/lake/addresses.shp.zip', '/data/latest/lake/county-addresses.geojson'] + }, 'convert-paths': { #todo: delete sumter-multi-modal-convert.py ? 'sumter': ['python', 'shp-to-geojson.py', '/data/latest/sumter/paths.shp.zip', '/data/latest/sumter/county-paths.geojson'], @@ -231,6 +238,31 @@ def cancel_job(): except Exception as e: return jsonify({'error': str(e)}), 500 +@app.route('/api/upload-file', methods=['POST']) +def upload_file(): + """Upload a shapefile ZIP to a path under /data/latest""" + dest = request.form.get('dest') + if not dest or not dest.startswith('/data/latest/'): + return jsonify({'error': 'Invalid destination path'}), 400 + + latest = '/data/latest' + if os.path.exists(latest) or os.path.islink(latest): + if os.path.isdir(latest) and not os.path.islink(latest): + return jsonify({'error': '/data/latest is a directory, not a symlink. Please run "Make New Latest" first.'}), 400 + else: + return jsonify({'error': '/data/latest does not exist. Please run "Make New Latest" first.'}), 400 + + if 'file' not in request.files: + return jsonify({'error': 'No file provided'}), 400 + + f = request.files['file'] + if not f.filename.lower().endswith('.zip'): + return jsonify({'error': 'File must be a ZIP archive (.zip)'}), 400 + + os.makedirs(os.path.dirname(dest), exist_ok=True) + f.save(dest) + return jsonify({'success': True, 'path': dest}) + @app.route('/api/list-files') def list_files(): """List available GeoJSON files""" diff --git a/web/templates/index.html b/web/templates/index.html index 68f8d93..a5f029d 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -205,6 +205,23 @@ .cancel-button.active { display: inline-block; } + + .upload-row { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; + } + + .upload-row span.unavailable { + color: #999; + font-size: 13px; + font-style: italic; + } + + .upload-row input[type="file"] { + font-size: 13px; + }
@@ -239,30 +256,56 @@ {# County-specific commands #} {% endif %} @@ -440,6 +483,42 @@ btn.disabled = false; }); } + + function uploadFile(inputId, dest) { + const input = document.getElementById(inputId); + if (!input || !input.files.length) { + showStatus('Please select a file first.', 'error'); + return; + } + + const file = input.files[0]; + const formData = new FormData(); + formData.append('file', file); + formData.append('dest', dest); + + showStatus(`Uploading ${file.name}...`, 'info'); + document.querySelectorAll('.script-button').forEach(btn => btn.disabled = true); + + fetch('/api/upload-file', { + method: 'POST', + body: formData + }) + .then(response => response.json()) + .then(data => { + if (data.error) { + showStatus(`Error: ${data.error}`, 'error'); + } else { + showStatus(`Uploaded to ${data.path}`, 'success'); + input.value = ''; + } + }) + .catch(error => { + showStatus(`Error: ${error.message}`, 'error'); + }) + .finally(() => { + enableButtons(); + }); + }