diff --git a/.github/workflows/containers.yml b/.github/workflows/containers.yml index baee7a851..3e694aa3b 100644 --- a/.github/workflows/containers.yml +++ b/.github/workflows/containers.yml @@ -56,6 +56,19 @@ jobs: - name: Log in to registry run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u $ --password-stdin + - name: '@basemaps/cli - Build and export to Docker' + uses: docker/build-push-action@v5 + with: + context: packages/cli + load: true + tags: | + ghcr.io/linz/basemaps/cli:latest + ghcr.io/linz/basemaps/cli:${{ steps.version.outputs.version }} + + - name: '@basemaps/cli - Test' + run: | + docker run --rm ghcr.io/linz/basemaps/cli:${{ steps.version.outputs.version }} --help + - name: '@basemaps/cli - Build and push' uses: docker/build-push-action@v5 with: @@ -77,9 +90,21 @@ jobs: ghcr.io/linz/basemaps/cli:${{ steps.version.outputs.version_major }} ghcr.io/linz/basemaps/cli:${{ steps.version.outputs.version_major_minor }} ghcr.io/linz/basemaps/cli:${{ steps.version.outputs.version }} - push: ${{github.ref == 'refs/heads/master' && startsWith(github.event.head_commit.message, 'release:')}} + - name: '@basemaps/server - Build and export to Docker' + uses: docker/build-push-action@v5 + with: + context: packages/server + load: true + tags: | + ghcr.io/linz/basemaps/server:latest + ghcr.io/linz/basemaps/server:${{ steps.version.outputs.version }} + + - name: '@basemaps/server - Test' + run: | + docker run --rm ghcr.io/linz/basemaps/server:${{ steps.version.outputs.version }} --version + - name: '@basemaps/server - Build and push' uses: docker/build-push-action@v5 with: @@ -102,3 +127,4 @@ jobs: ghcr.io/linz/basemaps/server:${{ steps.version.outputs.version_major_minor }} ghcr.io/linz/basemaps/server:${{ steps.version.outputs.version }} push: ${{github.ref == 'refs/heads/master' && startsWith(github.event.head_commit.message, 'release:')}} + diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fc32345c..5415bddaa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [7.11.1](https://github.com/linz/basemaps/compare/v7.11.0...v7.11.1) (2024-10-01) + + +### Bug Fixes + +* **cli:** Install server package in the cli container to inlcude lerc. ([#3353](https://github.com/linz/basemaps/issues/3353)) ([9b2b785](https://github.com/linz/basemaps/commit/9b2b785deaa87c94f1ec2254b91c1ed8993a72f3)) + + + + + # [7.11.0](https://github.com/linz/basemaps/compare/v7.10.0...v7.11.0) (2024-09-29) diff --git a/lerna.json b/lerna.json index 4f03dc8ea..d190accd5 100644 --- a/lerna.json +++ b/lerna.json @@ -7,5 +7,5 @@ "conventionalCommits": true } }, - "version": "7.11.0" + "version": "7.11.1" } diff --git a/package-lock.json b/package-lock.json index 71423d79f..a52acd3ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19328,10 +19328,10 @@ }, "packages/bathymetry": { "name": "@basemaps/bathymetry", - "version": "7.11.0", + "version": "7.11.1", "license": "MIT", "dependencies": { - "@basemaps/cli": "^7.11.0", + "@basemaps/cli": "^7.11.1", "@basemaps/geo": "^7.11.0", "@basemaps/shared": "^7.11.0", "@rushstack/ts-command-line": "^4.3.13", @@ -19356,7 +19356,7 @@ }, "packages/cli": { "name": "@basemaps/cli", - "version": "7.11.0", + "version": "7.11.1", "license": "MIT", "dependencies": { "@basemaps/config": "^7.11.0", @@ -19419,13 +19419,13 @@ }, "packages/cogify": { "name": "@basemaps/cogify", - "version": "7.11.0", + "version": "7.11.1", "license": "MIT", "bin": { "cogify": "build/bin.js" }, "devDependencies": { - "@basemaps/cli": "^7.11.0", + "@basemaps/cli": "^7.11.1", "@basemaps/config": "^7.11.0", "@basemaps/config-loader": "^7.11.0", "@basemaps/geo": "^7.11.0", diff --git a/packages/bathymetry/CHANGELOG.md b/packages/bathymetry/CHANGELOG.md index 4310caac5..6fc9894ea 100644 --- a/packages/bathymetry/CHANGELOG.md +++ b/packages/bathymetry/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [7.11.1](https://github.com/linz/basemaps/compare/v7.11.0...v7.11.1) (2024-10-01) + +**Note:** Version bump only for package @basemaps/bathymetry + + + + + # [7.11.0](https://github.com/linz/basemaps/compare/v7.10.0...v7.11.0) (2024-09-29) **Note:** Version bump only for package @basemaps/bathymetry diff --git a/packages/bathymetry/package.json b/packages/bathymetry/package.json index 3f8a8040e..2bbb3a39e 100644 --- a/packages/bathymetry/package.json +++ b/packages/bathymetry/package.json @@ -1,6 +1,6 @@ { "name": "@basemaps/bathymetry", - "version": "7.11.0", + "version": "7.11.1", "repository": { "type": "git", "url": "https://github.com/linz/basemaps.git", @@ -28,7 +28,7 @@ "build/" ], "dependencies": { - "@basemaps/cli": "^7.11.0", + "@basemaps/cli": "^7.11.1", "@basemaps/geo": "^7.11.0", "@basemaps/shared": "^7.11.0", "@rushstack/ts-command-line": "^4.3.13", diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 7635af908..83fe1f92a 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [7.11.1](https://github.com/linz/basemaps/compare/v7.11.0...v7.11.1) (2024-10-01) + + +### Bug Fixes + +* **cli:** Install server package in the cli container to inlcude lerc. ([#3353](https://github.com/linz/basemaps/issues/3353)) ([9b2b785](https://github.com/linz/basemaps/commit/9b2b785deaa87c94f1ec2254b91c1ed8993a72f3)) + + + + + # [7.11.0](https://github.com/linz/basemaps/compare/v7.10.0...v7.11.0) (2024-09-29) diff --git a/packages/cli/Dockerfile b/packages/cli/Dockerfile index 3ba8ea493..c436f6e51 100644 --- a/packages/cli/Dockerfile +++ b/packages/cli/Dockerfile @@ -21,11 +21,12 @@ RUN npm install sharp@0.33.0 COPY ./basemaps-landing*.tgz /app/ COPY ./basemaps-cogify*.tgz /app/ COPY ./basemaps-smoke*.tgz /app/ +COPY ./basemaps-server*.tgz /app/ # Copy the static files for v1/health check COPY ./static/ /app/static/ -RUN npm install ./basemaps-landing*.tgz ./basemaps-cogify*.tgz ./basemaps-smoke*.tgz +RUN npm install ./basemaps-landing*.tgz ./basemaps-cogify*.tgz ./basemaps-smoke*.tgz ./basemaps-server*.tgz COPY dist/index.cjs /app/ diff --git a/packages/cli/package.json b/packages/cli/package.json index 8f28e4837..3e8715fc7 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@basemaps/cli", - "version": "7.11.0", + "version": "7.11.1", "private": false, "repository": { "type": "git", diff --git a/packages/cogify/CHANGELOG.md b/packages/cogify/CHANGELOG.md index 0a34dd6fc..892033caa 100644 --- a/packages/cogify/CHANGELOG.md +++ b/packages/cogify/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [7.11.1](https://github.com/linz/basemaps/compare/v7.11.0...v7.11.1) (2024-10-01) + +**Note:** Version bump only for package @basemaps/cogify + + + + + # [7.11.0](https://github.com/linz/basemaps/compare/v7.10.0...v7.11.0) (2024-09-29) **Note:** Version bump only for package @basemaps/cogify diff --git a/packages/cogify/package.json b/packages/cogify/package.json index b3ae873d0..723d790dd 100644 --- a/packages/cogify/package.json +++ b/packages/cogify/package.json @@ -1,6 +1,6 @@ { "name": "@basemaps/cogify", - "version": "7.11.0", + "version": "7.11.1", "private": false, "repository": { "type": "git", @@ -40,7 +40,7 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "devDependencies": { - "@basemaps/cli": "^7.11.0", + "@basemaps/cli": "^7.11.1", "@basemaps/config": "^7.11.0", "@basemaps/config-loader": "^7.11.0", "@basemaps/geo": "^7.11.0", diff --git a/packages/lambda-tiler/src/index.ts b/packages/lambda-tiler/src/index.ts index 8c5257e7d..f14ffbd38 100644 --- a/packages/lambda-tiler/src/index.ts +++ b/packages/lambda-tiler/src/index.ts @@ -6,6 +6,7 @@ import { configImageryGet, configTileSetGet } from './routes/config.js'; import { fontGet, fontList } from './routes/fonts.js'; import { healthGet } from './routes/health.js'; import { imageryGet } from './routes/imagery.js'; +import { linkGet } from './routes/link.js'; import { pingGet } from './routes/ping.js'; import { previewIndexGet } from './routes/preview.index.js'; import { tilePreviewGet } from './routes/preview.js'; @@ -102,6 +103,9 @@ handler.router.get('/v1/preview/:tileSet/:tileMatrix/:z/:lon/:lat/:outputType', handler.router.get('/v1/@:location', previewIndexGet); handler.router.get('/@:location', previewIndexGet); +// Link +handler.router.get('/v1/link/:tileSet', linkGet); + // Attribution handler.router.get('/v1/tiles/:tileSet/:tileMatrix/attribution.json', tileAttributionGet); handler.router.get('/v1/attribution/:tileSet/:tileMatrix/summary.json', tileAttributionGet); diff --git a/packages/lambda-tiler/src/routes/__tests__/link.test.ts b/packages/lambda-tiler/src/routes/__tests__/link.test.ts new file mode 100644 index 000000000..81a7700a4 --- /dev/null +++ b/packages/lambda-tiler/src/routes/__tests__/link.test.ts @@ -0,0 +1,114 @@ +import { strictEqual } from 'node:assert'; +import { afterEach, describe, it } from 'node:test'; + +import { ConfigProviderMemory } from '@basemaps/config'; +import { Epsg } from '@basemaps/geo'; + +import { FakeData, Imagery3857 } from '../../__tests__/config.data.js'; +import { mockRequest } from '../../__tests__/xyz.util.js'; +import { handler } from '../../index.js'; +import { ConfigLoader } from '../../util/config.loader.js'; + +describe('/v1/link/:tileSet', () => { + const FakeTileSetName = 'tileset'; + const config = new ConfigProviderMemory(); + + afterEach(() => { + config.objects.clear(); + }); + + /** + * 3xx status responses + */ + + // tileset found, is raster type, has one layer, has '3857' entry, imagery found > 302 response + it('success: redirect to pre-zoomed imagery', async (t) => { + t.mock.method(ConfigLoader, 'getDefaultConfig', () => Promise.resolve(config)); + + config.put(FakeData.tileSetRaster(FakeTileSetName)); + config.put(Imagery3857); + + const req = mockRequest(`/v1/link/${FakeTileSetName}`); + const res = await handler.router.handle(req); + + strictEqual(res.status, 302); + strictEqual(res.statusDescription, 'Redirect to pre-zoomed imagery'); + }); + + /** + * 4xx status responses + */ + + // tileset not found > 404 response + it('failure: tileset not found', async (t) => { + t.mock.method(ConfigLoader, 'getDefaultConfig', () => Promise.resolve(config)); + + const req = mockRequest(`/v1/link/${FakeTileSetName}`); + const res = await handler.router.handle(req); + + strictEqual(res.status, 404); + strictEqual(res.statusDescription, 'Tileset not found'); + }); + + // tileset found, not raster type > 400 response + it('failure: tileset must be raster type', async (t) => { + t.mock.method(ConfigLoader, 'getDefaultConfig', () => Promise.resolve(config)); + + config.put(FakeData.tileSetVector(FakeTileSetName)); + + const req = mockRequest(`/v1/link/${FakeTileSetName}`); + const res = await handler.router.handle(req); + + strictEqual(res.status, 400); + strictEqual(res.statusDescription, 'Tileset must be raster type'); + }); + + // tileset found, is raster type, has more than one layer > 400 response + it('failure: too many layers', async (t) => { + t.mock.method(ConfigLoader, 'getDefaultConfig', () => Promise.resolve(config)); + + const tileSet = FakeData.tileSetRaster(FakeTileSetName); + + // add another layer + tileSet.layers.push(tileSet.layers[0]); + + config.put(tileSet); + + const req = mockRequest(`/v1/link/${FakeTileSetName}`); + const res = await handler.router.handle(req); + + strictEqual(res.status, 400); + strictEqual(res.statusDescription, 'Too many layers'); + }); + + // tileset found, is raster type, has one layer, no '3857' entry > 400 response + it("failure: no imagery for '3857' projection", async (t) => { + t.mock.method(ConfigLoader, 'getDefaultConfig', () => Promise.resolve(config)); + + const tileSet = FakeData.tileSetRaster(FakeTileSetName); + + // delete '3857' entry + delete tileSet.layers[0][Epsg.Google.code]; + + config.put(tileSet); + + const req = mockRequest(`/v1/link/${FakeTileSetName}`); + const res = await handler.router.handle(req); + + strictEqual(res.status, 400); + strictEqual(res.statusDescription, "No imagery for '3857' projection"); + }); + + // tileset found, is raster type, has one layer, has '3857' entry, imagery not found > 400 response + it('failure: imagery not found', async (t) => { + t.mock.method(ConfigLoader, 'getDefaultConfig', () => Promise.resolve(config)); + + config.put(FakeData.tileSetRaster(FakeTileSetName)); + + const req = mockRequest(`/v1/link/${FakeTileSetName}`); + const res = await handler.router.handle(req); + + strictEqual(res.status, 400); + strictEqual(res.statusDescription, 'Imagery not found'); + }); +}); diff --git a/packages/lambda-tiler/src/routes/link.ts b/packages/lambda-tiler/src/routes/link.ts new file mode 100644 index 000000000..6eb150d4e --- /dev/null +++ b/packages/lambda-tiler/src/routes/link.ts @@ -0,0 +1,55 @@ +import { TileSetType } from '@basemaps/config'; +import { Epsg } from '@basemaps/geo'; +import { getPreviewUrl } from '@basemaps/shared'; +import { LambdaHttpRequest, LambdaHttpResponse } from '@linzjs/lambda'; + +import { ConfigLoader } from '../util/config.loader.js'; + +export interface LinkGet { + Params: { + tileSet: string; + }; +} + +/** + * Redirect the client to a Basemaps URL that is already zoomed to the extent of the tileset's imagery. + * + * /v1/link/:tileSet + * + * @example + * '/v1/link/ashburton-2023-0.1m' + * + * @returns on success, 302 redirect response. on failure, 4xx status code response. + */ +export async function linkGet(req: LambdaHttpRequest): Promise { + const config = await ConfigLoader.load(req); + + // get tileset + + req.timer.start('tileset:load'); + const tileSet = await config.TileSet.get(req.params.tileSet); + req.timer.end('tileset:load'); + + if (tileSet == null) return new LambdaHttpResponse(404, 'Tileset not found'); + + if (tileSet.type !== TileSetType.Raster) return new LambdaHttpResponse(400, 'Tileset must be raster type'); + + // TODO: add support for 'aerial' and 'elevation' multi-layer tilesets + if (tileSet.layers.length !== 1) return new LambdaHttpResponse(400, 'Too many layers'); + + // get imagery + + const imageryId = tileSet.layers[0][Epsg.Google.code]; + if (imageryId === undefined) return new LambdaHttpResponse(400, "No imagery for '3857' projection"); + + const imagery = await config.Imagery.get(imageryId); + if (imagery == null) return new LambdaHttpResponse(400, 'Imagery not found'); + + // do redirect + + const url = getPreviewUrl({ imagery }); + + return new LambdaHttpResponse(302, 'Redirect to pre-zoomed imagery', { + location: `/${url.slug}?i=${url.name}`, + }); +}