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

keplergl / kepler.gl / 24408196683

14 Apr 2026 03:35PM UTC coverage: 59.861% (-0.1%) from 59.991%
24408196683

push

github

web-flow
fix: updates to attribution logic for tiled layers (#3375)

* fix: updates to attribution logic for tiled layers

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

* lint

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

* fix attribution selector

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

* enabled >

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

* fix version

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

---------

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

6736 of 13441 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.65
/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
          {...(logo.url ? {target: '_blank', rel: 'noopener noreferrer'} : {})}
×
337
          $enabled={Boolean(logo.url)}
338
          style={logo.bottom ? {marginBottom: logo.bottom} : undefined}
×
339
        >
340
          <img src={logo.logoUrl} style={{height: logo.height || 12}} alt={logo.title} />
×
341
        </StyledLogoLink>
342
      ))}
343
    </StyledAttributionLogoContainer>
344
  );
345
};
346

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

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

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

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

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

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

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

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

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

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

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

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

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

418
    private anyActiveLayerLoading = false;
25✔
419

420
    static contextType = MapViewStateContext;
14✔
421

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

723
    /* component render functions */
724

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

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

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

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

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

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

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

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

826
    /* eslint-enable complexity */
827

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1067
      this.previousLayers = mapboxLayers;
×
1068
    }
1069

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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