Search: close dialog when appropriate, move special layer logic to themeViewState

This commit is contained in:
Pieter Vander Vennet 2024-09-12 16:14:57 +02:00
parent 902a479e3b
commit 05e5a63a12
11 changed files with 165 additions and 81 deletions

View file

@ -11657,6 +11657,48 @@
"question": "Show the raw OpenStreetMap-tags?",
"questionHint": "<b>Tags</b> 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": "<h3>Editing settings</h3>"
},
"title-id": {
"render": "<h3>Mangrove ID management</h3>"
},
"title-map": {
"render": "<h3>Configure map</h3>"
},
"title-privacy-legal": {
"render": "<h3>Privacy and legal</h3>"
},
"translation-completeness": {
"mappings": {
"0": {

View file

@ -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.

View file

@ -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
}

View file

@ -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<string>
constructor(state: SpecialVisualizationState) {
this._state = state
this._layerWhitelist = new Set(state.layout.layers.map(l => l.id).filter(id => Constants.added_by_default.indexOf(<any> 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(<any> id) < 0))
}
static scoreLayers(query: string, layerWhitelist?: Set<string>): Record<string, number> {
@ -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]
})
}

View file

@ -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<string, Record<string, string[]>>
} = themeOverview
} = <any> themeOverview
public static readonly officialThemesById: Map<string, MinimalLayoutInformation> = new Map<string, MinimalLayoutInformation>()
static {
for (const th of ThemeSearch.officialThemes.themes ?? []) {
@ -31,15 +31,13 @@ export default class ThemeSearch {
}
private readonly _state: SpecialVisualizationState
private readonly _knownHiddenThemes: Store<Set<string>>
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)
}

View file

@ -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<boolean>
public readonly suggestionsSearchRunning: Store<boolean>
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)
}
}

View file

@ -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

View file

@ -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(<LayerConfigJson>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 = <GeocodeResult> {
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 = <GeocodeResult>{
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<string, string>){
public getMatchingLayer(properties: Record<string, string>) {
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")

View file

@ -36,6 +36,7 @@
activeFilter.control.setData(undefined)
}
loading = false
state.searchState.closeIfFullscreen()
})
}
</script>

View file

@ -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 = <LayerConfig> entry
let asFilter = <FilterSearchResult> entry
let asLayer = <LayerConfig>entry
let asFilter = <FilterSearchResult>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()
})
}
</script>
<button on:click={() => apply()}>
<button on:click={() => apply()} class:disabled={loading}>
{#if loading}
<Loading />
{/if}
<div class="flex flex-col items-start">
<div class="flex items-center gap-x-1">
{#if isLayer}

View file

@ -48,7 +48,7 @@
state.selectedElement.set(entry.feature)
}
state.userRelatedState.recentlyVisitedSearch.add(entry)
dispatch("select")
state.searchState.closeIfFullscreen()
}
</script>