From 05e5a63a125c529a26bd1bb0d512c3a47e966abe Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Thu, 12 Sep 2024 16:14:57 +0200 Subject: [PATCH] Search: close dialog when appropriate, move special layer logic to themeViewState --- langs/layers/en.json | 42 ++++++++++++ src/Logic/Osm/OsmPreferences.ts | 7 +- src/Logic/Search/FilterSearch.ts | 6 +- src/Logic/Search/LayerSearch.ts | 12 ++-- src/Logic/Search/ThemeSearch.ts | 14 ++-- src/Logic/State/SearchState.ts | 35 ++++++---- src/Models/Constants.ts | 2 +- src/Models/ThemeViewState.ts | 106 +++++++++++++++++------------ src/UI/Search/ActiveFilters.svelte | 1 + src/UI/Search/FilterResult.svelte | 19 ++++-- src/UI/Search/GeocodeResult.svelte | 2 +- 11 files changed, 165 insertions(+), 81 deletions(-) diff --git a/langs/layers/en.json b/langs/layers/en.json index ef4a219f6..ea04a61c7 100644 --- a/langs/layers/en.json +++ b/langs/layers/en.json @@ -11657,6 +11657,48 @@ "question": "Show the raw OpenStreetMap-tags?", "questionHint": "Tags are attributes that every element has. This is the technical data that is stored in the database. You don't need this information to edit with MapComplete, but advanced users might want to use this as reference." }, + "sync-visited-locations": { + "mappings": { + "0": { + "then": "Save the locations you search for and inspect and sync them via openstreetmap.org. OpenStreetMap and all apps you use can see this history" + }, + "1": { + "then": "Save the locations you search for and inspect on my device" + }, + "2": { + "then": "Don't save the locations you search for and inspect " + } + }, + "question": "Should the locations you search for and inspect be remembered?", + "questionHint": "Those locations will be offered in the search menu" + }, + "sync-visited-themes": { + "mappings": { + "0": { + "then": "Save the visited thematic maps and sync them via openstreetmap.org. OpenStreetMap and all apps you use can see this history" + }, + "1": { + "then": "Save the visited thematic maps on my device" + }, + "2": { + "then": "Don't save visited thematic maps" + } + }, + "question": "Should the thematic maps you visit be saved?", + "questionHint": "If you visit a map about a certain topic, MapComplete can remember this and offer this as suggestion." + }, + "title-editing": { + "render": "

Editing settings

" + }, + "title-id": { + "render": "

Mangrove ID management

" + }, + "title-map": { + "render": "

Configure map

" + }, + "title-privacy-legal": { + "render": "

Privacy and legal

" + }, "translation-completeness": { "mappings": { "0": { diff --git a/src/Logic/Osm/OsmPreferences.ts b/src/Logic/Osm/OsmPreferences.ts index 9e2fce8c3..d076e09ee 100644 --- a/src/Logic/Osm/OsmPreferences.ts +++ b/src/Logic/Osm/OsmPreferences.ts @@ -70,7 +70,11 @@ export class OsmPreferences { this.setPreferencesAll(key, initialValue) } pref.addCallback(v => { - length.set(Math.ceil(v.length / maxLength)) + if(v === null || v === undefined || v === ""){ + length.set(null) + return + } + length.set(Math.ceil((v?.length ?? 1) / maxLength)) let i = 0 while (v.length > 0) { this.UploadPreference(key + "-" + i, v.substring(0, maxLength)) @@ -97,6 +101,7 @@ export class OsmPreferences { } } + /** * OSM preferences can be at most 255 chars. * This method chains multiple together. diff --git a/src/Logic/Search/FilterSearch.ts b/src/Logic/Search/FilterSearch.ts index 208a18f53..d8424fc68 100644 --- a/src/Logic/Search/FilterSearch.ts +++ b/src/Logic/Search/FilterSearch.ts @@ -4,6 +4,8 @@ import Locale from "../../UI/i18n/Locale" import Constants from "../../Models/Constants" import FilterConfig, { FilterConfigOption } from "../../Models/ThemeConfig/FilterConfig" import LayerConfig from "../../Models/ThemeConfig/LayerConfig" +import LayerState from "../State/LayerState" +import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" export type FilterSearchResult = { option: FilterConfigOption, filter: FilterConfig, layer: LayerConfig, index: number } @@ -12,9 +14,9 @@ export type FilterSearchResult = { option: FilterConfigOption, filter: FilterCon * Searches matching filters */ export default class FilterSearch { - private readonly _state: SpecialVisualizationState + private readonly _state: {layerState: LayerState, layout: LayoutConfig} - constructor(state: SpecialVisualizationState) { + constructor(state: {layerState: LayerState, layout: LayoutConfig}) { this._state = state } diff --git a/src/Logic/Search/LayerSearch.ts b/src/Logic/Search/LayerSearch.ts index f2e2f3b6e..f72ef7abb 100644 --- a/src/Logic/Search/LayerSearch.ts +++ b/src/Logic/Search/LayerSearch.ts @@ -1,16 +1,16 @@ -import { SpecialVisualizationState } from "../../UI/SpecialVisualization" import Constants from "../../Models/Constants" import SearchUtils from "./SearchUtils" import ThemeSearch from "./ThemeSearch" import LayerConfig from "../../Models/ThemeConfig/LayerConfig" +import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" export default class LayerSearch { - private readonly _state: SpecialVisualizationState + private readonly _layout: LayoutConfig private readonly _layerWhitelist : Set - constructor(state: SpecialVisualizationState) { - this._state = state - this._layerWhitelist = new Set(state.layout.layers.map(l => l.id).filter(id => Constants.added_by_default.indexOf( id) < 0)) + constructor(layout: LayoutConfig) { + this._layout = layout + this._layerWhitelist = new Set(layout.layers.map(l => l.id).filter(id => Constants.added_by_default.indexOf( id) < 0)) } static scoreLayers(query: string, layerWhitelist?: Set): Record { @@ -35,7 +35,7 @@ export default class LayerSearch { const asList:({layer: LayerConfig, score:number})[] = [] for (const layer in scores) { asList.push({ - layer: this._state.layout.getLayer(layer), + layer: this._layout.getLayer(layer), score: scores[layer] }) } diff --git a/src/Logic/Search/ThemeSearch.ts b/src/Logic/Search/ThemeSearch.ts index a9c5ea40f..07d2d8bb0 100644 --- a/src/Logic/Search/ThemeSearch.ts +++ b/src/Logic/Search/ThemeSearch.ts @@ -1,5 +1,4 @@ -import { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig" -import { SpecialVisualizationState } from "../../UI/SpecialVisualization" +import LayoutConfig, { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig" import { Store } from "../UIEventSource" import UserRelatedState from "../State/UserRelatedState" import { Utils } from "../../Utils" @@ -7,6 +6,7 @@ import Locale from "../../UI/i18n/Locale" import themeOverview from "../../assets/generated/theme_overview.json" import LayerSearch from "./LayerSearch" import SearchUtils from "./SearchUtils" +import { OsmConnection } from "../Osm/OsmConnection" type ThemeSearchScore = { @@ -22,7 +22,7 @@ export default class ThemeSearch { public static readonly officialThemes: { themes: MinimalLayoutInformation[], layers: Record> - } = themeOverview + } = themeOverview public static readonly officialThemesById: Map = new Map() static { for (const th of ThemeSearch.officialThemes.themes ?? []) { @@ -31,15 +31,13 @@ export default class ThemeSearch { } - private readonly _state: SpecialVisualizationState private readonly _knownHiddenThemes: Store> private readonly _layersToIgnore: string[] private readonly _otherThemes: MinimalLayoutInformation[] - constructor(state: SpecialVisualizationState) { - this._state = state + constructor(state: {osmConnection: OsmConnection, layout: LayoutConfig}) { this._layersToIgnore = state.layout.layers.map(l => l.id) - this._knownHiddenThemes = UserRelatedState.initDiscoveredHiddenThemes(this._state.osmConnection).map(list => new Set(list)) + this._knownHiddenThemes = UserRelatedState.initDiscoveredHiddenThemes(state.osmConnection).map(list => new Set(list)) this._otherThemes = ThemeSearch.officialThemes.themes .filter(th => th.id !== state.layout.id) } @@ -144,7 +142,7 @@ export default class ThemeSearch { return scored } - public static sortedByLowest(search: string, themes: MinimalLayoutInformation[], ignoreLayers: string[] = [], maxDiff: number): MinimalLayoutInformation[] { + public static sortedByLowest(search: string, themes: MinimalLayoutInformation[], ignoreLayers: string[] = []): MinimalLayoutInformation[] { return this.sortedByLowestScores(search, themes, ignoreLayers) .map(th => th.theme) } diff --git a/src/Logic/State/SearchState.ts b/src/Logic/State/SearchState.ts index 62529bcf6..8672fda09 100644 --- a/src/Logic/State/SearchState.ts +++ b/src/Logic/State/SearchState.ts @@ -1,4 +1,4 @@ -import GeocodingProvider, { GeocodingUtils, type SearchResult } from "../Search/GeocodingProvider" +import GeocodingProvider, { type SearchResult } from "../Search/GeocodingProvider" import { ImmutableStore, Store, Stores, UIEventSource } from "../UIEventSource" import CombinedSearcher from "../Search/CombinedSearcher" import FilterSearch, { FilterSearchResult } from "../Search/FilterSearch" @@ -11,9 +11,10 @@ import ThemeViewState from "../../Models/ThemeViewState" import type { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig" import { Translation } from "../../UI/i18n/Translation" import GeocodingFeatureSource from "../Search/GeocodingFeatureSource" -import ShowDataLayer from "../../UI/Map/ShowDataLayer" import LayerSearch from "../Search/LayerSearch" import LayerConfig from "../../Models/ThemeConfig/LayerConfig" +import { FeatureSource } from "../FeatureSource/FeatureSource" +import { Feature } from "geojson" export default class SearchState { @@ -29,6 +30,7 @@ export default class SearchState { private readonly state: ThemeViewState public readonly showSearchDrawer: UIEventSource public readonly suggestionsSearchRunning: Store + public readonly locationResults: FeatureSource constructor(state: ThemeViewState) { this.state = state @@ -62,7 +64,7 @@ export default class SearchState { const themeSearch = new ThemeSearch(state) this.themeSuggestions = this.searchTerm.mapD(query => themeSearch.search(query, 3)) - const layerSearch = new LayerSearch(state) + const layerSearch = new LayerSearch(state.layout) this.layerSuggestions = this.searchTerm.mapD(query => layerSearch.search(query, 5)) const filterSearch = new FilterSearch(state) @@ -77,17 +79,7 @@ export default class SearchState { return !foundMatch }) }, [state.layerState.activeFilters]) - const geocodedFeatures = new GeocodingFeatureSource(this.suggestions.stabilized(250)) - state.featureProperties.trackFeatureSource(geocodedFeatures) - - new ShowDataLayer( - state.map, - { - layer: GeocodingUtils.searchLayer, - features: geocodedFeatures, - selectedElement: state.selectedElement, - }, - ) + this.locationResults =new GeocodingFeatureSource(this.suggestions.stabilized(250)) this.showSearchDrawer = new UIEventSource(false) @@ -131,4 +123,19 @@ export default class SearchState { } } + closeIfFullscreen() { + if(window.innerWidth < 640){ + this.showSearchDrawer.set(false) + } + } + + clickedOnMap(feature: Feature) { + const osmid = feature.properties.osm_id + const localElement = this.state.indexedFeatures.featuresById.data.get(osmid) + if(localElement){ + this.state.selectedElement.set(localElement) + return + } + console.log(">>>",feature) + } } diff --git a/src/Models/Constants.ts b/src/Models/Constants.ts index cbadf5f5a..98ebeeeda 100644 --- a/src/Models/Constants.ts +++ b/src/Models/Constants.ts @@ -26,6 +26,7 @@ export default class Constants { "last_click", "favourite", "summary", + "search" ] as const /** * Special layers which are not included in a theme by default @@ -39,7 +40,6 @@ export default class Constants { "usersettings", "icons", "filters", - "search" ] as const /** * Layer IDs of layers which have special properties through built-in hooks diff --git a/src/Models/ThemeViewState.ts b/src/Models/ThemeViewState.ts index 27d6da45c..070b5d713 100644 --- a/src/Models/ThemeViewState.ts +++ b/src/Models/ThemeViewState.ts @@ -58,7 +58,7 @@ import { GeolocationControlState } from "../UI/BigComponents/GeolocationControl" import Zoomcontrol from "../UI/Zoomcontrol" import { SummaryTileSource, - SummaryTileSourceRewriter, + SummaryTileSourceRewriter } from "../Logic/FeatureSource/TiledFeatureSource/SummaryTileSource" import summaryLayer from "../assets/generated/layers/summary.json" import last_click_layerconfig from "../assets/generated/layers/last_click.json" @@ -69,6 +69,7 @@ import { GeoOperations } from "../Logic/GeoOperations" import { CombinedFetcher } from "../Logic/Web/NearbyImagesSearch" import { GeocodeResult, GeocodingUtils } from "../Logic/Search/GeocodingProvider" import SearchState from "../Logic/State/SearchState" +import { ShowDataLayerOptions } from "../UI/Map/ShowDataLayerOptions" /** * @@ -175,7 +176,7 @@ export default class ThemeViewState implements SpecialVisualizationState { "oauth_token", undefined, "Used to complete the login" - ), + ) }) this.userRelatedState = new UserRelatedState( this.osmConnection, @@ -254,8 +255,8 @@ export default class ThemeViewState implements SpecialVisualizationState { bbox.asGeoJson({ zoom: this.mapProperties.zoom.data, ...this.mapProperties.location.data, - id: "current_view_" + currentViewIndex, - }), + id: "current_view_" + currentViewIndex + }) ] }) ) @@ -272,7 +273,7 @@ export default class ThemeViewState implements SpecialVisualizationState { featurePropertiesStore: this.featureProperties, osmConnection: this.osmConnection, historicalUserLocations: this.geolocation.historicalUserLocations, - featureSwitches: this.featureSwitches, + featureSwitches: this.featureSwitches }, layout?.isLeftRightSensitive() ?? false, (e, extraMsg) => this.reportError(e, extraMsg) @@ -300,7 +301,7 @@ export default class ThemeViewState implements SpecialVisualizationState { "leftover features, such as", features[0].properties ) - }, + } } ) this.perLayer = perLayer.perLayer @@ -356,7 +357,7 @@ export default class ThemeViewState implements SpecialVisualizationState { { currentZoom: this.mapProperties.zoom, layerState: this.layerState, - bounds: this.visualFeedbackViewportBounds, + bounds: this.visualFeedbackViewportBounds } ) this.hasDataInView = new NoElementsInViewDetector(this).hasFeatureInView @@ -453,7 +454,7 @@ export default class ThemeViewState implements SpecialVisualizationState { doShowLayer, metaTags: this.userRelatedState.preferencesAsTags, selectedElement: this.selectedElement, - fetchStore: (id) => this.featureProperties.getStore(id), + fetchStore: (id) => this.featureProperties.getStore(id) }) }) return filteringFeatureSource @@ -480,7 +481,7 @@ export default class ThemeViewState implements SpecialVisualizationState { doShowLayer: flayerGps.isDisplayed, layer: flayerGps.layerDef, metaTags: this.userRelatedState.preferencesAsTags, - selectedElement: this.selectedElement, + selectedElement: this.selectedElement }) } @@ -554,16 +555,16 @@ export default class ThemeViewState implements SpecialVisualizationState { this.previewedImage.setData(undefined) return } - if(this.selectedElement.data){ + if (this.selectedElement.data) { this.selectedElement.setData(undefined) return } - if (this.searchState.showSearchDrawer.data){ + if (this.searchState.showSearchDrawer.data) { this.searchState.showSearchDrawer.set(false) return } - if (this.guistate.closeAll()){ - return + if (this.guistate.closeAll()) { + return } Zoomcontrol.resetzoom() this.focusOnMap() @@ -573,10 +574,11 @@ export default class ThemeViewState implements SpecialVisualizationState { this.guistate.pageStates.favourites.set(true) }) + Hotkeys.RegisterHotkey( { nomod: " ", - onUp: true, + onUp: true }, docs.selectItem, () => { @@ -586,7 +588,7 @@ export default class ThemeViewState implements SpecialVisualizationState { if (this.guistate.isSomethingOpen() || this.previewedImage.data !== undefined) { return } - if(document.activeElement.tagName === "button" || document.activeElement.tagName === "input"){ + if (document.activeElement.tagName === "button" || document.activeElement.tagName === "input") { return } this.selectClosestAtCenter(0) @@ -605,7 +607,7 @@ export default class ThemeViewState implements SpecialVisualizationState { Hotkeys.RegisterHotkey( { nomod: "" + i, - onUp: true, + onUp: true }, doc, () => this.selectClosestAtCenter(i - 1) @@ -624,7 +626,7 @@ export default class ThemeViewState implements SpecialVisualizationState { } Hotkeys.RegisterHotkey( { - nomod: "b", + nomod: "b" }, docs.openLayersPanel, () => { @@ -635,7 +637,7 @@ export default class ThemeViewState implements SpecialVisualizationState { ) Hotkeys.RegisterHotkey( { - nomod: "s", + nomod: "s" }, Translations.t.hotkeyDocumentation.openFilterPanel, () => { @@ -713,7 +715,7 @@ export default class ThemeViewState implements SpecialVisualizationState { Hotkeys.RegisterHotkey( { - shift: "T", + shift: "T" }, Translations.t.hotkeyDocumentation.translationMode, () => { @@ -750,7 +752,7 @@ export default class ThemeViewState implements SpecialVisualizationState { this.mapProperties.zoom.map((z) => Math.max(Math.floor(z), 0)), this.mapProperties, { - isActive: this.mapProperties.zoom.map((z) => z < maxzoom), + isActive: this.mapProperties.zoom.map((z) => z < maxzoom) } ) @@ -783,6 +785,7 @@ export default class ThemeViewState implements SpecialVisualizationState { favourite: this.favourites, summary: this.featureSummary, last_click: this.lastClickObject, + search: this.searchState.locationResults } this.closestFeatures.registerSource(specialLayers.favourite, "favourite") @@ -832,20 +835,28 @@ export default class ThemeViewState implements SpecialVisualizationState { } this.featureProperties.trackFeatureSource(features) - new ShowDataLayer(this.map, { + const options: ShowDataLayerOptions & { layer: LayerConfig } = { features, doShowLayer: flayer.isDisplayed, layer: flayer.layerDef, metaTags: this.userRelatedState.preferencesAsTags, - selectedElement: this.selectedElement, - }) + selectedElement: this.selectedElement + + } + if (flayer.layerDef.id === "search") { + options.onClick = (feature) => { + this.searchState.clickedOnMap(feature) + } + delete options.selectedElement + } + new ShowDataLayer(this.map, options) }) const summaryLayerConfig = new LayerConfig(summaryLayer, "summaryLayer") new ShowDataLayer(this.map, { features: specialLayers.summary, layer: summaryLayerConfig, // doShowLayer: this.mapProperties.zoom.map((z) => z < maxzoom), - selectedElement: this.selectedElement, + selectedElement: this.selectedElement }) const lastClickLayerConfig = new LayerConfig( @@ -856,14 +867,14 @@ export default class ThemeViewState implements SpecialVisualizationState { lastClickLayerConfig.isShown === undefined ? specialLayers.last_click : specialLayers.last_click.features.mapD((fs) => - fs.filter((f) => { - const matches = lastClickLayerConfig.isShown.matchesProperties( - f.properties - ) - console.debug("LastClick ", f, "matches", matches) - return matches - }) - ) + fs.filter((f) => { + const matches = lastClickLayerConfig.isShown.matchesProperties( + f.properties + ) + console.debug("LastClick ", f, "matches", matches) + return matches + }) + ) new ShowDataLayer(this.map, { features: new StaticFeatureSource(lastClickFiltered), layer: lastClickLayerConfig, @@ -874,9 +885,9 @@ export default class ThemeViewState implements SpecialVisualizationState { } this.map.data.flyTo({ zoom: Constants.minZoomLevelToAddNewPoint, - center: GeoOperations.centerpointCoordinates(feature), + center: GeoOperations.centerpointCoordinates(feature) }) - }, + } }) } @@ -901,15 +912,24 @@ export default class ThemeViewState implements SpecialVisualizationState { }) }) + // Add the selected element to the recently visited history this.selectedElement.addCallbackD(selected => { const [osm_type, osm_id] = selected.properties.id.split("/") - const [lon, lat] = GeoOperations.centerpointCoordinates(selected) + const [lon, lat] = GeoOperations.centerpointCoordinates(selected) const layer = this.layout.getMatchingLayer(selected.properties) - const r = { + + const nameOptions = [ + selected?.properties?.name, + selected?.properties?.alt_name, selected?.properties?.local_name, + layer?.title.GetRenderValue(selected?.properties ?? {}).txt, + selected.properties.display_name, + selected.properties.id + ] + const r = { feature: selected, - display_name: selected.properties.name ?? selected.properties.alt_name ?? selected.properties.local_name ?? layer.title.GetRenderValue(selected.properties ?? {}).txt , + display_name: nameOptions.find(opt => opt !== undefined), osm_id, osm_type, - lon, lat, + lon, lat } this.userRelatedState.recentlyVisitedSearch.add(r) }) @@ -937,7 +957,7 @@ export default class ThemeViewState implements SpecialVisualizationState { /** * Searches the appropriate layer - will first try if a special layer matches; if not, a normal layer will be used by delegating to the layout */ - public getMatchingLayer(properties: Record){ + public getMatchingLayer(properties: Record) { const id = properties.id @@ -961,8 +981,8 @@ export default class ThemeViewState implements SpecialVisualizationState { return this.layout.getMatchingLayer(properties) } - public async reportError(message: string | Error | XMLHttpRequest, extramessage:string = "") { - if(Utils.runningFromConsole){ + public async reportError(message: string | Error | XMLHttpRequest, extramessage: string = "") { + if (Utils.runningFromConsole) { console.error("Got (in themeViewSTate.reportError):", message, extramessage) return } @@ -1014,8 +1034,8 @@ export default class ThemeViewState implements SpecialVisualizationState { userid: this.osmConnection.userDetails.data?.uid, pendingChanges: this.changes.pendingChanges.data, previousChanges: this.changes.allChanges.data, - changeRewrites: Utils.MapToObj(this.changes._changesetHandler._remappings), - }), + changeRewrites: Utils.MapToObj(this.changes._changesetHandler._remappings) + }) }) } catch (e) { console.error("Could not upload an error report") diff --git a/src/UI/Search/ActiveFilters.svelte b/src/UI/Search/ActiveFilters.svelte index bdbb20f7c..c2a8a47c3 100644 --- a/src/UI/Search/ActiveFilters.svelte +++ b/src/UI/Search/ActiveFilters.svelte @@ -36,6 +36,7 @@ activeFilter.control.setData(undefined) } loading = false + state.searchState.closeIfFullscreen() }) } diff --git a/src/UI/Search/FilterResult.svelte b/src/UI/Search/FilterResult.svelte index 18317f508..802993ea8 100644 --- a/src/UI/Search/FilterResult.svelte +++ b/src/UI/Search/FilterResult.svelte @@ -6,21 +6,30 @@ import ToSvelte from "../Base/ToSvelte.svelte" import type { FilterSearchResult } from "../../Logic/Search/FilterSearch" import LayerConfig from "../../Models/ThemeConfig/LayerConfig" + import Loading from "../Base/Loading.svelte" export let entry: FilterSearchResult | LayerConfig let isLayer = entry instanceof LayerConfig - let asLayer = entry - let asFilter = entry + let asLayer = entry + let asFilter = entry export let state: SpecialVisualizationState - let dispatch = createEventDispatcher<{ select }>() + let loading = false function apply() { + loading = true + console.log("Loading is now ", loading) + window.requestAnimationFrame(() => { state.searchState.apply(entry) - dispatch("select") + loading = false + state.searchState.closeIfFullscreen() + }) } -