add snap-to-edge functionality
This commit is contained in:
parent
87bbb15bad
commit
31939859c1
4 changed files with 285 additions and 0 deletions
BIN
NOI Hackathon 2025 - Open Data Hub.pdf
Normal file
BIN
NOI Hackathon 2025 - Open Data Hub.pdf
Normal file
Binary file not shown.
BIN
Open Data Hub challenge_more info.pdf
Normal file
BIN
Open Data Hub challenge_more info.pdf
Normal file
Binary file not shown.
|
@ -561,4 +561,62 @@ label.theme-color-label {
|
|||
&.leaflet-control-overlays-expanded > .content {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
/* Snap-to-edges control styles */
|
||||
.leaflet-control-snap {
|
||||
background-color: white;
|
||||
border-radius: 4px;
|
||||
border: 2px solid rgba(0,0,0,0.2);
|
||||
background-clip: padding-box;
|
||||
|
||||
.snap-toggle {
|
||||
display: block;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
line-height: 26px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
font-size: 18px;
|
||||
background-color: white;
|
||||
color: #666;
|
||||
border-radius: 2px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: #f4f4f4;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: #4CAF50;
|
||||
color: white;
|
||||
|
||||
&:hover {
|
||||
background-color: #45a049;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Snap indicator styles */
|
||||
.snap-indicator {
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
animation: snap-pulse 1s infinite;
|
||||
}
|
||||
|
||||
@keyframes snap-pulse {
|
||||
0% {
|
||||
opacity: 0.8;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
100% {
|
||||
opacity: 0.8;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
|
@ -182,6 +182,9 @@ editor = {
|
|||
|
||||
$('#sidebar').addClass('loading').find('.content').html('');
|
||||
editor._cancel_editing();
|
||||
|
||||
// Clear snap indicators when unloading
|
||||
editor._clear_snap_indicators();
|
||||
},
|
||||
_fill_level_control: function (level_control, level_list, geometryURLs) {
|
||||
var levels = level_list.find('a');
|
||||
|
@ -815,6 +818,9 @@ editor = {
|
|||
|
||||
editor.map.on('zoomend', editor._adjust_line_zoom);
|
||||
|
||||
// Initialize snap-to-edges functionality
|
||||
editor.init_snap_to_edges();
|
||||
|
||||
c3nav_api.get('editor/geometrystyles')
|
||||
.then(geometrystyles => {
|
||||
editor.geometrystyles = geometrystyles;
|
||||
|
@ -1442,6 +1448,9 @@ editor = {
|
|||
editor._editing_layer.disableEdit();
|
||||
editor._editing_layer = null;
|
||||
}
|
||||
|
||||
// Clear snap indicators when canceling editing
|
||||
editor._clear_snap_indicators();
|
||||
},
|
||||
_canceled_creating: function (e) {
|
||||
// called after we canceled creating so we can remove the temporary layer.
|
||||
|
@ -1652,6 +1661,224 @@ editor = {
|
|||
editor._wifi_scan_waits = true;
|
||||
mobileclient.scanNow();
|
||||
}
|
||||
},
|
||||
|
||||
// Snap-to-edges functionality
|
||||
_snap_enabled: true,
|
||||
_snap_distance: 15, // pixels
|
||||
_snap_to_base_map: false,
|
||||
_snap_indicator: null,
|
||||
_snap_candidates: [],
|
||||
|
||||
init_snap_to_edges: function() {
|
||||
// Initialize snap indicator layer
|
||||
editor._snap_indicator = L.layerGroup().addTo(editor.map);
|
||||
|
||||
// Override existing drawing event handlers to include snapping
|
||||
editor.map.on('editable:drawing:move', editor._handle_snap_during_draw);
|
||||
editor.map.on('editable:vertex:drag', editor._handle_snap_during_vertex_drag);
|
||||
|
||||
// Add snap toggle to UI
|
||||
editor._add_snap_controls();
|
||||
},
|
||||
|
||||
_add_snap_controls: function() {
|
||||
// Add snap toggle control to the map
|
||||
var snapControl = L.control({position: 'topleft'});
|
||||
snapControl.onAdd = function() {
|
||||
var container = L.DomUtil.create('div', 'leaflet-bar leaflet-control leaflet-control-snap');
|
||||
container.innerHTML = '<a href="#" title="Toggle Snap to Edges" class="snap-toggle ' +
|
||||
(editor._snap_enabled ? 'active' : '') + '">⚡</a>';
|
||||
|
||||
L.DomEvent.on(container.querySelector('.snap-toggle'), 'click', function(e) {
|
||||
e.preventDefault();
|
||||
editor._toggle_snap();
|
||||
});
|
||||
|
||||
L.DomEvent.disableClickPropagation(container);
|
||||
return container;
|
||||
};
|
||||
snapControl.addTo(editor.map);
|
||||
},
|
||||
|
||||
_toggle_snap: function() {
|
||||
editor._snap_enabled = !editor._snap_enabled;
|
||||
var toggle = document.querySelector('.snap-toggle');
|
||||
if (toggle) {
|
||||
toggle.classList.toggle('active', editor._snap_enabled);
|
||||
}
|
||||
// Clear any existing snap indicators
|
||||
editor._clear_snap_indicators();
|
||||
},
|
||||
|
||||
_handle_snap_during_draw: function(e) {
|
||||
if (!editor._snap_enabled || !editor._creating) return;
|
||||
|
||||
var snapped = editor._find_and_apply_snap(e.latlng);
|
||||
if (snapped) {
|
||||
e.latlng.lat = snapped.lat;
|
||||
e.latlng.lng = snapped.lng;
|
||||
}
|
||||
|
||||
// Apply existing rounding
|
||||
e.latlng.lat = Math.round(e.latlng.lat * 100) / 100;
|
||||
e.latlng.lng = Math.round(e.latlng.lng * 100) / 100;
|
||||
},
|
||||
|
||||
_handle_snap_during_vertex_drag: function(e) {
|
||||
if (!editor._snap_enabled) return;
|
||||
|
||||
var snapped = editor._find_and_apply_snap(e.latlng);
|
||||
if (snapped) {
|
||||
e.latlng.lat = snapped.lat;
|
||||
e.latlng.lng = snapped.lng;
|
||||
}
|
||||
|
||||
// Apply existing rounding and other constraints
|
||||
e.vertex.setLatLng([Math.round(e.latlng.lat * 100) / 100, Math.round(e.latlng.lng * 100) / 100]);
|
||||
},
|
||||
|
||||
_find_and_apply_snap: function(latlng) {
|
||||
if (!editor._geometries_layer) return null;
|
||||
|
||||
var mapPoint = editor.map.latLngToContainerPoint(latlng);
|
||||
var candidates = [];
|
||||
|
||||
// Find snap candidates from existing geometries
|
||||
editor._geometries_layer.eachLayer(function(layer) {
|
||||
if (layer === editor._editing_layer) return; // Don't snap to self
|
||||
|
||||
var snapPoint = editor._find_closest_point_on_geometry(layer, latlng, mapPoint);
|
||||
if (snapPoint && snapPoint.distance < editor._snap_distance) {
|
||||
candidates.push(snapPoint);
|
||||
}
|
||||
});
|
||||
|
||||
// Find the closest candidate
|
||||
if (candidates.length > 0) {
|
||||
candidates.sort(function(a, b) { return a.distance - b.distance; });
|
||||
var best = candidates[0];
|
||||
|
||||
// Show snap indicator
|
||||
editor._show_snap_indicator(best.latlng);
|
||||
|
||||
return best.latlng;
|
||||
} else {
|
||||
editor._clear_snap_indicators();
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
_find_closest_point_on_geometry: function(layer, targetLatLng, targetMapPoint) {
|
||||
if (!layer.getLatLngs) return null;
|
||||
|
||||
var closestPoint = null;
|
||||
var closestDistance = Infinity;
|
||||
|
||||
try {
|
||||
var coordinates = [];
|
||||
|
||||
// Handle different geometry types
|
||||
if (layer instanceof L.Polygon || layer instanceof L.Polyline) {
|
||||
coordinates = layer.getLatLngs();
|
||||
if (coordinates[0] && Array.isArray(coordinates[0])) {
|
||||
coordinates = coordinates[0]; // Handle polygon with holes
|
||||
}
|
||||
} else if (layer instanceof L.Circle || layer instanceof L.CircleMarker) {
|
||||
// For circles, snap to center
|
||||
var center = layer.getLatLng();
|
||||
var centerMapPoint = editor.map.latLngToContainerPoint(center);
|
||||
var distance = centerMapPoint.distanceTo(targetMapPoint);
|
||||
if (distance < editor._snap_distance) {
|
||||
return {
|
||||
latlng: center,
|
||||
distance: distance
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check each edge of the geometry
|
||||
for (var i = 0; i < coordinates.length; i++) {
|
||||
var p1 = coordinates[i];
|
||||
var p2 = coordinates[(i + 1) % coordinates.length];
|
||||
|
||||
var snapPoint = editor._find_closest_point_on_edge(p1, p2, targetLatLng, targetMapPoint);
|
||||
if (snapPoint && snapPoint.distance < closestDistance) {
|
||||
closestDistance = snapPoint.distance;
|
||||
closestPoint = snapPoint;
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
// Silently handle geometry access errors
|
||||
return null;
|
||||
}
|
||||
|
||||
return closestPoint;
|
||||
},
|
||||
|
||||
_find_closest_point_on_edge: function(p1, p2, targetLatLng, targetMapPoint) {
|
||||
var p1Map = editor.map.latLngToContainerPoint(p1);
|
||||
var p2Map = editor.map.latLngToContainerPoint(p2);
|
||||
|
||||
// Find closest point on line segment
|
||||
var dx = p2Map.x - p1Map.x;
|
||||
var dy = p2Map.y - p1Map.y;
|
||||
var length = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (length === 0) {
|
||||
// Points are the same, snap to the point
|
||||
var distance = p1Map.distanceTo(targetMapPoint);
|
||||
return {
|
||||
latlng: p1,
|
||||
distance: distance
|
||||
};
|
||||
}
|
||||
|
||||
// Calculate parameter t for closest point on line
|
||||
var t = ((targetMapPoint.x - p1Map.x) * dx + (targetMapPoint.y - p1Map.y) * dy) / (length * length);
|
||||
t = Math.max(0, Math.min(1, t)); // Clamp to line segment
|
||||
|
||||
// Calculate closest point
|
||||
var closestMapPoint = {
|
||||
x: p1Map.x + t * dx,
|
||||
y: p1Map.y + t * dy
|
||||
};
|
||||
|
||||
var distance = Math.sqrt(
|
||||
Math.pow(closestMapPoint.x - targetMapPoint.x, 2) +
|
||||
Math.pow(closestMapPoint.y - targetMapPoint.y, 2)
|
||||
);
|
||||
|
||||
// Convert back to lat/lng
|
||||
var closestLatLng = editor.map.containerPointToLatLng(closestMapPoint);
|
||||
|
||||
return {
|
||||
latlng: closestLatLng,
|
||||
distance: distance
|
||||
};
|
||||
},
|
||||
|
||||
_show_snap_indicator: function(latlng) {
|
||||
editor._clear_snap_indicators();
|
||||
|
||||
var indicator = L.circleMarker(latlng, {
|
||||
radius: 4,
|
||||
color: '#ff6b6b',
|
||||
fillColor: '#ff6b6b',
|
||||
fillOpacity: 0.8,
|
||||
weight: 2,
|
||||
className: 'snap-indicator'
|
||||
});
|
||||
|
||||
editor._snap_indicator.addLayer(indicator);
|
||||
},
|
||||
|
||||
_clear_snap_indicators: function() {
|
||||
if (editor._snap_indicator) {
|
||||
editor._snap_indicator.clearLayers();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue