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

keplergl / kepler.gl / 23948930176

03 Apr 2026 02:04PM UTC coverage: 60.018% (-1.7%) from 61.699%
23948930176

Pull #3271

github

web-flow
Merge 194def22d into bc59e880b
Pull Request #3271: chore: deck.gl 9.2 upgrade & loaders.gl, luma.gl upgrades

6517 of 12945 branches covered (50.34%)

Branch coverage included in aggregate %.

310 of 960 new or added lines in 57 files covered. (32.29%)

122 existing lines in 17 files now uncovered.

13299 of 20072 relevant lines covered (66.26%)

78.7 hits per line

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

40.95
/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 {PickingInfo, MapView} from '@deck.gl/core';
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
  StyledAttribution,
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
  isStyleUsingMapboxTiles,
47
  isStyleUsingOpenStreetMapTiles,
48
  getBaseMapLibrary,
49
  BaseMapLibraryConfig,
50
  transformRequest,
51
  observeDimensions,
52
  unobserveDimensions,
53
  hasMobileWidth,
54
  getMapLayersFromSplitMaps,
55
  onViewPortChange,
56
  getViewportFromMapState,
57
  normalizeEvent,
58
  rgbToHex,
59
  computeDeckEffects,
60
  getApplicationConfig,
61
  GetMapRef,
62
  getLayerBlendingParameters,
63
  patchDeckRendererForPostProcessing
64
} from '@kepler.gl/utils';
65
import {breakPointValues} from '@kepler.gl/styles';
66

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

77
import {DROPPABLE_MAP_CONTAINER_TYPE} from './common/dnd-layer-items';
78
// Contexts
79
import {MapViewStateContext} from './map-view-state-context';
80

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

95
import LoadingIndicator from './loading-indicator';
96

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

103
// How long should we wait between layer loading state changes before triggering a UI update
104
const DEBOUNCE_LOADING_STATE_PROPAGATE = 100;
7✔
105

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

122
const LOCALE_CODES_ARRAY = Object.keys(LOCALE_CODES);
7✔
123

124
interface StyledMapContainerProps {
125
  $mixBlendMode?: string;
126
  $mapLibCssClass: string;
127
}
128

129
const StyledMap = styled(StyledMapContainer)<StyledMapContainerProps>(
7✔
130
  ({$mixBlendMode = 'normal', $mapLibCssClass}) => `
29!
131
  #default-deckgl-overlay {
132
    mix-blend-mode: ${$mixBlendMode};
133
  };
134
  *[${$mapLibCssClass}-children] {
135
    position: absolute;
136
  }
137
`
138
);
139

140
const MAPBOXGL_STYLE_UPDATE = 'style.load';
7✔
141
const MAPBOXGL_RENDER = 'render';
7✔
142
const nop = () => {
7✔
143
  return;
×
144
};
145

146
type MapLibLogoProps = {
147
  baseMapLibraryConfig: BaseMapLibraryConfig;
148
};
149

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

164
interface StyledDroppableProps {
165
  isOver: boolean;
166
}
167

168
const StyledDroppable = styled.div<StyledDroppableProps>`
7✔
169
  background-color: ${props => (props.isOver ? props.theme.dndOverBackgroundColor : 'none')};
9!
170
  width: 100%;
171
  height: 100%;
172
  position: absolute;
173
  pointer-events: none;
174
  z-index: 1;
175
`;
176

177
export const isSplitSelector = props =>
7✔
178
  props.visState.splitMaps && props.visState.splitMaps.length > 1;
29✔
179

180
export const Droppable = ({containerId}) => {
7✔
181
  const {isOver, setNodeRef} = useDroppable({
9✔
182
    id: containerId,
183
    data: {type: DROPPABLE_MAP_CONTAINER_TYPE, index: containerId},
184
    disabled: !containerId
185
  });
186

187
  return <StyledDroppable ref={setNodeRef} isOver={isOver} />;
9✔
188
};
189

190
interface StyledDatasetAttributionsContainerProps {
191
  isPalm: boolean;
192
}
193

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

204
  &:hover {
205
    white-space: inherit;
206
  }
207
`;
208

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

235
type AttributionProps = {
236
  showBaseMapLibLogo: boolean;
237
  showOsmBasemapAttribution: boolean;
238
  datasetAttributions: DatasetAttribution[];
239
  baseMapLibraryConfig: BaseMapLibraryConfig;
240
};
241

242
export const Attribution: React.FC<AttributionProps> = ({
7✔
243
  showBaseMapLibLogo = true,
×
244
  showOsmBasemapAttribution = false,
×
245
  datasetAttributions,
246
  baseMapLibraryConfig
247
}: AttributionProps) => {
248
  const isPalm = hasMobileWidth(breakPointValues);
28✔
249

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

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

302
  return memoizedComponents;
28✔
303
};
304

305
MapContainerFactory.deps = [MapPopoverFactory, MapControlFactory, EditorFactory];
7✔
306

307
type MapboxStyle = string | object | undefined;
308
type PropSelector<R> = Selector<MapContainerProps, R>;
309

310
export interface MapContainerProps {
311
  visState: VisState;
312
  mapState: MapState;
313
  mapControls: MapControls;
314
  mapStyle: {bottomMapStyle?: MapboxStyle; topMapStyle?: MapboxStyle} & MapStyle;
315
  mapboxApiAccessToken: string;
316
  mapboxApiUrl: string;
317
  visStateActions: typeof VisStateActions;
318
  mapStateActions: typeof MapStateActions;
319
  uiStateActions: typeof UIStateActions;
320

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

333
  isLoadingIndicatorVisible?: boolean;
334
  activeSidePanel: string | null;
335
  sidePanelWidth?: number;
336

337
  locale?: any;
338
  theme?: any;
339
  editor?: any;
340
  MapComponent?: typeof Map;
341
  deckGlProps?: any;
342
  onDeckInitialized?: (a: any, b: any) => void;
343
  onViewStateChange?: (viewport: Viewport) => void;
344

345
  topMapContainerProps: any;
346
  bottomMapContainerProps: any;
347
  transformRequest?: (url: string, resourceType?: string) => {url: string};
348

349
  datasetAttributions?: DatasetAttribution[];
350

351
  generateMapboxLayers?: typeof generateMapboxLayers;
352
  generateDeckGLLayers?: typeof computeDeckLayers;
353

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

356
  children?: React.ReactNode;
357
  deckRenderCallbacks?: {
358
    onDeckLoad?: () => void;
359
    onDeckRender?: (deckProps: Record<string, unknown>) => Record<string, unknown> | null;
360
    onDeckAfterRender?: (deckProps: Record<string, unknown>) => any;
361
  };
362

363
  // Optional: override legend header logo in map controls (used by image export)
364
  logoComponent?: React.FC | React.ReactNode;
365
}
366

367
export default function MapContainerFactory(
368
  MapPopover: ReturnType<typeof MapPopoverFactory>,
369
  MapControl: ReturnType<typeof MapControlFactory>,
370
  Editor: ReturnType<typeof EditorFactory>
371
): React.ComponentType<MapContainerProps> {
372
  class MapContainer extends Component<MapContainerProps> {
373
    displayName = 'MapContainer';
25✔
374

375
    private anyActiveLayerLoading = false;
25✔
376

377
    static contextType = MapViewStateContext;
14✔
378

379
    declare context: React.ContextType<typeof MapViewStateContext>;
380

381
    static defaultProps = {
14✔
382
      MapComponent: Map,
383
      deckGlProps: {},
384
      index: 0,
385
      primary: true
386
    };
387

388
    constructor(props) {
389
      super(props);
25✔
390
      patchDeckRendererForPostProcessing();
25✔
391
    }
392

393
    state = {
25✔
394
      // Determines whether attribution should be visible based the result of loading the map style
395
      showBaseMapAttribution: true
396
    };
397

398
    componentDidMount() {
399
      if (!this._ref.current) {
25!
400
        return;
×
401
      }
402
      observeDimensions(this._ref.current, this._handleResize);
25✔
403
    }
404

405
    componentWillUnmount() {
406
      // unbind mapboxgl event listener
407
      if (this._map) {
2!
408
        this._map?.off(MAPBOXGL_STYLE_UPDATE, nop);
×
409
        this._map?.off(MAPBOXGL_RENDER, nop);
×
410
      }
411
      if (!this._ref.current) {
2!
412
        return;
×
413
      }
414
      unobserveDimensions(this._ref.current);
2✔
415
    }
416

417
    _deck: any = null;
25✔
418
    _map: GetMapRef | null = null;
25✔
419
    _ref = createRef<HTMLDivElement>();
25✔
420
    _deckGLErrorsElapsed: {[id: string]: number} = {};
25✔
421

422
    previousLayers = {
25✔
423
      // [layers.id]: mapboxLayerConfig
424
    };
425

426
    _handleResize = dimensions => {
25✔
427
      const {primary, index} = this.props;
×
428
      if (primary) {
×
429
        const {mapStateActions} = this.props;
×
430
        if (dimensions && dimensions.width > 0 && dimensions.height > 0) {
×
431
          mapStateActions.updateMap(dimensions, index);
×
432
        }
433
      }
434
    };
435

436
    layersSelector: PropSelector<VisState['layers']> = props => props.visState.layers;
56✔
437
    layerDataSelector: PropSelector<VisState['layers']> = props => props.visState.layerData;
56✔
438
    splitMapSelector: PropSelector<SplitMap[]> = props => props.visState.splitMaps;
28✔
439
    splitMapIndexSelector: PropSelector<number | undefined> = props => props.index;
28✔
440
    mapLayersSelector: PropSelector<SplitMapLayers | null | undefined> = createSelector(
25✔
441
      this.splitMapSelector,
442
      this.splitMapIndexSelector,
443
      getMapLayersFromSplitMaps
444
    );
445
    layerOrderSelector: PropSelector<VisState['layerOrder']> = props => props.visState.layerOrder;
25✔
446
    layersToRenderSelector: PropSelector<LayersToRender> = createSelector(
25✔
447
      this.layersSelector,
448
      this.layerDataSelector,
449
      this.mapLayersSelector,
450
      prepareLayersToRender
451
    );
452
    layersForDeckSelector = createSelector(
25✔
453
      this.layersSelector,
454
      this.layerDataSelector,
455
      prepareLayersForDeck
456
    );
457
    filtersSelector = props => props.visState.filters;
28✔
458
    polygonFiltersSelector = createSelector(this.filtersSelector, filters =>
25✔
459
      filters.filter(f => f.type === FILTER_TYPES.polygon && f.enabled !== false)
26!
460
    );
461
    featuresSelector = props => props.visState.editor.features;
28✔
462
    selectedFeatureSelector = props => props.visState.editor.selectedFeature;
28✔
463
    featureCollectionSelector = createSelector(
25✔
464
      this.polygonFiltersSelector,
465
      this.featuresSelector,
466
      (polygonFilters, features) => ({
26✔
467
        type: 'FeatureCollection',
468
        features: features.concat(polygonFilters.map(f => f.value))
×
469
      })
470
    );
471
    // @ts-ignore - No overload matches this call
472
    selectedPolygonIndexSelector = createSelector(
25✔
473
      this.featureCollectionSelector,
474
      this.selectedFeatureSelector,
475
      (collection, selectedFeature) =>
476
        collection.features.findIndex(f => f.id === selectedFeature?.id)
26✔
477
    );
478
    selectedFeatureIndexArraySelector = createSelector(
25✔
479
      (value: number) => value,
25✔
480
      value => {
481
        return value < 0 ? [] : [value];
25!
482
      }
483
    );
484

485
    generateMapboxLayerMethodSelector = props => props.generateMapboxLayers ?? generateMapboxLayers;
25!
486

487
    mapboxLayersSelector = createSelector(
25✔
488
      this.layersSelector,
489
      this.layerDataSelector,
490
      this.layerOrderSelector,
491
      this.layersToRenderSelector,
492
      this.generateMapboxLayerMethodSelector,
493
      (layer, layerData, layerOrder, layersToRender, generateMapboxLayerMethod) =>
494
        generateMapboxLayerMethod(layer, layerData, layerOrder, layersToRender)
×
495
    );
496

497
    // merge in a background-color style if the basemap choice is NO_MAP_ID
498
    // used by <StyledMap> inline style prop
499
    mapStyleTypeSelector = props => props.mapStyle.styleType;
28✔
500
    mapStyleBackgroundColorSelector = props => props.mapStyle.backgroundColor;
28✔
501
    styleSelector = createSelector(
25✔
502
      this.mapStyleTypeSelector,
503
      this.mapStyleBackgroundColorSelector,
504
      (styleType, backgroundColor) => ({
26✔
505
        ...MAP_STYLE.container,
506
        ...(styleType === NO_MAP_ID ? {backgroundColor: rgbToHex(backgroundColor)} : {})
26!
507
      })
508
    );
509

510
    /* component private functions */
511
    _onCloseMapPopover = () => {
25✔
512
      this.props.visStateActions.onLayerClick(null);
×
513
    };
514

515
    _onLayerHover = (_idx: number, info: PickingInfo<any> | null) => {
25✔
516
      this.props.visStateActions.onLayerHover(info, this.props.index);
×
517
    };
518

519
    _onLayerSetDomain = (
25✔
520
      idx: number,
521
      value: number[] | {domain: VisualChannelDomain; aggregatedBins: AggregatedBin[]}
522
    ) => {
523
      // deck.gl 9 native aggregation layers (Grid, Hexagon) pass [min, max],
524
      // while ClusterLayer's CPUAggregator still passes {domain, aggregatedBins}.
NEW
525
      const config = Array.isArray(value)
×
526
        ? {colorDomain: value as VisualChannelDomain}
527
        : {colorDomain: value.domain, aggregatedBins: value.aggregatedBins};
528

NEW
529
      const layer = this.props.visState.layers[idx];
×
NEW
530
      if (!layer) return;
×
531

NEW
532
      this.props.visStateActions.layerConfigChange(layer, config as Partial<LayerBaseConfig>);
×
533
    };
534

535
    _onRedrawNeeded = (_idx: number) => {
25✔
536
      // updateMapUpdater always returns a new state object reference, which triggers re-render
537
      const {mapStateActions, index} = this.props;
×
538
      mapStateActions.updateMap({}, index);
×
539
    };
540

541
    _onLayerFilteredItemsChange = (idx, event) => {
25✔
542
      this.props.visStateActions.layerFilteredItemsChange(this.props.visState.layers[idx], event);
×
543
    };
544

545
    _onWMSFeatureInfo = (
25✔
546
      idx: number,
547
      data: {
548
        featureInfo: Array<{name: string; value: string}> | string | null;
549
        coordinate?: [number, number] | null;
550
      }
551
    ) => {
552
      this.props.visStateActions.wmsFeatureInfo(
×
553
        this.props.visState.layers[idx],
554
        data.featureInfo,
555
        data.coordinate
556
      );
557
    };
558

559
    _handleMapToggleLayer = layerId => {
25✔
560
      const {index: mapIndex = 0, visStateActions} = this.props;
1!
561
      visStateActions.toggleLayerForMap(mapIndex, layerId);
1✔
562
    };
563

564
    _onMapboxStyleUpdate = update => {
25✔
565
      // force refresh mapboxgl layers
566
      this.previousLayers = {};
×
567
      this._updateMapboxLayers();
×
568

569
      if (update && update.style) {
×
570
        // No attributions are needed if the style doesn't reference Mapbox sources
571
        this.setState({
×
572
          showBaseMapAttribution:
573
            isStyleUsingMapboxTiles(update.style) || !isStyleUsingOpenStreetMapTiles(update.style)
×
574
        });
575
      }
576

577
      if (typeof this.props.onMapStyleLoaded === 'function') {
×
578
        this.props.onMapStyleLoaded(this._map);
×
579
      }
580
    };
581

582
    _setMapRef = mapRef => {
25✔
583
      // Handle change of the map library
584
      if (this._map && mapRef) {
×
585
        const map = mapRef.getMap();
×
586
        if (map && this._map !== map) {
×
587
          this._map?.off(MAPBOXGL_STYLE_UPDATE, nop);
×
588
          this._map?.off(MAPBOXGL_RENDER, nop);
×
589
          this._map = null;
×
590
        }
591
      }
592

593
      if (!this._map && mapRef) {
×
594
        this._map = mapRef.getMap();
×
595
        // i noticed in certain context we don't access the actual map element
596
        if (!this._map) {
×
597
          return;
×
598
        }
599
        // bind mapboxgl event listener
600
        this._map.on(MAPBOXGL_STYLE_UPDATE, this._onMapboxStyleUpdate);
×
601

602
        this._map.on(MAPBOXGL_RENDER, () => {
×
603
          if (typeof this.props.onMapRender === 'function') {
×
604
            this.props.onMapRender(this._map);
×
605
          }
606
        });
607
      }
608

609
      if (this.props.getMapboxRef) {
×
610
        // The parent component can gain access to our MapboxGlMap by
611
        // providing this callback. Note that 'mapbox' will be null when the
612
        // ref is unset (e.g. when a split map is closed).
613
        this.props.getMapboxRef(mapRef, this.props.index);
×
614
      }
615
    };
616

617
    _onDeckInitialized(device) {
618
      if (this.props.onDeckInitialized) {
×
NEW
619
        this.props.onDeckInitialized(this._deck, device);
×
620
      }
621
    }
622

623
    /**
624
     * 1) Allow effects only for the first view.
625
     * 2) Prevent effect:preRender call without valid generated viewports.
626
     * @param viewIndex View index.
627
     * @returns Returns true if effects can be used.
628
     */
629
    _isOKToRenderEffects(viewIndex?: number): boolean {
630
      return !viewIndex && Boolean(this._deck?.viewManager?._viewports?.length);
29✔
631
    }
632

633
    _onBeforeRender = () => {
25✔
634
      // no-op
635
    };
636

637
    _onDeckError = (error, layer) => {
25✔
UNCOV
638
      const errorMessage = error?.message || 'unknown-error';
×
UNCOV
639
      const layerMessage = layer?.id ? ` in ${layer.id} layer` : '';
×
640
      const errorMessageFull =
UNCOV
641
        errorMessage === 'WebGL context is lost'
×
642
          ? '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.'
643
          : `An error in deck.gl: ${errorMessage}${layerMessage}.`;
644

645
      // Throttle error notifications, as React doesn't like too many state changes from here.
UNCOV
646
      const lastShown = this._deckGLErrorsElapsed[errorMessageFull];
×
UNCOV
647
      if (!lastShown || lastShown < Date.now() - THROTTLE_NOTIFICATION_TIME) {
×
UNCOV
648
        this._deckGLErrorsElapsed[errorMessageFull] = Date.now();
×
649

650
        // Mark layer as invalid
UNCOV
651
        let extraLayerMessage = '';
×
UNCOV
652
        const {visStateActions} = this.props;
×
UNCOV
653
        if (layer) {
×
654
          let topMostLayer = layer;
×
655
          while (topMostLayer.parent) {
×
656
            topMostLayer = topMostLayer.parent;
×
657
          }
658
          if (topMostLayer.props?.id) {
×
659
            visStateActions.layerSetIsValid(topMostLayer, false);
×
660
            extraLayerMessage = 'The layer has been disabled and highlighted.';
×
661
          }
662
        }
663

664
        // Create new error notification or update existing one with same id.
665
        // Update is required to preserve the order of notifications as they probably are going to "jump" based on order of errors.
UNCOV
666
        const {uiStateActions} = this.props;
×
UNCOV
667
        uiStateActions.addNotification(
×
668
          errorNotification({
669
            message: `${errorMessageFull} ${extraLayerMessage}`,
670
            id: errorMessageFull // treat the error message as id
671
          })
672
        );
673
      }
674
    };
675

676
    /* component render functions */
677

678
    /* eslint-disable complexity */
679
    _renderMapPopover() {
680
      // this check is for limiting the display of the `<MapPopover>` to the `<MapContainer>` the user is interacting with
681
      // the DeckGL onHover event handler adds a `mapIndex` property which is available in the `hoverInfo` object of `visState`
682
      if (this.props.index !== this.props.visState.hoverInfo?.mapIndex) {
29!
683
        return null;
29✔
684
      }
685

686
      // TODO: move this into reducer so it can be tested
687
      const {
688
        mapState,
689
        visState: {
690
          hoverInfo,
691
          clicked,
692
          datasets,
693
          interactionConfig,
694
          animationConfig,
695
          layers,
696
          mousePos: {mousePosition, coordinate, pinned}
697
        }
698
      } = this.props;
×
699
      const layersToRender = this.layersToRenderSelector(this.props);
×
700

701
      if (!mousePosition || !interactionConfig.tooltip) {
×
702
        return null;
×
703
      }
704

705
      const layerHoverProp = getLayerHoverProp({
×
706
        animationConfig,
707
        interactionConfig,
708
        hoverInfo,
709
        layers,
710
        layersToRender,
711
        datasets
712
      });
713

714
      const compareMode = interactionConfig.tooltip.config
×
715
        ? interactionConfig.tooltip.config.compareMode
716
        : false;
717

718
      let pinnedPosition = {x: 0, y: 0};
×
719
      let layerPinnedProp: LayerHoverProp | null = null;
×
720
      if (pinned || clicked) {
×
721
        // project lnglat to screen so that tooltip follows the object on zoom
722
        const viewport = getViewportFromMapState(mapState);
×
723
        const lngLat = clicked ? clicked.coordinate : pinned.coordinate;
×
724
        pinnedPosition = this._getHoverXY(viewport, lngLat);
×
725
        layerPinnedProp = getLayerHoverProp({
×
726
          animationConfig,
727
          interactionConfig,
728
          hoverInfo: clicked,
729
          layers,
730
          layersToRender,
731
          datasets
732
        });
733
        if (layerHoverProp && layerPinnedProp) {
×
734
          layerHoverProp.primaryData = layerPinnedProp.data;
×
735
          layerHoverProp.compareType = interactionConfig.tooltip.config.compareType;
×
736
        }
737
      }
738

739
      const commonProp = {
×
740
        onClose: this._onCloseMapPopover,
741
        zoom: mapState.zoom,
742
        container: this._deck ? this._deck.canvas : undefined
×
743
      };
744

745
      return (
×
746
        <ErrorBoundary>
747
          {layerPinnedProp && (
×
748
            <MapPopover
749
              {...pinnedPosition}
750
              {...commonProp}
751
              layerHoverProp={layerPinnedProp}
752
              coordinate={interactionConfig.coordinate.enabled && (pinned || {}).coordinate}
×
753
              frozen={true}
754
              isBase={compareMode}
755
              onSetFeatures={this.props.visStateActions.setFeatures}
756
              setSelectedFeature={this.props.visStateActions.setSelectedFeature}
757
              // @ts-ignore Argument of type 'Readonly<MapContainerProps>' is not assignable to parameter of type 'never'
758
              featureCollection={this.featureCollectionSelector(this.props)}
759
            />
760
          )}
761
          {layerHoverProp && (!layerPinnedProp || compareMode) && (
×
762
            <MapPopover
763
              x={mousePosition[0]}
764
              y={mousePosition[1]}
765
              {...commonProp}
766
              layerHoverProp={layerHoverProp}
767
              frozen={false}
768
              coordinate={interactionConfig.coordinate.enabled && coordinate}
×
769
              onSetFeatures={this.props.visStateActions.setFeatures}
770
              setSelectedFeature={this.props.visStateActions.setSelectedFeature}
771
              // @ts-ignore Argument of type 'Readonly<MapContainerProps>' is not assignable to parameter of type 'never'
772
              featureCollection={this.featureCollectionSelector(this.props)}
773
            />
774
          )}
775
        </ErrorBoundary>
776
      );
777
    }
778

779
    /* eslint-enable complexity */
780

781
    _getHoverXY(viewport, lngLat) {
782
      const screenCoord = !viewport || !lngLat ? null : viewport.project(lngLat);
×
783
      return screenCoord && {x: screenCoord[0], y: screenCoord[1]};
×
784
    }
785

786
    _renderDeckOverlay(
787
      layersForDeck,
788
      options: {primaryMap: boolean; isInteractive?: boolean; children?: React.ReactNode} = {
×
789
        primaryMap: false
790
      }
791
    ) {
792
      const {
793
        mapStyle,
794
        visState,
795
        mapState,
796
        visStateActions,
797
        mapboxApiAccessToken,
798
        mapboxApiUrl,
799
        deckGlProps,
800
        index,
801
        mapControls,
802
        deckRenderCallbacks,
803
        theme,
804
        generateDeckGLLayers,
805
        onMouseMove
806
      } = this.props;
29✔
807

808
      const {hoverInfo, editor} = visState;
29✔
809
      const {primaryMap, isInteractive, children} = options;
29✔
810

811
      // disable double click zoom when editor is in any draw mode
812
      const {mapDraw} = mapControls;
29✔
813
      const {active: editorMenuActive = false} = mapDraw || {};
29✔
814
      const isEditorDrawingMode = EditorLayerUtils.isDrawingActive(editorMenuActive, editor.mode);
29✔
815

816
      const internalViewState = this.context?.getInternalViewState(index);
29✔
817
      const internalMapState = {...mapState, ...internalViewState};
29✔
818
      const viewport = getViewportFromMapState(internalMapState);
29✔
819

820
      const editorFeatureSelectedIndex = this.selectedPolygonIndexSelector(this.props);
29✔
821

822
      const {setFeatures, onLayerClick, setSelectedFeature} = visStateActions;
29✔
823

824
      const generateDeckGLLayersMethod = generateDeckGLLayers ?? computeDeckLayers;
29✔
825
      const deckGlLayers = generateDeckGLLayersMethod(
29✔
826
        {
827
          visState,
828
          mapState: internalMapState,
829
          mapStyle
830
        },
831
        {
832
          mapIndex: index,
833
          primaryMap,
834
          mapboxApiAccessToken,
835
          mapboxApiUrl,
836
          layersForDeck,
837
          editorInfo: primaryMap
29!
838
            ? {
839
                editor,
840
                editorMenuActive,
841
                onSetFeatures: setFeatures,
842
                setSelectedFeature,
843
                // @ts-ignore Argument of type 'Readonly<MapContainerProps>' is not assignable to parameter of type 'never'
844
                featureCollection: this.featureCollectionSelector(this.props),
845
                selectedFeatureIndexes: this.selectedFeatureIndexArraySelector(
846
                  // @ts-ignore Argument of type 'unknown' is not assignable to parameter of type 'number'.
847
                  editorFeatureSelectedIndex
848
                ),
849
                viewport
850
              }
851
            : undefined
852
        },
853
        {
854
          onLayerHover: this._onLayerHover,
855
          onSetLayerDomain: this._onLayerSetDomain,
856
          onFilteredItemsChange: this._onLayerFilteredItemsChange,
857
          onWMSFeatureInfo: this._onWMSFeatureInfo,
858
          onRedrawNeeded: this._onRedrawNeeded
859
        },
860
        deckGlProps
861
      );
862

863
      const extraDeckParams: {
864
        getTooltip?: (info: any) => object | null;
865
        getCursor?: ({isDragging}: {isDragging: boolean}) => string;
866
      } = {};
29✔
867
      if (primaryMap) {
29!
868
        // Omit hover updates when the pointer position is invalid, ie. over UI overlays or
869
        // outside the map container. In those cases x/y may be < 0
870
        extraDeckParams.getTooltip = info => {
29✔
871
          const x = Number(info?.x);
×
872
          const y = Number(info?.y);
×
873
          if (Number.isNaN(x) || Number.isNaN(y) || x < 0 || y < 0) return null;
×
874

875
          return EditorLayerUtils.getTooltip(info, {
×
876
            editorMenuActive,
877
            editor,
878
            theme
879
          });
880
        };
881

882
        extraDeckParams.getCursor = ({isDragging}: {isDragging: boolean}) => {
29✔
883
          const editorCursor = EditorLayerUtils.getCursor({
×
884
            editorMenuActive,
885
            editor,
886
            hoverInfo
887
          });
888
          if (editorCursor) return editorCursor;
×
889

890
          if (isDragging) return 'grabbing';
×
891
          if (hoverInfo?.layer) return 'pointer';
×
892
          return 'grab';
×
893
        };
894
      }
895

896
      const effects = this._isOKToRenderEffects(index)
29!
897
        ? computeDeckEffects({visState, mapState, isExport: this.props.isExport})
898
        : [];
899

900
      const views = deckGlProps?.views
29!
901
        ? deckGlProps?.views()
902
        : new MapView({legacyMeterSizes: true} as ConstructorParameters<typeof MapView>[0] & {
903
            legacyMeterSizes: boolean;
904
          });
905

906
      let allDeckGlProps = {
29✔
907
        ...deckGlProps,
908
        pickingRadius: DEFAULT_PICKING_RADIUS,
909
        views,
910
        layers: deckGlLayers,
911
        effects,
912
        parameters: getLayerBlendingParameters(visState.layerBlending)
913
      };
914

915
      if (typeof deckRenderCallbacks?.onDeckRender === 'function') {
29!
916
        allDeckGlProps = deckRenderCallbacks.onDeckRender(allDeckGlProps);
×
917
        if (!allDeckGlProps) {
×
918
          // if onDeckRender returns null, do not render deck.gl
919
          return null;
×
920
        }
921
      }
922

923
      return (
29✔
924
        <div
925
          {...(isInteractive
29!
926
            ? {
927
                onMouseMove: primaryMap
29!
928
                  ? event => {
929
                      onMouseMove?.(event);
×
930
                      this._onMouseMoveDebounced(event, viewport);
×
931
                    }
932
                  : undefined
933
              }
934
            : {style: {pointerEvents: 'none'}})}
935
        >
936
          <DeckGL
937
            id="default-deckgl-overlay"
938
            onLoad={() => {
939
              if (typeof deckRenderCallbacks?.onDeckLoad === 'function') {
×
940
                deckRenderCallbacks.onDeckLoad();
×
941
              }
942
            }}
943
            {...allDeckGlProps}
944
            controller={
945
              isInteractive
29!
946
                ? {
947
                    doubleClickZoom: !isEditorDrawingMode,
948
                    dragRotate: this.props.mapState.dragRotate
949
                  }
950
                : false
951
            }
952
            initialViewState={internalViewState}
953
            onBeforeRender={this._onBeforeRender}
954
            onViewStateChange={isInteractive ? this._onViewportChange : undefined}
29!
955
            {...extraDeckParams}
956
            onHover={
957
              isInteractive
29!
958
                ? data => {
959
                    const res = EditorLayerUtils.onHover(data, {
×
960
                      editorMenuActive,
961
                      editor,
962
                      hoverInfo
963
                    });
964
                    if (res) return;
×
965

966
                    this._onLayerHoverDebounced(data, index);
×
967
                  }
968
                : null
969
            }
970
            onClick={(data, event) => {
971
              // @ts-ignore
972
              normalizeEvent(event.srcEvent, viewport);
×
973
              const res = EditorLayerUtils.onClick(data, event, {
×
974
                editorMenuActive,
975
                editor,
976
                onLayerClick,
977
                setSelectedFeature,
978
                mapIndex: index
979
              });
980
              if (res) return;
×
981

982
              visStateActions.onLayerClick(data);
×
983
            }}
984
            onError={this._onDeckError}
985
            ref={comp => {
986
              // @ts-ignore
987
              if (comp && comp.deck && !this._deck) {
35✔
988
                // @ts-ignore
989
                this._deck = comp.deck;
1✔
990
              }
991
            }}
NEW
992
            onDeviceInitialized={device => this._onDeckInitialized(device)}
×
993
            onAfterRender={() => {
994
              if (typeof deckRenderCallbacks?.onDeckAfterRender === 'function') {
×
995
                deckRenderCallbacks.onDeckAfterRender(allDeckGlProps);
×
996
              }
997

998
              const anyActiveLayerLoading = areAnyDeckLayersLoading(allDeckGlProps.layers);
×
999
              if (anyActiveLayerLoading !== this.anyActiveLayerLoading) {
×
1000
                this._onLayerLoadingStateChange();
×
1001
                this.anyActiveLayerLoading = anyActiveLayerLoading;
×
1002
              }
1003
            }}
1004
          >
1005
            {children}
1006
          </DeckGL>
1007
        </div>
1008
      );
1009
    }
1010

1011
    _updateMapboxLayers() {
1012
      const mapboxLayers = this.mapboxLayersSelector(this.props);
×
1013
      if (!Object.keys(mapboxLayers).length && !Object.keys(this.previousLayers).length) {
×
1014
        return;
×
1015
      }
1016

1017
      updateMapboxLayers(this._map, mapboxLayers, this.previousLayers);
×
1018

1019
      this.previousLayers = mapboxLayers;
×
1020
    }
1021

1022
    _renderMapboxOverlays() {
1023
      if (this._map && this._map.isStyleLoaded()) {
29!
1024
        this._updateMapboxLayers();
×
1025
      }
1026
    }
1027
    _onViewportChangePropagateDebounced = debounce(() => {
25✔
1028
      const viewState = this.context?.getInternalViewState(this.props.index);
×
1029
      onViewPortChange(
×
1030
        viewState,
1031
        this.props.mapStateActions.updateMap,
1032
        this.props.onViewStateChange,
1033
        this.props.primary,
1034
        this.props.index
1035
      );
1036
    }, DEBOUNCE_VIEWPORT_PROPAGATE);
1037

1038
    _onViewportChange = viewport => {
25✔
1039
      const {viewState} = viewport;
×
1040
      if (this.props.isExport) {
×
1041
        // Image export map shouldn't be interactive (otherwise this callback can
1042
        // lead to inadvertent changes to the state of the main map)
1043
        return;
×
1044
      }
1045
      const {setInternalViewState} = this.context;
×
1046
      setInternalViewState(viewState, this.props.index);
×
1047
      this._onViewportChangePropagateDebounced();
×
1048
    };
1049

1050
    _onLayerHoverDebounced = debounce((data, index) => {
25✔
1051
      this.props.visStateActions.onLayerHover(data, index);
×
1052
    }, DEBOUNCE_MOUSE_MOVE_PROPAGATE);
1053

1054
    _onMouseMoveDebounced = debounce((event, viewport) => {
25✔
1055
      this.props.visStateActions.onMouseMove(normalizeEvent(event, viewport));
×
1056
    }, DEBOUNCE_MOUSE_MOVE_PROPAGATE);
1057

1058
    _onLayerLoadingStateChange = debounce(() => {
25✔
1059
      // trigger loading indicator update without any change to update UI
1060
      this.props.visStateActions.setLoadingIndicator({change: 0});
×
1061
    }, DEBOUNCE_LOADING_STATE_PROPAGATE);
1062

1063
    _toggleMapControl = panelId => {
25✔
1064
      const {index, uiStateActions} = this.props;
2✔
1065

1066
      uiStateActions.toggleMapControl(panelId, Number(index));
2✔
1067
    };
1068

1069
    /* eslint-disable complexity */
1070
    _renderMap() {
1071
      const {
1072
        visState,
1073
        mapState,
1074
        mapStyle,
1075
        mapStateActions,
1076
        MapComponent = Map,
×
1077
        mapboxApiAccessToken,
1078
        // mapboxApiUrl,
1079
        mapControls,
1080
        isExport,
1081
        locale,
1082
        uiStateActions,
1083
        visStateActions,
1084
        index,
1085
        primary,
1086
        bottomMapContainerProps,
1087
        topMapContainerProps,
1088
        theme,
1089
        datasetAttributions = [],
×
1090
        containerId = 0,
13✔
1091
        isLoadingIndicatorVisible,
1092
        activeSidePanel,
1093
        sidePanelWidth
1094
      } = this.props;
29✔
1095

1096
      const {layers, datasets, editor, interactionConfig} = visState;
29✔
1097

1098
      const layersToRender = this.layersToRenderSelector(this.props);
29✔
1099
      const layersForDeck = this.layersForDeckSelector(this.props);
29✔
1100

1101
      // Current style can be a custom style, from which we pull the mapbox API acccess token
1102
      const currentStyle = mapStyle.mapStyles?.[mapStyle.styleType];
29✔
1103
      const baseMapLibraryName = getBaseMapLibrary(currentStyle);
29✔
1104
      const baseMapLibraryConfig =
1105
        getApplicationConfig().baseMapLibraryConfig?.[baseMapLibraryName];
29✔
1106

1107
      const internalViewState = this.context?.getInternalViewState(index);
29✔
1108
      const mapProps = {
29✔
1109
        ...internalViewState,
1110
        preserveDrawingBuffer: true,
1111
        mapboxAccessToken: currentStyle?.accessToken || mapboxApiAccessToken,
58✔
1112
        // baseApiUrl: mapboxApiUrl,
1113
        mapLib: baseMapLibraryConfig.getMapLib(),
1114
        transformRequest:
1115
          this.props.transformRequest ||
58✔
1116
          transformRequest(currentStyle?.accessToken || mapboxApiAccessToken)
58✔
1117
      };
1118

1119
      const hasGeocoderLayer = Boolean(layers.find(l => l.id === GEOCODER_LAYER_ID));
29✔
1120
      const isSplit = Boolean(mapState.isSplit);
29✔
1121

1122
      const deck = this._renderDeckOverlay(layersForDeck, {
29✔
1123
        primaryMap: true,
1124
        isInteractive: true,
1125
        children: (
1126
          <MapComponent
1127
            key={`bottom-${baseMapLibraryName}`}
1128
            {...mapProps}
1129
            mapStyle={mapStyle.bottomMapStyle ?? EMPTY_MAPBOX_STYLE}
58✔
1130
            {...bottomMapContainerProps}
1131
            ref={this._setMapRef}
1132
          />
1133
        )
1134
      });
1135
      if (!deck) {
29!
1136
        // deckOverlay can be null if onDeckRender returns null
1137
        // in this case we don't want to render the map
1138
        return null;
×
1139
      }
1140
      return (
29✔
1141
        <>
1142
          <MapControl
1143
            mapState={mapState}
1144
            datasets={datasets}
1145
            availableLocales={LOCALE_CODES_ARRAY}
1146
            dragRotate={mapState.dragRotate}
1147
            isSplit={isSplit}
1148
            primary={Boolean(primary)}
1149
            isExport={isExport}
1150
            layers={layers}
1151
            layersToRender={layersToRender}
1152
            mapIndex={index || 0}
53✔
1153
            mapControls={mapControls}
1154
            readOnly={this.props.readOnly}
1155
            scale={mapState.scale || 1}
58✔
1156
            logoComponent={this.props.logoComponent}
1157
            top={
1158
              interactionConfig.geocoder && interactionConfig.geocoder.enabled
87!
1159
                ? theme.mapControlTop
1160
                : 0
1161
            }
1162
            editor={editor}
1163
            locale={locale}
1164
            onTogglePerspective={mapStateActions.togglePerspective}
1165
            onToggleSplitMap={mapStateActions.toggleSplitMap}
1166
            onMapToggleLayer={this._handleMapToggleLayer}
1167
            onToggleMapControl={this._toggleMapControl}
1168
            onToggleSplitMapViewport={mapStateActions.toggleSplitMapViewport}
1169
            onSetEditorMode={visStateActions.setEditorMode}
1170
            onSetLocale={uiStateActions.setLocale}
1171
            onToggleEditorVisibility={visStateActions.toggleEditorVisibility}
1172
            onLayerVisConfigChange={visStateActions.layerVisConfigChange}
1173
            mapHeight={mapState.height}
1174
            setMapControlSettings={uiStateActions.setMapControlSettings}
1175
            activeSidePanel={activeSidePanel}
1176
          />
1177
          {isSplitSelector(this.props) && <Droppable containerId={containerId} />}
38✔
1178

1179
          {deck}
1180
          {this._renderMapboxOverlays()}
1181
          <Editor
1182
            index={index || 0}
53✔
1183
            datasets={datasets}
1184
            editor={editor}
1185
            filters={this.polygonFiltersSelector(this.props)}
1186
            layers={layers}
1187
            onDeleteFeature={visStateActions.deleteFeature}
1188
            onSelect={visStateActions.setSelectedFeature}
1189
            onTogglePolygonFilter={visStateActions.setPolygonFilterLayer}
1190
            onSetEditorMode={visStateActions.setEditorMode}
1191
            style={{
1192
              pointerEvents: 'all',
1193
              position: 'absolute',
1194
              display: editor.visible ? 'block' : 'none'
29!
1195
            }}
1196
          />
1197
          {this.props.children}
1198
          {mapStyle.topMapStyle ? (
29!
1199
            <MapComponent
1200
              key={`top-${baseMapLibraryName}`}
1201
              viewState={internalViewState}
1202
              mapStyle={mapStyle.topMapStyle}
1203
              style={MAP_STYLE.top}
1204
              mapboxAccessToken={mapProps.mapboxAccessToken}
1205
              transformRequest={mapProps.transformRequest}
1206
              mapLib={baseMapLibraryConfig.getMapLib()}
1207
              {...topMapContainerProps}
1208
            />
1209
          ) : null}
1210

1211
          {hasGeocoderLayer
29!
1212
            ? this._renderDeckOverlay(
1213
                {[GEOCODER_LAYER_ID]: hasGeocoderLayer},
1214
                {primaryMap: false, isInteractive: false}
1215
              )
1216
            : null}
1217
          {this._renderMapPopover()}
1218
          {!isExport && primary !== isSplit ? (
83✔
1219
            <LoadingIndicator
1220
              isVisible={Boolean(isLoadingIndicatorVisible || this.anyActiveLayerLoading)}
34✔
1221
              activeSidePanel={Boolean(activeSidePanel)}
1222
              sidePanelWidth={sidePanelWidth}
1223
            />
1224
          ) : null}
1225
          {this.props.primary ? (
29✔
1226
            <Attribution
1227
              showBaseMapLibLogo={this.state.showBaseMapAttribution}
1228
              showOsmBasemapAttribution={true}
1229
              datasetAttributions={datasetAttributions}
1230
              baseMapLibraryConfig={baseMapLibraryConfig}
1231
            />
1232
          ) : null}
1233
        </>
1234
      );
1235
    }
1236

1237
    render() {
1238
      const {visState, mapStyle} = this.props;
29✔
1239
      const mapContent = this._renderMap();
29✔
1240
      if (!mapContent) {
29!
1241
        // mapContent can be null if onDeckRender returns null
1242
        // in this case we don't want to render the map
1243
        return null;
×
1244
      }
1245

1246
      const currentStyle = mapStyle.mapStyles?.[mapStyle.styleType];
29✔
1247
      const baseMapLibraryName = getBaseMapLibrary(currentStyle);
29✔
1248
      const baseMapLibraryConfig =
1249
        getApplicationConfig().baseMapLibraryConfig?.[baseMapLibraryName];
29✔
1250

1251
      return (
29✔
1252
        <StyledMap
1253
          ref={this._ref}
1254
          style={this.styleSelector(this.props)}
1255
          onContextMenu={event => event.preventDefault()}
×
1256
          $mixBlendMode={visState.overlayBlending}
1257
          $mapLibCssClass={baseMapLibraryConfig.mapLibCssClass}
1258
        >
1259
          {mapContent}
1260
        </StyledMap>
1261
      );
1262
    }
1263
  }
1264

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