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

keplergl / kepler.gl / 25295226964

04 May 2026 12:28AM UTC coverage: 58.931% (-0.1%) from 59.067%
25295226964

push

github

web-flow
fix: fix for open streat map attribution (#3411)

* fix: fix for open streat map attribution

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

* follow ups

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

---------

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

6939 of 14146 branches covered (49.05%)

Branch coverage included in aggregate %.

6 of 63 new or added lines in 2 files covered. (9.52%)

133 existing lines in 2 files now uncovered.

14294 of 21884 relevant lines covered (65.32%)

79.62 hits per line

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

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

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

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

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

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

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

71
// default-settings
72
import {
73
  FILTER_TYPES,
74
  GEOCODER_LAYER_ID,
75
  THROTTLE_NOTIFICATION_TIME,
76
  DEFAULT_PICKING_RADIUS,
77
  NO_MAP_ID,
78
  EMPTY_MAPBOX_STYLE,
79
  MAPBOX_MAX_PITCH,
80
  MAP_LIB_OPTIONS
81
} from '@kepler.gl/constants';
82

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

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

101
import LoadingIndicator from './loading-indicator';
102

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

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

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

128
const LOCALE_CODES_ARRAY = Object.keys(LOCALE_CODES);
7✔
129

130
interface StyledMapContainerProps {
131
  $mixBlendMode?: string;
132
  $mapLibCssClass: string;
133
}
134

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

146
const MAPBOXGL_STYLE_UPDATE = 'style.load';
7✔
147
const MAPBOXGL_RENDER = 'render';
7✔
148

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

312
  return memoizedComponents;
28✔
313
};
314

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

327
const StyledLogoLink = styled.a<{$enabled: boolean}>`
7✔
328
  cursor: ${props => (props.$enabled ? 'pointer' : 'default')};
×
329
  display: flex;
330
  align-items: flex-end;
331
`;
332

333
type AttributionLogosProps = {
334
  logos: AttributionWithStyle[];
335
  activeSidePanel?: boolean;
336
  sidePanelWidth?: number;
337
};
338

339
const LOGO_LEFT_ADJUSTMENT = 3;
7✔
340

341
export const AttributionLogos: React.FC<AttributionLogosProps> = ({
7✔
342
  logos,
343
  activeSidePanel,
344
  sidePanelWidth
345
}) => {
346
  const theme = useTheme() as any;
28✔
347
  const left =
348
    (activeSidePanel ? (sidePanelWidth || 0) + LOGO_LEFT_ADJUSTMENT : 0) +
28!
349
    theme.sidePanel.margin.left;
350

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

369
MapContainerFactory.deps = [MapPopoverFactory, MapControlFactory, EditorFactory];
7✔
370

371
type MapboxStyle = string | object | undefined;
372
type PropSelector<R> = Selector<MapContainerProps, R>;
373

374
export interface MapContainerProps {
375
  visState: VisState;
376
  mapState: MapState;
377
  mapControls: MapControls;
378
  mapStyle: {bottomMapStyle?: MapboxStyle; topMapStyle?: MapboxStyle} & MapStyle;
379
  mapboxApiAccessToken: string;
380
  mapboxApiUrl: string;
381
  visStateActions: typeof VisStateActions;
382
  mapStateActions: typeof MapStateActions;
383
  uiStateActions: typeof UIStateActions;
384

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

397
  isLoadingIndicatorVisible?: boolean;
398
  activeSidePanel: string | null;
399
  sidePanelWidth?: number;
400

401
  locale?: any;
402
  theme?: any;
403
  editor?: any;
404
  MapComponent?: typeof MapboxLegacyMap | typeof MaplibreMap;
405
  deckGlProps?: any;
406
  onDeckInitialized?: (a: any, b: any) => void;
407
  onViewStateChange?: (viewport: Viewport) => void;
408

409
  topMapContainerProps: any;
410
  bottomMapContainerProps: any;
411
  transformRequest?: (url: string, resourceType?: string) => {url: string};
412

413
  /** Pass `false` to disable the remote RTL text plugin, or a URL string to self-host it. */
414
  RTLTextPlugin?: string | false;
415

416
  datasetAttributions?: DatasetAttribution[];
417
  attributionLogos?: AttributionWithStyle[];
418

419
  generateMapboxLayers?: typeof generateMapboxLayers;
420
  generateDeckGLLayers?: typeof computeDeckLayers;
421

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

424
  children?: React.ReactNode;
425
  deckRenderCallbacks?: {
426
    onDeckLoad?: () => void;
427
    onDeckRender?: (deckProps: Record<string, unknown>) => Record<string, unknown> | null;
428
    onDeckAfterRender?: (deckProps: Record<string, unknown>) => any;
429
  };
430

431
  // Optional: override legend header logo in map controls (used by image export)
432
  logoComponent?: React.FC | React.ReactNode;
433
}
434

435
export default function MapContainerFactory(
436
  MapPopover: ReturnType<typeof MapPopoverFactory>,
437
  MapControl: ReturnType<typeof MapControlFactory>,
438
  Editor: ReturnType<typeof EditorFactory>
439
): React.ComponentType<MapContainerProps> {
440
  class MapContainer extends Component<MapContainerProps> {
441
    displayName = 'MapContainer';
25✔
442

443
    private anyActiveLayerLoading = false;
25✔
444

445
    static contextType = MapViewStateContext;
14✔
446

447
    declare context: React.ContextType<typeof MapViewStateContext>;
448

449
    static defaultProps = {
14✔
450
      deckGlProps: {},
451
      index: 0,
452
      primary: true
453
    };
454

455
    constructor(props) {
456
      super(props);
25✔
457
      patchDeckRendererForPostProcessing();
25✔
458
    }
459

460
    state = {
25✔
461
      showBaseMapAttribution: true,
462
      showOsmAttribution: false
463
    };
464

465
    componentDidMount() {
466
      if (!this._ref.current) {
25!
467
        return;
×
468
      }
469
      observeDimensions(this._ref.current, this._handleResize);
25✔
470
    }
471

472
    componentWillUnmount() {
473
      if (this._map) {
2!
NEW
474
        this._map.off(MAPBOXGL_STYLE_UPDATE, this._onMapboxStyleUpdate);
×
NEW
475
        this._map.off(MAPBOXGL_RENDER, this._onMapRender);
×
NEW
476
        this._removeOsmSourceDataListener();
×
477
      }
478
      if (!this._ref.current) {
2!
479
        return;
×
480
      }
481
      unobserveDimensions(this._ref.current);
2✔
482
    }
483

484
    componentDidUpdate(prevProps) {
485
      if (prevProps.mapStyle.styleType !== this.props.mapStyle.styleType) {
4!
NEW
486
        this._removeOsmSourceDataListener();
×
NEW
487
        if (this.props.mapStyle.styleType === NO_MAP_ID) {
×
NEW
488
          this.setState({
×
489
            showBaseMapAttribution: false,
490
            showOsmAttribution: false
491
          });
492
        } else {
NEW
493
          this._updateAttribution();
×
494
        }
495
      }
496
    }
497

498
    _deck: any = null;
25✔
499
    _map: GetMapRef | null = null;
25✔
500
    _ref = createRef<HTMLDivElement>();
25✔
501
    _deckGLErrorsElapsed: {[id: string]: number} = {};
25✔
502
    _osmSourceDataListener: ((e: any) => void) | null = null;
25✔
503

504
    _onMapRender = () => {
25✔
NEW
505
      if (typeof this.props.onMapRender === 'function') {
×
NEW
506
        this.props.onMapRender(this._map);
×
507
      }
508
    };
509

510
    previousLayers = {
25✔
511
      // [layers.id]: mapboxLayerConfig
512
    };
513

514
    _handleResize = dimensions => {
25✔
515
      const {primary, index} = this.props;
×
516
      if (primary) {
×
517
        const {mapStateActions} = this.props;
×
518
        if (dimensions && dimensions.width > 0 && dimensions.height > 0) {
×
519
          mapStateActions.updateMap(dimensions, index);
×
520
        }
521
      }
522
    };
523

524
    layersSelector: PropSelector<VisState['layers']> = props => props.visState.layers;
56✔
525
    layerDataSelector: PropSelector<VisState['layers']> = props => props.visState.layerData;
56✔
526
    splitMapSelector: PropSelector<SplitMap[]> = props => props.visState.splitMaps;
28✔
527
    splitMapIndexSelector: PropSelector<number | undefined> = props => props.index;
28✔
528
    mapLayersSelector: PropSelector<SplitMapLayers | null | undefined> = createSelector(
25✔
529
      this.splitMapSelector,
530
      this.splitMapIndexSelector,
531
      getMapLayersFromSplitMaps
532
    );
533
    layerOrderSelector: PropSelector<VisState['layerOrder']> = props => props.visState.layerOrder;
25✔
534
    layersToRenderSelector: PropSelector<LayersToRender> = createSelector(
25✔
535
      this.layersSelector,
536
      this.layerDataSelector,
537
      this.mapLayersSelector,
538
      prepareLayersToRender
539
    );
540
    layersForDeckSelector = createSelector(
25✔
541
      this.layersSelector,
542
      this.layerDataSelector,
543
      prepareLayersForDeck
544
    );
545
    filtersSelector = props => props.visState.filters;
28✔
546
    polygonFiltersSelector = createSelector(this.filtersSelector, filters =>
25✔
547
      filters.filter(f => f.type === FILTER_TYPES.polygon && f.enabled !== false)
26!
548
    );
549
    featuresSelector = props => props.visState.editor.features;
28✔
550
    selectedFeatureSelector = props => props.visState.editor.selectedFeature;
28✔
551
    featureCollectionSelector = createSelector(
25✔
552
      this.polygonFiltersSelector,
553
      this.featuresSelector,
554
      (polygonFilters, features) => ({
26✔
555
        type: 'FeatureCollection',
556
        features: features.concat(polygonFilters.map(f => f.value))
×
557
      })
558
    );
559
    // @ts-ignore - No overload matches this call
560
    selectedPolygonIndexSelector = createSelector(
25✔
561
      this.featureCollectionSelector,
562
      this.selectedFeatureSelector,
563
      (collection, selectedFeature) =>
564
        collection.features.findIndex(f => f.id === selectedFeature?.id)
26✔
565
    );
566
    selectedFeatureIndexArraySelector = createSelector(
25✔
567
      (value: number) => value,
25✔
568
      value => {
569
        return value < 0 ? [] : [value];
25!
570
      }
571
    );
572

573
    generateMapboxLayerMethodSelector = props => props.generateMapboxLayers ?? generateMapboxLayers;
25!
574

575
    mapboxLayersSelector = createSelector(
25✔
576
      this.layersSelector,
577
      this.layerDataSelector,
578
      this.layerOrderSelector,
579
      this.layersToRenderSelector,
580
      this.generateMapboxLayerMethodSelector,
581
      (layer, layerData, layerOrder, layersToRender, generateMapboxLayerMethod) =>
582
        generateMapboxLayerMethod(layer, layerData, layerOrder, layersToRender)
×
583
    );
584

585
    // merge in a background-color style if the basemap choice is NO_MAP_ID
586
    // used by <StyledMap> inline style prop
587
    mapStyleTypeSelector = props => props.mapStyle.styleType;
28✔
588
    mapStyleBackgroundColorSelector = props => props.mapStyle.backgroundColor;
28✔
589
    styleSelector = createSelector(
25✔
590
      this.mapStyleTypeSelector,
591
      this.mapStyleBackgroundColorSelector,
592
      (styleType, backgroundColor) => ({
26✔
593
        ...MAP_STYLE.container,
594
        ...(styleType === NO_MAP_ID ? {backgroundColor: rgbToHex(backgroundColor)} : {})
26!
595
      })
596
    );
597

598
    /* component private functions */
599
    _onCloseMapPopover = () => {
25✔
600
      this.props.visStateActions.onLayerClick(null);
×
601
    };
602

603
    _onLayerHover = (_idx: number, info: PickingInfo<any> | null) => {
25✔
604
      this.props.visStateActions.onLayerHover(info, this.props.index);
×
605
    };
606

607
    _onLayerSetDomain = (
25✔
608
      idx: number,
609
      value: number[] | {domain: VisualChannelDomain; aggregatedBins: Record<number, AggregatedBin>}
610
    ) => {
611
      // deck.gl 9 native aggregation layers (Grid, Hexagon) pass
612
      // {domain, aggregatedBins} via our ScaleEnhanced* overrides,
613
      // while ClusterLayer's CPUAggregator also passes {domain, aggregatedBins}.
614
      // Plain [min, max] is a fallback if the override is bypassed.
615
      const config = Array.isArray(value)
×
616
        ? {colorDomain: value as VisualChannelDomain}
617
        : {colorDomain: value.domain, aggregatedBins: value.aggregatedBins};
618

619
      const layer = this.props.visState.layers[idx];
×
620
      if (!layer) return;
×
621

622
      this.props.visStateActions.layerConfigChange(layer, config as Partial<LayerBaseConfig>);
×
623
    };
624

625
    _onRedrawNeeded = (_idx: number) => {
25✔
626
      // updateMapUpdater always returns a new state object reference, which triggers re-render
627
      const {mapStateActions, index} = this.props;
×
628
      mapStateActions.updateMap({}, index);
×
629
    };
630

631
    _onFitBounds = (_idx: number, bounds: [number, number, number, number]) => {
25✔
632
      this.props.mapStateActions.fitBounds(bounds);
×
633
    };
634

635
    _onLayerFilteredItemsChange = (idx, event) => {
25✔
636
      this.props.visStateActions.layerFilteredItemsChange(this.props.visState.layers[idx], event);
×
637
    };
638

639
    _onWMSFeatureInfo = (
25✔
640
      idx: number,
641
      data: {
642
        featureInfo: Array<{name: string; value: string}> | string | null;
643
        coordinate?: [number, number] | null;
644
      }
645
    ) => {
646
      this.props.visStateActions.wmsFeatureInfo(
×
647
        this.props.visState.layers[idx],
648
        data.featureInfo,
649
        data.coordinate
650
      );
651
    };
652

653
    _handleMapToggleLayer = layerId => {
25✔
654
      const {index: mapIndex = 0, visStateActions} = this.props;
1!
655
      visStateActions.toggleLayerForMap(mapIndex, layerId);
1✔
656
    };
657

658
    _onMapboxStyleUpdate = update => {
25✔
659
      // force refresh mapboxgl layers
660
      this.previousLayers = {};
×
661
      this._updateMapboxLayers();
×
662

NEW
663
      this._updateAttribution(update);
×
664

NEW
665
      if (typeof this.props.onMapStyleLoaded === 'function') {
×
NEW
666
        this.props.onMapStyleLoaded(this._map);
×
667
      }
668
    };
669

670
    _updateAttribution = (update?: any) => {
25✔
NEW
671
      this._removeOsmSourceDataListener();
×
672

NEW
673
      let styleObj = update?.style || null;
×
NEW
674
      if (!styleObj && this._map) {
×
NEW
675
        try {
×
NEW
676
          const rawStyle = this._map.isStyleLoaded?.() ? this._map.getStyle?.() : null;
×
NEW
677
          if (rawStyle) {
×
NEW
678
            styleObj = {stylesheet: rawStyle};
×
679
          }
680
        } catch {
681
          // map style not ready yet
682
        }
683
      }
NEW
684
      const usesMapbox = styleObj ? isStyleUsingMapboxTiles(styleObj) : false;
×
NEW
685
      const usesOsm = styleObj ? isStyleUsingOpenStreetMapTiles(styleObj) : false;
×
686

NEW
687
      if (usesMapbox || usesOsm) {
×
NEW
688
        this.setState({
×
689
          showBaseMapAttribution: true,
690
          showOsmAttribution: usesOsm
691
        });
692
      } else {
UNCOV
693
        this.setState({
×
694
          showBaseMapAttribution: false,
695
          showOsmAttribution: false
696
        });
NEW
697
        this._checkOsmAttributionOnSourceLoad();
×
698
      }
699
    };
700

701
    _removeOsmSourceDataListener = () => {
25✔
NEW
702
      if (this._osmSourceDataListener && this._map) {
×
NEW
703
        this._map.off('sourcedata', this._osmSourceDataListener);
×
NEW
704
        this._osmSourceDataListener = null;
×
705
      }
706
    };
707

708
    _checkOsmAttributionOnSourceLoad = () => {
25✔
NEW
709
      if (!this._map) return;
×
NEW
710
      this._removeOsmSourceDataListener();
×
NEW
711
      let attempts = 0;
×
NEW
712
      const MAX_ATTEMPTS = 50;
×
NEW
713
      const onSourceData = (e: any) => {
×
NEW
714
        if (!e?.isSourceLoaded) return;
×
NEW
715
        attempts++;
×
NEW
716
        if (mapHasOpenStreetMapAttribution(this._map)) {
×
NEW
717
          this._removeOsmSourceDataListener();
×
NEW
718
          this.setState({
×
719
            showBaseMapAttribution: true,
720
            showOsmAttribution: true
721
          });
NEW
722
        } else if (attempts >= MAX_ATTEMPTS) {
×
NEW
723
          this._removeOsmSourceDataListener();
×
724
        }
725
      };
NEW
726
      this._osmSourceDataListener = onSourceData;
×
NEW
727
      this._map.on('sourcedata', onSourceData);
×
728
    };
729

730
    _setMapRef = mapRef => {
25✔
731
      // Handle change of the map library
732
      if (this._map && mapRef) {
×
733
        const map = mapRef.getMap();
×
734
        if (map && this._map !== map) {
×
NEW
735
          this._map.off(MAPBOXGL_STYLE_UPDATE, this._onMapboxStyleUpdate);
×
NEW
736
          this._map.off(MAPBOXGL_RENDER, this._onMapRender);
×
NEW
737
          this._removeOsmSourceDataListener();
×
UNCOV
738
          this._map = null;
×
739
        }
740
      }
741

742
      if (!this._map && mapRef) {
×
743
        this._map = mapRef.getMap();
×
744
        // i noticed in certain context we don't access the actual map element
745
        if (!this._map) {
×
746
          return;
×
747
        }
748
        // bind mapboxgl event listener
749
        this._map.on(MAPBOXGL_STYLE_UPDATE, this._onMapboxStyleUpdate);
×
NEW
750
        this._map.on(MAPBOXGL_RENDER, this._onMapRender);
×
751
      }
752

753
      if (this.props.getMapboxRef) {
×
754
        // The parent component can gain access to our MapboxGlMap by
755
        // providing this callback. Note that 'mapbox' will be null when the
756
        // ref is unset (e.g. when a split map is closed).
757
        this.props.getMapboxRef(mapRef, this.props.index);
×
758
      }
759
    };
760

761
    _onDeckInitialized(device) {
762
      if (this.props.onDeckInitialized) {
×
763
        this.props.onDeckInitialized(this._deck, device);
×
764
      }
765
    }
766

767
    /**
768
     * 1) Allow effects only for the first view.
769
     * 2) Prevent effect:preRender call without valid generated viewports.
770
     * @param viewIndex View index.
771
     * @returns Returns true if effects can be used.
772
     */
773
    _isOKToRenderEffects(viewIndex?: number): boolean {
774
      return !viewIndex && Boolean(this._deck?.viewManager?._viewports?.length);
29✔
775
    }
776

777
    _onBeforeRender = () => {
25✔
778
      // no-op
779
    };
780

781
    _onDeckError = (error, layer) => {
25✔
782
      const errorMessage = error?.message || 'unknown-error';
×
783
      const layerMessage = layer?.id ? ` in ${layer.id} layer` : '';
×
784
      const errorMessageFull =
785
        errorMessage === 'WebGL context is lost'
×
786
          ? '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.'
787
          : `An error in deck.gl: ${errorMessage}${layerMessage}.`;
788

789
      // Throttle error notifications, as React doesn't like too many state changes from here.
790
      const lastShown = this._deckGLErrorsElapsed[errorMessageFull];
×
791
      if (!lastShown || lastShown < Date.now() - THROTTLE_NOTIFICATION_TIME) {
×
792
        this._deckGLErrorsElapsed[errorMessageFull] = Date.now();
×
793

794
        // Mark layer as invalid
795
        let extraLayerMessage = '';
×
796
        const {visStateActions} = this.props;
×
797
        if (layer) {
×
798
          let topMostLayer = layer;
×
799
          while (topMostLayer.parent) {
×
800
            topMostLayer = topMostLayer.parent;
×
801
          }
802
          if (topMostLayer.props?.id) {
×
803
            visStateActions.layerSetIsValid(topMostLayer, false);
×
804
            extraLayerMessage = 'The layer has been disabled and highlighted.';
×
805
          }
806
        }
807

808
        // Create new error notification or update existing one with same id.
809
        // Update is required to preserve the order of notifications as they probably are going to "jump" based on order of errors.
810
        const {uiStateActions} = this.props;
×
811
        uiStateActions.addNotification(
×
812
          errorNotification({
813
            message: `${errorMessageFull} ${extraLayerMessage}`,
814
            id: errorMessageFull // treat the error message as id
815
          })
816
        );
817
      }
818
    };
819

820
    /* component render functions */
821

822
    /* eslint-disable complexity */
823
    _renderMapPopover() {
824
      // this check is for limiting the display of the `<MapPopover>` to the `<MapContainer>` the user is interacting with
825
      // the DeckGL onHover event handler adds a `mapIndex` property which is available in the `hoverInfo` object of `visState`
826
      if (this.props.index !== this.props.visState.hoverInfo?.mapIndex) {
29!
827
        return null;
29✔
828
      }
829

830
      // TODO: move this into reducer so it can be tested
831
      const {
832
        mapState,
833
        visState: {
834
          hoverInfo,
835
          clicked,
836
          datasets,
837
          interactionConfig,
838
          animationConfig,
839
          layers,
840
          mousePos: {mousePosition, coordinate, pinned}
841
        }
842
      } = this.props;
×
843
      const layersToRender = this.layersToRenderSelector(this.props);
×
844

845
      if (!mousePosition || !interactionConfig.tooltip) {
×
846
        return null;
×
847
      }
848

849
      const layerHoverProp = getLayerHoverProp({
×
850
        animationConfig,
851
        interactionConfig,
852
        hoverInfo,
853
        layers,
854
        layersToRender,
855
        datasets
856
      });
857

858
      const compareMode = interactionConfig.tooltip.config
×
859
        ? interactionConfig.tooltip.config.compareMode
860
        : false;
861

862
      let pinnedPosition = {x: 0, y: 0};
×
863
      let layerPinnedProp: LayerHoverProp | null = null;
×
864
      if (pinned || clicked) {
×
865
        // project lnglat to screen so that tooltip follows the object on zoom
866
        const viewport = getViewportFromMapState(mapState);
×
867
        const lngLat = clicked ? clicked.coordinate : pinned.coordinate;
×
868
        pinnedPosition = this._getHoverXY(viewport, lngLat);
×
869
        layerPinnedProp = getLayerHoverProp({
×
870
          animationConfig,
871
          interactionConfig,
872
          hoverInfo: clicked,
873
          layers,
874
          layersToRender,
875
          datasets
876
        });
877
        if (layerHoverProp && layerPinnedProp) {
×
878
          layerHoverProp.primaryData = layerPinnedProp.data;
×
879
          layerHoverProp.compareType = interactionConfig.tooltip.config.compareType;
×
880
        }
881
      }
882

883
      const commonProp = {
×
884
        onClose: this._onCloseMapPopover,
885
        zoom: mapState.zoom,
886
        container: this._deck ? this._deck.canvas : undefined
×
887
      };
888

889
      return (
×
890
        <ErrorBoundary>
891
          {layerPinnedProp && (
×
892
            <MapPopover
893
              {...pinnedPosition}
894
              {...commonProp}
895
              layerHoverProp={layerPinnedProp}
896
              coordinate={interactionConfig.coordinate.enabled && (pinned || {}).coordinate}
×
897
              frozen={true}
898
              isBase={compareMode}
899
              onSetFeatures={this.props.visStateActions.setFeatures}
900
              setSelectedFeature={this.props.visStateActions.setSelectedFeature}
901
              // @ts-ignore Argument of type 'Readonly<MapContainerProps>' is not assignable to parameter of type 'never'
902
              featureCollection={this.featureCollectionSelector(this.props)}
903
            />
904
          )}
905
          {layerHoverProp && (!layerPinnedProp || compareMode) && (
×
906
            <MapPopover
907
              x={mousePosition[0]}
908
              y={mousePosition[1]}
909
              {...commonProp}
910
              layerHoverProp={layerHoverProp}
911
              frozen={false}
912
              coordinate={interactionConfig.coordinate.enabled && coordinate}
×
913
              onSetFeatures={this.props.visStateActions.setFeatures}
914
              setSelectedFeature={this.props.visStateActions.setSelectedFeature}
915
              // @ts-ignore Argument of type 'Readonly<MapContainerProps>' is not assignable to parameter of type 'never'
916
              featureCollection={this.featureCollectionSelector(this.props)}
917
            />
918
          )}
919
        </ErrorBoundary>
920
      );
921
    }
922

923
    /* eslint-enable complexity */
924

925
    _getHoverXY(viewport, lngLat) {
926
      const screenCoord = !viewport || !lngLat ? null : viewport.project(lngLat);
×
927
      return screenCoord && {x: screenCoord[0], y: screenCoord[1]};
×
928
    }
929

930
    _renderDeckOverlay(
931
      layersForDeck,
932
      options: {primaryMap: boolean; isInteractive?: boolean; children?: React.ReactNode} = {
×
933
        primaryMap: false
934
      }
935
    ) {
936
      const {
937
        mapStyle,
938
        visState,
939
        mapState,
940
        visStateActions,
941
        mapboxApiAccessToken,
942
        mapboxApiUrl,
943
        deckGlProps,
944
        index,
945
        mapControls,
946
        deckRenderCallbacks,
947
        theme,
948
        generateDeckGLLayers,
949
        onMouseMove
950
      } = this.props;
29✔
951

952
      const {hoverInfo, editor} = visState;
29✔
953
      const {primaryMap, isInteractive, children} = options;
29✔
954

955
      // disable double click zoom when editor is in any draw mode
956
      const {mapDraw} = mapControls;
29✔
957
      const {active: editorMenuActive = false} = mapDraw || {};
29✔
958
      const isEditorDrawingMode = EditorLayerUtils.isDrawingActive(editorMenuActive, editor.mode);
29✔
959

960
      const internalViewState = this.context?.getInternalViewState(index);
29✔
961
      const internalMapState = {...mapState, ...internalViewState};
29✔
962
      const viewport = getViewportFromMapState(internalMapState);
29✔
963

964
      const editorFeatureSelectedIndex = this.selectedPolygonIndexSelector(this.props);
29✔
965

966
      const {setFeatures, onLayerClick, setSelectedFeature} = visStateActions;
29✔
967

968
      const generateDeckGLLayersMethod = generateDeckGLLayers ?? computeDeckLayers;
29✔
969
      const deckGlLayers = generateDeckGLLayersMethod(
29✔
970
        {
971
          visState,
972
          mapState: internalMapState,
973
          mapStyle
974
        },
975
        {
976
          mapIndex: index,
977
          primaryMap,
978
          mapboxApiAccessToken,
979
          mapboxApiUrl,
980
          layersForDeck,
981
          editorInfo: primaryMap
29!
982
            ? {
983
                editor,
984
                editorMenuActive,
985
                onSetFeatures: setFeatures,
986
                setSelectedFeature,
987
                onApplyPolygonFilterAll: visStateActions.setPolygonFilterAllLayers,
988
                // @ts-ignore Argument of type 'Readonly<MapContainerProps>' is not assignable to parameter of type 'never'
989
                featureCollection: this.featureCollectionSelector(this.props),
990
                selectedFeatureIndexes: this.selectedFeatureIndexArraySelector(
991
                  // @ts-ignore Argument of type 'unknown' is not assignable to parameter of type 'number'.
992
                  editorFeatureSelectedIndex
993
                ),
994
                viewport
995
              }
996
            : undefined
997
        },
998
        {
999
          onLayerHover: this._onLayerHover,
1000
          onSetLayerDomain: this._onLayerSetDomain,
1001
          onFilteredItemsChange: this._onLayerFilteredItemsChange,
1002
          onWMSFeatureInfo: this._onWMSFeatureInfo,
1003
          onRedrawNeeded: this._onRedrawNeeded,
1004
          onFitBounds: this._onFitBounds
1005
        },
1006
        deckGlProps
1007
      );
1008

1009
      const extraDeckParams: {
1010
        getTooltip?: (info: any) => object | null;
1011
        getCursor?: ({isDragging}: {isDragging: boolean}) => string;
1012
      } = {};
29✔
1013
      if (primaryMap) {
29!
1014
        // Omit hover updates when the pointer position is invalid, ie. over UI overlays or
1015
        // outside the map container. In those cases x/y may be < 0
1016
        extraDeckParams.getTooltip = info => {
29✔
1017
          const x = Number(info?.x);
×
1018
          const y = Number(info?.y);
×
1019
          if (Number.isNaN(x) || Number.isNaN(y) || x < 0 || y < 0) return null;
×
1020

1021
          return EditorLayerUtils.getTooltip(info, {
×
1022
            editorMenuActive,
1023
            editor,
1024
            theme
1025
          });
1026
        };
1027

1028
        extraDeckParams.getCursor = ({isDragging}: {isDragging: boolean}) => {
29✔
1029
          const editorCursor = EditorLayerUtils.getCursor({
×
1030
            editorMenuActive,
1031
            editor,
1032
            hoverInfo
1033
          });
1034
          if (editorCursor) return editorCursor;
×
1035

1036
          if (isDragging) return 'grabbing';
×
1037
          if (hoverInfo?.layer) return 'pointer';
×
1038
          return 'grab';
×
1039
        };
1040
      }
1041

1042
      const effects = this._isOKToRenderEffects(index)
29!
1043
        ? computeDeckEffects({visState, mapState, isExport: this.props.isExport})
1044
        : [];
1045

1046
      const views = deckGlProps?.views ? deckGlProps?.views() : new MapView({farZMultiplier: 1.2});
29!
1047

1048
      let allDeckGlProps = {
29✔
1049
        ...deckGlProps,
1050
        pickingRadius: DEFAULT_PICKING_RADIUS,
1051
        views,
1052
        layers: deckGlLayers,
1053
        effects,
1054
        parameters: getLayerBlendingParameters(visState.layerBlending)
1055
      };
1056

1057
      if (typeof deckRenderCallbacks?.onDeckRender === 'function') {
29!
1058
        allDeckGlProps = deckRenderCallbacks.onDeckRender(allDeckGlProps);
×
1059
        if (!allDeckGlProps) {
×
1060
          // if onDeckRender returns null, do not render deck.gl
1061
          return null;
×
1062
        }
1063
      }
1064

1065
      return (
29✔
1066
        <div
1067
          {...(isInteractive
29!
1068
            ? {
1069
                onMouseMove: primaryMap
29!
1070
                  ? event => {
1071
                      onMouseMove?.(event);
×
1072
                      this._onMouseMoveDebounced(event, viewport);
×
1073
                    }
1074
                  : undefined
1075
              }
1076
            : {style: {pointerEvents: 'none'}})}
1077
        >
1078
          <DeckGL
1079
            id="default-deckgl-overlay"
1080
            onLoad={() => {
1081
              if (typeof deckRenderCallbacks?.onDeckLoad === 'function') {
×
1082
                deckRenderCallbacks.onDeckLoad();
×
1083
              }
1084
            }}
1085
            {...allDeckGlProps}
1086
            controller={
1087
              isInteractive
29!
1088
                ? {
1089
                    doubleClickZoom: !isEditorDrawingMode,
1090
                    dragRotate: this.props.mapState.dragRotate,
1091
                    maxPitch: this.props.mapState.maxPitch ?? getApplicationConfig().maxPitch
58✔
1092
                  }
1093
                : false
1094
            }
1095
            initialViewState={internalViewState}
1096
            onBeforeRender={this._onBeforeRender}
1097
            onViewStateChange={isInteractive ? this._onViewportChange : undefined}
29!
1098
            {...extraDeckParams}
1099
            onHover={
1100
              isInteractive
29!
1101
                ? data => {
1102
                    const res = EditorLayerUtils.onHover(data, {
×
1103
                      editorMenuActive,
1104
                      editor,
1105
                      hoverInfo
1106
                    });
1107
                    if (res) return;
×
1108

1109
                    this._onLayerHoverDebounced(data, index);
×
1110
                  }
1111
                : null
1112
            }
1113
            onClick={(data, event) => {
1114
              // @ts-ignore
1115
              normalizeEvent(event.srcEvent, viewport);
×
1116
              const res = EditorLayerUtils.onClick(data, event, {
×
1117
                editorMenuActive,
1118
                editor,
1119
                onLayerClick,
1120
                setSelectedFeature,
1121
                mapIndex: index
1122
              });
1123
              if (res) return;
×
1124

1125
              visStateActions.onLayerClick(data);
×
1126
            }}
1127
            onError={this._onDeckError}
1128
            ref={comp => {
1129
              // @ts-ignore
1130
              if (comp && comp.deck && !this._deck) {
35✔
1131
                // @ts-ignore
1132
                this._deck = comp.deck;
1✔
1133
              }
1134
            }}
1135
            onDeviceInitialized={device => this._onDeckInitialized(device)}
×
1136
            onAfterRender={() => {
1137
              if (typeof deckRenderCallbacks?.onDeckAfterRender === 'function') {
×
1138
                deckRenderCallbacks.onDeckAfterRender(allDeckGlProps);
×
1139
              }
1140

1141
              const anyActiveLayerLoading = areAnyDeckLayersLoading(allDeckGlProps.layers);
×
1142
              if (anyActiveLayerLoading !== this.anyActiveLayerLoading) {
×
1143
                this._onLayerLoadingStateChange();
×
1144
                this.anyActiveLayerLoading = anyActiveLayerLoading;
×
1145
              }
1146
            }}
1147
          >
1148
            {children}
1149
          </DeckGL>
1150
        </div>
1151
      );
1152
    }
1153

1154
    _updateMapboxLayers() {
1155
      const mapboxLayers = this.mapboxLayersSelector(this.props);
×
1156
      if (!Object.keys(mapboxLayers).length && !Object.keys(this.previousLayers).length) {
×
1157
        return;
×
1158
      }
1159

1160
      updateMapboxLayers(this._map, mapboxLayers, this.previousLayers);
×
1161

1162
      this.previousLayers = mapboxLayers;
×
1163
    }
1164

1165
    _renderMapboxOverlays() {
1166
      if (this._map && this._map.isStyleLoaded()) {
29!
1167
        this._updateMapboxLayers();
×
1168
      }
1169
    }
1170
    _onViewportChangePropagateDebounced = debounce(() => {
25✔
1171
      const viewState = this.context?.getInternalViewState(this.props.index);
×
1172
      onViewPortChange(
×
1173
        viewState,
1174
        this.props.mapStateActions.updateMap,
1175
        this.props.onViewStateChange,
1176
        this.props.primary,
1177
        this.props.index
1178
      );
1179
    }, DEBOUNCE_VIEWPORT_PROPAGATE);
1180

1181
    _onViewportChange = viewport => {
25✔
1182
      const {viewState} = viewport;
×
1183
      if (this.props.isExport) {
×
1184
        // Image export map shouldn't be interactive (otherwise this callback can
1185
        // lead to inadvertent changes to the state of the main map)
1186
        return;
×
1187
      }
1188
      const {setInternalViewState} = this.context;
×
1189
      setInternalViewState(viewState, this.props.index);
×
1190
      this._onViewportChangePropagateDebounced();
×
1191
    };
1192

1193
    _onLayerHoverDebounced = debounce((data, index) => {
25✔
1194
      this.props.visStateActions.onLayerHover(data, index);
×
1195
    }, DEBOUNCE_MOUSE_MOVE_PROPAGATE);
1196

1197
    _onMouseMoveDebounced = debounce((event, viewport) => {
25✔
1198
      this.props.visStateActions.onMouseMove(normalizeEvent(event, viewport));
×
1199
    }, DEBOUNCE_MOUSE_MOVE_PROPAGATE);
1200

1201
    _onLayerLoadingStateChange = debounce(() => {
25✔
1202
      // trigger loading indicator update without any change to update UI
1203
      this.props.visStateActions.setLoadingIndicator({change: 0});
×
1204
    }, DEBOUNCE_LOADING_STATE_PROPAGATE);
1205

1206
    _handleToggleLayerVisibility = (layer: Layer) => {
25✔
1207
      const {visStateActions} = this.props;
×
1208
      visStateActions.layerConfigChange(layer, {isVisible: !layer.config.isVisible});
×
1209
    };
1210

1211
    _toggleMapControl = panelId => {
25✔
1212
      const {index, uiStateActions} = this.props;
2✔
1213

1214
      uiStateActions.toggleMapControl(panelId, Number(index));
2✔
1215
    };
1216

1217
    /* eslint-disable complexity */
1218
    _renderMap() {
1219
      const {
1220
        visState,
1221
        mapState,
1222
        mapStyle,
1223
        mapStateActions,
1224
        mapboxApiAccessToken,
1225
        // mapboxApiUrl,
1226
        mapControls,
1227
        isExport,
1228
        locale,
1229
        uiStateActions,
1230
        visStateActions,
1231
        index,
1232
        primary,
1233
        bottomMapContainerProps,
1234
        topMapContainerProps,
1235
        theme,
1236
        datasetAttributions = [],
×
1237
        attributionLogos = [],
×
1238
        containerId = 0,
13✔
1239
        isLoadingIndicatorVisible,
1240
        activeSidePanel,
1241
        sidePanelWidth
1242
      } = this.props;
29✔
1243

1244
      const {layers, datasets, editor, interactionConfig} = visState;
29✔
1245

1246
      const layersToRender = this.layersToRenderSelector(this.props);
29✔
1247
      const layersForDeck = this.layersForDeckSelector(this.props);
29✔
1248

1249
      // Current style can be a custom style, from which we pull the mapbox API acccess token
1250
      const currentStyle = mapStyle.mapStyles?.[mapStyle.styleType];
29✔
1251
      const baseMapLibraryName = getBaseMapLibrary(currentStyle);
29✔
1252
      const baseMapLibraryConfig = getApplicationConfig().baseMapLibraryConfig[baseMapLibraryName];
29✔
1253

1254
      // Select the correct Map adapter based on the active base map library.
1255
      // Using the native adapter for each library avoids Transform API
1256
      // incompatibilities (e.g. mapbox-legacy's cloneTransform with MapLibre v5).
1257
      const ResolvedMapComponent =
1258
        this.props.MapComponent ??
29✔
1259
        (baseMapLibraryName === MAP_LIB_OPTIONS.MAPBOX ? MapboxLegacyMap : MaplibreMap);
29!
1260

1261
      const useMapboxAdapter = ResolvedMapComponent === MapboxLegacyMap;
29✔
1262

1263
      const internalViewState = this.context?.getInternalViewState(index);
29✔
1264
      const configMaxPitch = mapState.maxPitch ?? getApplicationConfig().maxPitch;
29✔
1265
      const effectiveMaxPitch = useMapboxAdapter
29!
1266
        ? Math.min(configMaxPitch, MAPBOX_MAX_PITCH)
1267
        : configMaxPitch;
1268
      const mapProps: Record<string, any> = {
29✔
1269
        ...internalViewState,
1270
        maxPitch: effectiveMaxPitch,
1271
        preserveDrawingBuffer: this.props.isExport ?? false,
54✔
1272
        mapboxAccessToken: currentStyle?.accessToken || mapboxApiAccessToken,
58✔
1273
        // baseApiUrl: mapboxApiUrl,
1274
        transformRequest:
1275
          this.props.transformRequest ||
58✔
1276
          transformRequest(currentStyle?.accessToken || mapboxApiAccessToken)
58✔
1277
      };
1278

1279
      if (this.props.RTLTextPlugin !== undefined) {
29!
1280
        mapProps.RTLTextPlugin = this.props.RTLTextPlugin;
×
1281
      }
1282

1283
      if (useMapboxAdapter) {
29!
1284
        const mapboxConfig = getApplicationConfig().baseMapLibraryConfig[MAP_LIB_OPTIONS.MAPBOX];
×
1285
        mapProps.mapLib = mapboxConfig.getMapLib();
×
1286
      }
1287

1288
      const hasGeocoderLayer = Boolean(layers.find(l => l.id === GEOCODER_LAYER_ID));
29✔
1289
      const isSplit = Boolean(mapState.isSplit);
29✔
1290

1291
      const deck = this._renderDeckOverlay(layersForDeck, {
29✔
1292
        primaryMap: true,
1293
        isInteractive: true,
1294
        children: (
1295
          <ResolvedMapComponent
1296
            key={`bottom-${baseMapLibraryName}`}
1297
            {...mapProps}
1298
            mapStyle={mapStyle.bottomMapStyle ?? EMPTY_MAPBOX_STYLE}
58✔
1299
            {...bottomMapContainerProps}
1300
            ref={this._setMapRef}
1301
          />
1302
        )
1303
      });
1304
      if (!deck) {
29!
1305
        // deckOverlay can be null if onDeckRender returns null
1306
        // in this case we don't want to render the map
1307
        return null;
×
1308
      }
1309
      return (
29✔
1310
        <>
1311
          <MapControl
1312
            mapState={mapState}
1313
            datasets={datasets}
1314
            availableLocales={LOCALE_CODES_ARRAY}
1315
            dragRotate={mapState.dragRotate}
1316
            isSplit={isSplit}
1317
            primary={Boolean(primary)}
1318
            isExport={isExport}
1319
            layers={layers}
1320
            layersToRender={layersToRender}
1321
            mapIndex={index || 0}
53✔
1322
            mapControls={mapControls}
1323
            readOnly={this.props.readOnly}
1324
            scale={mapState.scale || 1}
58✔
1325
            logoComponent={this.props.logoComponent}
1326
            top={
1327
              interactionConfig.geocoder && interactionConfig.geocoder.enabled
87!
1328
                ? theme.mapControlTop
1329
                : 0
1330
            }
1331
            editor={editor}
1332
            locale={locale}
1333
            onTogglePerspective={mapStateActions.togglePerspective}
1334
            onToggleSplitMap={mapStateActions.toggleSplitMap}
1335
            onMapToggleLayer={this._handleMapToggleLayer}
1336
            onToggleMapControl={this._toggleMapControl}
1337
            onToggleSplitMapViewport={mapStateActions.toggleSplitMapViewport}
1338
            onSetEditorMode={visStateActions.setEditorMode}
1339
            onSetLocale={uiStateActions.setLocale}
1340
            onToggleEditorVisibility={visStateActions.toggleEditorVisibility}
1341
            onLayerVisConfigChange={visStateActions.layerVisConfigChange}
1342
            onToggleLayerVisibility={this._handleToggleLayerVisibility}
1343
            mapHeight={mapState.height}
1344
            setMapControlSettings={uiStateActions.setMapControlSettings}
1345
            activeSidePanel={activeSidePanel}
1346
          />
1347
          {isSplitSelector(this.props) && <Droppable containerId={containerId} />}
38✔
1348

1349
          {deck}
1350
          {this._renderMapboxOverlays()}
1351
          <Editor
1352
            index={index || 0}
53✔
1353
            datasets={datasets}
1354
            editor={editor}
1355
            filters={this.polygonFiltersSelector(this.props)}
1356
            layers={layers}
1357
            onDeleteFeature={visStateActions.deleteFeature}
1358
            onSelect={visStateActions.setSelectedFeature}
1359
            onTogglePolygonFilter={visStateActions.setPolygonFilterLayer}
1360
            onSetEditorMode={visStateActions.setEditorMode}
1361
            style={{
1362
              pointerEvents: 'all',
1363
              position: 'absolute',
1364
              display: editor.visible ? 'block' : 'none'
29!
1365
            }}
1366
          />
1367
          {this.props.children}
1368
          {mapStyle.topMapStyle ? (
29!
1369
            <ResolvedMapComponent
1370
              key={`top-${baseMapLibraryName}`}
1371
              viewState={internalViewState}
1372
              maxPitch={effectiveMaxPitch}
1373
              mapStyle={mapStyle.topMapStyle}
1374
              style={MAP_STYLE.top}
1375
              mapboxAccessToken={mapProps.mapboxAccessToken}
1376
              transformRequest={mapProps.transformRequest}
1377
              {...(mapProps.RTLTextPlugin !== undefined
×
1378
                ? {RTLTextPlugin: mapProps.RTLTextPlugin}
1379
                : {})}
1380
              {...(useMapboxAdapter
×
1381
                ? {
1382
                    mapLib:
1383
                      getApplicationConfig().baseMapLibraryConfig[
1384
                        MAP_LIB_OPTIONS.MAPBOX
1385
                      ].getMapLib()
1386
                  }
1387
                : {})}
1388
              {...topMapContainerProps}
1389
            />
1390
          ) : null}
1391

1392
          {hasGeocoderLayer
29!
1393
            ? this._renderDeckOverlay(
1394
                {[GEOCODER_LAYER_ID]: hasGeocoderLayer},
1395
                {primaryMap: false, isInteractive: false}
1396
              )
1397
            : null}
1398
          {this._renderMapPopover()}
1399
          {!isExport && primary !== isSplit ? (
83✔
1400
            <LoadingIndicator
1401
              isVisible={Boolean(isLoadingIndicatorVisible || this.anyActiveLayerLoading)}
34✔
1402
              activeSidePanel={Boolean(activeSidePanel)}
1403
              sidePanelWidth={sidePanelWidth}
1404
              hasAttributionLogos={attributionLogos.length > 0}
1405
            />
1406
          ) : null}
1407
          {this.props.primary ? (
29✔
1408
            <Attribution
1409
              showBaseMapLibLogo={this.state.showBaseMapAttribution}
1410
              showOsmBasemapAttribution={this.state.showOsmAttribution}
1411
              datasetAttributions={datasetAttributions}
1412
              baseMapLibraryConfig={baseMapLibraryConfig}
1413
            />
1414
          ) : null}
1415
          {this.props.primary ? (
29✔
1416
            <AttributionLogos
1417
              logos={attributionLogos}
1418
              activeSidePanel={Boolean(activeSidePanel)}
1419
              sidePanelWidth={sidePanelWidth}
1420
            />
1421
          ) : null}
1422
        </>
1423
      );
1424
    }
1425

1426
    render() {
1427
      const {visState, mapStyle} = this.props;
29✔
1428
      const mapContent = this._renderMap();
29✔
1429
      if (!mapContent) {
29!
1430
        // mapContent can be null if onDeckRender returns null
1431
        // in this case we don't want to render the map
1432
        return null;
×
1433
      }
1434

1435
      const currentStyle = mapStyle.mapStyles?.[mapStyle.styleType];
29✔
1436
      const baseMapLibraryName = getBaseMapLibrary(currentStyle);
29✔
1437
      const baseMapLibraryConfig = getApplicationConfig().baseMapLibraryConfig[baseMapLibraryName];
29✔
1438

1439
      return (
29✔
1440
        <StyledMap
1441
          ref={this._ref}
1442
          style={this.styleSelector(this.props)}
1443
          onContextMenu={event => event.preventDefault()}
×
1444
          $mixBlendMode={visState.overlayBlending}
1445
          $mapLibCssClass={baseMapLibraryConfig.mapLibCssClass}
1446
        >
1447
          {mapContent}
1448
        </StyledMap>
1449
      );
1450
    }
1451
  }
1452

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