MapComplete/src/UI/BigComponents/Geosearch.svelte

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

208 lines
7 KiB
Svelte
Raw Normal View History

2023-03-28 05:13:48 +02:00
<script lang="ts">
2024-08-21 14:06:42 +02:00
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import type { Feature } from "geojson"
import Translations from "../i18n/Translations"
import Loading from "../Base/Loading.svelte"
import Hotkeys from "../Base/Hotkeys"
import { BBox } from "../../Logic/BBox"
import { GeoIndexedStoreForLayer } from "../../Logic/FeatureSource/Actors/GeoIndexedStore"
import { createEventDispatcher, onDestroy } from "svelte"
import { placeholder } from "../../Utils/placeholder"
import { SearchIcon } from "@rgossiaux/svelte-heroicons/solid"
import { ariaLabel } from "../../Utils/ariaLabel"
import { GeoLocationState } from "../../Logic/State/GeoLocationState"
2024-08-15 01:51:33 +02:00
import { NominatimGeocoding } from "../../Logic/Geocoding/NominatimGeocoding"
2024-08-22 22:50:37 +02:00
import { GeocodingUtils } from "../../Logic/Geocoding/GeocodingProvider"
2024-08-15 01:51:33 +02:00
import type { GeoCodeResult } from "../../Logic/Geocoding/GeocodingProvider"
2024-08-22 22:50:37 +02:00
import type GeocodingProvider from "../../Logic/Geocoding/GeocodingProvider"
2024-08-15 01:51:33 +02:00
import SearchResults from "./SearchResults.svelte"
import type { SpecialVisualizationState } from "../SpecialVisualization"
2024-08-22 22:50:37 +02:00
import MoreScreen from "./MoreScreen"
import type { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig"
import { focusWithArrows } from "../../Utils/focusWithArrows"
2023-03-28 05:13:48 +02:00
export let perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer> | undefined = undefined
export let bounds: UIEventSource<BBox>
export let selectedElement: UIEventSource<Feature> | undefined = undefined
2023-03-28 05:13:48 +02:00
export let geolocationState: GeoLocationState | undefined = undefined
export let clearAfterView: boolean = true
2024-08-21 14:06:42 +02:00
export let searcher: GeocodingProvider = new NominatimGeocoding()
export let state: SpecialVisualizationState
let searchContents: UIEventSource<string> = new UIEventSource<string>("")
export let triggerSearch: UIEventSource<any> = new UIEventSource<any>(undefined)
onDestroy(
triggerSearch.addCallback((_) => {
performSearch()
2023-12-21 01:46:18 +01:00
})
)
2023-03-28 05:13:48 +02:00
let isRunning: boolean = false
2023-03-28 05:13:48 +02:00
let inputElement: HTMLInputElement
let feedback: string = undefined
2023-12-19 22:21:34 +01:00
2024-08-21 14:06:42 +02:00
let isFocused = new UIEventSource(false)
2023-12-19 22:21:34 +01:00
function focusOnSearch() {
requestAnimationFrame(() => {
2023-12-19 22:21:34 +01:00
inputElement?.focus()
inputElement?.select()
})
2023-12-19 22:21:34 +01:00
}
Hotkeys.RegisterHotkey({ ctrl: "F" }, Translations.t.hotkeyDocumentation.selectSearch, () => {
feedback = undefined
focusOnSearch()
})
const dispatch = createEventDispatcher<{ searchCompleted; searchIsValid: boolean }>()
$: {
2024-08-21 14:06:42 +02:00
if (!$searchContents?.trim()) {
dispatch("searchIsValid", false)
} else {
dispatch("searchIsValid", true)
}
}
2024-08-15 01:51:33 +02:00
async function performSearch() {
try {
isRunning = true
geolocationState?.allowMoving.setData(true)
geolocationState?.requestMoment.setData(undefined) // If the GPS is still searching for a fix, we say that we don't want tozoom to it anymore
2024-08-21 14:06:42 +02:00
const searchContentsData = $searchContents?.trim() ?? ""
2023-05-18 15:44:54 +02:00
2024-08-21 14:06:42 +02:00
if (searchContentsData === "") {
return
}
2024-08-21 14:06:42 +02:00
const result = await searcher.search(searchContentsData, { bbox: bounds.data, limit: 10 })
2024-08-15 01:51:33 +02:00
console.log("Results are", result)
if (result.length == 0) {
feedback = Translations.t.general.search.nothing.txt
2023-12-19 22:21:34 +01:00
focusOnSearch()
return
}
const poi = result[0]
2024-08-22 22:50:37 +02:00
if (poi.payload !== undefined) {
// This is a theme
const theme = <MinimalLayoutInformation>poi.payload
const url = MoreScreen.createUrlFor(theme, false)
console.log("Found a theme, going to", url)
// @ts-ignore
window.location = url
return
}
if (poi.boundingbox) {
const [lat0, lat1, lon0, lon1] = poi.boundingbox
bounds.set(
new BBox([
[lon0, lat0],
[lon1, lat1]
]).pad(0.01)
)
} else if (poi.lon && poi.lat) {
state.mapProperties.flyTo(poi.lon, poi.lat, GeocodingUtils.categoryToZoomLevel[poi.category] ?? 16)
}
if (perLayer !== undefined) {
const id = poi.osm_type + "/" + poi.osm_id
const layers = Array.from(perLayer?.values() ?? [])
for (const layer of layers) {
const found = layer.features.data.find((f) => f.properties.id === id)
if (found === undefined) {
2023-12-19 22:21:34 +01:00
continue
}
2023-12-19 22:21:34 +01:00
selectedElement?.setData(found)
console.log("Found an element that probably matches:", selectedElement?.data)
break
}
}
if (clearAfterView) {
2024-08-21 14:06:42 +02:00
searchContents.setData("")
}
dispatch("searchIsValid", false)
dispatch("searchCompleted")
2024-08-22 22:50:37 +02:00
isFocused.setData(false)
} catch (e) {
console.error(e)
feedback = Translations.t.general.search.error.txt
2023-12-19 22:21:34 +01:00
focusOnSearch()
} finally {
isRunning = false
2023-03-28 05:13:48 +02:00
}
}
2024-08-15 01:51:33 +02:00
2024-08-22 22:50:37 +02:00
let suggestions: Store<GeoCodeResult[]> = searchContents.stabilized(250).bindD(search => {
if (search.length === 0) {
return undefined
}
return searcher.suggest(search, { bbox: bounds.data })
}
2024-08-21 14:06:42 +02:00
)
2024-08-15 01:51:33 +02:00
2024-08-22 22:50:37 +02:00
let geosearch: HTMLDivElement
function checkFocus() {
window.requestAnimationFrame(() => {
if (geosearch.contains(document.activeElement)) {
return
}
isFocused.setData(false)
})
}
document.addEventListener("focus",() => {
checkFocus()
}, true /* use 'capturing' instead of bubbling, needed for focus-events*/)
2023-03-28 05:13:48 +02:00
</script>
2024-08-22 22:50:37 +02:00
<div bind:this={geosearch} use:focusWithArrows={"searchresult"}>
<div class="normal-background flex justify-between rounded-full pl-2 w-full">
<form class="flex w-full flex-wrap">
{#if isRunning}
<Loading>{Translations.t.general.search.searching}</Loading>
{:else}
<input
type="search"
class="w-full outline-none"
bind:this={inputElement}
on:keypress={(keypr) => {
2023-12-21 01:46:18 +01:00
feedback = undefined
2024-08-22 22:50:37 +02:00
if(keypr.key === "Enter"){
performSearch()
keypr.preventDefault()
}
return undefined
2023-12-21 01:46:18 +01:00
}}
2024-08-22 22:50:37 +02:00
on:focus={() => {isFocused.setData(true)}}
on:blur={() => {checkFocus()}}
bind:value={$searchContents}
use:placeholder={Translations.t.general.search.search}
use:ariaLabel={Translations.t.general.search.search}
/>
{#if feedback !== undefined}
<!-- The feedback is _always_ shown for screenreaders and to make sure that the searchfield can still be selected by tabbing-->
<div class="alert" role="alert" aria-live="assertive">
{feedback}
</div>
{/if}
2023-12-19 22:21:34 +01:00
{/if}
2024-08-22 22:50:37 +02:00
</form>
<SearchIcon aria-hidden="true" class="h-6 w-6 self-end" on:click={performSearch} />
</div>
<div class="relative h-0" style="z-index: 10">
<div class="absolute right-0 w-full sm:w-96 h-fit max-h-96">
<SearchResults {isFocused} {state} results={$suggestions} searchTerm={searchContents}
on:select={() => {searchContents.set(""); isFocused.setData(false)}} />
</div>
</div>
2024-08-15 01:51:33 +02:00
</div>