diff --git a/web/server.py b/web/server.py index 59c11c2..9b2ef5d 100644 --- a/web/server.py +++ b/web/server.py @@ -426,7 +426,7 @@ def serve_data(filename): # ── Config file download / upload ────────────────────────────────────────────── -_ALLOWED_CONFIGS = {'counties.yml', 'exceptions.yml'} +_ALLOWED_CONFIGS = {'counties.yml', 'exceptions.yml', 'exclusions.yml'} @app.route('/api/config/', methods=['GET']) @require_auth @@ -450,6 +450,8 @@ def upload_config(filename): return jsonify({'error': 'Invalid counties.yml: missing counties list'}), 400 if filename == 'exceptions.yml' and not isinstance((data or {}).get('corrections'), list): return jsonify({'error': 'Invalid exceptions.yml: missing corrections list'}), 400 + if filename == 'exclusions.yml' and not isinstance((data or {}).get('exclusions'), list): + return jsonify({'error': 'Invalid exclusions.yml: missing exclusions list'}), 400 except Exception as e: return jsonify({'error': f'Invalid YAML: {e}'}), 400 dest = f'/data/{filename}' @@ -459,6 +461,80 @@ def upload_config(filename): os.replace(tmp, dest) return jsonify({'success': True}) +# ── Exclusions config ───────────────────────────────────────────────────────── + +EXCLUSIONS_FILE = '/data/exclusions.yml' + + +def _ensure_exclusions_file(): + if not os.path.exists(EXCLUSIONS_FILE): + os.makedirs(os.path.dirname(EXCLUSIONS_FILE), exist_ok=True) + with open(EXCLUSIONS_FILE, 'w') as f: + yaml.dump({'exclusions': []}, f, default_flow_style=False, allow_unicode=True) + +_ensure_exclusions_file() + + +def get_exclusions(): + try: + with open(EXCLUSIONS_FILE) as f: + data = yaml.safe_load(f) or {} + return data.get('exclusions', []) + except Exception: + return [] + + +@app.route('/exclusions') +def exclusions_page(): + return render_template('exclusions.html', current_user=session.get('username')) + + +@app.route('/api/exclusions', methods=['GET']) +def get_exclusions_api(): + return jsonify({'exclusions': get_exclusions()}) + + +@app.route('/api/exclusions', methods=['POST']) +@require_auth +def save_exclusions(): + data = request.json + items = data.get('exclusions', []) + for item in items: + if not isinstance(item.get('field'), str) or not item['field'].strip(): + return jsonify({'error': 'Each exclusion must have a non-empty "field"'}), 400 + if not isinstance(item.get('value'), str) or not item['value'].strip(): + return jsonify({'error': 'Each exclusion must have a non-empty "value"'}), 400 + tmp = EXCLUSIONS_FILE + '.tmp' + with open(tmp, 'w') as f: + yaml.dump({'exclusions': items}, f, default_flow_style=False, allow_unicode=True) + os.replace(tmp, EXCLUSIONS_FILE) + return jsonify({'success': True}) + + +@app.route('/api/exclusions/add', methods=['POST']) +@require_auth +def add_exclusions(): + """Append new exclusion entries, skipping duplicates.""" + data = request.json + new_items = data.get('exclusions', []) + existing = get_exclusions() + existing_keys = {(e['field'], e['value']) for e in existing} + added = 0 + for item in new_items: + field = (item.get('field') or '').strip() + value = (item.get('value') or '').strip() + if not field or not value: + continue + if (field, value) not in existing_keys: + existing.append({'field': field, 'value': value}) + existing_keys.add((field, value)) + added += 1 + tmp = EXCLUSIONS_FILE + '.tmp' + with open(tmp, 'w') as f: + yaml.dump({'exclusions': existing}, f, default_flow_style=False, allow_unicode=True) + os.replace(tmp, EXCLUSIONS_FILE) + return jsonify({'success': True, 'added': added}) + # ── Counties config ──────────────────────────────────────────────────────────── COUNTIES_FILE = '/data/counties.yml' diff --git a/web/static/map.js b/web/static/map.js index bfd6370..a56e7dd 100644 --- a/web/static/map.js +++ b/web/static/map.js @@ -13,6 +13,9 @@ let rejectedFeatures = new Set(); let featurePopup = null; let layerOrder = ['diff', 'osm', 'county']; // Default layer order (top to bottom) +// Exclusions loaded from server +let exclusions = []; + // Drag selection state let isDragging = false; let dragStartPoint = null; @@ -425,8 +428,19 @@ function createOsmLayer() { updateLayerZIndex(); } +// Check if a feature matches any server-side exclusion +function isExcluded(feature) { + if (!exclusions.length) return false; + const props = feature.properties || {}; + return exclusions.some(excl => { + const val = props[excl.field]; + return val !== undefined && val !== null && String(val) === String(excl.value); + }); +} + // Filter function for diff features function shouldShowFeature(feature) { + if (isExcluded(feature)) return false; const props = feature.properties || {}; const isRemoved = props.removed === true || props.removed === 'True'; const isService = props.highway === 'service' || props.highway === 'track'; @@ -803,6 +817,9 @@ function showMultiFeaturePopup(latlng) { html += ``; html += ``; html += ''; + html += '
'; + html += ``; + html += '
'; } } @@ -899,9 +916,78 @@ function rejectAllFeatures() { showStatus(`${acceptedFeatures.size} accepted, ${rejectedFeatures.size} rejected`, 'success'); } +// Exclude selected features by name/street and refresh diff layer +async function excludeSelectedFeatures() { + if (selectedFeatures.length === 0) return; + + const toExclude = []; + const seen = new Set(); + + for (const feature of selectedFeatures) { + const props = feature.properties || {}; + let field, value; + + if (props['name']) { + field = 'name'; + value = props['name']; + } else if (props['addr:street']) { + field = 'addr:street'; + value = props['addr:street']; + } else { + continue; + } + + const key = `${field}\0${value}`; + if (!seen.has(key)) { + seen.add(key); + toExclude.push({ field, value }); + } + } + + if (toExclude.length === 0) { + showStatus('No name or street property found to exclude by.', 'error'); + return; + } + + try { + const resp = await fetch('/api/exclusions/add', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ exclusions: toExclude }) + }); + const data = await resp.json(); + if (data.error) { + if (resp.status === 401) { + showStatus('Not authenticated — please log in to exclude features.', 'error'); + } else { + showStatus(`Error: ${data.error}`, 'error'); + } + return; + } + } catch (err) { + showStatus(`Error: ${err.message}`, 'error'); + return; + } + + // Update local list and refresh + for (const e of toExclude) { + if (!exclusions.some(ex => ex.field === e.field && ex.value === e.value)) { + exclusions.push(e); + } + } + + if (featurePopup) map.closePopup(featurePopup); + clearSelection(); + createDiffLayer(); + + const names = toExclude.map(e => e.value).join(', '); + showStatus(`Excluded: ${names}`, 'success'); +} + // Expose functions globally for onclick handlers window.acceptAllFeatures = acceptAllFeatures; window.rejectAllFeatures = rejectAllFeatures; +window.excludeSelectedFeatures = excludeSelectedFeatures; // Update save button state function updateSaveButton() { @@ -931,6 +1017,15 @@ async function loadFiles() { loadedCounty = county; loadedDataType = dataType; + // Load exclusions + try { + const exclResp = await fetch('/api/exclusions'); + if (exclResp.ok) { + const exclData = await exclResp.json(); + exclusions = exclData.exclusions || []; + } + } catch (_) {} + // Build file paths based on county and data type let osmFile, diffFile, countyFile, diffAddedFile, diffRemovedFile; diff --git a/web/templates/counties.html b/web/templates/counties.html index 4d911e4..5d19202 100644 --- a/web/templates/counties.html +++ b/web/templates/counties.html @@ -206,6 +206,7 @@
Counties Street Exceptions + Exclusions Open Map Viewer diff --git a/web/templates/exceptions.html b/web/templates/exceptions.html index ccfd1ee..4bac505 100644 --- a/web/templates/exceptions.html +++ b/web/templates/exceptions.html @@ -205,6 +205,7 @@
Counties Street Exceptions + Exclusions Open Map Viewer diff --git a/web/templates/exclusions.html b/web/templates/exclusions.html new file mode 100644 index 0000000..7a58698 --- /dev/null +++ b/web/templates/exclusions.html @@ -0,0 +1,332 @@ + + + + + + Exclusions — OSM Import Tools + + + +
+ +
+

OSM Import Tools

+
+ ← Back +
+ Counties + Street Exceptions + Exclusions + Open Map Viewer +
+ +
+
+
+
+

Diff Exclusions

+

Features matching these field/value pairs are hidden from the map diff view. Add entries here or click Exclude in the map popup.

+
+
+ +
+ + + + + + + + + + + + +
FieldValue
Loading...
+ + +
+
+ +
+ + + + diff --git a/web/templates/index.html b/web/templates/index.html index 6706cf6..e0db255 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -360,6 +360,7 @@
Counties Street Exceptions + Exclusions Open Map Viewer
{% if current_user %}