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

keplergl / kepler.gl / 25276066791

03 May 2026 09:58AM UTC coverage: 59.032% (-0.1%) from 59.129%
25276066791

Pull #3411

github

web-flow
Merge 200699e9a into 6c9294a8e
Pull Request #3411: fix: fix for open streat map attribution

6928 of 14100 branches covered (49.13%)

Branch coverage included in aggregate %.

5 of 48 new or added lines in 2 files covered. (10.42%)

1 existing line in 1 file now uncovered.

14284 of 21833 relevant lines covered (65.42%)

79.8 hits per line

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

39.2
/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 as MapboxLegacyMap, MapRef} from 'react-map-gl/mapbox-legacy';
8
import {Map as MaplibreMap} from '@vis.gl/react-maplibre';
9
import {PickingInfo, MapView} from '@deck.gl/core';
10
import DeckGL from '@deck.gl/react';
11
import {createSelector, Selector} from 'reselect';
12
import {useDroppable} from '@dnd-kit/core';
13
import debounce from 'lodash/debounce';
14

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

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

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

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

71
// default-settings
72
import {
73
  FILTER_TYPES,
74
  GEOCODER_LAYER_ID,
75
  THROTTLE_NOTIFICATION_TIME,
76
  DEFAULT_PICKING_RADIUS,
77
  NO_MAP_ID,
78
  EMPTY_MAPBOX_STYLE,
79
  MAPBOX_MAX_PITCH,
80
  MAP_LIB_OPTIONS
81
} from '@kepler.gl/constants';
82

83
import {DROPPABLE_MAP_CONTAINER_TYPE} from './common/dnd-layer-items';
84
// Contexts
85
import {MapViewStateContext} from './map-view-state-context';
86

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

101
import LoadingIndicator from './loading-indicator';
102

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

109
// How long should we wait between layer loading state changes before triggering a UI update
110
const DEBOUNCE_LOADING_STATE_PROPAGATE = 100;
7✔
111

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

128
const LOCALE_CODES_ARRAY = Object.keys(LOCALE_CODES);
7✔
129

130
interface StyledMapContainerProps {
131
  $mixBlendMode?: string;
132
  $mapLibCssClass: string;
133
}
134

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

146
const MAPBOXGL_STYLE_UPDATE = 'style.load';
7✔
147
const MAPBOXGL_RENDER = 'render';
7✔
148
const nop = () => {
7✔
149
  return;
×
150
};
151

152
type MapLibLogoProps = {
153
  baseMapLibraryConfig: BaseMapLibraryConfig;
154
};
155

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

170
interface StyledDroppableProps {
171
  isOver: boolean;
172
}
173

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

183
export const isSplitSelector = props =>
7✔
184
  props.visState.splitMaps && props.visState.splitMaps.length > 1;
29✔
185

186
export const Droppable = ({containerId}) => {
7✔
187
  const {isOver, setNodeRef} = useDroppable({
9✔
188
    id: containerId,
189
    data: {type: DROPPABLE_MAP_CONTAINER_TYPE, index: containerId},
190
    disabled: !containerId
191
  });
192

193
  return <StyledDroppable ref={setNodeRef} isOver={isOver} />;
9✔
194
};
195

196
interface StyledDatasetAttributionsContainerProps {
197
  isPalm: boolean;
198
}
199

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

210
  &:hover {
211
    white-space: inherit;
212
  }
213
`;
214

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

241
type AttributionProps = {
242
  showBaseMapLibLogo: boolean;
243
  showOsmBasemapAttribution: boolean;
244
  datasetAttributions: DatasetAttribution[];
245
  baseMapLibraryConfig: BaseMapLibraryConfig;
246
};
247

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

256
  const memoizedComponents = useMemo(() => {
28✔
257
    if (!showBaseMapLibLogo) {
25!
258
      return (
×
259
        <StyledAttribution
260
          mapLibCssClass={baseMapLibraryConfig.mapLibCssClass}
261
          mapLibAttributionCssClass={baseMapLibraryConfig.mapLibAttributionCssClass}
262
        >
263
          <EndHorizontalFlexbox>
264
            <DatasetAttributions datasetAttributions={datasetAttributions} isPalm={isPalm} />
265
            <div className="attrition-link">
266
              {datasetAttributions?.length ? <span className="pipe-separator">|</span> : null}
×
267
              <a href="https://kepler.gl/policy/" target="_blank" rel="noopener noreferrer">
268
                © kepler.gl
269
              </a>
270
            </div>
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
            {showOsmBasemapAttribution ? (
25!
290
              <a
291
                href="http://www.openstreetmap.org/copyright"
292
                target="_blank"
293
                rel="noopener noreferrer"
294
              >
295
                © OpenStreetMap |{' '}
296
              </a>
297
            ) : null}
298
            {!isPalm ? <MapLibLogo baseMapLibraryConfig={baseMapLibraryConfig} /> : null}
25!
299
          </div>
300
        </EndHorizontalFlexbox>
301
      </StyledAttribution>
302
    );
303
  }, [
304
    showBaseMapLibLogo,
305
    showOsmBasemapAttribution,
306
    datasetAttributions,
307
    isPalm,
308
    baseMapLibraryConfig
309
  ]);
310

311
  return memoizedComponents;
28✔
312
};
313

314
const StyledAttributionLogoContainer = styled.div<{$left: number}>`
7✔
315
  position: absolute;
316
  bottom: ${props => props.theme.sidePanel.margin.left}px;
×
317
  left: ${props => props.$left}px;
×
318
  z-index: 1;
319
  display: flex;
320
  align-items: flex-end;
321
  gap: 4px;
322
  pointer-events: auto;
323
  transition: left 250ms ease-in-out;
324
`;
325

326
const StyledLogoLink = styled.a<{$enabled: boolean}>`
7✔
327
  cursor: ${props => (props.$enabled ? 'pointer' : 'default')};
×
328
  display: flex;
329
  align-items: flex-end;
330
`;
331

332
type AttributionLogosProps = {
333
  logos: AttributionWithStyle[];
334
  activeSidePanel?: boolean;
335
  sidePanelWidth?: number;
336
};
337

338
const LOGO_LEFT_ADJUSTMENT = 3;
7✔
339

340
export const AttributionLogos: React.FC<AttributionLogosProps> = ({
7✔
341
  logos,
342
  activeSidePanel,
343
  sidePanelWidth
344
}) => {
345
  const theme = useTheme() as any;
28✔
346
  const left =
347
    (activeSidePanel ? (sidePanelWidth || 0) + LOGO_LEFT_ADJUSTMENT : 0) +
28!
348
    theme.sidePanel.margin.left;
349

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

368
MapContainerFactory.deps = [MapPopoverFactory, MapControlFactory, EditorFactory];
7✔
369

370
type MapboxStyle = string | object | undefined;
371
type PropSelector<R> = Selector<MapContainerProps, R>;
372

373
export interface MapContainerProps {
374
  visState: VisState;
375
  mapState: MapState;
376
  mapControls: MapControls;
377
  mapStyle: {bottomMapStyle?: MapboxStyle; topMapStyle?: MapboxStyle} & MapStyle;
378
  mapboxApiAccessToken: string;
379
  mapboxApiUrl: string;
380
  visStateActions: typeof VisStateActions;
381
  mapStateActions: typeof MapStateActions;
382
  uiStateActions: typeof UIStateActions;
383

384
  // optional
385
  primary?: boolean; // primary one will be reporting its size to appState
386
  readOnly?: boolean;
387
  isExport?: boolean;
388
  // onMapStyleLoaded?: (map: maplibregl.Map | ReturnType<MapRef['getMap']> | null) => void;
389
  onMapStyleLoaded?: (map: GetMapRef | null) => void;
390
  onMapRender?: (map: GetMapRef | null) => void;
391
  getMapboxRef?: (mapbox?: MapRef | null, index?: number) => void;
392
  index?: number;
393
  deleteMapLabels?: (containerId: string, layerId: string) => void;
394
  containerId?: number;
395

396
  isLoadingIndicatorVisible?: boolean;
397
  activeSidePanel: string | null;
398
  sidePanelWidth?: number;
399

400
  locale?: any;
401
  theme?: any;
402
  editor?: any;
403
  MapComponent?: typeof MapboxLegacyMap | typeof MaplibreMap;
404
  deckGlProps?: any;
405
  onDeckInitialized?: (a: any, b: any) => void;
406
  onViewStateChange?: (viewport: Viewport) => void;
407

408
  topMapContainerProps: any;
409
  bottomMapContainerProps: any;
410
  transformRequest?: (url: string, resourceType?: string) => {url: string};
411

412
  /** Pass `false` to disable the remote RTL text plugin, or a URL string to self-host it. */
413
  RTLTextPlugin?: string | false;
414

415
  datasetAttributions?: DatasetAttribution[];
416
  attributionLogos?: AttributionWithStyle[];
417

418
  generateMapboxLayers?: typeof generateMapboxLayers;
419
  generateDeckGLLayers?: typeof computeDeckLayers;
420

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

423
  children?: React.ReactNode;
424
  deckRenderCallbacks?: {
425
    onDeckLoad?: () => void;
426
    onDeckRender?: (deckProps: Record<string, unknown>) => Record<string, unknown> | null;
427
    onDeckAfterRender?: (deckProps: Record<string, unknown>) => any;
428
  };
429

430
  // Optional: override legend header logo in map controls (used by image export)
431
  logoComponent?: React.FC | React.ReactNode;
432
}
433

434
export default function MapContainerFactory(
435
  MapPopover: ReturnType<typeof MapPopoverFactory>,
436
  MapControl: ReturnType<typeof MapControlFactory>,
437
  Editor: ReturnType<typeof EditorFactory>
438
): React.ComponentType<MapContainerProps> {
439
  class MapContainer extends Component<MapContainerProps> {
440
    displayName = 'MapContainer';
25✔
441

442
    private anyActiveLayerLoading = false;
25✔
443

444
    static contextType = MapViewStateContext;
14✔
445

446
    declare context: React.ContextType<typeof MapViewStateContext>;
447

448
    static defaultProps = {
14✔
449
      deckGlProps: {},
450
      index: 0,
451
      primary: true
452
    };
453

454
    constructor(props) {
455
      super(props);
25✔
456
      patchDeckRendererForPostProcessing();
25✔
457
    }
458

459
    state = {
25✔
460
      showBaseMapAttribution: true,
461
      showOsmAttribution: false
462
    };
463

464
    componentDidMount() {
465
      if (!this._ref.current) {
25!
466
        return;
×
467
      }
468
      observeDimensions(this._ref.current, this._handleResize);
25✔
469
    }
470

471
    componentWillUnmount() {
472
      // unbind mapboxgl event listener
473
      if (this._map) {
2!
474
        this._map?.off(MAPBOXGL_STYLE_UPDATE, nop);
×
475
        this._map?.off(MAPBOXGL_RENDER, nop);
×
NEW
476
        this._removeOsmSourceDataListener();
×
477
      }
478
      if (!this._ref.current) {
2!
479
        return;
×
480
      }
481
      unobserveDimensions(this._ref.current);
2✔
482
    }
483

484
    componentDidUpdate(prevProps) {
485
      if (prevProps.mapStyle.styleType !== this.props.mapStyle.styleType) {
4!
NEW
486
        this._removeOsmSourceDataListener();
×
NEW
487
        if (this.props.mapStyle.styleType === NO_MAP_ID) {
×
NEW
488
          this.setState({
×
489
            showBaseMapAttribution: false,
490
            showOsmAttribution: false
491
          });
492
        } else {
NEW
493
          this._updateAttribution();
×
494
        }
495
      }
496
    }
497

498
    _deck: any = null;
25✔
499
    _map: GetMapRef | null = null;
25✔
500
    _ref = createRef<HTMLDivElement>();
25✔
501
    _deckGLErrorsElapsed: {[id: string]: number} = {};
25✔
502
    _osmSourceDataListener: (() => void) | null = null;
25✔
503

504
    previousLayers = {
25✔
505
      // [layers.id]: mapboxLayerConfig
506
    };
507

508
    _handleResize = dimensions => {
25✔
509
      const {primary, index} = this.props;
×
510
      if (primary) {
×
511
        const {mapStateActions} = this.props;
×
512
        if (dimensions && dimensions.width > 0 && dimensions.height > 0) {
×
513
          mapStateActions.updateMap(dimensions, index);
×
514
        }
515
      }
516
    };
517

518
    layersSelector: PropSelector<VisState['layers']> = props => props.visState.layers;
56✔
519
    layerDataSelector: PropSelector<VisState['layers']> = props => props.visState.layerData;
56✔
520
    splitMapSelector: PropSelector<SplitMap[]> = props => props.visState.splitMaps;
28✔
521
    splitMapIndexSelector: PropSelector<number | undefined> = props => props.index;
28✔
522
    mapLayersSelector: PropSelector<SplitMapLayers | null | undefined> = createSelector(
25✔
523
      this.splitMapSelector,
524
      this.splitMapIndexSelector,
525
      getMapLayersFromSplitMaps
526
    );
527
    layerOrderSelector: PropSelector<VisState['layerOrder']> = props => props.visState.layerOrder;
25✔
528
    layersToRenderSelector: PropSelector<LayersToRender> = createSelector(
25✔
529
      this.layersSelector,
530
      this.layerDataSelector,
531
      this.mapLayersSelector,
532
      prepareLayersToRender
533
    );
534
    layersForDeckSelector = createSelector(
25✔
535
      this.layersSelector,
536
      this.layerDataSelector,
537
      prepareLayersForDeck
538
    );
539
    filtersSelector = props => props.visState.filters;
28✔
540
    polygonFiltersSelector = createSelector(this.filtersSelector, filters =>
25✔
541
      filters.filter(f => f.type === FILTER_TYPES.polygon && f.enabled !== false)
26!
542
    );
543
    featuresSelector = props => props.visState.editor.features;
28✔
544
    selectedFeatureSelector = props => props.visState.editor.selectedFeature;
28✔
545
    featureCollectionSelector = createSelector(
25✔
546
      this.polygonFiltersSelector,
547
      this.featuresSelector,
548
      (polygonFilters, features) => ({
26✔
549
        type: 'FeatureCollection',
550
        features: features.concat(polygonFilters.map(f => f.value))
×
551
      })
552
    );
553
    // @ts-ignore - No overload matches this call
554
    selectedPolygonIndexSelector = createSelector(
25✔
555
      this.featureCollectionSelector,
556
      this.selectedFeatureSelector,
557
      (collection, selectedFeature) =>
558
        collection.features.findIndex(f => f.id === selectedFeature?.id)
26✔
559
    );
560
    selectedFeatureIndexArraySelector = createSelector(
25✔
561
      (value: number) => value,
25✔
562
      value => {
563
        return value < 0 ? [] : [value];
25!
564
      }
565
    );
566

567
    generateMapboxLayerMethodSelector = props => props.generateMapboxLayers ?? generateMapboxLayers;
25!
568

569
    mapboxLayersSelector = createSelector(
25✔
570
      this.layersSelector,
571
      this.layerDataSelector,
572
      this.layerOrderSelector,
573
      this.layersToRenderSelector,
574
      this.generateMapboxLayerMethodSelector,
575
      (layer, layerData, layerOrder, layersToRender, generateMapboxLayerMethod) =>
576
        generateMapboxLayerMethod(layer, layerData, layerOrder, layersToRender)
×
577
    );
578

579
    // merge in a background-color style if the basemap choice is NO_MAP_ID
580
    // used by <StyledMap> inline style prop
581
    mapStyleTypeSelector = props => props.mapStyle.styleType;
28✔
582
    mapStyleBackgroundColorSelector = props => props.mapStyle.backgroundColor;
28✔
583
    styleSelector = createSelector(
25✔
584
      this.mapStyleTypeSelector,
585
      this.mapStyleBackgroundColorSelector,
586
      (styleType, backgroundColor) => ({
26✔
587
        ...MAP_STYLE.container,
588
        ...(styleType === NO_MAP_ID ? {backgroundColor: rgbToHex(backgroundColor)} : {})
26!
589
      })
590
    );
591

592
    /* component private functions */
593
    _onCloseMapPopover = () => {
25✔
594
      this.props.visStateActions.onLayerClick(null);
×
595
    };
596

597
    _onLayerHover = (_idx: number, info: PickingInfo<any> | null) => {
25✔
598
      this.props.visStateActions.onLayerHover(info, this.props.index);
×
599
    };
600

601
    _onLayerSetDomain = (
25✔
602
      idx: number,
603
      value: number[] | {domain: VisualChannelDomain; aggregatedBins: Record<number, AggregatedBin>}
604
    ) => {
605
      // deck.gl 9 native aggregation layers (Grid, Hexagon) pass
606
      // {domain, aggregatedBins} via our ScaleEnhanced* overrides,
607
      // while ClusterLayer's CPUAggregator also passes {domain, aggregatedBins}.
608
      // Plain [min, max] is a fallback if the override is bypassed.
609
      const config = Array.isArray(value)
×
610
        ? {colorDomain: value as VisualChannelDomain}
611
        : {colorDomain: value.domain, aggregatedBins: value.aggregatedBins};
612

613
      const layer = this.props.visState.layers[idx];
×
614
      if (!layer) return;
×
615

616
      this.props.visStateActions.layerConfigChange(layer, config as Partial<LayerBaseConfig>);
×
617
    };
618

619
    _onRedrawNeeded = (_idx: number) => {
25✔
620
      // updateMapUpdater always returns a new state object reference, which triggers re-render
621
      const {mapStateActions, index} = this.props;
×
622
      mapStateActions.updateMap({}, index);
×
623
    };
624

625
    _onFitBounds = (_idx: number, bounds: [number, number, number, number]) => {
25✔
626
      this.props.mapStateActions.fitBounds(bounds);
×
627
    };
628

629
    _onLayerFilteredItemsChange = (idx, event) => {
25✔
630
      this.props.visStateActions.layerFilteredItemsChange(this.props.visState.layers[idx], event);
×
631
    };
632

633
    _onWMSFeatureInfo = (
25✔
634
      idx: number,
635
      data: {
636
        featureInfo: Array<{name: string; value: string}> | string | null;
637
        coordinate?: [number, number] | null;
638
      }
639
    ) => {
640
      this.props.visStateActions.wmsFeatureInfo(
×
641
        this.props.visState.layers[idx],
642
        data.featureInfo,
643
        data.coordinate
644
      );
645
    };
646

647
    _handleMapToggleLayer = layerId => {
25✔
648
      const {index: mapIndex = 0, visStateActions} = this.props;
1!
649
      visStateActions.toggleLayerForMap(mapIndex, layerId);
1✔
650
    };
651

652
    _onMapboxStyleUpdate = update => {
25✔
653
      // force refresh mapboxgl layers
654
      this.previousLayers = {};
×
655
      this._updateMapboxLayers();
×
656

NEW
657
      this._updateAttribution(update);
×
658

NEW
659
      if (typeof this.props.onMapStyleLoaded === 'function') {
×
NEW
660
        this.props.onMapStyleLoaded(this._map);
×
661
      }
662
    };
663

664
    _updateAttribution = (update?: any) => {
25✔
NEW
665
      this._removeOsmSourceDataListener();
×
666

NEW
667
      let styleObj = update?.style || null;
×
NEW
668
      if (!styleObj && this._map) {
×
NEW
669
        try {
×
NEW
670
          const rawStyle = this._map.isStyleLoaded?.() ? this._map.getStyle?.() : null;
×
NEW
671
          if (rawStyle) {
×
NEW
672
            styleObj = {stylesheet: rawStyle};
×
673
          }
674
        } catch {
675
          // map style not ready yet
676
        }
677
      }
NEW
678
      const usesMapbox = styleObj ? isStyleUsingMapboxTiles(styleObj) : false;
×
NEW
679
      const usesOsm = styleObj ? isStyleUsingOpenStreetMapTiles(styleObj) : false;
×
680

NEW
681
      if (usesMapbox || usesOsm) {
×
NEW
682
        this.setState({
×
683
          showBaseMapAttribution: true,
684
          showOsmAttribution: usesOsm
685
        });
686
      } else {
UNCOV
687
        this.setState({
×
688
          showBaseMapAttribution: false,
689
          showOsmAttribution: false
690
        });
NEW
691
        this._checkOsmAttributionOnSourceLoad();
×
692
      }
693
    };
694

695
    _removeOsmSourceDataListener = () => {
25✔
NEW
696
      if (this._osmSourceDataListener && this._map) {
×
NEW
697
        this._map.off('sourcedata', this._osmSourceDataListener);
×
NEW
698
        this._osmSourceDataListener = null;
×
699
      }
700
    };
701

702
    _checkOsmAttributionOnSourceLoad = () => {
25✔
NEW
703
      if (!this._map) return;
×
NEW
704
      this._removeOsmSourceDataListener();
×
NEW
705
      const onSourceData = () => {
×
NEW
706
        if (mapHasOpenStreetMapAttribution(this._map)) {
×
NEW
707
          this._removeOsmSourceDataListener();
×
NEW
708
          this.setState({
×
709
            showBaseMapAttribution: true,
710
            showOsmAttribution: true
711
          });
712
        }
713
      };
NEW
714
      this._osmSourceDataListener = onSourceData;
×
NEW
715
      this._map.on('sourcedata', onSourceData);
×
716
    };
717

718
    _setMapRef = mapRef => {
25✔
719
      // Handle change of the map library
720
      if (this._map && mapRef) {
×
721
        const map = mapRef.getMap();
×
722
        if (map && this._map !== map) {
×
723
          this._map?.off(MAPBOXGL_STYLE_UPDATE, nop);
×
724
          this._map?.off(MAPBOXGL_RENDER, nop);
×
725
          this._map = null;
×
726
        }
727
      }
728

729
      if (!this._map && mapRef) {
×
730
        this._map = mapRef.getMap();
×
731
        // i noticed in certain context we don't access the actual map element
732
        if (!this._map) {
×
733
          return;
×
734
        }
735
        // bind mapboxgl event listener
736
        this._map.on(MAPBOXGL_STYLE_UPDATE, this._onMapboxStyleUpdate);
×
737

738
        this._map.on(MAPBOXGL_RENDER, () => {
×
739
          if (typeof this.props.onMapRender === 'function') {
×
740
            this.props.onMapRender(this._map);
×
741
          }
742
        });
743
      }
744

745
      if (this.props.getMapboxRef) {
×
746
        // The parent component can gain access to our MapboxGlMap by
747
        // providing this callback. Note that 'mapbox' will be null when the
748
        // ref is unset (e.g. when a split map is closed).
749
        this.props.getMapboxRef(mapRef, this.props.index);
×
750
      }
751
    };
752

753
    _onDeckInitialized(device) {
754
      if (this.props.onDeckInitialized) {
×
755
        this.props.onDeckInitialized(this._deck, device);
×
756
      }
757
    }
758

759
    /**
760
     * 1) Allow effects only for the first view.
761
     * 2) Prevent effect:preRender call without valid generated viewports.
762
     * @param viewIndex View index.
763
     * @returns Returns true if effects can be used.
764
     */
765
    _isOKToRenderEffects(viewIndex?: number): boolean {
766
      return !viewIndex && Boolean(this._deck?.viewManager?._viewports?.length);
29✔
767
    }
768

769
    _onBeforeRender = () => {
25✔
770
      // no-op
771
    };
772

773
    _onDeckError = (error, layer) => {
25✔
774
      const errorMessage = error?.message || 'unknown-error';
×
775
      const layerMessage = layer?.id ? ` in ${layer.id} layer` : '';
×
776
      const errorMessageFull =
777
        errorMessage === 'WebGL context is lost'
×
778
          ? '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.'
779
          : `An error in deck.gl: ${errorMessage}${layerMessage}.`;
780

781
      // Throttle error notifications, as React doesn't like too many state changes from here.
782
      const lastShown = this._deckGLErrorsElapsed[errorMessageFull];
×
783
      if (!lastShown || lastShown < Date.now() - THROTTLE_NOTIFICATION_TIME) {
×
784
        this._deckGLErrorsElapsed[errorMessageFull] = Date.now();
×
785

786
        // Mark layer as invalid
787
        let extraLayerMessage = '';
×
788
        const {visStateActions} = this.props;
×
789
        if (layer) {
×
790
          let topMostLayer = layer;
×
791
          while (topMostLayer.parent) {
×
792
            topMostLayer = topMostLayer.parent;
×
793
          }
794
          if (topMostLayer.props?.id) {
×
795
            visStateActions.layerSetIsValid(topMostLayer, false);
×
796
            extraLayerMessage = 'The layer has been disabled and highlighted.';
×
797
          }
798
        }
799

800
        // Create new error notification or update existing one with same id.
801
        // Update is required to preserve the order of notifications as they probably are going to "jump" based on order of errors.
802
        const {uiStateActions} = this.props;
×
803
        uiStateActions.addNotification(
×
804
          errorNotification({
805
            message: `${errorMessageFull} ${extraLayerMessage}`,
806
            id: errorMessageFull // treat the error message as id
807
          })
808
        );
809
      }
810
    };
811

812
    /* component render functions */
813

814
    /* eslint-disable complexity */
815
    _renderMapPopover() {
816
      // this check is for limiting the display of the `<MapPopover>` to the `<MapContainer>` the user is interacting with
817
      // the DeckGL onHover event handler adds a `mapIndex` property which is available in the `hoverInfo` object of `visState`
818
      if (this.props.index !== this.props.visState.hoverInfo?.mapIndex) {
29!
819
        return null;
29✔
820
      }
821

822
      // TODO: move this into reducer so it can be tested
823
      const {
824
        mapState,
825
        visState: {
826
          hoverInfo,
827
          clicked,
828
          datasets,
829
          interactionConfig,
830
          animationConfig,
831
          layers,
832
          mousePos: {mousePosition, coordinate, pinned}
833
        }
834
      } = this.props;
×
835
      const layersToRender = this.layersToRenderSelector(this.props);
×
836

837
      if (!mousePosition || !interactionConfig.tooltip) {
×
838
        return null;
×
839
      }
840

841
      const layerHoverProp = getLayerHoverProp({
×
842
        animationConfig,
843
        interactionConfig,
844
        hoverInfo,
845
        layers,
846
        layersToRender,
847
        datasets
848
      });
849

850
      const compareMode = interactionConfig.tooltip.config
×
851
        ? interactionConfig.tooltip.config.compareMode
852
        : false;
853

854
      let pinnedPosition = {x: 0, y: 0};
×
855
      let layerPinnedProp: LayerHoverProp | null = null;
×
856
      if (pinned || clicked) {
×
857
        // project lnglat to screen so that tooltip follows the object on zoom
858
        const viewport = getViewportFromMapState(mapState);
×
859
        const lngLat = clicked ? clicked.coordinate : pinned.coordinate;
×
860
        pinnedPosition = this._getHoverXY(viewport, lngLat);
×
861
        layerPinnedProp = getLayerHoverProp({
×
862
          animationConfig,
863
          interactionConfig,
864
          hoverInfo: clicked,
865
          layers,
866
          layersToRender,
867
          datasets
868
        });
869
        if (layerHoverProp && layerPinnedProp) {
×
870
          layerHoverProp.primaryData = layerPinnedProp.data;
×
871
          layerHoverProp.compareType = interactionConfig.tooltip.config.compareType;
×
872
        }
873
      }
874

875
      const commonProp = {
×
876
        onClose: this._onCloseMapPopover,
877
        zoom: mapState.zoom,
878
        container: this._deck ? this._deck.canvas : undefined
×
879
      };
880

881
      return (
×
882
        <ErrorBoundary>
883
          {layerPinnedProp && (
×
884
            <MapPopover
885
              {...pinnedPosition}
886
              {...commonProp}
887
              layerHoverProp={layerPinnedProp}
888
              coordinate={interactionConfig.coordinate.enabled && (pinned || {}).coordinate}
×
889
              frozen={true}
890
              isBase={compareMode}
891
              onSetFeatures={this.props.visStateActions.setFeatures}
892
              setSelectedFeature={this.props.visStateActions.setSelectedFeature}
893
              // @ts-ignore Argument of type 'Readonly<MapContainerProps>' is not assignable to parameter of type 'never'
894
              featureCollection={this.featureCollectionSelector(this.props)}
895
            />
896
          )}
897
          {layerHoverProp && (!layerPinnedProp || compareMode) && (
×
898
            <MapPopover
899
              x={mousePosition[0]}
900
              y={mousePosition[1]}
901
              {...commonProp}
902
              layerHoverProp={layerHoverProp}
903
              frozen={false}
904
              coordinate={interactionConfig.coordinate.enabled && coordinate}
×
905
              onSetFeatures={this.props.visStateActions.setFeatures}
906
              setSelectedFeature={this.props.visStateActions.setSelectedFeature}
907
              // @ts-ignore Argument of type 'Readonly<MapContainerProps>' is not assignable to parameter of type 'never'
908
              featureCollection={this.featureCollectionSelector(this.props)}
909
            />
910
          )}
911
        </ErrorBoundary>
912
      );
913
    }
914

915
    /* eslint-enable complexity */
916

917
    _getHoverXY(viewport, lngLat) {
918
      const screenCoord = !viewport || !lngLat ? null : viewport.project(lngLat);
×
919
      return screenCoord && {x: screenCoord[0], y: screenCoord[1]};
×
920
    }
921

922
    _renderDeckOverlay(
923
      layersForDeck,
924
      options: {primaryMap: boolean; isInteractive?: boolean; children?: React.ReactNode} = {
×
925
        primaryMap: false
926
      }
927
    ) {
928
      const {
929
        mapStyle,
930
        visState,
931
        mapState,
932
        visStateActions,
933
        mapboxApiAccessToken,
934
        mapboxApiUrl,
935
        deckGlProps,
936
        index,
937
        mapControls,
938
        deckRenderCallbacks,
939
        theme,
940
        generateDeckGLLayers,
941
        onMouseMove
942
      } = this.props;
29✔
943

944
      const {hoverInfo, editor} = visState;
29✔
945
      const {primaryMap, isInteractive, children} = options;
29✔
946

947
      // disable double click zoom when editor is in any draw mode
948
      const {mapDraw} = mapControls;
29✔
949
      const {active: editorMenuActive = false} = mapDraw || {};
29✔
950
      const isEditorDrawingMode = EditorLayerUtils.isDrawingActive(editorMenuActive, editor.mode);
29✔
951

952
      const internalViewState = this.context?.getInternalViewState(index);
29✔
953
      const internalMapState = {...mapState, ...internalViewState};
29✔
954
      const viewport = getViewportFromMapState(internalMapState);
29✔
955

956
      const editorFeatureSelectedIndex = this.selectedPolygonIndexSelector(this.props);
29✔
957

958
      const {setFeatures, onLayerClick, setSelectedFeature} = visStateActions;
29✔
959

960
      const generateDeckGLLayersMethod = generateDeckGLLayers ?? computeDeckLayers;
29✔
961
      const deckGlLayers = generateDeckGLLayersMethod(
29✔
962
        {
963
          visState,
964
          mapState: internalMapState,
965
          mapStyle
966
        },
967
        {
968
          mapIndex: index,
969
          primaryMap,
970
          mapboxApiAccessToken,
971
          mapboxApiUrl,
972
          layersForDeck,
973
          editorInfo: primaryMap
29!
974
            ? {
975
                editor,
976
                editorMenuActive,
977
                onSetFeatures: setFeatures,
978
                setSelectedFeature,
979
                onApplyPolygonFilterAll: visStateActions.setPolygonFilterAllLayers,
980
                // @ts-ignore Argument of type 'Readonly<MapContainerProps>' is not assignable to parameter of type 'never'
981
                featureCollection: this.featureCollectionSelector(this.props),
982
                selectedFeatureIndexes: this.selectedFeatureIndexArraySelector(
983
                  // @ts-ignore Argument of type 'unknown' is not assignable to parameter of type 'number'.
984
                  editorFeatureSelectedIndex
985
                ),
986
                viewport
987
              }
988
            : undefined
989
        },
990
        {
991
          onLayerHover: this._onLayerHover,
992
          onSetLayerDomain: this._onLayerSetDomain,
993
          onFilteredItemsChange: this._onLayerFilteredItemsChange,
994
          onWMSFeatureInfo: this._onWMSFeatureInfo,
995
          onRedrawNeeded: this._onRedrawNeeded,
996
          onFitBounds: this._onFitBounds
997
        },
998
        deckGlProps
999
      );
1000

1001
      const extraDeckParams: {
1002
        getTooltip?: (info: any) => object | null;
1003
        getCursor?: ({isDragging}: {isDragging: boolean}) => string;
1004
      } = {};
29✔
1005
      if (primaryMap) {
29!
1006
        // Omit hover updates when the pointer position is invalid, ie. over UI overlays or
1007
        // outside the map container. In those cases x/y may be < 0
1008
        extraDeckParams.getTooltip = info => {
29✔
1009
          const x = Number(info?.x);
×
1010
          const y = Number(info?.y);
×
1011
          if (Number.isNaN(x) || Number.isNaN(y) || x < 0 || y < 0) return null;
×
1012

1013
          return EditorLayerUtils.getTooltip(info, {
×
1014
            editorMenuActive,
1015
            editor,
1016
            theme
1017
          });
1018
        };
1019

1020
        extraDeckParams.getCursor = ({isDragging}: {isDragging: boolean}) => {
29✔
1021
          const editorCursor = EditorLayerUtils.getCursor({
×
1022
            editorMenuActive,
1023
            editor,
1024
            hoverInfo
1025
          });
1026
          if (editorCursor) return editorCursor;
×
1027

1028
          if (isDragging) return 'grabbing';
×
1029
          if (hoverInfo?.layer) return 'pointer';
×
1030
          return 'grab';
×
1031
        };
1032
      }
1033

1034
      const effects = this._isOKToRenderEffects(index)
29!
1035
        ? computeDeckEffects({visState, mapState, isExport: this.props.isExport})
1036
        : [];
1037

1038
      const views = deckGlProps?.views
29!
1039
        ? deckGlProps?.views()
1040
        : new MapView({farZMultiplier: 1.2});
29✔
1041

1042
      let allDeckGlProps = {
1043
        ...deckGlProps,
1044
        pickingRadius: DEFAULT_PICKING_RADIUS,
1045
        views,
1046
        layers: deckGlLayers,
1047
        effects,
1048
        parameters: getLayerBlendingParameters(visState.layerBlending)
1049
      };
29!
1050

×
1051
      if (typeof deckRenderCallbacks?.onDeckRender === 'function') {
×
1052
        allDeckGlProps = deckRenderCallbacks.onDeckRender(allDeckGlProps);
1053
        if (!allDeckGlProps) {
×
1054
          // if onDeckRender returns null, do not render deck.gl
1055
          return null;
1056
        }
1057
      }
29✔
1058

1059
      return (
29!
1060
        <div
1061
          {...(isInteractive
29!
1062
            ? {
1063
                onMouseMove: primaryMap
×
1064
                  ? event => {
×
1065
                      onMouseMove?.(event);
1066
                      this._onMouseMoveDebounced(event, viewport);
1067
                    }
1068
                  : undefined
1069
              }
1070
            : {style: {pointerEvents: 'none'}})}
1071
        >
1072
          <DeckGL
1073
            id="default-deckgl-overlay"
×
1074
            onLoad={() => {
×
1075
              if (typeof deckRenderCallbacks?.onDeckLoad === 'function') {
1076
                deckRenderCallbacks.onDeckLoad();
1077
              }
1078
            }}
1079
            {...allDeckGlProps}
29!
1080
            controller={
1081
              isInteractive
1082
                ? {
1083
                    doubleClickZoom: !isEditorDrawingMode,
58✔
1084
                    dragRotate: this.props.mapState.dragRotate,
1085
                    maxPitch: this.props.mapState.maxPitch ?? getApplicationConfig().maxPitch
1086
                  }
1087
                : false
1088
            }
1089
            initialViewState={internalViewState}
29!
1090
            onBeforeRender={this._onBeforeRender}
1091
            onViewStateChange={isInteractive ? this._onViewportChange : undefined}
1092
            {...extraDeckParams}
29!
1093
            onHover={
1094
              isInteractive
×
1095
                ? data => {
1096
                    const res = EditorLayerUtils.onHover(data, {
1097
                      editorMenuActive,
1098
                      editor,
1099
                      hoverInfo
×
1100
                    });
1101
                    if (res) return;
×
1102

1103
                    this._onLayerHoverDebounced(data, index);
1104
                  }
1105
                : null
1106
            }
1107
            onClick={(data, event) => {
×
1108
              // @ts-ignore
×
1109
              normalizeEvent(event.srcEvent, viewport);
1110
              const res = EditorLayerUtils.onClick(data, event, {
1111
                editorMenuActive,
1112
                editor,
1113
                onLayerClick,
1114
                setSelectedFeature,
1115
                mapIndex: index
×
1116
              });
1117
              if (res) return;
×
1118

1119
              visStateActions.onLayerClick(data);
1120
            }}
1121
            onError={this._onDeckError}
1122
            ref={comp => {
35✔
1123
              // @ts-ignore
1124
              if (comp && comp.deck && !this._deck) {
1✔
1125
                // @ts-ignore
1126
                this._deck = comp.deck;
1127
              }
×
1128
            }}
1129
            onDeviceInitialized={device => this._onDeckInitialized(device)}
×
1130
            onAfterRender={() => {
×
1131
              if (typeof deckRenderCallbacks?.onDeckAfterRender === 'function') {
1132
                deckRenderCallbacks.onDeckAfterRender(allDeckGlProps);
1133
              }
×
1134

×
1135
              const anyActiveLayerLoading = areAnyDeckLayersLoading(allDeckGlProps.layers);
×
1136
              if (anyActiveLayerLoading !== this.anyActiveLayerLoading) {
×
1137
                this._onLayerLoadingStateChange();
1138
                this.anyActiveLayerLoading = anyActiveLayerLoading;
1139
              }
1140
            }}
1141
          >
1142
            {children}
1143
          </DeckGL>
1144
        </div>
1145
      );
1146
    }
1147

×
1148
    _updateMapboxLayers() {
×
1149
      const mapboxLayers = this.mapboxLayersSelector(this.props);
×
1150
      if (!Object.keys(mapboxLayers).length && !Object.keys(this.previousLayers).length) {
1151
        return;
1152
      }
×
1153

1154
      updateMapboxLayers(this._map, mapboxLayers, this.previousLayers);
×
1155

1156
      this.previousLayers = mapboxLayers;
1157
    }
1158

29!
1159
    _renderMapboxOverlays() {
×
1160
      if (this._map && this._map.isStyleLoaded()) {
1161
        this._updateMapboxLayers();
1162
      }
25✔
1163
    }
×
1164
    _onViewportChangePropagateDebounced = debounce(() => {
×
1165
      const viewState = this.context?.getInternalViewState(this.props.index);
1166
      onViewPortChange(
1167
        viewState,
1168
        this.props.mapStateActions.updateMap,
1169
        this.props.onViewStateChange,
1170
        this.props.primary,
1171
        this.props.index
1172
      );
1173
    }, DEBOUNCE_VIEWPORT_PROPAGATE);
25✔
1174

×
1175
    _onViewportChange = viewport => {
×
1176
      const {viewState} = viewport;
1177
      if (this.props.isExport) {
1178
        // Image export map shouldn't be interactive (otherwise this callback can
×
1179
        // lead to inadvertent changes to the state of the main map)
1180
        return;
×
1181
      }
×
1182
      const {setInternalViewState} = this.context;
×
1183
      setInternalViewState(viewState, this.props.index);
1184
      this._onViewportChangePropagateDebounced();
1185
    };
25✔
1186

×
1187
    _onLayerHoverDebounced = debounce((data, index) => {
1188
      this.props.visStateActions.onLayerHover(data, index);
1189
    }, DEBOUNCE_MOUSE_MOVE_PROPAGATE);
25✔
1190

×
1191
    _onMouseMoveDebounced = debounce((event, viewport) => {
1192
      this.props.visStateActions.onMouseMove(normalizeEvent(event, viewport));
1193
    }, DEBOUNCE_MOUSE_MOVE_PROPAGATE);
25✔
1194

1195
    _onLayerLoadingStateChange = debounce(() => {
×
1196
      // trigger loading indicator update without any change to update UI
1197
      this.props.visStateActions.setLoadingIndicator({change: 0});
1198
    }, DEBOUNCE_LOADING_STATE_PROPAGATE);
25✔
1199

×
1200
    _handleToggleLayerVisibility = (layer: Layer) => {
×
1201
      const {visStateActions} = this.props;
1202
      visStateActions.layerConfigChange(layer, {isVisible: !layer.config.isVisible});
1203
    };
25✔
1204

2✔
1205
    _toggleMapControl = panelId => {
1206
      const {index, uiStateActions} = this.props;
2✔
1207

1208
      uiStateActions.toggleMapControl(panelId, Number(index));
1209
    };
1210

1211
    /* eslint-disable complexity */
1212
    _renderMap() {
1213
      const {
1214
        visState,
1215
        mapState,
1216
        mapStyle,
1217
        mapStateActions,
1218
        mapboxApiAccessToken,
1219
        // mapboxApiUrl,
1220
        mapControls,
1221
        isExport,
1222
        locale,
1223
        uiStateActions,
1224
        visStateActions,
1225
        index,
1226
        primary,
1227
        bottomMapContainerProps,
1228
        topMapContainerProps,
×
1229
        theme,
×
1230
        datasetAttributions = [],
13✔
1231
        attributionLogos = [],
1232
        containerId = 0,
1233
        isLoadingIndicatorVisible,
1234
        activeSidePanel,
29✔
1235
        sidePanelWidth
1236
      } = this.props;
29✔
1237

1238
      const {layers, datasets, editor, interactionConfig} = visState;
29✔
1239

29✔
1240
      const layersToRender = this.layersToRenderSelector(this.props);
1241
      const layersForDeck = this.layersForDeckSelector(this.props);
1242

29✔
1243
      // Current style can be a custom style, from which we pull the mapbox API acccess token
29✔
1244
      const currentStyle = mapStyle.mapStyles?.[mapStyle.styleType];
29✔
1245
      const baseMapLibraryName = getBaseMapLibrary(currentStyle);
1246
      const baseMapLibraryConfig = getApplicationConfig().baseMapLibraryConfig[baseMapLibraryName];
1247

1248
      // Select the correct Map adapter based on the active base map library.
1249
      // Using the native adapter for each library avoids Transform API
1250
      // incompatibilities (e.g. mapbox-legacy's cloneTransform with MapLibre v5).
29✔
1251
      const ResolvedMapComponent =
29!
1252
        this.props.MapComponent ??
1253
        (baseMapLibraryName === MAP_LIB_OPTIONS.MAPBOX ? MapboxLegacyMap : MaplibreMap);
29✔
1254

1255
      const useMapboxAdapter = ResolvedMapComponent === MapboxLegacyMap;
29✔
1256

29✔
1257
      const internalViewState = this.context?.getInternalViewState(index);
29!
1258
      const configMaxPitch = mapState.maxPitch ?? getApplicationConfig().maxPitch;
1259
      const effectiveMaxPitch = useMapboxAdapter
1260
        ? Math.min(configMaxPitch, MAPBOX_MAX_PITCH)
29✔
1261
        : configMaxPitch;
1262
      const mapProps: Record<string, any> = {
1263
        ...internalViewState,
54✔
1264
        maxPitch: effectiveMaxPitch,
58✔
1265
        preserveDrawingBuffer: this.props.isExport ?? false,
1266
        mapboxAccessToken: currentStyle?.accessToken || mapboxApiAccessToken,
1267
        // baseApiUrl: mapboxApiUrl,
58✔
1268
        transformRequest:
58✔
1269
          this.props.transformRequest ||
1270
          transformRequest(currentStyle?.accessToken || mapboxApiAccessToken)
1271
      };
29!
1272

×
1273
      if (this.props.RTLTextPlugin !== undefined) {
1274
        mapProps.RTLTextPlugin = this.props.RTLTextPlugin;
1275
      }
29!
1276

×
1277
      if (useMapboxAdapter) {
×
1278
        const mapboxConfig = getApplicationConfig().baseMapLibraryConfig[MAP_LIB_OPTIONS.MAPBOX];
1279
        mapProps.mapLib = mapboxConfig.getMapLib();
1280
      }
29✔
1281

29✔
1282
      const hasGeocoderLayer = Boolean(layers.find(l => l.id === GEOCODER_LAYER_ID));
1283
      const isSplit = Boolean(mapState.isSplit);
29✔
1284

1285
      const deck = this._renderDeckOverlay(layersForDeck, {
1286
        primaryMap: true,
1287
        isInteractive: true,
1288
        children: (
1289
          <ResolvedMapComponent
1290
            key={`bottom-${baseMapLibraryName}`}
58✔
1291
            {...mapProps}
1292
            mapStyle={mapStyle.bottomMapStyle ?? EMPTY_MAPBOX_STYLE}
1293
            {...bottomMapContainerProps}
1294
            ref={this._setMapRef}
1295
          />
1296
        )
29!
1297
      });
1298
      if (!deck) {
1299
        // deckOverlay can be null if onDeckRender returns null
×
1300
        // in this case we don't want to render the map
1301
        return null;
29✔
1302
      }
1303
      return (
1304
        <>
1305
          <MapControl
1306
            mapState={mapState}
1307
            datasets={datasets}
1308
            availableLocales={LOCALE_CODES_ARRAY}
1309
            dragRotate={mapState.dragRotate}
1310
            isSplit={isSplit}
1311
            primary={Boolean(primary)}
1312
            isExport={isExport}
1313
            layers={layers}
53✔
1314
            layersToRender={layersToRender}
1315
            mapIndex={index || 0}
1316
            mapControls={mapControls}
58✔
1317
            readOnly={this.props.readOnly}
1318
            scale={mapState.scale || 1}
1319
            logoComponent={this.props.logoComponent}
87!
1320
            top={
1321
              interactionConfig.geocoder && interactionConfig.geocoder.enabled
1322
                ? theme.mapControlTop
1323
                : 0
1324
            }
1325
            editor={editor}
1326
            locale={locale}
1327
            onTogglePerspective={mapStateActions.togglePerspective}
1328
            onToggleSplitMap={mapStateActions.toggleSplitMap}
1329
            onMapToggleLayer={this._handleMapToggleLayer}
1330
            onToggleMapControl={this._toggleMapControl}
1331
            onToggleSplitMapViewport={mapStateActions.toggleSplitMapViewport}
1332
            onSetEditorMode={visStateActions.setEditorMode}
1333
            onSetLocale={uiStateActions.setLocale}
1334
            onToggleEditorVisibility={visStateActions.toggleEditorVisibility}
1335
            onLayerVisConfigChange={visStateActions.layerVisConfigChange}
1336
            onToggleLayerVisibility={this._handleToggleLayerVisibility}
1337
            mapHeight={mapState.height}
1338
            setMapControlSettings={uiStateActions.setMapControlSettings}
1339
            activeSidePanel={activeSidePanel}
38✔
1340
          />
1341
          {isSplitSelector(this.props) && <Droppable containerId={containerId} />}
1342

1343
          {deck}
1344
          {this._renderMapboxOverlays()}
53✔
1345
          <Editor
1346
            index={index || 0}
1347
            datasets={datasets}
1348
            editor={editor}
1349
            filters={this.polygonFiltersSelector(this.props)}
1350
            layers={layers}
1351
            onDeleteFeature={visStateActions.deleteFeature}
1352
            onSelect={visStateActions.setSelectedFeature}
1353
            onTogglePolygonFilter={visStateActions.setPolygonFilterLayer}
1354
            onSetEditorMode={visStateActions.setEditorMode}
1355
            style={{
1356
              pointerEvents: 'all',
29!
1357
              position: 'absolute',
1358
              display: editor.visible ? 'block' : 'none'
1359
            }}
1360
          />
29!
1361
          {this.props.children}
1362
          {mapStyle.topMapStyle ? (
1363
            <ResolvedMapComponent
1364
              key={`top-${baseMapLibraryName}`}
1365
              viewState={internalViewState}
1366
              maxPitch={effectiveMaxPitch}
1367
              mapStyle={mapStyle.topMapStyle}
1368
              style={MAP_STYLE.top}
1369
              mapboxAccessToken={mapProps.mapboxAccessToken}
×
1370
              transformRequest={mapProps.transformRequest}
1371
              {...(mapProps.RTLTextPlugin !== undefined
1372
                ? {RTLTextPlugin: mapProps.RTLTextPlugin}
×
1373
                : {})}
1374
              {...(useMapboxAdapter
1375
                ? {
1376
                    mapLib:
1377
                      getApplicationConfig().baseMapLibraryConfig[
1378
                        MAP_LIB_OPTIONS.MAPBOX
1379
                      ].getMapLib()
1380
                  }
1381
                : {})}
1382
              {...topMapContainerProps}
1383
            />
1384
          ) : null}
29!
1385

1386
          {hasGeocoderLayer
1387
            ? this._renderDeckOverlay(
1388
                {[GEOCODER_LAYER_ID]: hasGeocoderLayer},
1389
                {primaryMap: false, isInteractive: false}
1390
              )
1391
            : null}
83✔
1392
          {this._renderMapPopover()}
1393
          {!isExport && primary !== isSplit ? (
34✔
1394
            <LoadingIndicator
1395
              isVisible={Boolean(isLoadingIndicatorVisible || this.anyActiveLayerLoading)}
1396
              activeSidePanel={Boolean(activeSidePanel)}
1397
              sidePanelWidth={sidePanelWidth}
1398
              hasAttributionLogos={attributionLogos.length > 0}
1399
            />
29✔
1400
          ) : null}
1401
          {this.props.primary ? (
1402
            <Attribution
1403
              showBaseMapLibLogo={this.state.showBaseMapAttribution}
1404
              showOsmBasemapAttribution={this.state.showOsmAttribution}
1405
              datasetAttributions={datasetAttributions}
1406
              baseMapLibraryConfig={baseMapLibraryConfig}
1407
            />
29✔
1408
          ) : null}
1409
          {this.props.primary ? (
1410
            <AttributionLogos
1411
              logos={attributionLogos}
1412
              activeSidePanel={Boolean(activeSidePanel)}
1413
              sidePanelWidth={sidePanelWidth}
1414
            />
1415
          ) : null}
1416
        </>
1417
      );
1418
    }
1419

29✔
1420
    render() {
29✔
1421
      const {visState, mapStyle} = this.props;
29!
1422
      const mapContent = this._renderMap();
1423
      if (!mapContent) {
1424
        // mapContent can be null if onDeckRender returns null
×
1425
        // in this case we don't want to render the map
1426
        return null;
1427
      }
29✔
1428

29✔
1429
      const currentStyle = mapStyle.mapStyles?.[mapStyle.styleType];
29✔
1430
      const baseMapLibraryName = getBaseMapLibrary(currentStyle);
1431
      const baseMapLibraryConfig = getApplicationConfig().baseMapLibraryConfig[baseMapLibraryName];
29✔
1432

1433
      return (
1434
        <StyledMap
1435
          ref={this._ref}
×
1436
          style={this.styleSelector(this.props)}
1437
          onContextMenu={event => event.preventDefault()}
1438
          $mixBlendMode={visState.overlayBlending}
1439
          $mapLibCssClass={baseMapLibraryConfig.mapLibCssClass}
1440
        >
1441
          {mapContent}
1442
        </StyledMap>
1443
      );
1444
    }
1445
  }
14✔
1446

1447
  return withTheme(MapContainer);
1448
}
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