@basemaps/lambda-tiler 6.29.0 → 6.30.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 +25 -0
- package/build/__tests__/config.data.d.ts +6 -0
- package/build/__tests__/config.data.d.ts.map +1 -0
- package/build/__tests__/config.data.js +81 -0
- package/build/__tests__/config.data.js.map +1 -0
- package/build/__tests__/index.test.js +4 -3
- package/build/__tests__/index.test.js.map +1 -0
- package/build/__tests__/route.test.js +1 -0
- package/build/__tests__/route.test.js.map +1 -0
- package/build/__tests__/tiff.cache.test.js +1 -0
- package/build/__tests__/tiff.cache.test.js.map +1 -0
- package/build/__tests__/tile.cache.key.test.js +1 -0
- package/build/__tests__/tile.cache.key.test.js.map +1 -0
- package/build/__tests__/tile.set.cache.test.js +2 -53
- package/build/__tests__/tile.set.cache.test.js.map +1 -0
- package/build/__tests__/tile.set.test.js +1 -0
- package/build/__tests__/tile.set.test.js.map +1 -0
- package/build/__tests__/tile.style.json.test.js +1 -0
- package/build/__tests__/tile.style.json.test.js.map +1 -0
- package/build/__tests__/wmts.capability.test.d.ts +1 -1
- package/build/__tests__/wmts.capability.test.d.ts.map +1 -1
- package/build/__tests__/wmts.capability.test.js +207 -128
- package/build/__tests__/wmts.capability.test.js.map +1 -0
- package/build/__tests__/xyz.test.js +6 -37
- package/build/__tests__/xyz.test.js.map +1 -0
- package/build/__tests__/xyz.util.d.ts +0 -2
- package/build/__tests__/xyz.util.d.ts.map +1 -1
- package/build/__tests__/xyz.util.js +7 -29
- package/build/__tests__/xyz.util.js.map +1 -0
- package/build/api.key.js +1 -0
- package/build/api.key.js.map +1 -0
- package/build/cli/dump.js +1 -0
- package/build/cli/dump.js.map +1 -0
- package/build/cli/tile.set.local.js +1 -0
- package/build/cli/tile.set.local.js.map +1 -0
- package/build/cotar.cache.d.ts +25 -0
- package/build/cotar.cache.d.ts.map +1 -0
- package/build/cotar.cache.js +50 -0
- package/build/cotar.cache.js.map +1 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +31 -35
- package/build/index.js.map +1 -0
- package/build/router.js +1 -0
- package/build/router.js.map +1 -0
- package/build/routes/__tests__/attribution.test.js +5 -3
- package/build/routes/__tests__/attribution.test.js.map +1 -0
- package/build/routes/__tests__/fonts.test.js +9 -1
- package/build/routes/__tests__/fonts.test.js.map +1 -0
- package/build/routes/__tests__/health.test.js +1 -0
- package/build/routes/__tests__/health.test.js.map +1 -0
- package/build/routes/__tests__/imagery.test.js +1 -0
- package/build/routes/__tests__/imagery.test.js.map +1 -0
- package/build/routes/__tests__/memory.fs.js +1 -0
- package/build/routes/__tests__/memory.fs.js.map +1 -0
- package/build/routes/__tests__/sprites.test.js +1 -0
- package/build/routes/__tests__/sprites.test.js.map +1 -0
- package/build/routes/__tests__/wmts.test.js +59 -10
- package/build/routes/__tests__/wmts.test.js.map +1 -0
- package/build/routes/api.d.ts +0 -1
- package/build/routes/api.d.ts.map +1 -1
- package/build/routes/api.js +1 -3
- package/build/routes/api.js.map +1 -0
- package/build/routes/attribution.js +1 -0
- package/build/routes/attribution.js.map +1 -0
- package/build/routes/esri/rest.js +1 -0
- package/build/routes/esri/rest.js.map +1 -0
- package/build/routes/fonts.d.ts.map +1 -1
- package/build/routes/fonts.js +9 -1
- package/build/routes/fonts.js.map +1 -0
- package/build/routes/health.js +1 -0
- package/build/routes/health.js.map +1 -0
- package/build/routes/imagery.d.ts +8 -1
- package/build/routes/imagery.d.ts.map +1 -1
- package/build/routes/imagery.js +6 -7
- package/build/routes/imagery.js.map +1 -0
- package/build/routes/response.js +1 -0
- package/build/routes/response.js.map +1 -0
- package/build/routes/sprites.d.ts.map +1 -1
- package/build/routes/sprites.js +8 -14
- package/build/routes/sprites.js.map +1 -0
- package/build/routes/tile.etag.js +1 -0
- package/build/routes/tile.etag.js.map +1 -0
- package/build/routes/tile.js +1 -0
- package/build/routes/tile.js.map +1 -0
- package/build/routes/tile.json.d.ts.map +1 -1
- package/build/routes/tile.json.js +2 -2
- package/build/routes/tile.json.js.map +1 -0
- package/build/routes/tile.style.json.js +1 -0
- package/build/routes/tile.style.json.js.map +1 -0
- package/build/routes/tile.wmts.d.ts.map +1 -1
- package/build/routes/tile.wmts.js +19 -23
- package/build/routes/tile.wmts.js.map +1 -0
- package/build/routes/tile.xyz.js +1 -0
- package/build/routes/tile.xyz.js.map +1 -0
- package/build/source.tracer.js +1 -0
- package/build/source.tracer.js.map +1 -0
- package/build/tiff.cache.js +1 -0
- package/build/tiff.cache.js.map +1 -0
- package/build/tile.set.cache.d.ts +0 -1
- package/build/tile.set.cache.d.ts.map +1 -1
- package/build/tile.set.cache.js +1 -29
- package/build/tile.set.cache.js.map +1 -0
- package/build/tile.set.js +1 -0
- package/build/tile.set.js.map +1 -0
- package/build/tile.set.raster.d.ts.map +1 -1
- package/build/tile.set.raster.js +6 -3
- package/build/tile.set.raster.js.map +1 -0
- package/build/tile.set.vector.d.ts +0 -7
- package/build/tile.set.vector.d.ts.map +1 -1
- package/build/tile.set.vector.js +3 -20
- package/build/tile.set.vector.js.map +1 -0
- package/build/validate.js +1 -0
- package/build/validate.js.map +1 -0
- package/build/wmts.capability.d.ts +26 -12
- package/build/wmts.capability.d.ts.map +1 -1
- package/build/wmts.capability.js +136 -51
- package/build/wmts.capability.js.map +1 -0
- package/dist/index.js +86 -72
- package/dist/node_modules/.package-lock.json +1 -1
- package/dist/package-lock.json +2 -2
- package/dist/package.json +1 -1
- package/package.json +7 -7
- package/src/__tests__/config.data.ts +82 -0
- package/src/__tests__/index.test.ts +3 -3
- package/src/__tests__/tile.set.cache.test.ts +1 -67
- package/src/__tests__/wmts.capability.test.ts +224 -154
- package/src/__tests__/xyz.test.ts +5 -49
- package/src/__tests__/xyz.util.ts +6 -31
- package/src/cotar.cache.ts +54 -0
- package/src/index.ts +30 -33
- package/src/routes/__tests__/attribution.test.ts +4 -4
- package/src/routes/__tests__/fonts.test.ts +10 -1
- package/src/routes/__tests__/wmts.test.ts +75 -15
- package/src/routes/api.ts +0 -4
- package/src/routes/fonts.ts +9 -1
- package/src/routes/imagery.ts +9 -7
- package/src/routes/sprites.ts +7 -15
- package/src/routes/tile.json.ts +1 -2
- package/src/routes/tile.wmts.ts +20 -22
- package/src/tile.set.cache.ts +1 -28
- package/src/tile.set.raster.ts +4 -2
- package/src/tile.set.vector.ts +3 -21
- package/src/wmts.capability.ts +143 -58
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { fsa } from '@chunkd/fs';
|
|
2
|
+
import { Cotar } from '@cotar/core';
|
|
3
|
+
import { LambdaHttpResponse } from '@linzjs/lambda';
|
|
4
|
+
import { NotFound } from './routes/response.js';
|
|
5
|
+
import { St } from './source.tracer.js';
|
|
6
|
+
|
|
7
|
+
export class CotarCache {
|
|
8
|
+
static cache = new Map<string, Promise<Cotar | null>>();
|
|
9
|
+
|
|
10
|
+
static get(uri: string): Promise<Cotar | null> {
|
|
11
|
+
let existing = CotarCache.cache.get(uri);
|
|
12
|
+
if (existing == null) {
|
|
13
|
+
const source = fsa.source(uri);
|
|
14
|
+
St.trace(source);
|
|
15
|
+
existing = Cotar.fromTar(source);
|
|
16
|
+
CotarCache.cache.set(uri, existing);
|
|
17
|
+
}
|
|
18
|
+
return existing;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Load a cotar and look for a file inside the cotar returning the file back as a LambdaResponse
|
|
24
|
+
*
|
|
25
|
+
* This will also set two headers
|
|
26
|
+
* - Content-Encoding if the file starts with gzip magic
|
|
27
|
+
* - Content-Type from the parameter contentType
|
|
28
|
+
*/
|
|
29
|
+
export async function serveFromCotar(
|
|
30
|
+
cotarPath: string,
|
|
31
|
+
assetPath: string,
|
|
32
|
+
contentType: string,
|
|
33
|
+
): Promise<LambdaHttpResponse> {
|
|
34
|
+
const cotar = await CotarCache.get(cotarPath);
|
|
35
|
+
if (cotar == null) return NotFound;
|
|
36
|
+
const fileData = await cotar.get(assetPath);
|
|
37
|
+
if (fileData == null) return NotFound;
|
|
38
|
+
const buf = Buffer.from(fileData);
|
|
39
|
+
const ret = LambdaHttpResponse.ok().buffer(buf, contentType);
|
|
40
|
+
if (isGzip(buf)) ret.header('content-encoding', 'gzip');
|
|
41
|
+
return ret;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Does a buffer look like a gzipped document instead of raw json
|
|
46
|
+
*
|
|
47
|
+
* Determined by checking the first two bytes are the gzip magic bytes `0x1f 0x8b`
|
|
48
|
+
*
|
|
49
|
+
* @see https://en.wikipedia.org/wiki/Gzip
|
|
50
|
+
*
|
|
51
|
+
*/
|
|
52
|
+
export function isGzip(b: Buffer): boolean {
|
|
53
|
+
return b[0] === 0x1f && b[1] === 0x8b;
|
|
54
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,59 +1,56 @@
|
|
|
1
|
-
import { lf, LambdaHttpRequest, LambdaHttpResponse } from '@linzjs/lambda';
|
|
2
1
|
import { LogConfig } from '@basemaps/shared';
|
|
2
|
+
import { LambdaHttpRequest, LambdaHttpResponse, lf } from '@linzjs/lambda';
|
|
3
|
+
import { createHash } from 'crypto';
|
|
4
|
+
import { Router } from './router.js';
|
|
3
5
|
import { Ping, Version } from './routes/api.js';
|
|
6
|
+
import { fontGet, fontList } from './routes/fonts.js';
|
|
4
7
|
import { Health } from './routes/health.js';
|
|
8
|
+
import { imageryGet } from './routes/imagery.js';
|
|
9
|
+
import { spriteGet } from './routes/sprites.js';
|
|
5
10
|
import { Tiles } from './routes/tile.js';
|
|
6
|
-
import { Router } from './router.js';
|
|
7
|
-
import { createHash } from 'crypto';
|
|
8
|
-
import { Imagery } from './routes/imagery.js';
|
|
9
|
-
import { Esri } from './routes/esri/rest.js';
|
|
10
11
|
import { St } from './source.tracer.js';
|
|
11
|
-
import { spriteGet } from './routes/sprites.js';
|
|
12
|
-
import { fontGet, fontList } from './routes/fonts.js';
|
|
13
12
|
|
|
14
13
|
const app = new Router();
|
|
15
14
|
|
|
16
|
-
app.get('ping', Ping);
|
|
17
|
-
app.get('health', Health);
|
|
18
|
-
app.get('version', Version);
|
|
19
15
|
app.get('tiles', Tiles);
|
|
20
|
-
app.get('imagery', Imagery);
|
|
21
|
-
app.get('esri', Esri);
|
|
22
16
|
|
|
23
|
-
let slowTimer: NodeJS.Timer | null = null;
|
|
24
17
|
export async function handleRequest(req: LambdaHttpRequest): Promise<LambdaHttpResponse> {
|
|
25
|
-
|
|
26
|
-
|
|
18
|
+
const apiKey = Router.apiKey(req);
|
|
19
|
+
if (apiKey != null) {
|
|
20
|
+
const apiKeyHash = createHash('sha256').update(apiKey).digest('base64');
|
|
21
|
+
req.set('api', apiKeyHash);
|
|
22
|
+
}
|
|
23
|
+
return await app.handle(req);
|
|
24
|
+
}
|
|
27
25
|
|
|
28
|
-
|
|
29
|
-
if (slowTimer) clearTimeout(slowTimer);
|
|
30
|
-
slowTimer = setTimeout(() => req.log.warn(req.logContext, 'Lambda:Slow'), 10_000);
|
|
31
|
-
slowTimer.unref();
|
|
26
|
+
export const handler = lf.http(LogConfig.get());
|
|
32
27
|
|
|
28
|
+
handler.router.hook('request', (req) => {
|
|
33
29
|
req.set('name', 'LambdaTiler');
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const apiKeyHash = createHash('sha256').update(apiKey).digest('base64');
|
|
38
|
-
req.set('api', apiKeyHash);
|
|
39
|
-
}
|
|
40
|
-
const ret = await app.handle(req);
|
|
30
|
+
// Reset the request tracing before every request
|
|
31
|
+
St.reset();
|
|
32
|
+
});
|
|
41
33
|
|
|
34
|
+
handler.router.hook('response', (req) => {
|
|
35
|
+
if (St.requests.length > 0) {
|
|
42
36
|
// TODO this could be relaxed to every say 5% of requests if logging gets too verbose.
|
|
43
37
|
req.set('requests', St.requests.slice(0, 100)); // limit to 100 requests (some tiles need 100s of requests)
|
|
44
38
|
req.set('requestCount', St.requests.length);
|
|
45
|
-
|
|
46
|
-
return ret;
|
|
47
|
-
} finally {
|
|
48
|
-
if (slowTimer) clearTimeout(slowTimer);
|
|
49
|
-
slowTimer = null;
|
|
50
39
|
}
|
|
51
|
-
}
|
|
40
|
+
});
|
|
41
|
+
// TODO some internal health checks hit these routes, we should change them all to point at /v1/
|
|
42
|
+
handler.router.get('/ping', Ping);
|
|
43
|
+
handler.router.get('/health', Health);
|
|
44
|
+
handler.router.get('/version', Version);
|
|
52
45
|
|
|
53
|
-
|
|
46
|
+
handler.router.get('/v1/ping', Ping);
|
|
47
|
+
handler.router.get('/v1/health', Health);
|
|
48
|
+
handler.router.get('/v1/version', Version);
|
|
54
49
|
|
|
50
|
+
handler.router.get('/v1/imagery/:imageryId/:fileName', imageryGet);
|
|
55
51
|
handler.router.get('/v1/sprites/:spriteName', spriteGet);
|
|
56
52
|
handler.router.get('/v1/fonts.json', fontList);
|
|
57
53
|
handler.router.get('/v1/fonts/:fontStack/:range.pbf', fontGet);
|
|
58
54
|
|
|
55
|
+
// Catch all for old requests
|
|
59
56
|
handler.router.get('*', handleRequest);
|
|
@@ -9,10 +9,11 @@ import sinon from 'sinon';
|
|
|
9
9
|
const sandbox = sinon.createSandbox();
|
|
10
10
|
import { TileSets } from '../../tile.set.cache.js';
|
|
11
11
|
import { TileSetRaster } from '../../tile.set.raster.js';
|
|
12
|
-
import { FakeTileSet, mockRequest
|
|
12
|
+
import { FakeTileSet, mockRequest } from '../../__tests__/xyz.util.js';
|
|
13
13
|
import { attribution, createAttributionCollection } from '../attribution.js';
|
|
14
14
|
import { TileEtag } from '../tile.etag.js';
|
|
15
15
|
import { Attribution } from '@basemaps/attribution';
|
|
16
|
+
import { Provider } from '../../__tests__/config.data.js';
|
|
16
17
|
|
|
17
18
|
const ExpectedJson = {
|
|
18
19
|
id: 'aerial_WebMercatorQuad',
|
|
@@ -363,7 +364,7 @@ o.spec('attribution', () => {
|
|
|
363
364
|
const res = await attribution(request);
|
|
364
365
|
|
|
365
366
|
o(res.status).equals(200);
|
|
366
|
-
o(res.header(HttpHeader.ETag)).equals('
|
|
367
|
+
o(res.header(HttpHeader.ETag)).equals('GAwx9M7X1ygnn2kr9KPCin2gcaGq7DwYMY1dCpJj3no=');
|
|
367
368
|
o(res.header(HttpHeader.CacheControl)).equals('public, max-age=86400, stale-while-revalidate=604800');
|
|
368
369
|
|
|
369
370
|
const body = round(JSON.parse(res.body as string), 4);
|
|
@@ -372,10 +373,9 @@ o.spec('attribution', () => {
|
|
|
372
373
|
|
|
373
374
|
o('should 304 with etag match', async () => {
|
|
374
375
|
const request = mockRequest(`/v1/attribution/aerial/EPSG:3857/summary.json`, 'get', {
|
|
375
|
-
[HttpHeader.IfNoneMatch]: '
|
|
376
|
+
[HttpHeader.IfNoneMatch]: 'GAwx9M7X1ygnn2kr9KPCin2gcaGq7DwYMY1dCpJj3no=',
|
|
376
377
|
});
|
|
377
378
|
const res = await attribution(request);
|
|
378
|
-
|
|
379
379
|
o(res.status).equals(304);
|
|
380
380
|
});
|
|
381
381
|
|
|
@@ -60,7 +60,7 @@ o.spec('/v1/fonts', () => {
|
|
|
60
60
|
o('should get the correct font', async () => {
|
|
61
61
|
await fsa.write('memory://fonts/Roboto Thin/0-255.pbf', Buffer.from(''));
|
|
62
62
|
|
|
63
|
-
const res255 = await handler.router.handle(mockRequest('/v1/fonts/Roboto Thin/0-255.pbf'));
|
|
63
|
+
const res255 = await handler.router.handle(mockRequest(encodeURI('/v1/fonts/Roboto Thin/0-255.pbf')));
|
|
64
64
|
o(res255.status).equals(200);
|
|
65
65
|
o(res255.header('content-type')).equals('application/x-protobuf');
|
|
66
66
|
o(res255.header('content-encoding')).equals(undefined);
|
|
@@ -68,4 +68,13 @@ o.spec('/v1/fonts', () => {
|
|
|
68
68
|
const res404 = await handler.router.handle(mockRequest('/v1/fonts/Roboto Thin/256-512.pbf'));
|
|
69
69
|
o(res404.status).equals(404);
|
|
70
70
|
});
|
|
71
|
+
|
|
72
|
+
o('should get the correct utf8 font', async () => {
|
|
73
|
+
await fsa.write('memory://fonts/🦄 🌈/0-255.pbf', Buffer.from(''));
|
|
74
|
+
|
|
75
|
+
const res255 = await handler.router.handle(mockRequest(encodeURI('/v1/fonts/🦄 🌈/0-255.pbf')));
|
|
76
|
+
o(res255.status).equals(200);
|
|
77
|
+
o(res255.header('content-type')).equals('application/x-protobuf');
|
|
78
|
+
o(res255.header('content-encoding')).equals(undefined);
|
|
79
|
+
});
|
|
71
80
|
});
|
|
@@ -1,25 +1,29 @@
|
|
|
1
1
|
import { ImageFormat } from '@basemaps/geo';
|
|
2
|
-
import { LogConfig } from '@basemaps/shared';
|
|
2
|
+
import { Config, LogConfig } from '@basemaps/shared';
|
|
3
3
|
import { LambdaHttpRequest, LambdaUrlRequest } from '@linzjs/lambda';
|
|
4
4
|
import { Context } from 'aws-lambda';
|
|
5
5
|
import o from 'ospec';
|
|
6
|
+
import { handleRequest } from '../../index.js';
|
|
7
|
+
import { ulid } from 'ulid';
|
|
6
8
|
import { getImageFormats } from '../tile.wmts.js';
|
|
9
|
+
import { createSandbox } from 'sinon';
|
|
10
|
+
import { Imagery2193, Imagery3857, Provider, TileSetAerial } from '../../__tests__/config.data.js';
|
|
7
11
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
{
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
12
|
+
function newRequest(path: string, query: string): LambdaHttpRequest {
|
|
13
|
+
return new LambdaUrlRequest(
|
|
14
|
+
{
|
|
15
|
+
requestContext: { http: { method: 'GET' } },
|
|
16
|
+
headers: {},
|
|
17
|
+
rawPath: path,
|
|
18
|
+
rawQueryString: query,
|
|
19
|
+
isBase64Encoded: false,
|
|
20
|
+
} as any,
|
|
21
|
+
{} as Context,
|
|
22
|
+
LogConfig.get(),
|
|
23
|
+
);
|
|
24
|
+
}
|
|
22
25
|
|
|
26
|
+
o.spec('GetImageFormats', () => {
|
|
23
27
|
o('should parse all formats', () => {
|
|
24
28
|
const req = newRequest('/v1/blank', 'format=png&format=jpeg');
|
|
25
29
|
const formats = getImageFormats(req);
|
|
@@ -37,4 +41,60 @@ o.spec('GetImageFormats', () => {
|
|
|
37
41
|
const formats = getImageFormats(req);
|
|
38
42
|
o(formats).deepEquals([ImageFormat.Png, ImageFormat.Jpeg]);
|
|
39
43
|
});
|
|
44
|
+
|
|
45
|
+
o('should support "tileFormat" Alias all formats', () => {
|
|
46
|
+
const req = newRequest('/v1/blank', 'tileFormat=png&format=jpeg');
|
|
47
|
+
const formats = getImageFormats(req);
|
|
48
|
+
o(formats).deepEquals([ImageFormat.Jpeg, ImageFormat.Png]);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
o('should not duplicate "tileFormat" alias all formats', () => {
|
|
52
|
+
const req = newRequest('/v1/blank', 'tileFormat=jpeg&format=jpeg');
|
|
53
|
+
const formats = getImageFormats(req);
|
|
54
|
+
o(formats).deepEquals([ImageFormat.Jpeg]);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
o.spec('WMTSRouting', () => {
|
|
59
|
+
const sandbox = createSandbox();
|
|
60
|
+
o.afterEach(() => {
|
|
61
|
+
sandbox.restore();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
o('should default to the aerial layer', async () => {
|
|
65
|
+
const imagery = new Map();
|
|
66
|
+
imagery.set(Imagery3857.id, Imagery3857);
|
|
67
|
+
imagery.set(Imagery2193.id, Imagery2193);
|
|
68
|
+
|
|
69
|
+
const tileSetStub = sandbox.stub(Config.TileSet, 'get').returns(Promise.resolve(TileSetAerial));
|
|
70
|
+
const imageryStub = sandbox.stub(Config.Imagery, 'getAll').returns(Promise.resolve(imagery));
|
|
71
|
+
const providerStub = sandbox.stub(Config.Provider, 'get').returns(Promise.resolve(Provider));
|
|
72
|
+
|
|
73
|
+
const req = newRequest('/v1/tiles/WMTSCapabilities.xml', 'format=png&api=c' + ulid());
|
|
74
|
+
const res = await handleRequest(req);
|
|
75
|
+
|
|
76
|
+
o(tileSetStub.calledOnce).equals(true);
|
|
77
|
+
o(tileSetStub.args[0][0]).equals('ts_aerial');
|
|
78
|
+
|
|
79
|
+
o(providerStub.calledOnce).equals(true);
|
|
80
|
+
o(providerStub.args[0][0]).equals('pv_linz');
|
|
81
|
+
|
|
82
|
+
o(imageryStub.calledOnce).equals(true);
|
|
83
|
+
o([...imageryStub.args[0][0].values()]).deepEquals([
|
|
84
|
+
'im_01FYWKATAEK2ZTJQ2PX44Y0XNT',
|
|
85
|
+
'im_01FYWKAJ86W9P7RWM1VB62KD0H',
|
|
86
|
+
]);
|
|
87
|
+
|
|
88
|
+
o(res.status).equals(200);
|
|
89
|
+
const lines = Buffer.from(res.body, 'base64').toString().split('\n');
|
|
90
|
+
|
|
91
|
+
const titles = lines.filter((f) => f.startsWith(' <ows:Title>')).map((f) => f.trim());
|
|
92
|
+
|
|
93
|
+
o(titles).deepEquals([
|
|
94
|
+
'<ows:Title>Aerial Imagery</ows:Title>',
|
|
95
|
+
'<ows:Title>Ōtorohanga 0.1m Urban Aerial Photos (2021)</ows:Title>',
|
|
96
|
+
'<ows:Title>Google Maps Compatible for the World</ows:Title>',
|
|
97
|
+
'<ows:Title>LINZ NZTM2000 Map Tile Grid V2</ows:Title>',
|
|
98
|
+
]);
|
|
99
|
+
});
|
|
40
100
|
});
|
package/src/routes/api.ts
CHANGED
|
@@ -3,10 +3,6 @@ import { LambdaHttpResponse, HttpHeader } from '@linzjs/lambda';
|
|
|
3
3
|
const OkResponse = new LambdaHttpResponse(200, 'ok');
|
|
4
4
|
OkResponse.header(HttpHeader.CacheControl, 'no-store');
|
|
5
5
|
|
|
6
|
-
export async function Health(): Promise<LambdaHttpResponse> {
|
|
7
|
-
return OkResponse;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
6
|
export async function Ping(): Promise<LambdaHttpResponse> {
|
|
11
7
|
return OkResponse;
|
|
12
8
|
}
|
package/src/routes/fonts.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { Env } from '@basemaps/shared';
|
|
|
2
2
|
import { fsa } from '@chunkd/fs';
|
|
3
3
|
import { LambdaHttpRequest, LambdaHttpResponse } from '@linzjs/lambda';
|
|
4
4
|
import path from 'path';
|
|
5
|
+
import { serveFromCotar } from '../cotar.cache.js';
|
|
5
6
|
import { NotFound } from './response.js';
|
|
6
7
|
|
|
7
8
|
interface FontGet {
|
|
@@ -12,8 +13,13 @@ export async function fontGet(req: LambdaHttpRequest<FontGet>): Promise<LambdaHt
|
|
|
12
13
|
const assetLocation = Env.get(Env.AssetLocation);
|
|
13
14
|
if (assetLocation == null) return NotFound;
|
|
14
15
|
|
|
16
|
+
const fontStack = decodeURIComponent(req.params.fontStack);
|
|
17
|
+
const targetFile = path.join('fonts', fontStack, req.params.range) + '.pbf';
|
|
18
|
+
|
|
19
|
+
if (assetLocation.endsWith('.tar.co')) return serveFromCotar(assetLocation, targetFile, 'application/x-protobuf');
|
|
20
|
+
|
|
15
21
|
try {
|
|
16
|
-
const filePath = fsa.join(assetLocation,
|
|
22
|
+
const filePath = fsa.join(assetLocation, targetFile);
|
|
17
23
|
const buf = await fsa.read(filePath);
|
|
18
24
|
|
|
19
25
|
return LambdaHttpResponse.ok().buffer(buf, 'application/x-protobuf');
|
|
@@ -42,6 +48,8 @@ export async function fontList(): Promise<LambdaHttpResponse> {
|
|
|
42
48
|
const assetLocation = Env.get(Env.AssetLocation);
|
|
43
49
|
if (assetLocation == null) return NotFound;
|
|
44
50
|
|
|
51
|
+
if (assetLocation.endsWith('.tar.co')) return serveFromCotar(assetLocation, 'fonts.json', 'application/json');
|
|
52
|
+
|
|
45
53
|
try {
|
|
46
54
|
const filePath = fsa.join(assetLocation, '/fonts');
|
|
47
55
|
const fonts = await getFonts(filePath);
|
package/src/routes/imagery.ts
CHANGED
|
@@ -4,7 +4,6 @@ import { HttpHeader, LambdaHttpRequest, LambdaHttpResponse } from '@linzjs/lambd
|
|
|
4
4
|
import { createHash } from 'crypto';
|
|
5
5
|
import { promisify } from 'util';
|
|
6
6
|
import { gzip } from 'zlib';
|
|
7
|
-
import { Router } from '../router.js';
|
|
8
7
|
import { NotModified } from './response.js';
|
|
9
8
|
import { TileEtag } from './tile.etag.js';
|
|
10
9
|
|
|
@@ -17,6 +16,10 @@ export function isAllowedFile(f: string): boolean {
|
|
|
17
16
|
return false;
|
|
18
17
|
}
|
|
19
18
|
|
|
19
|
+
interface ImageryGet {
|
|
20
|
+
Params: { imageryId: string; fileName: string };
|
|
21
|
+
}
|
|
22
|
+
|
|
20
23
|
/**
|
|
21
24
|
* Get metadata around the imagery such as the source bounding box or the bounding box of the COGS
|
|
22
25
|
*
|
|
@@ -27,15 +30,14 @@ export function isAllowedFile(f: string): boolean {
|
|
|
27
30
|
* - /v1/imagery/:imageryId/collection.json - STAC Collection
|
|
28
31
|
* - /v1/imagery/:imageryId/15-32659-21603.json - STAC Item
|
|
29
32
|
*/
|
|
30
|
-
export async function
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
if (!isAllowedFile(requestType)) return new LambdaHttpResponse(404, 'Not found');
|
|
33
|
+
export async function imageryGet(req: LambdaHttpRequest<ImageryGet>): Promise<LambdaHttpResponse> {
|
|
34
|
+
const requestedFile = req.params.fileName;
|
|
35
|
+
if (!isAllowedFile(requestedFile)) return new LambdaHttpResponse(404, 'Not found');
|
|
34
36
|
|
|
35
|
-
const imagery = await Config.Imagery.get(Config.Imagery.id(imageryId));
|
|
37
|
+
const imagery = await Config.Imagery.get(Config.Imagery.id(req.params.imageryId));
|
|
36
38
|
if (imagery == null) return new LambdaHttpResponse(404, 'Not found');
|
|
37
39
|
|
|
38
|
-
const targetPath = fsa.join(imagery.uri,
|
|
40
|
+
const targetPath = fsa.join(imagery.uri, requestedFile);
|
|
39
41
|
|
|
40
42
|
try {
|
|
41
43
|
const buf = await fsa.read(targetPath);
|
package/src/routes/sprites.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { fsa } from '@chunkd/fs';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { LambdaHttpRequest, LambdaHttpResponse } from '@linzjs/lambda';
|
|
5
5
|
import { NotFound } from './response.js';
|
|
6
|
+
import { isGzip, serveFromCotar } from '../cotar.cache.js';
|
|
6
7
|
|
|
7
8
|
interface SpriteGet {
|
|
8
9
|
Params: {
|
|
@@ -14,28 +15,19 @@ const Extensions = new Map();
|
|
|
14
15
|
Extensions.set('.png', 'image/png');
|
|
15
16
|
Extensions.set('.json', 'application/json');
|
|
16
17
|
|
|
17
|
-
/**
|
|
18
|
-
* Does a buffer look like a gzipped document instead of raw json
|
|
19
|
-
*
|
|
20
|
-
* Determined by checking the first two bytes are the gzip magic bytes `0x1f 0x8b`
|
|
21
|
-
*
|
|
22
|
-
* @see https://en.wikipedia.org/wiki/Gzip
|
|
23
|
-
*
|
|
24
|
-
*/
|
|
25
|
-
function isGzip(b: Buffer): boolean {
|
|
26
|
-
return b[0] === 0x1f && b[1] === 0x8b;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
18
|
export async function spriteGet(req: LambdaHttpRequest<SpriteGet>): Promise<LambdaHttpResponse> {
|
|
30
|
-
const
|
|
31
|
-
if (
|
|
19
|
+
const assetLocation = Env.get(Env.AssetLocation);
|
|
20
|
+
if (assetLocation == null) return NotFound;
|
|
32
21
|
|
|
33
22
|
const extension = path.extname(req.params.spriteName);
|
|
34
23
|
const mimeType = Extensions.get(extension);
|
|
35
24
|
if (mimeType == null) return NotFound;
|
|
36
25
|
|
|
26
|
+
const targetFile = fsa.join('sprites', req.params.spriteName);
|
|
27
|
+
if (assetLocation.endsWith('.tar.co')) return serveFromCotar(assetLocation, targetFile, mimeType);
|
|
28
|
+
|
|
37
29
|
try {
|
|
38
|
-
const filePath = fsa.join(
|
|
30
|
+
const filePath = fsa.join(assetLocation, targetFile);
|
|
39
31
|
req.set('target', filePath);
|
|
40
32
|
|
|
41
33
|
const buf = await fsa.read(filePath);
|
package/src/routes/tile.json.ts
CHANGED
|
@@ -3,7 +3,6 @@ import { Env, extractTileMatrixSet } from '@basemaps/shared';
|
|
|
3
3
|
import { HttpHeader, LambdaHttpRequest, LambdaHttpResponse } from '@linzjs/lambda';
|
|
4
4
|
import { Router } from '../router.js';
|
|
5
5
|
import { TileSets } from '../tile.set.cache.js';
|
|
6
|
-
import { getTileMatrixId } from '../wmts.capability.js';
|
|
7
6
|
import { NotFound } from './response.js';
|
|
8
7
|
|
|
9
8
|
export async function tileJson(req: LambdaHttpRequest): Promise<LambdaHttpResponse> {
|
|
@@ -22,7 +21,7 @@ export async function tileJson(req: LambdaHttpRequest): Promise<LambdaHttpRespon
|
|
|
22
21
|
const host = Env.get(Env.PublicUrlBase) ?? '';
|
|
23
22
|
|
|
24
23
|
const tileUrl =
|
|
25
|
-
[host, version, name, tileSet.fullName,
|
|
24
|
+
[host, version, name, tileSet.fullName, tileMatrix.identifier, '{z}', '{x}', '{y}'].join('/') +
|
|
26
25
|
`.${tileSet.format}?api=${apiKey}`;
|
|
27
26
|
|
|
28
27
|
const tileJson: TileJson = { tiles: [tileUrl], tilejson: '3.0.0' };
|
package/src/routes/tile.wmts.ts
CHANGED
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
import { Config, TileSetType } from '@basemaps/config';
|
|
2
|
-
import { ImageFormat,
|
|
3
|
-
import { Env,
|
|
2
|
+
import { GoogleTms, ImageFormat, Nztm2000QuadTms } from '@basemaps/geo';
|
|
3
|
+
import { Env, tileWmtsFromPath } from '@basemaps/shared';
|
|
4
4
|
import { getImageFormat } from '@basemaps/tiler';
|
|
5
5
|
import { HttpHeader, LambdaHttpRequest, LambdaHttpResponse } from '@linzjs/lambda';
|
|
6
6
|
import { createHash } from 'crypto';
|
|
7
7
|
import { Router } from '../router.js';
|
|
8
|
-
import { TileSets } from '../tile.set.cache.js';
|
|
9
|
-
import { TileSetRaster } from '../tile.set.raster.js';
|
|
10
8
|
import { WmtsCapabilities } from '../wmts.capability.js';
|
|
11
9
|
import { NotFound, NotModified } from './response.js';
|
|
12
10
|
import { TileEtag } from './tile.etag.js';
|
|
13
11
|
|
|
14
12
|
export function getImageFormats(req: LambdaHttpRequest): ImageFormat[] | undefined {
|
|
15
|
-
const formats = req.query.getAll('format');
|
|
16
|
-
if (formats
|
|
13
|
+
const formats = [...req.query.getAll('format'), ...req.query.getAll('tileFormat')];
|
|
14
|
+
if (formats.length === 0) return;
|
|
17
15
|
|
|
18
16
|
const output: Set<ImageFormat> = new Set();
|
|
19
17
|
for (const fmt of formats) {
|
|
@@ -21,7 +19,7 @@ export function getImageFormats(req: LambdaHttpRequest): ImageFormat[] | undefin
|
|
|
21
19
|
if (parsed == null) continue;
|
|
22
20
|
output.add(parsed);
|
|
23
21
|
}
|
|
24
|
-
if (output.size === 0) return
|
|
22
|
+
if (output.size === 0) return;
|
|
25
23
|
return [...output.values()];
|
|
26
24
|
}
|
|
27
25
|
|
|
@@ -38,18 +36,28 @@ export async function wmts(req: LambdaHttpRequest): Promise<LambdaHttpResponse>
|
|
|
38
36
|
const host = Env.get(Env.PublicUrlBase) ?? '';
|
|
39
37
|
|
|
40
38
|
req.timer.start('tileset:load');
|
|
41
|
-
const
|
|
39
|
+
const tileSet = await Config.TileSet.get(Config.TileSet.id(wmtsData.name ?? 'aerial'));
|
|
42
40
|
req.timer.end('tileset:load');
|
|
43
|
-
if (
|
|
41
|
+
if (tileSet == null || tileSet.type !== TileSetType.Raster) return NotFound;
|
|
44
42
|
|
|
45
|
-
const
|
|
46
|
-
|
|
43
|
+
const provider = await Config.Provider.get(Config.Provider.id('linz'));
|
|
44
|
+
|
|
45
|
+
const tileMatrix = wmtsData.tileMatrix == null ? [GoogleTms, Nztm2000QuadTms] : [wmtsData.tileMatrix];
|
|
46
|
+
req.timer.start('imagery:load');
|
|
47
|
+
const imagery = await Config.getAllImagery(
|
|
48
|
+
tileSet.layers,
|
|
49
|
+
tileMatrix.map((tms) => tms.projection),
|
|
50
|
+
);
|
|
51
|
+
req.timer.end('imagery:load');
|
|
47
52
|
|
|
48
53
|
const apiKey = Router.apiKey(req);
|
|
49
54
|
const xml = new WmtsCapabilities({
|
|
50
55
|
httpBase: host,
|
|
51
56
|
provider: provider ?? undefined,
|
|
52
|
-
|
|
57
|
+
tileSet,
|
|
58
|
+
tileMatrix,
|
|
59
|
+
isIndividualLayers: wmtsData.tileMatrix == null,
|
|
60
|
+
imagery,
|
|
53
61
|
apiKey,
|
|
54
62
|
formats: getImageFormats(req),
|
|
55
63
|
}).toXml();
|
|
@@ -67,13 +75,3 @@ export async function wmts(req: LambdaHttpRequest): Promise<LambdaHttpResponse>
|
|
|
67
75
|
req.set('bytes', data.byteLength);
|
|
68
76
|
return response;
|
|
69
77
|
}
|
|
70
|
-
|
|
71
|
-
async function wmtsLoadTileSets(name: string, tileMatrix: TileMatrixSet | null): Promise<TileSetRaster[]> {
|
|
72
|
-
if (tileMatrix != null) {
|
|
73
|
-
const ts = await TileSets.get(name, tileMatrix);
|
|
74
|
-
if (ts == null || ts.type === TileSetType.Vector) return [];
|
|
75
|
-
return [ts];
|
|
76
|
-
}
|
|
77
|
-
if (name === '') name = TileSetName.aerial;
|
|
78
|
-
return (await TileSets.getAll(name, tileMatrix)).filter((f) => f.type === 'raster') as TileSetRaster[];
|
|
79
|
-
}
|
package/src/tile.set.cache.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { TileSetNameParser, TileSetType } from '@basemaps/config';
|
|
2
|
-
import { TileMatrixSet
|
|
2
|
+
import { TileMatrixSet } from '@basemaps/geo';
|
|
3
3
|
import { Config } from '@basemaps/shared';
|
|
4
4
|
import { TileSet } from './tile.set.js';
|
|
5
5
|
import { TileSetRaster } from './tile.set.raster.js';
|
|
@@ -79,33 +79,6 @@ export class TileSetCache {
|
|
|
79
79
|
this.tileSets.set(tileSetId, ts);
|
|
80
80
|
return ts;
|
|
81
81
|
}
|
|
82
|
-
|
|
83
|
-
async getAll(name: string, tileMatrix?: TileMatrixSet | null): Promise<TileSet[]> {
|
|
84
|
-
const nameComp = TileSetNameParser.parse(name);
|
|
85
|
-
const tileMatrices = tileMatrix == null ? Array.from(TileMatrixSets.Defaults.values()) : [tileMatrix];
|
|
86
|
-
|
|
87
|
-
const promises: Promise<TileSet | null>[] = [];
|
|
88
|
-
for (const tileMatrix of tileMatrices) promises.push(this.get(name, tileMatrix));
|
|
89
|
-
const tileMatrixSets = await Promise.all(promises);
|
|
90
|
-
|
|
91
|
-
const tileSets: TileSetRaster[] = [];
|
|
92
|
-
for (const parent of tileMatrixSets) {
|
|
93
|
-
if (parent == null) continue;
|
|
94
|
-
if (parent.type === TileSetType.Vector) continue;
|
|
95
|
-
|
|
96
|
-
tileSets.push(parent);
|
|
97
|
-
if (nameComp.layer != null) {
|
|
98
|
-
parent.components.name = nameComp.name;
|
|
99
|
-
} else if (parent.imagery != null && parent.imagery.size > 1) {
|
|
100
|
-
for (const imageId of parent.imagery.keys()) {
|
|
101
|
-
const childImg = parent.child(imageId);
|
|
102
|
-
if (childImg == null) continue;
|
|
103
|
-
tileSets.push(childImg);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
return tileSets.sort((a, b) => a.title.localeCompare(b.title));
|
|
108
|
-
}
|
|
109
82
|
}
|
|
110
83
|
|
|
111
84
|
export const TileSets = new TileSetCache();
|
package/src/tile.set.raster.ts
CHANGED
|
@@ -84,7 +84,7 @@ export class TileSetRaster {
|
|
|
84
84
|
|
|
85
85
|
async init(record: ConfigTileSetRaster): Promise<void> {
|
|
86
86
|
this.tileSet = record;
|
|
87
|
-
this.imagery = await Config.getAllImagery(this.tileSet.layers, this.tileMatrix.projection);
|
|
87
|
+
this.imagery = await Config.getAllImagery(this.tileSet.layers, [this.tileMatrix.projection]);
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
async initTiffs(tile: Tile, log: LogType): Promise<CogTiff[]> {
|
|
@@ -213,9 +213,11 @@ export class TileSetRaster {
|
|
|
213
213
|
child.tileSet = { ...this.tileSet };
|
|
214
214
|
child.tileSet.background = undefined;
|
|
215
215
|
const title = this.tileSet?.title ?? this.tileSet?.name;
|
|
216
|
-
child.tileSet.title = `${title} ${titleizeImageryName(image.name)}`;
|
|
216
|
+
child.tileSet.title = image.title ?? `${title} ${titleizeImageryName(image.name)}`;
|
|
217
217
|
child.extentOverride = Bounds.fromJson(image.bounds);
|
|
218
218
|
|
|
219
|
+
if (image.category) child.tileSet.category = image.category;
|
|
220
|
+
|
|
219
221
|
const layer: ConfigLayer = { name: image.name, minZoom: 0, maxZoom: 100 };
|
|
220
222
|
layer[this.tileMatrix.projection.code] = image.id;
|
|
221
223
|
|
package/src/tile.set.vector.ts
CHANGED
|
@@ -1,28 +1,10 @@
|
|
|
1
1
|
import { ConfigTileSetVector, TileSetNameComponents, TileSetNameParser, TileSetType } from '@basemaps/config';
|
|
2
2
|
import { GoogleTms, TileMatrixSet, VectorFormat } from '@basemaps/geo';
|
|
3
|
-
import {
|
|
4
|
-
import { Cotar } from '@cotar/core';
|
|
3
|
+
import { TileDataXyz } from '@basemaps/shared';
|
|
5
4
|
import { HttpHeader, LambdaHttpRequest, LambdaHttpResponse } from '@linzjs/lambda';
|
|
5
|
+
import { CotarCache } from './cotar.cache.js';
|
|
6
6
|
import { NotFound } from './routes/response.js';
|
|
7
7
|
import { TileSets } from './tile.set.cache.js';
|
|
8
|
-
import { St } from './source.tracer.js';
|
|
9
|
-
|
|
10
|
-
class CotarCache {
|
|
11
|
-
cache = new Map<string, Promise<Cotar | null>>();
|
|
12
|
-
|
|
13
|
-
get(uri: string): Promise<Cotar | null> {
|
|
14
|
-
let cotar = this.cache.get(uri);
|
|
15
|
-
if (cotar == null) {
|
|
16
|
-
const source = fsa.source(uri);
|
|
17
|
-
St.trace(source);
|
|
18
|
-
cotar = Cotar.fromTar(source);
|
|
19
|
-
this.cache.set(uri, cotar);
|
|
20
|
-
}
|
|
21
|
-
return cotar;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export const Layers = new CotarCache();
|
|
26
8
|
|
|
27
9
|
export class TileSetVector {
|
|
28
10
|
type: TileSetType.Vector = TileSetType.Vector;
|
|
@@ -59,7 +41,7 @@ export class TileSetVector {
|
|
|
59
41
|
if (layer[3857] == null) return new LambdaHttpResponse(500, 'Layer url not found from tileset Config');
|
|
60
42
|
|
|
61
43
|
req.timer.start('cotar:load');
|
|
62
|
-
const cotar = await
|
|
44
|
+
const cotar = await CotarCache.get(layer[3857]);
|
|
63
45
|
if (cotar == null) return new LambdaHttpResponse(500, 'Failed to load VectorTiles');
|
|
64
46
|
req.timer.end('cotar:load');
|
|
65
47
|
|