From 26956ce5399c1ad9fd84c9b173931944d9ff7348 Mon Sep 17 00:00:00 2001 From: Florent Gravin Date: Thu, 18 Dec 2025 15:55:14 +0100 Subject: [PATCH 1/2] chore: update ogc-client to last dev version --- package-lock.json | 103 +++++----------------------------------------- package.json | 2 +- 2 files changed, 11 insertions(+), 94 deletions(-) diff --git a/package-lock.json b/package-lock.json index 01cee17..83185aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "packages/*" ], "dependencies": { - "@camptocamp/ogc-client": "1.2.1-dev.8fa0859" + "@camptocamp/ogc-client": "^1.3.1-dev.53a6449" }, "devDependencies": { "@babel/cli": "^7.23.9", @@ -2425,12 +2425,15 @@ } }, "node_modules/@camptocamp/ogc-client": { - "version": "1.2.1-dev.8fa0859", - "resolved": "https://registry.npmjs.org/@camptocamp/ogc-client/-/ogc-client-1.2.1-dev.8fa0859.tgz", - "integrity": "sha512-Nn5nQK5URF/K5wN0gmLUK8AE8vSpPT4RaHmYGMrOG0YS7swKVqGCnMsL3GNcDSfvCTvVK3mS2BwsLB0BLkUZng==", + "version": "1.3.1-dev.53a6449", + "resolved": "https://registry.npmjs.org/@camptocamp/ogc-client/-/ogc-client-1.3.1-dev.53a6449.tgz", + "integrity": "sha512-UuCL9bVeHuY/apks7vFsPHRwnhd5DlRT4wW69/vcMNCwGsYs5VyyPBdOwC6m5TMqci4e1qfecnpkmw5FVjnLpQ==", + "license": "BSD-3-Clause", "dependencies": { - "@rgrove/parse-xml": "^4.1.0", - "node-fetch": "^3.3.1" + "@rgrove/parse-xml": "^4.1.0" + }, + "engines": { + "node": ">=20.0.0" }, "peerDependencies": { "ol": ">5.x", @@ -2445,23 +2448,6 @@ } } }, - "node_modules/@camptocamp/ogc-client/node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, "node_modules/@docsearch/css": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-3.6.2.tgz", @@ -6794,14 +6780,6 @@ "node": ">=8" } }, - "node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", - "engines": { - "node": ">= 12" - } - }, "node_modules/data-urls": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", @@ -7679,28 +7657,6 @@ "reusify": "^1.0.4" } }, - "node_modules/fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], - "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - }, - "engines": { - "node": "^12.20 || >= 14.13" - } - }, "node_modules/figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -7930,17 +7886,6 @@ "node": ">= 6" } }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "dependencies": { - "fetch-blob": "^3.1.2" - }, - "engines": { - "node": ">=12.20.0" - } - }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -10602,24 +10547,6 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], - "engines": { - "node": ">=10.5.0" - } - }, "node_modules/node-fetch": { "version": "2.6.7", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", @@ -14980,14 +14907,6 @@ "defaults": "^1.0.3" } }, - "node_modules/web-streams-polyfill": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", - "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", - "engines": { - "node": ">= 8" - } - }, "node_modules/web-worker": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.3.0.tgz", @@ -15414,11 +15333,9 @@ "version": "0.0.5-alpha.2", "license": "BSD-3-Clause", "dependencies": { - "@geospatial-sdk/core": "^0.0.5-alpha.2", - "chroma-js": "^2.4.2" + "@geospatial-sdk/core": "^0.0.5-alpha.2" }, "devDependencies": { - "@types/chroma-js": "^2.4.3", "maplibre-gl": "^5.7.3" }, "peerDependencies": { diff --git a/package.json b/package.json index e4aaa3d..2650684 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,6 @@ "prepublish": "npm run build" }, "dependencies": { - "@camptocamp/ogc-client": "1.2.1-dev.8fa0859" + "@camptocamp/ogc-client": "^1.3.1-dev.53a6449" } } From 913bbb3bcf576f65dbf2c9bda043a262b6e6a2b7 Mon Sep 17 00:00:00 2001 From: Florent Gravin Date: Thu, 18 Dec 2025 15:57:30 +0100 Subject: [PATCH 2/2] feat(tms): add createTmsLayer from TmsEndpoint --- packages/core/lib/utils/index.ts | 1 + packages/core/lib/utils/tms.test.ts | 101 ++++++++++++++++++++++++++++ packages/core/lib/utils/tms.ts | 50 ++++++++++++++ 3 files changed, 152 insertions(+) create mode 100644 packages/core/lib/utils/tms.test.ts create mode 100644 packages/core/lib/utils/tms.ts diff --git a/packages/core/lib/utils/index.ts b/packages/core/lib/utils/index.ts index 30d8f30..b589da4 100644 --- a/packages/core/lib/utils/index.ts +++ b/packages/core/lib/utils/index.ts @@ -10,3 +10,4 @@ export { changeLayerPositionInContext, } from "./map-context"; export { createViewFromLayer } from "./view"; +export { createXyzFromTms } from "./tms"; diff --git a/packages/core/lib/utils/tms.test.ts b/packages/core/lib/utils/tms.test.ts new file mode 100644 index 0000000..4466e41 --- /dev/null +++ b/packages/core/lib/utils/tms.test.ts @@ -0,0 +1,101 @@ +import { createXyzFromTms } from "./tms"; + +vitest.mock("@camptocamp/ogc-client", () => ({ + TmsEndpoint: class { + constructor(private url: string) {} + + get allTileMaps() { + return Promise.resolve([ + { + title: "states", + srs: "EPSG:4326", + href: "https://ahocevar.com/geoserver/gwc/service/tms/1.0.0/topp%3Astates@EPSG%3A4326@png", + }, + { + title: "countries", + srs: "EPSG:4326", + href: "https://ahocevar.com/geoserver/gwc/service/tms/1.0.0/topp%3Acountries@EPSG%3A4326@jpeg", + }, + ]); + } + + getTileMapInfo(href: string) { + if (href.includes("states")) { + return Promise.resolve({ + title: "states", + tileFormat: { + extension: "png", + mimeType: "image/png", + }, + tileSets: [ + { + href: "https://ahocevar.com/geoserver/gwc/service/tms/1.0.0/topp%3Astates@EPSG%3A4326@png/0", + order: 0, + }, + { + href: "https://ahocevar.com/geoserver/gwc/service/tms/1.0.0/topp%3Astates@EPSG%3A4326@png/1", + order: 1, + }, + ], + }); + } else if (href.includes("countries")) { + return Promise.resolve({ + title: "countries", + tileFormat: { + extension: "jpeg", + mimeType: "image/jpeg", + }, + tileSets: [ + { + href: "https://ahocevar.com/geoserver/gwc/service/tms/1.0.0/topp%3Acountries@EPSG%3A4326@jpeg/0", + order: 0, + }, + ], + }); + } + throw new Error("TileMap not found"); + } + }, +})); + +describe("tms", () => { + describe("createXyzFromTms", () => { + it("should create MapContextLayerXyz from TMS endpoint with PNG tiles", async () => { + const layer = await createXyzFromTms( + "https://ahocevar.com/geoserver/gwc/service/tms/1.0.0", + "states", + "EPSG:4326", + ); + + expect(layer).toEqual({ + type: "xyz", + url: "https://ahocevar.com/geoserver/gwc/service/tms/1.0.0/topp%3Astates@EPSG%3A4326@png/{z}/{x}/{y}.png", + }); + }); + + it("should create MapContextLayerXyz from TMS endpoint with JPEG tiles", async () => { + const layer = await createXyzFromTms( + "https://ahocevar.com/geoserver/gwc/service/tms/1.0.0", + "countries", + "EPSG:4326", + ); + + expect(layer).toEqual({ + type: "xyz", + url: "https://ahocevar.com/geoserver/gwc/service/tms/1.0.0/topp%3Acountries@EPSG%3A4326@jpeg/{z}/{x}/{y}.jpeg", + }); + }); + + it("should throw error when TileMap name not found", async () => { + await expect( + createXyzFromTms( + "https://ahocevar.com/geoserver/gwc/service/tms/1.0.0", + "non-existent", + "EPSG:4326", + ), + ).rejects.toThrow( + 'TileMap with title "non-existent" not found in TMS endpoint', + ); + }); + }); +}); diff --git a/packages/core/lib/utils/tms.ts b/packages/core/lib/utils/tms.ts new file mode 100644 index 0000000..ea6a653 --- /dev/null +++ b/packages/core/lib/utils/tms.ts @@ -0,0 +1,50 @@ +import { MapContextLayerXyz } from "../model"; +import { TmsEndpoint } from "@camptocamp/ogc-client"; + +/** + * Creates a MapContextLayerXyz from a TMS endpoint URL and tile map name + * @param tmsUrl - The URL of the TMS endpoint + * @param tileMapName - The title of the TileMap to use + * @param srs - SRS code (e.g., "EPSG:4326") + * @returns A MapContextLayerXyz object with the correct tile URL pattern + */ +export async function createXyzFromTms( + tmsUrl: string, + tileMapName: string, + srs: string, +): Promise { + const endpoint = await new TmsEndpoint(tmsUrl); + const tileMaps = await endpoint.allTileMaps; + const tileMap = tileMaps.find( + (tm) => tm.title === tileMapName && tm.srs === srs, + ); + + if (!tileMap) { + throw new Error( + `TileMap with title "${tileMapName}" not found in TMS endpoint. Available maps: ${tileMaps.map((tm) => tm.title).join(", ")}`, + ); + } + + const tileMapInfo = await endpoint.getTileMapInfo(tileMap.href); + const extension = tileMapInfo.tileFormat?.extension; + const tileSetHref = tileMapInfo.tileSets?.[0]?.href; + + if (!tileSetHref) { + throw new Error( + `No TileSets found for TileMap "${tileMapName}" in TMS endpoint`, + ); + } + + // Extract the base URL by removing the last path segment (the zoom level) + // Example: https://example.com/tms/1.0.0/layer@EPSG:4326@png/0 + // becomes: https://example.com/tms/1.0.0/layer@EPSG:4326@png/{z}/{x}/{y}.png + const baseUrl = tileSetHref.substring(0, tileSetHref.lastIndexOf("/")); + const urlPattern = `${baseUrl}/{z}/{x}/{y}.${extension}`; + + const layer: MapContextLayerXyz = { + type: "xyz", + url: urlPattern, + }; + + return layer; +}