add snap-to-edge functionality

This commit is contained in:
Degra02 2025-08-01 21:29:19 +02:00
parent 87bbb15bad
commit 31939859c1
4 changed files with 285 additions and 0 deletions

Binary file not shown.

Binary file not shown.

View file

@ -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);
}
}

View file

@ -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();
}
}
};