@loaders.gl/mvt 4.0.0 → 4.0.1

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.
@@ -1,21 +1,42 @@
1
1
  // loaders.gl, MIT license
2
2
  // Copyright (c) vis.gl contributors
3
3
 
4
+ export type TileJSONOptions = {
5
+ maxValues?: number | false;
6
+ };
7
+
4
8
  /** Parsed and typed TileJSON, merges Tilestats information if present */
5
9
  export type TileJSON = {
6
10
  name?: string;
7
11
  description?: string;
8
12
  version?: string;
13
+
14
+ tileFormat?: string;
15
+ tilesetType?: string;
16
+
17
+ /** Generating application. Tippecanoe adds this. */
18
+ generator?: string;
19
+ /** Generating application options. Tippecanoe adds this. */
20
+ generatorOptions?: string;
21
+
22
+ /** Tile indexing scheme */
9
23
  scheme?: 'xyz' | 'tms';
24
+ /** Sharded URLs */
10
25
  tiles?: string[];
11
26
  /** `[[w, s], [e, n]]`, indicates the limits of the bounding box using the axis units and order of the specified CRS. */
12
27
  boundingBox?: [min: [w: number, s: number], max: [e: number, n: number]];
13
- center: number[] | null;
14
- maxZoom: number | null;
15
- minZoom: number | null;
28
+ /** May be set to the maxZoom of the first layer */
29
+ maxZoom?: number | null;
30
+ /** May be set to the minZoom of the first layer */
31
+ minZoom?: number | null;
32
+ center?: number[] | null;
16
33
  htmlAttribution?: string;
17
34
  htmlLegend?: string;
35
+
36
+ // Combination of tilestats (if present) and tilejson layer information
18
37
  layers?: TileJSONLayer[];
38
+
39
+ /** Any nested JSON metadata */
19
40
  metaJson?: any | null;
20
41
  };
21
42
 
@@ -52,6 +73,8 @@ export type TileJSONField = {
52
73
  min?: number;
53
74
  /** max value (if there are *any* numbers in the values) */
54
75
  max?: number;
76
+ /** Number of unique values across the tileset */
77
+ uniqueValueCount?: number;
55
78
  /** An array of this attribute's first 100 unique values */
56
79
  values?: unknown[];
57
80
  };
@@ -93,50 +116,77 @@ type TilestatsLayerAttribute = {
93
116
  min?: number;
94
117
  /** max value (if there are *any* numbers in the values) */
95
118
  max?: number;
119
+ /** Number of unique values */
120
+ count?: number;
121
+ /** First 100 values */
122
+ values?: unknown[];
96
123
  };
97
124
 
98
125
  const isObject: (x: unknown) => boolean = (x) => x !== null && typeof x === 'object';
99
126
 
100
- export function parseTileJSON(jsonMetadata: any): TileJSON | null {
127
+ export function parseTileJSON(jsonMetadata: any, options: TileJSONOptions): TileJSON | null {
101
128
  if (!jsonMetadata || !isObject(jsonMetadata)) {
102
129
  return null;
103
130
  }
104
131
 
105
- const boundingBox = parseBounds(jsonMetadata.bounds);
106
- const center = parseCenter(jsonMetadata.center);
107
- const maxZoom = safeParseFloat(jsonMetadata.maxzoom);
108
- const minZoom = safeParseFloat(jsonMetadata.minzoom);
109
-
110
132
  let tileJSON: TileJSON = {
111
133
  name: jsonMetadata.name || '',
112
- description: jsonMetadata.description || '',
113
- boundingBox,
114
- center,
115
- maxZoom,
116
- minZoom,
117
- layers: []
134
+ description: jsonMetadata.description || ''
118
135
  };
119
136
 
120
- // try to parse json
137
+ // tippecanoe
138
+
139
+ if (typeof jsonMetadata.generator === 'string') {
140
+ tileJSON.generator = jsonMetadata.generator;
141
+ }
142
+ if (typeof jsonMetadata.generator_options === 'string') {
143
+ tileJSON.generatorOptions = jsonMetadata.generator_options;
144
+ }
145
+
146
+ // Tippecanoe emits `antimeridian_adjusted_bounds` instead of `bounds`
147
+ tileJSON.boundingBox =
148
+ parseBounds(jsonMetadata.bounds) || parseBounds(jsonMetadata.antimeridian_adjusted_bounds);
149
+
150
+ // TODO - can be undefined - we could set to center of bounds...
151
+ tileJSON.center = parseCenter(jsonMetadata.center);
152
+ // TODO - can be undefined, we could extract from layers...
153
+ tileJSON.maxZoom = safeParseFloat(jsonMetadata.maxzoom);
154
+ // TODO - can be undefined, we could extract from layers...
155
+ tileJSON.minZoom = safeParseFloat(jsonMetadata.minzoom);
156
+
157
+ // Look for nested metadata embedded in .json field
158
+ // TODO - document what source this applies to, when is this needed?
121
159
  if (typeof jsonMetadata?.json === 'string') {
160
+ // try to parse json
122
161
  try {
123
162
  tileJSON.metaJson = JSON.parse(jsonMetadata.json);
124
- } catch (err) {
163
+ } catch (error) {
164
+ console.warn('Failed to parse tilejson.json field', error);
125
165
  // do nothing
126
166
  }
127
167
  }
128
168
 
129
- let layers = parseTilestatsLayers(tileJSON.metaJson?.tilestats);
169
+ // Look for fields in tilestats
170
+
171
+ const tilestats = jsonMetadata.tilestats || tileJSON.metaJson?.tilestats;
172
+ const tileStatsLayers = parseTilestatsLayers(tilestats, options);
173
+ const tileJSONlayers = parseTileJSONLayers(jsonMetadata.vector_layers); // eslint-disable-line camelcase
130
174
  // TODO - merge in description from tilejson
131
- if (layers.length === 0) {
132
- layers = parseTileJSONLayers(jsonMetadata.vector_layers); // eslint-disable-line camelcase
133
- }
175
+ const layers = mergeLayers(tileJSONlayers, tileStatsLayers);
134
176
 
135
177
  tileJSON = {
136
178
  ...tileJSON,
137
179
  layers
138
180
  };
139
181
 
182
+ if (tileJSON.maxZoom === null && layers.length > 0) {
183
+ tileJSON.maxZoom = layers[0].maxZoom || null;
184
+ }
185
+
186
+ if (tileJSON.minZoom === null && layers.length > 0) {
187
+ tileJSON.minZoom = layers[0].minZoom || null;
188
+ }
189
+
140
190
  return tileJSON;
141
191
  }
142
192
 
@@ -145,41 +195,52 @@ function parseTileJSONLayers(layers: any[]): TileJSONLayer[] {
145
195
  if (!Array.isArray(layers)) {
146
196
  return [];
147
197
  }
148
- return layers.map((layer) => ({
149
- name: layer.id || '',
150
- fields: Object.entries(layer.fields || []).map(([key, datatype]) => ({
151
- name: key,
152
- ...attributeTypeToFieldType(String(datatype))
153
- }))
198
+ return layers.map((layer) => parseTileJSONLayer(layer));
199
+ }
200
+
201
+ function parseTileJSONLayer(layer: any): TileJSONLayer {
202
+ const fields = Object.entries(layer.fields || []).map(([key, datatype]) => ({
203
+ name: key,
204
+ ...attributeTypeToFieldType(String(datatype))
154
205
  }));
206
+ const layer2 = {...layer};
207
+ delete layer2.fields;
208
+ return {
209
+ name: layer.id || '',
210
+ ...layer2,
211
+ fields
212
+ };
155
213
  }
156
214
 
157
215
  /** parse Layers array from tilestats */
158
- function parseTilestatsLayers(tilestats: any): TileJSONLayer[] {
216
+ function parseTilestatsLayers(tilestats: any, options: TileJSONOptions): TileJSONLayer[] {
159
217
  if (isObject(tilestats) && Array.isArray(tilestats.layers)) {
160
218
  // we are in luck!
161
- return tilestats.layers.map((layer) => parseTilestatsForLayer(layer));
219
+ return tilestats.layers.map((layer) => parseTilestatsForLayer(layer, options));
162
220
  }
163
221
  return [];
164
222
  }
165
223
 
166
- function parseTilestatsForLayer(layer: TilestatsLayer): TileJSONLayer {
224
+ function parseTilestatsForLayer(layer: TilestatsLayer, options: TileJSONOptions): TileJSONLayer {
167
225
  const fields: TileJSONField[] = [];
168
226
  const indexedAttributes: {[key: string]: TilestatsLayerAttribute[]} = {};
169
227
 
170
228
  const attributes = layer.attributes || [];
171
- for (const attr of attributes) {
172
- const name = attr.attribute;
229
+ for (const attribute of attributes) {
230
+ const name = attribute.attribute;
173
231
  if (typeof name === 'string') {
232
+ // TODO - code copied from kepler.gl, need sample tilestats files to test
174
233
  if (name.split('|').length > 1) {
175
234
  // indexed field
176
235
  const fname = name.split('|')[0];
177
236
  indexedAttributes[fname] = indexedAttributes[fname] || [];
178
- indexedAttributes[fname].push(attr);
237
+ indexedAttributes[fname].push(attribute);
238
+ // eslint-disable-next-line no-console
239
+ console.warn('ignoring tilestats indexed field', fname);
179
240
  } else if (!fields[name]) {
180
- fields[name] = attributeToField(attr);
241
+ fields.push(attributeToField(attribute, options));
181
242
  } else {
182
- // return (fields[name], attr);
243
+ // return (fields[name], attribute);
183
244
  }
184
245
  }
185
246
  }
@@ -190,6 +251,21 @@ function parseTilestatsForLayer(layer: TilestatsLayer): TileJSONLayer {
190
251
  };
191
252
  }
192
253
 
254
+ function mergeLayers(layers: TileJSONLayer[], tilestatsLayers: TileJSONLayer[]): TileJSONLayer[] {
255
+ return layers.map((layer) => {
256
+ const tilestatsLayer = tilestatsLayers.find((tsLayer) => tsLayer.name === layer.name);
257
+ // For aesthetics in JSON dumps, we preserve field order (make sure layers is last)
258
+ const fields = tilestatsLayer?.fields || [];
259
+ const layer2: Partial<TileJSONLayer> = {...layer};
260
+ delete layer2.fields;
261
+ return {
262
+ ...layer2,
263
+ ...tilestatsLayer,
264
+ fields
265
+ } as TileJSONLayer;
266
+ });
267
+ }
268
+
193
269
  /**
194
270
  * bounds should be [minLng, minLat, maxLng, maxLat]
195
271
  *`[[w, s], [e, n]]`, indicates the limits of the bounding box using the axis units and order of the specified CRS.
@@ -289,19 +365,38 @@ const attrTypeMap = {
289
365
  }
290
366
  };
291
367
 
292
- function attributeToField(attribute: TilestatsLayerAttribute = {}): TileJSONField {
293
- // attribute: "_season_peaks_color"
294
- // count: 1000
295
- // max: 0.95
296
- // min: 0.24375
297
- // type: "number"
368
+ function attributeToField(
369
+ attribute: TilestatsLayerAttribute = {},
370
+ options: TileJSONOptions
371
+ ): TileJSONField {
298
372
  const fieldTypes = attributeTypeToFieldType(attribute.type!);
299
- return {
373
+ const field: TileJSONField = {
300
374
  name: attribute.attribute as string,
301
375
  // what happens if attribute type is string...
302
376
  // filterProps: getFilterProps(fieldTypes.type, attribute),
303
377
  ...fieldTypes
304
378
  };
379
+
380
+ // attribute: "_season_peaks_color"
381
+ // count: 1000
382
+ // max: 0.95
383
+ // min: 0.24375
384
+ // type: "number"
385
+
386
+ if (typeof attribute.min === 'number') {
387
+ field.min = attribute.min;
388
+ }
389
+ if (typeof attribute.max === 'number') {
390
+ field.max = attribute.max;
391
+ }
392
+ if (typeof attribute.count === 'number') {
393
+ field.uniqueValueCount = attribute.count;
394
+ }
395
+ if (options.maxValues !== false && attribute.values) {
396
+ // Too much data? Add option?
397
+ field.values = attribute.values?.slice(0, options.maxValues);
398
+ }
399
+ return field;
305
400
  }
306
401
 
307
402
  function attributeTypeToFieldType(aType: string): {type: string} {
package/src/mvt-source.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  import type {GetTileParameters, ImageType, DataSourceProps} from '@loaders.gl/loader-utils';
5
5
  import type {ImageTileSource, VectorTileSource} from '@loaders.gl/loader-utils';
6
6
  import {DataSource, resolvePath} from '@loaders.gl/loader-utils';
7
- import {ImageLoader} from '@loaders.gl/images';
7
+ import {ImageLoader, getBinaryImageMetadata} from '@loaders.gl/images';
8
8
  import {MVTLoader, MVTLoaderOptions, TileJSONLoader, TileJSON} from '@loaders.gl/mvt';
9
9
 
10
10
  import {TileLoadParameters} from '@loaders.gl/loader-utils';
@@ -21,13 +21,17 @@ export type MVTSourceProps = DataSourceProps & {
21
21
  export class MVTSource extends DataSource implements ImageTileSource, VectorTileSource {
22
22
  props: MVTSourceProps;
23
23
  url: string;
24
+ data: string;
24
25
  schema: 'tms' | 'xyz' = 'tms';
25
26
  metadata: Promise<TileJSON | null>;
27
+ extension = '.png';
28
+ mimeType: string | null = null;
26
29
 
27
30
  constructor(props: MVTSourceProps) {
28
31
  super(props);
29
32
  this.props = props;
30
33
  this.url = resolvePath(props.url);
34
+ this.data = this.url;
31
35
  this.getTileData = this.getTileData.bind(this);
32
36
  this.metadata = this.getMetadata();
33
37
  }
@@ -35,16 +39,31 @@ export class MVTSource extends DataSource implements ImageTileSource, VectorTile
35
39
  // @ts-ignore - Metadata type misalignment
36
40
  async getMetadata(): Promise<TileJSON | null> {
37
41
  const metadataUrl = this.getMetadataUrl();
38
- const response = await this.fetch(metadataUrl);
42
+ let response: Response;
43
+ try {
44
+ // Annoyingly, fetch throws on CORS errors which is common when requesting an unavailable resource
45
+ response = await this.fetch(metadataUrl);
46
+ } catch (error: unknown) {
47
+ console.error((error as TypeError).message);
48
+ return null;
49
+ }
39
50
  if (!response.ok) {
51
+ console.error(response.statusText);
40
52
  return null;
41
53
  }
42
54
  const tileJSON = await response.text();
43
55
  const metadata = TileJSONLoader.parseTextSync?.(JSON.stringify(tileJSON)) || null;
44
56
  // metadata.attributions = [...this.props.attributions, ...(metadata.attributions || [])];
57
+ // if (metadata?.mimeType) {
58
+ // this.mimeType = metadata?.tileMIMEType;
59
+ // }
45
60
  return metadata;
46
61
  }
47
62
 
63
+ getTileMIMEType(): string | null {
64
+ return this.mimeType;
65
+ }
66
+
48
67
  async getTile(tileParams: GetTileParameters): Promise<ArrayBuffer | null> {
49
68
  const {x, y, zoom: z} = tileParams;
50
69
  const tileUrl = this.getTileURL(x, y, z);
@@ -61,27 +80,48 @@ export class MVTSource extends DataSource implements ImageTileSource, VectorTile
61
80
 
62
81
  async getTileData(tileParams: TileLoadParameters): Promise<unknown | null> {
63
82
  const {x, y, z} = tileParams.index;
64
- const metadata = await this.metadata;
65
- // @ts-expect-error
66
- switch (metadata.mimeType || 'application/vnd.mapbox-vector-tile') {
83
+ // const metadata = await this.metadata;
84
+ // mimeType = metadata?.tileMIMEType || 'application/vnd.mapbox-vector-tile';
85
+
86
+ const arrayBuffer = await this.getTile({x, y, zoom: z, layers: []});
87
+ if (arrayBuffer === null) {
88
+ return null;
89
+ }
90
+
91
+ const imageMetadata = getBinaryImageMetadata(arrayBuffer);
92
+ this.mimeType =
93
+ this.mimeType || imageMetadata?.mimeType || 'application/vnd.mapbox-vector-tile';
94
+ switch (this.mimeType) {
67
95
  case 'application/vnd.mapbox-vector-tile':
68
- return await this.getVectorTile({x, y, zoom: z, layers: []});
96
+ return await this.parseVectorTile(arrayBuffer, {x, y, zoom: z, layers: []});
69
97
  default:
70
- return await this.getImageTile({x, y, zoom: z, layers: []});
98
+ return await this.parseImageTile(arrayBuffer);
71
99
  }
72
100
  }
101
+ x;
73
102
 
74
103
  // ImageTileSource interface implementation
75
104
 
76
105
  async getImageTile(tileParams: GetTileParameters): Promise<ImageType | null> {
77
106
  const arrayBuffer = await this.getTile(tileParams);
78
- return arrayBuffer ? await ImageLoader.parse(arrayBuffer, this.loadOptions) : null;
107
+ return arrayBuffer ? this.parseImageTile(arrayBuffer) : null;
108
+ }
109
+
110
+ protected async parseImageTile(arrayBuffer: ArrayBuffer): Promise<ImageType> {
111
+ return await ImageLoader.parse(arrayBuffer, this.loadOptions);
79
112
  }
80
113
 
81
114
  // VectorTileSource interface implementation
82
115
 
83
116
  async getVectorTile(tileParams: GetTileParameters): Promise<unknown | null> {
84
117
  const arrayBuffer = await this.getTile(tileParams);
118
+ return arrayBuffer ? this.parseVectorTile(arrayBuffer, tileParams) : null;
119
+ }
120
+
121
+ protected async parseVectorTile(
122
+ arrayBuffer: ArrayBuffer,
123
+ tileParams: GetTileParameters
124
+ ): Promise<unknown | null> {
85
125
  const loadOptions: MVTLoaderOptions = {
86
126
  shape: 'geojson-table',
87
127
  mvt: {
@@ -92,7 +132,7 @@ export class MVTSource extends DataSource implements ImageTileSource, VectorTile
92
132
  ...this.loadOptions
93
133
  };
94
134
 
95
- return arrayBuffer ? await MVTLoader.parse(arrayBuffer, loadOptions) : null;
135
+ return await MVTLoader.parse(arrayBuffer, loadOptions);
96
136
  }
97
137
 
98
138
  getMetadataUrl(): string {
@@ -102,10 +142,10 @@ export class MVTSource extends DataSource implements ImageTileSource, VectorTile
102
142
  getTileURL(x: number, y: number, z: number) {
103
143
  switch (this.schema) {
104
144
  case 'xyz':
105
- return `${this.url}/${x}/${y}/${z}`;
145
+ return `${this.url}/${x}/${y}/${z}${this.extension}`;
106
146
  case 'tms':
107
147
  default:
108
- return `${this.url}/${z}/${x}/${y}`;
148
+ return `${this.url}/${z}/${x}/${y}${this.extension}`;
109
149
  }
110
150
  }
111
151
  }
@@ -10,7 +10,9 @@ import {parseTileJSON} from './lib/parse-tilejson';
10
10
  const VERSION = typeof __VERSION__ !== 'undefined' ? __VERSION__ : 'latest';
11
11
 
12
12
  export type TileJSONLoaderOptions = LoaderOptions & {
13
- tilejson?: {};
13
+ tilejson?: {
14
+ maxValues?: number | false;
15
+ };
14
16
  };
15
17
 
16
18
  /**
@@ -26,15 +28,19 @@ export const TileJSONLoader: LoaderWithParser<TileJSON, never, TileJSONLoaderOpt
26
28
  mimeTypes: ['application/json'],
27
29
  text: true,
28
30
  options: {
29
- tilejson: {}
31
+ tilejson: {
32
+ maxValues: 10
33
+ }
30
34
  },
31
- parse: async (arrayBuffer, options) => {
35
+ parse: async (arrayBuffer, options?: TileJSONLoaderOptions) => {
32
36
  const jsonString = new TextDecoder().decode(arrayBuffer);
33
37
  const json = JSON.parse(jsonString);
34
- return parseTileJSON(json) as TileJSON;
38
+ const tilejsonOptions = {...TileJSONLoader.options.tilejson, ...options?.tilejson};
39
+ return parseTileJSON(json, tilejsonOptions) as TileJSON;
35
40
  },
36
41
  parseTextSync: (text, options) => {
37
42
  const json = JSON.parse(text);
38
- return parseTileJSON(json) as TileJSON;
43
+ const tilejsonOptions = {...TileJSONLoader.options.tilejson, ...options?.tilejson};
44
+ return parseTileJSON(json, tilejsonOptions) as TileJSON;
39
45
  }
40
46
  };