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

keplergl / kepler.gl / 25196520065

01 May 2026 12:39AM UTC coverage: 59.169%. First build
25196520065

Pull #3403

github

web-flow
Merge aad62c2ae into ea81bbf29
Pull Request #3403: chore: fix tests

6923 of 14060 branches covered (49.24%)

Branch coverage included in aggregate %.

1 of 2 new or added lines in 1 file covered. (50.0%)

14276 of 21768 relevant lines covered (65.58%)

80.03 hits per line

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

41.79
/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
  /** Pass `false` to disable the remote RTL text plugin, or a URL string to self-host it. */
409
  RTLTextPlugin?: string | false;
410

411
  datasetAttributions?: DatasetAttribution[];
412
  attributionLogos?: AttributionWithStyle[];
413

414
  generateMapboxLayers?: typeof generateMapboxLayers;
415
  generateDeckGLLayers?: typeof computeDeckLayers;
416

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

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

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

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

438
    private anyActiveLayerLoading = false;
25✔
439

440
    static contextType = MapViewStateContext;
14✔
441

442
    declare context: React.ContextType<typeof MapViewStateContext>;
443

444
    static defaultProps = {
14✔
445
      deckGlProps: {},
446
      index: 0,
447
      primary: true
448
    };
449

450
    constructor(props) {
451
      super(props);
25✔
452
      patchDeckRendererForPostProcessing();
25✔
453
    }
454

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

460
    componentDidMount() {
461
      if (!this._ref.current) {
25!
462
        return;
×
463
      }
464
      observeDimensions(this._ref.current, this._handleResize);
25✔
465
    }
466

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

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

484
    previousLayers = {
25✔
485
      // [layers.id]: mapboxLayerConfig
486
    };
487

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

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

547
    generateMapboxLayerMethodSelector = props => props.generateMapboxLayers ?? generateMapboxLayers;
25!
548

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

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

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

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

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

593
      const layer = this.props.visState.layers[idx];
×
594
      if (!layer) return;
×
595

596
      this.props.visStateActions.layerConfigChange(layer, config as Partial<LayerBaseConfig>);
×
597
    };
598

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

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

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

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

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

632
    _onMapboxStyleUpdate = update => {
25✔
633
      // force refresh mapboxgl layers
634
      this.previousLayers = {};
×
635
      this._updateMapboxLayers();
×
636

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

645
      if (typeof this.props.onMapStyleLoaded === 'function') {
×
646
        this.props.onMapStyleLoaded(this._map);
×
647
      }
648
    };
649

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

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

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

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

685
    _onDeckInitialized(device) {
686
      if (this.props.onDeckInitialized) {
×
687
        this.props.onDeckInitialized(this._deck, device);
×
688
      }
689
    }
690

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

701
    _onBeforeRender = () => {
25✔
702
      // no-op
703
    };
704

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

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

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

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

744
    /* component render functions */
745

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

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

769
      if (!mousePosition || !interactionConfig.tooltip) {
×
770
        return null;
×
771
      }
772

773
      const layerHoverProp = getLayerHoverProp({
×
774
        animationConfig,
775
        interactionConfig,
776
        hoverInfo,
777
        layers,
778
        layersToRender,
779
        datasets
780
      });
781

782
      const compareMode = interactionConfig.tooltip.config
×
783
        ? interactionConfig.tooltip.config.compareMode
784
        : false;
785

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

807
      const commonProp = {
×
808
        onClose: this._onCloseMapPopover,
809
        zoom: mapState.zoom,
810
        container: this._deck ? this._deck.canvas : undefined
×
811
      };
812

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

847
    /* eslint-enable complexity */
848

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

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

876
      const {hoverInfo, editor} = visState;
29✔
877
      const {primaryMap, isInteractive, children} = options;
29✔
878

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

884
      const internalViewState = this.context?.getInternalViewState(index);
29✔
885
      const internalMapState = {...mapState, ...internalViewState};
29✔
886
      const viewport = getViewportFromMapState(internalMapState);
29✔
887

888
      const editorFeatureSelectedIndex = this.selectedPolygonIndexSelector(this.props);
29✔
889

890
      const {setFeatures, onLayerClick, setSelectedFeature} = visStateActions;
29✔
891

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

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

944
          return EditorLayerUtils.getTooltip(info, {
×
945
            editorMenuActive,
946
            editor,
947
            theme
948
          });
949
        };
950

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

959
          if (isDragging) return 'grabbing';
×
960
          if (hoverInfo?.layer) return 'pointer';
×
961
          return 'grab';
×
962
        };
963
      }
964

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

969
      const views = deckGlProps?.views
29!
970
        ? deckGlProps?.views()
971
        : new MapView({farZMultiplier: 1.2});
29✔
972

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

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

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

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

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

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

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

1085
      updateMapboxLayers(this._map, mapboxLayers, this.previousLayers);
×
1086

1087
      this.previousLayers = mapboxLayers;
1088
    }
1089

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

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

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

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

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

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

2✔
1136
    _toggleMapControl = panelId => {
1137
      const {index, uiStateActions} = this.props;
2✔
1138

1139
      uiStateActions.toggleMapControl(panelId, Number(index));
1140
    };
1141

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

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

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

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

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

1186
      const useMapboxAdapter = ResolvedMapComponent === MapboxLegacyMap;
29✔
1187

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

×
1204
      if (this.props.RTLTextPlugin !== undefined) {
1205
        mapProps.RTLTextPlugin = this.props.RTLTextPlugin;
1206
      }
29!
NEW
1207

×
1208
      if (useMapboxAdapter) {
×
1209
        const mapboxConfig = getApplicationConfig().baseMapLibraryConfig[MAP_LIB_OPTIONS.MAPBOX];
1210
        mapProps.mapLib = mapboxConfig.getMapLib();
1211
      }
29✔
1212

29✔
1213
      const hasGeocoderLayer = Boolean(layers.find(l => l.id === GEOCODER_LAYER_ID));
1214
      const isSplit = Boolean(mapState.isSplit);
29✔
1215

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

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

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

29✔
1351
    render() {
29✔
1352
      const {visState, mapStyle} = this.props;
29!
1353
      const mapContent = this._renderMap();
1354
      if (!mapContent) {
1355
        // mapContent can be null if onDeckRender returns null
×
1356
        // in this case we don't want to render the map
1357
        return null;
1358
      }
29✔
1359

29✔
1360
      const currentStyle = mapStyle.mapStyles?.[mapStyle.styleType];
29✔
1361
      const baseMapLibraryName = getBaseMapLibrary(currentStyle);
1362
      const baseMapLibraryConfig = getApplicationConfig().baseMapLibraryConfig[baseMapLibraryName];
29✔
1363

1364
      return (
1365
        <StyledMap
1366
          ref={this._ref}
×
1367
          style={this.styleSelector(this.props)}
1368
          onContextMenu={event => event.preventDefault()}
1369
          $mixBlendMode={visState.overlayBlending}
1370
          $mapLibCssClass={baseMapLibraryConfig.mapLibCssClass}
1371
        >
1372
          {mapContent}
1373
        </StyledMap>
1374
      );
1375
    }
1376
  }
14✔
1377

1378
  return withTheme(MapContainer);
1379
}
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