• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

keplergl / kepler.gl / 12482165950

24 Dec 2024 01:12PM UTC coverage: 67.451% (-0.1%) from 67.586%
12482165950

push

github

web-flow
[chore] 3.1.0-alpha.2 release (#2855)

- bump version for release
- temporarily disable vector tile layer plumbing
- ts fixes required for npm-publish
- prepublish to prepublishOnly script

5810 of 10000 branches covered (58.1%)

Branch coverage included in aggregate %.

4 of 9 new or added lines in 7 files covered. (44.44%)

29 existing lines in 4 files now uncovered.

11954 of 16336 relevant lines covered (73.18%)

86.83 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

2.05
/src/layers/src/vector-tile/vector-tile-layer.ts
1
// SPDX-License-Identifier: MIT
2
// Copyright contributors to the kepler.gl project
3

4
import {FeatureCollection, Feature} from 'geojson';
5

6
import {Layer as DeckLayer} from '@deck.gl/core/typed';
7
import {_Tile2DHeader as Tile2DHeader} from '@deck.gl/geo-layers/typed';
8
import {GeoJsonLayer} from '@deck.gl/layers/typed';
9
import {MVTSource, MVTTileSource} from '@loaders.gl/mvt';
10
import {PMTilesSource, PMTilesTileSource} from '@loaders.gl/pmtiles';
11
import GL from '@luma.gl/constants';
12

13
import {notNullorUndefined} from '@kepler.gl/common-utils';
14
import {
15
  SCALE_TYPES,
16
  CHANNEL_SCALES,
17
  DEFAULT_COLOR_UI,
18
  LAYER_VIS_CONFIGS
19
} from '@kepler.gl/constants';
20
import {
21
  KeplerTable as KeplerDataset,
22
  Datasets as KeplerDatasets,
23
  GpuFilter
24
} from '@kepler.gl/table';
25
import {
26
  AnimationConfig,
27
  Field as KeplerField,
28
  LayerColorConfig,
29
  LayerHeightConfig,
30
  Merge,
31
  MapState,
32
  BindedLayerCallbacks,
33
  VisConfigRange,
34
  VisConfigNumber,
35
  DomainStops
36
} from '@kepler.gl/types';
37
import {DataContainerInterface} from '@kepler.gl/utils';
38

39
import {getTileUrl} from './utils/vector-tile-utils';
40
import {MVTLayer as CustomMVTLayer} from './mvt-layer';
41
import VectorTileIcon from './vector-tile-icon';
42
import {
43
  default as KeplerLayer,
44
  LayerBaseConfig,
45
  LayerBaseConfigPartial,
46
  VisualChannel,
47
  VisualChannelDomain,
48
  VisualChannelField
49
} from '../base-layer';
50
import {
51
  VectorTileMetadata,
52
  getLoaderOptions,
53
  DatasetType,
54
  TileType,
55
  VectorTileType,
56
  VectorTileDatasetMetadata
57
} from './utils/vector-tile-utils';
58

59
import AbstractTileLayer, {
60
  LayerData as CommonLayerData,
61
  commonTileVisConfigs,
62
  AbstractTileLayerConfig,
63
  AbstractTileLayerVisConfigSettings
64
} from './abstract-tile-layer';
65
import TileDataset from './common-tile/tile-dataset';
66
import {
67
  isDomainStops,
68
  isDomainQuantiles,
69
  isIndexedField,
70
  getPropertyByZoom
71
} from './common-tile/tile-utils';
72

73
export const DEFAULT_HIGHLIGHT_COLOR = [252, 242, 26, 150];
11✔
74
export const MAX_CACHE_SIZE_MOBILE = 1; // Minimize caching, visible tiles will always be loaded
11✔
75
export const DEFAULT_STROKE_WIDTH = 1;
11✔
76

77
/**
78
 * Type for transformRequest returned parameters.
79
 */
80
export type RequestParameters = {
81
  /** The URL to be requested. */
82
  url: string;
83
  /** Search parameters to be added onto the URL. */
84
  searchParams: URLSearchParams;
85
  /** Options passed to fetch. */
86
  options: RequestInit;
87
};
88

89
// This type *seems* to be what loaders.gl currently returns for tile content.
90
// Apparently this might be different depending on the loaders version, and for...
91
// reasons we use two different versions of loaders right now.
92
// TODO: The Features[] version should not be needed when we update to a newer
93
// version of Deck.gl and use only one version of loaders
94
type TileContent =
95
  | (FeatureCollection & {shape: 'geojson-table'})
96
  | (Feature[] & {shape: undefined});
97

98
type VectorTile = Tile2DHeader<TileContent>;
99

100
type LayerData = CommonLayerData & {
101
  tilesetDataUrl?: string | null;
102
  tileSource: MVTTileSource | PMTilesTileSource | null;
103
};
104

105
type VectorTileLayerRenderOptions = Merge<
106
  {
107
    idx: number;
108
    visible: boolean;
109
    mapState: MapState;
110
    data: any;
111
    animationConfig: AnimationConfig;
112
    gpuFilter: GpuFilter;
113
    layerCallbacks: BindedLayerCallbacks;
114
    objectHovered: {
115
      index: number;
116
      tile: VectorTile;
117
      sourceLayer: typeof GeoJsonLayer;
118
    };
119
  },
120
  LayerData
121
>;
122

123
export const vectorTileVisConfigs = {
11✔
124
  ...commonTileVisConfigs,
125

126
  stroked: {
127
    ...LAYER_VIS_CONFIGS.stroked,
128
    defaultValue: false
129
  },
130

131
  // TODO figure out why strokeColorScale can't be const
132
  strokeColorScale: 'strokeColorScale' as any,
133
  strokeColorRange: 'strokeColorRange' as const,
134

135
  sizeRange: 'strokeWidthRange' as const,
136
  strokeWidth: {
137
    ...LAYER_VIS_CONFIGS.thickness,
138
    property: 'strokeWidth',
139
    defaultValue: 0.5,
140
    allowCustomValue: false
141
  }
142
};
143

144
export type VectorTileLayerConfig = Merge<
145
  AbstractTileLayerConfig,
146
  {
147
    sizeField?: VisualChannelField;
148
    sizeScale?: string;
149
    sizeDomain?: VisualChannelDomain;
150
    strokeColorField: VisualChannelField;
151
  }
152
>;
153

154
export type VectorTileLayerVisConfigSettings = Merge<
155
  AbstractTileLayerVisConfigSettings,
156
  {
157
    sizeRange: VisConfigRange;
158
    strokeWidth: VisConfigNumber;
159
  }
160
>;
161

162
export default class VectorTileLayer extends AbstractTileLayer<VectorTile, Feature[]> {
163
  declare config: VectorTileLayerConfig;
164
  declare visConfigSettings: VectorTileLayerVisConfigSettings;
165

166
  constructor(props: ConstructorParameters<typeof AbstractTileLayer>[0]) {
UNCOV
167
    super(props);
×
UNCOV
168
    this.registerVisConfig(vectorTileVisConfigs);
×
UNCOV
169
    this.tileDataset = this.initTileDataset();
×
170
  }
171

UNCOV
172
  meta = {};
×
173

174
  static findDefaultLayerProps(dataset: KeplerDataset): {
175
    props: {dataId: string; label?: string; isVisible: boolean}[];
176
  } {
UNCOV
177
    if (dataset.type !== DatasetType.VECTOR_TILE) {
×
UNCOV
178
      return {props: []};
×
179
    }
180
    return super.findDefaultLayerProps(dataset);
×
181
  }
182

183
  initTileDataset(): TileDataset<VectorTile, Feature[]> {
UNCOV
184
    return new TileDataset({
×
185
      getTileId: (tile: VectorTile): string => tile.id,
×
186
      getIterable: (tile: VectorTile): Feature[] => {
187
        if (tile.content) {
×
188
          return tile.content.shape === 'geojson-table' ? tile.content.features : tile.content;
×
189
        }
190
        return [];
×
191
      },
192
      getRowCount: (features: Feature[]): number => features.length,
×
193
      getRowValue: this.accessRowValue
194
    });
195
  }
196

197
  get type() {
198
    return TileType.VECTOR_TILE;
×
199
  }
200

201
  get name(): string {
UNCOV
202
    return 'Vector Tile';
×
203
  }
204

205
  get layerIcon(): KeplerLayer['layerIcon'] {
UNCOV
206
    return VectorTileIcon;
×
207
  }
208

209
  get supportedDatasetTypes(): DatasetType[] {
210
    return [DatasetType.VECTOR_TILE];
×
211
  }
212

213
  get visualChannels(): Record<string, VisualChannel> {
214
    const visualChannels = super.visualChannels;
×
215
    return {
×
216
      ...visualChannels,
217
      strokeColor: {
218
        property: 'strokeColor',
219
        field: 'strokeColorField',
220
        scale: 'strokeColorScale',
221
        domain: 'strokeColorDomain',
222
        range: 'strokeColorRange',
223
        key: 'strokeColor',
224
        channelScaleType: CHANNEL_SCALES.color,
225
        accessor: 'getLineColor',
226
        condition: config => config.visConfig.stroked,
×
227
        nullValue: visualChannels.color.nullValue,
228
        getAttributeValue: config => config.visConfig.strokeColor || config.color
×
229
      },
230
      size: {
231
        property: 'stroke',
232
        field: 'sizeField',
233
        scale: 'sizeScale',
234
        domain: 'sizeDomain',
235
        range: 'sizeRange',
236
        key: 'size',
237
        channelScaleType: CHANNEL_SCALES.size,
238
        nullValue: 0,
239
        accessor: 'getLineWidth',
240
        condition: config => config.visConfig.stroked,
×
241
        getAttributeValue: config => config.visConfig.strokeWidth || DEFAULT_STROKE_WIDTH
×
242
      }
243
    };
244
  }
245

246
  getDefaultLayerConfig(
247
    props: LayerBaseConfigPartial
248
  ): LayerBaseConfig & Partial<LayerColorConfig & LayerHeightConfig> {
UNCOV
249
    const defaultLayerConfig = super.getDefaultLayerConfig(props);
×
UNCOV
250
    return {
×
251
      ...defaultLayerConfig,
252
      colorScale: SCALE_TYPES.quantize,
253

254
      strokeColorField: null,
255
      strokeColorDomain: [0, 1],
256
      strokeColorScale: SCALE_TYPES.quantile,
257
      colorUI: {
258
        ...defaultLayerConfig.colorUI,
259
        // @ts-expect-error LayerConfig
260
        strokeColorRange: DEFAULT_COLOR_UI
261
      }
262
    };
263
  }
264

265
  getHoverData(
266
    object: {properties?: Record<string, Record<string, unknown>>},
267
    dataContainer: DataContainerInterface,
268
    fields: KeplerField[]
269
  ): (Record<string, unknown> | null)[] {
270
    return fields.map(f => object.properties?.[f.name] ?? null);
×
271
  }
272

273
  calculateLayerDomain(
274
    dataset: KeplerDataset,
275
    visualChannel: VisualChannel
276
  ): DomainStops | number[] {
277
    const defaultDomain = [0, 1];
×
278

279
    const field = this.config[visualChannel.field];
×
280
    const scale = this.config[visualChannel.scale];
×
281
    if (!field) {
×
282
      // if colorField or sizeField were set back to null
283
      return defaultDomain;
×
284
    }
285
    if (scale === SCALE_TYPES.quantile && isDomainQuantiles(field?.filterProps?.domainQuantiles)) {
×
286
      return field.filterProps.domainQuantiles;
×
287
    }
288
    if (isDomainStops(field?.filterProps?.domainStops)) {
×
289
      return field.filterProps.domainStops;
×
290
    } else if (Array.isArray(field?.filterProps?.domain)) {
×
291
      return field.filterProps.domain;
×
292
    }
293

294
    return defaultDomain;
×
295
  }
296

297
  getScaleOptions(channelKey: string): string[] {
298
    let options = KeplerLayer.prototype.getScaleOptions.call(this, channelKey);
×
299

300
    const channel = this.visualChannels.strokeColor;
×
301
    const field = this.config[channel.field];
×
302
    if (
×
303
      !(
304
        isDomainQuantiles(field?.filterProps?.domainQuantiles) ||
×
305
        this.config.visConfig.dynamicColor ||
306
        // If we've set the scale to quantile, we need to include it - there's a loading
307
        // period in which the visConfig isn't set yet, but if we don't return the right
308
        // scale type we lose it
309
        this.config.colorScale === SCALE_TYPES.quantile
310
      )
311
    ) {
312
      options = options.filter(scale => scale !== SCALE_TYPES.quantile);
×
313
    }
314

315
    return options;
×
316
  }
317

318
  accessRowValue(
319
    field?: KeplerField,
320
    indexKey?: number | null
321
  ): (field: KeplerField, datum: Feature) => number | null {
322
    // if is indexed field
323
    if (isIndexedField(field) && indexKey !== null) {
×
324
      const fieldName = indexKey && field?.indexBy?.mappedValue[indexKey];
×
325
      if (fieldName) {
×
326
        return (f, datum) => {
×
327
          if (datum.properties) {
×
328
            return datum.properties[fieldName];
×
329
          }
330
          // TODO debug this with indexed tiled dataset
331
          return datum[fieldName];
×
332
        };
333
      }
334
    }
335

336
    // default
337
    return (f, datum) => {
×
338
      if (f && datum.properties) {
×
339
        return datum.properties[f.name];
×
340
      }
341
      // support picking & highlighting
342
      return f ? datum[f.fieldIdx] : null;
×
343
    };
344
  }
345

346
  updateLayerMeta(dataset: KeplerDataset, datasets: KeplerDatasets): void {
347
    if (dataset.type !== DatasetType.VECTOR_TILE) {
×
348
      return;
×
349
    }
350

351
    const datasetMeta = dataset.metadata as VectorTileMetadata & VectorTileDatasetMetadata;
×
352
    this.updateMeta({
×
353
      datasetId: dataset.id,
354
      datasets,
355
      bounds: datasetMeta.bounds
356
    });
357
  }
358

359
  formatLayerData(
360
    datasets: KeplerDatasets,
361
    oldLayerData: unknown,
362
    animationConfig: AnimationConfig
363
  ): LayerData {
364
    const {dataId} = this.config;
×
365
    if (!notNullorUndefined(dataId)) {
×
366
      return {tileSource: null};
×
367
    }
368
    const dataset = datasets[dataId];
×
369

370
    let tilesetDataUrl: string | undefined;
371
    let tileSource: LayerData['tileSource'] = null;
×
372

373
    if (dataset?.type === DatasetType.VECTOR_TILE) {
×
374
      const datasetMetadata = dataset.metadata as VectorTileMetadata & VectorTileDatasetMetadata;
×
375
      if (datasetMetadata?.type === VectorTileType.MVT) {
×
376
        const transformFetch = async (input: RequestInfo | URL, init?: RequestInit | undefined) => {
×
377
          const requestData: RequestParameters = {
×
378
            url: input as string,
379
            searchParams: new URLSearchParams(),
380
            options: init ?? {}
×
381
          };
382

383
          return fetch(requestData.url, requestData.options);
×
384
        };
385

386
        tilesetDataUrl = datasetMetadata?.tilesetDataUrl;
×
387
        tileSource = tilesetDataUrl
×
388
          ? MVTSource.createDataSource(decodeURIComponent(tilesetDataUrl), {
389
              mvt: {
390
                metadataUrl: datasetMetadata?.tilesetMetadataUrl ?? null,
×
391
                loadOptions: {
392
                  fetch: transformFetch
393
                }
394
              }
395
            })
396
          : null;
397
      } else if (datasetMetadata?.type === VectorTileType.PMTILES) {
×
398
        // TODO: to render image pmtiles need to use TileLayer and BitmapLayer (https://github.com/visgl/loaders.gl/blob/master/examples/website/tiles/components/tile-source-layer.ts)
399
        tilesetDataUrl = datasetMetadata?.tilesetDataUrl;
×
400
        tileSource = tilesetDataUrl ? PMTilesSource.createDataSource(tilesetDataUrl, {}) : null;
×
401
      }
402
    }
403

404
    return {
×
405
      ...super.formatLayerData(datasets, oldLayerData, animationConfig),
406
      tilesetDataUrl: typeof tilesetDataUrl === 'string' ? getTileUrl(tilesetDataUrl) : null,
×
407
      tileSource
408
    };
409
  }
410

411
  hasHoveredObject(objectInfo) {
412
    if (super.hasHoveredObject(objectInfo)) {
×
413
      const features = objectInfo?.tile?.content?.features;
×
414
      return features[objectInfo.index];
×
415
    }
416
    return null;
×
417
  }
418

419
  renderSubLayers(props: Record<string, any>): DeckLayer | DeckLayer[] {
420
    let {data} = props;
×
421

422
    data = data?.shape === 'geojson-table' ? data.features : data;
×
423
    if (!data?.length) {
×
424
      return [];
×
425
    }
426

427
    const tile: Tile2DHeader = props.tile;
×
428
    const zoom = tile.index.z;
×
429

430
    return new GeoJsonLayer({
×
431
      ...props,
432
      data,
433
      getFillColor: props.getFillColorByZoom ? props.getFillColor(zoom) : props.getFillColor,
×
434
      getElevation: props.getElevationByZoom ? props.getElevation(zoom) : props.getElevation,
×
435
      pointRadiusScale: props.getPointRadiusScaleByZoom(zoom),
436
      // For some reason tile Layer reset autoHighlight to false
437
      pickable: true,
438
      autoHighlight: true,
439
      stroked: props.stroked,
440
      // wrapLongitude: true causes missing side polygon when extrude is enabled
441
      wrapLongitude: false
442
    });
443
  }
444

445
  // generate a deck layer
446
  renderLayer(opts: VectorTileLayerRenderOptions): DeckLayer[] {
447
    const {mapState, data, animationConfig, gpuFilter, objectHovered, layerCallbacks} = opts;
×
448
    const {animation, visConfig} = this.config;
×
449

450
    this.setLayerDomain = layerCallbacks.onSetLayerDomain;
×
451

452
    const defaultLayerProps = this.getDefaultDeckLayerProps(opts);
×
453
    const eleZoomFactor = this.getElevationZoomFactor(mapState);
×
454

455
    const transitions = this.config.visConfig.transition
×
456
      ? {
457
          getFillColor: {
458
            duration: animationConfig.duration
459
          },
460
          getElevation: {
461
            duration: animationConfig.duration
462
          }
463
        }
464
      : undefined;
465

466
    const colorField = this.config.colorField as KeplerField;
×
467
    const heightField = this.config.heightField as KeplerField;
×
468
    const strokeColorField = this.config.strokeColorField as KeplerField;
×
469
    const sizeField = this.config.sizeField as KeplerField;
×
470

471
    if (data.tileSource) {
×
472
      const hoveredObject = this.hasHoveredObject(objectHovered);
×
473

474
      const layers = [
×
475
        new CustomMVTLayer({
476
          ...defaultLayerProps,
477
          ...data,
478
          onViewportLoad: this.onViewportLoad,
479
          data: data.tilesetDataUrl,
480
          getTileData: data.tileSource?.getTileData,
481
          tileSource: data.tileSource,
482
          getFilterValue: this.getGpuFilterValueAccessor(opts),
483
          filterRange: gpuFilter.filterRange,
484
          lineWidthUnits: 'pixels',
485
          binary: false,
486
          elevationScale: visConfig.elevationScale * eleZoomFactor,
487
          extruded: visConfig.enable3d,
488
          stroked: visConfig.stroked,
489

490
          // TODO: this is hard coded, design a UI to allow user assigned unique property id
491
          // uniqueIdProperty: 'ufid',
492
          renderSubLayers: this.renderSubLayers,
493
          // when radiusUnits is meter
494
          getPointRadiusScaleByZoom: getPropertyByZoom(visConfig.radiusByZoom, visConfig.radius),
495
          pointRadiusScale: 1,
496

497
          pointRadiusMinPixels: 1,
498
          autoHighlight: true,
499
          highlightColor: DEFAULT_HIGHLIGHT_COLOR,
500
          pickable: true,
501
          transitions,
502
          updateTriggers: {
503
            getFilterValue: {
504
              ...gpuFilter.filterValueUpdateTriggers,
505
              currentTime: animation.enabled ? animationConfig.currentTime : null
×
506
            },
507
            getFillColor: {
508
              color: this.config.color,
509
              colorField: this.config.colorField,
510
              colorScale: this.config.colorScale,
511
              colorDomain: this.config.colorDomain,
512
              colorRange: visConfig.colorRange,
513
              currentTime: isIndexedField(colorField) ? animationConfig.currentTime : null
×
514
            },
515
            getElevation: {
516
              heightField: this.config.heightField,
517
              heightScaleType: this.config.heightScale,
518
              heightRange: visConfig.heightRange,
519
              currentTime: isIndexedField(heightField) ? animationConfig.currentTime : null
×
520
            },
521
            getLineColor: {
522
              strokeColor: visConfig.strokeColor,
523
              strokeColorField: this.config.strokeColorField,
524
              // @ts-expect-error prop not in LayerConfig
525
              strokeColorScale: this.config.strokeColorScale,
526
              // @ts-expect-error prop not in LayerConfig
527
              strokeColorDomain: this.config.strokeColorDomain,
528
              // FIXME: Strip out empty arrays from individual color map steps, and replace with `null`, otherwise the layer may show the incorrect color.
529
              // So far it seems that it uses the previous color chosen in the palette rather than the currently chosen color for the specific custom ordinal value when there are "sparse" color maps.
530
              // In other words, a color map with "holes" of colors with unassigned field values, which may have been assigned in the past.
531
              // For example "abc" was green, stored as `["abc"]`. Then "abc" was reassigned to the red color map step, stored as `["abc"]`. Now the green color map step's stored value is `[]`, and the layer will incorrectly still render "abc" in green.
532
              // Quick patch example:
533
              // strokeColorRange: visConfig?.strokeColorRange?.colorMap?.map(cm =>
534
              //   cm[0]?.length === 0 ? [null, cm[1]] : cm
535
              // ),
536
              // Note: for regular scales the colorMap in the above patch is undefined and breaks strokeColorRange update trigger.
537
              strokeColorRange: visConfig.strokeColorRange,
538
              currentTime: isIndexedField(strokeColorField) ? animationConfig.currentTime : null
×
539
            },
540
            getLineWidth: {
541
              sizeRange: visConfig.sizeRange,
542
              strokeWidth: visConfig.strokeWidth,
543
              sizeField: this.config.sizeField,
544
              sizeScale: this.config.sizeScale,
545
              sizeDomain: this.config.sizeDomain,
546
              currentTime: isIndexedField(sizeField) ? animationConfig.currentTime : null
×
547
            }
548
          },
549
          _subLayerProps: {
550
            'polygons-stroke': {opacity: visConfig.strokeOpacity},
551
            'polygons-fill': {
552
              parameters: {
553
                cullFace: GL.BACK
554
              }
555
            }
556
          },
557
          loadOptions: {
558
            mvt: getLoaderOptions().mvt
559
          }
560
        }),
561
        // hover layer
562
        ...(hoveredObject
×
563
          ? [
564
              new GeoJsonLayer({
565
                // @ts-expect-error not typed
566
                ...objectHovered.sourceLayer?.props,
567
                ...(this.getDefaultHoverLayerProps() as any),
568
                visible: true,
569
                wrapLongitude: false,
570
                data: [hoveredObject],
571
                getLineColor: this.config.highlightColor,
572
                getFillColor: this.config.highlightColor,
573
                // always draw outline
574
                stroked: true,
575
                filled: false
576
              })
577
            ]
578
          : [])
579
      ];
580

581
      return layers;
×
582
    }
583
    return [];
×
584
  }
585
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc