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; |   margin-right: 3rem; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| .mb-4 { |  | ||||||
|   margin-bottom: 1rem; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| .mt-4 { | .mt-4 { | ||||||
|   margin-top: 1rem; |   margin-top: 1rem; | ||||||
| } | } | ||||||
|  | @ -881,6 +877,10 @@ video { | ||||||
|   margin-right: 0.25rem; |   margin-right: 0.25rem; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .mb-4 { | ||||||
|  |   margin-bottom: 1rem; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .ml-1 { | .ml-1 { | ||||||
|   margin-left: 0.25rem; |   margin-left: 0.25rem; | ||||||
| } | } | ||||||
|  | @ -1289,6 +1289,10 @@ video { | ||||||
|           appearance: none; |           appearance: none; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .grid-cols-3 { | ||||||
|  |   grid-template-columns: repeat(3, minmax(0, 1fr)); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .grid-cols-1 { | .grid-cols-1 { | ||||||
|   grid-template-columns: repeat(1, minmax(0, 1fr)); |   grid-template-columns: repeat(1, minmax(0, 1fr)); | ||||||
| } | } | ||||||
|  | @ -1441,6 +1445,18 @@ video { | ||||||
|   align-self: center; |   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 { | ||||||
|   overflow: auto; |   overflow: auto; | ||||||
| } | } | ||||||
|  | @ -2335,6 +2351,16 @@ button.disabled:hover, .button.disabled:hover { | ||||||
|   color: unset; |   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 { | .interactive button.disabled svg path, .interactive .button.disabled svg path { | ||||||
|   fill: var(--interactive-foreground) !important; |   fill: var(--interactive-foreground) !important; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -6,13 +6,21 @@ import { Changes } from "../Osm/Changes" | ||||||
| import { OsmConnection } from "../Osm/OsmConnection" | import { OsmConnection } from "../Osm/OsmConnection" | ||||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" | import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" | ||||||
| import SimpleMetaTagger from "../SimpleMetaTagger" | import SimpleMetaTagger from "../SimpleMetaTagger" | ||||||
| import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore" |  | ||||||
| import { Feature } from "geojson" | import { Feature } from "geojson" | ||||||
| import { OsmTags } from "../../Models/OsmFeature" | import { OsmTags } from "../../Models/OsmFeature" | ||||||
| import OsmObjectDownloader from "../Osm/OsmObjectDownloader" | import OsmObjectDownloader from "../Osm/OsmObjectDownloader" | ||||||
| import { IndexedFeatureSource } from "../FeatureSource/FeatureSource" | import { IndexedFeatureSource } from "../FeatureSource/FeatureSource" | ||||||
| import { Utils } from "../../Utils" | 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 { | export default class SelectedElementTagsUpdater { | ||||||
|     private static readonly metatags = new Set([ |     private static readonly metatags = new Set([ | ||||||
|         "timestamp", |         "timestamp", | ||||||
|  | @ -23,38 +31,18 @@ export default class SelectedElementTagsUpdater { | ||||||
|         "id", |         "id", | ||||||
|     ]) |     ]) | ||||||
| 
 | 
 | ||||||
|     private readonly state: { |     constructor(state: TagsUpdaterState) { | ||||||
|         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 |  | ||||||
|         state.osmConnection.isLoggedIn.addCallbackAndRun((isLoggedIn) => { |         state.osmConnection.isLoggedIn.addCallbackAndRun((isLoggedIn) => { | ||||||
|             if (!isLoggedIn && !Utils.runningFromConsole) { |             if (!isLoggedIn && !Utils.runningFromConsole) { | ||||||
|                 return |                 return | ||||||
|             } |             } | ||||||
|             this.installCallback() |             this.installCallback(state) | ||||||
|             // We only have to do this once...
 |             // We only have to do this once...
 | ||||||
|             return true |             return true | ||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private installCallback() { |     private installCallback(state: TagsUpdaterState) { | ||||||
|         const state = this.state |  | ||||||
|         state.selectedElement.addCallbackAndRunD(async (s) => { |         state.selectedElement.addCallbackAndRunD(async (s) => { | ||||||
|             let id = s.properties?.id |             let id = s.properties?.id | ||||||
|             if (!id) { |             if (!id) { | ||||||
|  | @ -94,7 +82,7 @@ export default class SelectedElementTagsUpdater { | ||||||
|                     oldFeature.geometry = newGeometry |                     oldFeature.geometry = newGeometry | ||||||
|                     state.featureProperties.getStore(id)?.ping() |                     state.featureProperties.getStore(id)?.ping() | ||||||
|                 } |                 } | ||||||
|                 this.applyUpdate(latestTags, id) |                 SelectedElementTagsUpdater.applyUpdate(latestTags, id, state) | ||||||
| 
 | 
 | ||||||
|                 console.log("Updated", id) |                 console.log("Updated", id) | ||||||
|             } catch (e) { |             } catch (e) { | ||||||
|  | @ -102,8 +90,7 @@ export default class SelectedElementTagsUpdater { | ||||||
|             } |             } | ||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
|     private applyUpdate(latestTags: OsmTags, id: string) { |     public static applyUpdate(latestTags: OsmTags, id: string, state: TagsUpdaterState) { | ||||||
|         const state = this.state |  | ||||||
|         try { |         try { | ||||||
|             const leftRightSensitive = state.layout.isLeftRightSensitive() |             const leftRightSensitive = state.layout.isLeftRightSensitive() | ||||||
| 
 | 
 | ||||||
|  | @ -162,11 +149,16 @@ export default class SelectedElementTagsUpdater { | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             if (somethingChanged) { |             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() |                 currentTagsSource.ping() | ||||||
|             } else { |             } else { | ||||||
|                 console.debug("Fetched latest tags for ", id, "but detected no changes") |                 console.debug("Fetched latest tags for ", id, "but detected no changes") | ||||||
|             } |             } | ||||||
|  |             return currentTags | ||||||
|         } catch (e) { |         } catch (e) { | ||||||
|             console.error("Updating the tags of selected element ", id, "failed due to", 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 { OsmConnection } from "../../Osm/OsmConnection" | ||||||
| import { OsmId } from "../../../Models/OsmFeature" | import { OsmId } from "../../../Models/OsmFeature" | ||||||
| import { GeoOperations } from "../../GeoOperations" | import { GeoOperations } from "../../GeoOperations" | ||||||
| import FeaturePropertiesStore from "../Actors/FeaturePropertiesStore" |  | ||||||
| import { IndexedFeatureSource } from "../FeatureSource" | 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 |  * 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[]> |     public readonly allFavourites: Store<Feature[]> | ||||||
| 
 | 
 | ||||||
|     constructor( |     constructor(state: SpecialVisualizationState) { | ||||||
|         connection: OsmConnection, |  | ||||||
|         indexedSource: FeaturePropertiesStore, |  | ||||||
|         allFeatures: IndexedFeatureSource, |  | ||||||
|         layout: LayoutConfig |  | ||||||
|     ) { |  | ||||||
|         const features: Store<Feature[]> = Stores.ListStabilized( |         const features: Store<Feature[]> = Stores.ListStabilized( | ||||||
|             connection.preferencesHandler.preferences.map((prefs) => { |             state.osmConnection.preferencesHandler.preferences.map((prefs) => { | ||||||
|                 const feats: Feature[] = [] |                 const feats: Feature[] = [] | ||||||
|                 const allIds = new Set<string>() |                 const allIds = new Set<string>() | ||||||
|                 for (const key in prefs) { |                 for (const key in prefs) { | ||||||
|  | @ -53,24 +49,49 @@ export default class FavouritesFeatureSource extends StaticFeatureSource { | ||||||
| 
 | 
 | ||||||
|         const featuresWithoutAlreadyPresent = features.map((features) => |         const featuresWithoutAlreadyPresent = features.map((features) => | ||||||
|             features.filter( |             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) |         super(featuresWithoutAlreadyPresent) | ||||||
|         this.allFavourites = features |         this.allFavourites = features | ||||||
| 
 | 
 | ||||||
|         this._osmConnection = connection |         this._osmConnection = state.osmConnection | ||||||
|         this._detectedIds = Stores.ListStabilized( |         this._detectedIds = Stores.ListStabilized( | ||||||
|             features.map((feats) => feats.map((f) => f.properties.id)) |             features.map((feats) => feats.map((f) => f.properties.id)) | ||||||
|         ) |         ) | ||||||
|  |         let allFeatures = state.indexedFeatures | ||||||
|         this._detectedIds.addCallbackAndRunD((detected) => |         this._detectedIds.addCallbackAndRunD((detected) => | ||||||
|             this.markFeatures(detected, indexedSource, allFeatures) |             this.markFeatures(detected, state.featureProperties, allFeatures) | ||||||
|         ) |         ) | ||||||
|         // We use the indexedFeatureSource as signal to update
 |         // We use the indexedFeatureSource as signal to update
 | ||||||
|         allFeatures.features.map((_) => |         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 { |     private static ExtractFavourite(key: string, prefs: Record<string, string>): Feature { | ||||||
|  | @ -115,6 +136,37 @@ export default class FavouritesFeatureSource extends StaticFeatureSource { | ||||||
|         return properties |         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( |     public markAsFavourite( | ||||||
|         feature: Feature, |         feature: Feature, | ||||||
|         layer: string, |         layer: string, | ||||||
|  | @ -123,42 +175,25 @@ export default class FavouritesFeatureSource extends StaticFeatureSource { | ||||||
|         isFavourite: boolean = true |         isFavourite: boolean = true | ||||||
|     ) { |     ) { | ||||||
|         { |         { | ||||||
|  |             if (!isFavourite) { | ||||||
|  |                 this.removeFavourite(feature, tags) | ||||||
|  |                 return | ||||||
|  |             } | ||||||
|             const id = tags.data.id.replace("/", "-") |             const id = tags.data.id.replace("/", "-") | ||||||
|             const pref = this._osmConnection.GetPreference("favourite-" + id) |             const pref = this._osmConnection.GetPreference("favourite-" + id) | ||||||
|             if (isFavourite) { |             const center = GeoOperations.centerpointCoordinates(feature) | ||||||
|                 const center = GeoOperations.centerpointCoordinates(feature) |             pref.setData(JSON.stringify(center)) | ||||||
|                 pref.setData(JSON.stringify(center)) |             this._osmConnection.GetPreference("favourite-" + id + "-layer").setData(layer) | ||||||
| 
 |             this._osmConnection.GetPreference("favourite-" + id + "-theme").setData(theme) | ||||||
|                 this._osmConnection.GetPreference("favourite-" + id + "-layer").setData(layer) |             this.updatePropertiesOfFavourite(tags.data) | ||||||
|                 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() |  | ||||||
|         } |         } | ||||||
|  |         tags.data._favourite = "yes" | ||||||
|  |         tags.ping() | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private markFeatures( |     private markFeatures( | ||||||
|         detected: string[], |         detected: string[], | ||||||
|         featureProperties: FeaturePropertiesStore, |         featureProperties: { getStore(id: string): UIEventSource<Record<string, string>> }, | ||||||
|         allFeatures: IndexedFeatureSource |         allFeatures: IndexedFeatureSource | ||||||
|     ) { |     ) { | ||||||
|         const feature = allFeatures.features.data |         const feature = allFeatures.features.data | ||||||
|  |  | ||||||
|  | @ -501,147 +501,43 @@ export class GeoOperations { | ||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public static IdentifieCommonSegments(coordinatess: [number, number][][]): { |     /** | ||||||
|         originalIndex: number |      * Given a list of points, convert into a GPX-list, e.g. for favourites | ||||||
|         segmentShardWith: number[] |      * @param locations | ||||||
|         coordinates: [] |      * @param title | ||||||
|     }[] { |      */ | ||||||
|         // 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])
 |     public static toGpxPoints( | ||||||
|         type edge = { |         locations: Feature<Point, { date?: string; altitude?: number | string }>[], | ||||||
|             start: [number, number] |         title?: string | ||||||
|             end: [number, number] |     ) { | ||||||
|             intermediate: [number, number][] |         title = title?.trim() | ||||||
|             members: { index: number; isReversed: boolean }[] |         if (title === undefined || title === "") { | ||||||
|  |             title = "Created with MapComplete" | ||||||
|         } |         } | ||||||
| 
 |         title = Utils.EncodeXmlValue(title) | ||||||
|         // The strategy:
 |         const trackPoints: string[] = [] | ||||||
|         // 1. Index _all_ edges from _every_ linestring. Index them by starting key, gather which relations run over them
 |         for (const l of locations) { | ||||||
|         // 2. Join these edges back together - as long as their membership groups are the same
 |             let trkpt = `    <wpt lat="${l.geometry.coordinates[1]}" lon="${l.geometry.coordinates[0]}">` | ||||||
|         // 3. Convert to results
 |             for (const key in l.properties) { | ||||||
| 
 |                 const keyCleaned = key.replaceAll(":", "__") | ||||||
|         const allEdgesByKey = new Map<string, edge>() |                 trkpt += `        <${keyCleaned}>${l.properties[key]}</${keyCleaned}>\n` | ||||||
| 
 |                 if (key === "website") { | ||||||
|         for (let index = 0; index < coordinatess.length; index++) { |                     trkpt += `        <link>${l.properties[key]}</link>\n` | ||||||
|             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 |  | ||||||
|                 } |                 } | ||||||
|                 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) | ||||||
|         } |         } | ||||||
| 
 |         const header = | ||||||
|         // Lets merge them back together!
 |             '<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 ( | ||||||
|         let didMergeSomething = false |             header + | ||||||
|         let allMergedEdges = Array.from(allEdgesByKey.values()) |             "\n<name>" + | ||||||
|         const allEdgesByStartPoint = new Map<string, edge[]>() |             title + | ||||||
|         for (const edge of allMergedEdges) { |             "</name>\n<trk><trkseg>\n" + | ||||||
|             edge.members.sort((m0, m1) => m0.index - m1.index) |             trackPoints.join("\n") + | ||||||
| 
 |             "\n</trkseg></trk></gpx>" | ||||||
|             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 [] |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  |  | ||||||
|  | @ -397,13 +397,6 @@ export default class UserRelatedState { | ||||||
|                 } |                 } | ||||||
|                 if (tags[key + "-combined-0"]) { |                 if (tags[key + "-combined-0"]) { | ||||||
|                     // A combined value exists
 |                     // 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]) |                     this.osmConnection.GetLongPreference(key, "").setData(tags[key]) | ||||||
|                 } else { |                 } else { | ||||||
|                     this.osmConnection |                     this.osmConnection | ||||||
|  |  | ||||||
|  | @ -244,12 +244,6 @@ export default class ThemeViewState implements SpecialVisualizationState { | ||||||
|             this.dataIsLoading = layoutSource.isLoading |             this.dataIsLoading = layoutSource.isLoading | ||||||
|             this.indexedFeatures = layoutSource |             this.indexedFeatures = layoutSource | ||||||
|             this.featureProperties = new FeaturePropertiesStore(layoutSource) |             this.featureProperties = new FeaturePropertiesStore(layoutSource) | ||||||
|             this.favourites = new FavouritesFeatureSource( |  | ||||||
|                 this.osmConnection, |  | ||||||
|                 this.featureProperties, |  | ||||||
|                 layoutSource, |  | ||||||
|                 layout |  | ||||||
|             ) |  | ||||||
| 
 | 
 | ||||||
|             this.changes = new Changes( |             this.changes = new Changes( | ||||||
|                 { |                 { | ||||||
|  | @ -333,10 +327,10 @@ export default class ThemeViewState implements SpecialVisualizationState { | ||||||
|             return sorted |             return sorted | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|         const lastClick = (this.lastClickObject = new LastClickFeatureSource( |         this.lastClickObject = new LastClickFeatureSource( | ||||||
|             this.mapProperties.lastClickLocation, |             this.mapProperties.lastClickLocation, | ||||||
|             this.layout |             this.layout | ||||||
|         )) |         ) | ||||||
| 
 | 
 | ||||||
|         this.osmObjectDownloader = new OsmObjectDownloader( |         this.osmObjectDownloader = new OsmObjectDownloader( | ||||||
|             this.osmConnection.Backend(), |             this.osmConnection.Backend(), | ||||||
|  | @ -359,6 +353,7 @@ export default class ThemeViewState implements SpecialVisualizationState { | ||||||
|             this.osmConnection, |             this.osmConnection, | ||||||
|             this.changes |             this.changes | ||||||
|         ) |         ) | ||||||
|  |         this.favourites = new FavouritesFeatureSource(this) | ||||||
| 
 | 
 | ||||||
|         this.initActors() |         this.initActors() | ||||||
|         this.drawSpecialLayers() |         this.drawSpecialLayers() | ||||||
|  | @ -472,6 +467,7 @@ export default class ThemeViewState implements SpecialVisualizationState { | ||||||
|         this.selectedLayer.setData(layer) |         this.selectedLayer.setData(layer) | ||||||
|         this.selectedElement.setData(toSelect) |         this.selectedElement.setData(toSelect) | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|     private initHotkeys() { |     private initHotkeys() { | ||||||
|         Hotkeys.RegisterHotkey( |         Hotkeys.RegisterHotkey( | ||||||
|             { nomod: "Escape", onUp: true }, |             { 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((_) => { |         this.mapProperties.lastKeyNavigation.addCallbackAndRunD((_) => { | ||||||
|             Hotkeys.RegisterHotkey( |             Hotkeys.RegisterHotkey( | ||||||
|                 { |                 { | ||||||
|  |  | ||||||
|  | @ -9,7 +9,7 @@ | ||||||
|     const uiElem = typeof construct === "function" ? construct() : construct |     const uiElem = typeof construct === "function" ? construct() : construct | ||||||
|     html = uiElem?.ConstructElement() |     html = uiElem?.ConstructElement() | ||||||
|     if (html !== undefined) { |     if (html !== undefined) { | ||||||
|       elem.replaceWith(html) |       elem?.replaceWith(html) | ||||||
|     } |     } | ||||||
|   }) |   }) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -49,14 +49,14 @@ | ||||||
| 
 | 
 | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <div class="px-1 my-1 border-2 border-dashed border-gray-300 rounded flex justify-between items-center"> | <div class="px-1 my-1 border-2 border-dashed border-gray-300 rounded grid grid-cols-3 items-center no-weblate"> | ||||||
|   <h3 on:click={() => select()} class="cursor-pointer ml-1 m-0"> |   <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} /> |     <TagRenderingAnswer extraClasses="underline" config={titleConfig} layer={favConfig} selectedElement={feature} {tags} /> | ||||||
|   </h3> |   </button> | ||||||
| 
 | 
 | ||||||
|   {$distance} |   {$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} |     {#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)} |       {#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"}> |         <div class={titleIconConfig.renderIconClass ?? "flex h-8 w-8 items-center"}> | ||||||
|  | @ -71,6 +71,7 @@ | ||||||
|         </div> |         </div> | ||||||
|       {/if} |       {/if} | ||||||
|     {/each} |     {/each} | ||||||
|  |      | ||||||
|     <button on:click={() => center()} class="p-1" ><Center class="w-6 h-6"/></button> |     <button on:click={() => center()} class="p-1" ><Center class="w-6 h-6"/></button> | ||||||
|   </div> |   </div> | ||||||
| </div> | </div> | ||||||
|  |  | ||||||
|  | @ -1,19 +1,58 @@ | ||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
|   import type { SpecialVisualizationState } from "../SpecialVisualization"; |   import type { SpecialVisualizationState } from "../SpecialVisualization"; | ||||||
|   import FavouriteSummary from "./FavouriteSummary.svelte"; |   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 |    * A panel showing all your favourites | ||||||
|    */ |    */ | ||||||
|   export let state: SpecialVisualizationState; |   export let state: SpecialVisualizationState; | ||||||
|   let favourites = state.favourites.allFavourites; |   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> | </script> | ||||||
| 
 | 
 | ||||||
| <div class="flex flex-col"> | <div class="flex flex-col" on:keypress={(e) => console.log("Got keypress", e)}> | ||||||
|   You marked {$favourites.length} locations as a favourite location. |   <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)} |   {#each $favourites as feature (feature.properties.id)} | ||||||
|     <FavouriteSummary {feature} {state} /> |     <FavouriteSummary {feature} {state} /> | ||||||
|   {/each} |   {/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> | </div> | ||||||
|  |  | ||||||
|  | @ -280,6 +280,16 @@ button.disabled:hover, .button.disabled:hover { | ||||||
|     color: unset; |     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 { | .interactive button.disabled svg path, .interactive .button.disabled svg path { | ||||||
|     fill: var(--interactive-foreground) !important;; |     fill: var(--interactive-foreground) !important;; | ||||||
| } | } | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue