Add layers to search menu

This commit is contained in:
Pieter Vander Vennet 2024-09-11 01:46:55 +02:00
parent e6dab1a83f
commit c591770eab
33 changed files with 332 additions and 195 deletions

View file

@ -109,7 +109,7 @@ Success: user sponteanously interacts with the questions!
> Then, the 'cuisine' was inspected. As the restaurant they visited is focusing on _vegetarian_ salads, the user wanted to use the freeform to enter 'vegetarian salad' > Then, the 'cuisine' was inspected. As the restaurant they visited is focusing on _vegetarian_ salads, the user wanted to use the freeform to enter 'vegetarian salad'
[ ] Failure: how to properly exp lain this? Move the 'vegetarian' question up? Should some options, such as 'chicken restaurant' be hidden if `vegetarian=only`? [ ] Failure: how to properly explain this? Move the 'vegetarian' question up? Should some options, such as 'chicken restaurant' be hidden if `vegetarian=only`?
[ ] UI: issue: the emojis (especially flags) slightly overlaps with the text on this browser [ ] UI: issue: the emojis (especially flags) slightly overlaps with the text on this browser

View file

@ -24,6 +24,14 @@
"ca": "Una capa que mostra fonts d'aigua potable", "ca": "Una capa que mostra fonts d'aigua potable",
"cs": "Vrstva zobrazující fontány s pitnou vodou" "cs": "Vrstva zobrazující fontány s pitnou vodou"
}, },
"searchTerms": {
"en": [
"drink","water","fountain","bubbler"
],
"nl": [
"drinken","water","drinkwater","waterfontein","fontein","kraan","kraantje"
]
},
"source": { "source": {
"osmTags": { "osmTags": {
"and": [ "and": [

View file

@ -40,7 +40,7 @@
] ]
} }
}, },
"minzoom": 12, "minzoom": 10,
"title": { "title": {
"render": { "render": {
"en": "Shop", "en": "Shop",

View file

@ -80,7 +80,7 @@
"override": { "override": {
"name=": null, "name=": null,
"minzoom": 17, "minzoom": 17,
"filter": { "=filter": {
"sameAs": "bike_shop" "sameAs": "bike_shop"
} }
} }
@ -182,7 +182,7 @@
"builtin": "charging_station", "builtin": "charging_station",
"override": { "override": {
"name": null, "name": null,
"filter": { "=filter": {
"sameAs": "charging_station_ebikes" "sameAs": "charging_station_ebikes"
}, },
"minzoom": 18, "minzoom": 18,
@ -209,7 +209,7 @@
"builtin": "vending_machine", "builtin": "vending_machine",
"override": { "override": {
"name": null, "name": null,
"filter": { "=filter": {
"sameAs": "vending_machine_bicycle" "sameAs": "vending_machine_bicycle"
}, },
"minzoom": 18, "minzoom": 18,

View file

@ -56,7 +56,7 @@
"trolley_bay" "trolley_bay"
], ],
"overrideAll": { "overrideAll": {
"minzoom": 14, "minzoom": 12,
"syncSelection": "theme-only" "syncSelection": "theme-only"
} }
} }

View file

@ -5,6 +5,9 @@ import { Utils } from "../../Utils"
import Locale from "../../UI/i18n/Locale" import Locale from "../../UI/i18n/Locale"
import Constants from "../../Models/Constants" import Constants from "../../Models/Constants"
/**
* Searches matching filters
*/
export default class FilterSearch implements GeocodingProvider { export default class FilterSearch implements GeocodingProvider {
private readonly _state: SpecialVisualizationState private readonly _state: SpecialVisualizationState
@ -13,18 +16,9 @@ export default class FilterSearch implements GeocodingProvider {
} }
async search(query: string): Promise<SearchResult[]> { async search(query: string): Promise<SearchResult[]> {
return this.searchDirectlyWrapped(query) return this.searchDirectly(query)
} }
public searchDirectly(query: string): FilterResult[] {
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) { if (query.length === 0) {
return [] return []
} }
@ -34,11 +28,14 @@ export default class FilterSearch implements GeocodingProvider {
} }
return query return query
}).filter(q => q.length > 0) }).filter(q => q.length > 0)
const possibleFilters: FilterPayload[] = [] const possibleFilters: FilterResult[] = []
for (const layer of this._state.layout.layers) { for (const layer of this._state.layout.layers) {
if (!Array.isArray(layer.filters)) { if (!Array.isArray(layer.filters)) {
continue continue
} }
if (layer.filterIsSameAs !== undefined) {
continue
}
for (const filter of layer.filters ?? []) { for (const filter of layer.filters ?? []) {
for (let i = 0; i < filter.options.length; i++) { for (let i = 0; i < filter.options.length; i++) {
const option = filter.options[i] const option = filter.options[i]
@ -64,7 +61,14 @@ export default class FilterSearch implements GeocodingProvider {
if (levehnsteinD > 0.25) { if (levehnsteinD > 0.25) {
continue continue
} }
possibleFilters.push({ option, layer, filter, index: i }) possibleFilters.push(<FilterResult>{
category: "filter",
osm_id: layer.id + "/" + filter.id + "/" + i,
payload: {
option, layer, filter, index:
i,
},
})
} }
} }
} }
@ -72,7 +76,7 @@ export default class FilterSearch implements GeocodingProvider {
} }
suggest(query: string, options?: GeocodingOptions): Store<SearchResult[]> { suggest(query: string, options?: GeocodingOptions): Store<SearchResult[]> {
return new ImmutableStore(this.searchDirectlyWrapped(query)) return new ImmutableStore(this.searchDirectly(query))
} }
@ -85,7 +89,7 @@ export default class FilterSearch implements GeocodingProvider {
if (!Array.isArray(filteredLayer.layerDef.filters)) { if (!Array.isArray(filteredLayer.layerDef.filters)) {
continue continue
} }
if (Constants.priviliged_layers.indexOf(id) >= 0) { if (Constants.priviliged_layers.indexOf(<any> id) >= 0) {
continue continue
} }
for (const filter of filteredLayer.layerDef.filters) { for (const filter of filteredLayer.layerDef.filters) {
@ -99,14 +103,14 @@ export default class FilterSearch implements GeocodingProvider {
option, option,
filter, filter,
index: i, index: i,
layer: filteredLayer.layerDef layer: filteredLayer.layerDef,
}) })
} }
Utils.shuffle(singleFilterResults) Utils.shuffle(singleFilterResults)
result.push(...singleFilterResults.slice(0,3)) result.push(...singleFilterResults.slice(0, 3))
} }
} }
Utils.shuffle(result) Utils.shuffle(result)
return result.slice(0,6) return result.slice(0, 6)
} }
} }

View file

@ -46,9 +46,11 @@ export type GeocodeResult = {
} }
export type FilterPayload = { option: FilterConfigOption, filter: FilterConfig, layer: LayerConfig, index: number } export type FilterPayload = { option: FilterConfigOption, filter: FilterConfig, layer: LayerConfig, index: number }
export type FilterResult = { category: "filter", osm_id: string, payload: FilterPayload } export type FilterResult = { category: "filter", osm_id: string, payload: FilterPayload }
export type LayerResult = {category: "layer", osm_id: string, payload: LayerConfig}
export type SearchResult = export type SearchResult =
| FilterResult | FilterResult
| { category: "theme", osm_id: string, payload: MinimalLayoutInformation } | { category: "theme", osm_id: string, payload: MinimalLayoutInformation }
| LayerResult
| GeocodeResult | GeocodeResult
export interface GeocodingOptions { export interface GeocodingOptions {

View file

@ -0,0 +1,53 @@
import GeocodingProvider, { GeocodingOptions, LayerResult, SearchResult } from "./GeocodingProvider"
import { SpecialVisualizationState } from "../../UI/SpecialVisualization"
import MoreScreen from "../../UI/BigComponents/MoreScreen"
import { ImmutableStore, Store } from "../UIEventSource"
import Constants from "../../Models/Constants"
export default class LayerSearch implements GeocodingProvider<LayerResult> {
private readonly _state: SpecialVisualizationState
private readonly _suggestionLimit: number
private readonly _layerWhitelist : Set<string>
constructor(state: SpecialVisualizationState, suggestionLimit: number) {
this._state = state
this._layerWhitelist = new Set(state.layout.layers.map(l => l.id).filter(id => Constants.added_by_default.indexOf(<any> id) < 0))
this._suggestionLimit = suggestionLimit
}
async search(query: string): Promise<LayerResult[]> {
return this.searchWrapped(query, 99)
}
suggest(query: string, options?: GeocodingOptions): Store<LayerResult[]> {
return new ImmutableStore(this.searchWrapped(query, this._suggestionLimit ?? 4))
}
private searchWrapped(query: string, limit: number): LayerResult[] {
return this.searchDirect(query, limit)
}
public searchDirect(query: string, limit: number): LayerResult[] {
if (query.length < 1) {
return []
}
const scores = MoreScreen.scoreLayers(query, this._layerWhitelist)
const asList:(LayerResult & {score:number})[] = []
for (const layer in scores) {
asList.push({
category: "layer",
payload: this._state.layout.getLayer(layer),
osm_id: layer,
score: scores[layer]
})
}
asList.sort((a, b) => a.score - b.score)
return asList
.filter(sorted => sorted.score < 2)
.slice(0, limit)
}
}

View file

@ -8,6 +8,7 @@ import Translations from "../../UI/i18n/Translations"
import { RegexTag } from "../Tags/RegexTag" import { RegexTag } from "../Tags/RegexTag"
import { Or } from "../Tags/Or" import { Or } from "../Tags/Or"
import FilterConfig from "../../Models/ThemeConfig/FilterConfig" import FilterConfig from "../../Models/ThemeConfig/FilterConfig"
import Constants from "../../Models/Constants"
export type ActiveFilter = { export type ActiveFilter = {
layer: LayerConfig, layer: LayerConfig,
@ -35,6 +36,10 @@ export default class LayerState {
private readonly _activeFilters: UIEventSource<ActiveFilter[]> = new UIEventSource([]) private readonly _activeFilters: UIEventSource<ActiveFilter[]> = new UIEventSource([])
public readonly activeFilters: Store<ActiveFilter[]> = this._activeFilters public readonly activeFilters: Store<ActiveFilter[]> = this._activeFilters
private readonly _activeLayers: UIEventSource<FilteredLayer[]> = new UIEventSource<FilteredLayer[]>(undefined)
public readonly activeLayers: Store<FilteredLayer[]> = this._activeLayers
private readonly _nonactiveLayers: UIEventSource<FilteredLayer[]> = new UIEventSource<FilteredLayer[]>(undefined)
public readonly nonactiveLayers: Store<FilteredLayer[]> = this._nonactiveLayers
private readonly osmConnection: OsmConnection private readonly osmConnection: OsmConnection
/** /**
@ -77,8 +82,16 @@ export default class LayerState {
private updateActiveFilters(){ private updateActiveFilters(){
const filters: ActiveFilter[] = [] const filters: ActiveFilter[] = []
const activeLayers: FilteredLayer[] = []
const nonactiveLayers: FilteredLayer[] = []
this.filteredLayers.forEach(fl => { this.filteredLayers.forEach(fl => {
if(!fl.isDisplayed.data){ if(!fl.isDisplayed.data){
nonactiveLayers.push(fl)
return
}
activeLayers.push(fl)
if(fl.layerDef.filterIsSameAs){
return return
} }
for (const [filtername, appliedFilter] of fl.appliedFilters) { for (const [filtername, appliedFilter] of fl.appliedFilters) {
@ -86,6 +99,7 @@ export default class LayerState {
continue continue
} }
const filter = fl.layerDef.filters.find(f => f.id === filtername) const filter = fl.layerDef.filters.find(f => f.id === filtername)
console.log("Updating active filters for flayer", fl.layerDef.id,"with filterconfig",filter)
if(typeof appliedFilter.data === "number"){ if(typeof appliedFilter.data === "number"){
if(filter.options[appliedFilter.data].osmTags === undefined){ if(filter.options[appliedFilter.data].osmTags === undefined){
// This is probably the first, generic option which doesn't _actually_ filter // This is probably the first, generic option which doesn't _actually_ filter
@ -99,6 +113,8 @@ export default class LayerState {
}) })
} }
}) })
this._activeLayers.set(activeLayers)
this._nonactiveLayers.set(nonactiveLayers)
this._activeFilters.set(filters) this._activeFilters.set(filters)
} }

View file

@ -1,25 +1,26 @@
import GeocodingProvider, { import GeocodingProvider, {
FilterPayload, FilterPayload, FilterResult,
GeocodeResult, GeocodeResult,
GeocodingUtils, GeocodingUtils, LayerResult,
type SearchResult type SearchResult,
} from "../Geocoding/GeocodingProvider" } from "../Search/GeocodingProvider"
import { ImmutableStore, Store, Stores, UIEventSource } from "../UIEventSource" import { ImmutableStore, Store, Stores, UIEventSource } from "../UIEventSource"
import CombinedSearcher from "../Geocoding/CombinedSearcher" import CombinedSearcher from "../Search/CombinedSearcher"
import FilterSearch from "../Geocoding/FilterSearch" import FilterSearch from "../Search/FilterSearch"
import LocalElementSearch from "../Geocoding/LocalElementSearch" import LocalElementSearch from "../Search/LocalElementSearch"
import CoordinateSearch from "../Geocoding/CoordinateSearch" import CoordinateSearch from "../Search/CoordinateSearch"
import ThemeSearch from "../Geocoding/ThemeSearch" import ThemeSearch from "../Search/ThemeSearch"
import OpenStreetMapIdSearch from "../Geocoding/OpenStreetMapIdSearch" import OpenStreetMapIdSearch from "../Search/OpenStreetMapIdSearch"
import PhotonSearch from "../Geocoding/PhotonSearch" import PhotonSearch from "../Search/PhotonSearch"
import ThemeViewState from "../../Models/ThemeViewState" import ThemeViewState from "../../Models/ThemeViewState"
import Translations from "../../UI/i18n/Translations" import Translations from "../../UI/i18n/Translations"
import type { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig" import type { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig"
import MoreScreen from "../../UI/BigComponents/MoreScreen" import MoreScreen from "../../UI/BigComponents/MoreScreen"
import { BBox } from "../BBox" import { BBox } from "../BBox"
import { Translation } from "../../UI/i18n/Translation" import { Translation } from "../../UI/i18n/Translation"
import GeocodingFeatureSource from "../Geocoding/GeocodingFeatureSource" import GeocodingFeatureSource from "../Search/GeocodingFeatureSource"
import ShowDataLayer from "../../UI/Map/ShowDataLayer" import ShowDataLayer from "../../UI/Map/ShowDataLayer"
import LayerSearch from "../Search/LayerSearch"
export default class SearchState { export default class SearchState {
@ -28,8 +29,9 @@ export default class SearchState {
public readonly searchTerm: UIEventSource<string> = new UIEventSource<string>("") public readonly searchTerm: UIEventSource<string> = new UIEventSource<string>("")
public readonly searchIsFocused = new UIEventSource(false) public readonly searchIsFocused = new UIEventSource(false)
public readonly suggestions: Store<SearchResult[]> public readonly suggestions: Store<SearchResult[]>
public readonly filterSuggestions: Store<FilterPayload[]> public readonly filterSuggestions: Store<FilterResult[]>
public readonly themeSuggestions: Store<MinimalLayoutInformation[]> public readonly themeSuggestions: Store<MinimalLayoutInformation[]>
public readonly layerSuggestions: Store<LayerResult[]>
public readonly locationSearchers: ReadonlyArray<GeocodingProvider<GeocodeResult>> public readonly locationSearchers: ReadonlyArray<GeocodingProvider<GeocodeResult>>
private readonly state: ThemeViewState private readonly state: ThemeViewState
@ -43,7 +45,7 @@ export default class SearchState {
// new LocalElementSearch(state, 5), // new LocalElementSearch(state, 5),
new CoordinateSearch(), new CoordinateSearch(),
new OpenStreetMapIdSearch(state), new OpenStreetMapIdSearch(state),
new PhotonSearch() // new NominatimGeocoding(), new PhotonSearch(), // new NominatimGeocoding(),
] ]
const bounds = state.mapProperties.bounds const bounds = state.mapProperties.bounds
@ -53,33 +55,36 @@ export default class SearchState {
} }
return this.locationSearchers.map(ls => ls.suggest(search, { bbox: bounds.data })) return this.locationSearchers.map(ls => ls.suggest(search, { bbox: bounds.data }))
}, [bounds] }, [bounds],
) )
this.suggestionsSearchRunning = suggestionsList.bind(suggestions => { this.suggestionsSearchRunning = suggestionsList.bind(suggestions => {
if(suggestions === undefined){ if (suggestions === undefined) {
return new ImmutableStore(true) return new ImmutableStore(true)
} }
return Stores.concat(suggestions).map(suggestions => suggestions.some(list => list === undefined)) return Stores.concat(suggestions).map(suggestions => suggestions.some(list => list === undefined))
}) })
this.suggestions = suggestionsList.bindD(suggestions => this.suggestions = suggestionsList.bindD(suggestions =>
Stores.concat(suggestions).map(suggestions => CombinedSearcher.merge(suggestions)) Stores.concat(suggestions).map(suggestions => CombinedSearcher.merge(suggestions)),
) )
const themeSearch = new ThemeSearch(state, 3) const themeSearch = new ThemeSearch(state, 3)
this.themeSuggestions = this.searchTerm.mapD(query => themeSearch.searchDirect(query, 3)) this.themeSuggestions = this.searchTerm.mapD(query => themeSearch.searchDirect(query, 3))
const layerSearch = new LayerSearch(state, 5)
this.layerSuggestions = this.searchTerm.mapD(query => layerSearch.searchDirect(query, 5))
const filterSearch = new FilterSearch(state) const filterSearch = new FilterSearch(state)
this.filterSuggestions = this.searchTerm.stabilized(50).mapD(query => filterSearch.searchDirectly(query) this.filterSuggestions = this.searchTerm.stabilized(50)
).mapD(filterResult => { .mapD(query => filterSearch.searchDirectly(query))
const active = state.layerState.activeFilters.data .mapD(filterResult => {
return filterResult.filter(({ filter, index, layer }) => { const active = state.layerState.activeFilters.data
const foundMatch = active.some(active => return filterResult.filter(({ payload: { filter, index, layer } }) => {
active.filter.id === filter.id && layer.id === active.layer.id && active.control.data === index) const foundMatch = active.some(active =>
active.filter.id === filter.id && layer.id === active.layer.id && active.control.data === index)
return !foundMatch return !foundMatch
}) })
}, [state.layerState.activeFilters]) }, [state.layerState.activeFilters])
const geocodedFeatures = new GeocodingFeatureSource(this.suggestions.stabilized(250)) const geocodedFeatures = new GeocodingFeatureSource(this.suggestions.stabilized(250))
state.featureProperties.trackFeatureSource(geocodedFeatures) state.featureProperties.trackFeatureSource(geocodedFeatures)
@ -88,8 +93,8 @@ export default class SearchState {
{ {
layer: GeocodingUtils.searchLayer, layer: GeocodingUtils.searchLayer,
features: geocodedFeatures, features: geocodedFeatures,
selectedElement: state.selectedElement selectedElement: state.selectedElement,
} },
) )
this.showSearchDrawer = new UIEventSource(false) this.showSearchDrawer = new UIEventSource(false)
@ -103,21 +108,31 @@ export default class SearchState {
} }
public async apply(result: FilterResult | LayerResult) {
public async apply(payload: FilterPayload) { if (result.category === "filter") {
const state = this.state return this.applyFilter(result.payload)
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)
} }
if (result.category === "layer") {
return this.applyLayer(result)
}
}
private async applyLayer(layer: LayerResult) {
for (const [name, otherLayer] of this.state.layerState.filteredLayers) {
otherLayer.isDisplayed.setData(name === layer.osm_id)
}
}
private async applyFilter(payload: FilterPayload) {
const state = this.state
const { layer, filter, index } = payload
for (const [name, otherLayer] of state.layerState.filteredLayers) {
otherLayer.isDisplayed.setData(name === layer.id)
}
const flayer = state.layerState.filteredLayers.get(layer.id)
flayer.isDisplayed.set(true)
const filtercontrol = flayer.appliedFilters.get(filter.id)
console.log("Could not apply", layer.id, ".", filter.id, index) console.log("Could not apply", layer.id, ".", filter.id, index)
if (filtercontrol.data === index) { if (filtercontrol.data === index) {
filtercontrol.setData(undefined) filtercontrol.setData(undefined)
@ -126,76 +141,4 @@ export default class SearchState {
} }
} }
/**
* 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 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
let poi: SearchResult
if(this.suggestions.data){
poi = this.suggestions.data[0]
}else{
const results = GeocodingUtils.mergeSimilarResults([].concat(...await Promise.all(this.locationSearchers.map(ls => ls.search(query, { bbox: bounds.data })))))
poi = results[0]
}
if (poi.category === "theme") {
const theme = <MinimalLayoutInformation>poi.payload
const url = MoreScreen.createUrlFor(theme)
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)
}
}
} }

View file

@ -20,7 +20,7 @@ import { ThemeMetaTagging } from "./UserSettingsMetaTagging"
import { MapProperties } from "../../Models/MapProperties" import { MapProperties } from "../../Models/MapProperties"
import Showdown from "showdown" import Showdown from "showdown"
import { LocalStorageSource } from "../Web/LocalStorageSource" import { LocalStorageSource } from "../Web/LocalStorageSource"
import { GeocodeResult } from "../Geocoding/GeocodingProvider" import { GeocodeResult } from "../Search/GeocodingProvider"
export class OptionallySyncedHistory<T> { export class OptionallySyncedHistory<T> {
@ -42,8 +42,6 @@ export class OptionallySyncedHistory<T> {
"preference-" + key + "-history", "preference-" + key + "-history",
"sync", "sync",
) )
console.log(">>>",key, this.syncPreference)
const synced = this.synced = UIEventSource.asObject<T[]>(osmconnection.GetLongPreference(key + "-history"), []) const synced = this.synced = UIEventSource.asObject<T[]>(osmconnection.GetLongPreference(key + "-history"), [])
const local = this.local = LocalStorageSource.GetParsed<T[]>(key + "-history", []) const local = this.local = LocalStorageSource.GetParsed<T[]>(key + "-history", [])
const thisSession = this.thisSession = new UIEventSource<T[]>([], "optionally-synced:"+key+"(session only)") const thisSession = this.thisSession = new UIEventSource<T[]>([], "optionally-synced:"+key+"(session only)")
@ -91,7 +89,6 @@ export class OptionallySyncedHistory<T> {
if (this._isSame) { if (this._isSame) {
oldList = oldList.filter(x => !this._isSame(t, x)) oldList = oldList.filter(x => !this._isSame(t, x))
} }
console.log("Setting new history:", store, [t, ...oldList])
store.set([t, ...oldList].slice(0, this._maxHistory)) store.set([t, ...oldList].slice(0, this._maxHistory))
} }
@ -198,7 +195,6 @@ export default class UserRelatedState {
this.showTags = this.osmConnection.GetPreference("show_tags") this.showTags = this.osmConnection.GetPreference("show_tags")
this.showCrosshair = this.osmConnection.GetPreference("show_crosshair") this.showCrosshair = this.osmConnection.GetPreference("show_crosshair")
this.fixateNorth = this.osmConnection.GetPreference("fixate-north") this.fixateNorth = this.osmConnection.GetPreference("fixate-north")
console.log("Fixate north is:", this.fixateNorth)
this.morePrivacy = this.osmConnection.GetPreference("more_privacy", "no") this.morePrivacy = this.osmConnection.GetPreference("more_privacy", "no")
this.a11y = this.osmConnection.GetPreference("a11y") this.a11y = this.osmConnection.GetPreference("a11y")

View file

@ -641,4 +641,18 @@ export default class LayerConfig extends WithContextLoader {
} }
return mostShadowed ?? matchingPresets[0] return mostShadowed ?? matchingPresets[0]
} }
public isNormal(){
if(this.id.startsWith("note_import")){
return false
}
if(Constants.added_by_default.indexOf(<any> this.id) >=0){
return false
}
if(this.filterIsSameAs !== undefined){
return false
}
return true
}
} }

View file

@ -67,7 +67,7 @@ import { LayerConfigJson } from "./ThemeConfig/Json/LayerConfigJson"
import Hash from "../Logic/Web/Hash" import Hash from "../Logic/Web/Hash"
import { GeoOperations } from "../Logic/GeoOperations" import { GeoOperations } from "../Logic/GeoOperations"
import { CombinedFetcher } from "../Logic/Web/NearbyImagesSearch" import { CombinedFetcher } from "../Logic/Web/NearbyImagesSearch"
import { GeocodeResult, GeocodingUtils } from "../Logic/Geocoding/GeocodingProvider" import { GeocodeResult, GeocodingUtils } from "../Logic/Search/GeocodingProvider"
import SearchState from "../Logic/State/SearchState" import SearchState from "../Logic/State/SearchState"
/** /**

View file

@ -8,7 +8,7 @@
import Loading from "./Loading.svelte" import Loading from "./Loading.svelte"
import { onDestroy } from "svelte" import { onDestroy } from "svelte"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig" import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import { GeocodingUtils } from "../../Logic/Geocoding/GeocodingProvider" import { GeocodingUtils } from "../../Logic/Search/GeocodingProvider"
import ThemeViewState from "../../Models/ThemeViewState" import ThemeViewState from "../../Models/ThemeViewState"
export let state: SpecialVisualizationState export let state: SpecialVisualizationState

View file

@ -66,7 +66,7 @@ export default class MoreScreen {
return Infinity return Infinity
} }
language ??= Locale.language.data language ??= Locale.language.data
const queryParts = query.split(" ").map(q => Utils.simplifyStringForSearch(q)) const queryParts = query.trim().split(" ").map(q => Utils.simplifyStringForSearch(q))
let terms: string[] let terms: string[]
if (Array.isArray(keywords)) { if (Array.isArray(keywords)) {
terms = keywords terms = keywords
@ -90,9 +90,12 @@ export default class MoreScreen {
return distanceSummed return distanceSummed
} }
public static scoreLayers(query: string): Record<string, number> { public static scoreLayers(query: string, layerWhitelist?: Set<string>): Record<string, number> {
const result: Record<string, number> = {} const result: Record<string, number> = {}
for (const id in this.officialThemes.layers) { for (const id in this.officialThemes.layers) {
if(layerWhitelist !== undefined && !layerWhitelist.has(id)){
continue
}
const keywords = this.officialThemes.layers[id] const keywords = this.officialThemes.layers[id]
const distance = this.scoreKeywords(query, keywords) const distance = this.scoreKeywords(query, keywords)
result[id] = distance result[id] = distance

View file

@ -3,7 +3,7 @@
* Shows the current address when shaken * Shows the current address when shaken
**/ **/
import Motion from "../../Sensors/Motion" import Motion from "../../Sensors/Motion"
import { NominatimGeocoding } from "../../Logic/Geocoding/NominatimGeocoding" import { NominatimGeocoding } from "../../Logic/Search/NominatimGeocoding"
import Hotkeys from "../Base/Hotkeys" import Hotkeys from "../Base/Hotkeys"
import Translations from "../i18n/Translations" import Translations from "../i18n/Translations"
import Locale from "../i18n/Locale" import Locale from "../i18n/Locale"

View file

@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import type { ActiveFilter } from "../../Logic/State/LayerState" import type { ActiveFilter } from "../../Logic/State/LayerState"
import FilterOption from "./FilterOption.svelte" import FilterOption from "./FilterOption.svelte"
import { XMarkIcon } from "@babeard/svelte-heroicons/mini"
import Loading from "../Base/Loading.svelte" import Loading from "../Base/Loading.svelte"
import FilterToggle from "./FilterToggle.svelte"
export let activeFilter: ActiveFilter export let activeFilter: ActiveFilter
@ -21,10 +21,7 @@
{#if loading} {#if loading}
<Loading /> <Loading />
{:else } {:else }
<div class="badge button-unstyled w-fit"> <FilterToggle on:click={() => clear()}>
<FilterOption option={$option} /> <FilterOption option={$option} />
<button on:click={() => clear()}> </FilterToggle>
<XMarkIcon class="w-5 h-5 pl-1" color="gray" />
</button>
</div>
{/if} {/if}

View file

@ -3,13 +3,33 @@
import type { ActiveFilter } from "../../Logic/State/LayerState" import type { ActiveFilter } from "../../Logic/State/LayerState"
import Loading from "../Base/Loading.svelte" import Loading from "../Base/Loading.svelte"
import SidebarUnit from "../Base/SidebarUnit.svelte" import SidebarUnit from "../Base/SidebarUnit.svelte"
import type { SpecialVisualizationState } from "../SpecialVisualization"
import FilteredLayer from "../../Models/FilteredLayer"
import FilterToggle from "./FilterToggle.svelte"
import ToSvelte from "../Base/ToSvelte.svelte"
import Tr from "../Base/Tr.svelte"
import Constants from "../../Models/Constants"
import { Store } from "../../Logic/UIEventSource"
export let activeFilters: ActiveFilter[] export let activeFilters: ActiveFilter[]
export let state: SpecialVisualizationState
let loading = false let loading = false
let activeLayers: Store<FilteredLayer[]> = state.layerState.activeLayers.mapD(l => l.filter(l => l.layerDef.isNormal()))
let nonactiveLayers: Store<FilteredLayer[]> = state.layerState.nonactiveLayers.mapD(l => l.filter(l => l.layerDef.isNormal()))
function enableAllLayers() {
for (const flayer of $nonactiveLayers) {
flayer.isDisplayed.set(true)
}
}
function clear() { function clear() {
loading = true loading = true
requestIdleCallback(() => { requestIdleCallback(() => {
enableAllLayers()
for (const activeFilter of activeFilters) { for (const activeFilter of activeFilters) {
activeFilter.control.setData(undefined) activeFilter.control.setData(undefined)
@ -18,25 +38,52 @@
}) })
} }
</script> </script>
{#if activeFilters.length > 0}
{#if activeFilters.length > 0 || $activeLayers.length === 1 || $nonactiveLayers.length > 0}
<SidebarUnit> <SidebarUnit>
<h3>Active filters</h3> <div class="flex justify-between">
<h3>Active filters</h3>
{#if loading}
<Loading />
{:else}
<div class="flex flex-wrap gap-x-1 gap-y-2">
{#each activeFilters as activeFilter (activeFilter)}
<div>
<ActiveFilterSvelte {activeFilter} />
</div>
{/each}
</div>
<button class="as-link subtle self-end" on:click={() => clear()} style="margin-right: 0.75rem"> <button class="as-link subtle self-end" on:click={() => clear()} style="margin-right: 0.75rem">
Clear filters Clear filters
</button> </button>
</div>
{#if loading}
<Loading />
{:else}
<div class="flex flex-wrap gap-x-1 gap-y-2 overflow-x-hidden overflow-y-auto">
{#if $activeLayers.length === 1}
<FilterToggle on:click={() => enableAllLayers()}>
<div class="w-8 h-8 p-1">
<ToSvelte construct={$activeLayers[0].layerDef.defaultIcon()} />
</div>
<b>
<Tr t={$activeLayers[0].layerDef.name} />
</b>
</FilterToggle>
{:else if $nonactiveLayers.length > 0}
{#each $nonactiveLayers as nonActive (nonActive.layerDef.id)}
<FilterToggle on:click={() => nonActive.isDisplayed.set(true)}>
<div class="w-8 h-8 p-1">
<ToSvelte construct={nonActive.layerDef.defaultIcon()} />
</div>
<del class="block-ruby">
<Tr t={nonActive.layerDef.name} />
</del>
</FilterToggle>
{/each}
{/if}
{#each activeFilters as activeFilter (activeFilter)}
<div>
<ActiveFilterSvelte {activeFilter} />
</div>
{/each}
</div>
{/if} {/if}
</SidebarUnit> </SidebarUnit>

View file

@ -1,27 +1,36 @@
<script lang="ts"> <script lang="ts">
import type { SpecialVisualizationState } from "../SpecialVisualization" import type { SpecialVisualizationState } from "../SpecialVisualization"
import Tr from "../Base/Tr.svelte" import Tr from "../Base/Tr.svelte"
import type { FilterPayload } from "../../Logic/Geocoding/GeocodingProvider" import type { FilterPayload, FilterResult, LayerResult } from "../../Logic/Search/GeocodingProvider"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import Icon from "../Map/Icon.svelte" import Icon from "../Map/Icon.svelte"
import Marker from "../Map/Marker.svelte"
import ToSvelte from "../Base/ToSvelte.svelte"
export let entry: FilterPayload export let entry: FilterResult | LayerResult
let { option } = entry
export let state: SpecialVisualizationState export let state: SpecialVisualizationState
let dispatch = createEventDispatcher<{ select }>() let dispatch = createEventDispatcher<{ select }>()
function apply() { function apply() {
state.searchState.apply(entry) state.searchState.apply(entry)
dispatch("select") dispatch("select")
} }
</script> </script>
<button on:click={() => apply()}> <button on:click={() => apply()}>
<div class="flex flex-col items-start"> <div class="flex flex-col items-start">
<div class="flex items-center gap-x-1"> <div class="flex items-center gap-x-1">
<Icon icon={option.icon ?? option.emoji} clss="w-4 h-4" emojiHeight="14px" /> {#if entry.category === "layer"}
<Tr cls="whitespace-nowrap" t={option.question} /> <div class="w-8 h-8 p-1">
<ToSvelte construct={entry.payload.defaultIcon()} />
</div>
<b>
<Tr t={entry.payload.name} />
</b>
{:else}
<Icon icon={entry.payload.option.icon ?? entry.payload. option.emoji} clss="w-4 h-4" emojiHeight="14px" />
<Tr cls="whitespace-nowrap" t={entry.payload.option.question} />
{/if}
</div> </div>
</div> </div>
</button> </button>

View file

@ -0,0 +1,9 @@
<script lang="ts">
import { XMarkIcon } from "@babeard/svelte-heroicons/mini"
</script>
<div class="badge button-unstyled w-fit">
<slot/>
<button on:click>
<XMarkIcon class="w-5 h-5 pl-1" color="gray" />
</button>
</div>

View file

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { GeocodingUtils } from "../../Logic/Geocoding/GeocodingProvider" import { GeocodingUtils } from "../../Logic/Search/GeocodingProvider"
import type { GeocodeResult } from "../../Logic/Geocoding/GeocodingProvider" import type { GeocodeResult } from "../../Logic/Search/GeocodingProvider"
import { GeoOperations } from "../../Logic/GeoOperations" import { GeoOperations } from "../../Logic/GeoOperations"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig" import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import { UIEventSource } from "../../Logic/UIEventSource" import { UIEventSource } from "../../Logic/UIEventSource"

View file

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import type { SearchResult } from "../../Logic/Geocoding/GeocodingProvider" import type { SearchResult } from "../../Logic/Search/GeocodingProvider"
import ThemeResult from "../Search/ThemeResult.svelte" import ThemeResult from "../Search/ThemeResult.svelte"
import FilterResult from "./FilterResult.svelte" import FilterResult from "./FilterResult.svelte"

View file

@ -1,51 +1,79 @@
<script lang="ts"> <script lang="ts">
import { Store, UIEventSource } from "../../Logic/UIEventSource" import { Store } from "../../Logic/UIEventSource"
import Loading from "../Base/Loading.svelte" import Loading from "../Base/Loading.svelte"
import Tr from "../Base/Tr.svelte" import Tr from "../Base/Tr.svelte"
import Translations from "../i18n/Translations" import Translations from "../i18n/Translations"
import { default as SearchResultSvelte } from "./SearchResult.svelte" import { default as SearchResultSvelte } from "./SearchResult.svelte"
import MoreScreen from "../BigComponents/MoreScreen" import MoreScreen from "../BigComponents/MoreScreen"
import type { GeocodeResult } from "../../Logic/Geocoding/GeocodingProvider" import type { FilterResult, GeocodeResult, LayerResult } from "../../Logic/Search/GeocodingProvider"
import ActiveFilters from "./ActiveFilters.svelte" import ActiveFilters from "./ActiveFilters.svelte"
import Constants from "../../Models/Constants" import Constants from "../../Models/Constants"
import type { ActiveFilter } from "../../Logic/State/LayerState" import type { ActiveFilter } from "../../Logic/State/LayerState"
import ThemeViewState from "../../Models/ThemeViewState" import ThemeViewState from "../../Models/ThemeViewState"
import FilterResult from "./FilterResult.svelte" import {default as FilterResultSvelte} from "./FilterResult.svelte"
import ThemeResult from "./ThemeResult.svelte" import ThemeResult from "./ThemeResult.svelte"
import SidebarUnit from "../Base/SidebarUnit.svelte" import SidebarUnit from "../Base/SidebarUnit.svelte"
import { TrashIcon } from "@babeard/svelte-heroicons/mini" import { TrashIcon } from "@babeard/svelte-heroicons/mini"
import { Dropdown, DropdownItem } from "flowbite-svelte"
import DotsCircleHorizontal from "@rgossiaux/svelte-heroicons/solid/DotsCircleHorizontal"
import DotMenu from "../Base/DotMenu.svelte" import DotMenu from "../Base/DotMenu.svelte"
import { CogIcon } from "@rgossiaux/svelte-heroicons/solid" import { CogIcon } from "@rgossiaux/svelte-heroicons/solid"
export let state: ThemeViewState 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 activeFilters: Store<ActiveFilter[]> = state.layerState.activeFilters.map(fs => fs.filter(f => Constants.priviliged_layers.indexOf(<any>f.layer.id) < 0))
let recentlySeen: Store<GeocodeResult[]> = state.userRelatedState.recentlyVisitedSearch.value let recentlySeen: Store<GeocodeResult[]> = state.userRelatedState.recentlyVisitedSearch.value
let recentThemes = state.userRelatedState.recentlyVisitedThemes.value.map(themes => themes.filter(th => th.id !== state.layout.id).slice(0, 6)) let recentThemes = state.userRelatedState.recentlyVisitedThemes.value.map(themes => themes.filter(th => th !== state.layout.id).slice(0, 6))
let allowOtherThemes = state.featureSwitches.featureSwitchBackToThemeOverview let allowOtherThemes = state.featureSwitches.featureSwitchBackToThemeOverview
let searchTerm = state.searchState.searchTerm let searchTerm = state.searchState.searchTerm
let results = state.searchState.suggestions let results = state.searchState.suggestions
let isSearching = state.searchState.suggestionsSearchRunning let isSearching = state.searchState.suggestionsSearchRunning
let filterResults = state.searchState.filterSuggestions let filterResults = state.searchState.filterSuggestions
let activeLayers = state.layerState.activeLayers
let layerResults = state.searchState.layerSuggestions.map(layers => {
const nowActive = activeLayers.data.filter(al => al.layerDef.isNormal())
if(nowActive.length === 1){
const shownInActiveFiltersView = nowActive[0]
layers = layers.filter(l => l.payload.id !== shownInActiveFiltersView.layerDef.id)
}
return layers
}, [activeLayers])
let filterResultsClipped = filterResults.mapD(filters => {
let layers = layerResults.data
const ls : (FilterResult | LayerResult)[] = [].concat(layers, filters)
if (ls.length <= 8) {
return ls
}
return ls.slice(0, 6)
}, [layerResults, activeLayers])
let themeResults = state.searchState.themeSuggestions let themeResults = state.searchState.themeSuggestions
</script> </script>
<div class="p-4 low-interaction flex gap-y-2 flex-col"> <div class="p-4 low-interaction flex gap-y-2 flex-col">
<ActiveFilters activeFilters={$activeFilters} /> <ActiveFilters {state} activeFilters={$activeFilters} />
{#if $searchTerm.length > 0 && $filterResults.length > 0} {#if $searchTerm.length === 0 && $filterResults.length === 0 && $activeFilters.length === 0 && $recentThemes.length === 0}
<div class="p-8 items-center text-center">
<b>Use the search bar above to search for locations, filters and other maps</b>
</div>
{/if}
{#if $searchTerm.length > 0 && ($filterResults.length > 0 || $layerResults.length > 0)}
<SidebarUnit> <SidebarUnit>
<h3>Pick a filter below</h3> <h3>Pick a filter below</h3>
<div class="flex flex-wrap"> <div class="flex flex-wrap">
{#each $filterResults as filterResult (filterResult)} {#each $filterResultsClipped as filterResult (filterResult)}
<FilterResult {state} entry={filterResult} /> <FilterResultSvelte {state} entry={filterResult} />
{/each} {/each}
</div> </div>
{#if $filterResults.length + $layerResults.length > $filterResultsClipped.length}
<div class="flex justify-center">
... and {$filterResults.length + $layerResults.length - $filterResultsClipped.length} more ...
</div>
{/if}
</SidebarUnit> </SidebarUnit>
{/if} {/if}
@ -68,7 +96,7 @@
{:else if !$isSearching} {:else if !$isSearching}
<b class="flex justify-center p-4"> <b class="flex justify-center p-4">
<Tr t={Translations.t.general.search.nothingFor.Subs({term: $searchTerm})} /> <Tr t={Translations.t.general.search.nothingFor.Subs({term: "<i>"+$searchTerm+"</i>"})} />
</b> </b>
{/if} {/if}
</SidebarUnit> </SidebarUnit>
@ -88,8 +116,16 @@
</SidebarUnit> </SidebarUnit>
{/if} {/if}
{#if $searchTerm.length === 0 && $recentlySeen?.length === 0 && $recentThemes.length === 0}
<SidebarUnit>
<h3>
{#if $searchTerm.length == 0 && $recentlySeen?.length > 0} Suggestions
</h3>
</SidebarUnit>
{/if}
{#if $searchTerm.length === 0 && $recentlySeen?.length > 0}
<SidebarUnit> <SidebarUnit>
<div class="flex justify-between"> <div class="flex justify-between">

View file

@ -326,7 +326,7 @@
<div <div
id="top-bar" id="top-bar"
class="flex bg-black-light-transparent pointer-events-auto items-center justify-between px-4 py-1 flex-wrap-reverse"> class="flex bg-black-light-transparent pointer-events-auto items-center justify-between px-4 py-1 flex-wrap">
<!-- Top bar with tools --> <!-- Top bar with tools -->
<div class="flex items-center"> <div class="flex items-center">
@ -361,7 +361,7 @@
{/if} {/if}
<If condition={state.featureSwitches.featureSwitchSearch}> <If condition={state.featureSwitches.featureSwitchSearch}>
<div class="flex items-center"> <div class="flex items-center flex-grow justify-end">
<div class="w-full sm:w-64"> <div class="w-full sm:w-64">
<Searchbar value={state.searchState.searchTerm} isFocused={state.searchState.searchIsFocused} /> <Searchbar value={state.searchState.searchTerm} isFocused={state.searchState.searchIsFocused} />
</div> </div>

View file

@ -391,8 +391,8 @@ h2.group {
align-items: center; align-items: center;
white-space: nowrap; white-space: nowrap;
border-radius: 999rem; border-radius: 999rem;
padding-left: 0.5rem; padding-left: 0.25rem;
padding-right: 0.5rem; padding-right: 0.25rem;
border: 1px solid var(--subtle-detail-color-light-contrast); border: 1px solid var(--subtle-detail-color-light-contrast);
background-color: var(--low-interaction-background); background-color: var(--low-interaction-background);
} }