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

keplergl / kepler.gl / 13707252193

06 Mar 2025 08:09PM UTC coverage: 66.122% (-0.2%) from 66.286%
13707252193

Pull #3015

github

web-flow
Merge 20d148ea6 into 7107e4177
Pull Request #3015: [fix] fix for potential freeze during add data

6051 of 10678 branches covered (56.67%)

Branch coverage included in aggregate %.

11 of 12 new or added lines in 2 files covered. (91.67%)

290 existing lines in 7 files now uncovered.

12415 of 17249 relevant lines covered (71.98%)

87.97 hits per line

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

7.87
/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, PathLayer} 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
  DatasetType,
16
  LAYER_TYPES,
17
  RemoteTileFormat,
18
  VectorTileDatasetMetadata,
19
  SCALE_TYPES,
20
  CHANNEL_SCALES,
21
  DEFAULT_COLOR_UI,
22
  LAYER_VIS_CONFIGS
23
} from '@kepler.gl/constants';
24
import {
25
  getTileUrl,
26
  getLoaderOptions,
27
  KeplerTable as KeplerDataset,
28
  Datasets as KeplerDatasets,
29
  GpuFilter,
30
  VectorTileMetadata
31
} from '@kepler.gl/table';
32
import {
33
  AnimationConfig,
34
  Field as KeplerField,
35
  LayerColorConfig,
36
  LayerHeightConfig,
37
  Merge,
38
  MapState,
39
  BindedLayerCallbacks,
40
  VisConfigRange,
41
  VisConfigNumber,
42
  DomainStops
43
} from '@kepler.gl/types';
44
import {DataContainerInterface} from '@kepler.gl/utils';
45

46
import {MVTLayer as CustomMVTLayer} from './mvt-layer';
47
import VectorTileIcon from './vector-tile-icon';
48
import {
49
  default as KeplerLayer,
50
  LayerBaseConfig,
51
  LayerBaseConfigPartial,
52
  VisualChannel,
53
  VisualChannelDomain,
54
  VisualChannelField
55
} from '../base-layer';
56
import {FindDefaultLayerPropsReturnValue} from '../layer-utils';
57

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

72
export const DEFAULT_HIGHLIGHT_FILL_COLOR = [252, 242, 26, 150];
13✔
73
export const DEFAULT_HIGHLIGHT_STROKE_COLOR = [252, 242, 26, 255];
13✔
74
export const MAX_CACHE_SIZE_MOBILE = 1; // Minimize caching, visible tiles will always be loaded
13✔
75
export const DEFAULT_STROKE_WIDTH = 1;
13✔
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 = {
13✔
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
  radiusScale: 'radiusScale' as any,
144
  radiusRange: {
145
    ...LAYER_VIS_CONFIGS.radiusRange,
146
    type: 'number',
147
    defaultValue: [0, 1],
148
    isRanged: true,
149
    range: [0, 1],
150
    step: 0.01
151
  } as VisConfigRange
152
};
153

154
export type VectorTileLayerConfig = Merge<
155
  AbstractTileLayerConfig,
156
  {
157
    sizeField?: VisualChannelField;
158
    sizeScale?: string;
159
    sizeDomain?: VisualChannelDomain;
160

161
    strokeColorField: VisualChannelField;
162

163
    radiusField?: VisualChannelField;
164
    radiusScale?: string;
165
    radiusDomain?: VisualChannelDomain;
166
    radiusRange?: any;
167
  }
168
>;
169

170
export type VectorTileLayerVisConfigSettings = Merge<
171
  AbstractTileLayerVisConfigSettings,
172
  {
173
    sizeRange: VisConfigRange;
174
    strokeWidth: VisConfigNumber;
175
  }
176
>;
177

178
export function tileLayerBoundsLayer(id: string, props: {bounds?: number[]}): DeckLayer[] {
UNCOV
179
  const {bounds} = props;
×
UNCOV
180
  if (bounds?.length !== 4) return [];
×
181

UNCOV
182
  const data = [
×
183
    {
184
      path: [
185
        [bounds[0], bounds[1]],
186
        [bounds[2], bounds[1]],
187
        [bounds[2], bounds[3]],
188
        [bounds[0], bounds[3]],
189
        [bounds[0], bounds[1]]
190
      ]
191
    }
192
  ];
193

194
  const layer = new PathLayer({
×
195
    id: `${id}-vector-tile-bounds`,
196
    data,
UNCOV
197
    getPath: d => d.path,
×
198
    getColor: [128, 128, 128, 255],
199
    getWidth: 1,
200
    widthUnits: 'pixels',
201
    pickable: false
202
  });
203

204
  return [layer];
×
205
}
206

207
export default class VectorTileLayer extends AbstractTileLayer<VectorTile, Feature[]> {
208
  declare config: VectorTileLayerConfig;
209
  declare visConfigSettings: VectorTileLayerVisConfigSettings;
210

211
  constructor(props: ConstructorParameters<typeof AbstractTileLayer>[0]) {
212
    super(props);
27✔
213
    this.registerVisConfig(vectorTileVisConfigs);
27✔
214
    this.tileDataset = this.initTileDataset();
27✔
215
  }
216

217
  meta = {};
27✔
218

219
  static findDefaultLayerProps(dataset: KeplerDataset): FindDefaultLayerPropsReturnValue {
220
    if (dataset.type !== DatasetType.VECTOR_TILE) {
90!
221
      return {props: []};
90✔
222
    }
UNCOV
223
    return super.findDefaultLayerProps(dataset);
×
224
  }
225

226
  initTileDataset(): TileDataset<VectorTile, Feature[]> {
227
    return new TileDataset({
54✔
228
      getTileId: (tile: VectorTile): string => tile.id,
×
229
      getIterable: (tile: VectorTile): Feature[] => {
UNCOV
230
        if (tile.content) {
×
UNCOV
231
          return tile.content.shape === 'geojson-table' ? tile.content.features : tile.content;
×
232
        }
UNCOV
233
        return [];
×
234
      },
UNCOV
235
      getRowCount: (features: Feature[]): number => features.length,
×
236
      getRowValue: this.accessRowValue
237
    });
238
  }
239

240
  get type() {
UNCOV
241
    return LAYER_TYPES.vectorTile;
×
242
  }
243

244
  get name(): string {
245
    return 'Vector Tile';
27✔
246
  }
247

248
  get layerIcon(): KeplerLayer['layerIcon'] {
249
    return VectorTileIcon;
27✔
250
  }
251

252
  get supportedDatasetTypes(): DatasetType[] {
UNCOV
253
    return [DatasetType.VECTOR_TILE];
×
254
  }
255

256
  get visualChannels(): Record<string, VisualChannel> {
UNCOV
257
    const visualChannels = super.visualChannels;
×
UNCOV
258
    return {
×
259
      ...visualChannels,
260
      strokeColor: {
261
        property: 'strokeColor',
262
        field: 'strokeColorField',
263
        scale: 'strokeColorScale',
264
        domain: 'strokeColorDomain',
265
        range: 'strokeColorRange',
266
        key: 'strokeColor',
267
        channelScaleType: CHANNEL_SCALES.color,
268
        accessor: 'getLineColor',
UNCOV
269
        condition: config => config.visConfig.stroked,
×
270
        nullValue: visualChannels.color.nullValue,
UNCOV
271
        getAttributeValue: config => config.visConfig.strokeColor || config.color
×
272
      },
273
      size: {
274
        property: 'stroke',
275
        field: 'sizeField',
276
        scale: 'sizeScale',
277
        domain: 'sizeDomain',
278
        range: 'sizeRange',
279
        key: 'size',
280
        channelScaleType: CHANNEL_SCALES.size,
281
        nullValue: 0,
282
        accessor: 'getLineWidth',
UNCOV
283
        condition: config => config.visConfig.stroked,
×
UNCOV
284
        getAttributeValue: config => config.visConfig.strokeWidth || DEFAULT_STROKE_WIDTH
×
285
      },
286
      radius: {
287
        property: 'radius',
288
        field: 'radiusField',
289
        scale: 'radiusScale',
290
        domain: 'radiusDomain',
291
        range: 'radiusRange',
292
        key: 'radius',
293
        channelScaleType: CHANNEL_SCALES.size,
294
        nullValue: 0,
295
        getAttributeValue: config => {
UNCOV
296
          return config.visConfig.radius || config.radius;
×
297
        },
298
        accessor: 'getPointRadius',
UNCOV
299
        defaultValue: config => config.radius
×
300
      }
301
    };
302
  }
303

304
  getDefaultLayerConfig(
305
    props: LayerBaseConfigPartial
306
  ): LayerBaseConfig & Partial<LayerColorConfig & LayerHeightConfig> {
307
    const defaultLayerConfig = super.getDefaultLayerConfig(props);
27✔
308
    return {
27✔
309
      ...defaultLayerConfig,
310
      colorScale: SCALE_TYPES.quantize,
311

312
      strokeColorField: null,
313
      strokeColorDomain: [0, 1],
314
      strokeColorScale: SCALE_TYPES.quantile,
315
      colorUI: {
316
        ...defaultLayerConfig.colorUI,
317
        // @ts-expect-error LayerConfig
318
        strokeColorRange: DEFAULT_COLOR_UI
319
      },
320

321
      radiusField: null,
322
      radiusDomain: [0, 1],
323
      radiusScale: SCALE_TYPES.linear
324
    };
325
  }
326

327
  getHoverData(
328
    object: {properties?: Record<string, Record<string, unknown>>},
329
    dataContainer: DataContainerInterface,
330
    fields: KeplerField[]
331
  ): (Record<string, unknown> | null)[] {
UNCOV
332
    return fields.map(f => object.properties?.[f.name] ?? null);
×
333
  }
334

335
  calculateLayerDomain(
336
    dataset: KeplerDataset,
337
    visualChannel: VisualChannel
338
  ): DomainStops | number[] {
UNCOV
339
    const defaultDomain = [0, 1];
×
340

UNCOV
341
    const field = this.config[visualChannel.field];
×
UNCOV
342
    const scale = this.config[visualChannel.scale];
×
UNCOV
343
    if (!field) {
×
344
      // if colorField or sizeField were set back to null
345
      return defaultDomain;
×
346
    }
UNCOV
347
    if (scale === SCALE_TYPES.quantile && isDomainQuantiles(field?.filterProps?.domainQuantiles)) {
×
348
      return field.filterProps.domainQuantiles;
×
349
    }
UNCOV
350
    if (isDomainStops(field?.filterProps?.domainStops)) {
×
UNCOV
351
      return field.filterProps.domainStops;
×
UNCOV
352
    } else if (Array.isArray(field?.filterProps?.domain)) {
×
UNCOV
353
      return field.filterProps.domain;
×
354
    }
355

356
    return defaultDomain;
×
357
  }
358

359
  getScaleOptions(channelKey: string): string[] {
360
    let options = KeplerLayer.prototype.getScaleOptions.call(this, channelKey);
×
361

UNCOV
362
    const channel = this.visualChannels.strokeColor;
×
UNCOV
363
    const field = this.config[channel.field];
×
364
    if (
×
365
      !(
366
        isDomainQuantiles(field?.filterProps?.domainQuantiles) ||
×
367
        this.config.visConfig.dynamicColor ||
368
        // If we've set the scale to quantile, we need to include it - there's a loading
369
        // period in which the visConfig isn't set yet, but if we don't return the right
370
        // scale type we lose it
371
        this.config.colorScale === SCALE_TYPES.quantile
372
      )
373
    ) {
UNCOV
374
      options = options.filter(scale => scale !== SCALE_TYPES.quantile);
×
375
    }
376

UNCOV
377
    return options;
×
378
  }
379

380
  accessRowValue(
381
    field?: KeplerField,
382
    indexKey?: number | null
383
  ): (field: KeplerField, datum: Feature) => number | null {
384
    // if is indexed field
385
    if (isIndexedField(field) && indexKey !== null) {
×
UNCOV
386
      const fieldName = indexKey && field?.indexBy?.mappedValue[indexKey];
×
UNCOV
387
      if (fieldName) {
×
UNCOV
388
        return (f, datum) => {
×
UNCOV
389
          if (datum.properties) {
×
UNCOV
390
            return datum.properties[fieldName];
×
391
          }
392
          // TODO debug this with indexed tiled dataset
UNCOV
393
          return datum[fieldName];
×
394
        };
395
      }
396
    }
397

398
    // default
399
    return (f, datum) => {
×
UNCOV
400
      if (f && datum.properties) {
×
401
        return datum.properties[f.name];
×
402
      }
403
      // support picking & highlighting
404
      return f ? datum[f.fieldIdx] : null;
×
405
    };
406
  }
407

408
  updateLayerMeta(dataset: KeplerDataset, datasets: KeplerDatasets): void {
409
    if (dataset.type !== DatasetType.VECTOR_TILE) {
×
410
      return;
×
411
    }
412

UNCOV
413
    const datasetMeta = dataset.metadata as VectorTileMetadata & VectorTileDatasetMetadata;
×
UNCOV
414
    this.updateMeta({
×
415
      datasetId: dataset.id,
416
      datasets,
417
      bounds: datasetMeta.bounds
418
    });
419
  }
420

421
  formatLayerData(
422
    datasets: KeplerDatasets,
423
    oldLayerData: unknown,
424
    animationConfig: AnimationConfig
425
  ): LayerData {
UNCOV
426
    const {dataId} = this.config;
×
UNCOV
427
    if (!notNullorUndefined(dataId)) {
×
UNCOV
428
      return {tileSource: null};
×
429
    }
UNCOV
430
    const dataset = datasets[dataId];
×
431

432
    let tilesetDataUrl: string | undefined;
433
    let tileSource: LayerData['tileSource'] = null;
×
434

UNCOV
435
    if (dataset?.type === DatasetType.VECTOR_TILE) {
×
UNCOV
436
      const datasetMetadata = dataset.metadata as VectorTileMetadata & VectorTileDatasetMetadata;
×
UNCOV
437
      const remoteTileFormat = datasetMetadata?.remoteTileFormat;
×
438
      if (remoteTileFormat === RemoteTileFormat.MVT) {
×
UNCOV
439
        const transformFetch = async (input: RequestInfo | URL, init?: RequestInit | undefined) => {
×
UNCOV
440
          const requestData: RequestParameters = {
×
441
            url: input as string,
442
            searchParams: new URLSearchParams(),
443
            options: init ?? {}
×
444
          };
445

446
          return fetch(requestData.url, requestData.options);
×
447
        };
448

UNCOV
449
        tilesetDataUrl = datasetMetadata?.tilesetDataUrl;
×
450
        tileSource = tilesetDataUrl
×
451
          ? MVTSource.createDataSource(decodeURIComponent(tilesetDataUrl), {
452
              mvt: {
453
                metadataUrl: datasetMetadata?.tilesetMetadataUrl ?? null,
×
454
                loadOptions: {
455
                  fetch: transformFetch
456
                }
457
              }
458
            })
459
          : null;
UNCOV
460
      } else if (remoteTileFormat === RemoteTileFormat.PMTILES) {
×
461
        // 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)
462
        tilesetDataUrl = datasetMetadata?.tilesetDataUrl;
×
UNCOV
463
        tileSource = tilesetDataUrl ? PMTilesSource.createDataSource(tilesetDataUrl, {}) : null;
×
464
      }
465
    }
466

UNCOV
467
    return {
×
468
      ...super.formatLayerData(datasets, oldLayerData, animationConfig),
469
      tilesetDataUrl: typeof tilesetDataUrl === 'string' ? getTileUrl(tilesetDataUrl) : null,
×
470
      tileSource
471
    };
472
  }
473

474
  hasHoveredObject(objectInfo) {
UNCOV
475
    if (super.hasHoveredObject(objectInfo)) {
×
UNCOV
476
      const features = objectInfo?.tile?.content?.features;
×
UNCOV
477
      return features[objectInfo.index];
×
478
    }
UNCOV
479
    return null;
×
480
  }
481

482
  renderSubLayers(props: Record<string, any>): DeckLayer | DeckLayer[] {
UNCOV
483
    let {data} = props;
×
484

485
    data = data?.shape === 'geojson-table' ? data.features : data;
×
UNCOV
486
    if (!data?.length) {
×
487
      return [];
×
488
    }
489

490
    const tile: Tile2DHeader = props.tile;
×
UNCOV
491
    const zoom = tile.index.z;
×
492

UNCOV
493
    return new GeoJsonLayer({
×
494
      ...props,
495
      data,
496
      getFillColor: props.getFillColorByZoom ? props.getFillColor(zoom) : props.getFillColor,
×
497
      getElevation: props.getElevationByZoom ? props.getElevation(zoom) : props.getElevation,
×
498
      // radius for points
499
      pointRadiusScale: props.pointRadiusScale, // props.getPointRadiusScaleByZoom(zoom),
500
      pointRadiusUnits: props.pointRadiusUnits,
501
      getPointRadius: props.getPointRadius,
502
      // For some reason tile Layer reset autoHighlight to false
503
      pickable: true,
504
      autoHighlight: true,
505
      stroked: props.stroked,
506
      // wrapLongitude: true causes missing side polygon when extrude is enabled
507
      wrapLongitude: false
508
    });
509
  }
510

511
  // generate a deck layer
512
  renderLayer(opts: VectorTileLayerRenderOptions): DeckLayer[] {
UNCOV
513
    const {mapState, data, animationConfig, gpuFilter, objectHovered, layerCallbacks} = opts;
×
UNCOV
514
    const {animation, visConfig} = this.config;
×
515

UNCOV
516
    this.setLayerDomain = layerCallbacks.onSetLayerDomain;
×
517

UNCOV
518
    const defaultLayerProps = this.getDefaultDeckLayerProps(opts);
×
UNCOV
519
    const eleZoomFactor = this.getElevationZoomFactor(mapState);
×
520

UNCOV
521
    const transitions = this.config.visConfig.transition
×
522
      ? {
523
          getFillColor: {
524
            duration: animationConfig.duration
525
          },
526
          getElevation: {
527
            duration: animationConfig.duration
528
          }
529
        }
530
      : undefined;
531

UNCOV
532
    const colorField = this.config.colorField as KeplerField;
×
UNCOV
533
    const heightField = this.config.heightField as KeplerField;
×
UNCOV
534
    const strokeColorField = this.config.strokeColorField as KeplerField;
×
UNCOV
535
    const sizeField = this.config.sizeField as KeplerField;
×
UNCOV
536
    const radiusField = this.config.radiusField as KeplerField;
×
537

UNCOV
538
    if (data.tileSource) {
×
UNCOV
539
      const hoveredObject = this.hasHoveredObject(objectHovered);
×
540

UNCOV
541
      const layers = [
×
542
        new CustomMVTLayer({
543
          ...defaultLayerProps,
544
          ...data,
545
          onViewportLoad: this.onViewportLoad,
546
          data: data.tilesetDataUrl,
547
          getTileData: data.tileSource?.getTileData,
548
          tileSource: data.tileSource,
549
          getFilterValue: this.getGpuFilterValueAccessor(opts),
550
          filterRange: gpuFilter.filterRange,
551
          lineWidthUnits: 'pixels',
552

553
          binary: false,
554
          elevationScale: visConfig.elevationScale * eleZoomFactor,
555
          extruded: visConfig.enable3d,
556
          stroked: visConfig.stroked,
557

558
          // TODO: this is hard coded, design a UI to allow user assigned unique property id
559
          // uniqueIdProperty: 'ufid',
560
          renderSubLayers: this.renderSubLayers,
561
          // when radiusUnits is meter
562
          getPointRadiusScaleByZoom: getPropertyByZoom(visConfig.radiusByZoom, visConfig.radius),
563
          pointRadiusUnits: visConfig.radiusUnits ? 'pixels' : 'meters',
×
564
          pointRadiusScale: radiusField ? visConfig.radius : 1,
×
565

566
          pointRadiusMinPixels: 1,
567
          autoHighlight: true,
568
          highlightColor: DEFAULT_HIGHLIGHT_FILL_COLOR,
569
          pickable: true,
570
          transitions,
571
          updateTriggers: {
572
            getFilterValue: {
573
              ...gpuFilter.filterValueUpdateTriggers,
574
              currentTime: animation.enabled ? animationConfig.currentTime : null
×
575
            },
576
            getFillColor: {
577
              color: this.config.color,
578
              colorField: this.config.colorField,
579
              colorScale: this.config.colorScale,
580
              colorDomain: this.config.colorDomain,
581
              colorRange: visConfig.colorRange,
582
              currentTime: isIndexedField(colorField) ? animationConfig.currentTime : null
×
583
            },
584
            getElevation: {
585
              heightField: this.config.heightField,
586
              heightScaleType: this.config.heightScale,
587
              heightRange: visConfig.heightRange,
588
              currentTime: isIndexedField(heightField) ? animationConfig.currentTime : null
×
589
            },
590
            getLineColor: {
591
              strokeColor: visConfig.strokeColor,
592
              strokeColorField: this.config.strokeColorField,
593
              // @ts-expect-error prop not in LayerConfig
594
              strokeColorScale: this.config.strokeColorScale,
595
              // @ts-expect-error prop not in LayerConfig
596
              strokeColorDomain: this.config.strokeColorDomain,
597
              // FIXME: Strip out empty arrays from individual color map steps, and replace with `null`, otherwise the layer may show the incorrect color.
598
              // 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.
599
              // In other words, a color map with "holes" of colors with unassigned field values, which may have been assigned in the past.
600
              // 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.
601
              // Quick patch example:
602
              // strokeColorRange: visConfig?.strokeColorRange?.colorMap?.map(cm =>
603
              //   cm[0]?.length === 0 ? [null, cm[1]] : cm
604
              // ),
605
              // Note: for regular scales the colorMap in the above patch is undefined and breaks strokeColorRange update trigger.
606
              strokeColorRange: visConfig.strokeColorRange,
607
              currentTime: isIndexedField(strokeColorField) ? animationConfig.currentTime : null
×
608
            },
609
            getLineWidth: {
610
              sizeRange: visConfig.sizeRange,
611
              strokeWidth: visConfig.strokeWidth,
612
              sizeField: this.config.sizeField,
613
              sizeScale: this.config.sizeScale,
614
              sizeDomain: this.config.sizeDomain,
615
              currentTime: isIndexedField(sizeField) ? animationConfig.currentTime : null
×
616
            },
617
            getPointRadius: {
618
              radius: visConfig.radius,
619
              radiusField: this.config.radiusField,
620
              radiusScale: this.config.radiusScale,
621
              radiusDomain: this.config.radiusDomain,
622
              radiusRange: this.config.radiusRange,
623
              currentTime: isIndexedField(radiusField) ? animationConfig.currentTime : null
×
624
            }
625
          },
626
          _subLayerProps: {
627
            'polygons-stroke': {opacity: visConfig.strokeOpacity},
628
            'polygons-fill': {
629
              parameters: {
630
                cullFace: GL.BACK
631
              }
632
            }
633
          },
634
          loadOptions: {
635
            mvt: getLoaderOptions().mvt
636
          }
637
        }),
638
        // hover layer
639
        ...(hoveredObject
×
640
          ? [
641
              new GeoJsonLayer({
642
                // @ts-expect-error props not typed?
643
                ...objectHovered.sourceLayer?.props,
644
                ...(this.getDefaultHoverLayerProps() as any),
645
                visible: true,
646
                wrapLongitude: false,
647
                data: [hoveredObject],
648
                getLineColor: DEFAULT_HIGHLIGHT_STROKE_COLOR,
649
                getFillColor: DEFAULT_HIGHLIGHT_FILL_COLOR,
650
                getLineWidth: visConfig.strokeWidth + 1,
651
                lineWidthUnits: 'pixels',
652
                stroked: true,
653
                filled: true
654
              })
655
            ]
656
          : [])
657
        // ...tileLayerBoundsLayer(defaultLayerProps.id, data),
658
      ];
659

UNCOV
660
      return layers;
×
661
    }
UNCOV
662
    return [];
×
663
  }
664
}
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