Add counties CRUD page, dynamic county config, crosshair cursor for multi-select
This commit is contained in:
+139
-74
@@ -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 = [
|
||||
|
||||
Reference in New Issue
Block a user