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

keplergl / kepler.gl / 25276649144

03 May 2026 10:26AM UTC coverage: 59.016% (-0.1%) from 59.129%
25276649144

Pull #3411

github

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

6928 of 14104 branches covered (49.12%)

Branch coverage included in aggregate %.

6 of 63 new or added lines in 2 files covered. (9.52%)

2 existing lines in 1 file now uncovered.

14284 of 21839 relevant lines covered (65.41%)

79.77 hits per line

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

38.6
/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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

312
  return memoizedComponents;
28✔
313
};
314

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

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

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

339
const LOGO_LEFT_ADJUSTMENT = 3;
7✔
340

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

443
    private anyActiveLayerLoading = false;
25✔
444

445
    static contextType = MapViewStateContext;
14✔
446

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

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

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

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

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

472
    componentWillUnmount() {
473
      if (this._map) {
2!
NEW
474
        this._map.off(MAPBOXGL_STYLE_UPDATE, this._onMapboxStyleUpdate);
×
NEW
475
        this._map.off(MAPBOXGL_RENDER, this._onMapRender);
×
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: ((e: any) => void) | null = null;
25✔
503

504
    _onMapRender = () => {
25✔
NEW
505
      if (typeof this.props.onMapRender === 'function') {
×
NEW
506
        this.props.onMapRender(this._map);
×
507
      }
508
    };
509

510
    previousLayers = {
25✔
511
      // [layers.id]: mapboxLayerConfig
512
    };
513

514
    _handleResize = dimensions => {
25✔
515
      const {primary, index} = this.props;
×
516
      if (primary) {
×
517
        const {mapStateActions} = this.props;
×
518
        if (dimensions && dimensions.width > 0 && dimensions.height > 0) {
×
519
          mapStateActions.updateMap(dimensions, index);
×
520
        }
521
      }
522
    };
523

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

573
    generateMapboxLayerMethodSelector = props => props.generateMapboxLayers ?? generateMapboxLayers;
25!
574

575
    mapboxLayersSelector = createSelector(
25✔
576
      this.layersSelector,
577
      this.layerDataSelector,
578
      this.layerOrderSelector,
579
      this.layersToRenderSelector,
580
      this.generateMapboxLayerMethodSelector,
581
      (layer, layerData, layerOrder, layersToRender, generateMapboxLayerMethod) =>
582
        generateMapboxLayerMethod(layer, layerData, layerOrder, layersToRender)
×
583
    );
584

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

598
    /* component private functions */
599
    _onCloseMapPopover = () => {
25✔
600
      this.props.visStateActions.onLayerClick(null);
×
601
    };
602

603
    _onLayerHover = (_idx: number, info: PickingInfo<any> | null) => {
25✔
604
      this.props.visStateActions.onLayerHover(info, this.props.index);
×
605
    };
606

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

619
      const layer = this.props.visState.layers[idx];
×
620
      if (!layer) return;
×
621

622
      this.props.visStateActions.layerConfigChange(layer, config as Partial<LayerBaseConfig>);
×
623
    };
624

625
    _onRedrawNeeded = (_idx: number) => {
25✔
626
      // updateMapUpdater always returns a new state object reference, which triggers re-render
627
      const {mapStateActions, index} = this.props;
×
628
      mapStateActions.updateMap({}, index);
×
629
    };
630

631
    _onFitBounds = (_idx: number, bounds: [number, number, number, number]) => {
25✔
632
      this.props.mapStateActions.fitBounds(bounds);
×
633
    };
634

635
    _onLayerFilteredItemsChange = (idx, event) => {
25✔
636
      this.props.visStateActions.layerFilteredItemsChange(this.props.visState.layers[idx], event);
×
637
    };
638

639
    _onWMSFeatureInfo = (
25✔
640
      idx: number,
641
      data: {
642
        featureInfo: Array<{name: string; value: string}> | string | null;
643
        coordinate?: [number, number] | null;
644
      }
645
    ) => {
646
      this.props.visStateActions.wmsFeatureInfo(
×
647
        this.props.visState.layers[idx],
648
        data.featureInfo,
649
        data.coordinate
650
      );
651
    };
652

653
    _handleMapToggleLayer = layerId => {
25✔
654
      const {index: mapIndex = 0, visStateActions} = this.props;
1!
655
      visStateActions.toggleLayerForMap(mapIndex, layerId);
1✔
656
    };
657

658
    _onMapboxStyleUpdate = update => {
25✔
659
      // force refresh mapboxgl layers
660
      this.previousLayers = {};
×
661
      this._updateMapboxLayers();
×
662

NEW
663
      this._updateAttribution(update);
×
664

NEW
665
      if (typeof this.props.onMapStyleLoaded === 'function') {
×
NEW
666
        this.props.onMapStyleLoaded(this._map);
×
667
      }
668
    };
669

670
    _updateAttribution = (update?: any) => {
25✔
NEW
671
      this._removeOsmSourceDataListener();
×
672

NEW
673
      let styleObj = update?.style || null;
×
NEW
674
      if (!styleObj && this._map) {
×
NEW
675
        try {
×
NEW
676
          const rawStyle = this._map.isStyleLoaded?.() ? this._map.getStyle?.() : null;
×
NEW
677
          if (rawStyle) {
×
NEW
678
            styleObj = {stylesheet: rawStyle};
×
679
          }
680
        } catch {
681
          // map style not ready yet
682
        }
683
      }
NEW
684
      const usesMapbox = styleObj ? isStyleUsingMapboxTiles(styleObj) : false;
×
NEW
685
      const usesOsm = styleObj ? isStyleUsingOpenStreetMapTiles(styleObj) : false;
×
686

NEW
687
      if (usesMapbox || usesOsm) {
×
NEW
688
        this.setState({
×
689
          showBaseMapAttribution: true,
690
          showOsmAttribution: usesOsm
691
        });
692
      } else {
UNCOV
693
        this.setState({
×
694
          showBaseMapAttribution: false,
695
          showOsmAttribution: false
696
        });
NEW
697
        this._checkOsmAttributionOnSourceLoad();
×
698
      }
699
    };
700

701
    _removeOsmSourceDataListener = () => {
25✔
NEW
702
      if (this._osmSourceDataListener && this._map) {
×
NEW
703
        this._map.off('sourcedata', this._osmSourceDataListener);
×
NEW
704
        this._osmSourceDataListener = null;
×
705
      }
706
    };
707

708
    _checkOsmAttributionOnSourceLoad = () => {
25✔
NEW
709
      if (!this._map) return;
×
NEW
710
      this._removeOsmSourceDataListener();
×
NEW
711
      let attempts = 0;
×
NEW
712
      const MAX_ATTEMPTS = 50;
×
NEW
713
      const onSourceData = (e: any) => {
×
NEW
714
        if (!e?.isSourceLoaded) return;
×
NEW
715
        attempts++;
×
NEW
716
        if (mapHasOpenStreetMapAttribution(this._map)) {
×
NEW
717
          this._removeOsmSourceDataListener();
×
NEW
718
          this.setState({
×
719
            showBaseMapAttribution: true,
720
            showOsmAttribution: true
721
          });
NEW
722
        } else if (attempts >= MAX_ATTEMPTS) {
×
NEW
723
          this._removeOsmSourceDataListener();
×
724
        }
725
      };
NEW
726
      this._osmSourceDataListener = onSourceData;
×
NEW
727
      this._map.on('sourcedata', onSourceData);
×
728
    };
729

730
    _setMapRef = mapRef => {
25✔
731
      // Handle change of the map library
732
      if (this._map && mapRef) {
×
733
        const map = mapRef.getMap();
×
734
        if (map && this._map !== map) {
×
NEW
735
          this._map.off(MAPBOXGL_STYLE_UPDATE, this._onMapboxStyleUpdate);
×
NEW
736
          this._map.off(MAPBOXGL_RENDER, this._onMapRender);
×
NEW
737
          this._removeOsmSourceDataListener();
×
UNCOV
738
          this._map = null;
×
739
        }
740
      }
741

742
      if (!this._map && mapRef) {
×
743
        this._map = mapRef.getMap();
×
744
        // i noticed in certain context we don't access the actual map element
745
        if (!this._map) {
×
746
          return;
×
747
        }
748
        // bind mapboxgl event listener
749
        this._map.on(MAPBOXGL_STYLE_UPDATE, this._onMapboxStyleUpdate);
×
NEW
750
        this._map.on(MAPBOXGL_RENDER, this._onMapRender);
×
751
      }
752

753
      if (this.props.getMapboxRef) {
×
754
        // The parent component can gain access to our MapboxGlMap by
755
        // providing this callback. Note that 'mapbox' will be null when the
756
        // ref is unset (e.g. when a split map is closed).
757
        this.props.getMapboxRef(mapRef, this.props.index);
×
758
      }
759
    };
760

761
    _onDeckInitialized(device) {
762
      if (this.props.onDeckInitialized) {
×
763
        this.props.onDeckInitialized(this._deck, device);
×
764
      }
765
    }
766

767
    /**
768
     * 1) Allow effects only for the first view.
769
     * 2) Prevent effect:preRender call without valid generated viewports.
770
     * @param viewIndex View index.
771
     * @returns Returns true if effects can be used.
772
     */
773
    _isOKToRenderEffects(viewIndex?: number): boolean {
774
      return !viewIndex && Boolean(this._deck?.viewManager?._viewports?.length);
29✔
775
    }
776

777
    _onBeforeRender = () => {
25✔
778
      // no-op
779
    };
780

781
    _onDeckError = (error, layer) => {
25✔
782
      const errorMessage = error?.message || 'unknown-error';
×
783
      const layerMessage = layer?.id ? ` in ${layer.id} layer` : '';
×
784
      const errorMessageFull =
785
        errorMessage === 'WebGL context is lost'
×
786
          ? '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.'
787
          : `An error in deck.gl: ${errorMessage}${layerMessage}.`;
788

789
      // Throttle error notifications, as React doesn't like too many state changes from here.
790
      const lastShown = this._deckGLErrorsElapsed[errorMessageFull];
×
791
      if (!lastShown || lastShown < Date.now() - THROTTLE_NOTIFICATION_TIME) {
×
792
        this._deckGLErrorsElapsed[errorMessageFull] = Date.now();
×
793

794
        // Mark layer as invalid
795
        let extraLayerMessage = '';
×
796
        const {visStateActions} = this.props;
×
797
        if (layer) {
×
798
          let topMostLayer = layer;
×
799
          while (topMostLayer.parent) {
×
800
            topMostLayer = topMostLayer.parent;
×
801
          }
802
          if (topMostLayer.props?.id) {
×
803
            visStateActions.layerSetIsValid(topMostLayer, false);
×
804
            extraLayerMessage = 'The layer has been disabled and highlighted.';
×
805
          }
806
        }
807

808
        // Create new error notification or update existing one with same id.
809
        // Update is required to preserve the order of notifications as they probably are going to "jump" based on order of errors.
810
        const {uiStateActions} = this.props;
×
811
        uiStateActions.addNotification(
×
812
          errorNotification({
813
            message: `${errorMessageFull} ${extraLayerMessage}`,
814
            id: errorMessageFull // treat the error message as id
815
          })
816
        );
817
      }
818
    };
819

820
    /* component render functions */
821

822
    /* eslint-disable complexity */
823
    _renderMapPopover() {
824
      // this check is for limiting the display of the `<MapPopover>` to the `<MapContainer>` the user is interacting with
825
      // the DeckGL onHover event handler adds a `mapIndex` property which is available in the `hoverInfo` object of `visState`
826
      if (this.props.index !== this.props.visState.hoverInfo?.mapIndex) {
29!
827
        return null;
29✔
828
      }
829

830
      // TODO: move this into reducer so it can be tested
831
      const {
832
        mapState,
833
        visState: {
834
          hoverInfo,
835
          clicked,
836
          datasets,
837
          interactionConfig,
838
          animationConfig,
839
          layers,
840
          mousePos: {mousePosition, coordinate, pinned}
841
        }
842
      } = this.props;
×
843
      const layersToRender = this.layersToRenderSelector(this.props);
×
844

845
      if (!mousePosition || !interactionConfig.tooltip) {
×
846
        return null;
×
847
      }
848

849
      const layerHoverProp = getLayerHoverProp({
×
850
        animationConfig,
851
        interactionConfig,
852
        hoverInfo,
853
        layers,
854
        layersToRender,
855
        datasets
856
      });
857

858
      const compareMode = interactionConfig.tooltip.config
×
859
        ? interactionConfig.tooltip.config.compareMode
860
        : false;
861

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

883
      const commonProp = {
×
884
        onClose: this._onCloseMapPopover,
885
        zoom: mapState.zoom,
886
        container: this._deck ? this._deck.canvas : undefined
×
887
      };
888

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

923
    /* eslint-enable complexity */
924

925
    _getHoverXY(viewport, lngLat) {
926
      const screenCoord = !viewport || !lngLat ? null : viewport.project(lngLat);
×
927
      return screenCoord && {x: screenCoord[0], y: screenCoord[1]};
×
928
    }
929

930
    _renderDeckOverlay(
931
      layersForDeck,
932
      options: {primaryMap: boolean; isInteractive?: boolean; children?: React.ReactNode} = {
×
933
        primaryMap: false
934
      }
935
    ) {
936
      const {
937
        mapStyle,
938
        visState,
939
        mapState,
940
        visStateActions,
941
        mapboxApiAccessToken,
942
        mapboxApiUrl,
943
        deckGlProps,
944
        index,
945
        mapControls,
946
        deckRenderCallbacks,
947
        theme,
948
        generateDeckGLLayers,
949
        onMouseMove
950
      } = this.props;
29✔
951

952
      const {hoverInfo, editor} = visState;
29✔
953
      const {primaryMap, isInteractive, children} = options;
29✔
954

955
      // disable double click zoom when editor is in any draw mode
956
      const {mapDraw} = mapControls;
29✔
957
      const {active: editorMenuActive = false} = mapDraw || {};
29✔
958
      const isEditorDrawingMode = EditorLayerUtils.isDrawingActive(editorMenuActive, editor.mode);
29✔
959

960
      const internalViewState = this.context?.getInternalViewState(index);
29✔
961
      const internalMapState = {...mapState, ...internalViewState};
29✔
962
      const viewport = getViewportFromMapState(internalMapState);
29✔
963

964
      const editorFeatureSelectedIndex = this.selectedPolygonIndexSelector(this.props);
29✔
965

966
      const {setFeatures, onLayerClick, setSelectedFeature} = visStateActions;
29✔
967

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

1009
      const extraDeckParams: {
1010
        getTooltip?: (info: any) => object | null;
1011
        getCursor?: ({isDragging}: {isDragging: boolean}) => string;
1012
      } = {};
29✔
1013
      if (primaryMap) {
29!
1014
        // Omit hover updates when the pointer position is invalid, ie. over UI overlays or
1015
        // outside the map container. In those cases x/y may be < 0
1016
        extraDeckParams.getTooltip = info => {
29✔
1017
          const x = Number(info?.x);
×
1018
          const y = Number(info?.y);
×
1019
          if (Number.isNaN(x) || Number.isNaN(y) || x < 0 || y < 0) return null;
×
1020

1021
          return EditorLayerUtils.getTooltip(info, {
×
1022
            editorMenuActive,
1023
            editor,
1024
            theme
1025
          });
1026
        };
1027

1028
        extraDeckParams.getCursor = ({isDragging}: {isDragging: boolean}) => {
29✔
1029
          const editorCursor = EditorLayerUtils.getCursor({
×
1030
            editorMenuActive,
1031
            editor,
1032
            hoverInfo
1033
          });
1034
          if (editorCursor) return editorCursor;
×
1035

1036
          if (isDragging) return 'grabbing';
×
1037
          if (hoverInfo?.layer) return 'pointer';
×
1038
          return 'grab';
×
1039
        };
1040
      }
1041

1042
      const effects = this._isOKToRenderEffects(index)
29!
1043
        ? computeDeckEffects({visState, mapState, isExport: this.props.isExport})
1044
        : [];
1045

1046
      const views = deckGlProps?.views
29!
1047
        ? deckGlProps?.views()
1048
        : new MapView({farZMultiplier: 1.2});
29✔
1049

1050
      let allDeckGlProps = {
1051
        ...deckGlProps,
1052
        pickingRadius: DEFAULT_PICKING_RADIUS,
1053
        views,
1054
        layers: deckGlLayers,
1055
        effects,
1056
        parameters: getLayerBlendingParameters(visState.layerBlending)
1057
      };
29!
1058

×
1059
      if (typeof deckRenderCallbacks?.onDeckRender === 'function') {
×
1060
        allDeckGlProps = deckRenderCallbacks.onDeckRender(allDeckGlProps);
1061
        if (!allDeckGlProps) {
×
1062
          // if onDeckRender returns null, do not render deck.gl
1063
          return null;
1064
        }
1065
      }
29✔
1066

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

1111
                    this._onLayerHoverDebounced(data, index);
1112
                  }
1113
                : null
1114
            }
1115
            onClick={(data, event) => {
×
1116
              // @ts-ignore
×
1117
              normalizeEvent(event.srcEvent, viewport);
1118
              const res = EditorLayerUtils.onClick(data, event, {
1119
                editorMenuActive,
1120
                editor,
1121
                onLayerClick,
1122
                setSelectedFeature,
1123
                mapIndex: index
×
1124
              });
1125
              if (res) return;
×
1126

1127
              visStateActions.onLayerClick(data);
1128
            }}
1129
            onError={this._onDeckError}
1130
            ref={comp => {
35✔
1131
              // @ts-ignore
1132
              if (comp && comp.deck && !this._deck) {
1✔
1133
                // @ts-ignore
1134
                this._deck = comp.deck;
1135
              }
×
1136
            }}
1137
            onDeviceInitialized={device => this._onDeckInitialized(device)}
×
1138
            onAfterRender={() => {
×
1139
              if (typeof deckRenderCallbacks?.onDeckAfterRender === 'function') {
1140
                deckRenderCallbacks.onDeckAfterRender(allDeckGlProps);
1141
              }
×
1142

×
1143
              const anyActiveLayerLoading = areAnyDeckLayersLoading(allDeckGlProps.layers);
×
1144
              if (anyActiveLayerLoading !== this.anyActiveLayerLoading) {
×
1145
                this._onLayerLoadingStateChange();
1146
                this.anyActiveLayerLoading = anyActiveLayerLoading;
1147
              }
1148
            }}
1149
          >
1150
            {children}
1151
          </DeckGL>
1152
        </div>
1153
      );
1154
    }
1155

×
1156
    _updateMapboxLayers() {
×
1157
      const mapboxLayers = this.mapboxLayersSelector(this.props);
×
1158
      if (!Object.keys(mapboxLayers).length && !Object.keys(this.previousLayers).length) {
1159
        return;
1160
      }
×
1161

1162
      updateMapboxLayers(this._map, mapboxLayers, this.previousLayers);
×
1163

1164
      this.previousLayers = mapboxLayers;
1165
    }
1166

29!
1167
    _renderMapboxOverlays() {
×
1168
      if (this._map && this._map.isStyleLoaded()) {
1169
        this._updateMapboxLayers();
1170
      }
25✔
1171
    }
×
1172
    _onViewportChangePropagateDebounced = debounce(() => {
×
1173
      const viewState = this.context?.getInternalViewState(this.props.index);
1174
      onViewPortChange(
1175
        viewState,
1176
        this.props.mapStateActions.updateMap,
1177
        this.props.onViewStateChange,
1178
        this.props.primary,
1179
        this.props.index
1180
      );
1181
    }, DEBOUNCE_VIEWPORT_PROPAGATE);
25✔
1182

×
1183
    _onViewportChange = viewport => {
×
1184
      const {viewState} = viewport;
1185
      if (this.props.isExport) {
1186
        // Image export map shouldn't be interactive (otherwise this callback can
×
1187
        // lead to inadvertent changes to the state of the main map)
1188
        return;
×
1189
      }
×
1190
      const {setInternalViewState} = this.context;
×
1191
      setInternalViewState(viewState, this.props.index);
1192
      this._onViewportChangePropagateDebounced();
1193
    };
25✔
1194

×
1195
    _onLayerHoverDebounced = debounce((data, index) => {
1196
      this.props.visStateActions.onLayerHover(data, index);
1197
    }, DEBOUNCE_MOUSE_MOVE_PROPAGATE);
25✔
1198

×
1199
    _onMouseMoveDebounced = debounce((event, viewport) => {
1200
      this.props.visStateActions.onMouseMove(normalizeEvent(event, viewport));
1201
    }, DEBOUNCE_MOUSE_MOVE_PROPAGATE);
25✔
1202

1203
    _onLayerLoadingStateChange = debounce(() => {
×
1204
      // trigger loading indicator update without any change to update UI
1205
      this.props.visStateActions.setLoadingIndicator({change: 0});
1206
    }, DEBOUNCE_LOADING_STATE_PROPAGATE);
25✔
1207

×
1208
    _handleToggleLayerVisibility = (layer: Layer) => {
×
1209
      const {visStateActions} = this.props;
1210
      visStateActions.layerConfigChange(layer, {isVisible: !layer.config.isVisible});
1211
    };
25✔
1212

2✔
1213
    _toggleMapControl = panelId => {
1214
      const {index, uiStateActions} = this.props;
2✔
1215

1216
      uiStateActions.toggleMapControl(panelId, Number(index));
1217
    };
1218

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

1246
      const {layers, datasets, editor, interactionConfig} = visState;
29✔
1247

29✔
1248
      const layersToRender = this.layersToRenderSelector(this.props);
1249
      const layersForDeck = this.layersForDeckSelector(this.props);
1250

29✔
1251
      // Current style can be a custom style, from which we pull the mapbox API acccess token
29✔
1252
      const currentStyle = mapStyle.mapStyles?.[mapStyle.styleType];
29✔
1253
      const baseMapLibraryName = getBaseMapLibrary(currentStyle);
1254
      const baseMapLibraryConfig = getApplicationConfig().baseMapLibraryConfig[baseMapLibraryName];
1255

1256
      // Select the correct Map adapter based on the active base map library.
1257
      // Using the native adapter for each library avoids Transform API
1258
      // incompatibilities (e.g. mapbox-legacy's cloneTransform with MapLibre v5).
29✔
1259
      const ResolvedMapComponent =
29!
1260
        this.props.MapComponent ??
1261
        (baseMapLibraryName === MAP_LIB_OPTIONS.MAPBOX ? MapboxLegacyMap : MaplibreMap);
29✔
1262

1263
      const useMapboxAdapter = ResolvedMapComponent === MapboxLegacyMap;
29✔
1264

29✔
1265
      const internalViewState = this.context?.getInternalViewState(index);
29!
1266
      const configMaxPitch = mapState.maxPitch ?? getApplicationConfig().maxPitch;
1267
      const effectiveMaxPitch = useMapboxAdapter
1268
        ? Math.min(configMaxPitch, MAPBOX_MAX_PITCH)
29✔
1269
        : configMaxPitch;
1270
      const mapProps: Record<string, any> = {
1271
        ...internalViewState,
54✔
1272
        maxPitch: effectiveMaxPitch,
58✔
1273
        preserveDrawingBuffer: this.props.isExport ?? false,
1274
        mapboxAccessToken: currentStyle?.accessToken || mapboxApiAccessToken,
1275
        // baseApiUrl: mapboxApiUrl,
58✔
1276
        transformRequest:
58✔
1277
          this.props.transformRequest ||
1278
          transformRequest(currentStyle?.accessToken || mapboxApiAccessToken)
1279
      };
29!
1280

×
1281
      if (this.props.RTLTextPlugin !== undefined) {
1282
        mapProps.RTLTextPlugin = this.props.RTLTextPlugin;
1283
      }
29!
1284

×
1285
      if (useMapboxAdapter) {
×
1286
        const mapboxConfig = getApplicationConfig().baseMapLibraryConfig[MAP_LIB_OPTIONS.MAPBOX];
1287
        mapProps.mapLib = mapboxConfig.getMapLib();
1288
      }
29✔
1289

29✔
1290
      const hasGeocoderLayer = Boolean(layers.find(l => l.id === GEOCODER_LAYER_ID));
1291
      const isSplit = Boolean(mapState.isSplit);
29✔
1292

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

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

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

29✔
1428
    render() {
29✔
1429
      const {visState, mapStyle} = this.props;
29!
1430
      const mapContent = this._renderMap();
1431
      if (!mapContent) {
1432
        // mapContent can be null if onDeckRender returns null
×
1433
        // in this case we don't want to render the map
1434
        return null;
1435
      }
29✔
1436

29✔
1437
      const currentStyle = mapStyle.mapStyles?.[mapStyle.styleType];
29✔
1438
      const baseMapLibraryName = getBaseMapLibrary(currentStyle);
1439
      const baseMapLibraryConfig = getApplicationConfig().baseMapLibraryConfig[baseMapLibraryName];
29✔
1440

1441
      return (
1442
        <StyledMap
1443
          ref={this._ref}
×
1444
          style={this.styleSelector(this.props)}
1445
          onContextMenu={event => event.preventDefault()}
1446
          $mixBlendMode={visState.overlayBlending}
1447
          $mapLibCssClass={baseMapLibraryConfig.mapLibCssClass}
1448
        >
1449
          {mapContent}
1450
        </StyledMap>
1451
      );
1452
    }
1453
  }
14✔
1454

1455
  return withTheme(MapContainer);
1456
}
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