@basemaps/lambda-tiler 7.9.0 → 7.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/build/__tests__/tile.style.json.test.js +13 -7
- package/build/__tests__/tile.style.json.test.js.map +1 -1
- package/build/routes/__tests__/health.test.js +40 -20
- package/build/routes/__tests__/health.test.js.map +1 -1
- package/build/routes/__tests__/tile.style.json.test.js +57 -0
- package/build/routes/__tests__/tile.style.json.test.js.map +1 -1
- package/build/routes/__tests__/xyz.test.js +13 -0
- package/build/routes/__tests__/xyz.test.js.map +1 -1
- package/build/routes/health.d.ts +17 -0
- package/build/routes/health.js +119 -21
- package/build/routes/health.js.map +1 -1
- package/build/routes/tile.style.json.d.ts +35 -13
- package/build/routes/tile.style.json.js +108 -123
- package/build/routes/tile.style.json.js.map +1 -1
- package/build/routes/tile.xyz.vector.js +9 -9
- package/build/routes/tile.xyz.vector.js.map +1 -1
- package/build/util/__test__/nztm.style.test.d.ts +1 -0
- package/build/util/__test__/nztm.style.test.js +87 -0
- package/build/util/__test__/nztm.style.test.js.map +1 -0
- package/build/util/nztm.style.d.ts +12 -0
- package/build/util/nztm.style.js +45 -0
- package/build/util/nztm.style.js.map +1 -0
- package/package.json +7 -6
- package/src/__tests__/tile.style.json.test.ts +16 -7
- package/src/routes/__tests__/health.test.ts +46 -22
- package/src/routes/__tests__/tile.style.json.test.ts +60 -0
- package/src/routes/__tests__/xyz.test.ts +18 -0
- package/src/routes/health.ts +129 -21
- package/src/routes/tile.style.json.ts +131 -145
- package/src/routes/tile.xyz.vector.ts +10 -6
- package/src/util/__test__/nztm.style.test.ts +100 -0
- package/src/util/nztm.style.ts +44 -0
- package/tsconfig.tsbuildinfo +1 -1
package/src/routes/health.ts
CHANGED
|
@@ -1,23 +1,67 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
|
|
3
|
-
import { ConfigTileSetRaster } from '@basemaps/config';
|
|
3
|
+
import { ConfigTileSetRaster, ConfigTileSetVector, TileSetType } from '@basemaps/config';
|
|
4
4
|
import { GoogleTms, Nztm2000QuadTms } from '@basemaps/geo';
|
|
5
5
|
import { HttpHeader, LambdaHttpRequest, LambdaHttpResponse } from '@linzjs/lambda';
|
|
6
|
+
import { VectorTile } from '@mapbox/vector-tile';
|
|
7
|
+
import Protobuf from 'pbf';
|
|
6
8
|
import PixelMatch from 'pixelmatch';
|
|
7
9
|
import Sharp from 'sharp';
|
|
10
|
+
import { gunzipSync } from 'zlib';
|
|
8
11
|
|
|
9
12
|
import { ConfigLoader } from '../util/config.loader.js';
|
|
13
|
+
import { isGzip } from '../util/cotar.serve.js';
|
|
10
14
|
import { TileXyz } from '../util/validate.js';
|
|
11
15
|
import { TileXyzRaster } from './tile.xyz.raster.js';
|
|
16
|
+
import { tileXyzVector } from './tile.xyz.vector.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Vector feature that need to check existence
|
|
20
|
+
*/
|
|
21
|
+
export interface TestFeature {
|
|
22
|
+
layer: string;
|
|
23
|
+
key: string;
|
|
24
|
+
value: string;
|
|
25
|
+
}
|
|
12
26
|
|
|
13
27
|
interface TestTile extends TileXyz {
|
|
14
28
|
buf?: Buffer;
|
|
29
|
+
testFeatures?: TestFeature[];
|
|
15
30
|
}
|
|
16
31
|
|
|
17
32
|
export const TestTiles: TestTile[] = [
|
|
18
33
|
{ tileSet: 'health', tileMatrix: GoogleTms, tileType: 'png', tile: { x: 252, y: 156, z: 8 } },
|
|
19
34
|
{ tileSet: 'health', tileMatrix: Nztm2000QuadTms, tileType: 'png', tile: { x: 30, y: 33, z: 6 } },
|
|
35
|
+
{
|
|
36
|
+
tileSet: 'topographic',
|
|
37
|
+
tileMatrix: GoogleTms,
|
|
38
|
+
tileType: 'pbf',
|
|
39
|
+
tile: { x: 1009, y: 641, z: 10 },
|
|
40
|
+
testFeatures: [
|
|
41
|
+
{ layer: 'aeroway', key: 'name', value: 'Wellington Airport' },
|
|
42
|
+
{ layer: 'place', key: 'name', value: 'Wellington' },
|
|
43
|
+
{ layer: 'coastline', key: 'class', value: 'coastline' },
|
|
44
|
+
{ layer: 'landcover', key: 'class', value: 'grass' },
|
|
45
|
+
{ layer: 'poi', key: 'name', value: 'Seatoun Wharf' },
|
|
46
|
+
{ layer: 'transportation', key: 'name', value: 'Mt Victoria Tunnel' },
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
tileSet: 'topographic',
|
|
51
|
+
tileMatrix: GoogleTms,
|
|
52
|
+
tileType: 'pbf',
|
|
53
|
+
tile: { x: 62, y: 40, z: 6 },
|
|
54
|
+
testFeatures: [
|
|
55
|
+
{ layer: 'landuse', key: 'name', value: 'Queenstown' },
|
|
56
|
+
{ layer: 'place', key: 'name', value: 'Christchurch' },
|
|
57
|
+
{ layer: 'water', key: 'name', value: 'Tasman Lake' },
|
|
58
|
+
{ layer: 'coastline', key: 'class', value: 'coastline' },
|
|
59
|
+
{ layer: 'landcover', key: 'class', value: 'wood' },
|
|
60
|
+
{ layer: 'transportation', key: 'name', value: 'STATE HIGHWAY 6' },
|
|
61
|
+
],
|
|
62
|
+
},
|
|
20
63
|
];
|
|
64
|
+
|
|
21
65
|
const TileSize = 256;
|
|
22
66
|
|
|
23
67
|
export async function getTestBuffer(test: TestTile): Promise<Buffer> {
|
|
@@ -40,6 +84,82 @@ export async function updateExpectedTile(test: TestTile, newTileData: Buffer, di
|
|
|
40
84
|
await fs.promises.writeFile(`${expectedFileName}.diff.png`, imgPng);
|
|
41
85
|
}
|
|
42
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Compare and validate the raster test tile from server with pixel match
|
|
89
|
+
*/
|
|
90
|
+
async function validateRasterTile(tileSet: ConfigTileSetRaster, test: TestTile, req: LambdaHttpRequest): Promise<void> {
|
|
91
|
+
// Get the parse response tile to raw buffer
|
|
92
|
+
const response = await TileXyzRaster.tile(req, tileSet, test);
|
|
93
|
+
if (response.status !== 200) throw new LambdaHttpResponse(500, response.statusDescription);
|
|
94
|
+
if (!Buffer.isBuffer(response._body)) throw new LambdaHttpResponse(500, 'Not a Buffer response content.');
|
|
95
|
+
const resImgBuffer = await Sharp(response._body).raw().toBuffer();
|
|
96
|
+
|
|
97
|
+
// Get test tile to compare
|
|
98
|
+
const testBuffer = await getTestBuffer(test);
|
|
99
|
+
test.buf = testBuffer;
|
|
100
|
+
const testImgBuffer = await Sharp(testBuffer).raw().toBuffer();
|
|
101
|
+
|
|
102
|
+
const outputBuffer = Buffer.alloc(testImgBuffer.length);
|
|
103
|
+
const missMatchedPixels = PixelMatch(testImgBuffer, resImgBuffer, outputBuffer, TileSize, TileSize);
|
|
104
|
+
if (missMatchedPixels) {
|
|
105
|
+
/** Uncomment this to overwite the expected files */
|
|
106
|
+
// await updateExpectedTile(test, response._body as Buffer, outputBuffer);
|
|
107
|
+
req.log.error({ missMatchedPixels, projection: test.tileMatrix.identifier, xyz: test.tile }, 'Health:MissMatch');
|
|
108
|
+
throw new LambdaHttpResponse(500, 'TileSet does not match.');
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function checkFeatureExists(tile: VectorTile, testFeature: TestFeature): boolean {
|
|
113
|
+
const layer = tile.layers[testFeature.layer];
|
|
114
|
+
for (let i = 0; i < layer.length; i++) {
|
|
115
|
+
const feature = layer.feature(i);
|
|
116
|
+
if (feature.properties[testFeature.key] === testFeature.value) return true;
|
|
117
|
+
}
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Fetch vector tile and decode into mapbox VectorTile
|
|
123
|
+
*/
|
|
124
|
+
export const VectorTileProvider = {
|
|
125
|
+
async getVectorTile(tileSet: ConfigTileSetVector, test: TestTile, req: LambdaHttpRequest): Promise<VectorTile> {
|
|
126
|
+
// Get the parse response tile to raw buffer
|
|
127
|
+
const response = await tileXyzVector.tile(req, tileSet, test);
|
|
128
|
+
if (response.status !== 200) throw new LambdaHttpResponse(500, response.statusDescription);
|
|
129
|
+
if (!Buffer.isBuffer(response._body)) throw new LambdaHttpResponse(500, 'Not a Buffer response content.');
|
|
130
|
+
const buffer = isGzip(response._body) ? gunzipSync(response._body) : response._body;
|
|
131
|
+
return new VectorTile(new Protobuf(buffer));
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Check the existence of a feature property in side the vector tile
|
|
137
|
+
*
|
|
138
|
+
* @throws LambdaHttpResponse if any test feature not found from vector tile
|
|
139
|
+
*/
|
|
140
|
+
function featureCheck(tile: VectorTile, testTile: TestTile): void {
|
|
141
|
+
const testTileName = `${testTile.tileSet}-${testTile.tile.x}/${testTile.tile.y}/z${testTile.tile.z}`;
|
|
142
|
+
if (testTile.testFeatures == null) {
|
|
143
|
+
throw new LambdaHttpResponse(500, `No test feature found from testTile: ${testTileName}`);
|
|
144
|
+
}
|
|
145
|
+
for (const testFeature of testTile.testFeatures) {
|
|
146
|
+
if (!checkFeatureExists(tile, testFeature)) {
|
|
147
|
+
throw new LambdaHttpResponse(500, `Failed to validate tile: ${testTileName} for layer: ${testFeature.layer}.`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Health check the test vector tiles that contains all the expected features.
|
|
154
|
+
*
|
|
155
|
+
* @throws LambdaHttpResponse if test tiles not returned or features not exists
|
|
156
|
+
*/
|
|
157
|
+
async function validateVectorTile(tileSet: ConfigTileSetVector, test: TestTile, req: LambdaHttpRequest): Promise<void> {
|
|
158
|
+
// Get the parse response tile to raw buffer
|
|
159
|
+
const tile = await VectorTileProvider.getVectorTile(tileSet, test, req);
|
|
160
|
+
featureCheck(tile, test);
|
|
161
|
+
}
|
|
162
|
+
|
|
43
163
|
/**
|
|
44
164
|
* Health request get health TileSets and validate with test TileSets
|
|
45
165
|
* - Valid response from get heath tile request
|
|
@@ -49,27 +169,15 @@ export async function updateExpectedTile(test: TestTile, newTileData: Buffer, di
|
|
|
49
169
|
*/
|
|
50
170
|
export async function healthGet(req: LambdaHttpRequest): Promise<LambdaHttpResponse> {
|
|
51
171
|
const config = await ConfigLoader.load(req);
|
|
52
|
-
const tileSet = await config.TileSet.get(config.TileSet.id('health'));
|
|
53
|
-
if (tileSet == null) throw new LambdaHttpResponse(500, 'TileSet: "health" not found');
|
|
54
172
|
for (const test of TestTiles) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if (
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
test.buf = testBuffer;
|
|
64
|
-
const testImgBuffer = await Sharp(testBuffer).raw().toBuffer();
|
|
65
|
-
|
|
66
|
-
const outputBuffer = Buffer.alloc(testImgBuffer.length);
|
|
67
|
-
const missMatchedPixels = PixelMatch(testImgBuffer, resImgBuffer, outputBuffer, TileSize, TileSize);
|
|
68
|
-
if (missMatchedPixels) {
|
|
69
|
-
/** Uncomment this to overwite the expected files */
|
|
70
|
-
// await updateExpectedTile(test, response._body as Buffer, outputBuffer);
|
|
71
|
-
req.log.error({ missMatchedPixels, projection: test.tileMatrix.identifier, xyz: test.tile }, 'Health:MissMatch');
|
|
72
|
-
return new LambdaHttpResponse(500, 'TileSet does not match.');
|
|
173
|
+
const tileSet = await config.TileSet.get(config.TileSet.id(test.tileSet));
|
|
174
|
+
if (tileSet == null) throw new LambdaHttpResponse(500, `TileSet: ${test.tileSet} not found`);
|
|
175
|
+
if (tileSet.type === TileSetType.Raster) {
|
|
176
|
+
await validateRasterTile(tileSet, test, req);
|
|
177
|
+
} else if (tileSet.type === TileSetType.Vector) {
|
|
178
|
+
await validateVectorTile(tileSet, test, req);
|
|
179
|
+
} else {
|
|
180
|
+
throw new LambdaHttpResponse(500, `Invalid TileSet type for tileSet ${test.tileSet}`);
|
|
73
181
|
}
|
|
74
182
|
}
|
|
75
183
|
|
|
@@ -1,20 +1,41 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
BasemapsConfigProvider,
|
|
3
|
+
ConfigId,
|
|
4
|
+
ConfigPrefix,
|
|
5
|
+
ConfigTileSetRaster,
|
|
6
|
+
Layer,
|
|
7
|
+
Sources,
|
|
8
|
+
StyleJson,
|
|
9
|
+
TileSetType,
|
|
10
|
+
} from '@basemaps/config';
|
|
2
11
|
import { DefaultExaggeration } from '@basemaps/config/build/config/vector.style.js';
|
|
3
|
-
import { GoogleTms, TileMatrixSet, TileMatrixSets } from '@basemaps/geo';
|
|
12
|
+
import { GoogleTms, Nztm2000QuadTms, TileMatrixSet, TileMatrixSets } from '@basemaps/geo';
|
|
4
13
|
import { Env, toQueryString } from '@basemaps/shared';
|
|
5
14
|
import { HttpHeader, LambdaHttpRequest, LambdaHttpResponse } from '@linzjs/lambda';
|
|
6
15
|
import { URL } from 'url';
|
|
7
16
|
|
|
8
17
|
import { ConfigLoader } from '../util/config.loader.js';
|
|
9
18
|
import { Etag } from '../util/etag.js';
|
|
19
|
+
import { convertStyleToNztmStyle } from '../util/nztm.style.js';
|
|
10
20
|
import { NotFound, NotModified } from '../util/response.js';
|
|
11
21
|
import { Validate } from '../util/validate.js';
|
|
12
22
|
|
|
13
23
|
/**
|
|
14
|
-
* Convert relative
|
|
24
|
+
* Convert relative URL into a full hostname URL, converting {tileMatrix} into the provided tileMatrix
|
|
25
|
+
*
|
|
26
|
+
* Will also add query parameters of apiKey and configuration if provided
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```typescript
|
|
30
|
+
* convertRelativeUrl("/v1/tiles/aerial/{tileMatrix}/{z}/{x}/{y}.webp", NZTM2000Quad)
|
|
31
|
+
* "https://basemaps.linz.govt.nz/v1/tiles/aerial/NZTM2000Quad/{z}/{x}/{y}.webp?api=c..."
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
15
34
|
* @param url possible url to update
|
|
16
35
|
* @param apiKey ApiKey to append with ?api= if required
|
|
17
|
-
* @
|
|
36
|
+
* @param tileMatrix replace {tileMatrix} with the tile matrix
|
|
37
|
+
*
|
|
38
|
+
* @returns Updated URL or empty string if url is empty
|
|
18
39
|
*/
|
|
19
40
|
export function convertRelativeUrl(
|
|
20
41
|
url?: string,
|
|
@@ -33,20 +54,17 @@ export function convertRelativeUrl(
|
|
|
33
54
|
}
|
|
34
55
|
|
|
35
56
|
/**
|
|
36
|
-
*
|
|
57
|
+
* Update the style JSON to have absolute urls to the current host and API Keys where required
|
|
58
|
+
*
|
|
37
59
|
* @param style style to update
|
|
60
|
+
* @param tileMatrix convert the tile matrix to the target tile matrix
|
|
38
61
|
* @param apiKey api key to inject
|
|
39
|
-
* @
|
|
62
|
+
* @param config optional configuration url to use
|
|
63
|
+
* @param layers replace the layers in the style json
|
|
64
|
+
* @returns new style JSON
|
|
40
65
|
*/
|
|
41
|
-
export function
|
|
42
|
-
style
|
|
43
|
-
tileMatrix: TileMatrixSet,
|
|
44
|
-
apiKey: string,
|
|
45
|
-
config: string | null,
|
|
46
|
-
layers?: Layer[],
|
|
47
|
-
): StyleJson {
|
|
48
|
-
const sources = JSON.parse(JSON.stringify(style.sources)) as Sources;
|
|
49
|
-
for (const [key, value] of Object.entries(sources)) {
|
|
66
|
+
export function setStyleUrls(style: StyleJson, tileMatrix: TileMatrixSet, apiKey: string, config: string | null): void {
|
|
67
|
+
for (const [key, value] of Object.entries(style.sources ?? {})) {
|
|
50
68
|
if (value.type === 'vector') {
|
|
51
69
|
value.url = convertRelativeUrl(value.url, tileMatrix, apiKey, config);
|
|
52
70
|
} else if ((value.type === 'raster' || value.type === 'raster-dem') && Array.isArray(value.tiles)) {
|
|
@@ -54,30 +72,11 @@ export function convertStyleJson(
|
|
|
54
72
|
value.tiles[i] = convertRelativeUrl(value.tiles[i], tileMatrix, apiKey, config);
|
|
55
73
|
}
|
|
56
74
|
}
|
|
57
|
-
sources[key] = value;
|
|
75
|
+
style.sources[key] = value;
|
|
58
76
|
}
|
|
59
77
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
id: style.id,
|
|
63
|
-
name: style.name,
|
|
64
|
-
sources,
|
|
65
|
-
layers: layers ? layers : style.layers,
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
if (style.metadata) styleJson.metadata = style.metadata;
|
|
69
|
-
if (style.glyphs) styleJson.glyphs = convertRelativeUrl(style.glyphs, undefined, undefined, config);
|
|
70
|
-
if (style.sprite) styleJson.sprite = convertRelativeUrl(style.sprite, undefined, undefined, config);
|
|
71
|
-
if (style.sky) styleJson.sky = style.sky;
|
|
72
|
-
if (style.terrain) styleJson.terrain = style.terrain;
|
|
73
|
-
|
|
74
|
-
return styleJson;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export interface StyleGet {
|
|
78
|
-
Params: {
|
|
79
|
-
styleName: string;
|
|
80
|
-
};
|
|
78
|
+
if (style.glyphs) style.glyphs = convertRelativeUrl(style.glyphs, undefined, undefined, config);
|
|
79
|
+
if (style.sprite) style.sprite = convertRelativeUrl(style.sprite, undefined, undefined, config);
|
|
81
80
|
}
|
|
82
81
|
|
|
83
82
|
export interface StyleConfig {
|
|
@@ -87,15 +86,21 @@ export interface StyleConfig {
|
|
|
87
86
|
labels: boolean;
|
|
88
87
|
}
|
|
89
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Turn on the terrain setting in the style json
|
|
91
|
+
*/
|
|
90
92
|
function setStyleTerrain(style: StyleJson, terrain: string, tileMatrix: TileMatrixSet): void {
|
|
91
93
|
const source = Object.keys(style.sources).find((s) => s === terrain);
|
|
92
|
-
if (source == null) throw new LambdaHttpResponse(400, `Terrain: ${terrain}
|
|
94
|
+
if (source == null) throw new LambdaHttpResponse(400, `Terrain: ${terrain} does not exists in the style source.`);
|
|
93
95
|
style.terrain = {
|
|
94
96
|
source,
|
|
95
97
|
exaggeration: DefaultExaggeration[tileMatrix.identifier] ?? DefaultExaggeration[GoogleTms.identifier],
|
|
96
98
|
};
|
|
97
99
|
}
|
|
98
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Merge the "labels" layer into the style json
|
|
103
|
+
*/
|
|
99
104
|
async function setStyleLabels(req: LambdaHttpRequest<StyleGet>, style: StyleJson): Promise<void> {
|
|
100
105
|
const config = await ConfigLoader.load(req);
|
|
101
106
|
const labels = await config.Style.get('labels');
|
|
@@ -122,6 +127,9 @@ async function setStyleLabels(req: LambdaHttpRequest<StyleGet>, style: StyleJson
|
|
|
122
127
|
style.layers = style.layers.concat(labels.style.layers);
|
|
123
128
|
}
|
|
124
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Ensure that a "LINZ-Terrain" layer is force added into the output styleJSON source
|
|
132
|
+
*/
|
|
125
133
|
async function ensureTerrain(
|
|
126
134
|
req: LambdaHttpRequest<StyleGet>,
|
|
127
135
|
tileMatrix: TileMatrixSet,
|
|
@@ -136,20 +144,26 @@ async function ensureTerrain(
|
|
|
136
144
|
style.sources['LINZ-Terrain'] = {
|
|
137
145
|
type: 'raster-dem',
|
|
138
146
|
tileSize: 256,
|
|
139
|
-
maxzoom: 18,
|
|
147
|
+
maxzoom: 18, // TODO: this should be configurable based on the elevation layer
|
|
140
148
|
tiles: [convertRelativeUrl(`/v1/tiles/elevation/${tileMatrix.identifier}/{z}/{x}/{y}.png${elevationQuery}`)],
|
|
141
149
|
};
|
|
142
150
|
}
|
|
143
151
|
|
|
144
|
-
|
|
152
|
+
/**
|
|
153
|
+
* Generate a StyleJSON from a tileset
|
|
154
|
+
* @returns
|
|
155
|
+
*/
|
|
156
|
+
export function tileSetToStyle(
|
|
145
157
|
req: LambdaHttpRequest<StyleGet>,
|
|
146
158
|
tileSet: ConfigTileSetRaster,
|
|
147
159
|
tileMatrix: TileMatrixSet,
|
|
148
160
|
apiKey: string,
|
|
149
|
-
|
|
150
|
-
|
|
161
|
+
): StyleJson {
|
|
162
|
+
// If the style has outputs defined it has a different process for generating the stylejson
|
|
163
|
+
if (tileSet.outputs) return tileSetOutputToStyle(req, tileSet, tileMatrix, apiKey);
|
|
164
|
+
|
|
151
165
|
const [tileFormat] = Validate.getRequestedFormats(req) ?? ['webp'];
|
|
152
|
-
if (tileFormat == null)
|
|
166
|
+
if (tileFormat == null) throw new LambdaHttpResponse(400, 'Invalid image format');
|
|
153
167
|
|
|
154
168
|
const pipeline = Validate.pipeline(tileSet, tileFormat, req.query.get('pipeline'));
|
|
155
169
|
const pipelineName = pipeline?.name === 'rgba' ? undefined : pipeline?.name;
|
|
@@ -162,162 +176,134 @@ export async function tileSetToStyle(
|
|
|
162
176
|
`/v1/tiles/${tileSet.name}/${tileMatrix.identifier}/{z}/{x}/{y}.${tileFormat}${query}`;
|
|
163
177
|
|
|
164
178
|
const styleId = `basemaps-${tileSet.name}`;
|
|
165
|
-
|
|
179
|
+
return {
|
|
166
180
|
id: ConfigId.prefix(ConfigPrefix.Style, tileSet.name),
|
|
167
181
|
name: tileSet.name,
|
|
168
182
|
version: 8,
|
|
169
183
|
sources: { [styleId]: { type: 'raster', tiles: [tileUrl], tileSize: 256 } },
|
|
170
184
|
layers: [{ id: styleId, type: 'raster', source: styleId }],
|
|
171
185
|
};
|
|
172
|
-
|
|
173
|
-
// Ensure elevation for individual tilesets
|
|
174
|
-
await ensureTerrain(req, tileMatrix, apiKey, style);
|
|
175
|
-
|
|
176
|
-
// Add terrain in style
|
|
177
|
-
if (cfg.terrain) setStyleTerrain(style, cfg.terrain, tileMatrix);
|
|
178
|
-
if (cfg.labels) await setStyleLabels(req, style);
|
|
179
|
-
|
|
180
|
-
const data = Buffer.from(JSON.stringify(convertStyleJson(style, tileMatrix, apiKey, configLocation)));
|
|
181
|
-
|
|
182
|
-
const cacheKey = Etag.key(data);
|
|
183
|
-
if (Etag.isNotModified(req, cacheKey)) return NotModified();
|
|
184
|
-
|
|
185
|
-
const response = new LambdaHttpResponse(200, 'ok');
|
|
186
|
-
response.header(HttpHeader.ETag, cacheKey);
|
|
187
|
-
response.header(HttpHeader.CacheControl, 'no-store');
|
|
188
|
-
response.buffer(data, 'application/json');
|
|
189
|
-
req.set('bytes', data.byteLength);
|
|
190
|
-
return response;
|
|
191
186
|
}
|
|
192
187
|
|
|
193
|
-
|
|
188
|
+
/**
|
|
189
|
+
* generate a style from a tile set which has a output
|
|
190
|
+
*/
|
|
191
|
+
export function tileSetOutputToStyle(
|
|
194
192
|
req: LambdaHttpRequest<StyleGet>,
|
|
195
193
|
tileSet: ConfigTileSetRaster,
|
|
196
194
|
tileMatrix: TileMatrixSet,
|
|
197
195
|
apiKey: string,
|
|
198
|
-
|
|
199
|
-
)
|
|
196
|
+
): StyleJson {
|
|
197
|
+
if (tileSet.outputs == null) throw new LambdaHttpResponse(400, 'TileSet does not have any outputs to generate');
|
|
200
198
|
const configLocation = ConfigLoader.extract(req);
|
|
201
|
-
const query = toQueryString({ config: configLocation, api: apiKey });
|
|
202
199
|
|
|
203
200
|
const styleId = `basemaps-${tileSet.name}`;
|
|
204
201
|
const sources: Sources = {};
|
|
205
202
|
const layers: Layer[] = [];
|
|
206
203
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
sources[`${styleId}-${output.name}-dem`] = {
|
|
222
|
-
type: 'raster-dem',
|
|
223
|
-
tiles: [tileUrl + `&pipeline=${output.name}`],
|
|
224
|
-
tileSize: 256,
|
|
225
|
-
};
|
|
226
|
-
} else {
|
|
227
|
-
// Add raster source other outputs
|
|
228
|
-
sources[`${styleId}-${output.name}`] = {
|
|
229
|
-
type: 'raster',
|
|
230
|
-
tiles: [tileUrl + `&pipeline=${output.name}`],
|
|
231
|
-
tileSize: 256,
|
|
232
|
-
};
|
|
233
|
-
}
|
|
204
|
+
for (const output of tileSet.outputs) {
|
|
205
|
+
const format = output.format?.[0] ?? 'webp';
|
|
206
|
+
const urlBase = Env.get(Env.PublicUrlBase) ?? '';
|
|
207
|
+
const query = toQueryString({ config: configLocation, api: apiKey, pipeline: output.name });
|
|
208
|
+
|
|
209
|
+
const tileUrl = `${urlBase}/v1/tiles/${tileSet.name}/${tileMatrix.identifier}/{z}/{x}/{y}.${format}${query}`;
|
|
210
|
+
|
|
211
|
+
if (output.name === 'terrain-rgb') {
|
|
212
|
+
// Add both raster source and dem raster source for terrain-rgb output
|
|
213
|
+
sources[`${styleId}-${output.name}`] = { type: 'raster', tiles: [tileUrl], tileSize: 256 };
|
|
214
|
+
sources[`${styleId}-${output.name}-dem`] = { type: 'raster-dem', tiles: [tileUrl], tileSize: 256 };
|
|
215
|
+
} else {
|
|
216
|
+
// Add raster source other outputs
|
|
217
|
+
sources[`${styleId}-${output.name}`] = { type: 'raster', tiles: [tileUrl], tileSize: 256 };
|
|
234
218
|
}
|
|
235
219
|
}
|
|
236
220
|
|
|
237
221
|
// Add first raster source as default layer
|
|
238
222
|
for (const source of Object.keys(sources)) {
|
|
239
223
|
if (sources[source].type === 'raster') {
|
|
240
|
-
layers.push({
|
|
241
|
-
id: styleId,
|
|
242
|
-
type: 'raster',
|
|
243
|
-
source,
|
|
244
|
-
});
|
|
224
|
+
layers.push({ id: styleId, type: 'raster', source });
|
|
245
225
|
break;
|
|
246
226
|
}
|
|
247
227
|
}
|
|
248
228
|
|
|
249
|
-
|
|
229
|
+
return {
|
|
250
230
|
id: ConfigId.prefix(ConfigPrefix.Style, tileSet.name),
|
|
251
231
|
name: tileSet.name,
|
|
252
232
|
version: 8,
|
|
253
233
|
sources,
|
|
254
234
|
layers,
|
|
255
235
|
};
|
|
236
|
+
}
|
|
256
237
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
238
|
+
async function generateStyleFromTileSet(
|
|
239
|
+
req: LambdaHttpRequest<StyleGet>,
|
|
240
|
+
config: BasemapsConfigProvider,
|
|
241
|
+
tileSetName: string,
|
|
242
|
+
tileMatrix: TileMatrixSet,
|
|
243
|
+
apiKey: string,
|
|
244
|
+
): Promise<StyleJson> {
|
|
245
|
+
const tileSet = await config.TileSet.get(tileSetName);
|
|
246
|
+
if (tileSet == null) throw NotFound();
|
|
247
|
+
if (tileSet.type !== TileSetType.Raster) {
|
|
248
|
+
throw new LambdaHttpResponse(400, 'Only raster tile sets can generate style JSON');
|
|
249
|
+
}
|
|
250
|
+
if (tileSet.outputs) return tileSetOutputToStyle(req, tileSet, tileMatrix, apiKey);
|
|
251
|
+
else return tileSetToStyle(req, tileSet, tileMatrix, apiKey);
|
|
252
|
+
}
|
|
268
253
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
req.set('bytes', data.byteLength);
|
|
274
|
-
return Promise.resolve(response);
|
|
254
|
+
export interface StyleGet {
|
|
255
|
+
Params: {
|
|
256
|
+
styleName: string;
|
|
257
|
+
};
|
|
275
258
|
}
|
|
276
259
|
|
|
277
260
|
export async function styleJsonGet(req: LambdaHttpRequest<StyleGet>): Promise<LambdaHttpResponse> {
|
|
278
261
|
const apiKey = Validate.apiKey(req);
|
|
279
262
|
const styleName = req.params.styleName;
|
|
280
|
-
|
|
281
|
-
const excluded = new Set(excludeLayers.map((l) => l.toLowerCase()));
|
|
263
|
+
|
|
282
264
|
const tileMatrix = TileMatrixSets.find(req.query.get('tileMatrix') ?? GoogleTms.identifier);
|
|
283
265
|
if (tileMatrix == null) return new LambdaHttpResponse(400, 'Invalid tile matrix');
|
|
266
|
+
|
|
267
|
+
// Remove layers from the output style json
|
|
268
|
+
const excludeLayers = req.query.getAll('exclude');
|
|
269
|
+
const excluded = new Set(excludeLayers.map((l) => l.toLowerCase()));
|
|
270
|
+
if (excluded.size > 0) req.set('excludedLayers', [...excluded]);
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Configuration options used for the landing page:
|
|
274
|
+
* "terrain" - force add a terrain layer
|
|
275
|
+
* "labels" - merge the labels style with the current style
|
|
276
|
+
*
|
|
277
|
+
* TODO: (2024-08) this is not a very scalable way of configuring styles, it would be good to provide a styleJSON merge
|
|
278
|
+
*/
|
|
284
279
|
const terrain = req.query.get('terrain') ?? undefined;
|
|
285
280
|
const labels = Boolean(req.query.get('labels') ?? false);
|
|
281
|
+
req.set('styleConfig', { terrain, labels });
|
|
286
282
|
|
|
287
283
|
// Get style Config from db
|
|
288
284
|
const config = await ConfigLoader.load(req);
|
|
289
|
-
const
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
req.set('styleConfig', { terrain, labels });
|
|
293
|
-
|
|
294
|
-
if (styleConfig == null) {
|
|
295
|
-
// Were we given a tileset name instead, generated
|
|
296
|
-
const tileSet = await config.TileSet.get(config.TileSet.id(styleName));
|
|
297
|
-
if (tileSet == null) return NotFound();
|
|
298
|
-
if (tileSet.type !== TileSetType.Raster) return NotFound();
|
|
299
|
-
if (tileSet.outputs) return await tileSetOutputToStyle(req, tileSet, tileMatrix, apiKey, { terrain, labels });
|
|
300
|
-
else return await tileSetToStyle(req, tileSet, tileMatrix, apiKey, { terrain, labels });
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// Prepare sources and add linz source
|
|
304
|
-
const style = convertStyleJson(
|
|
305
|
-
styleConfig.style,
|
|
306
|
-
tileMatrix,
|
|
307
|
-
apiKey,
|
|
308
|
-
ConfigLoader.extract(req),
|
|
309
|
-
styleConfig.style.layers.filter((f) => !excluded.has(f.id.toLowerCase())),
|
|
310
|
-
);
|
|
285
|
+
const styleConfig = await config.Style.get(styleName);
|
|
286
|
+
const styleSource =
|
|
287
|
+
styleConfig?.style ?? (await generateStyleFromTileSet(req, config, styleName, tileMatrix, apiKey));
|
|
311
288
|
|
|
289
|
+
const targetStyle = structuredClone(styleSource);
|
|
312
290
|
// Ensure elevation for style json config
|
|
313
291
|
// TODO: We should remove this after adding terrain source into style configs. PR-916
|
|
314
|
-
await ensureTerrain(req, tileMatrix, apiKey,
|
|
292
|
+
await ensureTerrain(req, tileMatrix, apiKey, targetStyle);
|
|
315
293
|
|
|
316
294
|
// Add terrain in style
|
|
317
|
-
if (terrain) setStyleTerrain(
|
|
318
|
-
if (labels) await setStyleLabels(req,
|
|
295
|
+
if (terrain) setStyleTerrain(targetStyle, terrain, tileMatrix);
|
|
296
|
+
if (labels) await setStyleLabels(req, targetStyle);
|
|
297
|
+
|
|
298
|
+
// convert sources to full URLS and convert style between projections
|
|
299
|
+
setStyleUrls(targetStyle, tileMatrix, apiKey, ConfigLoader.extract(req));
|
|
300
|
+
|
|
301
|
+
if (tileMatrix.identifier === Nztm2000QuadTms.identifier) convertStyleToNztmStyle(targetStyle, false);
|
|
302
|
+
|
|
303
|
+
// filter out any excluded layers
|
|
304
|
+
if (excluded.size > 0) targetStyle.layers = targetStyle.layers.filter((f) => !excluded.has(f.id.toLowerCase()));
|
|
319
305
|
|
|
320
|
-
const data = Buffer.from(JSON.stringify(
|
|
306
|
+
const data = Buffer.from(JSON.stringify(targetStyle));
|
|
321
307
|
|
|
322
308
|
const cacheKey = Etag.key(data);
|
|
323
309
|
if (Etag.isNotModified(req, cacheKey)) return NotModified();
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { ConfigTileSetVector } from '@basemaps/config';
|
|
2
|
-
import { GoogleTms } from '@basemaps/geo';
|
|
3
2
|
import { fsa } from '@basemaps/shared';
|
|
4
3
|
import { HttpHeader, LambdaHttpRequest, LambdaHttpResponse } from '@linzjs/lambda';
|
|
5
4
|
|
|
@@ -13,12 +12,17 @@ export const tileXyzVector = {
|
|
|
13
12
|
/** Serve a MVT vector tile */
|
|
14
13
|
async tile(req: LambdaHttpRequest, tileSet: ConfigTileSetVector, xyz: TileXyz): Promise<LambdaHttpResponse> {
|
|
15
14
|
if (xyz.tileType !== 'pbf') return NotFound();
|
|
16
|
-
if (xyz.tileMatrix.identifier !== GoogleTms.identifier) return NotFound();
|
|
17
15
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
// Vector tiles cannot be merged (yet!)
|
|
17
|
+
if (tileSet.layers.length > 1) {
|
|
18
|
+
return new LambdaHttpResponse(500, `Too many layers in vector tileset ${tileSet.layers.length}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const epsgCode = xyz.tileMatrix.projection.code;
|
|
22
|
+
const layerId = tileSet.layers[0][epsgCode];
|
|
23
|
+
if (layerId == null) {
|
|
24
|
+
return new LambdaHttpResponse(404, `No data found for tile matrix: ${xyz.tileMatrix.identifier}`);
|
|
25
|
+
}
|
|
22
26
|
|
|
23
27
|
// Flip Y coordinate because MBTiles files are TMS.
|
|
24
28
|
const y = (1 << xyz.tile.z) - 1 - xyz.tile.y;
|