This commit is contained in:
Alessio 2025-08-02 07:58:53 +02:00
commit 98794751d0
5 changed files with 483 additions and 68 deletions

View file

@ -607,3 +607,71 @@ label.theme-color-label {
z-index: 1000;
pointer-events: none;
}
@keyframes snap-pulse {
0% {
opacity: 0.8;
transform: scale(1);
}
50% {
opacity: 1;
transform: scale(1.2);
}
100% {
opacity: 0.8;
transform: scale(1);
}
}
/* Edge highlight styles for snap-to-edges */
.edge-highlight {
z-index: 999;
pointer-events: none;
animation: edge-fade-in 0.2s ease-in;
}
.original-edge-highlight {
z-index: 1000;
pointer-events: none;
animation: edge-fade-in 0.2s ease-in;
}
@keyframes edge-fade-in {
0% {
opacity: 0;
}
100% {
opacity: 0.6;
}
}
/* Right-angle snap indicators */
.right-angle-reference {
z-index: 998;
pointer-events: none;
animation: edge-fade-in 0.2s ease-in;
}
.right-angle-line {
z-index: 1001;
pointer-events: none;
animation: right-angle-pulse 2s infinite;
}
.right-angle-square {
z-index: 1002;
pointer-events: none;
animation: right-angle-pulse 2s infinite;
}
@keyframes right-angle-pulse {
0% {
opacity: 0.7;
}
50% {
opacity: 1;
}
100% {
opacity: 0.7;
}
}

View file

@ -182,7 +182,7 @@ editor = {
$('#sidebar').addClass('loading').find('.content').html('');
editor._cancel_editing();
// Clear snap indicators when unloading
editor._clear_snap_indicators();
},
@ -1448,7 +1448,7 @@ editor = {
editor._editing_layer.disableEdit();
editor._editing_layer = null;
}
// Clear snap indicators when canceling editing
editor._clear_snap_indicators();
},
@ -1665,36 +1665,33 @@ editor = {
// Snap-to-edges functionality
_snap_enabled: true,
_snap_distance: 15, // pixels
_snap_to_base_map: false,
_snap_distance: 30, // pixels
_extension_area_multiplier: 4, // Extension area = snap_distance * this multiplier
_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 ' +
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;
};
@ -1707,61 +1704,79 @@ editor = {
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
// Apply 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
// check for right-angle snap to current shape vertices
var rightAngleSnap = editor._find_right_angle_snap(latlng, mapPoint);
if (rightAngleSnap) {
candidates.push(rightAngleSnap);
}
// find snap candidates from existing geometries with area-limited infinite extension
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 (layer === editor._editing_layer) return; // don't snap to self
// check if layer is within the area limit for infinite extension
var allowInfiniteExtension = editor._is_layer_in_extension_area(layer, latlng, mapPoint);
var snapPoint = editor._find_closest_point_on_geometry(layer, latlng, mapPoint, allowInfiniteExtension);
if (snapPoint && snapPoint.distance < editor._snap_distance) {
candidates.push(snapPoint);
}
});
// check current editing shape with infinite extension enabled
if (editor._current_editing_shape) {
var currentShapeSnap = editor._find_closest_point_on_geometry(
editor._current_editing_shape, latlng, mapPoint, true // Always enable infinite extension for current shape
);
if (currentShapeSnap && currentShapeSnap.distance < editor._snap_distance) {
candidates.push(currentShapeSnap);
}
}
// 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);
// show snap indicator with edge highlighting
editor._show_snap_indicator(best.latlng, best);
return best.latlng;
} else {
editor._clear_snap_indicators();
@ -1769,99 +1784,296 @@ editor = {
}
},
_find_closest_point_on_geometry: function(layer, targetLatLng, targetMapPoint) {
if (!layer.getLatLngs) return null;
var closestPoint = null;
var closestDistance = Infinity;
_is_layer_in_extension_area: function(layer, targetLatLng, targetMapPoint) {
if (!layer.getLatLngs) return false;
// skip circles entirely for infinite extension
if (layer instanceof L.Circle || layer instanceof L.CircleMarker) {
return false;
}
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];
}
}
if (coordinates.length === 0) return false;
// extension area radius (in pixels)
var extensionAreaRadius = editor._snap_distance * editor._extension_area_multiplier;
// check if any vertex of the layer is within the extension area
for (var i = 0; i < coordinates.length; i++) {
var vertexMapPoint = editor.map.latLngToContainerPoint(coordinates[i]);
var distanceToVertex = vertexMapPoint.distanceTo(targetMapPoint);
if (distanceToVertex <= extensionAreaRadius) {
return true;
}
}
for (var i = 0; i < coordinates.length; i++) {
var p1 = coordinates[i];
var p2 = coordinates[(i + 1) % coordinates.length];
if (editor._edge_intersects_circle(p1, p2, targetLatLng, targetMapPoint, extensionAreaRadius)) {
return true;
}
}
return false;
} catch (error) {
return false;
}
},
_edge_intersects_circle: function(edgeStart, edgeEnd, circleCenter, circleCenterMap, radius) {
var p1Map = editor.map.latLngToContainerPoint(edgeStart);
var p2Map = editor.map.latLngToContainerPoint(edgeEnd);
// find closest point on edge to circle center
var dx = p2Map.x - p1Map.x;
var dy = p2Map.y - p1Map.y;
var length = Math.sqrt(dx * dx + dy * dy);
if (length === 0) {
return p1Map.distanceTo(circleCenterMap) <= radius;
}
var t = Math.max(0, Math.min(1,
((circleCenterMap.x - p1Map.x) * dx + (circleCenterMap.y - p1Map.y) * dy) / (length * length)
));
var closestPoint = {
x: p1Map.x + t * dx,
y: p1Map.y + t * dy
};
var distance = Math.sqrt(
Math.pow(closestPoint.x - circleCenterMap.x, 2) +
Math.pow(closestPoint.y - circleCenterMap.y, 2)
);
return distance <= radius;
},
_find_closest_point_on_geometry: function(layer, targetLatLng, targetMapPoint, allowInfiniteExtension) {
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
distance: distance,
edgeStart: center,
edgeEnd: center,
isInfiniteExtension: false,
isRightAngle: false
};
}
return null;
}
// Check each edge of the geometry
// 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);
var snapPoint = editor._find_closest_point_on_edge(p1, p2, targetLatLng, targetMapPoint, allowInfiniteExtension);
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) {
_find_closest_point_on_edge: function(p1, p2, targetLatLng, targetMapPoint, allowInfiniteExtension) {
var p1Map = editor.map.latLngToContainerPoint(p1);
var p2Map = editor.map.latLngToContainerPoint(p2);
// Find closest point on line segment
// find closest point on line (infinite or segment based on allowInfiniteExtension)
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
// points are the same, snap to the point
var distance = p1Map.distanceTo(targetMapPoint);
return {
latlng: p1,
distance: distance
distance: distance,
edgeStart: p1,
edgeEnd: p2,
isInfiniteExtension: false,
isRightAngle: false
};
}
// Calculate parameter t for closest point on line
// 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
// clamp t based on allowInfiniteExtension
var originalT = t;
if (!allowInfiniteExtension) {
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.x - targetMapPoint.x, 2) +
Math.pow(closestMapPoint.y - targetMapPoint.y, 2)
);
// Convert back to lat/lng
var closestLatLng = editor.map.containerPointToLatLng(closestMapPoint);
// determine if this is an infinite extension
var isInfiniteExtension = allowInfiniteExtension && (originalT < 0 || originalT > 1);
return {
latlng: closestLatLng,
distance: distance
distance: distance,
edgeStart: p1,
edgeEnd: p2,
isInfiniteExtension: isInfiniteExtension,
isRightAngle: false,
t: originalT
};
},
_show_snap_indicator: function(latlng) {
_find_right_angle_snap: function(targetLatLng, targetMapPoint) {
if (!editor._current_editing_shape) return null;
try {
var coordinates = [];
if (editor._current_editing_shape.getLatLngs) {
coordinates = editor._current_editing_shape.getLatLngs();
if (coordinates[0] && Array.isArray(coordinates[0])) {
coordinates = coordinates[0]; // Handle polygon with holes
}
} else {
return null;
}
if (coordinates.length < 2) return null;
var bestRightAngleSnap = null;
var closestDistance = Infinity;
// Check each vertex for potential right-angle formation
for (var i = 0; i < coordinates.length; i++) {
var vertex = coordinates[i];
var vertexMap = editor.map.latLngToContainerPoint(vertex);
var distanceToVertex = vertexMap.distanceTo(targetMapPoint);
if (distanceToVertex > editor._snap_distance * 2) continue; // Larger radius for right-angle detection
var prevVertex = coordinates[(i - 1 + coordinates.length) % coordinates.length];
var nextVertex = coordinates[(i + 1) % coordinates.length];
var rightAngleSnap1 = editor._calculate_right_angle_snap(vertex, prevVertex, targetLatLng, targetMapPoint);
if (rightAngleSnap1 && rightAngleSnap1.distance < closestDistance) {
closestDistance = rightAngleSnap1.distance;
bestRightAngleSnap = rightAngleSnap1;
}
var rightAngleSnap2 = editor._calculate_right_angle_snap(vertex, nextVertex, targetLatLng, targetMapPoint);
if (rightAngleSnap2 && rightAngleSnap2.distance < closestDistance) {
closestDistance = rightAngleSnap2.distance;
bestRightAngleSnap = rightAngleSnap2;
}
}
return bestRightAngleSnap;
} catch (error) {
return null;
}
},
_calculate_right_angle_snap: function(vertex, adjacentVertex, targetLatLng, targetMapPoint) {
var vertexMap = editor.map.latLngToContainerPoint(vertex);
var adjacentMap = editor.map.latLngToContainerPoint(adjacentVertex);
var edgeDx = adjacentMap.x - vertexMap.x;
var edgeDy = adjacentMap.y - vertexMap.y;
var edgeLength = Math.sqrt(edgeDx * edgeDx + edgeDy * edgeDy);
if (edgeLength === 0) return null;
var perpDx = -edgeDy / edgeLength;
var perpDy = edgeDx / edgeLength;
var targetDx = targetMapPoint.x - vertexMap.x;
var targetDy = targetMapPoint.y - vertexMap.y;
var targetLength = Math.sqrt(targetDx * targetDx + targetDy * targetDy);
if (targetLength === 0) return null;
var projectionLength = targetDx * perpDx + targetDy * perpDy;
var rightAngleMapPoint = {
x: vertexMap.x + projectionLength * perpDx,
y: vertexMap.y + projectionLength * perpDy
};
var distance = Math.sqrt(
Math.pow(rightAngleMapPoint.x - targetMapPoint.x, 2) +
Math.pow(rightAngleMapPoint.y - targetMapPoint.y, 2)
);
if (distance < editor._snap_distance && Math.abs(projectionLength) > 10) { // minimum 10 pixels away from vertex
var rightAngleLatLng = editor.map.containerPointToLatLng(rightAngleMapPoint);
return {
latlng: rightAngleLatLng,
distance: distance,
edgeStart: vertex,
edgeEnd: rightAngleLatLng,
isInfiniteExtension: false,
isRightAngle: true,
rightAngleVertex: vertex,
adjacentVertex: adjacentVertex
};
}
return null;
},
_show_snap_indicator: function(latlng, snapInfo) {
editor._clear_snap_indicators();
var size = 0.001; // adjust this to control square size
@ -1880,6 +2092,136 @@ editor = {
});
editor._snap_indicator.addLayer(indicator);
if (snapInfo && snapInfo.edgeStart && snapInfo.edgeEnd) {
editor._show_edge_highlight(snapInfo);
}
},
_show_edge_highlight: function(snapInfo) {
if (!snapInfo.edgeStart || !snapInfo.edgeEnd) return;
// handle right-angle visualization
if (snapInfo.isRightAngle) {
editor._show_right_angle_highlight(snapInfo);
return;
}
var startPoint = snapInfo.edgeStart;
var endPoint = snapInfo.edgeEnd;
var extendedStart, extendedEnd;
if (snapInfo.isInfiniteExtension && snapInfo.t !== undefined) {
// Extend the line significantly beyond the original edge
var startMap = editor.map.latLngToContainerPoint(startPoint);
var endMap = editor.map.latLngToContainerPoint(endPoint);
var dx = endMap.x - startMap.x;
var dy = endMap.y - startMap.y;
var length = Math.sqrt(dx * dx + dy * dy);
if (length > 0) {
dx /= length;
dy /= length;
var extensionDistance = 1000;
var extStartMap = {
x: startMap.x - dx * extensionDistance,
y: startMap.y - dy * extensionDistance
};
var extEndMap = {
x: endMap.x + dx * extensionDistance,
y: endMap.y + dy * extensionDistance
};
extendedStart = editor.map.containerPointToLatLng(extStartMap);
extendedEnd = editor.map.containerPointToLatLng(extEndMap);
} else {
extendedStart = startPoint;
extendedEnd = endPoint;
}
} else {
extendedStart = startPoint;
extendedEnd = endPoint;
}
// create edge highlight line
var edgeHighlight = L.polyline([extendedStart, extendedEnd], {
color: snapInfo.isInfiniteExtension ? '#ffaa00' : '#66dd66', // Orange for infinite, green for original edge
weight: snapInfo.isInfiniteExtension ? 2 : 3,
opacity: snapInfo.isInfiniteExtension ? 0.4 : 0.6,
dashArray: snapInfo.isInfiniteExtension ? '8, 4' : null, // Dashed for infinite extension
className: 'edge-highlight'
});
editor._snap_indicator.addLayer(edgeHighlight);
// if it's an infinite extension, also show the original edge segment more prominently
if (snapInfo.isInfiniteExtension) {
var originalEdge = L.polyline([startPoint, endPoint], {
color: '#66dd66',
weight: 3,
opacity: 0.8,
className: 'original-edge-highlight'
});
editor._snap_indicator.addLayer(originalEdge);
}
},
_show_right_angle_highlight: function(snapInfo) {
var vertex = snapInfo.rightAngleVertex;
var adjacentVertex = snapInfo.adjacentVertex;
var rightAnglePoint = snapInfo.latlng;
var referenceEdge = L.polyline([vertex, adjacentVertex], {
color: '#4488ff',
weight: 2,
opacity: 0.6,
className: 'right-angle-reference'
});
editor._snap_indicator.addLayer(referenceEdge);
var rightAngleLine = L.polyline([vertex, rightAnglePoint], {
color: '#ff4488',
weight: 3,
opacity: 0.8,
dashArray: '6, 3',
className: 'right-angle-line'
});
editor._snap_indicator.addLayer(rightAngleLine);
var vertexMap = editor.map.latLngToContainerPoint(vertex);
var adjacentMap = editor.map.latLngToContainerPoint(adjacentVertex);
var rightAngleMap = editor.map.latLngToContainerPoint(rightAnglePoint);
var size = 15; // Square size in pixels
var dx1 = adjacentMap.x - vertexMap.x;
var dy1 = adjacentMap.y - vertexMap.y;
var len1 = Math.sqrt(dx1 * dx1 + dy1 * dy1);
var dx2 = rightAngleMap.x - vertexMap.x;
var dy2 = rightAngleMap.y - vertexMap.y;
var len2 = Math.sqrt(dx2 * dx2 + dy2 * dy2);
if (len1 > 0 && len2 > 0) {
dx1 = (dx1 / len1) * size;
dy1 = (dy1 / len1) * size;
dx2 = (dx2 / len2) * size;
dy2 = (dy2 / len2) * size;
var corner1 = editor.map.containerPointToLatLng({x: vertexMap.x + dx1, y: vertexMap.y + dy1});
var corner2 = editor.map.containerPointToLatLng({x: vertexMap.x + dx1 + dx2, y: vertexMap.y + dy1 + dy2});
var corner3 = editor.map.containerPointToLatLng({x: vertexMap.x + dx2, y: vertexMap.y + dy2});
var rightAngleSquare = L.polyline([vertex, corner1, corner2, corner3, vertex], {
color: '#ff4488',
weight: 2,
opacity: 0.7,
fill: false,
className: 'right-angle-square'
});
editor._snap_indicator.addLayer(rightAngleSquare);
}
},
_clear_snap_indicators: function() {

View file

@ -125,7 +125,7 @@ def fetch_updates(request, response: HttpResponse):
}
if cross_origin is None:
result.update({
'user_data': request.user_data,
'user_data': dict(request.user_data),
})
if cross_origin is not None:

View file

@ -32,7 +32,6 @@ class LocalCacheProxy:
# not in our cache
result = cache.get(key, default=NoneFromCache)
if result is not NoneFromCache:
print("result", result, result is NoneFromCache)
if self._items.get(None) is None:
self._items.set(OrderedDict())
self._items.get()[key] = result

View file

@ -29,10 +29,16 @@ elif [[ $# == 1 ]] && [[ $1 == "run" ]]; then
python manage.py processupdates
python manage.py runserver
popd
elif [[ $# == 1 ]] && [[ $1 == "run_without_output" ]]; then
echo "Processing updates and running server without output"
pushd src 2>&1 > /dev/null
python manage.py processupdates 2>&1 | (grep -e "^ERROR" -e "^WARNING" -e "^HTTP" || true)
python manage.py runserver 2>&1 | (grep -e "^ERROR" -e "^WARNING" -e "^HTTP" || true)
popd 2>&1 > /dev/null
elif [[ $# > 0 ]] && [[ $1 == "manage" ]]; then
pushd src
python manage.py "${@:2}"
popd
else
echo "Usage: $0 [stop|db|run|manage]"
echo "Usage: $0 [stop|db|run|run_without_output|manage]"
fi