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

keplergl / kepler.gl / 12717940823

10 Jan 2025 10:06PM UTC coverage: 66.515% (-0.2%) from 66.757%
12717940823

push

github

web-flow
[feat] Vector Tile layer fixes (#2899)

- show Vector Tile layer tab
- changes to radius controls
- fix highlighted filled polygons
- hide show data table icon
- display loading spinner while we are loading metadata
- display errors metadata loading failed
- fix for custom ordinal crash when all colors are deleted
- collect dataset attributions from metadata and show in the lower right corner 
- refetch tile dataset metadata createNewDataEntry

Signed-off-by: Ihor Dykhta <dikhta.igor@gmail.com>

5976 of 10465 branches covered (57.1%)

Branch coverage included in aggregate %.

23 of 83 new or added lines in 13 files covered. (27.71%)

3 existing lines in 3 files now uncovered.

12261 of 16953 relevant lines covered (72.32%)

89.15 hits per line

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

2.42
/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
  DatasetType,
16
  TileType,
17
  VectorTileType,
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

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

71
export const DEFAULT_HIGHLIGHT_FILL_COLOR = [252, 242, 26, 150];
13✔
72
export const DEFAULT_HIGHLIGHT_STROKE_COLOR = [252, 242, 26, 255];
13✔
73
export const MAX_CACHE_SIZE_MOBILE = 1; // Minimize caching, visible tiles will always be loaded
13✔
74
export const DEFAULT_STROKE_WIDTH = 1;
13✔
75

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

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

97
type VectorTile = Tile2DHeader<TileContent>;
98

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

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

122
export const vectorTileVisConfigs = {
13✔
123
  ...commonTileVisConfigs,
124

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

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

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

142
  radiusScale: 'radiusScale' as any,
143
  radiusRange: {
144
    ...LAYER_VIS_CONFIGS.radiusRange,
145
    type: 'number',
146
    defaultValue: [0, 1],
147
    isRanged: true,
148
    range: [0, 1],
149
    step: 0.01
150
  } as VisConfigRange
151
};
152

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

160
    strokeColorField: VisualChannelField;
161

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

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

177
export default class VectorTileLayer extends AbstractTileLayer<VectorTile, Feature[]> {
178
  declare config: VectorTileLayerConfig;
179
  declare visConfigSettings: VectorTileLayerVisConfigSettings;
180

181
  constructor(props: ConstructorParameters<typeof AbstractTileLayer>[0]) {
182
    super(props);
×
183
    this.registerVisConfig(vectorTileVisConfigs);
×
184
    this.tileDataset = this.initTileDataset();
×
185
  }
186

187
  meta = {};
×
188

189
  static findDefaultLayerProps(dataset: KeplerDataset): {
190
    props: {dataId: string; label?: string; isVisible: boolean}[];
191
  } {
192
    if (dataset.type !== DatasetType.VECTOR_TILE) {
×
193
      return {props: []};
×
194
    }
195
    return super.findDefaultLayerProps(dataset);
×
196
  }
197

198
  initTileDataset(): TileDataset<VectorTile, Feature[]> {
199
    return new TileDataset({
×
200
      getTileId: (tile: VectorTile): string => tile.id,
×
201
      getIterable: (tile: VectorTile): Feature[] => {
202
        if (tile.content) {
×
203
          return tile.content.shape === 'geojson-table' ? tile.content.features : tile.content;
×
204
        }
205
        return [];
×
206
      },
207
      getRowCount: (features: Feature[]): number => features.length,
×
208
      getRowValue: this.accessRowValue
209
    });
210
  }
211

212
  get type() {
213
    return TileType.VECTOR_TILE;
×
214
  }
215

216
  get name(): string {
217
    return 'Vector Tile';
×
218
  }
219

220
  get layerIcon(): KeplerLayer['layerIcon'] {
221
    return VectorTileIcon;
×
222
  }
223

224
  get supportedDatasetTypes(): DatasetType[] {
225
    return [DatasetType.VECTOR_TILE];
×
226
  }
227

228
  get visualChannels(): Record<string, VisualChannel> {
229
    const visualChannels = super.visualChannels;
×
230
    return {
×
231
      ...visualChannels,
232
      strokeColor: {
233
        property: 'strokeColor',
234
        field: 'strokeColorField',
235
        scale: 'strokeColorScale',
236
        domain: 'strokeColorDomain',
237
        range: 'strokeColorRange',
238
        key: 'strokeColor',
239
        channelScaleType: CHANNEL_SCALES.color,
240
        accessor: 'getLineColor',
241
        condition: config => config.visConfig.stroked,
×
242
        nullValue: visualChannels.color.nullValue,
243
        getAttributeValue: config => config.visConfig.strokeColor || config.color
×
244
      },
245
      size: {
246
        property: 'stroke',
247
        field: 'sizeField',
248
        scale: 'sizeScale',
249
        domain: 'sizeDomain',
250
        range: 'sizeRange',
251
        key: 'size',
252
        channelScaleType: CHANNEL_SCALES.size,
253
        nullValue: 0,
254
        accessor: 'getLineWidth',
255
        condition: config => config.visConfig.stroked,
×
256
        getAttributeValue: config => config.visConfig.strokeWidth || DEFAULT_STROKE_WIDTH
×
257
      },
258
      radius: {
259
        property: 'radius',
260
        field: 'radiusField',
261
        scale: 'radiusScale',
262
        domain: 'radiusDomain',
263
        range: 'radiusRange',
264
        key: 'radius',
265
        channelScaleType: CHANNEL_SCALES.size,
266
        nullValue: 0,
267
        getAttributeValue: config => {
NEW
268
          return config.visConfig.radius || config.radius;
×
269
        },
270
        accessor: 'getPointRadius',
NEW
271
        defaultValue: config => config.radius
×
272
      }
273
    };
274
  }
275

276
  getDefaultLayerConfig(
277
    props: LayerBaseConfigPartial
278
  ): LayerBaseConfig & Partial<LayerColorConfig & LayerHeightConfig> {
279
    const defaultLayerConfig = super.getDefaultLayerConfig(props);
×
280
    return {
×
281
      ...defaultLayerConfig,
282
      colorScale: SCALE_TYPES.quantize,
283

284
      strokeColorField: null,
285
      strokeColorDomain: [0, 1],
286
      strokeColorScale: SCALE_TYPES.quantile,
287
      colorUI: {
288
        ...defaultLayerConfig.colorUI,
289
        // @ts-expect-error LayerConfig
290
        strokeColorRange: DEFAULT_COLOR_UI
291
      },
292

293
      radiusField: null,
294
      radiusDomain: [0, 1],
295
      radiusScale: SCALE_TYPES.linear
296
    };
297
  }
298

299
  getHoverData(
300
    object: {properties?: Record<string, Record<string, unknown>>},
301
    dataContainer: DataContainerInterface,
302
    fields: KeplerField[]
303
  ): (Record<string, unknown> | null)[] {
304
    return fields.map(f => object.properties?.[f.name] ?? null);
×
305
  }
306

307
  calculateLayerDomain(
308
    dataset: KeplerDataset,
309
    visualChannel: VisualChannel
310
  ): DomainStops | number[] {
311
    const defaultDomain = [0, 1];
×
312

313
    const field = this.config[visualChannel.field];
×
314
    const scale = this.config[visualChannel.scale];
×
315
    if (!field) {
×
316
      // if colorField or sizeField were set back to null
317
      return defaultDomain;
×
318
    }
319
    if (scale === SCALE_TYPES.quantile && isDomainQuantiles(field?.filterProps?.domainQuantiles)) {
×
320
      return field.filterProps.domainQuantiles;
×
321
    }
322
    if (isDomainStops(field?.filterProps?.domainStops)) {
×
323
      return field.filterProps.domainStops;
×
324
    } else if (Array.isArray(field?.filterProps?.domain)) {
×
325
      return field.filterProps.domain;
×
326
    }
327

328
    return defaultDomain;
×
329
  }
330

331
  getScaleOptions(channelKey: string): string[] {
332
    let options = KeplerLayer.prototype.getScaleOptions.call(this, channelKey);
×
333

334
    const channel = this.visualChannels.strokeColor;
×
335
    const field = this.config[channel.field];
×
336
    if (
×
337
      !(
338
        isDomainQuantiles(field?.filterProps?.domainQuantiles) ||
×
339
        this.config.visConfig.dynamicColor ||
340
        // If we've set the scale to quantile, we need to include it - there's a loading
341
        // period in which the visConfig isn't set yet, but if we don't return the right
342
        // scale type we lose it
343
        this.config.colorScale === SCALE_TYPES.quantile
344
      )
345
    ) {
346
      options = options.filter(scale => scale !== SCALE_TYPES.quantile);
×
347
    }
348

349
    return options;
×
350
  }
351

352
  accessRowValue(
353
    field?: KeplerField,
354
    indexKey?: number | null
355
  ): (field: KeplerField, datum: Feature) => number | null {
356
    // if is indexed field
357
    if (isIndexedField(field) && indexKey !== null) {
×
358
      const fieldName = indexKey && field?.indexBy?.mappedValue[indexKey];
×
359
      if (fieldName) {
×
360
        return (f, datum) => {
×
361
          if (datum.properties) {
×
362
            return datum.properties[fieldName];
×
363
          }
364
          // TODO debug this with indexed tiled dataset
365
          return datum[fieldName];
×
366
        };
367
      }
368
    }
369

370
    // default
371
    return (f, datum) => {
×
372
      if (f && datum.properties) {
×
373
        return datum.properties[f.name];
×
374
      }
375
      // support picking & highlighting
376
      return f ? datum[f.fieldIdx] : null;
×
377
    };
378
  }
379

380
  updateLayerMeta(dataset: KeplerDataset, datasets: KeplerDatasets): void {
381
    if (dataset.type !== DatasetType.VECTOR_TILE) {
×
382
      return;
×
383
    }
384

385
    const datasetMeta = dataset.metadata as VectorTileMetadata & VectorTileDatasetMetadata;
×
386
    this.updateMeta({
×
387
      datasetId: dataset.id,
388
      datasets,
389
      bounds: datasetMeta.bounds
390
    });
391
  }
392

393
  formatLayerData(
394
    datasets: KeplerDatasets,
395
    oldLayerData: unknown,
396
    animationConfig: AnimationConfig
397
  ): LayerData {
398
    const {dataId} = this.config;
×
399
    if (!notNullorUndefined(dataId)) {
×
400
      return {tileSource: null};
×
401
    }
402
    const dataset = datasets[dataId];
×
403

404
    let tilesetDataUrl: string | undefined;
405
    let tileSource: LayerData['tileSource'] = null;
×
406

407
    if (dataset?.type === DatasetType.VECTOR_TILE) {
×
408
      const datasetMetadata = dataset.metadata as VectorTileMetadata & VectorTileDatasetMetadata;
×
409
      if (datasetMetadata?.type === VectorTileType.MVT) {
×
410
        const transformFetch = async (input: RequestInfo | URL, init?: RequestInit | undefined) => {
×
411
          const requestData: RequestParameters = {
×
412
            url: input as string,
413
            searchParams: new URLSearchParams(),
414
            options: init ?? {}
×
415
          };
416

417
          return fetch(requestData.url, requestData.options);
×
418
        };
419

420
        tilesetDataUrl = datasetMetadata?.tilesetDataUrl;
×
421
        tileSource = tilesetDataUrl
×
422
          ? MVTSource.createDataSource(decodeURIComponent(tilesetDataUrl), {
423
              mvt: {
424
                metadataUrl: datasetMetadata?.tilesetMetadataUrl ?? null,
×
425
                loadOptions: {
426
                  fetch: transformFetch
427
                }
428
              }
429
            })
430
          : null;
431
      } else if (datasetMetadata?.type === VectorTileType.PMTILES) {
×
432
        // 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)
433
        tilesetDataUrl = datasetMetadata?.tilesetDataUrl;
×
434
        tileSource = tilesetDataUrl ? PMTilesSource.createDataSource(tilesetDataUrl, {}) : null;
×
435
      }
436
    }
437

438
    return {
×
439
      ...super.formatLayerData(datasets, oldLayerData, animationConfig),
440
      tilesetDataUrl: typeof tilesetDataUrl === 'string' ? getTileUrl(tilesetDataUrl) : null,
×
441
      tileSource
442
    };
443
  }
444

445
  hasHoveredObject(objectInfo) {
446
    if (super.hasHoveredObject(objectInfo)) {
×
447
      const features = objectInfo?.tile?.content?.features;
×
448
      return features[objectInfo.index];
×
449
    }
450
    return null;
×
451
  }
452

453
  renderSubLayers(props: Record<string, any>): DeckLayer | DeckLayer[] {
454
    let {data} = props;
×
455

456
    data = data?.shape === 'geojson-table' ? data.features : data;
×
457
    if (!data?.length) {
×
458
      return [];
×
459
    }
460

461
    const tile: Tile2DHeader = props.tile;
×
462
    const zoom = tile.index.z;
×
463

464
    return new GeoJsonLayer({
×
465
      ...props,
466
      data,
467
      getFillColor: props.getFillColorByZoom ? props.getFillColor(zoom) : props.getFillColor,
×
468
      getElevation: props.getElevationByZoom ? props.getElevation(zoom) : props.getElevation,
×
469
      // radius for points
470
      pointRadiusScale: props.pointRadiusScale, // props.getPointRadiusScaleByZoom(zoom),
471
      pointRadiusUnits: props.pointRadiusUnits,
472
      getPointRadius: props.getPointRadius,
473
      // For some reason tile Layer reset autoHighlight to false
474
      pickable: true,
475
      autoHighlight: true,
476
      stroked: props.stroked,
477
      // wrapLongitude: true causes missing side polygon when extrude is enabled
478
      wrapLongitude: false
479
    });
480
  }
481

482
  // generate a deck layer
483
  renderLayer(opts: VectorTileLayerRenderOptions): DeckLayer[] {
484
    const {mapState, data, animationConfig, gpuFilter, objectHovered, layerCallbacks} = opts;
×
485
    const {animation, visConfig} = this.config;
×
486

487
    this.setLayerDomain = layerCallbacks.onSetLayerDomain;
×
488

489
    const defaultLayerProps = this.getDefaultDeckLayerProps(opts);
×
490
    const eleZoomFactor = this.getElevationZoomFactor(mapState);
×
491

492
    const transitions = this.config.visConfig.transition
×
493
      ? {
494
          getFillColor: {
495
            duration: animationConfig.duration
496
          },
497
          getElevation: {
498
            duration: animationConfig.duration
499
          }
500
        }
501
      : undefined;
502

503
    const colorField = this.config.colorField as KeplerField;
×
504
    const heightField = this.config.heightField as KeplerField;
×
505
    const strokeColorField = this.config.strokeColorField as KeplerField;
×
506
    const sizeField = this.config.sizeField as KeplerField;
×
NEW
507
    const radiusField = this.config.radiusField as KeplerField;
×
508

509
    if (data.tileSource) {
×
510
      const hoveredObject = this.hasHoveredObject(objectHovered);
×
511

512
      const layers = [
×
513
        new CustomMVTLayer({
514
          ...defaultLayerProps,
515
          ...data,
516
          onViewportLoad: this.onViewportLoad,
517
          data: data.tilesetDataUrl,
518
          getTileData: data.tileSource?.getTileData,
519
          tileSource: data.tileSource,
520
          getFilterValue: this.getGpuFilterValueAccessor(opts),
521
          filterRange: gpuFilter.filterRange,
522
          lineWidthUnits: 'pixels',
523

524
          binary: false,
525
          elevationScale: visConfig.elevationScale * eleZoomFactor,
526
          extruded: visConfig.enable3d,
527
          stroked: visConfig.stroked,
528

529
          // TODO: this is hard coded, design a UI to allow user assigned unique property id
530
          // uniqueIdProperty: 'ufid',
531
          renderSubLayers: this.renderSubLayers,
532
          // when radiusUnits is meter
533
          getPointRadiusScaleByZoom: getPropertyByZoom(visConfig.radiusByZoom, visConfig.radius),
534
          pointRadiusUnits: visConfig.radiusUnits ? 'pixels' : 'meters',
×
535
          pointRadiusScale: radiusField ? visConfig.radius : 1,
×
536

537
          pointRadiusMinPixels: 1,
538
          autoHighlight: true,
539
          highlightColor: DEFAULT_HIGHLIGHT_FILL_COLOR,
540
          pickable: true,
541
          transitions,
542
          updateTriggers: {
543
            getFilterValue: {
544
              ...gpuFilter.filterValueUpdateTriggers,
545
              currentTime: animation.enabled ? animationConfig.currentTime : null
×
546
            },
547
            getFillColor: {
548
              color: this.config.color,
549
              colorField: this.config.colorField,
550
              colorScale: this.config.colorScale,
551
              colorDomain: this.config.colorDomain,
552
              colorRange: visConfig.colorRange,
553
              currentTime: isIndexedField(colorField) ? animationConfig.currentTime : null
×
554
            },
555
            getElevation: {
556
              heightField: this.config.heightField,
557
              heightScaleType: this.config.heightScale,
558
              heightRange: visConfig.heightRange,
559
              currentTime: isIndexedField(heightField) ? animationConfig.currentTime : null
×
560
            },
561
            getLineColor: {
562
              strokeColor: visConfig.strokeColor,
563
              strokeColorField: this.config.strokeColorField,
564
              // @ts-expect-error prop not in LayerConfig
565
              strokeColorScale: this.config.strokeColorScale,
566
              // @ts-expect-error prop not in LayerConfig
567
              strokeColorDomain: this.config.strokeColorDomain,
568
              // FIXME: Strip out empty arrays from individual color map steps, and replace with `null`, otherwise the layer may show the incorrect color.
569
              // 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.
570
              // In other words, a color map with "holes" of colors with unassigned field values, which may have been assigned in the past.
571
              // 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.
572
              // Quick patch example:
573
              // strokeColorRange: visConfig?.strokeColorRange?.colorMap?.map(cm =>
574
              //   cm[0]?.length === 0 ? [null, cm[1]] : cm
575
              // ),
576
              // Note: for regular scales the colorMap in the above patch is undefined and breaks strokeColorRange update trigger.
577
              strokeColorRange: visConfig.strokeColorRange,
578
              currentTime: isIndexedField(strokeColorField) ? animationConfig.currentTime : null
×
579
            },
580
            getLineWidth: {
581
              sizeRange: visConfig.sizeRange,
582
              strokeWidth: visConfig.strokeWidth,
583
              sizeField: this.config.sizeField,
584
              sizeScale: this.config.sizeScale,
585
              sizeDomain: this.config.sizeDomain,
586
              currentTime: isIndexedField(sizeField) ? animationConfig.currentTime : null
×
587
            },
588
            getPointRadius: {
589
              radius: visConfig.radius,
590
              radiusField: this.config.radiusField,
591
              radiusScale: this.config.radiusScale,
592
              radiusDomain: this.config.radiusDomain,
593
              radiusRange: this.config.radiusRange,
594
              currentTime: isIndexedField(radiusField) ? animationConfig.currentTime : null
×
595
            }
596
          },
597
          _subLayerProps: {
598
            'polygons-stroke': {opacity: visConfig.strokeOpacity},
599
            'polygons-fill': {
600
              parameters: {
601
                cullFace: GL.BACK
602
              }
603
            }
604
          },
605
          loadOptions: {
606
            mvt: getLoaderOptions().mvt
607
          }
608
        }),
609
        // hover layer
610
        ...(hoveredObject
×
611
          ? [
612
              new GeoJsonLayer({
613
                // @ts-expect-error props not typed?
614
                ...objectHovered.sourceLayer?.props,
615
                ...(this.getDefaultHoverLayerProps() as any),
616
                visible: true,
617
                wrapLongitude: false,
618
                data: [hoveredObject],
619
                getLineColor: DEFAULT_HIGHLIGHT_STROKE_COLOR,
620
                getFillColor: DEFAULT_HIGHLIGHT_FILL_COLOR,
621
                getLineWidth: visConfig.strokeWidth + 1,
622
                lineWidthUnits: 'pixels',
623
                stroked: true,
624
                filled: true
625
              })
626
            ]
627
          : [])
628
      ];
629

630
      return layers;
×
631
    }
632
    return [];
×
633
  }
634
}
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