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

keplergl / kepler.gl / 24401782527

14 Apr 2026 01:30PM UTC coverage: 59.864% (-0.1%) from 59.991%
24401782527

Pull #3375

github

web-flow
Merge 1c378ab79 into b0fc76059
Pull Request #3375: fix: updates to attribution logic for tiled layers

6736 of 13439 branches covered (50.12%)

Branch coverage included in aggregate %.

41 of 98 new or added lines in 9 files covered. (41.84%)

110 existing lines in 3 files now uncovered.

13798 of 20862 relevant lines covered (66.14%)

77.33 hits per line

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

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

4
// libraries
5
import React, {Component, createRef, useMemo} from 'react';
6
import styled, {withTheme} 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`
7✔
308
  position: absolute;
309
  bottom: 4px;
310
  left: 4px;
311
  z-index: 1;
312
  display: flex;
313
  align-items: flex-end;
314
  gap: 4px;
315
  pointer-events: auto;
316
`;
317

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

324
type AttributionLogosProps = {
325
  logos: AttributionWithStyle[];
326
};
327

328
export const AttributionLogos: React.FC<AttributionLogosProps> = ({logos}) => {
7✔
329
  if (!logos?.length) return null;
28!
NEW
330
  return (
×
331
    <StyledAttributionLogoContainer>
332
      {logos.map((logo, idx) => (
NEW
333
        <StyledLogoLink
×
334
          key={logo.logoUrl || idx}
×
335
          href={logo.url || undefined}
×
336
          target="_blank"
337
          rel="noopener noreferrer"
338
          enabled={Boolean(logo.url)}
339
          style={{bottom: logo.bottom ?? 0}}
×
340
        >
341
          <img src={logo.logoUrl} style={{height: logo.height || 12}} alt={logo.title} />
×
342
        </StyledLogoLink>
343
      ))}
344
    </StyledAttributionLogoContainer>
345
  );
346
};
347

348
MapContainerFactory.deps = [MapPopoverFactory, MapControlFactory, EditorFactory];
7✔
349

350
type MapboxStyle = string | object | undefined;
351
type PropSelector<R> = Selector<MapContainerProps, R>;
352

353
export interface MapContainerProps {
354
  visState: VisState;
355
  mapState: MapState;
356
  mapControls: MapControls;
357
  mapStyle: {bottomMapStyle?: MapboxStyle; topMapStyle?: MapboxStyle} & MapStyle;
358
  mapboxApiAccessToken: string;
359
  mapboxApiUrl: string;
360
  visStateActions: typeof VisStateActions;
361
  mapStateActions: typeof MapStateActions;
362
  uiStateActions: typeof UIStateActions;
363

364
  // optional
365
  primary?: boolean; // primary one will be reporting its size to appState
366
  readOnly?: boolean;
367
  isExport?: boolean;
368
  // onMapStyleLoaded?: (map: maplibregl.Map | ReturnType<MapRef['getMap']> | null) => void;
369
  onMapStyleLoaded?: (map: GetMapRef | null) => void;
370
  onMapRender?: (map: GetMapRef | null) => void;
371
  getMapboxRef?: (mapbox?: MapRef | null, index?: number) => void;
372
  index?: number;
373
  deleteMapLabels?: (containerId: string, layerId: string) => void;
374
  containerId?: number;
375

376
  isLoadingIndicatorVisible?: boolean;
377
  activeSidePanel: string | null;
378
  sidePanelWidth?: number;
379

380
  locale?: any;
381
  theme?: any;
382
  editor?: any;
383
  MapComponent?: typeof Map;
384
  deckGlProps?: any;
385
  onDeckInitialized?: (a: any, b: any) => void;
386
  onViewStateChange?: (viewport: Viewport) => void;
387

388
  topMapContainerProps: any;
389
  bottomMapContainerProps: any;
390
  transformRequest?: (url: string, resourceType?: string) => {url: string};
391

392
  datasetAttributions?: DatasetAttribution[];
393
  attributionLogos?: AttributionWithStyle[];
394

395
  generateMapboxLayers?: typeof generateMapboxLayers;
396
  generateDeckGLLayers?: typeof computeDeckLayers;
397

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

400
  children?: React.ReactNode;
401
  deckRenderCallbacks?: {
402
    onDeckLoad?: () => void;
403
    onDeckRender?: (deckProps: Record<string, unknown>) => Record<string, unknown> | null;
404
    onDeckAfterRender?: (deckProps: Record<string, unknown>) => any;
405
  };
406

407
  // Optional: override legend header logo in map controls (used by image export)
408
  logoComponent?: React.FC | React.ReactNode;
409
}
410

411
export default function MapContainerFactory(
412
  MapPopover: ReturnType<typeof MapPopoverFactory>,
413
  MapControl: ReturnType<typeof MapControlFactory>,
414
  Editor: ReturnType<typeof EditorFactory>
415
): React.ComponentType<MapContainerProps> {
416
  class MapContainer extends Component<MapContainerProps> {
417
    displayName = 'MapContainer';
25✔
418

419
    private anyActiveLayerLoading = false;
25✔
420

421
    static contextType = MapViewStateContext;
14✔
422

423
    declare context: React.ContextType<typeof MapViewStateContext>;
424

425
    static defaultProps = {
14✔
426
      MapComponent: Map,
427
      deckGlProps: {},
428
      index: 0,
429
      primary: true
430
    };
431

432
    constructor(props) {
433
      super(props);
25✔
434
      patchDeckRendererForPostProcessing();
25✔
435
    }
436

437
    state = {
25✔
438
      // Determines whether attribution should be visible based the result of loading the map style
439
      showBaseMapAttribution: true
440
    };
441

442
    componentDidMount() {
443
      if (!this._ref.current) {
25!
444
        return;
×
445
      }
446
      observeDimensions(this._ref.current, this._handleResize);
25✔
447
    }
448

449
    componentWillUnmount() {
450
      // unbind mapboxgl event listener
451
      if (this._map) {
2!
452
        this._map?.off(MAPBOXGL_STYLE_UPDATE, nop);
×
453
        this._map?.off(MAPBOXGL_RENDER, nop);
×
454
      }
455
      if (!this._ref.current) {
2!
456
        return;
×
457
      }
458
      unobserveDimensions(this._ref.current);
2✔
459
    }
460

461
    _deck: any = null;
25✔
462
    _map: GetMapRef | null = null;
25✔
463
    _ref = createRef<HTMLDivElement>();
25✔
464
    _deckGLErrorsElapsed: {[id: string]: number} = {};
25✔
465

466
    previousLayers = {
25✔
467
      // [layers.id]: mapboxLayerConfig
468
    };
469

470
    _handleResize = dimensions => {
25✔
471
      const {primary, index} = this.props;
×
472
      if (primary) {
×
473
        const {mapStateActions} = this.props;
×
474
        if (dimensions && dimensions.width > 0 && dimensions.height > 0) {
×
475
          mapStateActions.updateMap(dimensions, index);
×
476
        }
477
      }
478
    };
479

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

529
    generateMapboxLayerMethodSelector = props => props.generateMapboxLayers ?? generateMapboxLayers;
25!
530

531
    mapboxLayersSelector = createSelector(
25✔
532
      this.layersSelector,
533
      this.layerDataSelector,
534
      this.layerOrderSelector,
535
      this.layersToRenderSelector,
536
      this.generateMapboxLayerMethodSelector,
537
      (layer, layerData, layerOrder, layersToRender, generateMapboxLayerMethod) =>
538
        generateMapboxLayerMethod(layer, layerData, layerOrder, layersToRender)
×
539
    );
540

541
    // merge in a background-color style if the basemap choice is NO_MAP_ID
542
    // used by <StyledMap> inline style prop
543
    mapStyleTypeSelector = props => props.mapStyle.styleType;
28✔
544
    mapStyleBackgroundColorSelector = props => props.mapStyle.backgroundColor;
28✔
545
    styleSelector = createSelector(
25✔
546
      this.mapStyleTypeSelector,
547
      this.mapStyleBackgroundColorSelector,
548
      (styleType, backgroundColor) => ({
26✔
549
        ...MAP_STYLE.container,
550
        ...(styleType === NO_MAP_ID ? {backgroundColor: rgbToHex(backgroundColor)} : {})
26!
551
      })
552
    );
553

554
    /* component private functions */
555
    _onCloseMapPopover = () => {
25✔
556
      this.props.visStateActions.onLayerClick(null);
×
557
    };
558

559
    _onLayerHover = (_idx: number, info: PickingInfo<any> | null) => {
25✔
560
      this.props.visStateActions.onLayerHover(info, this.props.index);
×
561
    };
562

563
    _onLayerSetDomain = (
25✔
564
      idx: number,
565
      value: number[] | {domain: VisualChannelDomain; aggregatedBins: AggregatedBin[]}
566
    ) => {
567
      // deck.gl 9 native aggregation layers (Grid, Hexagon) pass [min, max],
568
      // while ClusterLayer's CPUAggregator still passes {domain, aggregatedBins}.
569
      const config = Array.isArray(value)
×
570
        ? {colorDomain: value as VisualChannelDomain}
571
        : {colorDomain: value.domain, aggregatedBins: value.aggregatedBins};
572

573
      const layer = this.props.visState.layers[idx];
×
574
      if (!layer) return;
×
575

576
      this.props.visStateActions.layerConfigChange(layer, config as Partial<LayerBaseConfig>);
×
577
    };
578

579
    _onRedrawNeeded = (_idx: number) => {
25✔
580
      // updateMapUpdater always returns a new state object reference, which triggers re-render
581
      const {mapStateActions, index} = this.props;
×
582
      mapStateActions.updateMap({}, index);
×
583
    };
584

585
    _onFitBounds = (_idx: number, bounds: [number, number, number, number]) => {
25✔
586
      this.props.mapStateActions.fitBounds(bounds);
×
587
    };
588

589
    _onLayerFilteredItemsChange = (idx, event) => {
25✔
590
      this.props.visStateActions.layerFilteredItemsChange(this.props.visState.layers[idx], event);
×
591
    };
592

593
    _onWMSFeatureInfo = (
25✔
594
      idx: number,
595
      data: {
596
        featureInfo: Array<{name: string; value: string}> | string | null;
597
        coordinate?: [number, number] | null;
598
      }
599
    ) => {
600
      this.props.visStateActions.wmsFeatureInfo(
×
601
        this.props.visState.layers[idx],
602
        data.featureInfo,
603
        data.coordinate
604
      );
605
    };
606

607
    _handleMapToggleLayer = layerId => {
25✔
608
      const {index: mapIndex = 0, visStateActions} = this.props;
1!
609
      visStateActions.toggleLayerForMap(mapIndex, layerId);
1✔
610
    };
611

612
    _onMapboxStyleUpdate = update => {
25✔
613
      // force refresh mapboxgl layers
614
      this.previousLayers = {};
×
615
      this._updateMapboxLayers();
×
616

617
      if (update && update.style) {
×
618
        // No attributions are needed if the style doesn't reference Mapbox sources
619
        this.setState({
×
620
          showBaseMapAttribution:
621
            isStyleUsingMapboxTiles(update.style) || !isStyleUsingOpenStreetMapTiles(update.style)
×
622
        });
623
      }
624

625
      if (typeof this.props.onMapStyleLoaded === 'function') {
×
626
        this.props.onMapStyleLoaded(this._map);
×
627
      }
628
    };
629

630
    _setMapRef = mapRef => {
25✔
631
      // Handle change of the map library
632
      if (this._map && mapRef) {
×
633
        const map = mapRef.getMap();
×
634
        if (map && this._map !== map) {
×
635
          this._map?.off(MAPBOXGL_STYLE_UPDATE, nop);
×
636
          this._map?.off(MAPBOXGL_RENDER, nop);
×
637
          this._map = null;
×
638
        }
639
      }
640

641
      if (!this._map && mapRef) {
×
642
        this._map = mapRef.getMap();
×
643
        // i noticed in certain context we don't access the actual map element
644
        if (!this._map) {
×
645
          return;
×
646
        }
647
        // bind mapboxgl event listener
648
        this._map.on(MAPBOXGL_STYLE_UPDATE, this._onMapboxStyleUpdate);
×
649

650
        this._map.on(MAPBOXGL_RENDER, () => {
×
651
          if (typeof this.props.onMapRender === 'function') {
×
652
            this.props.onMapRender(this._map);
×
653
          }
654
        });
655
      }
656

657
      if (this.props.getMapboxRef) {
×
658
        // The parent component can gain access to our MapboxGlMap by
659
        // providing this callback. Note that 'mapbox' will be null when the
660
        // ref is unset (e.g. when a split map is closed).
661
        this.props.getMapboxRef(mapRef, this.props.index);
×
662
      }
663
    };
664

665
    _onDeckInitialized(device) {
666
      if (this.props.onDeckInitialized) {
×
667
        this.props.onDeckInitialized(this._deck, device);
×
668
      }
669
    }
670

671
    /**
672
     * 1) Allow effects only for the first view.
673
     * 2) Prevent effect:preRender call without valid generated viewports.
674
     * @param viewIndex View index.
675
     * @returns Returns true if effects can be used.
676
     */
677
    _isOKToRenderEffects(viewIndex?: number): boolean {
678
      return !viewIndex && Boolean(this._deck?.viewManager?._viewports?.length);
29✔
679
    }
680

681
    _onBeforeRender = () => {
25✔
682
      // no-op
683
    };
684

685
    _onDeckError = (error, layer) => {
25✔
686
      const errorMessage = error?.message || 'unknown-error';
×
687
      const layerMessage = layer?.id ? ` in ${layer.id} layer` : '';
×
688
      const errorMessageFull =
689
        errorMessage === 'WebGL context is lost'
×
690
          ? '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.'
691
          : `An error in deck.gl: ${errorMessage}${layerMessage}.`;
692

693
      // Throttle error notifications, as React doesn't like too many state changes from here.
694
      const lastShown = this._deckGLErrorsElapsed[errorMessageFull];
×
695
      if (!lastShown || lastShown < Date.now() - THROTTLE_NOTIFICATION_TIME) {
×
696
        this._deckGLErrorsElapsed[errorMessageFull] = Date.now();
×
697

698
        // Mark layer as invalid
699
        let extraLayerMessage = '';
×
700
        const {visStateActions} = this.props;
×
701
        if (layer) {
×
702
          let topMostLayer = layer;
×
703
          while (topMostLayer.parent) {
×
704
            topMostLayer = topMostLayer.parent;
×
705
          }
706
          if (topMostLayer.props?.id) {
×
707
            visStateActions.layerSetIsValid(topMostLayer, false);
×
708
            extraLayerMessage = 'The layer has been disabled and highlighted.';
×
709
          }
710
        }
711

712
        // Create new error notification or update existing one with same id.
713
        // Update is required to preserve the order of notifications as they probably are going to "jump" based on order of errors.
714
        const {uiStateActions} = this.props;
×
715
        uiStateActions.addNotification(
×
716
          errorNotification({
717
            message: `${errorMessageFull} ${extraLayerMessage}`,
718
            id: errorMessageFull // treat the error message as id
719
          })
720
        );
721
      }
722
    };
723

724
    /* component render functions */
725

726
    /* eslint-disable complexity */
727
    _renderMapPopover() {
728
      // this check is for limiting the display of the `<MapPopover>` to the `<MapContainer>` the user is interacting with
729
      // the DeckGL onHover event handler adds a `mapIndex` property which is available in the `hoverInfo` object of `visState`
730
      if (this.props.index !== this.props.visState.hoverInfo?.mapIndex) {
29!
731
        return null;
29✔
732
      }
733

734
      // TODO: move this into reducer so it can be tested
735
      const {
736
        mapState,
737
        visState: {
738
          hoverInfo,
739
          clicked,
740
          datasets,
741
          interactionConfig,
742
          animationConfig,
743
          layers,
744
          mousePos: {mousePosition, coordinate, pinned}
745
        }
746
      } = this.props;
×
747
      const layersToRender = this.layersToRenderSelector(this.props);
×
748

749
      if (!mousePosition || !interactionConfig.tooltip) {
×
750
        return null;
×
751
      }
752

753
      const layerHoverProp = getLayerHoverProp({
×
754
        animationConfig,
755
        interactionConfig,
756
        hoverInfo,
757
        layers,
758
        layersToRender,
759
        datasets
760
      });
761

762
      const compareMode = interactionConfig.tooltip.config
×
763
        ? interactionConfig.tooltip.config.compareMode
764
        : false;
765

766
      let pinnedPosition = {x: 0, y: 0};
×
767
      let layerPinnedProp: LayerHoverProp | null = null;
×
768
      if (pinned || clicked) {
×
769
        // project lnglat to screen so that tooltip follows the object on zoom
770
        const viewport = getViewportFromMapState(mapState);
×
771
        const lngLat = clicked ? clicked.coordinate : pinned.coordinate;
×
772
        pinnedPosition = this._getHoverXY(viewport, lngLat);
×
773
        layerPinnedProp = getLayerHoverProp({
×
774
          animationConfig,
775
          interactionConfig,
776
          hoverInfo: clicked,
777
          layers,
778
          layersToRender,
779
          datasets
780
        });
781
        if (layerHoverProp && layerPinnedProp) {
×
782
          layerHoverProp.primaryData = layerPinnedProp.data;
×
783
          layerHoverProp.compareType = interactionConfig.tooltip.config.compareType;
×
784
        }
785
      }
786

787
      const commonProp = {
×
788
        onClose: this._onCloseMapPopover,
789
        zoom: mapState.zoom,
790
        container: this._deck ? this._deck.canvas : undefined
×
791
      };
792

793
      return (
×
794
        <ErrorBoundary>
795
          {layerPinnedProp && (
×
796
            <MapPopover
797
              {...pinnedPosition}
798
              {...commonProp}
799
              layerHoverProp={layerPinnedProp}
800
              coordinate={interactionConfig.coordinate.enabled && (pinned || {}).coordinate}
×
801
              frozen={true}
802
              isBase={compareMode}
803
              onSetFeatures={this.props.visStateActions.setFeatures}
804
              setSelectedFeature={this.props.visStateActions.setSelectedFeature}
805
              // @ts-ignore Argument of type 'Readonly<MapContainerProps>' is not assignable to parameter of type 'never'
806
              featureCollection={this.featureCollectionSelector(this.props)}
807
            />
808
          )}
809
          {layerHoverProp && (!layerPinnedProp || compareMode) && (
×
810
            <MapPopover
811
              x={mousePosition[0]}
812
              y={mousePosition[1]}
813
              {...commonProp}
814
              layerHoverProp={layerHoverProp}
815
              frozen={false}
816
              coordinate={interactionConfig.coordinate.enabled && coordinate}
×
817
              onSetFeatures={this.props.visStateActions.setFeatures}
818
              setSelectedFeature={this.props.visStateActions.setSelectedFeature}
819
              // @ts-ignore Argument of type 'Readonly<MapContainerProps>' is not assignable to parameter of type 'never'
820
              featureCollection={this.featureCollectionSelector(this.props)}
821
            />
822
          )}
823
        </ErrorBoundary>
824
      );
825
    }
826

827
    /* eslint-enable complexity */
828

829
    _getHoverXY(viewport, lngLat) {
830
      const screenCoord = !viewport || !lngLat ? null : viewport.project(lngLat);
×
831
      return screenCoord && {x: screenCoord[0], y: screenCoord[1]};
×
832
    }
833

834
    _renderDeckOverlay(
835
      layersForDeck,
836
      options: {primaryMap: boolean; isInteractive?: boolean; children?: React.ReactNode} = {
×
837
        primaryMap: false
838
      }
839
    ) {
840
      const {
841
        mapStyle,
842
        visState,
843
        mapState,
844
        visStateActions,
845
        mapboxApiAccessToken,
846
        mapboxApiUrl,
847
        deckGlProps,
848
        index,
849
        mapControls,
850
        deckRenderCallbacks,
851
        theme,
852
        generateDeckGLLayers,
853
        onMouseMove
854
      } = this.props;
29✔
855

856
      const {hoverInfo, editor} = visState;
29✔
857
      const {primaryMap, isInteractive, children} = options;
29✔
858

859
      // disable double click zoom when editor is in any draw mode
860
      const {mapDraw} = mapControls;
29✔
861
      const {active: editorMenuActive = false} = mapDraw || {};
29✔
862
      const isEditorDrawingMode = EditorLayerUtils.isDrawingActive(editorMenuActive, editor.mode);
29✔
863

864
      const internalViewState = this.context?.getInternalViewState(index);
29✔
865
      const internalMapState = {...mapState, ...internalViewState};
29✔
866
      const viewport = getViewportFromMapState(internalMapState);
29✔
867

868
      const editorFeatureSelectedIndex = this.selectedPolygonIndexSelector(this.props);
29✔
869

870
      const {setFeatures, onLayerClick, setSelectedFeature} = visStateActions;
29✔
871

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

912
      const extraDeckParams: {
913
        getTooltip?: (info: any) => object | null;
914
        getCursor?: ({isDragging}: {isDragging: boolean}) => string;
915
      } = {};
29✔
916
      if (primaryMap) {
29!
917
        // Omit hover updates when the pointer position is invalid, ie. over UI overlays or
918
        // outside the map container. In those cases x/y may be < 0
919
        extraDeckParams.getTooltip = info => {
29✔
920
          const x = Number(info?.x);
×
921
          const y = Number(info?.y);
×
922
          if (Number.isNaN(x) || Number.isNaN(y) || x < 0 || y < 0) return null;
×
923

924
          return EditorLayerUtils.getTooltip(info, {
×
925
            editorMenuActive,
926
            editor,
927
            theme
928
          });
929
        };
930

931
        extraDeckParams.getCursor = ({isDragging}: {isDragging: boolean}) => {
29✔
932
          const editorCursor = EditorLayerUtils.getCursor({
×
933
            editorMenuActive,
934
            editor,
935
            hoverInfo
936
          });
937
          if (editorCursor) return editorCursor;
×
938

939
          if (isDragging) return 'grabbing';
×
940
          if (hoverInfo?.layer) return 'pointer';
×
941
          return 'grab';
×
942
        };
943
      }
944

945
      const effects = this._isOKToRenderEffects(index)
29!
946
        ? computeDeckEffects({visState, mapState, isExport: this.props.isExport})
947
        : [];
948

949
      const views = deckGlProps?.views
29!
950
        ? deckGlProps?.views()
951
        : new MapView({legacyMeterSizes: true} as ConstructorParameters<typeof MapView>[0] & {
952
            legacyMeterSizes: boolean;
953
          });
954

955
      let allDeckGlProps = {
29✔
956
        ...deckGlProps,
957
        pickingRadius: DEFAULT_PICKING_RADIUS,
958
        views,
959
        layers: deckGlLayers,
960
        effects,
961
        parameters: getLayerBlendingParameters(visState.layerBlending)
962
      };
963

964
      if (typeof deckRenderCallbacks?.onDeckRender === 'function') {
29!
965
        allDeckGlProps = deckRenderCallbacks.onDeckRender(allDeckGlProps);
×
966
        if (!allDeckGlProps) {
×
967
          // if onDeckRender returns null, do not render deck.gl
968
          return null;
×
969
        }
970
      }
971

972
      return (
29✔
973
        <div
974
          {...(isInteractive
29!
975
            ? {
976
                onMouseMove: primaryMap
29!
977
                  ? event => {
978
                      onMouseMove?.(event);
×
979
                      this._onMouseMoveDebounced(event, viewport);
×
980
                    }
981
                  : undefined
982
              }
983
            : {style: {pointerEvents: 'none'}})}
984
        >
985
          <DeckGL
986
            id="default-deckgl-overlay"
987
            onLoad={() => {
988
              if (typeof deckRenderCallbacks?.onDeckLoad === 'function') {
×
989
                deckRenderCallbacks.onDeckLoad();
×
990
              }
991
            }}
992
            {...allDeckGlProps}
993
            controller={
994
              isInteractive
29!
995
                ? {
996
                    doubleClickZoom: !isEditorDrawingMode,
997
                    dragRotate: this.props.mapState.dragRotate
998
                  }
999
                : false
1000
            }
1001
            initialViewState={internalViewState}
1002
            onBeforeRender={this._onBeforeRender}
1003
            onViewStateChange={isInteractive ? this._onViewportChange : undefined}
29!
1004
            {...extraDeckParams}
1005
            onHover={
1006
              isInteractive
29!
1007
                ? data => {
1008
                    const res = EditorLayerUtils.onHover(data, {
×
1009
                      editorMenuActive,
1010
                      editor,
1011
                      hoverInfo
1012
                    });
1013
                    if (res) return;
×
1014

1015
                    this._onLayerHoverDebounced(data, index);
×
1016
                  }
1017
                : null
1018
            }
1019
            onClick={(data, event) => {
1020
              // @ts-ignore
1021
              normalizeEvent(event.srcEvent, viewport);
×
1022
              const res = EditorLayerUtils.onClick(data, event, {
×
1023
                editorMenuActive,
1024
                editor,
1025
                onLayerClick,
1026
                setSelectedFeature,
1027
                mapIndex: index
1028
              });
1029
              if (res) return;
×
1030

1031
              visStateActions.onLayerClick(data);
×
1032
            }}
1033
            onError={this._onDeckError}
1034
            ref={comp => {
1035
              // @ts-ignore
1036
              if (comp && comp.deck && !this._deck) {
35✔
1037
                // @ts-ignore
1038
                this._deck = comp.deck;
1✔
1039
              }
1040
            }}
1041
            onDeviceInitialized={device => this._onDeckInitialized(device)}
×
1042
            onAfterRender={() => {
1043
              if (typeof deckRenderCallbacks?.onDeckAfterRender === 'function') {
×
1044
                deckRenderCallbacks.onDeckAfterRender(allDeckGlProps);
×
1045
              }
1046

1047
              const anyActiveLayerLoading = areAnyDeckLayersLoading(allDeckGlProps.layers);
×
1048
              if (anyActiveLayerLoading !== this.anyActiveLayerLoading) {
×
1049
                this._onLayerLoadingStateChange();
×
1050
                this.anyActiveLayerLoading = anyActiveLayerLoading;
×
1051
              }
1052
            }}
1053
          >
1054
            {children}
1055
          </DeckGL>
1056
        </div>
1057
      );
1058
    }
1059

1060
    _updateMapboxLayers() {
1061
      const mapboxLayers = this.mapboxLayersSelector(this.props);
×
1062
      if (!Object.keys(mapboxLayers).length && !Object.keys(this.previousLayers).length) {
×
1063
        return;
×
1064
      }
1065

1066
      updateMapboxLayers(this._map, mapboxLayers, this.previousLayers);
×
1067

1068
      this.previousLayers = mapboxLayers;
×
1069
    }
1070

1071
    _renderMapboxOverlays() {
1072
      if (this._map && this._map.isStyleLoaded()) {
29!
1073
        this._updateMapboxLayers();
×
1074
      }
1075
    }
1076
    _onViewportChangePropagateDebounced = debounce(() => {
25✔
1077
      const viewState = this.context?.getInternalViewState(this.props.index);
×
1078
      onViewPortChange(
×
1079
        viewState,
1080
        this.props.mapStateActions.updateMap,
1081
        this.props.onViewStateChange,
1082
        this.props.primary,
1083
        this.props.index
1084
      );
1085
    }, DEBOUNCE_VIEWPORT_PROPAGATE);
1086

1087
    _onViewportChange = viewport => {
25✔
1088
      const {viewState} = viewport;
×
1089
      if (this.props.isExport) {
×
1090
        // Image export map shouldn't be interactive (otherwise this callback can
1091
        // lead to inadvertent changes to the state of the main map)
1092
        return;
×
1093
      }
1094
      const {setInternalViewState} = this.context;
×
1095
      setInternalViewState(viewState, this.props.index);
×
1096
      this._onViewportChangePropagateDebounced();
×
1097
    };
1098

1099
    _onLayerHoverDebounced = debounce((data, index) => {
25✔
1100
      this.props.visStateActions.onLayerHover(data, index);
×
1101
    }, DEBOUNCE_MOUSE_MOVE_PROPAGATE);
1102

1103
    _onMouseMoveDebounced = debounce((event, viewport) => {
25✔
1104
      this.props.visStateActions.onMouseMove(normalizeEvent(event, viewport));
×
1105
    }, DEBOUNCE_MOUSE_MOVE_PROPAGATE);
1106

1107
    _onLayerLoadingStateChange = debounce(() => {
25✔
1108
      // trigger loading indicator update without any change to update UI
1109
      this.props.visStateActions.setLoadingIndicator({change: 0});
×
1110
    }, DEBOUNCE_LOADING_STATE_PROPAGATE);
1111

1112
    _handleToggleLayerVisibility = (layer: Layer) => {
25✔
1113
      const {visStateActions} = this.props;
×
1114
      visStateActions.layerConfigChange(layer, {isVisible: !layer.config.isVisible});
×
1115
    };
1116

1117
    _toggleMapControl = panelId => {
25✔
1118
      const {index, uiStateActions} = this.props;
2✔
1119

1120
      uiStateActions.toggleMapControl(panelId, Number(index));
2✔
1121
    };
1122

1123
    /* eslint-disable complexity */
1124
    _renderMap() {
1125
      const {
1126
        visState,
1127
        mapState,
1128
        mapStyle,
1129
        mapStateActions,
1130
        MapComponent = Map,
×
1131
        mapboxApiAccessToken,
1132
        // mapboxApiUrl,
1133
        mapControls,
1134
        isExport,
1135
        locale,
1136
        uiStateActions,
1137
        visStateActions,
1138
        index,
1139
        primary,
1140
        bottomMapContainerProps,
1141
        topMapContainerProps,
1142
        theme,
1143
        datasetAttributions = [],
×
1144
        attributionLogos = [],
×
1145
        containerId = 0,
13✔
1146
        isLoadingIndicatorVisible,
1147
        activeSidePanel,
1148
        sidePanelWidth
1149
      } = this.props;
29✔
1150

1151
      const {layers, datasets, editor, interactionConfig} = visState;
29✔
1152

1153
      const layersToRender = this.layersToRenderSelector(this.props);
29✔
1154
      const layersForDeck = this.layersForDeckSelector(this.props);
29✔
1155

1156
      // Current style can be a custom style, from which we pull the mapbox API acccess token
1157
      const currentStyle = mapStyle.mapStyles?.[mapStyle.styleType];
29✔
1158
      const baseMapLibraryName = getBaseMapLibrary(currentStyle);
29✔
1159
      const baseMapLibraryConfig =
1160
        getApplicationConfig().baseMapLibraryConfig?.[baseMapLibraryName];
29✔
1161

1162
      const internalViewState = this.context?.getInternalViewState(index);
29✔
1163
      const mapProps = {
29✔
1164
        ...internalViewState,
1165
        preserveDrawingBuffer: this.props.isExport ?? false,
54✔
1166
        mapboxAccessToken: currentStyle?.accessToken || mapboxApiAccessToken,
58✔
1167
        // baseApiUrl: mapboxApiUrl,
1168
        mapLib: baseMapLibraryConfig.getMapLib(),
1169
        transformRequest:
1170
          this.props.transformRequest ||
58✔
1171
          transformRequest(currentStyle?.accessToken || mapboxApiAccessToken)
58✔
1172
      };
1173

1174
      const hasGeocoderLayer = Boolean(layers.find(l => l.id === GEOCODER_LAYER_ID));
29✔
1175
      const isSplit = Boolean(mapState.isSplit);
29✔
1176

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

1235
          {deck}
1236
          {this._renderMapboxOverlays()}
1237
          <Editor
1238
            index={index || 0}
53✔
1239
            datasets={datasets}
1240
            editor={editor}
1241
            filters={this.polygonFiltersSelector(this.props)}
1242
            layers={layers}
1243
            onDeleteFeature={visStateActions.deleteFeature}
1244
            onSelect={visStateActions.setSelectedFeature}
1245
            onTogglePolygonFilter={visStateActions.setPolygonFilterLayer}
1246
            onSetEditorMode={visStateActions.setEditorMode}
1247
            style={{
1248
              pointerEvents: 'all',
1249
              position: 'absolute',
1250
              display: editor.visible ? 'block' : 'none'
29!
1251
            }}
1252
          />
1253
          {this.props.children}
1254
          {mapStyle.topMapStyle ? (
29!
1255
            <MapComponent
1256
              key={`top-${baseMapLibraryName}`}
1257
              viewState={internalViewState}
1258
              mapStyle={mapStyle.topMapStyle}
1259
              style={MAP_STYLE.top}
1260
              mapboxAccessToken={mapProps.mapboxAccessToken}
1261
              transformRequest={mapProps.transformRequest}
1262
              mapLib={baseMapLibraryConfig.getMapLib()}
1263
              {...topMapContainerProps}
1264
            />
1265
          ) : null}
1266

1267
          {hasGeocoderLayer
29!
1268
            ? this._renderDeckOverlay(
1269
                {[GEOCODER_LAYER_ID]: hasGeocoderLayer},
1270
                {primaryMap: false, isInteractive: false}
1271
              )
1272
            : null}
1273
          {this._renderMapPopover()}
1274
          {!isExport && primary !== isSplit ? (
83✔
1275
            <LoadingIndicator
1276
              isVisible={Boolean(isLoadingIndicatorVisible || this.anyActiveLayerLoading)}
34✔
1277
              activeSidePanel={Boolean(activeSidePanel)}
1278
              sidePanelWidth={sidePanelWidth}
1279
            />
1280
          ) : null}
1281
          {this.props.primary ? (
29✔
1282
            <Attribution
1283
              showBaseMapLibLogo={this.state.showBaseMapAttribution}
1284
              showOsmBasemapAttribution={true}
1285
              datasetAttributions={datasetAttributions}
1286
              baseMapLibraryConfig={baseMapLibraryConfig}
1287
            />
1288
          ) : null}
1289
          {this.props.primary ? (
29✔
1290
            <AttributionLogos logos={attributionLogos} />
1291
          ) : null}
1292
        </>
1293
      );
1294
    }
1295

29✔
1296
    render() {
29✔
1297
      const {visState, mapStyle} = this.props;
29!
1298
      const mapContent = this._renderMap();
1299
      if (!mapContent) {
UNCOV
1300
        // mapContent can be null if onDeckRender returns null
×
1301
        // in this case we don't want to render the map
1302
        return null;
1303
      }
29✔
1304

29✔
1305
      const currentStyle = mapStyle.mapStyles?.[mapStyle.styleType];
1306
      const baseMapLibraryName = getBaseMapLibrary(currentStyle);
29✔
1307
      const baseMapLibraryConfig =
1308
        getApplicationConfig().baseMapLibraryConfig?.[baseMapLibraryName];
29✔
1309

1310
      return (
1311
        <StyledMap
UNCOV
1312
          ref={this._ref}
×
1313
          style={this.styleSelector(this.props)}
1314
          onContextMenu={event => event.preventDefault()}
1315
          $mixBlendMode={visState.overlayBlending}
1316
          $mapLibCssClass={baseMapLibraryConfig.mapLibCssClass}
1317
        >
1318
          {mapContent}
1319
        </StyledMap>
1320
      );
1321
    }
1322
  }
14✔
1323

1324
  return withTheme(MapContainer);
1325
}
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