forked from MapComplete/MapComplete
		
	Favourites: improve overview, update all features from OSM when loading
This commit is contained in:
		
							parent
							
								
									35f8b9d8f2
								
							
						
					
					
						commit
						56a23deb2d
					
				
					 10 changed files with 231 additions and 234 deletions
				
			
		|  | @ -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; | ||||
| } | ||||
|  |  | |||
|  | @ -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<Feature> | ||||
|     featureProperties: { getStore: (id: string) => UIEventSource<Record<string, string>> } | ||||
|     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<Feature> | ||||
|         featureProperties: FeaturePropertiesStore | ||||
|         changes: Changes | ||||
|         osmConnection: OsmConnection | ||||
|         layout: LayoutConfig | ||||
|         osmObjectDownloader: OsmObjectDownloader | ||||
|         indexedFeatures: IndexedFeatureSource | ||||
|     } | ||||
| 
 | ||||
|     constructor(state: { | ||||
|         selectedElement: UIEventSource<Feature> | ||||
|         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) | ||||
|         } | ||||
|  |  | |||
|  | @ -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<Feature[]> | ||||
| 
 | ||||
|     constructor( | ||||
|         connection: OsmConnection, | ||||
|         indexedSource: FeaturePropertiesStore, | ||||
|         allFeatures: IndexedFeatureSource, | ||||
|         layout: LayoutConfig | ||||
|     ) { | ||||
|     constructor(state: SpecialVisualizationState) { | ||||
|         const features: Store<Feature[]> = Stores.ListStabilized( | ||||
|             connection.preferencesHandler.preferences.map((prefs) => { | ||||
|             state.osmConnection.preferencesHandler.preferences.map((prefs) => { | ||||
|                 const feats: Feature[] = [] | ||||
|                 const allIds = new Set<string>() | ||||
|                 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<string, string>): 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<string, string>) { | ||||
|         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<Record<string, string>>) { | ||||
|         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<Record<string, string>> }, | ||||
|         allFeatures: IndexedFeatureSource | ||||
|     ) { | ||||
|         const feature = allFeatures.features.data | ||||
|  |  | |||
|  | @ -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<Point, { date?: string; altitude?: number | string }>[], | ||||
|         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<string, edge>() | ||||
| 
 | ||||
|         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 = `    <wpt lat="${l.geometry.coordinates[1]}" lon="${l.geometry.coordinates[0]}">` | ||||
|             for (const key in l.properties) { | ||||
|                 const keyCleaned = key.replaceAll(":", "__") | ||||
|                 trkpt += `        <${keyCleaned}>${l.properties[key]}</${keyCleaned}>\n` | ||||
|                 if (key === "website") { | ||||
|                     trkpt += `        <link>${l.properties[key]}</link>\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 += "    </wpt>\n" | ||||
|             trackPoints.push(trkpt) | ||||
|         } | ||||
| 
 | ||||
|         // Lets merge them back together!
 | ||||
| 
 | ||||
|         let didMergeSomething = false | ||||
|         let allMergedEdges = Array.from(allEdgesByKey.values()) | ||||
|         const allEdgesByStartPoint = new Map<string, edge[]>() | ||||
|         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<edge>() | ||||
|             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 = | ||||
|             '<gpx version="1.1" creator="mapcomplete.org" xmlns="http://www.topografix.com/GPX/1/1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">' | ||||
|         return ( | ||||
|             header + | ||||
|             "\n<name>" + | ||||
|             title + | ||||
|             "</name>\n<trk><trkseg>\n" + | ||||
|             trackPoints.join("\n") + | ||||
|             "\n</trkseg></trk></gpx>" | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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( | ||||
|                 { | ||||
|  |  | |||
|  | @ -9,7 +9,7 @@ | |||
|     const uiElem = typeof construct === "function" ? construct() : construct | ||||
|     html = uiElem?.ConstructElement() | ||||
|     if (html !== undefined) { | ||||
|       elem.replaceWith(html) | ||||
|       elem?.replaceWith(html) | ||||
|     } | ||||
|   }) | ||||
| 
 | ||||
|  |  | |||
|  | @ -49,14 +49,14 @@ | |||
| 
 | ||||
| </script> | ||||
| 
 | ||||
| <div class="px-1 my-1 border-2 border-dashed border-gray-300 rounded flex justify-between items-center"> | ||||
|   <h3 on:click={() => select()} class="cursor-pointer ml-1 m-0"> | ||||
| <div class="px-1 my-1 border-2 border-dashed border-gray-300 rounded grid grid-cols-3 items-center no-weblate"> | ||||
|   <button on:click={() => select()} class="cursor-pointer ml-1 m-0 link justify-self-start"> | ||||
|     <TagRenderingAnswer extraClasses="underline" config={titleConfig} layer={favConfig} selectedElement={feature} {tags} /> | ||||
|   </h3> | ||||
|   </button> | ||||
| 
 | ||||
|   {$distance} | ||||
|    | ||||
|   <div class="flex items-center"> | ||||
|   <div class="flex items-center justify-self-end title-icons links-as-button gap-x-0.5 p-1 pt-0.5 sm:pt-1"> | ||||
|     {#each favConfig.titleIcons as titleIconConfig} | ||||
|       {#if (titleIconBlacklist.indexOf(titleIconConfig.id) < 0) && (titleIconConfig.condition?.matchesProperties(properties) ?? true) && (titleIconConfig.metacondition?.matchesProperties( { ...properties, ...state.userRelatedState.preferencesAsTags.data } ) ?? true) && titleIconConfig.IsKnown(properties)} | ||||
|         <div class={titleIconConfig.renderIconClass ?? "flex h-8 w-8 items-center"}> | ||||
|  | @ -71,6 +71,7 @@ | |||
|         </div> | ||||
|       {/if} | ||||
|     {/each} | ||||
|      | ||||
|     <button on:click={() => center()} class="p-1" ><Center class="w-6 h-6"/></button> | ||||
|   </div> | ||||
| </div> | ||||
|  |  | |||
|  | @ -1,19 +1,58 @@ | |||
| <script lang="ts"> | ||||
|   import type { SpecialVisualizationState } from "../SpecialVisualization"; | ||||
|   import FavouriteSummary from "./FavouriteSummary.svelte"; | ||||
|   import Translations from "../i18n/Translations"; | ||||
|   import Tr from "../Base/Tr.svelte"; | ||||
|   import { DownloadIcon } from "@rgossiaux/svelte-heroicons/solid"; | ||||
|   import { Utils } from "../../Utils"; | ||||
|   import { GeoOperations } from "../../Logic/GeoOperations"; | ||||
|   import type { Feature, LineString, Point } from "geojson"; | ||||
| 
 | ||||
|   /** | ||||
|    * A panel showing all your favourites | ||||
|    */ | ||||
|   export let state: SpecialVisualizationState; | ||||
|   let favourites = state.favourites.allFavourites; | ||||
| 
 | ||||
|   function downloadGeojson() { | ||||
|     const contents = { features: favourites.data, type: "FeatureCollection" }; | ||||
|     Utils.offerContentsAsDownloadableFile( | ||||
|       JSON.stringify(contents), | ||||
|       "mapcomplete-favourites-" + (new Date().toISOString()) + ".geojson", | ||||
|       { | ||||
|         mimetype: "application/vnd.geo+json" | ||||
|       } | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   function downloadGPX() { | ||||
|     const gpx = GeoOperations.toGpxPoints(<Feature<Point>>favourites.data, "MapComplete favourites"); | ||||
|     Utils.offerContentsAsDownloadableFile(gpx, | ||||
|       "mapcomplete-favourites-" + (new Date().toISOString()) + ".gpx", | ||||
|       { | ||||
|         mimetype: "{gpx=application/gpx+xml}" | ||||
|       }); | ||||
| 
 | ||||
|   } | ||||
|    | ||||
| </script> | ||||
| 
 | ||||
| <div class="flex flex-col"> | ||||
|   You marked {$favourites.length} locations as a favourite location. | ||||
| <div class="flex flex-col" on:keypress={(e) => console.log("Got keypress", e)}> | ||||
|   <Tr t={Translations.t.favouritePoi.intro.Subs({length: $favourites?.length ?? 0})} /> | ||||
|   <Tr t={Translations.t.favouritePoi.privacy} /> | ||||
| 
 | ||||
|   This list is only visible to you | ||||
|   {#each $favourites as feature (feature.properties.id)} | ||||
|     <FavouriteSummary {feature} {state} /> | ||||
|   {/each} | ||||
| 
 | ||||
|   <div class="mt-8"> | ||||
|     <button class="flex p-2" on:click={() => downloadGeojson()}> | ||||
|       <DownloadIcon class="h-6 w-6" /> | ||||
|       <Tr t={Translations.t.favouritePoi.downloadGeojson} /> | ||||
|     </button> | ||||
|     <button class="flex p-2" on:click={() => downloadGPX()}> | ||||
|       <DownloadIcon class="h-6 w-6" /> | ||||
|       <Tr t={Translations.t.favouritePoi.downloadGpx} /> | ||||
|     </button> | ||||
|   </div> | ||||
| </div> | ||||
|  |  | |||
|  | @ -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;; | ||||
| } | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue