forked from MapComplete/MapComplete
WIP
This commit is contained in:
parent
3ab1a0a3f2
commit
617b4854fa
48 changed files with 662 additions and 491 deletions
|
@ -569,7 +569,7 @@
|
|||
},
|
||||
{
|
||||
"if": "dog=no",
|
||||
"icon": "./assets/layers/questions/no_dogs.svg",
|
||||
"icon": "\uD83D\uDC15 ⃠",
|
||||
"then": {
|
||||
"en": "Dogs are <b>not</b> allowed",
|
||||
"nl": "honden zijn <b>niet</b> toegelaten",
|
||||
|
|
|
@ -257,6 +257,7 @@
|
|||
{
|
||||
"builtin": "id_presets.shop_types",
|
||||
"override": {
|
||||
"id": "shop_types",
|
||||
"labels": [
|
||||
"description"
|
||||
],
|
||||
|
@ -1174,51 +1175,7 @@
|
|||
],
|
||||
"filter": [
|
||||
"open_now",
|
||||
{
|
||||
"id": "shop-type",
|
||||
"options": [
|
||||
{
|
||||
"fields": [
|
||||
{
|
||||
"name": "search",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"osmTags": "shop~i~.*{search}.*",
|
||||
"question": {
|
||||
"en": "Only show shops selling {search}",
|
||||
"de": "Nur Geschäfte, die {search} verkaufen",
|
||||
"nl": "Toon enkel winkels die {search} verkopen",
|
||||
"es": "Solo mostrar tiendas que vendan {search}",
|
||||
"fr": "N'afficher que les magasins vendant {search}",
|
||||
"ca": "Sols mostrar botigues que venen {search}",
|
||||
"cs": "Zobrazit pouze obchody prodávající {search}"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "shop-name",
|
||||
"options": [
|
||||
{
|
||||
"fields": [
|
||||
{
|
||||
"name": "search",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"osmTags": "name~i~.*{search}.*",
|
||||
"question": {
|
||||
"en": "Only show shops with name {search}",
|
||||
"de": "Nur Geschäfte mit dem Namen {search} anzeigen",
|
||||
"nl": "Toon enkel winkels met naam {search}",
|
||||
"es": "Solo mostrar tiendas con nombre {search}",
|
||||
"fr": "N'afficher que les magasins portant le nom {search}",
|
||||
"cs": "Zobrazit pouze obchody s názvem {search}"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"shop_types",
|
||||
{
|
||||
"id": "second_hand",
|
||||
"options": [
|
||||
|
|
|
@ -260,6 +260,7 @@
|
|||
"examples": "Examples",
|
||||
"fewChangesBefore": "Please, answer a few questions of existing features before adding a new feature.",
|
||||
"filterPanel": {
|
||||
"allTypes": "All types",
|
||||
"disableAll": "Disable all",
|
||||
"enableAll": "Enable all"
|
||||
},
|
||||
|
|
|
@ -6700,15 +6700,6 @@
|
|||
}
|
||||
},
|
||||
"description": "Una botiga",
|
||||
"filter": {
|
||||
"1": {
|
||||
"options": {
|
||||
"0": {
|
||||
"question": "Sols mostrar botigues que venen {search}"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"name": "Botiga",
|
||||
"presets": {
|
||||
"0": {
|
||||
|
|
|
@ -6979,21 +6979,7 @@
|
|||
},
|
||||
"description": "Obchod",
|
||||
"filter": {
|
||||
"1": {
|
||||
"options": {
|
||||
"0": {
|
||||
"question": "Zobrazit pouze obchody prodávající {search}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"2": {
|
||||
"options": {
|
||||
"0": {
|
||||
"question": "Zobrazit pouze obchody s názvem {search}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"3": {
|
||||
"options": {
|
||||
"0": {
|
||||
"question": "Zobrazit pouze obchody prodávající použité zboží"
|
||||
|
|
|
@ -1419,6 +1419,13 @@
|
|||
"arialabel": "Åbn på openstreetmap.org"
|
||||
}
|
||||
}
|
||||
},
|
||||
"2": {
|
||||
"then": {
|
||||
"special": {
|
||||
"arialabel": "Åbn på openstreetmap.org"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"render": {
|
||||
|
|
|
@ -5673,6 +5673,13 @@
|
|||
"arialabel": "Auf openstreetmap.org öffnen"
|
||||
}
|
||||
}
|
||||
},
|
||||
"2": {
|
||||
"then": {
|
||||
"special": {
|
||||
"arialabel": "Auf openstreetmap.org öffnen"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"render": {
|
||||
|
@ -8855,21 +8862,7 @@
|
|||
},
|
||||
"description": "Ein Geschäft",
|
||||
"filter": {
|
||||
"1": {
|
||||
"options": {
|
||||
"0": {
|
||||
"question": "Nur Geschäfte, die {search} verkaufen"
|
||||
}
|
||||
}
|
||||
},
|
||||
"2": {
|
||||
"options": {
|
||||
"0": {
|
||||
"question": "Nur Geschäfte mit dem Namen {search} anzeigen"
|
||||
}
|
||||
}
|
||||
},
|
||||
"3": {
|
||||
"options": {
|
||||
"0": {
|
||||
"question": "Nur Second-Hand-Geschäfte anzeigen"
|
||||
|
|
|
@ -5673,6 +5673,13 @@
|
|||
"arialabel": "Open on openstreetmap.org"
|
||||
}
|
||||
}
|
||||
},
|
||||
"2": {
|
||||
"then": {
|
||||
"special": {
|
||||
"arialabel": "Open on openstreetmap.org"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"render": {
|
||||
|
@ -8809,6 +8816,14 @@
|
|||
"render": "School <i>{name}</i>"
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"description": "Priviliged layer showing the search results",
|
||||
"tagRenderings": {
|
||||
"intro": {
|
||||
"render": "Search result"
|
||||
}
|
||||
}
|
||||
},
|
||||
"selected_element": {
|
||||
"description": "Highlights the currently selected element. Override this layer to have different colors"
|
||||
},
|
||||
|
@ -8855,21 +8870,7 @@
|
|||
},
|
||||
"description": "A shop",
|
||||
"filter": {
|
||||
"1": {
|
||||
"options": {
|
||||
"0": {
|
||||
"question": "Only show shops selling {search}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"2": {
|
||||
"options": {
|
||||
"0": {
|
||||
"question": "Only show shops with name {search}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"3": {
|
||||
"options": {
|
||||
"0": {
|
||||
"question": "Only show shops selling second-hand items"
|
||||
|
|
|
@ -3796,22 +3796,6 @@
|
|||
}
|
||||
},
|
||||
"description": "Una tienda",
|
||||
"filter": {
|
||||
"1": {
|
||||
"options": {
|
||||
"0": {
|
||||
"question": "Solo mostrar tiendas que vendan {search}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"2": {
|
||||
"options": {
|
||||
"0": {
|
||||
"question": "Solo mostrar tiendas con nombre {search}"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"name": "Tienda",
|
||||
"presets": {
|
||||
"0": {
|
||||
|
|
|
@ -5618,22 +5618,6 @@
|
|||
}
|
||||
},
|
||||
"description": "Un magasin",
|
||||
"filter": {
|
||||
"1": {
|
||||
"options": {
|
||||
"0": {
|
||||
"question": "N'afficher que les magasins vendant {search}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"2": {
|
||||
"options": {
|
||||
"0": {
|
||||
"question": "N'afficher que les magasins portant le nom {search}"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"name": "Magasin",
|
||||
"presets": {
|
||||
"0": {
|
||||
|
|
|
@ -4664,6 +4664,13 @@
|
|||
"arialabel": "Bekijk op openstreetmap.org"
|
||||
}
|
||||
}
|
||||
},
|
||||
"2": {
|
||||
"then": {
|
||||
"special": {
|
||||
"arialabel": "Bekijk op openstreetmap.org"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"render": {
|
||||
|
@ -7120,22 +7127,6 @@
|
|||
}
|
||||
},
|
||||
"description": "Een winkel",
|
||||
"filter": {
|
||||
"1": {
|
||||
"options": {
|
||||
"0": {
|
||||
"question": "Toon enkel winkels die {search} verkopen"
|
||||
}
|
||||
}
|
||||
},
|
||||
"2": {
|
||||
"options": {
|
||||
"0": {
|
||||
"question": "Toon enkel winkels met naam {search}"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"name": "Winkel",
|
||||
"presets": {
|
||||
"0": {
|
||||
|
|
|
@ -1948,6 +1948,13 @@
|
|||
"arialabel": "Otwórz na openstreetmap.org"
|
||||
}
|
||||
}
|
||||
},
|
||||
"2": {
|
||||
"then": {
|
||||
"special": {
|
||||
"arialabel": "Otwórz na openstreetmap.org"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"render": {
|
||||
|
|
|
@ -953,6 +953,11 @@ video {
|
|||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.my-8 {
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.-my-1\.5 {
|
||||
margin-top: -0.375rem;
|
||||
margin-bottom: -0.375rem;
|
||||
|
@ -978,11 +983,6 @@ video {
|
|||
margin-right: -0.25rem;
|
||||
}
|
||||
|
||||
.my-8 {
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.mx-12 {
|
||||
margin-left: 3rem;
|
||||
margin-right: 3rem;
|
||||
|
@ -993,10 +993,6 @@ video {
|
|||
margin-right: 4rem;
|
||||
}
|
||||
|
||||
.mb-4 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.mt-4 {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
@ -1029,6 +1025,10 @@ video {
|
|||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
.mb-4 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.ml-1 {
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
@ -1440,10 +1440,6 @@ video {
|
|||
max-height: 3rem;
|
||||
}
|
||||
|
||||
.max-h-96 {
|
||||
max-height: 24rem;
|
||||
}
|
||||
|
||||
.max-h-24 {
|
||||
max-height: 6rem;
|
||||
}
|
||||
|
@ -1452,6 +1448,10 @@ video {
|
|||
max-height: 16rem;
|
||||
}
|
||||
|
||||
.max-h-96 {
|
||||
max-height: 24rem;
|
||||
}
|
||||
|
||||
.max-h-full {
|
||||
max-height: 100%;
|
||||
}
|
||||
|
@ -4623,12 +4623,12 @@ button.as-link {
|
|||
padding: 0;
|
||||
}
|
||||
|
||||
button.unstyled {
|
||||
button.unstyled, .button-unstyled button {
|
||||
background-color: unset;
|
||||
display: inline-flex;
|
||||
justify-content: start;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
box-shadow: none !important;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
@ -7234,14 +7234,14 @@ svg.apply-fill path {
|
|||
width: 50%;
|
||||
}
|
||||
|
||||
.sm\:w-96 {
|
||||
width: 24rem;
|
||||
}
|
||||
|
||||
.sm\:w-11 {
|
||||
width: 2.75rem;
|
||||
}
|
||||
|
||||
.sm\:w-96 {
|
||||
width: 24rem;
|
||||
}
|
||||
|
||||
.sm\:w-auto {
|
||||
width: auto;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import GeocodingProvider, { GeoCodeResult, GeocodingOptions } from "./GeocodingProvider"
|
||||
import GeocodingProvider, { SearchResult, GeocodingOptions } from "./GeocodingProvider"
|
||||
import { Utils } from "../../Utils"
|
||||
import { Store, Stores } from "../UIEventSource"
|
||||
|
||||
|
@ -17,12 +17,17 @@ export default class CombinedSearcher implements GeocodingProvider {
|
|||
* @param geocoded
|
||||
* @private
|
||||
*/
|
||||
private merge(geocoded: GeoCodeResult[][]): GeoCodeResult[] {
|
||||
const results: GeoCodeResult[] = []
|
||||
private merge(geocoded: SearchResult[][]): SearchResult[] {
|
||||
const results: SearchResult[] = []
|
||||
const seenIds = new Set<string>()
|
||||
for (const geocodedElement of geocoded) {
|
||||
for (const entry of geocodedElement) {
|
||||
const id = entry.osm_type + entry.osm_id
|
||||
|
||||
|
||||
if (entry.osm_id === undefined) {
|
||||
throw "Invalid search result: a search result always must have an osm_id to be able to merge results from different sources"
|
||||
}
|
||||
const id = (entry["osm_type"] ?? "") + entry.osm_id
|
||||
if (seenIds.has(id)) {
|
||||
continue
|
||||
}
|
||||
|
@ -33,13 +38,14 @@ export default class CombinedSearcher implements GeocodingProvider {
|
|||
return results
|
||||
}
|
||||
|
||||
async search(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> {
|
||||
async search(query: string, options?: GeocodingOptions): Promise<SearchResult[]> {
|
||||
const results = (await Promise.all(this._providers.map(pr => pr.search(query, options))))
|
||||
return results.flatMap(x => x)
|
||||
return this.merge(results)
|
||||
}
|
||||
|
||||
suggest(query: string, options?: GeocodingOptions): Store<GeoCodeResult[]> {
|
||||
return Stores.concat(this._providersWithSuggest.map(pr => pr.suggest(query, options)))
|
||||
suggest(query: string, options?: GeocodingOptions): Store<SearchResult[]> {
|
||||
return Stores.concat(
|
||||
this._providersWithSuggest.map(pr => pr.suggest(query, options)))
|
||||
.map(gcrss => this.merge(gcrss))
|
||||
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import GeocodingProvider, { GeoCodeResult } from "./GeocodingProvider"
|
||||
import GeocodingProvider, { SearchResult } from "./GeocodingProvider"
|
||||
import { Utils } from "../../Utils"
|
||||
import { ImmutableStore, Store } from "../UIEventSource"
|
||||
|
||||
|
@ -52,9 +52,9 @@ export default class CoordinateSearch implements GeocodingProvider {
|
|||
* results.length // => 1
|
||||
* results[0] // => {lat: -57.5802905, lon: -12.7202538, display_name: "lon: -12.7202538, lat: -57.5802905", "category": "coordinate", "source": "coordinate:latlon"}
|
||||
*/
|
||||
private directSearch(query: string): GeoCodeResult[] {
|
||||
private directSearch(query: string): SearchResult[] {
|
||||
|
||||
const matches = Utils.NoNull(CoordinateSearch.latLonRegexes.map(r => query.match(r))).map(m => <GeoCodeResult>{
|
||||
const matches = Utils.NoNull(CoordinateSearch.latLonRegexes.map(r => query.match(r))).map(m => <SearchResult>{
|
||||
lat: Number(m[1]),
|
||||
lon: Number(m[2]),
|
||||
display_name: "lon: " + m[2] + ", lat: " + m[1],
|
||||
|
@ -64,7 +64,7 @@ export default class CoordinateSearch implements GeocodingProvider {
|
|||
|
||||
|
||||
const matchesLonLat = Utils.NoNull(CoordinateSearch.lonLatRegexes.map(r => query.match(r)))
|
||||
.map(m => <GeoCodeResult>{
|
||||
.map(m => <SearchResult>{
|
||||
lat: Number(m[2]),
|
||||
lon: Number(m[1]),
|
||||
display_name: "lon: " + m[1] + ", lat: " + m[2],
|
||||
|
@ -74,11 +74,11 @@ export default class CoordinateSearch implements GeocodingProvider {
|
|||
return matches.concat(matchesLonLat)
|
||||
}
|
||||
|
||||
suggest(query: string): Store<GeoCodeResult[]> {
|
||||
suggest(query: string): Store<SearchResult[]> {
|
||||
return new ImmutableStore(this.directSearch(query))
|
||||
}
|
||||
|
||||
async search (query: string): Promise<GeoCodeResult[]> {
|
||||
async search (query: string): Promise<SearchResult[]> {
|
||||
return this.directSearch(query)
|
||||
}
|
||||
|
||||
|
|
60
src/Logic/Geocoding/FilterSearch.ts
Normal file
60
src/Logic/Geocoding/FilterSearch.ts
Normal file
|
@ -0,0 +1,60 @@
|
|||
import { ImmutableStore, Store } from "../UIEventSource"
|
||||
import GeocodingProvider, { GeocodingOptions, SearchResult } from "./GeocodingProvider"
|
||||
import { SpecialVisualizationState } from "../../UI/SpecialVisualization"
|
||||
import { Utils } from "../../Utils"
|
||||
import Locale from "../../UI/i18n/Locale"
|
||||
|
||||
export default class FilterSearch implements GeocodingProvider {
|
||||
private readonly _state: SpecialVisualizationState
|
||||
|
||||
constructor(state: SpecialVisualizationState) {
|
||||
this._state = state
|
||||
|
||||
}
|
||||
|
||||
async search(query: string, options?: GeocodingOptions): Promise<SearchResult[]> {
|
||||
return []
|
||||
}
|
||||
|
||||
private searchDirectly(query: string): SearchResult[] {
|
||||
const possibleFilters: SearchResult[] = []
|
||||
for (const layer of this._state.layout.layers) {
|
||||
if (!Array.isArray(layer.filters)) {
|
||||
continue
|
||||
}
|
||||
for (const filter of layer.filters ?? []) {
|
||||
for (let i = 0; i < filter.options.length; i++) {
|
||||
const option = filter.options[i]
|
||||
if (option === undefined) {
|
||||
console.log("No options for", filter)
|
||||
continue
|
||||
}
|
||||
const terms = [option.question.txt,
|
||||
...(option.searchTerms?.[Locale.language.data] ?? option.searchTerms?.["en"] ?? [])].flatMap(term => term.split(" "))
|
||||
const levehnsteinD = Math.min(...
|
||||
terms.map(entry => {
|
||||
const simplified = Utils.simplifyStringForSearch(entry)
|
||||
return Utils.levenshteinDistance(query, simplified.slice(0, query.length))
|
||||
}))
|
||||
if (levehnsteinD / query.length > 0.25) {
|
||||
continue
|
||||
}
|
||||
possibleFilters.push({
|
||||
payload: { option, layer, filter, index: i },
|
||||
category: "filter",
|
||||
osm_id: layer.id + "/" + filter.id + "/" + option.osmTags?.asHumanString() ?? "none",
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return possibleFilters
|
||||
}
|
||||
|
||||
suggest(query: string, options?: GeocodingOptions): Store<SearchResult[]> {
|
||||
query = Utils.simplifyStringForSearch(query)
|
||||
|
||||
return new ImmutableStore(this.searchDirectly(query))
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import { GeoCodeResult } from "./GeocodingProvider"
|
||||
import { SearchResult } from "./GeocodingProvider"
|
||||
import { Store } from "../UIEventSource"
|
||||
import { FeatureSource } from "../FeatureSource/FeatureSource"
|
||||
import { Feature, Geometry } from "geojson"
|
||||
|
@ -6,7 +6,7 @@ import { Feature, Geometry } from "geojson"
|
|||
export default class GeocodingFeatureSource implements FeatureSource {
|
||||
public features: Store<Feature<Geometry, Record<string, string>>[]>
|
||||
|
||||
constructor(provider: Store<GeoCodeResult[]>) {
|
||||
constructor(provider: Store<SearchResult[]>) {
|
||||
this.features = provider.map(geocoded => {
|
||||
if(geocoded === undefined){
|
||||
return []
|
||||
|
|
|
@ -5,9 +5,22 @@ import { Store } from "../UIEventSource"
|
|||
import * as search from "../../assets/generated/layers/search.json"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"
|
||||
export type GeocodingCategory = "coordinate" | "city" | "house" | "street" | "locality" | "country" | "train_station" | "county" | "airport" | "shop"
|
||||
import FilterConfig, { FilterConfigOption } from "../../Models/ThemeConfig/FilterConfig"
|
||||
import { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig"
|
||||
|
||||
export type GeoCodeResult = {
|
||||
export type GeocodingCategory =
|
||||
"coordinate"
|
||||
| "city"
|
||||
| "house"
|
||||
| "street"
|
||||
| "locality"
|
||||
| "country"
|
||||
| "train_station"
|
||||
| "county"
|
||||
| "airport"
|
||||
| "shop"
|
||||
|
||||
export type GeocodeResult = {
|
||||
/**
|
||||
* The name of the feature being displayed
|
||||
*/
|
||||
|
@ -25,11 +38,16 @@ export type GeoCodeResult = {
|
|||
*/
|
||||
boundingbox?: number[]
|
||||
osm_type?: "node" | "way" | "relation"
|
||||
osm_id?: string,
|
||||
osm_id: string,
|
||||
category?: GeocodingCategory,
|
||||
payload?: object,
|
||||
source?: string
|
||||
}
|
||||
export type FilterPayload = { option: FilterConfigOption, filter: FilterConfig, layer: LayerConfig, index: number }
|
||||
export type SearchResult =
|
||||
| { category: "filter", osm_id: string, payload: FilterPayload }
|
||||
| { category: "theme", osm_id: string, payload: MinimalLayoutInformation }
|
||||
| GeocodeResult
|
||||
|
||||
export interface GeocodingOptions {
|
||||
bbox?: BBox,
|
||||
|
@ -40,16 +58,16 @@ export interface GeocodingOptions {
|
|||
export default interface GeocodingProvider {
|
||||
|
||||
|
||||
search(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]>
|
||||
search(query: string, options?: GeocodingOptions): Promise<SearchResult[]>
|
||||
|
||||
/**
|
||||
* @param query
|
||||
* @param options
|
||||
*/
|
||||
suggest?(query: string, options?: GeocodingOptions): Store<GeoCodeResult[]>
|
||||
suggest?(query: string, options?: GeocodingOptions): Store<SearchResult[]>
|
||||
}
|
||||
|
||||
export type ReverseGeocodingResult = Feature<Geometry,{
|
||||
export type ReverseGeocodingResult = Feature<Geometry, {
|
||||
osm_id: number,
|
||||
osm_type: "node" | "way" | "relation",
|
||||
country: string,
|
||||
|
@ -57,25 +75,26 @@ export type ReverseGeocodingResult = Feature<Geometry,{
|
|||
countrycode: string,
|
||||
type: GeocodingCategory,
|
||||
street: string
|
||||
} >
|
||||
}>
|
||||
|
||||
export interface ReverseGeocodingProvider {
|
||||
reverseSearch(
|
||||
coordinate: { lon: number; lat: number },
|
||||
zoom: number,
|
||||
language?: string
|
||||
): Promise<ReverseGeocodingResult[]> ;
|
||||
language?: string,
|
||||
): Promise<ReverseGeocodingResult[]>;
|
||||
}
|
||||
|
||||
export class GeocodingUtils {
|
||||
|
||||
public static searchLayer = GeocodingUtils.initSearchLayer()
|
||||
private static initSearchLayer():LayerConfig{
|
||||
if(search["id"] === undefined){
|
||||
public static searchLayer = GeocodingUtils.initSearchLayer()
|
||||
|
||||
private static initSearchLayer(): LayerConfig {
|
||||
if (search["id"] === undefined) {
|
||||
// We are resetting the layeroverview; trying to parse is useless
|
||||
return undefined
|
||||
}
|
||||
return new LayerConfig(<LayerConfigJson> search, "search")
|
||||
return new LayerConfig(<LayerConfigJson>search, "search")
|
||||
}
|
||||
|
||||
public static categoryToZoomLevel: Record<GeocodingCategory, number> = {
|
||||
|
@ -88,7 +107,7 @@ export class GeocodingUtils {
|
|||
street: 15,
|
||||
train_station: 14,
|
||||
airport: 13,
|
||||
shop:16
|
||||
shop: 16,
|
||||
|
||||
}
|
||||
|
||||
|
@ -103,7 +122,7 @@ export class GeocodingUtils {
|
|||
train_station: "train",
|
||||
county: "building_office_2",
|
||||
airport: "airport",
|
||||
shop: "building_storefront"
|
||||
shop: "building_storefront",
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import GeocodingProvider, { GeoCodeResult, GeocodingOptions } from "./GeocodingProvider"
|
||||
import GeocodingProvider, { SearchResult, GeocodingOptions } from "./GeocodingProvider"
|
||||
import ThemeViewState from "../../Models/ThemeViewState"
|
||||
import { Utils } from "../../Utils"
|
||||
import { Feature } from "geojson"
|
||||
|
@ -27,7 +27,7 @@ export default class LocalElementSearch implements GeocodingProvider {
|
|||
|
||||
}
|
||||
|
||||
async search(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> {
|
||||
async search(query: string, options?: GeocodingOptions): Promise<SearchResult[]> {
|
||||
return this.searchEntries(query, options, false).data
|
||||
}
|
||||
|
||||
|
@ -41,7 +41,6 @@ export default class LocalElementSearch implements GeocodingProvider {
|
|||
props["addr:street"] + props["addr:number"] : undefined])
|
||||
|
||||
let levehnsteinD: number
|
||||
console.log("Comparing nearby:", candidateId, props.id)
|
||||
if (candidateId === props.id) {
|
||||
levehnsteinD = 0
|
||||
} else {
|
||||
|
@ -54,7 +53,7 @@ export default class LocalElementSearch implements GeocodingProvider {
|
|||
}))
|
||||
}
|
||||
const center = GeoOperations.centerpointCoordinates(feature)
|
||||
if (levehnsteinD <= 2) {
|
||||
if ((levehnsteinD / query.length) <= 0.3) {
|
||||
|
||||
let description = ""
|
||||
if (feature.properties["addr:street"]) {
|
||||
|
@ -76,7 +75,7 @@ export default class LocalElementSearch implements GeocodingProvider {
|
|||
return results
|
||||
}
|
||||
|
||||
searchEntries(query: string, options?: GeocodingOptions, matchStart?: boolean): Store<GeoCodeResult[]> {
|
||||
searchEntries(query: string, options?: GeocodingOptions, matchStart?: boolean): Store<SearchResult[]> {
|
||||
if (query.length < 3) {
|
||||
return new ImmutableStore([])
|
||||
}
|
||||
|
@ -101,7 +100,7 @@ export default class LocalElementSearch implements GeocodingProvider {
|
|||
}
|
||||
return results.map(entry => {
|
||||
const [osm_type, osm_id] = entry.feature.properties.id.split("/")
|
||||
return <GeoCodeResult>{
|
||||
return <SearchResult>{
|
||||
lon: entry.center[0],
|
||||
lat: entry.center[1],
|
||||
osm_type,
|
||||
|
@ -118,7 +117,7 @@ export default class LocalElementSearch implements GeocodingProvider {
|
|||
|
||||
}
|
||||
|
||||
suggest(query: string, options?: GeocodingOptions): Store<GeoCodeResult[]> {
|
||||
suggest(query: string, options?: GeocodingOptions): Store<SearchResult[]> {
|
||||
return this.searchEntries(query, options, true)
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ import { BBox } from "../BBox"
|
|||
import Constants from "../../Models/Constants"
|
||||
import { FeatureCollection } from "geojson"
|
||||
import Locale from "../../UI/i18n/Locale"
|
||||
import GeocodingProvider, { GeoCodeResult } from "./GeocodingProvider"
|
||||
import GeocodingProvider, { SearchResult } from "./GeocodingProvider"
|
||||
|
||||
export class NominatimGeocoding implements GeocodingProvider {
|
||||
|
||||
|
@ -13,7 +13,7 @@ export class NominatimGeocoding implements GeocodingProvider {
|
|||
this._host = host
|
||||
}
|
||||
|
||||
public search(query: string, options?: { bbox?: BBox; limit?: number }): Promise<GeoCodeResult[]> {
|
||||
public search(query: string, options?: { bbox?: BBox; limit?: number }): Promise<SearchResult[]> {
|
||||
const b = options?.bbox ?? BBox.global
|
||||
const url = `${
|
||||
this._host
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Store, UIEventSource } from "../UIEventSource"
|
||||
import GeocodingProvider, { GeoCodeResult, GeocodingOptions } from "./GeocodingProvider"
|
||||
import GeocodingProvider, { SearchResult, GeocodingOptions } from "./GeocodingProvider"
|
||||
import { OsmId } from "../../Models/OsmFeature"
|
||||
import { SpecialVisualizationState } from "../../UI/SpecialVisualization"
|
||||
|
||||
|
@ -30,7 +30,7 @@ export default class OpenStreetMapIdSearch implements GeocodingProvider {
|
|||
return undefined
|
||||
}
|
||||
|
||||
async search(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> {
|
||||
async search(query: string, options?: GeocodingOptions): Promise<SearchResult[]> {
|
||||
const id = OpenStreetMapIdSearch.extractId(query)
|
||||
if (!id) {
|
||||
return []
|
||||
|
@ -59,7 +59,7 @@ export default class OpenStreetMapIdSearch implements GeocodingProvider {
|
|||
}]
|
||||
}
|
||||
|
||||
suggest?(query: string, options?: GeocodingOptions): Store<GeoCodeResult[]> {
|
||||
suggest?(query: string, options?: GeocodingOptions): Store<SearchResult[]> {
|
||||
return UIEventSource.FromPromise(this.search(query, options))
|
||||
}
|
||||
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import Constants from "../../Models/Constants"
|
||||
import GeocodingProvider, {
|
||||
GeoCodeResult,
|
||||
GeocodeResult,
|
||||
GeocodingCategory,
|
||||
GeocodingOptions,
|
||||
ReverseGeocodingProvider,
|
||||
ReverseGeocodingResult
|
||||
ReverseGeocodingResult,
|
||||
} from "./GeocodingProvider"
|
||||
import { Utils } from "../../Utils"
|
||||
import { Feature, FeatureCollection } from "geojson"
|
||||
|
@ -18,7 +18,7 @@ export default class PhotonSearch implements GeocodingProvider, ReverseGeocoding
|
|||
private static readonly types = {
|
||||
"R": "relation",
|
||||
"W": "way",
|
||||
"N": "node"
|
||||
"N": "node",
|
||||
}
|
||||
|
||||
|
||||
|
@ -54,7 +54,7 @@ export default class PhotonSearch implements GeocodingProvider, ReverseGeocoding
|
|||
|
||||
}
|
||||
|
||||
suggest(query: string, options?: GeocodingOptions): Store<GeoCodeResult[]> {
|
||||
suggest(query: string, options?: GeocodingOptions): Store<GeocodeResult[]> {
|
||||
return Stores.FromPromise(this.search(query, options))
|
||||
}
|
||||
|
||||
|
@ -95,7 +95,7 @@ export default class PhotonSearch implements GeocodingProvider, ReverseGeocoding
|
|||
|
||||
private getCategory(entry: Feature) {
|
||||
const p = entry.properties
|
||||
if(p.osm_key === "shop"){
|
||||
if (p.osm_key === "shop") {
|
||||
return "shop"
|
||||
}
|
||||
if (p.osm_value === "train_station" || p.osm_key === "railway") {
|
||||
|
@ -107,7 +107,7 @@ export default class PhotonSearch implements GeocodingProvider, ReverseGeocoding
|
|||
return p.type
|
||||
}
|
||||
|
||||
async search(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> {
|
||||
async search(query: string, options?: GeocodingOptions): Promise<GeocodeResult[]> {
|
||||
if (query.length < 3) {
|
||||
return []
|
||||
}
|
||||
|
@ -126,7 +126,7 @@ export default class PhotonSearch implements GeocodingProvider, ReverseGeocoding
|
|||
const [lon0, lat0, lon1, lat1] = f.properties.extent
|
||||
boundingbox = [lat0, lat1, lon0, lon1]
|
||||
}
|
||||
return <GeoCodeResult>{
|
||||
return <GeocodeResult>{
|
||||
feature: f,
|
||||
osm_id: f.properties.osm_id,
|
||||
display_name: f.properties.name,
|
||||
|
@ -135,7 +135,7 @@ export default class PhotonSearch implements GeocodingProvider, ReverseGeocoding
|
|||
category: this.getCategory(f),
|
||||
boundingbox,
|
||||
lon, lat,
|
||||
source: this._endpoint
|
||||
source: this._endpoint,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,36 +1,41 @@
|
|||
import { Store, UIEventSource } from "../UIEventSource"
|
||||
import { Feature } from "geojson"
|
||||
import { OsmConnection } from "../Osm/OsmConnection"
|
||||
import { GeoCodeResult } from "./GeocodingProvider"
|
||||
import { GeocodeResult } from "./GeocodingProvider"
|
||||
import { GeoOperations } from "../GeoOperations"
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
||||
|
||||
export class RecentSearch {
|
||||
|
||||
|
||||
private readonly _seenThisSession: UIEventSource<GeoCodeResult[]>
|
||||
public readonly seenThisSession: Store<GeoCodeResult[]>
|
||||
private readonly _seenThisSession: UIEventSource<GeocodeResult[]>
|
||||
public readonly seenThisSession: Store<GeocodeResult[]>
|
||||
|
||||
constructor(state: { layout: LayoutConfig, osmConnection: OsmConnection, selectedElement: Store<Feature> }) {
|
||||
const prefs = state.osmConnection.preferencesHandler.GetLongPreference("previous-searches")
|
||||
prefs.addCallbackAndRunD(prev => console.trace("Previous searches are:", prev))
|
||||
prefs.set(null)
|
||||
this._seenThisSession = new UIEventSource<GeoCodeResult[]>([])//UIEventSource.asObject<GeoCodeResult[]>(prefs, [])
|
||||
this._seenThisSession = new UIEventSource<GeocodeResult[]>([])//UIEventSource.asObject<GeoCodeResult[]>(prefs, [])
|
||||
this.seenThisSession = this._seenThisSession
|
||||
|
||||
prefs.addCallbackAndRunD(prefs => {
|
||||
if(prefs === ""){
|
||||
prefs.addCallbackAndRunD(pref => {
|
||||
if (pref === "") {
|
||||
return
|
||||
}
|
||||
const simpleArr = <GeoCodeResult[]> JSON.parse(prefs)
|
||||
if(simpleArr.length > 0){
|
||||
this._seenThisSession.set(simpleArr)
|
||||
return true
|
||||
try {
|
||||
|
||||
const simpleArr = <GeocodeResult[]>JSON.parse(pref)
|
||||
if (simpleArr.length > 0) {
|
||||
this._seenThisSession.set(simpleArr)
|
||||
return true
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e, pref)
|
||||
prefs.setData("")
|
||||
}
|
||||
})
|
||||
|
||||
this.seenThisSession.stabilized(2500).addCallbackAndRunD(seen => {
|
||||
const results= []
|
||||
const results = []
|
||||
for (let i = 0; i < Math.min(3, seen.length); i++) {
|
||||
const gc = seen[i]
|
||||
const simple = {
|
||||
|
@ -39,7 +44,7 @@ export class RecentSearch {
|
|||
display_name: gc.display_name,
|
||||
lat: gc.lat, lon: gc.lon,
|
||||
osm_id: gc.osm_id,
|
||||
osm_type: gc.osm_type
|
||||
osm_type: gc.osm_type,
|
||||
}
|
||||
results.push(simple)
|
||||
}
|
||||
|
@ -51,25 +56,25 @@ export class RecentSearch {
|
|||
state.selectedElement.addCallbackAndRunD(selected => {
|
||||
|
||||
const [osm_type, osm_id] = selected.properties.id.split("/")
|
||||
if(!osm_id){
|
||||
if (!osm_id) {
|
||||
return
|
||||
}
|
||||
console.log("Selected element is", selected)
|
||||
if(["node","way","relation"].indexOf(osm_type) < 0){
|
||||
if (["node", "way", "relation"].indexOf(osm_type) < 0) {
|
||||
return
|
||||
}
|
||||
const [lon, lat] = GeoOperations.centerpointCoordinates(selected)
|
||||
const entry = <GeoCodeResult>{
|
||||
const entry = <GeocodeResult>{
|
||||
feature: selected,
|
||||
osm_id, osm_type,
|
||||
lon, lat
|
||||
lon, lat,
|
||||
}
|
||||
this.addSelected(entry)
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
addSelected(entry: GeoCodeResult) {
|
||||
addSelected(entry: GeocodeResult) {
|
||||
const arr = [...(this.seenThisSession.data ?? []).slice(0, 20), entry]
|
||||
|
||||
const seenIds = new Set<string>()
|
||||
|
@ -81,7 +86,7 @@ export class RecentSearch {
|
|||
seenIds.add(id)
|
||||
}
|
||||
}
|
||||
console.log(">>>",arr)
|
||||
console.log(">>>", arr)
|
||||
this._seenThisSession.set(arr)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import GeocodingProvider, { GeoCodeResult, GeocodingOptions } from "./GeocodingProvider"
|
||||
import GeocodingProvider, { SearchResult, GeocodingOptions } from "./GeocodingProvider"
|
||||
import * as themeOverview from "../../assets/generated/theme_overview.json"
|
||||
import { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import { SpecialVisualizationState } from "../../UI/SpecialVisualization"
|
||||
|
@ -17,15 +17,15 @@ export default class ThemeSearch implements GeocodingProvider {
|
|||
this._knownHiddenThemes = MoreScreen.knownHiddenThemes(this._state.osmConnection)
|
||||
}
|
||||
|
||||
async search(query: string, options?: GeocodingOptions): Promise<GeoCodeResult[]> {
|
||||
async search(query: string, options?: GeocodingOptions): Promise<SearchResult[]> {
|
||||
return this.searchDirect(query, options)
|
||||
}
|
||||
|
||||
suggest(query: string, options?: GeocodingOptions): Store<GeoCodeResult[]> {
|
||||
suggest(query: string, options?: GeocodingOptions): Store<SearchResult[]> {
|
||||
return new ImmutableStore(this.searchDirect(query, options))
|
||||
}
|
||||
|
||||
private searchDirect(query: string, options?: GeocodingOptions): GeoCodeResult[] {
|
||||
private searchDirect(query: string, options?: GeocodingOptions): SearchResult[] {
|
||||
if(query.length < 1){
|
||||
return []
|
||||
}
|
||||
|
@ -37,8 +37,9 @@ export default class ThemeSearch implements GeocodingProvider {
|
|||
.filter(th => MoreScreen.MatchesLayout(th, query))
|
||||
.slice(0, limit + 1)
|
||||
|
||||
return withMatch.map(match => <GeoCodeResult> {
|
||||
return withMatch.map(match => <SearchResult> {
|
||||
payload: match,
|
||||
category: "theme",
|
||||
osm_id: match.id
|
||||
})
|
||||
}
|
||||
|
|
|
@ -7,7 +7,13 @@ import { Tag } from "../Tags/Tag"
|
|||
import Translations from "../../UI/i18n/Translations"
|
||||
import { RegexTag } from "../Tags/RegexTag"
|
||||
import { Or } from "../Tags/Or"
|
||||
import FilterConfig from "../../Models/ThemeConfig/FilterConfig"
|
||||
|
||||
export type ActiveFilter = {
|
||||
layer: LayerConfig,
|
||||
filter: FilterConfig,
|
||||
control: UIEventSource<string | number | undefined>
|
||||
}
|
||||
/**
|
||||
* The layer state keeps track of:
|
||||
* - Which layers are enabled
|
||||
|
@ -26,6 +32,9 @@ export default class LayerState {
|
|||
* Which layers are enabled in the current theme and what filters are applied onto them
|
||||
*/
|
||||
public readonly filteredLayers: ReadonlyMap<string, FilteredLayer>
|
||||
private readonly _activeFilters: UIEventSource<ActiveFilter[]> = new UIEventSource([])
|
||||
|
||||
public readonly activeFilters: Store<ActiveFilter[]> = this._activeFilters
|
||||
private readonly osmConnection: OsmConnection
|
||||
|
||||
/**
|
||||
|
@ -56,6 +65,41 @@ export default class LayerState {
|
|||
}
|
||||
this.filteredLayers = filteredLayers
|
||||
layers.forEach((l) => LayerState.linkFilterStates(l, filteredLayers))
|
||||
|
||||
this.filteredLayers.forEach(fl => {
|
||||
fl.isDisplayed.addCallback(() => this.updateActiveFilters())
|
||||
for (const [_, appliedFilter] of fl.appliedFilters) {
|
||||
appliedFilter.addCallback(() => this.updateActiveFilters())
|
||||
}
|
||||
})
|
||||
this.updateActiveFilters()
|
||||
}
|
||||
|
||||
private updateActiveFilters(){
|
||||
const filters: ActiveFilter[] = []
|
||||
this.filteredLayers.forEach(fl => {
|
||||
if(!fl.isDisplayed.data){
|
||||
return
|
||||
}
|
||||
for (const [filtername, appliedFilter] of fl.appliedFilters) {
|
||||
if (appliedFilter.data === undefined) {
|
||||
continue
|
||||
}
|
||||
const filter = fl.layerDef.filters.find(f => f.id === filtername)
|
||||
if(typeof appliedFilter.data === "number"){
|
||||
if(filter.options[appliedFilter.data].osmTags === undefined){
|
||||
// This is probably the first, generic option which doesn't _actually_ filter
|
||||
continue
|
||||
}
|
||||
}
|
||||
filters.push({
|
||||
layer: fl.layerDef,
|
||||
control: appliedFilter,
|
||||
filter,
|
||||
})
|
||||
}
|
||||
})
|
||||
this._activeFilters.set(filters)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -207,7 +207,6 @@ export default class UserRelatedState {
|
|||
isOfficial: boolean
|
||||
}
|
||||
| undefined {
|
||||
console.log("GETTING UNOFFICIAL THEME")
|
||||
const pref = this.osmConnection.GetLongPreference("unofficial-theme-" + id)
|
||||
const str = pref.data
|
||||
|
||||
|
@ -351,10 +350,8 @@ export default class UserRelatedState {
|
|||
const key = k.substring(0, k.length - "length".length)
|
||||
let combined = ""
|
||||
for (let i = 0; i < l; i++) {
|
||||
console.log("Building preference:",key,i,">>>", newPrefs[key + i], "<<<", newPrefs, )
|
||||
combined += (newPrefs[key + i])
|
||||
}
|
||||
console.log("Combined",key,">>>",combined)
|
||||
amendedPrefs.data[key.substring(0, key.length - "-combined-".length)] = combined
|
||||
} else {
|
||||
amendedPrefs.data[k] = newPrefs[k]
|
||||
|
|
|
@ -33,6 +33,7 @@ import LineRenderingConfigJson from "../Json/LineRenderingConfigJson"
|
|||
import { ConversionContext } from "./ConversionContext"
|
||||
import { ExpandRewrite } from "./ExpandRewrite"
|
||||
import { TagUtils } from "../../../Logic/Tags/TagUtils"
|
||||
import { Translatable } from "../Json/Translatable"
|
||||
|
||||
class ExpandFilter extends DesugaringStep<LayerConfigJson> {
|
||||
private static readonly predefinedFilters = ExpandFilter.load_filters()
|
||||
|
@ -40,7 +41,7 @@ class ExpandFilter extends DesugaringStep<LayerConfigJson> {
|
|||
|
||||
constructor(state: DesugaringContext) {
|
||||
super(
|
||||
"Expands filters: replaces a shorthand by the value found in 'filters.json'. If the string is formatted 'layername.filtername, it will be looked up into that layer instead",
|
||||
"Expands filters: replaces a shorthand by the value found in 'filters.json'. If the string is formatted 'layername.filtername, it will be looked up into that layer instead. If a tagRendering sets 'filter', this filter will also be included",
|
||||
["filter"],
|
||||
"ExpandFilter",
|
||||
)
|
||||
|
@ -67,6 +68,9 @@ class ExpandFilter extends DesugaringStep<LayerConfigJson> {
|
|||
const newFilters: FilterConfigJson[] = []
|
||||
const filters = <(FilterConfigJson | string)[]>json.filter
|
||||
|
||||
/**
|
||||
* Checks all tagRendering. If a tagrendering has 'filter' set, add this filter to the layer config
|
||||
*/
|
||||
for (let i = 0; i < json.tagRenderings?.length; i++) {
|
||||
const tagRendering = <TagRenderingConfigJson>json.tagRenderings[i]
|
||||
if (!tagRendering?.filter) {
|
||||
|
@ -94,6 +98,9 @@ class ExpandFilter extends DesugaringStep<LayerConfigJson> {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create filters based on builtin filters
|
||||
*/
|
||||
for (let i = 0; i < filters.length; i++) {
|
||||
const filter = filters[i]
|
||||
if (filter === undefined) {
|
||||
|
@ -115,15 +122,16 @@ class ExpandFilter extends DesugaringStep<LayerConfigJson> {
|
|||
"Found a matching tagRendering to base a filter on, but this tagRendering does not contain any mappings",
|
||||
)
|
||||
}
|
||||
const options = matchingTr.mappings.map((mapping) => ({
|
||||
const options = (<QuestionableTagRenderingConfigJson> matchingTr).mappings.map((mapping) => ({
|
||||
question: mapping.then,
|
||||
osmTags: mapping.if,
|
||||
searchTerms: mapping.searchTerms
|
||||
|
||||
}))
|
||||
options.unshift({
|
||||
question: matchingTr["question"] ?? {
|
||||
en: "All types",
|
||||
},
|
||||
question: matchingTr["question"] ?? Translations.t.general.filterPanel.allTypes,
|
||||
osmTags: undefined,
|
||||
searchTerms: undefined
|
||||
})
|
||||
newFilters.push({
|
||||
id: filter,
|
||||
|
|
|
@ -103,6 +103,10 @@ export class DoesImageExist extends DesugaringStep<string> {
|
|||
return image
|
||||
}
|
||||
|
||||
if(Utils.isEmoji(image)){
|
||||
return image
|
||||
}
|
||||
|
||||
if (!this._knownImagePaths.has(image)) {
|
||||
if (this.doesPathExist === undefined) {
|
||||
context.err(
|
||||
|
|
|
@ -8,12 +8,12 @@ import { UIEventSource } from "../../Logic/UIEventSource"
|
|||
import { QueryParameters } from "../../Logic/Web/QueryParameters"
|
||||
import { Utils } from "../../Utils"
|
||||
import { RegexTag } from "../../Logic/Tags/RegexTag"
|
||||
import BaseUIElement from "../../UI/BaseUIElement"
|
||||
import Table from "../../UI/Base/Table"
|
||||
import Combine from "../../UI/Base/Combine"
|
||||
import MarkdownUtils from "../../Utils/MarkdownUtils"
|
||||
|
||||
export type FilterConfigOption = {
|
||||
question: Translation
|
||||
searchTerms: Record<string, string[]>
|
||||
icon?: string
|
||||
osmTags: TagsFilter | undefined
|
||||
/* Only set if fields are present. Used to create `osmTags` (which are used to _actually_ filter) when the field is written*/
|
||||
readonly originalTagsSpec: TagConfigJson
|
||||
|
@ -105,8 +105,10 @@ export default class FilterConfig {
|
|||
return {
|
||||
question: question,
|
||||
osmTags: osmTags,
|
||||
searchTerms: option.searchTerms,
|
||||
fields,
|
||||
originalTagsSpec: option.osmTags,
|
||||
icon: option.icon
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -151,7 +153,7 @@ export default class FilterConfig {
|
|||
}
|
||||
|
||||
public initState(layerId: string): UIEventSource<undefined | number | string> {
|
||||
let defaultValue = ""
|
||||
let defaultValue: string
|
||||
if (this.options.length > 1) {
|
||||
defaultValue = "" + (this.defaultSelection ?? 0)
|
||||
} else if (this.options[0].fields?.length > 0) {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { TagConfigJson } from "./TagConfigJson"
|
||||
import { Translatable } from "./Translatable"
|
||||
|
||||
export default interface FilterConfigJson {
|
||||
/**
|
||||
|
@ -34,7 +35,9 @@ export default interface FilterConfigJson {
|
|||
* ```
|
||||
*/
|
||||
options: {
|
||||
question: string | any
|
||||
question: Translatable
|
||||
searchTerms?: Record<string, string[]>
|
||||
icon?: string
|
||||
osmTags?: TagConfigJson
|
||||
default?: boolean
|
||||
fields?: {
|
||||
|
|
|
@ -76,6 +76,7 @@ import { RecentSearch } from "../Logic/Geocoding/RecentSearch"
|
|||
import PhotonSearch from "../Logic/Geocoding/PhotonSearch"
|
||||
import ThemeSearch from "../Logic/Geocoding/ThemeSearch"
|
||||
import OpenStreetMapIdSearch from "../Logic/Geocoding/OpenStreetMapIdSearch"
|
||||
import FilterSearch from "../Logic/Geocoding/FilterSearch"
|
||||
|
||||
/**
|
||||
*
|
||||
|
@ -385,9 +386,10 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
|
||||
this.geosearch = new CombinedSearcher(
|
||||
new CoordinateSearch(),
|
||||
new LocalElementSearch(this, 5),
|
||||
new OpenStreetMapIdSearch(this),
|
||||
new PhotonSearch(), // new NominatimGeocoding(),
|
||||
new FilterSearch(this),
|
||||
//new LocalElementSearch(this, 5),
|
||||
//new OpenStreetMapIdSearch(this),
|
||||
// new PhotonSearch(), // new NominatimGeocoding(),
|
||||
this.featureSwitches.featureSwitchBackToThemeOverview.data ? new ThemeSearch(this) : undefined
|
||||
)
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
return
|
||||
}
|
||||
const v = value.data
|
||||
for (let option of htmlElement.getElementsByTagName("option")) {
|
||||
for (let option of Array.from(htmlElement.getElementsByTagName("option"))) {
|
||||
if (option.value === v) {
|
||||
option.selected = true
|
||||
return
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
import FilterviewWithFields from "./FilterviewWithFields.svelte"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import Translations from "../i18n/Translations"
|
||||
import { Utils } from "../../Utils"
|
||||
|
||||
export let filteredLayer: FilteredLayer
|
||||
export let highlightedLayer: Store<string | undefined> = new ImmutableStore(undefined)
|
||||
|
@ -28,7 +29,7 @@
|
|||
return state.sync(
|
||||
(f) => f === 0,
|
||||
[],
|
||||
(b) => (b ? 0 : undefined)
|
||||
(b) => (b ? 0 : undefined),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -75,6 +76,9 @@
|
|||
<Dropdown value={getStateFor(filter)}>
|
||||
{#each filter.options as option, i}
|
||||
<option value={i}>
|
||||
{#if Utils.isEmoji(option.icon)}
|
||||
{option.icon}
|
||||
{/if}
|
||||
<Tr t={option.question} />
|
||||
</option>
|
||||
{/each}
|
||||
|
|
|
@ -1,121 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type { GeoCodeResult } from "../../Logic/Geocoding/GeocodingProvider"
|
||||
import { GeocodingUtils } from "../../Logic/Geocoding/GeocodingProvider"
|
||||
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import ToSvelte from "../Base/ToSvelte.svelte"
|
||||
import { GeoOperations } from "../../Logic/GeoOperations"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import Icon from "../Map/Icon.svelte"
|
||||
import { BBox } from "../../Logic/BBox"
|
||||
import TagRenderingAnswer from "../Popup/TagRendering/TagRenderingAnswer.svelte"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import ArrowUp from "@babeard/svelte-heroicons/mini/ArrowUp"
|
||||
import { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import { Translation } from "../i18n/Translation"
|
||||
import MoreScreen from "./MoreScreen"
|
||||
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"
|
||||
|
||||
export let entry: GeoCodeResult
|
||||
export let state: SpecialVisualizationState
|
||||
let layer: LayerConfig
|
||||
let tags: UIEventSource<Record<string, string>>
|
||||
let descriptionTr: TagRenderingConfig = undefined
|
||||
if (entry.feature?.properties?.id) {
|
||||
layer = state.layout.getMatchingLayer(entry.feature.properties)
|
||||
tags = state.featureProperties.getStore(entry.feature.properties.id)
|
||||
descriptionTr = layer.tagRenderings.find(tr => tr.labels.indexOf("description") >= 0)
|
||||
}
|
||||
|
||||
let dispatch = createEventDispatcher<{ select }>()
|
||||
let distance = state.mapProperties.location.mapD(l => GeoOperations.distanceBetween([l.lon, l.lat], [entry.lon, entry.lat]))
|
||||
let bearing = state.mapProperties.location.mapD(l => GeoOperations.bearing([l.lon, l.lat], [entry.lon, entry.lat]))
|
||||
let mapRotation = state.mapProperties.rotation
|
||||
let inView = state.mapProperties.bounds.mapD(bounds => bounds.contains([entry.lon, entry.lat]))
|
||||
|
||||
let otherTheme: MinimalLayoutInformation | undefined = <MinimalLayoutInformation>entry.payload
|
||||
|
||||
function select() {
|
||||
if (entry.boundingbox) {
|
||||
const [lat0, lat1, lon0, lon1] = entry.boundingbox
|
||||
state.mapProperties.bounds.set(
|
||||
new BBox([
|
||||
[lon0, lat0],
|
||||
[lon1, lat1]
|
||||
]).pad(0.01)
|
||||
)
|
||||
} else {
|
||||
state.mapProperties.flyTo(entry.lon, entry.lat, GeocodingUtils.categoryToZoomLevel[entry.category] ?? 17)
|
||||
}
|
||||
if (entry.feature?.properties?.id) {
|
||||
state.selectedElement.set(entry.feature)
|
||||
}
|
||||
state.recentlySearched.addSelected(entry)
|
||||
dispatch("select")
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if otherTheme}
|
||||
<a href={ MoreScreen.createUrlFor(otherTheme, false)}
|
||||
class="flex items-center p-2 w-full gap-y-2 rounded-xl searchresult">
|
||||
|
||||
<Icon icon={otherTheme.icon} clss="w-6 h-6 m-1" />
|
||||
<div class="flex flex-col">
|
||||
<b>
|
||||
<Tr t={new Translation(otherTheme.title)} />
|
||||
</b>
|
||||
<!--<Tr t={new Translation(otherTheme.shortDescription)} /> -->
|
||||
</div>
|
||||
</a>
|
||||
|
||||
{:else}
|
||||
<button class="unstyled w-full link-no-underline searchresult" on:click={() => select() }>
|
||||
<div class="p-2 flex items-center w-full gap-y-2">
|
||||
{#if layer}
|
||||
<ToSvelte construct={() => layer.defaultIcon(entry.feature.properties).SetClass("w-6 h-6")} />
|
||||
{:else if entry.category}
|
||||
<Icon icon={GeocodingUtils.categoryToIcon[entry.category]} clss="w-6 h-6 shrink-0" color="#aaa" />
|
||||
{/if}
|
||||
<div class="flex flex-col items-start pl-2 w-full">
|
||||
<div class="flex flex-wrap gap-x-2 justify-between w-full">
|
||||
<b class="nowrap">
|
||||
{#if layer && $tags?.id}
|
||||
<TagRenderingAnswer config={layer.title} selectedElement={entry.feature} {state} {tags} {layer} />
|
||||
{:else}
|
||||
{entry.display_name ?? entry.osm_id}
|
||||
{/if}
|
||||
</b>
|
||||
{#if $distance > 50}
|
||||
<div class="flex gap-x-1 items-center">
|
||||
{#if $bearing && !$inView}
|
||||
<ArrowUp class="w-4 h-4 shrink-0" style={`transform: rotate(${$bearing - $mapRotation}deg)`} />
|
||||
{/if}
|
||||
{#if $distance}
|
||||
{GeoOperations.distanceToHuman($distance)}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-x-2">
|
||||
|
||||
{#if descriptionTr}
|
||||
<TagRenderingAnswer defaultSize="subtle" noIcons={true} config={descriptionTr} {tags} {state}
|
||||
selectedElement={entry.feature} {layer} />
|
||||
{/if}
|
||||
{#if descriptionTr && entry.description}
|
||||
–
|
||||
{/if}
|
||||
{#if entry.description}
|
||||
<div class="subtle flex justify-between w-full">
|
||||
{entry.description}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/if}
|
|
@ -1,100 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type { GeoCodeResult } from "../../Logic/Geocoding/GeocodingProvider"
|
||||
import SearchResult from "./SearchResult.svelte"
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import Loading from "../Base/Loading.svelte"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import Translations from "../i18n/Translations"
|
||||
import MoreScreen from "./MoreScreen"
|
||||
import { XCircleIcon } from "@babeard/svelte-heroicons/solid"
|
||||
|
||||
export let state: SpecialVisualizationState
|
||||
export let results: GeoCodeResult[]
|
||||
export let searchTerm: Store<string>
|
||||
export let isFocused: UIEventSource<boolean>
|
||||
|
||||
let recentlySeen: Store<GeoCodeResult[]> = state.recentlySearched.seenThisSession
|
||||
let recentThemes = state.userRelatedState.recentlyVisitedThemes.mapD(thms => thms.filter(th => th !== state.layout.id).slice(0, 3))
|
||||
let allowOtherThemes = state.featureSwitches.featureSwitchBackToThemeOverview
|
||||
</script>
|
||||
|
||||
<div class="relative w-full h-full collapsable " class:collapsed={!$isFocused}>
|
||||
<button class="absolute right-0 top-0 border-none p-0" on:click={() => isFocused.setData(false)} tabindex="-1">
|
||||
<XCircleIcon class="w-6 h-6" />
|
||||
</button>
|
||||
|
||||
<div class="searchbox normal-background">
|
||||
|
||||
{#if $searchTerm.length > 0 && results === undefined}
|
||||
<div class="flex justify-center m-4 my-8">
|
||||
<Loading />
|
||||
</div>
|
||||
{:else if results?.length > 0}
|
||||
<div style="max-height: calc(50vh - 1rem - 2px);" class="overflow-y-auto p-2" tabindex="-1">
|
||||
|
||||
{#each results as entry (entry)}
|
||||
<SearchResult on:select {entry} {state} />
|
||||
{/each}
|
||||
</div>
|
||||
{:else if $searchTerm.length > 0 || $recentlySeen?.length > 0 || $recentThemes?.length > 0}
|
||||
<div style="max-height: calc(50vh - 1rem - 2px);" class="overflow-y-auto p-2 flex flex-col gap-y-8" tabindex="-1">
|
||||
{#if $searchTerm.length > 0}
|
||||
<b class="flex justify-center p-4">
|
||||
<Tr t={Translations.t.general.search.nothingFor.Subs({term: $searchTerm})} />
|
||||
</b>
|
||||
{/if}
|
||||
|
||||
{#if $recentlySeen?.length > 0}
|
||||
<div>
|
||||
<h3 class="m-2">
|
||||
<Tr t={Translations.t.general.search.recents} />
|
||||
</h3>
|
||||
{#each $recentlySeen as entry}
|
||||
<SearchResult {entry} {state} on:select />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if $recentThemes?.length > 0 && $allowOtherThemes}
|
||||
<div>
|
||||
<h3 class="m-2">
|
||||
<Tr t={Translations.t.general.search.recentThemes} />
|
||||
</h3>
|
||||
{#each $recentThemes as themeId (themeId)}
|
||||
<SearchResult
|
||||
entry={{payload: MoreScreen.officialThemesById.get(themeId), display_name: themeId, lat: 0, lon: 0}}
|
||||
{state}
|
||||
on:select />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.searchbox {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid black;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.collapsable {
|
||||
max-height: 50vh;
|
||||
transition: max-height 400ms linear;
|
||||
transition-delay: 100ms;
|
||||
overflow: hidden;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.collapsed {
|
||||
padding-top: 0 !important;
|
||||
padding-bottom: 0 !important;
|
||||
max-height: 0 !important;
|
||||
}
|
||||
</style>
|
|
@ -2,7 +2,7 @@
|
|||
import Translations from "../i18n/Translations"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import NextButton from "../Base/NextButton.svelte"
|
||||
import Geosearch from "./Geosearch.svelte"
|
||||
import Geosearch from "../Search/Geosearch.svelte"
|
||||
import ThemeViewState from "../../Models/ThemeViewState"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { SearchIcon } from "@rgossiaux/svelte-heroicons/solid"
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
import { GeoOperations } from "../../Logic/GeoOperations"
|
||||
import LocationInput from "../InputElement/Helpers/LocationInput.svelte"
|
||||
import OpenBackgroundSelectorButton from "../BigComponents/OpenBackgroundSelectorButton.svelte"
|
||||
import Geosearch from "../BigComponents/Geosearch.svelte"
|
||||
import Geosearch from "../Search/Geosearch.svelte"
|
||||
import If from "../Base/If.svelte"
|
||||
import Constants from "../../Models/Constants"
|
||||
import LoginToggle from "../Base/LoginToggle.svelte"
|
||||
|
|
20
src/UI/Search/ActiveFilter.svelte
Normal file
20
src/UI/Search/ActiveFilter.svelte
Normal file
|
@ -0,0 +1,20 @@
|
|||
<script lang="ts">
|
||||
import type { ActiveFilter } from "../../Logic/State/LayerState"
|
||||
import { Badge } from "flowbite-svelte"
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
|
||||
export let state: SpecialVisualizationState
|
||||
|
||||
export let activeFilter: ActiveFilter
|
||||
let { control, layer, filter } = activeFilter
|
||||
let option = control.map(c => {
|
||||
if (typeof c === "number") {
|
||||
return filter.options[c]
|
||||
}
|
||||
return filter.options[0]
|
||||
})
|
||||
</script>
|
||||
<Badge dismissable large border rounded color="dark" on:close={() =>{ console.log( "dismiss"); return control.setData(undefined) }}>
|
||||
<Tr cls="whitespace-nowrap" t={$option.question} />
|
||||
</Badge>
|
17
src/UI/Search/ActiveFilters.svelte
Normal file
17
src/UI/Search/ActiveFilters.svelte
Normal file
|
@ -0,0 +1,17 @@
|
|||
<script lang="ts">
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import { Badge } from "flowbite-svelte"
|
||||
import ActiveFilter from "./ActiveFilter.svelte"
|
||||
|
||||
export let state: SpecialVisualizationState
|
||||
|
||||
let activeFilters = state.layerState.activeFilters
|
||||
|
||||
|
||||
</script>
|
||||
<div class="flex flex-wrap gap-y-1 gap-x-1 button-unstyled">
|
||||
|
||||
{#each $activeFilters as activeFilter (activeFilter)}
|
||||
<ActiveFilter {activeFilter} {state} />
|
||||
{/each}
|
||||
</div>
|
55
src/UI/Search/FilterResult.svelte
Normal file
55
src/UI/Search/FilterResult.svelte
Normal file
|
@ -0,0 +1,55 @@
|
|||
<script lang="ts">
|
||||
import type FilterConfig from "../../Models/ThemeConfig/FilterConfig"
|
||||
import type { FilterConfigOption } from "../../Models/ThemeConfig/FilterConfig"
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import Filter from "../../assets/svg/Filter.svelte"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import type { FilterPayload } from "../../Logic/Geocoding/GeocodingProvider"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { FilterIcon as FilterSolid } from "@rgossiaux/svelte-heroicons/solid"
|
||||
import { FilterIcon as FilterOutline } from "@rgossiaux/svelte-heroicons/outline"
|
||||
|
||||
export let entry: {
|
||||
category: "filter",
|
||||
payload: FilterPayload
|
||||
}
|
||||
let { option, filter, layer, index } = entry.payload
|
||||
export let state: SpecialVisualizationState
|
||||
let dispatch = createEventDispatcher<{ select }>()
|
||||
|
||||
let flayer = state.layerState.filteredLayers.get(layer.id)
|
||||
let filtercontrol = flayer.appliedFilters.get(filter.id)
|
||||
let isActive = filtercontrol.map(c => c === index)
|
||||
|
||||
function apply() {
|
||||
|
||||
for (const [name, otherLayer] of state.layerState.filteredLayers) {
|
||||
if(name === layer.id){
|
||||
otherLayer.isDisplayed.setData(true)
|
||||
continue
|
||||
}
|
||||
otherLayer.isDisplayed.setData(false)
|
||||
}
|
||||
|
||||
if(filtercontrol.data === index){
|
||||
filtercontrol.setData(undefined)
|
||||
}else{
|
||||
filtercontrol.setData(index)
|
||||
}
|
||||
dispatch("select")
|
||||
|
||||
|
||||
}
|
||||
</script>
|
||||
<button on:click={() => apply()}>
|
||||
{#if $isActive}
|
||||
<FilterSolid class="w-8 h-8 shrink-0" />
|
||||
{:else}
|
||||
<FilterOutline class="w-8 h-8 shrink-0" />
|
||||
{/if}
|
||||
<Tr t={option.question} />
|
||||
<div class="subtle">
|
||||
{layer.id}
|
||||
</div>
|
||||
</button>
|
100
src/UI/Search/GeocodeResult.svelte
Normal file
100
src/UI/Search/GeocodeResult.svelte
Normal file
|
@ -0,0 +1,100 @@
|
|||
<script lang="ts">
|
||||
import { GeocodingUtils } from "../../Logic/Geocoding/GeocodingProvider"
|
||||
import type { GeocodeResult } from "../../Logic/Geocoding/GeocodingProvider"
|
||||
import { GeoOperations } from "../../Logic/GeoOperations"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import { BBox } from "../../Logic/BBox"
|
||||
import ToSvelte from "../Base/ToSvelte.svelte"
|
||||
import Icon from "../Map/Icon.svelte"
|
||||
import TagRenderingAnswer from "../Popup/TagRendering/TagRenderingAnswer.svelte"
|
||||
import ArrowUp from "@babeard/svelte-heroicons/mini/ArrowUp"
|
||||
|
||||
export let entry: GeocodeResult
|
||||
export let state: SpecialVisualizationState
|
||||
|
||||
let layer: LayerConfig
|
||||
let tags: UIEventSource<Record<string, string>>
|
||||
let descriptionTr: TagRenderingConfig = undefined
|
||||
if (entry.feature?.properties?.id) {
|
||||
layer = state.layout.getMatchingLayer(entry.feature.properties)
|
||||
tags = state.featureProperties.getStore(entry.feature.properties.id)
|
||||
descriptionTr = layer.tagRenderings.find(tr => tr.labels.indexOf("description") >= 0)
|
||||
}
|
||||
|
||||
let dispatch = createEventDispatcher<{ select }>()
|
||||
let distance = state.mapProperties.location.mapD(l => GeoOperations.distanceBetween([l.lon, l.lat], [entry.lon, entry.lat]))
|
||||
let bearing = state.mapProperties.location.mapD(l => GeoOperations.bearing([l.lon, l.lat], [entry.lon, entry.lat]))
|
||||
let mapRotation = state.mapProperties.rotation
|
||||
let inView = state.mapProperties.bounds.mapD(bounds => bounds.contains([entry.lon, entry.lat]))
|
||||
|
||||
|
||||
function select() {
|
||||
if (entry.boundingbox) {
|
||||
const [lat0, lat1, lon0, lon1] = entry.boundingbox
|
||||
state.mapProperties.bounds.set(
|
||||
new BBox([
|
||||
[lon0, lat0],
|
||||
[lon1, lat1],
|
||||
]).pad(0.01),
|
||||
)
|
||||
} else {
|
||||
state.mapProperties.flyTo(entry.lon, entry.lat, GeocodingUtils.categoryToZoomLevel[entry.category] ?? 17)
|
||||
}
|
||||
if (entry.feature?.properties?.id) {
|
||||
state.selectedElement.set(entry.feature)
|
||||
}
|
||||
state.recentlySearched.addSelected(entry)
|
||||
dispatch("select")
|
||||
}
|
||||
</script>
|
||||
|
||||
<button class="unstyled w-full link-no-underline searchresult" on:click={() => select() }>
|
||||
<div class="p-2 flex items-center w-full gap-y-2">
|
||||
{#if layer}
|
||||
<ToSvelte construct={() => layer.defaultIcon(entry.feature.properties).SetClass("w-6 h-6")} />
|
||||
{:else if entry.category}
|
||||
<Icon icon={GeocodingUtils.categoryToIcon[entry.category]} clss="w-6 h-6 shrink-0" color="#aaa" />
|
||||
{/if}
|
||||
<div class="flex flex-col items-start pl-2 w-full">
|
||||
<div class="flex flex-wrap gap-x-2 justify-between w-full">
|
||||
<b class="nowrap">
|
||||
{#if layer && $tags?.id}
|
||||
<TagRenderingAnswer config={layer.title} selectedElement={entry.feature} {state} {tags} {layer} />
|
||||
{:else}
|
||||
{entry.display_name ?? entry.osm_id}
|
||||
{/if}
|
||||
</b>
|
||||
{#if $distance > 50}
|
||||
<div class="flex gap-x-1 items-center">
|
||||
{#if $bearing && !$inView}
|
||||
<ArrowUp class="w-4 h-4 shrink-0" style={`transform: rotate(${$bearing - $mapRotation}deg)`} />
|
||||
{/if}
|
||||
{#if $distance}
|
||||
{GeoOperations.distanceToHuman($distance)}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-x-2">
|
||||
|
||||
{#if descriptionTr}
|
||||
<TagRenderingAnswer defaultSize="subtle" noIcons={true} config={descriptionTr} {tags} {state}
|
||||
selectedElement={entry.feature} {layer} />
|
||||
{/if}
|
||||
{#if descriptionTr && entry.description}
|
||||
–
|
||||
{/if}
|
||||
{#if entry.description}
|
||||
<div class="subtle flex justify-between w-full">
|
||||
{entry.description}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
|
@ -13,18 +13,16 @@
|
|||
import { GeoLocationState } from "../../Logic/State/GeoLocationState"
|
||||
import { NominatimGeocoding } from "../../Logic/Geocoding/NominatimGeocoding"
|
||||
import { GeocodingUtils } from "../../Logic/Geocoding/GeocodingProvider"
|
||||
import type { GeoCodeResult } from "../../Logic/Geocoding/GeocodingProvider"
|
||||
import type { SearchResult } from "../../Logic/Geocoding/GeocodingProvider"
|
||||
import type GeocodingProvider from "../../Logic/Geocoding/GeocodingProvider"
|
||||
|
||||
import SearchResults from "./SearchResults.svelte"
|
||||
import MoreScreen from "./MoreScreen"
|
||||
import type { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import { focusWithArrows } from "../../Utils/focusWithArrows"
|
||||
import ShowDataLayer from "../Map/ShowDataLayer"
|
||||
import ThemeViewState from "../../Models/ThemeViewState"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import GeocodingFeatureSource from "../../Logic/Geocoding/GeocodingFeatureSource"
|
||||
import type { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson.js"
|
||||
import MoreScreen from "../BigComponents/MoreScreen"
|
||||
|
||||
export let perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer> | undefined = undefined
|
||||
export let bounds: UIEventSource<BBox>
|
||||
|
@ -90,8 +88,7 @@
|
|||
return
|
||||
}
|
||||
const poi = result[0]
|
||||
if (poi.payload !== undefined) {
|
||||
// This is a theme
|
||||
if (poi.category === "theme") {
|
||||
const theme = <MinimalLayoutInformation>poi.payload
|
||||
const url = MoreScreen.createUrlFor(theme, false)
|
||||
console.log("Found a theme, going to", url)
|
||||
|
@ -99,6 +96,9 @@
|
|||
window.location = url
|
||||
return
|
||||
}
|
||||
if(poi.category === "filter"){
|
||||
return // Should not happen
|
||||
}
|
||||
if (poi.boundingbox) {
|
||||
|
||||
const [lat0, lat1, lon0, lon1] = poi.boundingbox
|
||||
|
@ -139,13 +139,14 @@
|
|||
}
|
||||
}
|
||||
|
||||
let suggestions: Store<GeoCodeResult[]> = searchContents.stabilized(250).bindD(search => {
|
||||
let suggestions: Store<SearchResult[]> = searchContents.stabilized(250).bindD(search => {
|
||||
if (search.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
return Stores.holdDefined(bounds.bindD(bbox => searcher.suggest(search, { bbox, limit: 15 })))
|
||||
}
|
||||
)
|
||||
suggestions.addCallbackAndRun(suggestions => console.log(">>> suggestions are", suggestions))
|
||||
let geocededFeatures= new GeocodingFeatureSource(suggestions.stabilized(250))
|
||||
state.featureProperties.trackFeatureSource(geocededFeatures)
|
||||
|
20
src/UI/Search/SearchResult.svelte
Normal file
20
src/UI/Search/SearchResult.svelte
Normal file
|
@ -0,0 +1,20 @@
|
|||
<script lang="ts">
|
||||
import type { SearchResult } from "../../Logic/Geocoding/GeocodingProvider"
|
||||
|
||||
import ThemeResult from "../Search/ThemeResult.svelte"
|
||||
import FilterResult from "./FilterResult.svelte"
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import GeocodeResult from "./GeocodeResult.svelte"
|
||||
|
||||
export let entry: SearchResult
|
||||
export let state: SpecialVisualizationState
|
||||
</script>
|
||||
|
||||
{#if entry.category === "theme"}
|
||||
<ThemeResult {entry} />
|
||||
{:else if entry.category === "filter"}
|
||||
<FilterResult {entry} {state} />
|
||||
{:else}
|
||||
|
||||
<GeocodeResult {entry} {state} />
|
||||
{/if}
|
102
src/UI/Search/SearchResults.svelte
Normal file
102
src/UI/Search/SearchResults.svelte
Normal file
|
@ -0,0 +1,102 @@
|
|||
<script lang="ts">
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import Loading from "../Base/Loading.svelte"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import Translations from "../i18n/Translations"
|
||||
import { default as SearchResultSvelte } from "./SearchResult.svelte"
|
||||
import MoreScreen from "../BigComponents/MoreScreen"
|
||||
import type { GeocodeResult, SearchResult } from "../../Logic/Geocoding/GeocodingProvider"
|
||||
import ActiveFilters from "./ActiveFilters.svelte"
|
||||
|
||||
export let state: SpecialVisualizationState
|
||||
export let results: SearchResult[]
|
||||
export let searchTerm: Store<string>
|
||||
export let isFocused: UIEventSource<boolean>
|
||||
let hasActiveFilters = state.layerState.activeFilters.map(af => af.length > 0)
|
||||
|
||||
console.log("Results are", results)
|
||||
|
||||
let recentlySeen: Store<GeocodeResult[]> = state.recentlySearched.seenThisSession
|
||||
let recentThemes = state.userRelatedState.recentlyVisitedThemes.mapD(thms => thms.filter(th => th !== state.layout.id).slice(0, 3))
|
||||
let allowOtherThemes = state.featureSwitches.featureSwitchBackToThemeOverview
|
||||
</script>
|
||||
|
||||
<div class="relative w-full h-full collapsable " class:collapsed={!$isFocused && !$hasActiveFilters}>
|
||||
<div class="searchbox normal-background">
|
||||
<ActiveFilters {state} />
|
||||
{#if $isFocused}
|
||||
{#if $searchTerm.length > 0 && results === undefined}
|
||||
<div class="flex justify-center m-4 my-8">
|
||||
<Loading />
|
||||
</div>
|
||||
{:else if results?.length > 0}
|
||||
<div style="max-height: calc(50vh - 1rem - 2px);" class="overflow-y-auto p-2" tabindex="-1">
|
||||
|
||||
{#each results as entry (entry)}
|
||||
<SearchResultSvelte on:select {entry} {state} />
|
||||
{/each}
|
||||
</div>
|
||||
{:else if $searchTerm.length > 0 || $recentlySeen?.length > 0 || $recentThemes?.length > 0}
|
||||
<div style="max-height: calc(50vh - 1rem - 2px);" class="overflow-y-auto p-2 flex flex-col gap-y-8"
|
||||
tabindex="-1">
|
||||
{#if $searchTerm.length > 0}
|
||||
<b class="flex justify-center p-4">
|
||||
<Tr t={Translations.t.general.search.nothingFor.Subs({term: $searchTerm})} />
|
||||
</b>
|
||||
{/if}
|
||||
|
||||
{#if $recentlySeen?.length > 0}
|
||||
<div>
|
||||
<h3 class="m-2">
|
||||
<Tr t={Translations.t.general.search.recents} />
|
||||
</h3>
|
||||
{#each $recentlySeen as entry}
|
||||
<SearchResultSvelte {entry} {state} on:select />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if $recentThemes?.length > 0 && $allowOtherThemes}
|
||||
<div>
|
||||
<h3 class="m-2">
|
||||
<Tr t={Translations.t.general.search.recentThemes} />
|
||||
</h3>
|
||||
{#each $recentThemes as themeId (themeId)}
|
||||
<SearchResultSvelte
|
||||
entry={{payload: MoreScreen.officialThemesById.get(themeId), display_name: themeId, lat: 0, lon: 0}}
|
||||
{state}
|
||||
on:select />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.searchbox {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid black;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.collapsable {
|
||||
max-height: 50vh;
|
||||
transition: max-height 400ms linear;
|
||||
transition-delay: 100ms;
|
||||
overflow: hidden;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.collapsed {
|
||||
padding-top: 0 !important;
|
||||
padding-bottom: 0 !important;
|
||||
max-height: 0 !important;
|
||||
}
|
||||
</style>
|
22
src/UI/Search/ThemeResult.svelte
Normal file
22
src/UI/Search/ThemeResult.svelte
Normal file
|
@ -0,0 +1,22 @@
|
|||
<script lang="ts">
|
||||
import { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import MoreScreen from "../BigComponents/MoreScreen"
|
||||
import { Translation } from "../i18n/Translation"
|
||||
import Icon from "../Map/Icon.svelte"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
|
||||
export let entry: { category: "theme", payload: MinimalLayoutInformation }
|
||||
let otherTheme = entry.payload
|
||||
</script>
|
||||
|
||||
<a href={MoreScreen.createUrlFor(otherTheme, false)}
|
||||
class="flex items-center p-2 w-full gap-y-2 rounded-xl searchresult">
|
||||
|
||||
<Icon icon={otherTheme.icon} clss="w-6 h-6 m-1" />
|
||||
<div class="flex flex-col">
|
||||
<b>
|
||||
<Tr t={new Translation(otherTheme.title)} />
|
||||
</b>
|
||||
<!--<Tr t={new Translation(otherTheme.shortDescription)} /> -->
|
||||
</div>
|
||||
</a>
|
|
@ -11,7 +11,7 @@
|
|||
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
|
||||
import ThemeViewState from "../Models/ThemeViewState"
|
||||
import type { MapProperties } from "../Models/MapProperties"
|
||||
import Geosearch from "./BigComponents/Geosearch.svelte"
|
||||
import Geosearch from "./Search/Geosearch.svelte"
|
||||
import Translations from "./i18n/Translations"
|
||||
import usersettings from "../assets/generated/layers/usersettings.json"
|
||||
import {
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import DOMPurify from "dompurify"
|
||||
|
||||
export class Utils {
|
||||
/**
|
||||
* In the 'deploy'-step, some code needs to be run by ts-node.
|
||||
|
|
|
@ -279,12 +279,12 @@ button.as-link {
|
|||
padding: 0;
|
||||
}
|
||||
|
||||
button.unstyled {
|
||||
button.unstyled, .button-unstyled button {
|
||||
background-color: unset;
|
||||
display: inline-flex;
|
||||
justify-content: start;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
box-shadow: none !important;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue