From 60cc5e9d9c7c0e96a6a8df570194c9428cf37750 Mon Sep 17 00:00:00 2001 From: mborne Date: Sun, 12 Apr 2026 11:57:47 +0200 Subject: [PATCH 1/4] fix(distance): use haversine formula from turf/distance (refs #39) and return a distance in meters (refs #36) --- README.md | 1 + package-lock.json | 49 ++++++++++++++++++++++++++++++++--- package.json | 2 ++ src/gpf/urbanisme.js | 4 +-- src/helpers/distance.js | 27 ++++++++++++++++--- src/tools/AssietteSupTool.ts | 2 +- src/tools/CadastreTool.ts | 10 +++++-- src/tools/UrbanismeTool.ts | 2 +- test/helpers/distance.test.ts | 27 ++++++++++++++++--- 9 files changed, 106 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 07beae4..a57fb84 100644 --- a/README.md +++ b/README.md @@ -250,6 +250,7 @@ mcp add tool gpf_wmts_layers * [@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é). +* [turfjs/distance](https://turfjs.org/docs/api/distance) pour les **calculs de distance** avec la [formule de Harversine](https://en.wikipedia.org/wiki/Haversine_formula). ## Licence diff --git a/package-lock.json b/package-lock.json index c5a4575..4c7ed85 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,8 @@ "dependencies": { "@ignfab/gpf-schema-store": "^0.1.3", "@rgrove/parse-xml": "^4.2.0", + "@turf/distance": "^7.3.4", + "@turf/helpers": "^7.3.4", "https-proxy-agent": "^7.0.6", "jsts": "^2.12.1", "lodash": "^4.17.21", @@ -1339,6 +1341,48 @@ "dev": true, "license": "MIT" }, + "node_modules/@turf/distance": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@turf/distance/-/distance-7.3.4.tgz", + "integrity": "sha512-9drWgd46uHPPyzgrcRQLgSvdS/SjVlQ6ZIBoRQagS5P2kSjUbcOXHIMeOSPwfxwlKhEtobLyr+IiR2ns1TfF8w==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.3.4", + "@turf/invariant": "7.3.4", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/helpers": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-7.3.4.tgz", + "integrity": "sha512-U/S5qyqgx3WTvg4twaH0WxF3EixoTCfDsmk98g1E3/5e2YKp7JKYZdz0vivsS5/UZLJeZDEElOSFH4pUgp+l7g==", + "license": "MIT", + "dependencies": { + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/invariant": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@turf/invariant/-/invariant-7.3.4.tgz", + "integrity": "sha512-88Eo4va4rce9sNZs6XiMJowWkikM3cS2TBhaCKlU+GFHdNf8PFEpiU42VDU8q5tOF6/fu21Rvlke5odgOGW4AQ==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.3.4", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", @@ -1416,7 +1460,6 @@ "version": "7946.0.16", "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", - "dev": true, "license": "MIT" }, "node_modules/@types/istanbul-lib-coverage": { @@ -6689,9 +6732,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD", - "optional": true + "license": "0BSD" }, "node_modules/type-detect": { "version": "4.0.8", diff --git a/package.json b/package.json index cf96226..3de280c 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,8 @@ "dependencies": { "@ignfab/gpf-schema-store": "^0.1.3", "@rgrove/parse-xml": "^4.2.0", + "@turf/distance": "^7.3.4", + "@turf/helpers": "^7.3.4", "https-proxy-agent": "^7.0.6", "jsts": "^2.12.1", "lodash": "^4.17.21", diff --git a/src/gpf/urbanisme.js b/src/gpf/urbanisme.js index 978bf37..c2474b9 100644 --- a/src/gpf/urbanisme.js +++ b/src/gpf/urbanisme.js @@ -58,7 +58,7 @@ export async function getUrbanisme(lon, lat, fetcher) { return features.map((feature) => { const item = { ...mapWfsFeature(feature, URBANISME_TYPES), - distance: distance(sourceGeom, feature.geometry) * 1000.0, + distance: distance(sourceGeom, feature.geometry), }; return sanitizeUrbanismeItem(item); }); @@ -89,6 +89,6 @@ export async function getAssiettesServitudes(lon, lat, fetcher) { 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, + distance: distance(sourceGeom, feature.geometry), })); } diff --git a/src/helpers/distance.js b/src/helpers/distance.js index fc3afe8..9e7bae7 100644 --- a/src/helpers/distance.js +++ b/src/helpers/distance.js @@ -1,8 +1,11 @@ import GeoJSONReader from 'jsts/org/locationtech/jts/io/GeoJSONReader.js' import { DistanceOp } from 'jsts/org/locationtech/jts/operation/distance.js' +import turfDistance from '@turf/distance' +import {point as turfPoint} from '@turf/helpers' + /** - * Compute approximative distance in km between gA and gB. + * Compute approximative distance in meters between gA and gB. * * @param {object} gA GeoJSON Point * @param {object} gB GeoJSON Geometry @@ -12,8 +15,24 @@ export default function distance(gA, gB) { const a = geojsonReader.read(gA); const b = geojsonReader.read(gB); - // converts to kilometers assuming earth is a sphere - const distanceInDegree = DistanceOp.distance(a, b); - return 6480.0 * ( distanceInDegree * 2.0 * Math.PI / 360.0 ) ; + /* + * Get the 2 nearest points between a and b + * + * Note that it will project according to longitude and latitude axis, + * so it is not really accurate, but it is a good approximation + */ + const nearestPoints = DistanceOp.nearestPoints(a, b); + if ( nearestPoints.length !== 2 ) { + throw new Error('DistanceOp.nearestPoints should return 2 points'); + } + + /* + * harversine distance between the 2 nearest points (see https://turfjs.org/docs/api/distance) + */ + return turfDistance( + turfPoint([nearestPoints[0].x, nearestPoints[0].y]), + turfPoint([nearestPoints[1].x, nearestPoints[1].y]), + { units: 'meters' } + ); } diff --git a/src/tools/AssietteSupTool.ts b/src/tools/AssietteSupTool.ts index ceebbba..c71fc99 100644 --- a/src/tools/AssietteSupTool.ts +++ b/src/tools/AssietteSupTool.ts @@ -18,7 +18,7 @@ const assietteSupResultSchema = z id: z.string().describe("L'identifiant de l'assiette."), bbox: z.array(z.number()).describe("La boîte englobante de l'assiette.").optional(), feature_ref: featureRefSchema.describe("Référence WFS réutilisable, notamment avec `gpf_wfs_get_features` et `spatial_operator = \"intersects_feature\"`.").optional(), - distance: z.number().describe("La distance entre le point demandé et l'assiette retenue."), + distance: z.number().describe("La distance en mètre entre le point demandé et l'assiette retenue."), }) .catchall(z.unknown()); diff --git a/src/tools/CadastreTool.ts b/src/tools/CadastreTool.ts index 7910f1d..f46a2c8 100644 --- a/src/tools/CadastreTool.ts +++ b/src/tools/CadastreTool.ts @@ -18,7 +18,7 @@ const cadastreResultSchema = z id: z.string().describe("L'identifiant de l'objet cadastral."), bbox: z.array(z.number()).describe("La boîte englobante de l'objet cadastral.").optional(), feature_ref: featureRefSchema.describe("Référence WFS réutilisable, notamment avec `gpf_wfs_get_features` et `spatial_operator = \"intersects_feature\"`."), - distance: z.number().describe("La distance entre le point demandé et l'objet cadastral retenu."), + distance: z.number().describe("La distance en mètre entre le point demandé et l'objet cadastral retenu."), source: z.string().describe("La source des données cadastrales."), }) .catchall(z.unknown()); @@ -27,11 +27,17 @@ const cadastreOutputSchema = z.object({ results: z.array(cadastreResultSchema).describe("La liste des objets cadastraux les plus proches du point demandé."), }); +const CADASTRE_TOOL_DESCRIPTION = [ + `Renvoie, pour un point donné par sa longitude et sa latitude, la liste des objets cadastraux (${PARCELLAIRE_EXPRESS_TYPES.join(', ')}) les plus proches, avec leurs informations associées.`, + `Les résultats sont retournés au plus une fois par type lorsqu'ils sont disponibles et incluent un \`feature_ref\` WFS réutilisable.`, + 'La distance de recherche est fixée à 10 mètres.' +] + class CadastreTool extends MCPTool { name = "cadastre"; title = "Informations cadastrales"; annotations = READ_ONLY_OPEN_WORLD_TOOL_ANNOTATIONS; - description = `Renvoie, pour un point donné par sa longitude et sa latitude, la liste des objets cadastraux (${PARCELLAIRE_EXPRESS_TYPES.join(', ')}) les plus proches, avec leurs informations associées. Les résultats sont retournés au plus une fois par type lorsqu'ils sont disponibles et incluent un \`feature_ref\` WFS réutilisable. (source : ${PARCELLAIRE_EXPRESS_SOURCE}).`; + description = CADASTRE_TOOL_DESCRIPTION.join("\n"); protected outputSchemaShape = cadastreOutputSchema; schema = cadastreInputSchema; diff --git a/src/tools/UrbanismeTool.ts b/src/tools/UrbanismeTool.ts index fb7b8f4..16350e2 100644 --- a/src/tools/UrbanismeTool.ts +++ b/src/tools/UrbanismeTool.ts @@ -18,7 +18,7 @@ const urbanismeResultSchema = z id: z.string().describe("L'identifiant de l'objet d'urbanisme."), bbox: z.array(z.number()).describe("La boîte englobante de l'objet d'urbanisme.").optional(), feature_ref: featureRefSchema.describe("Référence WFS réutilisable, notamment avec `gpf_wfs_get_features` et `spatial_operator = \"intersects_feature\"`.").optional(), - distance: z.number().describe("La distance entre le point demandé et l'objet d'urbanisme retenu."), + distance: z.number().describe("La distance en mètre entre le point demandé et l'objet d'urbanisme retenu."), }) .catchall(z.unknown()); diff --git a/test/helpers/distance.test.ts b/test/helpers/distance.test.ts index b723b64..00097c8 100644 --- a/test/helpers/distance.test.ts +++ b/test/helpers/distance.test.ts @@ -4,15 +4,34 @@ import {paris, marseille, besancon, parisMarseille} from '../samples'; describe("Test distance",() => { describe("Test distance(Point,Point)", () => { - it("should return ~718.8km from Paris to Marseille",() => { + it("should return 662489.3m from Paris to Marseille",() => { const result = distance(paris,marseille); - expect(result).toBeCloseTo(718.8,1); + expect(result).toBeCloseTo(662489.3,1); }); }); describe("Test distance(Point,LineString)", () => { - it("should return ~276.7km from Besançon to [Paris,Marseille]",() => { + it("should return 209731.2m from Besançon to [Paris,Marseille]",() => { const result = distance(besancon,parisMarseille); - expect(result).toBeCloseTo(276.7,1); + expect(result).toBeCloseTo(209731.2,1); + }); + }); + + describe("Test distance(Point,Polygon)", () => { + it("should return 0m from Paris point to a polygon containing Paris",() => { + const polygonContainingParis = { + "type": "Polygon", + "coordinates": [ + [ + [2.0, 48.0], + [3.0, 48.0], + [3.0, 49.0], + [2.0, 49.0], + [2.0, 48.0] + ] + ] + }; + const result = distance(paris, polygonContainingParis); + expect(result).toEqual(0); }); }); }); From 9d3e3e5ff5a3b204c82ecf52c795743c37d3acc0 Mon Sep 17 00:00:00 2001 From: esgn <5435148+esgn@users.noreply.github.com> Date: Tue, 14 Apr 2026 17:13:44 +0200 Subject: [PATCH 2/4] fix(distance): correct grammatical error in distance description to use plural form --- src/tools/AssietteSupTool.ts | 2 +- src/tools/CadastreTool.ts | 2 +- src/tools/UrbanismeTool.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/tools/AssietteSupTool.ts b/src/tools/AssietteSupTool.ts index c71fc99..c52d62e 100644 --- a/src/tools/AssietteSupTool.ts +++ b/src/tools/AssietteSupTool.ts @@ -18,7 +18,7 @@ const assietteSupResultSchema = z id: z.string().describe("L'identifiant de l'assiette."), bbox: z.array(z.number()).describe("La boîte englobante de l'assiette.").optional(), feature_ref: featureRefSchema.describe("Référence WFS réutilisable, notamment avec `gpf_wfs_get_features` et `spatial_operator = \"intersects_feature\"`.").optional(), - distance: z.number().describe("La distance en mètre entre le point demandé et l'assiette retenue."), + distance: z.number().describe("La distance en mètres entre le point demandé et l'assiette retenue."), }) .catchall(z.unknown()); diff --git a/src/tools/CadastreTool.ts b/src/tools/CadastreTool.ts index f46a2c8..1c6d705 100644 --- a/src/tools/CadastreTool.ts +++ b/src/tools/CadastreTool.ts @@ -18,7 +18,7 @@ const cadastreResultSchema = z id: z.string().describe("L'identifiant de l'objet cadastral."), bbox: z.array(z.number()).describe("La boîte englobante de l'objet cadastral.").optional(), feature_ref: featureRefSchema.describe("Référence WFS réutilisable, notamment avec `gpf_wfs_get_features` et `spatial_operator = \"intersects_feature\"`."), - distance: z.number().describe("La distance en mètre entre le point demandé et l'objet cadastral retenu."), + distance: z.number().describe("La distance en mètres entre le point demandé et l'objet cadastral retenu."), source: z.string().describe("La source des données cadastrales."), }) .catchall(z.unknown()); diff --git a/src/tools/UrbanismeTool.ts b/src/tools/UrbanismeTool.ts index 16350e2..0d67385 100644 --- a/src/tools/UrbanismeTool.ts +++ b/src/tools/UrbanismeTool.ts @@ -18,7 +18,7 @@ const urbanismeResultSchema = z id: z.string().describe("L'identifiant de l'objet d'urbanisme."), bbox: z.array(z.number()).describe("La boîte englobante de l'objet d'urbanisme.").optional(), feature_ref: featureRefSchema.describe("Référence WFS réutilisable, notamment avec `gpf_wfs_get_features` et `spatial_operator = \"intersects_feature\"`.").optional(), - distance: z.number().describe("La distance en mètre entre le point demandé et l'objet d'urbanisme retenu."), + distance: z.number().describe("La distance en mètres entre le point demandé et l'objet d'urbanisme retenu."), }) .catchall(z.unknown()); From 6e8a93dfe85071620fea0c25fec9f54cf9e0ad46 Mon Sep 17 00:00:00 2001 From: esgn <5435148+esgn@users.noreply.github.com> Date: Tue, 14 Apr 2026 17:17:25 +0200 Subject: [PATCH 3/4] fix(docs): correct spelling of "Haversine" in README and distance.js comments --- README.md | 2 +- src/helpers/distance.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a57fb84..aef4c2b 100644 --- a/README.md +++ b/README.md @@ -250,7 +250,7 @@ mcp add tool gpf_wmts_layers * [@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é). -* [turfjs/distance](https://turfjs.org/docs/api/distance) pour les **calculs de distance** avec la [formule de Harversine](https://en.wikipedia.org/wiki/Haversine_formula). +* [turfjs/distance](https://turfjs.org/docs/api/distance) pour les **calculs de distance** avec la [formule de Haversine](https://en.wikipedia.org/wiki/Haversine_formula). ## Licence diff --git a/src/helpers/distance.js b/src/helpers/distance.js index 9e7bae7..d2732c8 100644 --- a/src/helpers/distance.js +++ b/src/helpers/distance.js @@ -27,7 +27,7 @@ export default function distance(gA, gB) { } /* - * harversine distance between the 2 nearest points (see https://turfjs.org/docs/api/distance) + * haversine distance between the 2 nearest points (see https://turfjs.org/docs/api/distance) */ return turfDistance( turfPoint([nearestPoints[0].x, nearestPoints[0].y]), From 55742f3c4bfffabc696720c18bfa365d15f6e01c Mon Sep 17 00:00:00 2001 From: esgn <5435148+esgn@users.noreply.github.com> Date: Tue, 14 Apr 2026 17:25:12 +0200 Subject: [PATCH 4/4] fix(distance): update parameter description for GeoJSON Geometry in distance function --- src/helpers/distance.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/helpers/distance.js b/src/helpers/distance.js index d2732c8..2e6944c 100644 --- a/src/helpers/distance.js +++ b/src/helpers/distance.js @@ -6,8 +6,10 @@ import {point as turfPoint} from '@turf/helpers' /** * Compute approximative distance in meters between gA and gB. + * + * TODO: replace the lon/lat planar nearest-point step with a geodesic geometry distance. * - * @param {object} gA GeoJSON Point + * @param {object} gA GeoJSON Geometry * @param {object} gB GeoJSON Geometry */ export default function distance(gA, gB) { @@ -35,4 +37,3 @@ export default function distance(gA, gB) { { units: 'meters' } ); } -