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

keplergl / kepler.gl / 25884645943

14 May 2026 08:43PM UTC coverage: 57.684% (-1.0%) from 58.684%
25884645943

push

github

web-flow
feat: basic annotations (#3434)

* feat: basic annotations

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

* fixes and improvements

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

* fix annotations lag

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

* tests, lint, fixes

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

* formatting/prettier

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

* update icon from target to letters

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

* fix tests

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

* fixes

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

* fix dragging

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

* fixes

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

* fixes

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

* fixes

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

* follow up

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

* fixes; follow ups

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

---------

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

7158 of 14867 branches covered (48.15%)

Branch coverage included in aggregate %.

217 of 737 new or added lines in 25 files covered. (29.44%)

70 existing lines in 2 files now uncovered.

14556 of 22776 relevant lines covered (63.91%)

77.67 hits per line

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

40.37
/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
import {AnnotationOverlay} from './annotations';
28

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

313
  return memoizedComponents;
28✔
314
};
315

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

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

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

340
const LOGO_LEFT_ADJUSTMENT = 3;
7✔
341

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

444
    private anyActiveLayerLoading = false;
25✔
445

446
    static contextType = MapViewStateContext;
14✔
447

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

664
      this._updateAttribution(update);
×
665

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

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

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

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

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

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

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

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

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

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

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

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

782
    _annotationViewportCache: {key: string; viewport: any} | null = null;
25✔
783

784
    _getAnnotationViewport(mapState: any, internalViewState: any) {
785
      const longitude = internalViewState?.longitude ?? mapState.longitude;
29!
786
      const latitude = internalViewState?.latitude ?? mapState.latitude;
29!
787
      const zoom = internalViewState?.zoom ?? mapState.zoom;
29!
788
      const pitch = internalViewState?.pitch ?? mapState.pitch ?? 0;
29!
789
      const bearing = internalViewState?.bearing ?? mapState.bearing ?? 0;
29!
790
      const width = mapState.width || 0;
29!
791
      const height = mapState.height || 0;
29!
792
      const key = `${longitude},${latitude},${zoom},${pitch},${bearing},${width},${height}`;
29✔
793

794
      if (this._annotationViewportCache?.key === key) {
29✔
795
        return this._annotationViewportCache.viewport;
3✔
796
      }
797

798
      const mergedState = {...mapState, ...internalViewState, width, height};
26✔
799
      const vp = getViewportFromMapState(mergedState) as any;
26✔
800
      const viewport = {
26✔
NEW
801
        project: (lngLat: [number, number]) => vp.project(lngLat) as [number, number],
×
NEW
802
        unproject: (xy: [number, number]) => vp.unproject(xy) as [number, number],
×
803
        longitude,
804
        latitude,
805
        width,
806
        height,
807
        zoom
808
      };
809
      this._annotationViewportCache = {key, viewport};
26✔
810
      return viewport;
26✔
811
    }
812

813
    _onDeckError = (error, layer) => {
25✔
814
      const errorMessage = error?.message || 'unknown-error';
×
815
      const layerMessage = layer?.id ? ` in ${layer.id} layer` : '';
×
816
      const errorMessageFull =
817
        errorMessage === 'WebGL context is lost'
×
818
          ? '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.'
819
          : `An error in deck.gl: ${errorMessage}${layerMessage}.`;
820

821
      // Throttle error notifications, as React doesn't like too many state changes from here.
822
      const lastShown = this._deckGLErrorsElapsed[errorMessageFull];
×
823
      if (!lastShown || lastShown < Date.now() - THROTTLE_NOTIFICATION_TIME) {
×
824
        this._deckGLErrorsElapsed[errorMessageFull] = Date.now();
×
825

826
        // Mark layer as invalid
827
        let extraLayerMessage = '';
×
828
        const {visStateActions} = this.props;
×
829
        if (layer) {
×
830
          let topMostLayer = layer;
×
831
          while (topMostLayer.parent) {
×
832
            topMostLayer = topMostLayer.parent;
×
833
          }
834
          if (topMostLayer.props?.id) {
×
835
            visStateActions.layerSetIsValid(topMostLayer, false);
×
836
            extraLayerMessage = 'The layer has been disabled and highlighted.';
×
837
          }
838
        }
839

840
        // Create new error notification or update existing one with same id.
841
        // Update is required to preserve the order of notifications as they probably are going to "jump" based on order of errors.
842
        const {uiStateActions} = this.props;
×
843
        uiStateActions.addNotification(
×
844
          errorNotification({
845
            message: `${errorMessageFull} ${extraLayerMessage}`,
846
            id: errorMessageFull // treat the error message as id
847
          })
848
        );
849
      }
850
    };
851

852
    /* component render functions */
853

854
    /* eslint-disable complexity */
855
    _renderMapPopover() {
856
      // this check is for limiting the display of the `<MapPopover>` to the `<MapContainer>` the user is interacting with
857
      // the DeckGL onHover event handler adds a `mapIndex` property which is available in the `hoverInfo` object of `visState`
858
      if (this.props.index !== this.props.visState.hoverInfo?.mapIndex) {
29!
859
        return null;
29✔
860
      }
861

862
      // TODO: move this into reducer so it can be tested
863
      const {
864
        mapState,
865
        visState: {
866
          hoverInfo,
867
          clicked,
868
          datasets,
869
          interactionConfig,
870
          animationConfig,
871
          layers,
872
          mousePos: {mousePosition, coordinate, pinned}
873
        }
874
      } = this.props;
×
875
      const layersToRender = this.layersToRenderSelector(this.props);
×
876

877
      if (!mousePosition || !interactionConfig.tooltip) {
×
878
        return null;
×
879
      }
880

881
      const layerHoverProp = getLayerHoverProp({
×
882
        animationConfig,
883
        interactionConfig,
884
        hoverInfo,
885
        layers,
886
        layersToRender,
887
        datasets
888
      });
889

890
      const compareMode = interactionConfig.tooltip.config
×
891
        ? interactionConfig.tooltip.config.compareMode
892
        : false;
893

894
      let pinnedPosition = {x: 0, y: 0};
×
895
      let layerPinnedProp: LayerHoverProp | null = null;
×
896
      if (pinned || clicked) {
×
897
        // project lnglat to screen so that tooltip follows the object on zoom
898
        const viewport = getViewportFromMapState(mapState);
×
899
        const lngLat = clicked ? clicked.coordinate : pinned.coordinate;
×
900
        pinnedPosition = this._getHoverXY(viewport, lngLat);
×
901
        layerPinnedProp = getLayerHoverProp({
×
902
          animationConfig,
903
          interactionConfig,
904
          hoverInfo: clicked,
905
          layers,
906
          layersToRender,
907
          datasets
908
        });
909
        if (layerHoverProp && layerPinnedProp) {
×
910
          layerHoverProp.primaryData = layerPinnedProp.data;
×
911
          layerHoverProp.compareType = interactionConfig.tooltip.config.compareType;
×
912
        }
913
      }
914

915
      const commonProp = {
×
916
        onClose: this._onCloseMapPopover,
917
        zoom: mapState.zoom,
918
        container: this._deck ? this._deck.canvas : undefined
×
919
      };
920

921
      return (
×
922
        <ErrorBoundary>
923
          {layerPinnedProp && (
×
924
            <MapPopover
925
              {...pinnedPosition}
926
              {...commonProp}
927
              layerHoverProp={layerPinnedProp}
928
              coordinate={interactionConfig.coordinate.enabled && (pinned || {}).coordinate}
×
929
              frozen={true}
930
              isBase={compareMode}
931
              onSetFeatures={this.props.visStateActions.setFeatures}
932
              setSelectedFeature={this.props.visStateActions.setSelectedFeature}
933
              // @ts-ignore Argument of type 'Readonly<MapContainerProps>' is not assignable to parameter of type 'never'
934
              featureCollection={this.featureCollectionSelector(this.props)}
935
            />
936
          )}
937
          {layerHoverProp && (!layerPinnedProp || compareMode) && (
×
938
            <MapPopover
939
              x={mousePosition[0]}
940
              y={mousePosition[1]}
941
              {...commonProp}
942
              layerHoverProp={layerHoverProp}
943
              frozen={false}
944
              coordinate={interactionConfig.coordinate.enabled && coordinate}
×
945
              onSetFeatures={this.props.visStateActions.setFeatures}
946
              setSelectedFeature={this.props.visStateActions.setSelectedFeature}
947
              // @ts-ignore Argument of type 'Readonly<MapContainerProps>' is not assignable to parameter of type 'never'
948
              featureCollection={this.featureCollectionSelector(this.props)}
949
            />
950
          )}
951
        </ErrorBoundary>
952
      );
953
    }
954

955
    /* eslint-enable complexity */
956

957
    _getHoverXY(viewport, lngLat) {
958
      const screenCoord = !viewport || !lngLat ? null : viewport.project(lngLat);
×
959
      return screenCoord && {x: screenCoord[0], y: screenCoord[1]};
×
960
    }
961

962
    _renderDeckOverlay(
963
      layersForDeck,
964
      options: {primaryMap: boolean; isInteractive?: boolean; children?: React.ReactNode} = {
×
965
        primaryMap: false
966
      }
967
    ) {
968
      const {
969
        mapStyle,
970
        visState,
971
        mapState,
972
        visStateActions,
973
        mapboxApiAccessToken,
974
        mapboxApiUrl,
975
        deckGlProps,
976
        index,
977
        mapControls,
978
        deckRenderCallbacks,
979
        theme,
980
        generateDeckGLLayers,
981
        onMouseMove
982
      } = this.props;
29✔
983

984
      const {hoverInfo, editor} = visState;
29✔
985
      const {primaryMap, isInteractive, children} = options;
29✔
986

987
      // disable double click zoom when editor is in any draw mode
988
      const {mapDraw} = mapControls;
29✔
989
      const {active: editorMenuActive = false} = mapDraw || {};
29✔
990
      const isEditorDrawingMode = EditorLayerUtils.isDrawingActive(editorMenuActive, editor.mode);
29✔
991

992
      const internalViewState = this.context?.getInternalViewState(index);
29✔
993
      const internalMapState = {...mapState, ...internalViewState};
29✔
994
      const viewport = getViewportFromMapState(internalMapState);
29✔
995

996
      const editorFeatureSelectedIndex = this.selectedPolygonIndexSelector(this.props);
29✔
997

998
      const {setFeatures, onLayerClick, setSelectedFeature} = visStateActions;
29✔
999

1000
      const generateDeckGLLayersMethod = generateDeckGLLayers ?? computeDeckLayers;
29✔
1001
      const deckGlLayers = generateDeckGLLayersMethod(
29✔
1002
        {
1003
          visState,
1004
          mapState: internalMapState,
1005
          mapStyle
1006
        },
1007
        {
1008
          mapIndex: index,
1009
          primaryMap,
1010
          mapboxApiAccessToken,
1011
          mapboxApiUrl,
1012
          layersForDeck,
1013
          editorInfo: primaryMap
29!
1014
            ? {
1015
                editor,
1016
                editorMenuActive,
1017
                onSetFeatures: setFeatures,
1018
                setSelectedFeature,
1019
                onApplyPolygonFilterAll: visStateActions.setPolygonFilterAllLayers,
1020
                // @ts-ignore Argument of type 'Readonly<MapContainerProps>' is not assignable to parameter of type 'never'
1021
                featureCollection: this.featureCollectionSelector(this.props),
1022
                selectedFeatureIndexes: this.selectedFeatureIndexArraySelector(
1023
                  // @ts-ignore Argument of type 'unknown' is not assignable to parameter of type 'number'.
1024
                  editorFeatureSelectedIndex
1025
                ),
1026
                viewport
1027
              }
1028
            : undefined
1029
        },
1030
        {
1031
          onLayerHover: this._onLayerHover,
1032
          onSetLayerDomain: this._onLayerSetDomain,
1033
          onFilteredItemsChange: this._onLayerFilteredItemsChange,
1034
          onWMSFeatureInfo: this._onWMSFeatureInfo,
1035
          onRedrawNeeded: this._onRedrawNeeded,
1036
          onFitBounds: this._onFitBounds
1037
        },
1038
        deckGlProps
1039
      );
1040

1041
      const extraDeckParams: {
1042
        getTooltip?: (info: any) => object | null;
1043
        getCursor?: ({isDragging}: {isDragging: boolean}) => string;
1044
      } = {};
29✔
1045
      if (primaryMap) {
29!
1046
        // Omit hover updates when the pointer position is invalid, ie. over UI overlays or
1047
        // outside the map container. In those cases x/y may be < 0
1048
        extraDeckParams.getTooltip = info => {
29✔
1049
          const x = Number(info?.x);
×
1050
          const y = Number(info?.y);
×
1051
          if (Number.isNaN(x) || Number.isNaN(y) || x < 0 || y < 0) return null;
×
1052

1053
          return EditorLayerUtils.getTooltip(info, {
×
1054
            editorMenuActive,
1055
            editor,
1056
            theme
1057
          });
1058
        };
1059

1060
        extraDeckParams.getCursor = ({isDragging}: {isDragging: boolean}) => {
29✔
1061
          const editorCursor = EditorLayerUtils.getCursor({
×
1062
            editorMenuActive,
1063
            editor,
1064
            hoverInfo
1065
          });
1066
          if (editorCursor) return editorCursor;
×
1067

1068
          if (isDragging) return 'grabbing';
×
1069
          if (hoverInfo?.layer) return 'pointer';
×
1070
          return 'grab';
×
1071
        };
1072
      }
1073

1074
      const effects = this._isOKToRenderEffects(index)
29!
1075
        ? computeDeckEffects({visState, mapState, isExport: this.props.isExport})
1076
        : [];
1077

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

1080
      let allDeckGlProps = {
29✔
1081
        ...deckGlProps,
1082
        pickingRadius: DEFAULT_PICKING_RADIUS,
1083
        views,
1084
        layers: deckGlLayers,
1085
        effects,
1086
        parameters: getLayerBlendingParameters(visState.layerBlending)
1087
      };
1088

1089
      if (typeof deckRenderCallbacks?.onDeckRender === 'function') {
29!
1090
        allDeckGlProps = deckRenderCallbacks.onDeckRender(allDeckGlProps);
×
1091
        if (!allDeckGlProps) {
×
1092
          // if onDeckRender returns null, do not render deck.gl
1093
          return null;
×
1094
        }
1095
      }
1096

1097
      return (
29✔
1098
        <div
1099
          {...(isInteractive
29!
1100
            ? {
1101
                onMouseMove: primaryMap
29!
1102
                  ? event => {
1103
                      onMouseMove?.(event);
×
1104
                      this._onMouseMoveDebounced(event, viewport);
×
1105
                    }
1106
                  : undefined
1107
              }
1108
            : {style: {pointerEvents: 'none'}})}
1109
        >
1110
          <DeckGL
1111
            id="default-deckgl-overlay"
1112
            onLoad={() => {
1113
              if (typeof deckRenderCallbacks?.onDeckLoad === 'function') {
×
1114
                deckRenderCallbacks.onDeckLoad();
×
1115
              }
1116
            }}
1117
            {...allDeckGlProps}
1118
            controller={
1119
              isInteractive
29!
1120
                ? {
1121
                    doubleClickZoom: !isEditorDrawingMode,
1122
                    dragRotate: this.props.mapState.dragRotate,
1123
                    maxPitch: this.props.mapState.maxPitch ?? getApplicationConfig().maxPitch
58✔
1124
                  }
1125
                : false
1126
            }
1127
            initialViewState={internalViewState}
1128
            onBeforeRender={this._onBeforeRender}
1129
            onViewStateChange={isInteractive ? this._onViewportChange : undefined}
29!
1130
            {...extraDeckParams}
1131
            onHover={
1132
              isInteractive
29!
1133
                ? data => {
1134
                    const res = EditorLayerUtils.onHover(data, {
×
1135
                      editorMenuActive,
1136
                      editor,
1137
                      hoverInfo
1138
                    });
1139
                    if (res) return;
×
1140

1141
                    this._onLayerHoverDebounced(data, index);
×
1142
                  }
1143
                : null
1144
            }
1145
            onClick={(data, event) => {
1146
              // @ts-ignore
1147
              normalizeEvent(event.srcEvent, viewport);
×
1148
              const res = EditorLayerUtils.onClick(data, event, {
×
1149
                editorMenuActive,
1150
                editor,
1151
                onLayerClick,
1152
                setSelectedFeature,
1153
                mapIndex: index
1154
              });
1155
              if (res) return;
×
1156

1157
              visStateActions.onLayerClick(data);
×
1158
            }}
1159
            onError={this._onDeckError}
1160
            ref={comp => {
1161
              // @ts-ignore
1162
              if (comp && comp.deck && !this._deck) {
35✔
1163
                // @ts-ignore
1164
                this._deck = comp.deck;
1✔
1165
              }
1166
            }}
1167
            onDeviceInitialized={device => this._onDeckInitialized(device)}
×
1168
            onAfterRender={() => {
1169
              if (typeof deckRenderCallbacks?.onDeckAfterRender === 'function') {
×
1170
                deckRenderCallbacks.onDeckAfterRender(allDeckGlProps);
×
1171
              }
1172

1173
              const anyActiveLayerLoading = areAnyDeckLayersLoading(allDeckGlProps.layers);
×
1174
              if (anyActiveLayerLoading !== this.anyActiveLayerLoading) {
×
1175
                this._onLayerLoadingStateChange();
×
1176
                this.anyActiveLayerLoading = anyActiveLayerLoading;
×
1177
              }
1178
            }}
1179
          >
1180
            {children}
1181
          </DeckGL>
1182
        </div>
1183
      );
1184
    }
1185

1186
    _updateMapboxLayers() {
1187
      const mapboxLayers = this.mapboxLayersSelector(this.props);
×
1188
      if (!Object.keys(mapboxLayers).length && !Object.keys(this.previousLayers).length) {
×
1189
        return;
×
1190
      }
1191

1192
      updateMapboxLayers(this._map, mapboxLayers, this.previousLayers);
×
1193

1194
      this.previousLayers = mapboxLayers;
×
1195
    }
1196

1197
    _renderMapboxOverlays() {
1198
      if (this._map && this._map.isStyleLoaded()) {
29!
1199
        this._updateMapboxLayers();
×
1200
      }
1201
    }
1202
    _onViewportChangePropagateDebounced = debounce(() => {
25✔
1203
      const viewState = this.context?.getInternalViewState(this.props.index);
×
1204
      onViewPortChange(
×
1205
        viewState,
1206
        this.props.mapStateActions.updateMap,
1207
        this.props.onViewStateChange,
1208
        this.props.primary,
1209
        this.props.index
1210
      );
1211
    }, DEBOUNCE_VIEWPORT_PROPAGATE);
1212

1213
    _onViewportChange = viewport => {
25✔
1214
      const {viewState} = viewport;
×
1215
      if (this.props.isExport) {
×
1216
        // Image export map shouldn't be interactive (otherwise this callback can
1217
        // lead to inadvertent changes to the state of the main map)
1218
        return;
×
1219
      }
1220
      const {setInternalViewState} = this.context;
×
1221
      setInternalViewState(viewState, this.props.index);
×
1222
      this._onViewportChangePropagateDebounced();
×
1223
    };
1224

1225
    _onLayerHoverDebounced = debounce((data, index) => {
25✔
1226
      this.props.visStateActions.onLayerHover(data, index);
×
1227
    }, DEBOUNCE_MOUSE_MOVE_PROPAGATE);
1228

1229
    _onMouseMoveDebounced = debounce((event, viewport) => {
25✔
1230
      this.props.visStateActions.onMouseMove(normalizeEvent(event, viewport));
×
1231
    }, DEBOUNCE_MOUSE_MOVE_PROPAGATE);
1232

1233
    _onLayerLoadingStateChange = debounce(() => {
25✔
1234
      // trigger loading indicator update without any change to update UI
1235
      this.props.visStateActions.setLoadingIndicator({change: 0});
×
1236
    }, DEBOUNCE_LOADING_STATE_PROPAGATE);
1237

1238
    _handleToggleLayerVisibility = (layer: Layer) => {
25✔
1239
      const {visStateActions} = this.props;
×
1240
      visStateActions.layerConfigChange(layer, {isVisible: !layer.config.isVisible});
×
1241
    };
1242

1243
    _toggleMapControl = panelId => {
25✔
1244
      const {index, uiStateActions} = this.props;
2✔
1245

1246
      uiStateActions.toggleMapControl(panelId, Number(index));
2✔
1247
    };
1248

1249
    /* eslint-disable complexity */
1250
    _renderMap() {
1251
      const {
1252
        visState,
1253
        mapState,
1254
        mapStyle,
1255
        mapStateActions,
1256
        mapboxApiAccessToken,
1257
        // mapboxApiUrl,
1258
        mapControls,
1259
        isExport,
1260
        locale,
1261
        uiStateActions,
1262
        visStateActions,
1263
        index,
1264
        primary,
1265
        bottomMapContainerProps,
1266
        topMapContainerProps,
1267
        theme,
1268
        datasetAttributions = [],
×
1269
        attributionLogos = [],
×
1270
        containerId = 0,
13✔
1271
        isLoadingIndicatorVisible,
1272
        activeSidePanel,
1273
        sidePanelWidth
1274
      } = this.props;
29✔
1275

1276
      const {layers, datasets, editor, interactionConfig} = visState;
29✔
1277

1278
      const layersToRender = this.layersToRenderSelector(this.props);
29✔
1279
      const layersForDeck = this.layersForDeckSelector(this.props);
29✔
1280

1281
      // Current style can be a custom style, from which we pull the mapbox API acccess token
1282
      const currentStyle = mapStyle.mapStyles?.[mapStyle.styleType];
29✔
1283
      const baseMapLibraryName = getBaseMapLibrary(currentStyle);
29✔
1284
      const baseMapLibraryConfig = getApplicationConfig().baseMapLibraryConfig[baseMapLibraryName];
29✔
1285

1286
      // Select the correct Map adapter based on the active base map library.
1287
      // Using the native adapter for each library avoids Transform API
1288
      // incompatibilities (e.g. mapbox-legacy's cloneTransform with MapLibre v5).
1289
      const ResolvedMapComponent =
1290
        this.props.MapComponent ??
29✔
1291
        (baseMapLibraryName === MAP_LIB_OPTIONS.MAPBOX ? MapboxLegacyMap : MaplibreMap);
29!
1292

1293
      const useMapboxAdapter = ResolvedMapComponent === MapboxLegacyMap;
29✔
1294

1295
      const internalViewState = this.context?.getInternalViewState(index);
29✔
1296
      const configMaxPitch = mapState.maxPitch ?? getApplicationConfig().maxPitch;
29✔
1297
      const effectiveMaxPitch = useMapboxAdapter
29!
1298
        ? Math.min(configMaxPitch, MAPBOX_MAX_PITCH)
1299
        : configMaxPitch;
1300
      const mapProps: Record<string, any> = {
29✔
1301
        ...internalViewState,
1302
        maxPitch: effectiveMaxPitch,
1303
        preserveDrawingBuffer: this.props.isExport ?? false,
54✔
1304
        mapboxAccessToken: currentStyle?.accessToken || mapboxApiAccessToken,
58✔
1305
        // baseApiUrl: mapboxApiUrl,
1306
        transformRequest:
1307
          this.props.transformRequest ||
58✔
1308
          transformRequest(currentStyle?.accessToken || mapboxApiAccessToken)
58✔
1309
      };
1310

1311
      if (this.props.RTLTextPlugin !== undefined) {
29!
1312
        mapProps.RTLTextPlugin = this.props.RTLTextPlugin;
×
1313
      }
1314

1315
      if (useMapboxAdapter) {
29!
1316
        const mapboxConfig = getApplicationConfig().baseMapLibraryConfig[MAP_LIB_OPTIONS.MAPBOX];
×
1317
        mapProps.mapLib = mapboxConfig.getMapLib();
×
1318
      }
1319

1320
      const hasGeocoderLayer = Boolean(layers.find(l => l.id === GEOCODER_LAYER_ID));
29✔
1321
      const isSplit = Boolean(mapState.isSplit);
29✔
1322

1323
      const deck = this._renderDeckOverlay(layersForDeck, {
29✔
1324
        primaryMap: true,
1325
        isInteractive: true,
1326
        children: (
1327
          <ResolvedMapComponent
1328
            key={`bottom-${baseMapLibraryName}`}
1329
            {...mapProps}
1330
            mapStyle={mapStyle.bottomMapStyle ?? EMPTY_MAPBOX_STYLE}
58✔
1331
            {...bottomMapContainerProps}
1332
            ref={this._setMapRef}
1333
          />
1334
        )
1335
      });
1336
      if (!deck) {
29!
1337
        // deckOverlay can be null if onDeckRender returns null
1338
        // in this case we don't want to render the map
1339
        return null;
×
1340
      }
1341
      return (
29✔
1342
        <>
1343
          <MapControl
1344
            mapState={mapState}
1345
            datasets={datasets}
1346
            availableLocales={LOCALE_CODES_ARRAY}
1347
            dragRotate={mapState.dragRotate}
1348
            isSplit={isSplit}
1349
            primary={Boolean(primary)}
1350
            isExport={isExport}
1351
            layers={layers}
1352
            layersToRender={layersToRender}
1353
            mapIndex={index || 0}
53✔
1354
            mapControls={mapControls}
1355
            readOnly={this.props.readOnly}
1356
            scale={mapState.scale || 1}
58✔
1357
            logoComponent={this.props.logoComponent}
1358
            top={
1359
              interactionConfig.geocoder && interactionConfig.geocoder.enabled
87!
1360
                ? theme.mapControlTop
1361
                : 0
1362
            }
1363
            editor={editor}
1364
            locale={locale}
1365
            onTogglePerspective={mapStateActions.togglePerspective}
1366
            onToggleSplitMap={mapStateActions.toggleSplitMap}
1367
            onMapToggleLayer={this._handleMapToggleLayer}
1368
            onToggleMapControl={this._toggleMapControl}
1369
            onToggleSplitMapViewport={mapStateActions.toggleSplitMapViewport}
1370
            onSetEditorMode={visStateActions.setEditorMode}
1371
            onSetLocale={uiStateActions.setLocale}
1372
            onToggleEditorVisibility={visStateActions.toggleEditorVisibility}
1373
            onLayerVisConfigChange={visStateActions.layerVisConfigChange}
1374
            onToggleLayerVisibility={this._handleToggleLayerVisibility}
1375
            mapHeight={mapState.height}
1376
            setMapControlSettings={uiStateActions.setMapControlSettings}
1377
            activeSidePanel={activeSidePanel}
1378
          />
1379
          {isSplitSelector(this.props) && <Droppable containerId={containerId} />}
38✔
1380

1381
          {deck}
1382
          {this._renderMapboxOverlays()}
1383
          <Editor
1384
            index={index || 0}
53✔
1385
            datasets={datasets}
1386
            editor={editor}
1387
            filters={this.polygonFiltersSelector(this.props)}
1388
            layers={layers}
1389
            onDeleteFeature={visStateActions.deleteFeature}
1390
            onSelect={visStateActions.setSelectedFeature}
1391
            onTogglePolygonFilter={visStateActions.setPolygonFilterLayer}
1392
            onSetEditorMode={visStateActions.setEditorMode}
1393
            style={{
1394
              pointerEvents: 'all',
1395
              position: 'absolute',
1396
              display: editor.visible ? 'block' : 'none'
29!
1397
            }}
1398
          />
1399
          <AnnotationOverlay
1400
            annotations={visState.annotations}
1401
            selectedAnnotationId={visState.selectedAnnotationId}
1402
            isEditingAnnotationText={visState.isEditingAnnotationText}
1403
            isAnnotationMode={Boolean(mapControls?.annotation?.active)}
1404
            mapIndex={index || 0}
53✔
1405
            viewport={this._getAnnotationViewport(mapState, internalViewState)}
1406
            updateAnnotation={visStateActions.updateAnnotation}
1407
            setSelectedAnnotation={visStateActions.setSelectedAnnotation}
1408
          />
1409
          {this.props.children}
1410
          {mapStyle.topMapStyle ? (
29!
1411
            <ResolvedMapComponent
1412
              key={`top-${baseMapLibraryName}`}
1413
              viewState={internalViewState}
1414
              maxPitch={effectiveMaxPitch}
1415
              mapStyle={mapStyle.topMapStyle}
1416
              style={MAP_STYLE.top}
1417
              mapboxAccessToken={mapProps.mapboxAccessToken}
1418
              transformRequest={mapProps.transformRequest}
1419
              {...(mapProps.RTLTextPlugin !== undefined
×
1420
                ? {RTLTextPlugin: mapProps.RTLTextPlugin}
1421
                : {})}
1422
              {...(useMapboxAdapter
×
1423
                ? {
1424
                    mapLib:
1425
                      getApplicationConfig().baseMapLibraryConfig[
1426
                        MAP_LIB_OPTIONS.MAPBOX
1427
                      ].getMapLib()
1428
                  }
1429
                : {})}
1430
              {...topMapContainerProps}
1431
            />
1432
          ) : null}
1433

1434
          {hasGeocoderLayer
29!
1435
            ? this._renderDeckOverlay(
1436
                {[GEOCODER_LAYER_ID]: hasGeocoderLayer},
1437
                {primaryMap: false, isInteractive: false}
1438
              )
1439
            : null}
1440
          {this._renderMapPopover()}
1441
          {!isExport && primary !== isSplit ? (
83✔
1442
            <LoadingIndicator
1443
              isVisible={Boolean(isLoadingIndicatorVisible || this.anyActiveLayerLoading)}
34✔
1444
              activeSidePanel={Boolean(activeSidePanel)}
1445
              sidePanelWidth={sidePanelWidth}
1446
              hasAttributionLogos={attributionLogos.length > 0}
1447
            />
1448
          ) : null}
1449
          {this.props.primary ? (
29✔
1450
            <Attribution
1451
              showBaseMapLibLogo={this.state.showBaseMapAttribution}
1452
              showOsmBasemapAttribution={this.state.showOsmAttribution}
1453
              datasetAttributions={datasetAttributions}
1454
              baseMapLibraryConfig={baseMapLibraryConfig}
1455
            />
1456
          ) : null}
1457
          {this.props.primary ? (
29✔
1458
            <AttributionLogos
1459
              logos={attributionLogos}
1460
              activeSidePanel={Boolean(activeSidePanel)}
1461
              sidePanelWidth={sidePanelWidth}
1462
            />
1463
          ) : null}
1464
        </>
1465
      );
1466
    }
1467

1468
    render() {
1469
      const {visState, mapStyle} = this.props;
29✔
1470
      const mapContent = this._renderMap();
29✔
1471
      if (!mapContent) {
29!
1472
        // mapContent can be null if onDeckRender returns null
1473
        // in this case we don't want to render the map
1474
        return null;
×
1475
      }
1476

1477
      const currentStyle = mapStyle.mapStyles?.[mapStyle.styleType];
29✔
1478
      const baseMapLibraryName = getBaseMapLibrary(currentStyle);
29✔
1479
      const baseMapLibraryConfig = getApplicationConfig().baseMapLibraryConfig[baseMapLibraryName];
29✔
1480

1481
      return (
29✔
1482
        <StyledMap
1483
          ref={this._ref}
1484
          style={this.styleSelector(this.props)}
1485
          onContextMenu={event => event.preventDefault()}
×
1486
          $mixBlendMode={visState.overlayBlending}
1487
          $mapLibCssClass={baseMapLibraryConfig.mapLibCssClass}
1488
        >
1489
          {mapContent}
1490
        </StyledMap>
1491
      );
1492
    }
1493
  }
1494

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