improve snapping issues
This commit is contained in:
@@ -194,6 +194,12 @@ docker-compose up -d
|
|||||||
|
|
||||||
## TODO
|
## TODO
|
||||||
|
|
||||||
|
- If >50% of a road is parallel and nearby a similar road, consider omitting the portion of the new road which is parallel.
|
||||||
|
- Highlight snap segments during multi-select, and move the enable/disable snap function to the snap geometry itself instead of the popup
|
||||||
|
- don't snap a node to a road if the node already contains a segment that's closer to the road, or if this segment already has a closer node. (prevent double snapping)
|
||||||
|
- use third party validators to check ourselves
|
||||||
|
- allow adjusting of per-node snap distances that override the default
|
||||||
|
- Click-to-remove OSM elements: allow selecting existing OSM ways/nodes in the map viewer and marking them for deletion in the JOSM export (analogous to accepting a "removed" diff feature but user-initiated)
|
||||||
- Validate whether a similar address is within 50 feet (same number?)
|
- Validate whether a similar address is within 50 feet (same number?)
|
||||||
- If addr:unit is like "Apartment 123" or "Apartment 123;Apartment 124;Apartment 125" remove the word Apartment so it's just "123" or "123;124;125"
|
- If addr:unit is like "Apartment 123" or "Apartment 123;Apartment 124;Apartment 125" remove the word Apartment so it's just "123" or "123;124;125"
|
||||||
- If an item gets through processing and is "<Null>" it should be cleared
|
- If an item gets through processing and is "<Null>" it should be cleared
|
||||||
@@ -212,3 +218,4 @@ addr:postcode=33513
|
|||||||
addr:state=FL
|
addr:state=FL
|
||||||
addr:street=Hideaway Circle
|
addr:street=Hideaway Circle
|
||||||
- Sr is SR (State Route)
|
- Sr is SR (State Route)
|
||||||
|
- Pky is Parkway
|
||||||
|
|||||||
+161
-67
@@ -27,12 +27,15 @@ let mobileControlsOpen = false;
|
|||||||
let loadedCounty = '';
|
let loadedCounty = '';
|
||||||
let loadedDataType = '';
|
let loadedDataType = '';
|
||||||
|
|
||||||
|
// Tracks county features that have been imported (so countyStyle can highlight them)
|
||||||
|
let importedCountyFeatures = new Set();
|
||||||
|
|
||||||
// OSM node index for JOSM export snapping
|
// OSM node index for JOSM export snapping
|
||||||
let osmVertices = [];
|
let osmVertices = [];
|
||||||
let osmNodeById = new Map();
|
let osmNodeById = new Map();
|
||||||
let osmCoordToNodeId = new Map();
|
let osmCoordToNodeId = new Map();
|
||||||
let osmNodeVersionById = new Map();
|
let osmNodeVersionById = new Map();
|
||||||
const SNAP_TOL = 0.000135; // ~15m in degrees
|
let SNAP_TOL = 10 / 111320; // default 10m in degrees; updated from UI
|
||||||
|
|
||||||
// Snap preview layer and popup state
|
// Snap preview layer and popup state
|
||||||
let snapPreviewLayer = null;
|
let snapPreviewLayer = null;
|
||||||
@@ -54,6 +57,8 @@ function initMap() {
|
|||||||
map.createPane('osmPane');
|
map.createPane('osmPane');
|
||||||
map.createPane('diffPane');
|
map.createPane('diffPane');
|
||||||
map.createPane('countyPane');
|
map.createPane('countyPane');
|
||||||
|
map.createPane('snapPane');
|
||||||
|
map.getPane('snapPane').style.zIndex = 660; // above road panes (~652), below popups (700)
|
||||||
|
|
||||||
// Set initial z-indices for panes based on layerOrder
|
// Set initial z-indices for panes based on layerOrder
|
||||||
updateLayerZIndex();
|
updateLayerZIndex();
|
||||||
@@ -216,7 +221,7 @@ function clearSelection() {
|
|||||||
|
|
||||||
selectedFeatures = [];
|
selectedFeatures = [];
|
||||||
selectedLayers = [];
|
selectedLayers = [];
|
||||||
if (snapPreviewLayer) snapPreviewLayer.clearLayers();
|
updateSnapPreview();
|
||||||
|
|
||||||
if (featurePopup) {
|
if (featurePopup) {
|
||||||
map.closePopup(featurePopup);
|
map.closePopup(featurePopup);
|
||||||
@@ -350,22 +355,17 @@ function diffMarkerStyle(feature) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function countyStyle(feature) {
|
function countyStyle(feature) {
|
||||||
return {
|
if (importedCountyFeatures.has(feature)) {
|
||||||
color: '#ff00ff',
|
return { color: '#007bff', weight: 3, opacity: 0.8 };
|
||||||
weight: 3,
|
}
|
||||||
opacity: 0.8
|
return { color: '#ff00ff', weight: 3, opacity: 0.8 };
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function countyMarkerStyle(feature) {
|
function countyMarkerStyle(feature) {
|
||||||
return {
|
if (importedCountyFeatures.has(feature)) {
|
||||||
radius: 6,
|
return { radius: 6, fillColor: '#007bff', color: '#0056b3', weight: 1, opacity: 0.8, fillOpacity: 0.7 };
|
||||||
fillColor: '#ff00ff',
|
}
|
||||||
color: '#cc00cc',
|
return { radius: 6, fillColor: '#ff00ff', color: '#cc00cc', weight: 1, opacity: 0.8, fillOpacity: 0.6 };
|
||||||
weight: 1,
|
|
||||||
opacity: 0.8,
|
|
||||||
fillOpacity: 0.6
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter function for OSM features
|
// Filter function for OSM features
|
||||||
@@ -699,11 +699,7 @@ function selectFeature(feature, layer, e, layerType = 'diff') {
|
|||||||
|
|
||||||
// Show popup for single or multiple features
|
// Show popup for single or multiple features
|
||||||
showMultiFeaturePopup(e.latlng);
|
showMultiFeaturePopup(e.latlng);
|
||||||
|
updateSnapPreview();
|
||||||
if (layerType === 'diff' && feature.geometry.type === 'LineString') {
|
|
||||||
const isAdded = !feature.properties || (feature.properties.removed !== true && feature.properties.removed !== 'True');
|
|
||||||
if (isAdded && osmVertices.length > 0) updateSnapPreview(feature);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -712,8 +708,9 @@ function showMultiFeaturePopup(latlng) {
|
|||||||
if (selectedFeatures.length === 0) return;
|
if (selectedFeatures.length === 0) return;
|
||||||
popupLatlng = latlng;
|
popupLatlng = latlng;
|
||||||
|
|
||||||
// Remove old popup if exists
|
// Remove old popup without triggering clearSelection
|
||||||
if (featurePopup) {
|
if (featurePopup) {
|
||||||
|
featurePopup.off('remove');
|
||||||
map.closePopup(featurePopup);
|
map.closePopup(featurePopup);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -764,9 +761,15 @@ function showMultiFeaturePopup(latlng) {
|
|||||||
return a.localeCompare(b);
|
return a.localeCompare(b);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Compute layer types early so property display can adapt
|
||||||
|
const layerTypes = selectedLayers.map(l => l.type);
|
||||||
|
const uniqueLayerTypes = [...new Set(layerTypes)];
|
||||||
|
const allCounty = uniqueLayerTypes.length === 1 && uniqueLayerTypes[0] === 'county';
|
||||||
|
|
||||||
// For each property, check if all values are the same
|
// For each property, check if all values are the same
|
||||||
html += '<div style="font-size: 11px;">';
|
let propsHtml = '<div style="font-size: 11px;">';
|
||||||
for (const key of sortedKeys) {
|
for (const key of sortedKeys) {
|
||||||
|
if (key === 'osm_nodes' || key === 'osm_node_versions') continue;
|
||||||
const values = selectedFeatures
|
const values = selectedFeatures
|
||||||
.map(f => f.properties[key])
|
.map(f => f.properties[key])
|
||||||
.filter(v => v !== null && v !== undefined);
|
.filter(v => v !== null && v !== undefined);
|
||||||
@@ -777,20 +780,20 @@ function showMultiFeaturePopup(latlng) {
|
|||||||
|
|
||||||
let displayValue;
|
let displayValue;
|
||||||
if (uniqueValues.length === 1) {
|
if (uniqueValues.length === 1) {
|
||||||
// All values are the same
|
|
||||||
displayValue = uniqueValues[0];
|
displayValue = uniqueValues[0];
|
||||||
} else {
|
} else {
|
||||||
// Different values
|
|
||||||
displayValue = `<${uniqueValues.length} different values>`;
|
displayValue = `<${uniqueValues.length} different values>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
html += `<div style="margin: 2px 0;"><strong>${key}:</strong> ${displayValue}</div>`;
|
propsHtml += `<div style="margin: 2px 0;"><strong>${key}:</strong> ${displayValue}</div>`;
|
||||||
}
|
}
|
||||||
html += '</div>';
|
propsHtml += '</div>';
|
||||||
|
|
||||||
// Show layer types if mixed
|
if (allCounty) {
|
||||||
const layerTypes = selectedLayers.map(l => l.type);
|
html += `<details style="margin-top:4px;"><summary style="font-size:11px;cursor:pointer;color:#555;">Raw county data</summary>${propsHtml}</details>`;
|
||||||
const uniqueLayerTypes = [...new Set(layerTypes)];
|
} else {
|
||||||
|
html += propsHtml;
|
||||||
|
}
|
||||||
if (uniqueLayerTypes.length > 1) {
|
if (uniqueLayerTypes.length > 1) {
|
||||||
html += `<div style="margin-top: 8px;"><strong>Layers:</strong> ${uniqueLayerTypes.join(', ')}</div>`;
|
html += `<div style="margin-top: 8px;"><strong>Layers:</strong> ${uniqueLayerTypes.join(', ')}</div>`;
|
||||||
} else {
|
} else {
|
||||||
@@ -818,6 +821,19 @@ function showMultiFeaturePopup(latlng) {
|
|||||||
} else if (removedCount > 0) {
|
} else if (removedCount > 0) {
|
||||||
html += `<div style="margin-top: 8px;"><strong>Status:</strong> All Removed (Red)</div>`;
|
html += `<div style="margin-top: 8px;"><strong>Status:</strong> All Removed (Red)</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Per-feature connectivity / snap toggles for multiselect
|
||||||
|
if (selectedFeatures.length > 1 && osmVertices.length > 0) {
|
||||||
|
selectedLayers.forEach(({ type }, i) => {
|
||||||
|
if (type !== 'diff') return;
|
||||||
|
const feat = selectedFeatures[i];
|
||||||
|
const isRemoved = feat.properties && (feat.properties.removed === true || feat.properties.removed === 'True');
|
||||||
|
if (isRemoved) return;
|
||||||
|
const name = (feat.properties && feat.properties.name) || `Road ${i + 1}`;
|
||||||
|
html += `<div style="margin-top:6px;padding-top:4px;border-top:1px solid #eee;font-size:11px;font-weight:bold;">${name}</div>`;
|
||||||
|
html += connectivitySection(feat, i);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show accept/reject status if any are from diff layer
|
// Show accept/reject status if any are from diff layer
|
||||||
@@ -1160,6 +1176,9 @@ function cacheSnapInfo(feature) {
|
|||||||
const coords = feature.geometry.coordinates;
|
const coords = feature.geometry.coordinates;
|
||||||
feature._snapA = findNearestOsmVertex(coords[0]);
|
feature._snapA = findNearestOsmVertex(coords[0]);
|
||||||
feature._snapB = findNearestOsmVertex(coords[coords.length - 1]);
|
feature._snapB = findNearestOsmVertex(coords[coords.length - 1]);
|
||||||
|
feature._segSnapA = feature._snapA ? null : findNearestOsmWayPoint(coords[0], SNAP_TOL * 3);
|
||||||
|
feature._segSnapB = feature._snapB ? null : findNearestOsmWayPoint(coords[coords.length - 1], SNAP_TOL * 3);
|
||||||
|
feature._snapTol = SNAP_TOL;
|
||||||
feature._snappedCoords = [
|
feature._snappedCoords = [
|
||||||
feature._snapA ? feature._snapA.coord : coords[0],
|
feature._snapA ? feature._snapA.coord : coords[0],
|
||||||
...coords.slice(1, -1),
|
...coords.slice(1, -1),
|
||||||
@@ -1167,6 +1186,15 @@ function cacheSnapInfo(feature) {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function precalculateSnaps() {
|
||||||
|
if (!diffData || osmVertices.length === 0) return;
|
||||||
|
diffData.features.forEach(feat => {
|
||||||
|
if (!feat.geometry || feat.geometry.type !== 'LineString') return;
|
||||||
|
const isRemoved = feat.properties && (feat.properties.removed === true || feat.properties.removed === 'True');
|
||||||
|
if (!isRemoved) cacheSnapInfo(feat);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ---- County road tag conversion (mirrors diff-highways.py logic) ----
|
// ---- County road tag conversion (mirrors diff-highways.py logic) ----
|
||||||
|
|
||||||
function formatStreetName(s) {
|
function formatStreetName(s) {
|
||||||
@@ -1243,68 +1271,110 @@ function countyPropsToOsmTags(props) {
|
|||||||
|
|
||||||
// ---- Snap preview markers ----
|
// ---- Snap preview markers ----
|
||||||
|
|
||||||
function updateSnapPreview(feature) {
|
// Draw snap preview for one feature onto snapPreviewLayer.
|
||||||
if (!snapPreviewLayer) return;
|
// interactive=true also adds clickable endpoint dots (only used for the selected feature).
|
||||||
snapPreviewLayer.clearLayers();
|
function drawSnapPreviewForFeature(feature, interactive) {
|
||||||
if (!feature || !feature.geometry || feature.geometry.type !== 'LineString') return;
|
if (!feature || !feature.geometry || feature.geometry.type !== 'LineString') return;
|
||||||
|
|
||||||
cacheSnapInfo(feature);
|
|
||||||
const rawCoords = feature.geometry.coordinates;
|
const rawCoords = feature.geometry.coordinates;
|
||||||
|
|
||||||
[0, rawCoords.length - 1].forEach(i => {
|
const ends = [0, rawCoords.length - 1].map(i => {
|
||||||
const isStart = i === 0;
|
const isStart = i === 0;
|
||||||
const snap = isStart ? feature._snapA : feature._snapB;
|
const snap = isStart ? feature._snapA : feature._snapB;
|
||||||
const enabledFlag = isStart ? '_snapAEnabled' : '_snapBEnabled';
|
const enabledFlag = isStart ? '_snapAEnabled' : '_snapBEnabled';
|
||||||
const isEnabled = feature[enabledFlag] !== false;
|
const isEnabled = feature[enabledFlag] !== false;
|
||||||
const segSnap = !snap ? findNearestOsmWayPoint(rawCoords[i], SNAP_TOL * 3) : null;
|
const segSnap = isStart ? feature._segSnapA : feature._segSnapB;
|
||||||
|
const rawCoord = rawCoords[i];
|
||||||
|
let finalCoord = rawCoord;
|
||||||
|
if (isEnabled && snap) finalCoord = snap.coord;
|
||||||
|
else if (isEnabled && segSnap) finalCoord = segSnap.coord;
|
||||||
|
return { isStart, snap, segSnap, enabledFlag, isEnabled, rawCoord, finalCoord };
|
||||||
|
});
|
||||||
|
|
||||||
let coord, color, canToggle;
|
// Draw only the connector segment where an endpoint actually moves (raw → snapped).
|
||||||
|
// Don't redraw the full road — that duplicates what's already visible in the diff layer.
|
||||||
|
ends.forEach(({ rawCoord, finalCoord }) => {
|
||||||
|
if (Math.hypot(finalCoord[0] - rawCoord[0], finalCoord[1] - rawCoord[1]) < 1e-8) return;
|
||||||
|
L.polyline([[rawCoord[1], rawCoord[0]], [finalCoord[1], finalCoord[0]]], {
|
||||||
|
color: '#ff6b00', weight: 3, opacity: 0.85, dashArray: '7,5', pane: 'snapPane'
|
||||||
|
}).addTo(snapPreviewLayer);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!interactive) return;
|
||||||
|
|
||||||
|
ends.forEach(({ isStart, snap, segSnap, enabledFlag, isEnabled, finalCoord }) => {
|
||||||
|
let color, canToggle;
|
||||||
if (snap) {
|
if (snap) {
|
||||||
coord = snap.coord;
|
color = isEnabled ? '#28a745' : '#dc3545'; canToggle = true;
|
||||||
color = isEnabled ? '#28a745' : '#dc3545';
|
|
||||||
canToggle = true;
|
|
||||||
} else if (segSnap) {
|
} else if (segSnap) {
|
||||||
coord = segSnap.coord;
|
color = isEnabled ? '#007bff' : '#dc3545'; canToggle = true;
|
||||||
color = isEnabled ? '#007bff' : '#dc3545';
|
|
||||||
canToggle = true;
|
|
||||||
} else {
|
} else {
|
||||||
coord = rawCoords[i];
|
color = '#ffc107'; canToggle = false;
|
||||||
color = '#ffc107';
|
|
||||||
canToggle = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const marker = L.circleMarker([coord[1], coord[0]], {
|
const marker = L.circleMarker([finalCoord[1], finalCoord[0]], {
|
||||||
radius: 8, fillColor: color, color: '#fff', weight: 2, fillOpacity: 0.9
|
radius: 8, fillColor: color, color: '#fff', weight: 2, fillOpacity: 0.9, pane: 'snapPane'
|
||||||
});
|
});
|
||||||
|
|
||||||
if (canToggle) {
|
if (canToggle) {
|
||||||
marker.bindTooltip(isEnabled ? `Click to disable ${isStart ? 'start' : 'end'} snap` : 'Click to re-enable snap', { sticky: true });
|
marker.bindTooltip(
|
||||||
|
isEnabled ? `Click to disable ${isStart ? 'start' : 'end'} snap` : 'Click to re-enable snap',
|
||||||
|
{ sticky: true }
|
||||||
|
);
|
||||||
marker.on('click', function(e) {
|
marker.on('click', function(e) {
|
||||||
L.DomEvent.stopPropagation(e);
|
L.DomEvent.stopPropagation(e);
|
||||||
feature[enabledFlag] = !isEnabled;
|
feature[enabledFlag] = !isEnabled;
|
||||||
updateSnapPreview(feature);
|
updateSnapPreview();
|
||||||
if (popupLatlng) showMultiFeaturePopup(popupLatlng);
|
if (popupLatlng) showMultiFeaturePopup(popupLatlng);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
marker.bindTooltip(`${isStart ? 'Start' : 'End'}: dangling`, { sticky: true });
|
marker.bindTooltip(`${isStart ? 'Start' : 'End'}: dangling`, { sticky: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
snapPreviewLayer.addLayer(marker);
|
snapPreviewLayer.addLayer(marker);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
window.toggleSnapA = function() {
|
// Redraw snap preview layer:
|
||||||
if (selectedFeatures.length !== 1) return;
|
// - all accepted added features get a preview line when the checkbox is on
|
||||||
const feat = selectedFeatures[0];
|
// - the currently selected diff added feature always gets the interactive dot markers
|
||||||
feat._snapAEnabled = feat._snapAEnabled !== false ? false : true;
|
function updateSnapPreview() {
|
||||||
updateSnapPreview(feat);
|
if (!snapPreviewLayer) return;
|
||||||
|
snapPreviewLayer.clearLayers();
|
||||||
|
if (osmVertices.length === 0) return;
|
||||||
|
|
||||||
|
const showAll = document.getElementById('showSnapPreview')?.checked;
|
||||||
|
const selectedSet = new Set(selectedFeatures);
|
||||||
|
|
||||||
|
if (showAll) {
|
||||||
|
acceptedFeatures.forEach(feat => {
|
||||||
|
if (!feat.geometry || feat.geometry.type !== 'LineString') return;
|
||||||
|
const isRemoved = feat.properties && (feat.properties.removed === true || feat.properties.removed === 'True');
|
||||||
|
if (isRemoved || selectedSet.has(feat)) return;
|
||||||
|
drawSnapPreviewForFeature(feat, false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Selected diff added features always get interactive dots regardless of checkbox
|
||||||
|
selectedLayers.forEach(({ type }, i) => {
|
||||||
|
if (type !== 'diff') return;
|
||||||
|
const feat = selectedFeatures[i];
|
||||||
|
if (!feat || !feat.geometry || feat.geometry.type !== 'LineString') return;
|
||||||
|
const isRemoved = feat.properties && (feat.properties.removed === true || feat.properties.removed === 'True');
|
||||||
|
if (!isRemoved) drawSnapPreviewForFeature(feat, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
window.toggleSnapA = function(idx) {
|
||||||
|
idx = idx === undefined ? 0 : idx;
|
||||||
|
if (idx >= selectedFeatures.length) return;
|
||||||
|
selectedFeatures[idx]._snapAEnabled = selectedFeatures[idx]._snapAEnabled !== false ? false : true;
|
||||||
|
updateSnapPreview();
|
||||||
if (popupLatlng) showMultiFeaturePopup(popupLatlng);
|
if (popupLatlng) showMultiFeaturePopup(popupLatlng);
|
||||||
};
|
};
|
||||||
window.toggleSnapB = function() {
|
window.toggleSnapB = function(idx) {
|
||||||
if (selectedFeatures.length !== 1) return;
|
idx = idx === undefined ? 0 : idx;
|
||||||
const feat = selectedFeatures[0];
|
if (idx >= selectedFeatures.length) return;
|
||||||
feat._snapBEnabled = feat._snapBEnabled !== false ? false : true;
|
selectedFeatures[idx]._snapBEnabled = selectedFeatures[idx]._snapBEnabled !== false ? false : true;
|
||||||
updateSnapPreview(feat);
|
updateSnapPreview();
|
||||||
if (popupLatlng) showMultiFeaturePopup(popupLatlng);
|
if (popupLatlng) showMultiFeaturePopup(popupLatlng);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1319,19 +1389,28 @@ window.importCountyFeature = function() {
|
|||||||
properties: osmTags,
|
properties: osmTags,
|
||||||
_countySource: source
|
_countySource: source
|
||||||
};
|
};
|
||||||
|
importedCountyFeatures.add(source);
|
||||||
acceptedFeatures.add(synthetic);
|
acceptedFeatures.add(synthetic);
|
||||||
cacheSnapInfo(synthetic);
|
cacheSnapInfo(synthetic);
|
||||||
|
if (countyLayer) {
|
||||||
|
countyLayer.eachLayer(l => { if (l.feature === source) l.setStyle(countyStyle(source)); });
|
||||||
|
}
|
||||||
|
updateSnapPreview();
|
||||||
showStatus(`Imported "${osmTags.name || 'unnamed'}" from county data`, 'success');
|
showStatus(`Imported "${osmTags.name || 'unnamed'}" from county data`, 'success');
|
||||||
if (popupLatlng) showMultiFeaturePopup(popupLatlng);
|
if (popupLatlng) showMultiFeaturePopup(popupLatlng);
|
||||||
};
|
};
|
||||||
|
|
||||||
function connectivitySection(feature) {
|
function connectivitySection(feature, idx) {
|
||||||
|
if (idx === undefined) idx = 0;
|
||||||
if (!feature.geometry || feature.geometry.type !== 'LineString') return '';
|
if (!feature.geometry || feature.geometry.type !== 'LineString') return '';
|
||||||
|
if (osmVertices.length > 0 && (feature._snapA === undefined || feature._snapTol !== SNAP_TOL)) {
|
||||||
|
cacheSnapInfo(feature);
|
||||||
|
}
|
||||||
const coords = feature.geometry.coordinates;
|
const coords = feature.geometry.coordinates;
|
||||||
const snapA = findNearestOsmVertex(coords[0]);
|
const snapA = feature._snapA;
|
||||||
const snapB = findNearestOsmVertex(coords[coords.length - 1]);
|
const snapB = feature._snapB;
|
||||||
const segA = snapA ? null : findNearestOsmWayPoint(coords[0], SNAP_TOL * 3);
|
const segA = feature._segSnapA;
|
||||||
const segB = snapB ? null : findNearestOsmWayPoint(coords[coords.length - 1], SNAP_TOL * 3);
|
const segB = feature._segSnapB;
|
||||||
|
|
||||||
// Check if an endpoint connects to another new diff road (shares coordinate within tolerance)
|
// Check if an endpoint connects to another new diff road (shares coordinate within tolerance)
|
||||||
function findDiffConnection(coord) {
|
function findDiffConnection(coord) {
|
||||||
@@ -1352,7 +1431,7 @@ function connectivitySection(feature) {
|
|||||||
const enabledFlag = toggleFn === 'toggleSnapA' ? '_snapAEnabled' : '_snapBEnabled';
|
const enabledFlag = toggleFn === 'toggleSnapA' ? '_snapAEnabled' : '_snapBEnabled';
|
||||||
const isEnabled = feature[enabledFlag] !== false;
|
const isEnabled = feature[enabledFlag] !== false;
|
||||||
const toggleBtn = (snap || segSnap)
|
const toggleBtn = (snap || segSnap)
|
||||||
? ` <button onclick="${toggleFn}()" style="font-size:10px;padding:1px 5px;cursor:pointer;">${isEnabled ? 'Disable snap' : 'Enable snap'}</button>`
|
? ` <button onclick="${toggleFn}(${idx})" style="font-size:10px;padding:1px 5px;cursor:pointer;">${isEnabled ? 'Disable snap' : 'Enable snap'}</button>`
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
if (snap) {
|
if (snap) {
|
||||||
@@ -2050,6 +2129,21 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
createCountyLayer();
|
createCountyLayer();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.getElementById('showSnapPreview').addEventListener('change', function() {
|
||||||
|
updateSnapPreview();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('snapDistance').addEventListener('change', function() {
|
||||||
|
const meters = Math.max(1, parseFloat(this.value) || 10);
|
||||||
|
this.value = meters;
|
||||||
|
SNAP_TOL = meters / 111320;
|
||||||
|
acceptedFeatures.forEach(feat => {
|
||||||
|
if (feat.geometry && feat.geometry.type === 'LineString') cacheSnapInfo(feat);
|
||||||
|
});
|
||||||
|
updateSnapPreview();
|
||||||
|
if (popupLatlng && selectedFeatures.length > 0) showMultiFeaturePopup(popupLatlng);
|
||||||
|
});
|
||||||
|
|
||||||
// Click on empty map area closes any open popup and clears selection
|
// Click on empty map area closes any open popup and clears selection
|
||||||
map.on('click', function(e) {
|
map.on('click', function(e) {
|
||||||
if (multiSelectMode) return;
|
if (multiSelectMode) return;
|
||||||
|
|||||||
+33
-23
@@ -30,37 +30,37 @@
|
|||||||
right: 10px;
|
right: 10px;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
background: white;
|
background: white;
|
||||||
padding: 15px;
|
padding: 10px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
|
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
|
||||||
min-width: 200px;
|
min-width: 175px;
|
||||||
max-height: calc(100vh - 20px);
|
max-height: calc(100vh - 20px);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.controls h3 {
|
.controls h3 {
|
||||||
margin: 0 0 10px 0;
|
margin: 0 0 6px 0;
|
||||||
font-size: 14px;
|
font-size: 12px;
|
||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
.controls label {
|
.controls label {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin: 8px 0;
|
margin: 4px 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 13px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.layer-item {
|
.layer-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin: 8px 0;
|
margin: 4px 0;
|
||||||
padding: 5px;
|
padding: 3px 5px;
|
||||||
background: #f8f9fa;
|
background: #f8f9fa;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
cursor: move;
|
cursor: move;
|
||||||
font-size: 13px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.layer-item.dragging {
|
.layer-item.dragging {
|
||||||
@@ -77,14 +77,14 @@
|
|||||||
|
|
||||||
.controls button {
|
.controls button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 10px;
|
padding: 7px;
|
||||||
margin-top: 10px;
|
margin-top: 7px;
|
||||||
background: #007bff;
|
background: #007bff;
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 13px;
|
font-size: 12px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,10 +98,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.status {
|
.status {
|
||||||
margin-top: 10px;
|
margin-top: 7px;
|
||||||
padding: 8px;
|
padding: 6px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 12px;
|
font-size: 11px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,8 +120,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.file-input-group {
|
.file-input-group {
|
||||||
margin-bottom: 15px;
|
margin-bottom: 8px;
|
||||||
padding-bottom: 15px;
|
padding-bottom: 8px;
|
||||||
border-bottom: 1px solid #eee;
|
border-bottom: 1px solid #eee;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,7 +131,7 @@
|
|||||||
|
|
||||||
.file-input-group label {
|
.file-input-group label {
|
||||||
display: block;
|
display: block;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 3px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,7 +139,7 @@
|
|||||||
.file-input-group select {
|
.file-input-group select {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
padding: 5px;
|
padding: 3px;
|
||||||
border: 1px solid #ccc;
|
border: 1px solid #ccc;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
@@ -302,7 +302,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 style="margin-top: 15px;">Diff Filters</h3>
|
<h3 style="margin-top: 10px;">Diff Filters</h3>
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" id="showAdded" checked>
|
<input type="checkbox" id="showAdded" checked>
|
||||||
Show Added (Green)
|
Show Added (Green)
|
||||||
@@ -319,16 +319,26 @@
|
|||||||
<input type="checkbox" id="hideUnclassified">
|
<input type="checkbox" id="hideUnclassified">
|
||||||
Hide unclassified
|
Hide unclassified
|
||||||
</label>
|
</label>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" id="showSnapPreview" checked>
|
||||||
|
Show snap preview
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Snap distance:
|
||||||
|
<input type="number" id="snapDistance" value="10" min="1" max="500" step="1"
|
||||||
|
style="width:42px;margin:0 3px;padding:1px 3px;border:1px solid #ccc;border-radius:3px;font-size:11px;">
|
||||||
|
m
|
||||||
|
</label>
|
||||||
|
|
||||||
<h3 style="margin-top: 15px;">Selection Mode</h3>
|
<h3 style="margin-top: 10px;">Selection Mode</h3>
|
||||||
<button id="multiSelectToggle" style="width: 100%; padding: 8px; background: #6c757d; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 13px;">
|
<button id="multiSelectToggle" style="width: 100%; padding: 8px; background: #6c757d; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 13px;">
|
||||||
Multi-Select: OFF
|
Multi-Select: OFF
|
||||||
</button>
|
</button>
|
||||||
<div style="margin-top: 8px; padding: 8px; background: #e7f3ff; border-radius: 4px; font-size: 11px; color: #004085;">
|
<div style="margin-top: 5px; padding: 5px; background: #e7f3ff; border-radius: 4px; font-size: 10px; color: #004085;">
|
||||||
<strong>Tip:</strong> Enable multi-select or hold Shift to select multiple features by clicking or dragging a box
|
<strong>Tip:</strong> Enable multi-select or hold Shift to select multiple features by clicking or dragging a box
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 style="margin-top: 15px;">Load Data</h3>
|
<h3 style="margin-top: 10px;">Load Data</h3>
|
||||||
<div class="file-input-group">
|
<div class="file-input-group">
|
||||||
<label for="countySelect">County:</label>
|
<label for="countySelect">County:</label>
|
||||||
<select id="countySelect">
|
<select id="countySelect">
|
||||||
|
|||||||
Reference in New Issue
Block a user