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

keplergl / kepler.gl / 12777571612

14 Jan 2025 10:31PM UTC coverage: 66.644% (+0.1%) from 66.515%
12777571612

Pull #2911

github

web-flow
Merge 33f3102ac into 933a91a2f
Pull Request #2911: [fix] vector tile layer fixes

5981 of 10467 branches covered (57.14%)

Branch coverage included in aggregate %.

1 of 8 new or added lines in 4 files covered. (12.5%)

62 existing lines in 2 files now uncovered.

12296 of 16958 relevant lines covered (72.51%)

89.59 hits per line

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

8.17
/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
  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

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);
27✔
183
    this.registerVisConfig(vectorTileVisConfigs);
27✔
184
    this.tileDataset = this.initTileDataset();
27✔
185
  }
186

187
  meta = {};
27✔
188

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

198
  initTileDataset(): TileDataset<VectorTile, Feature[]> {
199
    return new TileDataset({
54✔
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() {
NEW
213
    return LAYER_TYPES.vectorTile;
×
214
  }
215

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

220
  get layerIcon(): KeplerLayer['layerIcon'] {
221
    return VectorTileIcon;
27✔
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 => {
268
          return config.visConfig.radius || config.radius;
×
269
        },
270
        accessor: 'getPointRadius',
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);
27✔
280
    return {
27✔
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;
×
NEW
409
      const remoteTileFormat = datasetMetadata?.remoteTileFormat;
×
NEW
410
      if (remoteTileFormat === RemoteTileFormat.MVT) {
×
411
        const transformFetch = async (input: RequestInfo | URL, init?: RequestInit | undefined) => {
×
412
          const requestData: RequestParameters = {
×
413
            url: input as string,
414
            searchParams: new URLSearchParams(),
415
            options: init ?? {}
×
416
          };
417

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

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

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

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

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

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

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

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

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

488
    this.setLayerDomain = layerCallbacks.onSetLayerDomain;
×
489

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

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

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

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

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

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

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

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

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