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

keplergl / kepler.gl / 24838470154

23 Apr 2026 01:38PM UTC coverage: 59.469% (-0.1%) from 59.564%
24838470154

push

github

web-flow
fix: aggregation layers regressions after deck.gl upgrade (#3383)

* fix: fix Map legend is not following hex aggregation values

Signed-off-by: Ihor Dykhta <ihordykhta@Ihors-MacBook-Pro.local>

* more fixes

Signed-off-by: Ihor Dykhta <ihordykhta@Ihors-MacBook-Pro.local>

* revert, allow double event fire

Signed-off-by: Ihor Dykhta <ihordykhta@Ihors-MacBook-Pro.local>

* igr/fix-aggregation-regression

Signed-off-by: Ihor Dykhta <ihordykhta@Ihors-MacBook-Pro.local>

* fix tests

Signed-off-by: Ihor Dykhta <ihordykhta@Ihors-MacBook-Pro.local>

* nit

Signed-off-by: Ihor Dykhta <ihordykhta@Ihors-MacBook-Pro.local>

* move to shared utils

Signed-off-by: Ihor Dykhta <ihordykhta@Ihors-MacBook-Pro.local>

* follow ups

Signed-off-by: Ihor Dykhta <ihordykhta@Ihors-MacBook-Pro.local>

---------

Signed-off-by: Ihor Dykhta <ihordykhta@Ihors-MacBook-Pro.local>
Co-authored-by: Ihor Dykhta <ihordykhta@Ihors-MacBook-Pro.local>

6831 of 13763 branches covered (49.63%)

Branch coverage included in aggregate %.

35 of 82 new or added lines in 5 files covered. (42.68%)

20 existing lines in 2 files now uncovered.

14060 of 21366 relevant lines covered (65.81%)

75.92 hits per line

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

40.88
/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, useTheme} 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
  Layer,
32
  LayerBaseConfig,
33
  VisualChannelDomain,
34
  EditorLayerUtils,
35
  AggregatedBin
36
} from '@kepler.gl/layers';
37
import {
38
  AttributionWithStyle,
39
  DatasetAttribution,
40
  MapState,
41
  MapControls,
42
  Viewport,
43
  SplitMap,
44
  SplitMapLayers
45
} from '@kepler.gl/types';
46
import {
47
  errorNotification,
48
  isStyleUsingMapboxTiles,
49
  isStyleUsingOpenStreetMapTiles,
50
  getBaseMapLibrary,
51
  BaseMapLibraryConfig,
52
  transformRequest,
53
  observeDimensions,
54
  unobserveDimensions,
55
  hasMobileWidth,
56
  getMapLayersFromSplitMaps,
57
  onViewPortChange,
58
  getViewportFromMapState,
59
  normalizeEvent,
60
  rgbToHex,
61
  computeDeckEffects,
62
  getApplicationConfig,
63
  GetMapRef,
64
  getLayerBlendingParameters,
65
  patchDeckRendererForPostProcessing
66
} from '@kepler.gl/utils';
67
import {breakPointValues} from '@kepler.gl/styles';
68

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

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

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

97
import LoadingIndicator from './loading-indicator';
98

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

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

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

124
const LOCALE_CODES_ARRAY = Object.keys(LOCALE_CODES);
7✔
125

126
interface StyledMapContainerProps {
127
  $mixBlendMode?: string;
128
  $mapLibCssClass: string;
129
}
130

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

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

148
type MapLibLogoProps = {
149
  baseMapLibraryConfig: BaseMapLibraryConfig;
150
};
151

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

166
interface StyledDroppableProps {
167
  isOver: boolean;
168
}
169

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

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

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

189
  return <StyledDroppable ref={setNodeRef} isOver={isOver} />;
9✔
190
};
191

192
interface StyledDatasetAttributionsContainerProps {
193
  isPalm: boolean;
194
}
195

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

206
  &:hover {
207
    white-space: inherit;
208
  }
209
`;
210

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

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

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

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

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

304
  return memoizedComponents;
28✔
305
};
306

307
const StyledAttributionLogoContainer = styled.div<{$left: number}>`
7✔
308
  position: absolute;
309
  bottom: ${props => props.theme.sidePanel.margin.left}px;
×
310
  left: ${props => props.$left}px;
×
311
  z-index: 1;
312
  display: flex;
313
  align-items: flex-end;
314
  gap: 4px;
315
  pointer-events: auto;
316
  transition: left 250ms ease-in-out;
317
`;
318

319
const StyledLogoLink = styled.a<{$enabled: boolean}>`
7✔
320
  cursor: ${props => (props.$enabled ? 'pointer' : 'default')};
×
321
  display: flex;
322
  align-items: flex-end;
323
`;
324

325
type AttributionLogosProps = {
326
  logos: AttributionWithStyle[];
327
  activeSidePanel?: boolean;
328
  sidePanelWidth?: number;
329
};
330

331
const LOGO_LEFT_ADJUSTMENT = 3;
7✔
332

333
export const AttributionLogos: React.FC<AttributionLogosProps> = ({
7✔
334
  logos,
335
  activeSidePanel,
336
  sidePanelWidth
337
}) => {
338
  const theme = useTheme() as any;
28✔
339
  const left =
340
    (activeSidePanel ? (sidePanelWidth || 0) + LOGO_LEFT_ADJUSTMENT : 0) +
28!
341
    theme.sidePanel.margin.left;
342

343
  if (!logos?.length) return null;
28!
344
  return (
×
345
    <StyledAttributionLogoContainer $left={left}>
346
      {logos.map((logo, idx) => (
347
        <StyledLogoLink
×
348
          key={logo.logoUrl || idx}
×
349
          href={logo.url || undefined}
×
350
          {...(logo.url ? {target: '_blank', rel: 'noopener noreferrer'} : {})}
×
351
          $enabled={Boolean(logo.url)}
352
          style={logo.bottom ? {marginBottom: logo.bottom} : undefined}
×
353
        >
354
          <img src={logo.logoUrl} style={{height: logo.height || 12}} alt={logo.title} />
×
355
        </StyledLogoLink>
356
      ))}
357
    </StyledAttributionLogoContainer>
358
  );
359
};
360

361
MapContainerFactory.deps = [MapPopoverFactory, MapControlFactory, EditorFactory];
7✔
362

363
type MapboxStyle = string | object | undefined;
364
type PropSelector<R> = Selector<MapContainerProps, R>;
365

366
export interface MapContainerProps {
367
  visState: VisState;
368
  mapState: MapState;
369
  mapControls: MapControls;
370
  mapStyle: {bottomMapStyle?: MapboxStyle; topMapStyle?: MapboxStyle} & MapStyle;
371
  mapboxApiAccessToken: string;
372
  mapboxApiUrl: string;
373
  visStateActions: typeof VisStateActions;
374
  mapStateActions: typeof MapStateActions;
375
  uiStateActions: typeof UIStateActions;
376

377
  // optional
378
  primary?: boolean; // primary one will be reporting its size to appState
379
  readOnly?: boolean;
380
  isExport?: boolean;
381
  // onMapStyleLoaded?: (map: maplibregl.Map | ReturnType<MapRef['getMap']> | null) => void;
382
  onMapStyleLoaded?: (map: GetMapRef | null) => void;
383
  onMapRender?: (map: GetMapRef | null) => void;
384
  getMapboxRef?: (mapbox?: MapRef | null, index?: number) => void;
385
  index?: number;
386
  deleteMapLabels?: (containerId: string, layerId: string) => void;
387
  containerId?: number;
388

389
  isLoadingIndicatorVisible?: boolean;
390
  activeSidePanel: string | null;
391
  sidePanelWidth?: number;
392

393
  locale?: any;
394
  theme?: any;
395
  editor?: any;
396
  MapComponent?: typeof Map;
397
  deckGlProps?: any;
398
  onDeckInitialized?: (a: any, b: any) => void;
399
  onViewStateChange?: (viewport: Viewport) => void;
400

401
  topMapContainerProps: any;
402
  bottomMapContainerProps: any;
403
  transformRequest?: (url: string, resourceType?: string) => {url: string};
404

405
  datasetAttributions?: DatasetAttribution[];
406
  attributionLogos?: AttributionWithStyle[];
407

408
  generateMapboxLayers?: typeof generateMapboxLayers;
409
  generateDeckGLLayers?: typeof computeDeckLayers;
410

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

413
  children?: React.ReactNode;
414
  deckRenderCallbacks?: {
415
    onDeckLoad?: () => void;
416
    onDeckRender?: (deckProps: Record<string, unknown>) => Record<string, unknown> | null;
417
    onDeckAfterRender?: (deckProps: Record<string, unknown>) => any;
418
  };
419

420
  // Optional: override legend header logo in map controls (used by image export)
421
  logoComponent?: React.FC | React.ReactNode;
422
}
423

424
export default function MapContainerFactory(
425
  MapPopover: ReturnType<typeof MapPopoverFactory>,
426
  MapControl: ReturnType<typeof MapControlFactory>,
427
  Editor: ReturnType<typeof EditorFactory>
428
): React.ComponentType<MapContainerProps> {
429
  class MapContainer extends Component<MapContainerProps> {
430
    displayName = 'MapContainer';
25✔
431

432
    private anyActiveLayerLoading = false;
25✔
433

434
    static contextType = MapViewStateContext;
14✔
435

436
    declare context: React.ContextType<typeof MapViewStateContext>;
437

438
    static defaultProps = {
14✔
439
      MapComponent: Map,
440
      deckGlProps: {},
441
      index: 0,
442
      primary: true
443
    };
444

445
    constructor(props) {
446
      super(props);
25✔
447
      patchDeckRendererForPostProcessing();
25✔
448
    }
449

450
    state = {
25✔
451
      // Determines whether attribution should be visible based the result of loading the map style
452
      showBaseMapAttribution: true
453
    };
454

455
    componentDidMount() {
456
      if (!this._ref.current) {
25!
457
        return;
×
458
      }
459
      observeDimensions(this._ref.current, this._handleResize);
25✔
460
    }
461

462
    componentWillUnmount() {
463
      // unbind mapboxgl event listener
464
      if (this._map) {
2!
465
        this._map?.off(MAPBOXGL_STYLE_UPDATE, nop);
×
466
        this._map?.off(MAPBOXGL_RENDER, nop);
×
467
      }
468
      if (!this._ref.current) {
2!
469
        return;
×
470
      }
471
      unobserveDimensions(this._ref.current);
2✔
472
    }
473

474
    _deck: any = null;
25✔
475
    _map: GetMapRef | null = null;
25✔
476
    _ref = createRef<HTMLDivElement>();
25✔
477
    _deckGLErrorsElapsed: {[id: string]: number} = {};
25✔
478

479
    previousLayers = {
25✔
480
      // [layers.id]: mapboxLayerConfig
481
    };
482

483
    _handleResize = dimensions => {
25✔
484
      const {primary, index} = this.props;
×
485
      if (primary) {
×
486
        const {mapStateActions} = this.props;
×
487
        if (dimensions && dimensions.width > 0 && dimensions.height > 0) {
×
488
          mapStateActions.updateMap(dimensions, index);
×
489
        }
490
      }
491
    };
492

493
    layersSelector: PropSelector<VisState['layers']> = props => props.visState.layers;
56✔
494
    layerDataSelector: PropSelector<VisState['layers']> = props => props.visState.layerData;
56✔
495
    splitMapSelector: PropSelector<SplitMap[]> = props => props.visState.splitMaps;
28✔
496
    splitMapIndexSelector: PropSelector<number | undefined> = props => props.index;
28✔
497
    mapLayersSelector: PropSelector<SplitMapLayers | null | undefined> = createSelector(
25✔
498
      this.splitMapSelector,
499
      this.splitMapIndexSelector,
500
      getMapLayersFromSplitMaps
501
    );
502
    layerOrderSelector: PropSelector<VisState['layerOrder']> = props => props.visState.layerOrder;
25✔
503
    layersToRenderSelector: PropSelector<LayersToRender> = createSelector(
25✔
504
      this.layersSelector,
505
      this.layerDataSelector,
506
      this.mapLayersSelector,
507
      prepareLayersToRender
508
    );
509
    layersForDeckSelector = createSelector(
25✔
510
      this.layersSelector,
511
      this.layerDataSelector,
512
      prepareLayersForDeck
513
    );
514
    filtersSelector = props => props.visState.filters;
28✔
515
    polygonFiltersSelector = createSelector(this.filtersSelector, filters =>
25✔
516
      filters.filter(f => f.type === FILTER_TYPES.polygon && f.enabled !== false)
26!
517
    );
518
    featuresSelector = props => props.visState.editor.features;
28✔
519
    selectedFeatureSelector = props => props.visState.editor.selectedFeature;
28✔
520
    featureCollectionSelector = createSelector(
25✔
521
      this.polygonFiltersSelector,
522
      this.featuresSelector,
523
      (polygonFilters, features) => ({
26✔
524
        type: 'FeatureCollection',
525
        features: features.concat(polygonFilters.map(f => f.value))
×
526
      })
527
    );
528
    // @ts-ignore - No overload matches this call
529
    selectedPolygonIndexSelector = createSelector(
25✔
530
      this.featureCollectionSelector,
531
      this.selectedFeatureSelector,
532
      (collection, selectedFeature) =>
533
        collection.features.findIndex(f => f.id === selectedFeature?.id)
26✔
534
    );
535
    selectedFeatureIndexArraySelector = createSelector(
25✔
536
      (value: number) => value,
25✔
537
      value => {
538
        return value < 0 ? [] : [value];
25!
539
      }
540
    );
541

542
    generateMapboxLayerMethodSelector = props => props.generateMapboxLayers ?? generateMapboxLayers;
25!
543

544
    mapboxLayersSelector = createSelector(
25✔
545
      this.layersSelector,
546
      this.layerDataSelector,
547
      this.layerOrderSelector,
548
      this.layersToRenderSelector,
549
      this.generateMapboxLayerMethodSelector,
550
      (layer, layerData, layerOrder, layersToRender, generateMapboxLayerMethod) =>
551
        generateMapboxLayerMethod(layer, layerData, layerOrder, layersToRender)
×
552
    );
553

554
    // merge in a background-color style if the basemap choice is NO_MAP_ID
555
    // used by <StyledMap> inline style prop
556
    mapStyleTypeSelector = props => props.mapStyle.styleType;
28✔
557
    mapStyleBackgroundColorSelector = props => props.mapStyle.backgroundColor;
28✔
558
    styleSelector = createSelector(
25✔
559
      this.mapStyleTypeSelector,
560
      this.mapStyleBackgroundColorSelector,
561
      (styleType, backgroundColor) => ({
26✔
562
        ...MAP_STYLE.container,
563
        ...(styleType === NO_MAP_ID ? {backgroundColor: rgbToHex(backgroundColor)} : {})
26!
564
      })
565
    );
566

567
    /* component private functions */
568
    _onCloseMapPopover = () => {
25✔
569
      this.props.visStateActions.onLayerClick(null);
×
570
    };
571

572
    _onLayerHover = (_idx: number, info: PickingInfo<any> | null) => {
25✔
573
      this.props.visStateActions.onLayerHover(info, this.props.index);
×
574
    };
575

576
    _onLayerSetDomain = (
25✔
577
      idx: number,
578
      value: number[] | {domain: VisualChannelDomain; aggregatedBins: Record<number, AggregatedBin>}
579
    ) => {
580
      // deck.gl 9 native aggregation layers (Grid, Hexagon) pass
581
      // {domain, aggregatedBins} via our ScaleEnhanced* overrides,
582
      // while ClusterLayer's CPUAggregator also passes {domain, aggregatedBins}.
583
      // Plain [min, max] is a fallback if the override is bypassed.
UNCOV
584
      const config = Array.isArray(value)
×
585
        ? {colorDomain: value as VisualChannelDomain}
586
        : {colorDomain: value.domain, aggregatedBins: value.aggregatedBins};
587

588
      const layer = this.props.visState.layers[idx];
×
589
      if (!layer) return;
×
590

591
      this.props.visStateActions.layerConfigChange(layer, config as Partial<LayerBaseConfig>);
×
592
    };
593

594
    _onRedrawNeeded = (_idx: number) => {
25✔
595
      // updateMapUpdater always returns a new state object reference, which triggers re-render
596
      const {mapStateActions, index} = this.props;
×
597
      mapStateActions.updateMap({}, index);
×
598
    };
599

600
    _onFitBounds = (_idx: number, bounds: [number, number, number, number]) => {
25✔
601
      this.props.mapStateActions.fitBounds(bounds);
×
602
    };
603

604
    _onLayerFilteredItemsChange = (idx, event) => {
25✔
605
      this.props.visStateActions.layerFilteredItemsChange(this.props.visState.layers[idx], event);
×
606
    };
607

608
    _onWMSFeatureInfo = (
25✔
609
      idx: number,
610
      data: {
611
        featureInfo: Array<{name: string; value: string}> | string | null;
612
        coordinate?: [number, number] | null;
613
      }
614
    ) => {
615
      this.props.visStateActions.wmsFeatureInfo(
×
616
        this.props.visState.layers[idx],
617
        data.featureInfo,
618
        data.coordinate
619
      );
620
    };
621

622
    _handleMapToggleLayer = layerId => {
25✔
623
      const {index: mapIndex = 0, visStateActions} = this.props;
1!
624
      visStateActions.toggleLayerForMap(mapIndex, layerId);
1✔
625
    };
626

627
    _onMapboxStyleUpdate = update => {
25✔
628
      // force refresh mapboxgl layers
629
      this.previousLayers = {};
×
630
      this._updateMapboxLayers();
×
631

632
      if (update && update.style) {
×
633
        // No attributions are needed if the style doesn't reference Mapbox sources
634
        this.setState({
×
635
          showBaseMapAttribution:
636
            isStyleUsingMapboxTiles(update.style) || !isStyleUsingOpenStreetMapTiles(update.style)
×
637
        });
638
      }
639

640
      if (typeof this.props.onMapStyleLoaded === 'function') {
×
641
        this.props.onMapStyleLoaded(this._map);
×
642
      }
643
    };
644

645
    _setMapRef = mapRef => {
25✔
646
      // Handle change of the map library
647
      if (this._map && mapRef) {
×
648
        const map = mapRef.getMap();
×
649
        if (map && this._map !== map) {
×
650
          this._map?.off(MAPBOXGL_STYLE_UPDATE, nop);
×
651
          this._map?.off(MAPBOXGL_RENDER, nop);
×
652
          this._map = null;
×
653
        }
654
      }
655

656
      if (!this._map && mapRef) {
×
657
        this._map = mapRef.getMap();
×
658
        // i noticed in certain context we don't access the actual map element
659
        if (!this._map) {
×
660
          return;
×
661
        }
662
        // bind mapboxgl event listener
663
        this._map.on(MAPBOXGL_STYLE_UPDATE, this._onMapboxStyleUpdate);
×
664

665
        this._map.on(MAPBOXGL_RENDER, () => {
×
666
          if (typeof this.props.onMapRender === 'function') {
×
667
            this.props.onMapRender(this._map);
×
668
          }
669
        });
670
      }
671

672
      if (this.props.getMapboxRef) {
×
673
        // The parent component can gain access to our MapboxGlMap by
674
        // providing this callback. Note that 'mapbox' will be null when the
675
        // ref is unset (e.g. when a split map is closed).
676
        this.props.getMapboxRef(mapRef, this.props.index);
×
677
      }
678
    };
679

680
    _onDeckInitialized(device) {
681
      if (this.props.onDeckInitialized) {
×
682
        this.props.onDeckInitialized(this._deck, device);
×
683
      }
684
    }
685

686
    /**
687
     * 1) Allow effects only for the first view.
688
     * 2) Prevent effect:preRender call without valid generated viewports.
689
     * @param viewIndex View index.
690
     * @returns Returns true if effects can be used.
691
     */
692
    _isOKToRenderEffects(viewIndex?: number): boolean {
693
      return !viewIndex && Boolean(this._deck?.viewManager?._viewports?.length);
29✔
694
    }
695

696
    _onBeforeRender = () => {
25✔
697
      // no-op
698
    };
699

700
    _onDeckError = (error, layer) => {
25✔
701
      const errorMessage = error?.message || 'unknown-error';
×
702
      const layerMessage = layer?.id ? ` in ${layer.id} layer` : '';
×
703
      const errorMessageFull =
704
        errorMessage === 'WebGL context is lost'
×
705
          ? '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.'
706
          : `An error in deck.gl: ${errorMessage}${layerMessage}.`;
707

708
      // Throttle error notifications, as React doesn't like too many state changes from here.
709
      const lastShown = this._deckGLErrorsElapsed[errorMessageFull];
×
710
      if (!lastShown || lastShown < Date.now() - THROTTLE_NOTIFICATION_TIME) {
×
711
        this._deckGLErrorsElapsed[errorMessageFull] = Date.now();
×
712

713
        // Mark layer as invalid
714
        let extraLayerMessage = '';
×
715
        const {visStateActions} = this.props;
×
716
        if (layer) {
×
717
          let topMostLayer = layer;
×
718
          while (topMostLayer.parent) {
×
719
            topMostLayer = topMostLayer.parent;
×
720
          }
721
          if (topMostLayer.props?.id) {
×
722
            visStateActions.layerSetIsValid(topMostLayer, false);
×
723
            extraLayerMessage = 'The layer has been disabled and highlighted.';
×
724
          }
725
        }
726

727
        // Create new error notification or update existing one with same id.
728
        // Update is required to preserve the order of notifications as they probably are going to "jump" based on order of errors.
729
        const {uiStateActions} = this.props;
×
730
        uiStateActions.addNotification(
×
731
          errorNotification({
732
            message: `${errorMessageFull} ${extraLayerMessage}`,
733
            id: errorMessageFull // treat the error message as id
734
          })
735
        );
736
      }
737
    };
738

739
    /* component render functions */
740

741
    /* eslint-disable complexity */
742
    _renderMapPopover() {
743
      // this check is for limiting the display of the `<MapPopover>` to the `<MapContainer>` the user is interacting with
744
      // the DeckGL onHover event handler adds a `mapIndex` property which is available in the `hoverInfo` object of `visState`
745
      if (this.props.index !== this.props.visState.hoverInfo?.mapIndex) {
29!
746
        return null;
29✔
747
      }
748

749
      // TODO: move this into reducer so it can be tested
750
      const {
751
        mapState,
752
        visState: {
753
          hoverInfo,
754
          clicked,
755
          datasets,
756
          interactionConfig,
757
          animationConfig,
758
          layers,
759
          mousePos: {mousePosition, coordinate, pinned}
760
        }
761
      } = this.props;
×
762
      const layersToRender = this.layersToRenderSelector(this.props);
×
763

764
      if (!mousePosition || !interactionConfig.tooltip) {
×
765
        return null;
×
766
      }
767

768
      const layerHoverProp = getLayerHoverProp({
×
769
        animationConfig,
770
        interactionConfig,
771
        hoverInfo,
772
        layers,
773
        layersToRender,
774
        datasets
775
      });
776

777
      const compareMode = interactionConfig.tooltip.config
×
778
        ? interactionConfig.tooltip.config.compareMode
779
        : false;
780

781
      let pinnedPosition = {x: 0, y: 0};
×
782
      let layerPinnedProp: LayerHoverProp | null = null;
×
783
      if (pinned || clicked) {
×
784
        // project lnglat to screen so that tooltip follows the object on zoom
785
        const viewport = getViewportFromMapState(mapState);
×
786
        const lngLat = clicked ? clicked.coordinate : pinned.coordinate;
×
787
        pinnedPosition = this._getHoverXY(viewport, lngLat);
×
788
        layerPinnedProp = getLayerHoverProp({
×
789
          animationConfig,
790
          interactionConfig,
791
          hoverInfo: clicked,
792
          layers,
793
          layersToRender,
794
          datasets
795
        });
796
        if (layerHoverProp && layerPinnedProp) {
×
797
          layerHoverProp.primaryData = layerPinnedProp.data;
×
798
          layerHoverProp.compareType = interactionConfig.tooltip.config.compareType;
×
799
        }
800
      }
801

802
      const commonProp = {
×
803
        onClose: this._onCloseMapPopover,
804
        zoom: mapState.zoom,
805
        container: this._deck ? this._deck.canvas : undefined
×
806
      };
807

808
      return (
×
809
        <ErrorBoundary>
810
          {layerPinnedProp && (
×
811
            <MapPopover
812
              {...pinnedPosition}
813
              {...commonProp}
814
              layerHoverProp={layerPinnedProp}
815
              coordinate={interactionConfig.coordinate.enabled && (pinned || {}).coordinate}
×
816
              frozen={true}
817
              isBase={compareMode}
818
              onSetFeatures={this.props.visStateActions.setFeatures}
819
              setSelectedFeature={this.props.visStateActions.setSelectedFeature}
820
              // @ts-ignore Argument of type 'Readonly<MapContainerProps>' is not assignable to parameter of type 'never'
821
              featureCollection={this.featureCollectionSelector(this.props)}
822
            />
823
          )}
824
          {layerHoverProp && (!layerPinnedProp || compareMode) && (
×
825
            <MapPopover
826
              x={mousePosition[0]}
827
              y={mousePosition[1]}
828
              {...commonProp}
829
              layerHoverProp={layerHoverProp}
830
              frozen={false}
831
              coordinate={interactionConfig.coordinate.enabled && coordinate}
×
832
              onSetFeatures={this.props.visStateActions.setFeatures}
833
              setSelectedFeature={this.props.visStateActions.setSelectedFeature}
834
              // @ts-ignore Argument of type 'Readonly<MapContainerProps>' is not assignable to parameter of type 'never'
835
              featureCollection={this.featureCollectionSelector(this.props)}
836
            />
837
          )}
838
        </ErrorBoundary>
839
      );
840
    }
841

842
    /* eslint-enable complexity */
843

844
    _getHoverXY(viewport, lngLat) {
845
      const screenCoord = !viewport || !lngLat ? null : viewport.project(lngLat);
×
846
      return screenCoord && {x: screenCoord[0], y: screenCoord[1]};
×
847
    }
848

849
    _renderDeckOverlay(
850
      layersForDeck,
851
      options: {primaryMap: boolean; isInteractive?: boolean; children?: React.ReactNode} = {
×
852
        primaryMap: false
853
      }
854
    ) {
855
      const {
856
        mapStyle,
857
        visState,
858
        mapState,
859
        visStateActions,
860
        mapboxApiAccessToken,
861
        mapboxApiUrl,
862
        deckGlProps,
863
        index,
864
        mapControls,
865
        deckRenderCallbacks,
866
        theme,
867
        generateDeckGLLayers,
868
        onMouseMove
869
      } = this.props;
29✔
870

871
      const {hoverInfo, editor} = visState;
29✔
872
      const {primaryMap, isInteractive, children} = options;
29✔
873

874
      // disable double click zoom when editor is in any draw mode
875
      const {mapDraw} = mapControls;
29✔
876
      const {active: editorMenuActive = false} = mapDraw || {};
29✔
877
      const isEditorDrawingMode = EditorLayerUtils.isDrawingActive(editorMenuActive, editor.mode);
29✔
878

879
      const internalViewState = this.context?.getInternalViewState(index);
29✔
880
      const internalMapState = {...mapState, ...internalViewState};
29✔
881
      const viewport = getViewportFromMapState(internalMapState);
29✔
882

883
      const editorFeatureSelectedIndex = this.selectedPolygonIndexSelector(this.props);
29✔
884

885
      const {setFeatures, onLayerClick, setSelectedFeature} = visStateActions;
29✔
886

887
      const generateDeckGLLayersMethod = generateDeckGLLayers ?? computeDeckLayers;
29✔
888
      const deckGlLayers = generateDeckGLLayersMethod(
29✔
889
        {
890
          visState,
891
          mapState: internalMapState,
892
          mapStyle
893
        },
894
        {
895
          mapIndex: index,
896
          primaryMap,
897
          mapboxApiAccessToken,
898
          mapboxApiUrl,
899
          layersForDeck,
900
          editorInfo: primaryMap
29!
901
            ? {
902
                editor,
903
                editorMenuActive,
904
                onSetFeatures: setFeatures,
905
                setSelectedFeature,
906
                // @ts-ignore Argument of type 'Readonly<MapContainerProps>' is not assignable to parameter of type 'never'
907
                featureCollection: this.featureCollectionSelector(this.props),
908
                selectedFeatureIndexes: this.selectedFeatureIndexArraySelector(
909
                  // @ts-ignore Argument of type 'unknown' is not assignable to parameter of type 'number'.
910
                  editorFeatureSelectedIndex
911
                ),
912
                viewport
913
              }
914
            : undefined
915
        },
916
        {
917
          onLayerHover: this._onLayerHover,
918
          onSetLayerDomain: this._onLayerSetDomain,
919
          onFilteredItemsChange: this._onLayerFilteredItemsChange,
920
          onWMSFeatureInfo: this._onWMSFeatureInfo,
921
          onRedrawNeeded: this._onRedrawNeeded,
922
          onFitBounds: this._onFitBounds
923
        },
924
        deckGlProps
925
      );
926

927
      const extraDeckParams: {
928
        getTooltip?: (info: any) => object | null;
929
        getCursor?: ({isDragging}: {isDragging: boolean}) => string;
930
      } = {};
29✔
931
      if (primaryMap) {
29!
932
        // Omit hover updates when the pointer position is invalid, ie. over UI overlays or
933
        // outside the map container. In those cases x/y may be < 0
934
        extraDeckParams.getTooltip = info => {
29✔
935
          const x = Number(info?.x);
×
936
          const y = Number(info?.y);
×
937
          if (Number.isNaN(x) || Number.isNaN(y) || x < 0 || y < 0) return null;
×
938

939
          return EditorLayerUtils.getTooltip(info, {
×
940
            editorMenuActive,
941
            editor,
942
            theme
943
          });
944
        };
945

946
        extraDeckParams.getCursor = ({isDragging}: {isDragging: boolean}) => {
29✔
947
          const editorCursor = EditorLayerUtils.getCursor({
×
948
            editorMenuActive,
949
            editor,
950
            hoverInfo
951
          });
952
          if (editorCursor) return editorCursor;
×
953

954
          if (isDragging) return 'grabbing';
×
955
          if (hoverInfo?.layer) return 'pointer';
×
956
          return 'grab';
×
957
        };
958
      }
959

960
      const effects = this._isOKToRenderEffects(index)
29!
961
        ? computeDeckEffects({visState, mapState, isExport: this.props.isExport})
962
        : [];
963

964
      const views = deckGlProps?.views
29!
965
        ? deckGlProps?.views()
966
        : new MapView({legacyMeterSizes: true, farZMultiplier: 1.2} as ConstructorParameters<
967
            typeof MapView
968
          >[0] & {
969
            legacyMeterSizes: boolean;
970
          });
971

972
      let allDeckGlProps = {
29✔
973
        ...deckGlProps,
974
        pickingRadius: DEFAULT_PICKING_RADIUS,
975
        views,
976
        layers: deckGlLayers,
977
        effects,
978
        parameters: getLayerBlendingParameters(visState.layerBlending)
979
      };
980

981
      if (typeof deckRenderCallbacks?.onDeckRender === 'function') {
29!
982
        allDeckGlProps = deckRenderCallbacks.onDeckRender(allDeckGlProps);
×
983
        if (!allDeckGlProps) {
×
984
          // if onDeckRender returns null, do not render deck.gl
985
          return null;
×
986
        }
987
      }
988

989
      return (
29✔
990
        <div
991
          {...(isInteractive
29!
992
            ? {
993
                onMouseMove: primaryMap
29!
994
                  ? event => {
995
                      onMouseMove?.(event);
×
996
                      this._onMouseMoveDebounced(event, viewport);
×
997
                    }
998
                  : undefined
999
              }
1000
            : {style: {pointerEvents: 'none'}})}
1001
        >
1002
          <DeckGL
1003
            id="default-deckgl-overlay"
1004
            onLoad={() => {
1005
              if (typeof deckRenderCallbacks?.onDeckLoad === 'function') {
×
1006
                deckRenderCallbacks.onDeckLoad();
×
1007
              }
1008
            }}
1009
            {...allDeckGlProps}
1010
            controller={
1011
              isInteractive
29!
1012
                ? {
1013
                    doubleClickZoom: !isEditorDrawingMode,
1014
                    dragRotate: this.props.mapState.dragRotate
1015
                  }
1016
                : false
1017
            }
1018
            initialViewState={internalViewState}
1019
            onBeforeRender={this._onBeforeRender}
1020
            onViewStateChange={isInteractive ? this._onViewportChange : undefined}
29!
1021
            {...extraDeckParams}
1022
            onHover={
1023
              isInteractive
29!
1024
                ? data => {
1025
                    const res = EditorLayerUtils.onHover(data, {
×
1026
                      editorMenuActive,
1027
                      editor,
1028
                      hoverInfo
1029
                    });
1030
                    if (res) return;
×
1031

1032
                    this._onLayerHoverDebounced(data, index);
×
1033
                  }
1034
                : null
1035
            }
1036
            onClick={(data, event) => {
1037
              // @ts-ignore
1038
              normalizeEvent(event.srcEvent, viewport);
×
1039
              const res = EditorLayerUtils.onClick(data, event, {
×
1040
                editorMenuActive,
1041
                editor,
1042
                onLayerClick,
1043
                setSelectedFeature,
1044
                mapIndex: index
1045
              });
1046
              if (res) return;
×
1047

1048
              visStateActions.onLayerClick(data);
×
1049
            }}
1050
            onError={this._onDeckError}
1051
            ref={comp => {
1052
              // @ts-ignore
1053
              if (comp && comp.deck && !this._deck) {
35✔
1054
                // @ts-ignore
1055
                this._deck = comp.deck;
1✔
1056
              }
1057
            }}
1058
            onDeviceInitialized={device => this._onDeckInitialized(device)}
×
1059
            onAfterRender={() => {
1060
              if (typeof deckRenderCallbacks?.onDeckAfterRender === 'function') {
×
1061
                deckRenderCallbacks.onDeckAfterRender(allDeckGlProps);
×
1062
              }
1063

1064
              const anyActiveLayerLoading = areAnyDeckLayersLoading(allDeckGlProps.layers);
×
1065
              if (anyActiveLayerLoading !== this.anyActiveLayerLoading) {
×
1066
                this._onLayerLoadingStateChange();
×
1067
                this.anyActiveLayerLoading = anyActiveLayerLoading;
×
1068
              }
1069
            }}
1070
          >
1071
            {children}
1072
          </DeckGL>
1073
        </div>
1074
      );
1075
    }
1076

1077
    _updateMapboxLayers() {
1078
      const mapboxLayers = this.mapboxLayersSelector(this.props);
×
1079
      if (!Object.keys(mapboxLayers).length && !Object.keys(this.previousLayers).length) {
×
1080
        return;
×
1081
      }
1082

1083
      updateMapboxLayers(this._map, mapboxLayers, this.previousLayers);
×
1084

1085
      this.previousLayers = mapboxLayers;
×
1086
    }
1087

1088
    _renderMapboxOverlays() {
1089
      if (this._map && this._map.isStyleLoaded()) {
29!
1090
        this._updateMapboxLayers();
×
1091
      }
1092
    }
1093
    _onViewportChangePropagateDebounced = debounce(() => {
25✔
1094
      const viewState = this.context?.getInternalViewState(this.props.index);
×
1095
      onViewPortChange(
×
1096
        viewState,
1097
        this.props.mapStateActions.updateMap,
1098
        this.props.onViewStateChange,
1099
        this.props.primary,
1100
        this.props.index
1101
      );
1102
    }, DEBOUNCE_VIEWPORT_PROPAGATE);
1103

1104
    _onViewportChange = viewport => {
25✔
1105
      const {viewState} = viewport;
×
1106
      if (this.props.isExport) {
×
1107
        // Image export map shouldn't be interactive (otherwise this callback can
1108
        // lead to inadvertent changes to the state of the main map)
1109
        return;
×
1110
      }
1111
      const {setInternalViewState} = this.context;
×
1112
      setInternalViewState(viewState, this.props.index);
×
1113
      this._onViewportChangePropagateDebounced();
×
1114
    };
1115

1116
    _onLayerHoverDebounced = debounce((data, index) => {
25✔
1117
      this.props.visStateActions.onLayerHover(data, index);
×
1118
    }, DEBOUNCE_MOUSE_MOVE_PROPAGATE);
1119

1120
    _onMouseMoveDebounced = debounce((event, viewport) => {
25✔
1121
      this.props.visStateActions.onMouseMove(normalizeEvent(event, viewport));
×
1122
    }, DEBOUNCE_MOUSE_MOVE_PROPAGATE);
1123

1124
    _onLayerLoadingStateChange = debounce(() => {
25✔
1125
      // trigger loading indicator update without any change to update UI
1126
      this.props.visStateActions.setLoadingIndicator({change: 0});
×
1127
    }, DEBOUNCE_LOADING_STATE_PROPAGATE);
1128

1129
    _handleToggleLayerVisibility = (layer: Layer) => {
25✔
1130
      const {visStateActions} = this.props;
×
1131
      visStateActions.layerConfigChange(layer, {isVisible: !layer.config.isVisible});
×
1132
    };
1133

1134
    _toggleMapControl = panelId => {
25✔
1135
      const {index, uiStateActions} = this.props;
2✔
1136

1137
      uiStateActions.toggleMapControl(panelId, Number(index));
2✔
1138
    };
1139

1140
    /* eslint-disable complexity */
1141
    _renderMap() {
1142
      const {
1143
        visState,
1144
        mapState,
1145
        mapStyle,
1146
        mapStateActions,
1147
        MapComponent = Map,
×
1148
        mapboxApiAccessToken,
1149
        // mapboxApiUrl,
1150
        mapControls,
1151
        isExport,
1152
        locale,
1153
        uiStateActions,
1154
        visStateActions,
1155
        index,
1156
        primary,
1157
        bottomMapContainerProps,
1158
        topMapContainerProps,
1159
        theme,
1160
        datasetAttributions = [],
×
1161
        attributionLogos = [],
×
1162
        containerId = 0,
13✔
1163
        isLoadingIndicatorVisible,
1164
        activeSidePanel,
1165
        sidePanelWidth
1166
      } = this.props;
29✔
1167

1168
      const {layers, datasets, editor, interactionConfig} = visState;
29✔
1169

1170
      const layersToRender = this.layersToRenderSelector(this.props);
29✔
1171
      const layersForDeck = this.layersForDeckSelector(this.props);
29✔
1172

1173
      // Current style can be a custom style, from which we pull the mapbox API acccess token
1174
      const currentStyle = mapStyle.mapStyles?.[mapStyle.styleType];
29✔
1175
      const baseMapLibraryName = getBaseMapLibrary(currentStyle);
29✔
1176
      const baseMapLibraryConfig =
1177
        getApplicationConfig().baseMapLibraryConfig?.[baseMapLibraryName];
29✔
1178

1179
      const internalViewState = this.context?.getInternalViewState(index);
29✔
1180
      const mapProps = {
29✔
1181
        ...internalViewState,
1182
        preserveDrawingBuffer: this.props.isExport ?? false,
54✔
1183
        mapboxAccessToken: currentStyle?.accessToken || mapboxApiAccessToken,
58✔
1184
        // baseApiUrl: mapboxApiUrl,
1185
        mapLib: baseMapLibraryConfig.getMapLib(),
1186
        transformRequest:
1187
          this.props.transformRequest ||
58✔
1188
          transformRequest(currentStyle?.accessToken || mapboxApiAccessToken)
58✔
1189
      };
1190

1191
      const hasGeocoderLayer = Boolean(layers.find(l => l.id === GEOCODER_LAYER_ID));
29✔
1192
      const isSplit = Boolean(mapState.isSplit);
29✔
1193

1194
      const deck = this._renderDeckOverlay(layersForDeck, {
29✔
1195
        primaryMap: true,
1196
        isInteractive: true,
1197
        children: (
1198
          <MapComponent
1199
            key={`bottom-${baseMapLibraryName}`}
1200
            {...mapProps}
1201
            mapStyle={mapStyle.bottomMapStyle ?? EMPTY_MAPBOX_STYLE}
58✔
1202
            {...bottomMapContainerProps}
1203
            ref={this._setMapRef}
1204
          />
1205
        )
1206
      });
1207
      if (!deck) {
29!
1208
        // deckOverlay can be null if onDeckRender returns null
1209
        // in this case we don't want to render the map
1210
        return null;
×
1211
      }
1212
      return (
29✔
1213
        <>
1214
          <MapControl
1215
            mapState={mapState}
1216
            datasets={datasets}
1217
            availableLocales={LOCALE_CODES_ARRAY}
1218
            dragRotate={mapState.dragRotate}
1219
            isSplit={isSplit}
1220
            primary={Boolean(primary)}
1221
            isExport={isExport}
1222
            layers={layers}
1223
            layersToRender={layersToRender}
1224
            mapIndex={index || 0}
53✔
1225
            mapControls={mapControls}
1226
            readOnly={this.props.readOnly}
1227
            scale={mapState.scale || 1}
58✔
1228
            logoComponent={this.props.logoComponent}
1229
            top={
1230
              interactionConfig.geocoder && interactionConfig.geocoder.enabled
87!
1231
                ? theme.mapControlTop
1232
                : 0
1233
            }
1234
            editor={editor}
1235
            locale={locale}
1236
            onTogglePerspective={mapStateActions.togglePerspective}
1237
            onToggleSplitMap={mapStateActions.toggleSplitMap}
1238
            onMapToggleLayer={this._handleMapToggleLayer}
1239
            onToggleMapControl={this._toggleMapControl}
1240
            onToggleSplitMapViewport={mapStateActions.toggleSplitMapViewport}
1241
            onSetEditorMode={visStateActions.setEditorMode}
1242
            onSetLocale={uiStateActions.setLocale}
1243
            onToggleEditorVisibility={visStateActions.toggleEditorVisibility}
1244
            onLayerVisConfigChange={visStateActions.layerVisConfigChange}
1245
            onToggleLayerVisibility={this._handleToggleLayerVisibility}
1246
            mapHeight={mapState.height}
1247
            setMapControlSettings={uiStateActions.setMapControlSettings}
1248
            activeSidePanel={activeSidePanel}
1249
          />
1250
          {isSplitSelector(this.props) && <Droppable containerId={containerId} />}
38✔
1251

1252
          {deck}
1253
          {this._renderMapboxOverlays()}
1254
          <Editor
1255
            index={index || 0}
53✔
1256
            datasets={datasets}
1257
            editor={editor}
1258
            filters={this.polygonFiltersSelector(this.props)}
1259
            layers={layers}
1260
            onDeleteFeature={visStateActions.deleteFeature}
1261
            onSelect={visStateActions.setSelectedFeature}
1262
            onTogglePolygonFilter={visStateActions.setPolygonFilterLayer}
1263
            onSetEditorMode={visStateActions.setEditorMode}
1264
            style={{
1265
              pointerEvents: 'all',
1266
              position: 'absolute',
1267
              display: editor.visible ? 'block' : 'none'
29!
1268
            }}
1269
          />
1270
          {this.props.children}
1271
          {mapStyle.topMapStyle ? (
29!
1272
            <MapComponent
1273
              key={`top-${baseMapLibraryName}`}
1274
              viewState={internalViewState}
1275
              mapStyle={mapStyle.topMapStyle}
1276
              style={MAP_STYLE.top}
1277
              mapboxAccessToken={mapProps.mapboxAccessToken}
1278
              transformRequest={mapProps.transformRequest}
1279
              mapLib={baseMapLibraryConfig.getMapLib()}
1280
              {...topMapContainerProps}
1281
            />
1282
          ) : null}
1283

1284
          {hasGeocoderLayer
29!
1285
            ? this._renderDeckOverlay(
1286
                {[GEOCODER_LAYER_ID]: hasGeocoderLayer},
1287
                {primaryMap: false, isInteractive: false}
1288
              )
1289
            : null}
1290
          {this._renderMapPopover()}
1291
          {!isExport && primary !== isSplit ? (
83✔
1292
            <LoadingIndicator
1293
              isVisible={Boolean(isLoadingIndicatorVisible || this.anyActiveLayerLoading)}
34✔
1294
              activeSidePanel={Boolean(activeSidePanel)}
1295
              sidePanelWidth={sidePanelWidth}
1296
              hasAttributionLogos={attributionLogos.length > 0}
1297
            />
1298
          ) : null}
1299
          {this.props.primary ? (
29✔
1300
            <Attribution
1301
              showBaseMapLibLogo={this.state.showBaseMapAttribution}
1302
              showOsmBasemapAttribution={true}
1303
              datasetAttributions={datasetAttributions}
1304
              baseMapLibraryConfig={baseMapLibraryConfig}
1305
            />
1306
          ) : null}
1307
          {this.props.primary ? (
29✔
1308
            <AttributionLogos
1309
              logos={attributionLogos}
1310
              activeSidePanel={Boolean(activeSidePanel)}
1311
              sidePanelWidth={sidePanelWidth}
1312
            />
1313
          ) : null}
1314
        </>
1315
      );
1316
    }
1317

1318
    render() {
1319
      const {visState, mapStyle} = this.props;
29✔
1320
      const mapContent = this._renderMap();
29✔
1321
      if (!mapContent) {
29!
1322
        // mapContent can be null if onDeckRender returns null
1323
        // in this case we don't want to render the map
1324
        return null;
×
1325
      }
1326

1327
      const currentStyle = mapStyle.mapStyles?.[mapStyle.styleType];
29✔
1328
      const baseMapLibraryName = getBaseMapLibrary(currentStyle);
29✔
1329
      const baseMapLibraryConfig =
1330
        getApplicationConfig().baseMapLibraryConfig?.[baseMapLibraryName];
29✔
1331

1332
      return (
29✔
1333
        <StyledMap
1334
          ref={this._ref}
1335
          style={this.styleSelector(this.props)}
1336
          onContextMenu={event => event.preventDefault()}
×
1337
          $mixBlendMode={visState.overlayBlending}
1338
          $mapLibCssClass={baseMapLibraryConfig.mapLibCssClass}
1339
        >
1340
          {mapContent}
1341
        </StyledMap>
1342
      );
1343
    }
1344
  }
1345

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