Add exclusions feature: map popup Exclude button, exclusions page, server API

This commit is contained in:
zyphlar
2026-04-24 01:02:39 -07:00
parent 67f038e7f0
commit 8150e2ca1f
6 changed files with 507 additions and 1 deletions
+95
View File
@@ -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 += `<button onclick="acceptAllFeatures()" style="flex: 1; padding: 5px; background: #007bff; color: white; border: none; border-radius: 3px; cursor: pointer;">Accept${buttonText}</button>`;
html += `<button onclick="rejectAllFeatures()" style="flex: 1; padding: 5px; background: #6c757d; color: white; border: none; border-radius: 3px; cursor: pointer;">Reject${buttonText}</button>`;
html += '</div>';
html += '<div style="margin-top: 5px;">';
html += `<button onclick="excludeSelectedFeatures()" style="width: 100%; padding: 5px; background: #fd7e14; color: white; border: none; border-radius: 3px; cursor: pointer;">Exclude${buttonText} from future diffs</button>`;
html += '</div>';
}
}
@@ -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;