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 + "-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
|
||||||
|
|
|
@ -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 []
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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