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

keplergl / kepler.gl / 25205206405

01 May 2026 06:33AM UTC coverage: 59.129% (-0.05%) from 59.174%
25205206405

Pull #3402

github

web-flow
Merge f1b2ba04e into a9ca4a80c
Pull Request #3402: feat: Streamlined rectangle drag-to-filter for map layers

6926 of 14072 branches covered (49.22%)

Branch coverage included in aggregate %.

6 of 27 new or added lines in 6 files covered. (22.22%)

166 existing lines in 6 files now uncovered.

14279 of 21790 relevant lines covered (65.53%)

79.95 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!
UNCOV
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!
UNCOV
470
        this._map?.off(MAPBOXGL_STYLE_UPDATE, nop);
×
471
        this._map?.off(MAPBOXGL_RENDER, nop);
×
472
      }
473
      if (!this._ref.current) {
2!
UNCOV
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) {
×
UNCOV
491
        const {mapStateActions} = this.props;
×
UNCOV
492
        if (dimensions && dimensions.width > 0 && dimensions.height > 0) {
×
UNCOV
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',
UNCOV
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) =>
UNCOV
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✔
UNCOV
574
      this.props.visStateActions.onLayerClick(null);
×
575
    };
576

577
    _onLayerHover = (_idx: number, info: PickingInfo<any> | null) => {
25✔
UNCOV
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.
UNCOV
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];
×
UNCOV
594
      if (!layer) return;
×
595

UNCOV
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
UNCOV
601
      const {mapStateActions, index} = this.props;
×
UNCOV
602
      mapStateActions.updateMap({}, index);
×
603
    };
604

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

609
    _onLayerFilteredItemsChange = (idx, event) => {
25✔
UNCOV
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
    ) => {
UNCOV
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 = {};
×
UNCOV
635
      this._updateMapboxLayers();
×
636

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

UNCOV
645
      if (typeof this.props.onMapStyleLoaded === 'function') {
×
UNCOV
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) {
×
UNCOV
655
          this._map?.off(MAPBOXGL_STYLE_UPDATE, nop);
×
UNCOV
656
          this._map?.off(MAPBOXGL_RENDER, nop);
×
UNCOV
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
UNCOV
664
        if (!this._map) {
×
665
          return;
×
666
        }
667
        // bind mapboxgl event listener
668
        this._map.on(MAPBOXGL_STYLE_UPDATE, this._onMapboxStyleUpdate);
×
669

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

UNCOV
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).
UNCOV
681
        this.props.getMapboxRef(mapRef, this.props.index);
×
682
      }
683
    };
684

685
    _onDeckInitialized(device) {
UNCOV
686
      if (this.props.onDeckInitialized) {
×
UNCOV
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';
×
UNCOV
707
      const layerMessage = layer?.id ? ` in ${layer.id} layer` : '';
×
708
      const errorMessageFull =
UNCOV
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.
UNCOV
714
      const lastShown = this._deckGLErrorsElapsed[errorMessageFull];
×
UNCOV
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) {
×
UNCOV
722
          let topMostLayer = layer;
×
723
          while (topMostLayer.parent) {
×
724
            topMostLayer = topMostLayer.parent;
×
725
          }
UNCOV
726
          if (topMostLayer.props?.id) {
×
UNCOV
727
            visStateActions.layerSetIsValid(topMostLayer, false);
×
UNCOV
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.
UNCOV
734
        const {uiStateActions} = this.props;
×
UNCOV
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

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

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

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

UNCOV
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);
×
UNCOV
791
        const lngLat = clicked ? clicked.coordinate : pinned.coordinate;
×
UNCOV
792
        pinnedPosition = this._getHoverXY(viewport, lngLat);
×
UNCOV
793
        layerPinnedProp = getLayerHoverProp({
×
794
          animationConfig,
795
          interactionConfig,
796
          hoverInfo: clicked,
797
          layers,
798
          layersToRender,
799
          datasets
800
        });
UNCOV
801
        if (layerHoverProp && layerPinnedProp) {
×
UNCOV
802
          layerHoverProp.primaryData = layerPinnedProp.data;
×
UNCOV
803
          layerHoverProp.compareType = interactionConfig.tooltip.config.compareType;
×
804
        }
805
      }
806

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

UNCOV
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) {
UNCOV
850
      const screenCoord = !viewport || !lngLat ? null : viewport.project(lngLat);
×
UNCOV
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✔
UNCOV
941
          const x = Number(info?.x);
×
942
          const y = Number(info?.y);
×
UNCOV
943
          if (Number.isNaN(x) || Number.isNaN(y) || x < 0 || y < 0) return null;
×
944

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

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

UNCOV
960
          if (isDragging) return 'grabbing';
×
UNCOV
961
          if (hoverInfo?.layer) return 'pointer';
×
UNCOV
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
29!
971
        ? deckGlProps?.views()
972
        : new MapView({farZMultiplier: 1.2});
29✔
973

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

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

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

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

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

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

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

UNCOV
1086
      updateMapboxLayers(this._map, mapboxLayers, this.previousLayers);
×
1087

1088
      this.previousLayers = mapboxLayers;
1089
    }
1090

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

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

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

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

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

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

2✔
1137
    _toggleMapControl = panelId => {
1138
      const {index, uiStateActions} = this.props;
2✔
1139

1140
      uiStateActions.toggleMapControl(panelId, Number(index));
1141
    };
1142

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

1170
      const {layers, datasets, editor, interactionConfig} = visState;
29✔
1171

29✔
1172
      const layersToRender = this.layersToRenderSelector(this.props);
1173
      const layersForDeck = this.layersForDeckSelector(this.props);
1174

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

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

1187
      const useMapboxAdapter = ResolvedMapComponent === MapboxLegacyMap;
29✔
1188

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

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

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

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

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

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

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

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

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

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

1379
  return withTheme(MapContainer);
1380
}
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