Skip to content
Draft
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
103 changes: 10 additions & 93 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
1 change: 1 addition & 0 deletions packages/core/lib/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ export {
changeLayerPositionInContext,
} from "./map-context";
export { createViewFromLayer } from "./view";
export { createXyzFromTms } from "./tms";
101 changes: 101 additions & 0 deletions packages/core/lib/utils/tms.test.ts
Original file line number Diff line number Diff line change
@@ -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',
);
});
});
});
50 changes: 50 additions & 0 deletions packages/core/lib/utils/tms.ts
Original file line number Diff line number Diff line change
@@ -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<MapContextLayerXyz> {
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;
}
Loading