From b3fc35e6ecc2c2c27b262f90b716d1db680d9173 Mon Sep 17 00:00:00 2001 From: Peter Date: Sat, 1 Jun 2024 15:54:15 +0200 Subject: [PATCH 01/33] poi search on every key stroke --- src/App.tsx | 56 ++++++++++-- src/actions/Actions.ts | 23 +++++ src/api/Api.ts | 76 +++++++++++++++++ src/index.tsx | 5 +- src/layers/UsePOIsLayer.tsx | 106 +++++++++++++++++++++++ src/layers/createMarkerSVG.ts | 14 +++ src/pois/img/flight_takeoff.svg | 3 + src/pois/img/hotel.svg | 3 + src/pois/img/local_hospital.svg | 3 + src/pois/img/local_parking.svg | 3 + src/pois/img/local_pharmacy.svg | 3 + src/pois/img/luggage.svg | 3 + src/pois/img/museum.svg | 3 + src/pois/img/restaurant.svg | 3 + src/pois/img/school.svg | 3 + src/pois/img/sports_handball.svg | 3 + src/pois/img/store.svg | 3 + src/pois/img/train.svg | 3 + src/pois/img/universal_currency_alt.svg | 3 + src/sidebar/MobileSidebar.tsx | 8 +- src/sidebar/search/AddressInput.tsx | 109 ++++++++++++++++++++++-- src/sidebar/search/Search.tsx | 20 ++++- src/stores/POIsStore.ts | 34 ++++++++ src/stores/Stores.ts | 5 ++ test/DummyApi.ts | 8 ++ test/routing/Api.test.ts | 22 ++++- test/stores/QueryStore.test.ts | 13 ++- test/stores/RouteStore.test.ts | 6 +- 28 files changed, 523 insertions(+), 21 deletions(-) create mode 100644 src/layers/UsePOIsLayer.tsx create mode 100644 src/pois/img/flight_takeoff.svg create mode 100644 src/pois/img/hotel.svg create mode 100644 src/pois/img/local_hospital.svg create mode 100644 src/pois/img/local_parking.svg create mode 100644 src/pois/img/local_pharmacy.svg create mode 100644 src/pois/img/luggage.svg create mode 100644 src/pois/img/museum.svg create mode 100644 src/pois/img/restaurant.svg create mode 100644 src/pois/img/school.svg create mode 100644 src/pois/img/sports_handball.svg create mode 100644 src/pois/img/store.svg create mode 100644 src/pois/img/train.svg create mode 100644 src/pois/img/universal_currency_alt.svg create mode 100644 src/stores/POIsStore.ts diff --git a/src/App.tsx b/src/App.tsx index 40acc8bb..caa22baa 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,6 +7,7 @@ import { getMapFeatureStore, getMapOptionsStore, getPathDetailsStore, + getPOIsStore, getQueryStore, getRouteStore, getSettingsStore, @@ -17,7 +18,7 @@ import MobileSidebar from '@/sidebar/MobileSidebar' import { useMediaQuery } from 'react-responsive' import RoutingResults from '@/sidebar/RoutingResults' import PoweredBy from '@/sidebar/PoweredBy' -import { QueryStoreState, RequestState } from '@/stores/QueryStore' +import { Coordinate, QueryStoreState, RequestState } from '@/stores/QueryStore' import { RouteStoreState } from '@/stores/RouteStore' import { MapOptionsStoreState } from '@/stores/MapOptionsStore' import { ErrorStoreState } from '@/stores/ErrorStore' @@ -42,7 +43,10 @@ import PlainButton from '@/PlainButton' import useAreasLayer from '@/layers/UseAreasLayer' import useExternalMVTLayer from '@/layers/UseExternalMVTLayer' import LocationButton from '@/map/LocationButton' -import { SettingsContext } from './contexts/SettingsContext' +import { SettingsContext } from '@/contexts/SettingsContext' +import usePOIsLayer from '@/layers/UsePOIsLayer' +import { calcDist } from '@/distUtils' +import { toLonLat, transformExtent } from 'ol/proj' export const POPUP_CONTAINER_ID = 'popup-container' export const SIDEBAR_CONTENT_ID = 'sidebar-content' @@ -56,6 +60,7 @@ export default function App() { const [mapOptions, setMapOptions] = useState(getMapOptionsStore().state) const [pathDetails, setPathDetails] = useState(getPathDetailsStore().state) const [mapFeatures, setMapFeatures] = useState(getMapFeatureStore().state) + const [pois, setPOIs] = useState(getPOIsStore().state) const map = getMap() @@ -68,6 +73,7 @@ export default function App() { const onMapOptionsChanged = () => setMapOptions(getMapOptionsStore().state) const onPathDetailsChanged = () => setPathDetails(getPathDetailsStore().state) const onMapFeaturesChanged = () => setMapFeatures(getMapFeatureStore().state) + const onPOIsChanged = () => setPOIs(getPOIsStore().state) getSettingsStore().register(onSettingsChanged) getQueryStore().register(onQueryChanged) @@ -77,6 +83,7 @@ export default function App() { getMapOptionsStore().register(onMapOptionsChanged) getPathDetailsStore().register(onPathDetailsChanged) getMapFeatureStore().register(onMapFeaturesChanged) + getPOIsStore().register(onPOIsChanged) onQueryChanged() onInfoChanged() @@ -85,6 +92,7 @@ export default function App() { onMapOptionsChanged() onPathDetailsChanged() onMapFeaturesChanged() + onPOIsChanged() return () => { getSettingsStore().register(onSettingsChanged) @@ -95,6 +103,7 @@ export default function App() { getMapOptionsStore().deregister(onMapOptionsChanged) getPathDetailsStore().deregister(onPathDetailsChanged) getMapFeatureStore().deregister(onMapFeaturesChanged) + getPOIsStore().deregister(onPOIsChanged) } }, []) @@ -108,6 +117,15 @@ export default function App() { usePathsLayer(map, route.routingResult.paths, route.selectedPath, query.queryPoints) useQueryPointsLayer(map, query.queryPoints) usePathDetailsLayer(map, pathDetails) + usePOIsLayer(map, pois) + + const center = map.getView().getCenter() ? toLonLat(map.getView().getCenter()!) : [13.4, 52.5] + const mapCenter = { lng: center[0], lat: center[1] } + + const origExtent = map.getView().calculateExtent(map.getSize()) + var extent = transformExtent(origExtent, 'EPSG:3857', 'EPSG:4326') + const mapRadius = calcDist({ lng: extent[0], lat: extent[1] }, { lng: extent[2], lat: extent[3] }) / 2 / 1000 + const isSmallScreen = useMediaQuery({ query: '(max-width: 44rem)' }) return ( @@ -119,6 +137,8 @@ export default function App() { query={query} route={route} map={map} + mapCenter={mapCenter} + mapRadius={mapRadius} mapOptions={mapOptions} error={error} encodedValues={info.encoded_values} @@ -129,6 +149,8 @@ export default function App() { query={query} route={route} map={map} + mapCenter={mapCenter} + mapRadius={mapRadius} mapOptions={mapOptions} error={error} encodedValues={info.encoded_values} @@ -148,9 +170,21 @@ interface LayoutProps { error: ErrorStoreState encodedValues: object[] drawAreas: boolean + mapCenter: Coordinate + mapRadius: number } -function LargeScreenLayout({ query, route, map, error, mapOptions, encodedValues, drawAreas }: LayoutProps) { +function LargeScreenLayout({ + query, + route, + map, + error, + mapOptions, + encodedValues, + drawAreas, + mapCenter, + mapRadius, +}: LayoutProps) { const [showSidebar, setShowSidebar] = useState(true) const [showCustomModelBox, setShowCustomModelBox] = useState(false) return ( @@ -177,7 +211,7 @@ function LargeScreenLayout({ query, route, map, error, mapOptions, encodedValues drawAreas={drawAreas} /> )} - +
{!error.isDismissed && }
@@ -224,6 +268,8 @@ function SmallScreenLayout({ query, route, map, error, mapOptions, encodedValues error={error} encodedValues={encodedValues} drawAreas={drawAreas} + mapCenter={mapCenter} + mapRadius={mapRadius} />
diff --git a/src/actions/Actions.ts b/src/actions/Actions.ts index 5713eeb3..e78c8baa 100644 --- a/src/actions/Actions.ts +++ b/src/actions/Actions.ts @@ -2,6 +2,7 @@ import { Action } from '@/stores/Dispatcher' import { Coordinate, QueryPoint } from '@/stores/QueryStore' import { ApiInfo, Bbox, Path, RoutingArgs, RoutingProfile, RoutingResult } from '@/api/graphhopper' import { PathDetailsPoint } from '@/stores/PathDetailsStore' +import { POI } from '@/stores/POIsStore' import { Settings } from '@/stores/SettingsStore' export class InfoReceived implements Action { @@ -246,3 +247,25 @@ export class UpdateSettings implements Action { this.updatedSettings = updatedSettings } } + +export class SearchPOI implements Action { + readonly query: string + readonly coordinate: Coordinate + readonly radius: number + readonly icon: string + + constructor(icon: string, query: string, coordinate: Coordinate, radius: number) { + this.icon = icon + this.query = query + this.coordinate = coordinate + this.radius = radius + } +} + +export class SetPOI implements Action { + readonly pois: POI[] + + constructor(pois: POI[]) { + this.pois = pois + } +} diff --git a/src/api/Api.ts b/src/api/Api.ts index 2dab99f9..3759ac2b 100644 --- a/src/api/Api.ts +++ b/src/api/Api.ts @@ -16,6 +16,7 @@ import { import { LineString } from 'geojson' import { getTranslation, tr } from '@/translation/Translation' import * as config from 'config' +import { Coordinate } from '@/stores/QueryStore' interface ApiProfile { name: string @@ -30,6 +31,8 @@ export default interface Api { geocode(query: string, provider: string, additionalOptions?: Record): Promise + reverseGeocode(query: string | undefined, point: Coordinate, radius: number, tags?: string[]): Promise + supportsGeocoding(): boolean } @@ -117,6 +120,41 @@ export class ApiImpl implements Api { } } + async reverseGeocode(query: string | undefined, point: Coordinate, radius: number, tags?: string[]): Promise { + if (!this.supportsGeocoding()) + return { + hits: [], + took: 0, + } + const url = this.getGeocodingURLWithKey('geocode') + + url.searchParams.append('point', point.lat + ',' + point.lng) + url.searchParams.append('radius', '' + radius) + url.searchParams.append('reverse', 'true') + url.searchParams.append('limit', '50') + + if (query) url.searchParams.append('q', query) + + const langAndCountry = getTranslation().getLang().split('_') + url.searchParams.append('locale', langAndCountry.length > 0 ? langAndCountry[0] : 'en') + + if (tags) { + for (const value of tags) { + url.searchParams.append('osm_tag', value) + } + } + + const response = await fetch(url.toString(), { + headers: { Accept: 'application/json' }, + }) + + if (response.ok) { + return (await response.json()) as GeocodingResult + } else { + throw new Error('Geocoding went wrong ' + response.status) + } + } + supportsGeocoding(): boolean { return this.geocodingApi !== '' } @@ -379,4 +417,42 @@ export class ApiImpl implements Api { public static isTruck(profile: string) { return profile.includes('truck') } + + static parseAddress(query: string): AddressParseResult { + query = query.toLowerCase() + + const values = [ + { k: ['restaurant', 'restaurants'], t: ['amenity:restaurant'], i: 'restaurant' }, + { k: ['airport', 'airports'], t: ['aeroway:aerodrome'], i: 'flight_takeoff' }, + { k: ['public transit'], t: ['highway:bus_stop'], i: 'train' }, + { k: ['super market'], t: ['shop:supermarket'], i: 'store' }, + { k: ['hotel', 'hotels'], t: ['building:hotel'], i: 'hotel' }, + { k: ['tourism'], t: ['tourism'], i: 'luggage' }, + { k: ['museum'], t: ['building:museum'], i: 'museum' }, + { k: ['pharmacy'], t: ['amenity:pharmacy'], i: 'local_pharmacy' }, + { k: ['hospital'], t: ['amenity:hospital'], i: 'local_hospital' }, + { k: ['bank'], t: ['amenity:bank'], i: 'universal_currency_alt' }, + { k: ['education'], t: ['amenity:school', 'building:school', 'building:university'], i: 'school' }, + { k: ['leisure'], t: ['leisure'], i: 'sports_handball' }, + { k: ['parking'], t: ['amenity:parking'], i: 'local_parking' }, + ] + for (const val of values) { + if (val.k.some(keyword => query.includes(keyword))) + return { location: this.cleanQuery(query, val.k), tags: val.t, icon: val.i } + } + + return { location: '', tags: [], icon: '' } + } + + private static cleanQuery(query: string, inKeywords: string[]) { + const keywords = ['in', 'around'] + const locationWords = query.split(' ').filter(word => !keywords.includes(word) && !inKeywords.includes(word)) + return locationWords.join(' ') + } +} + +export interface AddressParseResult { + location: string + tags: string[] + icon: string } diff --git a/src/index.tsx b/src/index.tsx index 49dbc250..df916848 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -8,7 +8,7 @@ import { getErrorStore, getMapFeatureStore, getMapOptionsStore, - getPathDetailsStore, + getPathDetailsStore, getPOIsStore, getQueryStore, getRouteStore, getSettingsStore, @@ -29,6 +29,7 @@ import { createMap, getMap, setMap } from '@/map/map' import MapFeatureStore from '@/stores/MapFeatureStore' import SettingsStore from '@/stores/SettingsStore' import { ErrorAction, InfoReceived } from '@/actions/Actions' +import POIsStore from '@/stores/POIsStore' console.log(`Source code: https://github.com/graphhopper/graphhopper-maps/tree/${GIT_SHA}`) @@ -53,6 +54,7 @@ setStores({ mapOptionsStore: new MapOptionsStore(), pathDetailsStore: new PathDetailsStore(), mapFeatureStore: new MapFeatureStore(), + poisStore: new POIsStore(getApi()), }) setMap(createMap()) @@ -66,6 +68,7 @@ Dispatcher.register(getErrorStore()) Dispatcher.register(getMapOptionsStore()) Dispatcher.register(getPathDetailsStore()) Dispatcher.register(getMapFeatureStore()) +Dispatcher.register(getPOIsStore()) // register map action receiver const smallScreenMediaQuery = window.matchMedia('(max-width: 44rem)') diff --git a/src/layers/UsePOIsLayer.tsx b/src/layers/UsePOIsLayer.tsx new file mode 100644 index 00000000..7d5515a3 --- /dev/null +++ b/src/layers/UsePOIsLayer.tsx @@ -0,0 +1,106 @@ +import { Feature, Map } from 'ol' +import { useEffect } from 'react' +import { Point } from 'ol/geom' +import { fromLonLat } from 'ol/proj' +import VectorLayer from 'ol/layer/Vector' +import VectorSource from 'ol/source/Vector' +import { Icon, Style } from 'ol/style' +import { POI, POIsStoreState } from '@/stores/POIsStore' + +import flight_takeoff_svg from '/src/pois/img/flight_takeoff.svg' +import hotel_svg from '/src/pois/img/hotel.svg' +import local_hospital_svg from '/src/pois/img/local_hospital.svg' +import local_parking_svg from '/src/pois/img/local_parking.svg' +import local_pharmacy_svg from '/src/pois/img/local_pharmacy.svg' +import luggage_svg from '/src/pois/img/luggage.svg' +import museum_svg from '/src/pois/img/museum.svg' +import restaurant_svg from '/src/pois/img/restaurant.svg' +import school_svg from '/src/pois/img/school.svg' +import sports_handball_svg from '/src/pois/img/sports_handball.svg' +import store_svg from '/src/pois/img/store.svg' +import train_svg from '/src/pois/img/train.svg' +import universal_currency_alt_svg from '/src/pois/img/universal_currency_alt.svg' +import { createPOIMarker } from '@/layers/createMarkerSVG' + +const svgStrings: { [id: string]: string } = {} + +const svgObjects: { [id: string]: any } = { + flight_takeoff: flight_takeoff_svg(), + hotel: hotel_svg(), + local_hospital: local_hospital_svg(), + local_parking: local_parking_svg(), + local_pharmacy: local_pharmacy_svg(), + luggage: luggage_svg(), + museum: museum_svg(), + restaurant: restaurant_svg(), + school: school_svg(), + sports_handball: sports_handball_svg(), + store: store_svg(), + train: train_svg(), + universal_currency_alt: universal_currency_alt_svg(), +} +// -300 -1260 1560 1560 +// +for (const k in svgObjects) { + const svgObj = svgObjects[k] + svgStrings[k] = createPOIMarker(svgObj.props.children.props.d) + // console.log(svgStrings[k]) +} + +export default function usePOIsLayer(map: Map, poisState: POIsStoreState) { + useEffect(() => { + removePOIs(map) + console.log('poi count: ' + poisState.pois.length) + addPOIsLayer(map, poisState.pois) + return () => { + removePOIs(map) + } + }, [map, poisState.pois]) +} + +function removePOIs(map: Map) { + map.getLayers() + .getArray() + .filter(l => l.get('gh:pois')) + .forEach(l => map.removeLayer(l)) +} + +function addPOIsLayer(map: Map, pois: POI[]) { + const features = pois.map((poi, i) => { + const feature = new Feature({ + geometry: new Point(fromLonLat([poi.coordinate.lng, poi.coordinate.lat])), + }) + feature.set('gh:marker_props', { icon: poi.icon, poi: poi }) + return feature + }) + const poisLayer = new VectorLayer({ + source: new VectorSource({ + features: features, + }), + }) + poisLayer.set('gh:pois', true) + const cachedStyles: { [id: string]: Style } = {} + poisLayer.setStyle(feature => { + const props = feature.get('gh:marker_props') + let style = cachedStyles[props.icon] + if (style) return style + style = new Style({ + image: new Icon({ + src: 'data:image/svg+xml;utf8,' + svgStrings[props.icon], + displacement: [0, 18], + }), + }) + cachedStyles[props.icon] = style + return style + }) + map.addLayer(poisLayer) + map.on('click', e => { + poisLayer.getFeatures(e.pixel).then(features => { + if (features.length > 0) { + const props = features[0].getProperties().get('gh:marker_props') + props.poi.selected = true + } + }) + }) + return poisLayer +} diff --git a/src/layers/createMarkerSVG.ts b/src/layers/createMarkerSVG.ts index 7ffa6505..527c7e6b 100644 --- a/src/layers/createMarkerSVG.ts +++ b/src/layers/createMarkerSVG.ts @@ -8,6 +8,20 @@ interface MarkerProps { size?: number } +export function createPOI(pathD: string) { + return ` + + + ` +} + +export function createPOIMarker(pathD: string) { + return ` + + + ` +} + // todo: this is mostly duplicated from `Marker.tsx`, but we use a more elongated shape (MARKER_PATH). // To use `Marker.tsx` we would probably need to add ol.Overlays, i.e. create a div for each marker and insert the svg from `Marker.tsx`. export function createSvg({ color, number, size = 0 }: MarkerProps) { diff --git a/src/pois/img/flight_takeoff.svg b/src/pois/img/flight_takeoff.svg new file mode 100644 index 00000000..b54db32d --- /dev/null +++ b/src/pois/img/flight_takeoff.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/pois/img/hotel.svg b/src/pois/img/hotel.svg new file mode 100644 index 00000000..97ad41c2 --- /dev/null +++ b/src/pois/img/hotel.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/pois/img/local_hospital.svg b/src/pois/img/local_hospital.svg new file mode 100644 index 00000000..e8539f47 --- /dev/null +++ b/src/pois/img/local_hospital.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/pois/img/local_parking.svg b/src/pois/img/local_parking.svg new file mode 100644 index 00000000..e976212c --- /dev/null +++ b/src/pois/img/local_parking.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/pois/img/local_pharmacy.svg b/src/pois/img/local_pharmacy.svg new file mode 100644 index 00000000..9a1adf94 --- /dev/null +++ b/src/pois/img/local_pharmacy.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/pois/img/luggage.svg b/src/pois/img/luggage.svg new file mode 100644 index 00000000..ea0e04a0 --- /dev/null +++ b/src/pois/img/luggage.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/pois/img/museum.svg b/src/pois/img/museum.svg new file mode 100644 index 00000000..0504b1b4 --- /dev/null +++ b/src/pois/img/museum.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/pois/img/restaurant.svg b/src/pois/img/restaurant.svg new file mode 100644 index 00000000..ec17a3c5 --- /dev/null +++ b/src/pois/img/restaurant.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/pois/img/school.svg b/src/pois/img/school.svg new file mode 100644 index 00000000..c537f254 --- /dev/null +++ b/src/pois/img/school.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/pois/img/sports_handball.svg b/src/pois/img/sports_handball.svg new file mode 100644 index 00000000..8f86e4d1 --- /dev/null +++ b/src/pois/img/sports_handball.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/pois/img/store.svg b/src/pois/img/store.svg new file mode 100644 index 00000000..2775e7b3 --- /dev/null +++ b/src/pois/img/store.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/pois/img/train.svg b/src/pois/img/train.svg new file mode 100644 index 00000000..ec15bd80 --- /dev/null +++ b/src/pois/img/train.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/pois/img/universal_currency_alt.svg b/src/pois/img/universal_currency_alt.svg new file mode 100644 index 00000000..3fe4ba65 --- /dev/null +++ b/src/pois/img/universal_currency_alt.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/sidebar/MobileSidebar.tsx b/src/sidebar/MobileSidebar.tsx index e4d17318..5cd53669 100644 --- a/src/sidebar/MobileSidebar.tsx +++ b/src/sidebar/MobileSidebar.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useRef, useState } from 'react' -import { QueryPoint, QueryPointType, QueryStoreState, RequestState } from '@/stores/QueryStore' +import { Coordinate, QueryPoint, QueryPointType, QueryStoreState, RequestState } from '@/stores/QueryStore' import { RouteStoreState } from '@/stores/RouteStore' import { ErrorStoreState } from '@/stores/ErrorStore' import styles from './MobileSidebar.module.css' @@ -18,9 +18,11 @@ type MobileSidebarProps = { error: ErrorStoreState encodedValues: object[] drawAreas: boolean + mapCenter: Coordinate + mapRadius: number } -export default function ({ query, route, error, encodedValues, drawAreas }: MobileSidebarProps) { +export default function ({ query, route, error, encodedValues, drawAreas, mapCenter, mapRadius }: MobileSidebarProps) { const [showCustomModelBox, setShowCustomModelBox] = useState(false) // the following three elements control, whether the small search view is displayed const isShortScreen = useMediaQuery({ query: '(max-height: 55rem)' }) @@ -72,7 +74,7 @@ export default function ({ query, route, error, encodedValues, drawAreas }: Mobi drawAreas={drawAreas} /> )} - +
)} {!error.isDismissed && } diff --git a/src/sidebar/search/AddressInput.tsx b/src/sidebar/search/AddressInput.tsx index 1aea0c47..d673c6a9 100644 --- a/src/sidebar/search/AddressInput.tsx +++ b/src/sidebar/search/AddressInput.tsx @@ -11,13 +11,15 @@ import Autocomplete, { import ArrowBack from './arrow_back.svg' import Cross from '@/sidebar/times-solid-thin.svg' import styles from './AddressInput.module.css' -import Api, { getApi } from '@/api/Api' +import Api, { AddressParseResult, ApiImpl, getApi } from '@/api/Api' import { tr } from '@/translation/Translation' import { coordinateToText, hitToItem, nominatimHitToItem, textToCoordinate } from '@/Converters' import { useMediaQuery } from 'react-responsive' import PopUp from '@/sidebar/search/PopUp' import PlainButton from '@/PlainButton' import { onCurrentLocationSelected } from '@/map/MapComponent' +import Dispatcher from '@/stores/Dispatcher' +import { SetPOI } from '@/actions/Actions' export interface AddressInputProps { point: QueryPoint @@ -29,6 +31,8 @@ export interface AddressInputProps { moveStartIndex: number dropPreviewIndex: number index: number + mapCenter: Coordinate + mapRadius: number } export default function AddressInput(props: AddressInputProps) { @@ -67,6 +71,25 @@ export default function AddressInput(props: AddressInputProps) { } }) ) + + const [poiSearch] = useState( + new ReverseGeocoder(getApi(), (hits, parseResult) => { + const pois = hits.map(hit => { + const res = hitToItem(hit) + return { + name: res.mainText, + icon: parseResult.icon, + coordinate: hit.point, + selected: false, + } + }) + + // TODO NOW: if zoom is too far away: auto zoom to the results? + + Dispatcher.dispatch(new SetPOI(pois)) + }) + ) + // if item is selected we need to clear the autocompletion list useEffect(() => setAutocompleteItems([]), [props.point]) // if no items but input is selected show current location item @@ -170,10 +193,17 @@ export default function AddressInput(props: AddressInputProps) { ref={searchInput} autoComplete="off" onChange={e => { - setText(e.target.value) - const coordinate = textToCoordinate(e.target.value) - if (!coordinate) geocoder.request(e.target.value, biasCoord, 'default') - props.onChange(e.target.value) + const query = e.target.value + setText(query) + const coordinate = textToCoordinate(query) + if (!coordinate) { + // TODO NOW instead of querying for every key stroke include an auto suggest item with one from the POI keywords and only if click on this trigger poiSearch.request!! + const parseResult = ApiImpl.parseAddress(query) + if (parseResult.tags.length > 0) + poiSearch.request(parseResult, props.mapCenter, props.mapRadius) + else geocoder.request(query, biasCoord, 'default') + } + props.onChange(query) }} onKeyDown={onKeypress} onFocus={() => { @@ -293,7 +323,9 @@ class Geocoder { await this.timeout.wait() try { - const options: Record = bias ? { point: coordinateToText(bias), location_bias_scale: "0.5", zoom: "9" } : {} + const options: Record = bias + ? { point: coordinateToText(bias), location_bias_scale: '0.5', zoom: '9' } + : {} const result = await this.api.geocode(query, provider, options) const hits = Geocoder.filterDuplicates(result.hits) if (currentId === this.requestId) this.onSuccess(query, provider, hits) @@ -307,7 +339,7 @@ class Geocoder { return this.requestId } - private static filterDuplicates(hits: GeocodingHit[]) { + static filterDuplicates(hits: GeocodingHit[]) { const set: Set = new Set() return hits.filter(hit => { if (!set.has(hit.osm_id)) { @@ -319,6 +351,69 @@ class Geocoder { } } +class ReverseGeocoder { + private requestId = 0 + private readonly timeout = new Timout(200) + private readonly api: Api + private readonly onSuccess: (hits: GeocodingHit[], parseResult: AddressParseResult) => void + + constructor(api: Api, onSuccess: (hits: GeocodingHit[], parseResult: AddressParseResult) => void) { + this.api = api + this.onSuccess = onSuccess + } + + cancel() { + // invalidates last request if there is one + this.getNextId() + } + + request(query: AddressParseResult, point: Coordinate, radius: number) { + this.requestAsync(query, point, radius).then(() => {}) + } + + async requestAsync(parseResult: AddressParseResult, point: Coordinate, radius: number) { + const currentId = this.getNextId() + this.timeout.cancel() + await this.timeout.wait() + try { + let hits: GeocodingHit[] = [] + let searchCoordinate: Coordinate | undefined = undefined + if (parseResult.location) { + let options: Record = { + point: coordinateToText(point), + location_bias_scale: '0.5', + zoom: '9', + } + let result = await this.api.geocode(parseResult.location, 'default', options) + hits = result.hits + if (result.hits.length > 0) searchCoordinate = result.hits[0].point + else if (point) searchCoordinate = point + } else if (point) { + searchCoordinate = point + } + + if (!searchCoordinate) { + hits = [] + } else if (hits.length > 0) { + // TODO NOW should we include parseResult.location here again if searchCoordinate is from forward geocode request? + // parseResult.location + const result = await this.api.reverseGeocode('', searchCoordinate, radius, parseResult.tags) + hits = Geocoder.filterDuplicates(result.hits) + console.log(JSON.stringify(hits[0])) + } + + if (currentId === this.requestId) this.onSuccess(hits, parseResult) + } catch (reason) { + throw Error('Could not get geocoding results because: ' + reason) + } + } + + private getNextId() { + this.requestId++ + return this.requestId + } +} + class Timout { private readonly delay: number private handle: number = 0 diff --git a/src/sidebar/search/Search.tsx b/src/sidebar/search/Search.tsx index 2a28214b..574ecc4c 100644 --- a/src/sidebar/search/Search.tsx +++ b/src/sidebar/search/Search.tsx @@ -1,7 +1,7 @@ import { useEffect, useRef, useState } from 'react' import Dispatcher from '@/stores/Dispatcher' import styles from '@/sidebar/search/Search.module.css' -import { getBBoxFromCoord, QueryPoint } from '@/stores/QueryStore' +import { Coordinate, getBBoxFromCoord, QueryPoint } from '@/stores/QueryStore' import { AddPoint, ClearRoute, InvalidatePoint, MovePoint, RemovePoint, SetBBox, SetPoint } from '@/actions/Actions' import RemoveIcon from './minus-circle-solid.svg' import AddIcon from './plus-circle-solid.svg' @@ -13,7 +13,15 @@ import { MarkerComponent } from '@/map/Marker' import { tr } from '@/translation/Translation' import SettingsBox from '@/sidebar/SettingsBox' -export default function Search({ points }: { points: QueryPoint[] }) { +export default function Search({ + points, + mapCenter, + mapRadius, +}: { + points: QueryPoint[] + mapCenter: Coordinate + mapRadius: number +}) { const [showSettings, setShowSettings] = useState(false) const [showTargetIcons, setShowTargetIcons] = useState(true) const [moveStartIndex, onMoveStartSelect] = useState(-1) @@ -40,6 +48,8 @@ export default function Search({ points }: { points: QueryPoint[] }) { }} dropPreviewIndex={dropPreviewIndex} onDropPreviewSelect={onDropPreviewSelect} + mapCenter={mapCenter} + mapRadius={mapRadius} /> ))} @@ -75,6 +85,8 @@ const SearchBox = ({ onMoveStartSelect, dropPreviewIndex, onDropPreviewSelect, + mapCenter, + mapRadius, }: { index: number points: QueryPoint[] @@ -85,6 +97,8 @@ const SearchBox = ({ onMoveStartSelect: (index: number, showTargetIcon: boolean) => void dropPreviewIndex: number onDropPreviewSelect: (index: number) => void + mapCenter: Coordinate + mapRadius: number }) => { const point = points[index] @@ -162,6 +176,8 @@ const SearchBox = ({
{ + private readonly api: Api + + constructor(api: Api) { + super({ pois: [] }) + this.api = api + } + + reduce(state: POIsStoreState, action: Action): POIsStoreState { + if (action instanceof SetPOI) { + return { + pois: action.pois, + } + } + return state + } +} diff --git a/src/stores/Stores.ts b/src/stores/Stores.ts index 018e8b42..b7ba16d3 100644 --- a/src/stores/Stores.ts +++ b/src/stores/Stores.ts @@ -6,6 +6,7 @@ import MapOptionsStore from '@/stores/MapOptionsStore' import PathDetailsStore from '@/stores/PathDetailsStore' import MapFeatureStore from '@/stores/MapFeatureStore' import SettingsStore from '@/stores/SettingsStore' +import POIsStore from '@/stores/POIsStore' let settingsStore: SettingsStore let queryStore: QueryStore @@ -15,6 +16,7 @@ let errorStore: ErrorStore let mapOptionsStore: MapOptionsStore let pathDetailsStore: PathDetailsStore let mapFeatureStore: MapFeatureStore +let poisStore: POIsStore interface StoresInput { settingsStore: SettingsStore @@ -25,6 +27,7 @@ interface StoresInput { mapOptionsStore: MapOptionsStore pathDetailsStore: PathDetailsStore mapFeatureStore: MapFeatureStore + poisStore: POIsStore } export const setStores = function (stores: StoresInput) { @@ -36,6 +39,7 @@ export const setStores = function (stores: StoresInput) { mapOptionsStore = stores.mapOptionsStore pathDetailsStore = stores.pathDetailsStore mapFeatureStore = stores.mapFeatureStore + poisStore = stores.poisStore } export const getSettingsStore = () => settingsStore @@ -46,3 +50,4 @@ export const getErrorStore = () => errorStore export const getMapOptionsStore = () => mapOptionsStore export const getPathDetailsStore = () => pathDetailsStore export const getMapFeatureStore = () => mapFeatureStore +export const getPOIsStore = () => poisStore diff --git a/test/DummyApi.ts b/test/DummyApi.ts index 5513b562..5fecef4b 100644 --- a/test/DummyApi.ts +++ b/test/DummyApi.ts @@ -1,5 +1,6 @@ import Api from '../src/api/Api' import { ApiInfo, GeocodingResult, RoutingArgs, RoutingResult, RoutingResultInfo } from '../src/api/graphhopper' +import { Coordinate } from '@/stores/QueryStore' export default class DummyApi implements Api { geocode(query: string): Promise { @@ -9,6 +10,13 @@ export default class DummyApi implements Api { }) } + reverseGeocode(query: string | undefined, point: Coordinate, radius: number, tags?: string[]): Promise { + return Promise.resolve({ + took: 0, + hits: [], + }) + } + info(): Promise { return Promise.resolve({ bbox: [0, 0, 0, 0], diff --git a/test/routing/Api.test.ts b/test/routing/Api.test.ts index cc026e62..fabf0865 100644 --- a/test/routing/Api.test.ts +++ b/test/routing/Api.test.ts @@ -2,7 +2,7 @@ * @jest-environment node */ import fetchMock from 'jest-fetch-mock' -import { ErrorAction, InfoReceived, RouteRequestFailed, RouteRequestSuccess } from '@/actions/Actions' +import { RouteRequestFailed, RouteRequestSuccess } from '@/actions/Actions' import { setTranslation } from '@/translation/Translation' import Dispatcher from '@/stores/Dispatcher' @@ -343,6 +343,26 @@ describe('route', () => { }) }) +describe('reverse geocoder', () => { + it('should parse correctly', async () => { + let res = ApiImpl.parseAddress('dresden restaurant') + expect(res.location).toEqual('dresden') + expect(res.icon).toEqual('restaurant') + + res = ApiImpl.parseAddress('restaurant') + expect(res.location).toEqual('') + expect(res.icon).toEqual('restaurant') + + res = ApiImpl.parseAddress('restaurant in dresden') + expect(res.location).toEqual('dresden') + expect(res.icon).toEqual('restaurant') + + res = ApiImpl.parseAddress('airports around some thing else') + expect(res.location).toEqual('some thing else') + expect(res.icon).toEqual('flight_takeoff') + }) +}) + function getEmptyResult() { return { info: { copyright: [], road_data_timestamp: '', took: 0 } as RoutingResultInfo, diff --git a/test/stores/QueryStore.test.ts b/test/stores/QueryStore.test.ts index 04bd9400..c88133fe 100644 --- a/test/stores/QueryStore.test.ts +++ b/test/stores/QueryStore.test.ts @@ -1,6 +1,13 @@ import Api from '@/api/Api' import { ApiInfo, GeocodingResult, RoutingArgs, RoutingResult, RoutingResultInfo } from '@/api/graphhopper' -import QueryStore, { QueryPoint, QueryPointType, QueryStoreState, RequestState, SubRequest } from '@/stores/QueryStore' +import QueryStore, { + Coordinate, + QueryPoint, + QueryPointType, + QueryStoreState, + RequestState, + SubRequest, +} from '@/stores/QueryStore' import { AddPoint, ClearPoints, @@ -24,6 +31,10 @@ class ApiMock implements Api { throw Error('not implemented') } + reverseGeocode(query: string | undefined, point: Coordinate, radius: number, tags?: string[]): Promise { + throw Error('not implemented') + } + info(): Promise { throw Error('not implemented') } diff --git a/test/stores/RouteStore.test.ts b/test/stores/RouteStore.test.ts index 866dca78..cf8cde72 100644 --- a/test/stores/RouteStore.test.ts +++ b/test/stores/RouteStore.test.ts @@ -1,5 +1,5 @@ import RouteStore from '@/stores/RouteStore' -import QueryStore, { QueryPoint, QueryPointType } from '@/stores/QueryStore' +import QueryStore, { Coordinate, QueryPoint, QueryPointType } from '@/stores/QueryStore' import Api from '@/api/Api' import { ApiInfo, GeocodingResult, Path, RoutingArgs, RoutingResult } from '@/api/graphhopper' import Dispatcher, { Action } from '@/stores/Dispatcher' @@ -69,6 +69,10 @@ class DummyApi implements Api { throw Error('not implemented') } + reverseGeocode(query: string | undefined, point: Coordinate, radius: number, tags?: string[]): Promise { + throw Error('not implemented') + } + info(): Promise { throw Error('not implemented') } From 14911f54d8878c3c921e359fd10a5fcab959f6e0 Mon Sep 17 00:00:00 2001 From: Peter Date: Sat, 1 Jun 2024 22:56:45 +0200 Subject: [PATCH 02/33] new autocomplete item, better usability --- src/NavBar.ts | 18 +-- src/api/Api.ts | 106 +++++++++++++----- src/index.tsx | 3 +- src/sidebar/search/AddressInput.tsx | 47 +++++--- .../AddressInputAutocomplete.module.css | 15 ++- .../search/AddressInputAutocomplete.tsx | 31 +++++ test/DummyApi.ts | 7 +- test/routing/Api.test.ts | 24 +++- test/stores/QueryStore.test.ts | 7 +- test/stores/RouteStore.test.ts | 7 +- 10 files changed, 193 insertions(+), 72 deletions(-) diff --git a/src/NavBar.ts b/src/NavBar.ts index 0717e476..319d4a09 100644 --- a/src/NavBar.ts +++ b/src/NavBar.ts @@ -12,7 +12,7 @@ import QueryStore, { QueryStoreState, } from '@/stores/QueryStore' import MapOptionsStore, { MapOptionsStoreState } from './stores/MapOptionsStore' -import { getApi } from '@/api/Api' +import { ApiImpl, getApi } from '@/api/Api' import config from 'config' export default class NavBar { @@ -159,7 +159,7 @@ export default class NavBar { const bbox = initializedPoints.length == 1 ? getBBoxFromCoord(initializedPoints[0].coordinate) - : NavBar.getBBoxFromUrlPoints(initializedPoints.map(p => p.coordinate)) + : ApiImpl.getBBoxPoints(initializedPoints.map(p => p.coordinate)) if (bbox) Dispatcher.dispatch(new SetBBox(bbox)) return Dispatcher.dispatch(new SetQueryPoints(points)) } @@ -178,18 +178,4 @@ export default class NavBar { first ).toString() } - - private static getBBoxFromUrlPoints(urlPoints: Coordinate[]): Bbox | null { - const bbox: Bbox = urlPoints.reduce( - (res: Bbox, c) => [ - Math.min(res[0], c.lng), - Math.min(res[1], c.lat), - Math.max(res[2], c.lng), - Math.max(res[3], c.lat), - ], - [180, 90, -180, -90] as Bbox - ) - // return null if the bbox is not valid, e.g. if no url points were given at all - return bbox[0] < bbox[2] && bbox[1] < bbox[3] ? bbox : null - } } diff --git a/src/api/Api.ts b/src/api/Api.ts index 3759ac2b..063ead00 100644 --- a/src/api/Api.ts +++ b/src/api/Api.ts @@ -31,7 +31,12 @@ export default interface Api { geocode(query: string, provider: string, additionalOptions?: Record): Promise - reverseGeocode(query: string | undefined, point: Coordinate, radius: number, tags?: string[]): Promise + reverseGeocode( + query: string | undefined, + point: Coordinate, + radius: number, + tags?: string[] + ): Promise supportsGeocoding(): boolean } @@ -120,7 +125,12 @@ export class ApiImpl implements Api { } } - async reverseGeocode(query: string | undefined, point: Coordinate, radius: number, tags?: string[]): Promise { + async reverseGeocode( + query: string | undefined, + point: Coordinate, + radius: number, + tags?: string[] + ): Promise { if (!this.supportsGeocoding()) return { hits: [], @@ -418,41 +428,83 @@ export class ApiImpl implements Api { return profile.includes('truck') } - static parseAddress(query: string): AddressParseResult { + public static getBBoxPoints(points: Coordinate[]): Bbox | null { + const bbox: Bbox = points.reduce( + (res: Bbox, c) => [ + Math.min(res[0], c.lng), + Math.min(res[1], c.lat), + Math.max(res[2], c.lng), + Math.max(res[3], c.lat), + ], + [180, 90, -180, -90] as Bbox + ) + // return null if the bbox is not valid, e.g. if no url points were given at all + return bbox[0] < bbox[2] && bbox[1] < bbox[3] ? bbox : null + } +} + +export class AddressParseResult { + location: string + tags: string[] + icon: string + poi: string + + constructor(location: string, tags: string[], icon: string, poi: string) { + this.location = location + this.tags = tags + this.icon = icon + this.poi = poi + } + + hasPOIs(): boolean { + return this.tags.length > 0 + } + + public static parse(query: string, incomplete: boolean): AddressParseResult { query = query.toLowerCase() const values = [ - { k: ['restaurant', 'restaurants'], t: ['amenity:restaurant'], i: 'restaurant' }, - { k: ['airport', 'airports'], t: ['aeroway:aerodrome'], i: 'flight_takeoff' }, - { k: ['public transit'], t: ['highway:bus_stop'], i: 'train' }, - { k: ['super market'], t: ['shop:supermarket'], i: 'store' }, - { k: ['hotel', 'hotels'], t: ['building:hotel'], i: 'hotel' }, + { k: ['restaurants', 'restaurant'], t: ['amenity:restaurant'], i: 'restaurant' }, + { k: ['airports', 'airport'], t: ['aeroway:aerodrome'], i: 'flight_takeoff' }, + { k: ['bus stops'], t: ['highway:bus_stop'], i: 'train' }, + { k: ['railway stations', 'railway station'], t: ['railway:station'], i: 'train' }, + { k: ['super markets', 'super market'], t: ['shop:supermarket', 'building:supermarket'], i: 'store' }, + { k: ['hotels', 'hotel'], t: ['amenity:hotel', 'building:hotel'], i: 'hotel' }, { k: ['tourism'], t: ['tourism'], i: 'luggage' }, - { k: ['museum'], t: ['building:museum'], i: 'museum' }, - { k: ['pharmacy'], t: ['amenity:pharmacy'], i: 'local_pharmacy' }, - { k: ['hospital'], t: ['amenity:hospital'], i: 'local_hospital' }, - { k: ['bank'], t: ['amenity:bank'], i: 'universal_currency_alt' }, + { k: ['museums', 'museum'], t: ['tourism:museum', 'building:museum'], i: 'museum' }, + { k: ['pharmacies', 'pharmacy'], t: ['amenity:pharmacy'], i: 'local_pharmacy' }, + { k: ['hospitals', 'hospital'], t: ['amenity:hospital', 'building:hospital'], i: 'local_hospital' }, + { k: ['banks', 'bank'], t: ['amenity:bank'], i: 'universal_currency_alt' }, { k: ['education'], t: ['amenity:school', 'building:school', 'building:university'], i: 'school' }, + { k: ['schools', 'school'], t: ['amenity:school', 'building:school'], i: 'school' }, { k: ['leisure'], t: ['leisure'], i: 'sports_handball' }, + { k: ['parks', 'park'], t: ['leisure:park'], i: 'sports_handball' }, + { k: ['playgrounds', 'playground'], t: ['leisure:playground'], i: 'sports_handball' }, { k: ['parking'], t: ['amenity:parking'], i: 'local_parking' }, ] - for (const val of values) { - if (val.k.some(keyword => query.includes(keyword))) - return { location: this.cleanQuery(query, val.k), tags: val.t, icon: val.i } + const smallWords = ['in', 'around', 'nearby'] + const queryTokens: string[] = query.split(' ').filter(token => !smallWords.includes(token)) + const cleanQuery = queryTokens.join(' ') + const bigrams: string[] = [] + for (let i = 0; i < queryTokens.length - 1; i++) { + bigrams.push(queryTokens[i] + ' ' + queryTokens[i + 1]) } - return { location: '', tags: [], icon: '' } - } + for (const val of values) { + // two word phrases like 'public transit' must be checked before single word phrases + for(const keyword of val.k) { + const i = bigrams.indexOf(keyword) + if (i >= 0) + return new AddressParseResult(cleanQuery.replace(bigrams[i], '').trim(), val.t, val.i, val.k[0]) + } - private static cleanQuery(query: string, inKeywords: string[]) { - const keywords = ['in', 'around'] - const locationWords = query.split(' ').filter(word => !keywords.includes(word) && !inKeywords.includes(word)) - return locationWords.join(' ') - } -} + for(const keyword of val.k) { + const i = queryTokens.indexOf(keyword) + if(i >= 0) + return new AddressParseResult(cleanQuery.replace(queryTokens[i], '').trim(), val.t, val.i, val.k[0]) + } + } -export interface AddressParseResult { - location: string - tags: string[] - icon: string + return new AddressParseResult('', [], '', '') + } } diff --git a/src/index.tsx b/src/index.tsx index df916848..3aebedd8 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -8,7 +8,8 @@ import { getErrorStore, getMapFeatureStore, getMapOptionsStore, - getPathDetailsStore, getPOIsStore, + getPathDetailsStore, + getPOIsStore, getQueryStore, getRouteStore, getSettingsStore, diff --git a/src/sidebar/search/AddressInput.tsx b/src/sidebar/search/AddressInput.tsx index d673c6a9..09969f54 100644 --- a/src/sidebar/search/AddressInput.tsx +++ b/src/sidebar/search/AddressInput.tsx @@ -5,6 +5,7 @@ import Autocomplete, { AutocompleteItem, GeocodingItem, MoreResultsItem, + POIQueryItem, SelectCurrentLocationItem, } from '@/sidebar/search/AddressInputAutocomplete' @@ -19,7 +20,7 @@ import PopUp from '@/sidebar/search/PopUp' import PlainButton from '@/PlainButton' import { onCurrentLocationSelected } from '@/map/MapComponent' import Dispatcher from '@/stores/Dispatcher' -import { SetPOI } from '@/actions/Actions' +import { SetBBox, SetPOI } from '@/actions/Actions' export interface AddressInputProps { point: QueryPoint @@ -50,13 +51,19 @@ export default function AddressInput(props: AddressInputProps) { const [autocompleteItems, setAutocompleteItems] = useState([]) const [geocoder] = useState( new Geocoder(getApi(), (query, provider, hits) => { - const items: AutocompleteItem[] = hits.map(hit => { + const items: AutocompleteItem[] = [] + const parseResult = AddressParseResult.parse(query, true) + if (parseResult.hasPOIs()) items.push(new POIQueryItem(parseResult)) + + hits.forEach(hit => { const obj = provider === 'nominatim' ? nominatimHitToItem(hit) : hitToItem(hit) - return new GeocodingItem( - obj.mainText, - obj.secondText, - hit.point, - hit.extent ? hit.extent : getBBoxFromCoord(hit.point) + items.push( + new GeocodingItem( + obj.mainText, + obj.secondText, + hit.point, + hit.extent ? hit.extent : getBBoxFromCoord(hit.point) + ) ) }) @@ -84,9 +91,13 @@ export default function AddressInput(props: AddressInputProps) { } }) - // TODO NOW: if zoom is too far away: auto zoom to the results? - - Dispatcher.dispatch(new SetPOI(pois)) + const bbox = ApiImpl.getBBoxPoints(pois.map(p => p.coordinate)) + if (bbox) { + Dispatcher.dispatch(new SetBBox(bbox)) + Dispatcher.dispatch(new SetPOI(pois)) + } else { + console.warn('invalid bbox for points ' + JSON.stringify(pois) + " result was: " + JSON.stringify(parseResult)) + } }) ) @@ -196,13 +207,7 @@ export default function AddressInput(props: AddressInputProps) { const query = e.target.value setText(query) const coordinate = textToCoordinate(query) - if (!coordinate) { - // TODO NOW instead of querying for every key stroke include an auto suggest item with one from the POI keywords and only if click on this trigger poiSearch.request!! - const parseResult = ApiImpl.parseAddress(query) - if (parseResult.tags.length > 0) - poiSearch.request(parseResult, props.mapCenter, props.mapRadius) - else geocoder.request(query, biasCoord, 'default') - } + if (!coordinate) geocoder.request(query, biasCoord, 'default') props.onChange(query) }} onKeyDown={onKeypress} @@ -250,6 +255,12 @@ export default function AddressInput(props: AddressInputProps) { // do not hide autocomplete items const coordinate = textToCoordinate(item.search) if (!coordinate) geocoder.request(item.search, biasCoord, 'nominatim') + } else if (item instanceof POIQueryItem) { + hideSuggestions() + if (item.result.hasPOIs()) { + console.log(item.result.location) + poiSearch.request(item.result, props.mapCenter, Math.min(props.mapRadius, 100)) + } } searchInput.current!.blur() }} @@ -397,9 +408,9 @@ class ReverseGeocoder { } else if (hits.length > 0) { // TODO NOW should we include parseResult.location here again if searchCoordinate is from forward geocode request? // parseResult.location + console.log("radius "+radius) const result = await this.api.reverseGeocode('', searchCoordinate, radius, parseResult.tags) hits = Geocoder.filterDuplicates(result.hits) - console.log(JSON.stringify(hits[0])) } if (currentId === this.requestId) this.onSuccess(hits, parseResult) diff --git a/src/sidebar/search/AddressInputAutocomplete.module.css b/src/sidebar/search/AddressInputAutocomplete.module.css index 2be629c7..2a2defb6 100644 --- a/src/sidebar/search/AddressInputAutocomplete.module.css +++ b/src/sidebar/search/AddressInputAutocomplete.module.css @@ -48,7 +48,20 @@ } .moreResultsEntry { - padding: 0.5em 0em; + padding: 0.5em 0; +} + +.poiEntry { + padding: 0.5em 0; + display: flex; + flex-direction: row; + gap: 0.5rem; + text-align: start; + margin: 0.4rem 0.5rem; +} + +.poiEntryPrimaryText { + font-weight: bold; } .geocodingEntry { diff --git a/src/sidebar/search/AddressInputAutocomplete.tsx b/src/sidebar/search/AddressInputAutocomplete.tsx index 90b78bcd..902bf6db 100644 --- a/src/sidebar/search/AddressInputAutocomplete.tsx +++ b/src/sidebar/search/AddressInputAutocomplete.tsx @@ -3,6 +3,7 @@ import CurrentLocationIcon from './current-location.svg' import { tr } from '@/translation/Translation' import { Bbox } from '@/api/graphhopper' import { useState } from 'react' +import { AddressParseResult } from '@/api/Api' export interface AutocompleteItem {} @@ -34,6 +35,14 @@ export class MoreResultsItem implements AutocompleteItem { } } +export class POIQueryItem implements AutocompleteItem { + result: AddressParseResult + + constructor(result: AddressParseResult) { + this.result = result + } +} + export interface AutocompleteProps { items: AutocompleteItem[] highlightedItem: AutocompleteItem @@ -59,6 +68,8 @@ function mapToComponent(item: AutocompleteItem, isHighlighted: boolean, onSelect return else if (item instanceof MoreResultsItem) return + else if (item instanceof POIQueryItem) + return else throw Error('Unsupported item type: ' + typeof item) } @@ -80,6 +91,26 @@ export function MoreResultsEntry({ ) } +export function POIQueryEntry({ + item, + isHighlighted, + onSelect, +}: { + item: POIQueryItem + isHighlighted: boolean + onSelect: (item: POIQueryItem) => void +}) { + // TODO NOW translate! + return ( + onSelect(item)}> +
+ {item.result.poi} + {item.result.location ? 'in ' + item.result.location : 'nearby'} +
+
+ ) +} + export function SelectCurrentLocation({ item, isHighlighted, diff --git a/test/DummyApi.ts b/test/DummyApi.ts index 5fecef4b..69852a43 100644 --- a/test/DummyApi.ts +++ b/test/DummyApi.ts @@ -10,7 +10,12 @@ export default class DummyApi implements Api { }) } - reverseGeocode(query: string | undefined, point: Coordinate, radius: number, tags?: string[]): Promise { + reverseGeocode( + query: string | undefined, + point: Coordinate, + radius: number, + tags?: string[] + ): Promise { return Promise.resolve({ took: 0, hits: [], diff --git a/test/routing/Api.test.ts b/test/routing/Api.test.ts index fabf0865..1823586a 100644 --- a/test/routing/Api.test.ts +++ b/test/routing/Api.test.ts @@ -6,7 +6,7 @@ import { RouteRequestFailed, RouteRequestSuccess } from '@/actions/Actions' import { setTranslation } from '@/translation/Translation' import Dispatcher from '@/stores/Dispatcher' -import { ApiImpl } from '@/api/Api' +import {AddressParseResult, ApiImpl} from '@/api/Api' import { ApiInfo, ErrorResponse, RoutingArgs, RoutingRequest, RoutingResultInfo } from '@/api/graphhopper' beforeAll(() => { @@ -344,22 +344,34 @@ describe('route', () => { }) describe('reverse geocoder', () => { - it('should parse correctly', async () => { - let res = ApiImpl.parseAddress('dresden restaurant') + it('should parse fully', async () => { + let res = AddressParseResult.parse('dresden restaurant', false) expect(res.location).toEqual('dresden') expect(res.icon).toEqual('restaurant') - res = ApiImpl.parseAddress('restaurant') + res = AddressParseResult.parse('restaurant', false) expect(res.location).toEqual('') expect(res.icon).toEqual('restaurant') - res = ApiImpl.parseAddress('restaurant in dresden') + res = AddressParseResult.parse('restaurant in dresden', false) expect(res.location).toEqual('dresden') expect(res.icon).toEqual('restaurant') - res = ApiImpl.parseAddress('airports around some thing else') + res = AddressParseResult.parse('airports around some thing else', false) expect(res.location).toEqual('some thing else') expect(res.icon).toEqual('flight_takeoff') + + res = AddressParseResult.parse('dresden super market', false) + expect(res.location).toEqual('dresden') + expect(res.poi).toEqual('super markets') + + res = AddressParseResult.parse('dresden park', false) + expect(res.location).toEqual('dresden') + expect(res.poi).toEqual('parks') + + res = AddressParseResult.parse('dresden parking', false) + expect(res.location).toEqual('dresden') + expect(res.poi).toEqual('parking') }) }) diff --git a/test/stores/QueryStore.test.ts b/test/stores/QueryStore.test.ts index c88133fe..574563b4 100644 --- a/test/stores/QueryStore.test.ts +++ b/test/stores/QueryStore.test.ts @@ -31,7 +31,12 @@ class ApiMock implements Api { throw Error('not implemented') } - reverseGeocode(query: string | undefined, point: Coordinate, radius: number, tags?: string[]): Promise { + reverseGeocode( + query: string | undefined, + point: Coordinate, + radius: number, + tags?: string[] + ): Promise { throw Error('not implemented') } diff --git a/test/stores/RouteStore.test.ts b/test/stores/RouteStore.test.ts index cf8cde72..5093b812 100644 --- a/test/stores/RouteStore.test.ts +++ b/test/stores/RouteStore.test.ts @@ -69,7 +69,12 @@ class DummyApi implements Api { throw Error('not implemented') } - reverseGeocode(query: string | undefined, point: Coordinate, radius: number, tags?: string[]): Promise { + reverseGeocode( + query: string | undefined, + point: Coordinate, + radius: number, + tags?: string[] + ): Promise { throw Error('not implemented') } From a58721fbb57adda9a2ac6cc3995c2fb54ec63eb9 Mon Sep 17 00:00:00 2001 From: Peter Date: Sun, 2 Jun 2024 00:56:28 +0200 Subject: [PATCH 03/33] fix poi search without location keywords --- src/App.tsx | 46 ++--------------- src/api/Api.ts | 6 +-- src/sidebar/MobileSidebar.tsx | 8 +-- src/sidebar/search/AddressInput.tsx | 50 +++++++++++-------- .../search/AddressInputAutocomplete.tsx | 4 +- src/sidebar/search/Search.tsx | 23 +++------ test/routing/Api.test.ts | 2 +- 7 files changed, 51 insertions(+), 88 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index caa22baa..4b863dbc 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -18,7 +18,7 @@ import MobileSidebar from '@/sidebar/MobileSidebar' import { useMediaQuery } from 'react-responsive' import RoutingResults from '@/sidebar/RoutingResults' import PoweredBy from '@/sidebar/PoweredBy' -import { Coordinate, QueryStoreState, RequestState } from '@/stores/QueryStore' +import { QueryStoreState, RequestState } from '@/stores/QueryStore' import { RouteStoreState } from '@/stores/RouteStore' import { MapOptionsStoreState } from '@/stores/MapOptionsStore' import { ErrorStoreState } from '@/stores/ErrorStore' @@ -45,8 +45,6 @@ import useExternalMVTLayer from '@/layers/UseExternalMVTLayer' import LocationButton from '@/map/LocationButton' import { SettingsContext } from '@/contexts/SettingsContext' import usePOIsLayer from '@/layers/UsePOIsLayer' -import { calcDist } from '@/distUtils' -import { toLonLat, transformExtent } from 'ol/proj' export const POPUP_CONTAINER_ID = 'popup-container' export const SIDEBAR_CONTENT_ID = 'sidebar-content' @@ -119,13 +117,6 @@ export default function App() { usePathDetailsLayer(map, pathDetails) usePOIsLayer(map, pois) - const center = map.getView().getCenter() ? toLonLat(map.getView().getCenter()!) : [13.4, 52.5] - const mapCenter = { lng: center[0], lat: center[1] } - - const origExtent = map.getView().calculateExtent(map.getSize()) - var extent = transformExtent(origExtent, 'EPSG:3857', 'EPSG:4326') - const mapRadius = calcDist({ lng: extent[0], lat: extent[1] }, { lng: extent[2], lat: extent[3] }) / 2 / 1000 - const isSmallScreen = useMediaQuery({ query: '(max-width: 44rem)' }) return ( @@ -137,8 +128,6 @@ export default function App() { query={query} route={route} map={map} - mapCenter={mapCenter} - mapRadius={mapRadius} mapOptions={mapOptions} error={error} encodedValues={info.encoded_values} @@ -149,8 +138,6 @@ export default function App() { query={query} route={route} map={map} - mapCenter={mapCenter} - mapRadius={mapRadius} mapOptions={mapOptions} error={error} encodedValues={info.encoded_values} @@ -170,21 +157,9 @@ interface LayoutProps { error: ErrorStoreState encodedValues: object[] drawAreas: boolean - mapCenter: Coordinate - mapRadius: number } -function LargeScreenLayout({ - query, - route, - map, - error, - mapOptions, - encodedValues, - drawAreas, - mapCenter, - mapRadius, -}: LayoutProps) { +function LargeScreenLayout({ query, route, map, error, mapOptions, encodedValues, drawAreas }: LayoutProps) { const [showSidebar, setShowSidebar] = useState(true) const [showCustomModelBox, setShowCustomModelBox] = useState(false) return ( @@ -211,7 +186,7 @@ function LargeScreenLayout({ drawAreas={drawAreas} /> )} - +
{!error.isDismissed && }
@@ -268,8 +233,7 @@ function SmallScreenLayout({ error={error} encodedValues={encodedValues} drawAreas={drawAreas} - mapCenter={mapCenter} - mapRadius={mapRadius} + map={map} />
diff --git a/src/api/Api.ts b/src/api/Api.ts index 063ead00..3418d571 100644 --- a/src/api/Api.ts +++ b/src/api/Api.ts @@ -492,15 +492,15 @@ export class AddressParseResult { for (const val of values) { // two word phrases like 'public transit' must be checked before single word phrases - for(const keyword of val.k) { + for (const keyword of val.k) { const i = bigrams.indexOf(keyword) if (i >= 0) return new AddressParseResult(cleanQuery.replace(bigrams[i], '').trim(), val.t, val.i, val.k[0]) } - for(const keyword of val.k) { + for (const keyword of val.k) { const i = queryTokens.indexOf(keyword) - if(i >= 0) + if (i >= 0) return new AddressParseResult(cleanQuery.replace(queryTokens[i], '').trim(), val.t, val.i, val.k[0]) } } diff --git a/src/sidebar/MobileSidebar.tsx b/src/sidebar/MobileSidebar.tsx index 5cd53669..390ac194 100644 --- a/src/sidebar/MobileSidebar.tsx +++ b/src/sidebar/MobileSidebar.tsx @@ -11,6 +11,7 @@ import RoutingProfiles from '@/sidebar/search/routingProfiles/RoutingProfiles' import OpenInputsIcon from './unfold.svg' import CloseInputsIcon from './unfold_less.svg' import CustomModelBox from '@/sidebar/CustomModelBox' +import { Map } from 'ol' type MobileSidebarProps = { query: QueryStoreState @@ -18,11 +19,10 @@ type MobileSidebarProps = { error: ErrorStoreState encodedValues: object[] drawAreas: boolean - mapCenter: Coordinate - mapRadius: number + map: Map } -export default function ({ query, route, error, encodedValues, drawAreas, mapCenter, mapRadius }: MobileSidebarProps) { +export default function ({ query, route, error, encodedValues, drawAreas, map }: MobileSidebarProps) { const [showCustomModelBox, setShowCustomModelBox] = useState(false) // the following three elements control, whether the small search view is displayed const isShortScreen = useMediaQuery({ query: '(max-height: 55rem)' }) @@ -74,7 +74,7 @@ export default function ({ query, route, error, encodedValues, drawAreas, mapCen drawAreas={drawAreas} /> )} - +
)} {!error.isDismissed && } diff --git a/src/sidebar/search/AddressInput.tsx b/src/sidebar/search/AddressInput.tsx index 09969f54..039eb862 100644 --- a/src/sidebar/search/AddressInput.tsx +++ b/src/sidebar/search/AddressInput.tsx @@ -21,6 +21,9 @@ import PlainButton from '@/PlainButton' import { onCurrentLocationSelected } from '@/map/MapComponent' import Dispatcher from '@/stores/Dispatcher' import { SetBBox, SetPOI } from '@/actions/Actions' +import { toLonLat, transformExtent } from 'ol/proj' +import { calcDist } from '@/distUtils' +import { Map } from 'ol' export interface AddressInputProps { point: QueryPoint @@ -32,8 +35,7 @@ export interface AddressInputProps { moveStartIndex: number dropPreviewIndex: number index: number - mapCenter: Coordinate - mapRadius: number + map: Map } export default function AddressInput(props: AddressInputProps) { @@ -96,7 +98,9 @@ export default function AddressInput(props: AddressInputProps) { Dispatcher.dispatch(new SetBBox(bbox)) Dispatcher.dispatch(new SetPOI(pois)) } else { - console.warn('invalid bbox for points ' + JSON.stringify(pois) + " result was: " + JSON.stringify(parseResult)) + console.warn( + 'invalid bbox for points ' + JSON.stringify(pois) + ' result was: ' + JSON.stringify(parseResult) + ) } }) ) @@ -258,8 +262,22 @@ export default function AddressInput(props: AddressInputProps) { } else if (item instanceof POIQueryItem) { hideSuggestions() if (item.result.hasPOIs()) { - console.log(item.result.location) - poiSearch.request(item.result, props.mapCenter, Math.min(props.mapRadius, 100)) + const center = props.map.getView().getCenter() + ? toLonLat(props.map.getView().getCenter()!) + : [13.4, 52.5] + const mapCenter = { lng: center[0], lat: center[1] } + + const origExtent = props.map.getView().calculateExtent(props.map.getSize()) + var extent = transformExtent(origExtent, 'EPSG:3857', 'EPSG:4326') + const mapRadius = + calcDist( + { lng: extent[0], lat: extent[1] }, + { lng: extent[2], lat: extent[3] } + ) / + 2 / + 1000 + + poiSearch.request(item.result, mapCenter, Math.min(mapRadius, 100)) } } searchInput.current!.blur() @@ -388,31 +406,21 @@ class ReverseGeocoder { await this.timeout.wait() try { let hits: GeocodingHit[] = [] - let searchCoordinate: Coordinate | undefined = undefined + let result + if (parseResult.location) { let options: Record = { point: coordinateToText(point), location_bias_scale: '0.5', zoom: '9', } - let result = await this.api.geocode(parseResult.location, 'default', options) + result = await this.api.geocode(parseResult.location, 'default', options) hits = result.hits - if (result.hits.length > 0) searchCoordinate = result.hits[0].point - else if (point) searchCoordinate = point + if (hits.length > 0) result = await this.api.reverseGeocode('', hits[0].point, radius, parseResult.tags) } else if (point) { - searchCoordinate = point + result = await this.api.reverseGeocode('', point, radius, parseResult.tags) } - - if (!searchCoordinate) { - hits = [] - } else if (hits.length > 0) { - // TODO NOW should we include parseResult.location here again if searchCoordinate is from forward geocode request? - // parseResult.location - console.log("radius "+radius) - const result = await this.api.reverseGeocode('', searchCoordinate, radius, parseResult.tags) - hits = Geocoder.filterDuplicates(result.hits) - } - + if (result) hits = Geocoder.filterDuplicates(result.hits) if (currentId === this.requestId) this.onSuccess(hits, parseResult) } catch (reason) { throw Error('Could not get geocoding results because: ' + reason) diff --git a/src/sidebar/search/AddressInputAutocomplete.tsx b/src/sidebar/search/AddressInputAutocomplete.tsx index 902bf6db..fb62e1c0 100644 --- a/src/sidebar/search/AddressInputAutocomplete.tsx +++ b/src/sidebar/search/AddressInputAutocomplete.tsx @@ -105,7 +105,9 @@ export function POIQueryEntry({ onSelect(item)}>
{item.result.poi} - {item.result.location ? 'in ' + item.result.location : 'nearby'} + + {item.result.location ? 'in ' + item.result.location : 'nearby'} +
) diff --git a/src/sidebar/search/Search.tsx b/src/sidebar/search/Search.tsx index 574ecc4c..c62d31dd 100644 --- a/src/sidebar/search/Search.tsx +++ b/src/sidebar/search/Search.tsx @@ -7,21 +7,14 @@ import RemoveIcon from './minus-circle-solid.svg' import AddIcon from './plus-circle-solid.svg' import TargetIcon from './send.svg' import PlainButton from '@/PlainButton' +import { Map } from 'ol' import AddressInput from '@/sidebar/search/AddressInput' import { MarkerComponent } from '@/map/Marker' import { tr } from '@/translation/Translation' import SettingsBox from '@/sidebar/SettingsBox' -export default function Search({ - points, - mapCenter, - mapRadius, -}: { - points: QueryPoint[] - mapCenter: Coordinate - mapRadius: number -}) { +export default function Search({ points, map }: { points: QueryPoint[]; map: Map }) { const [showSettings, setShowSettings] = useState(false) const [showTargetIcons, setShowTargetIcons] = useState(true) const [moveStartIndex, onMoveStartSelect] = useState(-1) @@ -48,8 +41,7 @@ export default function Search({ }} dropPreviewIndex={dropPreviewIndex} onDropPreviewSelect={onDropPreviewSelect} - mapCenter={mapCenter} - mapRadius={mapRadius} + map={map} /> ))}
@@ -85,8 +77,7 @@ const SearchBox = ({ onMoveStartSelect, dropPreviewIndex, onDropPreviewSelect, - mapCenter, - mapRadius, + map, }: { index: number points: QueryPoint[] @@ -97,8 +88,7 @@ const SearchBox = ({ onMoveStartSelect: (index: number, showTargetIcon: boolean) => void dropPreviewIndex: number onDropPreviewSelect: (index: number) => void - mapCenter: Coordinate - mapRadius: number + map: Map }) => { const point = points[index] @@ -176,8 +166,7 @@ const SearchBox = ({
{ From 895c9c6cf095c4f995a1f5c56f33fb5bce07da5d Mon Sep 17 00:00:00 2001 From: Peter Date: Sun, 2 Jun 2024 01:28:27 +0200 Subject: [PATCH 04/33] proper marker Select --- src/api/Api.ts | 2 +- src/layers/UsePOIsLayer.tsx | 32 +++++++++++++++++++++++--------- src/stores/POIsStore.ts | 1 - 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/src/api/Api.ts b/src/api/Api.ts index 3418d571..f3ab5a69 100644 --- a/src/api/Api.ts +++ b/src/api/Api.ts @@ -469,7 +469,7 @@ export class AddressParseResult { { k: ['bus stops'], t: ['highway:bus_stop'], i: 'train' }, { k: ['railway stations', 'railway station'], t: ['railway:station'], i: 'train' }, { k: ['super markets', 'super market'], t: ['shop:supermarket', 'building:supermarket'], i: 'store' }, - { k: ['hotels', 'hotel'], t: ['amenity:hotel', 'building:hotel'], i: 'hotel' }, + { k: ['hotels', 'hotel'], t: ['amenity:hotel', 'building:hotel', 'tourism:hotel'], i: 'hotel' }, { k: ['tourism'], t: ['tourism'], i: 'luggage' }, { k: ['museums', 'museum'], t: ['tourism:museum', 'building:museum'], i: 'museum' }, { k: ['pharmacies', 'pharmacy'], t: ['amenity:pharmacy'], i: 'local_pharmacy' }, diff --git a/src/layers/UsePOIsLayer.tsx b/src/layers/UsePOIsLayer.tsx index 7d5515a3..3cbf2c07 100644 --- a/src/layers/UsePOIsLayer.tsx +++ b/src/layers/UsePOIsLayer.tsx @@ -21,6 +21,7 @@ import store_svg from '/src/pois/img/store.svg' import train_svg from '/src/pois/img/train.svg' import universal_currency_alt_svg from '/src/pois/img/universal_currency_alt.svg' import { createPOIMarker } from '@/layers/createMarkerSVG' +import {Select} from "ol/interaction"; const svgStrings: { [id: string]: string } = {} @@ -50,10 +51,11 @@ for (const k in svgObjects) { export default function usePOIsLayer(map: Map, poisState: POIsStoreState) { useEffect(() => { removePOIs(map) - console.log('poi count: ' + poisState.pois.length) addPOIsLayer(map, poisState.pois) + const select = addPOISelection(map) return () => { removePOIs(map) + map.removeInteraction(select) } }, [map, poisState.pois]) } @@ -65,6 +67,26 @@ function removePOIs(map: Map) { .forEach(l => map.removeLayer(l)) } +function addPOISelection(map: Map) { + const select = new Select(); + map.addInteraction(select); + select.on('select', event => { + const selectedFeatures = event.selected; + if (selectedFeatures.length > 0) { + const feature = selectedFeatures[0] + const props = feature.get('gh:marker_props') + feature.setStyle(new Style({ + image: new Icon({ + color: 'rgba(100%, 0, 0, 30%)', + src: 'data:image/svg+xml;utf8,' + svgStrings[props.icon], + displacement: [0, 18], + }), + })) + } + }) + return select +} + function addPOIsLayer(map: Map, pois: POI[]) { const features = pois.map((poi, i) => { const feature = new Feature({ @@ -94,13 +116,5 @@ function addPOIsLayer(map: Map, pois: POI[]) { return style }) map.addLayer(poisLayer) - map.on('click', e => { - poisLayer.getFeatures(e.pixel).then(features => { - if (features.length > 0) { - const props = features[0].getProperties().get('gh:marker_props') - props.poi.selected = true - } - }) - }) return poisLayer } diff --git a/src/stores/POIsStore.ts b/src/stores/POIsStore.ts index 1a0c6833..b58bd360 100644 --- a/src/stores/POIsStore.ts +++ b/src/stores/POIsStore.ts @@ -8,7 +8,6 @@ export interface POI { name: string icon: string coordinate: Coordinate - selected: boolean } export interface POIsStoreState { From d3124dab1d1519b6a067cc4d56a1177a1b1c6287 Mon Sep 17 00:00:00 2001 From: Peter Date: Sun, 2 Jun 2024 01:34:57 +0200 Subject: [PATCH 05/33] change scale, not color --- src/layers/UsePOIsLayer.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/layers/UsePOIsLayer.tsx b/src/layers/UsePOIsLayer.tsx index 3cbf2c07..a6053662 100644 --- a/src/layers/UsePOIsLayer.tsx +++ b/src/layers/UsePOIsLayer.tsx @@ -75,9 +75,10 @@ function addPOISelection(map: Map) { if (selectedFeatures.length > 0) { const feature = selectedFeatures[0] const props = feature.get('gh:marker_props') + // how to change just the scale? feature.setStyle(new Style({ image: new Icon({ - color: 'rgba(100%, 0, 0, 30%)', + scale: [1.4, 1.4], src: 'data:image/svg+xml;utf8,' + svgStrings[props.icon], displacement: [0, 18], }), From bd2f7ae74390d728ba0065a702cb7ae34f31949b Mon Sep 17 00:00:00 2001 From: Peter Date: Sun, 2 Jun 2024 14:50:38 +0200 Subject: [PATCH 06/33] keyboard support --- src/layers/UsePOIsLayer.tsx | 2 +- src/sidebar/search/AddressInput.tsx | 31 ++++++++----------- .../AddressInputAutocomplete.module.css | 2 +- .../search/AddressInputAutocomplete.tsx | 8 ++--- 4 files changed, 19 insertions(+), 24 deletions(-) diff --git a/src/layers/UsePOIsLayer.tsx b/src/layers/UsePOIsLayer.tsx index a6053662..498ac108 100644 --- a/src/layers/UsePOIsLayer.tsx +++ b/src/layers/UsePOIsLayer.tsx @@ -75,8 +75,8 @@ function addPOISelection(map: Map) { if (selectedFeatures.length > 0) { const feature = selectedFeatures[0] const props = feature.get('gh:marker_props') - // how to change just the scale? feature.setStyle(new Style({ + zIndex: 2, image: new Icon({ scale: [1.4, 1.4], src: 'data:image/svg+xml;utf8,' + svgStrings[props.icon], diff --git a/src/sidebar/search/AddressInput.tsx b/src/sidebar/search/AddressInput.tsx index 039eb862..566fc170 100644 --- a/src/sidebar/search/AddressInput.tsx +++ b/src/sidebar/search/AddressInput.tsx @@ -158,6 +158,7 @@ export default function AddressInput(props: AddressInputProps) { const index = highlightedResult >= 0 ? highlightedResult : 0 const item = autocompleteItems[index] if (item instanceof GeocodingItem) props.onAddressSelected(item.toText(), item.point, item.bbox) + else if (item instanceof POIQueryItem) handlePoiSearch(poiSearch, item.result, props.map) } inputElement.blur() // onBlur is deactivated for mobile so force: @@ -261,24 +262,7 @@ export default function AddressInput(props: AddressInputProps) { if (!coordinate) geocoder.request(item.search, biasCoord, 'nominatim') } else if (item instanceof POIQueryItem) { hideSuggestions() - if (item.result.hasPOIs()) { - const center = props.map.getView().getCenter() - ? toLonLat(props.map.getView().getCenter()!) - : [13.4, 52.5] - const mapCenter = { lng: center[0], lat: center[1] } - - const origExtent = props.map.getView().calculateExtent(props.map.getSize()) - var extent = transformExtent(origExtent, 'EPSG:3857', 'EPSG:4326') - const mapRadius = - calcDist( - { lng: extent[0], lat: extent[1] }, - { lng: extent[2], lat: extent[3] } - ) / - 2 / - 1000 - - poiSearch.request(item.result, mapCenter, Math.min(mapRadius, 100)) - } + handlePoiSearch(poiSearch, item.result, props.map) } searchInput.current!.blur() }} @@ -290,6 +274,17 @@ export default function AddressInput(props: AddressInputProps) { ) } +function handlePoiSearch(poiSearch: ReverseGeocoder, result: AddressParseResult, map: Map) { + if (!result.hasPOIs()) return + + const center = map.getView().getCenter() ? toLonLat(map.getView().getCenter()!) : [13.4, 52.5] + const mapCenter = { lng: center[0], lat: center[1] } + const origExtent = map.getView().calculateExtent(map.getSize()) + const extent = transformExtent(origExtent, 'EPSG:3857', 'EPSG:4326') + const mapDiagonal = calcDist({ lng: extent[0], lat: extent[1] },{ lng: extent[2], lat: extent[3] }) + poiSearch.request(result, mapCenter, Math.min(mapDiagonal / 2 / 1000, 100)) +} + function ResponsiveAutocomplete({ inputRef, children, diff --git a/src/sidebar/search/AddressInputAutocomplete.module.css b/src/sidebar/search/AddressInputAutocomplete.module.css index 2a2defb6..716fa163 100644 --- a/src/sidebar/search/AddressInputAutocomplete.module.css +++ b/src/sidebar/search/AddressInputAutocomplete.module.css @@ -55,7 +55,7 @@ padding: 0.5em 0; display: flex; flex-direction: row; - gap: 0.5rem; + gap: 0.4rem; text-align: start; margin: 0.4rem 0.5rem; } diff --git a/src/sidebar/search/AddressInputAutocomplete.tsx b/src/sidebar/search/AddressInputAutocomplete.tsx index fb62e1c0..b3f8d46d 100644 --- a/src/sidebar/search/AddressInputAutocomplete.tsx +++ b/src/sidebar/search/AddressInputAutocomplete.tsx @@ -100,13 +100,13 @@ export function POIQueryEntry({ isHighlighted: boolean onSelect: (item: POIQueryItem) => void }) { - // TODO NOW translate! + const poi = item.result.poi? item.result.poi : '' return ( onSelect(item)}>
- {item.result.poi} - - {item.result.location ? 'in ' + item.result.location : 'nearby'} + {poi.charAt(0).toUpperCase() + poi.slice(1)} + + {item.result.location ? tr('in') + ' ' + item.result.location : tr('nearby')}
From 59ea6abbc952c8b995c8e9ee43d70732657bede8 Mon Sep 17 00:00:00 2001 From: Peter Date: Sun, 2 Jun 2024 20:23:36 +0200 Subject: [PATCH 07/33] show popup and POI being used in route --- src/App.tsx | 2 +- src/NavBar.ts | 18 ++++- src/actions/Actions.ts | 20 ++--- src/api/Api.ts | 6 ++ src/index.tsx | 2 +- src/layers/MapFeaturePopup.module.css | 19 +++++ src/layers/POIPopup.tsx | 50 ++++++++++++ src/layers/UsePOIsLayer.tsx | 31 ++++--- src/map/MapPopups.tsx | 6 +- src/sidebar/search/AddressInput.tsx | 80 ++++++++++++------- .../search/AddressInputAutocomplete.tsx | 6 +- src/stores/POIsStore.ts | 24 +++--- 12 files changed, 190 insertions(+), 74 deletions(-) create mode 100644 src/layers/POIPopup.tsx diff --git a/src/App.tsx b/src/App.tsx index 4b863dbc..e0865131 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -121,7 +121,7 @@ export default function App() { return (
- + {isSmallScreen ? ( !p.isInitialized && p.queryText.length > 0)) { const promises = parsedPoints.map(p => { if (p.isInitialized) return Promise.resolve(p) + const result = AddressParseResult.parse(p.queryText, false) + if (result.hasPOIs() && result.location) { + // two stage POI search: 1. use extracted location to get coordinates 2. do reverse geocoding with this coordinates + getApi() + .geocode(result.location, 'nominatim') + .then(res => { + if (res.hits.length != 0) + getApi() + .reverseGeocode('', res.hits[0].point, 100, result.tags) + .then(res => ReverseGeocoder.handleGeocodingResponse(res.hits, result, p)) + }) + return Promise.resolve(p) + } return ( getApi() .geocode(p.queryText, 'nominatim') diff --git a/src/actions/Actions.ts b/src/actions/Actions.ts index e78c8baa..1aeefb4f 100644 --- a/src/actions/Actions.ts +++ b/src/actions/Actions.ts @@ -248,24 +248,20 @@ export class UpdateSettings implements Action { } } -export class SearchPOI implements Action { - readonly query: string - readonly coordinate: Coordinate - readonly radius: number - readonly icon: string +export class SelectPOI implements Action { + readonly selected: POI | null - constructor(icon: string, query: string, coordinate: Coordinate, radius: number) { - this.icon = icon - this.query = query - this.coordinate = coordinate - this.radius = radius + constructor(selected: POI | null) { + this.selected = selected } } -export class SetPOI implements Action { +export class SetPOIs implements Action { readonly pois: POI[] + readonly oldQueryPoint: QueryPoint | null - constructor(pois: POI[]) { + constructor(pois: POI[], oldQueryPoint: QueryPoint | null) { this.pois = pois + this.oldQueryPoint = oldQueryPoint } } diff --git a/src/api/Api.ts b/src/api/Api.ts index f3ab5a69..d411173c 100644 --- a/src/api/Api.ts +++ b/src/api/Api.ts @@ -4,6 +4,7 @@ import { ApiInfo, Bbox, ErrorResponse, + GeocodingHit, GeocodingResult, Path, RawPath, @@ -17,6 +18,7 @@ import { LineString } from 'geojson' import { getTranslation, tr } from '@/translation/Translation' import * as config from 'config' import { Coordinate } from '@/stores/QueryStore' +import { hitToItem } from '@/Converters' interface ApiProfile { name: string @@ -460,6 +462,10 @@ export class AddressParseResult { return this.tags.length > 0 } + text(prefix: string) { + return prefix + ' ' + (this.location ? tr('in') + ' ' + this.location : tr('nearby')) + } + public static parse(query: string, incomplete: boolean): AddressParseResult { query = query.toLowerCase() diff --git a/src/index.tsx b/src/index.tsx index 3aebedd8..2e45f12a 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -55,7 +55,7 @@ setStores({ mapOptionsStore: new MapOptionsStore(), pathDetailsStore: new PathDetailsStore(), mapFeatureStore: new MapFeatureStore(), - poisStore: new POIsStore(getApi()), + poisStore: new POIsStore(), }) setMap(createMap()) diff --git a/src/layers/MapFeaturePopup.module.css b/src/layers/MapFeaturePopup.module.css index fd957eef..c6f49572 100644 --- a/src/layers/MapFeaturePopup.module.css +++ b/src/layers/MapFeaturePopup.module.css @@ -5,3 +5,22 @@ padding: 5px 10px 5px 10px; border-radius: 10px; } + +.poiPopup { + position: relative; + top: -40px; + left: 20px; + max-width: 400px; + + background-color: white; + font-size: small; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2); + box-sizing: border-box; + padding: 5px 10px 5px 10px; + border-radius: 10px; +} + +.poiPopup div:first-child { + font-weight: bold; + padding-bottom: 5px; +} diff --git a/src/layers/POIPopup.tsx b/src/layers/POIPopup.tsx new file mode 100644 index 00000000..9fc2da8e --- /dev/null +++ b/src/layers/POIPopup.tsx @@ -0,0 +1,50 @@ +import React from 'react' +import styles from '@/layers/MapFeaturePopup.module.css' +import MapPopup from '@/layers/MapPopup' +import { Map } from 'ol' +import { POIsStoreState } from '@/stores/POIsStore' +import { tr } from '@/translation/Translation' +import Dispatcher from '@/stores/Dispatcher' +import {SelectPOI, SetPoint, SetPOIs} from '@/actions/Actions' +import PlainButton from '@/PlainButton' + +interface POIStatePopupProps { + map: Map + poiState: POIsStoreState +} + +/** + * The popup shown when certain map features are hovered. For example a road of the routing graph layer. + */ +export default function POIStatePopup({ map, poiState }: POIStatePopupProps) { + const selectedPOI = poiState.selected + const oldQueryPoint = poiState.oldQueryPoint + + return ( + +
+
{selectedPOI?.name}
+
{selectedPOI?.address}
+ { + if (selectedPOI && oldQueryPoint) { + // TODO NOW how to use the POI as either start or destination? + // Might be too unintuitive if it relies on with which input we searched the POIs + const queryPoint = { + ...oldQueryPoint, + queryText: selectedPOI?.name, + coordinate: selectedPOI?.coordinate, + isInitialized: true, + } + Dispatcher.dispatch(new SetPoint(queryPoint, false)) + Dispatcher.dispatch(new SelectPOI(null)) + Dispatcher.dispatch(new SetPOIs([], null)) + } + }} + > + {tr('Use in route')} + +
+
+ ) +} diff --git a/src/layers/UsePOIsLayer.tsx b/src/layers/UsePOIsLayer.tsx index 498ac108..fa97764a 100644 --- a/src/layers/UsePOIsLayer.tsx +++ b/src/layers/UsePOIsLayer.tsx @@ -21,7 +21,9 @@ import store_svg from '/src/pois/img/store.svg' import train_svg from '/src/pois/img/train.svg' import universal_currency_alt_svg from '/src/pois/img/universal_currency_alt.svg' import { createPOIMarker } from '@/layers/createMarkerSVG' -import {Select} from "ol/interaction"; +import { Select } from 'ol/interaction' +import Dispatcher from '@/stores/Dispatcher' +import { SelectPOI } from '@/actions/Actions' const svgStrings: { [id: string]: string } = {} @@ -68,22 +70,25 @@ function removePOIs(map: Map) { } function addPOISelection(map: Map) { - const select = new Select(); - map.addInteraction(select); + const select = new Select() + map.addInteraction(select) select.on('select', event => { - const selectedFeatures = event.selected; + const selectedFeatures = event.selected if (selectedFeatures.length > 0) { const feature = selectedFeatures[0] const props = feature.get('gh:marker_props') - feature.setStyle(new Style({ - zIndex: 2, - image: new Icon({ - scale: [1.4, 1.4], - src: 'data:image/svg+xml;utf8,' + svgStrings[props.icon], - displacement: [0, 18], - }), - })) - } + feature.setStyle( + new Style({ + zIndex: 2, + image: new Icon({ + scale: [1.4, 1.4], + src: 'data:image/svg+xml;utf8,' + svgStrings[props.icon], + displacement: [0, 18], + }), + }) + ) + Dispatcher.dispatch(new SelectPOI(props.poi)) + } else Dispatcher.dispatch(new SelectPOI(null)) }) return select } diff --git a/src/map/MapPopups.tsx b/src/map/MapPopups.tsx index 4578dd83..39770a31 100644 --- a/src/map/MapPopups.tsx +++ b/src/map/MapPopups.tsx @@ -5,14 +5,17 @@ import InstructionPopup from '@/layers/InstructionPopup' import React from 'react' import { PathDetailsStoreState } from '@/stores/PathDetailsStore' import { MapFeatureStoreState } from '@/stores/MapFeatureStore' +import { POI, POIsStoreState } from '@/stores/POIsStore' +import POIStatePopup from '@/layers/POIPopup' interface MapPopupProps { map: Map pathDetails: PathDetailsStoreState mapFeatures: MapFeatureStoreState + poiState: POIsStoreState } -export default function MapPopups({ map, pathDetails, mapFeatures }: MapPopupProps) { +export default function MapPopups({ map, pathDetails, mapFeatures, poiState }: MapPopupProps) { return ( <> @@ -26,6 +29,7 @@ export default function MapPopups({ map, pathDetails, mapFeatures }: MapPopupPro instructionText={mapFeatures.instructionText} coordinate={mapFeatures.instructionCoordinate} /> + ) } diff --git a/src/sidebar/search/AddressInput.tsx b/src/sidebar/search/AddressInput.tsx index 566fc170..f63c464f 100644 --- a/src/sidebar/search/AddressInput.tsx +++ b/src/sidebar/search/AddressInput.tsx @@ -20,7 +20,7 @@ import PopUp from '@/sidebar/search/PopUp' import PlainButton from '@/PlainButton' import { onCurrentLocationSelected } from '@/map/MapComponent' import Dispatcher from '@/stores/Dispatcher' -import { SetBBox, SetPOI } from '@/actions/Actions' +import { SetBBox, SetPOIs } from '@/actions/Actions' import { toLonLat, transformExtent } from 'ol/proj' import { calcDist } from '@/distUtils' import { Map } from 'ol' @@ -81,29 +81,7 @@ export default function AddressInput(props: AddressInputProps) { }) ) - const [poiSearch] = useState( - new ReverseGeocoder(getApi(), (hits, parseResult) => { - const pois = hits.map(hit => { - const res = hitToItem(hit) - return { - name: res.mainText, - icon: parseResult.icon, - coordinate: hit.point, - selected: false, - } - }) - - const bbox = ApiImpl.getBBoxPoints(pois.map(p => p.coordinate)) - if (bbox) { - Dispatcher.dispatch(new SetBBox(bbox)) - Dispatcher.dispatch(new SetPOI(pois)) - } else { - console.warn( - 'invalid bbox for points ' + JSON.stringify(pois) + ' result was: ' + JSON.stringify(parseResult) - ) - } - }) - ) + const [poiSearch] = useState(new ReverseGeocoder(getApi(), props.point, ReverseGeocoder.handleGeocodingResponse)) // if item is selected we need to clear the autocompletion list useEffect(() => setAutocompleteItems([]), [props.point]) @@ -158,7 +136,10 @@ export default function AddressInput(props: AddressInputProps) { const index = highlightedResult >= 0 ? highlightedResult : 0 const item = autocompleteItems[index] if (item instanceof GeocodingItem) props.onAddressSelected(item.toText(), item.point, item.bbox) - else if (item instanceof POIQueryItem) handlePoiSearch(poiSearch, item.result, props.map) + else if (item instanceof POIQueryItem) { + handlePoiSearch(poiSearch, item.result, props.map) + props.onAddressSelected(item.result.text(item.result.poi), undefined, undefined) + } } inputElement.blur() // onBlur is deactivated for mobile so force: @@ -263,6 +244,7 @@ export default function AddressInput(props: AddressInputProps) { } else if (item instanceof POIQueryItem) { hideSuggestions() handlePoiSearch(poiSearch, item.result, props.map) + setText(item.result.text(item.result.poi)) } searchInput.current!.blur() }} @@ -281,7 +263,7 @@ function handlePoiSearch(poiSearch: ReverseGeocoder, result: AddressParseResult, const mapCenter = { lng: center[0], lat: center[1] } const origExtent = map.getView().calculateExtent(map.getSize()) const extent = transformExtent(origExtent, 'EPSG:3857', 'EPSG:4326') - const mapDiagonal = calcDist({ lng: extent[0], lat: extent[1] },{ lng: extent[2], lat: extent[3] }) + const mapDiagonal = calcDist({ lng: extent[0], lat: extent[1] }, { lng: extent[2], lat: extent[3] }) poiSearch.request(result, mapCenter, Math.min(mapDiagonal / 2 / 1000, 100)) } @@ -375,15 +357,21 @@ class Geocoder { } } -class ReverseGeocoder { +export class ReverseGeocoder { private requestId = 0 private readonly timeout = new Timout(200) private readonly api: Api - private readonly onSuccess: (hits: GeocodingHit[], parseResult: AddressParseResult) => void - - constructor(api: Api, onSuccess: (hits: GeocodingHit[], parseResult: AddressParseResult) => void) { + private readonly onSuccess: (hits: GeocodingHit[], parseResult: AddressParseResult, queryPoint: QueryPoint) => void + private readonly queryPoint: QueryPoint + + constructor( + api: Api, + queryPoint: QueryPoint, + onSuccess: (hits: GeocodingHit[], parseResult: AddressParseResult, queryPoint: QueryPoint) => void + ) { this.api = api this.onSuccess = onSuccess + this.queryPoint = queryPoint } cancel() { @@ -416,7 +404,7 @@ class ReverseGeocoder { result = await this.api.reverseGeocode('', point, radius, parseResult.tags) } if (result) hits = Geocoder.filterDuplicates(result.hits) - if (currentId === this.requestId) this.onSuccess(hits, parseResult) + if (currentId === this.requestId) this.onSuccess(hits, parseResult, this.queryPoint) } catch (reason) { throw Error('Could not get geocoding results because: ' + reason) } @@ -426,6 +414,36 @@ class ReverseGeocoder { this.requestId++ return this.requestId } + + public static handleGeocodingResponse( + hits: GeocodingHit[], + parseResult: AddressParseResult, + queryPoint: QueryPoint + ) { + if (hits.length == 0) return + const pois = ReverseGeocoder.map(hits, parseResult) + const bbox = ApiImpl.getBBoxPoints(pois.map(p => p.coordinate)) + if (bbox) { + Dispatcher.dispatch(new SetBBox(bbox)) + Dispatcher.dispatch(new SetPOIs(pois, queryPoint)) + } else { + console.warn( + 'invalid bbox for points ' + JSON.stringify(pois) + ' result was: ' + JSON.stringify(parseResult) + ) + } + } + + private static map(hits: GeocodingHit[], parseResult: AddressParseResult) { + return hits.map(hit => { + const res = hitToItem(hit) + return { + name: res.mainText, + icon: parseResult.icon, + coordinate: hit.point, + address: res.secondText, + } + }) + } } class Timout { diff --git a/src/sidebar/search/AddressInputAutocomplete.tsx b/src/sidebar/search/AddressInputAutocomplete.tsx index b3f8d46d..b8d52d01 100644 --- a/src/sidebar/search/AddressInputAutocomplete.tsx +++ b/src/sidebar/search/AddressInputAutocomplete.tsx @@ -100,14 +100,12 @@ export function POIQueryEntry({ isHighlighted: boolean onSelect: (item: POIQueryItem) => void }) { - const poi = item.result.poi? item.result.poi : '' + const poi = item.result.poi ? item.result.poi : '' return ( onSelect(item)}>
{poi.charAt(0).toUpperCase() + poi.slice(1)} - - {item.result.location ? tr('in') + ' ' + item.result.location : tr('nearby')} - + {item.result.text('')}
) diff --git a/src/stores/POIsStore.ts b/src/stores/POIsStore.ts index b58bd360..950e9daa 100644 --- a/src/stores/POIsStore.ts +++ b/src/stores/POIsStore.ts @@ -1,31 +1,37 @@ -import { Coordinate } from '@/stores/QueryStore' +import { Coordinate, QueryPoint } from '@/stores/QueryStore' import Store from '@/stores/Store' import { Action } from '@/stores/Dispatcher' -import { SetPOI } from '@/actions/Actions' -import Api from '@/api/Api' +import { SelectPOI, SetPOIs } from '@/actions/Actions' export interface POI { name: string icon: string coordinate: Coordinate + address: string } export interface POIsStoreState { pois: POI[] + selected: POI | null + oldQueryPoint: QueryPoint | null } export default class POIsStore extends Store { - private readonly api: Api - - constructor(api: Api) { - super({ pois: [] }) - this.api = api + constructor() { + super({ pois: [], selected: null, oldQueryPoint: null }) } reduce(state: POIsStoreState, action: Action): POIsStoreState { - if (action instanceof SetPOI) { + if (action instanceof SetPOIs) { return { pois: action.pois, + oldQueryPoint: action.oldQueryPoint, + selected: null, + } + } else if (action instanceof SelectPOI) { + return { + ...state, + selected: action.selected, } } return state From bbb0f55162da43a166d29901360636e284423a17 Mon Sep 17 00:00:00 2001 From: Peter Date: Sun, 2 Jun 2024 20:47:09 +0200 Subject: [PATCH 08/33] do not zoom if no location; fix bbox if single point; minor layout fix --- src/api/Api.ts | 30 ++++++++++++------- src/layers/MapFeaturePopup.module.css | 11 +++++++ src/layers/POIPopup.tsx | 42 +++++++++++++++------------ src/sidebar/search/AddressInput.tsx | 2 +- 4 files changed, 54 insertions(+), 31 deletions(-) diff --git a/src/api/Api.ts b/src/api/Api.ts index d411173c..42e6691e 100644 --- a/src/api/Api.ts +++ b/src/api/Api.ts @@ -440,6 +440,13 @@ export class ApiImpl implements Api { ], [180, 90, -180, -90] as Bbox ) + if (points.length == 1) { + bbox[0] = bbox[0] - 0.001 + bbox[1] = bbox[1] - 0.001 + bbox[2] = bbox[2] + 0.001 + bbox[3] = bbox[3] + 0.001 + } + // return null if the bbox is not valid, e.g. if no url points were given at all return bbox[0] < bbox[2] && bbox[1] < bbox[3] ? bbox : null } @@ -470,23 +477,24 @@ export class AddressParseResult { query = query.toLowerCase() const values = [ - { k: ['restaurants', 'restaurant'], t: ['amenity:restaurant'], i: 'restaurant' }, { k: ['airports', 'airport'], t: ['aeroway:aerodrome'], i: 'flight_takeoff' }, - { k: ['bus stops'], t: ['highway:bus_stop'], i: 'train' }, - { k: ['railway stations', 'railway station'], t: ['railway:station'], i: 'train' }, - { k: ['super markets', 'super market'], t: ['shop:supermarket', 'building:supermarket'], i: 'store' }, - { k: ['hotels', 'hotel'], t: ['amenity:hotel', 'building:hotel', 'tourism:hotel'], i: 'hotel' }, - { k: ['tourism'], t: ['tourism'], i: 'luggage' }, - { k: ['museums', 'museum'], t: ['tourism:museum', 'building:museum'], i: 'museum' }, - { k: ['pharmacies', 'pharmacy'], t: ['amenity:pharmacy'], i: 'local_pharmacy' }, - { k: ['hospitals', 'hospital'], t: ['amenity:hospital', 'building:hospital'], i: 'local_hospital' }, { k: ['banks', 'bank'], t: ['amenity:bank'], i: 'universal_currency_alt' }, + { k: ['bus stops'], t: ['highway:bus_stop'], i: 'train' }, { k: ['education'], t: ['amenity:school', 'building:school', 'building:university'], i: 'school' }, - { k: ['schools', 'school'], t: ['amenity:school', 'building:school'], i: 'school' }, + { k: ['hospitals', 'hospital'], t: ['amenity:hospital', 'building:hospital'], i: 'local_hospital' }, + { k: ['hotels', 'hotel'], t: ['amenity:hotel', 'building:hotel', 'tourism:hotel'], i: 'hotel' }, { k: ['leisure'], t: ['leisure'], i: 'sports_handball' }, + { k: ['museums', 'museum'], t: ['tourism:museum', 'building:museum'], i: 'museum' }, + { k: ['parking'], t: ['amenity:parking'], i: 'local_parking' }, { k: ['parks', 'park'], t: ['leisure:park'], i: 'sports_handball' }, + { k: ['pharmacies', 'pharmacy'], t: ['amenity:pharmacy'], i: 'local_pharmacy' }, { k: ['playgrounds', 'playground'], t: ['leisure:playground'], i: 'sports_handball' }, - { k: ['parking'], t: ['amenity:parking'], i: 'local_parking' }, + { k: ['public transit'], t: ['railway:station', 'highway:bus_stop'], i: 'train' }, + { k: ['railway stations', 'railway station'], t: ['railway:station'], i: 'train' }, + { k: ['restaurants', 'restaurant'], t: ['amenity:restaurant'], i: 'restaurant' }, + { k: ['schools', 'school'], t: ['amenity:school', 'building:school'], i: 'school' }, + { k: ['super markets', 'super market'], t: ['shop:supermarket', 'building:supermarket'], i: 'store' }, + { k: ['tourism'], t: ['tourism'], i: 'luggage' }, ] const smallWords = ['in', 'around', 'nearby'] const queryTokens: string[] = query.split(' ').filter(token => !smallWords.includes(token)) diff --git a/src/layers/MapFeaturePopup.module.css b/src/layers/MapFeaturePopup.module.css index c6f49572..3ee45acd 100644 --- a/src/layers/MapFeaturePopup.module.css +++ b/src/layers/MapFeaturePopup.module.css @@ -24,3 +24,14 @@ font-weight: bold; padding-bottom: 5px; } + +.poiPopupButton svg { + margin-top: 3px; +} + +.poiPopupButton { + padding-top: 5px; + display: flex; + flex-direction: row; + gap: 7px; +} diff --git a/src/layers/POIPopup.tsx b/src/layers/POIPopup.tsx index 9fc2da8e..48bd3c73 100644 --- a/src/layers/POIPopup.tsx +++ b/src/layers/POIPopup.tsx @@ -5,8 +5,9 @@ import { Map } from 'ol' import { POIsStoreState } from '@/stores/POIsStore' import { tr } from '@/translation/Translation' import Dispatcher from '@/stores/Dispatcher' -import {SelectPOI, SetPoint, SetPOIs} from '@/actions/Actions' +import { SelectPOI, SetPoint, SetPOIs } from '@/actions/Actions' import PlainButton from '@/PlainButton' +import { MarkerComponent } from '@/map/Marker' interface POIStatePopupProps { map: Map @@ -25,25 +26,28 @@ export default function POIStatePopup({ map, poiState }: POIStatePopupProps) {
{selectedPOI?.name}
{selectedPOI?.address}
- { - if (selectedPOI && oldQueryPoint) { - // TODO NOW how to use the POI as either start or destination? - // Might be too unintuitive if it relies on with which input we searched the POIs - const queryPoint = { - ...oldQueryPoint, - queryText: selectedPOI?.name, - coordinate: selectedPOI?.coordinate, - isInitialized: true, +
+ {oldQueryPoint && } + { + if (selectedPOI && oldQueryPoint) { + // TODO NOW how to use the POI as either start or destination? + // Might be too unintuitive if it relies on with which input we searched the POIs + const queryPoint = { + ...oldQueryPoint, + queryText: selectedPOI?.name, + coordinate: selectedPOI?.coordinate, + isInitialized: true, + } + Dispatcher.dispatch(new SetPoint(queryPoint, false)) + Dispatcher.dispatch(new SelectPOI(null)) + Dispatcher.dispatch(new SetPOIs([], null)) } - Dispatcher.dispatch(new SetPoint(queryPoint, false)) - Dispatcher.dispatch(new SelectPOI(null)) - Dispatcher.dispatch(new SetPOIs([], null)) - } - }} - > - {tr('Use in route')} - + }} + > + {tr('Use in route')} + +
) diff --git a/src/sidebar/search/AddressInput.tsx b/src/sidebar/search/AddressInput.tsx index f63c464f..cc056bb6 100644 --- a/src/sidebar/search/AddressInput.tsx +++ b/src/sidebar/search/AddressInput.tsx @@ -424,7 +424,7 @@ export class ReverseGeocoder { const pois = ReverseGeocoder.map(hits, parseResult) const bbox = ApiImpl.getBBoxPoints(pois.map(p => p.coordinate)) if (bbox) { - Dispatcher.dispatch(new SetBBox(bbox)) + if (parseResult.location) Dispatcher.dispatch(new SetBBox(bbox)) Dispatcher.dispatch(new SetPOIs(pois, queryPoint)) } else { console.warn( From 006d1bc7a7a2737dcb36f314cbff44964430c1ad Mon Sep 17 00:00:00 2001 From: Peter Date: Mon, 3 Jun 2024 00:21:55 +0200 Subject: [PATCH 09/33] translation; move AddressParseResult into separate class and inject distanceFormat to reduce jest problems --- package-lock.json | 24 +- package.json | 2 +- src/Converters.ts | 6 +- src/NavBar.ts | 17 +- src/api/Api.ts | 72 -- src/index.tsx | 7 +- src/pois/AddressParseResult.ts | 125 ++ src/sidebar/search/AddressInput.tsx | 37 +- .../search/AddressInputAutocomplete.tsx | 3 +- src/translation/tr.json | 1096 +++++++++++++++-- test/NavBar.test.ts | 2 +- test/poi/AddressParseResult.test.ts | 40 + test/routing/Api.test.ts | 34 +- 13 files changed, 1182 insertions(+), 283 deletions(-) create mode 100644 src/pois/AddressParseResult.ts create mode 100644 test/poi/AddressParseResult.test.ts diff --git a/package-lock.json b/package-lock.json index 5c7daef6..ed424c8e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,7 +35,7 @@ "prettier": "2.8.8", "source-map-loader": "^4.0.0", "style-loader": "^3.3.1", - "ts-jest": "^29.0.5", + "ts-jest": "^29.1.4", "ts-loader": "^9.4.2", "typescript": "^5.1.3", "webpack": "^5.75.0", @@ -11954,9 +11954,9 @@ } }, "node_modules/ts-jest": { - "version": "29.1.0", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.0.tgz", - "integrity": "sha512-ZhNr7Z4PcYa+JjMl62ir+zPiNJfXJN6E8hSLnaUKhOgqcn8vb3e537cpkd0FuAfRK3sR1LSqM1MOhliXNgOFPA==", + "version": "29.1.4", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.4.tgz", + "integrity": "sha512-YiHwDhSvCiItoAgsKtoLFCuakDzDsJ1DLDnSouTaTmdOcOwIkSzbLXduaQ6M5DRVhuZC/NYaaZ/mtHbWMv/S6Q==", "dev": true, "dependencies": { "bs-logger": "0.x", @@ -11965,17 +11965,18 @@ "json5": "^2.2.3", "lodash.memoize": "4.x", "make-error": "1.x", - "semver": "7.x", + "semver": "^7.5.3", "yargs-parser": "^21.0.1" }, "bin": { "ts-jest": "cli.js" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" }, "peerDependencies": { "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0", "@jest/types": "^29.0.0", "babel-jest": "^29.0.0", "jest": "^29.0.0", @@ -11985,6 +11986,9 @@ "@babel/core": { "optional": true }, + "@jest/transform": { + "optional": true + }, "@jest/types": { "optional": true }, @@ -21859,9 +21863,9 @@ } }, "ts-jest": { - "version": "29.1.0", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.0.tgz", - "integrity": "sha512-ZhNr7Z4PcYa+JjMl62ir+zPiNJfXJN6E8hSLnaUKhOgqcn8vb3e537cpkd0FuAfRK3sR1LSqM1MOhliXNgOFPA==", + "version": "29.1.4", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.4.tgz", + "integrity": "sha512-YiHwDhSvCiItoAgsKtoLFCuakDzDsJ1DLDnSouTaTmdOcOwIkSzbLXduaQ6M5DRVhuZC/NYaaZ/mtHbWMv/S6Q==", "dev": true, "requires": { "bs-logger": "0.x", @@ -21870,7 +21874,7 @@ "json5": "^2.2.3", "lodash.memoize": "4.x", "make-error": "1.x", - "semver": "7.x", + "semver": "^7.5.3", "yargs-parser": "^21.0.1" }, "dependencies": { diff --git a/package.json b/package.json index 1f8a0d63..f08fc0ee 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "prettier": "2.8.8", "source-map-loader": "^4.0.0", "style-loader": "^3.3.1", - "ts-jest": "^29.0.5", + "ts-jest": "^29.1.4", "ts-loader": "^9.4.2", "typescript": "^5.1.3", "webpack": "^5.75.0", diff --git a/src/Converters.ts b/src/Converters.ts index 361c7e46..6a2f0ac1 100644 --- a/src/Converters.ts +++ b/src/Converters.ts @@ -1,6 +1,7 @@ import { GeocodingHit } from '@/api/graphhopper' import { Coordinate } from '@/stores/QueryStore' +import { Translation } from '@/translation/Translation' export function milliSecondsToText(ms: number) { const hours = Math.floor(ms / 3600000) @@ -12,7 +13,10 @@ export function milliSecondsToText(ms: number) { return (hourText ? hourText + ' ' : '') + minutes + ' min' } -const distanceFormat = new Intl.NumberFormat(navigator.language, { maximumFractionDigits: 1 }) +let distanceFormat: Intl.NumberFormat = new Intl.NumberFormat('en', { maximumFractionDigits: 1 }) +export function setDistanceFormat(_distanceFormat: Intl.NumberFormat) { + distanceFormat = _distanceFormat +} export function metersToText(meters: number, showDistanceInMiles: boolean, forceSmallUnits: boolean = false) { if (showDistanceInMiles) { diff --git a/src/NavBar.ts b/src/NavBar.ts index 8380cb4b..bbed7bbf 100644 --- a/src/NavBar.ts +++ b/src/NavBar.ts @@ -1,20 +1,13 @@ import { coordinateToText } from '@/Converters' -import { Bbox } from '@/api/graphhopper' import Dispatcher from '@/stores/Dispatcher' -import { ClearPoints, SelectMapLayer, SetBBox, SetPOIs, SetQueryPoints, SetVehicleProfile } from '@/actions/Actions' +import { ClearPoints, SelectMapLayer, SetBBox, SetQueryPoints, SetVehicleProfile } from '@/actions/Actions' // import the window like this so that it can be mocked during testing import { window } from '@/Window' -import QueryStore, { - Coordinate, - getBBoxFromCoord, - QueryPoint, - QueryPointType, - QueryStoreState, -} from '@/stores/QueryStore' +import QueryStore, { getBBoxFromCoord, QueryPoint, QueryPointType, QueryStoreState } from '@/stores/QueryStore' import MapOptionsStore, { MapOptionsStoreState } from './stores/MapOptionsStore' -import { AddressParseResult, ApiImpl, getApi } from '@/api/Api' +import { ApiImpl, getApi } from '@/api/Api' import config from 'config' -import { ReverseGeocoder } from '@/sidebar/search/AddressInput' +import { AddressParseResult } from '@/pois/AddressParseResult' export default class NavBar { private readonly queryStore: QueryStore @@ -134,7 +127,7 @@ export default class NavBar { if (res.hits.length != 0) getApi() .reverseGeocode('', res.hits[0].point, 100, result.tags) - .then(res => ReverseGeocoder.handleGeocodingResponse(res.hits, result, p)) + .then(res => AddressParseResult.handleGeocodingResponse(res.hits, result, p)) }) return Promise.resolve(p) } diff --git a/src/api/Api.ts b/src/api/Api.ts index 42e6691e..fce758a6 100644 --- a/src/api/Api.ts +++ b/src/api/Api.ts @@ -18,7 +18,6 @@ import { LineString } from 'geojson' import { getTranslation, tr } from '@/translation/Translation' import * as config from 'config' import { Coordinate } from '@/stores/QueryStore' -import { hitToItem } from '@/Converters' interface ApiProfile { name: string @@ -451,74 +450,3 @@ export class ApiImpl implements Api { return bbox[0] < bbox[2] && bbox[1] < bbox[3] ? bbox : null } } - -export class AddressParseResult { - location: string - tags: string[] - icon: string - poi: string - - constructor(location: string, tags: string[], icon: string, poi: string) { - this.location = location - this.tags = tags - this.icon = icon - this.poi = poi - } - - hasPOIs(): boolean { - return this.tags.length > 0 - } - - text(prefix: string) { - return prefix + ' ' + (this.location ? tr('in') + ' ' + this.location : tr('nearby')) - } - - public static parse(query: string, incomplete: boolean): AddressParseResult { - query = query.toLowerCase() - - const values = [ - { k: ['airports', 'airport'], t: ['aeroway:aerodrome'], i: 'flight_takeoff' }, - { k: ['banks', 'bank'], t: ['amenity:bank'], i: 'universal_currency_alt' }, - { k: ['bus stops'], t: ['highway:bus_stop'], i: 'train' }, - { k: ['education'], t: ['amenity:school', 'building:school', 'building:university'], i: 'school' }, - { k: ['hospitals', 'hospital'], t: ['amenity:hospital', 'building:hospital'], i: 'local_hospital' }, - { k: ['hotels', 'hotel'], t: ['amenity:hotel', 'building:hotel', 'tourism:hotel'], i: 'hotel' }, - { k: ['leisure'], t: ['leisure'], i: 'sports_handball' }, - { k: ['museums', 'museum'], t: ['tourism:museum', 'building:museum'], i: 'museum' }, - { k: ['parking'], t: ['amenity:parking'], i: 'local_parking' }, - { k: ['parks', 'park'], t: ['leisure:park'], i: 'sports_handball' }, - { k: ['pharmacies', 'pharmacy'], t: ['amenity:pharmacy'], i: 'local_pharmacy' }, - { k: ['playgrounds', 'playground'], t: ['leisure:playground'], i: 'sports_handball' }, - { k: ['public transit'], t: ['railway:station', 'highway:bus_stop'], i: 'train' }, - { k: ['railway stations', 'railway station'], t: ['railway:station'], i: 'train' }, - { k: ['restaurants', 'restaurant'], t: ['amenity:restaurant'], i: 'restaurant' }, - { k: ['schools', 'school'], t: ['amenity:school', 'building:school'], i: 'school' }, - { k: ['super markets', 'super market'], t: ['shop:supermarket', 'building:supermarket'], i: 'store' }, - { k: ['tourism'], t: ['tourism'], i: 'luggage' }, - ] - const smallWords = ['in', 'around', 'nearby'] - const queryTokens: string[] = query.split(' ').filter(token => !smallWords.includes(token)) - const cleanQuery = queryTokens.join(' ') - const bigrams: string[] = [] - for (let i = 0; i < queryTokens.length - 1; i++) { - bigrams.push(queryTokens[i] + ' ' + queryTokens[i + 1]) - } - - for (const val of values) { - // two word phrases like 'public transit' must be checked before single word phrases - for (const keyword of val.k) { - const i = bigrams.indexOf(keyword) - if (i >= 0) - return new AddressParseResult(cleanQuery.replace(bigrams[i], '').trim(), val.t, val.i, val.k[0]) - } - - for (const keyword of val.k) { - const i = queryTokens.indexOf(keyword) - if (i >= 0) - return new AddressParseResult(cleanQuery.replace(queryTokens[i], '').trim(), val.t, val.i, val.k[0]) - } - } - - return new AddressParseResult('', [], '', '') - } -} diff --git a/src/index.tsx b/src/index.tsx index 2e45f12a..459adb70 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,7 +1,7 @@ import React from 'react' import { createRoot } from 'react-dom/client' -import { setTranslation } from '@/translation/Translation' +import { getTranslation, setTranslation } from '@/translation/Translation' import App from '@/App' import { getApiInfoStore, @@ -31,6 +31,8 @@ import MapFeatureStore from '@/stores/MapFeatureStore' import SettingsStore from '@/stores/SettingsStore' import { ErrorAction, InfoReceived } from '@/actions/Actions' import POIsStore from '@/stores/POIsStore' +import { setDistanceFormat } from '@/Converters' +import { AddressParseResult } from '@/pois/AddressParseResult' console.log(`Source code: https://github.com/graphhopper/graphhopper-maps/tree/${GIT_SHA}`) @@ -38,6 +40,9 @@ const url = new URL(window.location.href) const locale = url.searchParams.get('locale') setTranslation(locale || navigator.language) +setDistanceFormat(new Intl.NumberFormat(navigator.language, { maximumFractionDigits: 1 })) +AddressParseResult.setPOITriggerPhrases(getTranslation()) + // use graphhopper api key from url or try using one from the config const apiKey = url.searchParams.has('key') ? url.searchParams.get('key') : config.keys.graphhopper setApi(config.routingApi, config.geocodingApi, apiKey || '') diff --git a/src/pois/AddressParseResult.ts b/src/pois/AddressParseResult.ts new file mode 100644 index 00000000..6e0e8ab7 --- /dev/null +++ b/src/pois/AddressParseResult.ts @@ -0,0 +1,125 @@ +import { ApiImpl } from '@/api/Api' +import Dispatcher from '@/stores/Dispatcher' +import { SetBBox, SetPOIs } from '@/actions/Actions' +import { hitToItem } from '@/Converters' +import { GeocodingHit } from '@/api/graphhopper' +import { QueryPoint } from '@/stores/QueryStore' +import { tr, Translation } from '@/translation/Translation' + +export class AddressParseResult { + location: string + tags: string[] + icon: string + poi: string + static VALUES: PoiTriggerPhrases[] + + constructor(location: string, tags: string[], icon: string, poi: string) { + this.location = location + this.tags = tags + this.icon = icon + this.poi = poi + } + + hasPOIs(): boolean { + return this.tags.length > 0 + } + + text(prefix: string) { + return prefix + ' ' + (this.location ? tr('in') + ' ' + this.location : tr('nearby')) + } + + /* it is a bit ugly that we have to inject the translated values here, but jest goes crazy otherwise */ + static parse(query: string, incomplete: boolean): AddressParseResult { + query = query.toLowerCase() + + const smallWords = ['in', 'around', 'nearby'] + const queryTokens: string[] = query.split(' ').filter(token => !smallWords.includes(token)) + const cleanQuery = queryTokens.join(' ') + const bigrams: string[] = [] + for (let i = 0; i < queryTokens.length - 1; i++) { + bigrams.push(queryTokens[i] + ' ' + queryTokens[i + 1]) + } + + for (const val of AddressParseResult.VALUES) { + // two word phrases like 'public transit' must be checked before single word phrases + for (const keyword of val.k) { + const i = bigrams.indexOf(keyword) + if (i >= 0) + return new AddressParseResult(cleanQuery.replace(bigrams[i], '').trim(), val.t, val.i, val.k[0]) + } + + for (const keyword of val.k) { + const i = queryTokens.indexOf(keyword) + if (i >= 0) + return new AddressParseResult(cleanQuery.replace(queryTokens[i], '').trim(), val.t, val.i, val.k[0]) + } + } + + return new AddressParseResult('', [], '', '') + } + + public static handleGeocodingResponse( + hits: GeocodingHit[], + parseResult: AddressParseResult, + queryPoint: QueryPoint + ) { + if (hits.length == 0) return + const pois = AddressParseResult.map(hits, parseResult) + const bbox = ApiImpl.getBBoxPoints(pois.map(p => p.coordinate)) + if (bbox) { + if (parseResult.location) Dispatcher.dispatch(new SetBBox(bbox)) + Dispatcher.dispatch(new SetPOIs(pois, queryPoint)) + } else { + console.warn( + 'invalid bbox for points ' + JSON.stringify(pois) + ' result was: ' + JSON.stringify(parseResult) + ) + } + } + + static map(hits: GeocodingHit[], parseResult: AddressParseResult) { + return hits.map(hit => { + const res = hitToItem(hit) + return { + name: res.mainText, + icon: parseResult.icon, + coordinate: hit.point, + address: res.secondText, + } + }) + } + + static s(s: string) { + return + } + + // because of the static method we need to inject the Translation object as otherwise jest has a problem + static setPOITriggerPhrases(translation: Translation) { + const t = (s: string) => + translation + .get(s) + .split(',') + .map(s => s.trim().toLowerCase()) + AddressParseResult.VALUES = [ + { k: t('poi_airports'), t: ['aeroway:aerodrome'], i: 'flight_takeoff' }, + { k: t('poi_banks'), t: ['amenity:bank'], i: 'universal_currency_alt' }, + { k: t('poi_bus_stops'), t: ['highway:bus_stop'], i: 'train' }, + { k: t('poi_education'), t: ['amenity:school', 'building:school', 'building:university'], i: 'school' }, + { k: t('poi_hospitals'), t: ['amenity:hospital', 'building:hospital'], i: 'local_hospital' }, + { k: t('poi_hotels'), t: ['amenity:hotel', 'building:hotel', 'tourism:hotel'], i: 'hotel' }, + { k: t('poi_leisure'), t: ['leisure'], i: 'sports_handball' }, + { k: t('poi_museums'), t: ['tourism:museum', 'building:museum'], i: 'museum' }, + { k: t('poi_parking'), t: ['amenity:parking'], i: 'local_parking' }, + { k: t('poi_parks'), t: ['leisure:park'], i: 'sports_handball' }, + { k: t('poi_pharmacies'), t: ['amenity:pharmacy'], i: 'local_pharmacy' }, + { k: t('poi_playgrounds'), t: ['leisure:playground'], i: 'sports_handball' }, + { k: t('poi_public_transit'), t: ['railway:station', 'highway:bus_stop'], i: 'train' }, + { k: t('poi_railway_station'), t: ['railway:station'], i: 'train' }, + { k: t('poi_restaurants'), t: ['amenity:restaurant'], i: 'restaurant' }, + { k: t('poi_schools'), t: ['amenity:school', 'building:school'], i: 'school' }, + { k: t('poi_super_markets'), t: ['shop:supermarket', 'building:supermarket'], i: 'store' }, + { k: t('poi_tourism'), t: ['tourism'], i: 'luggage' }, + ] + } +} + +export type PoiTriggerPhrases = { k: string[]; t: string[]; i: string } diff --git a/src/sidebar/search/AddressInput.tsx b/src/sidebar/search/AddressInput.tsx index cc056bb6..3f5e3f33 100644 --- a/src/sidebar/search/AddressInput.tsx +++ b/src/sidebar/search/AddressInput.tsx @@ -12,18 +12,17 @@ import Autocomplete, { import ArrowBack from './arrow_back.svg' import Cross from '@/sidebar/times-solid-thin.svg' import styles from './AddressInput.module.css' -import Api, { AddressParseResult, ApiImpl, getApi } from '@/api/Api' +import Api, { getApi } from '@/api/Api' import { tr } from '@/translation/Translation' import { coordinateToText, hitToItem, nominatimHitToItem, textToCoordinate } from '@/Converters' import { useMediaQuery } from 'react-responsive' import PopUp from '@/sidebar/search/PopUp' import PlainButton from '@/PlainButton' import { onCurrentLocationSelected } from '@/map/MapComponent' -import Dispatcher from '@/stores/Dispatcher' -import { SetBBox, SetPOIs } from '@/actions/Actions' import { toLonLat, transformExtent } from 'ol/proj' import { calcDist } from '@/distUtils' import { Map } from 'ol' +import { AddressParseResult } from '@/pois/AddressParseResult' export interface AddressInputProps { point: QueryPoint @@ -81,7 +80,7 @@ export default function AddressInput(props: AddressInputProps) { }) ) - const [poiSearch] = useState(new ReverseGeocoder(getApi(), props.point, ReverseGeocoder.handleGeocodingResponse)) + const [poiSearch] = useState(new ReverseGeocoder(getApi(), props.point, AddressParseResult.handleGeocodingResponse)) // if item is selected we need to clear the autocompletion list useEffect(() => setAutocompleteItems([]), [props.point]) @@ -414,36 +413,6 @@ export class ReverseGeocoder { this.requestId++ return this.requestId } - - public static handleGeocodingResponse( - hits: GeocodingHit[], - parseResult: AddressParseResult, - queryPoint: QueryPoint - ) { - if (hits.length == 0) return - const pois = ReverseGeocoder.map(hits, parseResult) - const bbox = ApiImpl.getBBoxPoints(pois.map(p => p.coordinate)) - if (bbox) { - if (parseResult.location) Dispatcher.dispatch(new SetBBox(bbox)) - Dispatcher.dispatch(new SetPOIs(pois, queryPoint)) - } else { - console.warn( - 'invalid bbox for points ' + JSON.stringify(pois) + ' result was: ' + JSON.stringify(parseResult) - ) - } - } - - private static map(hits: GeocodingHit[], parseResult: AddressParseResult) { - return hits.map(hit => { - const res = hitToItem(hit) - return { - name: res.mainText, - icon: parseResult.icon, - coordinate: hit.point, - address: res.secondText, - } - }) - } } class Timout { diff --git a/src/sidebar/search/AddressInputAutocomplete.tsx b/src/sidebar/search/AddressInputAutocomplete.tsx index b8d52d01..791009a6 100644 --- a/src/sidebar/search/AddressInputAutocomplete.tsx +++ b/src/sidebar/search/AddressInputAutocomplete.tsx @@ -2,8 +2,7 @@ import styles from './AddressInputAutocomplete.module.css' import CurrentLocationIcon from './current-location.svg' import { tr } from '@/translation/Translation' import { Bbox } from '@/api/graphhopper' -import { useState } from 'react' -import { AddressParseResult } from '@/api/Api' +import { AddressParseResult } from '@/pois/AddressParseResult' export interface AutocompleteItem {} diff --git a/src/translation/tr.json b/src/translation/tr.json index ce48c03a..2fccf16e 100644 --- a/src/translation/tr.json +++ b/src/translation/tr.json @@ -109,7 +109,25 @@ "track_type":"Track type", "toll":"Toll", "next":"Next", -"back":"Back" +"back":"Back", +"poi_airports":"airports, airport", +"poi_banks":"banks, bank", +"poi_bus_stops":"bus stops, bus stop, bus", +"poi_education":"education", +"poi_hospitals":"hospitals, hospital", +"poi_hotels":"hotels, hotel", +"poi_leisure":"leisure", +"poi_museums":"museums, museum", +"poi_parking":"parking, parking place", +"poi_parks":"parks, park", +"poi_pharmacies":"pharmacies, pharmacy", +"poi_playgrounds":"playgrounds, playground", +"poi_public_transit":"public transit", +"poi_railway_station":"railway stations, railway station, train", +"poi_restaurants":"restaurants, restaurant, eat", +"poi_schools":"schools, school", +"poi_super_markets":"super markets, super market", +"poi_tourism":"tourism" }, "ar":{ "total_ascend":"%1$s اجمالى صعود", @@ -222,7 +240,25 @@ "track_type":"", "toll":"", "next":"", -"back":"" +"back":"", +"poi_airports":"", +"poi_banks":"", +"poi_bus_stops":"", +"poi_education":"", +"poi_hospitals":"", +"poi_hotels":"", +"poi_leisure":"", +"poi_museums":"", +"poi_parking":"", +"poi_parks":"", +"poi_pharmacies":"", +"poi_playgrounds":"", +"poi_public_transit":"", +"poi_railway_station":"", +"poi_restaurants":"", +"poi_schools":"", +"poi_super_markets":"", +"poi_tourism":"" }, "ast":{ "total_ascend":"%1$s d'ascensu total", @@ -335,7 +371,25 @@ "track_type":"", "toll":"", "next":"", -"back":"" +"back":"", +"poi_airports":"", +"poi_banks":"", +"poi_bus_stops":"", +"poi_education":"", +"poi_hospitals":"", +"poi_hotels":"", +"poi_leisure":"", +"poi_museums":"", +"poi_parking":"", +"poi_parks":"", +"poi_pharmacies":"", +"poi_playgrounds":"", +"poi_public_transit":"", +"poi_railway_station":"", +"poi_restaurants":"", +"poi_schools":"", +"poi_super_markets":"", +"poi_tourism":"" }, "az":{ "total_ascend":"%1$s yüksəliş", @@ -448,7 +502,25 @@ "track_type":"", "toll":"", "next":"", -"back":"" +"back":"", +"poi_airports":"", +"poi_banks":"", +"poi_bus_stops":"", +"poi_education":"", +"poi_hospitals":"", +"poi_hotels":"", +"poi_leisure":"", +"poi_museums":"", +"poi_parking":"", +"poi_parks":"", +"poi_pharmacies":"", +"poi_playgrounds":"", +"poi_public_transit":"", +"poi_railway_station":"", +"poi_restaurants":"", +"poi_schools":"", +"poi_super_markets":"", +"poi_tourism":"" }, "bg":{ "total_ascend":"%1$s общо изкачване", @@ -561,7 +633,25 @@ "track_type":"", "toll":"", "next":"", -"back":"" +"back":"", +"poi_airports":"", +"poi_banks":"", +"poi_bus_stops":"", +"poi_education":"", +"poi_hospitals":"", +"poi_hotels":"", +"poi_leisure":"", +"poi_museums":"", +"poi_parking":"", +"poi_parks":"", +"poi_pharmacies":"", +"poi_playgrounds":"", +"poi_public_transit":"", +"poi_railway_station":"", +"poi_restaurants":"", +"poi_schools":"", +"poi_super_markets":"", +"poi_tourism":"" }, "bn_BN":{ "total_ascend":"", @@ -674,7 +764,25 @@ "track_type":"", "toll":"", "next":"", -"back":"" +"back":"", +"poi_airports":"", +"poi_banks":"", +"poi_bus_stops":"", +"poi_education":"", +"poi_hospitals":"", +"poi_hotels":"", +"poi_leisure":"", +"poi_museums":"", +"poi_parking":"", +"poi_parks":"", +"poi_pharmacies":"", +"poi_playgrounds":"", +"poi_public_transit":"", +"poi_railway_station":"", +"poi_restaurants":"", +"poi_schools":"", +"poi_super_markets":"", +"poi_tourism":"" }, "ca":{ "total_ascend":"%1$s de pujada total", @@ -787,7 +895,25 @@ "track_type":"", "toll":"", "next":"", -"back":"" +"back":"", +"poi_airports":"", +"poi_banks":"", +"poi_bus_stops":"", +"poi_education":"", +"poi_hospitals":"", +"poi_hotels":"", +"poi_leisure":"", +"poi_museums":"", +"poi_parking":"", +"poi_parks":"", +"poi_pharmacies":"", +"poi_playgrounds":"", +"poi_public_transit":"", +"poi_railway_station":"", +"poi_restaurants":"", +"poi_schools":"", +"poi_super_markets":"", +"poi_tourism":"" }, "cs_CZ":{ "total_ascend":"celkové stoupání %1$s", @@ -900,7 +1026,25 @@ "track_type":"", "toll":"", "next":"", -"back":"" +"back":"", +"poi_airports":"", +"poi_banks":"", +"poi_bus_stops":"", +"poi_education":"", +"poi_hospitals":"", +"poi_hotels":"", +"poi_leisure":"", +"poi_museums":"", +"poi_parking":"", +"poi_parks":"", +"poi_pharmacies":"", +"poi_playgrounds":"", +"poi_public_transit":"", +"poi_railway_station":"", +"poi_restaurants":"", +"poi_schools":"", +"poi_super_markets":"", +"poi_tourism":"" }, "da_DK":{ "total_ascend":"%1$s samlet stigning", @@ -1013,7 +1157,25 @@ "track_type":"", "toll":"", "next":"", -"back":"" +"back":"", +"poi_airports":"", +"poi_banks":"", +"poi_bus_stops":"", +"poi_education":"", +"poi_hospitals":"", +"poi_hotels":"", +"poi_leisure":"", +"poi_museums":"", +"poi_parking":"", +"poi_parks":"", +"poi_pharmacies":"", +"poi_playgrounds":"", +"poi_public_transit":"", +"poi_railway_station":"", +"poi_restaurants":"", +"poi_schools":"", +"poi_super_markets":"", +"poi_tourism":"" }, "de_DE":{ "total_ascend":"%1$s Gesamtaufstieg", @@ -1126,7 +1288,25 @@ "track_type":"Straßenbefestigung", "toll":"Maut", "next":"Weiter", -"back":"Zurück" +"back":"Zurück", +"poi_airports":"Flughäfen, Flughafen", +"poi_banks":"Bank", +"poi_bus_stops":"Haltestellen, Haltestelle, Bushaltestellen, Bushaltestelle", +"poi_education":"Bildung", +"poi_hospitals":"Krankenhäuser, Krankenhaus", +"poi_hotels":"Hotels, Hotel", +"poi_leisure":"Freizeit", +"poi_museums":"Museen, Museum", +"poi_parking":"Parkplätze, Parkplatz", +"poi_parks":"Parks, Park", +"poi_pharmacies":"Apotheken, Apotheke", +"poi_playgrounds":"Spielplätze, Spielplatz", +"poi_public_transit":"ÖPNV, Nahverkehr", +"poi_railway_station":"Bahnhöfe, Bahnhof, Zug", +"poi_restaurants":"Restaurants, Restaurant", +"poi_schools":"Schulen, Schule", +"poi_super_markets":"Supermärkte, Supermarkt, Einkauf, Einkaufsladen", +"poi_tourism":"Tourismus, Fremdenverkehr, Touristik" }, "el":{ "total_ascend":"%1$s συνολική ανάβαση", @@ -1239,7 +1419,25 @@ "track_type":"Τύπος δρόμου", "toll":"Διόδια", "next":"Επόμενο", -"back":"Πίσω" +"back":"Πίσω", +"poi_airports":"", +"poi_banks":"", +"poi_bus_stops":"", +"poi_education":"", +"poi_hospitals":"", +"poi_hotels":"", +"poi_leisure":"", +"poi_museums":"", +"poi_parking":"", +"poi_parks":"", +"poi_pharmacies":"", +"poi_playgrounds":"", +"poi_public_transit":"", +"poi_railway_station":"", +"poi_restaurants":"", +"poi_schools":"", +"poi_super_markets":"", +"poi_tourism":"" }, "eo":{ "total_ascend":"%1$s supreniro tute", @@ -1352,7 +1550,25 @@ "track_type":"", "toll":"", "next":"", -"back":"" +"back":"", +"poi_airports":"", +"poi_banks":"", +"poi_bus_stops":"", +"poi_education":"", +"poi_hospitals":"", +"poi_hotels":"", +"poi_leisure":"", +"poi_museums":"", +"poi_parking":"", +"poi_parks":"", +"poi_pharmacies":"", +"poi_playgrounds":"", +"poi_public_transit":"", +"poi_railway_station":"", +"poi_restaurants":"", +"poi_schools":"", +"poi_super_markets":"", +"poi_tourism":"" }, "es":{ "total_ascend":"Ascender %1$s en total", @@ -1465,7 +1681,25 @@ "track_type":"", "toll":"", "next":"", -"back":"" +"back":"", +"poi_airports":"", +"poi_banks":"", +"poi_bus_stops":"", +"poi_education":"", +"poi_hospitals":"", +"poi_hotels":"", +"poi_leisure":"", +"poi_museums":"", +"poi_parking":"", +"poi_parks":"", +"poi_pharmacies":"", +"poi_playgrounds":"", +"poi_public_transit":"", +"poi_railway_station":"", +"poi_restaurants":"", +"poi_schools":"", +"poi_super_markets":"", +"poi_tourism":"" }, "fa":{ "total_ascend":"مجموع صعود %1$s", @@ -1578,7 +1812,25 @@ "track_type":"", "toll":"", "next":"", -"back":"" +"back":"", +"poi_airports":"", +"poi_banks":"", +"poi_bus_stops":"", +"poi_education":"", +"poi_hospitals":"", +"poi_hotels":"", +"poi_leisure":"", +"poi_museums":"", +"poi_parking":"", +"poi_parks":"", +"poi_pharmacies":"", +"poi_playgrounds":"", +"poi_public_transit":"", +"poi_railway_station":"", +"poi_restaurants":"", +"poi_schools":"", +"poi_super_markets":"", +"poi_tourism":"" }, "fil":{ "total_ascend":"", @@ -1691,7 +1943,25 @@ "track_type":"", "toll":"", "next":"", -"back":"" +"back":"", +"poi_airports":"", +"poi_banks":"", +"poi_bus_stops":"", +"poi_education":"", +"poi_hospitals":"", +"poi_hotels":"", +"poi_leisure":"", +"poi_museums":"", +"poi_parking":"", +"poi_parks":"", +"poi_pharmacies":"", +"poi_playgrounds":"", +"poi_public_transit":"", +"poi_railway_station":"", +"poi_restaurants":"", +"poi_schools":"", +"poi_super_markets":"", +"poi_tourism":"" }, "fi":{ "total_ascend":"nousu yhteensä %1$s", @@ -1804,7 +2074,25 @@ "track_type":"", "toll":"", "next":"", -"back":"" +"back":"", +"poi_airports":"", +"poi_banks":"", +"poi_bus_stops":"", +"poi_education":"", +"poi_hospitals":"", +"poi_hotels":"", +"poi_leisure":"", +"poi_museums":"", +"poi_parking":"", +"poi_parks":"", +"poi_pharmacies":"", +"poi_playgrounds":"", +"poi_public_transit":"", +"poi_railway_station":"", +"poi_restaurants":"", +"poi_schools":"", +"poi_super_markets":"", +"poi_tourism":"" }, "fr_FR":{ "total_ascend":"%1$s de dénivelé positif", @@ -1917,7 +2205,25 @@ "track_type":"", "toll":"", "next":"", -"back":"" +"back":"", +"poi_airports":"", +"poi_banks":"", +"poi_bus_stops":"", +"poi_education":"", +"poi_hospitals":"", +"poi_hotels":"", +"poi_leisure":"", +"poi_museums":"", +"poi_parking":"", +"poi_parks":"", +"poi_pharmacies":"", +"poi_playgrounds":"", +"poi_public_transit":"", +"poi_railway_station":"", +"poi_restaurants":"", +"poi_schools":"", +"poi_super_markets":"", +"poi_tourism":"" }, "fr_CH":{ "total_ascend":"%1$s de dénivelé positif", @@ -2030,7 +2336,25 @@ "track_type":"", "toll":"", "next":"", -"back":"" +"back":"", +"poi_airports":"", +"poi_banks":"", +"poi_bus_stops":"", +"poi_education":"", +"poi_hospitals":"", +"poi_hotels":"", +"poi_leisure":"", +"poi_museums":"", +"poi_parking":"", +"poi_parks":"", +"poi_pharmacies":"", +"poi_playgrounds":"", +"poi_public_transit":"", +"poi_railway_station":"", +"poi_restaurants":"", +"poi_schools":"", +"poi_super_markets":"", +"poi_tourism":"" }, "gl":{ "total_ascend":"", @@ -2143,7 +2467,25 @@ "track_type":"", "toll":"", "next":"", -"back":"" +"back":"", +"poi_airports":"", +"poi_banks":"", +"poi_bus_stops":"", +"poi_education":"", +"poi_hospitals":"", +"poi_hotels":"", +"poi_leisure":"", +"poi_museums":"", +"poi_parking":"", +"poi_parks":"", +"poi_pharmacies":"", +"poi_playgrounds":"", +"poi_public_transit":"", +"poi_railway_station":"", +"poi_restaurants":"", +"poi_schools":"", +"poi_super_markets":"", +"poi_tourism":"" }, "he":{ "total_ascend":"עלייה כוללת של %1$s", @@ -2256,7 +2598,25 @@ "track_type":"", "toll":"", "next":"", -"back":"" +"back":"", +"poi_airports":"", +"poi_banks":"", +"poi_bus_stops":"", +"poi_education":"", +"poi_hospitals":"", +"poi_hotels":"", +"poi_leisure":"", +"poi_museums":"", +"poi_parking":"", +"poi_parks":"", +"poi_pharmacies":"", +"poi_playgrounds":"", +"poi_public_transit":"", +"poi_railway_station":"", +"poi_restaurants":"", +"poi_schools":"", +"poi_super_markets":"", +"poi_tourism":"" }, "hr_HR":{ "total_ascend":"%1$s ukupni uspon", @@ -2369,7 +2729,25 @@ "track_type":"", "toll":"", "next":"", -"back":"" +"back":"", +"poi_airports":"", +"poi_banks":"", +"poi_bus_stops":"", +"poi_education":"", +"poi_hospitals":"", +"poi_hotels":"", +"poi_leisure":"", +"poi_museums":"", +"poi_parking":"", +"poi_parks":"", +"poi_pharmacies":"", +"poi_playgrounds":"", +"poi_public_transit":"", +"poi_railway_station":"", +"poi_restaurants":"", +"poi_schools":"", +"poi_super_markets":"", +"poi_tourism":"" }, "hsb":{ "total_ascend":"", @@ -2482,7 +2860,25 @@ "track_type":"", "toll":"", "next":"", -"back":"" +"back":"", +"poi_airports":"", +"poi_banks":"", +"poi_bus_stops":"", +"poi_education":"", +"poi_hospitals":"", +"poi_hotels":"", +"poi_leisure":"", +"poi_museums":"", +"poi_parking":"", +"poi_parks":"", +"poi_pharmacies":"", +"poi_playgrounds":"", +"poi_public_transit":"", +"poi_railway_station":"", +"poi_restaurants":"", +"poi_schools":"", +"poi_super_markets":"", +"poi_tourism":"" }, "hu_HU":{ "total_ascend":"Összes szintemelkedés: %1$s", @@ -2595,26 +2991,44 @@ "track_type":"", "toll":"", "next":"", -"back":"" +"back":"", +"poi_airports":"", +"poi_banks":"", +"poi_bus_stops":"", +"poi_education":"", +"poi_hospitals":"", +"poi_hotels":"", +"poi_leisure":"", +"poi_museums":"", +"poi_parking":"", +"poi_parks":"", +"poi_pharmacies":"", +"poi_playgrounds":"", +"poi_public_transit":"", +"poi_railway_station":"", +"poi_restaurants":"", +"poi_schools":"", +"poi_super_markets":"", +"poi_tourism":"" }, "in_ID":{ "total_ascend":"naik dengan jarak %1$s", "total_descend":"turun dengan jarak %1$s", "way_contains_ford":"terdapat jalan untuk dilewati", -"way_contains_ferry":"", -"way_contains_private":"", -"way_contains_toll":"", -"way_crosses_border":"", -"way_contains":"", -"way_contains_restrictions":"", -"tracks":"", -"steps":"", -"footways":"", -"steep_sections":"", -"private_sections":"", -"challenging_sections":"", -"trunk_roads_warn":"", -"get_off_bike_for":"", +"way_contains_ferry":"Rute termasuk kapal feri", +"way_contains_private":"Rute dengan jalan pribadi", +"way_contains_toll":"Rute dengan jalan tol", +"way_crosses_border":"Rute melintasi perbatasan negara", +"way_contains":"Rute termasuk %1$s", +"way_contains_restrictions":"Rute dengan potensi pembatasan akses", +"tracks":"Jalan Tanah", +"steps":"Langkah", +"footways":"Jalan setapak", +"steep_sections":"bagian curam", +"private_sections":"bagian pribadi", +"challenging_sections":"Bagian yang menantang atau berbahaya", +"trunk_roads_warn":"Rute termasuk jalan utama yang berpotensi berbahaya atau lebih buruk", +"get_off_bike_for":"Turun dan dorong sepeda untuk %1$s", "start_label":"Mulai", "intermediate_label":"Antara", "end_label":"Akhir", @@ -2623,27 +3037,27 @@ "set_end":"Atur sebagai titik akhir", "center_map":"Tengahkan Peta", "show_coords":"Tampilkan koordinat", -"query_osm":"", +"query_osm":"Query OSM", "route":"Rute", -"add_to_route":"", +"add_to_route":"Tambah Lokasi", "delete_from_route":"Hapus dari rute", -"open_custom_model_box":"", -"draw_areas_enabled":"", -"help_custom_model":"", -"apply_custom_model":"", -"custom_model_enabled":"", -"settings":"", -"settings_close":"", -"exclude_motorway_example":"", -"exclude_disneyland_paris_example":"", -"simple_electric_car_example":"", -"avoid_tunnels_bridges_example":"", -"limit_speed_example":"", -"cargo_bike_example":"", -"prefer_bike_network":"", -"exclude_area_example":"", -"combined_example":"", -"examples_custom_model":"", +"open_custom_model_box":"Buka kotak model kustom", +"draw_areas_enabled":"Gambar dan ubah area di peta", +"help_custom_model":"Bantuan", +"apply_custom_model":"Terapkan", +"custom_model_enabled":"Model Kustom Aktif", +"settings":"Pengaturan", +"settings_close":"Tutup", +"exclude_motorway_example":"Tidak termasuk jalur motor", +"exclude_disneyland_paris_example":"Tidak termasuk Disneyland Paris", +"simple_electric_car_example":"Mobil Listrik Sederhana", +"avoid_tunnels_bridges_example":"Hindari Jembatan & Terowongan", +"limit_speed_example":"Batas Kecepatan", +"cargo_bike_example":"Sepeda Kargo", +"prefer_bike_network":"Lebih suka Rute Sepeda", +"exclude_area_example":"Kecualikan Area", +"combined_example":"Contoh Gabungan", +"examples_custom_model":"Contoh", "marker":"Titik", "gh_offline_info":"Pelayanan API Graphhopper dalam kondisi offline", "refresh_button":"Perbarui Halaman", @@ -2651,21 +3065,21 @@ "zoom_in":"Perbesaran", "zoom_out":"Pengecilan", "drag_to_reorder":"Drag untuk mengatur urutan", -"route_timed_out":"", -"route_request_failed":"", -"current_location":"", -"searching_location":"", -"searching_location_failed":"", +"route_timed_out":"Waktu Penghitungan Rute Habis", +"route_request_failed":"Permintaan Rute Gagal", +"current_location":"Lokasi Saat Ini", +"searching_location":"Mencari Lokasi", +"searching_location_failed":"Pencarian lokasi gagal", "via_hint":"melalui", "from_hint":"dari", "gpx_export_button":"Ekspor GPX", -"gpx_button":"", -"settings_gpx_export":"", -"settings_gpx_export_trk":"", -"settings_gpx_export_rte":"", -"settings_gpx_export_wpt":"", -"hide_button":"", -"details_button":"", +"gpx_button":"GPX", +"settings_gpx_export":"Ekspor GPX", +"settings_gpx_export_trk":"Dengan trek", +"settings_gpx_export_rte":"Dengan rute", +"settings_gpx_export_wpt":"Dengan titik jalan", +"hide_button":"Sembunyikan", +"details_button":"Detil", "to_hint":"ke", "route_info":"%1$s berada dalam waktu %2$s", "search_button":"pencarian", @@ -2673,13 +3087,13 @@ "pt_route_info":"sampai pada %1$s dengan %2$s jarak (%3$s)", "pt_route_info_walking":"sampai pada %1$s dengan berjalan kaki (%2$s)", "locations_not_found":"Penentuan rute tidak dapat dilakukan. Lokasi tidak ditemukan", -"search_with_nominatim":"", -"powered_by":"", -"info":"", -"feedback":"", -"imprint":"", -"privacy":"", -"terms":"", +"search_with_nominatim":"Pencarian dengan Nominatim", +"powered_by":"Didukung oleh", +"info":"Info", +"feedback":"Umpan Balik", +"imprint":"Jejak", +"privacy":"Privasi", +"terms":"Syarat", "bike":"Sepeda", "racingbike":"Sepeda Balap", "mtb":"Sepeda Gunung", @@ -2691,24 +3105,42 @@ "truck":"Truk", "staticlink":"Jalur tetap", "motorcycle":"Motor", -"scooter":"", -"back_to_map":"", -"distance_unit":"", -"waiting_for_gps":"", -"elevation":"", -"slope":"", +"scooter":"Vespa", +"back_to_map":"Kembali", +"distance_unit":"Jarak dalam %1$s", +"waiting_for_gps":"Menunggu sinyal GPS", +"elevation":"Ketinggian", +"slope":"Kemiringan", "towerslope":"", -"country":"", -"surface":"", -"road_environment":"", -"road_access":"", -"road_class":"", -"max_speed":"", -"average_speed":"", -"track_type":"", -"toll":"", -"next":"", -"back":"" +"country":"Negara", +"surface":"Permukaan", +"road_environment":"Lingkungan Jalan", +"road_access":"Akses Jalan", +"road_class":"Kelas Jalan", +"max_speed":"Kecepatan Maks", +"average_speed":"Kecepatan Rata-Rata", +"track_type":"Tipe trek", +"toll":"Tol", +"next":"Selanjutnya", +"back":"Sebelumnya", +"poi_airports":"", +"poi_banks":"", +"poi_bus_stops":"", +"poi_education":"", +"poi_hospitals":"", +"poi_hotels":"", +"poi_leisure":"", +"poi_museums":"", +"poi_parking":"", +"poi_parks":"", +"poi_pharmacies":"", +"poi_playgrounds":"", +"poi_public_transit":"", +"poi_railway_station":"", +"poi_restaurants":"", +"poi_schools":"", +"poi_super_markets":"", +"poi_tourism":"" }, "it":{ "total_ascend":"%1$s di dislivello positivo", @@ -2821,7 +3253,25 @@ "track_type":"", "toll":"", "next":"", -"back":"" +"back":"", +"poi_airports":"", +"poi_banks":"", +"poi_bus_stops":"", +"poi_education":"", +"poi_hospitals":"", +"poi_hotels":"", +"poi_leisure":"", +"poi_museums":"", +"poi_parking":"", +"poi_parks":"", +"poi_pharmacies":"", +"poi_playgrounds":"", +"poi_public_transit":"", +"poi_railway_station":"", +"poi_restaurants":"", +"poi_schools":"", +"poi_super_markets":"", +"poi_tourism":"" }, "ja":{ "total_ascend":"", @@ -2934,7 +3384,25 @@ "track_type":"", "toll":"", "next":"", -"back":"" +"back":"", +"poi_airports":"", +"poi_banks":"", +"poi_bus_stops":"", +"poi_education":"", +"poi_hospitals":"", +"poi_hotels":"", +"poi_leisure":"", +"poi_museums":"", +"poi_parking":"", +"poi_parks":"", +"poi_pharmacies":"", +"poi_playgrounds":"", +"poi_public_transit":"", +"poi_railway_station":"", +"poi_restaurants":"", +"poi_schools":"", +"poi_super_markets":"", +"poi_tourism":"" }, "ko":{ "total_ascend":"오르막길 총 %1$s", @@ -3047,7 +3515,25 @@ "track_type":"", "toll":"", "next":"", -"back":"" +"back":"", +"poi_airports":"", +"poi_banks":"", +"poi_bus_stops":"", +"poi_education":"", +"poi_hospitals":"", +"poi_hotels":"", +"poi_leisure":"", +"poi_museums":"", +"poi_parking":"", +"poi_parks":"", +"poi_pharmacies":"", +"poi_playgrounds":"", +"poi_public_transit":"", +"poi_railway_station":"", +"poi_restaurants":"", +"poi_schools":"", +"poi_super_markets":"", +"poi_tourism":"" }, "kz":{ "total_ascend":"%1$s көтерілу", @@ -3160,7 +3646,25 @@ "track_type":"", "toll":"", "next":"", -"back":"" +"back":"", +"poi_airports":"", +"poi_banks":"", +"poi_bus_stops":"", +"poi_education":"", +"poi_hospitals":"", +"poi_hotels":"", +"poi_leisure":"", +"poi_museums":"", +"poi_parking":"", +"poi_parks":"", +"poi_pharmacies":"", +"poi_playgrounds":"", +"poi_public_transit":"", +"poi_railway_station":"", +"poi_restaurants":"", +"poi_schools":"", +"poi_super_markets":"", +"poi_tourism":"" }, "lt_LT":{ "total_ascend":"", @@ -3273,7 +3777,25 @@ "track_type":"", "toll":"", "next":"", -"back":"" +"back":"", +"poi_airports":"", +"poi_banks":"", +"poi_bus_stops":"", +"poi_education":"", +"poi_hospitals":"", +"poi_hotels":"", +"poi_leisure":"", +"poi_museums":"", +"poi_parking":"", +"poi_parks":"", +"poi_pharmacies":"", +"poi_playgrounds":"", +"poi_public_transit":"", +"poi_railway_station":"", +"poi_restaurants":"", +"poi_schools":"", +"poi_super_markets":"", +"poi_tourism":"" }, "nb_NO":{ "total_ascend":"%1$s totale høydemeter", @@ -3386,7 +3908,25 @@ "track_type":"", "toll":"", "next":"", -"back":"" +"back":"", +"poi_airports":"", +"poi_banks":"", +"poi_bus_stops":"", +"poi_education":"", +"poi_hospitals":"", +"poi_hotels":"", +"poi_leisure":"", +"poi_museums":"", +"poi_parking":"", +"poi_parks":"", +"poi_pharmacies":"", +"poi_playgrounds":"", +"poi_public_transit":"", +"poi_railway_station":"", +"poi_restaurants":"", +"poi_schools":"", +"poi_super_markets":"", +"poi_tourism":"" }, "ne":{ "total_ascend":"", @@ -3499,7 +4039,25 @@ "track_type":"", "toll":"", "next":"", -"back":"" +"back":"", +"poi_airports":"", +"poi_banks":"", +"poi_bus_stops":"", +"poi_education":"", +"poi_hospitals":"", +"poi_hotels":"", +"poi_leisure":"", +"poi_museums":"", +"poi_parking":"", +"poi_parks":"", +"poi_pharmacies":"", +"poi_playgrounds":"", +"poi_public_transit":"", +"poi_railway_station":"", +"poi_restaurants":"", +"poi_schools":"", +"poi_super_markets":"", +"poi_tourism":"" }, "nl":{ "total_ascend":"%1$s totale klim", @@ -3612,7 +4170,25 @@ "track_type":"", "toll":"", "next":"", -"back":"" +"back":"", +"poi_airports":"", +"poi_banks":"", +"poi_bus_stops":"", +"poi_education":"", +"poi_hospitals":"", +"poi_hotels":"", +"poi_leisure":"", +"poi_museums":"", +"poi_parking":"", +"poi_parks":"", +"poi_pharmacies":"", +"poi_playgrounds":"", +"poi_public_transit":"", +"poi_railway_station":"", +"poi_restaurants":"", +"poi_schools":"", +"poi_super_markets":"", +"poi_tourism":"" }, "pl_PL":{ "total_ascend":"%1$s w górę", @@ -3725,7 +4301,25 @@ "track_type":"", "toll":"", "next":"", -"back":"" +"back":"", +"poi_airports":"", +"poi_banks":"", +"poi_bus_stops":"", +"poi_education":"", +"poi_hospitals":"", +"poi_hotels":"", +"poi_leisure":"", +"poi_museums":"", +"poi_parking":"", +"poi_parks":"", +"poi_pharmacies":"", +"poi_playgrounds":"", +"poi_public_transit":"", +"poi_railway_station":"", +"poi_restaurants":"", +"poi_schools":"", +"poi_super_markets":"", +"poi_tourism":"" }, "pt_BR":{ "total_ascend":"subida de %1$s", @@ -3838,7 +4432,25 @@ "track_type":"", "toll":"", "next":"", -"back":"" +"back":"", +"poi_airports":"", +"poi_banks":"", +"poi_bus_stops":"", +"poi_education":"", +"poi_hospitals":"", +"poi_hotels":"", +"poi_leisure":"", +"poi_museums":"", +"poi_parking":"", +"poi_parks":"", +"poi_pharmacies":"", +"poi_playgrounds":"", +"poi_public_transit":"", +"poi_railway_station":"", +"poi_restaurants":"", +"poi_schools":"", +"poi_super_markets":"", +"poi_tourism":"" }, "pt_PT":{ "total_ascend":"subida de %1$s", @@ -3951,7 +4563,25 @@ "track_type":"", "toll":"", "next":"", -"back":"" +"back":"", +"poi_airports":"", +"poi_banks":"", +"poi_bus_stops":"", +"poi_education":"", +"poi_hospitals":"", +"poi_hotels":"", +"poi_leisure":"", +"poi_museums":"", +"poi_parking":"", +"poi_parks":"", +"poi_pharmacies":"", +"poi_playgrounds":"", +"poi_public_transit":"", +"poi_railway_station":"", +"poi_restaurants":"", +"poi_schools":"", +"poi_super_markets":"", +"poi_tourism":"" }, "ro":{ "total_ascend":"urcare %1$s", @@ -4064,7 +4694,25 @@ "track_type":"", "toll":"", "next":"", -"back":"" +"back":"", +"poi_airports":"", +"poi_banks":"", +"poi_bus_stops":"", +"poi_education":"", +"poi_hospitals":"", +"poi_hotels":"", +"poi_leisure":"", +"poi_museums":"", +"poi_parking":"", +"poi_parks":"", +"poi_pharmacies":"", +"poi_playgrounds":"", +"poi_public_transit":"", +"poi_railway_station":"", +"poi_restaurants":"", +"poi_schools":"", +"poi_super_markets":"", +"poi_tourism":"" }, "ru":{ "total_ascend":"подъём на %1$s", @@ -4177,7 +4825,25 @@ "track_type":"", "toll":"", "next":"", -"back":"" +"back":"", +"poi_airports":"", +"poi_banks":"", +"poi_bus_stops":"", +"poi_education":"", +"poi_hospitals":"", +"poi_hotels":"", +"poi_leisure":"", +"poi_museums":"", +"poi_parking":"", +"poi_parks":"", +"poi_pharmacies":"", +"poi_playgrounds":"", +"poi_public_transit":"", +"poi_railway_station":"", +"poi_restaurants":"", +"poi_schools":"", +"poi_super_markets":"", +"poi_tourism":"" }, "sk":{ "total_ascend":"%1$s celkové stúpanie", @@ -4290,7 +4956,25 @@ "track_type":"", "toll":"", "next":"", -"back":"" +"back":"", +"poi_airports":"", +"poi_banks":"", +"poi_bus_stops":"", +"poi_education":"", +"poi_hospitals":"", +"poi_hotels":"", +"poi_leisure":"", +"poi_museums":"", +"poi_parking":"", +"poi_parks":"", +"poi_pharmacies":"", +"poi_playgrounds":"", +"poi_public_transit":"", +"poi_railway_station":"", +"poi_restaurants":"", +"poi_schools":"", +"poi_super_markets":"", +"poi_tourism":"" }, "sl_SI":{ "total_ascend":"Skupni vzpon: %1$s", @@ -4403,7 +5087,25 @@ "track_type":"", "toll":"", "next":"", -"back":"" +"back":"", +"poi_airports":"", +"poi_banks":"", +"poi_bus_stops":"", +"poi_education":"", +"poi_hospitals":"", +"poi_hotels":"", +"poi_leisure":"", +"poi_museums":"", +"poi_parking":"", +"poi_parks":"", +"poi_pharmacies":"", +"poi_playgrounds":"", +"poi_public_transit":"", +"poi_railway_station":"", +"poi_restaurants":"", +"poi_schools":"", +"poi_super_markets":"", +"poi_tourism":"" }, "sr_RS":{ "total_ascend":"%1$s ukupni uspon", @@ -4516,7 +5218,25 @@ "track_type":"", "toll":"", "next":"", -"back":"" +"back":"", +"poi_airports":"", +"poi_banks":"", +"poi_bus_stops":"", +"poi_education":"", +"poi_hospitals":"", +"poi_hotels":"", +"poi_leisure":"", +"poi_museums":"", +"poi_parking":"", +"poi_parks":"", +"poi_pharmacies":"", +"poi_playgrounds":"", +"poi_public_transit":"", +"poi_railway_station":"", +"poi_restaurants":"", +"poi_schools":"", +"poi_super_markets":"", +"poi_tourism":"" }, "sv_SE":{ "total_ascend":"%1$s stigning", @@ -4629,7 +5349,25 @@ "track_type":"", "toll":"", "next":"", -"back":"" +"back":"", +"poi_airports":"", +"poi_banks":"", +"poi_bus_stops":"", +"poi_education":"", +"poi_hospitals":"", +"poi_hotels":"", +"poi_leisure":"", +"poi_museums":"", +"poi_parking":"", +"poi_parks":"", +"poi_pharmacies":"", +"poi_playgrounds":"", +"poi_public_transit":"", +"poi_railway_station":"", +"poi_restaurants":"", +"poi_schools":"", +"poi_super_markets":"", +"poi_tourism":"" }, "tr":{ "total_ascend":"%1$s toplam tırmanış", @@ -4742,7 +5480,25 @@ "track_type":"", "toll":"", "next":"", -"back":"" +"back":"", +"poi_airports":"", +"poi_banks":"", +"poi_bus_stops":"", +"poi_education":"", +"poi_hospitals":"", +"poi_hotels":"", +"poi_leisure":"", +"poi_museums":"", +"poi_parking":"", +"poi_parks":"", +"poi_pharmacies":"", +"poi_playgrounds":"", +"poi_public_transit":"", +"poi_railway_station":"", +"poi_restaurants":"", +"poi_schools":"", +"poi_super_markets":"", +"poi_tourism":"" }, "uk":{ "total_ascend":"%1$s загалом підйому", @@ -4855,7 +5611,25 @@ "track_type":"", "toll":"", "next":"", -"back":"" +"back":"", +"poi_airports":"", +"poi_banks":"", +"poi_bus_stops":"", +"poi_education":"", +"poi_hospitals":"", +"poi_hotels":"", +"poi_leisure":"", +"poi_museums":"", +"poi_parking":"", +"poi_parks":"", +"poi_pharmacies":"", +"poi_playgrounds":"", +"poi_public_transit":"", +"poi_railway_station":"", +"poi_restaurants":"", +"poi_schools":"", +"poi_super_markets":"", +"poi_tourism":"" }, "uz":{ "total_ascend":"%1$s ga ko'tarilish", @@ -4968,7 +5742,25 @@ "track_type":"", "toll":"", "next":"", -"back":"" +"back":"", +"poi_airports":"", +"poi_banks":"", +"poi_bus_stops":"", +"poi_education":"", +"poi_hospitals":"", +"poi_hotels":"", +"poi_leisure":"", +"poi_museums":"", +"poi_parking":"", +"poi_parks":"", +"poi_pharmacies":"", +"poi_playgrounds":"", +"poi_public_transit":"", +"poi_railway_station":"", +"poi_restaurants":"", +"poi_schools":"", +"poi_super_markets":"", +"poi_tourism":"" }, "vi_VN":{ "total_ascend":"Đi tiếp %1$s nữa", @@ -5081,7 +5873,25 @@ "track_type":"", "toll":"", "next":"", -"back":"" +"back":"", +"poi_airports":"", +"poi_banks":"", +"poi_bus_stops":"", +"poi_education":"", +"poi_hospitals":"", +"poi_hotels":"", +"poi_leisure":"", +"poi_museums":"", +"poi_parking":"", +"poi_parks":"", +"poi_pharmacies":"", +"poi_playgrounds":"", +"poi_public_transit":"", +"poi_railway_station":"", +"poi_restaurants":"", +"poi_schools":"", +"poi_super_markets":"", +"poi_tourism":"" }, "zh_CN":{ "total_ascend":"总上升 %1$s", @@ -5194,7 +6004,25 @@ "track_type":"轨道类型", "toll":"收费", "next":"下一页", -"back":"返回" +"back":"返回", +"poi_airports":"", +"poi_banks":"", +"poi_bus_stops":"", +"poi_education":"", +"poi_hospitals":"", +"poi_hotels":"", +"poi_leisure":"", +"poi_museums":"", +"poi_parking":"", +"poi_parks":"", +"poi_pharmacies":"", +"poi_playgrounds":"", +"poi_public_transit":"", +"poi_railway_station":"", +"poi_restaurants":"", +"poi_schools":"", +"poi_super_markets":"", +"poi_tourism":"" }, "zh_HK":{ "total_ascend":"總共上昇 %1$s", @@ -5307,7 +6135,25 @@ "track_type":"軌道類型", "toll":"收費", "next":"下一頁", -"back":"返回" +"back":"返回", +"poi_airports":"", +"poi_banks":"", +"poi_bus_stops":"", +"poi_education":"", +"poi_hospitals":"", +"poi_hotels":"", +"poi_leisure":"", +"poi_museums":"", +"poi_parking":"", +"poi_parks":"", +"poi_pharmacies":"", +"poi_playgrounds":"", +"poi_public_transit":"", +"poi_railway_station":"", +"poi_restaurants":"", +"poi_schools":"", +"poi_super_markets":"", +"poi_tourism":"" }, "zh_TW":{ "total_ascend":"總共上昇 %1$s", @@ -5420,5 +6266,23 @@ "track_type":"", "toll":"", "next":"", -"back":"" +"back":"", +"poi_airports":"", +"poi_banks":"", +"poi_bus_stops":"", +"poi_education":"", +"poi_hospitals":"", +"poi_hotels":"", +"poi_leisure":"", +"poi_museums":"", +"poi_parking":"", +"poi_parks":"", +"poi_pharmacies":"", +"poi_playgrounds":"", +"poi_public_transit":"", +"poi_railway_station":"", +"poi_restaurants":"", +"poi_schools":"", +"poi_super_markets":"", +"poi_tourism":"" }} \ No newline at end of file diff --git a/test/NavBar.test.ts b/test/NavBar.test.ts index 8487e852..9dfb8f86 100644 --- a/test/NavBar.test.ts +++ b/test/NavBar.test.ts @@ -7,7 +7,7 @@ import { coordinateToText } from '@/Converters' // import the window and mock it with jest import { window } from '@/Window' -import MapOptionsStore, { StyleOption } from '@/stores/MapOptionsStore' +import MapOptionsStore from '@/stores/MapOptionsStore' import * as config from 'config' import { RoutingProfile } from '@/api/graphhopper' diff --git a/test/poi/AddressParseResult.test.ts b/test/poi/AddressParseResult.test.ts new file mode 100644 index 00000000..72da4619 --- /dev/null +++ b/test/poi/AddressParseResult.test.ts @@ -0,0 +1,40 @@ +import { AddressParseResult } from '@/pois/AddressParseResult' +import fetchMock from 'jest-fetch-mock' +import { getTranslation, setTranslation } from '@/translation/Translation' + +beforeAll(() => { + setTranslation('en', false) + AddressParseResult.setPOITriggerPhrases(getTranslation()) +}) + +describe('reverse geocoder', () => { + it('should parse fully', async () => { + let res = AddressParseResult.parse('dresden restaurant', false) + expect(res.location).toEqual('dresden') + expect(res.icon).toEqual('restaurant') + + res = AddressParseResult.parse('restaurant', false) + expect(res.location).toEqual('') + expect(res.icon).toEqual('restaurant') + + res = AddressParseResult.parse('restaurant in dresden', false) + expect(res.location).toEqual('dresden') + expect(res.icon).toEqual('restaurant') + + res = AddressParseResult.parse('airports around some thing else', false) + expect(res.location).toEqual('some thing else') + expect(res.icon).toEqual('flight_takeoff') + + res = AddressParseResult.parse('dresden super market', false) + expect(res.location).toEqual('dresden') + expect(res.poi).toEqual('super markets') + + res = AddressParseResult.parse('dresden park', false) + expect(res.location).toEqual('dresden') + expect(res.poi).toEqual('parks') + + res = AddressParseResult.parse('dresden parking', false) + expect(res.location).toEqual('dresden') + expect(res.poi).toEqual('parking') + }) +}) diff --git a/test/routing/Api.test.ts b/test/routing/Api.test.ts index 8149570e..5f2304ba 100644 --- a/test/routing/Api.test.ts +++ b/test/routing/Api.test.ts @@ -6,7 +6,7 @@ import { RouteRequestFailed, RouteRequestSuccess } from '@/actions/Actions' import { setTranslation } from '@/translation/Translation' import Dispatcher from '@/stores/Dispatcher' -import { AddressParseResult, ApiImpl } from '@/api/Api' +import { ApiImpl } from '@/api/Api' import { ApiInfo, ErrorResponse, RoutingArgs, RoutingRequest, RoutingResultInfo } from '@/api/graphhopper' beforeAll(() => { @@ -343,38 +343,6 @@ describe('route', () => { }) }) -describe('reverse geocoder', () => { - it('should parse fully', async () => { - let res = AddressParseResult.parse('dresden restaurant', false) - expect(res.location).toEqual('dresden') - expect(res.icon).toEqual('restaurant') - - res = AddressParseResult.parse('restaurant', false) - expect(res.location).toEqual('') - expect(res.icon).toEqual('restaurant') - - res = AddressParseResult.parse('restaurant in dresden', false) - expect(res.location).toEqual('dresden') - expect(res.icon).toEqual('restaurant') - - res = AddressParseResult.parse('airports around some thing else', false) - expect(res.location).toEqual('some thing else') - expect(res.icon).toEqual('flight_takeoff') - - res = AddressParseResult.parse('dresden super market', false) - expect(res.location).toEqual('dresden') - expect(res.poi).toEqual('super markets') - - res = AddressParseResult.parse('dresden park', false) - expect(res.location).toEqual('dresden') - expect(res.poi).toEqual('parks') - - res = AddressParseResult.parse('dresden parking', false) - expect(res.location).toEqual('dresden') - expect(res.poi).toEqual('parking') - }) -}) - function getEmptyResult() { return { info: { copyright: [], road_data_timestamp: '', took: 0 } as RoutingResultInfo, From 20bba5664b97e678db39fddc2c8976aff042108f Mon Sep 17 00:00:00 2001 From: Peter Date: Mon, 3 Jun 2024 01:23:45 +0200 Subject: [PATCH 10/33] fetch more information from OSM --- src/api/Api.ts | 1 - src/layers/MapFeaturePopup.module.css | 40 +++++++++++++--- src/layers/POIPopup.tsx | 69 ++++++++++++++++++++++++++- src/pois/AddressParseResult.ts | 2 + src/stores/POIsStore.ts | 2 + 5 files changed, 105 insertions(+), 9 deletions(-) diff --git a/src/api/Api.ts b/src/api/Api.ts index fce758a6..e72fc9ef 100644 --- a/src/api/Api.ts +++ b/src/api/Api.ts @@ -4,7 +4,6 @@ import { ApiInfo, Bbox, ErrorResponse, - GeocodingHit, GeocodingResult, Path, RawPath, diff --git a/src/layers/MapFeaturePopup.module.css b/src/layers/MapFeaturePopup.module.css index 3ee45acd..0cdb70a7 100644 --- a/src/layers/MapFeaturePopup.module.css +++ b/src/layers/MapFeaturePopup.module.css @@ -10,10 +10,9 @@ position: relative; top: -40px; left: 20px; - max-width: 400px; + max-width: 300px; background-color: white; - font-size: small; box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2); box-sizing: border-box; padding: 5px 10px 5px 10px; @@ -25,13 +24,40 @@ padding-bottom: 5px; } -.poiPopupButton svg { - margin-top: 3px; -} - .poiPopupButton { - padding-top: 5px; display: flex; flex-direction: row; gap: 7px; + padding-top: 5px; +} + +.poiPopupButton svg { + margin-top: 3px; +} + +.poiPopup a { + color: gray; + font-size: larger; + text-decoration: none; +} + +.poiPopup a:hover { + color: black; + text-decoration: underline; +} + +.poiPopupTable { + font-size: small; + padding: 6px 0; +} + +.poiPopupTable a { + font-size: small; +} + +.poiPopupTable th, +.poiPopupTable td { + border-bottom: 1px solid #ccc; + padding: 3px; + text-align: left; } diff --git a/src/layers/POIPopup.tsx b/src/layers/POIPopup.tsx index 48bd3c73..c36ced2e 100644 --- a/src/layers/POIPopup.tsx +++ b/src/layers/POIPopup.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { useEffect, useState } from 'react' import styles from '@/layers/MapFeaturePopup.module.css' import MapPopup from '@/layers/MapPopup' import { Map } from 'ol' @@ -14,18 +14,85 @@ interface POIStatePopupProps { poiState: POIsStoreState } +interface TagHash { + [key: string]: string +} + +async function fetchInfo(url: string): Promise { + try { + const response = await fetch(url) + if (!response.ok) return { status: response.statusText } + + // Parse the XML text + const xmlText = await response.text() + + // Convert the XML text to an XMLDocument + const parser = new DOMParser() + const xmlDoc = parser.parseFromString(xmlText, 'application/xml') + const tags = xmlDoc.querySelectorAll('tag') + const hash: Record = {} + + tags.forEach(tag => { + const key = tag.getAttribute('k') + const value = tag.getAttribute('v') + if (key && value) hash[key] = value + }) + + return hash + } catch (error) { + return { status: '' + error } + } +} + /** * The popup shown when certain map features are hovered. For example a road of the routing graph layer. */ export default function POIStatePopup({ map, poiState }: POIStatePopupProps) { const selectedPOI = poiState.selected const oldQueryPoint = poiState.oldQueryPoint + const t = selectedPOI?.osm_type + const path = (t === 'W' ? 'way' : t === 'N' ? 'node' : 'relation') + '/' + selectedPOI?.osm_id + const [kv, setKV] = useState({}) + + useEffect(() => { + return () => { + setKV({}) + } + }, [poiState.selected]) return (
{selectedPOI?.name}
{selectedPOI?.address}
+ { + fetchInfo('https://www.openstreetmap.org/api/0.6/' + path).then(tagHash => setKV(tagHash)) + }} + > + {Object.keys(kv).length == 0 && tr('More information')} + + + {Object.entries(kv).map( + ([key, value]) => + !key.startsWith('addr') && ( + + + + + ) + )} + +
{key} + {value.startsWith('https://') ? ( + + {value} + + ) : ( + value + )} +
+
{oldQueryPoint && } Date: Mon, 3 Jun 2024 02:28:34 +0200 Subject: [PATCH 11/33] minor fixes --- src/layers/MapFeaturePopup.module.css | 15 +++-- src/layers/POIPopup.tsx | 91 +++++++++++++++++---------- 2 files changed, 69 insertions(+), 37 deletions(-) diff --git a/src/layers/MapFeaturePopup.module.css b/src/layers/MapFeaturePopup.module.css index 0cdb70a7..bbd8726e 100644 --- a/src/layers/MapFeaturePopup.module.css +++ b/src/layers/MapFeaturePopup.module.css @@ -10,7 +10,7 @@ position: relative; top: -40px; left: 20px; - max-width: 300px; + max-width: 250px; background-color: white; box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2); @@ -37,7 +37,6 @@ .poiPopup a { color: gray; - font-size: larger; text-decoration: none; } @@ -51,13 +50,21 @@ padding: 6px 0; } -.poiPopupTable a { +.osmLink { + padding-bottom: 7px; +} + +.osmLink a { font-size: small; } .poiPopupTable th, .poiPopupTable td { border-bottom: 1px solid #ccc; - padding: 3px; + padding: 3px 1px 3px 0; text-align: left; + + /* strange, why do we need the following? */ + word-break: break-all; + min-width: 100px; } diff --git a/src/layers/POIPopup.tsx b/src/layers/POIPopup.tsx index c36ced2e..f9666a07 100644 --- a/src/layers/POIPopup.tsx +++ b/src/layers/POIPopup.tsx @@ -22,16 +22,11 @@ async function fetchInfo(url: string): Promise { try { const response = await fetch(url) if (!response.ok) return { status: response.statusText } - - // Parse the XML text const xmlText = await response.text() - - // Convert the XML text to an XMLDocument const parser = new DOMParser() const xmlDoc = parser.parseFromString(xmlText, 'application/xml') const tags = xmlDoc.querySelectorAll('tag') const hash: Record = {} - tags.forEach(tag => { const key = tag.getAttribute('k') const value = tag.getAttribute('v') @@ -44,6 +39,49 @@ async function fetchInfo(url: string): Promise { } } +function KVTable(props: { kv: TagHash }) { + return ( + + + {Object.entries(props.kv).map(([key, value]) => { + const url = value.startsWith('https://') + const tel = key.toLowerCase().includes('phone') + const email = key.toLowerCase().includes('email') + const valueArr = value.split(':').map(v => v.trim()) + const wiki = key.toLowerCase().includes('wikipedia') && valueArr.length == 2 + const wikiUrl = wiki + ? 'https://' + valueArr[0] + '.wikipedia.org/wiki/' + encodeURIComponent(valueArr[1]) + : '' + return ( + !key.startsWith('addr') && + !key.startsWith('name') && + !key.startsWith('building') && ( + + + + + ) + ) + })} + +
{key} + {url && ( + + {value} + + )} + {tel && {value}} + {email && {value}} + {wiki && ( + + {value} + + )} + {!url && !tel && !email && !wiki && value} +
+ ) +} + /** * The popup shown when certain map features are hovered. For example a road of the routing graph layer. */ @@ -65,34 +103,21 @@ export default function POIStatePopup({ map, poiState }: POIStatePopupProps) {
{selectedPOI?.name}
{selectedPOI?.address}
- { - fetchInfo('https://www.openstreetmap.org/api/0.6/' + path).then(tagHash => setKV(tagHash)) - }} - > - {Object.keys(kv).length == 0 && tr('More information')} - - - {Object.entries(kv).map( - ([key, value]) => - !key.startsWith('addr') && ( - - - - - ) - )} - -
{key} - {value.startsWith('https://') ? ( - - {value} - - ) : ( - value - )} -
-
+ {Object.keys(kv).length == 0 && ( + { + fetchInfo('https://www.openstreetmap.org/api/0.6/' + path).then(tagHash => setKV(tagHash)) + }} + > + {tr('Fetch more info')} + + )} + +
{oldQueryPoint && } Date: Mon, 3 Jun 2024 18:29:52 +0200 Subject: [PATCH 12/33] use overpass instead osm --- src/layers/POIPopup.tsx | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/src/layers/POIPopup.tsx b/src/layers/POIPopup.tsx index f9666a07..3467ef42 100644 --- a/src/layers/POIPopup.tsx +++ b/src/layers/POIPopup.tsx @@ -18,22 +18,18 @@ interface TagHash { [key: string]: string } -async function fetchInfo(url: string): Promise { +async function fetchInfo(type: string, ids: string[]): Promise { try { - const response = await fetch(url) - if (!response.ok) return { status: response.statusText } - const xmlText = await response.text() - const parser = new DOMParser() - const xmlDoc = parser.parseFromString(xmlText, 'application/xml') - const tags = xmlDoc.querySelectorAll('tag') - const hash: Record = {} - tags.forEach(tag => { - const key = tag.getAttribute('k') - const value = tag.getAttribute('v') - if (key && value) hash[key] = value + const data = `[out:json][timeout:15]; + (${type}(id:${ids.join(',')});); + out body;` + const result = await fetch('https://overpass-api.de/api/interpreter', { + method: 'POST', + body: 'data=' + encodeURIComponent(data), }) - - return hash + const json = await result.json() + if (json.elements.length > 0) return json.elements[0].tags + else return { status: 'empty' } } catch (error) { return { status: '' + error } } @@ -89,7 +85,7 @@ export default function POIStatePopup({ map, poiState }: POIStatePopupProps) { const selectedPOI = poiState.selected const oldQueryPoint = poiState.oldQueryPoint const t = selectedPOI?.osm_type - const path = (t === 'W' ? 'way' : t === 'N' ? 'node' : 'relation') + '/' + selectedPOI?.osm_id + const type = t === 'W' ? 'way' : t === 'N' ? 'node' : 'relation' const [kv, setKV] = useState({}) useEffect(() => { @@ -106,7 +102,7 @@ export default function POIStatePopup({ map, poiState }: POIStatePopupProps) { {Object.keys(kv).length == 0 && ( { - fetchInfo('https://www.openstreetmap.org/api/0.6/' + path).then(tagHash => setKV(tagHash)) + if (selectedPOI) fetchInfo(type, [selectedPOI.osm_id]).then(tagHash => setKV(tagHash)) }} > {tr('Fetch more info')} @@ -114,7 +110,7 @@ export default function POIStatePopup({ map, poiState }: POIStatePopupProps) { )} From b75c507ff5dade40a0abd444fe7fbdc944679d8f Mon Sep 17 00:00:00 2001 From: Peter Date: Tue, 4 Jun 2024 00:29:09 +0200 Subject: [PATCH 13/33] fetch on click; more i18n; minor fixes --- src/api/Api.ts | 14 ++--- src/layers/MapFeaturePopup.module.css | 4 +- src/layers/POIPopup.tsx | 25 +++++---- src/pois/AddressParseResult.ts | 74 ++++++++++++++++----------- src/stores/POIsStore.ts | 2 + src/translation/tr.json | 48 +++++++++++++++++ test/DummyApi.ts | 3 +- test/poi/AddressParseResult.test.ts | 4 ++ test/stores/QueryStore.test.ts | 3 +- test/stores/RouteStore.test.ts | 3 +- 10 files changed, 122 insertions(+), 58 deletions(-) diff --git a/src/api/Api.ts b/src/api/Api.ts index e72fc9ef..59ed5864 100644 --- a/src/api/Api.ts +++ b/src/api/Api.ts @@ -17,6 +17,7 @@ import { LineString } from 'geojson' import { getTranslation, tr } from '@/translation/Translation' import * as config from 'config' import { Coordinate } from '@/stores/QueryStore' +import { KV } from '@/pois/AddressParseResult' interface ApiProfile { name: string @@ -31,12 +32,7 @@ export default interface Api { geocode(query: string, provider: string, additionalOptions?: Record): Promise - reverseGeocode( - query: string | undefined, - point: Coordinate, - radius: number, - tags?: string[] - ): Promise + reverseGeocode(query: string | undefined, point: Coordinate, radius: number, tags?: KV[]): Promise supportsGeocoding(): boolean } @@ -129,7 +125,7 @@ export class ApiImpl implements Api { query: string | undefined, point: Coordinate, radius: number, - tags?: string[] + tags?: KV[] ): Promise { if (!this.supportsGeocoding()) return { @@ -149,8 +145,8 @@ export class ApiImpl implements Api { url.searchParams.append('locale', langAndCountry.length > 0 ? langAndCountry[0] : 'en') if (tags) { - for (const value of tags) { - url.searchParams.append('osm_tag', value) + for (const tag of tags) { + url.searchParams.append('osm_tag', tag.k + ':' + tag.v) } } diff --git a/src/layers/MapFeaturePopup.module.css b/src/layers/MapFeaturePopup.module.css index bbd8726e..2f96bdca 100644 --- a/src/layers/MapFeaturePopup.module.css +++ b/src/layers/MapFeaturePopup.module.css @@ -10,7 +10,7 @@ position: relative; top: -40px; left: 20px; - max-width: 250px; + max-width: 300px; background-color: white; box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2); @@ -66,5 +66,5 @@ /* strange, why do we need the following? */ word-break: break-all; - min-width: 100px; + min-width: 120px; } diff --git a/src/layers/POIPopup.tsx b/src/layers/POIPopup.tsx index 3467ef42..96ee6578 100644 --- a/src/layers/POIPopup.tsx +++ b/src/layers/POIPopup.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react' import styles from '@/layers/MapFeaturePopup.module.css' import MapPopup from '@/layers/MapPopup' import { Map } from 'ol' -import { POIsStoreState } from '@/stores/POIsStore' +import { POI, POIsStoreState } from '@/stores/POIsStore' import { tr } from '@/translation/Translation' import Dispatcher from '@/stores/Dispatcher' import { SelectPOI, SetPoint, SetPOIs } from '@/actions/Actions' @@ -35,12 +35,12 @@ async function fetchInfo(type: string, ids: string[]): Promise { } } -function KVTable(props: { kv: TagHash }) { +function KVTable(props: { kv: TagHash; poi: POI | null }) { return ( {Object.entries(props.kv).map(([key, value]) => { - const url = value.startsWith('https://') + const url = value.startsWith('https://') || value.startsWith('http://') const tel = key.toLowerCase().includes('phone') const email = key.toLowerCase().includes('email') const valueArr = value.split(':').map(v => v.trim()) @@ -48,7 +48,13 @@ function KVTable(props: { kv: TagHash }) { const wikiUrl = wiki ? 'https://' + valueArr[0] + '.wikipedia.org/wiki/' + encodeURIComponent(valueArr[1]) : '' + // tags like amenity:restaurant should not be shown if it is a restaurant (determined by poi.tags) + const poiInfoRepeated = props.poi ? props.poi.tags.some(kv => kv.k == key && kv.v === value) : false return ( + !poiInfoRepeated && + key !== 'source' && + key !== 'image' && + !key.includes('fax') && !key.startsWith('addr') && !key.startsWith('name') && !key.startsWith('building') && ( @@ -89,6 +95,7 @@ export default function POIStatePopup({ map, poiState }: POIStatePopupProps) { const [kv, setKV] = useState({}) useEffect(() => { + if (selectedPOI) fetchInfo(type, [selectedPOI.osm_id]).then(tagHash => setKV(tagHash)) return () => { setKV({}) } @@ -99,16 +106,8 @@ export default function POIStatePopup({ map, poiState }: POIStatePopupProps) {
{selectedPOI?.name}
{selectedPOI?.address}
- {Object.keys(kv).length == 0 && ( - { - if (selectedPOI) fetchInfo(type, [selectedPOI.osm_id]).then(tagHash => setKV(tagHash)) - }} - > - {tr('Fetch more info')} - - )} - + {Object.keys(kv).length == 0 && {tr('Fetching more info...')}} +
OpenStreetMap.org diff --git a/src/pois/AddressParseResult.ts b/src/pois/AddressParseResult.ts index ad73b6f6..ac9afb9c 100644 --- a/src/pois/AddressParseResult.ts +++ b/src/pois/AddressParseResult.ts @@ -8,12 +8,13 @@ import { tr, Translation } from '@/translation/Translation' export class AddressParseResult { location: string - tags: string[] + tags: KV[] icon: string poi: string - static VALUES: PoiTriggerPhrases[] + static TRIGGER_VALUES: PoiTriggerPhrases[] + static REMOVE_VALUES: string[] - constructor(location: string, tags: string[], icon: string, poi: string) { + constructor(location: string, tags: KV[], icon: string, poi: string) { this.location = location this.tags = tags this.icon = icon @@ -32,7 +33,7 @@ export class AddressParseResult { static parse(query: string, incomplete: boolean): AddressParseResult { query = query.toLowerCase() - const smallWords = ['in', 'around', 'nearby'] + const smallWords = AddressParseResult.REMOVE_VALUES // e.g. 'restaurants in this area' or 'restaurants in berlin' const queryTokens: string[] = query.split(' ').filter(token => !smallWords.includes(token)) const cleanQuery = queryTokens.join(' ') const bigrams: string[] = [] @@ -40,7 +41,7 @@ export class AddressParseResult { bigrams.push(queryTokens[i] + ' ' + queryTokens[i + 1]) } - for (const val of AddressParseResult.VALUES) { + for (const val of AddressParseResult.TRIGGER_VALUES) { // two word phrases like 'public transit' must be checked before single word phrases for (const keyword of val.k) { const i = bigrams.indexOf(keyword) @@ -81,6 +82,7 @@ export class AddressParseResult { const res = hitToItem(hit) return { name: res.mainText, + tags: parseResult.tags, icon: parseResult.icon, coordinate: hit.point, address: res.secondText, @@ -96,32 +98,42 @@ export class AddressParseResult { // because of the static method we need to inject the Translation object as otherwise jest has a problem static setPOITriggerPhrases(translation: Translation) { - const t = (s: string) => - translation - .get(s) - .split(',') - .map(s => s.trim().toLowerCase()) - AddressParseResult.VALUES = [ - { k: t('poi_airports'), t: ['aeroway:aerodrome'], i: 'flight_takeoff' }, - { k: t('poi_banks'), t: ['amenity:bank'], i: 'universal_currency_alt' }, - { k: t('poi_bus_stops'), t: ['highway:bus_stop'], i: 'train' }, - { k: t('poi_education'), t: ['amenity:school', 'building:school', 'building:university'], i: 'school' }, - { k: t('poi_hospitals'), t: ['amenity:hospital', 'building:hospital'], i: 'local_hospital' }, - { k: t('poi_hotels'), t: ['amenity:hotel', 'building:hotel', 'tourism:hotel'], i: 'hotel' }, - { k: t('poi_leisure'), t: ['leisure'], i: 'sports_handball' }, - { k: t('poi_museums'), t: ['tourism:museum', 'building:museum'], i: 'museum' }, - { k: t('poi_parking'), t: ['amenity:parking'], i: 'local_parking' }, - { k: t('poi_parks'), t: ['leisure:park'], i: 'sports_handball' }, - { k: t('poi_pharmacies'), t: ['amenity:pharmacy'], i: 'local_pharmacy' }, - { k: t('poi_playgrounds'), t: ['leisure:playground'], i: 'sports_handball' }, - { k: t('poi_public_transit'), t: ['railway:station', 'highway:bus_stop'], i: 'train' }, - { k: t('poi_railway_station'), t: ['railway:station'], i: 'train' }, - { k: t('poi_restaurants'), t: ['amenity:restaurant'], i: 'restaurant' }, - { k: t('poi_schools'), t: ['amenity:school', 'building:school'], i: 'school' }, - { k: t('poi_super_markets'), t: ['shop:supermarket', 'building:supermarket'], i: 'store' }, - { k: t('poi_tourism'), t: ['tourism'], i: 'luggage' }, - ] + const t = (s: string) => translation + .get(s) + .split(',') + .map(s => s.trim().toLowerCase()) + AddressParseResult.REMOVE_VALUES = t('poi_removal_words') + AddressParseResult.TRIGGER_VALUES = [ + { k: 'poi_airports', t: ['aeroway:aerodrome'], i: 'flight_takeoff' }, + { k: 'poi_banks', t: ['amenity:bank'], i: 'universal_currency_alt' }, + { k: 'poi_bus_stops', t: ['highway:bus_stop'], i: 'train' }, + { k: 'poi_education', t: ['amenity:school', 'building:school', 'building:university'], i: 'school' }, + { k: 'poi_hospitals', t: ['amenity:hospital', 'building:hospital'], i: 'local_hospital' }, + { k: 'poi_hotels', t: ['amenity:hotel', 'building:hotel', 'tourism:hotel'], i: 'hotel' }, + { k: 'poi_leisure', t: ['leisure'], i: 'sports_handball' }, + { k: 'poi_museums', t: ['tourism:museum', 'building:museum'], i: 'museum' }, + { k: 'poi_parking', t: ['amenity:parking'], i: 'local_parking' }, + { k: 'poi_parks', t: ['leisure:park'], i: 'sports_handball' }, + { k: 'poi_pharmacies', t: ['amenity:pharmacy'], i: 'local_pharmacy' }, + { k: 'poi_playgrounds', t: ['leisure:playground'], i: 'sports_handball' }, + { k: 'poi_public_transit', t: ['railway:station', 'highway:bus_stop'], i: 'train' }, + { k: 'poi_railway_station', t: ['railway:station'], i: 'train' }, + { k: 'poi_restaurants', t: ['amenity:restaurant'], i: 'restaurant' }, + { k: 'poi_schools', t: ['amenity:school', 'building:school'], i: 'school' }, + { k: 'poi_super_markets', t: ['shop:supermarket', 'building:supermarket'], i: 'store' }, + { k: 'poi_tourism', t: ['tourism'], i: 'luggage' }, + ].map(v => { + const tags = v.t.map(val => { + return { k: val.split(':')[0], v: val.split(':')[1] } + }) + return { + ...v, + k: t(v.k), + t: tags, + } + }) } } -export type PoiTriggerPhrases = { k: string[]; t: string[]; i: string } +export type KV = { k: string; v: string } +export type PoiTriggerPhrases = { k: string[]; t: KV[]; i: string } diff --git a/src/stores/POIsStore.ts b/src/stores/POIsStore.ts index 17e7df20..30c23a13 100644 --- a/src/stores/POIsStore.ts +++ b/src/stores/POIsStore.ts @@ -2,9 +2,11 @@ import { Coordinate, QueryPoint } from '@/stores/QueryStore' import Store from '@/stores/Store' import { Action } from '@/stores/Dispatcher' import { SelectPOI, SetPOIs } from '@/actions/Actions' +import { KV } from '@/pois/AddressParseResult' export interface POI { name: string + tags: KV[] osm_id: string osm_type: string icon: string diff --git a/src/translation/tr.json b/src/translation/tr.json index 2fccf16e..20dd474a 100644 --- a/src/translation/tr.json +++ b/src/translation/tr.json @@ -110,6 +110,7 @@ "toll":"Toll", "next":"Next", "back":"Back", +"poi_removal_words":"area, around, here, in, local, nearby, this", "poi_airports":"airports, airport", "poi_banks":"banks, bank", "poi_bus_stops":"bus stops, bus stop, bus", @@ -241,6 +242,7 @@ "toll":"", "next":"", "back":"", +"poi_removal_words":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -372,6 +374,7 @@ "toll":"", "next":"", "back":"", +"poi_removal_words":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -503,6 +506,7 @@ "toll":"", "next":"", "back":"", +"poi_removal_words":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -634,6 +638,7 @@ "toll":"", "next":"", "back":"", +"poi_removal_words":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -765,6 +770,7 @@ "toll":"", "next":"", "back":"", +"poi_removal_words":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -896,6 +902,7 @@ "toll":"", "next":"", "back":"", +"poi_removal_words":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -1027,6 +1034,7 @@ "toll":"", "next":"", "back":"", +"poi_removal_words":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -1158,6 +1166,7 @@ "toll":"", "next":"", "back":"", +"poi_removal_words":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -1289,6 +1298,7 @@ "toll":"Maut", "next":"Weiter", "back":"Zurück", +"poi_removal_words":"der, dem, gebiet, in, karte, lokal, lokale, nähe", "poi_airports":"Flughäfen, Flughafen", "poi_banks":"Bank", "poi_bus_stops":"Haltestellen, Haltestelle, Bushaltestellen, Bushaltestelle", @@ -1420,6 +1430,7 @@ "toll":"Διόδια", "next":"Επόμενο", "back":"Πίσω", +"poi_removal_words":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -1551,6 +1562,7 @@ "toll":"", "next":"", "back":"", +"poi_removal_words":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -1682,6 +1694,7 @@ "toll":"", "next":"", "back":"", +"poi_removal_words":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -1813,6 +1826,7 @@ "toll":"", "next":"", "back":"", +"poi_removal_words":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -1944,6 +1958,7 @@ "toll":"", "next":"", "back":"", +"poi_removal_words":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -2075,6 +2090,7 @@ "toll":"", "next":"", "back":"", +"poi_removal_words":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -2206,6 +2222,7 @@ "toll":"", "next":"", "back":"", +"poi_removal_words":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -2337,6 +2354,7 @@ "toll":"", "next":"", "back":"", +"poi_removal_words":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -2468,6 +2486,7 @@ "toll":"", "next":"", "back":"", +"poi_removal_words":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -2599,6 +2618,7 @@ "toll":"", "next":"", "back":"", +"poi_removal_words":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -2730,6 +2750,7 @@ "toll":"", "next":"", "back":"", +"poi_removal_words":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -2861,6 +2882,7 @@ "toll":"", "next":"", "back":"", +"poi_removal_words":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -2992,6 +3014,7 @@ "toll":"", "next":"", "back":"", +"poi_removal_words":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -3123,6 +3146,7 @@ "toll":"Tol", "next":"Selanjutnya", "back":"Sebelumnya", +"poi_removal_words":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -3254,6 +3278,7 @@ "toll":"", "next":"", "back":"", +"poi_removal_words":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -3385,6 +3410,7 @@ "toll":"", "next":"", "back":"", +"poi_removal_words":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -3516,6 +3542,7 @@ "toll":"", "next":"", "back":"", +"poi_removal_words":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -3647,6 +3674,7 @@ "toll":"", "next":"", "back":"", +"poi_removal_words":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -3778,6 +3806,7 @@ "toll":"", "next":"", "back":"", +"poi_removal_words":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -3909,6 +3938,7 @@ "toll":"", "next":"", "back":"", +"poi_removal_words":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -4040,6 +4070,7 @@ "toll":"", "next":"", "back":"", +"poi_removal_words":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -4171,6 +4202,7 @@ "toll":"", "next":"", "back":"", +"poi_removal_words":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -4302,6 +4334,7 @@ "toll":"", "next":"", "back":"", +"poi_removal_words":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -4433,6 +4466,7 @@ "toll":"", "next":"", "back":"", +"poi_removal_words":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -4564,6 +4598,7 @@ "toll":"", "next":"", "back":"", +"poi_removal_words":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -4695,6 +4730,7 @@ "toll":"", "next":"", "back":"", +"poi_removal_words":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -4826,6 +4862,7 @@ "toll":"", "next":"", "back":"", +"poi_removal_words":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -4957,6 +4994,7 @@ "toll":"", "next":"", "back":"", +"poi_removal_words":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -5088,6 +5126,7 @@ "toll":"", "next":"", "back":"", +"poi_removal_words":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -5219,6 +5258,7 @@ "toll":"", "next":"", "back":"", +"poi_removal_words":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -5350,6 +5390,7 @@ "toll":"", "next":"", "back":"", +"poi_removal_words":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -5481,6 +5522,7 @@ "toll":"", "next":"", "back":"", +"poi_removal_words":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -5612,6 +5654,7 @@ "toll":"", "next":"", "back":"", +"poi_removal_words":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -5743,6 +5786,7 @@ "toll":"", "next":"", "back":"", +"poi_removal_words":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -5874,6 +5918,7 @@ "toll":"", "next":"", "back":"", +"poi_removal_words":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -6005,6 +6050,7 @@ "toll":"收费", "next":"下一页", "back":"返回", +"poi_removal_words":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -6136,6 +6182,7 @@ "toll":"收費", "next":"下一頁", "back":"返回", +"poi_removal_words":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -6267,6 +6314,7 @@ "toll":"", "next":"", "back":"", +"poi_removal_words":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", diff --git a/test/DummyApi.ts b/test/DummyApi.ts index 69852a43..49aee410 100644 --- a/test/DummyApi.ts +++ b/test/DummyApi.ts @@ -1,6 +1,7 @@ import Api from '../src/api/Api' import { ApiInfo, GeocodingResult, RoutingArgs, RoutingResult, RoutingResultInfo } from '../src/api/graphhopper' import { Coordinate } from '@/stores/QueryStore' +import { KV } from '@/pois/AddressParseResult' export default class DummyApi implements Api { geocode(query: string): Promise { @@ -14,7 +15,7 @@ export default class DummyApi implements Api { query: string | undefined, point: Coordinate, radius: number, - tags?: string[] + tags?: KV[] ): Promise { return Promise.resolve({ took: 0, diff --git a/test/poi/AddressParseResult.test.ts b/test/poi/AddressParseResult.test.ts index 72da4619..3fa1b7a2 100644 --- a/test/poi/AddressParseResult.test.ts +++ b/test/poi/AddressParseResult.test.ts @@ -36,5 +36,9 @@ describe('reverse geocoder', () => { res = AddressParseResult.parse('dresden parking', false) expect(res.location).toEqual('dresden') expect(res.poi).toEqual('parking') + + res = AddressParseResult.parse('restaurants in this area', false) + expect(res.location).toEqual('') + expect(res.poi).toEqual('restaurants') }) }) diff --git a/test/stores/QueryStore.test.ts b/test/stores/QueryStore.test.ts index 574563b4..7778d62b 100644 --- a/test/stores/QueryStore.test.ts +++ b/test/stores/QueryStore.test.ts @@ -19,6 +19,7 @@ import { SetPoint, SetVehicleProfile, } from '@/actions/Actions' +import { KV } from '@/pois/AddressParseResult' class ApiMock implements Api { private readonly callback: { (args: RoutingArgs): void } @@ -35,7 +36,7 @@ class ApiMock implements Api { query: string | undefined, point: Coordinate, radius: number, - tags?: string[] + tags?: KV[] ): Promise { throw Error('not implemented') } diff --git a/test/stores/RouteStore.test.ts b/test/stores/RouteStore.test.ts index 5093b812..d77e1fc8 100644 --- a/test/stores/RouteStore.test.ts +++ b/test/stores/RouteStore.test.ts @@ -4,6 +4,7 @@ import Api from '@/api/Api' import { ApiInfo, GeocodingResult, Path, RoutingArgs, RoutingResult } from '@/api/graphhopper' import Dispatcher, { Action } from '@/stores/Dispatcher' import { ClearPoints, ClearRoute, RemovePoint, SetPoint, SetSelectedPath } from '@/actions/Actions' +import { KV } from '@/pois/AddressParseResult' describe('RouteStore', () => { afterEach(() => { @@ -73,7 +74,7 @@ class DummyApi implements Api { query: string | undefined, point: Coordinate, radius: number, - tags?: string[] + tags?: KV[] ): Promise { throw Error('not implemented') } From b539ae42c30fcfd79cf1c2a93d48c2b69cdc5202 Mon Sep 17 00:00:00 2001 From: Peter Date: Tue, 4 Jun 2024 14:22:36 +0200 Subject: [PATCH 14/33] reorder --- src/layers/MapFeaturePopup.module.css | 1 + src/layers/POIPopup.tsx | 43 +++++++++++++-------------- src/pois/AddressParseResult.ts | 9 +++--- 3 files changed, 27 insertions(+), 26 deletions(-) diff --git a/src/layers/MapFeaturePopup.module.css b/src/layers/MapFeaturePopup.module.css index 2f96bdca..8f42bf0a 100644 --- a/src/layers/MapFeaturePopup.module.css +++ b/src/layers/MapFeaturePopup.module.css @@ -29,6 +29,7 @@ flex-direction: row; gap: 7px; padding-top: 5px; + padding-bottom: 5px; } .poiPopupButton svg { diff --git a/src/layers/POIPopup.tsx b/src/layers/POIPopup.tsx index 96ee6578..425048e0 100644 --- a/src/layers/POIPopup.tsx +++ b/src/layers/POIPopup.tsx @@ -106,6 +106,27 @@ export default function POIStatePopup({ map, poiState }: POIStatePopupProps) {
{selectedPOI?.name}
{selectedPOI?.address}
+
{ + if (selectedPOI && oldQueryPoint) { + // TODO NOW how to use the POI as either start or destination? + // Might be too unintuitive if it relies on with which input we searched the POIs + const queryPoint = { + ...oldQueryPoint, + queryText: selectedPOI?.name, + coordinate: selectedPOI?.coordinate, + isInitialized: true, + } + Dispatcher.dispatch(new SetPoint(queryPoint, false)) + Dispatcher.dispatch(new SelectPOI(null)) + Dispatcher.dispatch(new SetPOIs([], null)) + } + }} + > + {oldQueryPoint && } + {tr('Use in route')} +
{Object.keys(kv).length == 0 && {tr('Fetching more info...')}}
-
- {oldQueryPoint && } - { - if (selectedPOI && oldQueryPoint) { - // TODO NOW how to use the POI as either start or destination? - // Might be too unintuitive if it relies on with which input we searched the POIs - const queryPoint = { - ...oldQueryPoint, - queryText: selectedPOI?.name, - coordinate: selectedPOI?.coordinate, - isInitialized: true, - } - Dispatcher.dispatch(new SetPoint(queryPoint, false)) - Dispatcher.dispatch(new SelectPOI(null)) - Dispatcher.dispatch(new SetPOIs([], null)) - } - }} - > - {tr('Use in route')} - -
) diff --git a/src/pois/AddressParseResult.ts b/src/pois/AddressParseResult.ts index ac9afb9c..68d8ee16 100644 --- a/src/pois/AddressParseResult.ts +++ b/src/pois/AddressParseResult.ts @@ -98,10 +98,11 @@ export class AddressParseResult { // because of the static method we need to inject the Translation object as otherwise jest has a problem static setPOITriggerPhrases(translation: Translation) { - const t = (s: string) => translation - .get(s) - .split(',') - .map(s => s.trim().toLowerCase()) + const t = (s: string) => + translation + .get(s) + .split(',') + .map(s => s.trim().toLowerCase()) AddressParseResult.REMOVE_VALUES = t('poi_removal_words') AddressParseResult.TRIGGER_VALUES = [ { k: 'poi_airports', t: ['aeroway:aerodrome'], i: 'flight_takeoff' }, From 7263c6536b20185bc8101ad248136844a73e69f6 Mon Sep 17 00:00:00 2001 From: Peter Date: Tue, 4 Jun 2024 14:29:31 +0200 Subject: [PATCH 15/33] one more exclusion --- src/layers/POIPopup.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/layers/POIPopup.tsx b/src/layers/POIPopup.tsx index 425048e0..ec1b174b 100644 --- a/src/layers/POIPopup.tsx +++ b/src/layers/POIPopup.tsx @@ -54,6 +54,7 @@ function KVTable(props: { kv: TagHash; poi: POI | null }) { !poiInfoRepeated && key !== 'source' && key !== 'image' && + key !== 'check_data' && !key.includes('fax') && !key.startsWith('addr') && !key.startsWith('name') && From a92f9ed62eef7cf39ee7992377f31790e2b678e7 Mon Sep 17 00:00:00 2001 From: Peter Date: Tue, 4 Jun 2024 17:20:36 +0200 Subject: [PATCH 16/33] added a few more POIs --- README.md | 1 + src/api/Api.ts | 2 +- src/layers/UsePOIsLayer.tsx | 13 + src/pois/AddressParseResult.ts | 18 +- src/pois/img/charger.svg | 1 + src/pois/img/home_and_garden.svg | 1 + src/pois/img/local_atm.svg | 1 + src/pois/img/local_gas_station.svg | 1 + src/pois/img/local_post_office.svg | 1 + src/pois/img/police.svg | 1 + src/pois/img/water_drop.svg | 1 + src/translation/tr.json | 480 ++++++++++++++++++++++++++--- 12 files changed, 470 insertions(+), 51 deletions(-) create mode 100644 src/pois/img/charger.svg create mode 100644 src/pois/img/home_and_garden.svg create mode 100644 src/pois/img/local_atm.svg create mode 100644 src/pois/img/local_gas_station.svg create mode 100644 src/pois/img/local_post_office.svg create mode 100644 src/pois/img/police.svg create mode 100644 src/pois/img/water_drop.svg diff --git a/README.md b/README.md index 9d65d099..e41d19c7 100644 --- a/README.md +++ b/README.md @@ -54,3 +54,4 @@ This project uses * the [codemirror](https://codemirror.net/) code editor for the custom model editor. * many icons from Google's [open source font library](https://fonts.google.com/icons). * many more open source projects - see the package.json + diff --git a/src/api/Api.ts b/src/api/Api.ts index 59ed5864..3856b20b 100644 --- a/src/api/Api.ts +++ b/src/api/Api.ts @@ -146,7 +146,7 @@ export class ApiImpl implements Api { if (tags) { for (const tag of tags) { - url.searchParams.append('osm_tag', tag.k + ':' + tag.v) + url.searchParams.append('osm_tag', tag.k + (tag.v ? ':' + tag.v : '')) } } diff --git a/src/layers/UsePOIsLayer.tsx b/src/layers/UsePOIsLayer.tsx index fa97764a..8d33a0ea 100644 --- a/src/layers/UsePOIsLayer.tsx +++ b/src/layers/UsePOIsLayer.tsx @@ -19,7 +19,13 @@ import school_svg from '/src/pois/img/school.svg' import sports_handball_svg from '/src/pois/img/sports_handball.svg' import store_svg from '/src/pois/img/store.svg' import train_svg from '/src/pois/img/train.svg' +import home_and_garden from '/src/pois/img/home_and_garden.svg' import universal_currency_alt_svg from '/src/pois/img/universal_currency_alt.svg' +import local_atm from '/src/pois/img/local_atm.svg' +import local_gas_station from '/src/pois/img/local_gas_station.svg' +import local_post_office from '/src/pois/img/local_post_office.svg' +import police from '/src/pois/img/police.svg' +import charger from '/src/pois/img/charger.svg' import { createPOIMarker } from '@/layers/createMarkerSVG' import { Select } from 'ol/interaction' import Dispatcher from '@/stores/Dispatcher' @@ -41,7 +47,14 @@ const svgObjects: { [id: string]: any } = { store: store_svg(), train: train_svg(), universal_currency_alt: universal_currency_alt_svg(), + home_and_garden: home_and_garden(), + local_atm: local_atm(), + local_gas_station: local_gas_station(), + local_post_office: local_post_office(), + police: police(), + charger: charger(), } + // -300 -1260 1560 1560 // for (const k in svgObjects) { diff --git a/src/pois/AddressParseResult.ts b/src/pois/AddressParseResult.ts index 68d8ee16..320d1839 100644 --- a/src/pois/AddressParseResult.ts +++ b/src/pois/AddressParseResult.ts @@ -106,9 +106,11 @@ export class AddressParseResult { AddressParseResult.REMOVE_VALUES = t('poi_removal_words') AddressParseResult.TRIGGER_VALUES = [ { k: 'poi_airports', t: ['aeroway:aerodrome'], i: 'flight_takeoff' }, + { k: 'poi_atm', t: ['amenity:atm', 'amenity:bank'], i: 'local_atm' }, { k: 'poi_banks', t: ['amenity:bank'], i: 'universal_currency_alt' }, { k: 'poi_bus_stops', t: ['highway:bus_stop'], i: 'train' }, { k: 'poi_education', t: ['amenity:school', 'building:school', 'building:university'], i: 'school' }, + { k: 'poi_gas_station', t: ['amenity:fuel'], i: 'local_gas_station' }, { k: 'poi_hospitals', t: ['amenity:hospital', 'building:hospital'], i: 'local_hospital' }, { k: 'poi_hotels', t: ['amenity:hotel', 'building:hotel', 'tourism:hotel'], i: 'hotel' }, { k: 'poi_leisure', t: ['leisure'], i: 'sports_handball' }, @@ -117,12 +119,24 @@ export class AddressParseResult { { k: 'poi_parks', t: ['leisure:park'], i: 'sports_handball' }, { k: 'poi_pharmacies', t: ['amenity:pharmacy'], i: 'local_pharmacy' }, { k: 'poi_playgrounds', t: ['leisure:playground'], i: 'sports_handball' }, - { k: 'poi_public_transit', t: ['railway:station', 'highway:bus_stop'], i: 'train' }, - { k: 'poi_railway_station', t: ['railway:station'], i: 'train' }, + { k: 'poi_police', t: ['amenity:police '], i: 'police' }, + // important to have this before "post" + { + k: 'poi_post_box', + t: ['amenity:post_box', 'amenity:post_office', 'amenity:post_depot'], + i: 'local_post_office', + }, + { k: 'poi_post', t: ['amenity:post_office', 'amenity:post_depot'], i: 'local_post_office' }, + { k: 'poi_public_transit', t: ['public_transport:station', 'highway:bus_stop'], i: 'train' }, + { k: 'poi_railway_station', t: ['public_transport:station'], i: 'train' }, { k: 'poi_restaurants', t: ['amenity:restaurant'], i: 'restaurant' }, { k: 'poi_schools', t: ['amenity:school', 'building:school'], i: 'school' }, + { k: 'poi_shopping', t: ['shop'], i: 'store' }, { k: 'poi_super_markets', t: ['shop:supermarket', 'building:supermarket'], i: 'store' }, + { k: 'poi_toilets', t: ['amenity:toilets'], i: 'home_and_garden' }, { k: 'poi_tourism', t: ['tourism'], i: 'luggage' }, + { k: 'poi_water', t: ['amenity:drinking_water '], i: 'water_drop' }, + { k: 'poi_charging_station', t: ['amenity:charging_station'], i: 'charger' }, ].map(v => { const tags = v.t.map(val => { return { k: val.split(':')[0], v: val.split(':')[1] } diff --git a/src/pois/img/charger.svg b/src/pois/img/charger.svg new file mode 100644 index 00000000..889ec6f4 --- /dev/null +++ b/src/pois/img/charger.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/pois/img/home_and_garden.svg b/src/pois/img/home_and_garden.svg new file mode 100644 index 00000000..ecdbf0a5 --- /dev/null +++ b/src/pois/img/home_and_garden.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/pois/img/local_atm.svg b/src/pois/img/local_atm.svg new file mode 100644 index 00000000..64126bee --- /dev/null +++ b/src/pois/img/local_atm.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/pois/img/local_gas_station.svg b/src/pois/img/local_gas_station.svg new file mode 100644 index 00000000..8a8c08c8 --- /dev/null +++ b/src/pois/img/local_gas_station.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/pois/img/local_post_office.svg b/src/pois/img/local_post_office.svg new file mode 100644 index 00000000..ce3ac78f --- /dev/null +++ b/src/pois/img/local_post_office.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/pois/img/police.svg b/src/pois/img/police.svg new file mode 100644 index 00000000..8a6acfb9 --- /dev/null +++ b/src/pois/img/police.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/pois/img/water_drop.svg b/src/pois/img/water_drop.svg new file mode 100644 index 00000000..98d0dab8 --- /dev/null +++ b/src/pois/img/water_drop.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/translation/tr.json b/src/translation/tr.json index 20dd474a..bfa0a0ca 100644 --- a/src/translation/tr.json +++ b/src/translation/tr.json @@ -128,7 +128,15 @@ "poi_restaurants":"restaurants, restaurant, eat", "poi_schools":"schools, school", "poi_super_markets":"super markets, super market", -"poi_tourism":"tourism" +"poi_tourism":"tourism", +"poi_police":"police", +"poi_atm":"atm", +"poi_gas_station":"gas stations, gas station, petrol stations, petrol station", +"poi_post":"post, postal office", +"poi_post_box":"mailbox, post box, letterbox, letter box", +"poi_shopping":"shops, shop, shopping", +"poi_toilets":"toilets, toilet", +"poi_charging_station":"charging stations, charging station, charger" }, "ar":{ "total_ascend":"%1$s اجمالى صعود", @@ -260,7 +268,15 @@ "poi_restaurants":"", "poi_schools":"", "poi_super_markets":"", -"poi_tourism":"" +"poi_tourism":"", +"poi_police":"", +"poi_atm":"", +"poi_gas_station":"", +"poi_post":"", +"poi_post_box":"", +"poi_shopping":"", +"poi_toilets":"", +"poi_charging_station":"" }, "ast":{ "total_ascend":"%1$s d'ascensu total", @@ -392,7 +408,15 @@ "poi_restaurants":"", "poi_schools":"", "poi_super_markets":"", -"poi_tourism":"" +"poi_tourism":"", +"poi_police":"", +"poi_atm":"", +"poi_gas_station":"", +"poi_post":"", +"poi_post_box":"", +"poi_shopping":"", +"poi_toilets":"", +"poi_charging_station":"" }, "az":{ "total_ascend":"%1$s yüksəliş", @@ -524,7 +548,15 @@ "poi_restaurants":"", "poi_schools":"", "poi_super_markets":"", -"poi_tourism":"" +"poi_tourism":"", +"poi_police":"", +"poi_atm":"", +"poi_gas_station":"", +"poi_post":"", +"poi_post_box":"", +"poi_shopping":"", +"poi_toilets":"", +"poi_charging_station":"" }, "bg":{ "total_ascend":"%1$s общо изкачване", @@ -656,7 +688,15 @@ "poi_restaurants":"", "poi_schools":"", "poi_super_markets":"", -"poi_tourism":"" +"poi_tourism":"", +"poi_police":"", +"poi_atm":"", +"poi_gas_station":"", +"poi_post":"", +"poi_post_box":"", +"poi_shopping":"", +"poi_toilets":"", +"poi_charging_station":"" }, "bn_BN":{ "total_ascend":"", @@ -788,7 +828,15 @@ "poi_restaurants":"", "poi_schools":"", "poi_super_markets":"", -"poi_tourism":"" +"poi_tourism":"", +"poi_police":"", +"poi_atm":"", +"poi_gas_station":"", +"poi_post":"", +"poi_post_box":"", +"poi_shopping":"", +"poi_toilets":"", +"poi_charging_station":"" }, "ca":{ "total_ascend":"%1$s de pujada total", @@ -920,7 +968,15 @@ "poi_restaurants":"", "poi_schools":"", "poi_super_markets":"", -"poi_tourism":"" +"poi_tourism":"", +"poi_police":"", +"poi_atm":"", +"poi_gas_station":"", +"poi_post":"", +"poi_post_box":"", +"poi_shopping":"", +"poi_toilets":"", +"poi_charging_station":"" }, "cs_CZ":{ "total_ascend":"celkové stoupání %1$s", @@ -1052,7 +1108,15 @@ "poi_restaurants":"", "poi_schools":"", "poi_super_markets":"", -"poi_tourism":"" +"poi_tourism":"", +"poi_police":"", +"poi_atm":"", +"poi_gas_station":"", +"poi_post":"", +"poi_post_box":"", +"poi_shopping":"", +"poi_toilets":"", +"poi_charging_station":"" }, "da_DK":{ "total_ascend":"%1$s samlet stigning", @@ -1184,7 +1248,15 @@ "poi_restaurants":"", "poi_schools":"", "poi_super_markets":"", -"poi_tourism":"" +"poi_tourism":"", +"poi_police":"", +"poi_atm":"", +"poi_gas_station":"", +"poi_post":"", +"poi_post_box":"", +"poi_shopping":"", +"poi_toilets":"", +"poi_charging_station":"" }, "de_DE":{ "total_ascend":"%1$s Gesamtaufstieg", @@ -1316,7 +1388,15 @@ "poi_restaurants":"Restaurants, Restaurant", "poi_schools":"Schulen, Schule", "poi_super_markets":"Supermärkte, Supermarkt, Einkauf, Einkaufsladen", -"poi_tourism":"Tourismus, Fremdenverkehr, Touristik" +"poi_tourism":"Tourismus, Fremdenverkehr, Touristik", +"poi_police":"Polizei", +"poi_atm":"Geldautomaten, Geldautomat", +"poi_gas_station":"Tankstellen, Tankstelle", +"poi_post":"Post, Postamt, Poststelle", +"poi_post_box":"Briefkästen, Briefkasten", +"poi_shopping":"Einkaufen, Einkauf", +"poi_toilets":"Toiletten, Toilette, WC", +"poi_charging_station":"Ladestation, Ladesäulen, Ladesäule" }, "el":{ "total_ascend":"%1$s συνολική ανάβαση", @@ -1448,7 +1528,15 @@ "poi_restaurants":"", "poi_schools":"", "poi_super_markets":"", -"poi_tourism":"" +"poi_tourism":"", +"poi_police":"", +"poi_atm":"", +"poi_gas_station":"", +"poi_post":"", +"poi_post_box":"", +"poi_shopping":"", +"poi_toilets":"", +"poi_charging_station":"" }, "eo":{ "total_ascend":"%1$s supreniro tute", @@ -1580,7 +1668,15 @@ "poi_restaurants":"", "poi_schools":"", "poi_super_markets":"", -"poi_tourism":"" +"poi_tourism":"", +"poi_police":"", +"poi_atm":"", +"poi_gas_station":"", +"poi_post":"", +"poi_post_box":"", +"poi_shopping":"", +"poi_toilets":"", +"poi_charging_station":"" }, "es":{ "total_ascend":"Ascender %1$s en total", @@ -1712,7 +1808,15 @@ "poi_restaurants":"", "poi_schools":"", "poi_super_markets":"", -"poi_tourism":"" +"poi_tourism":"", +"poi_police":"", +"poi_atm":"", +"poi_gas_station":"", +"poi_post":"", +"poi_post_box":"", +"poi_shopping":"", +"poi_toilets":"", +"poi_charging_station":"" }, "fa":{ "total_ascend":"مجموع صعود %1$s", @@ -1844,7 +1948,15 @@ "poi_restaurants":"", "poi_schools":"", "poi_super_markets":"", -"poi_tourism":"" +"poi_tourism":"", +"poi_police":"", +"poi_atm":"", +"poi_gas_station":"", +"poi_post":"", +"poi_post_box":"", +"poi_shopping":"", +"poi_toilets":"", +"poi_charging_station":"" }, "fil":{ "total_ascend":"", @@ -1976,7 +2088,15 @@ "poi_restaurants":"", "poi_schools":"", "poi_super_markets":"", -"poi_tourism":"" +"poi_tourism":"", +"poi_police":"", +"poi_atm":"", +"poi_gas_station":"", +"poi_post":"", +"poi_post_box":"", +"poi_shopping":"", +"poi_toilets":"", +"poi_charging_station":"" }, "fi":{ "total_ascend":"nousu yhteensä %1$s", @@ -2108,7 +2228,15 @@ "poi_restaurants":"", "poi_schools":"", "poi_super_markets":"", -"poi_tourism":"" +"poi_tourism":"", +"poi_police":"", +"poi_atm":"", +"poi_gas_station":"", +"poi_post":"", +"poi_post_box":"", +"poi_shopping":"", +"poi_toilets":"", +"poi_charging_station":"" }, "fr_FR":{ "total_ascend":"%1$s de dénivelé positif", @@ -2240,7 +2368,15 @@ "poi_restaurants":"", "poi_schools":"", "poi_super_markets":"", -"poi_tourism":"" +"poi_tourism":"", +"poi_police":"", +"poi_atm":"", +"poi_gas_station":"", +"poi_post":"", +"poi_post_box":"", +"poi_shopping":"", +"poi_toilets":"", +"poi_charging_station":"" }, "fr_CH":{ "total_ascend":"%1$s de dénivelé positif", @@ -2372,7 +2508,15 @@ "poi_restaurants":"", "poi_schools":"", "poi_super_markets":"", -"poi_tourism":"" +"poi_tourism":"", +"poi_police":"", +"poi_atm":"", +"poi_gas_station":"", +"poi_post":"", +"poi_post_box":"", +"poi_shopping":"", +"poi_toilets":"", +"poi_charging_station":"" }, "gl":{ "total_ascend":"", @@ -2504,7 +2648,15 @@ "poi_restaurants":"", "poi_schools":"", "poi_super_markets":"", -"poi_tourism":"" +"poi_tourism":"", +"poi_police":"", +"poi_atm":"", +"poi_gas_station":"", +"poi_post":"", +"poi_post_box":"", +"poi_shopping":"", +"poi_toilets":"", +"poi_charging_station":"" }, "he":{ "total_ascend":"עלייה כוללת של %1$s", @@ -2636,7 +2788,15 @@ "poi_restaurants":"", "poi_schools":"", "poi_super_markets":"", -"poi_tourism":"" +"poi_tourism":"", +"poi_police":"", +"poi_atm":"", +"poi_gas_station":"", +"poi_post":"", +"poi_post_box":"", +"poi_shopping":"", +"poi_toilets":"", +"poi_charging_station":"" }, "hr_HR":{ "total_ascend":"%1$s ukupni uspon", @@ -2768,7 +2928,15 @@ "poi_restaurants":"", "poi_schools":"", "poi_super_markets":"", -"poi_tourism":"" +"poi_tourism":"", +"poi_police":"", +"poi_atm":"", +"poi_gas_station":"", +"poi_post":"", +"poi_post_box":"", +"poi_shopping":"", +"poi_toilets":"", +"poi_charging_station":"" }, "hsb":{ "total_ascend":"", @@ -2900,7 +3068,15 @@ "poi_restaurants":"", "poi_schools":"", "poi_super_markets":"", -"poi_tourism":"" +"poi_tourism":"", +"poi_police":"", +"poi_atm":"", +"poi_gas_station":"", +"poi_post":"", +"poi_post_box":"", +"poi_shopping":"", +"poi_toilets":"", +"poi_charging_station":"" }, "hu_HU":{ "total_ascend":"Összes szintemelkedés: %1$s", @@ -3032,7 +3208,15 @@ "poi_restaurants":"", "poi_schools":"", "poi_super_markets":"", -"poi_tourism":"" +"poi_tourism":"", +"poi_police":"", +"poi_atm":"", +"poi_gas_station":"", +"poi_post":"", +"poi_post_box":"", +"poi_shopping":"", +"poi_toilets":"", +"poi_charging_station":"" }, "in_ID":{ "total_ascend":"naik dengan jarak %1$s", @@ -3164,7 +3348,15 @@ "poi_restaurants":"", "poi_schools":"", "poi_super_markets":"", -"poi_tourism":"" +"poi_tourism":"", +"poi_police":"", +"poi_atm":"", +"poi_gas_station":"", +"poi_post":"", +"poi_post_box":"", +"poi_shopping":"", +"poi_toilets":"", +"poi_charging_station":"" }, "it":{ "total_ascend":"%1$s di dislivello positivo", @@ -3296,7 +3488,15 @@ "poi_restaurants":"", "poi_schools":"", "poi_super_markets":"", -"poi_tourism":"" +"poi_tourism":"", +"poi_police":"", +"poi_atm":"", +"poi_gas_station":"", +"poi_post":"", +"poi_post_box":"", +"poi_shopping":"", +"poi_toilets":"", +"poi_charging_station":"" }, "ja":{ "total_ascend":"", @@ -3428,7 +3628,15 @@ "poi_restaurants":"", "poi_schools":"", "poi_super_markets":"", -"poi_tourism":"" +"poi_tourism":"", +"poi_police":"", +"poi_atm":"", +"poi_gas_station":"", +"poi_post":"", +"poi_post_box":"", +"poi_shopping":"", +"poi_toilets":"", +"poi_charging_station":"" }, "ko":{ "total_ascend":"오르막길 총 %1$s", @@ -3560,7 +3768,15 @@ "poi_restaurants":"", "poi_schools":"", "poi_super_markets":"", -"poi_tourism":"" +"poi_tourism":"", +"poi_police":"", +"poi_atm":"", +"poi_gas_station":"", +"poi_post":"", +"poi_post_box":"", +"poi_shopping":"", +"poi_toilets":"", +"poi_charging_station":"" }, "kz":{ "total_ascend":"%1$s көтерілу", @@ -3692,7 +3908,15 @@ "poi_restaurants":"", "poi_schools":"", "poi_super_markets":"", -"poi_tourism":"" +"poi_tourism":"", +"poi_police":"", +"poi_atm":"", +"poi_gas_station":"", +"poi_post":"", +"poi_post_box":"", +"poi_shopping":"", +"poi_toilets":"", +"poi_charging_station":"" }, "lt_LT":{ "total_ascend":"", @@ -3824,7 +4048,15 @@ "poi_restaurants":"", "poi_schools":"", "poi_super_markets":"", -"poi_tourism":"" +"poi_tourism":"", +"poi_police":"", +"poi_atm":"", +"poi_gas_station":"", +"poi_post":"", +"poi_post_box":"", +"poi_shopping":"", +"poi_toilets":"", +"poi_charging_station":"" }, "nb_NO":{ "total_ascend":"%1$s totale høydemeter", @@ -3956,7 +4188,15 @@ "poi_restaurants":"", "poi_schools":"", "poi_super_markets":"", -"poi_tourism":"" +"poi_tourism":"", +"poi_police":"", +"poi_atm":"", +"poi_gas_station":"", +"poi_post":"", +"poi_post_box":"", +"poi_shopping":"", +"poi_toilets":"", +"poi_charging_station":"" }, "ne":{ "total_ascend":"", @@ -4088,7 +4328,15 @@ "poi_restaurants":"", "poi_schools":"", "poi_super_markets":"", -"poi_tourism":"" +"poi_tourism":"", +"poi_police":"", +"poi_atm":"", +"poi_gas_station":"", +"poi_post":"", +"poi_post_box":"", +"poi_shopping":"", +"poi_toilets":"", +"poi_charging_station":"" }, "nl":{ "total_ascend":"%1$s totale klim", @@ -4220,7 +4468,15 @@ "poi_restaurants":"", "poi_schools":"", "poi_super_markets":"", -"poi_tourism":"" +"poi_tourism":"", +"poi_police":"", +"poi_atm":"", +"poi_gas_station":"", +"poi_post":"", +"poi_post_box":"", +"poi_shopping":"", +"poi_toilets":"", +"poi_charging_station":"" }, "pl_PL":{ "total_ascend":"%1$s w górę", @@ -4352,7 +4608,15 @@ "poi_restaurants":"", "poi_schools":"", "poi_super_markets":"", -"poi_tourism":"" +"poi_tourism":"", +"poi_police":"", +"poi_atm":"", +"poi_gas_station":"", +"poi_post":"", +"poi_post_box":"", +"poi_shopping":"", +"poi_toilets":"", +"poi_charging_station":"" }, "pt_BR":{ "total_ascend":"subida de %1$s", @@ -4484,7 +4748,15 @@ "poi_restaurants":"", "poi_schools":"", "poi_super_markets":"", -"poi_tourism":"" +"poi_tourism":"", +"poi_police":"", +"poi_atm":"", +"poi_gas_station":"", +"poi_post":"", +"poi_post_box":"", +"poi_shopping":"", +"poi_toilets":"", +"poi_charging_station":"" }, "pt_PT":{ "total_ascend":"subida de %1$s", @@ -4616,7 +4888,15 @@ "poi_restaurants":"", "poi_schools":"", "poi_super_markets":"", -"poi_tourism":"" +"poi_tourism":"", +"poi_police":"", +"poi_atm":"", +"poi_gas_station":"", +"poi_post":"", +"poi_post_box":"", +"poi_shopping":"", +"poi_toilets":"", +"poi_charging_station":"" }, "ro":{ "total_ascend":"urcare %1$s", @@ -4748,7 +5028,15 @@ "poi_restaurants":"", "poi_schools":"", "poi_super_markets":"", -"poi_tourism":"" +"poi_tourism":"", +"poi_police":"", +"poi_atm":"", +"poi_gas_station":"", +"poi_post":"", +"poi_post_box":"", +"poi_shopping":"", +"poi_toilets":"", +"poi_charging_station":"" }, "ru":{ "total_ascend":"подъём на %1$s", @@ -4880,7 +5168,15 @@ "poi_restaurants":"", "poi_schools":"", "poi_super_markets":"", -"poi_tourism":"" +"poi_tourism":"", +"poi_police":"", +"poi_atm":"", +"poi_gas_station":"", +"poi_post":"", +"poi_post_box":"", +"poi_shopping":"", +"poi_toilets":"", +"poi_charging_station":"" }, "sk":{ "total_ascend":"%1$s celkové stúpanie", @@ -5012,7 +5308,15 @@ "poi_restaurants":"", "poi_schools":"", "poi_super_markets":"", -"poi_tourism":"" +"poi_tourism":"", +"poi_police":"", +"poi_atm":"", +"poi_gas_station":"", +"poi_post":"", +"poi_post_box":"", +"poi_shopping":"", +"poi_toilets":"", +"poi_charging_station":"" }, "sl_SI":{ "total_ascend":"Skupni vzpon: %1$s", @@ -5144,7 +5448,15 @@ "poi_restaurants":"", "poi_schools":"", "poi_super_markets":"", -"poi_tourism":"" +"poi_tourism":"", +"poi_police":"", +"poi_atm":"", +"poi_gas_station":"", +"poi_post":"", +"poi_post_box":"", +"poi_shopping":"", +"poi_toilets":"", +"poi_charging_station":"" }, "sr_RS":{ "total_ascend":"%1$s ukupni uspon", @@ -5276,7 +5588,15 @@ "poi_restaurants":"", "poi_schools":"", "poi_super_markets":"", -"poi_tourism":"" +"poi_tourism":"", +"poi_police":"", +"poi_atm":"", +"poi_gas_station":"", +"poi_post":"", +"poi_post_box":"", +"poi_shopping":"", +"poi_toilets":"", +"poi_charging_station":"" }, "sv_SE":{ "total_ascend":"%1$s stigning", @@ -5408,7 +5728,15 @@ "poi_restaurants":"", "poi_schools":"", "poi_super_markets":"", -"poi_tourism":"" +"poi_tourism":"", +"poi_police":"", +"poi_atm":"", +"poi_gas_station":"", +"poi_post":"", +"poi_post_box":"", +"poi_shopping":"", +"poi_toilets":"", +"poi_charging_station":"" }, "tr":{ "total_ascend":"%1$s toplam tırmanış", @@ -5540,7 +5868,15 @@ "poi_restaurants":"", "poi_schools":"", "poi_super_markets":"", -"poi_tourism":"" +"poi_tourism":"", +"poi_police":"", +"poi_atm":"", +"poi_gas_station":"", +"poi_post":"", +"poi_post_box":"", +"poi_shopping":"", +"poi_toilets":"", +"poi_charging_station":"" }, "uk":{ "total_ascend":"%1$s загалом підйому", @@ -5672,7 +6008,15 @@ "poi_restaurants":"", "poi_schools":"", "poi_super_markets":"", -"poi_tourism":"" +"poi_tourism":"", +"poi_police":"", +"poi_atm":"", +"poi_gas_station":"", +"poi_post":"", +"poi_post_box":"", +"poi_shopping":"", +"poi_toilets":"", +"poi_charging_station":"" }, "uz":{ "total_ascend":"%1$s ga ko'tarilish", @@ -5804,7 +6148,15 @@ "poi_restaurants":"", "poi_schools":"", "poi_super_markets":"", -"poi_tourism":"" +"poi_tourism":"", +"poi_police":"", +"poi_atm":"", +"poi_gas_station":"", +"poi_post":"", +"poi_post_box":"", +"poi_shopping":"", +"poi_toilets":"", +"poi_charging_station":"" }, "vi_VN":{ "total_ascend":"Đi tiếp %1$s nữa", @@ -5936,7 +6288,15 @@ "poi_restaurants":"", "poi_schools":"", "poi_super_markets":"", -"poi_tourism":"" +"poi_tourism":"", +"poi_police":"", +"poi_atm":"", +"poi_gas_station":"", +"poi_post":"", +"poi_post_box":"", +"poi_shopping":"", +"poi_toilets":"", +"poi_charging_station":"" }, "zh_CN":{ "total_ascend":"总上升 %1$s", @@ -6068,7 +6428,15 @@ "poi_restaurants":"", "poi_schools":"", "poi_super_markets":"", -"poi_tourism":"" +"poi_tourism":"", +"poi_police":"", +"poi_atm":"", +"poi_gas_station":"", +"poi_post":"", +"poi_post_box":"", +"poi_shopping":"", +"poi_toilets":"", +"poi_charging_station":"" }, "zh_HK":{ "total_ascend":"總共上昇 %1$s", @@ -6200,7 +6568,15 @@ "poi_restaurants":"", "poi_schools":"", "poi_super_markets":"", -"poi_tourism":"" +"poi_tourism":"", +"poi_police":"", +"poi_atm":"", +"poi_gas_station":"", +"poi_post":"", +"poi_post_box":"", +"poi_shopping":"", +"poi_toilets":"", +"poi_charging_station":"" }, "zh_TW":{ "total_ascend":"總共上昇 %1$s", @@ -6332,5 +6708,13 @@ "poi_restaurants":"", "poi_schools":"", "poi_super_markets":"", -"poi_tourism":"" +"poi_tourism":"", +"poi_police":"", +"poi_atm":"", +"poi_gas_station":"", +"poi_post":"", +"poi_post_box":"", +"poi_shopping":"", +"poi_toilets":"", +"poi_charging_station":"" }} \ No newline at end of file From 6c9bec67ce10612bd6447ad448e25267b85fb947 Mon Sep 17 00:00:00 2001 From: Peter Date: Tue, 4 Jun 2024 17:41:20 +0200 Subject: [PATCH 17/33] photon does not know public_transport:station (?) --- src/pois/AddressParseResult.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pois/AddressParseResult.ts b/src/pois/AddressParseResult.ts index 320d1839..3dd5362e 100644 --- a/src/pois/AddressParseResult.ts +++ b/src/pois/AddressParseResult.ts @@ -127,8 +127,9 @@ export class AddressParseResult { i: 'local_post_office', }, { k: 'poi_post', t: ['amenity:post_office', 'amenity:post_depot'], i: 'local_post_office' }, - { k: 'poi_public_transit', t: ['public_transport:station', 'highway:bus_stop'], i: 'train' }, - { k: 'poi_railway_station', t: ['public_transport:station'], i: 'train' }, + // TODO NOW public_transport:station does not seem to be supported from photon? + { k: 'poi_public_transit', t: ['railway:station', 'highway:bus_stop'], i: 'train' }, + { k: 'poi_railway_station', t: ['railway:station'], i: 'train' }, { k: 'poi_restaurants', t: ['amenity:restaurant'], i: 'restaurant' }, { k: 'poi_schools', t: ['amenity:school', 'building:school'], i: 'school' }, { k: 'poi_shopping', t: ['shop'], i: 'store' }, From 50a73fc1d4556e751a73e38f09ac9165dba4d05a Mon Sep 17 00:00:00 2001 From: Peter Date: Sat, 15 Jun 2024 21:57:24 +0200 Subject: [PATCH 18/33] rename --- src/Converters.ts | 1 - src/actions/Actions.ts | 6 +++--- src/stores/POIsStore.ts | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Converters.ts b/src/Converters.ts index 6a2f0ac1..6d0e1be4 100644 --- a/src/Converters.ts +++ b/src/Converters.ts @@ -1,7 +1,6 @@ import { GeocodingHit } from '@/api/graphhopper' import { Coordinate } from '@/stores/QueryStore' -import { Translation } from '@/translation/Translation' export function milliSecondsToText(ms: number) { const hours = Math.floor(ms / 3600000) diff --git a/src/actions/Actions.ts b/src/actions/Actions.ts index 1aeefb4f..212efbf3 100644 --- a/src/actions/Actions.ts +++ b/src/actions/Actions.ts @@ -258,10 +258,10 @@ export class SelectPOI implements Action { export class SetPOIs implements Action { readonly pois: POI[] - readonly oldQueryPoint: QueryPoint | null + readonly sourceQueryPoint: QueryPoint | null - constructor(pois: POI[], oldQueryPoint: QueryPoint | null) { + constructor(pois: POI[], sourceQueryPoint: QueryPoint | null) { this.pois = pois - this.oldQueryPoint = oldQueryPoint + this.sourceQueryPoint = sourceQueryPoint } } diff --git a/src/stores/POIsStore.ts b/src/stores/POIsStore.ts index 30c23a13..9968d94e 100644 --- a/src/stores/POIsStore.ts +++ b/src/stores/POIsStore.ts @@ -29,7 +29,7 @@ export default class POIsStore extends Store { if (action instanceof SetPOIs) { return { pois: action.pois, - oldQueryPoint: action.oldQueryPoint, + oldQueryPoint: action.sourceQueryPoint, selected: null, } } else if (action instanceof SelectPOI) { From 7da457841399d4500ee85264158789496baacf2b Mon Sep 17 00:00:00 2001 From: Peter Date: Sun, 7 Jul 2024 11:34:58 +0200 Subject: [PATCH 19/33] use overpass API --- src/NavBar.ts | 4 +- src/api/Api.ts | 100 ++++++++++++++++++---------- src/api/graphhopper.d.ts | 15 ++++- src/layers/POIPopup.tsx | 47 +++---------- src/pois/AddressParseResult.ts | 68 ++++++++++--------- src/sidebar/search/AddressInput.tsx | 43 ++++++------ src/stores/POIsStore.ts | 6 +- test/DummyApi.ts | 25 ++++--- test/stores/QueryStore.test.ts | 19 +++--- test/stores/RouteStore.test.ts | 19 +++--- 10 files changed, 186 insertions(+), 160 deletions(-) diff --git a/src/NavBar.ts b/src/NavBar.ts index 3af009a5..7bde1f94 100644 --- a/src/NavBar.ts +++ b/src/NavBar.ts @@ -118,8 +118,8 @@ export default class NavBar { .then(res => { if (res.hits.length != 0) getApi() - .reverseGeocode('', res.hits[0].point, 100, result.tags) - .then(res => AddressParseResult.handleGeocodingResponse(res.hits, result, p)) + .reverseGeocode(res.hits[0].extent, result.queries) + .then(res => AddressParseResult.handleGeocodingResponse(res, result, p)) }) return Promise.resolve(p) } diff --git a/src/api/Api.ts b/src/api/Api.ts index e9631292..efcdabee 100644 --- a/src/api/Api.ts +++ b/src/api/Api.ts @@ -8,6 +8,7 @@ import { Path, RawPath, RawResult, + ReverseGeocodingHit, RoutingArgs, RoutingProfile, RoutingRequest, @@ -17,7 +18,7 @@ import { LineString } from 'geojson' import { getTranslation, tr } from '@/translation/Translation' import * as config from 'config' import { Coordinate } from '@/stores/QueryStore' -import { KV } from '@/pois/AddressParseResult' +import { POIQuery } from '@/pois/AddressParseResult' interface ApiProfile { name: string @@ -32,7 +33,7 @@ export default interface Api { geocode(query: string, provider: string, additionalOptions?: Record): Promise - reverseGeocode(query: string | undefined, point: Coordinate, radius: number, tags?: KV[]): Promise + reverseGeocode(bbox: Bbox, queries: POIQuery[]): Promise supportsGeocoding(): boolean } @@ -121,43 +122,74 @@ export class ApiImpl implements Api { } } - async reverseGeocode( - query: string | undefined, - point: Coordinate, - radius: number, - tags?: KV[] - ): Promise { - if (!this.supportsGeocoding()) - return { - hits: [], - took: 0, - } - const url = this.getGeocodingURLWithKey('geocode') - - url.searchParams.append('point', point.lat + ',' + point.lng) - url.searchParams.append('radius', '' + radius) - url.searchParams.append('reverse', 'true') - url.searchParams.append('limit', '50') - - if (query) url.searchParams.append('q', query) + async reverseGeocode(bbox: Bbox, queries: POIQuery[]): Promise { + if (!this.supportsGeocoding()) return [] + // why is main overpass api so much faster for certain queries like "restaurants berlin" + // const url = 'https://overpass.kumi.systems/api/interpreter' + const url = 'https://overpass-api.de/api/interpreter' - const langAndCountry = getTranslation().getLang().split('_') - url.searchParams.append('locale', langAndCountry.length > 0 ? langAndCountry[0] : 'en') + // bbox of overpass is minLat, minLon, maxLat, maxLon + let minLat = bbox[1], minLon = bbox[0], maxLat = bbox[3], maxLon = bbox[2] - if (tags) { - for (const tag of tags) { - url.searchParams.append('osm_tag', tag.k + (tag.v ? ':' + tag.v : '')) - } + // reduce bbox to improve overpass response time + if(maxLat-minLat > 0.2) { + const centerLat = (maxLat + minLat) / 2 + maxLat = centerLat + 0.1 + minLat = centerLat - 0.1 + } + if(maxLon - minLon > 0.2) { + const centerLon = (maxLon + minLon) / 2 + maxLon = centerLon + 0.1 + minLon = centerLon - 0.1 } - const response = await fetch(url.toString(), { - headers: { Accept: 'application/json' }, - }) + // nw means it searches for nodes and ways + let query = '' + for (const tag of queries) { + const value = tag.v ? `="${tag.v}"` : '' + query += `nw["${tag.k}"${value}];` + } - if (response.ok) { - return (await response.json()) as GeocodingResult - } else { - throw new Error('Geocoding went wrong ' + response.status) + try { + // (._;>;); => means it fetches the coordinates for ways. From this we create an index and calculate the center point + // Although this is ugly I did not find a faster way e.g. out geom or out center are all slower + const data = `[out:json][timeout:15][bbox:${minLat}, ${minLon}, ${maxLat}, ${maxLon}];${query}(._;>;);out 50;` + console.log(data) + const result = await fetch(url, { + method: 'POST', + body: 'data=' + encodeURIComponent(data), + }) + const json = await result.json() + if (json.elements) { + const elements = json.elements as any[] + const index: { [key: number]: any } = {} + elements.forEach(e => (index[e.id] = e)) + const res = elements + .map(e => { + if (e.nodes) { + const coords = e.nodes. + map((n: number) => (index[n] ? { lat: index[n].lat, lng: index[n].lon } : {})). + filter((c: Coordinate) => c.lat) + console.log(coords) + // minLon, minLat, maxLon, maxLat + const bbox = ApiImpl.getBBoxPoints(coords) + return bbox + ? ({ + ...e, + point: { lat: (bbox[1] + bbox[3]) / 2, lng: (bbox[0] + bbox[2]) / 2 }, + } as ReverseGeocodingHit) + : e + } else { + return { ...e, point: { lat: e.lat, lng: e.lon } } as ReverseGeocodingHit + } + }) + .filter(p => !!p.tags && p.point) + console.log(res) + return res + } else return [] + } catch (error) { + console.warn('error occured ' + error) + return [] } } diff --git a/src/api/graphhopper.d.ts b/src/api/graphhopper.d.ts index fd7a5229..d75670bb 100644 --- a/src/api/graphhopper.d.ts +++ b/src/api/graphhopper.d.ts @@ -1,5 +1,5 @@ import { LineString } from 'geojson' -import { CustomModel } from '@/stores/QueryStore' +import { Coordinate, CustomModel } from '@/stores/QueryStore' // minLon, minLat, maxLon, maxLat export type Bbox = [number, number, number, number] @@ -112,13 +112,24 @@ interface Details { readonly hike_rating: [number, number, boolean][] } +export interface TagHash { + [key: string]: string +} + +export interface ReverseGeocodingHit { + readonly tags: TagHash + readonly type: string + readonly id: number + readonly point: Coordinate +} + export interface GeocodingResult { readonly hits: GeocodingHit[] readonly took: number } export interface GeocodingHit { - readonly point: { lat: number; lng: number } + readonly point: Coordinate readonly extent: Bbox readonly osm_id: string readonly osm_type: string diff --git a/src/layers/POIPopup.tsx b/src/layers/POIPopup.tsx index ec1b174b..c3e5d0b1 100644 --- a/src/layers/POIPopup.tsx +++ b/src/layers/POIPopup.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react' +import React from 'react' import styles from '@/layers/MapFeaturePopup.module.css' import MapPopup from '@/layers/MapPopup' import { Map } from 'ol' @@ -14,32 +14,11 @@ interface POIStatePopupProps { poiState: POIsStoreState } -interface TagHash { - [key: string]: string -} - -async function fetchInfo(type: string, ids: string[]): Promise { - try { - const data = `[out:json][timeout:15]; - (${type}(id:${ids.join(',')});); - out body;` - const result = await fetch('https://overpass-api.de/api/interpreter', { - method: 'POST', - body: 'data=' + encodeURIComponent(data), - }) - const json = await result.json() - if (json.elements.length > 0) return json.elements[0].tags - else return { status: 'empty' } - } catch (error) { - return { status: '' + error } - } -} - -function KVTable(props: { kv: TagHash; poi: POI | null }) { +function POITable(props: { poi: POI }) { return (
- {Object.entries(props.kv).map(([key, value]) => { + {Object.entries(props.poi.tags).map(([key, value]) => { const url = value.startsWith('https://') || value.startsWith('http://') const tel = key.toLowerCase().includes('phone') const email = key.toLowerCase().includes('email') @@ -49,7 +28,9 @@ function KVTable(props: { kv: TagHash; poi: POI | null }) { ? 'https://' + valueArr[0] + '.wikipedia.org/wiki/' + encodeURIComponent(valueArr[1]) : '' // tags like amenity:restaurant should not be shown if it is a restaurant (determined by poi.tags) - const poiInfoRepeated = props.poi ? props.poi.tags.some(kv => kv.k == key && kv.v === value) : false + const poiInfoRepeated = props.poi.queries + ? props.poi.queries.some(q => q.k == key && q.v === value) + : false return ( !poiInfoRepeated && key !== 'source' && @@ -91,16 +72,7 @@ function KVTable(props: { kv: TagHash; poi: POI | null }) { export default function POIStatePopup({ map, poiState }: POIStatePopupProps) { const selectedPOI = poiState.selected const oldQueryPoint = poiState.oldQueryPoint - const t = selectedPOI?.osm_type - const type = t === 'W' ? 'way' : t === 'N' ? 'node' : 'relation' - const [kv, setKV] = useState({}) - - useEffect(() => { - if (selectedPOI) fetchInfo(type, [selectedPOI.osm_id]).then(tagHash => setKV(tagHash)) - return () => { - setKV({}) - } - }, [poiState.selected]) + const type = selectedPOI?.osm_type return ( @@ -111,8 +83,6 @@ export default function POIStatePopup({ map, poiState }: POIStatePopupProps) { className={styles.poiPopupButton} onClick={() => { if (selectedPOI && oldQueryPoint) { - // TODO NOW how to use the POI as either start or destination? - // Might be too unintuitive if it relies on with which input we searched the POIs const queryPoint = { ...oldQueryPoint, queryText: selectedPOI?.name, @@ -128,8 +98,7 @@ export default function POIStatePopup({ map, poiState }: POIStatePopupProps) { {oldQueryPoint && } {tr('Use in route')} - {Object.keys(kv).length == 0 && {tr('Fetching more info...')}} - + {selectedPOI && }
OpenStreetMap.org diff --git a/src/pois/AddressParseResult.ts b/src/pois/AddressParseResult.ts index 3dd5362e..e35169a5 100644 --- a/src/pois/AddressParseResult.ts +++ b/src/pois/AddressParseResult.ts @@ -2,27 +2,28 @@ import { ApiImpl } from '@/api/Api' import Dispatcher from '@/stores/Dispatcher' import { SetBBox, SetPOIs } from '@/actions/Actions' import { hitToItem } from '@/Converters' -import { GeocodingHit } from '@/api/graphhopper' +import { GeocodingHit, ReverseGeocodingHit } from '@/api/graphhopper' import { QueryPoint } from '@/stores/QueryStore' import { tr, Translation } from '@/translation/Translation' +import { POI } from '@/stores/POIsStore' export class AddressParseResult { location: string - tags: KV[] + queries: POIQuery[] icon: string poi: string static TRIGGER_VALUES: PoiTriggerPhrases[] static REMOVE_VALUES: string[] - constructor(location: string, tags: KV[], icon: string, poi: string) { + constructor(location: string, queries: POIQuery[], icon: string, poi: string) { this.location = location - this.tags = tags + this.queries = queries this.icon = icon this.poi = poi } hasPOIs(): boolean { - return this.tags.length > 0 + return this.queries.length > 0 } text(prefix: string) { @@ -46,13 +47,13 @@ export class AddressParseResult { for (const keyword of val.k) { const i = bigrams.indexOf(keyword) if (i >= 0) - return new AddressParseResult(cleanQuery.replace(bigrams[i], '').trim(), val.t, val.i, val.k[0]) + return new AddressParseResult(cleanQuery.replace(bigrams[i], '').trim(), val.q, val.i, val.k[0]) } for (const keyword of val.k) { const i = queryTokens.indexOf(keyword) if (i >= 0) - return new AddressParseResult(cleanQuery.replace(queryTokens[i], '').trim(), val.t, val.i, val.k[0]) + return new AddressParseResult(cleanQuery.replace(queryTokens[i], '').trim(), val.q, val.i, val.k[0]) } } @@ -60,12 +61,34 @@ export class AddressParseResult { } public static handleGeocodingResponse( - hits: GeocodingHit[], + hits: ReverseGeocodingHit[], parseResult: AddressParseResult, queryPoint: QueryPoint ) { if (hits.length == 0) return - const pois = AddressParseResult.map(hits, parseResult) + const pois = hits + .filter(hit => !!hit.point) + .map(hit => { + const res = hitToItem({ + name: hit.tags.name ? hit.tags.name : '', + country: hit.tags['addr:country'], + city: hit.tags['addr:city'], + state: hit.tags['addr:state'], + street: hit.tags['addr:street'], + housenumber: hit.tags['addr:housenumer'], + postcode: hit.tags['addr:postcode'], + } as GeocodingHit) + return { + name: res.mainText, + osm_id: '' + hit.id, + osm_type: hit.type, + queries: parseResult.queries, + tags: hit.tags, + icon: parseResult.icon, + coordinate: hit.point, + address: res.secondText, + } as POI + }) const bbox = ApiImpl.getBBoxPoints(pois.map(p => p.coordinate)) if (bbox) { if (parseResult.location) Dispatcher.dispatch(new SetBBox(bbox)) @@ -77,25 +100,6 @@ export class AddressParseResult { } } - static map(hits: GeocodingHit[], parseResult: AddressParseResult) { - return hits.map(hit => { - const res = hitToItem(hit) - return { - name: res.mainText, - tags: parseResult.tags, - icon: parseResult.icon, - coordinate: hit.point, - address: res.secondText, - osm_id: hit.osm_id, - osm_type: hit.osm_type, - } - }) - } - - static s(s: string) { - return - } - // because of the static method we need to inject the Translation object as otherwise jest has a problem static setPOITriggerPhrases(translation: Translation) { const t = (s: string) => @@ -143,13 +147,13 @@ export class AddressParseResult { return { k: val.split(':')[0], v: val.split(':')[1] } }) return { - ...v, k: t(v.k), - t: tags, + q: tags, + i: v.i, } }) } } -export type KV = { k: string; v: string } -export type PoiTriggerPhrases = { k: string[]; t: KV[]; i: string } +export type POIQuery = { k: string; v: string } +export type PoiTriggerPhrases = { k: string[]; q: POIQuery[]; i: string } diff --git a/src/sidebar/search/AddressInput.tsx b/src/sidebar/search/AddressInput.tsx index 3f5e3f33..89d0e17e 100644 --- a/src/sidebar/search/AddressInput.tsx +++ b/src/sidebar/search/AddressInput.tsx @@ -1,6 +1,6 @@ import { ReactNode, useCallback, useEffect, useRef, useState } from 'react' import { Coordinate, getBBoxFromCoord, QueryPoint, QueryPointType } from '@/stores/QueryStore' -import { Bbox, GeocodingHit } from '@/api/graphhopper' +import { Bbox, GeocodingHit, ReverseGeocodingHit } from '@/api/graphhopper' import Autocomplete, { AutocompleteItem, GeocodingItem, @@ -12,7 +12,7 @@ import Autocomplete, { import ArrowBack from './arrow_back.svg' import Cross from '@/sidebar/times-solid-thin.svg' import styles from './AddressInput.module.css' -import Api, { getApi } from '@/api/Api' +import Api, { ApiImpl, getApi } from '@/api/Api' import { tr } from '@/translation/Translation' import { coordinateToText, hitToItem, nominatimHitToItem, textToCoordinate } from '@/Converters' import { useMediaQuery } from 'react-responsive' @@ -259,11 +259,12 @@ function handlePoiSearch(poiSearch: ReverseGeocoder, result: AddressParseResult, if (!result.hasPOIs()) return const center = map.getView().getCenter() ? toLonLat(map.getView().getCenter()!) : [13.4, 52.5] - const mapCenter = { lng: center[0], lat: center[1] } + // const mapCenter = { lng: center[0], lat: center[1] } const origExtent = map.getView().calculateExtent(map.getSize()) const extent = transformExtent(origExtent, 'EPSG:3857', 'EPSG:4326') - const mapDiagonal = calcDist({ lng: extent[0], lat: extent[1] }, { lng: extent[2], lat: extent[3] }) - poiSearch.request(result, mapCenter, Math.min(mapDiagonal / 2 / 1000, 100)) + // const mapDiagonal = calcDist({ lng: extent[0], lat: extent[1] }, { lng: extent[2], lat: extent[3] }) + // Math.min(mapDiagonal / 2 / 1000, 100) + poiSearch.request(result, extent as Bbox) } function ResponsiveAutocomplete({ @@ -360,13 +361,17 @@ export class ReverseGeocoder { private requestId = 0 private readonly timeout = new Timout(200) private readonly api: Api - private readonly onSuccess: (hits: GeocodingHit[], parseResult: AddressParseResult, queryPoint: QueryPoint) => void + private readonly onSuccess: ( + hits: ReverseGeocodingHit[], + parseResult: AddressParseResult, + queryPoint: QueryPoint + ) => void private readonly queryPoint: QueryPoint constructor( api: Api, queryPoint: QueryPoint, - onSuccess: (hits: GeocodingHit[], parseResult: AddressParseResult, queryPoint: QueryPoint) => void + onSuccess: (hits: ReverseGeocodingHit[], parseResult: AddressParseResult, queryPoint: QueryPoint) => void ) { this.api = api this.onSuccess = onSuccess @@ -378,31 +383,29 @@ export class ReverseGeocoder { this.getNextId() } - request(query: AddressParseResult, point: Coordinate, radius: number) { - this.requestAsync(query, point, radius).then(() => {}) + request(query: AddressParseResult, bbox: Bbox) { + this.requestAsync(query, bbox).then(() => {}) } - async requestAsync(parseResult: AddressParseResult, point: Coordinate, radius: number) { + async requestAsync(parseResult: AddressParseResult, bbox: Bbox) { const currentId = this.getNextId() this.timeout.cancel() await this.timeout.wait() try { - let hits: GeocodingHit[] = [] - let result - + let hits: ReverseGeocodingHit[] if (parseResult.location) { let options: Record = { - point: coordinateToText(point), + point: coordinateToText({ lat: (bbox[1] + bbox[3]) / 2, lng: (bbox[0] + bbox[2]) / 2 }), location_bias_scale: '0.5', zoom: '9', } - result = await this.api.geocode(parseResult.location, 'default', options) - hits = result.hits - if (hits.length > 0) result = await this.api.reverseGeocode('', hits[0].point, radius, parseResult.tags) - } else if (point) { - result = await this.api.reverseGeocode('', point, radius, parseResult.tags) + const fwdSearch = await this.api.geocode(parseResult.location, 'default', options) + if (fwdSearch.hits.length > 0) + hits = await this.api.reverseGeocode(fwdSearch.hits[0].extent, parseResult.queries) + else hits = [] + } else { + hits = await this.api.reverseGeocode(bbox, parseResult.queries) } - if (result) hits = Geocoder.filterDuplicates(result.hits) if (currentId === this.requestId) this.onSuccess(hits, parseResult, this.queryPoint) } catch (reason) { throw Error('Could not get geocoding results because: ' + reason) diff --git a/src/stores/POIsStore.ts b/src/stores/POIsStore.ts index 9968d94e..b611c5a2 100644 --- a/src/stores/POIsStore.ts +++ b/src/stores/POIsStore.ts @@ -2,11 +2,13 @@ import { Coordinate, QueryPoint } from '@/stores/QueryStore' import Store from '@/stores/Store' import { Action } from '@/stores/Dispatcher' import { SelectPOI, SetPOIs } from '@/actions/Actions' -import { KV } from '@/pois/AddressParseResult' +import { POIQuery } from '@/pois/AddressParseResult' +import { TagHash } from '@/api/graphhopper' export interface POI { name: string - tags: KV[] + queries: POIQuery[] + tags: TagHash osm_id: string osm_type: string icon: string diff --git a/test/DummyApi.ts b/test/DummyApi.ts index 49aee410..284813c3 100644 --- a/test/DummyApi.ts +++ b/test/DummyApi.ts @@ -1,7 +1,14 @@ import Api from '../src/api/Api' -import { ApiInfo, GeocodingResult, RoutingArgs, RoutingResult, RoutingResultInfo } from '../src/api/graphhopper' -import { Coordinate } from '@/stores/QueryStore' -import { KV } from '@/pois/AddressParseResult' +import { + ApiInfo, + GeocodingResult, + ReverseGeocodingHit, + RoutingArgs, + RoutingResult, + RoutingResultInfo, + Bbox, +} from '../src/api/graphhopper' +import { POIQuery } from '@/pois/AddressParseResult' export default class DummyApi implements Api { geocode(query: string): Promise { @@ -11,16 +18,8 @@ export default class DummyApi implements Api { }) } - reverseGeocode( - query: string | undefined, - point: Coordinate, - radius: number, - tags?: KV[] - ): Promise { - return Promise.resolve({ - took: 0, - hits: [], - }) + reverseGeocode(bbox: Bbox, queries: POIQuery[]): Promise { + return Promise.resolve([]) } info(): Promise { diff --git a/test/stores/QueryStore.test.ts b/test/stores/QueryStore.test.ts index 456c0e67..66f42dd1 100644 --- a/test/stores/QueryStore.test.ts +++ b/test/stores/QueryStore.test.ts @@ -1,5 +1,13 @@ import Api from '@/api/Api' -import { ApiInfo, GeocodingResult, RoutingArgs, RoutingResult, RoutingResultInfo } from '@/api/graphhopper' +import { + ApiInfo, + Bbox, + GeocodingResult, + ReverseGeocodingHit, + RoutingArgs, + RoutingResult, + RoutingResultInfo, +} from '@/api/graphhopper' import QueryStore, { Coordinate, QueryPoint, @@ -19,7 +27,7 @@ import { SetPoint, SetVehicleProfile, } from '@/actions/Actions' -import { KV } from '@/pois/AddressParseResult' +import { POIQuery } from '@/pois/AddressParseResult' class ApiMock implements Api { private readonly callback: { (args: RoutingArgs): void } @@ -32,12 +40,7 @@ class ApiMock implements Api { throw Error('not implemented') } - reverseGeocode( - query: string | undefined, - point: Coordinate, - radius: number, - tags?: KV[] - ): Promise { + reverseGeocode(bbox: Bbox, queries: POIQuery[]): Promise { throw Error('not implemented') } diff --git a/test/stores/RouteStore.test.ts b/test/stores/RouteStore.test.ts index 6bd87d10..285ccbe4 100644 --- a/test/stores/RouteStore.test.ts +++ b/test/stores/RouteStore.test.ts @@ -1,10 +1,18 @@ import RouteStore from '@/stores/RouteStore' import QueryStore, { Coordinate, QueryPoint, QueryPointType } from '@/stores/QueryStore' import Api from '@/api/Api' -import { ApiInfo, GeocodingResult, Path, RoutingArgs, RoutingResult } from '@/api/graphhopper' +import { + ApiInfo, + Bbox, + GeocodingResult, + Path, + ReverseGeocodingHit, + RoutingArgs, + RoutingResult, +} from '@/api/graphhopper' import Dispatcher, { Action } from '@/stores/Dispatcher' import { ClearPoints, ClearRoute, RemovePoint, SetPoint, SetSelectedPath } from '@/actions/Actions' -import { KV } from '@/pois/AddressParseResult' +import { POIQuery } from '@/pois/AddressParseResult' describe('RouteStore', () => { afterEach(() => { @@ -70,12 +78,7 @@ class DummyApi implements Api { throw Error('not implemented') } - reverseGeocode( - query: string | undefined, - point: Coordinate, - radius: number, - tags?: KV[] - ): Promise { + reverseGeocode(bbox: Bbox, queries: POIQuery[]): Promise { throw Error('not implemented') } From 3bd15531e78990f29bdb967aa3fabc40c07e823a Mon Sep 17 00:00:00 2001 From: Peter Date: Sun, 7 Jul 2024 17:15:57 +0200 Subject: [PATCH 20/33] use out center and let overpass do the work; fix a few bugs --- src/api/Api.ts | 54 +++++++++++------------------ src/pois/AddressParseResult.ts | 7 ++-- src/sidebar/search/AddressInput.tsx | 13 +++---- 3 files changed, 29 insertions(+), 45 deletions(-) diff --git a/src/api/Api.ts b/src/api/Api.ts index efcdabee..eb5a948f 100644 --- a/src/api/Api.ts +++ b/src/api/Api.ts @@ -129,31 +129,34 @@ export class ApiImpl implements Api { const url = 'https://overpass-api.de/api/interpreter' // bbox of overpass is minLat, minLon, maxLat, maxLon - let minLat = bbox[1], minLon = bbox[0], maxLat = bbox[3], maxLon = bbox[2] - - // reduce bbox to improve overpass response time - if(maxLat-minLat > 0.2) { + let minLat = bbox[1], + minLon = bbox[0], + maxLat = bbox[3], + maxLon = bbox[2] + + // Reduce the bbox to improve overpass response time for larger cities or areas. + // This might lead to empty responses for POI queries with a small result set. + if(maxLat-minLat > 0.3) { const centerLat = (maxLat + minLat) / 2 - maxLat = centerLat + 0.1 - minLat = centerLat - 0.1 + maxLat = centerLat + 0.15 + minLat = centerLat - 0.15 } - if(maxLon - minLon > 0.2) { + if(maxLon - minLon > 0.3) { const centerLon = (maxLon + minLon) / 2 - maxLon = centerLon + 0.1 - minLon = centerLon - 0.1 + maxLon = centerLon + 0.15 + minLon = centerLon - 0.15 } - // nw means it searches for nodes and ways - let query = '' + let queryString = '' for (const tag of queries) { const value = tag.v ? `="${tag.v}"` : '' - query += `nw["${tag.k}"${value}];` + // nw means it searches for nodes and ways + const types = tag.k.includes('aeroway') ? 'nwr' : 'nw' // including relations in general is much slower + queryString += `${types}["${tag.k}"${value}];\n` } try { - // (._;>;); => means it fetches the coordinates for ways. From this we create an index and calculate the center point - // Although this is ugly I did not find a faster way e.g. out geom or out center are all slower - const data = `[out:json][timeout:15][bbox:${minLat}, ${minLon}, ${maxLat}, ${maxLon}];${query}(._;>;);out 50;` + const data = `[out:json][timeout:15][bbox:${minLat}, ${minLon}, ${maxLat}, ${maxLon}];\n(${queryString});\nout center 100;` console.log(data) const result = await fetch(url, { method: 'POST', @@ -161,30 +164,15 @@ export class ApiImpl implements Api { }) const json = await result.json() if (json.elements) { - const elements = json.elements as any[] - const index: { [key: number]: any } = {} - elements.forEach(e => (index[e.id] = e)) - const res = elements + const res = (json.elements as any[]) .map(e => { - if (e.nodes) { - const coords = e.nodes. - map((n: number) => (index[n] ? { lat: index[n].lat, lng: index[n].lon } : {})). - filter((c: Coordinate) => c.lat) - console.log(coords) - // minLon, minLat, maxLon, maxLat - const bbox = ApiImpl.getBBoxPoints(coords) - return bbox - ? ({ - ...e, - point: { lat: (bbox[1] + bbox[3]) / 2, lng: (bbox[0] + bbox[2]) / 2 }, - } as ReverseGeocodingHit) - : e + if (e.center) { + return { ...e, point: { lat: e.center.lat, lng: e.center.lon } } as ReverseGeocodingHit } else { return { ...e, point: { lat: e.lat, lng: e.lon } } as ReverseGeocodingHit } }) .filter(p => !!p.tags && p.point) - console.log(res) return res } else return [] } catch (error) { diff --git a/src/pois/AddressParseResult.ts b/src/pois/AddressParseResult.ts index e35169a5..fbe11d52 100644 --- a/src/pois/AddressParseResult.ts +++ b/src/pois/AddressParseResult.ts @@ -109,7 +109,7 @@ export class AddressParseResult { .map(s => s.trim().toLowerCase()) AddressParseResult.REMOVE_VALUES = t('poi_removal_words') AddressParseResult.TRIGGER_VALUES = [ - { k: 'poi_airports', t: ['aeroway:aerodrome'], i: 'flight_takeoff' }, + { k: 'poi_airports', t: ['aeroway:aerodrome'], i: 'flight_takeoff' }, // TODO exclude landuse = military AND military = airfield { k: 'poi_atm', t: ['amenity:atm', 'amenity:bank'], i: 'local_atm' }, { k: 'poi_banks', t: ['amenity:bank'], i: 'universal_currency_alt' }, { k: 'poi_bus_stops', t: ['highway:bus_stop'], i: 'train' }, @@ -131,9 +131,8 @@ export class AddressParseResult { i: 'local_post_office', }, { k: 'poi_post', t: ['amenity:post_office', 'amenity:post_depot'], i: 'local_post_office' }, - // TODO NOW public_transport:station does not seem to be supported from photon? - { k: 'poi_public_transit', t: ['railway:station', 'highway:bus_stop'], i: 'train' }, - { k: 'poi_railway_station', t: ['railway:station'], i: 'train' }, + { k: 'poi_public_transit', t: ['public_transport:station', 'railway:station', 'highway:bus_stop'], i: 'train' }, + { k: 'poi_railway_station', t: ['railway:station', 'railway:halt'], i: 'train' }, { k: 'poi_restaurants', t: ['amenity:restaurant'], i: 'restaurant' }, { k: 'poi_schools', t: ['amenity:school', 'building:school'], i: 'school' }, { k: 'poi_shopping', t: ['shop'], i: 'store' }, diff --git a/src/sidebar/search/AddressInput.tsx b/src/sidebar/search/AddressInput.tsx index 89d0e17e..92219b7a 100644 --- a/src/sidebar/search/AddressInput.tsx +++ b/src/sidebar/search/AddressInput.tsx @@ -258,12 +258,8 @@ export default function AddressInput(props: AddressInputProps) { function handlePoiSearch(poiSearch: ReverseGeocoder, result: AddressParseResult, map: Map) { if (!result.hasPOIs()) return - const center = map.getView().getCenter() ? toLonLat(map.getView().getCenter()!) : [13.4, 52.5] - // const mapCenter = { lng: center[0], lat: center[1] } const origExtent = map.getView().calculateExtent(map.getSize()) const extent = transformExtent(origExtent, 'EPSG:3857', 'EPSG:4326') - // const mapDiagonal = calcDist({ lng: extent[0], lat: extent[1] }, { lng: extent[2], lat: extent[3] }) - // Math.min(mapDiagonal / 2 / 1000, 100) poiSearch.request(result, extent as Bbox) } @@ -392,7 +388,7 @@ export class ReverseGeocoder { this.timeout.cancel() await this.timeout.wait() try { - let hits: ReverseGeocodingHit[] + let hits: ReverseGeocodingHit[] = [] if (parseResult.location) { let options: Record = { point: coordinateToText({ lat: (bbox[1] + bbox[3]) / 2, lng: (bbox[0] + bbox[2]) / 2 }), @@ -400,9 +396,10 @@ export class ReverseGeocoder { zoom: '9', } const fwdSearch = await this.api.geocode(parseResult.location, 'default', options) - if (fwdSearch.hits.length > 0) - hits = await this.api.reverseGeocode(fwdSearch.hits[0].extent, parseResult.queries) - else hits = [] + if (fwdSearch.hits.length > 0) { + const bbox = fwdSearch.hits[0].extent ? fwdSearch.hits[0].extent : getBBoxFromCoord(fwdSearch.hits[0].point, 0.01) + if(bbox) hits = await this.api.reverseGeocode(bbox, parseResult.queries) + } } else { hits = await this.api.reverseGeocode(bbox, parseResult.queries) } From 2a594d10d2bd53c4127431e2d625329e7c95fe11 Mon Sep 17 00:00:00 2001 From: Peter Date: Sun, 7 Jul 2024 17:22:29 +0200 Subject: [PATCH 21/33] translate 'nearby' --- src/pois/AddressParseResult.ts | 2 +- src/translation/tr.json | 194 ++++++++++++++++++++++++--------- 2 files changed, 146 insertions(+), 50 deletions(-) diff --git a/src/pois/AddressParseResult.ts b/src/pois/AddressParseResult.ts index fbe11d52..e77b3c90 100644 --- a/src/pois/AddressParseResult.ts +++ b/src/pois/AddressParseResult.ts @@ -27,7 +27,7 @@ export class AddressParseResult { } text(prefix: string) { - return prefix + ' ' + (this.location ? tr('in') + ' ' + this.location : tr('nearby')) + return this.location ? tr('poi_in', [prefix, this.location]) : tr('poi_nearby', [prefix]) } /* it is a bit ugly that we have to inject the translated values here, but jest goes crazy otherwise */ diff --git a/src/translation/tr.json b/src/translation/tr.json index cf8dc132..75ef6f5e 100644 --- a/src/translation/tr.json +++ b/src/translation/tr.json @@ -112,6 +112,8 @@ "next":"Next", "back":"Back", "poi_removal_words":"area, around, here, in, local, nearby, this", +"poi_nearby":"%1$s nearby", +"poi_in":"%1$s in %2$s", "poi_airports":"airports, airport", "poi_banks":"banks, bank", "poi_bus_stops":"bus stops, bus stop, bus", @@ -125,7 +127,7 @@ "poi_pharmacies":"pharmacies, pharmacy", "poi_playgrounds":"playgrounds, playground", "poi_public_transit":"public transit", -"poi_railway_station":"railway stations, railway station, train", +"poi_railway_station":"railway stations, railway station, trains, train", "poi_restaurants":"restaurants, restaurant, eat", "poi_schools":"schools, school", "poi_super_markets":"super markets, super market", @@ -253,6 +255,8 @@ "next":"", "back":"", "poi_removal_words":"", +"poi_nearby":"", +"poi_in":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -394,6 +398,8 @@ "next":"", "back":"", "poi_removal_words":"", +"poi_nearby":"", +"poi_in":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -535,6 +541,8 @@ "next":"", "back":"", "poi_removal_words":"", +"poi_nearby":"", +"poi_in":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -676,6 +684,8 @@ "next":"", "back":"", "poi_removal_words":"", +"poi_nearby":"", +"poi_in":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -817,6 +827,8 @@ "next":"", "back":"", "poi_removal_words":"", +"poi_nearby":"", +"poi_in":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -958,6 +970,8 @@ "next":"", "back":"", "poi_removal_words":"", +"poi_nearby":"", +"poi_in":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -1099,6 +1113,8 @@ "next":"", "back":"", "poi_removal_words":"", +"poi_nearby":"", +"poi_in":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -1240,6 +1256,8 @@ "next":"", "back":"", "poi_removal_words":"", +"poi_nearby":"", +"poi_in":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -1381,6 +1399,8 @@ "next":"Weiter", "back":"Zurück", "poi_removal_words":"der, dem, gebiet, in, karte, lokal, lokale, nähe", +"poi_nearby":"%1$s in der Nähe", +"poi_in":"", "poi_airports":"Flughäfen, Flughafen", "poi_banks":"Bank", "poi_bus_stops":"Haltestellen, Haltestelle, Bushaltestellen, Bushaltestelle", @@ -1394,8 +1414,8 @@ "poi_pharmacies":"Apotheken, Apotheke", "poi_playgrounds":"Spielplätze, Spielplatz", "poi_public_transit":"ÖPNV, Nahverkehr", -"poi_railway_station":"Bahnhöfe, Bahnhof, Zug", -"poi_restaurants":"Restaurants, Restaurant", +"poi_railway_station":"Bahnhöfe, Bahnhof, Züge, Zug", +"poi_restaurants":"Restaurants, Restaurant, Gasthof, Gaststätten, Gaststätte", "poi_schools":"Schulen, Schule", "poi_super_markets":"Supermärkte, Supermarkt, Einkauf, Einkaufsladen", "poi_tourism":"Tourismus, Fremdenverkehr, Touristik", @@ -1522,6 +1542,8 @@ "next":"Επόμενο", "back":"Πίσω", "poi_removal_words":"", +"poi_nearby":"", +"poi_in":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -1663,6 +1685,8 @@ "next":"", "back":"", "poi_removal_words":"", +"poi_nearby":"", +"poi_in":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -1804,6 +1828,8 @@ "next":"", "back":"", "poi_removal_words":"", +"poi_nearby":"", +"poi_in":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -1945,6 +1971,8 @@ "next":"", "back":"", "poi_removal_words":"", +"poi_nearby":"", +"poi_in":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -2086,6 +2114,8 @@ "next":"", "back":"", "poi_removal_words":"", +"poi_nearby":"", +"poi_in":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -2227,6 +2257,8 @@ "next":"", "back":"", "poi_removal_words":"", +"poi_nearby":"", +"poi_in":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -2368,6 +2400,8 @@ "next":"", "back":"", "poi_removal_words":"", +"poi_nearby":"", +"poi_in":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -2509,6 +2543,8 @@ "next":"", "back":"", "poi_removal_words":"", +"poi_nearby":"", +"poi_in":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -2650,6 +2686,8 @@ "next":"", "back":"", "poi_removal_words":"", +"poi_nearby":"", +"poi_in":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -2791,6 +2829,8 @@ "next":"", "back":"", "poi_removal_words":"", +"poi_nearby":"", +"poi_in":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -2932,6 +2972,8 @@ "next":"", "back":"", "poi_removal_words":"", +"poi_nearby":"", +"poi_in":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -3073,6 +3115,8 @@ "next":"", "back":"", "poi_removal_words":"", +"poi_nearby":"", +"poi_in":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -3109,13 +3153,13 @@ "way_contains_toll":"Az útvonalon útdíjat kell fizetni", "way_crosses_border":"Az útvonal országhatárt keresztez", "way_contains":"Az útvonalon előfordul %1$s", -"way_contains_restrictions":"", +"way_contains_restrictions":"Az útvonalon korlátozások lehetnek", "tracks":"burkolatlan földút", "steps":"lépcső", "footways":"gyalogút", "steep_sections":"meredek szakasz", "private_sections":"magánút", -"challenging_sections":"", +"challenging_sections":"nehéz vagy veszélyes szakaszok", "trunk_roads_warn":"Az útvonal potenciálisan veszélyes autóutat vagy forgalmasabbat is tartalmaz", "get_off_bike_for":"A kerékpárt tolni kell ennyit: %1$s", "start_label":"Indulás", @@ -3138,9 +3182,9 @@ "settings":"Beállítások", "settings_close":"Bezárás", "exclude_motorway_example":"Autópálya nélkül", -"exclude_disneyland_paris_example":"", -"simple_electric_car_example":"", -"avoid_tunnels_bridges_example":"", +"exclude_disneyland_paris_example":"Disneyland Párizs kizárása", +"simple_electric_car_example":"Egyszerű elektromos autó", +"avoid_tunnels_bridges_example":"Hidak és alagutak elkerülése", "limit_speed_example":"Sebességkorlátozás", "cargo_bike_example":"Viszikli (teherkerékpár)", "prefer_bike_network":"Kerékpárutak előnyben részesítése", @@ -3148,12 +3192,12 @@ "combined_example":"Kombinált példa", "examples_custom_model":"Példák", "marker":"Jelölő", -"gh_offline_info":"Lehet, hogy a GraphHopper API nem érhető el?", +"gh_offline_info":"Lehet, hogy a GraphHopper API nem elérhető?", "refresh_button":"Oldal frissítése", "server_status":"Állapot", "zoom_in":"Nagyítás", "zoom_out":"Kicsinyítés", -"zoom_to_route":"", +"zoom_to_route":"Útvonalra közelítés", "drag_to_reorder":"Húzza el az átrendezéshez", "route_timed_out":"Időtúllépés az útvonaltervezéskor", "route_request_failed":"Útvonaltervezés sikertelen", @@ -3167,7 +3211,7 @@ "settings_gpx_export":"GPX-be való exportálás beállításai", "settings_gpx_export_trk":"", "settings_gpx_export_rte":"", -"settings_gpx_export_wpt":"", +"settings_gpx_export_wpt":"Útpontokkal", "hide_button":"Elrejtés", "details_button":"Részletek", "to_hint":"Ide", @@ -3179,11 +3223,11 @@ "locations_not_found":"Útvonaltervezés nem lehetséges. A megadott hely(ek) nem található(k) meg a területen.", "search_with_nominatim":"Keresés a Nominatim segítségével", "powered_by":"Motor:", -"info":"", -"feedback":"", -"imprint":"", -"privacy":"", -"terms":"", +"info":"Információ", +"feedback":"Visszajelzés", +"imprint":"Impresszum", +"privacy":"Adatvédelem", +"terms":"Feltételek", "bike":"Kerékpár", "racingbike":"Versenykerékpár", "mtb":"Hegyi kerékpár", @@ -3193,52 +3237,54 @@ "small_truck":"Kisteherautó", "bus":"Busz", "truck":"Teherautó", -"staticlink":"Statikus hivatkozás", +"staticlink":"statikus hivatkozás", "motorcycle":"Motorkerékpár", -"scooter":"", +"scooter":"Robogó", "back_to_map":"Vissza", "distance_unit":"Távolság mértékegysége: %1$s", -"waiting_for_gps":"", -"elevation":"", -"slope":"", +"waiting_for_gps":"Várakozás a GPS jelre...", +"elevation":"Magasság", +"slope":"Meredekség", "towerslope":"", -"country":"", -"surface":"", +"country":"Ország", +"surface":"Útfelület", "road_environment":"", "road_access":"", -"road_class":"", -"max_speed":"", -"average_speed":"", -"track_type":"", -"toll":"", +"road_class":"Útbesorolás", +"max_speed":"Max. sebesség", +"average_speed":"Átl. sebesség", +"track_type":"Úttípus", +"toll":"Útdíj", "next":"", "back":"", "poi_removal_words":"", -"poi_airports":"", -"poi_banks":"", -"poi_bus_stops":"", -"poi_education":"", -"poi_hospitals":"", -"poi_hotels":"", -"poi_leisure":"", -"poi_museums":"", -"poi_parking":"", +"poi_nearby":"", +"poi_in":"", +"poi_airports":"repülőterek, repülőtér", +"poi_banks":"bankok, bank", +"poi_bus_stops":"buszmegállók, buszmegálló, busz", +"poi_education":"oktatás", +"poi_hospitals":"kórházak, kórház", +"poi_hotels":"hotelek, hotel", +"poi_leisure":"szabadidő", +"poi_museums":"múzeumok, múzeum", +"poi_parking":"parkoló, parkolóhely", "poi_parks":"", -"poi_pharmacies":"", -"poi_playgrounds":"", -"poi_public_transit":"", -"poi_railway_station":"", -"poi_restaurants":"", -"poi_schools":"", -"poi_super_markets":"", -"poi_tourism":"", -"poi_police":"", -"poi_atm":"", -"poi_gas_station":"", +"poi_pharmacies":"gyógyszertárak, gyógyszertár", +"poi_playgrounds":"játszóterek, játszótér", +"poi_public_transit":"közösségi közlekedés", +"poi_railway_station":"vasútállomások, vasútállomás, vonat", +"poi_restaurants":"éttermek, étterem", +"poi_schools":"iskolák, iskola", +"poi_super_markets":"szupermarketek, szupermarket", +"poi_tourism":"turizmus", +"poi_police":"rendőrség", +"poi_atm":"bankautomata", +"poi_gas_station":"benzinkutak, benzinkút, benzinkutak, benzinkút", "poi_post":"", "poi_post_box":"", "poi_shopping":"", -"poi_toilets":"", +"poi_toilets":"mosdók, mosdó", "poi_charging_station":"" }, "in_ID":{ @@ -3355,6 +3401,8 @@ "next":"Selanjutnya", "back":"Sebelumnya", "poi_removal_words":"", +"poi_nearby":"", +"poi_in":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -3496,6 +3544,8 @@ "next":"", "back":"", "poi_removal_words":"", +"poi_nearby":"", +"poi_in":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -3637,6 +3687,8 @@ "next":"", "back":"", "poi_removal_words":"", +"poi_nearby":"", +"poi_in":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -3778,6 +3830,8 @@ "next":"", "back":"", "poi_removal_words":"", +"poi_nearby":"", +"poi_in":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -3919,6 +3973,8 @@ "next":"", "back":"", "poi_removal_words":"", +"poi_nearby":"", +"poi_in":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -4060,6 +4116,8 @@ "next":"", "back":"", "poi_removal_words":"", +"poi_nearby":"", +"poi_in":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -4201,6 +4259,8 @@ "next":"", "back":"", "poi_removal_words":"", +"poi_nearby":"", +"poi_in":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -4342,6 +4402,8 @@ "next":"", "back":"", "poi_removal_words":"", +"poi_nearby":"", +"poi_in":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -4483,6 +4545,8 @@ "next":"", "back":"", "poi_removal_words":"", +"poi_nearby":"", +"poi_in":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -4624,6 +4688,8 @@ "next":"", "back":"", "poi_removal_words":"", +"poi_nearby":"", +"poi_in":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -4765,6 +4831,8 @@ "next":"", "back":"", "poi_removal_words":"", +"poi_nearby":"", +"poi_in":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -4906,6 +4974,8 @@ "next":"", "back":"", "poi_removal_words":"", +"poi_nearby":"", +"poi_in":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -5047,6 +5117,8 @@ "next":"", "back":"", "poi_removal_words":"", +"poi_nearby":"", +"poi_in":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -5188,6 +5260,8 @@ "next":"", "back":"", "poi_removal_words":"", +"poi_nearby":"", +"poi_in":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -5329,6 +5403,8 @@ "next":"", "back":"", "poi_removal_words":"", +"poi_nearby":"", +"poi_in":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -5470,6 +5546,8 @@ "next":"", "back":"", "poi_removal_words":"", +"poi_nearby":"", +"poi_in":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -5611,6 +5689,8 @@ "next":"", "back":"", "poi_removal_words":"", +"poi_nearby":"", +"poi_in":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -5752,6 +5832,8 @@ "next":"", "back":"", "poi_removal_words":"", +"poi_nearby":"", +"poi_in":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -5893,6 +5975,8 @@ "next":"", "back":"", "poi_removal_words":"", +"poi_nearby":"", +"poi_in":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -6034,6 +6118,8 @@ "next":"", "back":"", "poi_removal_words":"", +"poi_nearby":"", +"poi_in":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -6175,6 +6261,8 @@ "next":"", "back":"", "poi_removal_words":"", +"poi_nearby":"", +"poi_in":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -6316,6 +6404,8 @@ "next":"", "back":"", "poi_removal_words":"", +"poi_nearby":"", +"poi_in":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -6457,6 +6547,8 @@ "next":"下一页", "back":"返回", "poi_removal_words":"", +"poi_nearby":"", +"poi_in":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -6598,6 +6690,8 @@ "next":"下一頁", "back":"返回", "poi_removal_words":"", +"poi_nearby":"", +"poi_in":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", @@ -6739,6 +6833,8 @@ "next":"", "back":"", "poi_removal_words":"", +"poi_nearby":"", +"poi_in":"", "poi_airports":"", "poi_banks":"", "poi_bus_stops":"", From 1bbd00bce15c2784eb453dae16dd9704767e2525 Mon Sep 17 00:00:00 2001 From: Peter Date: Sun, 7 Jul 2024 21:23:26 +0200 Subject: [PATCH 22/33] make 'AND NOT' possible to exclude military airports --- src/NavBar.ts | 2 +- src/api/Api.ts | 24 ++++++++------- src/layers/POIPopup.tsx | 4 +-- src/pois/AddressParseResult.ts | 45 +++++++++++++++++++++-------- src/sidebar/RoutingResults.tsx | 1 + src/sidebar/search/AddressInput.tsx | 8 +++-- src/stores/POIsStore.ts | 2 +- test/DummyApi.ts | 2 +- test/stores/QueryStore.test.ts | 11 ++----- test/stores/RouteStore.test.ts | 2 +- 10 files changed, 61 insertions(+), 40 deletions(-) diff --git a/src/NavBar.ts b/src/NavBar.ts index 7bde1f94..061445ad 100644 --- a/src/NavBar.ts +++ b/src/NavBar.ts @@ -118,7 +118,7 @@ export default class NavBar { .then(res => { if (res.hits.length != 0) getApi() - .reverseGeocode(res.hits[0].extent, result.queries) + .reverseGeocode(result.query, res.hits[0].extent) .then(res => AddressParseResult.handleGeocodingResponse(res, result, p)) }) return Promise.resolve(p) diff --git a/src/api/Api.ts b/src/api/Api.ts index eb5a948f..37cc5a34 100644 --- a/src/api/Api.ts +++ b/src/api/Api.ts @@ -18,7 +18,7 @@ import { LineString } from 'geojson' import { getTranslation, tr } from '@/translation/Translation' import * as config from 'config' import { Coordinate } from '@/stores/QueryStore' -import { POIQuery } from '@/pois/AddressParseResult' +import { POIPhrase, POIQuery } from '@/pois/AddressParseResult' interface ApiProfile { name: string @@ -33,7 +33,7 @@ export default interface Api { geocode(query: string, provider: string, additionalOptions?: Record): Promise - reverseGeocode(bbox: Bbox, queries: POIQuery[]): Promise + reverseGeocode(query: POIQuery, bbox: Bbox): Promise supportsGeocoding(): boolean } @@ -122,9 +122,9 @@ export class ApiImpl implements Api { } } - async reverseGeocode(bbox: Bbox, queries: POIQuery[]): Promise { + async reverseGeocode(query: POIQuery, bbox: Bbox): Promise { if (!this.supportsGeocoding()) return [] - // why is main overpass api so much faster for certain queries like "restaurants berlin" + // why is main overpass api so much faster? // const url = 'https://overpass.kumi.systems/api/interpreter' const url = 'https://overpass-api.de/api/interpreter' @@ -136,23 +136,27 @@ export class ApiImpl implements Api { // Reduce the bbox to improve overpass response time for larger cities or areas. // This might lead to empty responses for POI queries with a small result set. - if(maxLat-minLat > 0.3) { + if (maxLat - minLat > 0.3) { const centerLat = (maxLat + minLat) / 2 maxLat = centerLat + 0.15 minLat = centerLat - 0.15 } - if(maxLon - minLon > 0.3) { + if (maxLon - minLon > 0.3) { const centerLon = (maxLon + minLon) / 2 maxLon = centerLon + 0.15 minLon = centerLon - 0.15 } let queryString = '' - for (const tag of queries) { + function getContent(p: POIPhrase) {} + for (const tag of query.include) { + let notStr = '' + for (const n of query.not) { + notStr += n.v ? `["${n.k}"!="${n.v}"]` : `["${n.k}"!~".*"]` + } const value = tag.v ? `="${tag.v}"` : '' - // nw means it searches for nodes and ways - const types = tag.k.includes('aeroway') ? 'nwr' : 'nw' // including relations in general is much slower - queryString += `${types}["${tag.k}"${value}];\n` + // nwr means it searches for nodes, ways and relations + queryString += `nwr["${tag.k}"${value}]${notStr};\n` } try { diff --git a/src/layers/POIPopup.tsx b/src/layers/POIPopup.tsx index c3e5d0b1..557e2b6a 100644 --- a/src/layers/POIPopup.tsx +++ b/src/layers/POIPopup.tsx @@ -28,8 +28,8 @@ function POITable(props: { poi: POI }) { ? 'https://' + valueArr[0] + '.wikipedia.org/wiki/' + encodeURIComponent(valueArr[1]) : '' // tags like amenity:restaurant should not be shown if it is a restaurant (determined by poi.tags) - const poiInfoRepeated = props.poi.queries - ? props.poi.queries.some(q => q.k == key && q.v === value) + const poiInfoRepeated = props.poi.query.include + ? props.poi.query.include.some(q => q.k == key && q.v === value) : false return ( !poiInfoRepeated && diff --git a/src/pois/AddressParseResult.ts b/src/pois/AddressParseResult.ts index e77b3c90..9b7d44da 100644 --- a/src/pois/AddressParseResult.ts +++ b/src/pois/AddressParseResult.ts @@ -9,21 +9,21 @@ import { POI } from '@/stores/POIsStore' export class AddressParseResult { location: string - queries: POIQuery[] + query: POIQuery icon: string poi: string static TRIGGER_VALUES: PoiTriggerPhrases[] static REMOVE_VALUES: string[] - constructor(location: string, queries: POIQuery[], icon: string, poi: string) { + constructor(location: string, query: POIQuery, icon: string, poi: string) { this.location = location - this.queries = queries + this.query = query this.icon = icon this.poi = poi } hasPOIs(): boolean { - return this.queries.length > 0 + return this.query.include.length > 0 } text(prefix: string) { @@ -47,17 +47,27 @@ export class AddressParseResult { for (const keyword of val.k) { const i = bigrams.indexOf(keyword) if (i >= 0) - return new AddressParseResult(cleanQuery.replace(bigrams[i], '').trim(), val.q, val.i, val.k[0]) + return new AddressParseResult( + cleanQuery.replace(bigrams[i], '').trim(), + { include: val.q, not: val.not }, + val.i, + val.k[0] + ) } for (const keyword of val.k) { const i = queryTokens.indexOf(keyword) if (i >= 0) - return new AddressParseResult(cleanQuery.replace(queryTokens[i], '').trim(), val.q, val.i, val.k[0]) + return new AddressParseResult( + cleanQuery.replace(queryTokens[i], '').trim(), + { include: val.q, not: val.not }, + val.i, + val.k[0] + ) } } - return new AddressParseResult('', [], '', '') + return new AddressParseResult('', { include: [], not: [] }, '', '') } public static handleGeocodingResponse( @@ -82,7 +92,7 @@ export class AddressParseResult { name: res.mainText, osm_id: '' + hit.id, osm_type: hit.type, - queries: parseResult.queries, + query: parseResult.query, tags: hit.tags, icon: parseResult.icon, coordinate: hit.point, @@ -109,7 +119,7 @@ export class AddressParseResult { .map(s => s.trim().toLowerCase()) AddressParseResult.REMOVE_VALUES = t('poi_removal_words') AddressParseResult.TRIGGER_VALUES = [ - { k: 'poi_airports', t: ['aeroway:aerodrome'], i: 'flight_takeoff' }, // TODO exclude landuse = military AND military = airfield + { k: 'poi_airports', t: ['aeroway:aerodrome'], i: 'flight_takeoff', not: ['military', 'landuse:military'] }, { k: 'poi_atm', t: ['amenity:atm', 'amenity:bank'], i: 'local_atm' }, { k: 'poi_banks', t: ['amenity:bank'], i: 'universal_currency_alt' }, { k: 'poi_bus_stops', t: ['highway:bus_stop'], i: 'train' }, @@ -131,7 +141,11 @@ export class AddressParseResult { i: 'local_post_office', }, { k: 'poi_post', t: ['amenity:post_office', 'amenity:post_depot'], i: 'local_post_office' }, - { k: 'poi_public_transit', t: ['public_transport:station', 'railway:station', 'highway:bus_stop'], i: 'train' }, + { + k: 'poi_public_transit', + t: ['public_transport:station', 'railway:station', 'highway:bus_stop'], + i: 'train', + }, { k: 'poi_railway_station', t: ['railway:station', 'railway:halt'], i: 'train' }, { k: 'poi_restaurants', t: ['amenity:restaurant'], i: 'restaurant' }, { k: 'poi_schools', t: ['amenity:school', 'building:school'], i: 'school' }, @@ -145,14 +159,21 @@ export class AddressParseResult { const tags = v.t.map(val => { return { k: val.split(':')[0], v: val.split(':')[1] } }) + const notTags = !v.not + ? [] + : v.not.map(val => { + return { k: val.split(':')[0], v: val.split(':')[1] } + }) return { k: t(v.k), q: tags, i: v.i, + not: notTags, } }) } } -export type POIQuery = { k: string; v: string } -export type PoiTriggerPhrases = { k: string[]; q: POIQuery[]; i: string } +export type POIQuery = { include: POIPhrase[]; not: POIPhrase[] } +export type POIPhrase = { k: string; v: string } +export type PoiTriggerPhrases = { k: string[]; q: POIPhrase[]; i: string; not: POIPhrase[] } diff --git a/src/sidebar/RoutingResults.tsx b/src/sidebar/RoutingResults.tsx index d39efcac..e370a9c5 100644 --- a/src/sidebar/RoutingResults.tsx +++ b/src/sidebar/RoutingResults.tsx @@ -463,6 +463,7 @@ function toCoordinate(pos: Position): Coordinate { } function toBBox(segment: Coordinate[]): Bbox { + // TODO replace with ApiImpl.getBBoxPoints const bbox = getBBoxFromCoord(segment[0], 0.002) if (segment.length == 1) bbox segment.forEach(c => { diff --git a/src/sidebar/search/AddressInput.tsx b/src/sidebar/search/AddressInput.tsx index 92219b7a..6866ecfa 100644 --- a/src/sidebar/search/AddressInput.tsx +++ b/src/sidebar/search/AddressInput.tsx @@ -397,11 +397,13 @@ export class ReverseGeocoder { } const fwdSearch = await this.api.geocode(parseResult.location, 'default', options) if (fwdSearch.hits.length > 0) { - const bbox = fwdSearch.hits[0].extent ? fwdSearch.hits[0].extent : getBBoxFromCoord(fwdSearch.hits[0].point, 0.01) - if(bbox) hits = await this.api.reverseGeocode(bbox, parseResult.queries) + const bbox = fwdSearch.hits[0].extent + ? fwdSearch.hits[0].extent + : getBBoxFromCoord(fwdSearch.hits[0].point, 0.01) + if (bbox) hits = await this.api.reverseGeocode(parseResult.query, bbox) } } else { - hits = await this.api.reverseGeocode(bbox, parseResult.queries) + hits = await this.api.reverseGeocode(parseResult.query, bbox) } if (currentId === this.requestId) this.onSuccess(hits, parseResult, this.queryPoint) } catch (reason) { diff --git a/src/stores/POIsStore.ts b/src/stores/POIsStore.ts index b611c5a2..4d3b4963 100644 --- a/src/stores/POIsStore.ts +++ b/src/stores/POIsStore.ts @@ -7,7 +7,7 @@ import { TagHash } from '@/api/graphhopper' export interface POI { name: string - queries: POIQuery[] + query: POIQuery tags: TagHash osm_id: string osm_type: string diff --git a/test/DummyApi.ts b/test/DummyApi.ts index 284813c3..279edbde 100644 --- a/test/DummyApi.ts +++ b/test/DummyApi.ts @@ -18,7 +18,7 @@ export default class DummyApi implements Api { }) } - reverseGeocode(bbox: Bbox, queries: POIQuery[]): Promise { + reverseGeocode(query: POIQuery, bbox: Bbox): Promise { return Promise.resolve([]) } diff --git a/test/stores/QueryStore.test.ts b/test/stores/QueryStore.test.ts index 66f42dd1..78e11793 100644 --- a/test/stores/QueryStore.test.ts +++ b/test/stores/QueryStore.test.ts @@ -8,14 +8,7 @@ import { RoutingResult, RoutingResultInfo, } from '@/api/graphhopper' -import QueryStore, { - Coordinate, - QueryPoint, - QueryPointType, - QueryStoreState, - RequestState, - SubRequest, -} from '@/stores/QueryStore' +import QueryStore, { QueryPoint, QueryPointType, QueryStoreState, RequestState, SubRequest } from '@/stores/QueryStore' import { AddPoint, ClearPoints, @@ -40,7 +33,7 @@ class ApiMock implements Api { throw Error('not implemented') } - reverseGeocode(bbox: Bbox, queries: POIQuery[]): Promise { + reverseGeocode(query: POIQuery, bbox: Bbox): Promise { throw Error('not implemented') } diff --git a/test/stores/RouteStore.test.ts b/test/stores/RouteStore.test.ts index 285ccbe4..9c884279 100644 --- a/test/stores/RouteStore.test.ts +++ b/test/stores/RouteStore.test.ts @@ -78,7 +78,7 @@ class DummyApi implements Api { throw Error('not implemented') } - reverseGeocode(bbox: Bbox, queries: POIQuery[]): Promise { + reverseGeocode(query: POIQuery, bbox: Bbox): Promise { throw Error('not implemented') } From 314b28995f10d0b249ae9a1c2ae16341f109aee1 Mon Sep 17 00:00:00 2001 From: Peter Date: Sun, 7 Jul 2024 21:36:41 +0200 Subject: [PATCH 23/33] fix a few POI queries and i18n --- src/layers/UsePOIsLayer.tsx | 2 + src/pois/AddressParseResult.ts | 4 +- src/translation/tr.json | 146 ++++++++++++++++++++++----------- 3 files changed, 101 insertions(+), 51 deletions(-) diff --git a/src/layers/UsePOIsLayer.tsx b/src/layers/UsePOIsLayer.tsx index 8d33a0ea..4876eb9b 100644 --- a/src/layers/UsePOIsLayer.tsx +++ b/src/layers/UsePOIsLayer.tsx @@ -26,6 +26,7 @@ import local_gas_station from '/src/pois/img/local_gas_station.svg' import local_post_office from '/src/pois/img/local_post_office.svg' import police from '/src/pois/img/police.svg' import charger from '/src/pois/img/charger.svg' +import water_drop from '../pois/img/water_drop.svg' import { createPOIMarker } from '@/layers/createMarkerSVG' import { Select } from 'ol/interaction' import Dispatcher from '@/stores/Dispatcher' @@ -53,6 +54,7 @@ const svgObjects: { [id: string]: any } = { local_post_office: local_post_office(), police: police(), charger: charger(), + water_drop: water_drop(), } // -300 -1260 1560 1560 diff --git a/src/pois/AddressParseResult.ts b/src/pois/AddressParseResult.ts index 9b7d44da..f8f7a53b 100644 --- a/src/pois/AddressParseResult.ts +++ b/src/pois/AddressParseResult.ts @@ -133,7 +133,7 @@ export class AddressParseResult { { k: 'poi_parks', t: ['leisure:park'], i: 'sports_handball' }, { k: 'poi_pharmacies', t: ['amenity:pharmacy'], i: 'local_pharmacy' }, { k: 'poi_playgrounds', t: ['leisure:playground'], i: 'sports_handball' }, - { k: 'poi_police', t: ['amenity:police '], i: 'police' }, + { k: 'poi_police', t: ['amenity:police'], i: 'police' }, // important to have this before "post" { k: 'poi_post_box', @@ -153,7 +153,7 @@ export class AddressParseResult { { k: 'poi_super_markets', t: ['shop:supermarket', 'building:supermarket'], i: 'store' }, { k: 'poi_toilets', t: ['amenity:toilets'], i: 'home_and_garden' }, { k: 'poi_tourism', t: ['tourism'], i: 'luggage' }, - { k: 'poi_water', t: ['amenity:drinking_water '], i: 'water_drop' }, + { k: 'poi_water', t: ['amenity:drinking_water'], i: 'water_drop' }, { k: 'poi_charging_station', t: ['amenity:charging_station'], i: 'charger' }, ].map(v => { const tags = v.t.map(val => { diff --git a/src/translation/tr.json b/src/translation/tr.json index 75ef6f5e..8299d457 100644 --- a/src/translation/tr.json +++ b/src/translation/tr.json @@ -139,7 +139,8 @@ "poi_post_box":"mailbox, post box, letterbox, letter box", "poi_shopping":"shops, shop, shopping", "poi_toilets":"toilets, toilet", -"poi_charging_station":"charging stations, charging station, charger" +"poi_charging_station":"charging stations, charging station, charging, charger", +"poi_water":"water" }, "ar":{ "total_ascend":"%1$s اجمالى صعود", @@ -282,7 +283,8 @@ "poi_post_box":"", "poi_shopping":"", "poi_toilets":"", -"poi_charging_station":"" +"poi_charging_station":"", +"poi_water":"" }, "ast":{ "total_ascend":"%1$s d'ascensu total", @@ -425,7 +427,8 @@ "poi_post_box":"", "poi_shopping":"", "poi_toilets":"", -"poi_charging_station":"" +"poi_charging_station":"", +"poi_water":"" }, "az":{ "total_ascend":"%1$s yüksəliş", @@ -568,7 +571,8 @@ "poi_post_box":"", "poi_shopping":"", "poi_toilets":"", -"poi_charging_station":"" +"poi_charging_station":"", +"poi_water":"" }, "bg":{ "total_ascend":"%1$s общо изкачване", @@ -711,7 +715,8 @@ "poi_post_box":"", "poi_shopping":"", "poi_toilets":"", -"poi_charging_station":"" +"poi_charging_station":"", +"poi_water":"" }, "bn_BN":{ "total_ascend":"", @@ -854,7 +859,8 @@ "poi_post_box":"", "poi_shopping":"", "poi_toilets":"", -"poi_charging_station":"" +"poi_charging_station":"", +"poi_water":"" }, "ca":{ "total_ascend":"%1$s de pujada total", @@ -997,7 +1003,8 @@ "poi_post_box":"", "poi_shopping":"", "poi_toilets":"", -"poi_charging_station":"" +"poi_charging_station":"", +"poi_water":"" }, "cs_CZ":{ "total_ascend":"celkové stoupání %1$s", @@ -1140,7 +1147,8 @@ "poi_post_box":"", "poi_shopping":"", "poi_toilets":"", -"poi_charging_station":"" +"poi_charging_station":"", +"poi_water":"" }, "da_DK":{ "total_ascend":"%1$s samlet stigning", @@ -1283,7 +1291,8 @@ "poi_post_box":"", "poi_shopping":"", "poi_toilets":"", -"poi_charging_station":"" +"poi_charging_station":"", +"poi_water":"" }, "de_DE":{ "total_ascend":"%1$s Gesamtaufstieg", @@ -1400,7 +1409,7 @@ "back":"Zurück", "poi_removal_words":"der, dem, gebiet, in, karte, lokal, lokale, nähe", "poi_nearby":"%1$s in der Nähe", -"poi_in":"", +"poi_in":"%1$s in %2$s", "poi_airports":"Flughäfen, Flughafen", "poi_banks":"Bank", "poi_bus_stops":"Haltestellen, Haltestelle, Bushaltestellen, Bushaltestelle", @@ -1426,7 +1435,8 @@ "poi_post_box":"Briefkästen, Briefkasten", "poi_shopping":"Einkaufen, Einkauf", "poi_toilets":"Toiletten, Toilette, WC", -"poi_charging_station":"Ladestation, Ladesäulen, Ladesäule" +"poi_charging_station":"Ladestation, Ladesäulen, Ladesäule, aufladen", +"poi_water":"Wasser, Wasserspender" }, "el":{ "total_ascend":"%1$s συνολική ανάβαση", @@ -1569,7 +1579,8 @@ "poi_post_box":"", "poi_shopping":"", "poi_toilets":"", -"poi_charging_station":"" +"poi_charging_station":"", +"poi_water":"" }, "eo":{ "total_ascend":"%1$s supreniro tute", @@ -1712,7 +1723,8 @@ "poi_post_box":"", "poi_shopping":"", "poi_toilets":"", -"poi_charging_station":"" +"poi_charging_station":"", +"poi_water":"" }, "es":{ "total_ascend":"Ascender %1$s en total", @@ -1855,7 +1867,8 @@ "poi_post_box":"", "poi_shopping":"", "poi_toilets":"", -"poi_charging_station":"" +"poi_charging_station":"", +"poi_water":"" }, "fa":{ "total_ascend":"مجموع صعود %1$s", @@ -1998,7 +2011,8 @@ "poi_post_box":"", "poi_shopping":"", "poi_toilets":"", -"poi_charging_station":"" +"poi_charging_station":"", +"poi_water":"" }, "fil":{ "total_ascend":"", @@ -2141,7 +2155,8 @@ "poi_post_box":"", "poi_shopping":"", "poi_toilets":"", -"poi_charging_station":"" +"poi_charging_station":"", +"poi_water":"" }, "fi":{ "total_ascend":"nousu yhteensä %1$s", @@ -2284,7 +2299,8 @@ "poi_post_box":"", "poi_shopping":"", "poi_toilets":"", -"poi_charging_station":"" +"poi_charging_station":"", +"poi_water":"" }, "fr_FR":{ "total_ascend":"%1$s de dénivelé positif", @@ -2427,7 +2443,8 @@ "poi_post_box":"", "poi_shopping":"", "poi_toilets":"", -"poi_charging_station":"" +"poi_charging_station":"", +"poi_water":"" }, "fr_CH":{ "total_ascend":"%1$s de dénivelé positif", @@ -2570,7 +2587,8 @@ "poi_post_box":"", "poi_shopping":"", "poi_toilets":"", -"poi_charging_station":"" +"poi_charging_station":"", +"poi_water":"" }, "gl":{ "total_ascend":"", @@ -2713,7 +2731,8 @@ "poi_post_box":"", "poi_shopping":"", "poi_toilets":"", -"poi_charging_station":"" +"poi_charging_station":"", +"poi_water":"" }, "he":{ "total_ascend":"עלייה כוללת של %1$s", @@ -2856,7 +2875,8 @@ "poi_post_box":"", "poi_shopping":"", "poi_toilets":"", -"poi_charging_station":"" +"poi_charging_station":"", +"poi_water":"" }, "hr_HR":{ "total_ascend":"%1$s ukupni uspon", @@ -2999,7 +3019,8 @@ "poi_post_box":"", "poi_shopping":"", "poi_toilets":"", -"poi_charging_station":"" +"poi_charging_station":"", +"poi_water":"" }, "hsb":{ "total_ascend":"", @@ -3142,7 +3163,8 @@ "poi_post_box":"", "poi_shopping":"", "poi_toilets":"", -"poi_charging_station":"" +"poi_charging_station":"", +"poi_water":"" }, "hu_HU":{ "total_ascend":"Összes szintemelkedés: %1$s", @@ -3285,7 +3307,8 @@ "poi_post_box":"", "poi_shopping":"", "poi_toilets":"mosdók, mosdó", -"poi_charging_station":"" +"poi_charging_station":"", +"poi_water":"" }, "in_ID":{ "total_ascend":"naik dengan jarak %1$s", @@ -3428,7 +3451,8 @@ "poi_post_box":"", "poi_shopping":"", "poi_toilets":"", -"poi_charging_station":"" +"poi_charging_station":"", +"poi_water":"" }, "it":{ "total_ascend":"%1$s di dislivello positivo", @@ -3571,7 +3595,8 @@ "poi_post_box":"", "poi_shopping":"", "poi_toilets":"", -"poi_charging_station":"" +"poi_charging_station":"", +"poi_water":"" }, "ja":{ "total_ascend":"", @@ -3714,7 +3739,8 @@ "poi_post_box":"", "poi_shopping":"", "poi_toilets":"", -"poi_charging_station":"" +"poi_charging_station":"", +"poi_water":"" }, "ko":{ "total_ascend":"오르막길 총 %1$s", @@ -3857,7 +3883,8 @@ "poi_post_box":"", "poi_shopping":"", "poi_toilets":"", -"poi_charging_station":"" +"poi_charging_station":"", +"poi_water":"" }, "kz":{ "total_ascend":"%1$s көтерілу", @@ -4000,7 +4027,8 @@ "poi_post_box":"", "poi_shopping":"", "poi_toilets":"", -"poi_charging_station":"" +"poi_charging_station":"", +"poi_water":"" }, "lt_LT":{ "total_ascend":"", @@ -4143,7 +4171,8 @@ "poi_post_box":"", "poi_shopping":"", "poi_toilets":"", -"poi_charging_station":"" +"poi_charging_station":"", +"poi_water":"" }, "nb_NO":{ "total_ascend":"%1$s totale høydemeter", @@ -4286,7 +4315,8 @@ "poi_post_box":"", "poi_shopping":"", "poi_toilets":"", -"poi_charging_station":"" +"poi_charging_station":"", +"poi_water":"" }, "ne":{ "total_ascend":"", @@ -4429,7 +4459,8 @@ "poi_post_box":"", "poi_shopping":"", "poi_toilets":"", -"poi_charging_station":"" +"poi_charging_station":"", +"poi_water":"" }, "nl":{ "total_ascend":"%1$s totale klim", @@ -4572,7 +4603,8 @@ "poi_post_box":"", "poi_shopping":"", "poi_toilets":"", -"poi_charging_station":"" +"poi_charging_station":"", +"poi_water":"" }, "pl_PL":{ "total_ascend":"%1$s w górę", @@ -4715,7 +4747,8 @@ "poi_post_box":"", "poi_shopping":"", "poi_toilets":"", -"poi_charging_station":"" +"poi_charging_station":"", +"poi_water":"" }, "pt_BR":{ "total_ascend":"subida de %1$s", @@ -4858,7 +4891,8 @@ "poi_post_box":"", "poi_shopping":"", "poi_toilets":"", -"poi_charging_station":"" +"poi_charging_station":"", +"poi_water":"" }, "pt_PT":{ "total_ascend":"subida de %1$s", @@ -5001,7 +5035,8 @@ "poi_post_box":"", "poi_shopping":"", "poi_toilets":"", -"poi_charging_station":"" +"poi_charging_station":"", +"poi_water":"" }, "ro":{ "total_ascend":"urcare %1$s", @@ -5144,7 +5179,8 @@ "poi_post_box":"", "poi_shopping":"", "poi_toilets":"", -"poi_charging_station":"" +"poi_charging_station":"", +"poi_water":"" }, "ru":{ "total_ascend":"подъём на %1$s", @@ -5287,7 +5323,8 @@ "poi_post_box":"", "poi_shopping":"", "poi_toilets":"", -"poi_charging_station":"" +"poi_charging_station":"", +"poi_water":"" }, "sk":{ "total_ascend":"%1$s celkové stúpanie", @@ -5430,7 +5467,8 @@ "poi_post_box":"", "poi_shopping":"", "poi_toilets":"", -"poi_charging_station":"" +"poi_charging_station":"", +"poi_water":"" }, "sl_SI":{ "total_ascend":"Skupni vzpon: %1$s", @@ -5573,7 +5611,8 @@ "poi_post_box":"", "poi_shopping":"", "poi_toilets":"", -"poi_charging_station":"" +"poi_charging_station":"", +"poi_water":"" }, "sr_RS":{ "total_ascend":"%1$s ukupni uspon", @@ -5716,7 +5755,8 @@ "poi_post_box":"", "poi_shopping":"", "poi_toilets":"", -"poi_charging_station":"" +"poi_charging_station":"", +"poi_water":"" }, "sv_SE":{ "total_ascend":"%1$s stigning", @@ -5859,7 +5899,8 @@ "poi_post_box":"", "poi_shopping":"", "poi_toilets":"", -"poi_charging_station":"" +"poi_charging_station":"", +"poi_water":"" }, "tr":{ "total_ascend":"%1$s toplam tırmanış", @@ -6002,7 +6043,8 @@ "poi_post_box":"", "poi_shopping":"", "poi_toilets":"", -"poi_charging_station":"" +"poi_charging_station":"", +"poi_water":"" }, "uk":{ "total_ascend":"%1$s загалом підйому", @@ -6145,7 +6187,8 @@ "poi_post_box":"", "poi_shopping":"", "poi_toilets":"", -"poi_charging_station":"" +"poi_charging_station":"", +"poi_water":"" }, "uz":{ "total_ascend":"%1$s ga ko'tarilish", @@ -6288,7 +6331,8 @@ "poi_post_box":"", "poi_shopping":"", "poi_toilets":"", -"poi_charging_station":"" +"poi_charging_station":"", +"poi_water":"" }, "vi_VN":{ "total_ascend":"Đi tiếp %1$s nữa", @@ -6431,7 +6475,8 @@ "poi_post_box":"", "poi_shopping":"", "poi_toilets":"", -"poi_charging_station":"" +"poi_charging_station":"", +"poi_water":"" }, "zh_CN":{ "total_ascend":"总上升 %1$s", @@ -6574,7 +6619,8 @@ "poi_post_box":"", "poi_shopping":"", "poi_toilets":"", -"poi_charging_station":"" +"poi_charging_station":"", +"poi_water":"" }, "zh_HK":{ "total_ascend":"總共上昇 %1$s", @@ -6717,7 +6763,8 @@ "poi_post_box":"", "poi_shopping":"", "poi_toilets":"", -"poi_charging_station":"" +"poi_charging_station":"", +"poi_water":"" }, "zh_TW":{ "total_ascend":"總共上昇 %1$s", @@ -6860,5 +6907,6 @@ "poi_post_box":"", "poi_shopping":"", "poi_toilets":"", -"poi_charging_station":"" +"poi_charging_station":"", +"poi_water":"" }} \ No newline at end of file From 929bd57137d773fe930c5d0a77d995a70452c657 Mon Sep 17 00:00:00 2001 From: Peter Date: Mon, 8 Jul 2024 00:30:02 +0200 Subject: [PATCH 24/33] allow generic query in simplified 'photon' format --- src/api/Api.ts | 1 - src/pois/AddressParseResult.ts | 64 ++++++++++++++++++++++++++--- test/poi/AddressParseResult.test.ts | 12 +++++- 3 files changed, 70 insertions(+), 7 deletions(-) diff --git a/src/api/Api.ts b/src/api/Api.ts index 37cc5a34..7213b237 100644 --- a/src/api/Api.ts +++ b/src/api/Api.ts @@ -148,7 +148,6 @@ export class ApiImpl implements Api { } let queryString = '' - function getContent(p: POIPhrase) {} for (const tag of query.include) { let notStr = '' for (const n of query.not) { diff --git a/src/pois/AddressParseResult.ts b/src/pois/AddressParseResult.ts index f8f7a53b..429defcc 100644 --- a/src/pois/AddressParseResult.ts +++ b/src/pois/AddressParseResult.ts @@ -36,6 +36,9 @@ export class AddressParseResult { const smallWords = AddressParseResult.REMOVE_VALUES // e.g. 'restaurants in this area' or 'restaurants in berlin' const queryTokens: string[] = query.split(' ').filter(token => !smallWords.includes(token)) + const res = AddressParseResult.getGeneric(queryTokens) + if (res.hasPOIs()) return res + const cleanQuery = queryTokens.join(' ') const bigrams: string[] = [] for (let i = 0; i < queryTokens.length - 1; i++) { @@ -70,6 +73,27 @@ export class AddressParseResult { return new AddressParseResult('', { include: [], not: [] }, '', '') } + public static getGeneric(tokens: string[]) { + let locations = [] + + let poiQuery = new POIQuery([], []) + for (const token of tokens) { + const index = token.indexOf(':') + if (token.startsWith('!')) { + if (index < 0) { + poiQuery.not.push(new POIPhrase(token.substring(1), '')) + } else { + poiQuery.not.push(new POIPhrase(token.substring(1, index), token.substring(index + 1))) + } + } else if (index < 0) { + locations.push(token) + } else { + poiQuery.include.push(new POIPhrase(token.substring(0, index), token.substring(index + 1))) + } + } + return new AddressParseResult(locations.join(' '), poiQuery, 'store', poiQuery.toString()) + } + public static handleGeocodingResponse( hits: ReverseGeocodingHit[], parseResult: AddressParseResult, @@ -157,23 +181,53 @@ export class AddressParseResult { { k: 'poi_charging_station', t: ['amenity:charging_station'], i: 'charger' }, ].map(v => { const tags = v.t.map(val => { - return { k: val.split(':')[0], v: val.split(':')[1] } + return new POIPhrase(val.split(':')[0], val.split(':')[1]) }) const notTags = !v.not ? [] : v.not.map(val => { - return { k: val.split(':')[0], v: val.split(':')[1] } + return new POIPhrase(val.split(':')[0], val.split(':')[1]) }) return { k: t(v.k), q: tags, i: v.i, not: notTags, - } + } as PoiTriggerPhrases }) } } -export type POIQuery = { include: POIPhrase[]; not: POIPhrase[] } -export type POIPhrase = { k: string; v: string } +export class POIQuery { + public include: POIPhrase[] + public not: POIPhrase[] + + constructor(include: POIPhrase[], not: POIPhrase[]) { + this.include = include + this.not = not + } + + toString(): string { + return ( + this.include.map(p => p.toString()).join(' ') + + ' ' + + this.not.map(p => '!' + p.toString()).join(' ') + ).trim() + } +} + +export class POIPhrase { + public k: string + public v: string + + constructor(k: string, v: string) { + this.k = k + this.v = v + } + + toString(): string { + return this.k + ':' + this.v + } +} + export type PoiTriggerPhrases = { k: string[]; q: POIPhrase[]; i: string; not: POIPhrase[] } diff --git a/test/poi/AddressParseResult.test.ts b/test/poi/AddressParseResult.test.ts index 3fa1b7a2..1052a2b3 100644 --- a/test/poi/AddressParseResult.test.ts +++ b/test/poi/AddressParseResult.test.ts @@ -1,4 +1,4 @@ -import { AddressParseResult } from '@/pois/AddressParseResult' +import { AddressParseResult, POIPhrase } from '@/pois/AddressParseResult' import fetchMock from 'jest-fetch-mock' import { getTranslation, setTranslation } from '@/translation/Translation' @@ -41,4 +41,14 @@ describe('reverse geocoder', () => { expect(res.location).toEqual('') expect(res.poi).toEqual('restaurants') }) + + it('should parse generic', async () => { + let res = AddressParseResult.parse('dresden amenity:bar', false) + expect(res.location).toEqual('dresden') + expect(res.query.toString()).toEqual('amenity:bar') + + res = AddressParseResult.parse('dresden !amenity:bar military:', false) + expect(res.location).toEqual('dresden') + expect(res.query.toString()).toEqual('military: !amenity:bar') // include comes first in toString + }) }) From 8177b7329e03a91e63e006565a03bc8d4b97df22 Mon Sep 17 00:00:00 2001 From: Peter Date: Thu, 18 Jul 2024 00:56:17 +0200 Subject: [PATCH 25/33] workaround for initial search --- src/NavBar.ts | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/NavBar.ts b/src/NavBar.ts index 061445ad..63781054 100644 --- a/src/NavBar.ts +++ b/src/NavBar.ts @@ -7,6 +7,7 @@ import QueryStore, { getBBoxFromCoord, QueryPoint, QueryPointType, QueryStoreSta import MapOptionsStore, { MapOptionsStoreState } from './stores/MapOptionsStore' import { ApiImpl, getApi } from '@/api/Api' import { AddressParseResult } from '@/pois/AddressParseResult' +import { getQueryStore } from '@/stores/Stores' export default class NavBar { private readonly queryStore: QueryStore @@ -113,15 +114,21 @@ export default class NavBar { const result = AddressParseResult.parse(p.queryText, false) if (result.hasPOIs() && result.location) { // two stage POI search: 1. use extracted location to get coordinates 2. do reverse geocoding with this coordinates - getApi() + return getApi() .geocode(result.location, 'nominatim') .then(res => { - if (res.hits.length != 0) - getApi() - .reverseGeocode(result.query, res.hits[0].extent) - .then(res => AddressParseResult.handleGeocodingResponse(res, result, p)) + if (res.hits.length == 0) Promise.resolve(p) + const qp = { + ...p, + id: getQueryStore().state.nextQueryPointId, // TODO hacky + color: QueryStore.getMarkerColor(QueryPointType.From), + type: QueryPointType.From, + } + getApi() + .reverseGeocode(result.query, res.hits[0].extent) + .then(res => AddressParseResult.handleGeocodingResponse(res, result, qp)) + return qp }) - return Promise.resolve(p) } return ( getApi() @@ -136,7 +143,7 @@ export default class NavBar { } }) // if the geocoding request fails we just keep the point as it is, just as if no results were found - .catch(() => Promise.resolve(p)) + .catch(() => p) ) }) const points = await Promise.all(promises) From a897bd69a1d9a37a0e38b7a229258f995e6e7efc Mon Sep 17 00:00:00 2001 From: Peter Date: Thu, 18 Jul 2024 01:06:25 +0200 Subject: [PATCH 26/33] bug fix in merge --- src/sidebar/search/AddressInput.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sidebar/search/AddressInput.tsx b/src/sidebar/search/AddressInput.tsx index e246a049..64aec477 100644 --- a/src/sidebar/search/AddressInput.tsx +++ b/src/sidebar/search/AddressInput.tsx @@ -59,12 +59,12 @@ export default function AddressInput(props: AddressInputProps) { hits.forEach(hit => { const obj = hitToItem(hit) - return new GeocodingItem( + items.push(new GeocodingItem( obj.mainText, obj.secondText, hit.point, hit.extent ? hit.extent : getBBoxFromCoord(hit.point) - ) + )) }) // TODO autocompleteItems is empty here because query point changed from outside somehow From 6b5624e9f6a5b2c665829a5a0fefb2ed9e2f5a23 Mon Sep 17 00:00:00 2001 From: Peter Date: Sun, 21 Jul 2024 22:32:44 +0200 Subject: [PATCH 27/33] support simple 'or' and 'and' queries with 'not'. use more overpass wizard query style --- src/api/Api.ts | 15 ++- src/layers/POIPopup.tsx | 4 +- src/pois/AddressParseResult.ts | 175 ++++++++++++++++------------ src/sidebar/search/AddressInput.tsx | 14 ++- test/DummyApi.ts | 2 +- test/poi/AddressParseResult.test.ts | 16 ++- test/stores/QueryStore.test.ts | 2 +- test/stores/RouteStore.test.ts | 4 +- 8 files changed, 131 insertions(+), 101 deletions(-) diff --git a/src/api/Api.ts b/src/api/Api.ts index 7213b237..7fe7d26b 100644 --- a/src/api/Api.ts +++ b/src/api/Api.ts @@ -18,7 +18,7 @@ import { LineString } from 'geojson' import { getTranslation, tr } from '@/translation/Translation' import * as config from 'config' import { Coordinate } from '@/stores/QueryStore' -import { POIPhrase, POIQuery } from '@/pois/AddressParseResult' +import { POIPhrase, POIAndQuery, POIQuery } from '@/pois/AddressParseResult' interface ApiProfile { name: string @@ -148,14 +148,13 @@ export class ApiImpl implements Api { } let queryString = '' - for (const tag of query.include) { - let notStr = '' - for (const n of query.not) { - notStr += n.v ? `["${n.k}"!="${n.v}"]` : `["${n.k}"!~".*"]` - } - const value = tag.v ? `="${tag.v}"` : '' + for (const q of query.queries) { // nwr means it searches for nodes, ways and relations - queryString += `nwr["${tag.k}"${value}]${notStr};\n` + queryString += 'nwr' + for (const p of q.phrases) { + queryString += `["${p.k}"${p.sign}"${p.v}"]` + } + queryString += `;\n` } try { diff --git a/src/layers/POIPopup.tsx b/src/layers/POIPopup.tsx index 557e2b6a..0a536493 100644 --- a/src/layers/POIPopup.tsx +++ b/src/layers/POIPopup.tsx @@ -28,8 +28,8 @@ function POITable(props: { poi: POI }) { ? 'https://' + valueArr[0] + '.wikipedia.org/wiki/' + encodeURIComponent(valueArr[1]) : '' // tags like amenity:restaurant should not be shown if it is a restaurant (determined by poi.tags) - const poiInfoRepeated = props.poi.query.include - ? props.poi.query.include.some(q => q.k == key && q.v === value) + const poiInfoRepeated = props.poi.query.queries + ? props.poi.query.queries.some(q => q.phrases.some(q => q.k == key && q.v === value)) : false return ( !poiInfoRepeated && diff --git a/src/pois/AddressParseResult.ts b/src/pois/AddressParseResult.ts index 429defcc..3e8a7110 100644 --- a/src/pois/AddressParseResult.ts +++ b/src/pois/AddressParseResult.ts @@ -23,7 +23,7 @@ export class AddressParseResult { } hasPOIs(): boolean { - return this.query.include.length > 0 + return this.query.queries.length > 0 } text(prefix: string) { @@ -50,47 +50,51 @@ export class AddressParseResult { for (const keyword of val.k) { const i = bigrams.indexOf(keyword) if (i >= 0) - return new AddressParseResult( - cleanQuery.replace(bigrams[i], '').trim(), - { include: val.q, not: val.not }, - val.i, - val.k[0] - ) + return new AddressParseResult(cleanQuery.replace(bigrams[i], '').trim(), val.q, val.i, val.k[0]) } for (const keyword of val.k) { const i = queryTokens.indexOf(keyword) if (i >= 0) - return new AddressParseResult( - cleanQuery.replace(queryTokens[i], '').trim(), - { include: val.q, not: val.not }, - val.i, - val.k[0] - ) + return new AddressParseResult(cleanQuery.replace(queryTokens[i], '').trim(), val.q, val.i, val.k[0]) } } - return new AddressParseResult('', { include: [], not: [] }, '', '') + return new AddressParseResult('', new POIQuery([]), '', '') } public static getGeneric(tokens: string[]) { - let locations = [] - - let poiQuery = new POIQuery([], []) + const locations = [] + const orPhrases = [] + const notPhrases = [] + const singleAndQuery = new POIQuery([new POIAndQuery([])]) + const singleAnd = tokens.includes('and') for (const token of tokens) { - const index = token.indexOf(':') - if (token.startsWith('!')) { - if (index < 0) { - poiQuery.not.push(new POIPhrase(token.substring(1), '')) - } else { - poiQuery.not.push(new POIPhrase(token.substring(1, index), token.substring(index + 1))) - } - } else if (index < 0) { - locations.push(token) + const indexNot = token.indexOf('!') + const index = token.indexOf('=') + if (indexNot >= 0) { + const sign = token.includes('~') ? '!~' : '!=' + const p = new POIPhrase(token.substring(0, indexNot), sign, token.substring(indexNot + 2)) + singleAndQuery.queries[0].phrases.push(p) + notPhrases.push(p) + } else if (index >= 0) { + const p = new POIPhrase(token.substring(0, index), '=', token.substring(index + 1)) + singleAndQuery.queries[0].phrases.push(p) + orPhrases.push(p) + } else if (token == 'and') { } else { - poiQuery.include.push(new POIPhrase(token.substring(0, index), token.substring(index + 1))) + locations.push(token) } } + + if (singleAnd) + return new AddressParseResult(locations.join(' '), singleAndQuery, 'store', singleAndQuery.toString()) + + const queries = [] + for (const p of orPhrases) { + queries.push(new POIAndQuery([p, ...notPhrases])) + } + const poiQuery = new POIQuery(queries) return new AddressParseResult(locations.join(' '), poiQuery, 'store', poiQuery.toString()) } @@ -99,7 +103,10 @@ export class AddressParseResult { parseResult: AddressParseResult, queryPoint: QueryPoint ) { - if (hits.length == 0) return + if (hits.length == 0) { + Dispatcher.dispatch(new SetPOIs([], null)) + return + } const pois = hits .filter(hit => !!hit.point) .map(hit => { @@ -143,91 +150,105 @@ export class AddressParseResult { .map(s => s.trim().toLowerCase()) AddressParseResult.REMOVE_VALUES = t('poi_removal_words') AddressParseResult.TRIGGER_VALUES = [ - { k: 'poi_airports', t: ['aeroway:aerodrome'], i: 'flight_takeoff', not: ['military', 'landuse:military'] }, - { k: 'poi_atm', t: ['amenity:atm', 'amenity:bank'], i: 'local_atm' }, - { k: 'poi_banks', t: ['amenity:bank'], i: 'universal_currency_alt' }, - { k: 'poi_bus_stops', t: ['highway:bus_stop'], i: 'train' }, - { k: 'poi_education', t: ['amenity:school', 'building:school', 'building:university'], i: 'school' }, - { k: 'poi_gas_station', t: ['amenity:fuel'], i: 'local_gas_station' }, - { k: 'poi_hospitals', t: ['amenity:hospital', 'building:hospital'], i: 'local_hospital' }, - { k: 'poi_hotels', t: ['amenity:hotel', 'building:hotel', 'tourism:hotel'], i: 'hotel' }, - { k: 'poi_leisure', t: ['leisure'], i: 'sports_handball' }, - { k: 'poi_museums', t: ['tourism:museum', 'building:museum'], i: 'museum' }, - { k: 'poi_parking', t: ['amenity:parking'], i: 'local_parking' }, - { k: 'poi_parks', t: ['leisure:park'], i: 'sports_handball' }, - { k: 'poi_pharmacies', t: ['amenity:pharmacy'], i: 'local_pharmacy' }, - { k: 'poi_playgrounds', t: ['leisure:playground'], i: 'sports_handball' }, - { k: 'poi_police', t: ['amenity:police'], i: 'police' }, + { k: 'poi_airports', q: ['aeroway=aerodrome and landuse!=military and military!~.*'], i: 'flight_takeoff' }, + { k: 'poi_atm', q: ['amenity=atm', 'amenity=bank'], i: 'local_atm' }, + { k: 'poi_banks', q: ['amenity=bank'], i: 'universal_currency_alt' }, + { k: 'poi_bus_stops', q: ['highway=bus_stop'], i: 'train' }, + { k: 'poi_education', q: ['amenity=school', 'building=school', 'building=university'], i: 'school' }, + { k: 'poi_gas_station', q: ['amenity=fuel'], i: 'local_gas_station' }, + { k: 'poi_hospitals', q: ['amenity=hospital', 'building=hospital'], i: 'local_hospital' }, + { k: 'poi_hotels', q: ['amenity=hotel', 'building=hotel', 'tourism=hotel'], i: 'hotel' }, + { k: 'poi_leisure', q: ['leisure=*'], i: 'sports_handball' }, + { k: 'poi_museums', q: ['tourism=museum', 'building=museum'], i: 'museum' }, + { k: 'poi_parking', q: ['amenity=parking'], i: 'local_parking' }, + { k: 'poi_parks', q: ['leisure=park'], i: 'sports_handball' }, + { k: 'poi_pharmacies', q: ['amenity=pharmacy'], i: 'local_pharmacy' }, + { k: 'poi_playgrounds', q: ['leisure=playground'], i: 'sports_handball' }, + { k: 'poi_police', q: ['amenity=police'], i: 'police' }, // important to have this before "post" { k: 'poi_post_box', - t: ['amenity:post_box', 'amenity:post_office', 'amenity:post_depot'], + q: ['amenity=post_box', 'amenity=post_office', 'amenity=post_depot'], i: 'local_post_office', }, - { k: 'poi_post', t: ['amenity:post_office', 'amenity:post_depot'], i: 'local_post_office' }, + { k: 'poi_post', q: ['amenity=post_office', 'amenity=post_depot'], i: 'local_post_office' }, { k: 'poi_public_transit', - t: ['public_transport:station', 'railway:station', 'highway:bus_stop'], + q: ['public_transport=station', 'railway=station', 'highway=bus_stop'], i: 'train', }, - { k: 'poi_railway_station', t: ['railway:station', 'railway:halt'], i: 'train' }, - { k: 'poi_restaurants', t: ['amenity:restaurant'], i: 'restaurant' }, - { k: 'poi_schools', t: ['amenity:school', 'building:school'], i: 'school' }, - { k: 'poi_shopping', t: ['shop'], i: 'store' }, - { k: 'poi_super_markets', t: ['shop:supermarket', 'building:supermarket'], i: 'store' }, - { k: 'poi_toilets', t: ['amenity:toilets'], i: 'home_and_garden' }, - { k: 'poi_tourism', t: ['tourism'], i: 'luggage' }, - { k: 'poi_water', t: ['amenity:drinking_water'], i: 'water_drop' }, - { k: 'poi_charging_station', t: ['amenity:charging_station'], i: 'charger' }, + { k: 'poi_railway_station', q: ['railway=station', 'railway=halt'], i: 'train' }, + { k: 'poi_restaurants', q: ['amenity=restaurant'], i: 'restaurant' }, + { k: 'poi_schools', q: ['amenity=school', 'building=school'], i: 'school' }, + { k: 'poi_shopping', q: ['shop=*'], i: 'store' }, + { k: 'poi_super_markets', q: ['shop=supermarket', 'building=supermarket'], i: 'store' }, + { k: 'poi_toilets', q: ['amenity=toilets'], i: 'home_and_garden' }, + { k: 'poi_tourism', q: ['tourism=*'], i: 'luggage' }, + { k: 'poi_water', q: ['amenity=drinking_water'], i: 'water_drop' }, + { k: 'poi_charging_station', q: ['amenity=charging_station'], i: 'charger' }, ].map(v => { - const tags = v.t.map(val => { - return new POIPhrase(val.split(':')[0], val.split(':')[1]) + const queries = v.q.map(val => { + return new POIAndQuery( + val.split(' and ').map(v => { + let kv = v.split('!=') + if (kv.length > 1) return new POIPhrase(kv[0], '!=', kv[1]) + kv = v.split('!~') + if (kv.length > 1) return new POIPhrase(kv[0], '!~', kv[1]) + kv = v.split('=') + return new POIPhrase(kv[0], '=', kv[1]) + }) + ) }) - const notTags = !v.not - ? [] - : v.not.map(val => { - return new POIPhrase(val.split(':')[0], val.split(':')[1]) - }) return { k: t(v.k), - q: tags, + q: new POIQuery(queries), i: v.i, - not: notTags, } as PoiTriggerPhrases }) } } export class POIQuery { - public include: POIPhrase[] - public not: POIPhrase[] + public queries: POIAndQuery[] + + constructor(queries: POIAndQuery[]) { + this.queries = queries + } + + toString(): string { + return this.queries.join(' ') + } +} + +export class POIAndQuery { + public phrases: POIPhrase[] - constructor(include: POIPhrase[], not: POIPhrase[]) { - this.include = include - this.not = not + constructor(phrases: POIPhrase[]) { + this.phrases = phrases } toString(): string { - return ( - this.include.map(p => p.toString()).join(' ') + - ' ' + - this.not.map(p => '!' + p.toString()).join(' ') - ).trim() + return this.phrases + .map(p => p.toString()) + .join(' and ') + .trim() } } export class POIPhrase { public k: string + public sign: string public v: string - constructor(k: string, v: string) { + constructor(k: string, sign: string, v: string) { this.k = k + this.sign = sign this.v = v } toString(): string { - return this.k + ':' + this.v + return this.k + this.sign + this.v } } -export type PoiTriggerPhrases = { k: string[]; q: POIPhrase[]; i: string; not: POIPhrase[] } +export type PoiTriggerPhrases = { k: string[]; q: POIQuery; i: string } diff --git a/src/sidebar/search/AddressInput.tsx b/src/sidebar/search/AddressInput.tsx index 64aec477..bbe3a5a7 100644 --- a/src/sidebar/search/AddressInput.tsx +++ b/src/sidebar/search/AddressInput.tsx @@ -59,12 +59,14 @@ export default function AddressInput(props: AddressInputProps) { hits.forEach(hit => { const obj = hitToItem(hit) - items.push(new GeocodingItem( - obj.mainText, - obj.secondText, - hit.point, - hit.extent ? hit.extent : getBBoxFromCoord(hit.point) - )) + items.push( + new GeocodingItem( + obj.mainText, + obj.secondText, + hit.point, + hit.extent ? hit.extent : getBBoxFromCoord(hit.point) + ) + ) }) // TODO autocompleteItems is empty here because query point changed from outside somehow diff --git a/test/DummyApi.ts b/test/DummyApi.ts index 279edbde..6c63dfc1 100644 --- a/test/DummyApi.ts +++ b/test/DummyApi.ts @@ -8,7 +8,7 @@ import { RoutingResultInfo, Bbox, } from '../src/api/graphhopper' -import { POIQuery } from '@/pois/AddressParseResult' +import { POIAndQuery, POIQuery } from '@/pois/AddressParseResult' export default class DummyApi implements Api { geocode(query: string): Promise { diff --git a/test/poi/AddressParseResult.test.ts b/test/poi/AddressParseResult.test.ts index 1052a2b3..d6711954 100644 --- a/test/poi/AddressParseResult.test.ts +++ b/test/poi/AddressParseResult.test.ts @@ -43,12 +43,20 @@ describe('reverse geocoder', () => { }) it('should parse generic', async () => { - let res = AddressParseResult.parse('dresden amenity:bar', false) + let res = AddressParseResult.parse('dresden amenity=bar', false) expect(res.location).toEqual('dresden') - expect(res.query.toString()).toEqual('amenity:bar') + expect(res.query.toString()).toEqual('amenity=bar') - res = AddressParseResult.parse('dresden !amenity:bar military:', false) + res = AddressParseResult.parse('dresden amenity=bar military!~.*', false) expect(res.location).toEqual('dresden') - expect(res.query.toString()).toEqual('military: !amenity:bar') // include comes first in toString + expect(res.query.toString()).toEqual('amenity=bar and military!~.*') + + res = AddressParseResult.parse('amenity=restaurant and wheelchair=yes in dresden', false) + expect(res.location).toEqual('dresden') + expect(res.query.toString()).toEqual('amenity=restaurant and wheelchair=yes') + + // no "select query", only 'not' queries => leads currently to no match + res = AddressParseResult.parse('dresden amenity!=bar military!~.*', false) + expect(res.hasPOIs()).toEqual(false) }) }) diff --git a/test/stores/QueryStore.test.ts b/test/stores/QueryStore.test.ts index 78e11793..5410c08f 100644 --- a/test/stores/QueryStore.test.ts +++ b/test/stores/QueryStore.test.ts @@ -20,7 +20,7 @@ import { SetPoint, SetVehicleProfile, } from '@/actions/Actions' -import { POIQuery } from '@/pois/AddressParseResult' +import { POIAndQuery, POIQuery } from '@/pois/AddressParseResult' class ApiMock implements Api { private readonly callback: { (args: RoutingArgs): void } diff --git a/test/stores/RouteStore.test.ts b/test/stores/RouteStore.test.ts index 9c884279..e333d7ee 100644 --- a/test/stores/RouteStore.test.ts +++ b/test/stores/RouteStore.test.ts @@ -1,5 +1,5 @@ import RouteStore from '@/stores/RouteStore' -import QueryStore, { Coordinate, QueryPoint, QueryPointType } from '@/stores/QueryStore' +import { QueryPoint, QueryPointType } from '@/stores/QueryStore' import Api from '@/api/Api' import { ApiInfo, @@ -12,7 +12,7 @@ import { } from '@/api/graphhopper' import Dispatcher, { Action } from '@/stores/Dispatcher' import { ClearPoints, ClearRoute, RemovePoint, SetPoint, SetSelectedPath } from '@/actions/Actions' -import { POIQuery } from '@/pois/AddressParseResult' +import { POIAndQuery, POIQuery } from '@/pois/AddressParseResult' describe('RouteStore', () => { afterEach(() => { From ce8a62bb38fbd2726263d5c0cbe7c22aeabdf4f2 Mon Sep 17 00:00:00 2001 From: Peter Date: Sun, 21 Jul 2024 23:36:32 +0200 Subject: [PATCH 28/33] special case for all value query --- src/api/Api.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/api/Api.ts b/src/api/Api.ts index 7fe7d26b..d7cbda0d 100644 --- a/src/api/Api.ts +++ b/src/api/Api.ts @@ -152,7 +152,8 @@ export class ApiImpl implements Api { // nwr means it searches for nodes, ways and relations queryString += 'nwr' for (const p of q.phrases) { - queryString += `["${p.k}"${p.sign}"${p.v}"]` + if(p.sign == '=' && p.v == '*') queryString += `["${p.k}"]` + else queryString += `["${p.k}"${p.sign}"${p.v}"]` } queryString += `;\n` } From 5edc798d875cf9e6e35d64b413896925ad73d5a7 Mon Sep 17 00:00:00 2001 From: Peter Date: Sat, 10 Aug 2024 19:17:02 +0200 Subject: [PATCH 29/33] avoid POI layer setup in most cases --- src/api/Api.ts | 2 +- src/layers/UsePOIsLayer.tsx | 12 ++++++++---- src/sidebar/search/AddressInput.tsx | 1 - 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/api/Api.ts b/src/api/Api.ts index d7cbda0d..f43aab28 100644 --- a/src/api/Api.ts +++ b/src/api/Api.ts @@ -152,7 +152,7 @@ export class ApiImpl implements Api { // nwr means it searches for nodes, ways and relations queryString += 'nwr' for (const p of q.phrases) { - if(p.sign == '=' && p.v == '*') queryString += `["${p.k}"]` + if (p.sign == '=' && p.v == '*') queryString += `["${p.k}"]` else queryString += `["${p.k}"${p.sign}"${p.v}"]` } queryString += `;\n` diff --git a/src/layers/UsePOIsLayer.tsx b/src/layers/UsePOIsLayer.tsx index 4876eb9b..6e55dbfd 100644 --- a/src/layers/UsePOIsLayer.tsx +++ b/src/layers/UsePOIsLayer.tsx @@ -31,6 +31,8 @@ import { createPOIMarker } from '@/layers/createMarkerSVG' import { Select } from 'ol/interaction' import Dispatcher from '@/stores/Dispatcher' import { SelectPOI } from '@/actions/Actions' +import { ObjectEvent } from 'ol/Object' +import { getMap } from '@/map/map' const svgStrings: { [id: string]: string } = {} @@ -68,11 +70,11 @@ for (const k in svgObjects) { export default function usePOIsLayer(map: Map, poisState: POIsStoreState) { useEffect(() => { removePOIs(map) - addPOIsLayer(map, poisState.pois) - const select = addPOISelection(map) + let select: Select | null = null + if (addPOIsLayer(map, poisState.pois)) select = addPOISelection(map) return () => { removePOIs(map) - map.removeInteraction(select) + if (select) map.removeInteraction(select) } }, [map, poisState.pois]) } @@ -109,6 +111,8 @@ function addPOISelection(map: Map) { } function addPOIsLayer(map: Map, pois: POI[]) { + if (pois.length == 0) return false + const features = pois.map((poi, i) => { const feature = new Feature({ geometry: new Point(fromLonLat([poi.coordinate.lng, poi.coordinate.lat])), @@ -137,5 +141,5 @@ function addPOIsLayer(map: Map, pois: POI[]) { return style }) map.addLayer(poisLayer) - return poisLayer + return true } diff --git a/src/sidebar/search/AddressInput.tsx b/src/sidebar/search/AddressInput.tsx index 17d4dc51..f4ff5dc8 100644 --- a/src/sidebar/search/AddressInput.tsx +++ b/src/sidebar/search/AddressInput.tsx @@ -182,7 +182,6 @@ export default function AddressInput(props: AddressInputProps) { // get the bias point for the geocoder // (the query point above the current one) - const autocompleteIndex = props.points.findIndex(point => !point.isInitialized) const lonlat = toLonLat(getMap().getView().getCenter()!) const biasCoord = { lng: lonlat[0], lat: lonlat[1] } From 6bb713deb55e3f06c93d61b24b9169329077b057 Mon Sep 17 00:00:00 2001 From: Peter Date: Mon, 12 Aug 2024 15:49:42 +0200 Subject: [PATCH 30/33] minor cosmetics --- src/layers/POIPopup.tsx | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/layers/POIPopup.tsx b/src/layers/POIPopup.tsx index 0a536493..48d3ed43 100644 --- a/src/layers/POIPopup.tsx +++ b/src/layers/POIPopup.tsx @@ -27,19 +27,15 @@ function POITable(props: { poi: POI }) { const wikiUrl = wiki ? 'https://' + valueArr[0] + '.wikipedia.org/wiki/' + encodeURIComponent(valueArr[1]) : '' - // tags like amenity:restaurant should not be shown if it is a restaurant (determined by poi.tags) + // tags like amenity:restaurant should not be shown if it is a restaurant const poiInfoRepeated = props.poi.query.queries ? props.poi.query.queries.some(q => q.phrases.some(q => q.k == key && q.v === value)) : false + // prettier-ignore + const showRow = !poiInfoRepeated && key !== 'source' && key !== 'image' && key !== 'check_data' + && !key.includes('fax') && !key.startsWith('addr') && !key.startsWith('name') && !key.startsWith('building') return ( - !poiInfoRepeated && - key !== 'source' && - key !== 'image' && - key !== 'check_data' && - !key.includes('fax') && - !key.startsWith('addr') && - !key.startsWith('name') && - !key.startsWith('building') && ( + showRow && (
{key} From fe1b29dcb4e345c8c7496b69f320e803025ccb94 Mon Sep 17 00:00:00 2001 From: Peter Date: Mon, 12 Aug 2024 16:33:40 +0200 Subject: [PATCH 31/33] simplify and avoid hack. UX is more clear too. --- src/App.tsx | 2 +- src/NavBar.ts | 10 +--- src/actions/Actions.ts | 4 +- src/layers/MapFeaturePopup.module.css | 4 +- src/layers/POIPopup.tsx | 66 ++++++++++++++------------- src/map/MapPopups.tsx | 6 ++- src/pois/AddressParseResult.ts | 7 ++- src/stores/POIsStore.ts | 4 +- 8 files changed, 49 insertions(+), 54 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index e0865131..0e700c9d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -121,7 +121,7 @@ export default function App() { return (
- + {isSmallScreen ? ( { if (res.hits.length == 0) Promise.resolve(p) - const qp = { - ...p, - id: getQueryStore().state.nextQueryPointId, // TODO hacky - color: QueryStore.getMarkerColor(QueryPointType.From), - type: QueryPointType.From, - } getApi() .reverseGeocode(result.query, res.hits[0].extent) - .then(res => AddressParseResult.handleGeocodingResponse(res, result, qp)) - return qp + .then(res => AddressParseResult.handleGeocodingResponse(res, result)) + return p }) } return ( diff --git a/src/actions/Actions.ts b/src/actions/Actions.ts index 1224dc11..73e2c42d 100644 --- a/src/actions/Actions.ts +++ b/src/actions/Actions.ts @@ -258,10 +258,8 @@ export class SelectPOI implements Action { export class SetPOIs implements Action { readonly pois: POI[] - readonly sourceQueryPoint: QueryPoint | null - constructor(pois: POI[], sourceQueryPoint: QueryPoint | null) { + constructor(pois: POI[]) { this.pois = pois - this.sourceQueryPoint = sourceQueryPoint } } diff --git a/src/layers/MapFeaturePopup.module.css b/src/layers/MapFeaturePopup.module.css index 8f42bf0a..ae20b28f 100644 --- a/src/layers/MapFeaturePopup.module.css +++ b/src/layers/MapFeaturePopup.module.css @@ -28,8 +28,8 @@ display: flex; flex-direction: row; gap: 7px; - padding-top: 5px; - padding-bottom: 5px; + padding-top: 12px; + padding-bottom: 8px; } .poiPopupButton svg { diff --git a/src/layers/POIPopup.tsx b/src/layers/POIPopup.tsx index 48d3ed43..0c53e14f 100644 --- a/src/layers/POIPopup.tsx +++ b/src/layers/POIPopup.tsx @@ -1,18 +1,14 @@ import React from 'react' import styles from '@/layers/MapFeaturePopup.module.css' import MapPopup from '@/layers/MapPopup' -import { Map } from 'ol' -import { POI, POIsStoreState } from '@/stores/POIsStore' -import { tr } from '@/translation/Translation' +import {POI, POIsStoreState} from '@/stores/POIsStore' +import {tr} from '@/translation/Translation' import Dispatcher from '@/stores/Dispatcher' -import { SelectPOI, SetPoint, SetPOIs } from '@/actions/Actions' +import {SelectPOI, SetPoint, SetPOIs} from '@/actions/Actions' import PlainButton from '@/PlainButton' -import { MarkerComponent } from '@/map/Marker' - -interface POIStatePopupProps { - map: Map - poiState: POIsStoreState -} +import {MarkerComponent} from '@/map/Marker' +import QueryStore, {QueryPoint, QueryPointType} from "@/stores/QueryStore"; +import {Map} from "ol"; function POITable(props: { poi: POI }) { return ( @@ -62,39 +58,47 @@ function POITable(props: { poi: POI }) { ) } +interface POIStatePopupProps { + map: Map + poiState: POIsStoreState + points: QueryPoint[] +} + /** * The popup shown when certain map features are hovered. For example a road of the routing graph layer. */ -export default function POIStatePopup({ map, poiState }: POIStatePopupProps) { +export default function POIStatePopup({ map, poiState, points }: POIStatePopupProps) { const selectedPOI = poiState.selected - const oldQueryPoint = poiState.oldQueryPoint const type = selectedPOI?.osm_type + function fire(index: number) { + if (selectedPOI && index < points.length) { + const queryPoint = { + ...points[index], + queryText: selectedPOI?.name, + coordinate: selectedPOI?.coordinate, + isInitialized: true, + } + Dispatcher.dispatch(new SetPoint(queryPoint, false)) + Dispatcher.dispatch(new SelectPOI(null)) + Dispatcher.dispatch(new SetPOIs([])) + } + } + return (
{selectedPOI?.name}
{selectedPOI?.address}
-
{ - if (selectedPOI && oldQueryPoint) { - const queryPoint = { - ...oldQueryPoint, - queryText: selectedPOI?.name, - coordinate: selectedPOI?.coordinate, - isInitialized: true, - } - Dispatcher.dispatch(new SetPoint(queryPoint, false)) - Dispatcher.dispatch(new SelectPOI(null)) - Dispatcher.dispatch(new SetPOIs([], null)) - } - }} - > - {oldQueryPoint && } - {tr('Use in route')} +
fire(0)}> + + {tr('As start')} +
+
fire(points.length - 1)}> + + {tr('As destination')}
- {selectedPOI && } + {selectedPOI && }
OpenStreetMap.org diff --git a/src/map/MapPopups.tsx b/src/map/MapPopups.tsx index 39770a31..6f3c20bd 100644 --- a/src/map/MapPopups.tsx +++ b/src/map/MapPopups.tsx @@ -7,15 +7,17 @@ import { PathDetailsStoreState } from '@/stores/PathDetailsStore' import { MapFeatureStoreState } from '@/stores/MapFeatureStore' import { POI, POIsStoreState } from '@/stores/POIsStore' import POIStatePopup from '@/layers/POIPopup' +import {QueryStoreState} from "@/stores/QueryStore"; interface MapPopupProps { map: Map pathDetails: PathDetailsStoreState mapFeatures: MapFeatureStoreState poiState: POIsStoreState + query: QueryStoreState } -export default function MapPopups({ map, pathDetails, mapFeatures, poiState }: MapPopupProps) { +export default function MapPopups({ map, pathDetails, mapFeatures, poiState, query }: MapPopupProps) { return ( <> @@ -29,7 +31,7 @@ export default function MapPopups({ map, pathDetails, mapFeatures, poiState }: M instructionText={mapFeatures.instructionText} coordinate={mapFeatures.instructionCoordinate} /> - + ) } diff --git a/src/pois/AddressParseResult.ts b/src/pois/AddressParseResult.ts index 3e8a7110..0c0dc117 100644 --- a/src/pois/AddressParseResult.ts +++ b/src/pois/AddressParseResult.ts @@ -100,11 +100,10 @@ export class AddressParseResult { public static handleGeocodingResponse( hits: ReverseGeocodingHit[], - parseResult: AddressParseResult, - queryPoint: QueryPoint + parseResult: AddressParseResult ) { if (hits.length == 0) { - Dispatcher.dispatch(new SetPOIs([], null)) + Dispatcher.dispatch(new SetPOIs([])) return } const pois = hits @@ -133,7 +132,7 @@ export class AddressParseResult { const bbox = ApiImpl.getBBoxPoints(pois.map(p => p.coordinate)) if (bbox) { if (parseResult.location) Dispatcher.dispatch(new SetBBox(bbox)) - Dispatcher.dispatch(new SetPOIs(pois, queryPoint)) + Dispatcher.dispatch(new SetPOIs(pois)) } else { console.warn( 'invalid bbox for points ' + JSON.stringify(pois) + ' result was: ' + JSON.stringify(parseResult) diff --git a/src/stores/POIsStore.ts b/src/stores/POIsStore.ts index 4d3b4963..78bb6dfd 100644 --- a/src/stores/POIsStore.ts +++ b/src/stores/POIsStore.ts @@ -19,19 +19,17 @@ export interface POI { export interface POIsStoreState { pois: POI[] selected: POI | null - oldQueryPoint: QueryPoint | null } export default class POIsStore extends Store { constructor() { - super({ pois: [], selected: null, oldQueryPoint: null }) + super({ pois: [], selected: null }) } reduce(state: POIsStoreState, action: Action): POIsStoreState { if (action instanceof SetPOIs) { return { pois: action.pois, - oldQueryPoint: action.sourceQueryPoint, selected: null, } } else if (action instanceof SelectPOI) { From 56df12847d49c6bcbfa98f179325505bcfb7e276 Mon Sep 17 00:00:00 2001 From: Peter Date: Mon, 12 Aug 2024 16:33:55 +0200 Subject: [PATCH 32/33] formatting --- src/App.tsx | 8 +++++++- src/layers/POIPopup.tsx | 18 +++++++++--------- src/map/MapPopups.tsx | 2 +- src/pois/AddressParseResult.ts | 5 +---- 4 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 0e700c9d..1fdf6098 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -121,7 +121,13 @@ export default function App() { return (
- + {isSmallScreen ? ( {selectedPOI?.name}
{selectedPOI?.address}
fire(0)}> - + {tr('As start')}
fire(points.length - 1)}> - + {tr('As destination')}
- {selectedPOI && } + {selectedPOI && }
OpenStreetMap.org diff --git a/src/map/MapPopups.tsx b/src/map/MapPopups.tsx index 6f3c20bd..033be02c 100644 --- a/src/map/MapPopups.tsx +++ b/src/map/MapPopups.tsx @@ -7,7 +7,7 @@ import { PathDetailsStoreState } from '@/stores/PathDetailsStore' import { MapFeatureStoreState } from '@/stores/MapFeatureStore' import { POI, POIsStoreState } from '@/stores/POIsStore' import POIStatePopup from '@/layers/POIPopup' -import {QueryStoreState} from "@/stores/QueryStore"; +import { QueryStoreState } from '@/stores/QueryStore' interface MapPopupProps { map: Map diff --git a/src/pois/AddressParseResult.ts b/src/pois/AddressParseResult.ts index 0c0dc117..ee96c051 100644 --- a/src/pois/AddressParseResult.ts +++ b/src/pois/AddressParseResult.ts @@ -98,10 +98,7 @@ export class AddressParseResult { return new AddressParseResult(locations.join(' '), poiQuery, 'store', poiQuery.toString()) } - public static handleGeocodingResponse( - hits: ReverseGeocodingHit[], - parseResult: AddressParseResult - ) { + public static handleGeocodingResponse(hits: ReverseGeocodingHit[], parseResult: AddressParseResult) { if (hits.length == 0) { Dispatcher.dispatch(new SetPOIs([])) return From b764401de3632e86330641687d136c9d676682b5 Mon Sep 17 00:00:00 2001 From: Peter Date: Mon, 12 Aug 2024 16:54:27 +0200 Subject: [PATCH 33/33] minor fix --- src/NavBar.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NavBar.ts b/src/NavBar.ts index 9f2c45a4..a55b6e73 100644 --- a/src/NavBar.ts +++ b/src/NavBar.ts @@ -117,7 +117,7 @@ export default class NavBar { return getApi() .geocode(result.location, 'nominatim') .then(res => { - if (res.hits.length == 0) Promise.resolve(p) + if (res.hits.length == 0) return p getApi() .reverseGeocode(result.query, res.hits[0].extent) .then(res => AddressParseResult.handleGeocodingResponse(res, result))