@basemaps/lambda-tiler 6.29.0 → 6.32.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 (259) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/build/__tests__/config.data.d.ts +11 -0
  3. package/build/__tests__/config.data.d.ts.map +1 -0
  4. package/build/__tests__/config.data.js +112 -0
  5. package/build/__tests__/config.data.js.map +1 -0
  6. package/build/__tests__/index.test.js +5 -14
  7. package/build/__tests__/index.test.js.map +1 -0
  8. package/build/__tests__/tile.style.json.test.js +1 -0
  9. package/build/__tests__/tile.style.json.test.js.map +1 -0
  10. package/build/__tests__/wmts.capability.test.d.ts +1 -1
  11. package/build/__tests__/wmts.capability.test.d.ts.map +1 -1
  12. package/build/__tests__/wmts.capability.test.js +286 -125
  13. package/build/__tests__/wmts.capability.test.js.map +1 -0
  14. package/build/__tests__/xyz.util.d.ts +7 -11
  15. package/build/__tests__/xyz.util.d.ts.map +1 -1
  16. package/build/__tests__/xyz.util.js +14 -42
  17. package/build/__tests__/xyz.util.js.map +1 -0
  18. package/build/index.d.ts +0 -2
  19. package/build/index.d.ts.map +1 -1
  20. package/build/index.js +68 -41
  21. package/build/index.js.map +1 -0
  22. package/build/routes/__tests__/attribution.test.js +351 -399
  23. package/build/routes/__tests__/attribution.test.js.map +1 -0
  24. package/build/routes/__tests__/fonts.test.js +17 -3
  25. package/build/routes/__tests__/fonts.test.js.map +1 -0
  26. package/build/routes/__tests__/health.test.js +17 -13
  27. package/build/routes/__tests__/health.test.js.map +1 -0
  28. package/build/routes/__tests__/imagery.test.js +1 -0
  29. package/build/routes/__tests__/imagery.test.js.map +1 -0
  30. package/build/routes/__tests__/memory.fs.js +1 -0
  31. package/build/routes/__tests__/memory.fs.js.map +1 -0
  32. package/build/routes/__tests__/sprites.test.js +7 -0
  33. package/build/routes/__tests__/sprites.test.js.map +1 -0
  34. package/build/routes/__tests__/tile.json.test.d.ts +2 -0
  35. package/build/routes/__tests__/tile.json.test.d.ts.map +1 -0
  36. package/build/routes/__tests__/tile.json.test.js +124 -0
  37. package/build/routes/__tests__/tile.json.test.js.map +1 -0
  38. package/build/routes/__tests__/tile.style.json.test.d.ts +2 -0
  39. package/build/routes/__tests__/tile.style.json.test.d.ts.map +1 -0
  40. package/build/routes/__tests__/tile.style.json.test.js +95 -0
  41. package/build/routes/__tests__/tile.style.json.test.js.map +1 -0
  42. package/build/routes/__tests__/wmts.test.js +37 -27
  43. package/build/routes/__tests__/wmts.test.js.map +1 -0
  44. package/build/{__tests__ → routes/__tests__}/xyz.test.d.ts +0 -0
  45. package/build/routes/__tests__/xyz.test.d.ts.map +1 -0
  46. package/build/routes/__tests__/xyz.test.js +99 -0
  47. package/build/routes/__tests__/xyz.test.js.map +1 -0
  48. package/build/routes/attribution.d.ts +7 -5
  49. package/build/routes/attribution.d.ts.map +1 -1
  50. package/build/routes/attribution.js +50 -91
  51. package/build/routes/attribution.js.map +1 -0
  52. package/build/routes/fonts.d.ts +1 -1
  53. package/build/routes/fonts.d.ts.map +1 -1
  54. package/build/routes/fonts.js +33 -10
  55. package/build/routes/fonts.js.map +1 -0
  56. package/build/routes/health.d.ts +3 -3
  57. package/build/routes/health.d.ts.map +1 -1
  58. package/build/routes/health.js +16 -13
  59. package/build/routes/health.js.map +1 -0
  60. package/build/routes/imagery.d.ts +8 -1
  61. package/build/routes/imagery.d.ts.map +1 -1
  62. package/build/routes/imagery.js +17 -17
  63. package/build/routes/imagery.js.map +1 -0
  64. package/build/routes/ping.d.ts +3 -0
  65. package/build/routes/ping.d.ts.map +1 -0
  66. package/build/routes/ping.js +7 -0
  67. package/build/routes/ping.js.map +1 -0
  68. package/build/routes/sprites.d.ts.map +1 -1
  69. package/build/routes/sprites.js +22 -22
  70. package/build/routes/sprites.js.map +1 -0
  71. package/build/routes/tile.json.d.ts +7 -1
  72. package/build/routes/tile.json.d.ts.map +1 -1
  73. package/build/routes/tile.json.js +19 -22
  74. package/build/routes/tile.json.js.map +1 -0
  75. package/build/routes/tile.style.json.d.ts +6 -1
  76. package/build/routes/tile.style.json.d.ts.map +1 -1
  77. package/build/routes/tile.style.json.js +11 -13
  78. package/build/routes/tile.style.json.js.map +1 -0
  79. package/build/routes/tile.wmts.d.ts +9 -3
  80. package/build/routes/tile.wmts.d.ts.map +1 -1
  81. package/build/routes/tile.wmts.js +37 -50
  82. package/build/routes/tile.wmts.js.map +1 -0
  83. package/build/routes/tile.xyz.d.ts +14 -4
  84. package/build/routes/tile.xyz.d.ts.map +1 -1
  85. package/build/routes/tile.xyz.js +22 -17
  86. package/build/routes/tile.xyz.js.map +1 -0
  87. package/build/routes/tile.xyz.raster.d.ts +11 -0
  88. package/build/routes/tile.xyz.raster.d.ts.map +1 -0
  89. package/build/routes/tile.xyz.raster.js +90 -0
  90. package/build/routes/tile.xyz.raster.js.map +1 -0
  91. package/build/routes/tile.xyz.vector.d.ts +8 -0
  92. package/build/routes/tile.xyz.vector.d.ts.map +1 -0
  93. package/build/routes/tile.xyz.vector.js +46 -0
  94. package/build/routes/tile.xyz.vector.js.map +1 -0
  95. package/build/routes/version.d.ts +3 -0
  96. package/build/routes/version.d.ts.map +1 -0
  97. package/build/routes/version.js +9 -0
  98. package/build/routes/version.js.map +1 -0
  99. package/build/util/__test__/validate.test.d.ts +2 -0
  100. package/build/util/__test__/validate.test.d.ts.map +1 -0
  101. package/build/util/__test__/validate.test.js +66 -0
  102. package/build/util/__test__/validate.test.js.map +1 -0
  103. package/build/util/cotar.serve.d.ts +20 -0
  104. package/build/util/cotar.serve.d.ts.map +1 -0
  105. package/build/util/cotar.serve.js +41 -0
  106. package/build/util/cotar.serve.js.map +1 -0
  107. package/build/util/etag.d.ts +6 -0
  108. package/build/util/etag.d.ts.map +1 -0
  109. package/build/util/etag.js +20 -0
  110. package/build/util/etag.js.map +1 -0
  111. package/build/util/response.d.ts +4 -0
  112. package/build/util/response.d.ts.map +1 -0
  113. package/build/util/response.js +4 -0
  114. package/build/util/response.js.map +1 -0
  115. package/build/util/source.cache.d.ts +28 -0
  116. package/build/util/source.cache.d.ts.map +1 -0
  117. package/build/util/source.cache.js +53 -0
  118. package/build/util/source.cache.js.map +1 -0
  119. package/build/{source.tracer.d.ts → util/source.tracer.d.ts} +1 -0
  120. package/build/util/source.tracer.d.ts.map +1 -0
  121. package/build/{source.tracer.js → util/source.tracer.js} +4 -0
  122. package/build/util/source.tracer.js.map +1 -0
  123. package/build/util/swapping.lru.d.ts +21 -0
  124. package/build/util/swapping.lru.d.ts.map +1 -0
  125. package/build/util/swapping.lru.js +56 -0
  126. package/build/util/swapping.lru.js.map +1 -0
  127. package/build/util/validate.d.ts +46 -0
  128. package/build/util/validate.d.ts.map +1 -0
  129. package/build/util/validate.js +107 -0
  130. package/build/util/validate.js.map +1 -0
  131. package/build/wmts.capability.d.ts +27 -13
  132. package/build/wmts.capability.d.ts.map +1 -1
  133. package/build/wmts.capability.js +156 -55
  134. package/build/wmts.capability.js.map +1 -0
  135. package/dist/index.js +89 -73
  136. package/dist/node_modules/.package-lock.json +1 -1
  137. package/dist/package-lock.json +2 -2
  138. package/dist/package.json +1 -1
  139. package/package.json +10 -10
  140. package/src/__tests__/config.data.ts +120 -0
  141. package/src/__tests__/index.test.ts +4 -20
  142. package/src/__tests__/wmts.capability.test.ts +312 -139
  143. package/src/__tests__/xyz.util.ts +17 -45
  144. package/src/index.ts +75 -41
  145. package/src/routes/__tests__/attribution.test.ts +356 -403
  146. package/src/routes/__tests__/fonts.test.ts +18 -3
  147. package/src/routes/__tests__/health.test.ts +17 -13
  148. package/src/routes/__tests__/sprites.test.ts +6 -1
  149. package/src/routes/__tests__/tile.json.test.ts +145 -0
  150. package/src/routes/__tests__/tile.style.json.test.ts +105 -0
  151. package/src/routes/__tests__/wmts.test.ts +44 -34
  152. package/src/routes/__tests__/xyz.test.ts +119 -0
  153. package/src/routes/attribution.ts +59 -111
  154. package/src/routes/fonts.ts +32 -10
  155. package/src/routes/health.ts +17 -16
  156. package/src/routes/imagery.ts +18 -15
  157. package/src/routes/ping.ts +8 -0
  158. package/src/routes/sprites.ts +20 -22
  159. package/src/routes/tile.json.ts +24 -19
  160. package/src/routes/tile.style.json.ts +15 -12
  161. package/src/routes/tile.wmts.ts +41 -44
  162. package/src/routes/tile.xyz.raster.ts +106 -0
  163. package/src/routes/tile.xyz.ts +31 -16
  164. package/src/routes/tile.xyz.vector.ts +47 -0
  165. package/src/routes/version.ts +8 -0
  166. package/src/util/__test__/validate.test.ts +74 -0
  167. package/src/util/cotar.serve.ts +46 -0
  168. package/src/util/etag.ts +20 -0
  169. package/src/util/response.ts +4 -0
  170. package/src/util/source.cache.ts +71 -0
  171. package/src/{source.tracer.ts → util/source.tracer.ts} +4 -0
  172. package/src/util/swapping.lru.ts +63 -0
  173. package/src/util/validate.ts +126 -0
  174. package/src/wmts.capability.ts +170 -68
  175. package/tsconfig.tsbuildinfo +1 -1
  176. package/build/__tests__/route.test.d.ts +0 -2
  177. package/build/__tests__/route.test.d.ts.map +0 -1
  178. package/build/__tests__/route.test.js +0 -20
  179. package/build/__tests__/tiff.cache.test.d.ts +0 -2
  180. package/build/__tests__/tiff.cache.test.d.ts.map +0 -1
  181. package/build/__tests__/tiff.cache.test.js +0 -58
  182. package/build/__tests__/tile.cache.key.test.d.ts +0 -2
  183. package/build/__tests__/tile.cache.key.test.d.ts.map +0 -1
  184. package/build/__tests__/tile.cache.key.test.js +0 -48
  185. package/build/__tests__/tile.set.cache.test.d.ts +0 -2
  186. package/build/__tests__/tile.set.cache.test.d.ts.map +0 -1
  187. package/build/__tests__/tile.set.cache.test.js +0 -123
  188. package/build/__tests__/tile.set.test.d.ts +0 -2
  189. package/build/__tests__/tile.set.test.d.ts.map +0 -1
  190. package/build/__tests__/tile.set.test.js +0 -11
  191. package/build/__tests__/xyz.test.d.ts.map +0 -1
  192. package/build/__tests__/xyz.test.js +0 -306
  193. package/build/api.key.d.ts +0 -2
  194. package/build/api.key.d.ts.map +0 -1
  195. package/build/api.key.js +0 -23
  196. package/build/cli/dump.d.ts +0 -2
  197. package/build/cli/dump.d.ts.map +0 -1
  198. package/build/cli/dump.js +0 -47
  199. package/build/cli/tile.set.local.d.ts +0 -12
  200. package/build/cli/tile.set.local.d.ts.map +0 -1
  201. package/build/cli/tile.set.local.js +0 -39
  202. package/build/router.d.ts +0 -15
  203. package/build/router.d.ts.map +0 -1
  204. package/build/router.js +0 -49
  205. package/build/routes/api.d.ts +0 -5
  206. package/build/routes/api.d.ts.map +0 -1
  207. package/build/routes/api.js +0 -16
  208. package/build/routes/esri/rest.d.ts +0 -10
  209. package/build/routes/esri/rest.d.ts.map +0 -1
  210. package/build/routes/esri/rest.js +0 -87
  211. package/build/routes/response.d.ts +0 -4
  212. package/build/routes/response.d.ts.map +0 -1
  213. package/build/routes/response.js +0 -3
  214. package/build/routes/tile.d.ts +0 -3
  215. package/build/routes/tile.d.ts.map +0 -1
  216. package/build/routes/tile.etag.d.ts +0 -11
  217. package/build/routes/tile.etag.d.ts.map +0 -1
  218. package/build/routes/tile.etag.js +0 -29
  219. package/build/routes/tile.js +0 -27
  220. package/build/source.tracer.d.ts.map +0 -1
  221. package/build/tiff.cache.d.ts +0 -17
  222. package/build/tiff.cache.d.ts.map +0 -1
  223. package/build/tiff.cache.js +0 -45
  224. package/build/tile.set.cache.d.ts +0 -21
  225. package/build/tile.set.cache.d.ts.map +0 -1
  226. package/build/tile.set.cache.js +0 -100
  227. package/build/tile.set.d.ts +0 -4
  228. package/build/tile.set.d.ts.map +0 -1
  229. package/build/tile.set.js +0 -1
  230. package/build/tile.set.raster.d.ts +0 -49
  231. package/build/tile.set.raster.d.ts.map +0 -1
  232. package/build/tile.set.raster.js +0 -186
  233. package/build/tile.set.vector.d.ts +0 -25
  234. package/build/tile.set.vector.d.ts.map +0 -1
  235. package/build/tile.set.vector.js +0 -71
  236. package/build/validate.d.ts +0 -16
  237. package/build/validate.d.ts.map +0 -1
  238. package/build/validate.js +0 -31
  239. package/src/__tests__/route.test.ts +0 -24
  240. package/src/__tests__/tiff.cache.test.ts +0 -73
  241. package/src/__tests__/tile.cache.key.test.ts +0 -56
  242. package/src/__tests__/tile.set.cache.test.ts +0 -146
  243. package/src/__tests__/tile.set.test.ts +0 -12
  244. package/src/__tests__/xyz.test.ts +0 -362
  245. package/src/api.key.ts +0 -23
  246. package/src/cli/dump.ts +0 -61
  247. package/src/cli/tile.set.local.ts +0 -51
  248. package/src/router.ts +0 -58
  249. package/src/routes/api.ts +0 -19
  250. package/src/routes/esri/rest.ts +0 -90
  251. package/src/routes/response.ts +0 -4
  252. package/src/routes/tile.etag.ts +0 -36
  253. package/src/routes/tile.ts +0 -23
  254. package/src/tiff.cache.ts +0 -51
  255. package/src/tile.set.cache.ts +0 -111
  256. package/src/tile.set.raster.ts +0 -228
  257. package/src/tile.set.ts +0 -4
  258. package/src/tile.set.vector.ts +0 -79
  259. package/src/validate.ts +0 -32
@@ -2,11 +2,10 @@ import { Sources, StyleJson } from '@basemaps/config';
2
2
  import { Config, Env } from '@basemaps/shared';
3
3
  import { fsa } from '@chunkd/fs';
4
4
  import { HttpHeader, LambdaHttpRequest, LambdaHttpResponse } from '@linzjs/lambda';
5
- import { createHash } from 'crypto';
6
5
  import { URL } from 'url';
7
- import { Router } from '../router.js';
8
- import { NotFound, NotModified } from './response.js';
9
- import { TileEtag } from './tile.etag.js';
6
+ import { NotFound, NotModified } from '../util/response.js';
7
+ import { Validate } from '../util/validate.js';
8
+ import { Etag } from '../util/etag.js';
10
9
 
11
10
  /**
12
11
  * Convert relative URLS into a full hostname url
@@ -54,23 +53,27 @@ export function convertStyleJson(style: StyleJson, apiKey: string): StyleJson {
54
53
  } as StyleJson;
55
54
  }
56
55
 
57
- export async function styleJson(req: LambdaHttpRequest, fileName: string): Promise<LambdaHttpResponse> {
58
- const apiKey = Router.apiKey(req);
59
- if (apiKey == null) return new LambdaHttpResponse(400, 'Invalid API Key.');
60
- const styleName = fileName.split('.json')[0];
56
+ export interface StyleGet {
57
+ Params: {
58
+ styleName: string;
59
+ };
60
+ }
61
+
62
+ export async function styleJsonGet(req: LambdaHttpRequest<StyleGet>): Promise<LambdaHttpResponse> {
63
+ const apiKey = Validate.apiKey(req);
64
+ const styleName = req.params.styleName;
61
65
 
62
66
  // Get style Config from db
63
67
  const dbId = Config.Style.id(styleName);
64
68
  const styleConfig = await Config.Style.get(dbId);
65
- if (styleConfig == null) return NotFound;
69
+ if (styleConfig == null) return NotFound();
66
70
 
67
71
  // Prepare sources and add linz source
68
72
  const style = convertStyleJson(styleConfig.style, apiKey);
69
73
  const data = Buffer.from(JSON.stringify(style));
70
74
 
71
- const cacheKey = createHash('sha256').update(data).digest('base64');
72
-
73
- if (TileEtag.isNotModified(req, cacheKey)) return NotModified;
75
+ const cacheKey = Etag.key(data);
76
+ if (Etag.isNotModified(req, cacheKey)) return NotModified();
74
77
 
75
78
  const response = new LambdaHttpResponse(200, 'ok');
76
79
  response.header(HttpHeader.ETag, cacheKey);
@@ -1,79 +1,76 @@
1
1
  import { Config, TileSetType } from '@basemaps/config';
2
- import { ImageFormat, TileMatrixSet } from '@basemaps/geo';
3
- import { Env, TileSetName, tileWmtsFromPath } from '@basemaps/shared';
4
- import { getImageFormat } from '@basemaps/tiler';
2
+ import { GoogleTms, Nztm2000QuadTms, TileMatrixSet } from '@basemaps/geo';
3
+ import { Env } from '@basemaps/shared';
5
4
  import { HttpHeader, LambdaHttpRequest, LambdaHttpResponse } from '@linzjs/lambda';
6
5
  import { createHash } from 'crypto';
7
- import { Router } from '../router.js';
8
- import { TileSets } from '../tile.set.cache.js';
9
- import { TileSetRaster } from '../tile.set.raster.js';
6
+ import { NotFound, NotModified } from '../util/response.js';
7
+ import { Validate } from '../util/validate.js';
10
8
  import { WmtsCapabilities } from '../wmts.capability.js';
11
- import { NotFound, NotModified } from './response.js';
12
- import { TileEtag } from './tile.etag.js';
9
+ import { Etag } from '../util/etag.js';
13
10
 
14
- export function getImageFormats(req: LambdaHttpRequest): ImageFormat[] | undefined {
15
- const formats = req.query.getAll('format');
16
- if (formats == null || formats.length === 0) return undefined;
17
-
18
- const output: Set<ImageFormat> = new Set();
19
- for (const fmt of formats) {
20
- const parsed = getImageFormat(fmt);
21
- if (parsed == null) continue;
22
- output.add(parsed);
23
- }
24
- if (output.size === 0) return undefined;
25
- return [...output.values()];
11
+ export interface WmtsCapabilitiesGet {
12
+ Params: {
13
+ tileSet?: string;
14
+ tileMatrix?: string;
15
+ };
26
16
  }
27
17
 
18
+ export function getWmtsTileMatrix(tileMatrixParam?: string): TileMatrixSet[] | null {
19
+ if (tileMatrixParam == null) return [GoogleTms, Nztm2000QuadTms];
20
+ const tileMatrix = Validate.getTileMatrixSet(tileMatrixParam);
21
+ if (tileMatrix == null) return null;
22
+ return [tileMatrix];
23
+ }
28
24
  /**
29
25
  * Serve a WMTS request
30
26
  *
31
27
  * /v1/tiles/:tileSet/:tileMatrixSet/WMTSCapabilities.xml
32
28
  * @example `/v1/tiles/aerial/NZTM2000Quad/WMTSCapabilities.xml`
33
29
  */
34
- export async function wmts(req: LambdaHttpRequest): Promise<LambdaHttpResponse> {
35
- const action = Router.action(req);
36
- const wmtsData = tileWmtsFromPath(action.rest);
37
- if (wmtsData == null) return NotFound;
30
+ export async function wmtsCapabilitiesGet(req: LambdaHttpRequest<WmtsCapabilitiesGet>): Promise<LambdaHttpResponse> {
31
+ const apiKey = Validate.apiKey(req);
32
+
33
+ const tileSetName = req.params.tileSet ?? 'aerial';
34
+ const tileMatrix = getWmtsTileMatrix(req.params.tileMatrix);
35
+ if (tileMatrix == null) return NotFound();
36
+
38
37
  const host = Env.get(Env.PublicUrlBase) ?? '';
39
38
 
40
39
  req.timer.start('tileset:load');
41
- const tileSets = await wmtsLoadTileSets(wmtsData.name, wmtsData.tileMatrix);
40
+ const tileSet = await Config.TileSet.get(Config.TileSet.id(tileSetName ?? 'aerial'));
42
41
  req.timer.end('tileset:load');
43
- if (tileSets.length === 0) return NotFound;
42
+ if (tileSet == null || tileSet.type !== TileSetType.Raster) return NotFound();
44
43
 
45
- const providerId = Config.Provider.id('linz');
46
- const provider = await Config.Provider.get(providerId);
44
+ const provider = await Config.Provider.get(Config.Provider.id('linz'));
45
+
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
- const apiKey = Router.apiKey(req);
49
53
  const xml = new WmtsCapabilities({
50
54
  httpBase: host,
51
55
  provider: provider ?? undefined,
52
- layers: tileSets,
56
+ tileSet,
57
+ tileMatrix,
58
+ isIndividualLayers: req.params.tileMatrix == null,
59
+ imagery,
53
60
  apiKey,
54
- formats: getImageFormats(req),
61
+ formats: Validate.getRequestedFormats(req),
55
62
  }).toXml();
56
- if (xml == null) return NotFound;
63
+ if (xml == null) return NotFound();
57
64
 
58
65
  const data = Buffer.from(xml);
59
66
 
60
67
  const cacheKey = createHash('sha256').update(data).digest('base64');
61
- if (TileEtag.isNotModified(req, cacheKey)) return NotModified;
68
+ if (Etag.isNotModified(req, cacheKey)) return NotModified();
62
69
 
63
70
  const response = new LambdaHttpResponse(200, 'ok');
64
71
  response.header(HttpHeader.ETag, cacheKey);
65
- response.header(HttpHeader.CacheControl, 'max-age=0');
72
+ response.header(HttpHeader.CacheControl, 'no-store');
66
73
  response.buffer(data, 'text/xml');
67
74
  req.set('bytes', data.byteLength);
68
75
  return response;
69
76
  }
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
- }
@@ -0,0 +1,106 @@
1
+ import { Config, ConfigTileSetRaster } from '@basemaps/config';
2
+ import { Bounds, Epsg, TileMatrixSet, TileMatrixSets, VectorFormat } from '@basemaps/geo';
3
+ import { Env, fsa } from '@basemaps/shared';
4
+ import { Tiler } from '@basemaps/tiler';
5
+ import { TileMakerSharp } from '@basemaps/tiler-sharp';
6
+ import { CogTiff } from '@cogeotiff/core';
7
+ import { HttpHeader, LambdaHttpRequest, LambdaHttpResponse } from '@linzjs/lambda';
8
+ import pLimit from 'p-limit';
9
+ import { Etag } from '../util/etag.js';
10
+ import { NotFound, NotModified } from '../util/response.js';
11
+ import { CoSources } from '../util/source.cache.js';
12
+ import { TileXyz } from '../util/validate.js';
13
+
14
+ const LoadingQueue = pLimit(Env.getNumber(Env.TiffConcurrency, 25));
15
+
16
+ export function getTiffName(name: string): string {
17
+ const lowerName = name.toLowerCase();
18
+ if (lowerName.endsWith('.tif') || lowerName.endsWith('.tiff')) return name;
19
+ return `${name}.tiff`;
20
+ }
21
+
22
+ export const TileComposer = new TileMakerSharp(256);
23
+
24
+ const DefaultResizeKernel = { in: 'lanczos3', out: 'lanczos3' } as const;
25
+ const DefaultBackground = { r: 0, g: 0, b: 0, alpha: 0 };
26
+
27
+ export const TileXyzRaster = {
28
+ async getTiffsForTile(req: LambdaHttpRequest, tileSet: ConfigTileSetRaster, xyz: TileXyz): Promise<string[]> {
29
+ const imagery = await Config.getAllImagery(tileSet.layers, [xyz.tileMatrix.projection]);
30
+
31
+ const output: string[] = [];
32
+ const tileBounds = xyz.tileMatrix.tileToSourceBounds(xyz.tile);
33
+
34
+ // All zoom level config is stored as Google zoom levels
35
+ const filterZoom = TileMatrixSet.convertZoomLevel(xyz.tile.z, xyz.tileMatrix, TileMatrixSets.get(Epsg.Google));
36
+ for (const layer of tileSet.layers) {
37
+ if (layer.maxZoom != null && filterZoom > layer.maxZoom) continue;
38
+ if (layer.minZoom != null && filterZoom < layer.minZoom) continue;
39
+
40
+ const imgId = layer[xyz.tileMatrix.projection.code];
41
+ if (imgId == null) {
42
+ req.log.warn({ layer: layer.name, projection: xyz.tileMatrix.projection.code }, 'Failed to lookup imagery');
43
+ continue;
44
+ }
45
+
46
+ const img = imagery.get(imgId);
47
+ if (img == null) {
48
+ req.log.warn(
49
+ { layer: layer.name, projection: xyz.tileMatrix.projection.code, imgId },
50
+ 'Failed to lookup imagery',
51
+ );
52
+ continue;
53
+ }
54
+ if (!tileBounds.intersects(Bounds.fromJson(img.bounds))) continue;
55
+
56
+ for (const c of img.files) {
57
+ if (!tileBounds.intersects(Bounds.fromJson(c))) continue;
58
+ const tiffPath = fsa.join(img.uri, getTiffName(c.name));
59
+ output.push(tiffPath);
60
+ }
61
+ }
62
+ return output;
63
+ },
64
+
65
+ async tile(req: LambdaHttpRequest, tileSet: ConfigTileSetRaster, xyz: TileXyz): Promise<LambdaHttpResponse> {
66
+ if (xyz.tileType === VectorFormat.MapboxVectorTiles) return NotFound();
67
+
68
+ const tiffPaths = await this.getTiffsForTile(req, tileSet, xyz);
69
+ const cacheKey = Etag.key(tiffPaths);
70
+ if (Etag.isNotModified(req, cacheKey)) return NotModified();
71
+
72
+ const toLoad: Promise<CogTiff | null>[] = [];
73
+ for (const tiffPath of tiffPaths) {
74
+ toLoad.push(
75
+ LoadingQueue(() => {
76
+ return CoSources.getCog(tiffPath).catch((error) => {
77
+ req.log.warn({ error, tiff: tiffPath }, 'TiffLoadFailed');
78
+ return null;
79
+ });
80
+ }),
81
+ );
82
+ }
83
+
84
+ const tiffs = (await Promise.all(toLoad)).filter((f) => f != null) as CogTiff[];
85
+
86
+ const tiler = new Tiler(xyz.tileMatrix);
87
+ const layers = await tiler.tile(tiffs, xyz.tile.x, xyz.tile.y, xyz.tile.z);
88
+
89
+ const res = await TileComposer.compose({
90
+ layers,
91
+ format: xyz.tileType,
92
+ background: tileSet.background ?? DefaultBackground,
93
+ resizeKernel: tileSet.resizeKernel ?? DefaultResizeKernel,
94
+ metrics: req.timer,
95
+ });
96
+
97
+ req.set('layersUsed', res.layers);
98
+ req.set('bytes', res.buffer.byteLength);
99
+
100
+ const response = new LambdaHttpResponse(200, 'ok');
101
+ response.header(HttpHeader.ETag, cacheKey);
102
+ response.header(HttpHeader.CacheControl, 'public, max-age=604800, stale-while-revalidate=86400');
103
+ response.buffer(res.buffer, 'image/' + xyz.tileType);
104
+ return response;
105
+ },
106
+ };
@@ -1,9 +1,20 @@
1
- import { tileXyzFromPath } from '@basemaps/shared';
1
+ import { Config, TileSetType } from '@basemaps/config';
2
2
  import { LambdaHttpRequest, LambdaHttpResponse } from '@linzjs/lambda';
3
- import { Router } from '../router.js';
4
- import { TileSets } from '../tile.set.cache.js';
5
- import { ValidateTilePath } from '../validate.js';
6
- import { NotFound } from './response.js';
3
+ import { NotFound } from '../util/response.js';
4
+ import { Validate } from '../util/validate.js';
5
+ import { TileXyzRaster } from './tile.xyz.raster.js';
6
+ import { tileXyzVector } from './tile.xyz.vector.js';
7
+
8
+ export interface TileXyzGet {
9
+ Params: {
10
+ tileSet: string;
11
+ tileMatrix: string;
12
+ z: string;
13
+ x: string;
14
+ y: string;
15
+ tileType: string;
16
+ };
17
+ }
7
18
 
8
19
  /**
9
20
  * Serve a tile
@@ -11,20 +22,24 @@ import { NotFound } from './response.js';
11
22
  * /v1/tiles/:tileSet/:tileMatrixSet/:z/:x/:y.:tileType
12
23
  *
13
24
  * @example
14
- * Vector Tile `/v1/tiles/topographic/EPSG:3857/2/1/1.pbf`
15
- * Raster Tile `/v1/tiles/aerial/EPSG:3857/6/0/38.webp`
16
- * @returns
25
+ * Vector Tile `/v1/tiles/topographic/WebMercatorQuad/2/1/1.pbf`
26
+ * Raster Tile `/v1/tiles/aerial/WebMercatorQuad/6/0/38.webp`
27
+ *
17
28
  */
18
- export async function tileXyz(req: LambdaHttpRequest): Promise<LambdaHttpResponse> {
19
- const action = Router.action(req);
20
- const xyzData = tileXyzFromPath(action.rest);
21
- if (xyzData == null) return NotFound;
22
- ValidateTilePath.validate(req, xyzData);
29
+ export async function tileXyzGet(req: LambdaHttpRequest<TileXyzGet>): Promise<LambdaHttpResponse> {
30
+ const xyzData = Validate.xyz(req);
23
31
 
24
32
  req.timer.start('tileset:load');
25
- const tileSet = await TileSets.get(xyzData.name, xyzData.tileMatrix);
33
+ const tileSet = await Config.TileSet.get(Config.TileSet.id(xyzData.tileSet));
26
34
  req.timer.end('tileset:load');
27
- if (tileSet == null) return NotFound;
35
+ if (tileSet == null) return NotFound();
28
36
 
29
- return await tileSet.tile(req, xyzData);
37
+ switch (tileSet.type) {
38
+ case TileSetType.Vector:
39
+ return tileXyzVector.tile(req, tileSet, xyzData);
40
+ case TileSetType.Raster:
41
+ return TileXyzRaster.tile(req, tileSet, xyzData);
42
+ default:
43
+ return new LambdaHttpResponse(400, 'Invalid tileset');
44
+ }
30
45
  }
@@ -0,0 +1,47 @@
1
+ import { ConfigTileSetVector } from '@basemaps/config';
2
+ import { GoogleTms, VectorFormat } from '@basemaps/geo';
3
+ import { HttpHeader, LambdaHttpRequest, LambdaHttpResponse } from '@linzjs/lambda';
4
+ import { isGzip } from '../util/cotar.serve.js';
5
+ import { Etag } from '../util/etag.js';
6
+ import { NotFound, NotModified } from '../util/response.js';
7
+ import { CoSources } from '../util/source.cache.js';
8
+ import { TileXyz } from '../util/validate.js';
9
+
10
+ export const tileXyzVector = {
11
+ /** Serve a MVT vector tile */
12
+ async tile(req: LambdaHttpRequest, tileSet: ConfigTileSetVector, xyz: TileXyz): Promise<LambdaHttpResponse> {
13
+ if (xyz.tileType !== VectorFormat.MapboxVectorTiles) return NotFound();
14
+ if (xyz.tileMatrix.identifier !== GoogleTms.identifier) return NotFound();
15
+
16
+ if (tileSet.layers.length > 1) return new LambdaHttpResponse(500, 'Too many layers in tileset');
17
+ const [layer] = tileSet.layers;
18
+ const layerId = layer[3857];
19
+ if (layerId == null) return new LambdaHttpResponse(500, 'Layer url not found from tileset Config');
20
+
21
+ // Flip Y coordinate because MBTiles files are TMS.
22
+ const y = (1 << xyz.tile.z) - 1 - xyz.tile.y;
23
+
24
+ const tilePath = `tiles/${xyz.tile.z}/${xyz.tile.x}/${y}.pbf.gz`;
25
+ const tileId = `${layerId}#${tilePath}`;
26
+
27
+ const cacheKey = Etag.key(tileId);
28
+ if (Etag.isNotModified(req, cacheKey)) return NotModified();
29
+
30
+ req.timer.start('cotar:load');
31
+ const cotar = await CoSources.getCotar(layerId);
32
+ if (cotar == null) return new LambdaHttpResponse(500, 'Failed to load VectorTiles');
33
+ req.timer.end('cotar:load');
34
+
35
+ req.timer.start('cotar:tile');
36
+ const tile = await cotar.get(tilePath);
37
+ if (tile == null) return NotFound();
38
+ req.timer.end('cotar:tile');
39
+
40
+ const tileBuffer = Buffer.from(tile);
41
+ const response = LambdaHttpResponse.ok().buffer(tileBuffer, 'application/x-protobuf');
42
+ response.header(HttpHeader.ETag, cacheKey);
43
+ response.header(HttpHeader.CacheControl, 'public, max-age=604800, stale-while-revalidate=86400');
44
+ if (isGzip(tileBuffer)) response.header(HttpHeader.ContentEncoding, 'gzip');
45
+ return response;
46
+ },
47
+ };
@@ -0,0 +1,8 @@
1
+ import { HttpHeader, LambdaHttpResponse } from '@linzjs/lambda';
2
+
3
+ export async function versionGet(): Promise<LambdaHttpResponse> {
4
+ const response = new LambdaHttpResponse(200, 'ok');
5
+ response.header(HttpHeader.CacheControl, 'no-store');
6
+ response.json({ version: process.env.GIT_VERSION ?? 'dev', hash: process.env.GIT_HASH });
7
+ return response;
8
+ }
@@ -0,0 +1,74 @@
1
+ import { GoogleTms, ImageFormat, Nztm2000QuadTms, Nztm2000Tms, VectorFormat } from '@basemaps/geo';
2
+ import o from 'ospec';
3
+ import { mockUrlRequest } from '../../__tests__/xyz.util.js';
4
+ import { Validate } from '../validate.js';
5
+
6
+ o.spec('GetImageFormats', () => {
7
+ o('should parse all formats', () => {
8
+ const req = mockUrlRequest('/v1/blank', 'format=png&format=jpeg');
9
+ const formats = Validate.getRequestedFormats(req);
10
+ o(formats).deepEquals([ImageFormat.Png, ImageFormat.Jpeg]);
11
+ });
12
+
13
+ o('should ignore bad formats', () => {
14
+ const req = mockUrlRequest('/v1/blank', 'format=fake&format=mvt');
15
+ const formats = Validate.getRequestedFormats(req);
16
+ o(formats).equals(null);
17
+ });
18
+
19
+ o('should de-dupe formats', () => {
20
+ const req = mockUrlRequest('/v1/blank', 'format=png&format=jpeg&format=png&format=jpeg&format=png&format=jpeg');
21
+ const formats = Validate.getRequestedFormats(req);
22
+ o(formats).deepEquals([ImageFormat.Png, ImageFormat.Jpeg]);
23
+ });
24
+
25
+ o('should support "tileFormat" Alias all formats', () => {
26
+ const req = mockUrlRequest('/v1/blank', 'tileFormat=png&format=jpeg');
27
+ const formats = Validate.getRequestedFormats(req);
28
+ o(formats).deepEquals([ImageFormat.Jpeg, ImageFormat.Png]);
29
+ });
30
+
31
+ o('should not duplicate "tileFormat" alias all formats', () => {
32
+ const req = mockUrlRequest('/v1/blank', 'tileFormat=jpeg&format=jpeg');
33
+ const formats = Validate.getRequestedFormats(req);
34
+ o(formats).deepEquals([ImageFormat.Jpeg]);
35
+ });
36
+ });
37
+
38
+ o.spec('getTileMatrixSet', () => {
39
+ o('should lookup epsg codes', () => {
40
+ o(Validate.getTileMatrixSet('EPSG:3857')?.identifier).equals(GoogleTms.identifier);
41
+ o(Validate.getTileMatrixSet('EPSG:2193')?.identifier).equals(Nztm2000Tms.identifier);
42
+
43
+ o(Validate.getTileMatrixSet('3857')?.identifier).equals(GoogleTms.identifier);
44
+ o(Validate.getTileMatrixSet('2193')?.identifier).equals(Nztm2000Tms.identifier);
45
+ });
46
+
47
+ o('should lookup by identifier', () => {
48
+ o(Validate.getTileMatrixSet('WebMercatorQuad')?.identifier).equals(GoogleTms.identifier);
49
+ o(Validate.getTileMatrixSet('NZTM2000Quad')?.identifier).equals(Nztm2000QuadTms.identifier);
50
+ o(Validate.getTileMatrixSet('Nztm2000')?.identifier).equals(Nztm2000Tms.identifier);
51
+ });
52
+
53
+ o('should be case sensitive', () => {
54
+ o(Validate.getTileMatrixSet('Nztm2000Quad')?.identifier).equals(undefined);
55
+ });
56
+ });
57
+
58
+ o.spec('getTileFormat', () => {
59
+ for (const ext of Object.values(ImageFormat)) {
60
+ o('should support image format:' + ext, () => {
61
+ o(Validate.getTileFormat(ext)).equals(ext);
62
+ });
63
+ }
64
+
65
+ o('should support vector format: mvt', () => {
66
+ o(Validate.getTileFormat('pbf')).equals(VectorFormat.MapboxVectorTiles);
67
+ });
68
+
69
+ for (const fmt of ['FAKE', /* 'JPEG' // TODO should this be case sensitive ,*/ 'mvt', 'json']) {
70
+ o('should not support format:' + fmt, () => {
71
+ o(Validate.getTileFormat(fmt)).equals(null);
72
+ });
73
+ }
74
+ });
@@ -0,0 +1,46 @@
1
+ import { HttpHeader, LambdaHttpRequest, LambdaHttpResponse } from '@linzjs/lambda';
2
+ import { Etag } from './etag.js';
3
+ import { NotFound, NotModified } from './response.js';
4
+ import { CoSources } from './source.cache.js';
5
+
6
+ /**
7
+ * Load a cotar and look for a file inside the cotar returning the file back as a LambdaResponse
8
+ *
9
+ * This will also set two headers
10
+ * - Content-Encoding if the file starts with gzip magic
11
+ * - Content-Type from the parameter contentType
12
+ */
13
+ export async function serveFromCotar(
14
+ req: LambdaHttpRequest,
15
+ cotarPath: string,
16
+ assetPath: string,
17
+ contentType: string,
18
+ ): Promise<LambdaHttpResponse> {
19
+ const cotar = await CoSources.getCotar(cotarPath);
20
+ if (cotar == null) return NotFound();
21
+ const fileData = await cotar.get(assetPath);
22
+ if (fileData == null) return NotFound();
23
+
24
+ const buf = Buffer.from(fileData);
25
+
26
+ const cacheKey = Etag.key(buf);
27
+ if (Etag.isNotModified(req, cacheKey)) return NotModified();
28
+
29
+ const response = LambdaHttpResponse.ok().buffer(buf, contentType);
30
+ response.header(HttpHeader.ETag, cacheKey);
31
+ response.header(HttpHeader.CacheControl, 'public, max-age=604800, stale-while-revalidate=86400');
32
+ if (isGzip(buf)) response.header(HttpHeader.ContentEncoding, 'gzip');
33
+ return response;
34
+ }
35
+
36
+ /**
37
+ * Does a buffer look like a gzipped document instead of raw json
38
+ *
39
+ * Determined by checking the first two bytes are the gzip magic bytes `0x1f 0x8b`
40
+ *
41
+ * @see https://en.wikipedia.org/wiki/Gzip
42
+ *
43
+ */
44
+ export function isGzip(b: Buffer): boolean {
45
+ return b[0] === 0x1f && b[1] === 0x8b;
46
+ }
@@ -0,0 +1,20 @@
1
+ import { sha256base58 } from '@basemaps/config';
2
+ import { HttpHeader, LambdaHttpRequest } from '@linzjs/lambda';
3
+
4
+ export const Etag = {
5
+ key(obj: unknown): string {
6
+ if (Buffer.isBuffer(obj) || typeof obj === 'string') return sha256base58(obj);
7
+ return sha256base58(JSON.stringify(obj));
8
+ },
9
+
10
+ isNotModified(req: LambdaHttpRequest, cacheKey: string): boolean {
11
+ // If the user has supplied a IfNoneMatch Header and it contains the full sha256 sum for our
12
+ // etag this tile has not been modified.
13
+ const ifNoneMatch = req.header(HttpHeader.IfNoneMatch);
14
+ if (ifNoneMatch != null && ifNoneMatch.indexOf(cacheKey) > -1) {
15
+ req.set('cache', { hit: true, match: ifNoneMatch });
16
+ return true;
17
+ }
18
+ return false;
19
+ },
20
+ };
@@ -0,0 +1,4 @@
1
+ import { LambdaHttpResponse } from '@linzjs/lambda';
2
+
3
+ export const NotFound = (): LambdaHttpResponse => new LambdaHttpResponse(404, 'Not Found');
4
+ export const NotModified = (): LambdaHttpResponse => new LambdaHttpResponse(304, 'Not modified');
@@ -0,0 +1,71 @@
1
+ import { fsa } from '@basemaps/shared';
2
+ import { ChunkSourceBase } from '@chunkd/core';
3
+ import { CogTiff } from '@cogeotiff/core';
4
+ import { Cotar } from '@cotar/core';
5
+ import { St } from './source.tracer.js';
6
+ import { SwappingLru } from './swapping.lru.js';
7
+
8
+ export type LruStrut = LruStrutCotar | LruStrutCog;
9
+
10
+ export interface LruStrutCotar {
11
+ type: 'cotar';
12
+ value: Promise<Cotar>;
13
+ _value?: Cotar;
14
+ }
15
+
16
+ export interface LruStrutCog {
17
+ type: 'cog';
18
+ value: Promise<CogTiff>;
19
+ _value?: CogTiff;
20
+ }
21
+
22
+ class LruStrutObj<T extends LruStrut> {
23
+ ob: T;
24
+ constructor(ob: T) {
25
+ this.ob = ob;
26
+ if (this.ob._value == null) this.ob.value.then((c) => (this.ob._value = c));
27
+ }
28
+
29
+ get size(): number {
30
+ const val = this.ob._value;
31
+ if (val == null) return 0;
32
+ return val.source.chunkSize * (val.source as ChunkSourceBase).chunks.size;
33
+ }
34
+ }
35
+
36
+ export class SourceCache {
37
+ cache: SwappingLru<LruStrutObj<LruStrutCotar | LruStrutCog>>;
38
+ constructor(maxSize: number) {
39
+ this.cache = new SwappingLru<LruStrutObj<LruStrut>>(maxSize);
40
+ }
41
+
42
+ getCog(location: string): Promise<CogTiff> {
43
+ const existing = this.cache.get(location)?.ob;
44
+
45
+ if (existing != null) {
46
+ if (existing.type === 'cog') return existing.value;
47
+ throw new Error(`Existing object of type: ${existing.type} made for location: ${location}`);
48
+ }
49
+ const source = fsa.source(location);
50
+ St.trace(source);
51
+ const value = CogTiff.create(source);
52
+ this.cache.set(location, new LruStrutObj({ type: 'cog', value }));
53
+ return value;
54
+ }
55
+
56
+ getCotar(location: string): Promise<Cotar> {
57
+ const existing = this.cache.get(location)?.ob;
58
+
59
+ if (existing != null) {
60
+ if (existing.type === 'cotar') return existing.value as Promise<Cotar>;
61
+ throw new Error(`Existing object of type: ${existing.type} made for location: ${location}`);
62
+ }
63
+ const source = fsa.source(location);
64
+ St.trace(source);
65
+ const value = Cotar.fromTar(source);
66
+ this.cache.set(location, new LruStrutObj({ type: 'cotar', value }));
67
+ return value;
68
+ }
69
+ }
70
+
71
+ export const CoSources = new SourceCache(256 * 1024 * 1024);
@@ -1,6 +1,8 @@
1
+ import { sha256base58 } from '@basemaps/config';
1
2
  import { ChunkSource } from '@chunkd/core';
2
3
 
3
4
  interface SourceRequest {
5
+ id?: string;
4
6
  offset: number;
5
7
  length?: number;
6
8
  source: string;
@@ -20,6 +22,8 @@ export class SourceTracer {
20
22
  const originFetch = source.fetchBytes;
21
23
  source.fetchBytes = async (offset: number, length?: number): Promise<ArrayBuffer> => {
22
24
  const request: SourceRequest = { source: source.uri, offset, length };
25
+ const traceId = sha256base58(`${request.source}:${request.offset}:${request.length}`);
26
+ request.id = traceId;
23
27
  this.requests.push(request);
24
28
  const startTime = Date.now();
25
29