Refactoring: port import flow

This commit is contained in:
Pieter Vander Vennet 2023-06-01 02:52:21 +02:00
parent 8ed4da4e9d
commit ace7caada1
48 changed files with 852 additions and 574 deletions

View file

@ -414,6 +414,9 @@ class GetParsed implements ExtraFunction {
if (value === undefined) {
return undefined
}
if(typeof value !== "string"){
return value
}
try {
const parsed = JSON.parse(value)
if (parsed === null) {

View file

@ -1,5 +1,5 @@
import { IdbLocalStorage } from "../../Web/IdbLocalStorage"
import { UIEventSource } from "../../UIEventSource"
import {IdbLocalStorage} from "../../Web/IdbLocalStorage"
import {UIEventSource} from "../../UIEventSource"
/**
* A class which allows to read/write a tile to local storage.
@ -10,23 +10,25 @@ import { UIEventSource } from "../../UIEventSource"
*/
export default class TileLocalStorage<T> {
private static perLayer: Record<string, TileLocalStorage<any>> = {}
private static readonly useIndexedDb = typeof indexedDB !== "undefined"
private readonly _layername: string
private readonly inUse = new UIEventSource(false)
private readonly cachedSources: Record<number, UIEventSource<T> & { flush: () => void }> = {}
private readonly _maxAgeSeconds: number;
private static readonly useIndexedDb = typeof indexedDB !== "undefined"
private constructor(layername: string) {
private constructor(layername: string, maxAgeSeconds: number) {
this._layername = layername
this._maxAgeSeconds = maxAgeSeconds;
}
public static construct<T>(backend: string, layername: string): TileLocalStorage<T> {
public static construct<T>(backend: string, layername: string, maxAgeS: number): TileLocalStorage<T> {
const key = backend + "_" + layername
const cached = TileLocalStorage.perLayer[key]
if (cached) {
return cached
}
const tls = new TileLocalStorage<T>(key)
const tls = new TileLocalStorage<T>(key, maxAgeS)
TileLocalStorage.perLayer[key] = tls
return tls
}
@ -50,13 +52,15 @@ export default class TileLocalStorage<T> {
}
private async SetIdb(tileIndex: number, data: any): Promise<void> {
if(!TileLocalStorage.useIndexedDb){
if (!TileLocalStorage.useIndexedDb) {
return
}
try {
await this.inUse.AsPromise((inUse) => !inUse)
this.inUse.setData(true)
await IdbLocalStorage.SetDirectly(this._layername + "_" + tileIndex, data)
await IdbLocalStorage.SetDirectly(this._layername + "_" + tileIndex + "_date", Date.now())
this.inUse.setData(false)
} catch (e) {
console.error(
@ -72,10 +76,24 @@ export default class TileLocalStorage<T> {
}
}
private GetIdb(tileIndex: number): Promise<any> {
if(!TileLocalStorage.useIndexedDb){
private async GetIdb(tileIndex: number): Promise<any> {
if (!TileLocalStorage.useIndexedDb) {
return undefined
}
return IdbLocalStorage.GetDirectly(this._layername + "_" + tileIndex)
const date = <any>await IdbLocalStorage.GetDirectly(this._layername + "_" + tileIndex + "_date")
const maxAge = this._maxAgeSeconds
const timeDiff = Date.now() - date
if (timeDiff >= maxAge) {
console.log("Dropping cache for", this._layername, tileIndex, "out of date")
await IdbLocalStorage.SetDirectly(this._layername + "_" + tileIndex, undefined)
return undefined
}
const data = await IdbLocalStorage.GetDirectly(this._layername + "_" + tileIndex)
return <any>data
}
invalidate(zoomlevel: number, tileIndex) {
this.getTileSource(tileIndex).setData(undefined)
}
}

View file

@ -10,11 +10,6 @@ export interface WritableFeatureSource<T extends Feature = Feature> extends Feat
features: UIEventSource<T[]>
}
export interface Tiled {
tileIndex: number
bbox: BBox
}
/**
* A feature source which only contains features for the defined layer
*/

View file

@ -10,6 +10,7 @@ import FeatureSourceMerger from "./FeatureSourceMerger"
import DynamicGeoJsonTileSource from "../TiledFeatureSource/DynamicGeoJsonTileSource"
import {BBox} from "../../BBox"
import LocalStorageFeatureSource from "../TiledFeatureSource/LocalStorageFeatureSource"
import FullNodeDatabaseSource from "../TiledFeatureSource/FullNodeDatabaseSource";
/**
* This source will fetch the needed data from various sources for the given layout.
@ -27,7 +28,8 @@ export default class LayoutSource extends FeatureSourceMerger {
featureSwitches: FeatureSwitchState,
mapProperties: { bounds: Store<BBox>; zoom: Store<number> },
backend: string,
isDisplayed: (id: string) => Store<boolean>
isDisplayed: (id: string) => Store<boolean>,
fullNodeDatabaseSource?: FullNodeDatabaseSource
) {
const { bounds, zoom } = mapProperties
// remove all 'special' layers
@ -39,6 +41,7 @@ export default class LayoutSource extends FeatureSourceMerger {
(l) =>
new LocalStorageFeatureSource(backend, l.id, 15, mapProperties, {
isActive: isDisplayed(l.id),
maxAge: l.maxAgeOfCache
})
)
@ -55,7 +58,8 @@ export default class LayoutSource extends FeatureSourceMerger {
bounds,
zoom,
backend,
featureSwitches
featureSwitches,
fullNodeDatabaseSource
)
const geojsonSources: FeatureSource[] = geojsonlayers.map((l) =>
LayoutSource.setupGeojsonSource(l, mapProperties, isDisplayed(l.id))
@ -96,7 +100,8 @@ export default class LayoutSource extends FeatureSourceMerger {
bounds: Store<BBox>,
zoom: Store<number>,
backend: string,
featureSwitches: FeatureSwitchState
featureSwitches: FeatureSwitchState,
fullNodeDatabase: FullNodeDatabaseSource
): OsmFeatureSource | undefined {
if (osmLayers.length == 0) {
return undefined
@ -121,8 +126,8 @@ export default class LayoutSource extends FeatureSourceMerger {
bounds,
backend,
isActive,
patchRelations: true
patchRelations: true,
fullNodeDatabase
})
}

View file

@ -7,6 +7,7 @@ import { TagsFilter } from "../../Tags/TagsFilter"
import { Feature } from "geojson"
import FeatureSourceMerger from "../Sources/FeatureSourceMerger"
import OsmObjectDownloader from "../../Osm/OsmObjectDownloader"
import FullNodeDatabaseSource from "../TiledFeatureSource/FullNodeDatabaseSource";
/**
* If a tile is needed (requested via the UIEventSource in the constructor), will download the appropriate tile and pass it via 'handleTile'
@ -16,9 +17,19 @@ export default class OsmFeatureSource extends FeatureSourceMerger {
private readonly isActive: Store<boolean>
private readonly _backend: string
private readonly allowedTags: TagsFilter
private options: {
bounds: Store<BBox>
readonly allowedFeatures: TagsFilter
backend?: "https://openstreetmap.org/" | string
/**
* If given: this featureSwitch will not update if the store contains 'false'
*/
isActive?: Store<boolean>,
patchRelations?: true | boolean,
fullNodeDatabase?: FullNodeDatabaseSource
};
public readonly isRunning: UIEventSource<boolean> = new UIEventSource<boolean>(false)
public rawDataHandlers: ((osmJson: any, tileIndex: number) => void)[] = []
private readonly _downloadedTiles: Set<number> = new Set<number>()
private readonly _downloadedData: Feature[][] = []
@ -35,9 +46,11 @@ export default class OsmFeatureSource extends FeatureSourceMerger {
* If given: this featureSwitch will not update if the store contains 'false'
*/
isActive?: Store<boolean>,
patchRelations?: true | boolean
patchRelations?: true | boolean,
fullNodeDatabase?: FullNodeDatabaseSource
}) {
super()
this.options = options;
this._bounds = options.bounds
this.allowedTags = options.allowedFeatures
this.isActive = options.isActive ?? new ImmutableStore(true)
@ -119,7 +132,7 @@ export default class OsmFeatureSource extends FeatureSourceMerger {
return feature
}
private async LoadTile(z, x, y): Promise<void> {
private async LoadTile(z: number, x: number, y: number): Promise<void> {
console.log("OsmFeatureSource: loading ", z, x, y, "from", this._backend)
if (z >= 22) {
throw "This is an absurd high zoom level"
@ -141,9 +154,7 @@ export default class OsmFeatureSource extends FeatureSourceMerger {
try {
const osmJson = await Utils.downloadJsonCached(url, 2000)
try {
this.rawDataHandlers.forEach((handler) =>
handler(osmJson, Tiles.tile_index(z, x, y))
)
this.options?.fullNodeDatabase?.handleOsmJson(osmJson, z, x, y)
let features = <Feature<any, { id: string }>[]>OsmToGeoJson(
osmJson,
// @ts-ignore
@ -181,7 +192,7 @@ export default class OsmFeatureSource extends FeatureSourceMerger {
y,
"due to",
e,
"; retrying with smaller bounds"
e === "rate limited" ? "; stopping now" : "; retrying with smaller bounds"
)
if (e === "rate limited") {
return

View file

@ -1,31 +1,25 @@
import {FeatureSource, FeatureSourceForLayer, Tiled } from "../FeatureSource"
import { OsmNode, OsmObject, OsmWay } from "../../Osm/OsmObject"
import SimpleFeatureSource from "../Sources/SimpleFeatureSource"
import FilteredLayer from "../../../Models/FilteredLayer"
import { UIEventSource } from "../../UIEventSource"
import { OsmTags } from "../../../Models/OsmFeature";
import { BBox } from "../../BBox";
import { Feature, Point } from "geojson";
import {OsmNode, OsmObject, OsmWay} from "../../Osm/OsmObject"
import {UIEventSource} from "../../UIEventSource"
import {BBox} from "../../BBox";
import StaticFeatureSource from "../Sources/StaticFeatureSource";
import {Tiles} from "../../../Models/TileRange";
export default class FullNodeDatabaseSource {
public readonly loadedTiles = new Map<number, FeatureSource & Tiled>()
private readonly onTileLoaded: (tile: Tiled & FeatureSourceForLayer) => void
private readonly layer: FilteredLayer
private readonly loadedTiles = new Map<number, Map<number, OsmNode>>()
private readonly nodeByIds = new Map<number, OsmNode>()
private readonly parentWays = new Map<number, UIEventSource<OsmWay[]>>()
constructor(layer: FilteredLayer, onTileLoaded: (tile: Tiled & FeatureSourceForLayer) => void) {
this.onTileLoaded = onTileLoaded
this.layer = layer
if (this.layer === undefined) {
throw "Layer is undefined"
}
}
private smallestZoom = 99
private largestZoom = 0
public handleOsmJson(osmJson: any, tileId: number) {
public handleOsmJson(osmJson: any, z: number, x: number, y: number) : void {
const allObjects = OsmObject.ParseObjects(osmJson.elements)
const nodesById = new Map<number, OsmNode>()
this.smallestZoom = Math.min(this.smallestZoom, z)
this.largestZoom = Math.max(this.largestZoom, z)
for (const osmObj of allObjects) {
if (osmObj.type !== "node") {
continue
@ -59,10 +53,9 @@ export default class FullNodeDatabaseSource {
osmNode.asGeoJson()
)
const featureSource = new SimpleFeatureSource(this.layer, tileId)
featureSource.features.setData(asGeojsonFeatures)
this.loadedTiles.set(tileId, featureSource)
this.onTileLoaded(featureSource)
const featureSource = new StaticFeatureSource(asGeojsonFeatures)
const tileId = Tiles.tile_index(z, x, y)
this.loadedTiles.set(tileId, nodesById)
}
/**
@ -76,7 +69,7 @@ export default class FullNodeDatabaseSource {
}
/**
* Gets the parent way list
* Gets all the ways that the given node is a part of
* @param nodeId
* @constructor
*/
@ -84,8 +77,20 @@ export default class FullNodeDatabaseSource {
return this.parentWays.get(nodeId)
}
getNodesWithin(bBox: BBox) : Feature<Point, OsmTags>[]{
// TODO
throw "TODO"
/**
* Gets (at least) all nodes which are part of this BBOX; might also return some nodes that fall outside of the bbox but are closeby
* @param bbox
*/
getNodesWithin(bbox: BBox) : Map<number, OsmNode>{
const allById = new Map<number, OsmNode>()
for (let z = this.smallestZoom; z < this.largestZoom; z++) {
const range = Tiles.tileRangeFrom(bbox, z)
Tiles.MapRange(range, (x, y ) => {
const tileId = Tiles.tile_index(z, x, y)
const nodesById = this.loadedTiles.get(tileId)
nodesById?.forEach((v,k) => allById.set(k,v))
})
}
return allById
}
}

View file

@ -1,9 +1,11 @@
import DynamicTileSource from "./DynamicTileSource"
import { Store } from "../../UIEventSource"
import { BBox } from "../../BBox"
import {Store} from "../../UIEventSource"
import {BBox} from "../../BBox"
import TileLocalStorage from "../Actors/TileLocalStorage"
import { Feature } from "geojson"
import {Feature} from "geojson"
import StaticFeatureSource from "../Sources/StaticFeatureSource"
import {constants} from "http2";
import HTTP_STATUS_CONTINUE = module
export default class LocalStorageFeatureSource extends DynamicTileSource {
constructor(
@ -15,18 +17,25 @@ export default class LocalStorageFeatureSource extends DynamicTileSource {
zoom: Store<number>
},
options?: {
isActive?: Store<boolean>
isActive?: Store<boolean>,
maxAge?: number // In seconds
}
) {
const storage = TileLocalStorage.construct<Feature[]>(backend, layername)
const storage = TileLocalStorage.construct<Feature[]>(backend, layername, options?.maxAge ?? 24 * 60 * 60)
super(
zoomlevel,
(tileIndex) =>
new StaticFeatureSource(
storage
.getTileSource(tileIndex)
.map((features) =>
features?.filter((f) => !f.properties.id.match(/(node|way)\/-[0-9]+/))
.mapD((features) => {
if (features.length === undefined) {
console.trace("These are not features:", features)
storage.invalidate(zoomlevel, tileIndex)
return []
}
return features.filter((f) => !f.properties.id.match(/(node|way)\/-[0-9]+/));
}
)
),
mapProperties,

View file

@ -9,6 +9,7 @@ import {IndexedFeatureSource} from "./FeatureSource/FeatureSource"
import OsmObjectDownloader from "./Osm/OsmObjectDownloader"
import {Utils} from "../Utils";
import {GeoJSONFeature} from "maplibre-gl";
import {UIEventSource} from "./UIEventSource";
/**
* Metatagging adds various tags to the elements, e.g. lat, lon, surface area, ...
@ -18,7 +19,7 @@ import {GeoJSONFeature} from "maplibre-gl";
export default class MetaTagging {
private static errorPrintCount = 0
private static readonly stopErrorOutputAt = 10
private static retaggingFuncCache = new Map<string, ((feature: Feature) => void)[]>()
private static retaggingFuncCache = new Map<string, ((feature: Feature, propertiesStore: UIEventSource<any>) => void)[]>()
constructor(state: {
layout: LayoutConfig
@ -27,19 +28,7 @@ export default class MetaTagging {
indexedFeatures: IndexedFeatureSource
featureProperties: FeaturePropertiesStore
}) {
const params: ExtraFuncParams = {
getFeatureById: (id) => state.indexedFeatures.featuresById.data.get(id),
getFeaturesWithin: (layerId, bbox) => {
if(layerId === '*' || layerId === null || layerId === undefined){
const feats: Feature[][] = []
state.perLayer.forEach((layer) => {
feats.push(layer.GetFeaturesWithin(bbox))
})
return feats
}
return [state.perLayer.get(layerId).GetFeaturesWithin(bbox)];
},
}
const params = MetaTagging.createExtraFuncParams(state)
for (const layer of state.layout.layers) {
if (layer.source === null) {
continue
@ -108,7 +97,7 @@ export default class MetaTagging {
// The calculated functions - per layer - which add the new keys
// Calculated functions are defined by the layer
const layerFuncs = this.createRetaggingFunc(layer, ExtraFunctions.constructHelpers(params))
const state: MetataggingState = { layout, osmObjectDownloader }
const state: MetataggingState = {layout, osmObjectDownloader}
let atLeastOneFeatureChanged = false
let strictlyEvaluated = 0
@ -117,15 +106,7 @@ export default class MetaTagging {
const tags = featurePropertiesStores?.getStore(feature.properties.id)
let somethingChanged = false
let definedTags = new Set(Object.getOwnPropertyNames(feature.properties))
if (layerFuncs !== undefined) {
let retaggingChanged = false
try {
retaggingChanged = layerFuncs(feature)
} catch (e) {
console.error(e)
}
somethingChanged = somethingChanged || retaggingChanged
}
for (const metatag of metatagsToApply) {
try {
if (!metatag.keys.some((key) => !(key in feature.properties))) {
@ -144,7 +125,7 @@ export default class MetaTagging {
if (options?.evaluateStrict) {
for (const key of metatag.keys) {
const evaluated = feature.properties[key]
if(evaluated !== undefined){
if (evaluated !== undefined) {
strictlyEvaluated++
}
}
@ -175,12 +156,18 @@ export default class MetaTagging {
)
}
}
if (layerFuncs !== undefined) {
try {
// We cannot do `somethingChanged || layerFuncs(feature)', due to the shortcutting behaviour it would not calculate the lazy functions
somethingChanged = layerFuncs(feature, tags) || somethingChanged
} catch (e) {
console.error(e)
}
}
if (somethingChanged) {
try {
featurePropertiesStores?.getStore(feature.properties.id)?.ping()
tags?.ping()
} catch (e) {
console.error("Could not ping a store for a changed property due to", e)
}
@ -190,6 +177,25 @@ export default class MetaTagging {
return atLeastOneFeatureChanged
}
public static createExtraFuncParams(state: {
indexedFeatures: IndexedFeatureSource,
perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer>
}) {
return {
getFeatureById: (id) => state.indexedFeatures.featuresById.data.get(id),
getFeaturesWithin: (layerId, bbox) => {
if (layerId === '*' || layerId === null || layerId === undefined) {
const feats: Feature[][] = []
state.perLayer.forEach((layer) => {
feats.push(layer.GetFeaturesWithin(bbox))
})
return feats
}
return [state.perLayer.get(layerId).GetFeaturesWithin(bbox)];
},
}
}
/**
* Creates a function that implements that calculates a property and adds this property onto the feature properties
* @param specification
@ -197,28 +203,28 @@ export default class MetaTagging {
* @param layerId
* @private
*/
private static createFunctionForFeature( [key, code, isStrict]: [string, string, boolean],
private static createFunctionForFeature([key, code, isStrict]: [string, string, boolean],
helperFunctions: Record<ExtraFuncType, (feature: Feature) => Function>,
layerId: string = "unkown layer"
): ((feature: GeoJSONFeature) => void) | undefined {
): ((feature: GeoJSONFeature, propertiesStore?: UIEventSource<any>) => void) | undefined {
if (code === undefined) {
return undefined
}
const calculateAndAssign: ((feat: GeoJSONFeature) => (string | undefined)) = (feat) => {
const calculateAndAssign: ((feat: GeoJSONFeature, store?: UIEventSource<any>) => string | any) = (feat, store) => {
try {
let result = new Function("feat", "{"+ExtraFunctions.types.join(", ")+"}", "return " + code + ";")(feat, helperFunctions)
let result = new Function("feat", "{" + ExtraFunctions.types.join(", ") + "}", "return " + code + ";")(feat, helperFunctions)
if (result === "") {
result = undefined
}
if (result !== undefined && typeof result !== "string") {
// Make sure it is a string!
result = JSON.stringify(result)
const oldValue= feat.properties[key]
if(oldValue == result){
return oldValue
}
delete feat.properties[key]
feat.properties[key] = result
store?.ping()
return result
} catch (e) {
if (MetaTagging.errorPrintCount < MetaTagging.stopErrorOutputAt) {
@ -250,13 +256,12 @@ export default class MetaTagging {
}
}
if(isStrict){
if (isStrict) {
return calculateAndAssign
}
return (feature: any) => {
return (feature: any, store?: UIEventSource<any>) => {
delete feature.properties[key]
Utils.AddLazyProperty(feature.properties, key, () => calculateAndAssign(feature))
return undefined
Utils.AddLazyProperty(feature.properties, key, () => calculateAndAssign(feature, store))
}
}
@ -266,19 +271,19 @@ export default class MetaTagging {
private static createRetaggingFunc(
layer: LayerConfig,
helpers: Record<ExtraFuncType, (feature: Feature) => Function>
): (feature: any) => boolean {
): (feature: Feature, tags: UIEventSource<Record<string, any>>) => boolean {
const calculatedTags: [string, string, boolean][] = layer.calculatedTags
if (calculatedTags === undefined || calculatedTags.length === 0) {
return undefined
}
let functions: ((feature: Feature) => void)[] = MetaTagging.retaggingFuncCache.get(layer.id)
let functions: ((feature: Feature, propertiesStore?: UIEventSource<any>) => void)[] = MetaTagging.retaggingFuncCache.get(layer.id)
if (functions === undefined) {
functions = calculatedTags.map(spec => this.createFunctionForFeature(spec, helpers, layer.id))
MetaTagging.retaggingFuncCache.set(layer.id, functions)
}
return (feature: Feature) => {
return (feature: Feature, store: UIEventSource<Record<string, any>>) => {
const tags = feature.properties
if (tags === undefined) {
return
@ -286,7 +291,7 @@ export default class MetaTagging {
try {
for (const f of functions) {
f(feature)
f(feature, store)
}
} catch (e) {
console.error("Invalid syntax in calculated tags or some other error: ", e)

View file

@ -1,4 +1,4 @@
import {OsmCreateAction} from "./OsmChangeAction"
import {OsmCreateAction, PreviewableAction} from "./OsmChangeAction"
import {Tag} from "../../Tags/Tag"
import {Changes} from "../Changes"
import {ChangeDescription} from "./ChangeDescription"
@ -6,7 +6,7 @@ import CreateNewWayAction from "./CreateNewWayAction"
import CreateWayWithPointReuseAction, {MergePointConfig} from "./CreateWayWithPointReuseAction"
import {And} from "../../Tags/And"
import {TagUtils} from "../../Tags/TagUtils"
import {IndexedFeatureSource} from "../../FeatureSource/FeatureSource"
import {FeatureSource, IndexedFeatureSource} from "../../FeatureSource/FeatureSource"
import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig";
import {Position} from "geojson";
import FullNodeDatabaseSource from "../../FeatureSource/TiledFeatureSource/FullNodeDatabaseSource";
@ -14,7 +14,7 @@ import FullNodeDatabaseSource from "../../FeatureSource/TiledFeatureSource/FullN
/**
* More or less the same as 'CreateNewWay', except that it'll try to reuse already existing points
*/
export default class CreateMultiPolygonWithPointReuseAction extends OsmCreateAction {
export default class CreateMultiPolygonWithPointReuseAction extends OsmCreateAction implements PreviewableAction {
public newElementId: string = undefined
public newElementIdNumber: number = undefined
private readonly _tags: Tag[]
@ -67,7 +67,6 @@ export default class CreateMultiPolygonWithPointReuseAction extends OsmCreateAct
}
protected async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
console.log("Running CMPWPRA")
const descriptions: ChangeDescription[] = []
descriptions.push(...(await this.createOuterWay.CreateChangeDescriptions(changes)))
for (const innerWay of this.createInnerWays) {
@ -103,4 +102,8 @@ export default class CreateMultiPolygonWithPointReuseAction extends OsmCreateAct
return descriptions
}
getPreview(): Promise<FeatureSource> {
return undefined
}
}

View file

@ -1,5 +1,5 @@
import { ChangeDescription } from "./ChangeDescription"
import { OsmCreateAction } from "./OsmChangeAction"
import {OsmCreateAction, PreviewableAction} from "./OsmChangeAction"
import { Changes } from "../Changes"
import { Tag } from "../../Tags/Tag"
import CreateNewNodeAction from "./CreateNewNodeAction"

View file

@ -1,4 +1,4 @@
import {OsmCreateAction} from "./OsmChangeAction"
import {OsmCreateAction, PreviewableAction} from "./OsmChangeAction"
import {Tag} from "../../Tags/Tag"
import {Changes} from "../Changes"
import {ChangeDescription} from "./ChangeDescription"
@ -56,7 +56,7 @@ interface CoordinateInfo {
/**
* More or less the same as 'CreateNewWay', except that it'll try to reuse already existing points
*/
export default class CreateWayWithPointReuseAction extends OsmCreateAction {
export default class CreateWayWithPointReuseAction extends OsmCreateAction implements PreviewableAction {
public newElementId: string = undefined
public newElementIdNumber: number = undefined
private readonly _tags: Tag[]

View file

@ -4,6 +4,7 @@
*/
import { Changes } from "../Changes"
import { ChangeDescription } from "./ChangeDescription"
import {FeatureSource} from "../../FeatureSource/FeatureSource";
export default abstract class OsmChangeAction {
public readonly trackStatistics: boolean
@ -35,3 +36,7 @@ export abstract class OsmCreateAction extends OsmChangeAction {
public newElementId: string
public newElementIdNumber: number
}
export interface PreviewableAction {
getPreview(): Promise<FeatureSource>
}

View file

@ -1,21 +1,21 @@
import OsmChangeAction from "./OsmChangeAction"
import { Changes } from "../Changes"
import { ChangeDescription } from "./ChangeDescription"
import { Tag } from "../../Tags/Tag"
import { FeatureSource } from "../../FeatureSource/FeatureSource"
import { OsmNode, OsmObject, OsmWay } from "../OsmObject"
import { GeoOperations } from "../../GeoOperations"
import OsmChangeAction, {PreviewableAction} from "./OsmChangeAction"
import {Changes} from "../Changes"
import {ChangeDescription} from "./ChangeDescription"
import {Tag} from "../../Tags/Tag"
import {FeatureSource} from "../../FeatureSource/FeatureSource"
import {OsmNode, OsmObject, OsmWay} from "../OsmObject"
import {GeoOperations} from "../../GeoOperations"
import StaticFeatureSource from "../../FeatureSource/Sources/StaticFeatureSource"
import CreateNewNodeAction from "./CreateNewNodeAction"
import ChangeTagAction from "./ChangeTagAction"
import { And } from "../../Tags/And"
import { Utils } from "../../../Utils"
import { OsmConnection } from "../OsmConnection"
import { Feature } from "@turf/turf"
import { Geometry, LineString, Point } from "geojson"
import {And} from "../../Tags/And"
import {Utils} from "../../../Utils"
import {OsmConnection} from "../OsmConnection"
import {Feature} from "@turf/turf"
import {Geometry, LineString, Point} from "geojson"
import FullNodeDatabaseSource from "../../FeatureSource/TiledFeatureSource/FullNodeDatabaseSource"
export default class ReplaceGeometryAction extends OsmChangeAction {
export default class ReplaceGeometryAction extends OsmChangeAction implements PreviewableAction{
/**
* The target feature - mostly used for the metadata
*/
@ -38,9 +38,14 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
private readonly identicalTo: number[]
private readonly newTags: Tag[] | undefined
/**
* Not really the 'new' element, but the target that has been applied.
* Added for compatibility with other systems
*/
public readonly newElementId: string
constructor(
state: {
osmConnection: OsmConnection
osmConnection: OsmConnection,
fullNodeDatabase?: FullNodeDatabaseSource
},
feature: any,
@ -55,6 +60,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
this.feature = feature
this.wayToReplaceId = wayToReplaceId
this.theme = options.theme
this.newElementId = wayToReplaceId
const geom = this.feature.geometry
let coordinates: [number, number][]
@ -81,7 +87,6 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
this.newTags = options.newTags
}
// noinspection JSUnusedGlobalSymbols
public async getPreview(): Promise<FeatureSource> {
const { closestIds, allNodesById, detachedNodes, reprojectedNodes } =
await this.GetClosestIds()
@ -455,6 +460,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
}
}
console.log("Adding tags", this.newTags,"to conflated way nr", this.wayToReplaceId)
if (this.newTags !== undefined && this.newTags.length > 0) {
const addExtraTags = new ChangeTagAction(
this.wayToReplaceId,

View file

@ -1,16 +1,16 @@
import { OsmNode, OsmObject, OsmRelation, OsmWay } from "./OsmObject"
import { Store, UIEventSource } from "../UIEventSource"
import {OsmNode, OsmObject, OsmRelation, OsmWay} from "./OsmObject"
import {Store, UIEventSource} from "../UIEventSource"
import Constants from "../../Models/Constants"
import OsmChangeAction from "./Actions/OsmChangeAction"
import { ChangeDescription, ChangeDescriptionTools } from "./Actions/ChangeDescription"
import { Utils } from "../../Utils"
import { LocalStorageSource } from "../Web/LocalStorageSource"
import {ChangeDescription, ChangeDescriptionTools} from "./Actions/ChangeDescription"
import {Utils} from "../../Utils"
import {LocalStorageSource} from "../Web/LocalStorageSource"
import SimpleMetaTagger from "../SimpleMetaTagger"
import { FeatureSource, IndexedFeatureSource } from "../FeatureSource/FeatureSource"
import { GeoLocationPointProperties } from "../State/GeoLocationState"
import { GeoOperations } from "../GeoOperations"
import { ChangesetHandler, ChangesetTag } from "./ChangesetHandler"
import { OsmConnection } from "./OsmConnection"
import {FeatureSource, IndexedFeatureSource} from "../FeatureSource/FeatureSource"
import {GeoLocationPointProperties} from "../State/GeoLocationState"
import {GeoOperations} from "../GeoOperations"
import {ChangesetHandler, ChangesetTag} from "./ChangesetHandler"
import {OsmConnection} from "./OsmConnection"
import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore"
import OsmObjectDownloader from "./OsmObjectDownloader"
@ -25,9 +25,9 @@ export class Changes {
public readonly state: { allElements?: IndexedFeatureSource; osmConnection: OsmConnection }
public readonly extraComment: UIEventSource<string> = new UIEventSource(undefined)
public readonly backend: string
public readonly isUploading = new UIEventSource(false)
private readonly historicalUserLocations?: FeatureSource
private _nextId: number = -1 // Newly assigned ID's are negative
public readonly isUploading = new UIEventSource(false)
private readonly previouslyCreated: OsmObject[] = []
private readonly _leftRightSensitive: boolean
private readonly _changesetHandler: ChangesetHandler
@ -246,11 +246,12 @@ export class Changes {
switch (change.type) {
case "node":
// @ts-ignore
const nlat = change.changes.lat
const nlat = Utils.Round7(change.changes.lat)
// @ts-ignore
const nlon = change.changes.lon
const nlon = Utils.Round7(change.changes.lon)
const n = <OsmNode>obj
if (n.lat !== nlat || n.lon !== nlon) {
console.log("Node moved:", n.lat, nlat, n.lon, nlon)
n.lat = nlat
n.lon = nlon
changed = true
@ -407,7 +408,7 @@ export class Changes {
neededIds.map(async (id) => {
try {
const osmObj = await downloader.DownloadObjectAsync(id)
return { id, osmObj }
return {id, osmObj}
} catch (e) {
console.error(
"Could not download OSM-object",
@ -421,7 +422,7 @@ export class Changes {
osmObjects = Utils.NoNull(osmObjects)
for (const { osmObj, id } of osmObjects) {
for (const {osmObj, id} of osmObjects) {
if (osmObj === "deleted") {
pending = pending.filter((ch) => ch.type + "/" + ch.id !== id)
}
@ -572,9 +573,9 @@ export class Changes {
)
console.log(
"Using current-open-changeset-" +
theme +
" from the preferences, got " +
openChangeset.data
theme +
" from the preferences, got " +
openChangeset.data
)
return await self.flushSelectChanges(pendingChanges, openChangeset)

View file

@ -129,9 +129,9 @@ export class ChangesetHandler {
const csId = await this.OpenChangeset(extraMetaTags)
openChangeset.setData(csId)
const changeset = generateChangeXML(csId, this._remappings)
console.trace(
console.log(
"Opened a new changeset (openChangeset.data is undefined):",
changeset
changeset, extraMetaTags
)
const changes = await this.UploadChange(csId, changeset)
const hasSpecialMotivationChanges = ChangesetHandler.rewriteMetaTags(

View file

@ -306,22 +306,26 @@ export default class SimpleMetaTaggers {
)
private static surfaceArea = new InlineMetaTagger(
{
keys: ["_surface", "_surface:ha"],
doc: "The surface area of the feature, in square meters and in hectare. Not set on points and ways",
keys: ["_surface"],
doc: "The surface area of the feature in square meters. Not set on points and ways",
isLazy: true,
},
(feature) => {
Object.defineProperty(feature.properties, "_surface", {
enumerable: false,
configurable: true,
get: () => {
const sqMeters = "" + GeoOperations.surfaceAreaInSqMeters(feature)
delete feature.properties["_surface"]
feature.properties["_surface"] = sqMeters
return sqMeters
},
Utils.AddLazyProperty(feature.properties, "_surface", () => {
return "" + GeoOperations.surfaceAreaInSqMeters(feature)
})
return true
}
)
private static surfaceAreaHa = new InlineMetaTagger(
{
keys: ["_surface:ha"],
doc: "The surface area of the feature in hectare. Not set on points and ways",
isLazy: true,
},
(feature) => {
Utils.AddLazyProperty(feature.properties, "_surface:ha", () => {
const sqMeters = GeoOperations.surfaceAreaInSqMeters(feature)
return "" + Math.floor(sqMeters / 1000) / 10
@ -581,6 +585,7 @@ export default class SimpleMetaTaggers {
SimpleMetaTaggers.latlon,
SimpleMetaTaggers.layerInfo,
SimpleMetaTaggers.surfaceArea,
SimpleMetaTaggers.surfaceAreaHa,
SimpleMetaTaggers.lngth,
SimpleMetaTaggers.canonicalize,
SimpleMetaTaggers.country,

View file

@ -1,5 +1,5 @@
import { Tag } from "./Tag"
import { TagsFilter } from "./TagsFilter"
import {Tag} from "./Tag"
import {TagsFilter} from "./TagsFilter"
export class RegexTag extends TagsFilter {
public readonly key: RegExp | string
@ -15,7 +15,20 @@ export class RegexTag extends TagsFilter {
this.matchesEmpty = RegexTag.doesMatch("", this.value)
}
private static doesMatch(fromTag: string, possibleRegex: string | RegExp): boolean {
/**
*
* Checks that the value provided by the object properties (`fromTag`) match the specified regex `possibleRegex
*
* RegexTag.doesMatch("abc", /abc/) // => true
* RegexTag.doesMatch("ab", /abc/) // => false
* RegexTag.doesMatch("", /.+/) // => false
* RegexTag.doesMatch("", new RegExp(".*")) // => true
*
* @param fromTag
* @param possibleRegex
* @private
*/
private static doesMatch(fromTag: string | number, possibleRegex: string | RegExp): boolean {
if (fromTag === undefined) {
return
}
@ -25,11 +38,8 @@ export class RegexTag extends TagsFilter {
if (typeof possibleRegex === "string") {
return fromTag === possibleRegex
}
if (typeof fromTag.match !== "function") {
console.error("Error: fromTag is not a regex: ", fromTag, possibleRegex)
throw "Error: fromTag is not a regex: " + fromTag + possibleRegex
}
return fromTag.match(possibleRegex) !== null
return possibleRegex.test(fromTag)
}
private static source(r: string | RegExp) {
@ -125,10 +135,38 @@ export class RegexTag extends TagsFilter {
*
* new RegexTag("key","value").matchesProperties({"otherkey":"value"}) // => false
* new RegexTag("key","value",true).matchesProperties({"otherkey":"something"}) // => true
*
* const v: string = <any> {someJson: ""}
* new RegexTag("key", new RegExp(".*")).matchesProperties({"key": v}) // => true
* new RegexTag("key", new RegExp(".*")).matchesProperties({"key": ""}) // => true
* new RegexTag("key", new RegExp(".*")).matchesProperties({"key": null}) // => true
* new RegexTag("key", new RegExp(".*")).matchesProperties({"key": undefined}) // => true
*
* const v: string = <any> {someJson: ""}
* new RegexTag("key", new RegExp(".+")).matchesProperties({"key": null}) // => false
* new RegexTag("key", new RegExp(".+")).matchesProperties({"key": undefined}) // => false
* new RegexTag("key", new RegExp(".+")).matchesProperties({"key": v}) // => false
* new RegexTag("key", new RegExp(".+")).matchesProperties({"key": ""}) // => false
*/
matchesProperties(tags: Record<string, string>): boolean {
if (typeof this.key === "string") {
const value = tags[this.key] ?? ""
const value = tags[this.key]
if(!value || value === ""){
// No tag is known, so we assume the empty string
// If this regexTag matches the empty string, we return true, otherwise false
// (Note: if inverted, we must reverse this)
return this.invert !== this.matchesEmpty
}
if(typeof value !== "string"){
if(typeof this.value !== "string"){
const regExp = this.value
if(regExp.source === ".*"){
// We match anything, and we do have a value
return !this.invert
}
return RegexTag.doesMatch(value, JSON.stringify(this.value)) != this.invert
}
}
return RegexTag.doesMatch(value, this.value) != this.invert
}

View file

@ -35,15 +35,27 @@ export class Tag extends TagsFilter {
* isEmpty.matchesProperties({"other_key": "value"}) // => true
* isEmpty.matchesProperties({"key": undefined}) // => true
*
* const isTrue = new Tag("key", "true")
* isTrue.matchesProperties({"key","true"}) // => true
* isTrue.matchesProperteis({"key", true}) // => true
*/
matchesProperties(properties: Record<string, string>): boolean {
const foundValue = properties[this.key]
let foundValue = properties[this.key]
if (foundValue === undefined && (this.value === "" || this.value === undefined)) {
// The tag was not found
// and it shouldn't be found!
return true
}
if(typeof foundValue !== "string"){
if(foundValue === true && (this.value === "true" || this.value === "yes" )){
return true
}
if(foundValue === false && (this.value === "false" || this.value === "no" )){
return true
}
foundValue = ""+foundValue
}
return foundValue === this.value
}

View file

@ -43,7 +43,7 @@ export class IdbLocalStorage {
return idb.set(key, copy)
}
static GetDirectly(key: string): Promise<void> {
static GetDirectly(key: string): Promise<any> {
return idb.get(key)
}
}