@basemaps/lambda-tiler 6.38.0 → 6.40.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 (126) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/build/__tests__/config.data.d.ts.map +1 -1
  3. package/build/__tests__/config.data.js +142 -1
  4. package/build/__tests__/config.data.js.map +1 -1
  5. package/build/__tests__/wmts.capability.test.js +9 -16
  6. package/build/__tests__/wmts.capability.test.js.map +1 -1
  7. package/build/__tests__/xyz.util.d.ts.map +1 -1
  8. package/build/__tests__/xyz.util.js +6 -2
  9. package/build/__tests__/xyz.util.js.map +1 -1
  10. package/build/cli/render.tile.d.ts +2 -0
  11. package/build/cli/render.tile.d.ts.map +1 -0
  12. package/build/cli/render.tile.js +33 -0
  13. package/build/cli/render.tile.js.map +1 -0
  14. package/build/routes/__tests__/attribution.test.js +63 -3
  15. package/build/routes/__tests__/attribution.test.js.map +1 -1
  16. package/build/routes/__tests__/tile.style.json.test.js +91 -43
  17. package/build/routes/__tests__/tile.style.json.test.js.map +1 -1
  18. package/build/routes/__tests__/wmts.test.js +50 -8
  19. package/build/routes/__tests__/wmts.test.js.map +1 -1
  20. package/build/routes/attribution.d.ts +12 -0
  21. package/build/routes/attribution.d.ts.map +1 -1
  22. package/build/routes/attribution.js +29 -27
  23. package/build/routes/attribution.js.map +1 -1
  24. package/build/routes/tile.json.d.ts.map +1 -1
  25. package/build/routes/tile.json.js +2 -1
  26. package/build/routes/tile.json.js.map +1 -1
  27. package/build/routes/tile.style.json.d.ts +2 -2
  28. package/build/routes/tile.style.json.d.ts.map +1 -1
  29. package/build/routes/tile.style.json.js +7 -4
  30. package/build/routes/tile.style.json.js.map +1 -1
  31. package/build/routes/tile.wmts.d.ts.map +1 -1
  32. package/build/routes/tile.wmts.js +3 -1
  33. package/build/routes/tile.wmts.js.map +1 -1
  34. package/build/routes/tile.xyz.raster.d.ts.map +1 -1
  35. package/build/routes/tile.xyz.raster.js +9 -6
  36. package/build/routes/tile.xyz.raster.js.map +1 -1
  37. package/build/util/__test__/filter.test.d.ts +2 -0
  38. package/build/util/__test__/filter.test.d.ts.map +1 -0
  39. package/build/util/__test__/filter.test.js +64 -0
  40. package/build/util/__test__/filter.test.js.map +1 -0
  41. package/build/util/config.loader.d.ts.map +1 -1
  42. package/build/util/config.loader.js +2 -3
  43. package/build/util/config.loader.js.map +1 -1
  44. package/build/util/filter.d.ts +15 -0
  45. package/build/util/filter.d.ts.map +1 -0
  46. package/build/util/filter.js +59 -0
  47. package/build/util/filter.js.map +1 -0
  48. package/build/wmts.capability.d.ts +8 -5
  49. package/build/wmts.capability.d.ts.map +1 -1
  50. package/build/wmts.capability.js +17 -15
  51. package/build/wmts.capability.js.map +1 -1
  52. package/dist/index.js +60 -60
  53. package/dist/node_modules/.package-lock.json +14 -14
  54. package/dist/node_modules/minimist/.eslintrc +25 -50
  55. package/dist/node_modules/minimist/CHANGELOG.md +87 -1
  56. package/dist/node_modules/minimist/README.md +14 -10
  57. package/dist/node_modules/minimist/example/parse.js +2 -0
  58. package/dist/node_modules/minimist/index.js +256 -242
  59. package/dist/node_modules/minimist/package.json +73 -73
  60. package/dist/node_modules/minimist/test/all_bool.js +26 -24
  61. package/dist/node_modules/minimist/test/bool.js +146 -147
  62. package/dist/node_modules/minimist/test/dash.js +33 -21
  63. package/dist/node_modules/minimist/test/default_bool.js +26 -24
  64. package/dist/node_modules/minimist/test/dotted.js +13 -11
  65. package/dist/node_modules/minimist/test/kv_short.js +26 -10
  66. package/dist/node_modules/minimist/test/long.js +28 -26
  67. package/dist/node_modules/minimist/test/num.js +30 -28
  68. package/dist/node_modules/minimist/test/parse.js +169 -157
  69. package/dist/node_modules/minimist/test/parse_modified.js +7 -5
  70. package/dist/node_modules/minimist/test/proto.js +41 -37
  71. package/dist/node_modules/minimist/test/short.js +57 -55
  72. package/dist/node_modules/minimist/test/stop_early.js +10 -8
  73. package/dist/node_modules/minimist/test/unknown.js +83 -81
  74. package/dist/node_modules/minimist/test/whitespace.js +6 -4
  75. package/dist/node_modules/node-abi/.circleci/config.yml +7 -25
  76. package/dist/node_modules/node-abi/.github/CODEOWNERS +1 -0
  77. package/dist/node_modules/node-abi/.github/workflows/update-abi.yml +5 -4
  78. package/dist/node_modules/node-abi/CONTRIBUTING.md +1 -1
  79. package/dist/node_modules/node-abi/README.md +5 -3
  80. package/dist/node_modules/node-abi/abi_registry.json +14 -0
  81. package/dist/node_modules/node-abi/package.json +5 -6
  82. package/dist/node_modules/node-addon-api/README.md +36 -12
  83. package/dist/node_modules/node-addon-api/index.js +3 -3
  84. package/dist/node_modules/node-addon-api/napi-inl.deprecated.h +121 -127
  85. package/dist/node_modules/node-addon-api/napi-inl.h +1166 -1122
  86. package/dist/node_modules/node-addon-api/napi.h +2786 -2675
  87. package/dist/node_modules/node-addon-api/package.json +42 -1
  88. package/dist/node_modules/node-addon-api/tools/check-napi.js +13 -14
  89. package/dist/node_modules/node-addon-api/tools/conversion.js +161 -169
  90. package/dist/node_modules/node-addon-api/tools/eslint-format.js +9 -1
  91. package/dist/node_modules/readable-stream/README.md +1 -1
  92. package/dist/node_modules/readable-stream/lib/_stream_duplex.js +12 -25
  93. package/dist/node_modules/readable-stream/lib/_stream_passthrough.js +2 -4
  94. package/dist/node_modules/readable-stream/lib/_stream_readable.js +176 -273
  95. package/dist/node_modules/readable-stream/lib/_stream_transform.js +26 -37
  96. package/dist/node_modules/readable-stream/lib/_stream_writable.js +118 -174
  97. package/dist/node_modules/readable-stream/lib/internal/streams/async_iterator.js +10 -37
  98. package/dist/node_modules/readable-stream/lib/internal/streams/buffer_list.js +20 -47
  99. package/dist/node_modules/readable-stream/lib/internal/streams/destroy.js +8 -17
  100. package/dist/node_modules/readable-stream/lib/internal/streams/end-of-stream.js +1 -19
  101. package/dist/node_modules/readable-stream/lib/internal/streams/from.js +12 -24
  102. package/dist/node_modules/readable-stream/lib/internal/streams/pipeline.js +5 -16
  103. package/dist/node_modules/readable-stream/lib/internal/streams/state.js +2 -7
  104. package/dist/node_modules/readable-stream/package.json +1 -1
  105. package/dist/package-lock.json +15 -346
  106. package/dist/package.json +1 -1
  107. package/package.json +8 -8
  108. package/src/__tests__/config.data.ts +142 -1
  109. package/src/__tests__/wmts.capability.test.ts +9 -16
  110. package/src/__tests__/xyz.util.ts +6 -2
  111. package/src/cli/render.tile.ts +38 -0
  112. package/src/routes/__tests__/attribution.test.ts +65 -3
  113. package/src/routes/__tests__/tile.style.json.test.ts +100 -44
  114. package/src/routes/__tests__/wmts.test.ts +70 -9
  115. package/src/routes/attribution.ts +24 -28
  116. package/src/routes/tile.json.ts +2 -1
  117. package/src/routes/tile.style.json.ts +13 -5
  118. package/src/routes/tile.wmts.ts +3 -1
  119. package/src/routes/tile.xyz.raster.ts +10 -7
  120. package/src/util/__test__/filter.test.ts +80 -0
  121. package/src/util/config.loader.ts +1 -2
  122. package/src/util/filter.ts +60 -0
  123. package/src/wmts.capability.ts +19 -14
  124. package/tsconfig.tsbuildinfo +1 -1
  125. package/test-dump.js +0 -6
  126. package/test-imagery.js +0 -3
@@ -9,18 +9,11 @@ import { Api, mockUrlRequest } from '../../__tests__/xyz.util.js';
9
9
  o.spec('WMTSRouting', () => {
10
10
  const sandbox = createSandbox();
11
11
  const config = new ConfigProviderMemory();
12
+ const imagery = new Map();
12
13
 
13
- o.before(() => {
14
+ o.beforeEach(() => {
14
15
  sandbox.stub(ConfigLoader, 'load').resolves(config);
15
- });
16
16
 
17
- o.afterEach(() => {
18
- config.objects.clear();
19
- sandbox.restore();
20
- });
21
-
22
- o('should default to the aerial layer', async () => {
23
- const imagery = new Map();
24
17
  imagery.set(Imagery3857.id, Imagery3857);
25
18
  imagery.set(Imagery2193.id, Imagery2193);
26
19
 
@@ -28,7 +21,14 @@ o.spec('WMTSRouting', () => {
28
21
  config.put(Imagery2193);
29
22
  config.put(Imagery3857);
30
23
  config.put(Provider);
24
+ });
31
25
 
26
+ o.afterEach(() => {
27
+ config.objects.clear();
28
+ sandbox.restore();
29
+ });
30
+
31
+ o('should default to the aerial layer', async () => {
32
32
  const req = mockUrlRequest(
33
33
  '/v1/tiles/WMTSCapabilities.xml',
34
34
  `format=png&api=${Api.key}&config=s3://linz-basemaps/config.json`,
@@ -53,4 +53,65 @@ o.spec('WMTSRouting', () => {
53
53
  '<ResourceURL format="image/png" resourceType="tile" template="https://tiles.test/v1/tiles/ōtorohanga-urban-2021-0.1m/{TileMatrixSet}/{TileMatrix}/{TileCol}/{TileRow}.png?api=d01f7w7rnhdzg0p7fyrc9v9ard1&amp;config=Q5pC4UjWdtFLU1CYtLcRSmB49RekgDgMa5EGJnB2M" />',
54
54
  ]);
55
55
  });
56
+
57
+ o('should filter out date[after] by year', async () => {
58
+ const req = mockUrlRequest(
59
+ '/v1/tiles/WMTSCapabilities.xml',
60
+ `format=png&api=${Api.key}&config=s3://linz-basemaps/config.json&date[after]=2022`,
61
+ );
62
+ const res = await handler.router.handle(req);
63
+
64
+ o(res.status).equals(200);
65
+ const lines = Buffer.from(res.body, 'base64').toString().split('\n');
66
+ const titles = lines.filter((f) => f.startsWith(' <ows:Title>')).map((f) => f.trim());
67
+
68
+ o(titles).deepEquals([
69
+ '<ows:Title>Aerial Imagery</ows:Title>',
70
+ '<ows:Title>Google Maps Compatible for the World</ows:Title>',
71
+ '<ows:Title>LINZ NZTM2000 Map Tile Grid V2</ows:Title>',
72
+ ]);
73
+ });
74
+
75
+ o('should filter out date[before] by year', async () => {
76
+ const req = mockUrlRequest(
77
+ '/v1/tiles/WMTSCapabilities.xml',
78
+ `format=png&api=${Api.key}&config=s3://linz-basemaps/config.json&date[before]=2020`,
79
+ );
80
+ const res = await handler.router.handle(req);
81
+
82
+ o(res.status).equals(200);
83
+ const lines = Buffer.from(res.body, 'base64').toString().split('\n');
84
+ const titles = lines.filter((f) => f.startsWith(' <ows:Title>')).map((f) => f.trim());
85
+
86
+ o(titles).deepEquals([
87
+ '<ows:Title>Aerial Imagery</ows:Title>',
88
+ '<ows:Title>Google Maps Compatible for the World</ows:Title>',
89
+ '<ows:Title>LINZ NZTM2000 Map Tile Grid V2</ows:Title>',
90
+ ]);
91
+ });
92
+
93
+ o('should filter inclusive date[before] by year', async () => {
94
+ const req = mockUrlRequest(
95
+ '/v1/tiles/WMTSCapabilities.xml',
96
+ `format=png&api=${Api.key}&config=s3://linz-basemaps/config.json&date[before]=2021`,
97
+ );
98
+ const res = await handler.router.handle(req);
99
+
100
+ o(res.status).equals(200);
101
+ const lines = Buffer.from(res.body, 'base64').toString().split('\n');
102
+ const titles = lines.filter((f) => f.startsWith(' <ows:Title>')).map((f) => f.trim());
103
+
104
+ o(titles).deepEquals([
105
+ '<ows:Title>Aerial Imagery</ows:Title>',
106
+ '<ows:Title>Ōtorohanga 0.1m Urban Aerial Photos (2021)</ows:Title>',
107
+ '<ows:Title>Google Maps Compatible for the World</ows:Title>',
108
+ '<ows:Title>LINZ NZTM2000 Map Tile Grid V2</ows:Title>',
109
+ ]);
110
+
111
+ const resourceURLs = lines.filter((f) => f.includes('<ResourceURL')).map((f) => f.trim());
112
+ o(resourceURLs).deepEquals([
113
+ '<ResourceURL format="image/png" resourceType="tile" template="https://tiles.test/v1/tiles/aerial/{TileMatrixSet}/{TileMatrix}/{TileCol}/{TileRow}.png?api=d01f7w7rnhdzg0p7fyrc9v9ard1&amp;config=Q5pC4UjWdtFLU1CYtLcRSmB49RekgDgMa5EGJnB2M&amp;date%5Bbefore%5D=2021" />',
114
+ '<ResourceURL format="image/png" resourceType="tile" template="https://tiles.test/v1/tiles/ōtorohanga-urban-2021-0.1m/{TileMatrixSet}/{TileMatrix}/{TileCol}/{TileRow}.png?api=d01f7w7rnhdzg0p7fyrc9v9ard1&amp;config=Q5pC4UjWdtFLU1CYtLcRSmB49RekgDgMa5EGJnB2M" />',
115
+ ]);
116
+ });
56
117
  });
@@ -11,12 +11,13 @@ import {
11
11
  StacProvider,
12
12
  TileMatrixSet,
13
13
  } from '@basemaps/geo';
14
- import { extractYearRangeFromName, Projection, titleizeImageryName } from '@basemaps/shared';
14
+ import { extractYearRangeFromName, extractYearRangeFromTitle, Projection } from '@basemaps/shared';
15
15
  import { BBox, MultiPolygon, multiPolygonToWgs84, Pair, union, Wgs84 } from '@linzjs/geojson';
16
16
  import { HttpHeader, LambdaHttpRequest, LambdaHttpResponse } from '@linzjs/lambda';
17
17
  import { ConfigLoader } from '../util/config.loader.js';
18
18
 
19
19
  import { Etag } from '../util/etag.js';
20
+ import { filterLayers, yearRangeToInterval } from '../util/filter.js';
20
21
  import { NotFound, NotModified } from '../util/response.js';
21
22
  import { Validate } from '../util/validate.js';
22
23
 
@@ -44,23 +45,19 @@ function roundPair(p: Pair): Pair {
44
45
  * @param files in target projection
45
46
  * @return MultiPolygon in WGS84
46
47
  */
47
- function createCoordinates(bbox: BBox, files: NamedBounds[], proj: Projection): MultiPolygon {
48
+ export function createCoordinates(bbox: BBox, files: NamedBounds[], proj: Projection): MultiPolygon {
48
49
  if (Wgs84.delta(bbox[0], bbox[2]) <= 0) {
49
50
  // This bounds spans more than half the globe which multiPolygonToWgs84 can't handle; just
50
51
  // return bbox as polygon
51
52
  return Wgs84.bboxToMultiPolygon(bbox);
52
53
  }
53
54
 
54
- let coordinates: MultiPolygon = [];
55
-
55
+ const polygons: MultiPolygon = [];
56
56
  // merge imagery bounds
57
- for (const image of files) {
58
- const poly = [Bounds.fromJson(image).pad(SmoothPadding).toPolygon()] as MultiPolygon;
59
- coordinates = union(coordinates, poly);
60
- }
57
+ for (const image of files) polygons.push(Bounds.fromJson(image).pad(SmoothPadding).toPolygon());
58
+ const coordinates = union(polygons);
61
59
 
62
60
  const roundToWgs84 = (p: number[]): number[] => roundPair(proj.toWgs84(p) as Pair);
63
-
64
61
  return multiPolygonToWgs84(coordinates, roundToWgs84);
65
62
  }
66
63
 
@@ -86,30 +83,23 @@ async function tileSetAttribution(
86
83
 
87
84
  const config = await ConfigLoader.load(req);
88
85
  const imagery = await getAllImagery(config, tileSet.layers, [tileMatrix.projection]);
86
+ const filteredLayers = filterLayers(req, tileSet.layers);
89
87
 
90
88
  const host = await config.Provider.get(config.Provider.id('linz'));
91
89
 
92
- for (const layer of tileSet.layers) {
90
+ for (const layer of filteredLayers) {
91
+ if (layer.disabled) continue;
93
92
  const imgId = layer[proj.epsg.code];
94
93
  if (imgId == null) continue;
95
94
  const im = imagery.get(imgId);
96
95
  if (im == null) continue;
96
+ const title = im.title;
97
97
 
98
98
  const bbox = proj.boundsToWgs84BoundingBox(im.bounds).map(roundNumber) as BBox;
99
99
 
100
- const years = extractYearRangeFromName(im.name);
101
- if (years[0] === -1) {
102
- req.log.debug({ imagery: im.name }, 'Attribution:DefaultYear');
103
- // Put it in the future so people know its a "fake" date
104
- years[0] = new Date().getUTCFullYear() + 1;
105
- years[1] = years[0] + 1;
106
- }
107
-
108
- const interval = [years.map((y) => `${y}-01-01T00:00:00Z`) as [string, string]];
109
-
110
- const extent: StacExtent = { spatial: { bbox: [bbox] }, temporal: { interval } };
100
+ const extent: StacExtent = { spatial: { bbox: [bbox] } };
111
101
 
112
- items.push({
102
+ const item: AttributionItem = {
113
103
  type: 'Feature',
114
104
  stac_version: Stac.Version,
115
105
  id: imgId + '_item',
@@ -119,13 +109,19 @@ async function tileSetAttribution(
119
109
  bbox,
120
110
  geometry: { type: 'MultiPolygon', coordinates: createCoordinates(bbox, im.files, proj) },
121
111
  properties: {
122
- title: im.title ?? titleizeImageryName(im.name),
112
+ title,
123
113
  category: im.category,
124
- datetime: null,
125
- start_datetime: interval[0][0],
126
- end_datetime: interval[0][1],
127
114
  },
128
- });
115
+ };
116
+ const years = extractYearRangeFromTitle(im.title) ?? extractYearRangeFromName(im.name);
117
+ if (years) {
118
+ const interval = yearRangeToInterval(years);
119
+ extent.temporal = { interval: [[interval[0].toISOString(), interval[1].toISOString()]] };
120
+ item.properties.datetime = null;
121
+ item.properties.start_datetime = interval[0].toISOString();
122
+ item.properties.end_datetime = interval[1].toISOString();
123
+ }
124
+ items.push(item);
129
125
 
130
126
  const zoomMin = TileMatrixSet.convertZoomLevel(layer.minZoom ? layer.minZoom : 0, GoogleTms, tileMatrix, true);
131
127
  const zoomMax = TileMatrixSet.convertZoomLevel(layer.maxZoom ? layer.maxZoom : 32, GoogleTms, tileMatrix, true);
@@ -134,7 +130,7 @@ async function tileSetAttribution(
134
130
  license: Stac.License,
135
131
  id: im.id,
136
132
  providers: getHost(host),
137
- title: im.title ?? titleizeImageryName(im.name),
133
+ title,
138
134
  description: 'No description',
139
135
  extent,
140
136
  links: [],
@@ -2,6 +2,7 @@ import { GoogleTms, TileJson, TileMatrixSet } from '@basemaps/geo';
2
2
  import { Env, toQueryString } from '@basemaps/shared';
3
3
  import { HttpHeader, LambdaHttpRequest, LambdaHttpResponse } from '@linzjs/lambda';
4
4
  import { ConfigLoader } from '../util/config.loader.js';
5
+ import { getFilters } from '../util/filter.js';
5
6
  import { NotFound } from '../util/response.js';
6
7
  import { Validate } from '../util/validate.js';
7
8
 
@@ -31,7 +32,7 @@ export async function tileJsonGet(req: LambdaHttpRequest<TileJsonGet>): Promise<
31
32
 
32
33
  const configLocation = ConfigLoader.extract(req);
33
34
 
34
- const query = toQueryString({ api: apiKey, config: configLocation });
35
+ const query = toQueryString({ api: apiKey, config: configLocation, ...getFilters(req) });
35
36
 
36
37
  const tileUrl =
37
38
  [host, 'v1', 'tiles', tileSet.name, tileMatrix.identifier, '{z}', '{x}', '{y}'].join('/') + `.${format[0]}${query}`;
@@ -1,4 +1,4 @@
1
- import { ConfigTileSetRaster, Sources, StyleJson, TileSetType } from '@basemaps/config';
1
+ import { ConfigTileSetRaster, Layer, Sources, StyleJson, TileSetType } from '@basemaps/config';
2
2
  import { Env, toQueryString } from '@basemaps/shared';
3
3
  import { fsa } from '@chunkd/fs';
4
4
  import { HttpHeader, LambdaHttpRequest, LambdaHttpResponse } from '@linzjs/lambda';
@@ -8,6 +8,7 @@ import { Validate } from '../util/validate.js';
8
8
  import { Etag } from '../util/etag.js';
9
9
  import { ConfigLoader } from '../util/config.loader.js';
10
10
  import { GoogleTms, ImageFormat, TileMatrixSets } from '@basemaps/geo';
11
+ import { getFilters } from '../util/filter.js';
11
12
 
12
13
  /**
13
14
  * Convert relative URLS into a full hostname url
@@ -31,7 +32,7 @@ export function convertRelativeUrl(url?: string, apiKey?: string, config?: strin
31
32
  * @param apiKey api key to inject
32
33
  * @returns new stylejson
33
34
  */
34
- export function convertStyleJson(style: StyleJson, apiKey: string, config: string | null): StyleJson {
35
+ export function convertStyleJson(style: StyleJson, apiKey: string, config: string | null, layers?: Layer[]): StyleJson {
35
36
  const sources: Sources = JSON.parse(JSON.stringify(style.sources));
36
37
  for (const [key, value] of Object.entries(sources)) {
37
38
  if (value.type === 'vector') {
@@ -49,7 +50,7 @@ export function convertStyleJson(style: StyleJson, apiKey: string, config: strin
49
50
  id: style.id,
50
51
  name: style.name,
51
52
  sources,
52
- layers: style.layers,
53
+ layers: layers ? layers : style.layers,
53
54
  metadata: style.metadata ?? {},
54
55
  glyphs: convertRelativeUrl(style.glyphs, undefined, config),
55
56
  sprite: convertRelativeUrl(style.sprite, undefined, config),
@@ -73,7 +74,7 @@ export async function tileSetToStyle(
73
74
  if (tileFormat == null) return new LambdaHttpResponse(400, 'Invalid image format');
74
75
 
75
76
  const configLocation = ConfigLoader.extract(req);
76
- const query = toQueryString({ config: configLocation, api: apiKey });
77
+ const query = toQueryString({ config: configLocation, api: apiKey, ...getFilters(req) });
77
78
 
78
79
  const tileUrl = fsa.join(
79
80
  Env.get(Env.PublicUrlBase) ?? '',
@@ -101,6 +102,8 @@ export async function tileSetToStyle(
101
102
  export async function styleJsonGet(req: LambdaHttpRequest<StyleGet>): Promise<LambdaHttpResponse> {
102
103
  const apiKey = Validate.apiKey(req);
103
104
  const styleName = req.params.styleName;
105
+ const excludeLayers = req.query.getAll('exclude');
106
+ const excluded = new Set(excludeLayers.map((l) => l.toLowerCase()));
104
107
 
105
108
  // Get style Config from db
106
109
  const config = await ConfigLoader.load(req);
@@ -115,7 +118,12 @@ export async function styleJsonGet(req: LambdaHttpRequest<StyleGet>): Promise<La
115
118
  }
116
119
 
117
120
  // Prepare sources and add linz source
118
- const style = convertStyleJson(styleConfig.style, apiKey, ConfigLoader.extract(req));
121
+ const style = convertStyleJson(
122
+ styleConfig.style,
123
+ apiKey,
124
+ ConfigLoader.extract(req),
125
+ styleConfig.style.layers.filter((f) => !excluded.has(f.id.toLowerCase())),
126
+ );
119
127
  const data = Buffer.from(JSON.stringify(style));
120
128
 
121
129
  const cacheKey = Etag.key(data);
@@ -8,6 +8,7 @@ import { Validate } from '../util/validate.js';
8
8
  import { WmtsCapabilities } from '../wmts.capability.js';
9
9
  import { Etag } from '../util/etag.js';
10
10
  import { ConfigLoader } from '../util/config.loader.js';
11
+ import { filterLayers, getFilters } from '../util/filter.js';
11
12
 
12
13
  export interface WmtsCapabilitiesGet {
13
14
  Params: {
@@ -59,11 +60,12 @@ export async function wmtsCapabilitiesGet(req: LambdaHttpRequest<WmtsCapabilitie
59
60
  provider: provider ?? undefined,
60
61
  tileSet,
61
62
  tileMatrix,
62
- isIndividualLayers: req.params.tileMatrix == null,
63
63
  imagery,
64
64
  apiKey,
65
65
  config: ConfigLoader.extract(req),
66
66
  formats: Validate.getRequestedFormats(req),
67
+ layers: req.params.tileMatrix == null ? filterLayers(req, tileSet.layers) : null,
68
+ filters: getFilters(req),
67
69
  }).toXml();
68
70
  if (xml == null) return NotFound();
69
71
 
@@ -9,6 +9,7 @@ import { HttpHeader, LambdaHttpRequest, LambdaHttpResponse } from '@linzjs/lambd
9
9
  import pLimit from 'p-limit';
10
10
  import { ConfigLoader } from '../util/config.loader.js';
11
11
  import { Etag } from '../util/etag.js';
12
+ import { filterLayers } from '../util/filter.js';
12
13
  import { NotFound, NotModified } from '../util/response.js';
13
14
  import { CoSources } from '../util/source.cache.js';
14
15
  import { TileXyz } from '../util/validate.js';
@@ -32,13 +33,15 @@ export const TileXyzRaster = {
32
33
  async getAssetsForTile(req: LambdaHttpRequest, tileSet: ConfigTileSetRaster, xyz: TileXyz): Promise<string[]> {
33
34
  const config = await ConfigLoader.load(req);
34
35
  const imagery = await getAllImagery(config, tileSet.layers, [xyz.tileMatrix.projection]);
36
+ const filteredLayers = filterLayers(req, tileSet.layers);
35
37
 
36
38
  const output: string[] = [];
37
39
  const tileBounds = xyz.tileMatrix.tileToSourceBounds(xyz.tile);
38
40
 
39
41
  // All zoom level config is stored as Google zoom levels
40
42
  const filterZoom = TileMatrixSet.convertZoomLevel(xyz.tile.z, xyz.tileMatrix, TileMatrixSets.get(Epsg.Google));
41
- for (const layer of tileSet.layers) {
43
+ for (const layer of filteredLayers) {
44
+ if (layer.disabled) continue;
42
45
  if (layer.maxZoom != null && filterZoom > layer.maxZoom) continue;
43
46
  if (layer.minZoom != null && filterZoom < layer.minZoom) continue;
44
47
 
@@ -58,14 +61,14 @@ export const TileXyzRaster = {
58
61
  }
59
62
  if (!tileBounds.intersects(Bounds.fromJson(img.bounds))) continue;
60
63
 
61
- // FIXME is this meant to be >= <=
62
- if (img.overviews && img.overviews.maxZoom >= filterZoom && img.overviews.minZoom <= filterZoom) {
63
- output.push(fsa.join(img.uri, img.overviews.path));
64
- continue;
65
- }
66
-
67
64
  for (const c of img.files) {
68
65
  if (!tileBounds.intersects(Bounds.fromJson(c))) continue;
66
+
67
+ if (img.overviews && img.overviews.maxZoom >= filterZoom && img.overviews.minZoom <= filterZoom) {
68
+ output.push(fsa.join(img.uri, img.overviews.path));
69
+ break;
70
+ }
71
+
69
72
  const tiffPath = fsa.join(img.uri, getTiffName(c.name));
70
73
  output.push(tiffPath);
71
74
  }
@@ -0,0 +1,80 @@
1
+ import { ConfigLayer } from '@basemaps/config';
2
+ import o from 'ospec';
3
+ import { mockUrlRequest } from '../../__tests__/xyz.util.js';
4
+ import { filterLayers } from '../filter.js';
5
+
6
+ o.spec('filterLayers', () => {
7
+ const sourceLayers: ConfigLayer[] = [
8
+ {
9
+ name: 'waikato-0_625m-snc12836-2004',
10
+ title: 'Waikato 0.625m SNC12836 (2004-2008)',
11
+ },
12
+ {
13
+ name: 'hawkes-bay--manawat-whanganui-0_75m-snc30001-2002',
14
+ title: 'Hawkes Bay / Manawatū-Whanganui 0.75m SNC30001 (2002)',
15
+ },
16
+ {
17
+ name: 'canterbury-0_75m-snc25054-2000-2001',
18
+ title: 'Canterbury 0.75m SNC25054 (2000-2001)',
19
+ },
20
+ {
21
+ name: 'otago-0_375m-sn3806-1975',
22
+ title: 'Otago 0.375m SN3806 (1975)',
23
+ },
24
+ ];
25
+
26
+ o('should not filter with empty parameters', () => {
27
+ const layers = filterLayers(mockUrlRequest('/foo/bar,js'), sourceLayers);
28
+ o(layers).deepEquals(sourceLayers);
29
+ });
30
+
31
+ o('should filter date[after]', () => {
32
+ const dateAfter = '2003-12-31T23:59:59.999';
33
+ const layers = filterLayers(mockUrlRequest('/foo/bar,js', `?date[after]=${dateAfter}`), sourceLayers);
34
+ o(layers).deepEquals([sourceLayers[0]]);
35
+ });
36
+
37
+ o('should filter date[before]', () => {
38
+ const dateBefore = '2003-01-01T00:00:00.000Z';
39
+ const layers = filterLayers(mockUrlRequest('/foo/bar,js', `?date[before]=${dateBefore}`), sourceLayers);
40
+ o(layers).deepEquals(sourceLayers.slice(1));
41
+ });
42
+
43
+ o('should filter date[before] in between years', () => {
44
+ const dateBefore = '2026-01-01T00:00:00.000Z';
45
+ const layer = [{ name: '', title: 'Waikato 0.625m SNC12836 (2020-2028)' }];
46
+ const layers = filterLayers(mockUrlRequest('/foo/bar,js', `?date[before]=${dateBefore}`), layer);
47
+ o(layers).deepEquals(layer);
48
+ });
49
+
50
+ o('should filter date[after] in between years', () => {
51
+ const dateAfter = '2026-12-31T23:59:59.999Z';
52
+ const layer = [{ name: '', title: 'Waikato 0.625m SNC12836 (2020-2028)' }];
53
+ const layers = filterLayers(mockUrlRequest('/foo/bar,js', `?date[after]=${dateAfter}`), layer);
54
+ o(layers).deepEquals(layer);
55
+ });
56
+
57
+ o('should filter date[after] and date[before] in between years', () => {
58
+ const dateAfter = '2026-12-31T23:59:59.999Z';
59
+ const dateBefore = '2028-01-01T00:00:00.000Z';
60
+
61
+ const layer = [{ name: '', title: 'Waikato 0.625m SNC12836 (2020-2028)' }];
62
+ const layers = filterLayers(
63
+ mockUrlRequest('/foo/bar,js', `?date[after]=${dateAfter}&date[before]=${dateBefore}`),
64
+ layer,
65
+ );
66
+ o(layers).deepEquals(layer);
67
+ });
68
+
69
+ o('should filter date[after] and date[before] in between years with single year', () => {
70
+ const dateAfter = '2026-12-31T23:59:59.999Z';
71
+ const dateBefore = '2028-01-01T00:00:00.000Z';
72
+
73
+ const layer = [{ name: '', title: 'Waikato 0.625m SNC12836 (2028)' }];
74
+ const layers = filterLayers(
75
+ mockUrlRequest('/foo/bar,js', `?date[after]=${dateAfter}&date[before]=${dateBefore}`),
76
+ layer,
77
+ );
78
+ o(layers).deepEquals(layer);
79
+ });
80
+ });
@@ -15,8 +15,7 @@ export class ConfigLoader {
15
15
  const config = getDefaultConfig();
16
16
  if (config.assets == null) {
17
17
  const cb = await config.ConfigBundle.get(config.ConfigBundle.id('latest'));
18
- if (cb == null) throw new LambdaHttpResponse(400, 'Unable to get lastest config bundle for asset.');
19
- config.assets = cb.assets;
18
+ if (cb) config.assets = cb.assets;
20
19
  }
21
20
  return config;
22
21
  }
@@ -0,0 +1,60 @@
1
+ import { ConfigLayer } from '@basemaps/config';
2
+ import { extractYearRangeFromTitle } from '@basemaps/shared';
3
+ import { LambdaHttpRequest, LambdaHttpResponse } from '@linzjs/lambda';
4
+
5
+ export const FilterNames = {
6
+ DateBefore: 'date[before]',
7
+ DateAfter: 'date[after]',
8
+ };
9
+
10
+ export function getFilters(req: LambdaHttpRequest): Record<string, string | undefined> {
11
+ return {
12
+ [FilterNames.DateBefore]: req.query.get(FilterNames.DateBefore) ?? undefined,
13
+ [FilterNames.DateAfter]: req.query.get(FilterNames.DateAfter) ?? undefined,
14
+ };
15
+ }
16
+ /**
17
+ * Convert the year range into full ISO date year range
18
+ *
19
+ * Expand to the full year of jan 1st 00:00 -> Dec 31st 23:59
20
+ */
21
+ export function yearRangeToInterval(x: [number] | [number, number]): [Date, Date] {
22
+ if (x.length === 1) return [new Date(`${x[0]}-01-01T00:00:00.000Z`), new Date(`${x[0]}-12-31T23:59:59.999Z`)];
23
+ return [new Date(`${x[0]}-01-01T00:00:00.000Z`), new Date(`${x[1]}-12-31T23:59:59.999Z`)];
24
+ }
25
+
26
+ function parseDateAsIso(s: string | null): Date | null {
27
+ if (s == null) return null;
28
+ const date = new Date(s);
29
+ if (isNaN(date.getTime())) throw new LambdaHttpResponse(400, `Invalid date format: "${s}"`);
30
+ return date;
31
+ }
32
+
33
+ export function filterLayers(req: LambdaHttpRequest, layers: ConfigLayer[]): ConfigLayer[] {
34
+ const dateAfterQuery = req.query.get(FilterNames.DateAfter);
35
+ const dateBeforeQuery = req.query.get(FilterNames.DateBefore);
36
+
37
+ if (dateAfterQuery == null && dateBeforeQuery == null) return layers;
38
+ const dateAfter = parseDateAsIso(dateAfterQuery);
39
+ const dateBefore = parseDateAsIso(dateBeforeQuery);
40
+
41
+ const filtered = layers.filter((l) => {
42
+ if (l.title == null) return false;
43
+ const yearRange = extractYearRangeFromTitle(l.title);
44
+ if (yearRange == null) return false;
45
+
46
+ const ranges = yearRangeToInterval(yearRange);
47
+ const startYear = ranges[0];
48
+ const endYear = ranges[1];
49
+ return (dateAfter == null || endYear >= dateAfter) && (dateBefore == null || startYear <= dateBefore);
50
+ });
51
+
52
+ // Trace that layers have been filtered
53
+ req.set('layerFilter', {
54
+ [FilterNames.DateBefore]: dateBefore?.toISOString(),
55
+ [FilterNames.DateAfter]: dateAfter?.toISOString(),
56
+ layerCount: layers.length,
57
+ filterCount: filtered.length,
58
+ });
59
+ return filtered;
60
+ }
@@ -28,8 +28,6 @@ export interface WmtsCapabilitiesParams {
28
28
  tileSet: ConfigTileSet;
29
29
  /** List of tile matrixes to output */
30
30
  tileMatrix: TileMatrixSet[];
31
- /** Should WMTS Layers be created for each imagery set inside this tileSet */
32
- isIndividualLayers: boolean;
33
31
  /** All the imagery used by the tileSet and tileMatrixes */
34
32
  imagery: Map<string, ConfigImagery>;
35
33
  /** API key to append to all resource urls */
@@ -38,6 +36,10 @@ export interface WmtsCapabilitiesParams {
38
36
  formats?: ImageFormat[] | null;
39
37
  /** Config location */
40
38
  config?: string | null;
39
+ /** Specific layers to add to the WMTS */
40
+ layers?: ConfigLayer[] | null;
41
+ /** Specific DateRange filter for the wmts layers */
42
+ filters?: Record<string, string | undefined>;
41
43
  }
42
44
 
43
45
  /** Number of decimal places to use in lat lng */
@@ -62,21 +64,23 @@ export class WmtsCapabilities {
62
64
  tileMatrixSets = new Map<string, TileMatrixSet>();
63
65
  imagery: Map<string, ConfigImagery>;
64
66
  formats: ImageFormat[];
65
- isIndividualLayers = false;
67
+ filters?: Record<string, string | undefined>;
66
68
 
67
69
  minZoom = 0;
68
70
  maxZoom = 32;
71
+ layers: ConfigLayer[] | null | undefined;
69
72
 
70
73
  constructor(params: WmtsCapabilitiesParams) {
71
74
  this.httpBase = params.httpBase;
72
75
  this.provider = params.provider;
73
76
  this.tileSet = params.tileSet;
74
77
  this.config = params.config;
75
- this.isIndividualLayers = params.isIndividualLayers;
76
78
  for (const tms of params.tileMatrix) this.tileMatrixSets.set(tms.identifier, tms);
77
79
  this.apiKey = params.apiKey;
78
80
  this.formats = params.formats ?? ImageFormatOrder;
79
81
  this.imagery = params.imagery;
82
+ this.layers = params.layers;
83
+ this.filters = params.filters;
80
84
  }
81
85
 
82
86
  buildWgs84BoundingBox(tms: TileMatrixSet, layers: Bounds[]): VNodeElement {
@@ -159,8 +163,9 @@ export class WmtsCapabilities {
159
163
  ];
160
164
  }
161
165
 
162
- buildTileUrl(tileSetId: string, suffix: string): string {
163
- const query = toQueryString({ api: this.apiKey, config: this.config });
166
+ buildTileUrl(tileSetId: string, suffix: string, addFilter = false): string {
167
+ let query = { api: this.apiKey, config: this.config };
168
+ if (addFilter) query = { api: this.apiKey, config: this.config, ...this.filters };
164
169
 
165
170
  return [
166
171
  this.httpBase,
@@ -170,15 +175,15 @@ export class WmtsCapabilities {
170
175
  '{TileMatrixSet}',
171
176
  '{TileMatrix}',
172
177
  '{TileCol}',
173
- `{TileRow}.${suffix}${query}`,
178
+ `{TileRow}.${suffix}${toQueryString(query)}`,
174
179
  ].join('/');
175
180
  }
176
181
 
177
- buildResourceUrl(tileSetId: string, suffix: string): VNodeElement {
182
+ buildResourceUrl(tileSetId: string, suffix: string, addFilter = false): VNodeElement {
178
183
  return V('ResourceURL', {
179
184
  format: 'image/' + suffix,
180
185
  resourceType: 'tile',
181
- template: this.buildTileUrl(tileSetId, suffix),
186
+ template: this.buildTileUrl(tileSetId, suffix, addFilter),
182
187
  });
183
188
  }
184
189
 
@@ -202,7 +207,7 @@ export class WmtsCapabilities {
202
207
  if (firstImg == null) return null;
203
208
 
204
209
  return V('Layer', [
205
- V('ows:Title', layer.title ?? layerNameId),
210
+ V('ows:Title', layer.title),
206
211
  V('ows:Abstract', ''),
207
212
  V('ows:Identifier', layerNameId),
208
213
  this.buildKeywords(firstImg),
@@ -241,7 +246,7 @@ export class WmtsCapabilities {
241
246
  }
242
247
 
243
248
  return V('Layer', [
244
- V('ows:Title', layer.title ?? layerNameId),
249
+ V('ows:Title', layer.title),
245
250
  V('ows:Abstract', layer.description ?? ''),
246
251
  V('ows:Identifier', layerNameId),
247
252
  this.buildKeywords(layer),
@@ -250,7 +255,7 @@ export class WmtsCapabilities {
250
255
  this.buildStyle(),
251
256
  ...this.formats.map((fmt) => V('Format', 'image/' + fmt)),
252
257
  ...matrixSetNodes,
253
- ...this.formats.map((fmt) => this.buildResourceUrl(layerNameId, fmt)),
258
+ ...this.formats.map((fmt) => this.buildResourceUrl(layerNameId, fmt, true)),
254
259
  ]);
255
260
  }
256
261
 
@@ -287,10 +292,10 @@ export class WmtsCapabilities {
287
292
  const layers: (VNodeElement | null)[] = [];
288
293
  layers.push(this.buildLayer(this.tileSet));
289
294
 
290
- if (this.isIndividualLayers) {
295
+ if (this.layers) {
291
296
  const layerByName = new Map<string, ConfigLayer>();
292
297
  // Dedupe the layers by unique name
293
- for (const img of this.tileSet.layers) layerByName.set(standardizeLayerName(img.name), img);
298
+ for (const img of this.layers) layerByName.set(standardizeLayerName(img.name), img);
294
299
  const orderedLayers = Array.from(layerByName.values()).sort((a, b) =>
295
300
  (a.title ?? a.name).localeCompare(b.title ?? b.name),
296
301
  );