Add counties CRUD page, dynamic county config, crosshair cursor for multi-select

This commit is contained in:
zyphlar
2026-04-09 22:11:38 -07:00
parent f3a1ca6020
commit d904fb4f3d
6 changed files with 547 additions and 113 deletions
+139 -74
View File
@@ -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 = [