forked from MapComplete/MapComplete
Merge pull request #1731 from pietervdvn/feature/favourites
Feature/favourites
This commit is contained in:
commit
4197ec0055
80 changed files with 2715 additions and 1059 deletions
|
|
@ -6,13 +6,21 @@ import { Changes } from "../Osm/Changes"
|
|||
import { OsmConnection } from "../Osm/OsmConnection"
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import SimpleMetaTagger from "../SimpleMetaTagger"
|
||||
import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore"
|
||||
import { Feature } from "geojson"
|
||||
import { OsmTags } from "../../Models/OsmFeature"
|
||||
import OsmObjectDownloader from "../Osm/OsmObjectDownloader"
|
||||
import { IndexedFeatureSource } from "../FeatureSource/FeatureSource"
|
||||
import { Utils } from "../../Utils"
|
||||
|
||||
interface TagsUpdaterState {
|
||||
selectedElement: UIEventSource<Feature>
|
||||
featureProperties: { getStore: (id: string) => UIEventSource<Record<string, string>> }
|
||||
changes: Changes
|
||||
osmConnection: OsmConnection
|
||||
layout: LayoutConfig
|
||||
osmObjectDownloader: OsmObjectDownloader
|
||||
indexedFeatures: IndexedFeatureSource
|
||||
}
|
||||
export default class SelectedElementTagsUpdater {
|
||||
private static readonly metatags = new Set([
|
||||
"timestamp",
|
||||
|
|
@ -23,38 +31,18 @@ export default class SelectedElementTagsUpdater {
|
|||
"id",
|
||||
])
|
||||
|
||||
private readonly state: {
|
||||
selectedElement: UIEventSource<Feature>
|
||||
featureProperties: FeaturePropertiesStore
|
||||
changes: Changes
|
||||
osmConnection: OsmConnection
|
||||
layout: LayoutConfig
|
||||
osmObjectDownloader: OsmObjectDownloader
|
||||
indexedFeatures: IndexedFeatureSource
|
||||
}
|
||||
|
||||
constructor(state: {
|
||||
selectedElement: UIEventSource<Feature>
|
||||
featureProperties: FeaturePropertiesStore
|
||||
indexedFeatures: IndexedFeatureSource
|
||||
changes: Changes
|
||||
osmConnection: OsmConnection
|
||||
layout: LayoutConfig
|
||||
osmObjectDownloader: OsmObjectDownloader
|
||||
}) {
|
||||
this.state = state
|
||||
constructor(state: TagsUpdaterState) {
|
||||
state.osmConnection.isLoggedIn.addCallbackAndRun((isLoggedIn) => {
|
||||
if (!isLoggedIn && !Utils.runningFromConsole) {
|
||||
return
|
||||
}
|
||||
this.installCallback()
|
||||
this.installCallback(state)
|
||||
// We only have to do this once...
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
private installCallback() {
|
||||
const state = this.state
|
||||
private installCallback(state: TagsUpdaterState) {
|
||||
state.selectedElement.addCallbackAndRunD(async (s) => {
|
||||
let id = s.properties?.id
|
||||
if (!id) {
|
||||
|
|
@ -94,7 +82,7 @@ export default class SelectedElementTagsUpdater {
|
|||
oldFeature.geometry = newGeometry
|
||||
state.featureProperties.getStore(id)?.ping()
|
||||
}
|
||||
this.applyUpdate(latestTags, id)
|
||||
SelectedElementTagsUpdater.applyUpdate(latestTags, id, state)
|
||||
|
||||
console.log("Updated", id)
|
||||
} catch (e) {
|
||||
|
|
@ -102,8 +90,7 @@ export default class SelectedElementTagsUpdater {
|
|||
}
|
||||
})
|
||||
}
|
||||
private applyUpdate(latestTags: OsmTags, id: string) {
|
||||
const state = this.state
|
||||
public static applyUpdate(latestTags: OsmTags, id: string, state: TagsUpdaterState) {
|
||||
try {
|
||||
const leftRightSensitive = state.layout.isLeftRightSensitive()
|
||||
|
||||
|
|
@ -162,11 +149,16 @@ export default class SelectedElementTagsUpdater {
|
|||
}
|
||||
|
||||
if (somethingChanged) {
|
||||
console.log("Detected upstream changes to the object when opening it, updating...")
|
||||
console.log(
|
||||
"Detected upstream changes to the object " +
|
||||
id +
|
||||
" when opening it, updating..."
|
||||
)
|
||||
currentTagsSource.ping()
|
||||
} else {
|
||||
console.debug("Fetched latest tags for ", id, "but detected no changes")
|
||||
}
|
||||
return currentTags
|
||||
} catch (e) {
|
||||
console.error("Updating the tags of selected element ", id, "failed due to", e)
|
||||
}
|
||||
|
|
|
|||
220
src/Logic/FeatureSource/Sources/FavouritesFeatureSource.ts
Normal file
220
src/Logic/FeatureSource/Sources/FavouritesFeatureSource.ts
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
import StaticFeatureSource from "./StaticFeatureSource"
|
||||
import { Feature } from "geojson"
|
||||
import { Store, Stores, UIEventSource } from "../../UIEventSource"
|
||||
import { OsmConnection } from "../../Osm/OsmConnection"
|
||||
import { OsmId } from "../../../Models/OsmFeature"
|
||||
import { GeoOperations } from "../../GeoOperations"
|
||||
import { IndexedFeatureSource } from "../FeatureSource"
|
||||
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
|
||||
*/
|
||||
export default class FavouritesFeatureSource extends StaticFeatureSource {
|
||||
public static readonly prefix = "mapcomplete-favourite-"
|
||||
private readonly _osmConnection: OsmConnection
|
||||
private readonly _detectedIds: Store<string[]>
|
||||
|
||||
/**
|
||||
* All favourites, including the ones which are filtered away because they are already displayed
|
||||
*/
|
||||
public readonly allFavourites: Store<Feature[]>
|
||||
|
||||
constructor(state: SpecialVisualizationState) {
|
||||
const features: Store<Feature[]> = Stores.ListStabilized(
|
||||
state.osmConnection.preferencesHandler.preferences.map((prefs) => {
|
||||
const feats: Feature[] = []
|
||||
const allIds = new Set<string>()
|
||||
for (const key in prefs) {
|
||||
if (!key.startsWith(FavouritesFeatureSource.prefix)) {
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
const feat = FavouritesFeatureSource.ExtractFavourite(key, prefs)
|
||||
if (!feat) {
|
||||
continue
|
||||
}
|
||||
feats.push(feat)
|
||||
allIds.add(feat.properties.id)
|
||||
} catch (e) {
|
||||
console.error("Could not create favourite from", key, "due to", e)
|
||||
}
|
||||
}
|
||||
return feats
|
||||
})
|
||||
)
|
||||
|
||||
const featuresWithoutAlreadyPresent = features.map((features) =>
|
||||
features.filter(
|
||||
(feat) => !state.layout.layers.some((l) => l.id === feat.properties._orig_layer)
|
||||
)
|
||||
)
|
||||
|
||||
super(featuresWithoutAlreadyPresent)
|
||||
this.allFavourites = features
|
||||
|
||||
this._osmConnection = state.osmConnection
|
||||
this._detectedIds = Stores.ListStabilized(
|
||||
features.map((feats) => feats.map((f) => f.properties.id))
|
||||
)
|
||||
let allFeatures = state.indexedFeatures
|
||||
this._detectedIds.addCallbackAndRunD((detected) =>
|
||||
this.markFeatures(detected, state.featureProperties, allFeatures)
|
||||
)
|
||||
// We use the indexedFeatureSource as signal to update
|
||||
allFeatures.features.map((_) =>
|
||||
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 {
|
||||
const id = key.substring(FavouritesFeatureSource.prefix.length)
|
||||
const osmId = id.replace("-", "/")
|
||||
if (id.indexOf("-property-") > 0 || id.endsWith("-layer") || id.endsWith("-theme")) {
|
||||
return undefined
|
||||
}
|
||||
const geometry = <[number, number]>JSON.parse(prefs[key])
|
||||
const properties = FavouritesFeatureSource.getPropertiesFor(prefs, id)
|
||||
properties._orig_layer = prefs[FavouritesFeatureSource.prefix + id + "-layer"]
|
||||
properties._orig_theme = prefs[FavouritesFeatureSource.prefix + id + "-theme"]
|
||||
|
||||
properties.id = osmId
|
||||
properties._favourite = "yes"
|
||||
return {
|
||||
type: "Feature",
|
||||
properties,
|
||||
geometry: {
|
||||
type: "Point",
|
||||
coordinates: geometry,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
private static getPropertiesFor(
|
||||
prefs: Record<string, string>,
|
||||
id: string
|
||||
): Record<string, string> {
|
||||
const properties: Record<string, string> = {}
|
||||
const minLength = FavouritesFeatureSource.prefix.length + id.length + "-property-".length
|
||||
for (const key in prefs) {
|
||||
if (key.length < minLength) {
|
||||
continue
|
||||
}
|
||||
if (!key.startsWith(FavouritesFeatureSource.prefix + id)) {
|
||||
continue
|
||||
}
|
||||
const propertyName = key.substring(minLength).replaceAll("__", ":")
|
||||
properties[propertyName] = prefs[key]
|
||||
}
|
||||
return properties
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets all the (normal) properties as the feature is updated
|
||||
*/
|
||||
private updatePropertiesOfFavourite(properties: Record<string, string>) {
|
||||
const id = properties?.id?.replace("/", "-")
|
||||
if (!id) {
|
||||
return
|
||||
}
|
||||
console.log("Updating store for", id)
|
||||
for (const key in properties) {
|
||||
const pref = this._osmConnection.GetPreference(
|
||||
"favourite-" + id + "-property-" + key.replaceAll(":", "__")
|
||||
)
|
||||
const v = properties[key]
|
||||
if (v === "" || !v) {
|
||||
continue
|
||||
}
|
||||
pref.setData("" + v)
|
||||
}
|
||||
}
|
||||
|
||||
public removeFavourite(feature: Feature, tags?: UIEventSource<Record<string, string>>) {
|
||||
const id = feature.properties.id.replace("/", "-")
|
||||
const pref = this._osmConnection.GetPreference("favourite-" + id)
|
||||
this._osmConnection.preferencesHandler.removeAllWithPrefix("mapcomplete-favourite-" + id)
|
||||
if (tags) {
|
||||
delete tags.data._favourite
|
||||
tags.ping()
|
||||
}
|
||||
}
|
||||
|
||||
public markAsFavourite(
|
||||
feature: Feature,
|
||||
layer: string,
|
||||
theme: string,
|
||||
tags: UIEventSource<Record<string, string> & { id: OsmId }>,
|
||||
isFavourite: boolean = true
|
||||
) {
|
||||
{
|
||||
if (!isFavourite) {
|
||||
this.removeFavourite(feature, tags)
|
||||
return
|
||||
}
|
||||
const id = tags.data.id.replace("/", "-")
|
||||
const pref = this._osmConnection.GetPreference("favourite-" + id)
|
||||
const center = GeoOperations.centerpointCoordinates(feature)
|
||||
pref.setData(JSON.stringify(center))
|
||||
this._osmConnection.GetPreference("favourite-" + id + "-layer").setData(layer)
|
||||
this._osmConnection.GetPreference("favourite-" + id + "-theme").setData(theme)
|
||||
this.updatePropertiesOfFavourite(tags.data)
|
||||
}
|
||||
tags.data._favourite = "yes"
|
||||
tags.ping()
|
||||
}
|
||||
|
||||
private markFeatures(
|
||||
detected: string[],
|
||||
featureProperties: { getStore(id: string): UIEventSource<Record<string, string>> },
|
||||
allFeatures: IndexedFeatureSource
|
||||
) {
|
||||
const feature = allFeatures.features.data
|
||||
for (const f of feature) {
|
||||
const id = f.properties.id
|
||||
if (!id) {
|
||||
continue
|
||||
}
|
||||
const store = featureProperties.getStore(id)
|
||||
const origValue = store.data._favourite
|
||||
if (detected.indexOf(id) >= 0) {
|
||||
if (origValue !== "yes") {
|
||||
store.data._favourite = "yes"
|
||||
store.ping()
|
||||
}
|
||||
} else {
|
||||
if (origValue) {
|
||||
store.data._favourite = ""
|
||||
store.ping()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,10 +6,14 @@ import FilteringFeatureSource from "./FilteringFeatureSource"
|
|||
import LayerState from "../../State/LayerState"
|
||||
|
||||
export default class NearbyFeatureSource implements FeatureSource {
|
||||
private readonly _result = new UIEventSource<Feature[]>(undefined)
|
||||
|
||||
public readonly features: Store<Feature[]>
|
||||
private readonly _targetPoint: Store<{ lon: number; lat: number }>
|
||||
private readonly _numberOfNeededFeatures: number
|
||||
private readonly _layerState?: LayerState
|
||||
private readonly _currentZoom: Store<number>
|
||||
private readonly _allSources: Store<{ feat: Feature; d: number }[]>[] = []
|
||||
|
||||
constructor(
|
||||
targetPoint: Store<{ lon: number; lat: number }>,
|
||||
|
|
@ -18,43 +22,46 @@ export default class NearbyFeatureSource implements FeatureSource {
|
|||
layerState?: LayerState,
|
||||
currentZoom?: Store<number>
|
||||
) {
|
||||
this._layerState = layerState
|
||||
this._targetPoint = targetPoint.stabilized(100)
|
||||
this._numberOfNeededFeatures = numberOfNeededFeatures
|
||||
this._currentZoom = currentZoom.stabilized(500)
|
||||
|
||||
const allSources: Store<{ feat: Feature; d: number }[]>[] = []
|
||||
let minzoom = 999
|
||||
|
||||
const result = new UIEventSource<Feature[]>(undefined)
|
||||
this.features = Stores.ListStabilized(result)
|
||||
|
||||
function update() {
|
||||
let features: { feat: Feature; d: number }[] = []
|
||||
for (const src of allSources) {
|
||||
features.push(...src.data)
|
||||
}
|
||||
features.sort((a, b) => a.d - b.d)
|
||||
if (numberOfNeededFeatures !== undefined) {
|
||||
features = features.slice(0, numberOfNeededFeatures)
|
||||
}
|
||||
result.setData(features.map((f) => f.feat))
|
||||
}
|
||||
this.features = Stores.ListStabilized(this._result)
|
||||
|
||||
sources.forEach((source, layer) => {
|
||||
const flayer = layerState?.filteredLayers.get(layer)
|
||||
minzoom = Math.min(minzoom, flayer.layerDef.minzoom)
|
||||
const calcSource = this.createSource(
|
||||
source.features,
|
||||
flayer.layerDef.minzoom,
|
||||
flayer.isDisplayed
|
||||
)
|
||||
calcSource.addCallbackAndRunD((features) => {
|
||||
update()
|
||||
})
|
||||
allSources.push(calcSource)
|
||||
this.registerSource(source, layer)
|
||||
})
|
||||
}
|
||||
|
||||
public registerSource(source: FeatureSource, layerId: string) {
|
||||
const flayer = this._layerState?.filteredLayers.get(layerId)
|
||||
if (!flayer) {
|
||||
return
|
||||
}
|
||||
const calcSource = this.createSource(
|
||||
source.features,
|
||||
flayer.layerDef.minzoom,
|
||||
flayer.isDisplayed
|
||||
)
|
||||
calcSource.addCallbackAndRunD((features) => {
|
||||
this.update()
|
||||
})
|
||||
this._allSources.push(calcSource)
|
||||
}
|
||||
|
||||
private update() {
|
||||
let features: { feat: Feature; d: number }[] = []
|
||||
for (const src of this._allSources) {
|
||||
features.push(...src.data)
|
||||
}
|
||||
features.sort((a, b) => a.d - b.d)
|
||||
if (this._numberOfNeededFeatures !== undefined) {
|
||||
features = features.slice(0, this._numberOfNeededFeatures)
|
||||
}
|
||||
this._result.setData(features.map((f) => f.feat))
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts the given source by distance, slices down to the required number
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -501,147 +501,43 @@ export class GeoOperations {
|
|||
)
|
||||
}
|
||||
|
||||
public static IdentifieCommonSegments(coordinatess: [number, number][][]): {
|
||||
originalIndex: number
|
||||
segmentShardWith: number[]
|
||||
coordinates: []
|
||||
}[] {
|
||||
// An edge. Note that the edge might be reversed to fix the sorting condition: start[0] < end[0] && (start[0] != end[0] || start[0] < end[1])
|
||||
type edge = {
|
||||
start: [number, number]
|
||||
end: [number, number]
|
||||
intermediate: [number, number][]
|
||||
members: { index: number; isReversed: boolean }[]
|
||||
/**
|
||||
* Given a list of points, convert into a GPX-list, e.g. for favourites
|
||||
* @param locations
|
||||
* @param title
|
||||
*/
|
||||
public static toGpxPoints(
|
||||
locations: Feature<Point, { date?: string; altitude?: number | string }>[],
|
||||
title?: string
|
||||
) {
|
||||
title = title?.trim()
|
||||
if (title === undefined || title === "") {
|
||||
title = "Created with MapComplete"
|
||||
}
|
||||
|
||||
// The strategy:
|
||||
// 1. Index _all_ edges from _every_ linestring. Index them by starting key, gather which relations run over them
|
||||
// 2. Join these edges back together - as long as their membership groups are the same
|
||||
// 3. Convert to results
|
||||
|
||||
const allEdgesByKey = new Map<string, edge>()
|
||||
|
||||
for (let index = 0; index < coordinatess.length; index++) {
|
||||
const coordinates = coordinatess[index]
|
||||
for (let i = 0; i < coordinates.length - 1; i++) {
|
||||
const c0 = coordinates[i]
|
||||
const c1 = coordinates[i + 1]
|
||||
const isReversed = c0[0] > c1[0] || (c0[0] == c1[0] && c0[1] > c1[1])
|
||||
|
||||
let key: string
|
||||
if (isReversed) {
|
||||
key = "" + c1 + ";" + c0
|
||||
} else {
|
||||
key = "" + c0 + ";" + c1
|
||||
title = Utils.EncodeXmlValue(title)
|
||||
const trackPoints: string[] = []
|
||||
for (const l of locations) {
|
||||
let trkpt = ` <wpt lat="${l.geometry.coordinates[1]}" lon="${l.geometry.coordinates[0]}">`
|
||||
for (const key in l.properties) {
|
||||
const keyCleaned = key.replaceAll(":", "__")
|
||||
trkpt += ` <${keyCleaned}>${l.properties[key]}</${keyCleaned}>\n`
|
||||
if (key === "website") {
|
||||
trkpt += ` <link>${l.properties[key]}</link>\n`
|
||||
}
|
||||
const member = { index, isReversed }
|
||||
if (allEdgesByKey.has(key)) {
|
||||
allEdgesByKey.get(key).members.push(member)
|
||||
continue
|
||||
}
|
||||
|
||||
let edge: edge
|
||||
if (!isReversed) {
|
||||
edge = {
|
||||
start: c0,
|
||||
end: c1,
|
||||
members: [member],
|
||||
intermediate: [],
|
||||
}
|
||||
} else {
|
||||
edge = {
|
||||
start: c1,
|
||||
end: c0,
|
||||
members: [member],
|
||||
intermediate: [],
|
||||
}
|
||||
}
|
||||
allEdgesByKey.set(key, edge)
|
||||
}
|
||||
trkpt += " </wpt>\n"
|
||||
trackPoints.push(trkpt)
|
||||
}
|
||||
|
||||
// Lets merge them back together!
|
||||
|
||||
let didMergeSomething = false
|
||||
let allMergedEdges = Array.from(allEdgesByKey.values())
|
||||
const allEdgesByStartPoint = new Map<string, edge[]>()
|
||||
for (const edge of allMergedEdges) {
|
||||
edge.members.sort((m0, m1) => m0.index - m1.index)
|
||||
|
||||
const kstart = edge.start + ""
|
||||
if (!allEdgesByStartPoint.has(kstart)) {
|
||||
allEdgesByStartPoint.set(kstart, [])
|
||||
}
|
||||
allEdgesByStartPoint.get(kstart).push(edge)
|
||||
}
|
||||
|
||||
function membersAreCompatible(first: edge, second: edge): boolean {
|
||||
// There must be an exact match between the members
|
||||
if (first.members === second.members) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (first.members.length !== second.members.length) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Members are sorted and have the same length, so we can check quickly
|
||||
for (let i = 0; i < first.members.length; i++) {
|
||||
const m0 = first.members[i]
|
||||
const m1 = second.members[i]
|
||||
if (m0.index !== m1.index || m0.isReversed !== m1.isReversed) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Allrigth, they are the same, lets mark this permanently
|
||||
second.members = first.members
|
||||
return true
|
||||
}
|
||||
|
||||
do {
|
||||
didMergeSomething = false
|
||||
// We use 'allMergedEdges' as our running list
|
||||
const consumed = new Set<edge>()
|
||||
for (const edge of allMergedEdges) {
|
||||
// Can we make this edge longer at the end?
|
||||
if (consumed.has(edge)) {
|
||||
continue
|
||||
}
|
||||
|
||||
console.log("Considering edge", edge)
|
||||
const matchingEndEdges = allEdgesByStartPoint.get(edge.end + "")
|
||||
console.log("Matchign endpoints:", matchingEndEdges)
|
||||
if (matchingEndEdges === undefined) {
|
||||
continue
|
||||
}
|
||||
|
||||
for (let i = 0; i < matchingEndEdges.length; i++) {
|
||||
const endEdge = matchingEndEdges[i]
|
||||
|
||||
if (consumed.has(endEdge)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (!membersAreCompatible(edge, endEdge)) {
|
||||
continue
|
||||
}
|
||||
|
||||
// We can make the segment longer!
|
||||
didMergeSomething = true
|
||||
console.log("Merging ", edge, "with ", endEdge)
|
||||
edge.intermediate.push(edge.end)
|
||||
edge.end = endEdge.end
|
||||
consumed.add(endEdge)
|
||||
matchingEndEdges.splice(i, 1)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
allMergedEdges = allMergedEdges.filter((edge) => !consumed.has(edge))
|
||||
} while (didMergeSomething)
|
||||
|
||||
return []
|
||||
const header =
|
||||
'<gpx version="1.1" creator="mapcomplete.org" xmlns="http://www.topografix.com/GPX/1/1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">'
|
||||
return (
|
||||
header +
|
||||
"\n<name>" +
|
||||
title +
|
||||
"</name>\n<trk><trkseg>\n" +
|
||||
trackPoints.join("\n") +
|
||||
"\n</trkseg></trk></gpx>"
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -107,7 +107,8 @@ export class ImageUploadManager {
|
|||
title,
|
||||
description,
|
||||
file,
|
||||
targetKey
|
||||
targetKey,
|
||||
tags.data["_orig_theme"]
|
||||
)
|
||||
if (!isNaN(Number(featureId))) {
|
||||
// This is a map note
|
||||
|
|
@ -126,7 +127,8 @@ export class ImageUploadManager {
|
|||
title: string,
|
||||
description: string,
|
||||
blob: File,
|
||||
targetKey: string | undefined
|
||||
targetKey: string | undefined,
|
||||
theme?: string
|
||||
): Promise<LinkImageAction> {
|
||||
this.increaseCountFor(this._uploadStarted, featureId)
|
||||
const properties = this._featureProperties.getStore(featureId)
|
||||
|
|
@ -148,7 +150,7 @@ export class ImageUploadManager {
|
|||
console.log("Uploading done, creating action for", featureId)
|
||||
key = targetKey ?? key
|
||||
const action = new LinkImageAction(featureId, key, value, properties, {
|
||||
theme: this._layout.id,
|
||||
theme: theme ?? this._layout.id,
|
||||
changeType: "add-image",
|
||||
})
|
||||
this.increaseCountFor(this._uploadFinished, featureId)
|
||||
|
|
|
|||
|
|
@ -12,6 +12,10 @@ export class OsmPreferences {
|
|||
"all-osm-preferences",
|
||||
{}
|
||||
)
|
||||
/**
|
||||
* A map containing the individual preference sources
|
||||
* @private
|
||||
*/
|
||||
private readonly preferenceSources = new Map<string, UIEventSource<string>>()
|
||||
private auth: any
|
||||
private userDetails: UIEventSource<UserDetails>
|
||||
|
|
@ -21,7 +25,10 @@ export class OsmPreferences {
|
|||
this.auth = auth
|
||||
this.userDetails = osmConnection.userDetails
|
||||
const self = this
|
||||
osmConnection.OnLoggedIn(() => self.UpdatePreferences())
|
||||
osmConnection.OnLoggedIn(() => {
|
||||
self.UpdatePreferences(true)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -72,11 +79,19 @@ export class OsmPreferences {
|
|||
let i = 0
|
||||
while (str !== "") {
|
||||
if (str === undefined || str === "undefined") {
|
||||
source.setData(undefined)
|
||||
throw (
|
||||
"Got 'undefined' or a literal string containing 'undefined' for a long preference with name " +
|
||||
key
|
||||
)
|
||||
}
|
||||
if (str === "undefined") {
|
||||
source.setData(undefined)
|
||||
throw (
|
||||
"Got a literal string containing 'undefined' for a long preference with name " +
|
||||
key
|
||||
)
|
||||
}
|
||||
if (i > 100) {
|
||||
throw "This long preference is getting very long... "
|
||||
}
|
||||
|
|
@ -197,7 +212,7 @@ export class OsmPreferences {
|
|||
})
|
||||
}
|
||||
|
||||
private UpdatePreferences() {
|
||||
private UpdatePreferences(forceUpdate?: boolean) {
|
||||
const self = this
|
||||
this.auth.xhr(
|
||||
{
|
||||
|
|
@ -210,11 +225,22 @@ export class OsmPreferences {
|
|||
return
|
||||
}
|
||||
const prefs = value.getElementsByTagName("preference")
|
||||
const seenKeys = new Set<string>()
|
||||
for (let i = 0; i < prefs.length; i++) {
|
||||
const pref = prefs[i]
|
||||
const k = pref.getAttribute("k")
|
||||
const v = pref.getAttribute("v")
|
||||
self.preferences.data[k] = v
|
||||
seenKeys.add(k)
|
||||
}
|
||||
if (forceUpdate) {
|
||||
for (let key in self.preferences.data) {
|
||||
if (seenKeys.has(key)) {
|
||||
continue
|
||||
}
|
||||
console.log("Deleting key", key, "as we didn't find it upstream")
|
||||
delete self.preferences.data[key]
|
||||
}
|
||||
}
|
||||
|
||||
// We merge all the preferences: new keys are uploaded
|
||||
|
|
@ -285,4 +311,14 @@ export class OsmPreferences {
|
|||
}
|
||||
)
|
||||
}
|
||||
|
||||
removeAllWithPrefix(prefix: string) {
|
||||
for (const key in this.preferences.data) {
|
||||
if (key.startsWith(prefix)) {
|
||||
this.GetPreference(key, "", { prefix: "" }).setData(undefined)
|
||||
console.log("Clearing preference", key)
|
||||
}
|
||||
}
|
||||
this.preferences.ping()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -294,6 +294,9 @@ export default class UserRelatedState {
|
|||
osmConnection.preferencesHandler.preferences.addCallback((newPrefs) => {
|
||||
for (const k in newPrefs) {
|
||||
const v = newPrefs[k]
|
||||
if (v === "undefined" || !v) {
|
||||
continue
|
||||
}
|
||||
if (k.endsWith("-combined-length")) {
|
||||
const l = Number(v)
|
||||
const key = k.substring(0, k.length - "length".length)
|
||||
|
|
@ -308,7 +311,6 @@ export default class UserRelatedState {
|
|||
}
|
||||
|
||||
amendedPrefs.ping()
|
||||
console.log("Amended prefs are:", amendedPrefs.data)
|
||||
})
|
||||
const translationMode = osmConnection.GetPreference("translation-mode")
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { Or } from "./Or"
|
|||
import { TagUtils } from "./TagUtils"
|
||||
import { Tag } from "./Tag"
|
||||
import { RegexTag } from "./RegexTag"
|
||||
import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson"
|
||||
|
||||
export class And extends TagsFilter {
|
||||
public and: TagsFilter[]
|
||||
|
|
@ -72,6 +73,10 @@ export class And extends TagsFilter {
|
|||
return allChoices
|
||||
}
|
||||
|
||||
asJson(): TagConfigJson {
|
||||
return { and: this.and.map((a) => a.asJson()) }
|
||||
}
|
||||
|
||||
asHumanString(linkToWiki: boolean, shorten: boolean, properties: Record<string, string>) {
|
||||
return this.and
|
||||
.map((t) => {
|
||||
|
|
@ -228,6 +233,15 @@ export class And extends TagsFilter {
|
|||
return And.construct(newAnds)
|
||||
}
|
||||
|
||||
/**
|
||||
* const raw = {"and": [{"or":["leisure=playground","playground!=forest"]},{"or":["leisure=playground","playground!=forest"]}]}
|
||||
* const parsed = TagUtils.Tag(raw)
|
||||
* parsed.optimize().asJson() // => {"or":["leisure=playground","playground!=forest"]}
|
||||
*
|
||||
* const raw = {"and": [{"and":["advertising=screen"]}, {"and":["advertising~*"]}]}]
|
||||
* const parsed = TagUtils.Tag(raw)
|
||||
* parsed.optimize().asJson() // => "advertising=screen"
|
||||
*/
|
||||
optimize(): TagsFilter | boolean {
|
||||
if (this.and.length === 0) {
|
||||
return true
|
||||
|
|
@ -289,9 +303,17 @@ export class And extends TagsFilter {
|
|||
optimized.splice(i, 1)
|
||||
i--
|
||||
}
|
||||
} else if (v !== opt.value) {
|
||||
// detected an internal conflict
|
||||
return false
|
||||
} else {
|
||||
if (!v.match(opt.value)) {
|
||||
// We _know_ that for the key of the RegexTag `opt`, the value will be `v`.
|
||||
// As such, if `opt.value` cannot match `v`, we detected an internal conflict and can fail
|
||||
|
||||
return false
|
||||
} else {
|
||||
// Another tag already provided a _stricter_ value then this regex, so we can remove this one!
|
||||
optimized.splice(i, 1)
|
||||
i--
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -369,10 +391,13 @@ export class And extends TagsFilter {
|
|||
const elements = containedOr.or.filter(
|
||||
(candidate) => !commonValues.some((cv) => cv.shadows(candidate))
|
||||
)
|
||||
newOrs.push(Or.construct(elements))
|
||||
if (elements.length > 0) {
|
||||
newOrs.push(Or.construct(elements))
|
||||
}
|
||||
}
|
||||
if (newOrs.length > 0) {
|
||||
commonValues.push(And.construct(newOrs))
|
||||
}
|
||||
|
||||
commonValues.push(And.construct(newOrs))
|
||||
const result = new Or(commonValues).optimize()
|
||||
if (result === false) {
|
||||
return false
|
||||
|
|
|
|||
|
|
@ -1,18 +1,23 @@
|
|||
import { TagsFilter } from "./TagsFilter"
|
||||
import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson"
|
||||
import { Tag } from "./Tag"
|
||||
|
||||
export default class ComparingTag implements TagsFilter {
|
||||
private readonly _key: string
|
||||
private readonly _predicate: (value: string) => boolean
|
||||
private readonly _representation: string
|
||||
private readonly _representation: "<" | ">" | "<=" | ">="
|
||||
private readonly _boundary: string
|
||||
|
||||
constructor(
|
||||
key: string,
|
||||
predicate: (value: string | undefined) => boolean,
|
||||
representation: string = ""
|
||||
representation: "<" | ">" | "<=" | ">=",
|
||||
boundary: string
|
||||
) {
|
||||
this._key = key
|
||||
this._predicate = predicate
|
||||
this._representation = representation
|
||||
this._boundary = boundary
|
||||
}
|
||||
|
||||
asChange(properties: Record<string, string>): { k: string; v: string }[] {
|
||||
|
|
@ -20,15 +25,64 @@ export default class ComparingTag implements TagsFilter {
|
|||
}
|
||||
|
||||
asHumanString(linkToWiki: boolean, shorten: boolean, properties: Record<string, string>) {
|
||||
return this._key + this._representation
|
||||
return this._key + this._representation + this._boundary
|
||||
}
|
||||
|
||||
asOverpass(): string[] {
|
||||
throw "A comparable tag can not be used as overpass filter"
|
||||
}
|
||||
|
||||
/**
|
||||
* const tg = new ComparingTag("key", value => (Number(value) < 42), "<", "42")
|
||||
* const tg0 = new ComparingTag("key", value => (Number(value) < 42), "<", "42")
|
||||
* const tg1 = new ComparingTag("key", value => (Number(value) <= 42), "<=", "42")
|
||||
* const against = new ComparingTag("key", value => (Number(value) > 0), ">", "0")
|
||||
* tg.shadows(new Tag("key", "41")) // => true
|
||||
* tg.shadows(new Tag("key", "0")) // => true
|
||||
* tg.shadows(new Tag("key", "43")) // => false
|
||||
* tg.shadows(new Tag("key", "0")) // => true
|
||||
* tg.shadows(tg) // => true
|
||||
* tg.shadows(tg0) // => true
|
||||
* tg.shadows(against) // => false
|
||||
* tg1.shadows(tg0) // => true
|
||||
* tg0.shadows(tg1) // => false
|
||||
*
|
||||
*/
|
||||
shadows(other: TagsFilter): boolean {
|
||||
return other === this
|
||||
if (other === this) {
|
||||
return true
|
||||
}
|
||||
if (other instanceof ComparingTag) {
|
||||
if (other._key !== this._key) {
|
||||
return false
|
||||
}
|
||||
const selfDesc = this._representation === "<" || this._representation === "<="
|
||||
const otherDesc = other._representation === "<" || other._representation === "<="
|
||||
if (selfDesc !== otherDesc) {
|
||||
return false
|
||||
}
|
||||
if (
|
||||
this._boundary === other._boundary &&
|
||||
this._representation === other._representation
|
||||
) {
|
||||
return true
|
||||
}
|
||||
if (this._predicate(other._boundary)) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
if (other instanceof Tag) {
|
||||
if (other.key !== this._key) {
|
||||
return false
|
||||
}
|
||||
if (this.matchesProperties({ [other.key]: other.value })) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
isUsableAsAnswer(): boolean {
|
||||
|
|
@ -38,7 +92,7 @@ export default class ComparingTag implements TagsFilter {
|
|||
/**
|
||||
* Checks if the properties match
|
||||
*
|
||||
* const t = new ComparingTag("key", (x => Number(x) < 42))
|
||||
* const t = new ComparingTag("key", (x => Number(x) < 42), "<", "42")
|
||||
* t.matchesProperties({key: 42}) // => false
|
||||
* t.matchesProperties({key: 41}) // => true
|
||||
* t.matchesProperties({key: 0}) // => true
|
||||
|
|
@ -56,6 +110,10 @@ export default class ComparingTag implements TagsFilter {
|
|||
return []
|
||||
}
|
||||
|
||||
asJson(): TagConfigJson {
|
||||
return this._key + this._representation
|
||||
}
|
||||
|
||||
optimize(): TagsFilter | boolean {
|
||||
return this
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { TagsFilter } from "./TagsFilter"
|
||||
import { TagUtils } from "./TagUtils"
|
||||
import { And } from "./And"
|
||||
import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson"
|
||||
|
||||
export class Or extends TagsFilter {
|
||||
public or: TagsFilter[]
|
||||
|
|
@ -27,6 +28,10 @@ export class Or extends TagsFilter {
|
|||
return false
|
||||
}
|
||||
|
||||
asJson(): TagConfigJson {
|
||||
return { or: this.or.map((o) => o.asJson()) }
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* import {Tag} from "./Tag";
|
||||
|
|
@ -157,6 +162,12 @@ export class Or extends TagsFilter {
|
|||
return Or.construct(newOrs)
|
||||
}
|
||||
|
||||
/**
|
||||
* const raw = {"or": [{"and":["leisure=playground","playground!=forest"]},{"and":["leisure=playground","playground!=forest"]}]}
|
||||
* const parsed = TagUtils.Tag(raw)
|
||||
* parsed.optimize().asJson() // => {"and":["leisure=playground","playground!=forest"]}
|
||||
*
|
||||
*/
|
||||
optimize(): TagsFilter | boolean {
|
||||
if (this.or.length === 0) {
|
||||
return false
|
||||
|
|
@ -174,9 +185,9 @@ export class Or extends TagsFilter {
|
|||
const newOrs: TagsFilter[] = []
|
||||
let containedAnds: And[] = []
|
||||
for (const tf of optimized) {
|
||||
if (tf instanceof Or) {
|
||||
if (tf["or"]) {
|
||||
// expand all the nested ors...
|
||||
newOrs.push(...tf.or)
|
||||
newOrs.push(...tf["or"])
|
||||
} else if (tf instanceof And) {
|
||||
// partition of all the ands
|
||||
containedAnds.push(tf)
|
||||
|
|
@ -191,7 +202,7 @@ export class Or extends TagsFilter {
|
|||
const cleanedContainedANds: And[] = []
|
||||
outer: for (let containedAnd of containedAnds) {
|
||||
for (const known of newOrs) {
|
||||
// input for optimazation: (K=V | (X=Y & K=V))
|
||||
// input for optimization: (K=V | (X=Y & K=V))
|
||||
// containedAnd: (X=Y & K=V)
|
||||
// newOrs (and thus known): (K=V) --> false
|
||||
const cleaned = containedAnd.removePhraseConsideredKnown(known, false)
|
||||
|
|
@ -236,16 +247,21 @@ export class Or extends TagsFilter {
|
|||
const elements = containedAnd.and.filter(
|
||||
(candidate) => !commonValues.some((cv) => cv.shadows(candidate))
|
||||
)
|
||||
if (elements.length == 0) {
|
||||
continue
|
||||
}
|
||||
newAnds.push(And.construct(elements))
|
||||
}
|
||||
if (newAnds.length > 0) {
|
||||
commonValues.push(Or.construct(newAnds))
|
||||
}
|
||||
|
||||
commonValues.push(Or.construct(newAnds))
|
||||
const result = new And(commonValues).optimize()
|
||||
if (result === true) {
|
||||
return true
|
||||
} else if (result === false) {
|
||||
// neutral element: skip
|
||||
} else {
|
||||
} else if (commonValues.length > 0) {
|
||||
newOrs.push(And.construct(commonValues))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { Tag } from "./Tag"
|
||||
import { TagsFilter } from "./TagsFilter"
|
||||
import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson"
|
||||
|
||||
export class RegexTag extends TagsFilter {
|
||||
public readonly key: RegExp | string
|
||||
|
|
@ -11,6 +12,9 @@ export class RegexTag extends TagsFilter {
|
|||
super()
|
||||
this.key = key
|
||||
this.value = value
|
||||
if (this.value instanceof RegExp && ("" + this.value).startsWith("^(^(")) {
|
||||
throw "Detected a duplicate start marker ^(^( in a regextag:" + this.value
|
||||
}
|
||||
this.invert = invert
|
||||
this.matchesEmpty = RegexTag.doesMatch("", this.value)
|
||||
}
|
||||
|
|
@ -41,11 +45,21 @@ export class RegexTag extends TagsFilter {
|
|||
return possibleRegex.test(fromTag)
|
||||
}
|
||||
|
||||
private static source(r: string | RegExp) {
|
||||
private static source(r: string | RegExp, includeStartMarker: boolean = true) {
|
||||
if (typeof r === "string") {
|
||||
return r
|
||||
}
|
||||
return r.source
|
||||
if (r === undefined) {
|
||||
return undefined
|
||||
}
|
||||
const src = r.source
|
||||
if (includeStartMarker) {
|
||||
return src
|
||||
}
|
||||
if (src.startsWith("^(") && src.endsWith(")$")) {
|
||||
return src.substring(2, src.length - 2)
|
||||
}
|
||||
return src
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -82,6 +96,24 @@ export class RegexTag extends TagsFilter {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* import { TagUtils } from "./TagUtils";
|
||||
*
|
||||
* const t = TagUtils.Tag("a~b")
|
||||
* t.asJson() // => "a~b"
|
||||
*
|
||||
* const t = TagUtils.Tag("a=")
|
||||
* t.asJson() // => "a="
|
||||
*/
|
||||
asJson(): TagConfigJson {
|
||||
const v = RegexTag.source(this.value, false)
|
||||
if (typeof this.key === "string") {
|
||||
const oper = typeof this.value === "string" ? "=" : "~"
|
||||
return `${this.key}${this.invert ? "!" : ""}${oper}${v}`
|
||||
}
|
||||
return `${this.key.source}${this.invert ? "!" : ""}~~${v}`
|
||||
}
|
||||
|
||||
isUsableAsAnswer(): boolean {
|
||||
return false
|
||||
}
|
||||
|
|
@ -293,7 +325,7 @@ export class RegexTag extends TagsFilter {
|
|||
if (typeof this.key === "string") {
|
||||
return [this.key]
|
||||
}
|
||||
throw "Key cannot be determined as it is a regex"
|
||||
return []
|
||||
}
|
||||
|
||||
usedTags(): { key: string; value: string }[] {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { TagsFilter } from "./TagsFilter"
|
||||
import { Tag } from "./Tag"
|
||||
import { Utils } from "../../Utils"
|
||||
import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson"
|
||||
|
||||
/**
|
||||
* The substituting-tag uses the tags of a feature a variables and replaces them.
|
||||
|
|
@ -45,6 +46,10 @@ export default class SubstitutingTag implements TagsFilter {
|
|||
)
|
||||
}
|
||||
|
||||
asJson(): TagConfigJson {
|
||||
return this._key + (this._invert ? "!" : "") + ":=" + this._value
|
||||
}
|
||||
|
||||
asOverpass(): string[] {
|
||||
throw "A variable with substitution can not be used to query overpass"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { Utils } from "../../Utils"
|
||||
import { TagsFilter } from "./TagsFilter"
|
||||
import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson"
|
||||
|
||||
export class Tag extends TagsFilter {
|
||||
public key: string
|
||||
|
|
@ -67,6 +68,10 @@ export class Tag extends TagsFilter {
|
|||
return [`["${this.key}"="${this.value}"]`]
|
||||
}
|
||||
|
||||
asJson(): TagConfigJson {
|
||||
return this.key + "=" + this.value
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
const t = new Tag("key", "value")
|
||||
|
|
|
|||
|
|
@ -15,13 +15,14 @@ type Tags = Record<string, string>
|
|||
export type UploadableTag = Tag | SubstitutingTag | And
|
||||
|
||||
export class TagUtils {
|
||||
public static readonly comparators: ReadonlyArray<[string, (a: number, b: number) => boolean]> =
|
||||
[
|
||||
["<=", (a, b) => a <= b],
|
||||
[">=", (a, b) => a >= b],
|
||||
["<", (a, b) => a < b],
|
||||
[">", (a, b) => a > b],
|
||||
]
|
||||
public static readonly comparators: ReadonlyArray<
|
||||
["<" | ">" | "<=" | ">=", (a: number, b: number) => boolean]
|
||||
> = [
|
||||
["<=", (a, b) => a <= b],
|
||||
[">=", (a, b) => a >= b],
|
||||
["<", (a, b) => a < b],
|
||||
[">", (a, b) => a > b],
|
||||
]
|
||||
public static modeDocumentation: Record<
|
||||
string,
|
||||
{ name: string; docs: string; uploadable?: boolean; overpassSupport: boolean }
|
||||
|
|
@ -324,6 +325,14 @@ export class TagUtils {
|
|||
return tags
|
||||
}
|
||||
|
||||
static optimzeJson(json: TagConfigJson): TagConfigJson | boolean {
|
||||
const optimized = TagUtils.Tag(json).optimize()
|
||||
if (optimized === true || optimized === false) {
|
||||
return optimized
|
||||
}
|
||||
return optimized.asJson()
|
||||
}
|
||||
|
||||
/**
|
||||
* Given multiple tagsfilters which can be used as answer, will take the tags with the same keys together as set.
|
||||
*
|
||||
|
|
@ -735,11 +744,10 @@ export class TagUtils {
|
|||
const tag = json as string
|
||||
for (const [operator, comparator] of TagUtils.comparators) {
|
||||
if (tag.indexOf(operator) >= 0) {
|
||||
const split = Utils.SplitFirst(tag, operator)
|
||||
|
||||
let val = Number(split[1].trim())
|
||||
const split = Utils.SplitFirst(tag, operator).map((v) => v.trim())
|
||||
let val = Number(split[1])
|
||||
if (isNaN(val)) {
|
||||
val = new Date(split[1].trim()).getTime()
|
||||
val = new Date(split[1]).getTime()
|
||||
}
|
||||
|
||||
const f = (value: string | number | undefined) => {
|
||||
|
|
@ -762,7 +770,7 @@ export class TagUtils {
|
|||
}
|
||||
return comparator(b, val)
|
||||
}
|
||||
return new ComparingTag(split[0], f, operator + val)
|
||||
return new ComparingTag(split[0], f, operator, "" + val)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -861,6 +869,27 @@ export class TagUtils {
|
|||
return TagUtils.keyCounts.keys[key]
|
||||
}
|
||||
|
||||
public static GetPopularity(tag: TagsFilter): number | undefined {
|
||||
if (tag instanceof And) {
|
||||
return Math.min(...Utils.NoNull(tag.and.map((t) => TagUtils.GetPopularity(t)))) - 1
|
||||
}
|
||||
if (tag instanceof Or) {
|
||||
return Math.max(...Utils.NoNull(tag.or.map((t) => TagUtils.GetPopularity(t)))) + 1
|
||||
}
|
||||
if (tag instanceof Tag) {
|
||||
return TagUtils.GetCount(tag.key, tag.value)
|
||||
}
|
||||
if (tag instanceof RegexTag) {
|
||||
const key = tag.key
|
||||
if (key instanceof RegExp || tag.invert || tag.isNegative()) {
|
||||
return undefined
|
||||
}
|
||||
return TagUtils.GetCount(key)
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
private static order(a: TagsFilter, b: TagsFilter, usePopularity: boolean): number {
|
||||
const rta = a instanceof RegexTag
|
||||
const rtb = b instanceof RegexTag
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson"
|
||||
|
||||
export abstract class TagsFilter {
|
||||
abstract asOverpass(): string[]
|
||||
|
||||
|
|
@ -17,6 +19,8 @@ export abstract class TagsFilter {
|
|||
properties: Record<string, string>
|
||||
): string
|
||||
|
||||
abstract asJson(): TagConfigJson
|
||||
|
||||
abstract usedKeys(): string[]
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ export class MangroveIdentity {
|
|||
const keypairEventSource = new UIEventSource<CryptoKeyPair>(undefined)
|
||||
this.keypair = keypairEventSource
|
||||
mangroveIdentity.addCallbackAndRunD(async (data) => {
|
||||
if (data === "") {
|
||||
if (!data) {
|
||||
return
|
||||
}
|
||||
const keypair = await MangroveReviews.jwkToKeypair(JSON.parse(data))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue