Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 20 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ Si `GPF_WFS_MINISEARCH_OPTIONS` est absent ou vide, les options par défaut rest

Remarque :

- Les outils `gpf_wfs_list_types`, `gpf_wfs_search_types` et `gpf_wfs_describe_type` s'appuient sur un catalogue de schémas embarqué fourni par `@ignfab/gpf-schema-store`.
- Les outils `gpf_wfs_search_types` et `gpf_wfs_describe_type` s'appuient sur un catalogue de schémas embarqué fourni par `@ignfab/gpf-schema-store`.
- L'outil `gpf_wfs_get_features` interroge toujours le service WFS de la Géoplateforme en direct.
- Le catalogue embarqué améliore la description des featureTypes mais il peut être légèrement décalé par rapport à l'état courant du WFS.

Expand Down Expand Up @@ -177,12 +177,13 @@ L'idée est ici de répondre à des précises en traitant côté serveur les app

* [assiette_sup(lon,lat)](src/tools/AssietteSupTool.ts) permet de **récupérer les Servitude d'Utilité Publiques (SUP)**

Les tools WFS orientés "objet" (`adminexpress`, `cadastre`, `urbanisme`, `assiette_sup`) exposent un `feature_ref { typename, feature_id }` quand l'objet source est réutilisable tel quel dans un appel ultérieur à `gpf_wfs_get_features`, notamment avec `spatial_operator="intersects_feature"`.

### Explorer les données vecteurs

#### Explorer les tables

* [gpf_wfs_list_types()](src/tools/GpfWfsListTypesTool.ts) pour **lister de façon exhaustive les types WFS connus du catalogue de schémas embarqué**. Cet outil est surtout utile pour un inventaire complet ou une exploration globale du catalogue ; pour trouver rapidement un type pertinent, préférer `gpf_wfs_search_types`.
* [gpf_wfs_search_types(keywords,max_results=10)](src/tools/GpfWfsSearchTypesTool.ts) pour **rechercher un type WFS pertinent à partir de mots-clés et obtenir un `typename` valide**. La recherche est textuelle, configurable via `GPF_WFS_MINISEARCH_OPTIONS`, et renvoie aussi un score de pertinence quand il est disponible.
* [gpf_wfs_search_types(keywords,max_results=10)](src/tools/GpfWfsSearchTypesTool.ts) pour **rechercher un type WFS pertinent à partir de mots-clés et obtenir un `typename` valide**. La recherche est textuelle et configurable via `GPF_WFS_MINISEARCH_OPTIONS`.

> - Quels sont les millésimes ADMINEXPRESS disponibles sur la Géoplateforme?
> - Quelle est la table de la BDTOPO correspondant aux bâtiments?
Expand All @@ -197,7 +198,22 @@ L'idée est ici de répondre à des précises en traitant côté serveur les app

#### Explorer les données des tables

* [gpf_wfs_get_features(typename,...)](src/tools/GpfWfsGetFeaturesTool.ts) pour **récupérer les données d'une table** depuis le service WFS de la Géoplateforme ([GetFeature](https://data.geopf.fr/wfs/ows?service=WFS&version=2.0.0&request=GetFeature&typename=ADMINEXPRESS-COG.LATEST:commune&outputFormat=application/json&count=1))
* [gpf_wfs_get_features(typename,...)](src/tools/GpfWfsGetFeaturesTool.ts) pour **récupérer les données d'une table** depuis le service WFS de la Géoplateforme sans écrire de CQL à la main.

Le tool accepte un contrat structuré :

- `select` pour choisir les propriétés à renvoyer
- `where` pour filtrer les objets
- `order_by` pour trier les résultats
- `spatial_operator` et ses paramètres dédiés pour le spatial en `lon/lat`
- `result_type="request"` pour récupérer la requête compilée en `POST`, ainsi qu'une `get_url` dérivée quand elle reste raisonnablement portable en GET

Exemples :

- `where=[{ property: "code_insee", operator: "eq", value: "25000" }]`
- `spatial_operator="bbox"` avec `bbox_west`, `bbox_south`, `bbox_east`, `bbox_north`
- `spatial_operator="dwithin_point"` avec `dwithin_lon`, `dwithin_lat`, `dwithin_distance_m`
- `spatial_operator="intersects_feature"` avec `intersects_feature_typename` et `intersects_feature_id` issus d'une `feature_ref`

> - Quelles sont les 5 communes les plus peuplées du Doubs (25)?
> - Combien y-a-t'il de bâtiments à moins de 5 km de la tour Eiffel?
Expand Down Expand Up @@ -231,7 +247,6 @@ N'hésitez pas :
mcp add tool gpf_wmts_layers
```

* [@camptocamp/ogc-client](https://camptocamp.github.io/ogc-client/#/) pour la **lecture des réponses XML des services WFS, WMTS,...**
* [@ignfab/gpf-schema-store](https://www.npmjs.com/package/@ignfab/gpf-schema-store) pour le **catalogue de schémas embarqué** utilisé par les outils d'exploration WFS.
* [MiniSearch](https://github.com/lucaong/minisearch) pour la **recherche par mot clé** utilisée dans `@ignfab/gpf-schema-store`.
* [jsts](https://bjornharrtell.github.io/jsts/) pour les **traitements géométriques** (ex : tri des réponses par distance au point recherché).
Expand Down
39 changes: 19 additions & 20 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 1 addition & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,7 @@
"homepage": "https://github.com/ignfab/geocontext#readme",
"scripts": {
"clean:dist": "rm -rf dist",
"copy:resources": "mkdir -p dist/resources/content && cp -R src/resources/content/. dist/resources/content/",
"build": "npm run clean:dist && tsc && npx mcp-build && npm run copy:resources",
"build": "npm run clean:dist && tsc && npx mcp-build",
"watch": "tsc --watch",
"start": "node dist/index.js",
"test": "node --no-warnings --experimental-vm-modules ./node_modules/jest/bin/jest.js",
Expand All @@ -38,7 +37,6 @@
"fresh": "npm run reset && npm cache verify && npm install && npm run build && npm test"
},
"dependencies": {
"@camptocamp/ogc-client": "^1.3.0",
"@ignfab/gpf-schema-store": "^0.1.3",
"@rgrove/parse-xml": "^4.2.0",
"https-proxy-agent": "^7.0.6",
Expand Down
36 changes: 7 additions & 29 deletions src/gpf/adminexpress.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import _ from 'lodash';

import { fetchJSON } from '../helpers/http.js';
import { fetchWfsFeatures, mapWfsFeature } from '../helpers/wfs.js';
import logger from '../logger.js';

/**
Expand All @@ -27,33 +25,13 @@ export const ADMINEXPRESS_TYPES = [
* @param {(url: string) => Promise<any>} [fetcher]
* @returns {object[]}
*/
export async function getAdminUnits(lon, lat, fetcher = fetchJSON) {
export async function getAdminUnits(lon, lat, fetcher) {
logger.info(`[adminexpress] getAdminUnits(${lon},${lat})...`);

// note that EPSG:4326 means lat,lon order for GeoServer -> flipped coordinates...
const cql_filter = `INTERSECTS(geometrie,Point(${lat} ${lon}))`;

// TODO : avoid useless geometry retrieval at WFS level
const url = 'https://data.geopf.fr/wfs?' + new URLSearchParams({
service: 'WFS',
request: 'GetFeature',
typeName: ADMINEXPRESS_TYPES.map((type) => { return `ADMINEXPRESS-COG.LATEST:${type}` }).join(','),
outputFormat: 'application/json',
cql_filter: cql_filter
}).toString();
// Using EWKT format with SRID=4326 prefix for standard lon,lat order
const cql_filter = `INTERSECTS(geometrie,SRID=4326;POINT(${lon} ${lat}))`;
const typeNames = ADMINEXPRESS_TYPES.map((type) => `ADMINEXPRESS-COG.LATEST:${type}`);

const featureCollection = await fetcher(url);
if (!Array.isArray(featureCollection?.features)) {
throw new Error("Le service ADMINEXPRESS n'a pas retourné de collection d'objets exploitable");
}
return featureCollection.features.map((feature) => {
// parse type from id (ex: "commune.3837")
const type = feature.id.split('.')[0];
// ignore geometry and extend properties
return Object.assign({
type: type,
id: feature.id,
bbox: feature.bbox
}, feature.properties);
});
const features = await fetchWfsFeatures(typeNames, cql_filter, 'ADMINEXPRESS', fetcher);
return features.map((feature) => mapWfsFeature(feature, typeNames));
}
48 changes: 12 additions & 36 deletions src/gpf/parcellaire-express.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import _ from 'lodash';

import distance from '../helpers/distance.js';
import { fetchJSON } from '../helpers/http.js';
import { fetchWfsFeatures, mapWfsFeature, toGeoJsonPoint } from '../helpers/wfs.js';
import logger from '../logger.js';

// CADASTRALPARCELS.PARCELLAIRE_EXPRESS:
Expand Down Expand Up @@ -49,44 +49,20 @@ function filterByDistance(items){
* @param {(url: string) => Promise<any>} [fetcher]
* @returns
*/
export async function getParcellaireExpress(lon, lat, fetcher = fetchJSON) {
export async function getParcellaireExpress(lon, lat, fetcher) {
logger.info(`getParcellaireExpress(${lon},${lat}) ...`);
// note that EPSG:4326 means lat,lon order for GeoServer -> flipped coordinates...
const cql_filter = `DWITHIN(geom,Point(${lat} ${lon}),10,meters)`;
// Using EWKT format with SRID=4326 prefix for standard lon,lat order
const cql_filter = `DWITHIN(geom,SRID=4326;POINT(${lon} ${lat}),10,meters)`;
const typeNames = PARCELLAIRE_EXPRESS_TYPES.map((type) => `CADASTRALPARCELS.PARCELLAIRE_EXPRESS:${type}`);

const sourceGeom = {
"type": "Point",
"coordinates": [lon,lat]
};
const sourceGeom = toGeoJsonPoint(lon, lat);

// TODO : avoid useless geometry retrieval at WFS level
const url = 'https://data.geopf.fr/wfs?' + new URLSearchParams({
service: 'WFS',
request: 'GetFeature',
typeName: PARCELLAIRE_EXPRESS_TYPES.map((type) => { return `CADASTRALPARCELS.PARCELLAIRE_EXPRESS:${type}` }).join(','),
outputFormat: 'application/json',
cql_filter: cql_filter
}).toString();

const featureCollection = await fetcher(url);
if (!Array.isArray(featureCollection?.features)) {
throw new Error("Le service PARCELLAIRE_EXPRESS n'a pas retourné de collection d'objets exploitable");
}
return filterByDistance(featureCollection.features.map((feature) => {
// parse type from id (ex: "commune.3837")
const type = feature.id.split('.')[0];
// ignore geometry and extend properties
return Object.assign({
type: type,
id: feature.id,
bbox: feature.bbox,
distance: distance(
sourceGeom,
feature.geometry
),
source: PARCELLAIRE_EXPRESS_SOURCE,
}, feature.properties);
}));
const features = await fetchWfsFeatures(typeNames, cql_filter, 'PARCELLAIRE_EXPRESS', fetcher);
return filterByDistance(features.map((feature) => ({
...mapWfsFeature(feature, typeNames),
distance: distance(sourceGeom, feature.geometry),
source: PARCELLAIRE_EXPRESS_SOURCE,
})));
}


89 changes: 20 additions & 69 deletions src/gpf/urbanisme.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import distance from "../helpers/distance.js";
import { fetchJSON } from "../helpers/http.js";
import { fetchWfsFeatures, mapWfsFeature, toGeoJsonPoint } from "../helpers/wfs.js";
import logger from "../logger.js";

// https://data.geopf.fr/wfs/ows?service=WFS&version=2.0.0&request=GetCapabilities
Expand All @@ -17,7 +17,6 @@ export const URBANISME_TYPES = [
];

export const URBANISME_SOURCE = "Géoplateforme - (WFS Géoportail de l'Urbanisme)";
const URBANISME_INVALID_COLLECTION_ERROR = "Le service Urbanisme n'a pas retourné de collection d'objets exploitable";

const URBANISME_EXCLUDED_PROPERTIES = new Set([
'gpu_status',
Expand Down Expand Up @@ -47,43 +46,20 @@ function sanitizeUrbanismeItem(item) {
* @param {(url: string) => Promise<any>} [fetcher]
* @returns
*/
export async function getUrbanisme(lon, lat, fetcher = fetchJSON) {
export async function getUrbanisme(lon, lat, fetcher) {
logger.info(`getUrbanisme(${lon},${lat})...`);

// note that EPSG:4326 means lat,lon order for GeoServer -> flipped coordinates...
const cql_filter = `DWITHIN(the_geom,Point(${lat} ${lon}),30,meters)`;
// Using EWKT format with SRID=4326 prefix for standard lon,lat order
const cql_filter = `DWITHIN(the_geom,SRID=4326;POINT(${lon} ${lat}),30,meters)`;

const sourceGeom = {
"type": "Point",
"coordinates": [lon,lat]
};
const sourceGeom = toGeoJsonPoint(lon, lat);

// TODO : avoid useless geometry retrieval at WFS level
const url = 'https://data.geopf.fr/wfs?' + new URLSearchParams({
service: 'WFS',
request: 'GetFeature',
typeName: URBANISME_TYPES.join(','),
outputFormat: 'application/json',
cql_filter: cql_filter
}).toString();

const featureCollection = await fetcher(url);
if (!Array.isArray(featureCollection?.features)) {
throw new Error(URBANISME_INVALID_COLLECTION_ERROR);
}
return featureCollection.features.map((feature) => {
// parse type from id (ex: "commune.3837")
const type = feature.id.split('.')[0];
// ignore geometry and extend properties
const item = Object.assign({
type: type,
id: feature.id,
bbox: feature.bbox,
distance: (distance(
sourceGeom,
feature.geometry
) * 1000.0)
}, feature.properties);
const features = await fetchWfsFeatures(URBANISME_TYPES, cql_filter, 'Urbanisme', fetcher);
return features.map((feature) => {
const item = {
...mapWfsFeature(feature, URBANISME_TYPES),
distance: distance(sourceGeom, feature.geometry) * 1000.0,
};
return sanitizeUrbanismeItem(item);
});
}
Expand All @@ -102,42 +78,17 @@ const ASSIETTES_SUP_TYPES = [
* @param {(url: string) => Promise<any>} [fetcher]
* @returns
*/
export async function getAssiettesServitudes(lon, lat, fetcher = fetchJSON) {
export async function getAssiettesServitudes(lon, lat, fetcher) {
logger.info(`getAssiettesServitudes(${lon},${lat})...`);

// note that EPSG:4326 means lat,lon order for GeoServer -> flipped coordinates...
const cql_filter = `DWITHIN(the_geom,Point(${lat} ${lon}),30,meters)`;

const sourceGeom = {
"type": "Point",
"coordinates": [lon,lat]
};
// Using EWKT format with SRID=4326 prefix for standard lon,lat order
const cql_filter = `DWITHIN(the_geom,SRID=4326;POINT(${lon} ${lat}),30,meters)`;

// TODO : avoid useless geometry retrieval at WFS level
const url = 'https://data.geopf.fr/wfs?' + new URLSearchParams({
service: 'WFS',
request: 'GetFeature',
typeName: ASSIETTES_SUP_TYPES.join(','),
outputFormat: 'application/json',
cql_filter: cql_filter
}).toString();
const sourceGeom = toGeoJsonPoint(lon, lat);

const featureCollection = await fetcher(url);
if (!Array.isArray(featureCollection?.features)) {
throw new Error(URBANISME_INVALID_COLLECTION_ERROR);
}
return featureCollection.features.map((feature) => {
// parse type from id (ex: "commune.3837")
const type = feature.id.split('.')[0];
// ignore geometry and extend properties
return Object.assign({
type: type,
id: feature.id,
bbox: feature.bbox,
distance: (distance(
sourceGeom,
feature.geometry
) * 1000.0)
}, feature.properties);
});
const features = await fetchWfsFeatures(ASSIETTES_SUP_TYPES, cql_filter, 'Urbanisme', fetcher);
return features.map((feature) => ({
...mapWfsFeature(feature, ASSIETTES_SUP_TYPES),
distance: distance(sourceGeom, feature.geometry) * 1000.0,
}));
}
Loading
Loading