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

keplergl / kepler.gl / 13608482230

01 Mar 2025 08:22PM UTC coverage: 66.282% (+0.1%) from 66.159%
13608482230

Pull #3011

github

web-flow
Merge 252d3c6ad into 9762dc379
Pull Request #3011: [chore] fix react deprecation warnings

6044 of 10638 branches covered (56.82%)

Branch coverage included in aggregate %.

79 of 99 new or added lines in 6 files covered. (79.8%)

1 existing line in 1 file now uncovered.

12407 of 17199 relevant lines covered (72.14%)

88.2 hits per line

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

72.41
/src/components/src/geocoder-panel.tsx
1
// SPDX-License-Identifier: MIT
2
// Copyright contributors to the kepler.gl project
3

4
import React, {useCallback} from 'react';
5
import styled, {IStyledComponent} from 'styled-components';
6
import classnames from 'classnames';
7
import {processRowObject} from '@kepler.gl/processors';
8
import {FlyToInterpolator} from '@deck.gl/core/typed';
9
import {getCenterAndZoomFromBounds} from '@kepler.gl/utils';
10
import {
11
  GEOCODER_DATASET_NAME,
12
  GEOCODER_LAYER_ID,
13
  GEOCODER_GEO_OFFSET,
14
  GEOCODER_ICON_COLOR,
15
  GEOCODER_ICON_SIZE
16
} from '@kepler.gl/constants';
17
import {AddDataToMapOptions, MapState, ProtoDataset, UiState, Viewport} from '@kepler.gl/types';
18
import {ActionHandler, removeDataset, updateMap, updateVisData} from '@kepler.gl/actions';
19

20
import Geocoder, {Result} from './geocoder/geocoder';
21
import {MapViewState} from '@deck.gl/core/typed';
22

23
import {BaseComponentProps} from './types';
24

25
const ICON_LAYER = {
7✔
26
  id: GEOCODER_LAYER_ID,
27
  type: 'icon',
28
  config: {
29
    label: 'Geocoder Layer',
30
    color: GEOCODER_ICON_COLOR,
31
    dataId: GEOCODER_DATASET_NAME,
32
    columns: {
33
      lat: 'lt',
34
      lng: 'ln',
35
      icon: 'icon',
36
      label: 'text'
37
    },
38
    isVisible: true,
39
    hidden: true,
40
    visConfig: {
41
      radius: GEOCODER_ICON_SIZE
42
    }
43
  }
44
};
45

46
function generateConfig(layerOrder) {
47
  return {
2✔
48
    visState: {
49
      layers: [ICON_LAYER],
50
      layerOrder: [ICON_LAYER.id, ...layerOrder]
51
    }
52
  };
53
}
54

55
export type StyledGeocoderPanelProps = BaseComponentProps & {
56
  unsyncedViewports?: boolean;
57
  index?: number;
58
  width?: number;
59
};
60

61
const StyledGeocoderPanel: IStyledComponent<
62
  'web',
63
  StyledGeocoderPanelProps
64
> = styled.div<StyledGeocoderPanelProps>`
7✔
65
  position: absolute;
66
  top: ${props => props.theme.geocoderTop}px;
1✔
67
  right: ${props =>
68
    props.unsyncedViewports
1!
69
      ? // 2 geocoders: split mode and unsynced viewports
70
        Number.isFinite(props.index) && props.index === 0
×
71
        ? `calc(50% + ${props.theme.geocoderRight}px)` // unsynced left geocoder (index 0)
72
        : `${props.theme.geocoderRight}px` // unsynced right geocoder (index 1)
73
      : // 1 geocoder: single mode OR split mode and synced viewports
74
        `${props.theme.geocoderRight}px`};
75
  width: ${props => (Number.isFinite(props.width) ? props.width : props.theme.geocoderWidth)}px;
1!
76
  box-shadow: ${props => props.theme.boxShadow};
1✔
77
  z-index: 100;
78
`;
79

80
function generateGeocoderDataset(lat: number, lon: number, text?: string): ProtoDataset | null {
81
  const data = processRowObject([
4✔
82
    {
83
      lt: lat,
84
      ln: lon,
85
      icon: 'place',
86
      text
87
    }
88
  ]);
89
  if (!data) {
4!
90
    return null;
×
91
  }
92

93
  return {
4✔
94
    data,
95
    info: {
96
      hidden: true,
97
      id: GEOCODER_DATASET_NAME,
98
      label: GEOCODER_DATASET_NAME
99
    }
100
  };
101
}
102

103
function isValid(key) {
104
  return /pk\..*\..*/.test(key);
1✔
105
}
106

107
export function getUpdateVisDataPayload(
108
  lat: number,
109
  lon: number,
110
  text?: string
111
): [ProtoDataset[], AddDataToMapOptions] | null {
112
  const dataset = generateGeocoderDataset(lat, lon, text);
4✔
113
  if (!dataset) {
4!
114
    return null;
×
115
  }
116
  return [
4✔
117
    [dataset],
118
    {
119
      keepExistingConfig: true
120
    }
121
  ];
122
}
123

124
interface GeocoderPanelProps {
125
  isGeocoderEnabled: boolean;
126
  mapState: MapState;
127
  uiState: UiState;
128
  mapboxApiAccessToken: string;
129
  updateVisData: ActionHandler<typeof updateVisData>;
130
  removeDataset: ActionHandler<typeof removeDataset>;
131
  updateMap: ActionHandler<typeof updateMap>;
132
  layerOrder: string[];
133

134
  transitionDuration?: MapViewState['transitionDuration'];
135
  width?: number;
136
  className?: string;
137
  index: number;
138
  unsyncedViewports: boolean;
139
}
140

141
export default function GeocoderPanelFactory(): React.FC<GeocoderPanelProps> {
142
  const GeocoderPanel = ({
14✔
143
    isGeocoderEnabled,
144
    mapState,
145
    mapboxApiAccessToken,
146
    updateVisData,
147
    removeDataset,
148
    updateMap,
149
    layerOrder,
150
    transitionDuration = 3000,
1✔
151
    width,
152
    className,
153
    index,
154
    unsyncedViewports
155
  }: GeocoderPanelProps) => {
156
    const removeGeocoderDataset = useCallback(() => {
1✔
157
      removeDataset(GEOCODER_DATASET_NAME);
3✔
158
    }, [removeDataset]);
159

160
    const onSelected = useCallback(
1✔
161
      (_viewport: Viewport | null = null, geoItem: Result) => {
×
162
        const {
163
          center: [lon, lat],
164
          text,
165
          bbox
166
        } = geoItem;
2✔
167

168
        const updateVisDataPayload = getUpdateVisDataPayload(lat, lon, text);
2✔
169
        if (updateVisDataPayload) {
2!
170
          removeGeocoderDataset();
2✔
171
          updateVisData(...updateVisDataPayload, generateConfig(layerOrder));
2✔
172
        }
173

174
        const bounds = bbox || [
2✔
175
          lon - GEOCODER_GEO_OFFSET,
176
          lat - GEOCODER_GEO_OFFSET,
177
          lon + GEOCODER_GEO_OFFSET,
178
          lat + GEOCODER_GEO_OFFSET
179
        ];
180
        const centerAndZoom = getCenterAndZoomFromBounds(bounds, {
2✔
181
          width: mapState.width,
182
          height: mapState.height
183
        });
184

185
        if (!centerAndZoom) {
2!
186
          // failed to fit bounds
NEW
187
          return;
×
188
        }
189

190
        updateMap(
2✔
191
          {
192
            latitude: centerAndZoom.center[1],
193
            longitude: centerAndZoom.center[0],
194
            // For marginal or invalid bounds, zoom may be NaN. Make sure to provide a valid value in order
195
            // to avoid corrupt state and potential crashes as zoom is expected to be a number
196
            ...(Number.isFinite(centerAndZoom.zoom) ? {zoom: centerAndZoom.zoom} : {}),
2!
197
            pitch: 0,
198
            bearing: 0,
199
            transitionDuration,
200
            transitionInterpolator: new FlyToInterpolator()
201
          },
202
          index
203
        );
204
      },
205
      [
206
        index,
207
        layerOrder,
208
        mapState,
209
        removeGeocoderDataset,
210
        transitionDuration,
211
        updateMap,
212
        updateVisData
213
      ]
214
    );
215

216
    return (
1✔
217
      <StyledGeocoderPanel
218
        className={classnames('geocoder-panel', className)}
219
        width={width}
220
        index={index}
221
        unsyncedViewports={unsyncedViewports}
222
        style={{display: isGeocoderEnabled ? 'block' : 'none'}}
1!
223
      >
224
        {isValid(mapboxApiAccessToken) && (
2✔
225
          <Geocoder
226
            mapboxApiAccessToken={mapboxApiAccessToken}
227
            onSelected={onSelected}
228
            onDeleteMarker={removeGeocoderDataset}
229
            width={width}
230
          />
231
        )}
232
      </StyledGeocoderPanel>
233
    );
234
  };
235

236
  return GeocoderPanel;
14✔
237
}
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