import GeoJSON from 'ol/format/GeoJSON'
import { bbox as bboxStrategy, tile as tileStrategy } from 'ol/loadingstrategy'
import VectorSource from 'ol/source/Vector'
import { createXYZ } from 'ol/tilegrid'
import * as MapviewerServices from '../tools/mapviewer-services'
import isEmpty from 'lodash/isEmpty'
/**
* @param {Object?} wfsOptions Option de chargement des données via un service wfs
* @param {String} wfsOptions.url Url du service wfs
* @param {string} wfsOptions.geometryName nom du champs geométrie du service (défaut geo)
* @param {Object} wfsOptions.params paramètre du service wfs
* @param {string} wfs.params.typename Nom de la couche du service WFS à exploiter
* @param {string} [wfsOptions.params.srsname=EPSG:3857] projection de retour des données
* @param {string} [wfsOptions.params.version=1.1.1] version du service WFS
* @param {string} wfsOptions.params.* autres paramètres du service WFS
* @param {string} [wfsOptions.projection=EPSG:3857] projection de la bbox pour interroger le service
* @param {string} [wfsOptions.geometryName=geometry] nom de la géométrie pour interroger la bbox
* @param {boolean} [wfsOptions.bboxStrategy=false] Utiliser une stratégie de chargement par bbox (true) ou par tuile (false). Par défaut false
* @param {boolean | Object} [wfsOptions.tileStrategy=false] Utiliser une stratégie de chargemen par tuile (false). Par défaut true
* @param {boolean | Object} [wfsOptions.tileStrategy.tileSize=256] Taille des tuiles pour la stratégie par tuile
* @param {string} wfsOptions.cqlFilter filtre cql
* @param {Object<string,(string|Array)>} wfsOptions.filters objet represant des filtres a générer (exemple {code_insee:[9420,8521]}). Ignoré si cqlFilter est défini
* @param {import('../services/identity-manager.js').identityOptions | String} [wfsOptions.identity] options de gestion d'identité
* @param {Object} [context] contexte d'exécution
* @param {import('../services/identity-manager-pool.js').default} [context.identityManagerPool] pool de gestion d'identité
* @param {String} [context.resourceType='layer'] type de resource associée
* @param {String} [context.resourceId=''] identifiant de resource associée
* @returns VectorSource | Null
* @example
* // Bearer token via identity
* getWfsVectorSource({
* url: 'https://my-geoserver/wfs',
* params: { typename: 'my:layer' },
* identity: {
* type: 'TOKEN',
* token: 'abc123',
* apply: { type: 'bearer' },
* },
* }, {}, {
* identityManagerPool,
* resourceType: 'layer',
* resourceId: 'my-layer-id',
* })
* @example
* // Multiple headers via identity
* getWfsVectorSource({
* url: 'https://my-geoserver/wfs',
* params: { typename: 'my:layer' },
* identity: {
* type: 'IDENTITY',
* identity: {
* Authorization: 'Bearer abc123',
* 'X-Api-Key': 'my-key',
* },
* apply: { type: 'headers' },
* },
* }, {}, {
* identityManagerPool,
* resourceType: 'layer',
* resourceId: 'my-layer-id',
* })
*/
export function getWfsVectorSource (wfsOptions, otherSourceOptions = {}, context = {}) {
if (!wfsOptions) {
return null
}
const options = {
projection: 'EPSG:3857', // format d'interrogation de la l'etendue des données
geometryName: 'geometry', // nom de la géométrie par défaut si rien n'est demandé
...wfsOptions,
params: {
version: '1.1.0', // 1.1.0 si rien n'est demandé
request: 'GetFeature', // GetFeature si rien n'est demandé
srsname: 'EPSG:3857', // format de retour des données
// projection: 'EPSG:3857', // format d'interrogation de la l'etendue des données
// geometryName: 'geometry', // nom de la géométrie par défaut si rien n'est demandé
...wfsOptions.params,
outputFormat: 'application/json',
},
}
if (isEmpty(options.geometryName)) {
throw new Error('paramètre options.wfs.geometryName manquant')
}
const wfsUrl = options.url + (options.url.indexOf('?') > -1 ? '&' : '?') +
Object.entries(options.params).map(([param, value]) => `${param}=${value}`).join('&')
// Gestion des filtres
let filters = options?.cqlFilter
? options.cqlFilter
: options.filters
? Object.entries(options.filters || {}).map(([property, value]) => {
if (Array.isArray(value)) {
// in
value = value.map(val => (typeof val === 'string' ? `'${val}'` : val)).join(',')
return `${property} in (${value})`
} else {
// Equal
value = typeof value === 'string' ? `'${value}'` : value
return `${property}=${value}`
}
}).join(' AND ')
: null
filters = filters !== null && !isEmpty(filters) ? ` AND ${filters}` : ''
// pour le wfs, si on a certaine options on passe par un loader personnalisé, sinon undefined donne le loader par défaut
function getWfsLoader () {
const hasLegacyHeaders = !!options?.headers
const canUseIdentityPool = !!context?.identityManagerPool
const hasIdentity = canUseIdentityPool && (!!options?.identity || hasLegacyHeaders)
if (!hasIdentity) {
if (hasLegacyHeaders && !canUseIdentityPool) {
console.warn('[datalayer-getWfsLoader] options.headers ignoré: identityManagerPool manquant')
}
return undefined
}
let identityManagerPromise = null
const getLegacyIdentityFromHeaders = () => {
if (!hasLegacyHeaders) {
return null
}
const headerEntries = Object.entries(options.headers || {}).filter(([, value]) => value !== undefined && value !== null)
if (headerEntries.length === 0) {
return null
}
const headersIdentity = Object.fromEntries(
headerEntries.map(([key, value]) => [key, String(value)]),
)
return {
type: 'IDENTITY',
identity: headersIdentity,
apply: { type: 'headers' },
}
}
const getIdentityManager = async () => {
if (!identityManagerPromise) {
const identityOptions = options.identity || getLegacyIdentityFromHeaders()
if (!identityOptions) {
return null
}
identityManagerPromise = context.identityManagerPool.addIdentitySource(
context.resourceType || 'layer',
context.resourceId || '',
identityOptions,
)
}
return identityManagerPromise
}
return async function (extent, resolution, projection, success, failure) {
const url = typeof this.getUrl() === 'function' ? this.getUrl()(extent, resolution, projection) : this.getUrl()
try {
const identityManager = await getIdentityManager()
const request = identityManager ? await identityManager.getRequest(url) : new Request(url)
const response = await fetch(request)
if (!response.ok) {
throw new Error(`Response status: ${response.status}`)
}
const json = await response.json()
// le wfs service renvoi du geojson, on a juste à utiliser le service afin de les convertir en features
const features = MapviewerServices.geoJsonToFeature(json, options.params.srsname, projection.getCode())
this.addFeatures(features)
success(features)
} catch (error) {
console.error('[datalayer-getWfsLoader]', error)
this.removeLoadedExtent(extent)
// si on est pas dans le scope on peut faire ceci:
failure()
}
}
}
return new VectorSource({
format: new GeoJSON(),
url: function (extent, resolution, projection) {
const dataProjection = options.projection
const extentTransformed = MapviewerServices.convertExtent(extent, projection.getCode(), dataProjection)
return `${wfsUrl}&cql_filter=BBOX(${options.geometryName},${extentTransformed.join(',')},'${dataProjection}')${filters}`
/* 'https://wxs.ign.fr/parcellaire/geoportail/wfs?SERVICE=WFS&' +
'VERSION=2.0.0&request=GetFeature&typename=CADASTRALPARCELS.PARCELLAIRE_EXPRESS:parcelle&' +
'outputFormat=application/json&srsname=EPSG:3857&' +
'bbox=' +
extent.join(',') +
',EPSG:3857' */
},
strategy: options?.bboxStrategy ? bboxStrategy : tileStrategy(createXYZ({ tileSize: options?.tileStrategy?.tileSize || 256 })),
loader: getWfsLoader(),
...otherSourceOptions,
})
}