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

keplergl / kepler.gl / 23959483528

03 Apr 2026 07:36PM UTC coverage: 59.888% (-1.8%) from 61.699%
23959483528

Pull #3271

github

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

6517 of 12973 branches covered (50.24%)

Branch coverage included in aggregate %.

324 of 1027 new or added lines in 58 files covered. (31.55%)

123 existing lines in 18 files now uncovered.

13313 of 20139 relevant lines covered (66.11%)

78.44 hits per line

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

40.99
/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
    _onFitBounds = (_idx: number, bounds: [number, number, number, number]) => {
25✔
NEW
542
      this.props.mapStateActions.fitBounds(bounds);
×
543
    };
544

545
    _onLayerFilteredItemsChange = (idx, event) => {
25✔
546
      this.props.visStateActions.layerFilteredItemsChange(this.props.visState.layers[idx], event);
×
547
    };
548

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

563
    _handleMapToggleLayer = layerId => {
25✔
564
      const {index: mapIndex = 0, visStateActions} = this.props;
1!
565
      visStateActions.toggleLayerForMap(mapIndex, layerId);
1✔
566
    };
567

568
    _onMapboxStyleUpdate = update => {
25✔
569
      // force refresh mapboxgl layers
570
      this.previousLayers = {};
×
571
      this._updateMapboxLayers();
×
572

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

581
      if (typeof this.props.onMapStyleLoaded === 'function') {
×
582
        this.props.onMapStyleLoaded(this._map);
×
583
      }
584
    };
585

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

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

606
        this._map.on(MAPBOXGL_RENDER, () => {
×
607
          if (typeof this.props.onMapRender === 'function') {
×
608
            this.props.onMapRender(this._map);
×
609
          }
610
        });
611
      }
612

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

621
    _onDeckInitialized(device) {
622
      if (this.props.onDeckInitialized) {
×
NEW
623
        this.props.onDeckInitialized(this._deck, device);
×
624
      }
625
    }
626

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

637
    _onBeforeRender = () => {
25✔
638
      // no-op
639
    };
640

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

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

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

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

680
    /* component render functions */
681

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

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

705
      if (!mousePosition || !interactionConfig.tooltip) {
×
706
        return null;
×
707
      }
708

709
      const layerHoverProp = getLayerHoverProp({
×
710
        animationConfig,
711
        interactionConfig,
712
        hoverInfo,
713
        layers,
714
        layersToRender,
715
        datasets
716
      });
717

718
      const compareMode = interactionConfig.tooltip.config
×
719
        ? interactionConfig.tooltip.config.compareMode
720
        : false;
721

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

743
      const commonProp = {
×
744
        onClose: this._onCloseMapPopover,
745
        zoom: mapState.zoom,
746
        container: this._deck ? this._deck.canvas : undefined
×
747
      };
748

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

783
    /* eslint-enable complexity */
784

785
    _getHoverXY(viewport, lngLat) {
786
      const screenCoord = !viewport || !lngLat ? null : viewport.project(lngLat);
×
787
      return screenCoord && {x: screenCoord[0], y: screenCoord[1]};
×
788
    }
789

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

812
      const {hoverInfo, editor} = visState;
29✔
813
      const {primaryMap, isInteractive, children} = options;
29✔
814

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

820
      const internalViewState = this.context?.getInternalViewState(index);
29✔
821
      const internalMapState = {...mapState, ...internalViewState};
29✔
822
      const viewport = getViewportFromMapState(internalMapState);
29✔
823

824
      const editorFeatureSelectedIndex = this.selectedPolygonIndexSelector(this.props);
29✔
825

826
      const {setFeatures, onLayerClick, setSelectedFeature} = visStateActions;
29✔
827

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

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

880
          return EditorLayerUtils.getTooltip(info, {
×
881
            editorMenuActive,
882
            editor,
883
            theme
884
          });
885
        };
886

887
        extraDeckParams.getCursor = ({isDragging}: {isDragging: boolean}) => {
29✔
888
          const editorCursor = EditorLayerUtils.getCursor({
×
889
            editorMenuActive,
890
            editor,
891
            hoverInfo
892
          });
893
          if (editorCursor) return editorCursor;
×
894

895
          if (isDragging) return 'grabbing';
×
896
          if (hoverInfo?.layer) return 'pointer';
×
897
          return 'grab';
×
898
        };
899
      }
900

901
      const effects = this._isOKToRenderEffects(index)
29!
902
        ? computeDeckEffects({visState, mapState, isExport: this.props.isExport})
903
        : [];
904

905
      const views = deckGlProps?.views
29!
906
        ? deckGlProps?.views()
907
        : new MapView({legacyMeterSizes: true} as ConstructorParameters<typeof MapView>[0] & {
908
            legacyMeterSizes: boolean;
909
          });
910

911
      let allDeckGlProps = {
29✔
912
        ...deckGlProps,
913
        pickingRadius: DEFAULT_PICKING_RADIUS,
914
        views,
915
        layers: deckGlLayers,
916
        effects,
917
        parameters: getLayerBlendingParameters(visState.layerBlending)
918
      };
919

920
      if (typeof deckRenderCallbacks?.onDeckRender === 'function') {
29!
921
        allDeckGlProps = deckRenderCallbacks.onDeckRender(allDeckGlProps);
×
922
        if (!allDeckGlProps) {
×
923
          // if onDeckRender returns null, do not render deck.gl
924
          return null;
×
925
        }
926
      }
927

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

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

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

1003
              const anyActiveLayerLoading = areAnyDeckLayersLoading(allDeckGlProps.layers);
×
1004
              if (anyActiveLayerLoading !== this.anyActiveLayerLoading) {
×
1005
                this._onLayerLoadingStateChange();
×
1006
                this.anyActiveLayerLoading = anyActiveLayerLoading;
×
1007
              }
1008
            }}
1009
          >
1010
            {children}
1011
          </DeckGL>
1012
        </div>
1013
      );
1014
    }
1015

1016
    _updateMapboxLayers() {
1017
      const mapboxLayers = this.mapboxLayersSelector(this.props);
×
1018
      if (!Object.keys(mapboxLayers).length && !Object.keys(this.previousLayers).length) {
×
1019
        return;
×
1020
      }
1021

1022
      updateMapboxLayers(this._map, mapboxLayers, this.previousLayers);
×
1023

1024
      this.previousLayers = mapboxLayers;
×
1025
    }
1026

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

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

1055
    _onLayerHoverDebounced = debounce((data, index) => {
25✔
1056
      this.props.visStateActions.onLayerHover(data, index);
×
1057
    }, DEBOUNCE_MOUSE_MOVE_PROPAGATE);
1058

1059
    _onMouseMoveDebounced = debounce((event, viewport) => {
25✔
1060
      this.props.visStateActions.onMouseMove(normalizeEvent(event, viewport));
×
1061
    }, DEBOUNCE_MOUSE_MOVE_PROPAGATE);
1062

1063
    _onLayerLoadingStateChange = debounce(() => {
25✔
1064
      // trigger loading indicator update without any change to update UI
1065
      this.props.visStateActions.setLoadingIndicator({change: 0});
×
1066
    }, DEBOUNCE_LOADING_STATE_PROPAGATE);
1067

1068
    _toggleMapControl = panelId => {
25✔
1069
      const {index, uiStateActions} = this.props;
2✔
1070

1071
      uiStateActions.toggleMapControl(panelId, Number(index));
2✔
1072
    };
1073

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

1101
      const {layers, datasets, editor, interactionConfig} = visState;
29✔
1102

1103
      const layersToRender = this.layersToRenderSelector(this.props);
29✔
1104
      const layersForDeck = this.layersForDeckSelector(this.props);
29✔
1105

1106
      // Current style can be a custom style, from which we pull the mapbox API acccess token
1107
      const currentStyle = mapStyle.mapStyles?.[mapStyle.styleType];
29✔
1108
      const baseMapLibraryName = getBaseMapLibrary(currentStyle);
29✔
1109
      const baseMapLibraryConfig =
1110
        getApplicationConfig().baseMapLibraryConfig?.[baseMapLibraryName];
29✔
1111

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

1124
      const hasGeocoderLayer = Boolean(layers.find(l => l.id === GEOCODER_LAYER_ID));
29✔
1125
      const isSplit = Boolean(mapState.isSplit);
29✔
1126

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

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

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

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

1251
      const currentStyle = mapStyle.mapStyles?.[mapStyle.styleType];
29✔
1252
      const baseMapLibraryName = getBaseMapLibrary(currentStyle);
29✔
1253
      const baseMapLibraryConfig =
1254
        getApplicationConfig().baseMapLibraryConfig?.[baseMapLibraryName];
29✔
1255

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

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