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

keplergl / kepler.gl / 24851934028

23 Apr 2026 06:28PM UTC coverage: 59.477% (+0.008%) from 59.469%
24851934028

push

github

web-flow
feat: add optional higher pitch option (#3384)

* feat: add optional higher pitch option

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

* tests, lint

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

* fall back to default - 60 for mapbox

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

---------

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

6836 of 13769 branches covered (49.65%)

Branch coverage included in aggregate %.

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

21 existing lines in 1 file now uncovered.

14063 of 21369 relevant lines covered (65.81%)

75.92 hits per line

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

41.54
/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
  MAPBOX_MAX_PITCH,
78
  MAP_LIB_OPTIONS
79
} from '@kepler.gl/constants';
80

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

306
  return memoizedComponents;
28✔
307
};
308

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

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

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

333
const LOGO_LEFT_ADJUSTMENT = 3;
7✔
334

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

434
    private anyActiveLayerLoading = false;
25✔
435

436
    static contextType = MapViewStateContext;
14✔
437

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

702
    _onDeckError = (error, layer) => {
25✔
703
      const errorMessage = error?.message || 'unknown-error';
×
704
      const layerMessage = layer?.id ? ` in ${layer.id} layer` : '';
×
705
      const errorMessageFull =
706
        errorMessage === 'WebGL context is lost'
×
707
          ? 'Your GPU was disconnected. This can happen if your computer goes to sleep. It can also occur for other reasons, such as if you are running too many GPU applications.'
708
          : `An error in deck.gl: ${errorMessage}${layerMessage}.`;
709

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

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

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

741
    /* component render functions */
742

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

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

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

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

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

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

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

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

844
    /* eslint-enable complexity */
845

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

×
1087
      updateMapboxLayers(this._map, mapboxLayers, this.previousLayers);
UNCOV
1088

×
1089
      this.previousLayers = mapboxLayers;
1090
    }
1091

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

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

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

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

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

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

25✔
1138
    _toggleMapControl = panelId => {
2✔
1139
      const {index, uiStateActions} = this.props;
1140

2✔
1141
      uiStateActions.toggleMapControl(panelId, Number(index));
1142
    };
1143

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

29✔
1172
      const {layers, datasets, editor, interactionConfig} = visState;
1173

29✔
1174
      const layersToRender = this.layersToRenderSelector(this.props);
29✔
1175
      const layersForDeck = this.layersForDeckSelector(this.props);
1176

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

29✔
1183
      const internalViewState = this.context?.getInternalViewState(index);
29✔
1184
      const configMaxPitch = mapState.maxPitch ?? getApplicationConfig().maxPitch;
1185
      const effectiveMaxPitch = baseMapLibraryName === MAP_LIB_OPTIONS.MAPBOX
29!
1186
        ? Math.min(configMaxPitch, MAPBOX_MAX_PITCH)
1187
        : configMaxPitch;
1188
      const mapProps = {
29✔
1189
        ...internalViewState,
1190
        maxPitch: effectiveMaxPitch,
1191
        preserveDrawingBuffer: this.props.isExport ?? false,
54✔
1192
        mapboxAccessToken: currentStyle?.accessToken || mapboxApiAccessToken,
58✔
1193
        // baseApiUrl: mapboxApiUrl,
1194
        mapLib: baseMapLibraryConfig.getMapLib(),
1195
        transformRequest:
1196
          this.props.transformRequest ||
58✔
1197
          transformRequest(currentStyle?.accessToken || mapboxApiAccessToken)
58✔
1198
      };
1199

1200
      const hasGeocoderLayer = Boolean(layers.find(l => l.id === GEOCODER_LAYER_ID));
29✔
1201
      const isSplit = Boolean(mapState.isSplit);
29✔
1202

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

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

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

1328
    render() {
1329
      const {visState, mapStyle} = this.props;
29✔
1330
      const mapContent = this._renderMap();
29✔
1331
      if (!mapContent) {
29!
1332
        // mapContent can be null if onDeckRender returns null
1333
        // in this case we don't want to render the map
1334
        return null;
×
1335
      }
1336

1337
      const currentStyle = mapStyle.mapStyles?.[mapStyle.styleType];
29✔
1338
      const baseMapLibraryName = getBaseMapLibrary(currentStyle);
29✔
1339
      const baseMapLibraryConfig =
1340
        getApplicationConfig().baseMapLibraryConfig?.[baseMapLibraryName];
29✔
1341

1342
      return (
29✔
1343
        <StyledMap
1344
          ref={this._ref}
1345
          style={this.styleSelector(this.props)}
1346
          onContextMenu={event => event.preventDefault()}
×
1347
          $mixBlendMode={visState.overlayBlending}
1348
          $mapLibCssClass={baseMapLibraryConfig.mapLibCssClass}
1349
        >
1350
          {mapContent}
1351
        </StyledMap>
1352
      );
1353
    }
1354
  }
1355

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