Skip to content
Open
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
80 changes: 64 additions & 16 deletions docs/mcp-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -1088,12 +1088,12 @@ Lecture d’objets GPF

```
Interroge un type GPF et renvoie des résultats structurés (propriétés attributaires ; les géométries ne sont pas incluses). Pour obtenir une couche cartographiable, utiliser `gpf_get_features_layer`.
Utiliser `select` pour choisir les propriétés, `where` pour filtrer, `order_by` pour trier et un filtre spatial dédié (`bbox_filter`, `intersects_point_filter`, `dwithin_point_filter`, `intersects_feature_filter` ou `travel_time_filter`) pour le spatial.
Utiliser `select` pour choisir les propriétés, `where` pour filtrer, `order_by` pour trier et un filtre spatial dédié (`bbox_filter`, `intersects_point_filter`, `dwithin_point_filter`, `intersects_feature_filter`, `adjacent_feature_filter` ou `travel_time_filter`) pour le spatial.
Exemple attributaire : `where=[{ property: "code_insee", operator: "eq", value: "75056" }]`.
Exemple bbox : `bbox_filter={ west: 2.1, south: 48.7, east: 2.5, north: 48.9 }`.
Exemple point dans géométrie : `intersects_point_filter={ lon: 2.35, lat: 48.85 }`.
Exemple distance : `dwithin_point_filter={ lon: 2.35, lat: 48.85, distance_m: 500 }`.
Exemple réutilisation : `intersects_feature_filter={ typename, feature_id }` avec `typename` et `feature_id` issus d'une `feature_ref`.
Exemple réutilisation : `intersects_feature_filter={ typename, feature_id }` ou bien `adjacent_feature_filter={ feature_id }` avec `typename` et `feature_id` issus d'une `feature_ref`.
Exemple temps de trajet : `travel_time_filter={ lon: 2.35, lat: 48.85, minutes: 15, profile: "pedestrian" }` pour les objets atteignables en 15 minutes à pied depuis ce point.
⚠️ Quand `typename` et `intersects_feature_filter.typename` sont identiques, utiliser `gpf_get_feature_by_id` pour récupérer exactement l'objet ciblé.
**OBLIGATOIRE : toujours appeler `gpf_describe_type` avant ce tool, sauf si `gpf_describe_type` a déjà été appelé pour ce même typename dans la conversation en cours.**
Expand All @@ -1104,9 +1104,10 @@ Les noms de propriétés **ne peuvent pas être devinés** : ils sont spécifiqu

| Champ | Type | Requis | Description |
| --- | --- | --- | --- |
| `adjacent_feature_filter` | object | non | Filtre spatial par adjacence avec un objet GPF de référence. Exclusif avec les autres filtres spatiaux. |
| `bbox_filter` | object | non | Filtre spatial par boîte englobante. Exclusif avec les autres filtres spatiaux. |
| `dwithin_point_filter` | object | non | Filtre spatial par distance à un point. Exclusif avec les autres filtres spatiaux. |
| `intersects_feature_filter` | object | non | Filtre spatial par intersection avec un feature GPF de référence. Exclusif avec les autres filtres spatiaux. |
| `intersects_feature_filter` | object | non | Filtre spatial par intersection avec un objet GPF de référence. Exclusif avec les autres filtres spatiaux. |
| `intersects_point_filter` | object | non | Filtre spatial par intersection avec un point. Exclusif avec les autres filtres spatiaux. |
| `limit` | integer | non | Nombre maximum d'objets à renvoyer. Valeur par défaut : 100. Maximum : 5000. Valeur par défaut : 100. |
| `order_by` | array | non | Liste ordonnée des critères de tri. |
Expand Down Expand Up @@ -1281,20 +1282,35 @@ Les noms de propriétés **ne peuvent pas être devinés** : ils sont spécifiqu
"typename": {
"type": "string",
"minLength": 1,
"description": "Type GPF du feature de référence."
"description": "Type GPF de l'objet de référence."
},
"feature_id": {
"type": "string",
"minLength": 1,
"description": "Identifiant du feature de référence."
"description": "Identifiant de l'objet de référence."
}
},
"required": [
"typename",
"feature_id"
],
"additionalProperties": false,
"description": "Filtre spatial par intersection avec un feature GPF de référence. Exclusif avec les autres filtres spatiaux."
"description": "Filtre spatial par intersection avec un objet GPF de référence. Exclusif avec les autres filtres spatiaux."
},
"adjacent_feature_filter": {
"type": "object",
"properties": {
"feature_id": {
"type": "string",
"minLength": 1,
"description": "Identifiant de l'objet de référence."
}
},
"required": [
"feature_id"
],
"additionalProperties": false,
"description": "Filtre spatial par adjacence avec un objet GPF de référence. Exclusif avec les autres filtres spatiaux."
},
"travel_time_filter": {
"type": "object",
Expand Down Expand Up @@ -1422,17 +1438,18 @@ Couche cartographiable d’objets GPF
```
Interroge un type GPF et renvoie une **URL de couche cartographiable** (`data_url`) : une URL opaque, à passer telle quelle à un outil d'affichage cartographique (MCP Carto, ...). L'ouvrir renvoie une FeatureCollection GeoJSON avec les géométries complètes.
À utiliser dès qu'il faut **afficher / cartographier** des objets GPF. Pour des attributs sans géométrie, utiliser `gpf_get_features`.
Mêmes filtres que `gpf_get_features` : `select` pour choisir les propriétés, `where` pour filtrer, `order_by` pour trier et un filtre spatial dédié (`bbox_filter`, `intersects_point_filter`, `dwithin_point_filter`, `intersects_feature_filter` ou `travel_time_filter`) pour le spatial.
Mêmes filtres que `gpf_get_features` : `select` pour choisir les propriétés, `where` pour filtrer, `order_by` pour trier et un filtre spatial dédié (`bbox_filter`, `intersects_point_filter`, `dwithin_point_filter`, `intersects_feature_filter`, `adjacent_feature_filter` ou `travel_time_filter`) pour le spatial.
**OBLIGATOIRE : toujours appeler `gpf_describe_type` avant ce tool, sauf si `gpf_describe_type` a déjà été appelé pour ce même typename dans la conversation en cours.** Les noms de propriétés ne peuvent pas être devinés.
```

### Schéma d’entrée

| Champ | Type | Requis | Description |
| --- | --- | --- | --- |
| `adjacent_feature_filter` | object | non | Filtre spatial par adjacence avec un objet GPF de référence. Exclusif avec les autres filtres spatiaux. |
| `bbox_filter` | object | non | Filtre spatial par boîte englobante. Exclusif avec les autres filtres spatiaux. |
| `dwithin_point_filter` | object | non | Filtre spatial par distance à un point. Exclusif avec les autres filtres spatiaux. |
| `intersects_feature_filter` | object | non | Filtre spatial par intersection avec un feature GPF de référence. Exclusif avec les autres filtres spatiaux. |
| `intersects_feature_filter` | object | non | Filtre spatial par intersection avec un objet GPF de référence. Exclusif avec les autres filtres spatiaux. |
| `intersects_point_filter` | object | non | Filtre spatial par intersection avec un point. Exclusif avec les autres filtres spatiaux. |
| `limit` | integer | non | Nombre maximum d'objets à cartographier. Valeur par défaut : 5000 (plafond du service). Réduire pour alléger la carte. Maximum : 5000. Une requête produisant plus de 5000 objets sera tronquée. Valeur par défaut : 5000. |
| `order_by` | array | non | Liste ordonnée des critères de tri. |
Expand Down Expand Up @@ -1606,20 +1623,35 @@ Mêmes filtres que `gpf_get_features` : `select` pour choisir les propriétés,
"typename": {
"type": "string",
"minLength": 1,
"description": "Type GPF du feature de référence."
"description": "Type GPF de l'objet de référence."
},
"feature_id": {
"type": "string",
"minLength": 1,
"description": "Identifiant du feature de référence."
"description": "Identifiant de l'objet de référence."
}
},
"required": [
"typename",
"feature_id"
],
"additionalProperties": false,
"description": "Filtre spatial par intersection avec un feature GPF de référence. Exclusif avec les autres filtres spatiaux."
"description": "Filtre spatial par intersection avec un objet GPF de référence. Exclusif avec les autres filtres spatiaux."
},
"adjacent_feature_filter": {
"type": "object",
"properties": {
"feature_id": {
"type": "string",
"minLength": 1,
"description": "Identifiant de l'objet de référence."
}
},
"required": [
"feature_id"
],
"additionalProperties": false,
"description": "Filtre spatial par adjacence avec un objet GPF de référence. Exclusif avec les autres filtres spatiaux."
},
"travel_time_filter": {
"type": "object",
Expand Down Expand Up @@ -1753,7 +1785,7 @@ Décompte d’objets GPF

```
Interroge un type GPF et renvoie le nombre de résultats obtenus.
Utiliser `where` pour filtrer et un filtre spatial dédié (`bbox_filter`, `intersects_point_filter`, `dwithin_point_filter`, `intersects_feature_filter` ou `travel_time_filter`) pour le spatial.
Utiliser `where` pour filtrer et un filtre spatial dédié (`bbox_filter`, `intersects_point_filter`, `dwithin_point_filter`, `intersects_feature_filter`, `adjacent_feature_filter` ou `travel_time_filter`) pour le spatial.
Exemple attributaire : `where=[{ property: "code_insee", operator: "eq", value: "75056" }]`.
Exemple bbox : `bbox_filter={ west: 2.1, south: 48.7, east: 2.5, north: 48.9 }`.
Exemple point dans géométrie : `intersects_point_filter={ lon: 2.35, lat: 48.85 }`.
Expand All @@ -1769,9 +1801,10 @@ Les noms de propriétés utilisés dans `where` **ne peuvent pas être devinés*

| Champ | Type | Requis | Description |
| --- | --- | --- | --- |
| `adjacent_feature_filter` | object | non | Filtre spatial par adjacence avec un objet GPF de référence. Exclusif avec les autres filtres spatiaux. |
| `bbox_filter` | object | non | Filtre spatial par boîte englobante. Exclusif avec les autres filtres spatiaux. |
| `dwithin_point_filter` | object | non | Filtre spatial par distance à un point. Exclusif avec les autres filtres spatiaux. |
| `intersects_feature_filter` | object | non | Filtre spatial par intersection avec un feature GPF de référence. Exclusif avec les autres filtres spatiaux. |
| `intersects_feature_filter` | object | non | Filtre spatial par intersection avec un objet GPF de référence. Exclusif avec les autres filtres spatiaux. |
| `intersects_point_filter` | object | non | Filtre spatial par intersection avec un point. Exclusif avec les autres filtres spatiaux. |
| `travel_time_filter` | object | non | Filtre spatial par temps de trajet depuis un point (`profile` voiture ou piéton). Exclusif avec les autres filtres spatiaux. |
| `typename` | string | oui | Nom exact du type GPF à interroger de la forme `prefixe:nom`. Utiliser `gpf_search_types` pour trouver un `typename` valide. |
Expand Down Expand Up @@ -1933,20 +1966,35 @@ Les noms de propriétés utilisés dans `where` **ne peuvent pas être devinés*
"typename": {
"type": "string",
"minLength": 1,
"description": "Type GPF du feature de référence."
"description": "Type GPF de l'objet de référence."
},
"feature_id": {
"type": "string",
"minLength": 1,
"description": "Identifiant du feature de référence."
"description": "Identifiant de l'objet de référence."
}
},
"required": [
"typename",
"feature_id"
],
"additionalProperties": false,
"description": "Filtre spatial par intersection avec un feature GPF de référence. Exclusif avec les autres filtres spatiaux."
"description": "Filtre spatial par intersection avec un objet GPF de référence. Exclusif avec les autres filtres spatiaux."
},
"adjacent_feature_filter": {
"type": "object",
"properties": {
"feature_id": {
"type": "string",
"minLength": 1,
"description": "Identifiant de l'objet de référence."
}
},
"required": [
"feature_id"
],
"additionalProperties": false,
"description": "Filtre spatial par adjacence avec un objet GPF de référence. Exclusif avec les autres filtres spatiaux."
},
"travel_time_filter": {
"type": "object",
Expand Down
2 changes: 1 addition & 1 deletion src/tools/GpfGetFeaturesTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class GpfGetFeaturesTool extends BaseTool<GpfGetFeaturesInput> {
"Exemple bbox : `bbox_filter={ west: 2.1, south: 48.7, east: 2.5, north: 48.9 }`.",
"Exemple point dans géométrie : `intersects_point_filter={ lon: 2.35, lat: 48.85 }`.",
"Exemple distance : `dwithin_point_filter={ lon: 2.35, lat: 48.85, distance_m: 500 }`.",
"Exemple réutilisation : `intersects_feature_filter={ typename, feature_id }` avec `typename` et `feature_id` issus d'une `feature_ref`.",
"Exemple réutilisation : `intersects_feature_filter={ typename, feature_id }` ou bien `adjacent_feature_filter={ feature_id }` avec `typename` et `feature_id` issus d'une `feature_ref`.",
"Exemple temps de trajet : `travel_time_filter={ lon: 2.35, lat: 48.85, minutes: 15, profile: \"pedestrian\" }` pour les objets atteignables en 15 minutes à pied depuis ce point.",
"⚠️ Quand `typename` et `intersects_feature_filter.typename` sont identiques, utiliser `gpf_get_feature_by_id` pour récupérer exactement l'objet ciblé.",
"**OBLIGATOIRE : toujours appeler `gpf_describe_type` avant ce tool, sauf si `gpf_describe_type` a déjà été appelé pour ce même typename dans la conversation en cours.**",
Expand Down
19 changes: 11 additions & 8 deletions src/wfs/features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,30 +58,33 @@ export function ensureIntersectsFeatureTargetsOtherTypename(
input.typename === spatialFilter.typename
) {
throw new Error(
"Le filtre `intersects_feature` sur le même `typename` retourne potentiellement plusieurs objets. " +
"Utiliser `gpf_get_feature_by_id` avec `{ typename, feature_id: intersects_feature_filter.feature_id }` pour cibler exactement un objet.",
"Le filtre `intersects_feature` ne peut pas être utilisé sur le même `typename`. " +
"Utiliser `gpf_get_feature_by_id` avec `{ typename, feature_id: intersects_feature_filter.feature_id }` pour cibler exactement un objet. " +
"Alternativement, utiliser le filtre `adjacent_feature_filter` pour obtenir les objets adjacents.",
);
}
}

// --- Reference Geometry ---

/**
* Resolves the geometry of a reference feature when `intersects_feature` is used.
* Resolves the geometry of a reference feature when `intersects_feature` or `adjacent_feature` are used.
*
* @param input Normalized tool input.
* @returns The resolved reference geometry, or `undefined` when no reference feature is needed.
*/
export async function resolveIntersectsFeatureGeometry(
export async function resolveFeatureFilterGeometry(
input: GpfQueryFeaturesInput,
): Promise<Geometry | undefined> {
const spatialFilter = getSpatialFilter(input);
if (!spatialFilter || spatialFilter.operator !== "intersects_feature") {
if (!spatialFilter || (spatialFilter.operator !== "intersects_feature" && spatialFilter.operator !== "adjacent_feature")) {
return undefined;
}

const typename = spatialFilter.operator == "intersects_feature" ? spatialFilter.typename : input.typename;

return resolveFeatureGeometry(wfsClient, {
typename: spatialFilter.typename,
typename: typename,
feature_id: spatialFilter.feature_id,
});
}
Expand Down Expand Up @@ -121,7 +124,8 @@ export async function resolveSpatialFilterGeometry(

switch (spatialFilter?.operator) {
case "intersects_feature":
return resolveIntersectsFeatureGeometry(input);
case "adjacent_feature":
return resolveFeatureFilterGeometry(input);
case "travel_time":
return resolveTravelTimeGeometry(input);
default:
Expand All @@ -144,7 +148,6 @@ export async function resolveSpatialFilterGeometry(
export async function prepareQueryFeaturesRequest(
input: GpfQueryFeaturesInput
): Promise<PreparedGetFeaturesRequest> {
// TODO: Assess if this guard does not prevent legitimate use cases.
ensureIntersectsFeatureTargetsOtherTypename(input);
// Get the feature type definition from the embedded catalog to access
// property definitions and the geometry column name.
Expand Down
4 changes: 2 additions & 2 deletions src/wfs/geometry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* GeoJSON geometry serialization helpers for the structured WFS engine.
*
* This module converts GeoJSON-like geometries to EWKT so they can be reused
* in spatial CQL predicates such as `intersects_feature`.
* in spatial CQL predicates such as `intersects_feature` or `adjacent_feature`.
*/

import { Geometry } from "geojson";
Expand Down Expand Up @@ -44,6 +44,6 @@ export function geometryToEwkt(geometry: Geometry) {
case "MultiPolygon":
return `SRID=4326;MULTIPOLYGON(${(geometry.coordinates as [number, number][][][]).map((polygon) => `(${polygon.map((ring) => `(${ring.map(positionToWkt).join(",")})`).join(",")})`).join(",")})`;
default:
throw new Error(`Le type de géométrie '${geometry.type}' n'est pas supporté pour \`intersects_feature\`.`);
throw new Error(`Le type de géométrie '${geometry.type}' n'est pas supporté pour \`intersects_feature\` et \`adjacent_feature\`.`);
}
}
31 changes: 22 additions & 9 deletions src/wfs/queryPreparation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
compileBboxSpatialFilter,
compileDwithinSpatialFilter,
compileIntersectsFeatureSpatialFilter,
compileAdjacentFeatureSpatialFilter,
compileIntersectsPointSpatialFilter,
} from "./spatialCql.js";
Comment thread
LionelZoubritzky-IGN marked this conversation as resolved.
import { Geometry } from "geojson";
Expand Down Expand Up @@ -172,12 +173,23 @@ function compileOrderByClause(featureType: GpfFeatureType, clause: OrderByClause

// --- Query Compilation ---

function resolvedGeometry(
operator : string,
geometryKind: string,
resolvedGeometryRef? : Geometry,
) : Geometry {
if (!resolvedGeometryRef) {
throw new Error(`Le filtre spatial \`${operator}\` exige la résolution préalable de la géométrie ${geometryKind}.`);
}
return resolvedGeometryRef
}

/**
* Compiles normalized tool input into query fragments ready to be turned into a WFS request.
*
* @param input Normalized tool input.
* @param featureType Feature type definition loaded from the embedded catalog.
* @param resolvedGeometryRef Optional resolved reference geometry for `intersects_feature`.
* @param resolvedGeometryRef Optional resolved reference geometry for `intersects_feature` and `adjacent_feature`.
* @returns Compiled query parts used by request builders.
*/
export function compileQueryParts(
Expand All @@ -190,6 +202,7 @@ export function compileQueryParts(
const spatialFilter = getSpatialFilter(input);
const spatialExtras = isGetFeatures ? input.spatial_extras : [];
const fragments: string[] = [];
let resolved : Geometry;

// Keep the spatial predicate first: the GeoPlateforme GeoServer is sensitive
// to filter ordering and may reject equivalent filters when attributes come first.
Expand All @@ -206,16 +219,16 @@ export function compileQueryParts(
fragments.push(compileDwithinSpatialFilter(geometryName, spatialFilter));
break;
case "intersects_feature":
if (!resolvedGeometryRef) {
throw new Error("Le filtre spatial `intersects_feature` exige la résolution préalable de la géométrie de référence.");
}
fragments.push(compileIntersectsFeatureSpatialFilter(geometryName, resolvedGeometryRef));
resolved = resolvedGeometry(spatialFilter.operator, "de référence", resolvedGeometryRef)
fragments.push(compileIntersectsFeatureSpatialFilter(geometryName, resolved));
break;
case "adjacent_feature":
resolved = resolvedGeometry(spatialFilter.operator, "de référence", resolvedGeometryRef)
fragments.push(compileAdjacentFeatureSpatialFilter(geometryName, resolved));
break;
case "travel_time":
if (!resolvedGeometryRef) {
throw new Error("Le filtre spatial `travel_time` exige la résolution préalable de la géométrie d'isochrone.");
}
fragments.push(compileIntersectsFeatureSpatialFilter(geometryName, resolvedGeometryRef));
resolved = resolvedGeometry(spatialFilter.operator, "d'isochrone", resolvedGeometryRef)
fragments.push(compileIntersectsFeatureSpatialFilter(geometryName, resolved));
break;
}
} else if (spatialExtras.length > 0) {
Expand Down
Loading
Loading