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

keplergl / kepler.gl / 12650563989

07 Jan 2025 11:20AM UTC coverage: 66.757% (-0.06%) from 66.819%
12650563989

push

github

web-flow
[feat] mapbox and maplibre simultaneous support (#2897)

- switch between basemap style libraries
- add back mapbox styles in Basemap Style tab

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

5967 of 10394 branches covered (57.41%)

Branch coverage included in aggregate %.

26 of 46 new or added lines in 6 files covered. (56.52%)

1 existing line in 1 file now uncovered.

12243 of 16884 relevant lines covered (72.51%)

89.5 hits per line

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

45.29
/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 {MapState, MapControls, Viewport, SplitMap, SplitMapLayers} from '@kepler.gl/types';
37
import {
38
  errorNotification,
39
  setLayerBlending,
40
  isStyleUsingMapboxTiles,
41
  isStyleUsingOpenStreetMapTiles,
42
  getBaseMapLibrary,
43
  BaseMapLibraryConfig,
44
  transformRequest,
45
  observeDimensions,
46
  unobserveDimensions,
47
  hasMobileWidth,
48
  getMapLayersFromSplitMaps,
49
  onViewPortChange,
50
  getViewportFromMapState,
51
  normalizeEvent,
52
  rgbToHex,
53
  computeDeckEffects,
54
  getApplicationConfig,
55
  GetMapRef
56
} from '@kepler.gl/utils';
57
import {breakPointValues} from '@kepler.gl/styles';
58

59
// default-settings
60
import {
61
  FILTER_TYPES,
62
  GEOCODER_LAYER_ID,
63
  THROTTLE_NOTIFICATION_TIME,
64
  DEFAULT_PICKING_RADIUS,
65
  NO_MAP_ID,
66
  EMPTY_MAPBOX_STYLE,
67
  DROPPABLE_MAP_CONTAINER_TYPE
68
} from '@kepler.gl/constants';
69

70
// Contexts
71
import {MapViewStateContext} from './map-view-state-context';
72

73
import ErrorBoundary from './common/error-boundary';
74
import {DatasetAttribution} from './types';
75
import {LOCALE_CODES} from '@kepler.gl/localization';
76
import {MapView} from '@deck.gl/core';
77
import {
78
  MapStyle,
79
  computeDeckLayers,
80
  getLayerHoverProp,
81
  LayerHoverProp,
82
  prepareLayersForDeck,
83
  prepareLayersToRender,
84
  LayersToRender
85
} from '@kepler.gl/reducers';
86
import {VisState} from '@kepler.gl/schemas';
87

88
// Debounce the propagation of viewport change and mouse moves to redux store.
89
// This is to avoid too many renders of other components when the map is
90
// being panned/zoomed (leading to laggy basemap/deck syncing).
91
const DEBOUNCE_VIEWPORT_PROPAGATE = 10;
7✔
92
const DEBOUNCE_MOUSE_MOVE_PROPAGATE = 10;
7✔
93

94
const MAP_STYLE: {[key: string]: React.CSSProperties} = {
7✔
95
  container: {
96
    display: 'inline-block',
97
    position: 'relative',
98
    width: '100%',
99
    height: '100%'
100
  },
101
  top: {
102
    position: 'absolute',
103
    top: 0,
104
    width: '100%',
105
    height: '100%',
106
    pointerEvents: 'none'
107
  }
108
};
109

110
const LOCALE_CODES_ARRAY = Object.keys(LOCALE_CODES);
7✔
111

112
interface StyledMapContainerProps {
113
  mixBlendMode?: string;
114
  mapLibCssClass: string;
115
}
116

117
const StyledMap = styled(StyledMapContainer)<StyledMapContainerProps>(
7✔
118
  ({mixBlendMode = 'normal', mapLibCssClass}) => `
29!
119
  #default-deckgl-overlay {
120
    mix-blend-mode: ${mixBlendMode};
121
  };
122
  *[${mapLibCssClass}-children] {
123
    position: absolute;
124
  }
125
`
126
);
127

128
const MAPBOXGL_STYLE_UPDATE = 'style.load';
7✔
129
const MAPBOXGL_RENDER = 'render';
7✔
130
const nop = () => {
7✔
131
  return;
×
132
};
133

134
type MapLibLogoProps = {
135
  baseMapLibraryConfig: BaseMapLibraryConfig;
136
};
137

138
const MapLibLogo = ({baseMapLibraryConfig}: MapLibLogoProps) => (
7✔
139
  <div className="attrition-logo">
28✔
140
    Basemap by:
141
    <a
142
      style={{marginLeft: '5px'}}
143
      className={`${baseMapLibraryConfig.mapLibCssClass}-ctrl-logo`}
144
      target="_blank"
145
      rel="noopener noreferrer"
146
      href={baseMapLibraryConfig.mapLibUrl}
147
      aria-label={`${baseMapLibraryConfig.mapLibName} logo`}
148
    />
149
  </div>
150
);
151

152
interface StyledDroppableProps {
153
  isOver: boolean;
154
}
155

156
const StyledDroppable = styled.div<StyledDroppableProps>`
7✔
157
  background-color: ${props => (props.isOver ? props.theme.dndOverBackgroundColor : 'none')};
9!
158
  width: 100%;
159
  height: 100%;
160
  position: absolute;
161
  pointer-events: none;
162
  z-index: 1;
163
`;
164

165
export const isSplitSelector = props =>
7✔
166
  props.visState.splitMaps && props.visState.splitMaps.length > 1;
29✔
167

168
export const Droppable = ({containerId}) => {
7✔
169
  const {isOver, setNodeRef} = useDroppable({
9✔
170
    id: containerId,
171
    data: {type: DROPPABLE_MAP_CONTAINER_TYPE, index: containerId},
172
    disabled: !containerId
173
  });
174

175
  return <StyledDroppable ref={setNodeRef} isOver={isOver} />;
9✔
176
};
177

178
interface StyledDatasetAttributionsContainerProps {
179
  isPalm: boolean;
180
}
181

182
const StyledDatasetAttributionsContainer = styled.div<StyledDatasetAttributionsContainerProps>`
7✔
183
  max-width: ${props => (props.isPalm ? '130px' : '180px')};
×
184
  text-overflow: ellipsis;
185
  white-space: nowrap;
186
  overflow: hidden;
187
  color: ${props => props.theme.labelColor};
×
188
  margin-right: 2px;
189
  line-height: ${props => (props.isPalm ? '1em' : '1.4em')};
×
190

191
  &:hover {
192
    white-space: inherit;
193
  }
194
`;
195

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

222
type AttributionProps = {
223
  showBaseMapLibLogo: boolean;
224
  showOsmBasemapAttribution: boolean;
225
  datasetAttributions: DatasetAttribution[];
226
  baseMapLibraryConfig: BaseMapLibraryConfig;
227
};
228

229
export const Attribution: React.FC<AttributionProps> = ({
7✔
230
  showBaseMapLibLogo = true,
×
231
  showOsmBasemapAttribution = false,
×
232
  datasetAttributions,
233
  baseMapLibraryConfig
234
}: AttributionProps) => {
235
  const isPalm = hasMobileWidth(breakPointValues);
28✔
236

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

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

289
  return memoizedComponents;
28✔
290
};
291

292
MapContainerFactory.deps = [MapPopoverFactory, MapControlFactory, EditorFactory];
7✔
293

294
type MapboxStyle = string | object | undefined;
295
type PropSelector<R> = Selector<MapContainerProps, R>;
296

297
export interface MapContainerProps {
298
  visState: VisState;
299
  mapState: MapState;
300
  mapControls: MapControls;
301
  mapStyle: {bottomMapStyle?: MapboxStyle; topMapStyle?: MapboxStyle} & MapStyle;
302
  mapboxApiAccessToken: string;
303
  mapboxApiUrl: string;
304
  visStateActions: typeof VisStateActions;
305
  mapStateActions: typeof MapStateActions;
306
  uiStateActions: typeof UIStateActions;
307

308
  // optional
309
  primary?: boolean; // primary one will be reporting its size to appState
310
  readOnly?: boolean;
311
  isExport?: boolean;
312
  // onMapStyleLoaded?: (map: maplibregl.Map | ReturnType<MapRef['getMap']> | null) => void;
313
  onMapStyleLoaded?: (map: GetMapRef | null) => void;
314
  onMapRender?: (map: GetMapRef | null) => void;
315
  getMapboxRef?: (mapbox?: MapRef | null, index?: number) => void;
316
  index?: number;
317
  deleteMapLabels?: (containerId: string, layerId: string) => void;
318
  containerId?: number;
319

320
  locale?: any;
321
  theme?: any;
322
  editor?: any;
323
  MapComponent?: typeof Map;
324
  deckGlProps?: any;
325
  onDeckInitialized?: (a: any, b: any) => void;
326
  onViewStateChange?: (viewport: Viewport) => void;
327

328
  topMapContainerProps: any;
329
  bottomMapContainerProps: any;
330
  transformRequest?: (url: string, resourceType?: string) => {url: string};
331

332
  datasetAttributions?: DatasetAttribution[];
333

334
  generateMapboxLayers?: typeof generateMapboxLayers;
335
  generateDeckGLLayers?: typeof computeDeckLayers;
336

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

339
  children?: React.ReactNode;
340
  deckRenderCallbacks?: {
341
    onDeckLoad?: () => void;
342
    onDeckRender?: (deckProps: Record<string, unknown>) => Record<string, unknown> | null;
343
    onDeckAfterRender?: (deckProps: Record<string, unknown>) => any;
344
  };
345
}
346

347
export default function MapContainerFactory(
348
  MapPopover: ReturnType<typeof MapPopoverFactory>,
349
  MapControl: ReturnType<typeof MapControlFactory>,
350
  Editor: ReturnType<typeof EditorFactory>
351
): React.ComponentType<MapContainerProps> {
352
  class MapContainer extends Component<MapContainerProps> {
353
    displayName = 'MapContainer';
25✔
354

355
    static contextType = MapViewStateContext;
14✔
356

357
    declare context: React.ContextType<typeof MapViewStateContext>;
358

359
    static defaultProps = {
14✔
360
      MapComponent: Map,
361
      deckGlProps: {},
362
      index: 0,
363
      primary: true
364
    };
365

366
    constructor(props) {
367
      super(props);
25✔
368
    }
369

370
    state = {
25✔
371
      // Determines whether attribution should be visible based the result of loading the map style
372
      showBaseMapAttribution: true
373
    };
374

375
    componentDidMount() {
376
      if (!this._ref.current) {
25!
377
        return;
×
378
      }
379
      observeDimensions(this._ref.current, this._handleResize);
25✔
380
    }
381

382
    componentWillUnmount() {
383
      // unbind mapboxgl event listener
384
      if (this._map) {
2!
385
        this._map?.off(MAPBOXGL_STYLE_UPDATE, nop);
×
386
        this._map?.off(MAPBOXGL_RENDER, nop);
×
387
      }
388
      if (!this._ref.current) {
2!
389
        return;
×
390
      }
391
      unobserveDimensions(this._ref.current);
2✔
392
    }
393

394
    _deck: any = null;
25✔
395
    _map: GetMapRef | null = null;
25✔
396
    _ref = createRef<HTMLDivElement>();
25✔
397
    _deckGLErrorsElapsed: {[id: string]: number} = {};
25✔
398

399
    previousLayers = {
25✔
400
      // [layers.id]: mapboxLayerConfig
401
    };
402

403
    _handleResize = dimensions => {
25✔
404
      const {primary, index} = this.props;
×
405
      if (primary) {
×
406
        const {mapStateActions} = this.props;
×
407
        if (dimensions && dimensions.width > 0 && dimensions.height > 0) {
×
408
          mapStateActions.updateMap(dimensions, index);
×
409
        }
410
      }
411
    };
412

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

462
    generateMapboxLayerMethodSelector = props => props.generateMapboxLayers ?? generateMapboxLayers;
25!
463

464
    mapboxLayersSelector = createSelector(
25✔
465
      this.layersSelector,
466
      this.layerDataSelector,
467
      this.layerOrderSelector,
468
      this.layersToRenderSelector,
469
      this.generateMapboxLayerMethodSelector,
470
      (layer, layerData, layerOrder, layersToRender, generateMapboxLayerMethod) =>
471
        generateMapboxLayerMethod(layer, layerData, layerOrder, layersToRender)
×
472
    );
473

474
    // merge in a background-color style if the basemap choice is NO_MAP_ID
475
    // used by <StyledMap> inline style prop
476
    mapStyleTypeSelector = props => props.mapStyle.styleType;
28✔
477
    mapStyleBackgroundColorSelector = props => props.mapStyle.backgroundColor;
28✔
478
    styleSelector = createSelector(
25✔
479
      this.mapStyleTypeSelector,
480
      this.mapStyleBackgroundColorSelector,
481
      (styleType, backgroundColor) => ({
26✔
482
        ...MAP_STYLE.container,
483
        ...(styleType === NO_MAP_ID ? {backgroundColor: rgbToHex(backgroundColor)} : {})
26!
484
      })
485
    );
486

487
    /* component private functions */
488
    _onCloseMapPopover = () => {
25✔
489
      this.props.visStateActions.onLayerClick(null);
×
490
    };
491

492
    _onLayerHover = (_idx: number, info: PickInfo<any> | null) => {
25✔
493
      this.props.visStateActions.onLayerHover(info, this.props.index);
×
494
    };
495

496
    _onLayerSetDomain = (
25✔
497
      idx: number,
498
      value: {domain: VisualChannelDomain; aggregatedBins: AggregatedBin[]}
499
    ) => {
500
      this.props.visStateActions.layerConfigChange(this.props.visState.layers[idx], {
×
501
        colorDomain: value.domain,
502
        aggregatedBins: value.aggregatedBins
503
      } as Partial<LayerBaseConfig>);
504
    };
505

506
    _onLayerFilteredItemsChange = (idx, event) => {
25✔
507
      this.props.visStateActions.layerFilteredItemsChange(this.props.visState.layers[idx], event);
×
508
    };
509

510
    _handleMapToggleLayer = layerId => {
25✔
511
      const {index: mapIndex = 0, visStateActions} = this.props;
1!
512
      visStateActions.toggleLayerForMap(mapIndex, layerId);
1✔
513
    };
514

515
    _onMapboxStyleUpdate = update => {
25✔
516
      // force refresh mapboxgl layers
517
      this.previousLayers = {};
×
518
      this._updateMapboxLayers();
×
519

520
      if (update && update.style) {
×
521
        // No attributions are needed if the style doesn't reference Mapbox sources
NEW
522
        this.setState({
×
523
          showBaseMapAttribution:
524
            isStyleUsingMapboxTiles(update.style) || !isStyleUsingOpenStreetMapTiles(update.style)
×
525
        });
526
      }
527

528
      if (typeof this.props.onMapStyleLoaded === 'function') {
×
529
        this.props.onMapStyleLoaded(this._map);
×
530
      }
531
    };
532

533
    _setMapRef = mapRef => {
25✔
534
      // Handle change of the map library
NEW
535
      if (this._map && mapRef) {
×
NEW
536
        const map = mapRef.getMap();
×
NEW
537
        if (map && this._map !== map) {
×
NEW
538
          this._map?.off(MAPBOXGL_STYLE_UPDATE, nop);
×
NEW
539
          this._map?.off(MAPBOXGL_RENDER, nop);
×
NEW
540
          this._map = null;
×
541
        }
542
      }
543

544
      if (!this._map && mapRef) {
×
NEW
545
        this._map = mapRef.getMap();
×
546
        // i noticed in certain context we don't access the actual map element
547
        if (!this._map) {
×
548
          return;
×
549
        }
550
        // bind mapboxgl event listener
551
        this._map.on(MAPBOXGL_STYLE_UPDATE, this._onMapboxStyleUpdate);
×
552

553
        this._map.on(MAPBOXGL_RENDER, () => {
×
554
          if (typeof this.props.onMapRender === 'function') {
×
555
            this.props.onMapRender(this._map);
×
556
          }
557
        });
558
      }
559

560
      if (this.props.getMapboxRef) {
×
561
        // The parent component can gain access to our MapboxGlMap by
562
        // providing this callback. Note that 'mapbox' will be null when the
563
        // ref is unset (e.g. when a split map is closed).
564
        this.props.getMapboxRef(mapRef, this.props.index);
×
565
      }
566
    };
567

568
    _onDeckInitialized(gl) {
569
      if (this.props.onDeckInitialized) {
×
570
        this.props.onDeckInitialized(this._deck, gl);
×
571
      }
572
    }
573

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

584
    _onBeforeRender = ({gl}) => {
25✔
585
      setLayerBlending(gl, this.props.visState.layerBlending);
×
586
    };
587

588
    _onDeckError = (error, layer) => {
25✔
589
      const errorMessage = error?.message || 'unknown-error';
23!
590
      const layerMessage = layer?.id ? ` in ${layer.id} layer` : '';
23!
591
      const errorMessageFull =
592
        errorMessage === 'WebGL context is lost'
23!
593
          ? '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.'
594
          : `An error in deck.gl: ${errorMessage}${layerMessage}.`;
595

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

601
        // Mark layer as invalid
602
        let extraLayerMessage = '';
23✔
603
        const {visStateActions} = this.props;
23✔
604
        if (layer) {
23!
605
          let topMostLayer = layer;
×
606
          while (topMostLayer.parent) {
×
607
            topMostLayer = topMostLayer.parent;
×
608
          }
609
          if (topMostLayer.props?.id) {
×
610
            visStateActions.layerSetIsValid(topMostLayer, false);
×
611
            extraLayerMessage = 'The layer has been disabled and highlighted.';
×
612
          }
613
        }
614

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

627
    /* component render functions */
628

629
    /* eslint-disable complexity */
630
    _renderMapPopover() {
631
      // this check is for limiting the display of the `<MapPopover>` to the `<MapContainer>` the user is interacting with
632
      // the DeckGL onHover event handler adds a `mapIndex` property which is available in the `hoverInfo` object of `visState`
633
      if (this.props.index !== this.props.visState.hoverInfo?.mapIndex) {
29!
634
        return null;
29✔
635
      }
636

637
      // TODO: move this into reducer so it can be tested
638
      const {
639
        mapState,
640
        visState: {
641
          hoverInfo,
642
          clicked,
643
          datasets,
644
          interactionConfig,
645
          animationConfig,
646
          layers,
647
          mousePos: {mousePosition, coordinate, pinned}
648
        }
649
      } = this.props;
×
650
      const layersToRender = this.layersToRenderSelector(this.props);
×
651

652
      if (!mousePosition || !interactionConfig.tooltip) {
×
653
        return null;
×
654
      }
655

656
      const layerHoverProp = getLayerHoverProp({
×
657
        animationConfig,
658
        interactionConfig,
659
        hoverInfo,
660
        layers,
661
        layersToRender,
662
        datasets
663
      });
664

665
      const compareMode = interactionConfig.tooltip.config
×
666
        ? interactionConfig.tooltip.config.compareMode
667
        : false;
668

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

690
      const commonProp = {
×
691
        onClose: this._onCloseMapPopover,
692
        zoom: mapState.zoom,
693
        container: this._deck ? this._deck.canvas : undefined
×
694
      };
695

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

730
    /* eslint-enable complexity */
731

732
    _getHoverXY(viewport, lngLat) {
733
      const screenCoord = !viewport || !lngLat ? null : viewport.project(lngLat);
×
734
      return screenCoord && {x: screenCoord[0], y: screenCoord[1]};
×
735
    }
736

737
    _renderDeckOverlay(
738
      layersForDeck,
739
      options: {primaryMap: boolean; isInteractive?: boolean; children?: React.ReactNode} = {
×
740
        primaryMap: false
741
      }
742
    ) {
743
      const {
744
        mapStyle,
745
        visState,
746
        mapState,
747
        visStateActions,
748
        mapboxApiAccessToken,
749
        mapboxApiUrl,
750
        deckGlProps,
751
        index,
752
        mapControls,
753
        deckRenderCallbacks,
754
        theme,
755
        generateDeckGLLayers,
756
        onMouseMove
757
      } = this.props;
29✔
758

759
      const {hoverInfo, editor} = visState;
29✔
760
      const {primaryMap, isInteractive, children} = options;
29✔
761

762
      // disable double click zoom when editor is in any draw mode
763
      const {mapDraw} = mapControls;
29✔
764
      const {active: editorMenuActive = false} = mapDraw || {};
29✔
765
      const isEditorDrawingMode = EditorLayerUtils.isDrawingActive(editorMenuActive, editor.mode);
29✔
766

767
      const internalViewState = this.context?.getInternalViewState(index);
29✔
768
      const internalMapState = {...mapState, ...internalViewState};
29✔
769
      const viewport = getViewportFromMapState(internalMapState);
29✔
770

771
      const editorFeatureSelectedIndex = this.selectedPolygonIndexSelector(this.props);
29✔
772

773
      const {setFeatures, onLayerClick, setSelectedFeature} = visStateActions;
29✔
774

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

812
      const extraDeckParams: {
813
        getTooltip?: (info: any) => object | null;
814
        getCursor?: ({isDragging}: {isDragging: boolean}) => string;
815
      } = {};
29✔
816
      if (primaryMap) {
29!
817
        extraDeckParams.getTooltip = info =>
29✔
818
          EditorLayerUtils.getTooltip(info, {
×
819
            editorMenuActive,
820
            editor,
821
            theme
822
          });
823

824
        extraDeckParams.getCursor = ({isDragging}: {isDragging: boolean}) => {
29✔
825
          const editorCursor = EditorLayerUtils.getCursor({
×
826
            editorMenuActive,
827
            editor,
828
            hoverInfo
829
          });
830
          if (editorCursor) return editorCursor;
×
831

832
          if (isDragging) return 'grabbing';
×
833
          if (hoverInfo?.layer) return 'pointer';
×
834
          return 'grab';
×
835
        };
836
      }
837

838
      const effects = this._isOKToRenderEffects(index)
29!
839
        ? computeDeckEffects({visState, mapState})
840
        : [];
841

842
      const views = deckGlProps?.views
29!
843
        ? deckGlProps?.views()
844
        : new MapView({legacyMeterSizes: true});
845

846
      let allDeckGlProps = {
29✔
847
        ...deckGlProps,
848
        pickingRadius: DEFAULT_PICKING_RADIUS,
849
        views,
850
        layers: deckGlLayers,
851
        effects
852
      };
853

854
      if (typeof deckRenderCallbacks?.onDeckRender === 'function') {
29!
855
        allDeckGlProps = deckRenderCallbacks.onDeckRender(allDeckGlProps);
×
856
        if (!allDeckGlProps) {
×
857
          // if onDeckRender returns null, do not render deck.gl
858
          return null;
×
859
        }
860
      }
861

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

905
                    this._onLayerHoverDebounced(data, index);
×
906
                  }
907
                : null
908
            }
909
            onClick={(data, event) => {
910
              // @ts-ignore
911
              normalizeEvent(event.srcEvent, viewport);
×
912
              const res = EditorLayerUtils.onClick(data, event, {
×
913
                editorMenuActive,
914
                editor,
915
                onLayerClick,
916
                setSelectedFeature,
917
                mapIndex: index
918
              });
919
              if (res) return;
×
920

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

944
    _updateMapboxLayers() {
945
      const mapboxLayers = this.mapboxLayersSelector(this.props);
×
946
      if (!Object.keys(mapboxLayers).length && !Object.keys(this.previousLayers).length) {
×
947
        return;
×
948
      }
949

950
      updateMapboxLayers(this._map, mapboxLayers, this.previousLayers);
×
951

952
      this.previousLayers = mapboxLayers;
×
953
    }
954

955
    _renderMapboxOverlays() {
956
      if (this._map && this._map.isStyleLoaded()) {
29!
957
        this._updateMapboxLayers();
×
958
      }
959
    }
960
    _onViewportChangePropagateDebounced = debounce(() => {
25✔
961
      const viewState = this.context?.getInternalViewState(this.props.index);
×
962
      onViewPortChange(
×
963
        viewState,
964
        this.props.mapStateActions.updateMap,
965
        this.props.onViewStateChange,
966
        this.props.primary,
967
        this.props.index
968
      );
969
    }, DEBOUNCE_VIEWPORT_PROPAGATE);
970

971
    _onViewportChange = viewport => {
25✔
972
      const {viewState} = viewport;
×
973
      if (this.props.isExport) {
×
974
        // Image export map shouldn't be interactive (otherwise this callback can
975
        // lead to inadvertent changes to the state of the main map)
976
        return;
×
977
      }
978
      const {setInternalViewState} = this.context;
×
979
      setInternalViewState(viewState, this.props.index);
×
980
      this._onViewportChangePropagateDebounced();
×
981
    };
982

983
    _onLayerHoverDebounced = debounce((data, index) => {
25✔
984
      this.props.visStateActions.onLayerHover(data, index);
×
985
    }, DEBOUNCE_MOUSE_MOVE_PROPAGATE);
986

987
    _onMouseMoveDebounced = debounce((event, viewport) => {
25✔
988
      this.props.visStateActions.onMouseMove(normalizeEvent(event, viewport));
×
989
    }, DEBOUNCE_MOUSE_MOVE_PROPAGATE);
990

991
    _toggleMapControl = panelId => {
25✔
992
      const {index, uiStateActions} = this.props;
2✔
993

994
      uiStateActions.toggleMapControl(panelId, Number(index));
2✔
995
    };
996

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

1021
      const {layers, datasets, editor, interactionConfig} = visState;
29✔
1022

1023
      const layersToRender = this.layersToRenderSelector(this.props);
29✔
1024
      const layersForDeck = this.layersForDeckSelector(this.props);
29✔
1025

1026
      // Current style can be a custom style, from which we pull the mapbox API acccess token
1027
      const currentStyle = mapStyle.mapStyles?.[mapStyle.styleType];
29✔
1028
      const baseMapLibraryName = getBaseMapLibrary(currentStyle);
29✔
1029
      const baseMapLibraryConfig =
1030
        getApplicationConfig().baseMapLibraryConfig?.[baseMapLibraryName];
29✔
1031

1032
      const internalViewState = this.context?.getInternalViewState(index);
29✔
1033
      const mapProps = {
29✔
1034
        ...internalViewState,
1035
        preserveDrawingBuffer: true,
1036
        mapboxAccessToken: currentStyle?.accessToken || mapboxApiAccessToken,
58✔
1037
        // baseApiUrl: mapboxApiUrl,
1038
        mapLib: baseMapLibraryConfig.getMapLib(),
1039
        transformRequest:
1040
          this.props.transformRequest ||
58✔
1041
          transformRequest(currentStyle?.accessToken || mapboxApiAccessToken)
58✔
1042
      };
1043

1044
      const hasGeocoderLayer = Boolean(layers.find(l => l.id === GEOCODER_LAYER_ID));
29✔
1045
      const isSplit = Boolean(mapState.isSplit);
29✔
1046

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

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

1133
          {hasGeocoderLayer
29!
1134
            ? this._renderDeckOverlay(
1135
                {[GEOCODER_LAYER_ID]: hasGeocoderLayer},
1136
                {primaryMap: false, isInteractive: false}
1137
              )
1138
            : null}
1139
          {this._renderMapPopover()}
1140
          {this.props.primary ? (
29✔
1141
            <Attribution
1142
              showBaseMapLibLogo={this.state.showBaseMapAttribution}
1143
              showOsmBasemapAttribution={true}
1144
              datasetAttributions={datasetAttributions}
1145
              baseMapLibraryConfig={baseMapLibraryConfig}
1146
            />
1147
          ) : null}
1148
        </>
1149
      );
1150
    }
1151

1152
    render() {
1153
      const {visState, mapStyle} = this.props;
29✔
1154
      const mapContent = this._renderMap();
29✔
1155
      if (!mapContent) {
29!
1156
        // mapContent can be null if onDeckRender returns null
1157
        // in this case we don't want to render the map
1158
        return null;
×
1159
      }
1160

1161
      const currentStyle = mapStyle.mapStyles?.[mapStyle.styleType];
29✔
1162
      const baseMapLibraryName = getBaseMapLibrary(currentStyle);
29✔
1163
      const baseMapLibraryConfig =
1164
        getApplicationConfig().baseMapLibraryConfig?.[baseMapLibraryName];
29✔
1165

1166
      return (
29✔
1167
        <StyledMap
1168
          ref={this._ref}
1169
          style={this.styleSelector(this.props)}
1170
          onContextMenu={event => event.preventDefault()}
×
1171
          mixBlendMode={visState.overlayBlending}
1172
          mapLibCssClass={baseMapLibraryConfig.mapLibCssClass}
1173
        >
1174
          {mapContent}
1175
        </StyledMap>
1176
      );
1177
    }
1178
  }
1179

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