Improve search UI
This commit is contained in:
parent
3be286c2b1
commit
93f03ddbaf
22 changed files with 564 additions and 499 deletions
|
@ -537,7 +537,7 @@
|
|||
},
|
||||
{
|
||||
"if": "cuisine=mexican ",
|
||||
"icon": "\uD83C\uDDF2\uD83C\uDDFD",
|
||||
"icon": "🇲🇽",
|
||||
"then": {
|
||||
"en": "Mexican dishes are served here",
|
||||
"nl": "Dit is een mexicaans restaurant"
|
||||
|
|
|
@ -1913,6 +1913,10 @@ input[type="range"].range-lg::-moz-range-thumb {
|
|||
max-height: 3rem;
|
||||
}
|
||||
|
||||
.max-h-screen {
|
||||
max-height: 100vh;
|
||||
}
|
||||
|
||||
.max-h-full {
|
||||
max-height: 100%;
|
||||
}
|
||||
|
@ -1925,10 +1929,6 @@ input[type="range"].range-lg::-moz-range-thumb {
|
|||
max-height: 16rem;
|
||||
}
|
||||
|
||||
.max-h-96 {
|
||||
max-height: 24rem;
|
||||
}
|
||||
|
||||
.max-h-60 {
|
||||
max-height: 15rem;
|
||||
}
|
||||
|
@ -8168,12 +8168,12 @@ svg.apply-fill path {
|
|||
width: 16rem;
|
||||
}
|
||||
|
||||
.sm\:w-11 {
|
||||
width: 2.75rem;
|
||||
.sm\:w-80 {
|
||||
width: 20rem;
|
||||
}
|
||||
|
||||
.sm\:w-96 {
|
||||
width: 24rem;
|
||||
.sm\:w-11 {
|
||||
width: 2.75rem;
|
||||
}
|
||||
|
||||
.sm\:w-auto {
|
||||
|
@ -8188,6 +8188,10 @@ svg.apply-fill path {
|
|||
width: 1.5rem;
|
||||
}
|
||||
|
||||
.sm\:w-96 {
|
||||
width: 24rem;
|
||||
}
|
||||
|
||||
.sm\:grid-cols-2 {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
@ -8373,6 +8377,10 @@ svg.apply-fill path {
|
|||
height: 100%;
|
||||
}
|
||||
|
||||
.md\:w-96 {
|
||||
width: 24rem;
|
||||
}
|
||||
|
||||
.md\:w-6\/12 {
|
||||
width: 50%;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { ImmutableStore, Store } from "../UIEventSource"
|
||||
import GeocodingProvider, { GeocodingOptions, SearchResult } from "./GeocodingProvider"
|
||||
import GeocodingProvider, { FilterPayload, FilterResult, GeocodingOptions, SearchResult } from "./GeocodingProvider"
|
||||
import { SpecialVisualizationState } from "../../UI/SpecialVisualization"
|
||||
import { Utils } from "../../Utils"
|
||||
import Locale from "../../UI/i18n/Locale"
|
||||
|
@ -13,17 +13,29 @@ export default class FilterSearch implements GeocodingProvider {
|
|||
}
|
||||
|
||||
async search(query: string): Promise<SearchResult[]> {
|
||||
return this.searchDirectly(query)
|
||||
return this.searchDirectlyWrapped(query)
|
||||
}
|
||||
|
||||
private searchDirectly(query: string): SearchResult[] {
|
||||
const possibleFilters: SearchResult[] = []
|
||||
private searchDirectlyWrapped(query: string): FilterResult[] {
|
||||
return this.searchDirectly(query).map(payload => ({
|
||||
payload,
|
||||
category: "filter",
|
||||
osm_id: payload.layer.id + "/" + payload.filter.id + "/" + payload.option.osmTags?.asHumanString() ?? "none"
|
||||
}))
|
||||
}
|
||||
|
||||
public searchDirectly(query: string): FilterPayload[] {
|
||||
if (query.length === 0) {
|
||||
return []
|
||||
}
|
||||
if(!Utils.isEmoji(query)){
|
||||
query = Utils.simplifyStringForSearch(query)
|
||||
const queries = query.split(" ").map(query => {
|
||||
if (!Utils.isEmoji(query)) {
|
||||
return Utils.simplifyStringForSearch(query)
|
||||
}
|
||||
return query
|
||||
}).filter(q => q.length > 0)
|
||||
console.log("Queries:",queries)
|
||||
const possibleFilters: FilterPayload[] = []
|
||||
for (const layer of this._state.layout.layers) {
|
||||
if (!Array.isArray(layer.filters)) {
|
||||
continue
|
||||
|
@ -34,22 +46,27 @@ export default class FilterSearch implements GeocodingProvider {
|
|||
if (option === undefined) {
|
||||
continue
|
||||
}
|
||||
if (!option.osmTags) {
|
||||
continue
|
||||
}
|
||||
let terms = ([option.question.txt,
|
||||
...(option.searchTerms?.[Locale.language.data] ?? option.searchTerms?.["en"] ?? [])]
|
||||
.flatMap(term => [term, ...term?.split(" ")]))
|
||||
.flatMap(term => [term, ...(term?.split(" ") ?? [])]))
|
||||
terms = terms.map(t => Utils.simplifyStringForSearch(t))
|
||||
terms.push(option.emoji)
|
||||
Utils.NoNullInplace(terms)
|
||||
const levehnsteinD = Math.min(...
|
||||
terms.map(entry => Utils.levenshteinDistance(query, entry.slice(0, query.length))))
|
||||
if (levehnsteinD / query.length > 0.25) {
|
||||
const distances = queries.flatMap(query => terms.map(entry => {
|
||||
const d = Utils.levenshteinDistance(query, entry.slice(0, query.length))
|
||||
console.log(query,"? +",terms, "=",d)
|
||||
const dRelative = d / query.length
|
||||
return dRelative
|
||||
}))
|
||||
|
||||
const levehnsteinD = Math.min(...distances)
|
||||
if (levehnsteinD > 0.25) {
|
||||
continue
|
||||
}
|
||||
possibleFilters.push({
|
||||
payload: { option, layer, filter, index: i },
|
||||
category: "filter",
|
||||
osm_id: layer.id + "/" + filter.id + "/" + option.osmTags?.asHumanString() ?? "none",
|
||||
})
|
||||
possibleFilters.push({ option, layer, filter, index: i })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -57,11 +74,7 @@ export default class FilterSearch implements GeocodingProvider {
|
|||
}
|
||||
|
||||
suggest(query: string, options?: GeocodingOptions): Store<SearchResult[]> {
|
||||
if (Utils.isEmoji(query)) {
|
||||
return new ImmutableStore(this.searchDirectly(query))
|
||||
}
|
||||
query = Utils.simplifyStringForSearch(query)
|
||||
return new ImmutableStore(this.searchDirectly(query))
|
||||
return new ImmutableStore(this.searchDirectlyWrapped(query))
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -45,8 +45,9 @@ export type GeocodeResult = {
|
|||
source?: string
|
||||
}
|
||||
export type FilterPayload = { option: FilterConfigOption, filter: FilterConfig, layer: LayerConfig, index: number }
|
||||
export type FilterResult = { category: "filter", osm_id: string, payload: FilterPayload }
|
||||
export type SearchResult =
|
||||
| { category: "filter", osm_id: string, payload: FilterPayload }
|
||||
| FilterResult
|
||||
| { category: "theme", osm_id: string, payload: MinimalLayoutInformation }
|
||||
| GeocodeResult
|
||||
|
||||
|
|
|
@ -20,31 +20,33 @@ export default class ThemeSearch implements GeocodingProvider {
|
|||
}
|
||||
|
||||
async search(query: string): Promise<SearchResult[]> {
|
||||
return this.searchDirect(query, 99)
|
||||
return this.searchWrapped(query, 99)
|
||||
}
|
||||
|
||||
suggest(query: string, options?: GeocodingOptions): Store<SearchResult[]> {
|
||||
return new ImmutableStore(this.searchDirect(query, this._suggestionLimit ?? 4))
|
||||
return new ImmutableStore(this.searchWrapped(query, this._suggestionLimit ?? 4))
|
||||
}
|
||||
|
||||
private searchDirect(query: string, limit: number): SearchResult[] {
|
||||
if(query.length < 1){
|
||||
return []
|
||||
}
|
||||
query = Utils.simplifyStringForSearch(query)
|
||||
const withMatch = ThemeSearch.allThemes
|
||||
.filter(th => !th.hideFromOverview || this._knownHiddenThemes.data.has(th.id))
|
||||
.filter(th => th.id !== this._state.layout.id)
|
||||
.filter(th => MoreScreen.MatchesLayout(th, query))
|
||||
.slice(0, limit)
|
||||
console.log("Matched", withMatch, limit)
|
||||
|
||||
return withMatch.map(match => <SearchResult> {
|
||||
private searchWrapped(query: string, limit: number): SearchResult[] {
|
||||
return this.searchDirect(query, limit).map(match => <SearchResult>{
|
||||
payload: match,
|
||||
category: "theme",
|
||||
osm_id: match.id
|
||||
})
|
||||
}
|
||||
|
||||
public searchDirect(query: string, limit: number): MinimalLayoutInformation[] {
|
||||
if (query.length < 1) {
|
||||
return []
|
||||
}
|
||||
query = Utils.simplifyStringForSearch(query)
|
||||
return ThemeSearch.allThemes
|
||||
.filter(th => !th.hideFromOverview || this._knownHiddenThemes.data.has(th.id))
|
||||
.filter(th => th.id !== this._state.layout.id)
|
||||
.filter(th => MoreScreen.MatchesLayout(th, query))
|
||||
.slice(0, limit)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
189
src/Logic/State/SearchState.ts
Normal file
189
src/Logic/State/SearchState.ts
Normal file
|
@ -0,0 +1,189 @@
|
|||
import GeocodingProvider, { FilterPayload, GeocodingUtils, type SearchResult } from "../Geocoding/GeocodingProvider"
|
||||
import { RecentSearch } from "../Geocoding/RecentSearch"
|
||||
import { Store, Stores, UIEventSource } from "../UIEventSource"
|
||||
import CombinedSearcher from "../Geocoding/CombinedSearcher"
|
||||
import FilterSearch from "../Geocoding/FilterSearch"
|
||||
import LocalElementSearch from "../Geocoding/LocalElementSearch"
|
||||
import CoordinateSearch from "../Geocoding/CoordinateSearch"
|
||||
import ThemeSearch from "../Geocoding/ThemeSearch"
|
||||
import OpenStreetMapIdSearch from "../Geocoding/OpenStreetMapIdSearch"
|
||||
import PhotonSearch from "../Geocoding/PhotonSearch"
|
||||
import ThemeViewState from "../../Models/ThemeViewState"
|
||||
import Translations from "../../UI/i18n/Translations"
|
||||
import type { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import MoreScreen from "../../UI/BigComponents/MoreScreen"
|
||||
import { BBox } from "../BBox"
|
||||
import { Translation } from "../../UI/i18n/Translation"
|
||||
import GeocodingFeatureSource from "../Geocoding/GeocodingFeatureSource"
|
||||
import ShowDataLayer from "../../UI/Map/ShowDataLayer"
|
||||
|
||||
export default class SearchState {
|
||||
|
||||
public readonly isSearching = new UIEventSource(false)
|
||||
public readonly geosearch: GeocodingProvider
|
||||
public readonly recentlySearched: RecentSearch
|
||||
public readonly feedback: UIEventSource<Translation> = new UIEventSource<Translation>(undefined)
|
||||
public readonly searchTerm: UIEventSource<string> = new UIEventSource<string>("")
|
||||
public readonly searchIsFocused = new UIEventSource(false)
|
||||
public readonly suggestions: Store<SearchResult[]>
|
||||
public readonly filterSuggestions: Store<FilterPayload[]>
|
||||
public readonly themeSuggestions: Store<MinimalLayoutInformation[]>
|
||||
|
||||
private readonly state: ThemeViewState
|
||||
public readonly showSearchDrawer: UIEventSource<boolean>
|
||||
|
||||
constructor(state: ThemeViewState) {
|
||||
this.state = state
|
||||
|
||||
this.geosearch = new CombinedSearcher(
|
||||
// new LocalElementSearch(state, 5),
|
||||
new CoordinateSearch(),
|
||||
new OpenStreetMapIdSearch(state),
|
||||
new PhotonSearch() // new NominatimGeocoding(),
|
||||
)
|
||||
|
||||
this.recentlySearched = new RecentSearch(state)
|
||||
const bounds = state.mapProperties.bounds
|
||||
this.suggestions = this.searchTerm.stabilized(250).bindD(search => {
|
||||
if (search.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
return Stores.holdDefined(bounds.bindD(bbox => this.geosearch.suggest(search, { bbox })))
|
||||
}
|
||||
)
|
||||
|
||||
const themeSearch = new ThemeSearch(state, 3)
|
||||
this.themeSuggestions = this.searchTerm.mapD(query => themeSearch.searchDirect(query, 3))
|
||||
|
||||
|
||||
const filterSearch = new FilterSearch(state)
|
||||
this.filterSuggestions = this.searchTerm.stabilized(50).mapD(query =>
|
||||
filterSearch.searchDirectly(query)
|
||||
).mapD(filterResult => {
|
||||
const active = state.layerState.activeFilters.data
|
||||
return filterResult.filter(({ filter, index, layer }) => {
|
||||
const foundMatch = active.some(active =>
|
||||
active.filter.id === filter.id && layer.id === active.layer.id && active.control.data === index)
|
||||
|
||||
return !foundMatch
|
||||
})
|
||||
}, [state.layerState.activeFilters])
|
||||
const geocodedFeatures = new GeocodingFeatureSource(this.suggestions.stabilized(250))
|
||||
state.featureProperties.trackFeatureSource(geocodedFeatures)
|
||||
|
||||
new ShowDataLayer(
|
||||
state.map,
|
||||
{
|
||||
layer: GeocodingUtils.searchLayer,
|
||||
features: geocodedFeatures,
|
||||
selectedElement: state.selectedElement
|
||||
}
|
||||
)
|
||||
|
||||
this.showSearchDrawer = new UIEventSource(false)
|
||||
this.suggestions.addCallbackAndRunD(sugg => {
|
||||
if (sugg.length > 0) {
|
||||
this.showSearchDrawer.set(true)
|
||||
}
|
||||
})
|
||||
this.searchIsFocused.addCallbackAndRunD(sugg => {
|
||||
if (sugg) {
|
||||
this.showSearchDrawer.set(true)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
public async apply(payload: FilterPayload) {
|
||||
const state = this.state
|
||||
const { layer, filter, index } = payload
|
||||
|
||||
const flayer = state.layerState.filteredLayers.get(layer.id)
|
||||
const filtercontrol = flayer.appliedFilters.get(filter.id)
|
||||
for (const [name, otherLayer] of state.layerState.filteredLayers) {
|
||||
if (name === layer.id) {
|
||||
otherLayer.isDisplayed.setData(true)
|
||||
continue
|
||||
}
|
||||
otherLayer.isDisplayed.setData(false)
|
||||
}
|
||||
|
||||
console.log("Could not apply", layer.id, ".", filter.id, index)
|
||||
if (filtercontrol.data === index) {
|
||||
filtercontrol.setData(undefined)
|
||||
} else {
|
||||
filtercontrol.setData(index)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to search and goto a given location
|
||||
* Returns 'false' if search failed
|
||||
*/
|
||||
public async performSearch(): Promise<boolean> {
|
||||
const query = this.searchTerm.data?.trim() ?? ""
|
||||
if (query === "") {
|
||||
return
|
||||
}
|
||||
const geolocationState = this.state.geolocation.geolocationState
|
||||
const searcher = this.state.searchState.geosearch
|
||||
const bounds = this.state.mapProperties.bounds
|
||||
const bbox = this.state.mapProperties.bounds.data
|
||||
try {
|
||||
this.isSearching.set(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
|
||||
const result = await searcher.search(query, { bbox })
|
||||
if (result.length == 0) {
|
||||
this.feedback.set(Translations.t.general.search.nothing)
|
||||
return false
|
||||
}
|
||||
const poi = result[0]
|
||||
if (poi.category === "theme") {
|
||||
const theme = <MinimalLayoutInformation>poi.payload
|
||||
const url = MoreScreen.createUrlFor(theme, false)
|
||||
window.location = <any>url
|
||||
return true
|
||||
}
|
||||
if (poi.category === "filter") {
|
||||
await this.apply(poi.payload)
|
||||
return true
|
||||
}
|
||||
if (poi.boundingbox) {
|
||||
const [lat0, lat1, lon0, lon1] = poi.boundingbox
|
||||
// Will trigger a 'fly to'
|
||||
bounds.set(
|
||||
new BBox([
|
||||
[lon0, lat0],
|
||||
[lon1, lat1]
|
||||
]).pad(0.01)
|
||||
)
|
||||
} else if (poi.lon && poi.lat) {
|
||||
this.state.mapProperties.flyTo(poi.lon, poi.lat, GeocodingUtils.categoryToZoomLevel[poi.category] ?? 16)
|
||||
}
|
||||
const perLayer = this.state.perLayer
|
||||
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) {
|
||||
continue
|
||||
}
|
||||
this.state.selectedElement?.setData(found)
|
||||
}
|
||||
}
|
||||
return true
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
this.feedback.set(Translations.t.general.search.error)
|
||||
return false
|
||||
} finally {
|
||||
this.isSearching.set(false)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -238,8 +238,8 @@ export abstract class Store<T> implements Readable<T> {
|
|||
* src.setData(0)
|
||||
* lastValue // => "def"
|
||||
*/
|
||||
public bind<X>(f: (t: T) => Store<X>): Store<X> {
|
||||
const mapped = this.map(f)
|
||||
public bind<X>(f: (t: T) => Store<X>, extraSources: Store<object>[] = []): Store<X> {
|
||||
const mapped = this.map(f, extraSources)
|
||||
const sink = new UIEventSource<X>(undefined)
|
||||
const seenEventSources = new Set<Store<X>>()
|
||||
mapped.addCallbackAndRun((newEventSource) => {
|
||||
|
@ -270,7 +270,7 @@ export abstract class Store<T> implements Readable<T> {
|
|||
return sink
|
||||
}
|
||||
|
||||
public bindD<X>(f: (t: Exclude<T, undefined | null>) => Store<X>): Store<X> {
|
||||
public bindD<X>(f: (t: Exclude<T, undefined | null>) => Store<X>, extraSources: UIEventSource<object>[] =[]): Store<X> {
|
||||
return this.bind((t) => {
|
||||
if (t === null) {
|
||||
return null
|
||||
|
@ -279,7 +279,7 @@ export abstract class Store<T> implements Readable<T> {
|
|||
return undefined
|
||||
}
|
||||
return f(<Exclude<T, undefined | null>>t)
|
||||
})
|
||||
}, extraSources)
|
||||
}
|
||||
|
||||
public stabilized(millisToStabilize): Store<T> {
|
||||
|
|
|
@ -340,7 +340,7 @@ export default class LayoutConfig implements LayoutInformation {
|
|||
}
|
||||
}
|
||||
}
|
||||
console.trace("Fallthrough: could not find the appropraite layer for an object with tags", tags, "within layout", this)
|
||||
console.trace("Fallthrough: could not find the appropriate layer for an object with tags", tags, "within layout", this)
|
||||
return undefined
|
||||
}
|
||||
|
||||
|
|
|
@ -83,6 +83,7 @@ import PhotonSearch from "../Logic/Geocoding/PhotonSearch"
|
|||
import ThemeSearch from "../Logic/Geocoding/ThemeSearch"
|
||||
import OpenStreetMapIdSearch from "../Logic/Geocoding/OpenStreetMapIdSearch"
|
||||
import FilterSearch from "../Logic/Geocoding/FilterSearch"
|
||||
import SearchState from "../Logic/State/SearchState"
|
||||
|
||||
/**
|
||||
*
|
||||
|
@ -164,8 +165,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
|
||||
public readonly nearbyImageSearcher: CombinedFetcher
|
||||
|
||||
public readonly geosearch: GeocodingProvider
|
||||
public readonly recentlySearched: RecentSearch
|
||||
public readonly searchState: SearchState
|
||||
|
||||
constructor(layout: LayoutConfig, mvtAvailableLayers: Set<string>) {
|
||||
Utils.initDomPurify()
|
||||
|
@ -390,16 +390,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
this.featureSummary = this.setupSummaryLayer()
|
||||
this.toCacheSavers = layout.enableCache ? this.initSaveToLocalStorage() : undefined
|
||||
|
||||
this.geosearch = new CombinedSearcher(
|
||||
new FilterSearch(this),
|
||||
new LocalElementSearch(this, 5),
|
||||
new CoordinateSearch(),
|
||||
this.featureSwitches.featureSwitchBackToThemeOverview.data ? new ThemeSearch(this, 3) : undefined,
|
||||
new OpenStreetMapIdSearch(this),
|
||||
new PhotonSearch(), // new NominatimGeocoding(),
|
||||
)
|
||||
|
||||
this.recentlySearched = new RecentSearch(this)
|
||||
this.searchState = new SearchState(this)
|
||||
|
||||
this.initActors()
|
||||
this.drawSpecialLayers()
|
||||
|
@ -931,7 +922,6 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
|
||||
/**
|
||||
* Searches the appropriate layer - will first try if a special layer matches; if not, a normal layer will be used by delegating to the layout
|
||||
* @param tags
|
||||
*/
|
||||
public getMatchingLayer(properties: Record<string, string>){
|
||||
|
||||
|
|
43
src/UI/Base/DrawerRight.svelte
Normal file
43
src/UI/Base/DrawerRight.svelte
Normal file
|
@ -0,0 +1,43 @@
|
|||
<script lang="ts">
|
||||
import { Drawer } from "flowbite-svelte"
|
||||
import { sineIn } from "svelte/easing"
|
||||
import { Store } from "../../Logic/UIEventSource.js"
|
||||
import { onMount } from "svelte"
|
||||
|
||||
export let shown: Store<boolean>
|
||||
let transitionParams = {
|
||||
x: 640,
|
||||
duration: 200,
|
||||
easing: sineIn
|
||||
}
|
||||
let hidden = !shown.data
|
||||
|
||||
shown.addCallback(sh => {
|
||||
hidden = !sh
|
||||
})
|
||||
|
||||
let height = 0
|
||||
onMount(() => {
|
||||
let topbar = document.getElementById("top-bar")
|
||||
height = topbar.clientHeight
|
||||
})
|
||||
</script>
|
||||
|
||||
<Drawer placement="right"
|
||||
transitionType="fly" {transitionParams}
|
||||
activateClickOutside={false}
|
||||
divClass="overflow-y-auto"
|
||||
backdrop={false}
|
||||
id="drawer-right"
|
||||
width="w-full sm:w-80 md:w-96"
|
||||
rightOffset="inset-y-0 right-0"
|
||||
bind:hidden={hidden}>
|
||||
|
||||
<div class="normal-background h-screen">
|
||||
<div class="h-full" style={`padding-top: ${height}px`}>
|
||||
<div class="flex flex-col h-full overflow-y-auto">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Drawer>
|
|
@ -2,17 +2,12 @@
|
|||
import Translations from "../i18n/Translations"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import NextButton from "../Base/NextButton.svelte"
|
||||
import Geosearch from "../Search/Geosearch.svelte"
|
||||
import ThemeViewState from "../../Models/ThemeViewState"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { SearchIcon } from "@rgossiaux/svelte-heroicons/solid"
|
||||
import { twJoin } from "tailwind-merge"
|
||||
import { Utils } from "../../Utils"
|
||||
import { Store } from "../../Logic/UIEventSource"
|
||||
import type { GeolocationPermissionState } from "../../Logic/State/GeoLocationState"
|
||||
import { GeoLocationState } from "../../Logic/State/GeoLocationState"
|
||||
import If from "../Base/If.svelte"
|
||||
import { ExclamationTriangleIcon } from "@babeard/svelte-heroicons/mini"
|
||||
import ChevronDoubleLeft from "@babeard/svelte-heroicons/solid/ChevronDoubleLeft"
|
||||
import GeolocationIndicator from "./GeolocationIndicator.svelte"
|
||||
|
||||
/**
|
||||
|
@ -20,10 +15,6 @@
|
|||
*/
|
||||
export let state: ThemeViewState
|
||||
let layout = state.layout
|
||||
let selectedElement = state.selectedElement
|
||||
|
||||
let triggerSearch: UIEventSource<any> = new UIEventSource<any>(undefined)
|
||||
let searchEnabled = false
|
||||
|
||||
let geolocation = state.geolocation.geolocationState
|
||||
let geopermission: Store<GeolocationPermissionState> = geolocation.permission
|
||||
|
@ -35,7 +26,7 @@
|
|||
state.geolocationControl.handleClick()
|
||||
const glstate = state.geolocation.geolocationState
|
||||
if (glstate.currentGPSLocation.data !== undefined) {
|
||||
const c: GeolocationCoordinates = glstate.currentGPSLocation.data
|
||||
const c = glstate.currentGPSLocation.data
|
||||
state.guistate.pageStates.about_theme.setData(false)
|
||||
const coor = { lon: c.longitude, lat: c.latitude }
|
||||
state.mapProperties.location.setData(coor)
|
||||
|
@ -86,38 +77,6 @@
|
|||
<Tr t={$gpsExplanation} />
|
||||
</button>
|
||||
</If>
|
||||
|
||||
<If condition={state.featureSwitches.featureSwitchSearch}>
|
||||
<div
|
||||
class=".button low-interaction m-1 flex h-fit w-full flex-wrap items-center justify-end gap-x-2 gap-y-2 rounded border p-1"
|
||||
>
|
||||
<div style="min-width: 16rem; " class="grow">
|
||||
<Geosearch
|
||||
bounds={state.mapProperties.bounds}
|
||||
on:searchCompleted={() => state.guistate.pageStates.about_theme.setData(false)}
|
||||
on:searchIsValid={(event) => {
|
||||
searchEnabled = event.detail
|
||||
}}
|
||||
perLayer={state.perLayer}
|
||||
{selectedElement}
|
||||
{triggerSearch}
|
||||
geolocationState={state.geolocation.geolocationState}
|
||||
searcher={state.geosearch}
|
||||
{state}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
class={twJoin(
|
||||
"small flex w-fit shrink-0 items-center justify-between gap-x-2",
|
||||
!searchEnabled && "disabled"
|
||||
)}
|
||||
on:click={() => triggerSearch.ping()}
|
||||
>
|
||||
<Tr t={Translations.t.general.search.searchShort} />
|
||||
<SearchIcon class="h-6 w-6" />
|
||||
</button>
|
||||
</div>
|
||||
</If>
|
||||
</div>
|
||||
|
||||
{#if $currentGPSLocation === undefined && $geopermission === "requested" && GeoLocationState.isSafari()}
|
||||
|
|
|
@ -104,8 +104,7 @@
|
|||
</div>
|
||||
|
||||
{#if $reason.includeSearch}
|
||||
searcher={state.geosearch}
|
||||
<Geosearch bounds={currentMapProperties.bounds} clearAfterView={false} searcher={state.geosearch} {state}/>
|
||||
<Geosearch {state}/>
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-wrap">
|
||||
|
|
|
@ -1,20 +1,30 @@
|
|||
<script lang="ts">
|
||||
import type { ActiveFilter } from "../../Logic/State/LayerState"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import Icon from "../Map/Icon.svelte"
|
||||
import { Badge } from "flowbite-svelte"
|
||||
import FilterOption from "./FilterOption.svelte"
|
||||
import { XMarkIcon } from "@babeard/svelte-heroicons/mini"
|
||||
import Loading from "../Base/Loading.svelte"
|
||||
|
||||
|
||||
export let activeFilter: ActiveFilter
|
||||
let { control, layer, filter } = activeFilter
|
||||
let { control, filter } = activeFilter
|
||||
let option = control.map(c => filter.options[c] ?? filter.options[0])
|
||||
</script>
|
||||
<div class="badge">
|
||||
<FilterOption option={$option} />
|
||||
<button on:click={() => control.setData(undefined)}>
|
||||
let loading = false
|
||||
|
||||
function clear() {
|
||||
loading = true
|
||||
requestIdleCallback(() => {
|
||||
control.setData(undefined)
|
||||
loading = false
|
||||
})
|
||||
}
|
||||
</script>
|
||||
{#if loading}
|
||||
<Loading />
|
||||
{:else }
|
||||
<div class="badge">
|
||||
<FilterOption option={$option} />
|
||||
<button on:click={() => clear()}>
|
||||
<XMarkIcon class="w-5 h-5 pl-1" color="gray" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
@ -1,17 +1,29 @@
|
|||
<script lang="ts">
|
||||
import { default as ActiveFilterSvelte } from "./ActiveFilter.svelte"
|
||||
import type { ActiveFilter } from "../../Logic/State/LayerState"
|
||||
import Loading from "../Base/Loading.svelte"
|
||||
|
||||
export let activeFilters: ActiveFilter[]
|
||||
let loading = false
|
||||
|
||||
function clear() {
|
||||
loading = true
|
||||
requestIdleCallback(() => {
|
||||
|
||||
for (const activeFilter of activeFilters) {
|
||||
activeFilter.control.setData(undefined)
|
||||
}
|
||||
loading = false
|
||||
})
|
||||
}
|
||||
</script>
|
||||
{#if activeFilters.length > 0}
|
||||
<div class="flex flex-wrap gap-y-1 gap-x-1 button-unstyled">
|
||||
<h3>Active filters</h3>
|
||||
|
||||
{#if loading}
|
||||
<Loading />
|
||||
{:else}
|
||||
{#each activeFilters as activeFilter (activeFilter)}
|
||||
<ActiveFilterSvelte {activeFilter} />
|
||||
{/each}
|
||||
|
@ -19,6 +31,7 @@
|
|||
<button class="as-link subtle" on:click={() => clear()}>
|
||||
Clear filters
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{/if}
|
||||
|
|
|
@ -4,19 +4,15 @@
|
|||
import type { FilterPayload } from "../../Logic/Geocoding/GeocodingProvider"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import Icon from "../Map/Icon.svelte"
|
||||
import SearchResultUtils from "./SearchResultUtils"
|
||||
|
||||
export let entry: {
|
||||
category: "filter",
|
||||
payload: FilterPayload
|
||||
}
|
||||
let { option, filter, layer, index } = entry.payload
|
||||
export let entry: FilterPayload
|
||||
let { option } = entry
|
||||
export let state: SpecialVisualizationState
|
||||
let dispatch = createEventDispatcher<{ select }>()
|
||||
|
||||
|
||||
function apply() {
|
||||
SearchResultUtils.apply(entry.payload, state)
|
||||
state.searchState.apply(entry)
|
||||
dispatch("select")
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,53 +1,28 @@
|
|||
<script lang="ts">
|
||||
import { Store, Stores, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import type { Feature } from "geojson"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
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"
|
||||
import { NominatimGeocoding } from "../../Logic/Geocoding/NominatimGeocoding"
|
||||
import { GeocodingUtils } from "../../Logic/Geocoding/GeocodingProvider"
|
||||
import type { SearchResult } from "../../Logic/Geocoding/GeocodingProvider"
|
||||
import type GeocodingProvider from "../../Logic/Geocoding/GeocodingProvider"
|
||||
|
||||
import SearchResults from "./SearchResults.svelte"
|
||||
import type { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import { focusWithArrows } from "../../Utils/focusWithArrows"
|
||||
import ShowDataLayer from "../Map/ShowDataLayer"
|
||||
import ThemeViewState from "../../Models/ThemeViewState"
|
||||
import GeocodingFeatureSource from "../../Logic/Geocoding/GeocodingFeatureSource"
|
||||
import MoreScreen from "../BigComponents/MoreScreen"
|
||||
import SearchResultUtils from "./SearchResultUtils"
|
||||
|
||||
export let perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer> | undefined = undefined
|
||||
export let bounds: UIEventSource<BBox>
|
||||
export let selectedElement: UIEventSource<Feature> | undefined = undefined
|
||||
|
||||
export let geolocationState: GeoLocationState | undefined = undefined
|
||||
export let clearAfterView: boolean = true
|
||||
export let searcher: GeocodingProvider = new NominatimGeocoding()
|
||||
export let state: ThemeViewState
|
||||
let searchContents: UIEventSource<string> = new UIEventSource<string>("")
|
||||
export let triggerSearch: UIEventSource<any> = new UIEventSource<any>(undefined)
|
||||
onDestroy(
|
||||
triggerSearch.addCallback(() => {
|
||||
performSearch()
|
||||
})
|
||||
)
|
||||
export let searchContents: UIEventSource<string> = new UIEventSource<string>("")
|
||||
|
||||
let isRunning: boolean = false
|
||||
function performSearch() {
|
||||
state.searchState.performSearch()
|
||||
}
|
||||
|
||||
let isRunning = state.searchState.isSearching
|
||||
|
||||
let inputElement: HTMLInputElement
|
||||
|
||||
let feedback: string = undefined
|
||||
|
||||
let isFocused = new UIEventSource(false)
|
||||
export let isFocused = new UIEventSource(false)
|
||||
|
||||
function focusOnSearch() {
|
||||
requestAnimationFrame(() => {
|
||||
|
@ -57,7 +32,7 @@
|
|||
}
|
||||
|
||||
Hotkeys.RegisterHotkey({ ctrl: "F" }, Translations.t.hotkeyDocumentation.selectSearch, () => {
|
||||
feedback = undefined
|
||||
state.searchState.feedback.set(undefined)
|
||||
focusOnSearch()
|
||||
})
|
||||
|
||||
|
@ -70,95 +45,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
const searchContentsData = $searchContents?.trim() ?? ""
|
||||
|
||||
if (searchContentsData === "") {
|
||||
return
|
||||
}
|
||||
const result = await searcher.search(searchContentsData, { bbox: bounds.data, limit: 10 })
|
||||
if (result.length == 0) {
|
||||
feedback = Translations.t.general.search.nothing.txt
|
||||
focusOnSearch()
|
||||
return
|
||||
}
|
||||
const poi = result[0]
|
||||
if (poi.category === "theme") {
|
||||
const theme = <MinimalLayoutInformation>poi.payload
|
||||
const url = MoreScreen.createUrlFor(theme, false)
|
||||
// @ts-ignore
|
||||
window.location = url
|
||||
return
|
||||
}
|
||||
if(poi.category === "filter"){
|
||||
SearchResultUtils.apply(poi.payload, state)
|
||||
}
|
||||
if(poi.category === "filter"){
|
||||
return // Should not happen
|
||||
}
|
||||
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) {
|
||||
continue
|
||||
}
|
||||
selectedElement?.setData(found)
|
||||
break
|
||||
}
|
||||
}
|
||||
if (clearAfterView) {
|
||||
searchContents.setData("")
|
||||
}
|
||||
dispatch("searchIsValid", false)
|
||||
dispatch("searchCompleted")
|
||||
isFocused.setData(false)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
feedback = Translations.t.general.search.error.txt
|
||||
focusOnSearch()
|
||||
} finally {
|
||||
isRunning = false
|
||||
}
|
||||
}
|
||||
|
||||
let suggestions: Store<SearchResult[]> = searchContents.stabilized(250).bindD(search => {
|
||||
if (search.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
return Stores.holdDefined(bounds.bindD(bbox => searcher.suggest(search, { bbox, limit: 15 })))
|
||||
}
|
||||
)
|
||||
let geocededFeatures= new GeocodingFeatureSource(suggestions.stabilized(250))
|
||||
state.featureProperties.trackFeatureSource(geocededFeatures)
|
||||
|
||||
new ShowDataLayer(
|
||||
state.map,
|
||||
{
|
||||
layer: GeocodingUtils.searchLayer,
|
||||
features: geocededFeatures,
|
||||
selectedElement: state.selectedElement
|
||||
}
|
||||
)
|
||||
|
||||
let geosearch: HTMLDivElement
|
||||
|
||||
function checkFocus() {
|
||||
|
@ -181,7 +67,7 @@
|
|||
|
||||
<div class="normal-background flex justify-between rounded-full pl-2 w-full">
|
||||
<form class="flex w-full flex-wrap">
|
||||
{#if isRunning}
|
||||
{#if $isRunning}
|
||||
<Loading>{Translations.t.general.search.searching}</Loading>
|
||||
{:else}
|
||||
<input
|
||||
|
@ -189,7 +75,6 @@
|
|||
class="w-full outline-none"
|
||||
bind:this={inputElement}
|
||||
on:keypress={(keypr) => {
|
||||
feedback = undefined
|
||||
if(keypr.key === "Enter"){
|
||||
performSearch()
|
||||
keypr.preventDefault()
|
||||
|
@ -202,21 +87,9 @@
|
|||
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}
|
||||
|
||||
{/if}
|
||||
</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>
|
||||
<SearchIcon aria-hidden="true" class="h-6 w-6 self-end" on:click={() => performSearch()} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -11,9 +11,9 @@
|
|||
</script>
|
||||
|
||||
{#if entry.category === "theme"}
|
||||
<ThemeResult {entry} on:select />
|
||||
<ThemeResult entry={entry.payload} on:select />
|
||||
{:else if entry.category === "filter"}
|
||||
<FilterResult {entry} {state} on:select />
|
||||
<FilterResult entry={entry.payload} {state} on:select />
|
||||
{:else}
|
||||
<GeocodeResult {entry} {state} on:select />
|
||||
{/if}
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
import { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import { FilterPayload } from "../../Logic/Geocoding/GeocodingProvider"
|
||||
|
||||
export default class SearchResultUtils {
|
||||
static apply(payload: FilterPayload, state: SpecialVisualizationState) {
|
||||
const { layer, filter, index, option } = payload
|
||||
|
||||
let flayer = state.layerState.filteredLayers.get(layer.id)
|
||||
let filtercontrol = flayer.appliedFilters.get(filter.id)
|
||||
|
||||
for (const [name, otherLayer] of state.layerState.filteredLayers) {
|
||||
if (name === layer.id) {
|
||||
otherLayer.isDisplayed.setData(true)
|
||||
continue
|
||||
}
|
||||
otherLayer.isDisplayed.setData(false)
|
||||
}
|
||||
|
||||
if (filtercontrol.data === index) {
|
||||
filtercontrol.setData(undefined)
|
||||
} else {
|
||||
filtercontrol.setData(index)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,46 +1,57 @@
|
|||
<script lang="ts">
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { Store } from "../../Logic/UIEventSource"
|
||||
import Loading from "../Base/Loading.svelte"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import Translations from "../i18n/Translations"
|
||||
import { default as SearchResultSvelte } from "./SearchResult.svelte"
|
||||
import MoreScreen from "../BigComponents/MoreScreen"
|
||||
import type { GeocodeResult, SearchResult } from "../../Logic/Geocoding/GeocodingProvider"
|
||||
import type { GeocodeResult } from "../../Logic/Geocoding/GeocodingProvider"
|
||||
|
||||
import ActiveFilters from "./ActiveFilters.svelte"
|
||||
import Constants from "../../Models/Constants"
|
||||
import type { ActiveFilter } from "../../Logic/State/LayerState"
|
||||
import ThemeViewState from "../../Models/ThemeViewState"
|
||||
import FilterResult from "./FilterResult.svelte"
|
||||
import ThemeResult from "./ThemeResult.svelte"
|
||||
|
||||
export let state: SpecialVisualizationState
|
||||
export let results: SearchResult[]
|
||||
export let searchTerm: Store<string>
|
||||
export let isFocused: UIEventSource<boolean>
|
||||
export let state: ThemeViewState
|
||||
let activeFilters: Store<ActiveFilter[]> = state.layerState.activeFilters.map(fs => fs.filter(f => Constants.priviliged_layers.indexOf(<any>f.layer.id) < 0))
|
||||
|
||||
let hasActiveFilters = activeFilters.map(afs => afs.length > 0)
|
||||
|
||||
let recentlySeen: Store<GeocodeResult[]> = state.recentlySearched.seenThisSession
|
||||
let recentlySeen: Store<GeocodeResult[]> = state.searchState.recentlySearched.seenThisSession
|
||||
let recentThemes = state.userRelatedState.recentlyVisitedThemes.mapD(thms => thms.filter(th => th !== state.layout.id).slice(0, 3))
|
||||
let allowOtherThemes = state.featureSwitches.featureSwitchBackToThemeOverview
|
||||
</script>
|
||||
let searchTerm = state.searchState.searchTerm
|
||||
let results = state.searchState.suggestions
|
||||
let filterResults = state.searchState.filterSuggestions
|
||||
let themeResults = state.searchState.themeSuggestions
|
||||
|
||||
</script>
|
||||
<div class="p-4">
|
||||
|
||||
<div class="relative w-full h-full collapsable " class:collapsed={!$isFocused && !$hasActiveFilters}>
|
||||
<div class="searchbox normal-background">
|
||||
<ActiveFilters activeFilters={$activeFilters} />
|
||||
{#if $isFocused}
|
||||
{#if $searchTerm.length > 0 && results === undefined}
|
||||
|
||||
{#if $filterResults.length > 0}
|
||||
<h3>Pick a filter below</h3>
|
||||
|
||||
<div class="flex flex-wrap">
|
||||
{#each $filterResults as filterResult (filterResult)}
|
||||
<FilterResult {state} entry={filterResult} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if $searchTerm.length > 0}
|
||||
<h3>Locations</h3>
|
||||
{/if}
|
||||
{#if $searchTerm.length > 0 && $results === undefined}
|
||||
<div class="flex justify-center m-4 my-8">
|
||||
<Loading />
|
||||
</div>
|
||||
{:else if results?.length > 0}
|
||||
<div style="max-height: calc(50vh - 1rem - 2px);" class="overflow-y-auto p-2" tabindex="-1">
|
||||
|
||||
{#each results as entry (entry)}
|
||||
{:else if $results?.length > 0}
|
||||
{#each $results as entry (entry)}
|
||||
<SearchResultSvelte on:select {entry} {state} />
|
||||
{/each}
|
||||
</div>
|
||||
{:else if $searchTerm.length > 0 || $recentlySeen?.length > 0 || $recentThemes?.length > 0}
|
||||
<div style="max-height: calc(50vh - 1rem - 2px);" class="overflow-y-auto p-2 flex flex-col gap-y-8"
|
||||
<div class="flex flex-col gap-y-8"
|
||||
tabindex="-1">
|
||||
{#if $searchTerm.length > 0}
|
||||
<b class="flex justify-center p-4">
|
||||
|
@ -74,31 +85,15 @@
|
|||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
{#if $themeResults.length > 0}
|
||||
<h3>
|
||||
Other maps
|
||||
</h3>
|
||||
{#each $themeResults as entry}
|
||||
<ThemeResult {state} {entry} />
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.searchbox {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid black;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.collapsable {
|
||||
max-height: 50vh;
|
||||
transition: max-height 400ms linear;
|
||||
transition-delay: 100ms;
|
||||
overflow: hidden;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.collapsed {
|
||||
padding-top: 0 !important;
|
||||
padding-bottom: 0 !important;
|
||||
max-height: 0 !important;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -5,8 +5,8 @@
|
|||
import Icon from "../Map/Icon.svelte"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
|
||||
export let entry: { category: "theme", payload: MinimalLayoutInformation }
|
||||
let otherTheme = entry.payload
|
||||
export let entry: MinimalLayoutInformation
|
||||
let otherTheme = entry
|
||||
</script>
|
||||
|
||||
<a href={MoreScreen.createUrlFor(otherTheme, false)}
|
||||
|
|
|
@ -29,7 +29,7 @@ import LayoutSource from "../Logic/FeatureSource/Sources/LayoutSource"
|
|||
import { Map as MlMap } from "maplibre-gl"
|
||||
import ShowDataLayer from "./Map/ShowDataLayer"
|
||||
import { CombinedFetcher } from "../Logic/Web/NearbyImagesSearch"
|
||||
import { RecentSearch } from "../Logic/Geocoding/RecentSearch"
|
||||
import SearchState from "../Logic/State/SearchState"
|
||||
|
||||
/**
|
||||
* The state needed to render a special Visualisation.
|
||||
|
@ -95,7 +95,7 @@ export interface SpecialVisualizationState {
|
|||
readonly previewedImage: UIEventSource<ProvidedImage>
|
||||
readonly nearbyImageSearcher: CombinedFetcher
|
||||
readonly geolocation: GeoLocationHandler
|
||||
readonly recentlySearched: RecentSearch
|
||||
readonly searchState: SearchState
|
||||
|
||||
getMatchingLayer(properties: Record<string, string>);
|
||||
|
||||
|
|
|
@ -13,9 +13,7 @@
|
|||
import type { MapProperties } from "../Models/MapProperties"
|
||||
import Geosearch from "./Search/Geosearch.svelte"
|
||||
import Translations from "./i18n/Translations"
|
||||
import {
|
||||
MenuIcon
|
||||
} from "@rgossiaux/svelte-heroicons/solid"
|
||||
import { MenuIcon } from "@rgossiaux/svelte-heroicons/solid"
|
||||
import Tr from "./Base/Tr.svelte"
|
||||
import FloatOver from "./Base/FloatOver.svelte"
|
||||
import Constants from "../Models/Constants"
|
||||
|
@ -41,13 +39,13 @@
|
|||
import ReverseGeocoding from "./BigComponents/ReverseGeocoding.svelte"
|
||||
import { BBox } from "../Logic/BBox"
|
||||
import ExtraLinkButton from "./BigComponents/ExtraLinkButton.svelte"
|
||||
import { LastClickFeatureSource } from "../Logic/FeatureSource/Sources/LastClickFeatureSource"
|
||||
import Marker from "./Map/Marker.svelte"
|
||||
import SelectedElementPanel from "./Base/SelectedElementPanel.svelte"
|
||||
import MenuDrawer from "./BigComponents/MenuDrawer.svelte"
|
||||
import DrawerLeft from "./Base/DrawerLeft.svelte"
|
||||
import type { LayerConfigJson } from "../Models/ThemeConfig/Json/LayerConfigJson"
|
||||
import { GeocodingUtils } from "../Logic/Geocoding/GeocodingProvider"
|
||||
import DrawerRight from "./Base/DrawerRight.svelte"
|
||||
import SearchResults from "./Search/SearchResults.svelte"
|
||||
import { CloseButton } from "flowbite-svelte"
|
||||
|
||||
export let state: ThemeViewState
|
||||
let layout = state.layout
|
||||
|
@ -174,107 +172,6 @@
|
|||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="pointer-events-none absolute top-0 left-0 w-full">
|
||||
<!-- Top components -->
|
||||
|
||||
<div
|
||||
class="flex bg-black-light-transparent pointer-events-auto items-center justify-between px-4 py-1 flex-wrap-reverse">
|
||||
<!-- Top bar with tools -->
|
||||
<div class="flex items-center">
|
||||
|
||||
<MapControlButton
|
||||
cls="m-0.5 p-0.5 sm:p-1"
|
||||
arialabel={Translations.t.general.labels.menu}
|
||||
on:click={() => {console.log("Opening...."); state.guistate.menuIsOpened.setData(true)}}
|
||||
on:keydown={forwardEventToMap}
|
||||
>
|
||||
<MenuIcon class="h-6 w-6 cursor-pointer" />
|
||||
</MapControlButton>
|
||||
|
||||
<MapControlButton
|
||||
on:click={() => state.guistate.pageStates.about_theme.set(true)}
|
||||
on:keydown={forwardEventToMap}
|
||||
>
|
||||
<div
|
||||
class="m-0.5 mx-1 flex cursor-pointer items-center max-[480px]:w-full sm:mx-1 mr-2"
|
||||
>
|
||||
<Marker icons={layout.icon} size="h-6 w-6 shrink-0 mr-0.5 sm:mr-1 md:mr-2" />
|
||||
<b class="mr-1">
|
||||
<Tr t={layout.title} />
|
||||
</b>
|
||||
</div>
|
||||
</MapControlButton>
|
||||
</div>
|
||||
|
||||
|
||||
<If condition={state.featureSwitches.featureSwitchSearch}>
|
||||
<div class="w-full sm:w-64 my-2 sm:mt-0">
|
||||
|
||||
<Geosearch
|
||||
bounds={state.mapProperties.bounds}
|
||||
on:searchCompleted={() => {
|
||||
state.map?.data?.getCanvas()?.focus()
|
||||
}}
|
||||
perLayer={state.perLayer}
|
||||
selectedElement={state.selectedElement}
|
||||
geolocationState={state.geolocation.geolocationState}
|
||||
searcher={state.geosearch}
|
||||
{state}
|
||||
/>
|
||||
</div>
|
||||
</If>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="pointer-events-auto float-right mt-1 flex flex-col px-1 max-[480px]:w-full sm:m-2">
|
||||
<If condition={state.visualFeedback}>
|
||||
{#if $selectedElement === undefined}
|
||||
<div class="w-fit">
|
||||
<VisualFeedbackPanel {state} />
|
||||
</div>
|
||||
{/if}
|
||||
</If>
|
||||
|
||||
</div>
|
||||
<div class="float-left m-1 flex flex-col sm:mt-2">
|
||||
<If condition={state.featureSwitches.featureSwitchWelcomeMessage}>
|
||||
|
||||
|
||||
</If>
|
||||
{#if currentViewLayer?.tagRenderings && currentViewLayer.defaultIcon()}
|
||||
<MapControlButton
|
||||
on:click={() => {
|
||||
state.selectCurrentView()
|
||||
}}
|
||||
on:keydown={forwardEventToMap}
|
||||
>
|
||||
<div class="h-8 w-8 cursor-pointer">
|
||||
<ToSvelte construct={() => currentViewLayer.defaultIcon()} />
|
||||
</div>
|
||||
</MapControlButton>
|
||||
{/if}
|
||||
<ExtraLinkButton {state} />
|
||||
<UploadingImageCounter featureId="*" showThankYou={false} {state} />
|
||||
<PendingChangesIndicator {state} />
|
||||
<If condition={state.featureSwitchIsTesting}>
|
||||
<div class="alert w-fit">Testmode</div>
|
||||
</If>
|
||||
{#if state.osmConnection.Backend().startsWith("https://master.apis.dev.openstreetmap.org")}
|
||||
<div class="thanks">Testserver</div>
|
||||
{/if}
|
||||
<If condition={state.featureSwitches.featureSwitchFakeUser}>
|
||||
<div class="alert w-fit">Faking a user (Testmode)</div>
|
||||
</If>
|
||||
</div>
|
||||
<div class="flex w-full flex-col items-center justify-center">
|
||||
<!-- Flex and w-full are needed for the positioning -->
|
||||
<!-- Centermessage -->
|
||||
<StateIndicator {state} />
|
||||
<ReverseGeocoding {state} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pointer-events-none absolute bottom-0 left-0 mb-4 w-screen">
|
||||
<!-- bottom controls -->
|
||||
<div class="flex w-full items-end justify-between px-4">
|
||||
|
@ -380,8 +277,110 @@
|
|||
</If>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<DrawerRight shown={state.searchState.showSearchDrawer} }>
|
||||
<div class="relative">
|
||||
<div class="absolute right-0 top-0 ">
|
||||
<div class="mr-4 mt-4">
|
||||
<CloseButton on:click={() => state.searchState.showSearchDrawer.set(false)} />
|
||||
</div>
|
||||
</div>
|
||||
<SearchResults {state} />
|
||||
</div>
|
||||
</DrawerRight>
|
||||
|
||||
|
||||
<div class="pointer-events-none absolute top-0 left-0 w-full">
|
||||
<!-- Top components -->
|
||||
<div
|
||||
id="top-bar"
|
||||
class="flex bg-black-light-transparent pointer-events-auto items-center justify-between px-4 py-1 flex-wrap-reverse">
|
||||
<!-- Top bar with tools -->
|
||||
<div class="flex items-center">
|
||||
|
||||
<MapControlButton
|
||||
cls="m-0.5 p-0.5 sm:p-1"
|
||||
arialabel={Translations.t.general.labels.menu}
|
||||
on:click={() => {state.guistate.menuIsOpened.setData(true)}}
|
||||
on:keydown={forwardEventToMap}
|
||||
>
|
||||
<MenuIcon class="h-6 w-6 cursor-pointer" />
|
||||
</MapControlButton>
|
||||
|
||||
<MapControlButton
|
||||
on:click={() => state.guistate.pageStates.about_theme.set(true)}
|
||||
on:keydown={forwardEventToMap}
|
||||
>
|
||||
<div
|
||||
class="m-0.5 mx-1 flex cursor-pointer items-center max-[480px]:w-full sm:mx-1 mr-2"
|
||||
>
|
||||
<Marker icons={layout.icon} size="h-6 w-6 shrink-0 mr-0.5 sm:mr-1 md:mr-2" />
|
||||
<b class="mr-1">
|
||||
<Tr t={layout.title} />
|
||||
</b>
|
||||
</div>
|
||||
</MapControlButton>
|
||||
</div>
|
||||
|
||||
|
||||
<If condition={state.featureSwitches.featureSwitchSearch}>
|
||||
<div class="w-full sm:w-80 md:w-96 my-2 sm:mt-0">
|
||||
<Geosearch {state} isFocused={state.searchState.searchIsFocused}
|
||||
searchContents={state.searchState.searchTerm} />
|
||||
</div>
|
||||
</If>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="pointer-events-auto float-right mt-1 flex flex-col px-1 max-[480px]:w-full sm:m-2">
|
||||
<If condition={state.visualFeedback}>
|
||||
{#if $selectedElement === undefined}
|
||||
<div class="w-fit">
|
||||
<VisualFeedbackPanel {state} />
|
||||
</div>
|
||||
{/if}
|
||||
</If>
|
||||
|
||||
</div>
|
||||
<div class="float-left m-1 flex flex-col sm:mt-2">
|
||||
|
||||
{#if currentViewLayer?.tagRenderings && currentViewLayer.defaultIcon()}
|
||||
<MapControlButton
|
||||
on:click={() => {
|
||||
state.selectCurrentView()
|
||||
}}
|
||||
on:keydown={forwardEventToMap}
|
||||
>
|
||||
<div class="h-8 w-8 cursor-pointer">
|
||||
<ToSvelte construct={() => currentViewLayer.defaultIcon()} />
|
||||
</div>
|
||||
</MapControlButton>
|
||||
{/if}
|
||||
<ExtraLinkButton {state} />
|
||||
<UploadingImageCounter featureId="*" showThankYou={false} {state} />
|
||||
<PendingChangesIndicator {state} />
|
||||
<If condition={state.featureSwitchIsTesting}>
|
||||
<div class="alert w-fit">Testmode</div>
|
||||
</If>
|
||||
{#if state.osmConnection.Backend().startsWith("https://master.apis.dev.openstreetmap.org")}
|
||||
<div class="thanks">Testserver</div>
|
||||
{/if}
|
||||
<If condition={state.featureSwitches.featureSwitchFakeUser}>
|
||||
<div class="alert w-fit">Faking a user (Testmode)</div>
|
||||
</If>
|
||||
</div>
|
||||
<div class="flex w-full flex-col items-center justify-center">
|
||||
<!-- Flex and w-full are needed for the positioning -->
|
||||
<!-- Centermessage -->
|
||||
<StateIndicator {state} />
|
||||
<ReverseGeocoding {state} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<LoginToggle ignoreLoading={true} {state}>
|
||||
{#if ($showCrosshair === "yes" && $currentZoom >= 17) || $showCrosshair === "always" || $visualFeedback}
|
||||
<!-- Don't use h-full: h-full does _not_ include the area under the URL-bar, which offsets the crosshair a bit -->
|
||||
|
|
Loading…
Reference in a new issue