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

keplergl / kepler.gl / 25284823262

03 May 2026 04:41PM UTC coverage: 59.108% (-0.02%) from 59.129%
25284823262

Pull #3412

github

web-flow
Merge e9463103c into 6c9294a8e
Pull Request #3412: fix: arcgis tile 3d model crash due to unknown coord system (post deck.gl upgrade)

6926 of 14080 branches covered (49.19%)

Branch coverage included in aggregate %.

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

36 existing lines in 1 file now uncovered.

14280 of 21797 relevant lines covered (65.51%)

79.92 hits per line

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

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

4
// libraries
5
import React, {Component, createRef, useMemo} from 'react';
6
import styled, {withTheme, 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
  getBaseMapLibrary,
52
  BaseMapLibraryConfig,
53
  transformRequest,
54
  observeDimensions,
55
  unobserveDimensions,
56
  hasMobileWidth,
57
  getMapLayersFromSplitMaps,
58
  onViewPortChange,
59
  getViewportFromMapState,
60
  normalizeEvent,
61
  rgbToHex,
62
  computeDeckEffects,
63
  getApplicationConfig,
64
  GetMapRef,
65
  getLayerBlendingParameters,
66
  patchDeckRendererForPostProcessing
67
} from '@kepler.gl/utils';
68
import {breakPointValues} from '@kepler.gl/styles';
69

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

307
  return memoizedComponents;
28✔
308
};
309

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

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

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

334
const LOGO_LEFT_ADJUSTMENT = 3;
7✔
335

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

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

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

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

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

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

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

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

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

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

411
  datasetAttributions?: DatasetAttribution[];
412
  attributionLogos?: AttributionWithStyle[];
413

414
  generateMapboxLayers?: typeof generateMapboxLayers;
415
  generateDeckGLLayers?: typeof computeDeckLayers;
416

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

419
  children?: React.ReactNode;
420
  deckRenderCallbacks?: {
421
    onDeckLoad?: () => void;
422
    onDeckRender?: (deckProps: Record<string, unknown>) => Record<string, unknown> | null;
423
    onDeckAfterRender?: (deckProps: Record<string, unknown>) => any;
424
  };
425

426
  // Optional: override legend header logo in map controls (used by image export)
427
  logoComponent?: React.FC | React.ReactNode;
428
}
429

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

438
    private anyActiveLayerLoading = false;
25✔
439

440
    static contextType = MapViewStateContext;
14✔
441

442
    declare context: React.ContextType<typeof MapViewStateContext>;
443

444
    static defaultProps = {
14✔
445
      deckGlProps: {},
446
      index: 0,
447
      primary: true
448
    };
449

450
    constructor(props) {
451
      super(props);
25✔
452
      patchDeckRendererForPostProcessing();
25✔
453
    }
454

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

460
    componentDidMount() {
461
      if (!this._ref.current) {
25!
462
        return;
×
463
      }
464
      observeDimensions(this._ref.current, this._handleResize);
25✔
465
    }
466

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

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

484
    previousLayers = {
25✔
485
      // [layers.id]: mapboxLayerConfig
486
    };
487

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

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

547
    generateMapboxLayerMethodSelector = props => props.generateMapboxLayers ?? generateMapboxLayers;
25!
548

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

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

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

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

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

593
      const layer = this.props.visState.layers[idx];
×
594
      if (!layer) return;
×
595

596
      this.props.visStateActions.layerConfigChange(layer, config as Partial<LayerBaseConfig>);
×
597
    };
598

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

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

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

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

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

632
    _onMapboxStyleUpdate = update => {
25✔
633
      // force refresh mapboxgl layers
634
      this.previousLayers = {};
×
635
      this._updateMapboxLayers();
×
636

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

645
      if (typeof this.props.onMapStyleLoaded === 'function') {
×
646
        this.props.onMapStyleLoaded(this._map);
×
647
      }
648
    };
649

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

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

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

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

685
    _onDeckInitialized(device) {
686
      if (this.props.onDeckInitialized) {
×
687
        this.props.onDeckInitialized(this._deck, device);
×
688
      }
689
    }
690

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

701
    _onBeforeRender = () => {
25✔
702
      // no-op
703
    };
704

705
    _onDeckError = (error, layer) => {
25✔
706
      const errorMessage = error?.message || 'unknown-error';
×
707
      const layerMessage = layer?.id ? ` in ${layer.id} layer` : '';
×
708
      const errorMessageFull =
709
        errorMessage === 'WebGL context is lost'
×
710
          ? '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.'
711
          : `An error in deck.gl: ${errorMessage}${layerMessage}.`;
712

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

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

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

744
    /* component render functions */
745

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

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

769
      if (!mousePosition || !interactionConfig.tooltip) {
×
770
        return null;
×
771
      }
772

773
      const layerHoverProp = getLayerHoverProp({
×
774
        animationConfig,
775
        interactionConfig,
776
        hoverInfo,
777
        layers,
778
        layersToRender,
779
        datasets
780
      });
781

782
      const compareMode = interactionConfig.tooltip.config
×
783
        ? interactionConfig.tooltip.config.compareMode
784
        : false;
785

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

807
      const commonProp = {
×
808
        onClose: this._onCloseMapPopover,
809
        zoom: mapState.zoom,
810
        container: this._deck ? this._deck.canvas : undefined
×
811
      };
812

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

847
    /* eslint-enable complexity */
848

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

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

876
      const {hoverInfo, editor} = visState;
29✔
877
      const {primaryMap, isInteractive, children} = options;
29✔
878

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

884
      const internalViewState = this.context?.getInternalViewState(index);
29✔
885
      const internalMapState = {...mapState, ...internalViewState};
29✔
886
      const viewport = getViewportFromMapState(internalMapState);
29✔
887

888
      const editorFeatureSelectedIndex = this.selectedPolygonIndexSelector(this.props);
29✔
889

890
      const {setFeatures, onLayerClick, setSelectedFeature} = visStateActions;
29✔
891

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

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

945
          return EditorLayerUtils.getTooltip(info, {
×
946
            editorMenuActive,
947
            editor,
948
            theme
949
          });
950
        };
951

952
        extraDeckParams.getCursor = ({isDragging}: {isDragging: boolean}) => {
29✔
953
          const editorCursor = EditorLayerUtils.getCursor({
×
954
            editorMenuActive,
955
            editor,
956
            hoverInfo
957
          });
958
          if (editorCursor) return editorCursor;
×
959

960
          if (isDragging) return 'grabbing';
×
961
          if (hoverInfo?.layer) return 'pointer';
×
962
          return 'grab';
×
963
        };
964
      }
965

966
      const effects = this._isOKToRenderEffects(index)
29!
967
        ? computeDeckEffects({visState, mapState, isExport: this.props.isExport})
968
        : [];
969

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

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

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

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

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

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

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

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

1084
      updateMapboxLayers(this._map, mapboxLayers, this.previousLayers);
×
1085

UNCOV
1086
      this.previousLayers = mapboxLayers;
×
1087
    }
1088

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

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

1117
    _onLayerHoverDebounced = debounce((data, index) => {
25✔
UNCOV
1118
      this.props.visStateActions.onLayerHover(data, index);
×
1119
    }, DEBOUNCE_MOUSE_MOVE_PROPAGATE);
1120

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

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

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

1135
    _toggleMapControl = panelId => {
25✔
1136
      const {index, uiStateActions} = this.props;
2✔
1137

1138
      uiStateActions.toggleMapControl(panelId, Number(index));
2✔
1139
    };
1140

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

1168
      const {layers, datasets, editor, interactionConfig} = visState;
29✔
1169

1170
      const layersToRender = this.layersToRenderSelector(this.props);
29✔
1171
      const layersForDeck = this.layersForDeckSelector(this.props);
29✔
1172

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

1178
      // Select the correct Map adapter based on the active base map library.
1179
      // Using the native adapter for each library avoids Transform API
1180
      // incompatibilities (e.g. mapbox-legacy's cloneTransform with MapLibre v5).
1181
      const ResolvedMapComponent =
1182
        this.props.MapComponent ??
29✔
1183
        (baseMapLibraryName === MAP_LIB_OPTIONS.MAPBOX ? MapboxLegacyMap : MaplibreMap);
29!
1184

1185
      const useMapboxAdapter = ResolvedMapComponent === MapboxLegacyMap;
29✔
1186

1187
      const internalViewState = this.context?.getInternalViewState(index);
29✔
1188
      const configMaxPitch = mapState.maxPitch ?? getApplicationConfig().maxPitch;
29✔
1189
      const effectiveMaxPitch = useMapboxAdapter
29!
1190
        ? Math.min(configMaxPitch, MAPBOX_MAX_PITCH)
1191
        : configMaxPitch;
1192
      const mapProps: Record<string, any> = {
29✔
1193
        ...internalViewState,
1194
        maxPitch: effectiveMaxPitch,
1195
        preserveDrawingBuffer: this.props.isExport ?? false,
54✔
1196
        mapboxAccessToken: currentStyle?.accessToken || mapboxApiAccessToken,
58✔
1197
        // baseApiUrl: mapboxApiUrl,
1198
        transformRequest:
1199
          this.props.transformRequest ||
58✔
1200
          transformRequest(currentStyle?.accessToken || mapboxApiAccessToken)
58✔
1201
      };
1202

1203
      if (this.props.RTLTextPlugin !== undefined) {
29!
UNCOV
1204
        mapProps.RTLTextPlugin = this.props.RTLTextPlugin;
×
1205
      }
1206

1207
      if (useMapboxAdapter) {
29!
UNCOV
1208
        const mapboxConfig = getApplicationConfig().baseMapLibraryConfig[MAP_LIB_OPTIONS.MAPBOX];
×
UNCOV
1209
        mapProps.mapLib = mapboxConfig.getMapLib();
×
1210
      }
1211

1212
      const hasGeocoderLayer = Boolean(layers.find(l => l.id === GEOCODER_LAYER_ID));
29✔
1213
      const isSplit = Boolean(mapState.isSplit);
29✔
1214

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

1273
          {deck}
1274
          {this._renderMapboxOverlays()}
1275
          <Editor
1276
            index={index || 0}
53✔
1277
            datasets={datasets}
1278
            editor={editor}
1279
            filters={this.polygonFiltersSelector(this.props)}
1280
            layers={layers}
1281
            onDeleteFeature={visStateActions.deleteFeature}
1282
            onSelect={visStateActions.setSelectedFeature}
1283
            onTogglePolygonFilter={visStateActions.setPolygonFilterLayer}
1284
            onSetEditorMode={visStateActions.setEditorMode}
1285
            style={{
1286
              pointerEvents: 'all',
1287
              position: 'absolute',
1288
              display: editor.visible ? 'block' : 'none'
29!
1289
            }}
1290
          />
1291
          {this.props.children}
1292
          {mapStyle.topMapStyle ? (
29!
1293
            <ResolvedMapComponent
1294
              key={`top-${baseMapLibraryName}`}
1295
              viewState={internalViewState}
1296
              maxPitch={effectiveMaxPitch}
1297
              mapStyle={mapStyle.topMapStyle}
1298
              style={MAP_STYLE.top}
1299
              mapboxAccessToken={mapProps.mapboxAccessToken}
1300
              transformRequest={mapProps.transformRequest}
1301
              {...(mapProps.RTLTextPlugin !== undefined
×
1302
                ? {RTLTextPlugin: mapProps.RTLTextPlugin}
1303
                : {})}
1304
              {...(useMapboxAdapter
×
1305
                ? {
1306
                    mapLib:
1307
                      getApplicationConfig().baseMapLibraryConfig[
1308
                        MAP_LIB_OPTIONS.MAPBOX
1309
                      ].getMapLib()
1310
                  }
1311
                : {})}
1312
              {...topMapContainerProps}
1313
            />
1314
          ) : null}
1315

1316
          {hasGeocoderLayer
29!
1317
            ? this._renderDeckOverlay(
1318
                {[GEOCODER_LAYER_ID]: hasGeocoderLayer},
1319
                {primaryMap: false, isInteractive: false}
1320
              )
1321
            : null}
1322
          {this._renderMapPopover()}
1323
          {!isExport && primary !== isSplit ? (
83✔
1324
            <LoadingIndicator
1325
              isVisible={Boolean(isLoadingIndicatorVisible || this.anyActiveLayerLoading)}
34✔
1326
              activeSidePanel={Boolean(activeSidePanel)}
1327
              sidePanelWidth={sidePanelWidth}
1328
              hasAttributionLogos={attributionLogos.length > 0}
1329
            />
1330
          ) : null}
1331
          {this.props.primary ? (
29✔
1332
            <Attribution
1333
              showBaseMapLibLogo={this.state.showBaseMapAttribution}
1334
              showOsmBasemapAttribution={true}
1335
              datasetAttributions={datasetAttributions}
1336
              baseMapLibraryConfig={baseMapLibraryConfig}
1337
            />
1338
          ) : null}
1339
          {this.props.primary ? (
29✔
1340
            <AttributionLogos
1341
              logos={attributionLogos}
1342
              activeSidePanel={Boolean(activeSidePanel)}
1343
              sidePanelWidth={sidePanelWidth}
1344
            />
1345
          ) : null}
1346
        </>
1347
      );
1348
    }
1349

1350
    render() {
1351
      const {visState, mapStyle} = this.props;
29✔
1352
      const mapContent = this._renderMap();
29✔
1353
      if (!mapContent) {
29!
1354
        // mapContent can be null if onDeckRender returns null
1355
        // in this case we don't want to render the map
UNCOV
1356
        return null;
×
1357
      }
1358

1359
      const currentStyle = mapStyle.mapStyles?.[mapStyle.styleType];
29✔
1360
      const baseMapLibraryName = getBaseMapLibrary(currentStyle);
29✔
1361
      const baseMapLibraryConfig = getApplicationConfig().baseMapLibraryConfig[baseMapLibraryName];
29✔
1362

1363
      return (
29✔
1364
        <StyledMap
1365
          ref={this._ref}
1366
          style={this.styleSelector(this.props)}
UNCOV
1367
          onContextMenu={event => event.preventDefault()}
×
1368
          $mixBlendMode={visState.overlayBlending}
1369
          $mapLibCssClass={baseMapLibraryConfig.mapLibCssClass}
1370
        >
1371
          {mapContent}
1372
        </StyledMap>
1373
      );
1374
    }
1375
  }
1376

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