Favourites: improve overview, update all features from OSM when loading

This commit is contained in:
Pieter Vander Vennet 2023-12-03 20:03:47 +01:00
parent 35f8b9d8f2
commit 56a23deb2d
10 changed files with 231 additions and 234 deletions

View file

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

View file

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

View file

@ -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 + "-layer").setData(layer)
this._osmConnection.GetPreference("favourite-" + id + "-theme").setData(theme) this._osmConnection.GetPreference("favourite-" + id + "-theme").setData(theme)
for (const key in tags.data) { this.updatePropertiesOfFavourite(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.data._favourite = "yes"
tags.ping() tags.ping()
} else {
delete tags.data._favourite
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

View file

@ -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 =
'<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">'
// Lets merge them back together! return (
header +
let didMergeSomething = false "\n<name>" +
let allMergedEdges = Array.from(allEdgesByKey.values()) title +
const allEdgesByStartPoint = new Map<string, edge[]>() "</name>\n<trk><trkseg>\n" +
for (const edge of allMergedEdges) { trackPoints.join("\n") +
edge.members.sort((m0, m1) => m0.index - m1.index) "\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 []
} }
/** /**

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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