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
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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": [
|
||||||
|
|
|
@ -40,7 +40,7 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"minzoom": 12,
|
"minzoom": 10,
|
||||||
"title": {
|
"title": {
|
||||||
"render": {
|
"render": {
|
||||||
"en": "Shop",
|
"en": "Shop",
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -56,7 +56,7 @@
|
||||||
"trolley_bay"
|
"trolley_bay"
|
||||||
],
|
],
|
||||||
"overrideAll": {
|
"overrideAll": {
|
||||||
"minzoom": 14,
|
"minzoom": 12,
|
||||||
"syncSelection": "theme-only"
|
"syncSelection": "theme-only"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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 {
|
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 { 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
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">
|
<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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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">
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue