diff --git a/apps/examples/src/App.vue b/apps/examples/src/App.vue index 1603fcd..bf7b8d5 100644 --- a/apps/examples/src/App.vue +++ b/apps/examples/src/App.vue @@ -15,6 +15,8 @@ import ExampleMaplibreRaw from '@/examples/Example-Maplibre.vue?raw' import ExampleMaplibre from '@/examples/Example-Maplibre.vue' import ExampleGeoTIFF from '@/examples/Example-GeoTIFF.vue' import ExampleGeoTIFFRaw from '@/examples/Example-GeoTIFF.vue?raw' +import ExampleMaplibreCOG from '@/examples/Example-MaplibreCOG.vue' +import ExampleMaplibreCOGRaw from '@/examples/Example-MaplibreCOG.vue?raw' import { onMounted, ref } from 'vue' import hljs from 'highlight.js' import '@geospatial-sdk/elements' @@ -105,6 +107,13 @@ onMounted(() => { > + + + diff --git a/apps/examples/src/examples/Example-MaplibreCOG.vue b/apps/examples/src/examples/Example-MaplibreCOG.vue new file mode 100644 index 0000000..25a993a --- /dev/null +++ b/apps/examples/src/examples/Example-MaplibreCOG.vue @@ -0,0 +1,34 @@ + + + diff --git a/package-lock.json b/package-lock.json index 6113154..7f09892 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2859,6 +2859,35 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@geomatico/maplibre-cog-protocol": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@geomatico/maplibre-cog-protocol/-/maplibre-cog-protocol-0.8.0.tgz", + "integrity": "sha512-q5Pg7pOp/fRB/UrAUEZ4AEF/B68XwvxMg0YRk0z+UwwHfOKQThkY7u91Kusx2AwIme6QOURFK6Fvw39ooj68RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@mapbox/sphericalmercator": "^1.2.0", + "d3-scale": "^4.0.2", + "geotiff": "^2.1.3", + "quick-lru": "^7.1.0" + }, + "peerDependencies": { + "maplibre-gl": "^4.5.0 || ^5.0.0" + } + }, + "node_modules/@geomatico/maplibre-cog-protocol/node_modules/quick-lru": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-7.3.0.tgz", + "integrity": "sha512-k9lSsjl36EJdK7I06v7APZCbyGT2vMTsYSRX1Q2nbYmnkBqgUhRkAuzH08Ciotteu/PLJmIF2+tti7o3C/ts2g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@geospatial-sdk/core": { "resolved": "packages/core", "link": true @@ -3265,6 +3294,18 @@ "integrity": "sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==", "dev": true }, + "node_modules/@mapbox/sphericalmercator": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@mapbox/sphericalmercator/-/sphericalmercator-1.2.0.tgz", + "integrity": "sha512-ZTOuuwGuMOJN+HEmG/68bSEw15HHaMWmQ5gdTsWdWsjDe56K1kGvLOK6bOSC8gWgIvEO0w6un/2Gvv1q5hJSkQ==", + "dev": true, + "bin": { + "bbox": "bin/bbox.js", + "to4326": "bin/to4326.js", + "to900913": "bin/to900913.js", + "xyz": "bin/xyz.js" + } + }, "node_modules/@mapbox/tiny-sdf": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.0.7.tgz", @@ -7016,6 +7057,95 @@ "dev": true, "license": "MIT" }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "dev": true, + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/dargs": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/dargs/-/dargs-7.0.0.tgz", @@ -9680,6 +9810,16 @@ "node": ">= 0.4" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/ip": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", @@ -17848,10 +17988,12 @@ "@geospatial-sdk/core": "^0.0.5-alpha.2" }, "devDependencies": { + "@geomatico/maplibre-cog-protocol": "^0.8.0", "maplibre-gl": "^5.19.0" }, "peerDependencies": { - "maplibre-gl": "^5.7.3" + "@geomatico/maplibre-cog-protocol": "^0.8.0", + "maplibre-gl": "^5.19.0" } }, "packages/openlayers": { diff --git a/packages/maplibre/lib/map/create-map.test.ts b/packages/maplibre/lib/map/create-map.test.ts index d7fc651..6143997 100644 --- a/packages/maplibre/lib/map/create-map.test.ts +++ b/packages/maplibre/lib/map/create-map.test.ts @@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { MAP_CTX_LAYER_GEOJSON_FIXTURE, MAP_CTX_LAYER_GEOJSON_REMOTE_FIXTURE, + MAP_CTX_LAYER_GEOTIFF_FIXTURE, MAP_CTX_LAYER_OGCAPI_FIXTURE, MAP_CTX_LAYER_WFS_FIXTURE, MAP_CTX_LAYER_WMS_FIXTURE, @@ -370,6 +371,38 @@ describe("MapContextService", () => { }); }); + describe("GeoTIFF", () => { + beforeEach(async () => { + layerModel = MAP_CTX_LAYER_GEOTIFF_FIXTURE; + style = (await createLayer(layerModel)) as PartialStyleSpecification; + }); + it("create a raster layer and source", () => { + const sourceId = "123456"; + const sourcesIds = Object.keys(style.sources); + expect(sourcesIds.length).toBe(1); + expect(sourcesIds[0]).toBe(sourceId); + + const source = style.sources[sourceId] as RasterSourceSpecification; + expect(source.type).toBe("raster"); + expect(source.url).toBe( + "cog://https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/36/Q/WD/2020/7/S2A_36QWD_20200701_0_L2A/TCI.tif", + ); + expect(source.tileSize).toBe(256); + }); + it("create a layer with correct properties", () => { + expect(style.layers.length).toBe(1); + const layer = style.layers[0] as RasterLayerSpecification; + const metadata = layer.metadata as LayerMetadataSpecification; + + expect(layer.id).toBe("123456"); + expect(layer.type).toBe("raster"); + expect(layer.source).toBe("123456"); + expect(layer.paint?.["raster-opacity"]).toBe(1); + expect(layer.layout?.visibility).toBe("visible"); + expect(metadata.layerHash).toBeTypeOf("string"); + }); + }); + describe("WMTS", () => { beforeEach(async () => { layerModel = MAP_CTX_LAYER_WMTS_FIXTURE; diff --git a/packages/maplibre/lib/map/create-map.ts b/packages/maplibre/lib/map/create-map.ts index ca032bc..763d244 100644 --- a/packages/maplibre/lib/map/create-map.ts +++ b/packages/maplibre/lib/map/create-map.ts @@ -5,7 +5,8 @@ import { ViewByZoomAndCenter, } from "@geospatial-sdk/core"; -import { LayerSpecification, Map, MapOptions } from "maplibre-gl"; +import { addProtocol, LayerSpecification, Map, MapOptions } from "maplibre-gl"; +import { cogProtocol } from "@geomatico/maplibre-cog-protocol"; import { FeatureCollection, Geometry } from "geojson"; import { OgcApiEndpoint, @@ -22,6 +23,8 @@ import { PartialStyleSpecification, } from "../maplibre.models.js"; +let cogProtocolRegistered = false; + const featureCollection: FeatureCollection = { type: "FeatureCollection", features: [], @@ -153,6 +156,36 @@ export async function createLayer( ], }; } + case "geotiff": { + if (!cogProtocolRegistered) { + addProtocol("cog", cogProtocol); + cogProtocolRegistered = true; + } + const sourceId = layerId; + return { + sources: { + [sourceId]: { + type: "raster", + url: `cog://${layerModel.url}`, + tileSize: 256, + }, + }, + layers: [ + { + id: layerId, + type: "raster", + source: sourceId, + paint: { + "raster-opacity": layerModel.opacity ?? 1, + }, + layout: { + visibility: layerModel.visibility === false ? "none" : "visible", + }, + metadata, + }, + ], + }; + } case "wmts": { console.warn(`WMTS layers are not yet supported in Maplibre`, layerModel); return null; diff --git a/packages/maplibre/package.json b/packages/maplibre/package.json index 1f8330a..62e591c 100644 --- a/packages/maplibre/package.json +++ b/packages/maplibre/package.json @@ -27,10 +27,12 @@ "build": "tsc" }, "devDependencies": { - "maplibre-gl": "^5.19.0" + "maplibre-gl": "^5.19.0", + "@geomatico/maplibre-cog-protocol": "^0.8.0" }, "peerDependencies": { - "maplibre-gl": "^5.19.0" + "maplibre-gl": "^5.19.0", + "@geomatico/maplibre-cog-protocol": "^0.8.0" }, "dependencies": { "@geospatial-sdk/core": "^0.0.5-alpha.2"