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

keplergl / kepler.gl / 25178462165

30 Apr 2026 05:00PM UTC coverage: 59.41% (-0.02%) from 59.425%
25178462165

push

github

web-flow
fix: integrate Maplibre support and update dependencies (#3395)

* integrate Maplibre support and update dependencies

- Added support for Maplibre by introducing a new Map component from @vis.gl/react-maplibre.
- Updated the MapComponent prop to allow selection between Mapbox and Maplibre.
- Updated package.json to include @vis.gl/react-maplibre and adjusted react-map-gl versioning.
- Cleaned up imports and adjusted map properties to accommodate the new library integration.

* chore: synchronize @vis.gl/react-maplibre version in package.json and yarn.lock

- Updated package.json and yarn.lock to ensure consistent versioning of @vis.gl/react-maplibre to 8.1.1.

* address comments

* fix build error

* fix website build

* fix lint

6903 of 13951 branches covered (49.48%)

Branch coverage included in aggregate %.

7 of 13 new or added lines in 2 files covered. (53.85%)

3 existing lines in 3 files now uncovered.

14247 of 21649 relevant lines covered (65.81%)

80.3 hits per line

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

41.88
/src/components/src/map-container.tsx
1
// SPDX-License-Identifier: MIT
2
// Copyright contributors to the kepler.gl project
3

4
// libraries
5
import React, {Component, createRef, useMemo} from 'react';
6
import styled, {withTheme, useTheme} from 'styled-components';
7
import {Map 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
  getBaseMapLibrary,
52
  BaseMapLibraryConfig,
53
  transformRequest,
54
  observeDimensions,
55
  unobserveDimensions,
56
  hasMobileWidth,
57
  getMapLayersFromSplitMaps,
58
  onViewPortChange,
59
  getViewportFromMapState,
60
  normalizeEvent,
61
  rgbToHex,
62
  computeDeckEffects,
63
  getApplicationConfig,
64
  GetMapRef,
65
  getLayerBlendingParameters,
66
  patchDeckRendererForPostProcessing
67
} from '@kepler.gl/utils';
68
import {breakPointValues} from '@kepler.gl/styles';
69

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

307
  return memoizedComponents;
28✔
308
};
309

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

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

328
type AttributionLogosProps = {
329
  logos: AttributionWithStyle[];
330
  activeSidePanel?: boolean;
331
  sidePanelWidth?: number;
332
};
333

334
const LOGO_LEFT_ADJUSTMENT = 3;
7✔
335

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

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

364
MapContainerFactory.deps = [MapPopoverFactory, MapControlFactory, EditorFactory];
7✔
365

366
type MapboxStyle = string | object | undefined;
367
type PropSelector<R> = Selector<MapContainerProps, R>;
368

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

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

392
  isLoadingIndicatorVisible?: boolean;
393
  activeSidePanel: string | null;
394
  sidePanelWidth?: number;
395

396
  locale?: any;
397
  theme?: any;
398
  editor?: any;
399
  MapComponent?: typeof MapboxLegacyMap | typeof MaplibreMap;
400
  deckGlProps?: any;
401
  onDeckInitialized?: (a: any, b: any) => void;
402
  onViewStateChange?: (viewport: Viewport) => void;
403

404
  topMapContainerProps: any;
405
  bottomMapContainerProps: any;
406
  transformRequest?: (url: string, resourceType?: string) => {url: string};
407

408
  datasetAttributions?: DatasetAttribution[];
409
  attributionLogos?: AttributionWithStyle[];
410

411
  generateMapboxLayers?: typeof generateMapboxLayers;
412
  generateDeckGLLayers?: typeof computeDeckLayers;
413

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

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

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

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

435
    private anyActiveLayerLoading = false;
25✔
436

437
    static contextType = MapViewStateContext;
14✔
438

439
    declare context: React.ContextType<typeof MapViewStateContext>;
440

441
    static defaultProps = {
14✔
442
      deckGlProps: {},
443
      index: 0,
444
      primary: true
445
    };
446

447
    constructor(props) {
448
      super(props);
25✔
449
      patchDeckRendererForPostProcessing();
25✔
450
    }
451

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

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

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

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

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

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

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

544
    generateMapboxLayerMethodSelector = props => props.generateMapboxLayers ?? generateMapboxLayers;
25!
545

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

741
    /* component render functions */
742

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

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

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

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

779
      const compareMode = interactionConfig.tooltip.config
×
780
        ? interactionConfig.tooltip.config.compareMode
781
        : false;
782

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

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

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

844
    /* eslint-enable complexity */
845

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

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

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

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

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

885
      const editorFeatureSelectedIndex = this.selectedPolygonIndexSelector(this.props);
29✔
886

887
      const {setFeatures, onLayerClick, setSelectedFeature} = visStateActions;
29✔
888

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

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

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

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

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

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

966
      const views = deckGlProps?.views
29!
967
        ? deckGlProps?.views()
968
        : new MapView({farZMultiplier: 1.2});
29✔
969

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

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

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

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

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

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

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

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

1084
      this.previousLayers = mapboxLayers;
1085
    }
1086

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

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

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

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

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

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

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

1136
      uiStateActions.toggleMapControl(panelId, Number(index));
1137
    };
1138

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

1166
      const {layers, datasets, editor, interactionConfig} = visState;
29✔
1167

29✔
1168
      const layersToRender = this.layersToRenderSelector(this.props);
1169
      const layersForDeck = this.layersForDeckSelector(this.props);
1170

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

1176
      // Select the correct Map adapter based on the active base map library.
1177
      // Using the native adapter for each library avoids Transform API
1178
      // incompatibilities (e.g. mapbox-legacy's cloneTransform with MapLibre v5).
29✔
1179
      const ResolvedMapComponent =
29!
1180
        this.props.MapComponent ??
1181
        (baseMapLibraryName === MAP_LIB_OPTIONS.MAPBOX ? MapboxLegacyMap : MaplibreMap);
29✔
1182

1183
      const useMapboxAdapter = ResolvedMapComponent === MapboxLegacyMap;
29✔
1184

29✔
1185
      const internalViewState = this.context?.getInternalViewState(index);
29!
1186
      const configMaxPitch = mapState.maxPitch ?? getApplicationConfig().maxPitch;
1187
      const effectiveMaxPitch = useMapboxAdapter
1188
        ? Math.min(configMaxPitch, MAPBOX_MAX_PITCH)
29✔
1189
        : configMaxPitch;
1190
      const mapProps: Record<string, any> = {
1191
        ...internalViewState,
54✔
1192
        maxPitch: effectiveMaxPitch,
58✔
1193
        preserveDrawingBuffer: this.props.isExport ?? false,
1194
        mapboxAccessToken: currentStyle?.accessToken || mapboxApiAccessToken,
1195
        // baseApiUrl: mapboxApiUrl,
58✔
1196
        transformRequest:
58✔
1197
          this.props.transformRequest ||
1198
          transformRequest(currentStyle?.accessToken || mapboxApiAccessToken)
1199
      };
29!
UNCOV
1200

×
NEW
1201
      if (useMapboxAdapter) {
×
1202
        const mapboxConfig = getApplicationConfig().baseMapLibraryConfig[MAP_LIB_OPTIONS.MAPBOX];
1203
        mapProps.mapLib = mapboxConfig.getMapLib();
1204
      }
29✔
1205

29✔
1206
      const hasGeocoderLayer = Boolean(layers.find(l => l.id === GEOCODER_LAYER_ID));
1207
      const isSplit = Boolean(mapState.isSplit);
29✔
1208

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

1267
          {deck}
1268
          {this._renderMapboxOverlays()}
53✔
1269
          <Editor
1270
            index={index || 0}
1271
            datasets={datasets}
1272
            editor={editor}
1273
            filters={this.polygonFiltersSelector(this.props)}
1274
            layers={layers}
1275
            onDeleteFeature={visStateActions.deleteFeature}
1276
            onSelect={visStateActions.setSelectedFeature}
1277
            onTogglePolygonFilter={visStateActions.setPolygonFilterLayer}
1278
            onSetEditorMode={visStateActions.setEditorMode}
1279
            style={{
1280
              pointerEvents: 'all',
29!
1281
              position: 'absolute',
1282
              display: editor.visible ? 'block' : 'none'
1283
            }}
1284
          />
29!
1285
          {this.props.children}
1286
          {mapStyle.topMapStyle ? (
1287
            <ResolvedMapComponent
1288
              key={`top-${baseMapLibraryName}`}
1289
              viewState={internalViewState}
1290
              maxPitch={effectiveMaxPitch}
1291
              mapStyle={mapStyle.topMapStyle}
1292
              style={MAP_STYLE.top}
1293
              mapboxAccessToken={mapProps.mapboxAccessToken}
×
1294
              transformRequest={mapProps.transformRequest}
1295
              {...(useMapboxAdapter
1296
                ? {
1297
                    mapLib:
1298
                      getApplicationConfig().baseMapLibraryConfig[
1299
                        MAP_LIB_OPTIONS.MAPBOX
1300
                      ].getMapLib()
1301
                  }
1302
                : {})}
1303
              {...topMapContainerProps}
1304
            />
1305
          ) : null}
29!
1306

1307
          {hasGeocoderLayer
1308
            ? this._renderDeckOverlay(
1309
                {[GEOCODER_LAYER_ID]: hasGeocoderLayer},
1310
                {primaryMap: false, isInteractive: false}
1311
              )
1312
            : null}
83✔
1313
          {this._renderMapPopover()}
1314
          {!isExport && primary !== isSplit ? (
34✔
1315
            <LoadingIndicator
1316
              isVisible={Boolean(isLoadingIndicatorVisible || this.anyActiveLayerLoading)}
1317
              activeSidePanel={Boolean(activeSidePanel)}
1318
              sidePanelWidth={sidePanelWidth}
1319
              hasAttributionLogos={attributionLogos.length > 0}
1320
            />
29✔
1321
          ) : null}
1322
          {this.props.primary ? (
1323
            <Attribution
1324
              showBaseMapLibLogo={this.state.showBaseMapAttribution}
1325
              showOsmBasemapAttribution={true}
1326
              datasetAttributions={datasetAttributions}
1327
              baseMapLibraryConfig={baseMapLibraryConfig}
1328
            />
29✔
1329
          ) : null}
1330
          {this.props.primary ? (
1331
            <AttributionLogos
1332
              logos={attributionLogos}
1333
              activeSidePanel={Boolean(activeSidePanel)}
1334
              sidePanelWidth={sidePanelWidth}
1335
            />
1336
          ) : null}
1337
        </>
1338
      );
1339
    }
1340

29✔
1341
    render() {
29✔
1342
      const {visState, mapStyle} = this.props;
29!
1343
      const mapContent = this._renderMap();
1344
      if (!mapContent) {
1345
        // mapContent can be null if onDeckRender returns null
×
1346
        // in this case we don't want to render the map
1347
        return null;
1348
      }
29✔
1349

29✔
1350
      const currentStyle = mapStyle.mapStyles?.[mapStyle.styleType];
29✔
1351
      const baseMapLibraryName = getBaseMapLibrary(currentStyle);
1352
      const baseMapLibraryConfig = getApplicationConfig().baseMapLibraryConfig[baseMapLibraryName];
29✔
1353

1354
      return (
1355
        <StyledMap
1356
          ref={this._ref}
×
1357
          style={this.styleSelector(this.props)}
1358
          onContextMenu={event => event.preventDefault()}
1359
          $mixBlendMode={visState.overlayBlending}
1360
          $mapLibCssClass={baseMapLibraryConfig.mapLibCssClass}
1361
        >
1362
          {mapContent}
1363
        </StyledMap>
1364
      );
1365
    }
1366
  }
14✔
1367

1368
  return withTheme(MapContainer);
1369
}
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