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

keplergl / kepler.gl / 24678467843

20 Apr 2026 04:38PM UTC coverage: 59.525% (-0.4%) from 59.936%
24678467843

push

github

web-flow
fix(effects): fixes for effects regressions (#3376)

* fix: fixes for effects regressions

Signed-off-by: Ihor Dykhta <dikhta.igor@gmail.com>

* possible fixes

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

* fix: update allow hover tooltip (#3377)

* update allow hover tooltip

Signed-off-by: bdjulbic <bdjulbic@foursquare.com>

* Update src/constants/src/layers.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: bdjulbic <bdjulbic@foursquare.com>

* update allow hover tooltip

Signed-off-by: bdjulbic <bdjulbic@foursquare.com>

---------

Signed-off-by: bdjulbic <bdjulbic@foursquare.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Ihor Dykhta <ihordykhta@Ihors-MacBook-Pro.local>

* fix: video export fixes (#3378)

* fix: video export fixes

Signed-off-by: Ihor Dykhta <dikhta.igor@gmail.com>

* cleanup; patch on mount

Signed-off-by: Ihor Dykhta <dikhta.igor@gmail.com>

* tests

Signed-off-by: Ihor Dykhta <dikhta.igor@gmail.com>

---------

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

* revert extra

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

* color fixes

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

* fix video export and tiled layers

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

* fix light intensity

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

* extend far frustum - prevent cutting off far geometries

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

* animate fog/sea level

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

* get 'water leve' animation

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

* lint

Signed-off-by: Ihor Dykhta <ihordykhta@Ihors-MacBo... (continued)

6795 of 13681 branches covered (49.67%)

Branch coverage included in aggregate %.

148 of 331 new or added lines in 15 files covered. (44.71%)

143 existing lines in 7 files now uncovered.

14002 of 21257 relevant lines covered (65.87%)

76.03 hits per line

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

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

4
// libraries
5
import React, {Component, createRef, useMemo} from 'react';
6
import styled, {withTheme, useTheme} from 'styled-components';
7
import {Map, MapRef} from 'react-map-gl';
8
import {PickingInfo, MapView} from '@deck.gl/core';
9
import DeckGL from '@deck.gl/react';
10
import {createSelector, Selector} from 'reselect';
11
import {useDroppable} from '@dnd-kit/core';
12
import debounce from 'lodash/debounce';
13

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

304
  return memoizedComponents;
28✔
305
};
306

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

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

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

331
const LOGO_LEFT_ADJUSTMENT = 3;
7✔
332

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

432
    private anyActiveLayerLoading = false;
25✔
433

434
    static contextType = MapViewStateContext;
14✔
435

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

576
    _onLayerSetDomain = (
25✔
577
      idx: number,
578
      value: number[] | {domain: VisualChannelDomain; aggregatedBins: AggregatedBin[]}
579
    ) => {
580
      // deck.gl 9 native aggregation layers (Grid, Hexagon) pass [min, max],
581
      // while ClusterLayer's CPUAggregator still passes {domain, aggregatedBins}.
582
      const config = Array.isArray(value)
×
583
        ? {colorDomain: value as VisualChannelDomain}
584
        : {colorDomain: value.domain, aggregatedBins: value.aggregatedBins};
585

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

737
    /* component render functions */
738

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

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

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

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

775
      const compareMode = interactionConfig.tooltip.config
×
776
        ? interactionConfig.tooltip.config.compareMode
777
        : false;
778

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

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

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

840
    /* eslint-enable complexity */
841

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

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

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

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

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

881
      const editorFeatureSelectedIndex = this.selectedPolygonIndexSelector(this.props);
29✔
882

883
      const {setFeatures, onLayerClick, setSelectedFeature} = visStateActions;
29✔
884

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

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

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

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

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

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

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

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

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

987
      return (
29✔
988
        <div
989
          {...(isInteractive
29!
990
            ? {
991
                onMouseMove: primaryMap
29!
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}
1008
            controller={
1009
              isInteractive
29!
1010
                ? {
1011
                    doubleClickZoom: !isEditorDrawingMode,
1012
                    dragRotate: this.props.mapState.dragRotate
1013
                  }
1014
                : false
1015
            }
1016
            initialViewState={internalViewState}
1017
            onBeforeRender={this._onBeforeRender}
1018
            onViewStateChange={isInteractive ? this._onViewportChange : undefined}
29!
1019
            {...extraDeckParams}
1020
            onHover={
1021
              isInteractive
29!
1022
                ? data => {
1023
                    const res = EditorLayerUtils.onHover(data, {
×
1024
                      editorMenuActive,
1025
                      editor,
1026
                      hoverInfo
1027
                    });
1028
                    if (res) return;
×
1029

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

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

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

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

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

1083
      this.previousLayers = mapboxLayers;
×
1084
    }
1085

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

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

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

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

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

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

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

1135
      uiStateActions.toggleMapControl(panelId, Number(index));
2✔
1136
    };
1137

1138
    /* eslint-disable complexity */
1139
    _renderMap() {
1140
      const {
1141
        visState,
1142
        mapState,
1143
        mapStyle,
1144
        mapStateActions,
1145
        MapComponent = Map,
×
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 = [],
×
1159
        attributionLogos = [],
×
1160
        containerId = 0,
13✔
1161
        isLoadingIndicatorVisible,
1162
        activeSidePanel,
1163
        sidePanelWidth
1164
      } = this.props;
29✔
1165

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

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

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

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

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

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

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

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

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

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

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

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