@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.
Files changed (144) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/build/__tests__/config.data.d.ts +6 -0
  3. package/build/__tests__/config.data.d.ts.map +1 -0
  4. package/build/__tests__/config.data.js +81 -0
  5. package/build/__tests__/config.data.js.map +1 -0
  6. package/build/__tests__/index.test.js +4 -3
  7. package/build/__tests__/index.test.js.map +1 -0
  8. package/build/__tests__/route.test.js +1 -0
  9. package/build/__tests__/route.test.js.map +1 -0
  10. package/build/__tests__/tiff.cache.test.js +1 -0
  11. package/build/__tests__/tiff.cache.test.js.map +1 -0
  12. package/build/__tests__/tile.cache.key.test.js +1 -0
  13. package/build/__tests__/tile.cache.key.test.js.map +1 -0
  14. package/build/__tests__/tile.set.cache.test.js +2 -53
  15. package/build/__tests__/tile.set.cache.test.js.map +1 -0
  16. package/build/__tests__/tile.set.test.js +1 -0
  17. package/build/__tests__/tile.set.test.js.map +1 -0
  18. package/build/__tests__/tile.style.json.test.js +1 -0
  19. package/build/__tests__/tile.style.json.test.js.map +1 -0
  20. package/build/__tests__/wmts.capability.test.d.ts +1 -1
  21. package/build/__tests__/wmts.capability.test.d.ts.map +1 -1
  22. package/build/__tests__/wmts.capability.test.js +207 -128
  23. package/build/__tests__/wmts.capability.test.js.map +1 -0
  24. package/build/__tests__/xyz.test.js +6 -37
  25. package/build/__tests__/xyz.test.js.map +1 -0
  26. package/build/__tests__/xyz.util.d.ts +0 -2
  27. package/build/__tests__/xyz.util.d.ts.map +1 -1
  28. package/build/__tests__/xyz.util.js +7 -29
  29. package/build/__tests__/xyz.util.js.map +1 -0
  30. package/build/api.key.js +1 -0
  31. package/build/api.key.js.map +1 -0
  32. package/build/cli/dump.js +1 -0
  33. package/build/cli/dump.js.map +1 -0
  34. package/build/cli/tile.set.local.js +1 -0
  35. package/build/cli/tile.set.local.js.map +1 -0
  36. package/build/cotar.cache.d.ts +25 -0
  37. package/build/cotar.cache.d.ts.map +1 -0
  38. package/build/cotar.cache.js +50 -0
  39. package/build/cotar.cache.js.map +1 -0
  40. package/build/index.d.ts.map +1 -1
  41. package/build/index.js +31 -35
  42. package/build/index.js.map +1 -0
  43. package/build/router.js +1 -0
  44. package/build/router.js.map +1 -0
  45. package/build/routes/__tests__/attribution.test.js +5 -3
  46. package/build/routes/__tests__/attribution.test.js.map +1 -0
  47. package/build/routes/__tests__/fonts.test.js +9 -1
  48. package/build/routes/__tests__/fonts.test.js.map +1 -0
  49. package/build/routes/__tests__/health.test.js +1 -0
  50. package/build/routes/__tests__/health.test.js.map +1 -0
  51. package/build/routes/__tests__/imagery.test.js +1 -0
  52. package/build/routes/__tests__/imagery.test.js.map +1 -0
  53. package/build/routes/__tests__/memory.fs.js +1 -0
  54. package/build/routes/__tests__/memory.fs.js.map +1 -0
  55. package/build/routes/__tests__/sprites.test.js +1 -0
  56. package/build/routes/__tests__/sprites.test.js.map +1 -0
  57. package/build/routes/__tests__/wmts.test.js +59 -10
  58. package/build/routes/__tests__/wmts.test.js.map +1 -0
  59. package/build/routes/api.d.ts +0 -1
  60. package/build/routes/api.d.ts.map +1 -1
  61. package/build/routes/api.js +1 -3
  62. package/build/routes/api.js.map +1 -0
  63. package/build/routes/attribution.js +1 -0
  64. package/build/routes/attribution.js.map +1 -0
  65. package/build/routes/esri/rest.js +1 -0
  66. package/build/routes/esri/rest.js.map +1 -0
  67. package/build/routes/fonts.d.ts.map +1 -1
  68. package/build/routes/fonts.js +9 -1
  69. package/build/routes/fonts.js.map +1 -0
  70. package/build/routes/health.js +1 -0
  71. package/build/routes/health.js.map +1 -0
  72. package/build/routes/imagery.d.ts +8 -1
  73. package/build/routes/imagery.d.ts.map +1 -1
  74. package/build/routes/imagery.js +6 -7
  75. package/build/routes/imagery.js.map +1 -0
  76. package/build/routes/response.js +1 -0
  77. package/build/routes/response.js.map +1 -0
  78. package/build/routes/sprites.d.ts.map +1 -1
  79. package/build/routes/sprites.js +8 -14
  80. package/build/routes/sprites.js.map +1 -0
  81. package/build/routes/tile.etag.js +1 -0
  82. package/build/routes/tile.etag.js.map +1 -0
  83. package/build/routes/tile.js +1 -0
  84. package/build/routes/tile.js.map +1 -0
  85. package/build/routes/tile.json.d.ts.map +1 -1
  86. package/build/routes/tile.json.js +2 -2
  87. package/build/routes/tile.json.js.map +1 -0
  88. package/build/routes/tile.style.json.js +1 -0
  89. package/build/routes/tile.style.json.js.map +1 -0
  90. package/build/routes/tile.wmts.d.ts.map +1 -1
  91. package/build/routes/tile.wmts.js +19 -23
  92. package/build/routes/tile.wmts.js.map +1 -0
  93. package/build/routes/tile.xyz.js +1 -0
  94. package/build/routes/tile.xyz.js.map +1 -0
  95. package/build/source.tracer.js +1 -0
  96. package/build/source.tracer.js.map +1 -0
  97. package/build/tiff.cache.js +1 -0
  98. package/build/tiff.cache.js.map +1 -0
  99. package/build/tile.set.cache.d.ts +0 -1
  100. package/build/tile.set.cache.d.ts.map +1 -1
  101. package/build/tile.set.cache.js +1 -29
  102. package/build/tile.set.cache.js.map +1 -0
  103. package/build/tile.set.js +1 -0
  104. package/build/tile.set.js.map +1 -0
  105. package/build/tile.set.raster.d.ts.map +1 -1
  106. package/build/tile.set.raster.js +6 -3
  107. package/build/tile.set.raster.js.map +1 -0
  108. package/build/tile.set.vector.d.ts +0 -7
  109. package/build/tile.set.vector.d.ts.map +1 -1
  110. package/build/tile.set.vector.js +3 -20
  111. package/build/tile.set.vector.js.map +1 -0
  112. package/build/validate.js +1 -0
  113. package/build/validate.js.map +1 -0
  114. package/build/wmts.capability.d.ts +26 -12
  115. package/build/wmts.capability.d.ts.map +1 -1
  116. package/build/wmts.capability.js +136 -51
  117. package/build/wmts.capability.js.map +1 -0
  118. package/dist/index.js +86 -72
  119. package/dist/node_modules/.package-lock.json +1 -1
  120. package/dist/package-lock.json +2 -2
  121. package/dist/package.json +1 -1
  122. package/package.json +7 -7
  123. package/src/__tests__/config.data.ts +82 -0
  124. package/src/__tests__/index.test.ts +3 -3
  125. package/src/__tests__/tile.set.cache.test.ts +1 -67
  126. package/src/__tests__/wmts.capability.test.ts +224 -154
  127. package/src/__tests__/xyz.test.ts +5 -49
  128. package/src/__tests__/xyz.util.ts +6 -31
  129. package/src/cotar.cache.ts +54 -0
  130. package/src/index.ts +30 -33
  131. package/src/routes/__tests__/attribution.test.ts +4 -4
  132. package/src/routes/__tests__/fonts.test.ts +10 -1
  133. package/src/routes/__tests__/wmts.test.ts +75 -15
  134. package/src/routes/api.ts +0 -4
  135. package/src/routes/fonts.ts +9 -1
  136. package/src/routes/imagery.ts +9 -7
  137. package/src/routes/sprites.ts +7 -15
  138. package/src/routes/tile.json.ts +1 -2
  139. package/src/routes/tile.wmts.ts +20 -22
  140. package/src/tile.set.cache.ts +1 -28
  141. package/src/tile.set.raster.ts +4 -2
  142. package/src/tile.set.vector.ts +3 -21
  143. package/src/wmts.capability.ts +143 -58
  144. 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
- // Reset the request tracing
26
- St.reset();
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
- // Warn if a request takes more than 10 seconds to process
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
- try {
35
- const apiKey = Router.apiKey(req);
36
- if (apiKey != null) {
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
- export const handler = lf.http(LogConfig.get());
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, Provider } from '../../__tests__/xyz.util.js';
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('3edkgmltK4/LUyTCTYU9MeiNSwlfUvJAx/qORSisUzM=');
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]: '3edkgmltK4/LUyTCTYU9MeiNSwlfUvJAx/qORSisUzM=',
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
- o.spec('GetImageFormats', () => {
9
- function newRequest(path: string, query: string): LambdaHttpRequest {
10
- return new LambdaUrlRequest(
11
- {
12
- requestContext: { http: { method: 'GET' } },
13
- headers: {},
14
- rawPath: path,
15
- rawQueryString: query,
16
- isBase64Encoded: false,
17
- } as any,
18
- {} as Context,
19
- LogConfig.get(),
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
  }
@@ -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, path.join('fonts', req.params.fontStack, req.params.range)) + '.pbf';
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);
@@ -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 Imagery(req: LambdaHttpRequest): Promise<LambdaHttpResponse> {
31
- const { rest } = Router.action(req);
32
- const [imageryId, requestType] = rest;
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, requestType);
40
+ const targetPath = fsa.join(imagery.uri, requestedFile);
39
41
 
40
42
  try {
41
43
  const buf = await fsa.read(targetPath);
@@ -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 spriteLocation = Env.get(Env.AssetLocation);
31
- if (spriteLocation == null) return NotFound;
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(spriteLocation, fsa.join('/sprites', req.params.spriteName));
30
+ const filePath = fsa.join(assetLocation, targetFile);
39
31
  req.set('target', filePath);
40
32
 
41
33
  const buf = await fsa.read(filePath);
@@ -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, getTileMatrixId(tileMatrix), '{z}', '{x}', '{y}'].join('/') +
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' };
@@ -1,19 +1,17 @@
1
1
  import { Config, TileSetType } from '@basemaps/config';
2
- import { ImageFormat, TileMatrixSet } from '@basemaps/geo';
3
- import { Env, TileSetName, tileWmtsFromPath } from '@basemaps/shared';
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 == null || formats.length === 0) return undefined;
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 undefined;
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 tileSets = await wmtsLoadTileSets(wmtsData.name, wmtsData.tileMatrix);
39
+ const tileSet = await Config.TileSet.get(Config.TileSet.id(wmtsData.name ?? 'aerial'));
42
40
  req.timer.end('tileset:load');
43
- if (tileSets.length === 0) return NotFound;
41
+ if (tileSet == null || tileSet.type !== TileSetType.Raster) return NotFound;
44
42
 
45
- const providerId = Config.Provider.id('linz');
46
- const provider = await Config.Provider.get(providerId);
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
- layers: tileSets,
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
- }
@@ -1,5 +1,5 @@
1
1
  import { TileSetNameParser, TileSetType } from '@basemaps/config';
2
- import { TileMatrixSet, TileMatrixSets } from '@basemaps/geo';
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();
@@ -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
 
@@ -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 { fsa, TileDataXyz } from '@basemaps/shared';
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 Layers.get(layer[3857]);
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