@basemaps/lambda-tiler 7.15.0 → 8.0.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 +24 -0
- package/build/__tests__/config.data.d.ts +1 -0
- package/build/__tests__/config.data.js +18 -0
- package/build/__tests__/config.data.js.map +1 -1
- package/build/routes/__tests__/fonts.test.js +17 -8
- package/build/routes/__tests__/fonts.test.js.map +1 -1
- package/build/routes/__tests__/xyz.test.js +13 -1
- package/build/routes/__tests__/xyz.test.js.map +1 -1
- package/build/util/__test__/validate.test.js +15 -0
- package/build/util/__test__/validate.test.js.map +1 -1
- package/build/util/config.loader.d.ts +1 -1
- package/build/util/config.loader.js +14 -7
- package/build/util/config.loader.js.map +1 -1
- package/build/util/validate.d.ts +2 -0
- package/build/util/validate.js +19 -2
- package/build/util/validate.js.map +1 -1
- package/dist/index.js +56191 -51597
- package/dist/node_modules/.package-lock.json +4 -4
- package/dist/node_modules/detect-libc/package.json +3 -2
- package/dist/package-lock.json +5 -5
- package/dist/package.json +1 -1
- package/package.json +10 -10
- package/src/__tests__/config.data.ts +18 -0
- package/src/routes/__tests__/fonts.test.ts +23 -8
- package/src/routes/__tests__/xyz.test.ts +19 -1
- package/src/util/__test__/validate.test.ts +18 -0
- package/src/util/config.loader.ts +14 -6
- package/src/util/validate.ts +23 -2
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@basemaps/lambda-tiler",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "8.0.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
@@ -94,9 +94,9 @@
|
|
|
94
94
|
}
|
|
95
95
|
},
|
|
96
96
|
"node_modules/detect-libc": {
|
|
97
|
-
"version": "2.0.
|
|
98
|
-
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.
|
|
99
|
-
"integrity": "sha512-
|
|
97
|
+
"version": "2.0.4",
|
|
98
|
+
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
|
|
99
|
+
"integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==",
|
|
100
100
|
"license": "Apache-2.0",
|
|
101
101
|
"engines": {
|
|
102
102
|
"node": ">=8"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "detect-libc",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.4",
|
|
4
4
|
"description": "Node.js module to detect the C standard library (libc) implementation family and version",
|
|
5
5
|
"main": "lib/detect-libc.js",
|
|
6
6
|
"files": [
|
|
@@ -36,5 +36,6 @@
|
|
|
36
36
|
},
|
|
37
37
|
"engines": {
|
|
38
38
|
"node": ">=8"
|
|
39
|
-
}
|
|
39
|
+
},
|
|
40
|
+
"types": "index.d.ts"
|
|
40
41
|
}
|
package/dist/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@basemaps/lambda-tiler",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "8.0.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@basemaps/lambda-tiler",
|
|
9
|
-
"version": "
|
|
9
|
+
"version": "8.0.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"lerc": "4.0.4",
|
|
@@ -518,9 +518,9 @@
|
|
|
518
518
|
}
|
|
519
519
|
},
|
|
520
520
|
"node_modules/detect-libc": {
|
|
521
|
-
"version": "2.0.
|
|
522
|
-
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.
|
|
523
|
-
"integrity": "sha512-
|
|
521
|
+
"version": "2.0.4",
|
|
522
|
+
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
|
|
523
|
+
"integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==",
|
|
524
524
|
"license": "Apache-2.0",
|
|
525
525
|
"engines": {
|
|
526
526
|
"node": ">=8"
|
package/dist/package.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@basemaps/lambda-tiler",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "8.0.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/linz/basemaps.git",
|
|
@@ -22,13 +22,13 @@
|
|
|
22
22
|
"types": "./build/index.d.ts",
|
|
23
23
|
"license": "MIT",
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@basemaps/config": "^
|
|
26
|
-
"@basemaps/config-loader": "^
|
|
27
|
-
"@basemaps/geo": "^
|
|
28
|
-
"@basemaps/shared": "^
|
|
29
|
-
"@basemaps/tiler": "^
|
|
30
|
-
"@basemaps/tiler-sharp": "^
|
|
31
|
-
"@linzjs/geojson": "^
|
|
25
|
+
"@basemaps/config": "^8.0.0",
|
|
26
|
+
"@basemaps/config-loader": "^8.0.0",
|
|
27
|
+
"@basemaps/geo": "^8.0.0",
|
|
28
|
+
"@basemaps/shared": "^8.0.0",
|
|
29
|
+
"@basemaps/tiler": "^8.0.0",
|
|
30
|
+
"@basemaps/tiler-sharp": "^8.0.0",
|
|
31
|
+
"@linzjs/geojson": "^8.0.0",
|
|
32
32
|
"@linzjs/lambda": "^4.0.0",
|
|
33
33
|
"@mapbox/vector-tile": "^2.0.3",
|
|
34
34
|
"p-limit": "^4.0.0",
|
|
@@ -50,11 +50,11 @@
|
|
|
50
50
|
"bundle": "./bundle.sh"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
|
-
"@basemaps/attribution": "^
|
|
53
|
+
"@basemaps/attribution": "^8.0.0",
|
|
54
54
|
"@chunkd/fs": "^11.2.0",
|
|
55
55
|
"@types/aws-lambda": "^8.10.75",
|
|
56
56
|
"@types/pixelmatch": "^5.0.0",
|
|
57
57
|
"pretty-json-log": "^1.0.0"
|
|
58
58
|
},
|
|
59
|
-
"gitHead": "
|
|
59
|
+
"gitHead": "852c7c8bcbe06d065de732f23a6ebc2dd6a19cfc"
|
|
60
60
|
}
|
|
@@ -63,6 +63,24 @@ export const TileSetElevation: ConfigTileSetRaster = {
|
|
|
63
63
|
],
|
|
64
64
|
outputs: [DefaultTerrainRgbOutput, DefaultColorRampOutput],
|
|
65
65
|
};
|
|
66
|
+
export const TileSetHillshadeElevation: ConfigTileSetRaster = {
|
|
67
|
+
id: 'ts_hillshade',
|
|
68
|
+
name: 'hillshade',
|
|
69
|
+
type: TileSetType.Raster,
|
|
70
|
+
description: 'hillshade__description',
|
|
71
|
+
title: 'hillshade Imagery',
|
|
72
|
+
category: 'hillshade',
|
|
73
|
+
layers: [
|
|
74
|
+
{
|
|
75
|
+
// TODO: create one band imagery to reference
|
|
76
|
+
2193: 'im_01FYWKAJ86W9P7RWM1VB62KD0H',
|
|
77
|
+
3857: 'im_01FYWKATAEK2ZTJQ2PX44Y0XNT',
|
|
78
|
+
title: 'New Zealand 8m Hillshade DEM (2012)',
|
|
79
|
+
name: 'new-zealand_hillshade_2012_dem_8m',
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
outputs: [DefaultColorRampOutput],
|
|
83
|
+
};
|
|
66
84
|
|
|
67
85
|
export const Imagery2193: ConfigImagery = {
|
|
68
86
|
id: 'im_01FYWKAJ86W9P7RWM1VB62KD0H',
|
|
@@ -8,6 +8,7 @@ import { fsa, FsMemory } from '@chunkd/fs';
|
|
|
8
8
|
import { Api, mockRequest, mockUrlRequest } from '../../__tests__/xyz.util.js';
|
|
9
9
|
import { handler } from '../../index.js';
|
|
10
10
|
import { CachedConfig } from '../../util/config.cache.js';
|
|
11
|
+
import { ConfigLoader } from '../../util/config.loader.js';
|
|
11
12
|
import { CoSources } from '../../util/source.cache.js';
|
|
12
13
|
import { fontList } from '../fonts.js';
|
|
13
14
|
|
|
@@ -28,12 +29,15 @@ describe('/v1/fonts', () => {
|
|
|
28
29
|
memory.files.clear();
|
|
29
30
|
});
|
|
30
31
|
|
|
31
|
-
it('should return 404 if no font found', async () => {
|
|
32
|
+
it('should return 404 if no font found', async (t) => {
|
|
33
|
+
t.mock.method(ConfigLoader, 'getDefaultConfig', () => config);
|
|
32
34
|
const res = await fontList(mockRequest('/v1/fonts.json'));
|
|
33
35
|
assert.equal(res.status, 404);
|
|
34
36
|
});
|
|
35
37
|
|
|
36
|
-
it('should return a list of fonts found', async () => {
|
|
38
|
+
it('should return a list of fonts found', async (t) => {
|
|
39
|
+
t.mock.method(ConfigLoader, 'getDefaultConfig', () => config);
|
|
40
|
+
|
|
37
41
|
await fsa.write(
|
|
38
42
|
new URL('memory://assets/fonts/fonts.json'),
|
|
39
43
|
Buffer.from(JSON.stringify(['Roboto Black', 'Roboto Thin'])),
|
|
@@ -45,7 +49,9 @@ describe('/v1/fonts', () => {
|
|
|
45
49
|
assert.equal(res._body?.toString(), JSON.stringify(['Roboto Black', 'Roboto Thin']));
|
|
46
50
|
});
|
|
47
51
|
|
|
48
|
-
it('should get the correct font', async () => {
|
|
52
|
+
it('should get the correct font', async (t) => {
|
|
53
|
+
t.mock.method(ConfigLoader, 'getDefaultConfig', () => config);
|
|
54
|
+
|
|
49
55
|
await fsa.write(new URL('memory://assets/fonts/Roboto Thin/0-255.pbf'), Buffer.from(''));
|
|
50
56
|
const res255 = await handler.router.handle(mockRequest('/v1/fonts/Roboto Thin/0-255.pbf'));
|
|
51
57
|
assert.equal(res255.status, 200);
|
|
@@ -58,7 +64,9 @@ describe('/v1/fonts', () => {
|
|
|
58
64
|
assert.equal(res404.status, 404);
|
|
59
65
|
});
|
|
60
66
|
|
|
61
|
-
it('should get the correct utf8 font', async () => {
|
|
67
|
+
it('should get the correct utf8 font', async (t) => {
|
|
68
|
+
t.mock.method(ConfigLoader, 'getDefaultConfig', () => config);
|
|
69
|
+
|
|
62
70
|
await fsa.write(new URL('memory://assets/fonts/🦄 🌈/0-255.pbf'), Buffer.from(''));
|
|
63
71
|
const res255 = await handler.router.handle(mockRequest('/v1/fonts/🦄 🌈/0-255.pbf'));
|
|
64
72
|
assert.equal(res255.status, 200);
|
|
@@ -68,7 +76,9 @@ describe('/v1/fonts', () => {
|
|
|
68
76
|
assert.equal(res255.header('cache-control'), 'public, max-age=604800, stale-while-revalidate=86400');
|
|
69
77
|
});
|
|
70
78
|
|
|
71
|
-
it('should return 404 if no asset location set', async () => {
|
|
79
|
+
it('should return 404 if no asset location set', async (t) => {
|
|
80
|
+
t.mock.method(ConfigLoader, 'getDefaultConfig', () => config);
|
|
81
|
+
|
|
72
82
|
config.assets = undefined;
|
|
73
83
|
const res = await fontList(mockRequest('/v1/fonts.json'));
|
|
74
84
|
assert.equal(res.status, 404);
|
|
@@ -76,11 +86,14 @@ describe('/v1/fonts', () => {
|
|
|
76
86
|
|
|
77
87
|
it('should get the correct utf8 font with default assets', async () => {
|
|
78
88
|
config.assets = undefined;
|
|
89
|
+
const configJson = config.toJson();
|
|
90
|
+
await fsa.write(new URL(`memory://config-${configJson.hash}.json`), JSON.stringify(configJson));
|
|
91
|
+
|
|
79
92
|
config.objects.set('cb_latest', {
|
|
80
93
|
id: 'cb_latest',
|
|
81
94
|
name: 'latest',
|
|
82
|
-
path:
|
|
83
|
-
hash:
|
|
95
|
+
path: `memory://config-${configJson.hash}.json`,
|
|
96
|
+
hash: configJson.hash,
|
|
84
97
|
assets: 'memory://new-location/',
|
|
85
98
|
} as BaseConfig);
|
|
86
99
|
|
|
@@ -93,7 +106,9 @@ describe('/v1/fonts', () => {
|
|
|
93
106
|
assert.equal(res255.header('cache-control'), 'public, max-age=604800, stale-while-revalidate=86400');
|
|
94
107
|
});
|
|
95
108
|
|
|
96
|
-
it('should get the correct utf8 font with config assets', async () => {
|
|
109
|
+
it('should get the correct utf8 font with config assets', async (t) => {
|
|
110
|
+
t.mock.method(ConfigLoader, 'getDefaultConfig', () => config);
|
|
111
|
+
|
|
97
112
|
const cfgBundle = new ConfigProviderMemory();
|
|
98
113
|
cfgBundle.assets = 'memory://config/assets/';
|
|
99
114
|
await fsa.write(new URL('memory://linz-basemaps/bar.json'), JSON.stringify(cfgBundle.toJson()));
|
|
@@ -135,7 +135,10 @@ describe('/v1/tiles', () => {
|
|
|
135
135
|
|
|
136
136
|
const elevation = FakeData.tileSetRaster('elevation');
|
|
137
137
|
|
|
138
|
-
elevation.outputs = [
|
|
138
|
+
elevation.outputs = [
|
|
139
|
+
{ title: 'Terrain RGB', name: 'terrain-rgb' },
|
|
140
|
+
{ title: 'Color Ramp', name: 'color-ramp' },
|
|
141
|
+
];
|
|
139
142
|
config.put(elevation);
|
|
140
143
|
|
|
141
144
|
const request = mockRequest('/v1/tiles/elevation/3857/11/2022/1283.webp', 'get', Api.header);
|
|
@@ -145,6 +148,21 @@ describe('/v1/tiles', () => {
|
|
|
145
148
|
assert.equal(res.status, 404, res.statusDescription);
|
|
146
149
|
});
|
|
147
150
|
|
|
151
|
+
it('should default to the pipeline if only one pipeline is defined', async (t) => {
|
|
152
|
+
t.mock.method(ConfigLoader, 'getDefaultConfig', () => Promise.resolve(config));
|
|
153
|
+
|
|
154
|
+
const elevation = FakeData.tileSetRaster('elevation');
|
|
155
|
+
|
|
156
|
+
elevation.outputs = [{ title: 'Terrain RGB', name: 'terrain-rgb' }];
|
|
157
|
+
config.put(elevation);
|
|
158
|
+
|
|
159
|
+
const request = mockRequest('/v1/tiles/elevation/3857/11/2022/1283.webp', 'get', Api.header);
|
|
160
|
+
|
|
161
|
+
const res = await handler.router.handle(request);
|
|
162
|
+
|
|
163
|
+
assert.equal(res.status, 200, res.statusDescription);
|
|
164
|
+
});
|
|
165
|
+
|
|
148
166
|
it('should generate a terrain-rgb 11/2022/1283 in webp', async (t) => {
|
|
149
167
|
t.mock.method(ConfigLoader, 'getDefaultConfig', () => Promise.resolve(config));
|
|
150
168
|
|
|
@@ -6,6 +6,24 @@ import { GoogleTms, Nztm2000QuadTms, Nztm2000Tms } from '@basemaps/geo';
|
|
|
6
6
|
import { mockUrlRequest } from '../../__tests__/xyz.util.js';
|
|
7
7
|
import { Validate } from '../validate.js';
|
|
8
8
|
|
|
9
|
+
describe('Validate.blockedApiKeys', () => {
|
|
10
|
+
const validApiKey = 'c01jswmpe1yn3mwne7e0ggtp8vg';
|
|
11
|
+
it('should disable api keys', () => {
|
|
12
|
+
const req = mockUrlRequest('/v1/blank', `api=${validApiKey}`);
|
|
13
|
+
const parsedKey = Validate.apiKey(req);
|
|
14
|
+
assert.equal(parsedKey, validApiKey);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should disable api keys', () => {
|
|
18
|
+
const req = mockUrlRequest('/v1/blank', `api=${validApiKey}`);
|
|
19
|
+
Validate.blockedApiKeys.add(validApiKey);
|
|
20
|
+
assert.throws(() => Validate.apiKey(req));
|
|
21
|
+
|
|
22
|
+
Validate.blockedApiKeys.delete(validApiKey);
|
|
23
|
+
assert.equal(Validate.apiKey(req), validApiKey);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
9
27
|
describe('GetImageFormats', () => {
|
|
10
28
|
it('should parse all formats', () => {
|
|
11
29
|
const req = mockUrlRequest('/v1/blank', 'format=png&format=jpeg');
|
|
@@ -18,13 +18,21 @@ const SafeProtocols = new Set([new URL('s3://foo').protocol, new URL('memory://f
|
|
|
18
18
|
|
|
19
19
|
export class ConfigLoader {
|
|
20
20
|
/** Exposed for testing */
|
|
21
|
-
static async getDefaultConfig(): Promise<BasemapsConfigProvider> {
|
|
21
|
+
static async getDefaultConfig(req?: LambdaHttpRequest): Promise<BasemapsConfigProvider> {
|
|
22
22
|
const config = getDefaultConfig();
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
|
|
24
|
+
// Look up the latest config bundle out of dynamodb, then load the config from the provided path
|
|
25
|
+
const cb = await config.ConfigBundle.get('cb_latest');
|
|
26
|
+
if (cb == null) return config;
|
|
27
|
+
|
|
28
|
+
req?.timer.start('config:load');
|
|
29
|
+
|
|
30
|
+
return CachedConfig.get(fsa.toUrl(cb.path)).then((cfg) => {
|
|
31
|
+
req?.timer.end('config:load');
|
|
32
|
+
if (cfg == null) throw new LambdaHttpResponse(500, 'Unable to find latest configuration');
|
|
33
|
+
if (cfg.assets == null) cfg.assets = cb.assets;
|
|
34
|
+
return cfg;
|
|
35
|
+
});
|
|
28
36
|
}
|
|
29
37
|
|
|
30
38
|
/** Lookup the config path from a request and return a standardized location */
|
package/src/util/validate.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ConfigTileSetRaster, ConfigTileSetRasterOutput } from '@basemaps/config';
|
|
2
2
|
import { ImageFormat, LatLon, Projection, TileMatrixSet, TileMatrixSets } from '@basemaps/geo';
|
|
3
|
-
import { Const, isValidApiKey, truncateApiKey } from '@basemaps/shared';
|
|
3
|
+
import { Const, Env, isValidApiKey, LogConfig, truncateApiKey } from '@basemaps/shared';
|
|
4
4
|
import { getImageFormat } from '@basemaps/tiler';
|
|
5
5
|
import { LambdaHttpRequest, LambdaHttpResponse } from '@linzjs/lambda';
|
|
6
6
|
|
|
@@ -23,7 +23,19 @@ export interface TileMatrixRequest {
|
|
|
23
23
|
Params: { tileMatrix?: string };
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
function getBlockedApiKeys(): string[] {
|
|
27
|
+
try {
|
|
28
|
+
return JSON.parse(Env.get(Env.BlockedApiKeys) ?? '[]') as string[];
|
|
29
|
+
} catch (e) {
|
|
30
|
+
LogConfig.get().error(`"$${Env.BlockedApiKeys}" is invalid`);
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
26
35
|
export const Validate = {
|
|
36
|
+
/** list of API Keys that have been disabled */
|
|
37
|
+
blockedApiKeys: new Set<string>(getBlockedApiKeys()),
|
|
38
|
+
|
|
27
39
|
/**
|
|
28
40
|
* Validate that the api key exists and is valid
|
|
29
41
|
*
|
|
@@ -36,6 +48,11 @@ export const Validate = {
|
|
|
36
48
|
if (!valid.valid) throw new LambdaHttpResponse(400, 'API Key Invalid: ' + valid.message);
|
|
37
49
|
// Truncate the API Key so we are not logging the full key
|
|
38
50
|
req.set('api', truncateApiKey(apiKey));
|
|
51
|
+
|
|
52
|
+
if (this.blockedApiKeys.has(apiKey as string)) {
|
|
53
|
+
throw new LambdaHttpResponse(429, 'Too many requests! Please contact basemaps@linz.govt.nz for a developer key');
|
|
54
|
+
}
|
|
55
|
+
|
|
39
56
|
return apiKey as string;
|
|
40
57
|
},
|
|
41
58
|
|
|
@@ -125,6 +142,9 @@ export const Validate = {
|
|
|
125
142
|
* Defaults to standard image format output if no outputs are defined on the tileset
|
|
126
143
|
*/
|
|
127
144
|
pipeline(tileSet: ConfigTileSetRaster, tileType: string, pipeline?: string | null): ConfigTileSetRasterOutput | null {
|
|
145
|
+
// If there is only one pipeline force the use of it
|
|
146
|
+
if (tileSet.outputs?.length === 1 && pipeline == null) pipeline = tileSet.outputs[0].name;
|
|
147
|
+
|
|
128
148
|
if (pipeline != null && pipeline !== 'rgba') {
|
|
129
149
|
if (tileSet.outputs == null) throw new LambdaHttpResponse(404, 'TileSet has no pipelines');
|
|
130
150
|
const output = tileSet.outputs.find((f) => f.name === pipeline);
|
|
@@ -136,7 +156,8 @@ export const Validate = {
|
|
|
136
156
|
}
|
|
137
157
|
return output;
|
|
138
158
|
}
|
|
139
|
-
|
|
159
|
+
|
|
160
|
+
// If the tileset has multiple pipelines defined the user MUST specify which one
|
|
140
161
|
if (tileSet.outputs) {
|
|
141
162
|
throw new LambdaHttpResponse(404, 'TileSet needs pipeline: ' + tileSet.outputs.map((f) => f.name).join(', '));
|
|
142
163
|
}
|