@basemaps/lambda-tiler 6.31.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 (271) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/build/__tests__/config.data.d.ts +6 -1
  3. package/build/__tests__/config.data.d.ts.map +1 -1
  4. package/build/__tests__/config.data.js +33 -2
  5. package/build/__tests__/config.data.js.map +1 -1
  6. package/build/__tests__/index.test.js +3 -13
  7. package/build/__tests__/index.test.js.map +1 -1
  8. package/build/__tests__/xyz.util.d.ts +7 -9
  9. package/build/__tests__/xyz.util.d.ts.map +1 -1
  10. package/build/__tests__/xyz.util.js +14 -20
  11. package/build/__tests__/xyz.util.js.map +1 -1
  12. package/build/index.d.ts +0 -2
  13. package/build/index.d.ts.map +1 -1
  14. package/build/index.js +57 -26
  15. package/build/index.js.map +1 -1
  16. package/build/routes/__tests__/attribution.test.js +350 -400
  17. package/build/routes/__tests__/attribution.test.js.map +1 -1
  18. package/build/routes/__tests__/fonts.test.js +11 -5
  19. package/build/routes/__tests__/fonts.test.js.map +1 -1
  20. package/build/routes/__tests__/health.test.js +16 -13
  21. package/build/routes/__tests__/health.test.js.map +1 -1
  22. package/build/routes/__tests__/sprites.test.js +6 -0
  23. package/build/routes/__tests__/sprites.test.js.map +1 -1
  24. package/build/routes/__tests__/tile.json.test.d.ts +2 -0
  25. package/build/routes/__tests__/tile.json.test.d.ts.map +1 -0
  26. package/build/routes/__tests__/tile.json.test.js +124 -0
  27. package/build/routes/__tests__/tile.json.test.js.map +1 -0
  28. package/build/routes/__tests__/tile.style.json.test.d.ts +2 -0
  29. package/build/routes/__tests__/tile.style.json.test.d.ts.map +1 -0
  30. package/build/routes/__tests__/tile.style.json.test.js +95 -0
  31. package/build/routes/__tests__/tile.style.json.test.js.map +1 -0
  32. package/build/routes/__tests__/wmts.test.js +5 -44
  33. package/build/routes/__tests__/wmts.test.js.map +1 -1
  34. package/build/{__tests__ → routes/__tests__}/xyz.test.d.ts +0 -0
  35. package/build/routes/__tests__/xyz.test.d.ts.map +1 -0
  36. package/build/routes/__tests__/xyz.test.js +99 -0
  37. package/build/routes/__tests__/xyz.test.js.map +1 -0
  38. package/build/routes/attribution.d.ts +7 -5
  39. package/build/routes/attribution.d.ts.map +1 -1
  40. package/build/routes/attribution.js +43 -81
  41. package/build/routes/attribution.js.map +1 -1
  42. package/build/routes/fonts.d.ts +1 -1
  43. package/build/routes/fonts.d.ts.map +1 -1
  44. package/build/routes/fonts.js +30 -15
  45. package/build/routes/fonts.js.map +1 -1
  46. package/build/routes/health.d.ts +3 -3
  47. package/build/routes/health.d.ts.map +1 -1
  48. package/build/routes/health.js +15 -13
  49. package/build/routes/health.js.map +1 -1
  50. package/build/routes/imagery.d.ts.map +1 -1
  51. package/build/routes/imagery.js +11 -10
  52. package/build/routes/imagery.js.map +1 -1
  53. package/build/routes/ping.d.ts +3 -0
  54. package/build/routes/ping.d.ts.map +1 -0
  55. package/build/routes/ping.js +7 -0
  56. package/build/routes/ping.js.map +1 -0
  57. package/build/routes/sprites.d.ts.map +1 -1
  58. package/build/routes/sprites.js +16 -10
  59. package/build/routes/sprites.js.map +1 -1
  60. package/build/routes/tile.json.d.ts +7 -1
  61. package/build/routes/tile.json.d.ts.map +1 -1
  62. package/build/routes/tile.json.js +18 -21
  63. package/build/routes/tile.json.js.map +1 -1
  64. package/build/routes/tile.style.json.d.ts +6 -1
  65. package/build/routes/tile.style.json.d.ts.map +1 -1
  66. package/build/routes/tile.style.json.js +10 -13
  67. package/build/routes/tile.style.json.js.map +1 -1
  68. package/build/routes/tile.wmts.d.ts +9 -3
  69. package/build/routes/tile.wmts.d.ts.map +1 -1
  70. package/build/routes/tile.wmts.js +26 -35
  71. package/build/routes/tile.wmts.js.map +1 -1
  72. package/build/routes/tile.xyz.d.ts +14 -4
  73. package/build/routes/tile.xyz.d.ts.map +1 -1
  74. package/build/routes/tile.xyz.js +21 -17
  75. package/build/routes/tile.xyz.js.map +1 -1
  76. package/build/routes/tile.xyz.raster.d.ts +11 -0
  77. package/build/routes/tile.xyz.raster.d.ts.map +1 -0
  78. package/build/routes/tile.xyz.raster.js +90 -0
  79. package/build/routes/tile.xyz.raster.js.map +1 -0
  80. package/build/routes/tile.xyz.vector.d.ts +8 -0
  81. package/build/routes/tile.xyz.vector.d.ts.map +1 -0
  82. package/build/routes/tile.xyz.vector.js +46 -0
  83. package/build/routes/tile.xyz.vector.js.map +1 -0
  84. package/build/routes/version.d.ts +3 -0
  85. package/build/routes/version.d.ts.map +1 -0
  86. package/build/routes/version.js +9 -0
  87. package/build/routes/version.js.map +1 -0
  88. package/build/util/__test__/validate.test.d.ts +2 -0
  89. package/build/util/__test__/validate.test.d.ts.map +1 -0
  90. package/build/util/__test__/validate.test.js +66 -0
  91. package/build/util/__test__/validate.test.js.map +1 -0
  92. package/build/{cotar.cache.d.ts → util/cotar.serve.d.ts} +3 -8
  93. package/build/util/cotar.serve.d.ts.map +1 -0
  94. package/build/util/cotar.serve.js +41 -0
  95. package/build/util/cotar.serve.js.map +1 -0
  96. package/build/util/etag.d.ts +6 -0
  97. package/build/util/etag.d.ts.map +1 -0
  98. package/build/util/etag.js +20 -0
  99. package/build/util/etag.js.map +1 -0
  100. package/build/util/response.d.ts +4 -0
  101. package/build/util/response.d.ts.map +1 -0
  102. package/build/util/response.js +4 -0
  103. package/build/util/response.js.map +1 -0
  104. package/build/util/source.cache.d.ts +28 -0
  105. package/build/util/source.cache.d.ts.map +1 -0
  106. package/build/util/source.cache.js +53 -0
  107. package/build/util/source.cache.js.map +1 -0
  108. package/build/{source.tracer.d.ts → util/source.tracer.d.ts} +1 -0
  109. package/build/util/source.tracer.d.ts.map +1 -0
  110. package/build/{source.tracer.js → util/source.tracer.js} +3 -0
  111. package/build/util/source.tracer.js.map +1 -0
  112. package/build/util/swapping.lru.d.ts +21 -0
  113. package/build/util/swapping.lru.d.ts.map +1 -0
  114. package/build/util/swapping.lru.js +56 -0
  115. package/build/util/swapping.lru.js.map +1 -0
  116. package/build/util/validate.d.ts +46 -0
  117. package/build/util/validate.d.ts.map +1 -0
  118. package/build/util/validate.js +107 -0
  119. package/build/util/validate.js.map +1 -0
  120. package/build/wmts.capability.d.ts +1 -1
  121. package/build/wmts.capability.d.ts.map +1 -1
  122. package/dist/index.js +62 -60
  123. package/dist/node_modules/.package-lock.json +1 -1
  124. package/dist/package-lock.json +2 -2
  125. package/dist/package.json +1 -1
  126. package/package.json +6 -6
  127. package/src/__tests__/config.data.ts +41 -3
  128. package/src/__tests__/index.test.ts +3 -19
  129. package/src/__tests__/xyz.util.ts +18 -21
  130. package/src/index.ts +66 -29
  131. package/src/routes/__tests__/attribution.test.ts +356 -403
  132. package/src/routes/__tests__/fonts.test.ts +11 -5
  133. package/src/routes/__tests__/health.test.ts +17 -13
  134. package/src/routes/__tests__/sprites.test.ts +6 -1
  135. package/src/routes/__tests__/tile.json.test.ts +145 -0
  136. package/src/routes/__tests__/tile.style.json.test.ts +105 -0
  137. package/src/routes/__tests__/wmts.test.ts +5 -55
  138. package/src/routes/__tests__/xyz.test.ts +119 -0
  139. package/src/routes/attribution.ts +53 -99
  140. package/src/routes/fonts.ts +29 -15
  141. package/src/routes/health.ts +17 -16
  142. package/src/routes/imagery.ts +10 -9
  143. package/src/routes/ping.ts +8 -0
  144. package/src/routes/sprites.ts +16 -10
  145. package/src/routes/tile.json.ts +24 -18
  146. package/src/routes/tile.style.json.ts +15 -12
  147. package/src/routes/tile.wmts.ts +30 -31
  148. package/src/routes/tile.xyz.raster.ts +106 -0
  149. package/src/routes/tile.xyz.ts +31 -16
  150. package/src/routes/tile.xyz.vector.ts +47 -0
  151. package/src/routes/version.ts +8 -0
  152. package/src/util/__test__/validate.test.ts +74 -0
  153. package/src/util/cotar.serve.ts +46 -0
  154. package/src/util/etag.ts +20 -0
  155. package/src/util/response.ts +4 -0
  156. package/src/util/source.cache.ts +71 -0
  157. package/src/{source.tracer.ts → util/source.tracer.ts} +4 -0
  158. package/src/util/swapping.lru.ts +63 -0
  159. package/src/util/validate.ts +126 -0
  160. package/src/wmts.capability.ts +1 -1
  161. package/tsconfig.tsbuildinfo +1 -1
  162. package/build/__tests__/route.test.d.ts +0 -2
  163. package/build/__tests__/route.test.d.ts.map +0 -1
  164. package/build/__tests__/route.test.js +0 -21
  165. package/build/__tests__/route.test.js.map +0 -1
  166. package/build/__tests__/tiff.cache.test.d.ts +0 -2
  167. package/build/__tests__/tiff.cache.test.d.ts.map +0 -1
  168. package/build/__tests__/tiff.cache.test.js +0 -59
  169. package/build/__tests__/tiff.cache.test.js.map +0 -1
  170. package/build/__tests__/tile.cache.key.test.d.ts +0 -2
  171. package/build/__tests__/tile.cache.key.test.d.ts.map +0 -1
  172. package/build/__tests__/tile.cache.key.test.js +0 -49
  173. package/build/__tests__/tile.cache.key.test.js.map +0 -1
  174. package/build/__tests__/tile.set.cache.test.d.ts +0 -2
  175. package/build/__tests__/tile.set.cache.test.d.ts.map +0 -1
  176. package/build/__tests__/tile.set.cache.test.js +0 -72
  177. package/build/__tests__/tile.set.cache.test.js.map +0 -1
  178. package/build/__tests__/tile.set.test.d.ts +0 -2
  179. package/build/__tests__/tile.set.test.d.ts.map +0 -1
  180. package/build/__tests__/tile.set.test.js +0 -12
  181. package/build/__tests__/tile.set.test.js.map +0 -1
  182. package/build/__tests__/xyz.test.d.ts.map +0 -1
  183. package/build/__tests__/xyz.test.js +0 -275
  184. package/build/__tests__/xyz.test.js.map +0 -1
  185. package/build/api.key.d.ts +0 -2
  186. package/build/api.key.d.ts.map +0 -1
  187. package/build/api.key.js +0 -24
  188. package/build/api.key.js.map +0 -1
  189. package/build/cli/dump.d.ts +0 -2
  190. package/build/cli/dump.d.ts.map +0 -1
  191. package/build/cli/dump.js +0 -48
  192. package/build/cli/dump.js.map +0 -1
  193. package/build/cli/tile.set.local.d.ts +0 -12
  194. package/build/cli/tile.set.local.d.ts.map +0 -1
  195. package/build/cli/tile.set.local.js +0 -40
  196. package/build/cli/tile.set.local.js.map +0 -1
  197. package/build/cotar.cache.d.ts.map +0 -1
  198. package/build/cotar.cache.js +0 -50
  199. package/build/cotar.cache.js.map +0 -1
  200. package/build/router.d.ts +0 -15
  201. package/build/router.d.ts.map +0 -1
  202. package/build/router.js +0 -50
  203. package/build/router.js.map +0 -1
  204. package/build/routes/api.d.ts +0 -4
  205. package/build/routes/api.d.ts.map +0 -1
  206. package/build/routes/api.js +0 -14
  207. package/build/routes/api.js.map +0 -1
  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 -88
  211. package/build/routes/esri/rest.js.map +0 -1
  212. package/build/routes/response.d.ts +0 -4
  213. package/build/routes/response.d.ts.map +0 -1
  214. package/build/routes/response.js +0 -4
  215. package/build/routes/response.js.map +0 -1
  216. package/build/routes/tile.d.ts +0 -3
  217. package/build/routes/tile.d.ts.map +0 -1
  218. package/build/routes/tile.etag.d.ts +0 -11
  219. package/build/routes/tile.etag.d.ts.map +0 -1
  220. package/build/routes/tile.etag.js +0 -30
  221. package/build/routes/tile.etag.js.map +0 -1
  222. package/build/routes/tile.js +0 -28
  223. package/build/routes/tile.js.map +0 -1
  224. package/build/source.tracer.d.ts.map +0 -1
  225. package/build/source.tracer.js.map +0 -1
  226. package/build/tiff.cache.d.ts +0 -17
  227. package/build/tiff.cache.d.ts.map +0 -1
  228. package/build/tiff.cache.js +0 -46
  229. package/build/tiff.cache.js.map +0 -1
  230. package/build/tile.set.cache.d.ts +0 -20
  231. package/build/tile.set.cache.d.ts.map +0 -1
  232. package/build/tile.set.cache.js +0 -72
  233. package/build/tile.set.cache.js.map +0 -1
  234. package/build/tile.set.d.ts +0 -4
  235. package/build/tile.set.d.ts.map +0 -1
  236. package/build/tile.set.js +0 -2
  237. package/build/tile.set.js.map +0 -1
  238. package/build/tile.set.raster.d.ts +0 -49
  239. package/build/tile.set.raster.d.ts.map +0 -1
  240. package/build/tile.set.raster.js +0 -189
  241. package/build/tile.set.raster.js.map +0 -1
  242. package/build/tile.set.vector.d.ts +0 -18
  243. package/build/tile.set.vector.d.ts.map +0 -1
  244. package/build/tile.set.vector.js +0 -54
  245. package/build/tile.set.vector.js.map +0 -1
  246. package/build/validate.d.ts +0 -16
  247. package/build/validate.d.ts.map +0 -1
  248. package/build/validate.js +0 -32
  249. package/build/validate.js.map +0 -1
  250. package/src/__tests__/route.test.ts +0 -24
  251. package/src/__tests__/tiff.cache.test.ts +0 -73
  252. package/src/__tests__/tile.cache.key.test.ts +0 -56
  253. package/src/__tests__/tile.set.cache.test.ts +0 -80
  254. package/src/__tests__/tile.set.test.ts +0 -12
  255. package/src/__tests__/xyz.test.ts +0 -318
  256. package/src/api.key.ts +0 -23
  257. package/src/cli/dump.ts +0 -61
  258. package/src/cli/tile.set.local.ts +0 -51
  259. package/src/cotar.cache.ts +0 -54
  260. package/src/router.ts +0 -58
  261. package/src/routes/api.ts +0 -15
  262. package/src/routes/esri/rest.ts +0 -90
  263. package/src/routes/response.ts +0 -4
  264. package/src/routes/tile.etag.ts +0 -36
  265. package/src/routes/tile.ts +0 -23
  266. package/src/tiff.cache.ts +0 -51
  267. package/src/tile.set.cache.ts +0 -84
  268. package/src/tile.set.raster.ts +0 -230
  269. package/src/tile.set.ts +0 -4
  270. package/src/tile.set.vector.ts +0 -61
  271. package/src/validate.ts +0 -32
@@ -1,48 +1,48 @@
1
1
  import { Config, TileSetType } from '@basemaps/config';
2
- import { GoogleTms, ImageFormat, Nztm2000QuadTms } from '@basemaps/geo';
3
- import { Env, 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';
6
+ import { NotFound, NotModified } from '../util/response.js';
7
+ import { Validate } from '../util/validate.js';
8
8
  import { WmtsCapabilities } from '../wmts.capability.js';
9
- import { NotFound, NotModified } from './response.js';
10
- import { TileEtag } from './tile.etag.js';
9
+ import { Etag } from '../util/etag.js';
11
10
 
12
- export function getImageFormats(req: LambdaHttpRequest): ImageFormat[] | undefined {
13
- const formats = [...req.query.getAll('format'), ...req.query.getAll('tileFormat')];
14
- if (formats.length === 0) return;
15
-
16
- const output: Set<ImageFormat> = new Set();
17
- for (const fmt of formats) {
18
- const parsed = getImageFormat(fmt);
19
- if (parsed == null) continue;
20
- output.add(parsed);
21
- }
22
- if (output.size === 0) return;
23
- return [...output.values()];
11
+ export interface WmtsCapabilitiesGet {
12
+ Params: {
13
+ tileSet?: string;
14
+ tileMatrix?: string;
15
+ };
24
16
  }
25
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
+ }
26
24
  /**
27
25
  * Serve a WMTS request
28
26
  *
29
27
  * /v1/tiles/:tileSet/:tileMatrixSet/WMTSCapabilities.xml
30
28
  * @example `/v1/tiles/aerial/NZTM2000Quad/WMTSCapabilities.xml`
31
29
  */
32
- export async function wmts(req: LambdaHttpRequest): Promise<LambdaHttpResponse> {
33
- const action = Router.action(req);
34
- const wmtsData = tileWmtsFromPath(action.rest);
35
- 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
+
36
37
  const host = Env.get(Env.PublicUrlBase) ?? '';
37
38
 
38
39
  req.timer.start('tileset:load');
39
- const tileSet = await Config.TileSet.get(Config.TileSet.id(wmtsData.name ?? 'aerial'));
40
+ const tileSet = await Config.TileSet.get(Config.TileSet.id(tileSetName ?? 'aerial'));
40
41
  req.timer.end('tileset:load');
41
- if (tileSet == null || tileSet.type !== TileSetType.Raster) return NotFound;
42
+ if (tileSet == null || tileSet.type !== TileSetType.Raster) return NotFound();
42
43
 
43
44
  const provider = await Config.Provider.get(Config.Provider.id('linz'));
44
45
 
45
- const tileMatrix = wmtsData.tileMatrix == null ? [GoogleTms, Nztm2000QuadTms] : [wmtsData.tileMatrix];
46
46
  req.timer.start('imagery:load');
47
47
  const imagery = await Config.getAllImagery(
48
48
  tileSet.layers,
@@ -50,27 +50,26 @@ export async function wmts(req: LambdaHttpRequest): Promise<LambdaHttpResponse>
50
50
  );
51
51
  req.timer.end('imagery:load');
52
52
 
53
- const apiKey = Router.apiKey(req);
54
53
  const xml = new WmtsCapabilities({
55
54
  httpBase: host,
56
55
  provider: provider ?? undefined,
57
56
  tileSet,
58
57
  tileMatrix,
59
- isIndividualLayers: wmtsData.tileMatrix == null,
58
+ isIndividualLayers: req.params.tileMatrix == null,
60
59
  imagery,
61
60
  apiKey,
62
- formats: getImageFormats(req),
61
+ formats: Validate.getRequestedFormats(req),
63
62
  }).toXml();
64
- if (xml == null) return NotFound;
63
+ if (xml == null) return NotFound();
65
64
 
66
65
  const data = Buffer.from(xml);
67
66
 
68
67
  const cacheKey = createHash('sha256').update(data).digest('base64');
69
- if (TileEtag.isNotModified(req, cacheKey)) return NotModified;
68
+ if (Etag.isNotModified(req, cacheKey)) return NotModified();
70
69
 
71
70
  const response = new LambdaHttpResponse(200, 'ok');
72
71
  response.header(HttpHeader.ETag, cacheKey);
73
- response.header(HttpHeader.CacheControl, 'max-age=0');
72
+ response.header(HttpHeader.CacheControl, 'no-store');
74
73
  response.buffer(data, 'text/xml');
75
74
  req.set('bytes', data.byteLength);
76
75
  return response;
@@ -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
 
@@ -0,0 +1,63 @@
1
+ export class SwappingLru<T extends { size: number }> {
2
+ cacheA: Map<string, T> = new Map();
3
+ cacheB: Map<string, T> = new Map();
4
+ maxSize: number;
5
+
6
+ hits = 0;
7
+ misses = 0;
8
+ resets = 0;
9
+
10
+ _lastCheckedAt = -1;
11
+
12
+ constructor(maxSize: number) {
13
+ this.maxSize = maxSize;
14
+ }
15
+
16
+ get(id: string): T | null {
17
+ const cacheA = this.cacheA.get(id);
18
+ if (cacheA) {
19
+ this.hits++;
20
+ return cacheA;
21
+ }
22
+
23
+ const cacheB = this.cacheB.get(id);
24
+ if (cacheB == null) {
25
+ this.misses++;
26
+ return null;
27
+ }
28
+
29
+ this.hits++;
30
+ // If a object is still useful move it into the main cache
31
+ this.cacheA.set(id, cacheB);
32
+ this.cacheB.delete(id);
33
+ return cacheB;
34
+ }
35
+
36
+ /** Reset the cache */
37
+ clear(): void {
38
+ this.cacheA.clear();
39
+ this.cacheB.clear();
40
+ }
41
+
42
+ set(id: string, tiff: T): void {
43
+ this.cacheA.set(id, tiff);
44
+ this.check();
45
+ }
46
+
47
+ /** Validate the size of the cache has not exploded */
48
+ check(): void {
49
+ this._lastCheckedAt = Date.now();
50
+ if (this.maxSize <= 0) return;
51
+ if (this.currentSize <= this.maxSize) return;
52
+ this.resets++;
53
+ this.cacheB = this.cacheA;
54
+ this.cacheA = new Map();
55
+ }
56
+
57
+ /** Calculate the total number of bytes used by this cache */
58
+ get currentSize(): number {
59
+ let size = 0;
60
+ for (const value of this.cacheA.values()) size += value.size;
61
+ return size;
62
+ }
63
+ }