• 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

45.08
/src/components/src/map-container.tsx
1
// SPDX-License-Identifier: MIT
2
// Copyright contributors to the kepler.gl project
3

4
// libraries
5
import React, {Component, createRef, useMemo} from 'react';
6
import styled, {withTheme} from 'styled-components';
7
import {Map, MapRef} from 'react-map-gl';
8
import {PickInfo} from '@deck.gl/core/lib/deck';
9
import DeckGL from '@deck.gl/react';
10
import {createSelector, Selector} from 'reselect';
11
import {useDroppable} from '@dnd-kit/core';
12
import debounce from 'lodash.debounce';
13

14
import {VisStateActions, MapStateActions, UIStateActions} from '@kepler.gl/actions';
15

16
// components
17
import MapPopoverFactory from './map/map-popover';
18
import MapControlFactory from './map/map-control';
19
import {
20
  StyledMapContainer,
21
  StyledAttrbution,
22
  EndHorizontalFlexbox
23
} from './common/styled-components';
24

25
import EditorFactory from './editor/editor';
26

27
// utils
28
import {
29
  generateMapboxLayers,
30
  updateMapboxLayers,
31
  LayerBaseConfig,
32
  VisualChannelDomain,
33
  EditorLayerUtils,
34
  AggregatedBin
35
} from '@kepler.gl/layers';
36
import {
37
  DatasetAttribution,
38
  MapState,
39
  MapControls,
40
  Viewport,
41
  SplitMap,
42
  SplitMapLayers
43
} from '@kepler.gl/types';
44
import {
45
  errorNotification,
46
  setLayerBlending,
47
  isStyleUsingMapboxTiles,
48
  isStyleUsingOpenStreetMapTiles,
49
  getBaseMapLibrary,
50
  BaseMapLibraryConfig,
51
  transformRequest,
52
  observeDimensions,
53
  unobserveDimensions,
54
  hasMobileWidth,
55
  getMapLayersFromSplitMaps,
56
  onViewPortChange,
57
  getViewportFromMapState,
58
  normalizeEvent,
59
  rgbToHex,
60
  computeDeckEffects,
61
  getApplicationConfig,
62
  GetMapRef
63
} from '@kepler.gl/utils';
64
import {breakPointValues} from '@kepler.gl/styles';
65

66
// default-settings
67
import {
68
  FILTER_TYPES,
69
  GEOCODER_LAYER_ID,
70
  THROTTLE_NOTIFICATION_TIME,
71
  DEFAULT_PICKING_RADIUS,
72
  NO_MAP_ID,
73
  EMPTY_MAPBOX_STYLE,
74
  DROPPABLE_MAP_CONTAINER_TYPE
75
} from '@kepler.gl/constants';
76

77
// Contexts
78
import {MapViewStateContext} from './map-view-state-context';
79

80
import ErrorBoundary from './common/error-boundary';
81
import {LOCALE_CODES} from '@kepler.gl/localization';
82
import {MapView} from '@deck.gl/core';
83
import {
84
  MapStyle,
85
  computeDeckLayers,
86
  getLayerHoverProp,
87
  LayerHoverProp,
88
  prepareLayersForDeck,
89
  prepareLayersToRender,
90
  LayersToRender
91
} from '@kepler.gl/reducers';
92
import {VisState} from '@kepler.gl/schemas';
93

94
// Debounce the propagation of viewport change and mouse moves to redux store.
95
// This is to avoid too many renders of other components when the map is
96
// being panned/zoomed (leading to laggy basemap/deck syncing).
97
const DEBOUNCE_VIEWPORT_PROPAGATE = 10;
7✔
98
const DEBOUNCE_MOUSE_MOVE_PROPAGATE = 10;
7✔
99

100
const MAP_STYLE: {[key: string]: React.CSSProperties} = {
7✔
101
  container: {
102
    display: 'inline-block',
103
    position: 'relative',
104
    width: '100%',
105
    height: '100%'
106
  },
107
  top: {
108
    position: 'absolute',
109
    top: 0,
110
    width: '100%',
111
    height: '100%',
112
    pointerEvents: 'none'
113
  }
114
};
115

116
const LOCALE_CODES_ARRAY = Object.keys(LOCALE_CODES);
7✔
117

118
interface StyledMapContainerProps {
119
  mixBlendMode?: string;
120
  mapLibCssClass: string;
121
}
122

123
const StyledMap = styled(StyledMapContainer)<StyledMapContainerProps>(
7✔
124
  ({mixBlendMode = 'normal', mapLibCssClass}) => `
29!
125
  #default-deckgl-overlay {
126
    mix-blend-mode: ${mixBlendMode};
127
  };
128
  *[${mapLibCssClass}-children] {
129
    position: absolute;
130
  }
131
`
132
);
133

134
const MAPBOXGL_STYLE_UPDATE = 'style.load';
7✔
135
const MAPBOXGL_RENDER = 'render';
7✔
136
const nop = () => {
7✔
137
  return;
×
138
};
139

140
type MapLibLogoProps = {
141
  baseMapLibraryConfig: BaseMapLibraryConfig;
142
};
143

144
const MapLibLogo = ({baseMapLibraryConfig}: MapLibLogoProps) => (
7✔
145
  <div className="attrition-logo">
25✔
146
    Basemap by:
147
    <a
148
      style={{marginLeft: '5px'}}
149
      className={`${baseMapLibraryConfig.mapLibCssClass}-ctrl-logo`}
150
      target="_blank"
151
      rel="noopener noreferrer"
152
      href={baseMapLibraryConfig.mapLibUrl}
153
      aria-label={`${baseMapLibraryConfig.mapLibName} logo`}
154
    />
155
  </div>
156
);
157

158
interface StyledDroppableProps {
159
  isOver: boolean;
160
}
161

162
const StyledDroppable = styled.div<StyledDroppableProps>`
7✔
163
  background-color: ${props => (props.isOver ? props.theme.dndOverBackgroundColor : 'none')};
9!
164
  width: 100%;
165
  height: 100%;
166
  position: absolute;
167
  pointer-events: none;
168
  z-index: 1;
169
`;
170

171
export const isSplitSelector = props =>
7✔
172
  props.visState.splitMaps && props.visState.splitMaps.length > 1;
29✔
173

174
export const Droppable = ({containerId}) => {
7✔
175
  const {isOver, setNodeRef} = useDroppable({
9✔
176
    id: containerId,
177
    data: {type: DROPPABLE_MAP_CONTAINER_TYPE, index: containerId},
178
    disabled: !containerId
179
  });
180

181
  return <StyledDroppable ref={setNodeRef} isOver={isOver} />;
9✔
182
};
183

184
interface StyledDatasetAttributionsContainerProps {
185
  isPalm: boolean;
186
}
187

188
const StyledDatasetAttributionsContainer = styled.div<StyledDatasetAttributionsContainerProps>`
7✔
NEW
189
  max-width: ${props => (props.isPalm ? '200px' : '300px')};
×
190
  text-overflow: ellipsis;
191
  white-space: nowrap;
192
  overflow: hidden;
193
  color: ${props => props.theme.labelColor};
×
194
  margin-right: 2px;
195
  margin-bottom: 1px;
UNCOV
196
  line-height: ${props => (props.isPalm ? '1em' : '1.4em')};
×
197

198
  &:hover {
199
    white-space: inherit;
200
  }
201
`;
202

203
const DatasetAttributions = ({
7✔
204
  datasetAttributions,
205
  isPalm
206
}: {
207
  datasetAttributions: DatasetAttribution[];
208
  isPalm: boolean;
209
}) => (
210
  <>
25✔
211
    {datasetAttributions?.length ? (
25!
212
      <StyledDatasetAttributionsContainer isPalm={isPalm}>
213
        {datasetAttributions.map((ds, idx) => (
214
          <a
×
215
            {...(ds.url ? {href: ds.url} : null)}
×
216
            target="_blank"
217
            rel="noopener noreferrer"
218
            key={`${ds.title}_${idx}`}
219
          >
220
            {ds.title}
221
            {idx !== datasetAttributions.length - 1 ? ', ' : null}
×
222
          </a>
223
        ))}
224
      </StyledDatasetAttributionsContainer>
225
    ) : null}
226
  </>
227
);
228

229
type AttributionProps = {
230
  showBaseMapLibLogo: boolean;
231
  showOsmBasemapAttribution: boolean;
232
  datasetAttributions: DatasetAttribution[];
233
  baseMapLibraryConfig: BaseMapLibraryConfig;
234
};
235

236
export const Attribution: React.FC<AttributionProps> = ({
7✔
237
  showBaseMapLibLogo = true,
×
238
  showOsmBasemapAttribution = false,
×
239
  datasetAttributions,
240
  baseMapLibraryConfig
241
}: AttributionProps) => {
242
  const isPalm = hasMobileWidth(breakPointValues);
28✔
243

244
  const memoizedComponents = useMemo(() => {
28✔
245
    if (!showBaseMapLibLogo) {
25!
246
      return (
×
247
        <StyledAttrbution
248
          mapLibCssClass={baseMapLibraryConfig.mapLibCssClass}
249
          mapLibAttributionCssClass={baseMapLibraryConfig.mapLibAttributionCssClass}
250
        >
251
          <EndHorizontalFlexbox>
252
            <DatasetAttributions datasetAttributions={datasetAttributions} isPalm={isPalm} />
253
            {showOsmBasemapAttribution ? (
×
254
              <div className="attrition-link">
255
                {datasetAttributions?.length ? <span className="pipe-separator">|</span> : null}
×
256
                <a
257
                  href="http://www.openstreetmap.org/copyright"
258
                  target="_blank"
259
                  rel="noopener noreferrer"
260
                >
261
                  © OpenStreetMap
262
                </a>
263
              </div>
264
            ) : null}
265
          </EndHorizontalFlexbox>
266
        </StyledAttrbution>
267
      );
268
    }
269

270
    return (
25✔
271
      <StyledAttrbution
272
        mapLibCssClass={baseMapLibraryConfig.mapLibCssClass}
273
        mapLibAttributionCssClass={baseMapLibraryConfig.mapLibAttributionCssClass}
274
      >
275
        <EndHorizontalFlexbox>
276
          <DatasetAttributions datasetAttributions={datasetAttributions} isPalm={isPalm} />
277
          <div className="attrition-link">
278
            {datasetAttributions?.length ? <span className="pipe-separator">|</span> : null}
25!
279
            {isPalm ? <MapLibLogo baseMapLibraryConfig={baseMapLibraryConfig} /> : null}
25!
280
            <a href="https://kepler.gl/policy/" target="_blank" rel="noopener noreferrer">
281
              © kepler.gl |{' '}
282
            </a>
283
            {!isPalm ? <MapLibLogo baseMapLibraryConfig={baseMapLibraryConfig} /> : null}
25!
284
          </div>
285
        </EndHorizontalFlexbox>
286
      </StyledAttrbution>
287
    );
288
  }, [
289
    showBaseMapLibLogo,
290
    showOsmBasemapAttribution,
291
    datasetAttributions,
292
    isPalm,
293
    baseMapLibraryConfig
294
  ]);
295

296
  return memoizedComponents;
28✔
297
};
298

299
MapContainerFactory.deps = [MapPopoverFactory, MapControlFactory, EditorFactory];
7✔
300

301
type MapboxStyle = string | object | undefined;
302
type PropSelector<R> = Selector<MapContainerProps, R>;
303

304
export interface MapContainerProps {
305
  visState: VisState;
306
  mapState: MapState;
307
  mapControls: MapControls;
308
  mapStyle: {bottomMapStyle?: MapboxStyle; topMapStyle?: MapboxStyle} & MapStyle;
309
  mapboxApiAccessToken: string;
310
  mapboxApiUrl: string;
311
  visStateActions: typeof VisStateActions;
312
  mapStateActions: typeof MapStateActions;
313
  uiStateActions: typeof UIStateActions;
314

315
  // optional
316
  primary?: boolean; // primary one will be reporting its size to appState
317
  readOnly?: boolean;
318
  isExport?: boolean;
319
  // onMapStyleLoaded?: (map: maplibregl.Map | ReturnType<MapRef['getMap']> | null) => void;
320
  onMapStyleLoaded?: (map: GetMapRef | null) => void;
321
  onMapRender?: (map: GetMapRef | null) => void;
322
  getMapboxRef?: (mapbox?: MapRef | null, index?: number) => void;
323
  index?: number;
324
  deleteMapLabels?: (containerId: string, layerId: string) => void;
325
  containerId?: number;
326

327
  locale?: any;
328
  theme?: any;
329
  editor?: any;
330
  MapComponent?: typeof Map;
331
  deckGlProps?: any;
332
  onDeckInitialized?: (a: any, b: any) => void;
333
  onViewStateChange?: (viewport: Viewport) => void;
334

335
  topMapContainerProps: any;
336
  bottomMapContainerProps: any;
337
  transformRequest?: (url: string, resourceType?: string) => {url: string};
338

339
  datasetAttributions?: DatasetAttribution[];
340

341
  generateMapboxLayers?: typeof generateMapboxLayers;
342
  generateDeckGLLayers?: typeof computeDeckLayers;
343

344
  onMouseMove?: (event: React.MouseEvent & {lngLat?: [number, number]}) => void;
345

346
  children?: React.ReactNode;
347
  deckRenderCallbacks?: {
348
    onDeckLoad?: () => void;
349
    onDeckRender?: (deckProps: Record<string, unknown>) => Record<string, unknown> | null;
350
    onDeckAfterRender?: (deckProps: Record<string, unknown>) => any;
351
  };
352
}
353

354
export default function MapContainerFactory(
355
  MapPopover: ReturnType<typeof MapPopoverFactory>,
356
  MapControl: ReturnType<typeof MapControlFactory>,
357
  Editor: ReturnType<typeof EditorFactory>
358
): React.ComponentType<MapContainerProps> {
359
  class MapContainer extends Component<MapContainerProps> {
360
    displayName = 'MapContainer';
25✔
361

362
    static contextType = MapViewStateContext;
14✔
363

364
    declare context: React.ContextType<typeof MapViewStateContext>;
365

366
    static defaultProps = {
14✔
367
      MapComponent: Map,
368
      deckGlProps: {},
369
      index: 0,
370
      primary: true
371
    };
372

373
    constructor(props) {
374
      super(props);
25✔
375
    }
376

377
    state = {
25✔
378
      // Determines whether attribution should be visible based the result of loading the map style
379
      showBaseMapAttribution: true
380
    };
381

382
    componentDidMount() {
383
      if (!this._ref.current) {
25!
384
        return;
×
385
      }
386
      observeDimensions(this._ref.current, this._handleResize);
25✔
387
    }
388

389
    componentWillUnmount() {
390
      // unbind mapboxgl event listener
391
      if (this._map) {
2!
392
        this._map?.off(MAPBOXGL_STYLE_UPDATE, nop);
×
393
        this._map?.off(MAPBOXGL_RENDER, nop);
×
394
      }
395
      if (!this._ref.current) {
2!
396
        return;
×
397
      }
398
      unobserveDimensions(this._ref.current);
2✔
399
    }
400

401
    _deck: any = null;
25✔
402
    _map: GetMapRef | null = null;
25✔
403
    _ref = createRef<HTMLDivElement>();
25✔
404
    _deckGLErrorsElapsed: {[id: string]: number} = {};
25✔
405

406
    previousLayers = {
25✔
407
      // [layers.id]: mapboxLayerConfig
408
    };
409

410
    _handleResize = dimensions => {
25✔
411
      const {primary, index} = this.props;
×
412
      if (primary) {
×
413
        const {mapStateActions} = this.props;
×
414
        if (dimensions && dimensions.width > 0 && dimensions.height > 0) {
×
415
          mapStateActions.updateMap(dimensions, index);
×
416
        }
417
      }
418
    };
419

420
    layersSelector: PropSelector<VisState['layers']> = props => props.visState.layers;
56✔
421
    layerDataSelector: PropSelector<VisState['layers']> = props => props.visState.layerData;
56✔
422
    splitMapSelector: PropSelector<SplitMap[]> = props => props.visState.splitMaps;
28✔
423
    splitMapIndexSelector: PropSelector<number | undefined> = props => props.index;
28✔
424
    mapLayersSelector: PropSelector<SplitMapLayers | null | undefined> = createSelector(
25✔
425
      this.splitMapSelector,
426
      this.splitMapIndexSelector,
427
      getMapLayersFromSplitMaps
428
    );
429
    layerOrderSelector: PropSelector<VisState['layerOrder']> = props => props.visState.layerOrder;
25✔
430
    layersToRenderSelector: PropSelector<LayersToRender> = createSelector(
25✔
431
      this.layersSelector,
432
      this.layerDataSelector,
433
      this.mapLayersSelector,
434
      prepareLayersToRender
435
    );
436
    layersForDeckSelector = createSelector(
25✔
437
      this.layersSelector,
438
      this.layerDataSelector,
439
      prepareLayersForDeck
440
    );
441
    filtersSelector = props => props.visState.filters;
28✔
442
    polygonFiltersSelector = createSelector(this.filtersSelector, filters =>
25✔
443
      filters.filter(f => f.type === FILTER_TYPES.polygon && f.enabled !== false)
26!
444
    );
445
    featuresSelector = props => props.visState.editor.features;
28✔
446
    selectedFeatureSelector = props => props.visState.editor.selectedFeature;
28✔
447
    featureCollectionSelector = createSelector(
25✔
448
      this.polygonFiltersSelector,
449
      this.featuresSelector,
450
      (polygonFilters, features) => ({
26✔
451
        type: 'FeatureCollection',
452
        features: features.concat(polygonFilters.map(f => f.value))
×
453
      })
454
    );
455
    // @ts-ignore - No overload matches this call
456
    selectedPolygonIndexSelector = createSelector(
25✔
457
      this.featureCollectionSelector,
458
      this.selectedFeatureSelector,
459
      (collection, selectedFeature) =>
460
        collection.features.findIndex(f => f.id === selectedFeature?.id)
26✔
461
    );
462
    selectedFeatureIndexArraySelector = createSelector(
25✔
463
      (value: number) => value,
25✔
464
      value => {
465
        return value < 0 ? [] : [value];
25!
466
      }
467
    );
468

469
    generateMapboxLayerMethodSelector = props => props.generateMapboxLayers ?? generateMapboxLayers;
25!
470

471
    mapboxLayersSelector = createSelector(
25✔
472
      this.layersSelector,
473
      this.layerDataSelector,
474
      this.layerOrderSelector,
475
      this.layersToRenderSelector,
476
      this.generateMapboxLayerMethodSelector,
477
      (layer, layerData, layerOrder, layersToRender, generateMapboxLayerMethod) =>
478
        generateMapboxLayerMethod(layer, layerData, layerOrder, layersToRender)
×
479
    );
480

481
    // merge in a background-color style if the basemap choice is NO_MAP_ID
482
    // used by <StyledMap> inline style prop
483
    mapStyleTypeSelector = props => props.mapStyle.styleType;
28✔
484
    mapStyleBackgroundColorSelector = props => props.mapStyle.backgroundColor;
28✔
485
    styleSelector = createSelector(
25✔
486
      this.mapStyleTypeSelector,
487
      this.mapStyleBackgroundColorSelector,
488
      (styleType, backgroundColor) => ({
26✔
489
        ...MAP_STYLE.container,
490
        ...(styleType === NO_MAP_ID ? {backgroundColor: rgbToHex(backgroundColor)} : {})
26!
491
      })
492
    );
493

494
    /* component private functions */
495
    _onCloseMapPopover = () => {
25✔
496
      this.props.visStateActions.onLayerClick(null);
×
497
    };
498

499
    _onLayerHover = (_idx: number, info: PickInfo<any> | null) => {
25✔
500
      this.props.visStateActions.onLayerHover(info, this.props.index);
×
501
    };
502

503
    _onLayerSetDomain = (
25✔
504
      idx: number,
505
      value: {domain: VisualChannelDomain; aggregatedBins: AggregatedBin[]}
506
    ) => {
507
      this.props.visStateActions.layerConfigChange(this.props.visState.layers[idx], {
×
508
        colorDomain: value.domain,
509
        aggregatedBins: value.aggregatedBins
510
      } as Partial<LayerBaseConfig>);
511
    };
512

513
    _onLayerFilteredItemsChange = (idx, event) => {
25✔
514
      this.props.visStateActions.layerFilteredItemsChange(this.props.visState.layers[idx], event);
×
515
    };
516

517
    _handleMapToggleLayer = layerId => {
25✔
518
      const {index: mapIndex = 0, visStateActions} = this.props;
1!
519
      visStateActions.toggleLayerForMap(mapIndex, layerId);
1✔
520
    };
521

522
    _onMapboxStyleUpdate = update => {
25✔
523
      // force refresh mapboxgl layers
524
      this.previousLayers = {};
×
525
      this._updateMapboxLayers();
×
526

527
      if (update && update.style) {
×
528
        // No attributions are needed if the style doesn't reference Mapbox sources
529
        this.setState({
×
530
          showBaseMapAttribution:
531
            isStyleUsingMapboxTiles(update.style) || !isStyleUsingOpenStreetMapTiles(update.style)
×
532
        });
533
      }
534

535
      if (typeof this.props.onMapStyleLoaded === 'function') {
×
536
        this.props.onMapStyleLoaded(this._map);
×
537
      }
538
    };
539

540
    _setMapRef = mapRef => {
25✔
541
      // Handle change of the map library
542
      if (this._map && mapRef) {
×
543
        const map = mapRef.getMap();
×
544
        if (map && this._map !== map) {
×
545
          this._map?.off(MAPBOXGL_STYLE_UPDATE, nop);
×
546
          this._map?.off(MAPBOXGL_RENDER, nop);
×
547
          this._map = null;
×
548
        }
549
      }
550

551
      if (!this._map && mapRef) {
×
552
        this._map = mapRef.getMap();
×
553
        // i noticed in certain context we don't access the actual map element
554
        if (!this._map) {
×
555
          return;
×
556
        }
557
        // bind mapboxgl event listener
558
        this._map.on(MAPBOXGL_STYLE_UPDATE, this._onMapboxStyleUpdate);
×
559

560
        this._map.on(MAPBOXGL_RENDER, () => {
×
561
          if (typeof this.props.onMapRender === 'function') {
×
562
            this.props.onMapRender(this._map);
×
563
          }
564
        });
565
      }
566

567
      if (this.props.getMapboxRef) {
×
568
        // The parent component can gain access to our MapboxGlMap by
569
        // providing this callback. Note that 'mapbox' will be null when the
570
        // ref is unset (e.g. when a split map is closed).
571
        this.props.getMapboxRef(mapRef, this.props.index);
×
572
      }
573
    };
574

575
    _onDeckInitialized(gl) {
576
      if (this.props.onDeckInitialized) {
×
577
        this.props.onDeckInitialized(this._deck, gl);
×
578
      }
579
    }
580

581
    /**
582
     * 1) Allow effects only for the first view.
583
     * 2) Prevent effect:preRender call without valid generated viewports.
584
     * @param viewIndex View index.
585
     * @returns Returns true if effects can be used.
586
     */
587
    _isOKToRenderEffects(viewIndex?: number): boolean {
588
      return !viewIndex && Boolean(this._deck?.viewManager?._viewports?.length);
29✔
589
    }
590

591
    _onBeforeRender = ({gl}) => {
25✔
592
      setLayerBlending(gl, this.props.visState.layerBlending);
×
593
    };
594

595
    _onDeckError = (error, layer) => {
25✔
596
      const errorMessage = error?.message || 'unknown-error';
23!
597
      const layerMessage = layer?.id ? ` in ${layer.id} layer` : '';
23!
598
      const errorMessageFull =
599
        errorMessage === 'WebGL context is lost'
23!
600
          ? 'Your GPU was disconnected. This can happen if your computer goes to sleep. It can also occur for other reasons, such as if you are running too many GPU applications.'
601
          : `An error in deck.gl: ${errorMessage}${layerMessage}.`;
602

603
      // Throttle error notifications, as React doesn't like too many state changes from here.
604
      const lastShown = this._deckGLErrorsElapsed[errorMessageFull];
23✔
605
      if (!lastShown || lastShown < Date.now() - THROTTLE_NOTIFICATION_TIME) {
23!
606
        this._deckGLErrorsElapsed[errorMessageFull] = Date.now();
23✔
607

608
        // Mark layer as invalid
609
        let extraLayerMessage = '';
23✔
610
        const {visStateActions} = this.props;
23✔
611
        if (layer) {
23!
612
          let topMostLayer = layer;
×
613
          while (topMostLayer.parent) {
×
614
            topMostLayer = topMostLayer.parent;
×
615
          }
616
          if (topMostLayer.props?.id) {
×
617
            visStateActions.layerSetIsValid(topMostLayer, false);
×
618
            extraLayerMessage = 'The layer has been disabled and highlighted.';
×
619
          }
620
        }
621

622
        // Create new error notification or update existing one with same id.
623
        // Update is required to preserve the order of notifications as they probably are going to "jump" based on order of errors.
624
        const {uiStateActions} = this.props;
23✔
625
        uiStateActions.addNotification(
23✔
626
          errorNotification({
627
            message: `${errorMessageFull} ${extraLayerMessage}`,
628
            id: errorMessageFull // treat the error message as id
629
          })
630
        );
631
      }
632
    };
633

634
    /* component render functions */
635

636
    /* eslint-disable complexity */
637
    _renderMapPopover() {
638
      // this check is for limiting the display of the `<MapPopover>` to the `<MapContainer>` the user is interacting with
639
      // the DeckGL onHover event handler adds a `mapIndex` property which is available in the `hoverInfo` object of `visState`
640
      if (this.props.index !== this.props.visState.hoverInfo?.mapIndex) {
29!
641
        return null;
29✔
642
      }
643

644
      // TODO: move this into reducer so it can be tested
645
      const {
646
        mapState,
647
        visState: {
648
          hoverInfo,
649
          clicked,
650
          datasets,
651
          interactionConfig,
652
          animationConfig,
653
          layers,
654
          mousePos: {mousePosition, coordinate, pinned}
655
        }
656
      } = this.props;
×
657
      const layersToRender = this.layersToRenderSelector(this.props);
×
658

659
      if (!mousePosition || !interactionConfig.tooltip) {
×
660
        return null;
×
661
      }
662

663
      const layerHoverProp = getLayerHoverProp({
×
664
        animationConfig,
665
        interactionConfig,
666
        hoverInfo,
667
        layers,
668
        layersToRender,
669
        datasets
670
      });
671

672
      const compareMode = interactionConfig.tooltip.config
×
673
        ? interactionConfig.tooltip.config.compareMode
674
        : false;
675

676
      let pinnedPosition = {x: 0, y: 0};
×
677
      let layerPinnedProp: LayerHoverProp | null = null;
×
678
      if (pinned || clicked) {
×
679
        // project lnglat to screen so that tooltip follows the object on zoom
680
        const viewport = getViewportFromMapState(mapState);
×
681
        const lngLat = clicked ? clicked.coordinate : pinned.coordinate;
×
682
        pinnedPosition = this._getHoverXY(viewport, lngLat);
×
683
        layerPinnedProp = getLayerHoverProp({
×
684
          animationConfig,
685
          interactionConfig,
686
          hoverInfo: clicked,
687
          layers,
688
          layersToRender,
689
          datasets
690
        });
691
        if (layerHoverProp && layerPinnedProp) {
×
692
          layerHoverProp.primaryData = layerPinnedProp.data;
×
693
          layerHoverProp.compareType = interactionConfig.tooltip.config.compareType;
×
694
        }
695
      }
696

697
      const commonProp = {
×
698
        onClose: this._onCloseMapPopover,
699
        zoom: mapState.zoom,
700
        container: this._deck ? this._deck.canvas : undefined
×
701
      };
702

703
      return (
×
704
        <ErrorBoundary>
705
          {layerPinnedProp && (
×
706
            <MapPopover
707
              {...pinnedPosition}
708
              {...commonProp}
709
              layerHoverProp={layerPinnedProp}
710
              coordinate={interactionConfig.coordinate.enabled && (pinned || {}).coordinate}
×
711
              frozen={true}
712
              isBase={compareMode}
713
              onSetFeatures={this.props.visStateActions.setFeatures}
714
              setSelectedFeature={this.props.visStateActions.setSelectedFeature}
715
              // @ts-ignore Argument of type 'Readonly<MapContainerProps>' is not assignable to parameter of type 'never'
716
              featureCollection={this.featureCollectionSelector(this.props)}
717
            />
718
          )}
719
          {layerHoverProp && (!layerPinnedProp || compareMode) && (
×
720
            <MapPopover
721
              x={mousePosition[0]}
722
              y={mousePosition[1]}
723
              {...commonProp}
724
              layerHoverProp={layerHoverProp}
725
              frozen={false}
726
              coordinate={interactionConfig.coordinate.enabled && coordinate}
×
727
              onSetFeatures={this.props.visStateActions.setFeatures}
728
              setSelectedFeature={this.props.visStateActions.setSelectedFeature}
729
              // @ts-ignore Argument of type 'Readonly<MapContainerProps>' is not assignable to parameter of type 'never'
730
              featureCollection={this.featureCollectionSelector(this.props)}
731
            />
732
          )}
733
        </ErrorBoundary>
734
      );
735
    }
736

737
    /* eslint-enable complexity */
738

739
    _getHoverXY(viewport, lngLat) {
740
      const screenCoord = !viewport || !lngLat ? null : viewport.project(lngLat);
×
741
      return screenCoord && {x: screenCoord[0], y: screenCoord[1]};
×
742
    }
743

744
    _renderDeckOverlay(
745
      layersForDeck,
746
      options: {primaryMap: boolean; isInteractive?: boolean; children?: React.ReactNode} = {
×
747
        primaryMap: false
748
      }
749
    ) {
750
      const {
751
        mapStyle,
752
        visState,
753
        mapState,
754
        visStateActions,
755
        mapboxApiAccessToken,
756
        mapboxApiUrl,
757
        deckGlProps,
758
        index,
759
        mapControls,
760
        deckRenderCallbacks,
761
        theme,
762
        generateDeckGLLayers,
763
        onMouseMove
764
      } = this.props;
29✔
765

766
      const {hoverInfo, editor} = visState;
29✔
767
      const {primaryMap, isInteractive, children} = options;
29✔
768

769
      // disable double click zoom when editor is in any draw mode
770
      const {mapDraw} = mapControls;
29✔
771
      const {active: editorMenuActive = false} = mapDraw || {};
29✔
772
      const isEditorDrawingMode = EditorLayerUtils.isDrawingActive(editorMenuActive, editor.mode);
29✔
773

774
      const internalViewState = this.context?.getInternalViewState(index);
29✔
775
      const internalMapState = {...mapState, ...internalViewState};
29✔
776
      const viewport = getViewportFromMapState(internalMapState);
29✔
777

778
      const editorFeatureSelectedIndex = this.selectedPolygonIndexSelector(this.props);
29✔
779

780
      const {setFeatures, onLayerClick, setSelectedFeature} = visStateActions;
29✔
781

782
      const generateDeckGLLayersMethod = generateDeckGLLayers ?? computeDeckLayers;
29✔
783
      const deckGlLayers = generateDeckGLLayersMethod(
29✔
784
        {
785
          visState,
786
          mapState: internalMapState,
787
          mapStyle
788
        },
789
        {
790
          mapIndex: index,
791
          primaryMap,
792
          mapboxApiAccessToken,
793
          mapboxApiUrl,
794
          layersForDeck,
795
          editorInfo: primaryMap
29!
796
            ? {
797
                editor,
798
                editorMenuActive,
799
                onSetFeatures: setFeatures,
800
                setSelectedFeature,
801
                // @ts-ignore Argument of type 'Readonly<MapContainerProps>' is not assignable to parameter of type 'never'
802
                featureCollection: this.featureCollectionSelector(this.props),
803
                selectedFeatureIndexes: this.selectedFeatureIndexArraySelector(
804
                  // @ts-ignore Argument of type 'unknown' is not assignable to parameter of type 'number'.
805
                  editorFeatureSelectedIndex
806
                ),
807
                viewport
808
              }
809
            : undefined
810
        },
811
        {
812
          onLayerHover: this._onLayerHover,
813
          onSetLayerDomain: this._onLayerSetDomain,
814
          onFilteredItemsChange: this._onLayerFilteredItemsChange
815
        },
816
        deckGlProps
817
      );
818

819
      const extraDeckParams: {
820
        getTooltip?: (info: any) => object | null;
821
        getCursor?: ({isDragging}: {isDragging: boolean}) => string;
822
      } = {};
29✔
823
      if (primaryMap) {
29!
824
        extraDeckParams.getTooltip = info =>
29✔
825
          EditorLayerUtils.getTooltip(info, {
×
826
            editorMenuActive,
827
            editor,
828
            theme
829
          });
830

831
        extraDeckParams.getCursor = ({isDragging}: {isDragging: boolean}) => {
29✔
832
          const editorCursor = EditorLayerUtils.getCursor({
×
833
            editorMenuActive,
834
            editor,
835
            hoverInfo
836
          });
837
          if (editorCursor) return editorCursor;
×
838

839
          if (isDragging) return 'grabbing';
×
840
          if (hoverInfo?.layer) return 'pointer';
×
841
          return 'grab';
×
842
        };
843
      }
844

845
      const effects = this._isOKToRenderEffects(index)
29!
846
        ? computeDeckEffects({visState, mapState})
847
        : [];
848

849
      const views = deckGlProps?.views
29!
850
        ? deckGlProps?.views()
851
        : new MapView({legacyMeterSizes: true});
852

853
      let allDeckGlProps = {
29✔
854
        ...deckGlProps,
855
        pickingRadius: DEFAULT_PICKING_RADIUS,
856
        views,
857
        layers: deckGlLayers,
858
        effects
859
      };
860

861
      if (typeof deckRenderCallbacks?.onDeckRender === 'function') {
29!
862
        allDeckGlProps = deckRenderCallbacks.onDeckRender(allDeckGlProps);
×
863
        if (!allDeckGlProps) {
×
864
          // if onDeckRender returns null, do not render deck.gl
865
          return null;
×
866
        }
867
      }
868

869
      return (
29✔
870
        <div
871
          {...(isInteractive
29!
872
            ? {
873
                onMouseMove: primaryMap
29!
874
                  ? event => {
875
                      onMouseMove?.(event);
×
876
                      this._onMouseMoveDebounced(event, viewport);
×
877
                    }
878
                  : undefined
879
              }
880
            : {style: {pointerEvents: 'none'}})}
881
        >
882
          <DeckGL
883
            id="default-deckgl-overlay"
884
            onLoad={() => {
885
              if (typeof deckRenderCallbacks?.onDeckLoad === 'function') {
×
886
                deckRenderCallbacks.onDeckLoad();
×
887
              }
888
            }}
889
            {...allDeckGlProps}
890
            controller={
891
              isInteractive
29!
892
                ? {
893
                    doubleClickZoom: !isEditorDrawingMode,
894
                    dragRotate: this.props.mapState.dragRotate
895
                  }
896
                : false
897
            }
898
            initialViewState={internalViewState}
899
            onBeforeRender={this._onBeforeRender}
900
            onViewStateChange={isInteractive ? this._onViewportChange : undefined}
29!
901
            {...extraDeckParams}
902
            onHover={
903
              isInteractive
29!
904
                ? data => {
905
                    const res = EditorLayerUtils.onHover(data, {
×
906
                      editorMenuActive,
907
                      editor,
908
                      hoverInfo
909
                    });
910
                    if (res) return;
×
911

912
                    this._onLayerHoverDebounced(data, index);
×
913
                  }
914
                : null
915
            }
916
            onClick={(data, event) => {
917
              // @ts-ignore
918
              normalizeEvent(event.srcEvent, viewport);
×
919
              const res = EditorLayerUtils.onClick(data, event, {
×
920
                editorMenuActive,
921
                editor,
922
                onLayerClick,
923
                setSelectedFeature,
924
                mapIndex: index
925
              });
926
              if (res) return;
×
927

928
              visStateActions.onLayerClick(data);
×
929
            }}
930
            onError={this._onDeckError}
931
            ref={comp => {
932
              // @ts-ignore
933
              if (comp && comp.deck && !this._deck) {
35✔
934
                // @ts-ignore
935
                this._deck = comp.deck;
1✔
936
              }
937
            }}
938
            onWebGLInitialized={gl => this._onDeckInitialized(gl)}
×
939
            onAfterRender={() => {
940
              if (typeof deckRenderCallbacks?.onDeckAfterRender === 'function') {
×
941
                deckRenderCallbacks.onDeckAfterRender(allDeckGlProps);
×
942
              }
943
            }}
944
          >
945
            {children}
946
          </DeckGL>
947
        </div>
948
      );
949
    }
950

951
    _updateMapboxLayers() {
952
      const mapboxLayers = this.mapboxLayersSelector(this.props);
×
953
      if (!Object.keys(mapboxLayers).length && !Object.keys(this.previousLayers).length) {
×
954
        return;
×
955
      }
956

957
      updateMapboxLayers(this._map, mapboxLayers, this.previousLayers);
×
958

959
      this.previousLayers = mapboxLayers;
×
960
    }
961

962
    _renderMapboxOverlays() {
963
      if (this._map && this._map.isStyleLoaded()) {
29!
964
        this._updateMapboxLayers();
×
965
      }
966
    }
967
    _onViewportChangePropagateDebounced = debounce(() => {
25✔
968
      const viewState = this.context?.getInternalViewState(this.props.index);
×
969
      onViewPortChange(
×
970
        viewState,
971
        this.props.mapStateActions.updateMap,
972
        this.props.onViewStateChange,
973
        this.props.primary,
974
        this.props.index
975
      );
976
    }, DEBOUNCE_VIEWPORT_PROPAGATE);
977

978
    _onViewportChange = viewport => {
25✔
979
      const {viewState} = viewport;
×
980
      if (this.props.isExport) {
×
981
        // Image export map shouldn't be interactive (otherwise this callback can
982
        // lead to inadvertent changes to the state of the main map)
983
        return;
×
984
      }
985
      const {setInternalViewState} = this.context;
×
986
      setInternalViewState(viewState, this.props.index);
×
987
      this._onViewportChangePropagateDebounced();
×
988
    };
989

990
    _onLayerHoverDebounced = debounce((data, index) => {
25✔
991
      this.props.visStateActions.onLayerHover(data, index);
×
992
    }, DEBOUNCE_MOUSE_MOVE_PROPAGATE);
993

994
    _onMouseMoveDebounced = debounce((event, viewport) => {
25✔
995
      this.props.visStateActions.onMouseMove(normalizeEvent(event, viewport));
×
996
    }, DEBOUNCE_MOUSE_MOVE_PROPAGATE);
997

998
    _toggleMapControl = panelId => {
25✔
999
      const {index, uiStateActions} = this.props;
2✔
1000

1001
      uiStateActions.toggleMapControl(panelId, Number(index));
2✔
1002
    };
1003

1004
    /* eslint-disable complexity */
1005
    _renderMap() {
1006
      const {
1007
        visState,
1008
        mapState,
1009
        mapStyle,
1010
        mapStateActions,
1011
        MapComponent = Map,
×
1012
        mapboxApiAccessToken,
1013
        // mapboxApiUrl,
1014
        mapControls,
1015
        isExport,
1016
        locale,
1017
        uiStateActions,
1018
        visStateActions,
1019
        index,
1020
        primary,
1021
        bottomMapContainerProps,
1022
        topMapContainerProps,
1023
        theme,
1024
        datasetAttributions = [],
×
1025
        containerId = 0
13✔
1026
      } = this.props;
29✔
1027

1028
      const {layers, datasets, editor, interactionConfig} = visState;
29✔
1029

1030
      const layersToRender = this.layersToRenderSelector(this.props);
29✔
1031
      const layersForDeck = this.layersForDeckSelector(this.props);
29✔
1032

1033
      // Current style can be a custom style, from which we pull the mapbox API acccess token
1034
      const currentStyle = mapStyle.mapStyles?.[mapStyle.styleType];
29✔
1035
      const baseMapLibraryName = getBaseMapLibrary(currentStyle);
29✔
1036
      const baseMapLibraryConfig =
1037
        getApplicationConfig().baseMapLibraryConfig?.[baseMapLibraryName];
29✔
1038

1039
      const internalViewState = this.context?.getInternalViewState(index);
29✔
1040
      const mapProps = {
29✔
1041
        ...internalViewState,
1042
        preserveDrawingBuffer: true,
1043
        mapboxAccessToken: currentStyle?.accessToken || mapboxApiAccessToken,
58✔
1044
        // baseApiUrl: mapboxApiUrl,
1045
        mapLib: baseMapLibraryConfig.getMapLib(),
1046
        transformRequest:
1047
          this.props.transformRequest ||
58✔
1048
          transformRequest(currentStyle?.accessToken || mapboxApiAccessToken)
58✔
1049
      };
1050

1051
      const hasGeocoderLayer = Boolean(layers.find(l => l.id === GEOCODER_LAYER_ID));
29✔
1052
      const isSplit = Boolean(mapState.isSplit);
29✔
1053

1054
      const deck = this._renderDeckOverlay(layersForDeck, {
29✔
1055
        primaryMap: true,
1056
        isInteractive: true,
1057
        children: (
1058
          <MapComponent
1059
            key={`bottom-${baseMapLibraryName}`}
1060
            {...mapProps}
1061
            mapStyle={mapStyle.bottomMapStyle ?? EMPTY_MAPBOX_STYLE}
58✔
1062
            {...bottomMapContainerProps}
1063
            ref={this._setMapRef}
1064
          />
1065
        )
1066
      });
1067
      if (!deck) {
29!
1068
        // deckOverlay can be null if onDeckRender returns null
1069
        // in this case we don't want to render the map
1070
        return null;
×
1071
      }
1072
      return (
29✔
1073
        <>
1074
          <MapControl
1075
            mapState={mapState}
1076
            datasets={datasets}
1077
            availableLocales={LOCALE_CODES_ARRAY}
1078
            dragRotate={mapState.dragRotate}
1079
            isSplit={isSplit}
1080
            primary={Boolean(primary)}
1081
            isExport={isExport}
1082
            layers={layers}
1083
            layersToRender={layersToRender}
1084
            mapIndex={index || 0}
53✔
1085
            mapControls={mapControls}
1086
            readOnly={this.props.readOnly}
1087
            scale={mapState.scale || 1}
58✔
1088
            top={
1089
              interactionConfig.geocoder && interactionConfig.geocoder.enabled
87!
1090
                ? theme.mapControlTop
1091
                : 0
1092
            }
1093
            editor={editor}
1094
            locale={locale}
1095
            onTogglePerspective={mapStateActions.togglePerspective}
1096
            onToggleSplitMap={mapStateActions.toggleSplitMap}
1097
            onMapToggleLayer={this._handleMapToggleLayer}
1098
            onToggleMapControl={this._toggleMapControl}
1099
            onToggleSplitMapViewport={mapStateActions.toggleSplitMapViewport}
1100
            onSetEditorMode={visStateActions.setEditorMode}
1101
            onSetLocale={uiStateActions.setLocale}
1102
            onToggleEditorVisibility={visStateActions.toggleEditorVisibility}
1103
            onLayerVisConfigChange={visStateActions.layerVisConfigChange}
1104
            mapHeight={mapState.height}
1105
          />
1106
          {isSplitSelector(this.props) && <Droppable containerId={containerId} />}
38✔
1107

1108
          {deck}
1109
          {this._renderMapboxOverlays()}
1110
          <Editor
1111
            index={index || 0}
53✔
1112
            datasets={datasets}
1113
            editor={editor}
1114
            filters={this.polygonFiltersSelector(this.props)}
1115
            layers={layers}
1116
            onDeleteFeature={visStateActions.deleteFeature}
1117
            onSelect={visStateActions.setSelectedFeature}
1118
            onTogglePolygonFilter={visStateActions.setPolygonFilterLayer}
1119
            onSetEditorMode={visStateActions.setEditorMode}
1120
            style={{
1121
              pointerEvents: 'all',
1122
              position: 'absolute',
1123
              display: editor.visible ? 'block' : 'none'
29!
1124
            }}
1125
          />
1126
          {this.props.children}
1127
          {mapStyle.topMapStyle ? (
29!
1128
            <MapComponent
1129
              key={`top-${baseMapLibraryName}`}
1130
              viewState={internalViewState}
1131
              mapStyle={mapStyle.topMapStyle}
1132
              style={MAP_STYLE.top}
1133
              mapboxAccessToken={mapProps.mapboxAccessToken}
1134
              transformRequest={mapProps.transformRequest}
1135
              mapLib={baseMapLibraryConfig.getMapLib()}
1136
              {...topMapContainerProps}
1137
            />
1138
          ) : null}
1139

1140
          {hasGeocoderLayer
29!
1141
            ? this._renderDeckOverlay(
1142
                {[GEOCODER_LAYER_ID]: hasGeocoderLayer},
1143
                {primaryMap: false, isInteractive: false}
1144
              )
1145
            : null}
1146
          {this._renderMapPopover()}
1147
          {this.props.primary ? (
29✔
1148
            <Attribution
1149
              showBaseMapLibLogo={this.state.showBaseMapAttribution}
1150
              showOsmBasemapAttribution={true}
1151
              datasetAttributions={datasetAttributions}
1152
              baseMapLibraryConfig={baseMapLibraryConfig}
1153
            />
1154
          ) : null}
1155
        </>
1156
      );
1157
    }
1158

1159
    render() {
1160
      const {visState, mapStyle} = this.props;
29✔
1161
      const mapContent = this._renderMap();
29✔
1162
      if (!mapContent) {
29!
1163
        // mapContent can be null if onDeckRender returns null
1164
        // in this case we don't want to render the map
1165
        return null;
×
1166
      }
1167

1168
      const currentStyle = mapStyle.mapStyles?.[mapStyle.styleType];
29✔
1169
      const baseMapLibraryName = getBaseMapLibrary(currentStyle);
29✔
1170
      const baseMapLibraryConfig =
1171
        getApplicationConfig().baseMapLibraryConfig?.[baseMapLibraryName];
29✔
1172

1173
      return (
29✔
1174
        <StyledMap
1175
          ref={this._ref}
1176
          style={this.styleSelector(this.props)}
1177
          onContextMenu={event => event.preventDefault()}
×
1178
          mixBlendMode={visState.overlayBlending}
1179
          mapLibCssClass={baseMapLibraryConfig.mapLibCssClass}
1180
        >
1181
          {mapContent}
1182
        </StyledMap>
1183
      );
1184
    }
1185
  }
1186

1187
  return withTheme(MapContainer);
14✔
1188
}
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