// Copyright (c) 2022 Uber Technologies, Inc. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import React, {Component, ComponentType} from 'react'; import styled from 'styled-components'; import {processRowObject} from 'processors'; import {FlyToInterpolator} from '@deck.gl/core'; import KeplerGlSchema from 'schemas'; import {getCenterAndZoomFromBounds} from 'utils/projection-utils'; import Geocoder, {Result} from './geocoder/geocoder'; import { GEOCODER_DATASET_NAME, GEOCODER_LAYER_ID, GEOCODER_GEO_OFFSET, GEOCODER_ICON_COLOR, GEOCODER_ICON_SIZE } from 'constants/default-settings'; import {MapState, Viewport} from 'reducers'; const ICON_LAYER = { id: GEOCODER_LAYER_ID, type: 'icon', config: { label: 'Geocoder Layer', color: GEOCODER_ICON_COLOR, dataId: GEOCODER_DATASET_NAME, columns: { lat: 'lt', lng: 'ln', icon: 'icon', label: 'text' }, isVisible: true, hidden: true, visConfig: { radius: GEOCODER_ICON_SIZE } } }; const PARSED_CONFIG = KeplerGlSchema.parseSavedConfig({ version: 'v1', config: { visState: { layers: [ICON_LAYER] } } }); interface StyledGeocoderPanelProps { width?: number; } const StyledGeocoderPanel = styled.div` position: absolute; top: ${props => props.theme.geocoderTop}px; right: ${props => props.theme.geocoderRight}px; width: ${props => (Number.isFinite(props.width) ? props.width : props.theme.geocoderWidth)}px; box-shadow: ${props => props.theme.boxShadow}; z-index: 100; `; function generateGeocoderDataset(lat, lon, text) { return { data: processRowObject([ { lt: lat, ln: lon, icon: 'place', text } ]), id: GEOCODER_DATASET_NAME, info: { hidden: true, id: GEOCODER_DATASET_NAME, label: GEOCODER_DATASET_NAME } }; } function isValid(key) { return /pk\..*\..*/.test(key); } export function getUpdateVisDataPayload(lat, lon, text) { return [ [generateGeocoderDataset(lat, lon, text)], { keepExistingConfig: true }, PARSED_CONFIG ]; } interface GeocoderPanelProps { isGeocoderEnabled: boolean; mapState: MapState; mapboxApiAccessToken: string; updateVisData: Function; removeDataset: Function; updateMap: Function; transitionDuration?: number; width?: number; } export default function GeocoderPanelFactory(): ComponentType { class GeocoderPanel extends Component { defaultProps = { transitionDuration: 3000 }; removeGeocoderDataset() { this.props.removeDataset(GEOCODER_DATASET_NAME); } onSelected = (viewport: Viewport | null = null, geoItem: Result) => { const { center: [lon, lat], text, bbox } = geoItem; this.removeGeocoderDataset(); this.props.updateVisData(...getUpdateVisDataPayload(lat, lon, text)); const bounds = bbox || [ lon - GEOCODER_GEO_OFFSET, lat - GEOCODER_GEO_OFFSET, lon + GEOCODER_GEO_OFFSET, lat + GEOCODER_GEO_OFFSET ]; const centerAndZoom = getCenterAndZoomFromBounds(bounds, { width: this.props.mapState.width, height: this.props.mapState.height }); if (!centerAndZoom) { // failed to fit bounds return; } this.props.updateMap({ latitude: centerAndZoom.center[1], longitude: centerAndZoom.center[0], // For marginal or invalid bounds, zoom may be NaN. Make sure to provide a valid value in order // to avoid corrupt state and potential crashes as zoom is expected to be a number ...(Number.isFinite(centerAndZoom.zoom) ? {zoom: centerAndZoom.zoom} : {}), pitch: 0, bearing: 0, transitionDuration: this.props.transitionDuration, transitionInterpolator: new FlyToInterpolator() }); }; removeMarker = () => { this.removeGeocoderDataset(); }; render() { const {isGeocoderEnabled, mapboxApiAccessToken, width} = this.props; return ( {isValid(mapboxApiAccessToken) && ( )} ); } } return GeocoderPanel; }