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

keplergl / kepler.gl / 24840185847

23 Apr 2026 02:12PM UTC coverage: 59.472% (-0.09%) from 59.564%
24840185847

Pull #3384

github

web-flow
Merge fd6849e76 into 72ea4614c
Pull Request #3384: feat: add optional higher pitch option

6833 of 13765 branches covered (49.64%)

Branch coverage included in aggregate %.

2 of 2 new or added lines in 2 files covered. (100.0%)

219 existing lines in 6 files now uncovered.

14060 of 21366 relevant lines covered (65.81%)

75.92 hits per line

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

41.09
/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;
309
  bottom: ${props => props.theme.sidePanel.margin.left}px;
×
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: Record<number, AggregatedBin>}
579
    ) => {
580
      // deck.gl 9 native aggregation layers (Grid, Hexagon) pass
581
      // {domain, aggregatedBins} via our ScaleEnhanced* overrides,
582
      // while ClusterLayer's CPUAggregator also passes {domain, aggregatedBins}.
583
      // Plain [min, max] is a fallback if the override is bypassed.
UNCOV
584
      const config = Array.isArray(value)
×
585
        ? {colorDomain: value as VisualChannelDomain}
586
        : {colorDomain: value.domain, aggregatedBins: value.aggregatedBins};
587

UNCOV
588
      const layer = this.props.visState.layers[idx];
×
589
      if (!layer) return;
×
590

UNCOV
591
      this.props.visStateActions.layerConfigChange(layer, config as Partial<LayerBaseConfig>);
×
592
    };
593

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

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

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

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

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

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

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

UNCOV
640
      if (typeof this.props.onMapStyleLoaded === 'function') {
×
UNCOV
641
        this.props.onMapStyleLoaded(this._map);
×
642
      }
643
    };
644

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

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

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

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

680
    _onDeckInitialized(device) {
UNCOV
681
      if (this.props.onDeckInitialized) {
×
UNCOV
682
        this.props.onDeckInitialized(this._deck, device);
×
683
      }
684
    }
685

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

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

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

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

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

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

739
    /* component render functions */
740

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

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

UNCOV
764
      if (!mousePosition || !interactionConfig.tooltip) {
×
UNCOV
765
        return null;
×
766
      }
767

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

UNCOV
777
      const compareMode = interactionConfig.tooltip.config
×
778
        ? interactionConfig.tooltip.config.compareMode
779
        : false;
780

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

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

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

842
    /* eslint-enable complexity */
843

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

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

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

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

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

883
      const editorFeatureSelectedIndex = this.selectedPolygonIndexSelector(this.props);
29✔
884

885
      const {setFeatures, onLayerClick, setSelectedFeature} = visStateActions;
29✔
886

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

UNCOV
1086
      this.previousLayers = mapboxLayers;
×
1087
    }
1088

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

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

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

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

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

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

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

1138
      uiStateActions.toggleMapControl(panelId, Number(index));
2✔
1139
    };
1140

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

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

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

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

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

1192
      const hasGeocoderLayer = Boolean(layers.find(l => l.id === GEOCODER_LAYER_ID));
29✔
1193
      const isSplit = Boolean(mapState.isSplit);
29✔
1194

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

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

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

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

1328
      const currentStyle = mapStyle.mapStyles?.[mapStyle.styleType];
29✔
1329
      const baseMapLibraryName = getBaseMapLibrary(currentStyle);
29✔
1330
      const baseMapLibraryConfig =
1331
        getApplicationConfig().baseMapLibraryConfig?.[baseMapLibraryName];
29✔
1332

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

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