@@ -91,8 +80,7 @@ export default class Snapping extends HTMLElement {
},
[
'snapping.config',
- 'snapping.active',
- 'snapping.refreshable'
+ 'snapping.active'
]
);
}
diff --git a/assets/src/components/edition/ReverseGeom.js b/assets/src/components/edition/ReverseGeom.js
index 69213a436f..07fa90c932 100644
--- a/assets/src/components/edition/ReverseGeom.js
+++ b/assets/src/components/edition/ReverseGeom.js
@@ -6,7 +6,7 @@
* @license MPL-2.0
*/
-import { mainLizmap } from '../../modules/Globals.js';
+import { mainLizmap, mainEventDispatcher } from '../../modules/Globals.js';
/**
* Web component used to reverse vertices order for a modified feature
@@ -25,25 +25,32 @@ export default class reverseGeom extends HTMLElement {
}
_reverse(){
- if (!mainLizmap.edition.modifyFeatureControl
- || !mainLizmap.edition.modifyFeatureControl.active
- || mainLizmap.edition.modifyFeatureControl.vertices.length == 0){
- return;
- }
-
- const lonLat = [];
+ // OL10 path: operate on the feature currently in the digitizing draw layer.
+ // (The legacy OL2 modifyFeatureControl is no longer activated.)
+ const features = mainLizmap.digitizing?.featureDrawn;
+ if (!features || features.length === 0) return;
- for (const vertice of mainLizmap.edition.modifyFeatureControl.vertices) {
- lonLat.push([vertice.geometry.x, vertice.geometry.y]);
- }
-
- lonLat.reverse();
+ const feature = features[0];
+ const geom = feature.getGeometry();
+ const type = geom.getType();
- for (let index = 0; index < lonLat.length; index++) {
- mainLizmap.edition.modifyFeatureControl.vertices[index].move(new OpenLayers.LonLat(lonLat[index][0], lonLat[index][1]));
+ if (type === 'LineString' || type === 'MultiPoint') {
+ geom.setCoordinates(geom.getCoordinates().slice().reverse());
+ } else if (type === 'Polygon') {
+ geom.setCoordinates(geom.getCoordinates().map(ring => ring.slice().reverse()));
+ } else if (type === 'MultiLineString') {
+ geom.setCoordinates(geom.getCoordinates().map(line => line.slice().reverse()));
+ } else if (type === 'MultiPolygon') {
+ geom.setCoordinates(
+ geom.getCoordinates().map(poly => poly.map(ring => ring.slice().reverse()))
+ );
+ } else {
+ // Point geometries have nothing to reverse.
+ return;
}
- mainLizmap.edition.modifyFeatureControl.layer.events.triggerEvent("featuremodified", { feature: mainLizmap.edition.modifyFeatureControl.feature });
+ // Sync the hidden geom form field via the digitizing change event.
+ mainEventDispatcher.dispatch('digitizing.geometryChanged');
// Tell user reverse is done
lizMap.addMessage(lizDict['edition.revertgeom.success'], 'success', true).attr('id', 'lizmap-edition-message');
diff --git a/assets/src/legacy/edition.js b/assets/src/legacy/edition.js
index 7db5e84df9..dd9ed711aa 100644
--- a/assets/src/legacy/edition.js
+++ b/assets/src/legacy/edition.js
@@ -281,133 +281,14 @@ var lizEdition = function() {
if ( $EditionMessage.length != 0 ) {
$EditionMessage.remove();
}
+ // Also drop the OL10-edition help toast (e.g. "Draw on the map") so it
+ // does not coexist with this status/error message in #message — tests
+ // assert #message > div is a single element with a specific class.
+ $('#lizmap-editing-message').remove();
lizMap.addMessage(aMessage, aType, aClose).attr('id','lizmap-edition-message');
editionMessageTimeoutId = window.setTimeout(cleanEditionMessage, 5000);
}
- /**
- *
- * @param evt
- */
- function afterReshapeSpliting(evt) {
- var splitFeatures = evt.features;
- var geometryType = editionLayer.geometryType;
- var removableFeat = null;
- if ( geometryType == 'line' ) {
- if ( splitFeatures[0].geometry.getLength() < splitFeatures[1].geometry.getLength() )
- removableFeat = splitFeatures[0];
- else
- removableFeat = splitFeatures[1];
- }
- else if ( geometryType == 'polygon' ) {
- if ( splitFeatures[0].geometry.getArea() < splitFeatures[1].geometry.getArea() )
- removableFeat = splitFeatures[0];
- else
- removableFeat = splitFeatures[1];
- }
- if (removableFeat) {
- editionLayer.removeEditedFeature(removableFeat);
- }
- // Update form geometry field from added geometry
- var feat = editionLayer.getFeature();
- if (feat) {
- updateGeometryColumnFromFeature(feat);
- }
- $('#edition-geomtool-nodetool').click();
- return false;
- }
-
-
- /**
- *
- * @param evt
- */
- function beforeFeatureSpliting(evt) {
-
- var form = $('#edition-form-container form');
- if (checkFormBeforeSubmit(form) !== 'ok') {
- // content of the form is not good, we couldn't create a new feature
- addEditionMessage(lizDict['edition.splitfeat.form.error'],'error',true);
- return false;
- }
- if (!form.attr('data-new-feature-action')) {
- addEditionMessage(lizDict['edition.splitfeat.tech.error'],'error',true);
- return false;
- }
- return true;
- }
-
-
- /**
- *
- * @param evt
- */
- function afterFeatureSpliting(evt) {
-
- // determine the two new geometry
- var splitFeatures = evt.features;
- var geometryType = editionLayer.geometryType;
- var newFeature = null;
- if ( geometryType == 'line' ) {
- if ( splitFeatures[0].geometry.getLength() < splitFeatures[1].geometry.getLength() )
- newFeature = splitFeatures[0];
- else
- newFeature = splitFeatures[1];
- }
- else if ( geometryType == 'polygon' ) {
- if ( splitFeatures[0].geometry.getArea() < splitFeatures[1].geometry.getArea() )
- newFeature = splitFeatures[0];
- else
- newFeature = splitFeatures[1];
- }
-
- // store one of the new geometry (the most little one), as a new feature
- if (newFeature) {
- var form = $('#edition-form-container form');
- // Get edition datasource geometry column name
- var gColumn = form.find('input[name="liz_geometryColumn"]').val();
- var geom = '';
- // create a new form that will be used to store the new feature
- var data = new FormData(form.get(0));
- if ('set' in data) {
- data.set('liz_featureId', '');
- data.set('__JFORMS_TOKEN__', '');
- if (gColumn) {
- geom = calculateGeometryColumnFromFeature(newFeature);
- data.set(gColumn, geom);
- }
- }
- else {
- // IE/Edge<12 workaround - no support of FormData.set()
- var featureIdField = form.find('input[name="liz_featureId"]');
- var geomField = form.find('input[name="'+gColumn+'"]');
- var tokenField = form.find('input[name="__JFORMS_TOKEN__"]');
- var oldFeatureId = featureIdField.val();
- var oldGeom = geomField.val();
- var oldToken = tokenField.val();
- featureIdField.val('');
- tokenField.val('');
- if (gColumn) {
- geom = calculateGeometryColumnFromFeature(newFeature);
- geomField.val(geom);
- }
- data = new FormData(form.get(0));
- featureIdField.val(oldFeatureId);
- geomField.val(oldGeom);
- tokenField.val(oldToken)
- }
- // move new feature into the temporary layer
- editionLayer.moveEditedFeatureToSplitLayer(newFeature, data);
- }
-
- // Update geometry column with the other geometry
- var feat = editionLayer.getFeature();
- if (feat) {
- updateGeometryColumnFromFeature( feat );
- }
- $('#edition-geomtool-nodetool').click();
- return false;
- }
/**
@@ -487,10 +368,14 @@ var lizEdition = function() {
var mapSrid = mapProjCode.replace('EPSG:','');
$('#edition-point-coord-crs-map').html(lizDict['edition.point.coord.crs.map']+' - EPSG:'+mapSrid).val(mapSrid).show();
- if ( editionLayer.geometryType == 'point' )
- $('#edition-point-coord-add').hide();
- else
- $('#edition-point-coord-add').show();
+ if ( editionLayer.geometryType == 'point' ) {
+ // Hide Add/Finalize buttons and their parent form-group to avoid whitespace
+ $('#edition-point-coord-add').closest('.form-group').hide();
+ $('#edition-segment-length').parents('.form-group').addClass('hidden');
+ $('#edition-segment-angle').parents('.form-group').addClass('hidden');
+ } else {
+ $('#edition-point-coord-add').closest('.form-group').show();
+ }
$('#handle-point-coord').show();
@@ -510,25 +395,20 @@ var lizEdition = function() {
var x = parseFloat($('#edition-point-coord-x').val());
var y = parseFloat($('#edition-point-coord-y').val());
if ( !isNaN(x) && !isNaN(y) ) {
- var vertex = new OpenLayers.Geometry.Point(x,y);
- // Get SRID and transform geometry
+ // Get SRID and transform to map projection
var srid = $('#edition-point-coord-crs').val();
- vertex.transform( 'EPSG:'+srid, editionLayer['ol'].projection );
- var geometryType = editionLayer.geometryType;
- if ( !editCtrls[geometryType].handler.point ) {
- var px = editCtrls[geometryType].handler.layer.getViewPortPxFromLonLat({lon:vertex.x,lat:vertex.y});
- editCtrls[geometryType].handler.createFeature(px);
- editCtrls[geometryType].handler.point.geometry.x = vertex.x;
- editCtrls[geometryType].handler.point.geometry.y = vertex.y;
- editCtrls[geometryType].handler.point.geometry.clearBounds();
- } else {
- editCtrls[geometryType].handler.point.geometry.x = vertex.x;
- editCtrls[geometryType].handler.point.geometry.y = vertex.y;
- editCtrls[geometryType].handler.point.geometry.clearBounds();
+ var mapProjection = lizMap.mainLizmap.map.getView().getProjection().getCode();
+ var coord = lizMap.ol.proj.transform([x, y], 'EPSG:' + srid, mapProjection);
- displaySegmentsLengthAndAngle(editCtrls[geometryType].handler.layer.features[0].geometry);
+ var geometryType = editionLayer.geometryType;
+ if (geometryType === 'point') {
+ // For point: create/move point feature at coord
+ var feature = new lizMap.ol.Feature(new lizMap.ol.geom.Point(coord));
+ lizMap.mainLizmap.digitizing._drawSource.clear();
+ lizMap.mainLizmap.digitizing._drawSource.addFeature(feature);
+ lizMap.mainEventDispatcher.dispatch('digitizing.geometryChanged');
}
- editCtrls[geometryType].handler.drawFeature();
+ // For line/polygon, the coordinate is used with the Add button (handled separately)
}
}
@@ -654,8 +534,7 @@ var lizEdition = function() {
*/
function finishEdition() {
- // Put old OL2 map at bottom
- lizMap.mainLizmap.newOlMap = true;
+ // OL6 digitizing handles the drawing now
// Lift the constraint on edition
lizMap.editionPending = false;
@@ -811,14 +690,7 @@ var lizEdition = function() {
deactivate: deactivateDrawFeature
}
}),
- modify: new OpenLayers.Control.ModifyFeature(editLayer),
- reshape: new OpenLayers.Control.Split({layer:editLayer,eventListeners: {aftersplit:afterReshapeSpliting}}),
- featsplit: new OpenLayers.Control.Split({
- layer:editLayer,
- eventListeners: {
- beforesplit:beforeFeatureSpliting,
- aftersplit:afterFeatureSpliting
- }})
+ modify: new OpenLayers.Control.ModifyFeature(editLayer)
};
for ( var ctrl in editCtrls ) {
if ( ctrl != 'panel' )
@@ -836,86 +708,28 @@ var lizEdition = function() {
// edit layer events
+ // OL2 edit layer events are no longer primary
+ // OL6 Digitizing module handles drawing, modification, and geometry updates
+ // These legacy events are kept as no-ops for backward compatibility
editLayer.events.on({
-
featureadded: function() {
- // Deactivate draw control
- if( !editCtrls )
- return false;
- var geometryType = editionLayer.geometryType;
- var drawWasActivated = editCtrls[geometryType].active;
- if (drawWasActivated)
- editCtrls[geometryType].deactivate();
-
- // Get feature
- var feat = editLayer.features[0];
-
- // Update form geometry field from added geometry
- updateGeometryColumnFromFeature( feat );
-
- // Activate modify control
- if (drawWasActivated || editionLayer['config'].capabilities.modifyGeometry == "True"){
- // activate edition
- editCtrls.panel.activate();
- // then modify
- editCtrls.modify.activate();
- $('#edition-geomtool-nodetool').click();
- editCtrls.modify.selectFeature( feat );
- if (geometryType === 'line'){
- $('#edition-geomtool-container button i').addClass('line');
- }
- if (geometryType !== 'point'){
- $('#edition-geomtool-container').show();
- }
- }
-
- // Display form tab and hide tool to handle coords for point geometry
- // TODO : allow use of coords tool when editing point
- if (geometryType === 'point') {
- $('.edition-tabs a[href="#tabform"]').tab('show');
- $('#handle-point-coord').hide();
- }
-
- // Inform user he can now modify
- addEditionMessage(lizDict['edition.select.modify.activate'],'info',true);
- },
-
- featuremodified: function(evt) {
- if ( evt.feature.geometry == null )
- return;
- // Update form geometry field from added geometry
- updateGeometryColumnFromFeature( evt.feature );
-
+ // Handled by OL6 Digitizing module via digitizing.geometryChanged event
},
-
- featureselected: function(evt) {
- if ( evt.feature.geometry == null )
- return;
-
- },
-
- featureunselected: function(evt) {
- if ( evt.feature.geometry == null )
- return;
-
- updateGeometryColumnFromFeature(evt.feature);
+ featuremodified: function() {
+ // Handled by OL6 Digitizing module via digitizing.geometryChanged event
},
+ featureselected: function() {},
+ featureunselected: function() {},
+ sketchmodified: function() {},
+ vertexmodified: function() {}
+ });
- sketchmodified: function(evt) {
- // Force drawing point on geolocation position
- if ($('#edition-point-coord-geolocation').is(':checked')){
- var [lon, lat] = lizMap.mainLizmap.geolocation.getPositionInCRS(editionLayer['ol'].projection);
- evt.vertex.x = lon;
- evt.vertex.y = lat;
- }else{
- var vertex = evt.vertex.clone();
- displayCoordinates(vertex);
+ // Handle split complete from OL6 Edition module
+ lizMap.events.on({
+ lizmapeditionsplitcomplete: function(evt) {
+ if (evt.newFeatureFormData) {
+ editionLayer.currentFeature.newfeatures.push([null, evt.newFeatureFormData]);
}
-
- displaySegmentsLengthAndAngle(evt.feature.geometry);
- },
-
- vertexmodified: function(evt) {
}
});
@@ -952,9 +766,16 @@ var lizEdition = function() {
return false;
});
$('#edition-point-coord-crs').change(function(){
- if (editCtrls[editionLayer.geometryType].handler.point !== null) {
- var vertex = editCtrls[editionLayer.geometryType].handler.point.geometry.clone();
- displayCoordinates(vertex);
+ // Update displayed coordinates based on current digitizing feature
+ var features = lizMap.mainLizmap.digitizing.featureDrawn;
+ if (features && features.length > 0) {
+ var geom = features[0].getGeometry();
+ if (geom.getType() === 'Point') {
+ displayCoordinates(geom.getCoordinates());
+ } else {
+ var lastCoord = geom.getLastCoordinate();
+ if (lastCoord) displayCoordinates(lastCoord);
+ }
}
});
$('#edition-point-coord-x').keyup(keyUpPointCoord);
@@ -965,14 +786,10 @@ var lizEdition = function() {
$('#edition-point-coord-y').attr('disabled','disabled');
if (lizMap.mainLizmap.geolocation.isTracking){
- var geometryType = editionLayer.geometryType;
- var [lon, lat] = lizMap.mainLizmap.geolocation.getPositionInCRS(editionLayer['ol'].projection);
- if (lon && lat){
- var px = editCtrls[geometryType].handler.layer.getViewPortPxFromLonLat({ lon: lon, lat: lat });
- editCtrls[geometryType].handler.modifyFeature(px);
-
- // Set X and Y input with geolocation position value as it is more precise than position given by edit controls
- displayCoordinates(new OpenLayers.Geometry.Point(lon, lat));
+ var mapProjection = lizMap.mainLizmap.map.getView().getProjection().getCode();
+ var position = lizMap.mainLizmap.geolocation.getPositionInCRS(mapProjection);
+ if (position && position[0] && position[1]){
+ displayCoordinates([position[0], position[1]]);
}
}
} else {
@@ -982,114 +799,60 @@ var lizEdition = function() {
lizMap.mainLizmap.geolocation.isLinkedToEdition = $(this).is(':checked');
});
$('#edition-point-coord-add').click(function(){
- var geometryType = editionLayer.geometryType;
- if (geometryType != 'point' && editCtrls[geometryType].handler.point) {
- var node = editCtrls[geometryType].handler.point.geometry;
- editCtrls[geometryType].handler.insertXY(node.x, node.y);
+ var x = parseFloat($('#edition-point-coord-x').val());
+ var y = parseFloat($('#edition-point-coord-y').val());
+ if (isNaN(x) || isNaN(y)) return;
+
+ var srid = $('#edition-point-coord-crs').val();
+ var mapProjection = lizMap.mainLizmap.map.getView().getProjection().getCode();
+ var coord = lizMap.ol.proj.transform([x, y], 'EPSG:' + srid, mapProjection);
+
+ // Append coordinate to line/polygon via draw interaction
+ var drawInteraction = lizMap.mainLizmap.digitizing._drawInteraction;
+ if (drawInteraction && typeof drawInteraction.appendCoordinates === 'function') {
+ drawInteraction.appendCoordinates([coord]);
}
});
$('#edition-point-coord-submit').click(function(){
var geometryType = editionLayer.geometryType;
- // Assert we have a geometry
- if (editCtrls[geometryType].handler.getGeometry()){
- if (geometryType === 'point') {
- // Take average point if mode is enabled
- if (lizMap.mainLizmap.geolocationSurvey.averageRecordMode && lizMap.mainLizmap.geolocationSurvey.positionAverageInMapCRS !== undefined){
- editCtrls[geometryType].handler.point.geometry.x = lizMap.mainLizmap.geolocationSurvey.positionAverageInMapCRS[0];
- editCtrls[geometryType].handler.point.geometry.y = lizMap.mainLizmap.geolocationSurvey.positionAverageInMapCRS[1];
- editCtrls[geometryType].handler.drawFeature();
- }
- editCtrls[geometryType].handler.finalize();
- } else {
- editCtrls[geometryType].handler.finishGeometry();
+ if (geometryType === 'point') {
+ // Take average point if mode is enabled
+ if (lizMap.mainLizmap.geolocationSurvey.averageRecordMode && lizMap.mainLizmap.geolocationSurvey.positionAverageInMapCRS !== undefined){
+ var avgCoord = lizMap.mainLizmap.geolocationSurvey.positionAverageInMapCRS;
+ var feature = new lizMap.ol.Feature(new lizMap.ol.geom.Point([avgCoord[0], avgCoord[1]]));
+ lizMap.mainLizmap.digitizing._drawSource.clear();
+ lizMap.mainLizmap.digitizing._drawSource.addFeature(feature);
+ }
+ // For point, the feature is already placed; dispatch geometry change
+ lizMap.mainEventDispatcher.dispatch('digitizing.geometryChanged');
+ // Switch to edit mode
+ lizMap.mainLizmap.digitizing.isEdited = true;
+ } else {
+ // For line/polygon, finish the draw interaction
+ var drawInteraction = lizMap.mainLizmap.digitizing._drawInteraction;
+ if (drawInteraction) {
+ drawInteraction.finishDrawing();
}
}
});
- $('#edition-geomtool-restart-drawing').click(function(){
- if ( !confirm( lizDict['edition.confirm.restart-drawing'] ) ) {
- return false;
- }
-
- var ctrl = editCtrls[editionLayer.geometryType];
- // Check drawing is active
- if (ctrl.active)
- return false;
-
- // Disable every modifying geometry controls
- editCtrls.featsplit.deactivate();
- editCtrls.reshape.deactivate();
- editCtrls.modify.mode = OpenLayers.Control.ModifyFeature.RESHAPE;
- editCtrls.modify.createVertices = true;
- editCtrls.modify.deactivate();
- editCtrls.panel.deactivate();
- // Clear edition layers
- editionLayer.clearLayers();
- // activate drawing control
- ctrl.activate();
- });
-
- $('#edition-geomtool-nodetool').click(function(){
- editCtrls.reshape.deactivate();
- editCtrls.featsplit.deactivate();
- editCtrls.modify.mode = OpenLayers.Control.ModifyFeature.RESHAPE;
- editCtrls.modify.createVertices = true;
- editCtrls.modify.activate();
- var feat = editionLayer.getFeature();
- if (feat.geometry) {
- // we unselect then select, to trigger corresponding events
- if ( editCtrls.modify.feature )
- editCtrls.modify.unselectFeature( feat );
- editCtrls.modify.selectFeature( feat );
- }
+$('#edition-geomtool-nodetool').click(function(){
+ // Node tool = edit mode in OL6 Digitizing
+ lizMap.mainLizmap.digitizing.isEdited = true;
});
$('#edition-geomtool-drag').click(function(){
- editCtrls.reshape.deactivate();
- editCtrls.featsplit.deactivate();
- editCtrls.modify.mode = OpenLayers.Control.ModifyFeature.DRAG;
- editCtrls.modify.createVertices = false;
- editCtrls.modify.activate();
- var feat = editionLayer.getFeature();
- if (feat) {
- // we unselect then select, to trigger corresponding events
- if ( editCtrls.modify.feature )
- editCtrls.modify.unselectFeature( feat );
- editCtrls.modify.selectFeature( feat );
- }
+ // Drag = translate mode — edit mode handles translate via the Translate interaction
+ lizMap.mainLizmap.digitizing.isEdited = true;
});
$('#edition-geomtool-rotate').click(function(){
- editCtrls.reshape.deactivate();
- editCtrls.featsplit.deactivate();
- editCtrls.modify.mode = OpenLayers.Control.ModifyFeature.ROTATE;
- editCtrls.modify.createVertices = false;
- editCtrls.modify.activate();
- var feat = editionLayer.getFeature();
- if (feat) {
- // we unselect then select, to trigger corresponding events
- if ( editCtrls.modify.feature )
- editCtrls.modify.unselectFeature( feat );
- editCtrls.modify.selectFeature( feat );
- }
- });
- $('#edition-geomtool-reshape').click(function(){
- var feat = editionLayer.getFeature();
- if (feat && editCtrls.modify.feature) {
- editCtrls.modify.unselectFeature(feat);
- }
- editCtrls.modify.deactivate();
- editCtrls.featsplit.deactivate();
- editCtrls.reshape.activate();
+ // Rotate mode in OL6 Digitizing
+ lizMap.mainLizmap.digitizing.isRotate = true;
});
$('#edition-geomtool-split').click(function(){
- var feat = editionLayer.getFeature();
- if (feat && editCtrls.modify.feature) {
- editCtrls.modify.unselectFeature(feat);
- }
- editCtrls.modify.deactivate();
- editCtrls.reshape.deactivate();
- editCtrls.featsplit.activate();
+ // Feature split uses the split tool in OL6 Digitizing
+ lizMap.mainLizmap.digitizing.isSplitting = true;
});
$('#edition-geomtool-container button, lizmap-reverse-geom').tooltip( {
@@ -1116,19 +879,18 @@ var lizEdition = function() {
'geolocation.isTracking'
);
- // Make modifyFeature follow geolocation when active
+ // Geolocation position updates are now handled by OL6 Edition.js
+ // Just update coordinate display when geolocation is linked
lizMap.mainEventDispatcher.addListener(
() => {
if (editionLayer && ('config' in editionLayer) ) {
$('#edition-point-coord-geolocation').removeAttr('disabled');
- var geometryType = editionLayer.geometryType;
- if ($('#edition-point-coord-geolocation').is(':checked') && editCtrls[geometryType].active ) {
- // Move point
- var [lon, lat] = lizMap.mainLizmap.geolocation.getPositionInCRS(editionLayer['ol'].projection);
- var px = editCtrls[geometryType].handler.layer.getViewPortPxFromLonLat({ lon: lon, lat: lat});
- editCtrls[geometryType].handler.modifyFeature(px);
-
- displayCoordinates(new OpenLayers.Geometry.Point(lon, lat));
+ if ($('#edition-point-coord-geolocation').is(':checked')) {
+ var mapProjection = lizMap.mainLizmap.map.getView().getProjection().getCode();
+ var position = lizMap.mainLizmap.geolocation.getPositionInCRS(mapProjection);
+ if (position && position[0] && position[1]) {
+ displayCoordinates([position[0], position[1]]);
+ }
}
}
},
@@ -1147,19 +909,18 @@ var lizEdition = function() {
*
* @param vertex
*/
- function displayCoordinates(vertex){
- // Get SRID and transform geometry
+ function displayCoordinates(coordInMapProj){
+ // Get SRID and transform coordinate
var srid = $('#edition-point-coord-crs').val();
- var displayProj = new OpenLayers.Projection('EPSG:' + srid);
- vertex.transform(editionLayer['ol'].projection, displayProj);
+ var mapProjection = lizMap.mainLizmap.map.getView().getProjection().getCode();
+ var coord = lizMap.ol.proj.transform(coordInMapProj, mapProjection, 'EPSG:' + srid);
- if (displayProj.getUnits() === 'degrees') {
- $('#edition-point-coord-x').val(vertex.x.toFixed(6));
- $('#edition-point-coord-y').val(vertex.y.toFixed(6));
- } else {
- $('#edition-point-coord-x').val(vertex.x.toFixed(3));
- $('#edition-point-coord-y').val(vertex.y.toFixed(3));
- }
+ // Check if target is degrees (EPSG:4326 or similar)
+ var isDegrees = (srid === '4326');
+ var decimals = isDegrees ? 6 : 3;
+
+ $('#edition-point-coord-x').val(coord[0].toFixed(decimals));
+ $('#edition-point-coord-y').val(coord[1].toFixed(decimals));
}
/**
@@ -1486,8 +1247,7 @@ var lizEdition = function() {
// See "Check li (tabs) visibility" in displayEditionForm method
displayEditionForm( data );
- // Put old OL2 map on top and synchronize position with new OL map
- lizMap.mainLizmap.newOlMap = false;
+ // OL6 digitizing handles the drawing now
if( aCallback )
aCallback( editionLayer['id'], featureId );
@@ -1720,31 +1480,13 @@ var lizEdition = function() {
&& geometryType in editCtrls ){
$('#edition-geomtool-container button i').removeClass('line');
$('#edition-geomtool-container').hide();
- editCtrls.modify.mode = OpenLayers.Control.ModifyFeature.RESHAPE;
- editCtrls.modify.createVertices = true;
- editCtrls.modify.deactivate();
- editionLayer.clearLayers();
- var ctrl = editCtrls[geometryType];
- if ( !ctrl.active ) {
- ctrl.activate();
-
- if(lizMap.checkMobile()){
- addEditionMessage(lizDict['edition.draw.activate.mobile'], 'info', true);
- }else{
- addEditionMessage(lizDict['edition.draw.activate'],'info',true);
- }
- }
- // Need to get geometry from form and add feature to the openlayer layer
- if( feat ){
- editionLayer['ol'].addFeatures([feat]);
- editCtrls.modify.activate();
- $('#edition-geomtool-nodetool').click();
- editCtrls.modify.selectFeature( feat );
- if (geometryType == 'line')
- $('#edition-geomtool-container button i').addClass('line');
- if (geometryType != 'point')
- $('#edition-geomtool-container').show();
- }
+
+ // Fire event to let OL6 Edition.js handle the drawing
+ // (and show the appropriate edition.draw.activate message)
+ activateDrawFeature();
+
+ // Legacy geomtool buttons replaced by OL6 digitizing toolbar
+ $('#edition-geomtool-container').hide();
} else {
$('.edition-tabs button[data-bs-target="#tabdigitization"]').hide();
}
@@ -1754,22 +1496,15 @@ var lizEdition = function() {
// Activate modification control
if ( editionLayer['config'].capabilities.modifyGeometry == "True"
&& geometryType in editCtrls ){
- // Need to get geometry from form and add feature to the openlayer layer
- feat = updateFeatureFromGeometryColumn();
- if( feat ){
- editCtrls.modify.activate();
- $('#edition-geomtool-nodetool').click();
- editCtrls.modify.selectFeature( feat );
- if (geometryType == 'line')
- $('#edition-geomtool-container button i').addClass('line');
- if (geometryType != 'point')
- $('#edition-geomtool-container').show();
- }
+ // Fire event to let OL6 Edition.js handle loading and editing
+ // (and show the appropriate edition.select.modify.activate message)
+ activateDrawFeature();
+
+ // Legacy geomtool buttons replaced by OL6 digitizing toolbar
+ $('#edition-geomtool-container').hide();
}else{
$('.edition-tabs button[data-bs-target="#tabdigitization"]').hide();
}
-
- addEditionMessage(lizDict['edition.select.modify.activate'],'info',true);
}
}
@@ -2083,6 +1818,10 @@ var lizEdition = function() {
});
sendFormPromise.then(() => {
displayEditionForm( formResult );
+ }).catch(e => {
+ console.error('Error saving split feature:', e);
+ $('#edition-waiter').hide();
+ addEditionMessage(lizDict['edition.message.error.send.feature'], 'error', true);
});
return false;
@@ -2103,9 +1842,20 @@ var lizEdition = function() {
request.open("POST", url);
request.onload = function() {
if (request.status == 200) {
+ try {
+ var json = JSON.parse(request.responseText);
+ if (json && json.success === false) {
+ var msg = json.message || 'unknown server error';
+ console.warn('saveNewFeature returned success:false —', msg);
+ reject(new Error('saveNewFeature: ' + msg));
+ return;
+ }
+ } catch (e) {
+ // Response is not JSON; treat as success (legacy behaviour)
+ }
resolve(request.responseText);
} else {
- reject(new Error(`Nouveau message d'erreur`, { cause: request }));
+ reject(new Error(`saveNewFeature failed with status ${request.status}: ${request.responseText}`, { cause: request }));
}
};
request.addEventListener("error", reject);
diff --git a/assets/src/legacy/map.js b/assets/src/legacy/map.js
index ac9331fa9a..4f33496534 100644
--- a/assets/src/legacy/map.js
+++ b/assets/src/legacy/map.js
@@ -1635,6 +1635,21 @@ window.lizMap = function() {
"layerSelectionChanged": function( evt ) {
refreshGetFeatureInfo(evt);
},
+ "lizmapeditionfeaturecreated": function() {
+ // OL10 singleclick fires ~250ms after the draw click. If the
+ // edition form save completes inside that window the click
+ // guard against duplicated GFI requests is already lifted, so
+ // the next legitimate identify click is silently deduplicated.
+ // Clearing lastLonLatInfo here forces the next click to issue
+ // a fresh GetFeatureInfo request.
+ lastLonLatInfo = null;
+ },
+ "lizmapeditionfeaturemodified": function() {
+ lastLonLatInfo = null;
+ },
+ "lizmapeditionformclosed": function() {
+ lastLonLatInfo = null;
+ },
"lizmapeditionfeaturedeleted": function( evt ) {
if ( $('div.lizmapPopupContent input.lizmap-popup-layer-feature-id').length > 1 ) {
refreshGetFeatureInfo(evt);
diff --git a/assets/src/modules/Digitizing.js b/assets/src/modules/Digitizing.js
index 08d2dbafbe..b9ef9c2d68 100644
--- a/assets/src/modules/Digitizing.js
+++ b/assets/src/modules/Digitizing.js
@@ -4,7 +4,7 @@
* @copyright 2023 3Liz
* @license MPL-2.0
*/
-import { mainEventDispatcher } from '../modules/Globals.js';
+import { mainLizmap, mainEventDispatcher } from '../modules/Globals.js';
import { deepFreeze } from './config/Tools.js';
import { createEnum } from './utils/Enums.js';
import { Utils } from './Utils.js';
@@ -15,10 +15,11 @@ import GPX from 'ol/format/GPX.js';
import KML from 'ol/format/KML.js';
import WKT from 'ol/format/WKT.js';
-import { Draw, Modify, Select, Translate } from 'ol/interaction.js';
+import { Draw, Modify, Select, Translate, DoubleClickZoom } from 'ol/interaction.js';
+import { click } from 'ol/events/condition.js';
import { createBox } from 'ol/interaction/Draw.js';
-import { Circle, Fill, Stroke, RegularShape, Style, Text } from 'ol/style.js';
+import { Circle, Fill, Stroke, Style, Text } from 'ol/style.js';
import { Vector as VectorSource } from 'ol/source.js';
import { Vector as VectorLayer } from 'ol/layer.js';
@@ -36,14 +37,12 @@ import {
GeometryCollection
} from 'ol/geom.js';
-import { circular, fromCircle } from 'ol/geom/Polygon.js';
+import { fromCircle } from 'ol/geom/Polygon.js';
import { getArea, getLength } from 'ol/sphere.js';
import Overlay from 'ol/Overlay.js';
import { unByKey } from 'ol/Observable.js';
-import { transform } from 'ol/proj.js';
-
import shp from 'shpjs';
import * as flatgeobuf from 'flatgeobuf';
import { register } from "ol/proj/proj4.js";
@@ -132,6 +131,7 @@ export class Digitizing {
this._isErasing = false;
this._drawInteraction;
+ this._dblClickListener = null;
this._segmentMeasureTooltipElement;
this._totalMeasureTooltipElement;
@@ -146,7 +146,10 @@ export class Digitizing {
this._strokeWidth = 2;
this._selectInteraction = new Select({
+ condition: click,
+ hitTolerance: 5,
wrapX: false,
+ layers: (layer) => layer === this._drawLayer,
style: (feature) => {
let color = feature.get('color') || this._drawColor;
@@ -222,33 +225,55 @@ export class Digitizing {
if (event.selected.length) {
this.drawColor = event.selected[0].get('color');
} else {
+ // In edition context, prevent deselection — always keep the feature selected
+ if (this._context === 'edition' && this.featureDrawn && this.featureDrawn.length === 1) {
+ this._selectInteraction.getFeatures().push(this.featureDrawn[0]);
+ return;
+ }
// When a feature is deselected, set the color from the first selected feature if any
const selectedFeatures = this._selectInteraction.getFeatures().getArray();
if (selectedFeatures.length) {
this.drawColor = selectedFeatures[0].get('color');
}
}
+ mainEventDispatcher.dispatch('digitizing.editionBegins');
});
this._modifyInteraction = new Modify({
features: this._selectInteraction.getFeatures(),
});
+ this._modifyInteraction.on('modifyend', () => {
+ mainEventDispatcher.dispatch('digitizing.geometryChanged');
+ });
+
this._translateInteraction = new Translate({
features: this._selectInteraction.getFeatures(),
hitTolerance: 20
});
+ this._translateInteraction.on('translateend', () => {
+ mainEventDispatcher.dispatch('digitizing.geometryChanged');
+ });
+
this._transformRotateInteraction = new Transform({
rotate: true,
scale: false,
});
+ this._transformRotateInteraction.on('rotateend', () => {
+ mainEventDispatcher.dispatch('digitizing.geometryChanged');
+ });
+
this._transformScaleInteraction = new Transform({
rotate: false,
scale: true,
});
+ this._transformScaleInteraction.on('scaleend', () => {
+ mainEventDispatcher.dispatch('digitizing.geometryChanged');
+ });
+
this._drawStyleFunction = (feature) => {
let color = feature.get('color') || this._drawColor;
@@ -353,37 +378,6 @@ export class Digitizing {
this._map.addToolLayer(this._drawLayer);
- // Constraint layer
- this._constraintLayer = new VectorLayer({
- source: new VectorSource({ wrapX: false }),
- style: new Style({
- image: new RegularShape({
- fill: new Fill({
- color: 'black',
- }),
- stroke: new Stroke({
- color: 'black',
- }),
- points: 4,
- radius: 10,
- radius2: 0,
- angle: 0,
- }),
- stroke: new Stroke({
- color: 'black',
- lineDash: [10]
- }),
- })
- });
- this._constraintLayer.setProperties({
- name: 'LizmapDigitizingConstraintLayer'
- });
- this._map.addToolLayer(this._constraintLayer);
-
- // Constraints values
- this._distanceConstraint = 0;
- this._angleConstraint = 0;
-
// Load and display saved feature if any
this.loadFeatureDrawnToMap();
@@ -401,6 +395,7 @@ export class Digitizing {
minidockclosed: (e) => {
if (e.id == 'draw') {
this.toolSelected = this._tools[0]; // DigitizingTools.Deactivate
+ this.toggleVisibility(false);
}
}
});
@@ -595,10 +590,118 @@ export class Digitizing {
* @param {string} tool - The tool to select
* @fires digitizingToolSelected
*/
+ /**
+ * Disable DoubleClickZoom interaction on the map
+ * @private
+ */
+ _disableDoubleClickZoom() {
+ this._map.getInteractions().forEach(interaction => {
+ if (interaction instanceof DoubleClickZoom) {
+ interaction.setActive(false);
+ }
+ });
+ }
+
+ /**
+ * Enable DoubleClickZoom interaction on the map
+ * @private
+ */
+ _enableDoubleClickZoom() {
+ // Delay re-enabling past OL's 250ms dblclick detection window
+ // to prevent the finishing double-click from also triggering a zoom
+ setTimeout(() => {
+ this._map.getInteractions().forEach(interaction => {
+ if (interaction instanceof DoubleClickZoom) {
+ interaction.setActive(true);
+ }
+ });
+ }, 300);
+ }
+
+ /**
+ * Restore edit mode if in edition context with a drawn feature.
+ * Called when other tools (rotate, scale, split) deactivate.
+ * @private
+ */
+ _restoreEditionEditMode() {
+ if (this._context === 'edition' && this.featureDrawn) {
+ this.isEdited = true;
+ }
+ }
+
+ /**
+ * Deactivate all tools by directly manipulating internal state.
+ * Avoids setter re-entrancy issues. Call before activating a new tool.
+ * @private
+ */
+ _deactivateAllTools() {
+ // Deactivate edit mode
+ if (this._isEdited) {
+ this._isEdited = false;
+ this._selectInteraction.getFeatures().clear();
+ this._map.removeInteraction(this._selectInteraction);
+ this._map.removeInteraction(this._modifyInteraction);
+ this.saveFeatureDrawn();
+ mainEventDispatcher.dispatch('digitizing.editionEnds');
+ }
+
+ // Deactivate rotate
+ if (this._isRotate) {
+ this._isRotate = false;
+ this._transformRotateInteraction.getFeatures().clear();
+ this._map.removeInteraction(this._transformRotateInteraction);
+ mainEventDispatcher.dispatch({ type: 'digitizing.rotate', isRotate: false });
+ }
+
+ // Deactivate scale
+ if (this._isScaling) {
+ this._isScaling = false;
+ this._transformScaleInteraction.getFeatures().clear();
+ this._map.removeInteraction(this._transformScaleInteraction);
+ mainEventDispatcher.dispatch({ type: 'digitizing.scaling', isScaling: false });
+ }
+
+ // Deactivate split
+ if (this._isSplitting) {
+ this._isSplitting = false;
+ this._map.removeInteraction(this._splitInteraction);
+ if (this._splitSource) {
+ this._splitSource.clear();
+ this._splitSource = null;
+ }
+ mainEventDispatcher.dispatch({ type: 'digitizing.split', isSplitting: false });
+ }
+
+ // Deactivate erasing
+ if (this._isErasing) {
+ this._map.un('singleclick', this._erasingCallBack);
+ this._isErasing = false;
+ mainEventDispatcher.dispatch('digitizing.erasingEnds');
+ }
+
+ // Deactivate draw tool
+ this._map.removeInteraction(this._drawInteraction);
+ if (this._dblClickListener) {
+ this._map.getViewport().removeEventListener('dblclick', this._dblClickListener);
+ this._dblClickListener = null;
+ }
+ this._drawSource.un('addfeature', this._addFeatureColorListener);
+ this._drawSource.un('addfeature', this._addFeatureTextListener);
+ this._drawSource.un('addfeature', this._addFeatureSinglePartGeometryListener);
+ this._drawSource.un('addfeature', this._addFeatureSaveDispatchListener);
+ this._toolSelected = this._tools[0]; // deactivate
+
+ this._enableDoubleClickZoom();
+ }
+
set toolSelected(tool) {
if (this._tools.includes(tool)) {
// Disable all tools
this._map.removeInteraction(this._drawInteraction);
+ if (this._dblClickListener) {
+ this._map.getViewport().removeEventListener('dblclick', this._dblClickListener);
+ this._dblClickListener = null;
+ }
this._drawSource.un('addfeature', this._addFeatureColorListener);
this._drawSource.un('addfeature', this._addFeatureTextListener);
this._drawSource.un('addfeature', this._addFeatureSinglePartGeometryListener);
@@ -607,6 +710,7 @@ export class Digitizing {
// If tool === 'deactivate' or current selected tool is selected again => deactivate
if (tool === this._toolSelected || tool === this._tools[0]) {
this._toolSelected = this._tools[0];
+ this._enableDoubleClickZoom();
} else {
const drawOptions = {
source: this._drawLayer.getSource(),
@@ -619,15 +723,9 @@ export class Digitizing {
break;
case this._tools[2]:
drawOptions.type = 'LineString';
- drawOptions.geometryFunction = (coords, geom) => {
- return this._contraintsHandler(coords, geom, drawOptions.type);
- }
break;
case this._tools[3]:
drawOptions.type = 'Polygon';
- drawOptions.geometryFunction = (coords, geom) => {
- return this._contraintsHandler(coords, geom, drawOptions.type);
- }
break;
case this._tools[4]:
drawOptions.type = 'Circle';
@@ -676,14 +774,13 @@ export class Digitizing {
this._drawInteraction.on('drawend', event => {
const geom = event.feature.getGeometry();
- // Close linear ring if needed
- if (geom instanceof Polygon) {
- const coordsLinearRing = geom.getCoordinates()[0];
- if (coordsLinearRing[0] !== coordsLinearRing[coordsLinearRing.length - 1]) {
- coordsLinearRing.push(coordsLinearRing[0]);
- geom.setCoordinates([coordsLinearRing]);
- }
- }
+ // OL10's Draw already returns properly closed polygon rings —
+ // do NOT manually push another closing coordinate. (Earlier
+ // code compared the first/last coordinate arrays with !==,
+ // which is always true for distinct array instances regardless
+ // of value, so it kept double-closing the ring. The duplicate
+ // closing point shows up in WKT output and breaks tests that
+ // expect a single closure.)
// Attach total overlay to its geom to update
// content when the geom is modified
@@ -693,8 +790,6 @@ export class Digitizing {
this._setTooltipContentByGeom(geom);
});
- this._constraintLayer.setVisible(false);
-
// Remove segment measure and change total measure tooltip style
this._segmentMeasureTooltipElement.remove();
this._totalMeasureTooltipElement.className = 'ol-tooltip ol-tooltip-static';
@@ -713,9 +808,28 @@ export class Digitizing {
Array.from(this._measureTooltips).pop()[1],
);
}
+
+ // Dispatch after microtask so the feature is in the source
+ Promise.resolve().then(() => {
+ mainEventDispatcher.dispatch('digitizing.geometryChanged');
+ });
});
this._map.addInteraction(this._drawInteraction);
+ this._disableDoubleClickZoom();
+
+ // With constraints active, OL's atFinish_() compares the actual click pixel
+ // against the constrained coordinate (which can be far from the click). This
+ // causes double-click to add a vertex instead of finishing. We intercept the
+ // DOM dblclick to remove the extra vertex OL added on the second pointer-up
+ // and call finishDrawing() ourselves. Safe when constraints are off too:
+ // finishDrawing() was already called by OL and is a no-op by then.
+ this._dblClickListener = () => {
+ this._drawInteraction.removeLastPoint();
+ this._drawInteraction.finishDrawing();
+ };
+ this._map.getViewport().addEventListener('dblclick', this._dblClickListener);
+
this._drawSource.on('addfeature', this._addFeatureColorListener);
this._drawSource.on('addfeature', this._addFeatureTextListener);
this._drawSource.on('addfeature', this._addFeatureSinglePartGeometryListener);
@@ -744,6 +858,11 @@ export class Digitizing {
type: 'digitizing.toolSelected',
tool: this._toolSelected,
});
+
+ // Ensure snap interaction is ordered after the new draw interaction
+ if (mainLizmap.snapping) {
+ mainLizmap.snapping.reorderSnapInteraction();
+ }
}
}
@@ -836,27 +955,19 @@ export class Digitizing {
*/
set isEdited(edited) {
if (this._isEdited !== edited) {
- this._isEdited = edited;
+ if (edited) {
+ this._deactivateAllTools();
+ this._isEdited = true;
- if (this._isEdited) {
// Automatically edit the feature if unique
- if (this.featureDrawn.length === 1) {
+ if (this.featureDrawn && this.featureDrawn.length === 1) {
this._selectInteraction.getFeatures().push(this.featureDrawn[0]);
this.drawColor = this.featureDrawn[0].get('color');
}
- this._map.removeInteraction(this._drawInteraction);
-
- this._map.addInteraction(this._translateInteraction);
this._map.addInteraction(this._selectInteraction);
this._map.addInteraction(this._modifyInteraction);
- this.toolSelected = 'deactivate';
- this.isErasing = false;
- this.isRotate = false;
- this.isScaling = false;
- this.isSplitting = false;
-
/**
* @event digitizingEditionBegins
* @type {object}
@@ -868,9 +979,9 @@ export class Digitizing {
*/
mainEventDispatcher.dispatch('digitizing.editionBegins');
} else {
+ this._isEdited = false;
// Clear selection
this._selectInteraction.getFeatures().clear();
- this._map.removeInteraction(this._translateInteraction);
this._map.removeInteraction(this._selectInteraction);
this._map.removeInteraction(this._modifyInteraction);
@@ -905,22 +1016,20 @@ export class Digitizing {
*/
set isRotate(isRotate) {
if (this._isRotate !== isRotate) {
- this._isRotate = isRotate;
+ if (isRotate) {
+ this._deactivateAllTools();
+ this._isRotate = true;
- if (this._isRotate) {
- this.toolSelected = 'deactivate';
- this.isErasing = false;
- this.isEdited = false;
- this.isScaling = false;
- this.isSplitting = false;
-
- // Automatically scaling the feature if unique
- if (this.featureDrawn.length === 1) {
+ // Automatically select the feature if unique
+ if (this.featureDrawn && this.featureDrawn.length === 1) {
this._transformRotateInteraction.getFeatures().push(this.featureDrawn[0]);
}
this._map.addInteraction(this._transformRotateInteraction);
} else {
+ this._isRotate = false;
+ this._transformRotateInteraction.getFeatures().clear();
this._map.removeInteraction(this._transformRotateInteraction);
+ this._restoreEditionEditMode();
}
/**
@@ -954,22 +1063,20 @@ export class Digitizing {
*/
set isScaling(isScaling) {
if (this._isScaling !== isScaling) {
- this._isScaling = isScaling;
+ if (isScaling) {
+ this._deactivateAllTools();
+ this._isScaling = true;
- if (this._isScaling) {
- this.toolSelected = 'deactivate';
- this.isErasing = false;
- this.isEdited = false;
- this.isRotate = false;
- this.isSplitting = false;
-
- // Automatically scaling the feature if unique
- if (this.featureDrawn.length === 1) {
+ // Automatically select the feature if unique
+ if (this.featureDrawn && this.featureDrawn.length === 1) {
this._transformScaleInteraction.getFeatures().push(this.featureDrawn[0]);
}
this._map.addInteraction(this._transformScaleInteraction);
} else {
+ this._isScaling = false;
+ this._transformScaleInteraction.getFeatures().clear();
this._map.removeInteraction(this._transformScaleInteraction);
+ this._restoreEditionEditMode();
}
/**
@@ -1003,35 +1110,44 @@ export class Digitizing {
*/
set isSplitting(isSplitting) {
if (this._isSplitting !== isSplitting) {
- this._isSplitting = isSplitting;
-
- if (this._isSplitting) {
- // Disable other tools
- this.toolSelected = 'deactivate';
- this.isEdited = false;
- this.isRotate = false;
- this.isScaling = false;
- this.isErasing = false;
+ if (isSplitting) {
+ this._deactivateAllTools();
+ this._isSplitting = true;
+ // Use a separate source for the split line so it doesn't trigger
+ // addfeature listeners (singlePartGeometry would remove existing features)
+ this._splitSource = new VectorSource();
this._splitInteraction = new Draw({
- source: this._drawSource,
- type: 'LineString'
+ source: this._splitSource,
+ type: 'LineString',
+ style: this._drawStyleFunction
});
this._splitInteraction.on('drawend', event => {
+ if (this._isOperationInProgress) return;
+ this._isOperationInProgress = true;
+
+ // Get the split line geometry and clear the temporary source
+ const splitLineGeom = event.feature.getGeometry();
+ this._splitSource.clear();
+
+ // Take a snapshot of existing features (avoid modifying during iteration)
+ const existingFeatures = [...this._drawSource.getFeatures()];
+
+ // Find features that intersect the split line
+ const featuresToSplit = existingFeatures.filter(
+ f => splitLineGeom.intersectsExtent(f.getGeometry().getExtent())
+ );
+ if (featuresToSplit.length === 0) {
+ this._isOperationInProgress = false;
+ return;
+ }
+
+ // Lazy-load geometry libraries
Promise.all([
- import(
- /* webpackChunkName: 'OLparser' */ 'jsts/org/locationtech/jts/io/OL3Parser.js'
- ),
- import(
- /* webpackChunkName: 'UnionOp' */ 'jsts/org/locationtech/jts/operation/union/UnionOp.js'
- ),
- import(
- /* webpackChunkName: 'Polygonizer' */
- 'jsts/org/locationtech/jts/operation/polygonize/Polygonizer.js'
- ),
- import(
- /* webpackChunkName: 'lineSplit' */ '@turf/line-split'
- ),
+ import(/* webpackChunkName: 'OLparser' */ 'jsts/org/locationtech/jts/io/OL3Parser.js'),
+ import(/* webpackChunkName: 'UnionOp' */ 'jsts/org/locationtech/jts/operation/union/UnionOp.js'),
+ import(/* webpackChunkName: 'Polygonizer' */ 'jsts/org/locationtech/jts/operation/polygonize/Polygonizer.js'),
+ import(/* webpackChunkName: 'lineSplit' */ '@turf/line-split'),
]).then(([
{ default: OLparser },
{ default: UnionOp },
@@ -1039,83 +1155,90 @@ export class Digitizing {
{ default: lineSplit }
]) => {
const parser = new OLparser();
- parser.inject(
- Point,
- LineString,
- LinearRing,
- Polygon,
- MultiPoint,
- MultiLineString,
- MultiPolygon
- );
-
- const lineGeometry = event.feature.getGeometry();
+ parser.inject(Point, LineString, LinearRing, Polygon, MultiPoint, MultiLineString, MultiPolygon);
+ const format = new GeoJSON();
- // Remove line used for splitting
- this._drawSource.removeFeature(event.feature);
+ const allSplitFeatures = [];
- for (const feature of this._drawSource.getFeatures()) {
- // Check if split line intersects with drawn feature
- if (!lineGeometry.intersectsExtent(feature.getGeometry().getExtent())) {
- continue;
- }
+ for (const feature of featuresToSplit) {
const geomType = feature.getGeometry().getType();
- if ( geomType === 'Polygon') {
- // Convert the OpenLayers geometry to a JSTS geometry
- const jstsLine = parser.read(lineGeometry);
- const jstsDrawnGeom = parser.read(feature.getGeometry());
-
- // Perform union of Polygon and Line and use Polygonizer to split the polygon by line
- let union = UnionOp.union(jstsDrawnGeom.getExteriorRing(), jstsLine);
- let polygonizer = new Polygonizer();
+ const featureColor = feature.get('color') || this._drawColor;
+ let newFeatures = null;
- // Splitting polygon in two parts
+ if (geomType === 'Polygon') {
+ const jstsLine = parser.read(splitLineGeom);
+ const jstsDrawnGeom = parser.read(feature.getGeometry());
+ const union = UnionOp.union(jstsDrawnGeom.getExteriorRing(), jstsLine);
+ const polygonizer = new Polygonizer();
polygonizer.add(union);
- let polygons = polygonizer.getPolygons();
-
- // This will execute only if polygon is successfully splitted into two parts
- if (polygons.array.length == 2) {
- // Remove original polygon
- this._drawSource.removeFeature(feature);
-
- // Iterate through splitted polygons
- polygons.array.forEach(geom => {
- let splitted_polygon = new Feature({
- geometry: new Polygon(parser.write(geom).getCoordinates())
- });
-
- // Add splitted polygon to vector layer
- this._drawSource.addFeature(splitted_polygon);
- this._selectInteraction.getFeatures().push(splitted_polygon);
- });
+ const polygons = polygonizer.getPolygons();
- this.isEdited = true;
+ if (polygons.array.length >= 2) {
+ newFeatures = polygons.array.map(geom => {
+ const f = new Feature({ geometry: new Polygon(parser.write(geom).getCoordinates()) });
+ f.set('color', featureColor);
+ return f;
+ });
}
} else if (geomType === 'LineString') {
- const format = new GeoJSON();
- const turfDrawnFeature = format.writeFeatureObject(feature);
- const turfSplitterFeature = format.writeFeatureObject(event.feature);
-
- const split = lineSplit(turfDrawnFeature, turfSplitterFeature);
+ const turfDrawn = format.writeFeatureObject(feature);
+ const turfSplitter = format.writeFeatureObject(event.feature);
+ const split = lineSplit(turfDrawn, turfSplitter);
if (split.features.length > 1) {
- // Remove original lineString
- this._drawSource.removeFeature(feature);
-
- split.features.forEach((feature) => {
- let splitted_line = format.readFeature(feature);
- this._drawSource.addFeature(splitted_line);
- this._selectInteraction.getFeatures().push(splitted_line);
+ newFeatures = split.features.map(sf => {
+ const f = format.readFeature(sf);
+ f.set('color', featureColor);
+ return f;
});
}
- this.isEdited = true;
+ }
+
+ if (newFeatures && newFeatures.length > 1) {
+ // Disable single-part constraint before adding multiple features
+ // so the addfeature listener doesn't remove the first part
+ // when the second is added.
+ this._singlePartGeometry = false;
+ // Remove original, add all split parts
+ this._drawSource.removeFeature(feature);
+ newFeatures.forEach(f => this._drawSource.addFeature(f));
+ allSplitFeatures.push(...newFeatures);
}
}
+
+ if (allSplitFeatures.length > 0) {
+ // Multiple parts now exist — disable single part constraint
+ this._singlePartGeometry = false;
+
+ // Switch to edit mode and select all split features
+ this.isEdited = true;
+ allSplitFeatures.forEach(f => {
+ this._selectInteraction.getFeatures().push(f);
+ });
+
+ this.isSplitting = false;
+ }
+ this._isOperationInProgress = false;
+ }).catch(error => {
+ console.error('Split operation failed:', error);
+ this._isOperationInProgress = false;
});
});
this._map.addInteraction(this._splitInteraction);
+ this._disableDoubleClickZoom();
+ // Re-order snap interaction so it processes before the split Draw
+ if (mainLizmap.snapping) {
+ mainLizmap.snapping.reorderSnapInteraction();
+ }
} else {
+ this._isSplitting = false;
this._map.removeInteraction(this._splitInteraction);
+ if (this._splitSource) {
+ this._splitSource.clear();
+ this._splitSource = null;
+ }
+ this._enableDoubleClickZoom();
+ this._restoreEditionEditMode();
}
/**
@@ -1151,15 +1274,9 @@ export class Digitizing {
*/
set isErasing(isErasing) {
if (this._isErasing !== isErasing) {
- this._isErasing = isErasing;
-
- if (this._isErasing) {
- // deactivate other tools
- this.toolSelected = 'deactivate';
- this.isEdited = false;
- this.isRotate = false;
- this.isScaling = false;
- this.isSplitting = false;
+ if (isErasing) {
+ this._deactivateAllTools();
+ this._isErasing = true;
this._erasingCallBack = event => {
const features = this._map.getFeaturesAtPixel(event.pixel, {
@@ -1192,6 +1309,7 @@ export class Digitizing {
* }, 'digitizing.erase');
*/
mainEventDispatcher.dispatch('digitizing.erase');
+ mainEventDispatcher.dispatch('digitizing.geometryChanged');
}
};
@@ -1227,6 +1345,7 @@ export class Digitizing {
* console.log('The digitizing erasing tool ends');
* }, 'digitizing.erasingEnds');
*/
+ this._isErasing = false;
mainEventDispatcher.dispatch('digitizing.erasingEnds');
}
}
@@ -1266,14 +1385,6 @@ export class Digitizing {
});
}
- /**
- * Is the digitizing constraints panel visible or not
- * @type {boolean}
- */
- get hasConstraintsPanelVisible() {
- return this._hasMeasureVisible && ['line', 'polygon'].includes(this.toolSelected);
- }
-
/**
* Is the digitizing save tool active or not?
* @type {boolean}
@@ -1282,38 +1393,6 @@ export class Digitizing {
return this._isSaved;
}
- /**
- * Get the distance constraint
- * @type {number}
- */
- get distanceConstraint(){
- return this._distanceConstraint;
- }
-
- /**
- * Set the distance constraint
- * @type {number}
- */
- set distanceConstraint(distanceConstraint){
- this._distanceConstraint = parseFloat(distanceConstraint)
- }
-
- /**
- * Get the angle constraint
- * @type {number}
- */
- get angleConstraint(){
- return this._angleConstraint;
- }
-
- /**
- * Set the angle constraint
- * @type {number}
- */
- set angleConstraint(angleConstraint){
- this._angleConstraint = parseFloat(angleConstraint)
- }
-
/**
* Erase a feature from the draw source
* @private
@@ -1358,142 +1437,6 @@ export class Digitizing {
});
}
- /**
- * The constraints handler
- * @private
- * @param {*} coords - the mouse coordinates
- * @param {*} geom - the geometry
- * @param {*} geomType - the geometry type
- * @returns {void}
- */
- _contraintsHandler(coords, geom, geomType) {
- // Create geom if undefined
- if (!geom) {
- if (geomType === 'Polygon') {
- geom = new Polygon(coords);
- } else {
- geom = new LineString(coords);
- }
- }
-
- let _coords;
-
- if (geomType === 'Polygon') {
- // Handle first linearRing in polygon
- // TODO: Polygons with holes are not handled yet
- _coords = coords[0];
- } else {
- _coords = coords;
- }
-
- if (this._distanceConstraint || this._angleConstraint) {
- // Clear previous visual constraint features
- this._constraintLayer.getSource().clear();
- // Display constraint layer
- this._constraintLayer.setVisible(true);
-
- // Last point drawn on click
- const lastDrawnPointCoords = _coords[_coords.length - 2];
- // Point under cursor
- const cursorPointCoords = _coords[_coords.length - 1];
-
- // Contraint where point will be drawn on click
- let constrainedPointCoords = cursorPointCoords;
-
- const mapProjection = this._map.getView().getProjection();
-
- if (this._distanceConstraint) {
- // Draw circle with distanceConstraint as radius
- const circle = circular(
- transform(lastDrawnPointCoords, mapProjection, 'EPSG:4326'),
- this._distanceConstraint,
- 128
- );
-
- constrainedPointCoords = transform(
- circle.getClosestPoint(
- transform(cursorPointCoords, mapProjection, 'EPSG:4326')
- ),
- 'EPSG:4326',
- mapProjection,
- );
-
- // Draw visual constraint features
- this._constraintLayer.getSource().addFeature(
- new Feature({
- geometry: circle.transform('EPSG:4326', mapProjection)
- })
- );
-
- if (!this._angleConstraint) {
- this._constraintLayer.getSource().addFeature(
- new Feature({
- geometry: new Point(constrainedPointCoords)
- })
- );
- }
- }
-
- if (this._angleConstraint && _coords.length > 2) {
- const constrainedAngleClockwise = new LineString([_coords[_coords.length - 3], lastDrawnPointCoords]);
- const constrainedAngleAntiClockwise = constrainedAngleClockwise.clone();
- // Rotate clockwise
- constrainedAngleClockwise.rotate(-1 * this._angleConstraint * (Math.PI / 180.0), lastDrawnPointCoords);
- const closestClockwise = constrainedAngleClockwise.getClosestPoint(cursorPointCoords);
- // Rotate anticlockwise
- constrainedAngleAntiClockwise.rotate(this._angleConstraint * (Math.PI / 180.0), lastDrawnPointCoords);
- const closestAntiClockwise = constrainedAngleAntiClockwise.getClosestPoint(cursorPointCoords);
-
- // Stretch lines
- const scaleFactor = 50;
- constrainedAngleClockwise.scale(scaleFactor, scaleFactor, lastDrawnPointCoords);
- constrainedAngleAntiClockwise.scale(scaleFactor, scaleFactor, lastDrawnPointCoords);
-
- this._constraintLayer.getSource().addFeatures([
- new Feature({
- geometry: constrainedAngleClockwise
- }),
- new Feature({
- geometry: constrainedAngleAntiClockwise
- })
- ]);
-
- let constrainedAngleLineString;
-
- // Display clockwise or anticlockwise angle
- // Closest from cursor is displayed
- if (this.getProjectedLength(
- new LineString([closestClockwise, cursorPointCoords])
- ) < this.getProjectedLength(
- new LineString([closestAntiClockwise, cursorPointCoords])
- )) {
- constrainedAngleLineString = constrainedAngleClockwise.clone();
- } else {
- constrainedAngleLineString = constrainedAngleAntiClockwise.clone();
- }
-
- if (this._distanceConstraint) {
- const ratio = this._distanceConstraint / this.getProjectedLength(constrainedAngleLineString);
- constrainedAngleLineString.scale(ratio, ratio, constrainedAngleLineString.getLastCoordinate());
-
- constrainedPointCoords = constrainedAngleLineString.getFirstCoordinate();
- } else {
- constrainedPointCoords = constrainedAngleLineString.getClosestPoint(cursorPointCoords);
- }
-
- }
- _coords[_coords.length - 1] = constrainedPointCoords;
- }
-
- if (geomType === 'Polygon') {
- geom.setCoordinates([_coords]);
- } else {
- geom.setCoordinates(_coords);
- }
-
- return geom;
- }
-
/**
* The tooltips handler
* @private
@@ -1710,6 +1653,7 @@ export class Digitizing {
}
const color = this.featureDrawn[index].get('color') || this._drawColor;
let opacityFactor = this.featureDrawn[index].get('mode') == 'textonly' ? 0 : 1;
+ let symbolizer;
let strokeAndFill =
`
${color}
@@ -1722,7 +1666,6 @@ export class Digitizing {
`;
// We consider LINESTRING and POLYGON together currently
- let symbolizer;
if (this.featureDrawn[index].getGeometry().getType() === 'Point') {
symbolizer =
`
@@ -1894,6 +1837,45 @@ export class Digitizing {
*/
mainEventDispatcher.dispatch('digitizing.erase.all');
mainEventDispatcher.dispatch('digitizing.erase');
+ mainEventDispatcher.dispatch('digitizing.geometryChanged');
+ }
+
+ /**
+ * Get a feature as WKT in the given SRID
+ * @param {string|number} srid - Target SRID (e.g. 4326)
+ * @param {Feature} [feature] - Optional specific feature. Defaults to first drawn feature.
+ * @returns {string} WKT string or empty string if no features
+ */
+ getFeatureAsWKT(srid, feature) {
+ if (!feature) {
+ const features = this.featureDrawn;
+ if (!features || features.length === 0) return '';
+ feature = features[0];
+ }
+ const wktFormat = new WKT();
+ const geom = feature.getGeometry().clone();
+ geom.transform(this._map.getView().getProjection(), 'EPSG:' + srid);
+ return wktFormat.writeGeometry(geom);
+ }
+
+ /**
+ * Load a feature from WKT string and add it to the draw source
+ * @param {string} wktString - WKT geometry string
+ * @param {string|number} srid - Source SRID of the WKT
+ * @returns {Feature|null} The loaded feature or null
+ */
+ loadFeatureFromWKT(wktString, srid) {
+ if (!wktString) return null;
+ const wktFormat = new WKT();
+ const feature = wktFormat.readFeature(wktString, {
+ dataProjection: 'EPSG:' + srid,
+ featureProjection: this._map.getView().getProjection()
+ });
+ this.eraseAll();
+ feature.set('color', this._drawColor);
+ this._drawSource.addFeature(feature);
+ mainEventDispatcher.dispatch('digitizing.geometryChanged');
+ return feature;
}
/**
@@ -1922,10 +1904,18 @@ export class Digitizing {
});
}
}
- localStorage.setItem(
- this._repoAndProjectString + '_' + this._context + '_drawLayer',
- JSON.stringify(savedFeatures),
- );
+ try {
+ localStorage.setItem(
+ this._repoAndProjectString + '_' + this._context + '_drawLayer',
+ JSON.stringify(savedFeatures),
+ );
+ } catch (e) {
+ if (e.name === 'QuotaExceededError') {
+ this._lizmap3.addMessage(lizDict['digitizing.save.quota.error'] || 'Drawing storage quota exceeded', 'warning', true);
+ } else {
+ console.error('Failed to save drawing to localStorage:', e);
+ }
+ }
} else {
localStorage.removeItem(this._repoAndProjectString + '_' + this._context + '_drawLayer');
}
@@ -1983,12 +1973,12 @@ export class Digitizing {
}
} catch(json_error) {
// the saved data is an invalid JSON
- console.log('`'+savedGeomJSON+'` is not a JSON!');
+ console.warn('`'+savedGeomJSON+'` is not a JSON!');
// the saved data could be a WKT from previous lizmap version
try {
const formatWKT = new WKT();
loadedFeatures = formatWKT.readFeatures(savedGeomJSON);
- console.log(loadedFeatures.length+' features read from WKT!');
+ console.warn(loadedFeatures.length+' features read from WKT!');
// set color
for(const loadedFeature of loadedFeatures){
// init measure tooltip
@@ -2000,7 +1990,7 @@ export class Digitizing {
localStorage.removeItem(this._repoAndProjectString + '_' + this._context + '_drawLayer');
}
} catch(wkt_error) {
- console.log('`'+savedGeomJSON+'` is not a WKT!');
+ console.warn('`'+savedGeomJSON+'` is not a WKT!');
console.error(json_error);
console.error(wkt_error);
}
@@ -2170,7 +2160,7 @@ export class Digitizing {
OL6features = features;
}
} catch (error) {
- this._lizmap3.addMessage(error, 'danger', true)
+ this._lizmap3.addMessage(error.message || String(error), 'danger', true);
}
if (OL6features) {
diff --git a/assets/src/modules/Edition.js b/assets/src/modules/Edition.js
index f1fdd2570a..732fd03a44 100644
--- a/assets/src/modules/Edition.js
+++ b/assets/src/modules/Edition.js
@@ -6,7 +6,12 @@
* @license MPL-2.0
*/
-import { mainEventDispatcher } from '../modules/Globals.js';
+import { mainLizmap, mainEventDispatcher } from '../modules/Globals.js';
+import { getArea, getLength } from 'ol/sphere.js';
+import { Feature } from 'ol';
+import { Point, LineString } from 'ol/geom.js';
+import proj4 from 'proj4';
+import { register } from 'ol/proj/proj4.js';
/**
* @class
@@ -26,12 +31,18 @@ export default class Edition {
this.layerGeometry = undefined;
this.drawControl = undefined;
this._lastSegmentLength = undefined;
+ this._geometryChangedListener = null;
+ this._featureDrawnListener = null;
+ this._geolocationListener = null;
+ this._splitCompleteListener = null;
+ this._savedDrawColor = null;
lizmap3.events.on({
lizmapeditiondrawfeatureactivated: (properties) => {
this.drawFeatureActivated = true;
this.layerGeometry = properties.editionConfig.geometryType;
this.drawControl = properties.drawControl;
+ this.activateDigitizing();
mainEventDispatcher.dispatch('edition.drawFeatureActivated');
},
lizmapeditiondrawfeaturedeactivated: () => {
@@ -39,18 +50,60 @@ export default class Edition {
this.layerGeometry = undefined;
this.drawControl = undefined;
this.lastSegmentLength = undefined;
+ this.deactivateDigitizing();
mainEventDispatcher.dispatch('edition.drawFeatureActivated');
},
lizmapeditionformdisplayed: (evt) => {
this._layerId = (evt['layerId']);
+ // When the layer allows geometry editing, lizmapeditiondrawfeatureactivated
+ // fires shortly after this event and activateDigitizing() will show the
+ // appropriate toast. When the layer only allows attribute modification,
+ // that event never fires (the legacy modify branch skips activateDrawFeature)
+ // so we surface the modify message here as a fallback.
+ setTimeout(() => {
+ if (!this.drawFeatureActivated && !document.getElementById('lizmap-editing-message')) {
+ this._showEditingMessage('edition.select.modify.activate');
+ }
+ }, 50);
mainEventDispatcher.dispatch('edition.formDisplayed');
},
lizmapeditionformclosed: () => {
+ this.deactivateDigitizing();
+ // The legacy lizmapeditiondrawfeaturedeactivated event used to fire
+ // from the OL2 DrawFeature / ModifyFeature controls' "deactivate"
+ // listeners; those controls no longer activate, so we reset the
+ // mirroring state here instead.
+ this.drawFeatureActivated = false;
+ this.layerGeometry = undefined;
+ this.drawControl = undefined;
+ this.lastSegmentLength = undefined;
+ // Remove any lingering edit-mode toast so the next interaction
+ // (popup query, base-layer switch, etc.) starts from a clean state.
+ document.getElementById('lizmap-editing-message')?.remove();
+ // Suppress the OL10 map for a tick so the browser's native click
+ // tracking doesn't fuse the next click with the most recent
+ // edition click into a dblclick — that would cancel OL10's
+ // singleclick event and the popup-on-click handler with it.
+ // Affects fast post-submit identify clicks (~<500ms apart) in
+ // automated tests; harmless in real interactive usage.
+ const newOlMap = document.getElementById('newOlMap');
+ if (newOlMap) {
+ newOlMap.style.pointerEvents = 'none';
+ setTimeout(() => { newOlMap.style.pointerEvents = ''; }, 350);
+ }
mainEventDispatcher.dispatch('edition.formClosed');
}
});
}
+ /**
+ * Get the digitizing web component element
+ * @returns {HTMLElement|null}
+ */
+ get digitizingElement() {
+ return document.querySelector('lizmap-digitizing[context="edition"]');
+ }
+
get layerId() {
return this._layerId;
}
@@ -86,6 +139,351 @@ export default class Edition {
}
}
+ /**
+ * Show an editing message popup for the current tool
+ * @param {string} messageKey - The lizDict key for the message
+ */
+ _showEditingMessage(messageKey) {
+ const msg = lizDict[messageKey];
+ if (!msg) return;
+ $('#lizmap-editing-message').remove();
+ lizMap.addMessage(msg, 'info', true, 10000).attr('id', 'lizmap-editing-message');
+ }
+
+ /**
+ * Activate the digitizing module for edition
+ */
+ activateDigitizing() {
+ if (!this.layerGeometry) return;
+
+ // Map edition geometry types to digitizing tools
+ const toolMap = { point: 'point', line: 'line', polygon: 'polygon' };
+ const tool = toolMap[this.layerGeometry] || 'point';
+
+ // Set digitizing context to edition and ensure draw layer is visible
+ mainLizmap.digitizing.context = 'edition';
+ mainLizmap.digitizing.toggleVisibility(true);
+ mainLizmap.digitizing.singlePartGeometry = true;
+
+ // Ensure the measure tool is inactive when starting a new editing session
+ if (mainLizmap.digitizing.hasMeasureVisible) {
+ mainLizmap.digitizing.hasMeasureVisible = false;
+ }
+
+ // Hide digitizing toolbar immediately for point layers
+ const digEl = this.digitizingElement;
+ if (digEl) {
+ digEl.style.display = this.layerGeometry === 'point' ? 'none' : '';
+ }
+
+ // Force blue color for edition (like OL2 style) without persisting to localStorage
+ this._savedDrawColor = mainLizmap.digitizing.drawColor;
+ mainLizmap.digitizing._drawColor = '#3388ff';
+ mainEventDispatcher.dispatch({ type: 'digitizing.drawColor', color: '#3388ff' });
+
+ // Load existing geometry from form if editing an existing feature
+ const eform = document.querySelector('#edition-form-container form');
+ if (eform) {
+ const gColumn = eform.querySelector('input[name="liz_geometryColumn"]')?.value;
+ const srid = eform.querySelector('input[name="liz_srid"]')?.value;
+ const proj4Def = eform.querySelector('input[name="liz_proj4"]')?.value;
+
+ // Register the layer's SRID in proj4/OL6 if not already known
+ if (srid && proj4Def) {
+ const epsgCode = 'EPSG:' + srid;
+ if (!proj4.defs(epsgCode)) {
+ proj4.defs(epsgCode, proj4Def);
+ register(proj4);
+ }
+ }
+
+ const wkt = gColumn ? eform.querySelector(`input[name="${gColumn}"]`)?.value : '';
+ if (wkt) {
+ mainLizmap.digitizing.loadFeatureFromWKT(wkt, srid);
+ // Switch to edit mode since we have an existing feature
+ mainLizmap.digitizing.isEdited = true;
+ this._showEditingMessage('edition.select.modify.activate');
+ } else {
+ // Activate the drawing tool for new geometry
+ mainLizmap.digitizing.toolSelected = tool;
+ this._showEditingMessage('edition.draw.activate');
+ }
+ } else {
+ mainLizmap.digitizing.toolSelected = tool;
+ this._showEditingMessage('edition.draw.activate');
+ }
+
+ // Listen to geometry changes and update form
+ if (!this._geometryChangedListener) {
+ this._geometryChangedListener = () => {
+ this.updateFormGeometry();
+ };
+ mainEventDispatcher.addListener(
+ this._geometryChangedListener,
+ 'digitizing.geometryChanged'
+ );
+ }
+
+ // Auto-switch to edit mode after first feature is drawn (like OL2 behavior)
+ if (!this._featureDrawnListener) {
+ this._featureDrawnListener = () => {
+ if (mainLizmap.digitizing.context === 'edition' && mainLizmap.digitizing.featureDrawn) {
+ mainLizmap.digitizing.isEdited = true;
+ // Ensure draw layer stays visible after switching to edit mode
+ mainLizmap.digitizing.toggleVisibility(true);
+ this._showEditingMessage('edition.select.modify.activate');
+ }
+ };
+ mainEventDispatcher.addListener(
+ this._featureDrawnListener,
+ 'digitizing.featureDrawn'
+ );
+ }
+
+ // Listen to geolocation position for GPS-linked drawing
+ if (!this._geolocationListener) {
+ this._geolocationListener = () => {
+ this._handleGeolocationPosition();
+ };
+ mainEventDispatcher.addListener(
+ this._geolocationListener,
+ 'geolocation.position'
+ );
+ }
+
+ // Listen to split complete to store new features for saving
+ if (!this._splitCompleteListener) {
+ this._splitCompleteListener = () => {
+ this._handleSplitComplete();
+ };
+ mainEventDispatcher.addListener(
+ this._splitCompleteListener,
+ 'digitizing.splitComplete'
+ );
+ }
+ }
+
+ /**
+ * Handle geolocation position updates for GPS-linked edition
+ * @private
+ */
+ _handleGeolocationPosition() {
+ if (!mainLizmap.geolocation.isLinkedToEdition) return;
+ if (!this.layerGeometry) return;
+
+ const mapProjection = mainLizmap.map.getView().getProjection().getCode();
+ const position = mainLizmap.geolocation.getPositionInCRS(mapProjection);
+ if (!position || !position[0] || !position[1]) return;
+
+ const coord = [position[0], position[1]];
+
+ if (this.layerGeometry === 'point') {
+ // For point geometry: create/move point at GPS position
+ const feature = new Feature(new Point(coord));
+ mainLizmap.digitizing._drawSource.clear();
+ mainLizmap.digitizing._drawSource.addFeature(feature);
+ mainEventDispatcher.dispatch('digitizing.geometryChanged');
+ } else if (this.layerGeometry === 'line' || this.layerGeometry === 'polygon') {
+ // For line/polygon: append coordinate if draw interaction is active
+ const drawInteraction = mainLizmap.digitizing._drawInteraction;
+ if (drawInteraction && typeof drawInteraction.appendCoordinates === 'function') {
+ drawInteraction.appendCoordinates([coord]);
+ }
+ }
+
+ // Update last segment length
+ const features = mainLizmap.digitizing.featureDrawn;
+ if (features && features.length > 0) {
+ const geom = features[0].getGeometry();
+ if (geom.getType() === 'LineString') {
+ const coords = geom.getCoordinates();
+ if (coords.length >= 2) {
+ const lastSegment = new LineString([coords[coords.length - 2], coords[coords.length - 1]]);
+ this.lastSegmentLength = getLength(lastSegment, {
+ projection: mainLizmap.map.getView().getProjection()
+ }).toFixed(3);
+ }
+ }
+ }
+ }
+
+ /**
+ * Handle split complete in edition context.
+ * Determines the smaller/larger split parts and triggers the legacy split workflow.
+ * @param {object} evt - The split event with features array and geometryType
+ * @private
+ */
+ _handleSplitComplete() {
+ if (mainLizmap.digitizing.context !== 'edition') return;
+
+ // Read split results from Digitizing instance (not from event —
+ // OL Feature objects can't pass through EventDispatcher's JSON.stringify)
+ const splitFeatures = mainLizmap.digitizing._lastSplitFeatures;
+ const splitGeometryType = mainLizmap.digitizing._lastSplitGeometryType;
+ if (!splitFeatures || splitFeatures.length < 2) return;
+
+ const eform = document.querySelector('#edition-form-container form');
+ if (!eform) return;
+
+ const gColumn = eform.querySelector('input[name="liz_geometryColumn"]')?.value;
+ const srid = eform.querySelector('input[name="liz_srid"]')?.value;
+ if (!gColumn || !srid) return;
+
+ // Determine smaller and larger features by length/area
+ const f0 = splitFeatures[0];
+ const f1 = splitFeatures[1];
+ const g0 = f0.getGeometry();
+ const g1 = f1.getGeometry();
+ let smallerFeature, largerFeature;
+
+ if (splitGeometryType === 'line') {
+ const len0 = getLength(g0, { projection: mainLizmap.map.getView().getProjection() });
+ const len1 = getLength(g1, { projection: mainLizmap.map.getView().getProjection() });
+ smallerFeature = len0 < len1 ? f0 : f1;
+ largerFeature = len0 < len1 ? f1 : f0;
+ } else {
+ const area0 = getArea(g0, { projection: mainLizmap.map.getView().getProjection() });
+ const area1 = getArea(g1, { projection: mainLizmap.map.getView().getProjection() });
+ smallerFeature = area0 < area1 ? f0 : f1;
+ largerFeature = area0 < area1 ? f1 : f0;
+ }
+
+ // Serialize smaller feature as a new record.
+ // Capture the current form data including disabled (read-only) elements so
+ // that required attributes which are rendered as disabled inputs are still
+ // sent to saveNewFeature. PK field clearing is handled server-side in the
+ // PHP saveNewFeature action (which knows the exact PK column names).
+ const smallerWkt = mainLizmap.digitizing.getFeatureAsWKT(srid, smallerFeature);
+ if (!smallerWkt) return;
+ const formData = new FormData(eform);
+
+ // Include values from disabled elements (read-only required fields, PK columns).
+ for (const el of eform.querySelectorAll('[name]:disabled')) {
+ if (!formData.has(el.name)) {
+ formData.set(el.name, el.value ?? '');
+ }
+ }
+
+ formData.set('liz_featureId', '');
+ formData.set('__JFORMS_TOKEN__', '');
+ if (gColumn) {
+ formData.set(gColumn, smallerWkt);
+ }
+
+ // Keep both split features visible in the draw source so the user can
+ // see the full result of the split operation on the map.
+
+ // Trigger legacy event so edition.js stores the new feature for saving
+ this._lizmap3.events.triggerEvent('lizmapeditionsplitcomplete', {
+ newFeatureFormData: formData
+ });
+
+ // Set the form geometry explicitly to the larger feature
+ const largerWkt = mainLizmap.digitizing.getFeatureAsWKT(srid, largerFeature);
+ const input = eform.querySelector(`input[name="${gColumn}"]`);
+ if (input) {
+ input.value = largerWkt;
+ input.dispatchEvent(new Event('change'));
+ }
+
+ // Trigger backward compat event
+ const featureId = eform.querySelector('input[name="liz_featureId"]')?.value;
+ const layerId = eform.querySelector('input[name="liz_layerId"]')?.value;
+ lizMap.events.triggerEvent('lizmapeditiongeometryupdated', {
+ layerId, featureId, geometry: largerWkt, srid
+ });
+
+ // Notify the user that editing tools are locked until both features are saved
+ this._showEditingMessage('edition.split.locked.message');
+ }
+
+ /**
+ * Deactivate the digitizing module for edition
+ */
+ deactivateDigitizing() {
+ // Remove geometry change listener
+ if (this._geometryChangedListener) {
+ mainEventDispatcher.removeListener(
+ this._geometryChangedListener,
+ 'digitizing.geometryChanged'
+ );
+ this._geometryChangedListener = null;
+ }
+
+ // Remove feature drawn listener
+ if (this._featureDrawnListener) {
+ mainEventDispatcher.removeListener(
+ this._featureDrawnListener,
+ 'digitizing.featureDrawn'
+ );
+ this._featureDrawnListener = null;
+ }
+
+ // Remove geolocation listener
+ if (this._geolocationListener) {
+ mainEventDispatcher.removeListener(
+ this._geolocationListener,
+ 'geolocation.position'
+ );
+ this._geolocationListener = null;
+ }
+
+ // Remove split complete listener
+ if (this._splitCompleteListener) {
+ mainEventDispatcher.removeListener(
+ this._splitCompleteListener,
+ 'digitizing.splitComplete'
+ );
+ this._splitCompleteListener = null;
+ }
+
+ // Clear digitizing features for edition context
+ if (mainLizmap.digitizing && mainLizmap.digitizing.context === 'edition') {
+ mainLizmap.digitizing.toolSelected = 'deactivate';
+ mainLizmap.digitizing.eraseAll();
+ mainLizmap.digitizing.context = 'draw';
+ mainLizmap.digitizing.toggleVisibility(false);
+
+ // Restore digitizing component visibility
+ const digEl = this.digitizingElement;
+ if (digEl) {
+ digEl.style.display = '';
+ }
+
+ // Restore user's draw color
+ if (this._savedDrawColor) {
+ mainLizmap.digitizing._drawColor = this._savedDrawColor;
+ mainEventDispatcher.dispatch({ type: 'digitizing.drawColor', color: this._savedDrawColor });
+ this._savedDrawColor = null;
+ }
+ }
+ }
+
+ /**
+ * Update the edition form geometry field from the digitizing module
+ */
+ updateFormGeometry() {
+ const eform = document.querySelector('#edition-form-container form');
+ if (!eform) return;
+ const gColumn = eform.querySelector('input[name="liz_geometryColumn"]')?.value;
+ const srid = eform.querySelector('input[name="liz_srid"]')?.value;
+ if (!gColumn || !srid) return;
+
+ const wkt = mainLizmap.digitizing.getFeatureAsWKT(srid);
+ const input = eform.querySelector(`input[name="${gColumn}"]`);
+ if (input) {
+ input.value = wkt;
+ input.dispatchEvent(new Event('change'));
+ }
+
+ // Backward compat event
+ const featureId = eform.querySelector('input[name="liz_featureId"]')?.value;
+ const layerId = eform.querySelector('input[name="liz_layerId"]')?.value;
+ lizMap.events.triggerEvent('lizmapeditiongeometryupdated', {
+ layerId, featureId, geometry: wkt, srid
+ });
+ }
+
/**
* Fetch editable features for given array of layer IDs
* @param {Array} layerIds - Array of layer IDs
@@ -135,6 +533,9 @@ export default class Edition {
layerIndex++;
}
+ }).catch(error => {
+ console.error('fetchEditableFeatures failed:', error);
+ lizMap.addMessage(error.message || String(error), 'danger', true);
});
}
}
diff --git a/assets/src/modules/FeaturePickerPopup.js b/assets/src/modules/FeaturePickerPopup.js
index e694998efd..1f6f851f66 100644
--- a/assets/src/modules/FeaturePickerPopup.js
+++ b/assets/src/modules/FeaturePickerPopup.js
@@ -5,6 +5,12 @@
* @license MPL-2.0
*/
+import { mainLizmap } from './Globals.js';
+import VectorLayer from 'ol/layer/Vector.js';
+import VectorSource from 'ol/source/Vector.js';
+import { Style, Fill, Stroke } from 'ol/style.js';
+import WKT from 'ol/format/WKT.js';
+
/**
* Popup for selecting features at a clicked position
* @class
@@ -14,28 +20,29 @@ export default class FeaturePickerPopup {
this._map = map;
this._popup = null;
this._features = [];
+ this._highlightSource = null;
this._highlightLayer = null;
+ this._wktFormat = new WKT();
this._createHighlightLayer();
}
/**
- * Create OpenLayers layer for feature highlighting
+ * Create OL6 vector layer for feature highlighting
*/
_createHighlightLayer() {
- // Create temporary vector layer for highlighting
- this._highlightLayer = new OpenLayers.Layer.Vector('FeaturePickerHighlight', {
- displayInLayerSwitcher: false,
- styleMap: new OpenLayers.StyleMap({
- 'default': new OpenLayers.Style({
- fillColor: '#ffaa00',
- fillOpacity: 0.3,
- strokeColor: '#ff6600',
- strokeWidth: 3,
- strokeOpacity: 0.8
- })
- })
+ this._highlightSource = new VectorSource();
+ this._highlightLayer = new VectorLayer({
+ source: this._highlightSource,
+ style: new Style({
+ fill: new Fill({ color: 'rgba(255, 170, 0, 0.3)' }),
+ stroke: new Stroke({ color: '#ff6600', width: 3, opacity: 0.8 })
+ }),
+ zIndex: 900,
+ displayInLayerSwitcher: false
});
- this._map.addLayer(this._highlightLayer);
+ if (mainLizmap?.map) {
+ mainLizmap.map.addLayer(this._highlightLayer);
+ }
}
/**
@@ -69,7 +76,9 @@ export default class FeaturePickerPopup {
minWidth: '250px',
maxWidth: '400px',
maxHeight: '300px',
- overflow: 'auto'
+ overflow: 'auto',
+ // #map has pointer-events:none; override so clicks reach popup items
+ 'pointer-events': 'auto'
});
// Add to map container
@@ -149,19 +158,26 @@ export default class FeaturePickerPopup {
}
/**
- * Highlight a feature on the map
+ * Highlight a feature on the OL6 map
* @param {number} index - Index of feature to highlight
*/
_highlightFeature(index) {
this._clearHighlight();
const feature = this._features[index];
- if (feature && feature.geometry) {
- // Create OpenLayers feature for highlighting
- const highlightFeature = new OpenLayers.Feature.Vector(
- feature.geometry.clone()
- );
- this._highlightLayer.addFeatures([highlightFeature]);
+ if (!feature) return;
+
+ // Prefer WKT-based OL6 feature (set by GeometryCopyHandler)
+ if (feature.geometryWKT && this._highlightSource) {
+ try {
+ const ol6Feature = this._wktFormat.readFeature(feature.geometryWKT, {
+ dataProjection: mainLizmap?.map?.getView()?.getProjection() ?? 'EPSG:3857',
+ featureProjection: mainLizmap?.map?.getView()?.getProjection() ?? 'EPSG:3857'
+ });
+ this._highlightSource.addFeature(ol6Feature);
+ } catch {
+ // silently ignore parse errors
+ }
}
}
@@ -169,7 +185,9 @@ export default class FeaturePickerPopup {
* Clear feature highlight
*/
_clearHighlight() {
- this._highlightLayer.removeAllFeatures();
+ if (this._highlightSource) {
+ this._highlightSource.clear();
+ }
}
/**
diff --git a/assets/src/modules/Snapping.js b/assets/src/modules/Snapping.js
index 4593cb0d68..17e37c6796 100644
--- a/assets/src/modules/Snapping.js
+++ b/assets/src/modules/Snapping.js
@@ -5,11 +5,16 @@
* @license MPL-2.0
*/
-import { mainEventDispatcher } from '../modules/Globals.js';
+import { mainLizmap, mainEventDispatcher } from '../modules/Globals.js';
import Edition from './Edition.js';
import { MapLayerLoadStatus, MapRootState } from './state/MapLayer.js';
import { TreeRootState } from './state/LayerTree.js';
-import WFS from './WFS.js';
+
+import { Snap } from 'ol/interaction.js';
+import { Vector as VectorSource } from 'ol/source.js';
+import { Vector as VectorLayer } from 'ol/layer.js';
+import { Circle, Fill, Stroke, Style } from 'ol/style.js';
+import GeoJSON from 'ol/format/GeoJSON.js';
/**
* @class
@@ -32,7 +37,6 @@ export default class Snapping {
this._lizmap3 = lizmap3;
this._active = false;
- this._snapLayersRefreshable = false;
this._maxFeatures = 1000;
this._restrictToMapExtent = true;
@@ -42,36 +46,38 @@ export default class Snapping {
this._snapLayers = [];
this._snapOnStart = false;
this._pendingMapReadyListener = null;
- this._wfsErrorNotified = false;
-
- // Create layer to store snap features
- const snapLayer = new OpenLayers.Layer.Vector('snaplayer', {
- visibility: false,
- styleMap: new OpenLayers.StyleMap({
- pointRadius: 2,
- fill: false,
- stroke: false,
- strokeWidth: 3,
- strokeColor: 'red',
- strokeOpacity: 0.8
+
+ // Create OL6 snap source and layer with a subtle solid style
+ this._snapSource = new VectorSource();
+ this._snapLayer = new VectorLayer({
+ source: this._snapSource,
+ visible: false,
+ style: new Style({
+ stroke: new Stroke({
+ color: 'rgba(255, 140, 0, 0.7)',
+ width: 1.5
+ }),
+ fill: new Fill({
+ color: 'rgba(255, 140, 0, 0.05)'
+ }),
+ image: new Circle({
+ radius: 4,
+ fill: new Fill({ color: 'rgba(255, 140, 0, 0.4)' }),
+ stroke: new Stroke({ color: 'rgba(255, 140, 0, 0.8)', width: 1 })
+ })
})
});
+ this._snapLayer.setProperties({ name: 'snaplayer' });
- this._lizmap3.map.addLayer(snapLayer);
- this._snapLayer = snapLayer;
+ // Will be added to map once mainLizmap.map is ready
+ this._snapInteraction = null;
+ this._mapReady = false;
- const snapControl = new OpenLayers.Control.Snapping({
- layer: this._edition.editLayer,
- targets: [{
- layer: snapLayer
- }]
- });
- this._lizmap3.map.addControls([snapControl]);
- this._lizmap3.controls['snapControl'] = snapControl;
-
- this._setSnapLayersRefreshable = () => {
- if(this._active){
- this.snapLayersRefreshable = true;
+ // OL6 Snap source feeds off in-memory features; reload them after the
+ // map view moves so off-extent geometry comes into snap range.
+ this._refreshSnapDataOnMoveEnd = () => {
+ if (this._active) {
+ this.getSnappingData();
}
}
@@ -87,7 +93,6 @@ export default class Snapping {
config.snap_enabled = this._snapEnabled;
this.config = config;
- this.snapLayersRefreshable = true;
// dispatch an event, it might be useful to know when the list of visible layer for snap changed
mainEventDispatcher.dispatch('snapping.layer.visibility.changed');
@@ -108,9 +113,19 @@ export default class Snapping {
this._snapLayers = visibleLayers.concat(snapLayers);
}
+ // Ensure snap layer is added to map when available
+ this._ensureMapReady = () => {
+ if (!this._mapReady && mainLizmap.map) {
+ mainLizmap.map.addToolLayer(this._snapLayer);
+ this._mapReady = true;
+ }
+ };
+
// Activate snap when a layer is edited
mainEventDispatcher.addListener(
() => {
+ this._ensureMapReady();
+
// Get snapping configuration for edited layer
for (const editionLayer in this._lizmap3.config.editionLayers) {
if (this._lizmap3.config.editionLayers.hasOwnProperty(editionLayer)) {
@@ -150,21 +165,8 @@ export default class Snapping {
}
if (this._config !== undefined){
- // Configure snapping
- const snapControl = this._lizmap3.controls.snapControl;
-
- // Set edition layer as main layer
- snapControl.setLayer(this._edition.editLayer);
-
- snapControl.targets[0].node = this._config.snap_vertices;
- snapControl.targets[0].vertex = this._config.snap_intersections;
- snapControl.targets[0].edge = this._config.snap_segments;
- snapControl.targets[0].nodeTolerance = this._config.snap_vertices_tolerance;
- snapControl.targets[0].vertexTolerance = this._config.snap_intersections_tolerance;
- snapControl.targets[0].edgeTolerance = this._config.snap_segments_tolerance;
-
// Listen to moveend event and to layers visibility changes to able data refreshing
- this._lizmap3.map.events.register('moveend', this, this._setSnapLayersRefreshable);
+ mainLizmap.map.on('moveend', this._refreshSnapDataOnMoveEnd);
this._rootMapGroup.addListener(
this._setSnapLayersVisibility,
['layer.visibility.changed','group.visibility.changed']
@@ -191,11 +193,13 @@ export default class Snapping {
this._pendingMapReadyListener = null;
}
this.active = false;
- this._snapLayer.destroyFeatures();
+ this._snapSource.clear();
this.config = undefined;
- // Remove listener to moveend event to layers visibility event
- this._lizmap3.map.events.unregister('moveend', this, this._setSnapLayersRefreshable);
+ // Remove listener to moveend event and layers visibility event
+ if (mainLizmap.map) {
+ mainLizmap.map.un('moveend', this._refreshSnapDataOnMoveEnd);
+ }
this._rootMapGroup.removeListener(
this._setSnapLayersVisibility,
['layer.visibility.changed','group.visibility.changed']
@@ -244,97 +248,43 @@ export default class Snapping {
}
}
- /**
- * Log a WFS snap failure and surface a user-visible message, once per refresh batch.
- * Called from both the rejection and the "no valid FeatureCollection" path.
- * @param {string} layerName - the layer whose WFS request failed
- * @param {Error|object|string} detail - the error or payload that triggered the notice
- * @private
- */
- _notifySnapWfsError(layerName, detail) {
- console.warn('Snapping: WFS request failed for layer', layerName, detail);
- if (this._wfsErrorNotified) return;
- this._wfsErrorNotified = true;
- const msg = lizDict['snapping.message.wfs_error']
- || 'Snapping: failed to load data for some layers — snap may be incomplete.';
- this._lizmap3.addMessage(msg, 'error', true, 7000);
- }
-
getSnappingData () {
- // Empty snapping layer first
- this._snapLayer.destroyFeatures();
-
- // Reset the once-per-refresh error notification flag so a new batch of
- // requests can re-surface a user-visible message if WFS still fails.
- this._wfsErrorNotified = false;
+ // Empty snapping source first
+ this._snapSource.clear();
// filter only visible layers and toggled layers on the the snap list
const currentSnapLayers = this._snapLayers.filter(
(layerId) => this._snapEnabled[layerId] && this._snapToggled[layerId]
);
- // Request the map projection so QGIS Server reprojects server-side with full
- // PROJ accuracy (including datum grid shifts). This prevents the ~cm coordinate
- // drift that occurs when OL2 performs a client-side EPSG:4326 → map-projection
- // transform using a simplified Helmert approximation instead of an NTv2 grid.
- const mapProjection = this._lizmap3.map.getProjection();
- const mapExtent = this._restrictToMapExtent ? this._lizmap3.map.getExtent() : null;
- const wfs = new WFS();
- const gFormat = new OpenLayers.Format.GeoJSON({ ignoreExtraDims: true });
+ const mapProjection = mainLizmap.map.getView().getProjection().getCode();
+ // TODO : group async calls with Promises
for (const snapLayer of currentSnapLayers) {
- const layerConfigById = this._lizmap3.getLayerConfigById(snapLayer);
- if (!layerConfigById || !layerConfigById[0]) continue;
-
- const layerName = layerConfigById[0];
- const layerConf = this._lizmap3.config.layers[layerName];
- if (!layerConf) continue;
-
- // Resolve typename (same logic as getVectorLayerWfsUrl)
- let typeName = layerName.split(' ').join('_');
- if (layerConf.hasOwnProperty('shortname') && layerConf['shortname']) typeName = layerConf['shortname'];
- else if (layerConf.hasOwnProperty('typename') && layerConf['typename']) typeName = layerConf['typename'];
-
- const wfsOptions = {
- VERSION: '1.1.0',
- TYPENAME: typeName,
- SRSNAME: mapProjection,
- MAXFEATURES: this._maxFeatures,
- };
- // Apply existing layer filter if present (e.g. login-based filter)
- if (layerConf.hasOwnProperty('request_params') && layerConf['request_params'].hasOwnProperty('filter')) {
- const layerFilter = layerConf['request_params']['filter'];
- if (layerFilter) {
- wfsOptions['EXP_FILTER'] = layerFilter.replace(layerName + ':', '');
- }
- }
+ lizMap.getFeatureData(this._lizmap3.getLayerConfigById(snapLayer)[0], null, null, 'geom', this._restrictToMapExtent, null, this._maxFeatures,
+ (fName, fFilter, fFeatures) => {
- // Append CRS code so the server interprets the extent in the map projection
- if (mapExtent) {
- wfsOptions['BBOX'] = [mapExtent.left, mapExtent.bottom, mapExtent.right, mapExtent.top].join(',') + ',' + mapProjection;
- }
+ // Transform features
+ const snapLayerConfig = lizMap.config.layers[fName];
+ let snapLayerCrs = snapLayerConfig['featureCrs'];
+ if (!snapLayerCrs) {
+ snapLayerCrs = snapLayerConfig['crs'];
+ }
- wfs.getFeature(wfsOptions).then(data => {
- if (!data || !Array.isArray(data.features)) {
- // The WFS endpoint returned something (no rejection) but not a
- // FeatureCollection — most likely an OGC ExceptionReport wrapped as
- // JSON. Treat as a failure so the user sees that snap may be incomplete.
- this._notifySnapWfsError(layerName, data);
- return;
- }
- // Features are already in map projection — no client-side reprojection needed.
- const tfeatures = gFormat.read({
- type: 'FeatureCollection',
- features: data.features
+ const gFormat = new GeoJSON();
+ const tfeatures = gFormat.readFeatures(
+ { type: 'FeatureCollection', features: fFeatures },
+ {
+ dataProjection: snapLayerCrs,
+ featureProjection: mapProjection
+ }
+ );
+
+ // Add features
+ this._snapSource.addFeatures(tfeatures);
});
- this._snapLayer.addFeatures(tfeatures);
- }).catch(err => {
- this._notifySnapWfsError(layerName, err);
- });
}
-
- this.snapLayersRefreshable = false;
}
toggle(){
@@ -382,16 +332,6 @@ export default class Snapping {
config.snap_on_layers = this._snapToggled;
this.config = config;
- this.snapLayersRefreshable = true;
- }
-
- get snapLayersRefreshable(){
- return this._snapLayersRefreshable;
- }
-
- set snapLayersRefreshable(refreshable) {
- this._snapLayersRefreshable = refreshable;
- mainEventDispatcher.dispatch('snapping.refreshable');
}
get active() {
@@ -401,22 +341,68 @@ export default class Snapping {
set active(active) {
this._active = active;
- // (de)activate snap control
+ // (de)activate snap interaction
if (this._active) {
this.getSnappingData();
- this._lizmap3.controls.snapControl.activate();
+ this._createSnapInteraction();
} else {
- // Disable refresh button when snapping is inactive
- this.snapLayersRefreshable = false;
- this._lizmap3.controls.snapControl.deactivate();
+ this._removeSnapInteraction();
}
- // Set snap layer visibility
- this._snapLayer.setVisibility(this._active);
+ // Show snap layer when active so users can see snappable features
+ this._snapLayer.setVisible(this._active);
mainEventDispatcher.dispatch('snapping.active');
}
+ /**
+ * Create and add the OL6 Snap interaction to the map
+ * @private
+ */
+ _createSnapInteraction() {
+ this._removeSnapInteraction();
+
+ if (!this._config || !mainLizmap.map) return;
+
+ this._snapInteraction = new Snap({
+ source: this._snapSource,
+ vertex: this._config.snap_vertices || this._config.snap_intersections,
+ edge: this._config.snap_segments,
+ pixelTolerance: Math.max(
+ parseInt(this._config.snap_vertices_tolerance) || 10,
+ parseInt(this._config.snap_segments_tolerance) || 10
+ )
+ });
+
+ mainLizmap.map.addInteraction(this._snapInteraction);
+ }
+
+ /**
+ * Remove the OL6 Snap interaction from the map
+ * @private
+ */
+ _removeSnapInteraction() {
+ if (this._snapInteraction && mainLizmap.map) {
+ mainLizmap.map.removeInteraction(this._snapInteraction);
+ this._snapInteraction = null;
+ }
+ }
+
+ /**
+ * Re-add the Snap interaction so it sits at the top of the map's
+ * interactions list, which means OL processes it *before* whatever
+ * Draw / Modify / Translate was just added by another module.
+ *
+ * Callers (e.g. the Digitizing module) invoke this after adding a new
+ * drawing interaction so snapping continues to work against the latest
+ * interaction in the stack. No-op when snapping is inactive.
+ */
+ reorderSnapInteraction() {
+ if (this._active && mainLizmap.map && this._config) {
+ this._createSnapInteraction();
+ }
+ }
+
get config() {
return this._config;
}
@@ -424,6 +410,11 @@ export default class Snapping {
set config(config) {
this._config = config;
+ // Re-create snap interaction with updated config when active
+ if (this._active && this._config) {
+ this._createSnapInteraction();
+ }
+
mainEventDispatcher.dispatch('snapping.config');
}
}
diff --git a/lizmap/modules/view/locales/en_US/dictionnary.UTF-8.properties b/lizmap/modules/view/locales/en_US/dictionnary.UTF-8.properties
index 3f7d396878..e3a909481a 100644
--- a/lizmap/modules/view/locales/en_US/dictionnary.UTF-8.properties
+++ b/lizmap/modules/view/locales/en_US/dictionnary.UTF-8.properties
@@ -87,6 +87,7 @@ edition.datepicker.reset=Reset the date
edition.confirm.delete=Are you sure you want to delete the selected feature?
edition.confirm.cancel=Are you sure you want to cancel the edition? You will lose any modifications made on the currently edited object.
edition.confirm.restart-drawing=Are you sure you want to delete the existing geometry and redraw it from scratch?
+edition.toolbar.redraw=Redraw the geometry
edition.confirm.paste=Are you sure you want to replace the existing geometry with the one in the clipboard?
edition.geom.paste=Paste the geometry
edition.message.error.fetch.form=Error while fetching the editing form for that layer. Please contact the system administrator.
@@ -249,10 +250,14 @@ digitizing.toolbar.newText=New text
digitizing.toolbar.textRotation=Rotation
digitizing.toolbar.textScale=Size
digitizing.toolbar.import=Import in GeoJSON, GPX, KML, zipped Shapefile or FlatGeobuf
-digitizing.toolbar.edit=Edit (hold "Shift" to select multiple features, hold "Alt" and click to delete a vertex)
-digitizing.toolbar.split=Split the geometry intersecting the drawing line
-digitizing.toolbar.rotate=Rotate the selected geometry
-digitizing.toolbar.scaling=Scale the selected geometry (hold "Shift" to lock proportions)
+digitizing.toolbar.edit=Edit
+digitizing.toolbar.edit.help=Edit vertices. Hold "Shift" to select multiple features, hold "Alt" and click to delete a vertex.
+digitizing.toolbar.split=Split
+digitizing.toolbar.split.help=Draw a line across the geometry to split it into two parts.
+digitizing.toolbar.rotate=Rotate
+digitizing.toolbar.rotate.help=Drag a corner handle to rotate the geometry around its center.
+digitizing.toolbar.scaling=Scale
+digitizing.toolbar.scaling.help=Drag a corner handle to scale the geometry. Hold "Shift" to lock proportions.
digitizing.toolbar.erase=Delete some features
digitizing.toolbar.erase.all=Delete all features
digitizing.confirm.erase=Are you sure you want to delete this feature?
diff --git a/lizmap/modules/view/templates/map.tpl b/lizmap/modules/view/templates/map.tpl
index 76d53085fb..a86c68b23d 100644
--- a/lizmap/modules/view/templates/map.tpl
+++ b/lizmap/modules/view/templates/map.tpl
@@ -51,7 +51,7 @@
{zone 'view~map_minidock', array('repository'=>$repository,'project'=>$project,'dockable'=>$minidockable)}
diff --git a/lizmap/modules/view/templates/map_edition.tpl b/lizmap/modules/view/templates/map_edition.tpl
index ebfb3f9300..932ca92a8d 100644
--- a/lizmap/modules/view/templates/map_edition.tpl
+++ b/lizmap/modules/view/templates/map_edition.tpl
@@ -35,6 +35,12 @@