@basemaps/lambda-tiler 7.11.0 → 7.13.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 (181) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/build/index.js +9 -4
  3. package/build/index.js.map +1 -1
  4. package/build/routes/__tests__/link.test.d.ts +1 -0
  5. package/build/routes/__tests__/link.test.js +82 -0
  6. package/build/routes/__tests__/link.test.js.map +1 -0
  7. package/build/routes/__tests__/tile.style.json.attribution.test.d.ts +1 -0
  8. package/build/routes/__tests__/tile.style.json.attribution.test.js +172 -0
  9. package/build/routes/__tests__/tile.style.json.attribution.test.js.map +1 -0
  10. package/build/routes/__tests__/tile.style.json.test.js +3 -1
  11. package/build/routes/__tests__/tile.style.json.test.js.map +1 -1
  12. package/build/routes/attribution.d.ts +19 -1
  13. package/build/routes/attribution.js +38 -3
  14. package/build/routes/attribution.js.map +1 -1
  15. package/build/routes/link.d.ts +17 -0
  16. package/build/routes/link.js +42 -0
  17. package/build/routes/link.js.map +1 -0
  18. package/build/routes/tile.style.json.d.ts +6 -2
  19. package/build/routes/tile.style.json.js +13 -5
  20. package/build/routes/tile.style.json.js.map +1 -1
  21. package/build/util/__test__/nztm.style.test.js +16 -7
  22. package/build/util/__test__/nztm.style.test.js.map +1 -1
  23. package/build/util/nztm.style.js +0 -3
  24. package/build/util/nztm.style.js.map +1 -1
  25. package/dist/index.js +86535 -0
  26. package/dist/node_modules/.package-lock.json +179 -0
  27. package/dist/node_modules/@img/sharp-libvips-linux-arm64/README.md +47 -0
  28. package/dist/node_modules/@img/sharp-libvips-linux-arm64/lib/glib-2.0/include/glibconfig.h +219 -0
  29. package/dist/node_modules/@img/sharp-libvips-linux-arm64/lib/index.js +1 -0
  30. package/dist/node_modules/@img/sharp-libvips-linux-arm64/lib/libvips-cpp.so.42 +0 -0
  31. package/dist/node_modules/@img/sharp-libvips-linux-arm64/package.json +45 -0
  32. package/dist/node_modules/@img/sharp-libvips-linux-arm64/versions.json +31 -0
  33. package/dist/node_modules/@img/sharp-linux-arm64/LICENSE +191 -0
  34. package/dist/node_modules/@img/sharp-linux-arm64/README.md +18 -0
  35. package/dist/node_modules/@img/sharp-linux-arm64/lib/sharp-linux-arm64.node +0 -0
  36. package/dist/node_modules/@img/sharp-linux-arm64/package.json +47 -0
  37. package/dist/node_modules/color/LICENSE +21 -0
  38. package/dist/node_modules/color/README.md +123 -0
  39. package/dist/node_modules/color/index.js +496 -0
  40. package/dist/node_modules/color/package.json +47 -0
  41. package/dist/node_modules/color-convert/CHANGELOG.md +54 -0
  42. package/dist/node_modules/color-convert/LICENSE +21 -0
  43. package/dist/node_modules/color-convert/README.md +68 -0
  44. package/dist/node_modules/color-convert/conversions.js +839 -0
  45. package/dist/node_modules/color-convert/index.js +81 -0
  46. package/dist/node_modules/color-convert/package.json +48 -0
  47. package/dist/node_modules/color-convert/route.js +97 -0
  48. package/dist/node_modules/color-name/LICENSE +8 -0
  49. package/dist/node_modules/color-name/README.md +11 -0
  50. package/dist/node_modules/color-name/index.js +152 -0
  51. package/dist/node_modules/color-name/package.json +28 -0
  52. package/dist/node_modules/color-string/LICENSE +21 -0
  53. package/dist/node_modules/color-string/README.md +62 -0
  54. package/dist/node_modules/color-string/index.js +242 -0
  55. package/dist/node_modules/color-string/package.json +39 -0
  56. package/dist/node_modules/detect-libc/LICENSE +201 -0
  57. package/dist/node_modules/detect-libc/README.md +163 -0
  58. package/dist/node_modules/detect-libc/index.d.ts +14 -0
  59. package/dist/node_modules/detect-libc/lib/detect-libc.js +267 -0
  60. package/dist/node_modules/detect-libc/lib/filesystem.js +41 -0
  61. package/dist/node_modules/detect-libc/lib/process.js +24 -0
  62. package/dist/node_modules/detect-libc/package.json +40 -0
  63. package/dist/node_modules/is-arrayish/LICENSE +21 -0
  64. package/dist/node_modules/is-arrayish/README.md +16 -0
  65. package/dist/node_modules/is-arrayish/index.js +9 -0
  66. package/dist/node_modules/is-arrayish/package.json +45 -0
  67. package/dist/node_modules/is-arrayish/yarn-error.log +1443 -0
  68. package/dist/node_modules/lerc/CHANGELOG.md +69 -0
  69. package/dist/node_modules/lerc/LercDecode.d.ts +61 -0
  70. package/dist/node_modules/lerc/LercDecode.es.d.ts +61 -0
  71. package/dist/node_modules/lerc/LercDecode.es.js +434 -0
  72. package/dist/node_modules/lerc/LercDecode.es.min.js +17 -0
  73. package/dist/node_modules/lerc/LercDecode.js +448 -0
  74. package/dist/node_modules/lerc/LercDecode.min.js +17 -0
  75. package/dist/node_modules/lerc/README.md +123 -0
  76. package/dist/node_modules/lerc/lerc-wasm.wasm +0 -0
  77. package/dist/node_modules/lerc/package.json +30 -0
  78. package/dist/node_modules/semver/LICENSE +15 -0
  79. package/dist/node_modules/semver/README.md +654 -0
  80. package/dist/node_modules/semver/bin/semver.js +188 -0
  81. package/dist/node_modules/semver/classes/comparator.js +141 -0
  82. package/dist/node_modules/semver/classes/index.js +5 -0
  83. package/dist/node_modules/semver/classes/range.js +554 -0
  84. package/dist/node_modules/semver/classes/semver.js +302 -0
  85. package/dist/node_modules/semver/functions/clean.js +6 -0
  86. package/dist/node_modules/semver/functions/cmp.js +52 -0
  87. package/dist/node_modules/semver/functions/coerce.js +60 -0
  88. package/dist/node_modules/semver/functions/compare-build.js +7 -0
  89. package/dist/node_modules/semver/functions/compare-loose.js +3 -0
  90. package/dist/node_modules/semver/functions/compare.js +5 -0
  91. package/dist/node_modules/semver/functions/diff.js +65 -0
  92. package/dist/node_modules/semver/functions/eq.js +3 -0
  93. package/dist/node_modules/semver/functions/gt.js +3 -0
  94. package/dist/node_modules/semver/functions/gte.js +3 -0
  95. package/dist/node_modules/semver/functions/inc.js +19 -0
  96. package/dist/node_modules/semver/functions/lt.js +3 -0
  97. package/dist/node_modules/semver/functions/lte.js +3 -0
  98. package/dist/node_modules/semver/functions/major.js +3 -0
  99. package/dist/node_modules/semver/functions/minor.js +3 -0
  100. package/dist/node_modules/semver/functions/neq.js +3 -0
  101. package/dist/node_modules/semver/functions/parse.js +16 -0
  102. package/dist/node_modules/semver/functions/patch.js +3 -0
  103. package/dist/node_modules/semver/functions/prerelease.js +6 -0
  104. package/dist/node_modules/semver/functions/rcompare.js +3 -0
  105. package/dist/node_modules/semver/functions/rsort.js +3 -0
  106. package/dist/node_modules/semver/functions/satisfies.js +10 -0
  107. package/dist/node_modules/semver/functions/sort.js +3 -0
  108. package/dist/node_modules/semver/functions/valid.js +6 -0
  109. package/dist/node_modules/semver/index.js +89 -0
  110. package/dist/node_modules/semver/internal/constants.js +35 -0
  111. package/dist/node_modules/semver/internal/debug.js +9 -0
  112. package/dist/node_modules/semver/internal/identifiers.js +23 -0
  113. package/dist/node_modules/semver/internal/lrucache.js +40 -0
  114. package/dist/node_modules/semver/internal/parse-options.js +15 -0
  115. package/dist/node_modules/semver/internal/re.js +217 -0
  116. package/dist/node_modules/semver/package.json +77 -0
  117. package/dist/node_modules/semver/preload.js +2 -0
  118. package/dist/node_modules/semver/range.bnf +16 -0
  119. package/dist/node_modules/semver/ranges/gtr.js +4 -0
  120. package/dist/node_modules/semver/ranges/intersects.js +7 -0
  121. package/dist/node_modules/semver/ranges/ltr.js +4 -0
  122. package/dist/node_modules/semver/ranges/max-satisfying.js +25 -0
  123. package/dist/node_modules/semver/ranges/min-satisfying.js +24 -0
  124. package/dist/node_modules/semver/ranges/min-version.js +61 -0
  125. package/dist/node_modules/semver/ranges/outside.js +80 -0
  126. package/dist/node_modules/semver/ranges/simplify.js +47 -0
  127. package/dist/node_modules/semver/ranges/subset.js +247 -0
  128. package/dist/node_modules/semver/ranges/to-comparators.js +8 -0
  129. package/dist/node_modules/semver/ranges/valid.js +11 -0
  130. package/dist/node_modules/sharp/LICENSE +191 -0
  131. package/dist/node_modules/sharp/README.md +118 -0
  132. package/dist/node_modules/sharp/install/check.js +36 -0
  133. package/dist/node_modules/sharp/lib/channel.js +174 -0
  134. package/dist/node_modules/sharp/lib/colour.js +182 -0
  135. package/dist/node_modules/sharp/lib/composite.js +210 -0
  136. package/dist/node_modules/sharp/lib/constructor.js +444 -0
  137. package/dist/node_modules/sharp/lib/index.d.ts +1717 -0
  138. package/dist/node_modules/sharp/lib/index.js +16 -0
  139. package/dist/node_modules/sharp/lib/input.js +657 -0
  140. package/dist/node_modules/sharp/lib/is.js +169 -0
  141. package/dist/node_modules/sharp/lib/libvips.js +171 -0
  142. package/dist/node_modules/sharp/lib/operation.js +919 -0
  143. package/dist/node_modules/sharp/lib/output.js +1561 -0
  144. package/dist/node_modules/sharp/lib/resize.js +582 -0
  145. package/dist/node_modules/sharp/lib/sharp.js +86 -0
  146. package/dist/node_modules/sharp/lib/utility.js +287 -0
  147. package/dist/node_modules/sharp/package.json +219 -0
  148. package/dist/node_modules/sharp/src/binding.gyp +277 -0
  149. package/dist/node_modules/sharp/src/common.cc +1090 -0
  150. package/dist/node_modules/sharp/src/common.h +393 -0
  151. package/dist/node_modules/sharp/src/metadata.cc +287 -0
  152. package/dist/node_modules/sharp/src/metadata.h +82 -0
  153. package/dist/node_modules/sharp/src/operations.cc +471 -0
  154. package/dist/node_modules/sharp/src/operations.h +125 -0
  155. package/dist/node_modules/sharp/src/pipeline.cc +1724 -0
  156. package/dist/node_modules/sharp/src/pipeline.h +385 -0
  157. package/dist/node_modules/sharp/src/sharp.cc +40 -0
  158. package/dist/node_modules/sharp/src/stats.cc +183 -0
  159. package/dist/node_modules/sharp/src/stats.h +59 -0
  160. package/dist/node_modules/sharp/src/utilities.cc +269 -0
  161. package/dist/node_modules/sharp/src/utilities.h +19 -0
  162. package/dist/node_modules/simple-swizzle/LICENSE +21 -0
  163. package/dist/node_modules/simple-swizzle/README.md +39 -0
  164. package/dist/node_modules/simple-swizzle/index.js +29 -0
  165. package/dist/node_modules/simple-swizzle/package.json +36 -0
  166. package/dist/package-lock.json +610 -0
  167. package/dist/package.json +40 -0
  168. package/dist/static/expected_tile_2193_153_255_z7.png +0 -0
  169. package/dist/static/expected_tile_NZTM2000Quad_30_33_z6.png +0 -0
  170. package/dist/static/expected_tile_WebMercatorQuad_252_156_z8.png +0 -0
  171. package/package.json +9 -9
  172. package/src/index.ts +10 -4
  173. package/src/routes/__tests__/link.test.ts +114 -0
  174. package/src/routes/__tests__/tile.style.json.attribution.test.ts +224 -0
  175. package/src/routes/__tests__/tile.style.json.test.ts +3 -2
  176. package/src/routes/attribution.ts +50 -5
  177. package/src/routes/link.ts +55 -0
  178. package/src/routes/tile.style.json.ts +16 -5
  179. package/src/util/__test__/nztm.style.test.ts +18 -8
  180. package/src/util/nztm.style.ts +0 -3
  181. package/tsconfig.tsbuildinfo +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@basemaps/lambda-tiler",
3
- "version": "7.11.0",
3
+ "version": "7.13.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/linz/basemaps.git",
@@ -22,12 +22,12 @@
22
22
  "types": "./build/index.d.ts",
23
23
  "license": "MIT",
24
24
  "dependencies": {
25
- "@basemaps/config": "^7.11.0",
26
- "@basemaps/config-loader": "^7.11.0",
27
- "@basemaps/geo": "^7.11.0",
28
- "@basemaps/shared": "^7.11.0",
29
- "@basemaps/tiler": "^7.11.0",
30
- "@basemaps/tiler-sharp": "^7.11.0",
25
+ "@basemaps/config": "^7.12.0",
26
+ "@basemaps/config-loader": "^7.12.0",
27
+ "@basemaps/geo": "^7.12.0",
28
+ "@basemaps/shared": "^7.12.0",
29
+ "@basemaps/tiler": "^7.12.0",
30
+ "@basemaps/tiler-sharp": "^7.12.0",
31
31
  "@linzjs/geojson": "^7.10.0",
32
32
  "@linzjs/lambda": "^4.0.0",
33
33
  "@mapbox/vector-tile": "^2.0.3",
@@ -50,11 +50,11 @@
50
50
  "bundle": "./bundle.sh"
51
51
  },
52
52
  "devDependencies": {
53
- "@basemaps/attribution": "^7.11.0",
53
+ "@basemaps/attribution": "^7.12.0",
54
54
  "@chunkd/fs": "^11.2.0",
55
55
  "@types/aws-lambda": "^8.10.75",
56
56
  "@types/pixelmatch": "^5.0.0",
57
57
  "pretty-json-log": "^1.0.0"
58
58
  },
59
- "gitHead": "99c1a7568dca77b55e44f2d446c00f092e14870d"
59
+ "gitHead": "9c1d78fe9e4cccb6309b761f17dc249681ce1769"
60
60
  }
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { FsaCache, FsaLog, LogConfig } from '@basemaps/shared';
1
+ import { FsaCache, FsaLog, LogConfig, LogStorage } from '@basemaps/shared';
2
2
  import { LambdaHttpRequest, LambdaHttpResponse, lf } from '@linzjs/lambda';
3
3
 
4
4
  import { tileAttributionGet } from './routes/attribution.js';
@@ -6,6 +6,7 @@ import { configImageryGet, configTileSetGet } from './routes/config.js';
6
6
  import { fontGet, fontList } from './routes/fonts.js';
7
7
  import { healthGet } from './routes/health.js';
8
8
  import { imageryGet } from './routes/imagery.js';
9
+ import { linkGet } from './routes/link.js';
9
10
  import { pingGet } from './routes/ping.js';
10
11
  import { previewIndexGet } from './routes/preview.index.js';
11
12
  import { tilePreviewGet } from './routes/preview.js';
@@ -33,13 +34,14 @@ function randomTrace(req: LambdaHttpRequest): void {
33
34
  const rand = Math.random();
34
35
  // 1% trace
35
36
  if (rand < 0.01) req.log.level = 'trace';
36
- // 5% debug
37
- else if (rand < 0.04) req.log.level = 'debug';
37
+ // 25% debug
38
+ else if (rand < 0.25) req.log.level = 'debug';
38
39
  // everything else info
39
40
  else req.log.level = 'info';
40
41
  }
41
42
 
42
43
  handler.router.hook('request', (req) => {
44
+ LogStorage.enterWith({ log: req.log });
43
45
  FsaLog.reset();
44
46
 
45
47
  randomTrace(req);
@@ -48,7 +50,8 @@ handler.router.hook('request', (req) => {
48
50
  });
49
51
 
50
52
  handler.router.hook('response', (req, res) => {
51
- req.set('requestCount', FsaLog.requests.length);
53
+ req.set('fetchCount', FsaLog.count);
54
+ req.set('fetches', FsaLog.requests);
52
55
  req.set('cacheSize', FsaCache.size);
53
56
  // Force access-control-allow-origin to everything
54
57
  res.header('access-control-allow-origin', '*');
@@ -102,6 +105,9 @@ handler.router.get('/v1/preview/:tileSet/:tileMatrix/:z/:lon/:lat/:outputType',
102
105
  handler.router.get('/v1/@:location', previewIndexGet);
103
106
  handler.router.get('/@:location', previewIndexGet);
104
107
 
108
+ // Link
109
+ handler.router.get('/v1/link/:tileSet', linkGet);
110
+
105
111
  // Attribution
106
112
  handler.router.get('/v1/tiles/:tileSet/:tileMatrix/attribution.json', tileAttributionGet);
107
113
  handler.router.get('/v1/attribution/:tileSet/:tileMatrix/summary.json', tileAttributionGet);
@@ -0,0 +1,114 @@
1
+ import { strictEqual } from 'node:assert';
2
+ import { afterEach, describe, it } from 'node:test';
3
+
4
+ import { ConfigProviderMemory } from '@basemaps/config';
5
+ import { Epsg } from '@basemaps/geo';
6
+
7
+ import { FakeData, Imagery3857 } from '../../__tests__/config.data.js';
8
+ import { mockRequest } from '../../__tests__/xyz.util.js';
9
+ import { handler } from '../../index.js';
10
+ import { ConfigLoader } from '../../util/config.loader.js';
11
+
12
+ describe('/v1/link/:tileSet', () => {
13
+ const FakeTileSetName = 'tileset';
14
+ const config = new ConfigProviderMemory();
15
+
16
+ afterEach(() => {
17
+ config.objects.clear();
18
+ });
19
+
20
+ /**
21
+ * 3xx status responses
22
+ */
23
+
24
+ // tileset found, is raster type, has one layer, has '3857' entry, imagery found > 302 response
25
+ it('success: redirect to pre-zoomed imagery', async (t) => {
26
+ t.mock.method(ConfigLoader, 'getDefaultConfig', () => Promise.resolve(config));
27
+
28
+ config.put(FakeData.tileSetRaster(FakeTileSetName));
29
+ config.put(Imagery3857);
30
+
31
+ const req = mockRequest(`/v1/link/${FakeTileSetName}`);
32
+ const res = await handler.router.handle(req);
33
+
34
+ strictEqual(res.status, 302);
35
+ strictEqual(res.statusDescription, 'Redirect to pre-zoomed imagery');
36
+ });
37
+
38
+ /**
39
+ * 4xx status responses
40
+ */
41
+
42
+ // tileset not found > 404 response
43
+ it('failure: tileset not found', async (t) => {
44
+ t.mock.method(ConfigLoader, 'getDefaultConfig', () => Promise.resolve(config));
45
+
46
+ const req = mockRequest(`/v1/link/${FakeTileSetName}`);
47
+ const res = await handler.router.handle(req);
48
+
49
+ strictEqual(res.status, 404);
50
+ strictEqual(res.statusDescription, 'Tileset not found');
51
+ });
52
+
53
+ // tileset found, not raster type > 400 response
54
+ it('failure: tileset must be raster type', async (t) => {
55
+ t.mock.method(ConfigLoader, 'getDefaultConfig', () => Promise.resolve(config));
56
+
57
+ config.put(FakeData.tileSetVector(FakeTileSetName));
58
+
59
+ const req = mockRequest(`/v1/link/${FakeTileSetName}`);
60
+ const res = await handler.router.handle(req);
61
+
62
+ strictEqual(res.status, 400);
63
+ strictEqual(res.statusDescription, 'Tileset must be raster type');
64
+ });
65
+
66
+ // tileset found, is raster type, has more than one layer > 400 response
67
+ it('failure: too many layers', async (t) => {
68
+ t.mock.method(ConfigLoader, 'getDefaultConfig', () => Promise.resolve(config));
69
+
70
+ const tileSet = FakeData.tileSetRaster(FakeTileSetName);
71
+
72
+ // add another layer
73
+ tileSet.layers.push(tileSet.layers[0]);
74
+
75
+ config.put(tileSet);
76
+
77
+ const req = mockRequest(`/v1/link/${FakeTileSetName}`);
78
+ const res = await handler.router.handle(req);
79
+
80
+ strictEqual(res.status, 400);
81
+ strictEqual(res.statusDescription, 'Too many layers');
82
+ });
83
+
84
+ // tileset found, is raster type, has one layer, no '3857' entry > 400 response
85
+ it("failure: no imagery for '3857' projection", async (t) => {
86
+ t.mock.method(ConfigLoader, 'getDefaultConfig', () => Promise.resolve(config));
87
+
88
+ const tileSet = FakeData.tileSetRaster(FakeTileSetName);
89
+
90
+ // delete '3857' entry
91
+ delete tileSet.layers[0][Epsg.Google.code];
92
+
93
+ config.put(tileSet);
94
+
95
+ const req = mockRequest(`/v1/link/${FakeTileSetName}`);
96
+ const res = await handler.router.handle(req);
97
+
98
+ strictEqual(res.status, 400);
99
+ strictEqual(res.statusDescription, "No imagery for '3857' projection");
100
+ });
101
+
102
+ // tileset found, is raster type, has one layer, has '3857' entry, imagery not found > 400 response
103
+ it('failure: imagery not found', async (t) => {
104
+ t.mock.method(ConfigLoader, 'getDefaultConfig', () => Promise.resolve(config));
105
+
106
+ config.put(FakeData.tileSetRaster(FakeTileSetName));
107
+
108
+ const req = mockRequest(`/v1/link/${FakeTileSetName}`);
109
+ const res = await handler.router.handle(req);
110
+
111
+ strictEqual(res.status, 400);
112
+ strictEqual(res.statusDescription, 'Imagery not found');
113
+ });
114
+ });
@@ -0,0 +1,224 @@
1
+ import assert, { strictEqual } from 'node:assert';
2
+ import { afterEach, before, describe, it } from 'node:test';
3
+
4
+ import { copyright, createLicensorAttribution } from '@basemaps/attribution/build/utils.js';
5
+ import { ConfigProviderMemory, StyleJson } from '@basemaps/config';
6
+ import { StacProvider } from '@basemaps/geo';
7
+ import { Env } from '@basemaps/shared';
8
+
9
+ import { FakeData, Imagery3857 } from '../../__tests__/config.data.js';
10
+ import { Api, mockRequest } from '../../__tests__/xyz.util.js';
11
+ import { handler } from '../../index.js';
12
+ import { ConfigLoader } from '../../util/config.loader.js';
13
+
14
+ const defaultAttribution = `${copyright} LINZ`;
15
+
16
+ describe('/v1/styles', () => {
17
+ const host = 'https://tiles.test';
18
+ const config = new ConfigProviderMemory();
19
+
20
+ const FakeTileSetName = 'tileset';
21
+ const FakeLicensor1: StacProvider = {
22
+ name: 'L1',
23
+ roles: ['licensor'],
24
+ };
25
+ const FakeLicensor2: StacProvider = {
26
+ name: 'L2',
27
+ roles: ['licensor'],
28
+ };
29
+
30
+ before(() => {
31
+ process.env[Env.PublicUrlBase] = host;
32
+ });
33
+ afterEach(() => {
34
+ config.objects.clear();
35
+ });
36
+
37
+ // tileset exists, imagery not found
38
+ it('default: imagery not found', async (t) => {
39
+ t.mock.method(ConfigLoader, 'getDefaultConfig', () => Promise.resolve(config));
40
+
41
+ // insert
42
+ config.put(FakeData.tileSetRaster(FakeTileSetName));
43
+
44
+ // request
45
+ const req = mockRequest(`/v1/styles/${FakeTileSetName}.json`, 'get', Api.header);
46
+ const res = await handler.router.handle(req);
47
+ strictEqual(res.status, 200);
48
+
49
+ // extract
50
+ const body = Buffer.from(res.body, 'base64').toString();
51
+ const json = JSON.parse(body) as StyleJson;
52
+
53
+ const source = Object.values(json.sources)[0];
54
+ assert(source != null);
55
+ assert(source.attribution != null);
56
+
57
+ // verify
58
+ strictEqual(source.attribution, defaultAttribution);
59
+ });
60
+
61
+ // tileset exists, imagery found, more than one layer
62
+ it('default: too many layers', async (t) => {
63
+ t.mock.method(ConfigLoader, 'getDefaultConfig', () => Promise.resolve(config));
64
+
65
+ // insert
66
+ const tileset = FakeData.tileSetRaster(FakeTileSetName);
67
+ assert(tileset.layers[0] != null);
68
+
69
+ tileset.layers.push(tileset.layers[0]);
70
+ assert(tileset.layers.length > 1);
71
+
72
+ config.put(tileset);
73
+ config.put(Imagery3857);
74
+
75
+ // request
76
+ const req = mockRequest(`/v1/styles/${FakeTileSetName}.json`, 'get', Api.header);
77
+ const res = await handler.router.handle(req);
78
+ strictEqual(res.status, 200);
79
+
80
+ // extract
81
+ const body = Buffer.from(res.body, 'base64').toString();
82
+ const json = JSON.parse(body) as StyleJson;
83
+
84
+ const source = Object.values(json.sources)[0];
85
+ assert(source != null);
86
+ assert(source.attribution != null);
87
+
88
+ // verify
89
+ strictEqual(source.attribution, defaultAttribution);
90
+ });
91
+
92
+ // tileset exists, imagery found, one layer, no providers
93
+ it('default: no providers', async (t) => {
94
+ t.mock.method(ConfigLoader, 'getDefaultConfig', () => Promise.resolve(config));
95
+
96
+ // insert
97
+ const tileset = FakeData.tileSetRaster(FakeTileSetName);
98
+ assert(tileset.layers[0] != null);
99
+ assert(tileset.layers.length === 1);
100
+
101
+ const imagery = Imagery3857;
102
+ assert(imagery.providers == null);
103
+
104
+ config.put(tileset);
105
+ config.put(imagery);
106
+
107
+ // request
108
+ const req = mockRequest(`/v1/styles/${FakeTileSetName}.json`, 'get', Api.header);
109
+ const res = await handler.router.handle(req);
110
+ strictEqual(res.status, 200);
111
+
112
+ // extract
113
+ const body = Buffer.from(res.body, 'base64').toString();
114
+ const json = JSON.parse(body) as StyleJson;
115
+
116
+ const source = Object.values(json.sources)[0];
117
+ assert(source != null);
118
+ assert(source.attribution != null);
119
+
120
+ // verify
121
+ strictEqual(source.attribution, defaultAttribution);
122
+ });
123
+
124
+ // tileset exists, imagery found, one layer, has providers, no licensors
125
+ it('default: no licensors', async (t) => {
126
+ t.mock.method(ConfigLoader, 'getDefaultConfig', () => Promise.resolve(config));
127
+
128
+ // insert
129
+ const tileset = FakeData.tileSetRaster(FakeTileSetName);
130
+ assert(tileset.layers[0] != null);
131
+ assert(tileset.layers.length === 1);
132
+
133
+ const imagery = Imagery3857;
134
+ imagery.providers = [];
135
+ assert(imagery.providers != null);
136
+
137
+ config.put(tileset);
138
+ config.put(imagery);
139
+
140
+ // request
141
+ const req = mockRequest(`/v1/styles/${FakeTileSetName}.json`, 'get', Api.header);
142
+ const res = await handler.router.handle(req);
143
+ strictEqual(res.status, 200);
144
+
145
+ // extract
146
+ const body = Buffer.from(res.body, 'base64').toString();
147
+ const json = JSON.parse(body) as StyleJson;
148
+
149
+ const source = Object.values(json.sources)[0];
150
+ assert(source != null);
151
+ assert(source.attribution != null);
152
+
153
+ // verify
154
+ strictEqual(source.attribution, defaultAttribution);
155
+ });
156
+
157
+ // tileset exists, imagery found, one layer, has providers, one licensor
158
+ it('custom: one licensor', async (t) => {
159
+ t.mock.method(ConfigLoader, 'getDefaultConfig', () => Promise.resolve(config));
160
+
161
+ // insert
162
+ const tileset = FakeData.tileSetRaster(FakeTileSetName);
163
+ assert(tileset.layers[0] != null);
164
+ assert(tileset.layers.length === 1);
165
+
166
+ const imagery = Imagery3857;
167
+ imagery.providers = [FakeLicensor1];
168
+ assert(imagery.providers != null);
169
+
170
+ config.put(tileset);
171
+ config.put(imagery);
172
+
173
+ // request
174
+ const req = mockRequest(`/v1/styles/${FakeTileSetName}.json`, 'get', Api.header);
175
+ const res = await handler.router.handle(req);
176
+ strictEqual(res.status, 200);
177
+
178
+ // extract
179
+ const body = Buffer.from(res.body, 'base64').toString();
180
+ const json = JSON.parse(body) as StyleJson;
181
+
182
+ const source = Object.values(json.sources)[0];
183
+ assert(source != null);
184
+ assert(source.attribution != null);
185
+
186
+ // verify
187
+ strictEqual(source.attribution, `${copyright} ${FakeLicensor1.name}`);
188
+ strictEqual(source.attribution, createLicensorAttribution([FakeLicensor1]));
189
+ });
190
+
191
+ // tileset exists, imagery found, one layer, has providers, two licensors
192
+ it('custom: two licensors', async (t) => {
193
+ t.mock.method(ConfigLoader, 'getDefaultConfig', () => Promise.resolve(config));
194
+
195
+ // insert
196
+ const tileset = FakeData.tileSetRaster(FakeTileSetName);
197
+ assert(tileset.layers[0] != null);
198
+ assert(tileset.layers.length === 1);
199
+
200
+ const imagery = Imagery3857;
201
+ imagery.providers = [FakeLicensor1, FakeLicensor2];
202
+ assert(imagery.providers != null);
203
+
204
+ config.put(tileset);
205
+ config.put(imagery);
206
+
207
+ // request
208
+ const req = mockRequest(`/v1/styles/${FakeTileSetName}.json`, 'get', Api.header);
209
+ const res = await handler.router.handle(req);
210
+ strictEqual(res.status, 200);
211
+
212
+ // extract
213
+ const body = Buffer.from(res.body, 'base64').toString();
214
+ const json = JSON.parse(body) as StyleJson;
215
+
216
+ const source = Object.values(json.sources)[0];
217
+ assert(source != null);
218
+ assert(source.attribution != null);
219
+
220
+ // verify
221
+ strictEqual(source.attribution, `${copyright} ${FakeLicensor1.name}, ${FakeLicensor2.name}`);
222
+ strictEqual(source.attribution, createLicensorAttribution([FakeLicensor1, FakeLicensor2]));
223
+ });
224
+ });
@@ -2,7 +2,8 @@ import assert from 'node:assert';
2
2
  import { afterEach, before, beforeEach, describe, it } from 'node:test';
3
3
 
4
4
  import { ConfigProviderMemory, SourceRaster, StyleJson } from '@basemaps/config';
5
- import { Terrain } from '@basemaps/config/src/config/vector.style.js';
5
+ import { DefaultExaggeration, Terrain } from '@basemaps/config/build/config/vector.style.js';
6
+ import { Nztm2000QuadTms } from '@basemaps/geo';
6
7
  import { Env } from '@basemaps/shared';
7
8
  import { createSandbox } from 'sinon';
8
9
 
@@ -441,7 +442,7 @@ describe('/v1/styles', () => {
441
442
  },
442
443
  ],
443
444
  terrain: {
444
- exaggeration: 1.1,
445
+ exaggeration: DefaultExaggeration[Nztm2000QuadTms.identifier],
445
446
  },
446
447
  },
447
448
  };
@@ -1,9 +1,11 @@
1
- import { ConfigProvider, ConfigTileSet, getAllImagery, TileSetType } from '@basemaps/config';
1
+ import { createLicensorAttribution } from '@basemaps/attribution/build/utils.js';
2
+ import { BasemapsConfigProvider, ConfigProvider, ConfigTileSet, getAllImagery, TileSetType } from '@basemaps/config';
2
3
  import {
3
4
  AttributionCollection,
4
5
  AttributionItem,
5
6
  AttributionStac,
6
7
  Bounds,
8
+ Epsg,
7
9
  GoogleTms,
8
10
  NamedBounds,
9
11
  Projection,
@@ -93,10 +95,11 @@ async function tileSetAttribution(
93
95
 
94
96
  const config = await ConfigLoader.load(req);
95
97
  const imagery = await getAllImagery(config, tileSet.layers, [tileMatrix.projection]);
96
-
97
98
  const host = await config.Provider.get(config.Provider.id('linz'));
98
99
 
99
- for (const layer of tileSet.layers) {
100
+ for (let i = 0; i < tileSet.layers.length; i++) {
101
+ const layer = tileSet.layers[i];
102
+
100
103
  const imgId = layer[proj.epsg.code];
101
104
  if (imgId == null) continue;
102
105
 
@@ -138,11 +141,12 @@ async function tileSetAttribution(
138
141
 
139
142
  const zoomMin = TileMatrixSet.convertZoomLevel(layer.minZoom ? layer.minZoom : 0, GoogleTms, tileMatrix, true);
140
143
  const zoomMax = TileMatrixSet.convertZoomLevel(layer.maxZoom ? layer.maxZoom : 32, GoogleTms, tileMatrix, true);
144
+
141
145
  cols.push({
142
146
  stac_version: Stac.Version,
143
147
  license: Stac.License,
144
148
  id: im.id,
145
- providers: getHost(host),
149
+ providers: im.providers ?? getHost(host),
146
150
  title,
147
151
  description: 'No description',
148
152
  extent,
@@ -150,10 +154,11 @@ async function tileSetAttribution(
150
154
  summaries: {
151
155
  'linz:category': im.category,
152
156
  'linz:zoom': { min: zoomMin, max: zoomMax },
153
- 'linz:priority': [1000 + tileSet.layers.indexOf(layer)],
157
+ 'linz:priority': [1000 + i],
154
158
  },
155
159
  });
156
160
  }
161
+
157
162
  return {
158
163
  id: tileSet.id,
159
164
  type: 'FeatureCollection',
@@ -205,3 +210,43 @@ export async function tileAttributionGet(req: LambdaHttpRequest<TileAttributionG
205
210
  response.json(attributions);
206
211
  return response;
207
212
  }
213
+
214
+ /**
215
+ * Construct a licensor attribution for a given tileSet.
216
+ *
217
+ * @param provider The BasemapsConfigProvider object.
218
+ * @param tileSet The tileset from which to build the attribution.
219
+ * @param projection The projection to consider.
220
+ *
221
+ * @returns A default attribution, if the tileset has more than one layer or no such imagery for the given projection exists.
222
+ * Otherwise, a copyright string comprising the names of the tileset's licensors.
223
+ *
224
+ * @example
225
+ * "CC BY 4.0 LINZ"
226
+ *
227
+ * @example
228
+ * "CC BY 4.0 Nelson City Council, Tasman District Council, Waka Kotahi"
229
+ */
230
+ export async function createTileSetAttribution(
231
+ provider: BasemapsConfigProvider,
232
+ tileSet: ConfigTileSet,
233
+ projection: Epsg,
234
+ ): Promise<string> {
235
+ // ensure the tileset has exactly one layer
236
+ if (tileSet.layers.length > 1 || tileSet.layers[0] == null) {
237
+ return createLicensorAttribution();
238
+ }
239
+
240
+ // ensure imagery exist for the given projection
241
+ const imgId = tileSet.layers[0][projection.code];
242
+ if (imgId == null) return '';
243
+
244
+ // attempt to load the imagery
245
+ const imagery = await provider.Imagery.get(imgId);
246
+ if (imagery?.providers == null) {
247
+ return createLicensorAttribution();
248
+ }
249
+
250
+ // return a licensor attribution string
251
+ return createLicensorAttribution(imagery.providers);
252
+ }
@@ -0,0 +1,55 @@
1
+ import { TileSetType } from '@basemaps/config';
2
+ import { Epsg } from '@basemaps/geo';
3
+ import { getPreviewUrl } from '@basemaps/shared';
4
+ import { LambdaHttpRequest, LambdaHttpResponse } from '@linzjs/lambda';
5
+
6
+ import { ConfigLoader } from '../util/config.loader.js';
7
+
8
+ export interface LinkGet {
9
+ Params: {
10
+ tileSet: string;
11
+ };
12
+ }
13
+
14
+ /**
15
+ * Redirect the client to a Basemaps URL that is already zoomed to the extent of the tileset's imagery.
16
+ *
17
+ * /v1/link/:tileSet
18
+ *
19
+ * @example
20
+ * '/v1/link/ashburton-2023-0.1m'
21
+ *
22
+ * @returns on success, 302 redirect response. on failure, 4xx status code response.
23
+ */
24
+ export async function linkGet(req: LambdaHttpRequest<LinkGet>): Promise<LambdaHttpResponse> {
25
+ const config = await ConfigLoader.load(req);
26
+
27
+ // get tileset
28
+
29
+ req.timer.start('tileset:load');
30
+ const tileSet = await config.TileSet.get(req.params.tileSet);
31
+ req.timer.end('tileset:load');
32
+
33
+ if (tileSet == null) return new LambdaHttpResponse(404, 'Tileset not found');
34
+
35
+ if (tileSet.type !== TileSetType.Raster) return new LambdaHttpResponse(400, 'Tileset must be raster type');
36
+
37
+ // TODO: add support for 'aerial' and 'elevation' multi-layer tilesets
38
+ if (tileSet.layers.length !== 1) return new LambdaHttpResponse(400, 'Too many layers');
39
+
40
+ // get imagery
41
+
42
+ const imageryId = tileSet.layers[0][Epsg.Google.code];
43
+ if (imageryId === undefined) return new LambdaHttpResponse(400, "No imagery for '3857' projection");
44
+
45
+ const imagery = await config.Imagery.get(imageryId);
46
+ if (imagery == null) return new LambdaHttpResponse(400, 'Imagery not found');
47
+
48
+ // do redirect
49
+
50
+ const url = getPreviewUrl({ imagery });
51
+
52
+ return new LambdaHttpResponse(302, 'Redirect to pre-zoomed imagery', {
53
+ location: `/${url.slug}?i=${url.name}`,
54
+ });
55
+ }
@@ -19,6 +19,7 @@ import { Etag } from '../util/etag.js';
19
19
  import { convertStyleToNztmStyle } from '../util/nztm.style.js';
20
20
  import { NotFound, NotModified } from '../util/response.js';
21
21
  import { Validate } from '../util/validate.js';
22
+ import { createTileSetAttribution } from './attribution.js';
22
23
 
23
24
  /**
24
25
  * Convert relative URL into a full hostname URL, converting {tileMatrix} into the provided tileMatrix
@@ -89,7 +90,7 @@ export interface StyleConfig {
89
90
  /**
90
91
  * Turn on the terrain setting in the style json
91
92
  */
92
- function setStyleTerrain(style: StyleJson, terrain: string, tileMatrix: TileMatrixSet): void {
93
+ export function setStyleTerrain(style: StyleJson, terrain: string, tileMatrix: TileMatrixSet): void {
93
94
  const source = Object.keys(style.sources).find((s) => s === terrain);
94
95
  if (source == null) throw new LambdaHttpResponse(400, `Terrain: ${terrain} does not exists in the style source.`);
95
96
  style.terrain = {
@@ -153,12 +154,13 @@ async function ensureTerrain(
153
154
  * Generate a StyleJSON from a tileset
154
155
  * @returns
155
156
  */
156
- export function tileSetToStyle(
157
+ export async function tileSetToStyle(
157
158
  req: LambdaHttpRequest<StyleGet>,
159
+ config: BasemapsConfigProvider,
158
160
  tileSet: ConfigTileSetRaster,
159
161
  tileMatrix: TileMatrixSet,
160
162
  apiKey: string,
161
- ): StyleJson {
163
+ ): Promise<StyleJson> {
162
164
  // If the style has outputs defined it has a different process for generating the stylejson
163
165
  if (tileSet.outputs) return tileSetOutputToStyle(req, tileSet, tileMatrix, apiKey);
164
166
 
@@ -175,12 +177,21 @@ export function tileSetToStyle(
175
177
  (Env.get(Env.PublicUrlBase) ?? '') +
176
178
  `/v1/tiles/${tileSet.name}/${tileMatrix.identifier}/{z}/{x}/{y}.${tileFormat}${query}`;
177
179
 
180
+ const attribution = await createTileSetAttribution(config, tileSet, tileMatrix.projection);
181
+
178
182
  const styleId = `basemaps-${tileSet.name}`;
179
183
  return {
180
184
  id: ConfigId.prefix(ConfigPrefix.Style, tileSet.name),
181
185
  name: tileSet.name,
182
186
  version: 8,
183
- sources: { [styleId]: { type: 'raster', tiles: [tileUrl], tileSize: 256 } },
187
+ sources: {
188
+ [styleId]: {
189
+ type: 'raster',
190
+ tiles: [tileUrl],
191
+ tileSize: 256,
192
+ attribution,
193
+ },
194
+ },
184
195
  layers: [{ id: styleId, type: 'raster', source: styleId }],
185
196
  };
186
197
  }
@@ -248,7 +259,7 @@ async function generateStyleFromTileSet(
248
259
  throw new LambdaHttpResponse(400, 'Only raster tile sets can generate style JSON');
249
260
  }
250
261
  if (tileSet.outputs) return tileSetOutputToStyle(req, tileSet, tileMatrix, apiKey);
251
- else return tileSetToStyle(req, tileSet, tileMatrix, apiKey);
262
+ return tileSetToStyle(req, config, tileSet, tileMatrix, apiKey);
252
263
  }
253
264
 
254
265
  export interface StyleGet {