11import { JSX , ReactNode , useCallback , useEffect , useRef , useState } from 'react'
22import { QueryPoint , QueryPointType } from '@/stores/QueryStore'
33import { Bbox , GeocodingHit , ReverseGeocodingHit } from '@/api/graphhopper'
4- import Autocomplete , { AutocompleteItem , GeocodingItem , POIQueryItem } from '@/sidebar/search/AddressInputAutocomplete'
4+ import Autocomplete , {
5+ AutocompleteItem ,
6+ GeocodingItem ,
7+ POIQueryItem ,
8+ RecentLocationItem ,
9+ } from '@/sidebar/search/AddressInputAutocomplete'
10+ import { getRecentLocations } from '@/sidebar/search/RecentLocations'
511import ArrowBack from './arrow_back.svg'
612import Cross from '@/sidebar/times-solid-thin.svg'
713import CurrentLocationIcon from './current-location.svg'
@@ -17,13 +23,13 @@ import { toLonLat, transformExtent } from 'ol/proj'
1723import { Map } from 'ol'
1824import { AddressParseResult } from '@/pois/AddressParseResult'
1925import { getMap } from '@/map/map'
20- import { Coordinate , getBBoxFromCoord } from '@/utils'
26+ import { calcDist , Coordinate , getBBoxFromCoord } from '@/utils'
2127
2228export interface AddressInputProps {
2329 point : QueryPoint
2430 points : QueryPoint [ ]
2531 onCancel : ( ) => void
26- onAddressSelected : ( queryText : string , coord : Coordinate | undefined ) => void
32+ onLocationSelected : ( mainText : string , secondText : string | undefined , coord : Coordinate | undefined ) => void
2733 onChange : ( value : string ) => void
2834 clearDragDrop : ( ) => void
2935 moveStartIndex : number
@@ -41,6 +47,8 @@ export default function AddressInput(props: AddressInputProps) {
4147 // keep track of focus and toggle fullscreen display on small screens
4248 const [ hasFocus , setHasFocus ] = useState ( false )
4349 const isSmallScreen = useMediaQuery ( { query : '(max-width: 44rem)' } )
50+ const prevPoint = props . index > 0 ? props . points [ props . index - 1 ] : undefined
51+ const excludeCoord = prevPoint ?. isInitialized ? prevPoint . coordinate : undefined
4452
4553 // container for geocoding results which gets set by the geocoder class and set to empty if the underlying query
4654 // point gets changed from outside also gets filled with an item to select the current location as input if input
@@ -72,7 +80,21 @@ export default function AddressInput(props: AddressInputProps) {
7280 const [ poiSearch ] = useState ( new ReverseGeocoder ( getApi ( ) , props . point , AddressParseResult . handleGeocodingResponse ) )
7381
7482 // if item is selected we need to clear the autocompletion list
75- useEffect ( ( ) => setAutocompleteItems ( [ ] ) , [ props . point ] )
83+ useEffect ( ( ) => {
84+ if ( props . point . isInitialized ) setAutocompleteItems ( [ ] )
85+ } , [ props . point ] )
86+
87+ useEffect ( ( ) => {
88+ if ( ! hasFocus ) return
89+ if ( isInitialFocus . current ) {
90+ isInitialFocus . current = false
91+ return
92+ }
93+ if ( text === '' ) {
94+ const recents = buildRecentItems ( undefined , 5 , excludeCoord )
95+ if ( recents . length > 0 ) setAutocompleteItems ( recents )
96+ }
97+ } , [ hasFocus , excludeCoord ] )
7698
7799 // highlighted result of geocoding results. Keep track which index is highlighted and change things on ArrowUp and Down
78100 // on Enter select highlighted result or the 0th if nothing is highlighted
@@ -105,7 +127,8 @@ export default function AddressInput(props: AddressInputProps) {
105127 setText ( origText )
106128 } else if ( nextIndex >= 0 ) {
107129 const item = autocompleteItems [ nextIndex ]
108- if ( item instanceof GeocodingItem ) setText ( item . mainText )
130+ if ( item instanceof GeocodingItem || item instanceof RecentLocationItem )
131+ setText ( item . mainText )
109132 else setText ( origText )
110133 }
111134 }
@@ -119,13 +142,15 @@ export default function AddressInput(props: AddressInputProps) {
119142 // try to parse input as coordinate. Otherwise query nominatim
120143 const coordinate = textToCoordinate ( text )
121144 if ( coordinate ) {
122- props . onAddressSelected ( text , coordinate )
145+ props . onLocationSelected ( text , undefined , coordinate )
123146 } else if ( autocompleteItems . length > 0 ) {
124147 const index = highlightedResult >= 0 ? highlightedResult : 0
125148 const item = autocompleteItems [ index ]
126149 if ( item instanceof POIQueryItem ) {
127150 handlePoiSearch ( poiSearch , item . result , props . map )
128- props . onAddressSelected ( item . result . text ( item . result . poi ) , undefined )
151+ props . onLocationSelected ( item . result . text ( item . result . poi ) , undefined , undefined )
152+ } else if ( item instanceof RecentLocationItem ) {
153+ props . onLocationSelected ( item . mainText , item . secondText , item . point )
129154 } else if ( highlightedResult < 0 && ! props . point . isInitialized ) {
130155 // by default use the first result, otherwise the highlighted one
131156 getApi ( )
@@ -134,13 +159,13 @@ export default function AddressInput(props: AddressInputProps) {
134159 if ( result && result . hits . length > 0 ) {
135160 const hit : GeocodingHit = result . hits [ 0 ]
136161 const res = nominatimHitToItem ( hit )
137- props . onAddressSelected ( res . mainText + ', ' + res . secondText , hit . point )
162+ props . onLocationSelected ( res . mainText , res . secondText , hit . point )
138163 } else if ( item instanceof GeocodingItem ) {
139- props . onAddressSelected ( item . toText ( ) , item . point )
164+ props . onLocationSelected ( item . mainText , item . secondText , item . point )
140165 }
141166 } )
142167 } else if ( item instanceof GeocodingItem ) {
143- props . onAddressSelected ( item . toText ( ) , item . point )
168+ props . onLocationSelected ( item . mainText , item . secondText , item . point )
144169 }
145170 }
146171 if ( event . key === 'Enter' ) focusNextOrBlur ( )
@@ -168,6 +193,7 @@ export default function AddressInput(props: AddressInputProps) {
168193
169194 // do not focus on mobile as we would hide the map with the "input"-view
170195 const focusFirstInput = props . index == 0 && ! isSmallScreen
196+ const isInitialFocus = useRef ( focusFirstInput )
171197
172198 return (
173199 < div className = { containerClass } >
@@ -202,8 +228,21 @@ export default function AddressInput(props: AddressInputProps) {
202228 onChange = { e => {
203229 const query = e . target . value
204230 setText ( query )
205- const coordinate = textToCoordinate ( query )
206- if ( ! coordinate ) geocoder . request ( e . target . value , biasCoord , getMap ( ) . getView ( ) . getZoom ( ) )
231+ if ( query === '' ) {
232+ geocoder . cancel ( )
233+ const recents = buildRecentItems ( undefined , 5 , excludeCoord )
234+ if ( recents . length > 0 ) setAutocompleteItems ( recents )
235+ else setAutocompleteItems ( [ ] )
236+ } else {
237+ const coordinate = textToCoordinate ( query )
238+ if ( ! coordinate ) {
239+ if ( query . length < 2 ) {
240+ const recents = buildRecentItems ( query , 5 , excludeCoord )
241+ if ( recents . length > 0 ) setAutocompleteItems ( recents )
242+ }
243+ geocoder . request ( query , biasCoord , getMap ( ) . getView ( ) . getZoom ( ) )
244+ }
245+ }
207246 props . onChange ( query )
208247 } }
209248 onKeyDown = { onKeypress }
@@ -232,6 +271,9 @@ export default function AddressInput(props: AddressInputProps) {
232271 onClick = { e => {
233272 setText ( '' )
234273 props . onChange ( '' )
274+ const recents = buildRecentItems ( undefined , 5 , excludeCoord )
275+ if ( recents . length > 0 ) setAutocompleteItems ( recents )
276+ else setAutocompleteItems ( [ ] )
235277 // if we clear the text without focus then explicitly request it to improve usability:
236278 searchInput . current ! . focus ( )
237279 } }
@@ -247,15 +289,15 @@ export default function AddressInput(props: AddressInputProps) {
247289 e => e . preventDefault ( ) // prevents that input->onBlur is called when clicking the button (would hide this button and prevent onClick)
248290 }
249291 onClick = { ( ) => {
250- onCurrentLocationSelected ( props . onAddressSelected )
292+ onCurrentLocationSelected ( ( text , coord ) => props . onLocationSelected ( text , undefined , coord ) )
251293 // but when clicked => we want to lose the focus e.g. to close mobile-input view
252294 searchInput . current ! . blur ( )
253295 } }
254296 >
255297 < CurrentLocationIcon />
256298 </ PlainButton >
257299
258- { autocompleteItems . length > 0 && (
300+ { hasFocus && autocompleteItems . length > 0 && (
259301 < ResponsiveAutocomplete
260302 inputRef = { searchInputContainer . current ! }
261303 index = { props . index }
@@ -267,7 +309,10 @@ export default function AddressInput(props: AddressInputProps) {
267309 onSelect = { item => {
268310 if ( item instanceof GeocodingItem ) {
269311 setText ( item . toText ( ) )
270- props . onAddressSelected ( item . toText ( ) , item . point )
312+ props . onLocationSelected ( item . mainText , item . secondText , item . point )
313+ } else if ( item instanceof RecentLocationItem ) {
314+ setText ( item . toText ( ) )
315+ props . onLocationSelected ( item . mainText , item . secondText , item . point )
271316 } else if ( item instanceof POIQueryItem ) {
272317 handlePoiSearch ( poiSearch , item . result , props . map )
273318 setText ( item . result . text ( item . result . poi ) )
@@ -282,6 +327,24 @@ export default function AddressInput(props: AddressInputProps) {
282327 )
283328}
284329
330+ function buildRecentItems ( filter ?: string , limit ?: number , excludeCoord ?: Coordinate ) : RecentLocationItem [ ] {
331+ let recents = getRecentLocations ( 0 )
332+ if ( excludeCoord ) recents = recents . filter ( e => calcDist ( { lat : e . lat , lng : e . lng } , excludeCoord ) > 0 )
333+ if ( filter ) {
334+ const lower = filter . toLowerCase ( )
335+ recents = recents . filter (
336+ e =>
337+ e . mainText . toLowerCase ( ) . startsWith ( lower ) ||
338+ e . secondText
339+ . toLowerCase ( )
340+ . split ( / [ \s , ] + / )
341+ . some ( word => word . startsWith ( lower ) ) ,
342+ )
343+ }
344+ if ( limit ) recents = recents . slice ( 0 , limit )
345+ return recents . map ( e => new RecentLocationItem ( e . mainText , e . secondText , { lat : e . lat , lng : e . lng } ) )
346+ }
347+
285348function handlePoiSearch ( poiSearch : ReverseGeocoder , result : AddressParseResult , map : Map ) {
286349 if ( ! result . hasPOIs ( ) ) return
287350
0 commit comments