From 3cd04df60bbcdbb8c671247e09f6bc9b307f0159 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Thu, 15 Aug 2024 01:51:33 +0200 Subject: [PATCH] First search with suggestions --- assets/layers/usersettings/usersettings.json | 23 +++-- langs/layers/ca.json | 18 ++++ langs/layers/cs.json | 18 ++++ langs/layers/da.json | 7 ++ langs/layers/de.json | 18 ++++ langs/layers/en.json | 18 ++++ langs/layers/fi.json | 20 +++++ langs/layers/fr.json | 7 ++ langs/layers/nb_NO.json | 11 +++ langs/layers/nl.json | 18 ++++ langs/layers/pl.json | 22 +++++ langs/layers/pt.json | 18 ++++ langs/layers/zh_Hant.json | 18 ++++ public/css/index-tailwind-output.css | 44 +++++++--- .../TiledFeatureSource/SummaryTileSource.ts | 9 +- src/Logic/Geocoding/CombinedSearcher.ts | 21 +++++ src/Logic/Geocoding/CoordinateSearch.ts | 67 +++++++++++++++ src/Logic/Geocoding/GeocodingProvider.ts | 43 ++++++++++ src/Logic/Geocoding/LocalElementSearch.ts | 86 +++++++++++++++++++ src/Logic/Geocoding/NominatimGeocoding.ts | 39 +++++++++ src/Logic/Osm/Geocoding.ts | 45 ---------- src/Models/MapProperties.ts | 3 + .../ThemeConfig/Conversion/Validation.ts | 3 +- src/Models/ThemeConfig/LayerConfig.ts | 13 +-- src/Models/ThemeViewState.ts | 16 +++- src/UI/BigComponents/Geosearch.svelte | 27 +++++- src/UI/BigComponents/MoreScreen.ts | 2 +- src/UI/BigComponents/ReverseGeocoding.svelte | 6 +- src/UI/BigComponents/SearchResult.svelte | 46 ++++++++++ src/UI/BigComponents/SearchResults.svelte | 27 ++++++ src/UI/BigComponents/ThemeIntroPanel.svelte | 2 + src/UI/Map/MapLibreAdaptor.ts | 7 ++ src/UI/Popup/MoveWizard.svelte | 3 +- src/UI/SpecialVisualization.ts | 2 +- src/UI/ThemeViewGUI.svelte | 2 + src/Utils.ts | 22 ++++- src/index.css | 11 +++ 37 files changed, 677 insertions(+), 85 deletions(-) create mode 100644 src/Logic/Geocoding/CombinedSearcher.ts create mode 100644 src/Logic/Geocoding/CoordinateSearch.ts create mode 100644 src/Logic/Geocoding/GeocodingProvider.ts create mode 100644 src/Logic/Geocoding/LocalElementSearch.ts create mode 100644 src/Logic/Geocoding/NominatimGeocoding.ts delete mode 100644 src/Logic/Osm/Geocoding.ts create mode 100644 src/UI/BigComponents/SearchResult.svelte create mode 100644 src/UI/BigComponents/SearchResults.svelte diff --git a/assets/layers/usersettings/usersettings.json b/assets/layers/usersettings/usersettings.json index 855de0b76..c9f98f991 100644 --- a/assets/layers/usersettings/usersettings.json +++ b/assets/layers/usersettings/usersettings.json @@ -45,7 +45,9 @@ }, { "id": "profile-title", - "labels": ["hidden"], + "labels": [ + "hidden" + ], "icon": "user_circle", "render": { "*": "

{_name}

" @@ -63,7 +65,8 @@ { "id": "profile-description", "labels": [ - "profile-content","hidden" + "profile-content", + "hidden" ], "render": { "*": "{_description_html}" @@ -71,7 +74,6 @@ "mappings": [ { "if": "_description=", - "then": { "special": { "type": "link", @@ -98,7 +100,8 @@ { "id": "edit-profile", "labels": [ - "profile-content","hidden" + "profile-content", + "hidden" ], "condition": "_description!=", "render": { @@ -126,7 +129,8 @@ { "id": "verified-mastodon", "labels": [ - "profile-content","hidden" + "profile-content", + "hidden" ], "mappings": [ { @@ -157,7 +161,8 @@ { "id": "cscount-thanks", "labels": [ - "profile-content","hidden" + "profile-content", + "hidden" ], "mappings": [ { @@ -180,7 +185,8 @@ { "id": "translation-thanks", "labels": [ - "profile-content","hidden" + "profile-content", + "hidden" ], "mappings": [ { @@ -197,7 +203,8 @@ { "id": "contributor-thanks", "labels": [ - "profile-content","hidden" + "profile-content", + "hidden" ], "mappings": [ { diff --git a/langs/layers/ca.json b/langs/layers/ca.json index cf4593967..5a5e59f6e 100644 --- a/langs/layers/ca.json +++ b/langs/layers/ca.json @@ -8468,6 +8468,13 @@ } } }, + "edit-profile": { + "render": { + "special": { + "text": "Editeu la descripció del vostre perfil" + } + } + }, "fixate-north": { "mappings": { "0": { @@ -8529,6 +8536,17 @@ }, "question": "Sota quina llicència vols publicar les teves fotos?" }, + "profile-description": { + "mappings": { + "0": { + "then": { + "special": { + "text": "Afegeix una descripció del perfil" + } + } + } + } + }, "settings-link": { "render": { "special": { diff --git a/langs/layers/cs.json b/langs/layers/cs.json index 632be827e..41835caf4 100644 --- a/langs/layers/cs.json +++ b/langs/layers/cs.json @@ -8723,6 +8723,13 @@ } } }, + "edit-profile": { + "render": { + "special": { + "text": "Úprava popisu vašeho profilu" + } + } + }, "fixate-north": { "mappings": { "0": { @@ -8784,6 +8791,17 @@ }, "question": "Pod jakou licencí chcete své fotografie zveřejnit?" }, + "profile-description": { + "mappings": { + "0": { + "then": { + "special": { + "text": "Přidat popis profilu" + } + } + } + } + }, "settings-link": { "render": { "special": { diff --git a/langs/layers/da.json b/langs/layers/da.json index 3ff20dea0..9d7541137 100644 --- a/langs/layers/da.json +++ b/langs/layers/da.json @@ -2485,6 +2485,13 @@ } } }, + "edit-profile": { + "render": { + "special": { + "text": "Ret din profilbeskrivelse" + } + } + }, "fixate-north": { "mappings": { "0": { diff --git a/langs/layers/de.json b/langs/layers/de.json index 595f678a4..ab37d4ee7 100644 --- a/langs/layers/de.json +++ b/langs/layers/de.json @@ -11119,6 +11119,13 @@ } } }, + "edit-profile": { + "render": { + "special": { + "text": "Eigene Profilbeschreibung bearbeiten" + } + } + }, "fixate-north": { "mappings": { "0": { @@ -11207,6 +11214,17 @@ }, "question": "Unter welcher Lizenz möchten Sie Ihre Bilder veröffentlichen?" }, + "profile-description": { + "mappings": { + "0": { + "then": { + "special": { + "text": "Profilbeschreibung hinzufügen" + } + } + } + } + }, "settings-link": { "render": { "special": { diff --git a/langs/layers/en.json b/langs/layers/en.json index 72f4da118..18de13279 100644 --- a/langs/layers/en.json +++ b/langs/layers/en.json @@ -11170,6 +11170,13 @@ "debug-title": { "render": "

Debugging options

" }, + "edit-profile": { + "render": { + "special": { + "text": "Edit your profile description" + } + } + }, "fixate-north": { "mappings": { "0": { @@ -11258,6 +11265,17 @@ }, "question": "Under what license do you want to publish your pictures?" }, + "profile-description": { + "mappings": { + "0": { + "then": { + "special": { + "text": "Add a profile description" + } + } + } + } + }, "settings-link": { "render": { "special": { diff --git a/langs/layers/fi.json b/langs/layers/fi.json index 6ad318e18..2bb7ab510 100644 --- a/langs/layers/fi.json +++ b/langs/layers/fi.json @@ -122,6 +122,26 @@ } }, "usersettings": { + "tagRenderings": { + "edit-profile": { + "render": { + "special": { + "text": "Muokkaa profiilin kuvausta" + } + } + }, + "profile-description": { + "mappings": { + "0": { + "then": { + "special": { + "text": "Lisää profiilin kuvaus" + } + } + } + } + } + }, "title": { "render": "Asetukset" } diff --git a/langs/layers/fr.json b/langs/layers/fr.json index 88af4f47e..78c9a486b 100644 --- a/langs/layers/fr.json +++ b/langs/layers/fr.json @@ -6925,6 +6925,13 @@ } } }, + "edit-profile": { + "render": { + "special": { + "text": "Modifier ton profil" + } + } + }, "fixate-north": { "mappings": { "0": { diff --git a/langs/layers/nb_NO.json b/langs/layers/nb_NO.json index 31e212e2c..83c930c1f 100644 --- a/langs/layers/nb_NO.json +++ b/langs/layers/nb_NO.json @@ -842,6 +842,17 @@ }, "usersettings": { "tagRenderings": { + "profile-description": { + "mappings": { + "0": { + "then": { + "special": { + "text": "Legg til profilbeskrivelse" + } + } + } + } + }, "translation-completeness": { "render": "Oversettelsen for {_theme} i {_language} har {_translation_percentage}% dekning: {_translation_translated_count} strenger av {_translation_total} har blitt oversatt" } diff --git a/langs/layers/nl.json b/langs/layers/nl.json index a34b5fc90..1dd81b10d 100644 --- a/langs/layers/nl.json +++ b/langs/layers/nl.json @@ -8871,6 +8871,13 @@ } } }, + "edit-profile": { + "render": { + "special": { + "text": "Pas je profielbeschrijving aan" + } + } + }, "fixate-north": { "mappings": { "0": { @@ -8959,6 +8966,17 @@ }, "question": "Met welke licentie wil je je afbeeldingen toevoegen?" }, + "profile-description": { + "mappings": { + "0": { + "then": { + "special": { + "text": "Voeg een profielbeschrijving toe" + } + } + } + } + }, "settings-link": { "render": { "special": { diff --git a/langs/layers/pl.json b/langs/layers/pl.json index db175d09a..ad81f5ba7 100644 --- a/langs/layers/pl.json +++ b/langs/layers/pl.json @@ -3338,6 +3338,28 @@ } } }, + "usersettings": { + "tagRenderings": { + "edit-profile": { + "render": { + "special": { + "text": "Edytuj opis swojego profilu" + } + } + }, + "profile-description": { + "mappings": { + "0": { + "then": { + "special": { + "text": "Dodaj opis profilu" + } + } + } + } + } + } + }, "walls_and_buildings": { "description": "Specjalna warstwa zabudowana zapewniająca wszystkie mury i budynki. Warstwa ta jest przydatna w ustawieniach wstępnych obiektów, które można umieścić przy ścianach (np. AED, skrzynki pocztowe, wejścia, adresy, kamery monitorujące itp.). Warstwa ta jest domyślnie niewidoczna i użytkownik nie może jej przełączać." }, diff --git a/langs/layers/pt.json b/langs/layers/pt.json index 5633ea9b8..604a9488b 100644 --- a/langs/layers/pt.json +++ b/langs/layers/pt.json @@ -1768,6 +1768,13 @@ } } }, + "edit-profile": { + "render": { + "special": { + "text": "Editar a descrição do seu perfil" + } + } + }, "picture-license": { "mappings": { "0": { @@ -1785,6 +1792,17 @@ }, "question": "Sob que licença você deseja publicar suas fotos?" }, + "profile-description": { + "mappings": { + "0": { + "then": { + "special": { + "text": "Adicionar uma descrição do perfil" + } + } + } + } + }, "show_debug": { "mappings": { "0": { diff --git a/langs/layers/zh_Hant.json b/langs/layers/zh_Hant.json index 615b0e8fd..370066886 100644 --- a/langs/layers/zh_Hant.json +++ b/langs/layers/zh_Hant.json @@ -782,6 +782,24 @@ }, "usersettings": { "tagRenderings": { + "edit-profile": { + "render": { + "special": { + "text": "編輯你的個人檔敘述" + } + } + }, + "profile-description": { + "mappings": { + "0": { + "then": { + "special": { + "text": "新增個人檔敘述" + } + } + } + } + }, "translation-completeness": { "render": "{_theme} 的 {_language} 翻譯目前是 {_translation_percentage}%:{_translation_total} 中的 {_translation_translated_count} 已經翻譯了" }, diff --git a/public/css/index-tailwind-output.css b/public/css/index-tailwind-output.css index 320d427ce..df125fb0e 100644 --- a/public/css/index-tailwind-output.css +++ b/public/css/index-tailwind-output.css @@ -711,6 +711,14 @@ video { top: 2.5rem; } +.top-2 { + top: 0.5rem; +} + +.right-2 { + right: 0.5rem; +} + .left-1\/4 { left: 25%; } @@ -779,10 +787,6 @@ video { top: 0.25rem; } -.top-2 { - top: 0.5rem; -} - .top-\[calc\(100\%\+1rem\)\] { top: calc(100% + 1rem); } @@ -1221,14 +1225,14 @@ video { height: 6rem; } -.h-full { - height: 100%; -} - .h-screen { height: 100vh; } +.h-full { + height: 100%; +} + .h-fit { height: -webkit-fit-content; height: -moz-fit-content; @@ -1280,6 +1284,10 @@ video { height: 2.75rem; } +.h-2\/3 { + height: 66.666667%; +} + .h-5 { height: 1.25rem; } @@ -2043,10 +2051,6 @@ video { column-gap: 0px; } -.gap-x-4 { - column-gap: 1rem; -} - .gap-y-8 { row-gap: 2rem; } @@ -4627,6 +4631,17 @@ button.as-link { padding: 0; } +button.unstyled { + background-color: unset; + display: inline-flex; + justify-content: start; + border: none; + border-radius: 0; + box-shadow: none; + margin: 0; + padding: 0; +} + /******* Other input elements ******/ .hover-alert:hover { @@ -5284,6 +5299,11 @@ svg.apply-fill path { border-color: rgb(209 213 219 / var(--tw-border-opacity)); } +.hover\:bg-stone-200:hover { + --tw-bg-opacity: 1; + background-color: rgb(231 229 228 / var(--tw-bg-opacity)); +} + .hover\:bg-indigo-200:hover { --tw-bg-opacity: 1; background-color: rgb(199 210 254 / var(--tw-bg-opacity)); diff --git a/src/Logic/FeatureSource/TiledFeatureSource/SummaryTileSource.ts b/src/Logic/FeatureSource/TiledFeatureSource/SummaryTileSource.ts index 74ffa9745..c0dcb8bb7 100644 --- a/src/Logic/FeatureSource/TiledFeatureSource/SummaryTileSource.ts +++ b/src/Logic/FeatureSource/TiledFeatureSource/SummaryTileSource.ts @@ -28,10 +28,10 @@ export class SummaryTileSourceRewriter implements FeatureSource { !l.layerDef.id.startsWith("note_import") ) this._summarySource = summarySource - filteredLayers.forEach((v, k) => { - v.isDisplayed.addCallback((_) => this.update()) + filteredLayers.forEach((v) => { + v.isDisplayed.addCallback(() => this.update()) }) - this._summarySource.features.addCallbackAndRunD((_) => this.update()) + this._summarySource.features.addCallbackAndRunD(() => this.update()) } private update() { @@ -78,6 +78,9 @@ export class SummaryTileSource extends DynamicTileSource { isActive?: Store } ) { + if(layers.length === 0){ + return + } const layersSummed = layers.join("+") const zDiff = 2 super( diff --git a/src/Logic/Geocoding/CombinedSearcher.ts b/src/Logic/Geocoding/CombinedSearcher.ts new file mode 100644 index 000000000..68e0170ab --- /dev/null +++ b/src/Logic/Geocoding/CombinedSearcher.ts @@ -0,0 +1,21 @@ +import GeocodingProvider, { GeoCodeResult, GeocodingOptions } from "./GeocodingProvider" + +export default class CombinedSearcher implements GeocodingProvider { + private _providers: ReadonlyArray + private _providersWithSuggest: ReadonlyArray + + constructor(...providers: ReadonlyArray) { + this._providers = providers + this._providersWithSuggest = providers.filter(pr => pr.suggest !== undefined) + } + + async search(query: string, options?: GeocodingOptions): Promise { + const results = await Promise.all(this._providers.map(pr => pr.search(query, options))) + return results.flatMap(x => x) + } + + async suggest(query: string, options?: GeocodingOptions): Promise { + const results = await Promise.all(this._providersWithSuggest.map(pr => pr.suggest(query, options))) + return results.flatMap(x => x) + } +} diff --git a/src/Logic/Geocoding/CoordinateSearch.ts b/src/Logic/Geocoding/CoordinateSearch.ts new file mode 100644 index 000000000..6a0a789fe --- /dev/null +++ b/src/Logic/Geocoding/CoordinateSearch.ts @@ -0,0 +1,67 @@ +import GeocodingProvider, { GeoCodeResult, GeocodingOptions } from "./GeocodingProvider" +import { Utils } from "../../Utils" + +/** + * A simple search-class which interprets possible locations + */ +export default class CoordinateSearch implements GeocodingProvider { + private static readonly latLonRegexes: ReadonlyArray = [ + /([0-9]+\.[0-9]+)[ ,;]+([0-9]+\.[0-9]+)/, + /lat:?[ ]*([0-9]+\.[0-9]+)[ ,;]+lon:?[ ]*([0-9]+\.[0-9]+)/, + /https:\/\/www.openstreetmap.org\/.*#map=[0-9]+\/([0-9]+\.[0-9]+)\/([0-9]+\.[0-9]+)/, + /https:\/\/www.google.com\/maps\/@([0-9]+.[0-9]+),([0-9]+.[0-9]+).*/ + ] + + private static readonly lonLatRegexes: ReadonlyArray = [ + /([0-9]+\.[0-9]+)[ ,;]+([0-9]+\.[0-9]+)/ + ] + + /** + * + * @param query + * @param options + * + * const ls = new CoordinateSearch() + * const results = await ls.search("https://www.openstreetmap.org/search?query=Brugge#map=11/51.2611/3.2217") + * results.length // => 1 + * results[0] // => {lat: 51.2611, lon: 3.2217, display_name: "lon: 3.2217, lat: 51.2611"} + * + * const ls = new CoordinateSearch() + * const results = await ls.search("https://www.openstreetmap.org/#map=11/51.2611/3.2217") + * results.length // => 1 + * results[0] // => {lat: 51.2611, lon: 3.2217, display_name: "lon: 3.2217, lat: 51.2611"} + * + * const ls = new CoordinateSearch() + * const results = await ls.search("51.2611 3.2217") + * results.length // => 2 + * results[0] // => {lat: 51.2611, lon: 3.2217, display_name: "lon: 3.2217, lat: 51.2611"} + * results[1] // => {lon: 51.2611, lat: 3.2217, display_name: "lon: 51.2611, lat: 3.2217"} + * + */ + async search(query: string, options?: GeocodingOptions): Promise { + + const matches = Utils.NoNull(CoordinateSearch.latLonRegexes.map(r => query.match(r))).map(m => { + lat: Number(m[1]), + lon: Number(m[2]), + display_name: "lon: " + m[2] + ", lat: " + m[1], + source: "coordinateSearch" + }) + + + + const matchesLonLat = Utils.NoNull(CoordinateSearch.lonLatRegexes.map(r => query.match(r))) + .map(m => { + lat: Number(m[2]), + lon: Number(m[1]), + display_name: "lon: " + m[1] + ", lat: " + m[2], + source: "coordinateSearch" + }) + + return matches.concat(matchesLonLat) + } + + suggest(query: string, options?: GeocodingOptions): Promise { + return this.search(query, options) + } + +} diff --git a/src/Logic/Geocoding/GeocodingProvider.ts b/src/Logic/Geocoding/GeocodingProvider.ts new file mode 100644 index 000000000..3d8ed7426 --- /dev/null +++ b/src/Logic/Geocoding/GeocodingProvider.ts @@ -0,0 +1,43 @@ +import { BBox } from "../BBox" +import { Feature, FeatureCollection } from "geojson" + +export type GeoCodeResult = { + display_name: string + feature?: Feature, + lat: number + lon: number + /** + * Format: + * [lat, lat, lon, lon] + */ + boundingbox?: number[] + osm_type?: "node" | "way" | "relation" + osm_id?: string +} + +export interface GeocodingOptions { + bbox?: BBox, + limit?: number +} + + +export default interface GeocodingProvider { + + + search(query: string, options?: GeocodingOptions): Promise + + /** + * @param query + * @param options + */ + suggest?(query: string, options?: GeocodingOptions): Promise +} + +export interface ReverseGeocodingProvider { + reverseSearch( + coordinate: { lon: number; lat: number }, + zoom: number, + language?: string + ): Promise ; +} + diff --git a/src/Logic/Geocoding/LocalElementSearch.ts b/src/Logic/Geocoding/LocalElementSearch.ts new file mode 100644 index 000000000..a39920ed9 --- /dev/null +++ b/src/Logic/Geocoding/LocalElementSearch.ts @@ -0,0 +1,86 @@ +import GeocodingProvider, { GeoCodeResult, GeocodingOptions } from "./GeocodingProvider" +import ThemeViewState from "../../Models/ThemeViewState" +import { Utils } from "../../Utils" +import { Feature } from "geojson" +import { GeoOperations } from "../GeoOperations" + +export default class LocalElementSearch implements GeocodingProvider { + private readonly _state: ThemeViewState + + constructor(state: ThemeViewState) { + this._state = state + + } + + async search(query: string, options?: GeocodingOptions): Promise { + return this.searchEntries(query, options, false) + } + + searchEntries(query: string, options?: GeocodingOptions, matchStart?: boolean): GeoCodeResult[] { + if (query.length < 3) { + return [] + } + const center: { lon: number; lat: number } = this._state.mapProperties.location.data + const centerPoint: [number, number] = [center.lon, center.lat] + let results: { + feature: Feature, + /** + * Lon, lat + */ + center: [number, number], + levehnsteinD: number, + physicalDistance: number, + searchTerms: string[] + }[] = [] + const properties = this._state.perLayer + query = Utils.simplifyStringForSearch(query) + for (const [_, geoIndexedStore] of properties) { + for (const feature of geoIndexedStore.features.data) { + const props = feature.properties + const searchTerms: string[] = Utils.NoNull([props.name, props.alt_name, props.local_name, + (props["addr:street"] && props["addr:number"]) ? + props["addr:street"] + props["addr:number"] : undefined]) + + + const levehnsteinD = Math.min(...searchTerms.flatMap(entry => entry.split(/ /)).map(entry => { + let simplified = Utils.simplifyStringForSearch(entry) + if (matchStart) { + simplified = simplified.slice(0, query.length) + } + return Utils.levenshteinDistance(query, simplified) + })) + const center = GeoOperations.centerpointCoordinates(feature) + if (levehnsteinD <= 2) { + results.push({ + feature, + center, + physicalDistance: GeoOperations.distanceBetween(centerPoint, center), + levehnsteinD, + searchTerms + }) + } + } + } + results.sort((a, b) => (a.physicalDistance + a.levehnsteinD * 25) - (b.physicalDistance + b.levehnsteinD * 25)) + if (options?.limit) { + results = results.slice(0, options.limit) + } + return results.map(entry => { + const id = entry.feature.properties.id.split("/") + return { + lon: entry.center[0], + lat: entry.center[1], + osm_type: id[0], + osm_id: id[1], + display_name: entry.searchTerms[0], + source: "localElementSearch", + feature: entry.feature + } + }) + } + + async suggest(query: string, options?: GeocodingOptions): Promise { + return this.searchEntries(query, options, true) + } + +} diff --git a/src/Logic/Geocoding/NominatimGeocoding.ts b/src/Logic/Geocoding/NominatimGeocoding.ts new file mode 100644 index 000000000..f693a0f4e --- /dev/null +++ b/src/Logic/Geocoding/NominatimGeocoding.ts @@ -0,0 +1,39 @@ +import { Utils } from "../../Utils" +import { BBox } from "../BBox" +import Constants from "../../Models/Constants" +import { FeatureCollection } from "geojson" +import Locale from "../../UI/i18n/Locale" +import GeocodingProvider, { GeoCodeResult, ReverseGeocodingProvider } from "./GeocodingProvider" + +export class NominatimGeocoding implements GeocodingProvider, ReverseGeocodingProvider { + + private readonly _host ; + + constructor(host: string = Constants.nominatimEndpoint) { + this._host = host + } + + public async search(query: string, options?: { bbox?: BBox; limit?: number }): Promise { + const b = options?.bbox ?? BBox.global + const url = `${ + this._host + }search?format=json&limit=${options?.limit ?? 1}&viewbox=${b.getEast()},${b.getNorth()},${b.getWest()},${b.getSouth()}&accept-language=${ + Locale.language.data + }&q=${query}` + return await Utils.downloadJson(url) + } + + + async reverseSearch( + coordinate: { lon: number; lat: number }, + zoom: number = 17, + language?: string + ): Promise { + // https://nominatim.org/release-docs/develop/api/Reverse/ + // IF the zoom is low, it'll only return a country instead of an address + const url = `${this._host}reverse?format=geojson&lat=${coordinate.lat}&lon=${ + coordinate.lon + }&zoom=${Math.ceil(zoom) + 1}&accept-language=${language}` + return Utils.downloadJson(url) + } +} diff --git a/src/Logic/Osm/Geocoding.ts b/src/Logic/Osm/Geocoding.ts deleted file mode 100644 index 075a3745d..000000000 --- a/src/Logic/Osm/Geocoding.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { Utils } from "../../Utils" -import { BBox } from "../BBox" -import Constants from "../../Models/Constants" -import { FeatureCollection } from "geojson" -import Locale from "../../UI/i18n/Locale" - -export interface GeoCodeResult { - display_name: string - lat: number - lon: number - /** - * Format: - * [lat, lat, lon, lon] - */ - boundingbox: number[] - osm_type: "node" | "way" | "relation" - osm_id: string -} - -export class Geocoding { - public static readonly host = Constants.nominatimEndpoint - - static async Search(query: string, bbox: BBox): Promise { - const b = bbox ?? BBox.global - const url = `${ - Geocoding.host - }search?format=json&limit=1&viewbox=${b.getEast()},${b.getNorth()},${b.getWest()},${b.getSouth()}&accept-language=${ - Locale.language.data - }&q=${query}` - return Utils.downloadJson(url) - } - - static async reverse( - coordinate: { lon: number; lat: number }, - zoom: number = 17, - language?: string - ): Promise { - // https://nominatim.org/release-docs/develop/api/Reverse/ - // IF the zoom is low, it'll only return a country instead of an address - const url = `${Geocoding.host}reverse?format=geojson&lat=${coordinate.lat}&lon=${ - coordinate.lon - }&zoom=${Math.ceil(zoom) + 1}&accept-language=${language}` - return Utils.downloadJson(url) - } -} diff --git a/src/Models/MapProperties.ts b/src/Models/MapProperties.ts index eb86c31d6..39e8b6cd2 100644 --- a/src/Models/MapProperties.ts +++ b/src/Models/MapProperties.ts @@ -29,6 +29,9 @@ export interface MapProperties { * @param f */ onKeyNavigationEvent(f: (event: KeyNavigationEvent) => void | boolean): () => void + + flyTo(lon: number, lat: number, zoom: number): void + } export interface ExportableMap { diff --git a/src/Models/ThemeConfig/Conversion/Validation.ts b/src/Models/ThemeConfig/Conversion/Validation.ts index f0f860c51..daec62a41 100644 --- a/src/Models/ThemeConfig/Conversion/Validation.ts +++ b/src/Models/ThemeConfig/Conversion/Validation.ts @@ -1701,7 +1701,8 @@ export class ValidateLayer extends Conversion< try { layerConfig = new LayerConfig(json, "validation", true) } catch (e) { - context.err("Could not parse layer due to:" + e) + console.error("Could not parse layer due to", e) + context.err("Could not parse layer due to: " + e) return undefined } diff --git a/src/Models/ThemeConfig/LayerConfig.ts b/src/Models/ThemeConfig/LayerConfig.ts index 4f132bf9f..730417007 100644 --- a/src/Models/ThemeConfig/LayerConfig.ts +++ b/src/Models/ThemeConfig/LayerConfig.ts @@ -19,11 +19,10 @@ import { Utils } from "../../Utils" import { TagsFilter } from "../../Logic/Tags/TagsFilter" import FilterConfigJson from "./Json/FilterConfigJson" import { Overpass } from "../../Logic/Osm/Overpass" -import { ImmutableStore } from "../../Logic/UIEventSource" -import { OsmTags } from "../OsmFeature" import Constants from "../Constants" import { QuestionableTagRenderingConfigJson } from "./Json/QuestionableTagRenderingConfigJson" import MarkdownUtils from "../../Utils/MarkdownUtils" +import Combine from "../../UI/Base/Combine" export default class LayerConfig extends WithContextLoader { public static readonly syncSelectionAllowed = ["no", "local", "theme-only", "global"] as const @@ -344,15 +343,17 @@ export default class LayerConfig extends WithContextLoader { this.popupInFloatover = json.popupInFloatover ?? false } - public defaultIcon(): BaseUIElement | undefined { + public defaultIcon(tags?: Record): BaseUIElement | undefined { if (this.mapRendering === undefined || this.mapRendering === null) { return undefined } - const mapRendering = this.mapRendering.filter((r) => r.location.has("point"))[0] - if (mapRendering === undefined) { + const mapRenderings = this.mapRendering.filter((r) => r.location.has("point")) + if (mapRenderings.length === 0) { return undefined } - return mapRendering.GetBaseIcon(this.GetBaseTags()) + return new Combine(mapRenderings.map( + mr => mr.GetBaseIcon(tags ?? this.GetBaseTags()).SetClass("absolute left-0 top-0 w-full h-full")) + ).SetClass("relative block w-full h-full") } public GetBaseTags(): Record { diff --git a/src/Models/ThemeViewState.ts b/src/Models/ThemeViewState.ts index b0f63f503..617d09498 100644 --- a/src/Models/ThemeViewState.ts +++ b/src/Models/ThemeViewState.ts @@ -74,6 +74,11 @@ import Locale from "../UI/i18n/Locale" import Hash from "../Logic/Web/Hash" import { GeoOperations } from "../Logic/GeoOperations" import { CombinedFetcher } from "../Logic/Web/NearbyImagesSearch" +import GeocodingProvider from "../Logic/Geocoding/GeocodingProvider" +import CombinedSearcher from "../Logic/Geocoding/CombinedSearcher" +import { NominatimGeocoding } from "../Logic/Geocoding/NominatimGeocoding" +import CoordinateSearch from "../Logic/Geocoding/CoordinateSearch" +import LocalElementSearch from "../Logic/Geocoding/LocalElementSearch" /** * @@ -153,7 +158,8 @@ export default class ThemeViewState implements SpecialVisualizationState { public readonly visualFeedback: UIEventSource = new UIEventSource(false) public readonly toCacheSavers: ReadonlyMap - public readonly nearbyImageSearcher + public readonly nearbyImageSearcher: CombinedFetcher + public readonly geosearch: GeocodingProvider constructor(layout: LayoutConfig, mvtAvailableLayers: Set) { Utils.initDomPurify() @@ -379,6 +385,14 @@ export default class ThemeViewState implements SpecialVisualizationState { new LayerConfig(summaryLayer, "summaryLayer", true) ) this.toCacheSavers = layout.enableCache ? this.initSaveToLocalStorage() : undefined + + this.geosearch = new CombinedSearcher( + new NominatimGeocoding(), + new CoordinateSearch(), + new LocalElementSearch(this) + ) + + this.initActors() this.drawSpecialLayers() this.initHotkeys() diff --git a/src/UI/BigComponents/Geosearch.svelte b/src/UI/BigComponents/Geosearch.svelte index 87369e37a..308743ac0 100644 --- a/src/UI/BigComponents/Geosearch.svelte +++ b/src/UI/BigComponents/Geosearch.svelte @@ -4,7 +4,6 @@ import Translations from "../i18n/Translations" import Loading from "../Base/Loading.svelte" import Hotkeys from "../Base/Hotkeys" - import { Geocoding } from "../../Logic/Osm/Geocoding" import { BBox } from "../../Logic/BBox" import { GeoIndexedStoreForLayer } from "../../Logic/FeatureSource/Actors/GeoIndexedStore" import { createEventDispatcher, onDestroy } from "svelte" @@ -12,6 +11,12 @@ import { SearchIcon } from "@rgossiaux/svelte-heroicons/solid" import { ariaLabel } from "../../Utils/ariaLabel" import { GeoLocationState } from "../../Logic/State/GeoLocationState" + import { NominatimGeocoding } from "../../Logic/Geocoding/NominatimGeocoding" + import type GeocodingProvider from "../../Logic/Geocoding/GeocodingProvider" + import type { GeoCodeResult } from "../../Logic/Geocoding/GeocodingProvider" + + import SearchResults from "./SearchResults.svelte" + import type { SpecialVisualizationState } from "../SpecialVisualization" export let perLayer: ReadonlyMap | undefined = undefined export let bounds: UIEventSource @@ -19,6 +24,8 @@ export let geolocationState: GeoLocationState | undefined = undefined export let clearAfterView: boolean = true + export let searcher : GeocodingProvider = new NominatimGeocoding() + export let state : SpecialVisualizationState let searchContents: string = "" export let triggerSearch: UIEventSource = new UIEventSource(undefined) onDestroy( @@ -54,6 +61,7 @@ } } + async function performSearch() { try { isRunning = true @@ -64,7 +72,8 @@ if (searchContents === "") { return } - const result = await Geocoding.Search(searchContents, bounds.data) + const result = await searcher.search(searchContents, { bbox: bounds.data, limit: 10 }) + console.log("Results are", result) if (result.length == 0) { feedback = Translations.t.general.search.nothing.txt focusOnSearch() @@ -104,6 +113,16 @@ isRunning = false } } + + let suggestions: GeoCodeResult[] = [] + + async function updateSuggestions(search){ + + suggestions = await searcher.suggest(search, {limit: 5}) + } + + $: updateSuggestions(searchContents) +
@@ -133,3 +152,7 @@
+ +
+ +
diff --git a/src/UI/BigComponents/MoreScreen.ts b/src/UI/BigComponents/MoreScreen.ts index 5531ee056..9ae7510eb 100644 --- a/src/UI/BigComponents/MoreScreen.ts +++ b/src/UI/BigComponents/MoreScreen.ts @@ -60,7 +60,7 @@ export default class MoreScreen { if (search === undefined) { return true } - search = Utils.RemoveDiacritics(search.toLocaleLowerCase()) + search = Utils.RemoveDiacritics(search.toLocaleLowerCase()) // See #1729 if (search.length > 3 && layout.id.toLowerCase().indexOf(search) >= 0) { return true } diff --git a/src/UI/BigComponents/ReverseGeocoding.svelte b/src/UI/BigComponents/ReverseGeocoding.svelte index 2674fa2be..90e91b69f 100644 --- a/src/UI/BigComponents/ReverseGeocoding.svelte +++ b/src/UI/BigComponents/ReverseGeocoding.svelte @@ -3,7 +3,7 @@ * Shows the current address when shaken **/ import Motion from "../../Sensors/Motion" - import { Geocoding } from "../../Logic/Osm/Geocoding" + import { NominatimGeocoding } from "../../Logic/Geocoding/NominatimGeocoding" import Hotkeys from "../Base/Hotkeys" import Translations from "../i18n/Translations" import Locale from "../i18n/Locale" @@ -15,9 +15,11 @@ let lastDisplayed: Date = undefined let currentLocation: string = undefined + let geocoder = new NominatimGeocoding() + async function displayLocation() { lastDisplayed = new Date() - let result = await Geocoding.reverse( + let result = await geocoder.reverseSearch( mapProperties.location.data, mapProperties.zoom.data, Locale.language.data diff --git a/src/UI/BigComponents/SearchResult.svelte b/src/UI/BigComponents/SearchResult.svelte new file mode 100644 index 000000000..8cd67cd5c --- /dev/null +++ b/src/UI/BigComponents/SearchResult.svelte @@ -0,0 +1,46 @@ + + diff --git a/src/UI/BigComponents/SearchResults.svelte b/src/UI/BigComponents/SearchResults.svelte new file mode 100644 index 000000000..124e1545b --- /dev/null +++ b/src/UI/BigComponents/SearchResults.svelte @@ -0,0 +1,27 @@ + + +{#if results.length > 0} +
+ +
+ {#each results as entry (entry)} + close()} {entry} {state} /> + {/each} +
+
close()}> + +
+
+{/if} diff --git a/src/UI/BigComponents/ThemeIntroPanel.svelte b/src/UI/BigComponents/ThemeIntroPanel.svelte index d0c88a4a6..12aa75195 100644 --- a/src/UI/BigComponents/ThemeIntroPanel.svelte +++ b/src/UI/BigComponents/ThemeIntroPanel.svelte @@ -100,6 +100,8 @@ {selectedElement} {triggerSearch} geolocationState={state.geolocation.geolocationState} + searcher={state.geosearch} + {state} />