@@ -9,6 +9,7 @@ import { mainEventDispatcher } from '../modules/Globals.js';
99import Edition from './Edition.js' ;
1010import { MapLayerLoadStatus , MapRootState } from './state/MapLayer.js' ;
1111import { TreeRootState } from './state/LayerTree.js' ;
12+ import WFS from './WFS.js' ;
1213
1314/**
1415 * @class
@@ -56,6 +57,7 @@ export default class Snapping {
5657 } ) ;
5758
5859 this . _lizmap3 . map . addLayer ( snapLayer ) ;
60+ this . _snapLayer = snapLayer ;
5961
6062 const snapControl = new OpenLayers . Control . Snapping ( {
6163 layer : this . _edition . editLayer ,
@@ -188,7 +190,7 @@ export default class Snapping {
188190 this . _pendingMapReadyListener = null ;
189191 }
190192 this . active = false ;
191- this . _lizmap3 . map . getLayersByName ( 'snaplayer' ) [ 0 ] . destroyFeatures ( ) ;
193+ this . _snapLayer . destroyFeatures ( ) ;
192194 this . config = undefined ;
193195
194196 // Remove listener to moveend event to layers visibility event
@@ -243,41 +245,66 @@ export default class Snapping {
243245
244246 getSnappingData ( ) {
245247 // Empty snapping layer first
246- this . _lizmap3 . map . getLayersByName ( 'snaplayer' ) [ 0 ] . destroyFeatures ( ) ;
248+ this . _snapLayer . destroyFeatures ( ) ;
247249
248250 // filter only visible layers and toggled layers on the the snap list
249251 const currentSnapLayers = this . _snapLayers . filter (
250252 ( layerId ) => this . _snapEnabled [ layerId ] && this . _snapToggled [ layerId ]
251253 ) ;
252254
253- // TODO : group aync calls with Promises
254- for ( const snapLayer of currentSnapLayers ) {
255-
256- lizMap . getFeatureData ( this . _lizmap3 . getLayerConfigById ( snapLayer ) [ 0 ] , null , null , 'geom' , this . _restrictToMapExtent , null , this . _maxFeatures ,
257- ( fName , fFilter , fFeatures ) => {
255+ // Request the map projection so QGIS Server reprojects server-side with full
256+ // PROJ accuracy (including datum grid shifts). This prevents the ~cm coordinate
257+ // drift that occurs when OL2 performs a client-side EPSG:4326 → map-projection
258+ // transform using a simplified Helmert approximation instead of an NTv2 grid.
259+ const mapProjection = this . _lizmap3 . map . getProjection ( ) ;
260+ const mapExtent = this . _restrictToMapExtent ? this . _lizmap3 . map . getExtent ( ) : null ;
261+ const wfs = new WFS ( ) ;
262+ const gFormat = new OpenLayers . Format . GeoJSON ( { ignoreExtraDims : true } ) ;
258263
259- // Transform features
260- const snapLayerConfig = lizMap . config . layers [ fName ] ;
261- let snapLayerCrs = snapLayerConfig [ 'featureCrs' ] ;
262- if ( ! snapLayerCrs ) {
263- snapLayerCrs = snapLayerConfig [ 'crs' ] ;
264- }
264+ for ( const snapLayer of currentSnapLayers ) {
265+ const layerConfigById = this . _lizmap3 . getLayerConfigById ( snapLayer ) ;
266+ if ( ! layerConfigById || ! layerConfigById [ 0 ] ) continue ;
267+
268+ const layerName = layerConfigById [ 0 ] ;
269+ const layerConf = this . _lizmap3 . config . layers [ layerName ] ;
270+ if ( ! layerConf ) continue ;
271+
272+ // Resolve typename (same logic as getVectorLayerWfsUrl)
273+ let typeName = layerName . split ( ' ' ) . join ( '_' ) ;
274+ if ( layerConf . hasOwnProperty ( 'shortname' ) && layerConf [ 'shortname' ] ) typeName = layerConf [ 'shortname' ] ;
275+ else if ( layerConf . hasOwnProperty ( 'typename' ) && layerConf [ 'typename' ] ) typeName = layerConf [ 'typename' ] ;
276+
277+ const wfsOptions = {
278+ VERSION : '1.1.0' ,
279+ TYPENAME : typeName ,
280+ SRSNAME : mapProjection ,
281+ MAXFEATURES : this . _maxFeatures ,
282+ } ;
265283
266- // TODO : use OL 6 instead ?
267- const gFormat = new OpenLayers . Format . GeoJSON ( {
268- ignoreExtraDims : true ,
269- externalProjection : snapLayerCrs ,
270- internalProjection : this . _lizmap3 . map . getProjection ( )
271- } ) ;
284+ // Apply existing layer filter if present (e.g. login-based filter)
285+ if ( layerConf . hasOwnProperty ( 'request_params' ) && layerConf [ 'request_params' ] . hasOwnProperty ( 'filter' ) ) {
286+ const layerFilter = layerConf [ 'request_params' ] [ 'filter' ] ;
287+ if ( layerFilter ) {
288+ wfsOptions [ 'EXP_FILTER' ] = layerFilter . replace ( layerName + ':' , '' ) ;
289+ }
290+ }
272291
273- const tfeatures = gFormat . read ( {
274- type : 'FeatureCollection' ,
275- features : fFeatures
276- } ) ;
292+ // Append CRS code so the server interprets the extent in the map projection
293+ if ( mapExtent ) {
294+ wfsOptions [ 'BBOX' ] = [ mapExtent . left , mapExtent . bottom , mapExtent . right , mapExtent . top ] . join ( ',' ) + ',' + mapProjection ;
295+ }
277296
278- // Add features
279- this . _lizmap3 . map . getLayersByName ( 'snaplayer' ) [ 0 ] . addFeatures ( tfeatures ) ;
297+ wfs . getFeature ( wfsOptions ) . then ( data => {
298+ if ( ! data || ! Array . isArray ( data . features ) ) return ;
299+ // Features are already in map projection — no client-side reprojection needed.
300+ const tfeatures = gFormat . read ( {
301+ type : 'FeatureCollection' ,
302+ features : data . features
280303 } ) ;
304+ this . _snapLayer . addFeatures ( tfeatures ) ;
305+ } ) . catch ( err => {
306+ console . warn ( 'Snapping: WFS request failed for layer' , layerName , err ) ;
307+ } ) ;
281308 }
282309
283310 this . snapLayersRefreshable = false ;
@@ -358,7 +385,7 @@ export default class Snapping {
358385 }
359386
360387 // Set snap layer visibility
361- this . _lizmap3 . map . getLayersByName ( 'snaplayer' ) [ 0 ] . setVisibility ( this . _active ) ;
388+ this . _snapLayer . setVisibility ( this . _active ) ;
362389
363390 mainEventDispatcher . dispatch ( 'snapping.active' ) ;
364391 }
0 commit comments