forked from MapComplete/MapComplete
Add layers to search menu
This commit is contained in:
parent
e6dab1a83f
commit
c591770eab
33 changed files with 332 additions and 195 deletions
|
@ -24,6 +24,14 @@
|
|||
"ca": "Una capa que mostra fonts d'aigua potable",
|
||||
"cs": "Vrstva zobrazující fontány s pitnou vodou"
|
||||
},
|
||||
"searchTerms": {
|
||||
"en": [
|
||||
"drink","water","fountain","bubbler"
|
||||
],
|
||||
"nl": [
|
||||
"drinken","water","drinkwater","waterfontein","fontein","kraan","kraantje"
|
||||
]
|
||||
},
|
||||
"source": {
|
||||
"osmTags": {
|
||||
"and": [
|
||||
|
|
|
@ -40,7 +40,7 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"minzoom": 12,
|
||||
"minzoom": 10,
|
||||
"title": {
|
||||
"render": {
|
||||
"en": "Shop",
|
||||
|
|
|
@ -80,7 +80,7 @@
|
|||
"override": {
|
||||
"name=": null,
|
||||
"minzoom": 17,
|
||||
"filter": {
|
||||
"=filter": {
|
||||
"sameAs": "bike_shop"
|
||||
}
|
||||
}
|
||||
|
@ -182,7 +182,7 @@
|
|||
"builtin": "charging_station",
|
||||
"override": {
|
||||
"name": null,
|
||||
"filter": {
|
||||
"=filter": {
|
||||
"sameAs": "charging_station_ebikes"
|
||||
},
|
||||
"minzoom": 18,
|
||||
|
@ -209,7 +209,7 @@
|
|||
"builtin": "vending_machine",
|
||||
"override": {
|
||||
"name": null,
|
||||
"filter": {
|
||||
"=filter": {
|
||||
"sameAs": "vending_machine_bicycle"
|
||||
},
|
||||
"minzoom": 18,
|
||||
|
|
|
@ -56,7 +56,7 @@
|
|||
"trolley_bay"
|
||||
],
|
||||
"overrideAll": {
|
||||
"minzoom": 14,
|
||||
"minzoom": 12,
|
||||
"syncSelection": "theme-only"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,9 @@ import { Utils } from "../../Utils"
|
|||
import Locale from "../../UI/i18n/Locale"
|
||||
import Constants from "../../Models/Constants"
|
||||
|
||||
/**
|
||||
* Searches matching filters
|
||||
*/
|
||||
export default class FilterSearch implements GeocodingProvider {
|
||||
private readonly _state: SpecialVisualizationState
|
||||
|
||||
|
@ -13,18 +16,9 @@ export default class FilterSearch implements GeocodingProvider {
|
|||
}
|
||||
|
||||
async search(query: string): Promise<SearchResult[]> {
|
||||
return this.searchDirectlyWrapped(query)
|
||||
return this.searchDirectly(query)
|
||||
}
|
||||
|
||||
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[] {
|
||||
public searchDirectly(query: string): FilterResult[] {
|
||||
if (query.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
@ -34,11 +28,14 @@ export default class FilterSearch implements GeocodingProvider {
|
|||
}
|
||||
return query
|
||||
}).filter(q => q.length > 0)
|
||||
const possibleFilters: FilterPayload[] = []
|
||||
const possibleFilters: FilterResult[] = []
|
||||
for (const layer of this._state.layout.layers) {
|
||||
if (!Array.isArray(layer.filters)) {
|
||||
continue
|
||||
}
|
||||
if (layer.filterIsSameAs !== undefined) {
|
||||
continue
|
||||
}
|
||||
for (const filter of layer.filters ?? []) {
|
||||
for (let i = 0; i < filter.options.length; i++) {
|
||||
const option = filter.options[i]
|
||||
|
@ -64,7 +61,14 @@ export default class FilterSearch implements GeocodingProvider {
|
|||
if (levehnsteinD > 0.25) {
|
||||
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[]> {
|
||||
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)) {
|
||||
continue
|
||||
}
|
||||
if (Constants.priviliged_layers.indexOf(id) >= 0) {
|
||||
if (Constants.priviliged_layers.indexOf(<any> id) >= 0) {
|
||||
continue
|
||||
}
|
||||
for (const filter of filteredLayer.layerDef.filters) {
|
||||
|
@ -99,7 +103,7 @@ export default class FilterSearch implements GeocodingProvider {
|
|||
option,
|
||||
filter,
|
||||
index: i,
|
||||
layer: filteredLayer.layerDef
|
||||
layer: filteredLayer.layerDef,
|
||||
})
|
||||
}
|
||||
Utils.shuffle(singleFilterResults)
|
|
@ -46,9 +46,11 @@ export type GeocodeResult = {
|
|||
}
|
||||
export type FilterPayload = { option: FilterConfigOption, filter: FilterConfig, layer: LayerConfig, index: number }
|
||||
export type FilterResult = { category: "filter", osm_id: string, payload: FilterPayload }
|
||||
export type LayerResult = {category: "layer", osm_id: string, payload: LayerConfig}
|
||||
export type SearchResult =
|
||||
| FilterResult
|
||||
| { category: "theme", osm_id: string, payload: MinimalLayoutInformation }
|
||||
| LayerResult
|
||||
| GeocodeResult
|
||||
|
||||
export interface GeocodingOptions {
|
53
src/Logic/Search/LayerSearch.ts
Normal file
53
src/Logic/Search/LayerSearch.ts
Normal 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)
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -8,6 +8,7 @@ import Translations from "../../UI/i18n/Translations"
|
|||
import { RegexTag } from "../Tags/RegexTag"
|
||||
import { Or } from "../Tags/Or"
|
||||
import FilterConfig from "../../Models/ThemeConfig/FilterConfig"
|
||||
import Constants from "../../Models/Constants"
|
||||
|
||||
export type ActiveFilter = {
|
||||
layer: LayerConfig,
|
||||
|
@ -35,6 +36,10 @@ export default class LayerState {
|
|||
private readonly _activeFilters: UIEventSource<ActiveFilter[]> = new UIEventSource([])
|
||||
|
||||
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
|
||||
|
||||
/**
|
||||
|
@ -77,8 +82,16 @@ export default class LayerState {
|
|||
|
||||
private updateActiveFilters(){
|
||||
const filters: ActiveFilter[] = []
|
||||
const activeLayers: FilteredLayer[] = []
|
||||
const nonactiveLayers: FilteredLayer[] = []
|
||||
this.filteredLayers.forEach(fl => {
|
||||
if(!fl.isDisplayed.data){
|
||||
nonactiveLayers.push(fl)
|
||||
return
|
||||
}
|
||||
activeLayers.push(fl)
|
||||
|
||||
if(fl.layerDef.filterIsSameAs){
|
||||
return
|
||||
}
|
||||
for (const [filtername, appliedFilter] of fl.appliedFilters) {
|
||||
|
@ -86,6 +99,7 @@ export default class LayerState {
|
|||
continue
|
||||
}
|
||||
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(filter.options[appliedFilter.data].osmTags === undefined){
|
||||
// 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)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,25 +1,26 @@
|
|||
import GeocodingProvider, {
|
||||
FilterPayload,
|
||||
FilterPayload, FilterResult,
|
||||
GeocodeResult,
|
||||
GeocodingUtils,
|
||||
type SearchResult
|
||||
} from "../Geocoding/GeocodingProvider"
|
||||
GeocodingUtils, LayerResult,
|
||||
type SearchResult,
|
||||
} from "../Search/GeocodingProvider"
|
||||
import { ImmutableStore, 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 CombinedSearcher from "../Search/CombinedSearcher"
|
||||
import FilterSearch from "../Search/FilterSearch"
|
||||
import LocalElementSearch from "../Search/LocalElementSearch"
|
||||
import CoordinateSearch from "../Search/CoordinateSearch"
|
||||
import ThemeSearch from "../Search/ThemeSearch"
|
||||
import OpenStreetMapIdSearch from "../Search/OpenStreetMapIdSearch"
|
||||
import PhotonSearch from "../Search/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 GeocodingFeatureSource from "../Search/GeocodingFeatureSource"
|
||||
import ShowDataLayer from "../../UI/Map/ShowDataLayer"
|
||||
import LayerSearch from "../Search/LayerSearch"
|
||||
|
||||
export default class SearchState {
|
||||
|
||||
|
@ -28,8 +29,9 @@ export default class SearchState {
|
|||
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 filterSuggestions: Store<FilterResult[]>
|
||||
public readonly themeSuggestions: Store<MinimalLayoutInformation[]>
|
||||
public readonly layerSuggestions: Store<LayerResult[]>
|
||||
public readonly locationSearchers: ReadonlyArray<GeocodingProvider<GeocodeResult>>
|
||||
|
||||
private readonly state: ThemeViewState
|
||||
|
@ -43,7 +45,7 @@ export default class SearchState {
|
|||
// new LocalElementSearch(state, 5),
|
||||
new CoordinateSearch(),
|
||||
new OpenStreetMapIdSearch(state),
|
||||
new PhotonSearch() // new NominatimGeocoding(),
|
||||
new PhotonSearch(), // new NominatimGeocoding(),
|
||||
]
|
||||
|
||||
const bounds = state.mapProperties.bounds
|
||||
|
@ -53,7 +55,7 @@ export default class SearchState {
|
|||
}
|
||||
return this.locationSearchers.map(ls => ls.suggest(search, { bbox: bounds.data }))
|
||||
|
||||
}, [bounds]
|
||||
}, [bounds],
|
||||
)
|
||||
this.suggestionsSearchRunning = suggestionsList.bind(suggestions => {
|
||||
if (suggestions === undefined) {
|
||||
|
@ -62,18 +64,21 @@ export default class SearchState {
|
|||
return Stores.concat(suggestions).map(suggestions => suggestions.some(list => list === undefined))
|
||||
})
|
||||
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)
|
||||
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)
|
||||
this.filterSuggestions = this.searchTerm.stabilized(50).mapD(query => filterSearch.searchDirectly(query)
|
||||
).mapD(filterResult => {
|
||||
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 }) => {
|
||||
return filterResult.filter(({ payload: { filter, index, layer } }) => {
|
||||
const foundMatch = active.some(active =>
|
||||
active.filter.id === filter.id && layer.id === active.layer.id && active.control.data === index)
|
||||
|
||||
|
@ -88,8 +93,8 @@ export default class SearchState {
|
|||
{
|
||||
layer: GeocodingUtils.searchLayer,
|
||||
features: geocodedFeatures,
|
||||
selectedElement: state.selectedElement
|
||||
}
|
||||
selectedElement: state.selectedElement,
|
||||
},
|
||||
)
|
||||
|
||||
this.showSearchDrawer = new UIEventSource(false)
|
||||
|
@ -103,21 +108,31 @@ export default class SearchState {
|
|||
|
||||
}
|
||||
|
||||
public async apply(result: FilterResult | LayerResult) {
|
||||
if (result.category === "filter") {
|
||||
return this.applyFilter(result.payload)
|
||||
}
|
||||
if (result.category === "layer") {
|
||||
return this.applyLayer(result)
|
||||
}
|
||||
}
|
||||
|
||||
public async apply(payload: FilterPayload) {
|
||||
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
|
||||
|
||||
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(name === layer.id)
|
||||
}
|
||||
otherLayer.isDisplayed.setData(false)
|
||||
}
|
||||
|
||||
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)
|
||||
if (filtercontrol.data === index) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ import { ThemeMetaTagging } from "./UserSettingsMetaTagging"
|
|||
import { MapProperties } from "../../Models/MapProperties"
|
||||
import Showdown from "showdown"
|
||||
import { LocalStorageSource } from "../Web/LocalStorageSource"
|
||||
import { GeocodeResult } from "../Geocoding/GeocodingProvider"
|
||||
import { GeocodeResult } from "../Search/GeocodingProvider"
|
||||
|
||||
|
||||
export class OptionallySyncedHistory<T> {
|
||||
|
@ -42,8 +42,6 @@ export class OptionallySyncedHistory<T> {
|
|||
"preference-" + key + "-history",
|
||||
"sync",
|
||||
)
|
||||
console.log(">>>",key, this.syncPreference)
|
||||
|
||||
const synced = this.synced = UIEventSource.asObject<T[]>(osmconnection.GetLongPreference(key + "-history"), [])
|
||||
const local = this.local = LocalStorageSource.GetParsed<T[]>(key + "-history", [])
|
||||
const thisSession = this.thisSession = new UIEventSource<T[]>([], "optionally-synced:"+key+"(session only)")
|
||||
|
@ -91,7 +89,6 @@ export class OptionallySyncedHistory<T> {
|
|||
if (this._isSame) {
|
||||
oldList = oldList.filter(x => !this._isSame(t, x))
|
||||
}
|
||||
console.log("Setting new history:", store, [t, ...oldList])
|
||||
store.set([t, ...oldList].slice(0, this._maxHistory))
|
||||
}
|
||||
|
||||
|
@ -198,7 +195,6 @@ export default class UserRelatedState {
|
|||
this.showTags = this.osmConnection.GetPreference("show_tags")
|
||||
this.showCrosshair = this.osmConnection.GetPreference("show_crosshair")
|
||||
this.fixateNorth = this.osmConnection.GetPreference("fixate-north")
|
||||
console.log("Fixate north is:", this.fixateNorth)
|
||||
this.morePrivacy = this.osmConnection.GetPreference("more_privacy", "no")
|
||||
|
||||
this.a11y = this.osmConnection.GetPreference("a11y")
|
||||
|
|
|
@ -641,4 +641,18 @@ export default class LayerConfig extends WithContextLoader {
|
|||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -67,7 +67,7 @@ import { LayerConfigJson } from "./ThemeConfig/Json/LayerConfigJson"
|
|||
import Hash from "../Logic/Web/Hash"
|
||||
import { GeoOperations } from "../Logic/GeoOperations"
|
||||
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"
|
||||
|
||||
/**
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import Loading from "./Loading.svelte"
|
||||
import { onDestroy } from "svelte"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import { GeocodingUtils } from "../../Logic/Geocoding/GeocodingProvider"
|
||||
import { GeocodingUtils } from "../../Logic/Search/GeocodingProvider"
|
||||
import ThemeViewState from "../../Models/ThemeViewState"
|
||||
|
||||
export let state: SpecialVisualizationState
|
||||
|
|
|
@ -66,7 +66,7 @@ export default class MoreScreen {
|
|||
return Infinity
|
||||
}
|
||||
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[]
|
||||
if (Array.isArray(keywords)) {
|
||||
terms = keywords
|
||||
|
@ -90,9 +90,12 @@ export default class MoreScreen {
|
|||
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> = {}
|
||||
for (const id in this.officialThemes.layers) {
|
||||
if(layerWhitelist !== undefined && !layerWhitelist.has(id)){
|
||||
continue
|
||||
}
|
||||
const keywords = this.officialThemes.layers[id]
|
||||
const distance = this.scoreKeywords(query, keywords)
|
||||
result[id] = distance
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* Shows the current address when shaken
|
||||
**/
|
||||
import Motion from "../../Sensors/Motion"
|
||||
import { NominatimGeocoding } from "../../Logic/Geocoding/NominatimGeocoding"
|
||||
import { NominatimGeocoding } from "../../Logic/Search/NominatimGeocoding"
|
||||
import Hotkeys from "../Base/Hotkeys"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Locale from "../i18n/Locale"
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<script lang="ts">
|
||||
import type { ActiveFilter } from "../../Logic/State/LayerState"
|
||||
import FilterOption from "./FilterOption.svelte"
|
||||
import { XMarkIcon } from "@babeard/svelte-heroicons/mini"
|
||||
import Loading from "../Base/Loading.svelte"
|
||||
import FilterToggle from "./FilterToggle.svelte"
|
||||
|
||||
|
||||
export let activeFilter: ActiveFilter
|
||||
|
@ -21,10 +21,7 @@
|
|||
{#if loading}
|
||||
<Loading />
|
||||
{:else }
|
||||
<div class="badge button-unstyled w-fit">
|
||||
<FilterToggle on:click={() => clear()}>
|
||||
<FilterOption option={$option} />
|
||||
<button on:click={() => clear()}>
|
||||
<XMarkIcon class="w-5 h-5 pl-1" color="gray" />
|
||||
</button>
|
||||
</div>
|
||||
</FilterToggle>
|
||||
{/if}
|
||||
|
|
|
@ -3,13 +3,33 @@
|
|||
import type { ActiveFilter } from "../../Logic/State/LayerState"
|
||||
import Loading from "../Base/Loading.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 state: SpecialVisualizationState
|
||||
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() {
|
||||
loading = true
|
||||
requestIdleCallback(() => {
|
||||
enableAllLayers()
|
||||
|
||||
|
||||
for (const activeFilter of activeFilters) {
|
||||
activeFilter.control.setData(undefined)
|
||||
|
@ -18,14 +38,43 @@
|
|||
})
|
||||
}
|
||||
</script>
|
||||
{#if activeFilters.length > 0}
|
||||
|
||||
{#if activeFilters.length > 0 || $activeLayers.length === 1 || $nonactiveLayers.length > 0}
|
||||
<SidebarUnit>
|
||||
<div class="flex justify-between">
|
||||
<h3>Active filters</h3>
|
||||
|
||||
<button class="as-link subtle self-end" on:click={() => clear()} style="margin-right: 0.75rem">
|
||||
Clear filters
|
||||
</button>
|
||||
</div>
|
||||
{#if loading}
|
||||
<Loading />
|
||||
{:else}
|
||||
<div class="flex flex-wrap gap-x-1 gap-y-2">
|
||||
|
||||
<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>
|
||||
|
@ -34,9 +83,7 @@
|
|||
{/each}
|
||||
</div>
|
||||
|
||||
<button class="as-link subtle self-end" on:click={() => clear()} style="margin-right: 0.75rem">
|
||||
Clear filters
|
||||
</button>
|
||||
|
||||
{/if}
|
||||
</SidebarUnit>
|
||||
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
<script lang="ts">
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
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 Icon from "../Map/Icon.svelte"
|
||||
import Marker from "../Map/Marker.svelte"
|
||||
import ToSvelte from "../Base/ToSvelte.svelte"
|
||||
|
||||
export let entry: FilterPayload
|
||||
let { option } = entry
|
||||
export let entry: FilterResult | LayerResult
|
||||
export let state: SpecialVisualizationState
|
||||
let dispatch = createEventDispatcher<{ select }>()
|
||||
|
||||
|
@ -18,10 +19,18 @@
|
|||
</script>
|
||||
<button on:click={() => apply()}>
|
||||
<div class="flex flex-col items-start">
|
||||
|
||||
<div class="flex items-center gap-x-1">
|
||||
<Icon icon={option.icon ?? option.emoji} clss="w-4 h-4" emojiHeight="14px" />
|
||||
<Tr cls="whitespace-nowrap" t={option.question} />
|
||||
{#if entry.category === "layer"}
|
||||
<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>
|
||||
</button>
|
||||
|
|
9
src/UI/Search/FilterToggle.svelte
Normal file
9
src/UI/Search/FilterToggle.svelte
Normal 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>
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { GeocodingUtils } from "../../Logic/Geocoding/GeocodingProvider"
|
||||
import type { GeocodeResult } from "../../Logic/Geocoding/GeocodingProvider"
|
||||
import { GeocodingUtils } from "../../Logic/Search/GeocodingProvider"
|
||||
import type { GeocodeResult } from "../../Logic/Search/GeocodingProvider"
|
||||
import { GeoOperations } from "../../Logic/GeoOperations"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import type { SearchResult } from "../../Logic/Geocoding/GeocodingProvider"
|
||||
import type { SearchResult } from "../../Logic/Search/GeocodingProvider"
|
||||
|
||||
import ThemeResult from "../Search/ThemeResult.svelte"
|
||||
import FilterResult from "./FilterResult.svelte"
|
||||
|
|
|
@ -1,51 +1,79 @@
|
|||
<script lang="ts">
|
||||
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 } from "../../Logic/Geocoding/GeocodingProvider"
|
||||
import type { FilterResult, GeocodeResult, LayerResult } from "../../Logic/Search/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 {default as FilterResultSvelte} from "./FilterResult.svelte"
|
||||
import ThemeResult from "./ThemeResult.svelte"
|
||||
import SidebarUnit from "../Base/SidebarUnit.svelte"
|
||||
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 { CogIcon } from "@rgossiaux/svelte-heroicons/solid"
|
||||
|
||||
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 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 searchTerm = state.searchState.searchTerm
|
||||
let results = state.searchState.suggestions
|
||||
let isSearching = state.searchState.suggestionsSearchRunning
|
||||
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
|
||||
|
||||
</script>
|
||||
<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>
|
||||
|
||||
<h3>Pick a filter below</h3>
|
||||
|
||||
<div class="flex flex-wrap">
|
||||
{#each $filterResults as filterResult (filterResult)}
|
||||
<FilterResult {state} entry={filterResult} />
|
||||
{#each $filterResultsClipped as filterResult (filterResult)}
|
||||
<FilterResultSvelte {state} entry={filterResult} />
|
||||
{/each}
|
||||
</div>
|
||||
{#if $filterResults.length + $layerResults.length > $filterResultsClipped.length}
|
||||
<div class="flex justify-center">
|
||||
... and {$filterResults.length + $layerResults.length - $filterResultsClipped.length} more ...
|
||||
</div>
|
||||
{/if}
|
||||
</SidebarUnit>
|
||||
{/if}
|
||||
|
||||
|
@ -68,7 +96,7 @@
|
|||
|
||||
{:else if !$isSearching}
|
||||
<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>
|
||||
{/if}
|
||||
</SidebarUnit>
|
||||
|
@ -88,8 +116,16 @@
|
|||
</SidebarUnit>
|
||||
{/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>
|
||||
<div class="flex justify-between">
|
||||
|
||||
|
|
|
@ -326,7 +326,7 @@
|
|||
|
||||
<div
|
||||
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 -->
|
||||
<div class="flex items-center">
|
||||
|
||||
|
@ -361,7 +361,7 @@
|
|||
{/if}
|
||||
|
||||
<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">
|
||||
<Searchbar value={state.searchState.searchTerm} isFocused={state.searchState.searchIsFocused} />
|
||||
</div>
|
||||
|
|
|
@ -391,8 +391,8 @@ h2.group {
|
|||
align-items: center;
|
||||
white-space: nowrap;
|
||||
border-radius: 999rem;
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
padding-left: 0.25rem;
|
||||
padding-right: 0.25rem;
|
||||
border: 1px solid var(--subtle-detail-color-light-contrast);
|
||||
background-color: var(--low-interaction-background);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue