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 {
|
&.leaflet-control-overlays-expanded > .content {
|
||||||
display: flex;
|
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('');
|
$('#sidebar').addClass('loading').find('.content').html('');
|
||||||
editor._cancel_editing();
|
editor._cancel_editing();
|
||||||
|
|
||||||
|
// Clear snap indicators when unloading
|
||||||
|
editor._clear_snap_indicators();
|
||||||
},
|
},
|
||||||
_fill_level_control: function (level_control, level_list, geometryURLs) {
|
_fill_level_control: function (level_control, level_list, geometryURLs) {
|
||||||
var levels = level_list.find('a');
|
var levels = level_list.find('a');
|
||||||
|
@ -815,6 +818,9 @@ editor = {
|
||||||
|
|
||||||
editor.map.on('zoomend', editor._adjust_line_zoom);
|
editor.map.on('zoomend', editor._adjust_line_zoom);
|
||||||
|
|
||||||
|
// Initialize snap-to-edges functionality
|
||||||
|
editor.init_snap_to_edges();
|
||||||
|
|
||||||
c3nav_api.get('editor/geometrystyles')
|
c3nav_api.get('editor/geometrystyles')
|
||||||
.then(geometrystyles => {
|
.then(geometrystyles => {
|
||||||
editor.geometrystyles = geometrystyles;
|
editor.geometrystyles = geometrystyles;
|
||||||
|
@ -1442,6 +1448,9 @@ editor = {
|
||||||
editor._editing_layer.disableEdit();
|
editor._editing_layer.disableEdit();
|
||||||
editor._editing_layer = null;
|
editor._editing_layer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear snap indicators when canceling editing
|
||||||
|
editor._clear_snap_indicators();
|
||||||
},
|
},
|
||||||
_canceled_creating: function (e) {
|
_canceled_creating: function (e) {
|
||||||
// called after we canceled creating so we can remove the temporary layer.
|
// called after we canceled creating so we can remove the temporary layer.
|
||||||
|
@ -1652,6 +1661,224 @@ editor = {
|
||||||
editor._wifi_scan_waits = true;
|
editor._wifi_scan_waits = true;
|
||||||
mobileclient.scanNow();
|
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