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

keplergl / kepler.gl / 12508767309

26 Dec 2024 09:53PM UTC coverage: 67.491% (-0.02%) from 67.511%
12508767309

push

github

web-flow
[Feat] Add custom color scale for aggregate layers (#2860)

Signed-off-by: Ihor Dykhta <dikhta.igor@gmail.com>
Co-authored-by: Ilya Boyandin <iboyandin@foursquare.com>

5841 of 10041 branches covered (58.17%)

Branch coverage included in aggregate %.

54 of 64 new or added lines in 8 files covered. (84.38%)

1 existing line in 1 file now uncovered.

11978 of 16361 relevant lines covered (73.21%)

87.57 hits per line

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

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

4
// libraries
5
import React, {Component, createRef, useMemo} from 'react';
6
import styled, {withTheme} from 'styled-components';
7
import {Map, MapRef} from 'react-map-gl';
8
import {PickInfo} from '@deck.gl/core/lib/deck';
9
import DeckGL from '@deck.gl/react';
10
import {createSelector, Selector} from 'reselect';
11
import {useDroppable} from '@dnd-kit/core';
12
import debounce from 'lodash.debounce';
13

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

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

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

27
// utils
28
import {
29
  generateMapboxLayers,
30
  updateMapboxLayers,
31
  LayerBaseConfig,
32
  VisualChannelDomain,
33
  EditorLayerUtils,
34
  AggregatedBin
35
} from '@kepler.gl/layers';
36
import {MapState, MapControls, Viewport, SplitMap, SplitMapLayers} from '@kepler.gl/types';
37
import {
38
  errorNotification,
39
  setLayerBlending,
40
  isStyleUsingMapboxTiles,
41
  transformRequest,
42
  observeDimensions,
43
  unobserveDimensions,
44
  hasMobileWidth,
45
  getMapLayersFromSplitMaps,
46
  onViewPortChange,
47
  getViewportFromMapState,
48
  normalizeEvent,
49
  rgbToHex,
50
  computeDeckEffects,
51
  getApplicationConfig
52
} from '@kepler.gl/utils';
53
import {breakPointValues} from '@kepler.gl/styles';
54

55
// default-settings
56
import {
57
  FILTER_TYPES,
58
  GEOCODER_LAYER_ID,
59
  THROTTLE_NOTIFICATION_TIME,
60
  DEFAULT_PICKING_RADIUS,
61
  NO_MAP_ID,
62
  EMPTY_MAPBOX_STYLE,
63
  DROPPABLE_MAP_CONTAINER_TYPE
64
} from '@kepler.gl/constants';
65

66
// Contexts
67
import {MapViewStateContext} from './map-view-state-context';
68

69
import ErrorBoundary from './common/error-boundary';
70
import {DatasetAttribution} from './types';
71
import {LOCALE_CODES} from '@kepler.gl/localization';
72
import {MapView} from '@deck.gl/core';
73
import {
74
  MapStyle,
75
  computeDeckLayers,
76
  getLayerHoverProp,
77
  LayerHoverProp,
78
  prepareLayersForDeck,
79
  prepareLayersToRender,
80
  LayersToRender
81
} from '@kepler.gl/reducers';
82
import {VisState} from '@kepler.gl/schemas';
83

84
// Debounce the propagation of viewport change and mouse moves to redux store.
85
// This is to avoid too many renders of other components when the map is
86
// being panned/zoomed (leading to laggy basemap/deck syncing).
87
const DEBOUNCE_VIEWPORT_PROPAGATE = 10;
6✔
88
const DEBOUNCE_MOUSE_MOVE_PROPAGATE = 10;
6✔
89

90
/** @type {{[key: string]: React.CSSProperties}} */
91
const MAP_STYLE: {[key: string]: React.CSSProperties} = {
6✔
92
  container: {
93
    display: 'inline-block',
94
    position: 'relative',
95
    width: '100%',
96
    height: '100%'
97
  },
98
  top: {
99
    position: 'absolute',
100
    top: 0,
101
    width: '100%',
102
    height: '100%',
103
    pointerEvents: 'none'
104
  }
105
};
106

107
const LOCALE_CODES_ARRAY = Object.keys(LOCALE_CODES);
6✔
108

109
interface StyledMapContainerProps {
110
  mixBlendMode?: string;
111
}
112

113
const StyledMap = styled(StyledMapContainer)<StyledMapContainerProps>(
6✔
114
  ({mixBlendMode = 'normal'}) => `
30!
115
  #default-deckgl-overlay {
116
    mix-blend-mode: ${mixBlendMode};
117
  };
118
  *[${getApplicationConfig().mapLibCssClass}-children] {
119
    position: absolute;
120
  }
121
`
122
);
123

124
const MAPBOXGL_STYLE_UPDATE = 'style.load';
6✔
125
const MAPBOXGL_RENDER = 'render';
6✔
126
const nop = () => {
6✔
127
  return;
×
128
};
129

130
const MapLibLogo = () => (
6✔
131
  <div className="attrition-logo">
29✔
132
    Basemap by:
133
    <a
134
      style={{marginLeft: '5px'}}
135
      className={`${getApplicationConfig().mapLibCssClass}-ctrl-logo`}
136
      target="_blank"
137
      rel="noopener noreferrer"
138
      href={getApplicationConfig().mapLibUrl}
139
      aria-label={`${getApplicationConfig().mapLibName} logo`}
140
    />
141
  </div>
142
);
143

144
interface StyledDroppableProps {
145
  isOver: boolean;
146
}
147

148
const StyledDroppable = styled.div<StyledDroppableProps>`
6✔
149
  background-color: ${props => (props.isOver ? props.theme.dndOverBackgroundColor : 'none')};
10!
150
  width: 100%;
151
  height: 100%;
152
  position: absolute;
153
  pointer-events: none;
154
  z-index: 1;
155
`;
156

157
export const isSplitSelector = props =>
6✔
158
  props.visState.splitMaps && props.visState.splitMaps.length > 1;
30✔
159

160
export const Droppable = ({containerId}) => {
6✔
161
  const {isOver, setNodeRef} = useDroppable({
10✔
162
    id: containerId,
163
    data: {type: DROPPABLE_MAP_CONTAINER_TYPE, index: containerId},
164
    disabled: !containerId
165
  });
166

167
  return <StyledDroppable ref={setNodeRef} isOver={isOver} />;
10✔
168
};
169

170
interface StyledDatasetAttributionsContainerProps {
171
  isPalm: boolean;
172
}
173

174
const StyledDatasetAttributionsContainer = styled.div<StyledDatasetAttributionsContainerProps>`
6✔
175
  max-width: ${props => (props.isPalm ? '130px' : '180px')};
×
176
  text-overflow: ellipsis;
177
  white-space: nowrap;
178
  overflow: hidden;
179
  color: ${props => props.theme.labelColor};
×
180
  margin-right: 2px;
181
  line-height: ${props => (props.isPalm ? '1em' : '1.4em')};
×
182
  :hover {
183
    white-space: inherit;
184
  }
185
`;
186

187
const DatasetAttributions = ({
6✔
188
  datasetAttributions,
189
  isPalm
190
}: {
191
  datasetAttributions: DatasetAttribution[];
192
  isPalm: boolean;
193
}) => (
194
  <>
29✔
195
    {datasetAttributions?.length ? (
29!
196
      <StyledDatasetAttributionsContainer isPalm={isPalm}>
197
        {datasetAttributions.map((ds, idx) => (
198
          <a
×
199
            {...(ds.url ? {href: ds.url} : null)}
×
200
            target="_blank"
201
            rel="noopener noreferrer"
202
            key={`${ds.title}_${idx}`}
203
          >
204
            {ds.title}
205
            {idx !== datasetAttributions.length - 1 ? ', ' : null}
×
206
          </a>
207
        ))}
208
      </StyledDatasetAttributionsContainer>
209
    ) : null}
210
  </>
211
);
212

213
export const Attribution: React.FC<{
214
  showMapboxLogo: boolean;
215
  showOsmBasemapAttribution: boolean;
216
  datasetAttributions: DatasetAttribution[];
217
}> = ({showMapboxLogo = true, showOsmBasemapAttribution = false, datasetAttributions}) => {
6!
218
  const isPalm = hasMobileWidth(breakPointValues);
29✔
219

220
  const memoizedComponents = useMemo(() => {
29✔
221
    if (!showMapboxLogo) {
29!
222
      return (
×
223
        <StyledAttrbution>
224
          <EndHorizontalFlexbox>
225
            <DatasetAttributions datasetAttributions={datasetAttributions} isPalm={isPalm} />
226
            {showOsmBasemapAttribution ? (
×
227
              <div className="attrition-link">
228
                {datasetAttributions?.length ? <span className="pipe-separator">|</span> : null}
×
229
                <a
230
                  href="http://www.openstreetmap.org/copyright"
231
                  target="_blank"
232
                  rel="noopener noreferrer"
233
                >
234
                  © OpenStreetMap
235
                </a>
236
              </div>
237
            ) : null}
238
          </EndHorizontalFlexbox>
239
        </StyledAttrbution>
240
      );
241
    }
242

243
    return (
29✔
244
      <StyledAttrbution>
245
        <EndHorizontalFlexbox>
246
          <DatasetAttributions datasetAttributions={datasetAttributions} isPalm={isPalm} />
247
          <div className="attrition-link">
248
            {datasetAttributions?.length ? <span className="pipe-separator">|</span> : null}
29!
249
            {isPalm ? <MapLibLogo /> : null}
29!
250
            <a href="https://kepler.gl/policy/" target="_blank" rel="noopener noreferrer">
251
              © kepler.gl |{' '}
252
            </a>
253
            {!isPalm ? <MapLibLogo /> : null}
29!
254
          </div>
255
        </EndHorizontalFlexbox>
256
      </StyledAttrbution>
257
    );
258
  }, [showMapboxLogo, showOsmBasemapAttribution, datasetAttributions, isPalm]);
259

260
  return memoizedComponents;
29✔
261
};
262

263
MapContainerFactory.deps = [MapPopoverFactory, MapControlFactory, EditorFactory];
6✔
264

265
type MapboxStyle = string | object | undefined;
266
type PropSelector<R> = Selector<MapContainerProps, R>;
267

268
type GetMapRef = ReturnType<ReturnType<typeof getApplicationConfig>['getMap']>;
269

270
export interface MapContainerProps {
271
  visState: VisState;
272
  mapState: MapState;
273
  mapControls: MapControls;
274
  mapStyle: {bottomMapStyle?: MapboxStyle; topMapStyle?: MapboxStyle} & MapStyle;
275
  mapboxApiAccessToken: string;
276
  mapboxApiUrl: string;
277
  visStateActions: typeof VisStateActions;
278
  mapStateActions: typeof MapStateActions;
279
  uiStateActions: typeof UIStateActions;
280

281
  // optional
282
  primary?: boolean; // primary one will be reporting its size to appState
283
  readOnly?: boolean;
284
  isExport?: boolean;
285
  // onMapStyleLoaded?: (map: maplibregl.Map | ReturnType<MapRef['getMap']> | null) => void;
286
  onMapStyleLoaded?: (map: GetMapRef | null) => void;
287
  onMapRender?: (map: GetMapRef | null) => void;
288
  getMapboxRef?: (mapbox?: MapRef | null, index?: number) => void;
289
  index?: number;
290
  deleteMapLabels?: (containerId: string, layerId: string) => void;
291
  containerId?: number;
292

293
  locale?: any;
294
  theme?: any;
295
  editor?: any;
296
  MapComponent?: typeof Map;
297
  deckGlProps?: any;
298
  onDeckInitialized?: (a: any, b: any) => void;
299
  onViewStateChange?: (viewport: Viewport) => void;
300

301
  topMapContainerProps: any;
302
  bottomMapContainerProps: any;
303
  transformRequest?: any;
304

305
  datasetAttributions?: DatasetAttribution[];
306

307
  generateMapboxLayers?: typeof generateMapboxLayers;
308
  generateDeckGLLayers?: typeof computeDeckLayers;
309

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

312
  children?: React.ReactNode;
313
  deckRenderCallbacks?: {
314
    onDeckLoad?: () => void;
315
    onDeckRender?: (deckProps: Record<string, unknown>) => Record<string, unknown> | null;
316
    onDeckAfterRender?: (deckProps: Record<string, unknown>) => any;
317
  };
318
}
319

320
export default function MapContainerFactory(
321
  MapPopover: ReturnType<typeof MapPopoverFactory>,
322
  MapControl: ReturnType<typeof MapControlFactory>,
323
  Editor: ReturnType<typeof EditorFactory>
324
): React.ComponentType<MapContainerProps> {
325
  class MapContainer extends Component<MapContainerProps> {
326
    displayName = 'MapContainer';
23✔
327

328
    static contextType = MapViewStateContext;
13✔
329

330
    declare context: React.ContextType<typeof MapViewStateContext>;
331

332
    static defaultProps = {
13✔
333
      MapComponent: Map,
334
      deckGlProps: {},
335
      index: 0,
336
      primary: true
337
    };
338

339
    constructor(props) {
340
      super(props);
23✔
341
    }
342

343
    state = {
23✔
344
      // Determines whether attribution should be visible based the result of loading the map style
345
      showMapboxAttribution: true
346
    };
347

348
    componentDidMount() {
349
      if (!this._ref.current) {
23!
350
        return;
×
351
      }
352
      observeDimensions(this._ref.current, this._handleResize);
23✔
353
    }
354

355
    componentWillUnmount() {
356
      // unbind mapboxgl event listener
357
      if (this._map) {
×
358
        this._map?.off(MAPBOXGL_STYLE_UPDATE, nop);
×
359
        this._map?.off(MAPBOXGL_RENDER, nop);
×
360
      }
361
      if (!this._ref.current) {
×
362
        return;
×
363
      }
364
      unobserveDimensions(this._ref.current);
×
365
    }
366

367
    _deck: any = null;
23✔
368
    _map: GetMapRef | null = null;
23✔
369
    _ref = createRef<HTMLDivElement>();
23✔
370
    _deckGLErrorsElapsed: {[id: string]: number} = {};
23✔
371

372
    previousLayers = {
23✔
373
      // [layers.id]: mapboxLayerConfig
374
    };
375

376
    _handleResize = dimensions => {
23✔
377
      const {primary, index} = this.props;
×
378
      if (primary) {
×
379
        const {mapStateActions} = this.props;
×
380
        if (dimensions && dimensions.width > 0 && dimensions.height > 0) {
×
381
          mapStateActions.updateMap(dimensions, index);
×
382
        }
383
      }
384
    };
385

386
    layersSelector: PropSelector<VisState['layers']> = props => props.visState.layers;
56✔
387
    layerDataSelector: PropSelector<VisState['layers']> = props => props.visState.layerData;
56✔
388
    splitMapSelector: PropSelector<SplitMap[]> = props => props.visState.splitMaps;
28✔
389
    splitMapIndexSelector: PropSelector<number | undefined> = props => props.index;
28✔
390
    mapLayersSelector: PropSelector<SplitMapLayers | null | undefined> = createSelector(
23✔
391
      this.splitMapSelector,
392
      this.splitMapIndexSelector,
393
      getMapLayersFromSplitMaps
394
    );
395
    layerOrderSelector: PropSelector<VisState['layerOrder']> = props => props.visState.layerOrder;
23✔
396
    layersToRenderSelector: PropSelector<LayersToRender> = createSelector(
23✔
397
      this.layersSelector,
398
      this.layerDataSelector,
399
      this.mapLayersSelector,
400
      prepareLayersToRender
401
    );
402
    layersForDeckSelector = createSelector(
23✔
403
      this.layersSelector,
404
      this.layerDataSelector,
405
      prepareLayersForDeck
406
    );
407
    filtersSelector = props => props.visState.filters;
28✔
408
    polygonFiltersSelector = createSelector(this.filtersSelector, filters =>
23✔
409
      filters.filter(f => f.type === FILTER_TYPES.polygon && f.enabled !== false)
25!
410
    );
411
    featuresSelector = props => props.visState.editor.features;
28✔
412
    selectedFeatureSelector = props => props.visState.editor.selectedFeature;
28✔
413
    featureCollectionSelector = createSelector(
23✔
414
      this.polygonFiltersSelector,
415
      this.featuresSelector,
416
      (polygonFilters, features) => ({
25✔
417
        type: 'FeatureCollection',
418
        features: features.concat(polygonFilters.map(f => f.value))
×
419
      })
420
    );
421
    // @ts-ignore - No overload matches this call
422
    selectedPolygonIndexSelector = createSelector(
23✔
423
      this.featureCollectionSelector,
424
      this.selectedFeatureSelector,
425
      (collection, selectedFeature) =>
426
        collection.features.findIndex(f => f.id === selectedFeature?.id)
25✔
427
    );
428
    selectedFeatureIndexArraySelector = createSelector(
23✔
429
      (value: number) => value,
23✔
430
      value => {
431
        return value < 0 ? [] : [value];
23!
432
      }
433
    );
434

435
    generateMapboxLayerMethodSelector = props => props.generateMapboxLayers ?? generateMapboxLayers;
23!
436

437
    mapboxLayersSelector = createSelector(
23✔
438
      this.layersSelector,
439
      this.layerDataSelector,
440
      this.layerOrderSelector,
441
      this.layersToRenderSelector,
442
      this.generateMapboxLayerMethodSelector,
443
      (layer, layerData, layerOrder, layersToRender, generateMapboxLayerMethod) =>
444
        generateMapboxLayerMethod(layer, layerData, layerOrder, layersToRender)
×
445
    );
446

447
    // merge in a background-color style if the basemap choice is NO_MAP_ID
448
    // used by <StyledMap> inline style prop
449
    mapStyleTypeSelector = props => props.mapStyle.styleType;
28✔
450
    mapStyleBackgroundColorSelector = props => props.mapStyle.backgroundColor;
28✔
451
    styleSelector = createSelector(
23✔
452
      this.mapStyleTypeSelector,
453
      this.mapStyleBackgroundColorSelector,
454
      (styleType, backgroundColor) => ({
25✔
455
        ...MAP_STYLE.container,
456
        ...(styleType === NO_MAP_ID ? {backgroundColor: rgbToHex(backgroundColor)} : {})
25!
457
      })
458
    );
459

460
    /* component private functions */
461
    _onCloseMapPopover = () => {
23✔
462
      this.props.visStateActions.onLayerClick(null);
×
463
    };
464

465
    _onLayerHover = (_idx: number, info: PickInfo<any> | null) => {
23✔
466
      this.props.visStateActions.onLayerHover(info, this.props.index);
×
467
    };
468

469
    _onLayerSetDomain = (
23✔
470
      idx: number,
471
      value: {domain: VisualChannelDomain; aggregatedBins: AggregatedBin[]}
472
    ) => {
UNCOV
473
      this.props.visStateActions.layerConfigChange(this.props.visState.layers[idx], {
×
474
        colorDomain: value.domain,
475
        aggregatedBins: value.aggregatedBins
476
      } as Partial<LayerBaseConfig>);
477
    };
478

479
    _onLayerFilteredItemsChange = (idx, event) => {
23✔
480
      this.props.visStateActions.layerFilteredItemsChange(this.props.visState.layers[idx], event);
×
481
    };
482

483
    _handleMapToggleLayer = layerId => {
23✔
484
      const {index: mapIndex = 0, visStateActions} = this.props;
1!
485
      visStateActions.toggleLayerForMap(mapIndex, layerId);
1✔
486
    };
487

488
    _onMapboxStyleUpdate = update => {
23✔
489
      // force refresh mapboxgl layers
490
      this.previousLayers = {};
×
491
      this._updateMapboxLayers();
×
492

493
      if (update && update.style) {
×
494
        // No attributions are needed if the style doesn't reference Mapbox sources
495
        this.setState({showMapboxAttribution: isStyleUsingMapboxTiles(update.style)});
×
496
      }
497

498
      if (typeof this.props.onMapStyleLoaded === 'function') {
×
499
        this.props.onMapStyleLoaded(this._map);
×
500
      }
501
    };
502

503
    _setMapRef = mapRef => {
23✔
504
      if (!this._map && mapRef) {
×
505
        this._map = getApplicationConfig().getMap(mapRef);
×
506
        // i noticed in certain context we don't access the actual map element
507
        if (!this._map) {
×
508
          return;
×
509
        }
510
        // bind mapboxgl event listener
511
        this._map.on(MAPBOXGL_STYLE_UPDATE, this._onMapboxStyleUpdate);
×
512

513
        this._map.on(MAPBOXGL_RENDER, () => {
×
514
          if (typeof this.props.onMapRender === 'function') {
×
515
            this.props.onMapRender(this._map);
×
516
          }
517
        });
518
      }
519

520
      if (this.props.getMapboxRef) {
×
521
        // The parent component can gain access to our MapboxGlMap by
522
        // providing this callback. Note that 'mapbox' will be null when the
523
        // ref is unset (e.g. when a split map is closed).
524
        this.props.getMapboxRef(mapRef, this.props.index);
×
525
      }
526
    };
527

528
    _onDeckInitialized(gl) {
529
      if (this.props.onDeckInitialized) {
×
530
        this.props.onDeckInitialized(this._deck, gl);
×
531
      }
532
    }
533

534
    /**
535
     * 1) Allow effects only for the first view.
536
     * 2) Prevent effect:preRender call without valid generated viewports.
537
     * @param viewIndex View index.
538
     * @returns Returns true if effects can be used.
539
     */
540
    _isOKToRenderEffects(viewIndex?: number): boolean {
541
      return !viewIndex && Boolean(this._deck?.viewManager?._viewports?.length);
30✔
542
    }
543

544
    _onBeforeRender = ({gl}) => {
23✔
545
      setLayerBlending(gl, this.props.visState.layerBlending);
×
546
    };
547

548
    _onDeckError = (error, layer) => {
23✔
549
      const errorMessage = error?.message || 'unknown-error';
23!
550
      const layerMessage = layer?.id ? ` in ${layer.id} layer` : '';
23!
551
      const errorMessageFull =
552
        errorMessage === 'WebGL context is lost'
23!
553
          ? '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.'
554
          : `An error in deck.gl: ${errorMessage}${layerMessage}.`;
555

556
      // Throttle error notifications, as React doesn't like too many state changes from here.
557
      const lastShown = this._deckGLErrorsElapsed[errorMessageFull];
23✔
558
      if (!lastShown || lastShown < Date.now() - THROTTLE_NOTIFICATION_TIME) {
23!
559
        this._deckGLErrorsElapsed[errorMessageFull] = Date.now();
23✔
560

561
        // Mark layer as invalid
562
        let extraLayerMessage = '';
23✔
563
        const {visStateActions} = this.props;
23✔
564
        if (layer) {
23!
565
          let topMostLayer = layer;
×
566
          while (topMostLayer.parent) {
×
567
            topMostLayer = topMostLayer.parent;
×
568
          }
569
          if (topMostLayer.props?.id) {
×
570
            visStateActions.layerSetIsValid(topMostLayer, false);
×
571
            extraLayerMessage = 'The layer has been disabled and highlighted.';
×
572
          }
573
        }
574

575
        // Create new error notification or update existing one with same id.
576
        // Update is required to preserve the order of notifications as they probably are going to "jump" based on order of errors.
577
        const {uiStateActions} = this.props;
23✔
578
        uiStateActions.addNotification(
23✔
579
          errorNotification({
580
            message: `${errorMessageFull} ${extraLayerMessage}`,
581
            id: errorMessageFull // treat the error message as id
582
          })
583
        );
584
      }
585
    };
586

587
    /* component render functions */
588

589
    /* eslint-disable complexity */
590
    _renderMapPopover() {
591
      // this check is for limiting the display of the `<MapPopover>` to the `<MapContainer>` the user is interacting with
592
      // the DeckGL onHover event handler adds a `mapIndex` property which is available in the `hoverInfo` object of `visState`
593
      if (this.props.index !== this.props.visState.hoverInfo?.mapIndex) {
30!
594
        return null;
30✔
595
      }
596

597
      // TODO: move this into reducer so it can be tested
598
      const {
599
        mapState,
600
        visState: {
601
          hoverInfo,
602
          clicked,
603
          datasets,
604
          interactionConfig,
605
          animationConfig,
606
          layers,
607
          mousePos: {mousePosition, coordinate, pinned}
608
        }
609
      } = this.props;
×
610
      const layersToRender = this.layersToRenderSelector(this.props);
×
611

612
      if (!mousePosition || !interactionConfig.tooltip) {
×
613
        return null;
×
614
      }
615

616
      const layerHoverProp = getLayerHoverProp({
×
617
        animationConfig,
618
        interactionConfig,
619
        hoverInfo,
620
        layers,
621
        layersToRender,
622
        datasets
623
      });
624

625
      const compareMode = interactionConfig.tooltip.config
×
626
        ? interactionConfig.tooltip.config.compareMode
627
        : false;
628

629
      let pinnedPosition = {x: 0, y: 0};
×
630
      let layerPinnedProp: LayerHoverProp | null = null;
×
631
      if (pinned || clicked) {
×
632
        // project lnglat to screen so that tooltip follows the object on zoom
633
        const viewport = getViewportFromMapState(mapState);
×
634
        const lngLat = clicked ? clicked.coordinate : pinned.coordinate;
×
635
        pinnedPosition = this._getHoverXY(viewport, lngLat);
×
636
        layerPinnedProp = getLayerHoverProp({
×
637
          animationConfig,
638
          interactionConfig,
639
          hoverInfo: clicked,
640
          layers,
641
          layersToRender,
642
          datasets
643
        });
644
        if (layerHoverProp && layerPinnedProp) {
×
645
          layerHoverProp.primaryData = layerPinnedProp.data;
×
646
          layerHoverProp.compareType = interactionConfig.tooltip.config.compareType;
×
647
        }
648
      }
649

650
      const commonProp = {
×
651
        onClose: this._onCloseMapPopover,
652
        zoom: mapState.zoom,
653
        container: this._deck ? this._deck.canvas : undefined
×
654
      };
655

656
      return (
×
657
        <ErrorBoundary>
658
          {layerPinnedProp && (
×
659
            <MapPopover
660
              {...pinnedPosition}
661
              {...commonProp}
662
              layerHoverProp={layerPinnedProp}
663
              coordinate={interactionConfig.coordinate.enabled && (pinned || {}).coordinate}
×
664
              frozen={true}
665
              isBase={compareMode}
666
              onSetFeatures={this.props.visStateActions.setFeatures}
667
              setSelectedFeature={this.props.visStateActions.setSelectedFeature}
668
              // @ts-ignore Argument of type 'Readonly<MapContainerProps>' is not assignable to parameter of type 'never'
669
              featureCollection={this.featureCollectionSelector(this.props)}
670
            />
671
          )}
672
          {layerHoverProp && (!layerPinnedProp || compareMode) && (
×
673
            <MapPopover
674
              x={mousePosition[0]}
675
              y={mousePosition[1]}
676
              {...commonProp}
677
              layerHoverProp={layerHoverProp}
678
              frozen={false}
679
              coordinate={interactionConfig.coordinate.enabled && coordinate}
×
680
              onSetFeatures={this.props.visStateActions.setFeatures}
681
              setSelectedFeature={this.props.visStateActions.setSelectedFeature}
682
              // @ts-ignore Argument of type 'Readonly<MapContainerProps>' is not assignable to parameter of type 'never'
683
              featureCollection={this.featureCollectionSelector(this.props)}
684
            />
685
          )}
686
        </ErrorBoundary>
687
      );
688
    }
689

690
    /* eslint-enable complexity */
691

692
    _getHoverXY(viewport, lngLat) {
693
      const screenCoord = !viewport || !lngLat ? null : viewport.project(lngLat);
×
694
      return screenCoord && {x: screenCoord[0], y: screenCoord[1]};
×
695
    }
696

697
    _renderDeckOverlay(
698
      layersForDeck,
699
      options: {primaryMap: boolean; isInteractive?: boolean; children?: React.ReactNode} = {
×
700
        primaryMap: false
701
      }
702
    ) {
703
      const {
704
        mapStyle,
705
        visState,
706
        mapState,
707
        visStateActions,
708
        mapboxApiAccessToken,
709
        mapboxApiUrl,
710
        deckGlProps,
711
        index,
712
        mapControls,
713
        deckRenderCallbacks,
714
        theme,
715
        generateDeckGLLayers,
716
        onMouseMove
717
      } = this.props;
30✔
718

719
      const {hoverInfo, editor} = visState;
30✔
720
      const {primaryMap, isInteractive, children} = options;
30✔
721

722
      // disable double click zoom when editor is in any draw mode
723
      const {mapDraw} = mapControls;
30✔
724
      const {active: editorMenuActive = false} = mapDraw || {};
30✔
725
      const isEditorDrawingMode = EditorLayerUtils.isDrawingActive(editorMenuActive, editor.mode);
30✔
726

727
      const internalViewState = this.context?.getInternalViewState(index);
30✔
728
      const internalMapState = {...mapState, ...internalViewState};
30✔
729
      const viewport = getViewportFromMapState(internalMapState);
30✔
730

731
      const editorFeatureSelectedIndex = this.selectedPolygonIndexSelector(this.props);
30✔
732

733
      const {setFeatures, onLayerClick, setSelectedFeature} = visStateActions;
30✔
734

735
      const generateDeckGLLayersMethod = generateDeckGLLayers ?? computeDeckLayers;
30✔
736
      const deckGlLayers = generateDeckGLLayersMethod(
30✔
737
        {
738
          visState,
739
          mapState: internalMapState,
740
          mapStyle
741
        },
742
        {
743
          mapIndex: index,
744
          primaryMap,
745
          mapboxApiAccessToken,
746
          mapboxApiUrl,
747
          layersForDeck,
748
          editorInfo: primaryMap
30!
749
            ? {
750
                editor,
751
                editorMenuActive,
752
                onSetFeatures: setFeatures,
753
                setSelectedFeature,
754
                // @ts-ignore Argument of type 'Readonly<MapContainerProps>' is not assignable to parameter of type 'never'
755
                featureCollection: this.featureCollectionSelector(this.props),
756
                selectedFeatureIndexes: this.selectedFeatureIndexArraySelector(
757
                  // @ts-ignore Argument of type 'unknown' is not assignable to parameter of type 'number'.
758
                  editorFeatureSelectedIndex
759
                ),
760
                viewport
761
              }
762
            : undefined
763
        },
764
        {
765
          onLayerHover: this._onLayerHover,
766
          onSetLayerDomain: this._onLayerSetDomain,
767
          onFilteredItemsChange: this._onLayerFilteredItemsChange
768
        },
769
        deckGlProps
770
      );
771

772
      const extraDeckParams: {
773
        getTooltip?: (info: any) => object | null;
774
        getCursor?: ({isDragging}: {isDragging: boolean}) => string;
775
      } = {};
30✔
776
      if (primaryMap) {
30!
777
        extraDeckParams.getTooltip = info =>
30✔
778
          EditorLayerUtils.getTooltip(info, {
×
779
            editorMenuActive,
780
            editor,
781
            theme
782
          });
783

784
        extraDeckParams.getCursor = ({isDragging}: {isDragging: boolean}) => {
30✔
785
          const editorCursor = EditorLayerUtils.getCursor({
×
786
            editorMenuActive,
787
            editor,
788
            hoverInfo
789
          });
790
          if (editorCursor) return editorCursor;
×
791

792
          if (isDragging) return 'grabbing';
×
793
          if (hoverInfo?.layer) return 'pointer';
×
794
          return 'grab';
×
795
        };
796
      }
797

798
      const effects = this._isOKToRenderEffects(index)
30!
799
        ? computeDeckEffects({visState, mapState})
800
        : [];
801

802
      const views = deckGlProps?.views
30!
803
        ? deckGlProps?.views()
804
        : new MapView({legacyMeterSizes: true});
805

806
      let allDeckGlProps = {
30✔
807
        ...deckGlProps,
808
        pickingRadius: DEFAULT_PICKING_RADIUS,
809
        views,
810
        layers: deckGlLayers,
811
        effects
812
      };
813

814
      if (typeof deckRenderCallbacks?.onDeckRender === 'function') {
30!
815
        allDeckGlProps = deckRenderCallbacks.onDeckRender(allDeckGlProps);
×
816
        if (!allDeckGlProps) {
×
817
          // if onDeckRender returns null, do not render deck.gl
818
          return null;
×
819
        }
820
      }
821

822
      return (
30✔
823
        <div
824
          {...(isInteractive
30!
825
            ? {
826
                onMouseMove: primaryMap
30!
827
                  ? event => {
828
                      onMouseMove?.(event);
×
829
                      this._onMouseMoveDebounced(event, viewport);
×
830
                    }
831
                  : undefined
832
              }
833
            : {style: {pointerEvents: 'none'}})}
834
        >
835
          <DeckGL
836
            id="default-deckgl-overlay"
837
            onLoad={() => {
838
              if (typeof deckRenderCallbacks?.onDeckLoad === 'function') {
×
839
                deckRenderCallbacks.onDeckLoad();
×
840
              }
841
            }}
842
            {...allDeckGlProps}
843
            controller={
844
              isInteractive
30!
845
                ? {
846
                    doubleClickZoom: !isEditorDrawingMode,
847
                    dragRotate: this.props.mapState.dragRotate
848
                  }
849
                : false
850
            }
851
            initialViewState={internalViewState}
852
            onBeforeRender={this._onBeforeRender}
853
            onViewStateChange={isInteractive ? this._onViewportChange : undefined}
30!
854
            {...extraDeckParams}
855
            onHover={
856
              isInteractive
30!
857
                ? data => {
858
                    const res = EditorLayerUtils.onHover(data, {
×
859
                      editorMenuActive,
860
                      editor,
861
                      hoverInfo
862
                    });
863
                    if (res) return;
×
864

865
                    this._onLayerHoverDebounced(data, index);
×
866
                  }
867
                : null
868
            }
869
            onClick={(data, event) => {
870
              // @ts-ignore
871
              normalizeEvent(event.srcEvent, viewport);
×
872
              const res = EditorLayerUtils.onClick(data, event, {
×
873
                editorMenuActive,
874
                editor,
875
                onLayerClick,
876
                setSelectedFeature,
877
                mapIndex: index
878
              });
879
              if (res) return;
×
880

881
              visStateActions.onLayerClick(data);
×
882
            }}
883
            onError={this._onDeckError}
884
            ref={comp => {
885
              // @ts-ignore
886
              if (comp && comp.deck && !this._deck) {
37✔
887
                // @ts-ignore
888
                this._deck = comp.deck;
2✔
889
              }
890
            }}
891
            onWebGLInitialized={gl => this._onDeckInitialized(gl)}
×
892
            onAfterRender={() => {
893
              if (typeof deckRenderCallbacks?.onDeckAfterRender === 'function') {
×
894
                deckRenderCallbacks.onDeckAfterRender(allDeckGlProps);
×
895
              }
896
            }}
897
          >
898
            {children}
899
          </DeckGL>
900
        </div>
901
      );
902
    }
903

904
    _updateMapboxLayers() {
905
      const mapboxLayers = this.mapboxLayersSelector(this.props);
×
906
      if (!Object.keys(mapboxLayers).length && !Object.keys(this.previousLayers).length) {
×
907
        return;
×
908
      }
909

910
      updateMapboxLayers(this._map, mapboxLayers, this.previousLayers);
×
911

912
      this.previousLayers = mapboxLayers;
×
913
    }
914

915
    _renderMapboxOverlays() {
916
      if (this._map && this._map.isStyleLoaded()) {
30!
917
        this._updateMapboxLayers();
×
918
      }
919
    }
920
    _onViewportChangePropagateDebounced = debounce(() => {
23✔
921
      const viewState = this.context?.getInternalViewState(this.props.index);
×
922
      onViewPortChange(
×
923
        viewState,
924
        this.props.mapStateActions.updateMap,
925
        this.props.onViewStateChange,
926
        this.props.primary,
927
        this.props.index
928
      );
929
    }, DEBOUNCE_VIEWPORT_PROPAGATE);
930

931
    _onViewportChange = viewport => {
23✔
932
      const {viewState} = viewport;
×
933
      if (this.props.isExport) {
×
934
        // Image export map shouldn't be interactive (otherwise this callback can
935
        // lead to inadvertent changes to the state of the main map)
936
        return;
×
937
      }
938
      const {setInternalViewState} = this.context;
×
939
      setInternalViewState(viewState, this.props.index);
×
940
      this._onViewportChangePropagateDebounced();
×
941
    };
942

943
    _onLayerHoverDebounced = debounce((data, index) => {
23✔
944
      this.props.visStateActions.onLayerHover(data, index);
×
945
    }, DEBOUNCE_MOUSE_MOVE_PROPAGATE);
946

947
    _onMouseMoveDebounced = debounce((event, viewport) => {
23✔
948
      this.props.visStateActions.onMouseMove(normalizeEvent(event, viewport));
×
949
    }, DEBOUNCE_MOUSE_MOVE_PROPAGATE);
950

951
    _toggleMapControl = panelId => {
23✔
952
      const {index, uiStateActions} = this.props;
3✔
953

954
      uiStateActions.toggleMapControl(panelId, Number(index));
3✔
955
    };
956

957
    /* eslint-disable complexity */
958
    _renderMap() {
959
      const {
960
        visState,
961
        mapState,
962
        mapStyle,
963
        mapStateActions,
964
        MapComponent = Map,
×
965
        mapboxApiAccessToken,
966
        mapboxApiUrl,
967
        mapControls,
968
        isExport,
969
        locale,
970
        uiStateActions,
971
        visStateActions,
972
        index,
973
        primary,
974
        bottomMapContainerProps,
975
        topMapContainerProps,
976
        theme,
977
        datasetAttributions = [],
30✔
978
        containerId = 0
14✔
979
      } = this.props;
30✔
980

981
      const {layers, datasets, editor, interactionConfig} = visState;
30✔
982

983
      const layersToRender = this.layersToRenderSelector(this.props);
30✔
984
      const layersForDeck = this.layersForDeckSelector(this.props);
30✔
985

986
      // Current style can be a custom style, from which we pull the mapbox API acccess token
987
      const currentStyle = mapStyle.mapStyles?.[mapStyle.styleType];
30✔
988
      const internalViewState = this.context?.getInternalViewState(index);
30✔
989
      const mapProps = {
30✔
990
        ...internalViewState,
991
        preserveDrawingBuffer: true,
992
        mapboxAccessToken: currentStyle?.accessToken || mapboxApiAccessToken,
60✔
993
        baseApiUrl: mapboxApiUrl,
994
        mapLib: getApplicationConfig().getMapLib(),
995
        transformRequest:
996
          this.props.transformRequest ||
60✔
997
          transformRequest(currentStyle?.accessToken || mapboxApiAccessToken)
60✔
998
      };
999

1000
      const hasGeocoderLayer = Boolean(layers.find(l => l.id === GEOCODER_LAYER_ID));
30✔
1001
      const isSplit = Boolean(mapState.isSplit);
30✔
1002

1003
      const deck = this._renderDeckOverlay(layersForDeck, {
30✔
1004
        primaryMap: true,
1005
        isInteractive: true,
1006
        children: (
1007
          <MapComponent
1008
            key="bottom"
1009
            {...mapProps}
1010
            mapStyle={mapStyle.bottomMapStyle ?? EMPTY_MAPBOX_STYLE}
60✔
1011
            {...bottomMapContainerProps}
1012
            ref={this._setMapRef}
1013
          />
1014
        )
1015
      });
1016
      if (!deck) {
30!
1017
        // deckOverlay can be null if onDeckRender returns null
1018
        // in this case we don't want to render the map
1019
        return null;
×
1020
      }
1021
      return (
30✔
1022
        <>
1023
          <MapControl
1024
            mapState={mapState}
1025
            datasets={datasets}
1026
            availableLocales={LOCALE_CODES_ARRAY}
1027
            dragRotate={mapState.dragRotate}
1028
            isSplit={isSplit}
1029
            primary={Boolean(primary)}
1030
            isExport={isExport}
1031
            layers={layers}
1032
            layersToRender={layersToRender}
1033
            mapIndex={index || 0}
55✔
1034
            mapControls={mapControls}
1035
            readOnly={this.props.readOnly}
1036
            scale={mapState.scale || 1}
60✔
1037
            top={
1038
              interactionConfig.geocoder && interactionConfig.geocoder.enabled
90!
1039
                ? theme.mapControlTop
1040
                : 0
1041
            }
1042
            editor={editor}
1043
            locale={locale}
1044
            onTogglePerspective={mapStateActions.togglePerspective}
1045
            onToggleSplitMap={mapStateActions.toggleSplitMap}
1046
            onMapToggleLayer={this._handleMapToggleLayer}
1047
            onToggleMapControl={this._toggleMapControl}
1048
            onToggleSplitMapViewport={mapStateActions.toggleSplitMapViewport}
1049
            onSetEditorMode={visStateActions.setEditorMode}
1050
            onSetLocale={uiStateActions.setLocale}
1051
            onToggleEditorVisibility={visStateActions.toggleEditorVisibility}
1052
            onLayerVisConfigChange={visStateActions.layerVisConfigChange}
1053
            mapHeight={mapState.height}
1054
          />
1055
          {isSplitSelector(this.props) && <Droppable containerId={containerId} />}
40✔
1056

1057
          {deck}
1058
          {this._renderMapboxOverlays()}
1059
          <Editor
1060
            index={index || 0}
55✔
1061
            datasets={datasets}
1062
            editor={editor}
1063
            filters={this.polygonFiltersSelector(this.props)}
1064
            layers={layers}
1065
            onDeleteFeature={visStateActions.deleteFeature}
1066
            onSelect={visStateActions.setSelectedFeature}
1067
            onTogglePolygonFilter={visStateActions.setPolygonFilterLayer}
1068
            onSetEditorMode={visStateActions.setEditorMode}
1069
            style={{
1070
              pointerEvents: 'all',
1071
              position: 'absolute',
1072
              display: editor.visible ? 'block' : 'none'
30!
1073
            }}
1074
          />
1075
          {this.props.children}
1076
          {mapStyle.topMapStyle ? (
30!
1077
            <MapComponent
1078
              key="top"
1079
              viewState={internalViewState}
1080
              mapStyle={mapStyle.topMapStyle}
1081
              style={MAP_STYLE.top}
1082
              mapboxAccessToken={mapProps.mapboxAccessToken}
1083
              baseApiUrl={mapProps.baseApiUrl}
1084
              mapLib={getApplicationConfig().getMapLib()}
1085
              {...topMapContainerProps}
1086
            />
1087
          ) : null}
1088

1089
          {hasGeocoderLayer
30!
1090
            ? this._renderDeckOverlay(
1091
                {[GEOCODER_LAYER_ID]: hasGeocoderLayer},
1092
                {primaryMap: false, isInteractive: false}
1093
              )
1094
            : null}
1095
          {this._renderMapPopover()}
1096
          {this.props.primary ? (
30✔
1097
            <Attribution
1098
              showMapboxLogo={this.state.showMapboxAttribution}
1099
              showOsmBasemapAttribution={true}
1100
              datasetAttributions={datasetAttributions}
1101
            />
1102
          ) : null}
1103
        </>
1104
      );
1105
    }
1106

1107
    render() {
1108
      const {visState} = this.props;
30✔
1109
      const mapContent = this._renderMap();
30✔
1110
      if (!mapContent) {
30!
1111
        // mapContent can be null if onDeckRender returns null
1112
        // in this case we don't want to render the map
1113
        return null;
×
1114
      }
1115
      return (
30✔
1116
        <StyledMap
1117
          ref={this._ref}
1118
          style={this.styleSelector(this.props)}
1119
          onContextMenu={event => event.preventDefault()}
×
1120
          mixBlendMode={visState.overlayBlending}
1121
        >
1122
          {mapContent}
1123
        </StyledMap>
1124
      );
1125
    }
1126
  }
1127

1128
  return withTheme(MapContainer);
13✔
1129
}
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