From 56a23deb2db177f67f01932a06028c743cc58603 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Sun, 3 Dec 2023 20:03:47 +0100 Subject: [PATCH] Favourites: improve overview, update all features from OSM when loading --- public/css/index-tailwind-output.css | 34 +++- .../Actors/SelectedElementTagsUpdater.ts | 48 +++-- .../Sources/FavouritesFeatureSource.ts | 119 +++++++----- src/Logic/GeoOperations.ts | 170 ++++-------------- src/Logic/State/UserRelatedState.ts | 7 - src/Models/ThemeViewState.ts | 21 ++- src/UI/Base/ToSvelte.svelte | 2 +- src/UI/Favourites/FavouriteSummary.svelte | 9 +- src/UI/Favourites/Favourites.svelte | 45 ++++- src/index.css | 10 ++ 10 files changed, 231 insertions(+), 234 deletions(-) diff --git a/public/css/index-tailwind-output.css b/public/css/index-tailwind-output.css index 32fb419c0d..13d12ea52a 100644 --- a/public/css/index-tailwind-output.css +++ b/public/css/index-tailwind-output.css @@ -841,10 +841,6 @@ video { margin-right: 3rem; } -.mb-4 { - margin-bottom: 1rem; -} - .mt-4 { margin-top: 1rem; } @@ -881,6 +877,10 @@ video { margin-right: 0.25rem; } +.mb-4 { + margin-bottom: 1rem; +} + .ml-1 { margin-left: 0.25rem; } @@ -1289,6 +1289,10 @@ video { appearance: none; } +.grid-cols-3 { + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + .grid-cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); } @@ -1441,6 +1445,18 @@ video { align-self: center; } +.justify-self-start { + justify-self: start; +} + +.justify-self-end { + justify-self: end; +} + +.justify-self-center { + justify-self: center; +} + .overflow-auto { overflow: auto; } @@ -2335,6 +2351,16 @@ button.disabled:hover, .button.disabled:hover { color: unset; } +button.link { + border: none; + text-decoration: underline; + background-color: unset; +} + +button.link:hover { + color:unset; +} + .interactive button.disabled svg path, .interactive .button.disabled svg path { fill: var(--interactive-foreground) !important; } diff --git a/src/Logic/Actors/SelectedElementTagsUpdater.ts b/src/Logic/Actors/SelectedElementTagsUpdater.ts index 91078a53b5..8bc510c56b 100644 --- a/src/Logic/Actors/SelectedElementTagsUpdater.ts +++ b/src/Logic/Actors/SelectedElementTagsUpdater.ts @@ -6,13 +6,21 @@ import { Changes } from "../Osm/Changes" import { OsmConnection } from "../Osm/OsmConnection" import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" import SimpleMetaTagger from "../SimpleMetaTagger" -import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore" import { Feature } from "geojson" import { OsmTags } from "../../Models/OsmFeature" import OsmObjectDownloader from "../Osm/OsmObjectDownloader" import { IndexedFeatureSource } from "../FeatureSource/FeatureSource" import { Utils } from "../../Utils" +interface TagsUpdaterState { + selectedElement: UIEventSource + featureProperties: { getStore: (id: string) => UIEventSource> } + changes: Changes + osmConnection: OsmConnection + layout: LayoutConfig + osmObjectDownloader: OsmObjectDownloader + indexedFeatures: IndexedFeatureSource +} export default class SelectedElementTagsUpdater { private static readonly metatags = new Set([ "timestamp", @@ -23,38 +31,18 @@ export default class SelectedElementTagsUpdater { "id", ]) - private readonly state: { - selectedElement: UIEventSource - featureProperties: FeaturePropertiesStore - changes: Changes - osmConnection: OsmConnection - layout: LayoutConfig - osmObjectDownloader: OsmObjectDownloader - indexedFeatures: IndexedFeatureSource - } - - constructor(state: { - selectedElement: UIEventSource - featureProperties: FeaturePropertiesStore - indexedFeatures: IndexedFeatureSource - changes: Changes - osmConnection: OsmConnection - layout: LayoutConfig - osmObjectDownloader: OsmObjectDownloader - }) { - this.state = state + constructor(state: TagsUpdaterState) { state.osmConnection.isLoggedIn.addCallbackAndRun((isLoggedIn) => { if (!isLoggedIn && !Utils.runningFromConsole) { return } - this.installCallback() + this.installCallback(state) // We only have to do this once... return true }) } - private installCallback() { - const state = this.state + private installCallback(state: TagsUpdaterState) { state.selectedElement.addCallbackAndRunD(async (s) => { let id = s.properties?.id if (!id) { @@ -94,7 +82,7 @@ export default class SelectedElementTagsUpdater { oldFeature.geometry = newGeometry state.featureProperties.getStore(id)?.ping() } - this.applyUpdate(latestTags, id) + SelectedElementTagsUpdater.applyUpdate(latestTags, id, state) console.log("Updated", id) } catch (e) { @@ -102,8 +90,7 @@ export default class SelectedElementTagsUpdater { } }) } - private applyUpdate(latestTags: OsmTags, id: string) { - const state = this.state + public static applyUpdate(latestTags: OsmTags, id: string, state: TagsUpdaterState) { try { const leftRightSensitive = state.layout.isLeftRightSensitive() @@ -162,11 +149,16 @@ export default class SelectedElementTagsUpdater { } if (somethingChanged) { - console.log("Detected upstream changes to the object when opening it, updating...") + console.log( + "Detected upstream changes to the object " + + id + + " when opening it, updating..." + ) currentTagsSource.ping() } else { console.debug("Fetched latest tags for ", id, "but detected no changes") } + return currentTags } catch (e) { console.error("Updating the tags of selected element ", id, "failed due to", e) } diff --git a/src/Logic/FeatureSource/Sources/FavouritesFeatureSource.ts b/src/Logic/FeatureSource/Sources/FavouritesFeatureSource.ts index 1a4c37df23..1f2764062b 100644 --- a/src/Logic/FeatureSource/Sources/FavouritesFeatureSource.ts +++ b/src/Logic/FeatureSource/Sources/FavouritesFeatureSource.ts @@ -4,9 +4,10 @@ import { Store, Stores, UIEventSource } from "../../UIEventSource" import { OsmConnection } from "../../Osm/OsmConnection" import { OsmId } from "../../../Models/OsmFeature" import { GeoOperations } from "../../GeoOperations" -import FeaturePropertiesStore from "../Actors/FeaturePropertiesStore" import { IndexedFeatureSource } from "../FeatureSource" -import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig" +import OsmObjectDownloader from "../../Osm/OsmObjectDownloader" +import { SpecialVisualizationState } from "../../../UI/SpecialVisualization" +import SelectedElementTagsUpdater from "../../Actors/SelectedElementTagsUpdater" /** * Generates the favourites from the preferences and marks them as favourite @@ -21,14 +22,9 @@ export default class FavouritesFeatureSource extends StaticFeatureSource { */ public readonly allFavourites: Store - constructor( - connection: OsmConnection, - indexedSource: FeaturePropertiesStore, - allFeatures: IndexedFeatureSource, - layout: LayoutConfig - ) { + constructor(state: SpecialVisualizationState) { const features: Store = Stores.ListStabilized( - connection.preferencesHandler.preferences.map((prefs) => { + state.osmConnection.preferencesHandler.preferences.map((prefs) => { const feats: Feature[] = [] const allIds = new Set() for (const key in prefs) { @@ -53,24 +49,49 @@ export default class FavouritesFeatureSource extends StaticFeatureSource { const featuresWithoutAlreadyPresent = features.map((features) => features.filter( - (feat) => !layout.layers.some((l) => l.id === feat.properties._orig_layer) + (feat) => !state.layout.layers.some((l) => l.id === feat.properties._orig_layer) ) ) super(featuresWithoutAlreadyPresent) this.allFavourites = features - this._osmConnection = connection + this._osmConnection = state.osmConnection this._detectedIds = Stores.ListStabilized( features.map((feats) => feats.map((f) => f.properties.id)) ) + let allFeatures = state.indexedFeatures this._detectedIds.addCallbackAndRunD((detected) => - this.markFeatures(detected, indexedSource, allFeatures) + this.markFeatures(detected, state.featureProperties, allFeatures) ) // We use the indexedFeatureSource as signal to update allFeatures.features.map((_) => - this.markFeatures(this._detectedIds.data, indexedSource, allFeatures) + this.markFeatures(this._detectedIds.data, state.featureProperties, allFeatures) ) + + this.allFavourites.addCallbackD((features) => { + for (const feature of features) { + this.updateFeature(feature, state.osmObjectDownloader, state) + } + + return true + }) + } + + private async updateFeature( + feature: Feature, + osmObjectDownloader: OsmObjectDownloader, + state: SpecialVisualizationState + ) { + const id = feature.properties.id + const upstream = await osmObjectDownloader.DownloadObjectAsync(id) + if (upstream === "deleted") { + this.removeFavourite(feature) + return + } + console.log("Updating metadata due to favourite of", id) + const latestTags = SelectedElementTagsUpdater.applyUpdate(upstream.tags, id, state) + this.updatePropertiesOfFavourite(latestTags) } private static ExtractFavourite(key: string, prefs: Record): Feature { @@ -115,6 +136,37 @@ export default class FavouritesFeatureSource extends StaticFeatureSource { return properties } + /** + * Sets all the (normal) properties as the feature is updated + */ + private updatePropertiesOfFavourite(properties: Record) { + const id = properties?.id?.replace("/", "-") + if (!id) { + return + } + console.log("Updating store for", id) + for (const key in properties) { + const pref = this._osmConnection.GetPreference( + "favourite-" + id + "-property-" + key.replaceAll(":", "__") + ) + const v = properties[key] + if (v === "" || !v) { + continue + } + pref.setData("" + v) + } + } + + public removeFavourite(feature: Feature, tags?: UIEventSource>) { + const id = feature.properties.id.replace("/", "-") + const pref = this._osmConnection.GetPreference("favourite-" + id) + this._osmConnection.preferencesHandler.removeAllWithPrefix("mapcomplete-favourite-" + id) + if (tags) { + delete tags.data._favourite + tags.ping() + } + } + public markAsFavourite( feature: Feature, layer: string, @@ -123,42 +175,25 @@ export default class FavouritesFeatureSource extends StaticFeatureSource { isFavourite: boolean = true ) { { + if (!isFavourite) { + this.removeFavourite(feature, tags) + return + } const id = tags.data.id.replace("/", "-") const pref = this._osmConnection.GetPreference("favourite-" + id) - if (isFavourite) { - const center = GeoOperations.centerpointCoordinates(feature) - pref.setData(JSON.stringify(center)) - - this._osmConnection.GetPreference("favourite-" + id + "-layer").setData(layer) - this._osmConnection.GetPreference("favourite-" + id + "-theme").setData(theme) - for (const key in tags.data) { - const pref = this._osmConnection.GetPreference( - "favourite-" + id + "-property-" + key.replaceAll(":", "__") - ) - const v = tags.data[key] - if (v === "" || !v) { - continue - } - pref.setData("" + v) - } - } else { - this._osmConnection.preferencesHandler.removeAllWithPrefix( - "mapcomplete-favourite-" + id - ) - } - } - if (isFavourite) { - tags.data._favourite = "yes" - tags.ping() - } else { - delete tags.data._favourite - tags.ping() + const center = GeoOperations.centerpointCoordinates(feature) + pref.setData(JSON.stringify(center)) + this._osmConnection.GetPreference("favourite-" + id + "-layer").setData(layer) + this._osmConnection.GetPreference("favourite-" + id + "-theme").setData(theme) + this.updatePropertiesOfFavourite(tags.data) } + tags.data._favourite = "yes" + tags.ping() } private markFeatures( detected: string[], - featureProperties: FeaturePropertiesStore, + featureProperties: { getStore(id: string): UIEventSource> }, allFeatures: IndexedFeatureSource ) { const feature = allFeatures.features.data diff --git a/src/Logic/GeoOperations.ts b/src/Logic/GeoOperations.ts index e03cc13b1b..61128d1b06 100644 --- a/src/Logic/GeoOperations.ts +++ b/src/Logic/GeoOperations.ts @@ -501,147 +501,43 @@ export class GeoOperations { ) } - public static IdentifieCommonSegments(coordinatess: [number, number][][]): { - originalIndex: number - segmentShardWith: number[] - coordinates: [] - }[] { - // An edge. Note that the edge might be reversed to fix the sorting condition: start[0] < end[0] && (start[0] != end[0] || start[0] < end[1]) - type edge = { - start: [number, number] - end: [number, number] - intermediate: [number, number][] - members: { index: number; isReversed: boolean }[] + /** + * Given a list of points, convert into a GPX-list, e.g. for favourites + * @param locations + * @param title + */ + public static toGpxPoints( + locations: Feature[], + title?: string + ) { + title = title?.trim() + if (title === undefined || title === "") { + title = "Created with MapComplete" } - - // The strategy: - // 1. Index _all_ edges from _every_ linestring. Index them by starting key, gather which relations run over them - // 2. Join these edges back together - as long as their membership groups are the same - // 3. Convert to results - - const allEdgesByKey = new Map() - - for (let index = 0; index < coordinatess.length; index++) { - const coordinates = coordinatess[index] - for (let i = 0; i < coordinates.length - 1; i++) { - const c0 = coordinates[i] - const c1 = coordinates[i + 1] - const isReversed = c0[0] > c1[0] || (c0[0] == c1[0] && c0[1] > c1[1]) - - let key: string - if (isReversed) { - key = "" + c1 + ";" + c0 - } else { - key = "" + c0 + ";" + c1 + title = Utils.EncodeXmlValue(title) + const trackPoints: string[] = [] + for (const l of locations) { + let trkpt = ` ` + for (const key in l.properties) { + const keyCleaned = key.replaceAll(":", "__") + trkpt += ` <${keyCleaned}>${l.properties[key]}\n` + if (key === "website") { + trkpt += ` ${l.properties[key]}\n` } - const member = { index, isReversed } - if (allEdgesByKey.has(key)) { - allEdgesByKey.get(key).members.push(member) - continue - } - - let edge: edge - if (!isReversed) { - edge = { - start: c0, - end: c1, - members: [member], - intermediate: [], - } - } else { - edge = { - start: c1, - end: c0, - members: [member], - intermediate: [], - } - } - allEdgesByKey.set(key, edge) } + trkpt += " \n" + trackPoints.push(trkpt) } - - // Lets merge them back together! - - let didMergeSomething = false - let allMergedEdges = Array.from(allEdgesByKey.values()) - const allEdgesByStartPoint = new Map() - for (const edge of allMergedEdges) { - edge.members.sort((m0, m1) => m0.index - m1.index) - - const kstart = edge.start + "" - if (!allEdgesByStartPoint.has(kstart)) { - allEdgesByStartPoint.set(kstart, []) - } - allEdgesByStartPoint.get(kstart).push(edge) - } - - function membersAreCompatible(first: edge, second: edge): boolean { - // There must be an exact match between the members - if (first.members === second.members) { - return true - } - - if (first.members.length !== second.members.length) { - return false - } - - // Members are sorted and have the same length, so we can check quickly - for (let i = 0; i < first.members.length; i++) { - const m0 = first.members[i] - const m1 = second.members[i] - if (m0.index !== m1.index || m0.isReversed !== m1.isReversed) { - return false - } - } - - // Allrigth, they are the same, lets mark this permanently - second.members = first.members - return true - } - - do { - didMergeSomething = false - // We use 'allMergedEdges' as our running list - const consumed = new Set() - for (const edge of allMergedEdges) { - // Can we make this edge longer at the end? - if (consumed.has(edge)) { - continue - } - - console.log("Considering edge", edge) - const matchingEndEdges = allEdgesByStartPoint.get(edge.end + "") - console.log("Matchign endpoints:", matchingEndEdges) - if (matchingEndEdges === undefined) { - continue - } - - for (let i = 0; i < matchingEndEdges.length; i++) { - const endEdge = matchingEndEdges[i] - - if (consumed.has(endEdge)) { - continue - } - - if (!membersAreCompatible(edge, endEdge)) { - continue - } - - // We can make the segment longer! - didMergeSomething = true - console.log("Merging ", edge, "with ", endEdge) - edge.intermediate.push(edge.end) - edge.end = endEdge.end - consumed.add(endEdge) - matchingEndEdges.splice(i, 1) - break - } - } - - allMergedEdges = allMergedEdges.filter((edge) => !consumed.has(edge)) - } while (didMergeSomething) - - return [] + const header = + '' + return ( + header + + "\n" + + title + + "\n\n" + + trackPoints.join("\n") + + "\n" + ) } /** diff --git a/src/Logic/State/UserRelatedState.ts b/src/Logic/State/UserRelatedState.ts index bbc7c9f8ce..2fbbdc3697 100644 --- a/src/Logic/State/UserRelatedState.ts +++ b/src/Logic/State/UserRelatedState.ts @@ -397,13 +397,6 @@ export default class UserRelatedState { } if (tags[key + "-combined-0"]) { // A combined value exists - console.log( - "Trying to get a long preference for ", - key, - "with length value", - tags[key], - "as -combined-0 exists" - ) this.osmConnection.GetLongPreference(key, "").setData(tags[key]) } else { this.osmConnection diff --git a/src/Models/ThemeViewState.ts b/src/Models/ThemeViewState.ts index fbf17de776..1db7f7ac5e 100644 --- a/src/Models/ThemeViewState.ts +++ b/src/Models/ThemeViewState.ts @@ -244,12 +244,6 @@ export default class ThemeViewState implements SpecialVisualizationState { this.dataIsLoading = layoutSource.isLoading this.indexedFeatures = layoutSource this.featureProperties = new FeaturePropertiesStore(layoutSource) - this.favourites = new FavouritesFeatureSource( - this.osmConnection, - this.featureProperties, - layoutSource, - layout - ) this.changes = new Changes( { @@ -333,10 +327,10 @@ export default class ThemeViewState implements SpecialVisualizationState { return sorted }) - const lastClick = (this.lastClickObject = new LastClickFeatureSource( + this.lastClickObject = new LastClickFeatureSource( this.mapProperties.lastClickLocation, this.layout - )) + ) this.osmObjectDownloader = new OsmObjectDownloader( this.osmConnection.Backend(), @@ -359,6 +353,7 @@ export default class ThemeViewState implements SpecialVisualizationState { this.osmConnection, this.changes ) + this.favourites = new FavouritesFeatureSource(this) this.initActors() this.drawSpecialLayers() @@ -472,6 +467,7 @@ export default class ThemeViewState implements SpecialVisualizationState { this.selectedLayer.setData(layer) this.selectedElement.setData(toSelect) } + private initHotkeys() { Hotkeys.RegisterHotkey( { nomod: "Escape", onUp: true }, @@ -483,6 +479,15 @@ export default class ThemeViewState implements SpecialVisualizationState { } ) + Hotkeys.RegisterHotkey( + { nomod: "f" }, + Translations.t.hotkeyDocumentation.selectFavourites, + () => { + this.guistate.menuViewTab.setData("favourites") + this.guistate.menuIsOpened.setData(true) + } + ) + this.mapProperties.lastKeyNavigation.addCallbackAndRunD((_) => { Hotkeys.RegisterHotkey( { diff --git a/src/UI/Base/ToSvelte.svelte b/src/UI/Base/ToSvelte.svelte index 01386bf903..88428eb2dc 100644 --- a/src/UI/Base/ToSvelte.svelte +++ b/src/UI/Base/ToSvelte.svelte @@ -9,7 +9,7 @@ const uiElem = typeof construct === "function" ? construct() : construct html = uiElem?.ConstructElement() if (html !== undefined) { - elem.replaceWith(html) + elem?.replaceWith(html) } }) diff --git a/src/UI/Favourites/FavouriteSummary.svelte b/src/UI/Favourites/FavouriteSummary.svelte index c890f5a59b..7cc402381a 100644 --- a/src/UI/Favourites/FavouriteSummary.svelte +++ b/src/UI/Favourites/FavouriteSummary.svelte @@ -49,14 +49,14 @@ -
-

select()} class="cursor-pointer ml-1 m-0"> +
+

+ {$distance} -
+
diff --git a/src/UI/Favourites/Favourites.svelte b/src/UI/Favourites/Favourites.svelte index 790dc9a7bd..f4452ef5bc 100644 --- a/src/UI/Favourites/Favourites.svelte +++ b/src/UI/Favourites/Favourites.svelte @@ -1,19 +1,58 @@ -
- You marked {$favourites.length} locations as a favourite location. +
console.log("Got keypress", e)}> + + - This list is only visible to you {#each $favourites as feature (feature.properties.id)} {/each} + +
+ + +
diff --git a/src/index.css b/src/index.css index 84cf251b0f..c9f89ccb78 100644 --- a/src/index.css +++ b/src/index.css @@ -280,6 +280,16 @@ button.disabled:hover, .button.disabled:hover { color: unset; } +button.link { + border: none; + text-decoration: underline; + background-color: unset; +} + +button.link:hover { + color:unset; +} + .interactive button.disabled svg path, .interactive .button.disabled svg path { fill: var(--interactive-foreground) !important;; }