forked from MapComplete/MapComplete
Refactoring: port import flow
This commit is contained in:
parent
8ed4da4e9d
commit
ace7caada1
48 changed files with 852 additions and 574 deletions
|
@ -1,3 +1,4 @@
|
|||
|
||||
[//]: # (WARNING: this file is automatically generated. Please find the sources at the bottom and edit those sources)
|
||||
|
||||
Special tag renderings
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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[]
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,8 +14,8 @@ import {TagConfigJson} from "../Json/TagConfigJson"
|
|||
import PointRenderingConfigJson from "../Json/PointRenderingConfigJson"
|
||||
import LineRenderingConfigJson from "../Json/LineRenderingConfigJson"
|
||||
import ValidationUtils from "./ValidationUtils"
|
||||
import { RenderingSpecification } from "../../../UI/SpecialVisualization"
|
||||
import { QuestionableTagRenderingConfigJson } from "../Json/QuestionableTagRenderingConfigJson"
|
||||
import {RenderingSpecification} from "../../../UI/SpecialVisualization"
|
||||
import {QuestionableTagRenderingConfigJson} from "../Json/QuestionableTagRenderingConfigJson"
|
||||
|
||||
class ExpandFilter extends DesugaringStep<LayerConfigJson> {
|
||||
private static readonly predefinedFilters = ExpandFilter.load_filters()
|
||||
|
@ -201,7 +201,7 @@ class ExpandTagRendering extends Conversion<
|
|||
if (state.tagRenderings.has(name)) {
|
||||
return [state.tagRenderings.get(name)]
|
||||
}
|
||||
if(this._tagRenderingsByLabel.has(name)){
|
||||
if (this._tagRenderingsByLabel.has(name)) {
|
||||
return this._tagRenderingsByLabel.get(name)
|
||||
}
|
||||
|
||||
|
@ -437,11 +437,11 @@ class DetectInline extends DesugaringStep<QuestionableTagRenderingConfigJson> {
|
|||
information?: string[]
|
||||
} {
|
||||
if (json.freeform === undefined) {
|
||||
return { result: json }
|
||||
return {result: json}
|
||||
}
|
||||
let spec: Record<string, string>
|
||||
if (typeof json.render === "string") {
|
||||
spec = { "*": json.render }
|
||||
spec = {"*": json.render}
|
||||
} else {
|
||||
spec = <Record<string, string>>json.render
|
||||
}
|
||||
|
@ -450,7 +450,7 @@ class DetectInline extends DesugaringStep<QuestionableTagRenderingConfigJson> {
|
|||
if (spec[key].indexOf("<a ") >= 0) {
|
||||
// We have a link element, it probably contains something that needs to be substituted...
|
||||
// Let's play this safe and not inline it
|
||||
return { result: json }
|
||||
return {result: json}
|
||||
}
|
||||
const fullSpecification = SpecialVisualizations.constructSpecification(spec[key])
|
||||
if (fullSpecification.length > 1) {
|
||||
|
@ -458,19 +458,19 @@ class DetectInline extends DesugaringStep<QuestionableTagRenderingConfigJson> {
|
|||
if (json.freeform.inline === true) {
|
||||
errors.push(
|
||||
"At " +
|
||||
context +
|
||||
": 'inline' is set, but the rendering contains a special visualisation...\n " +
|
||||
spec[key]
|
||||
context +
|
||||
": 'inline' is set, but the rendering contains a special visualisation...\n " +
|
||||
spec[key]
|
||||
)
|
||||
}
|
||||
json = JSON.parse(JSON.stringify(json))
|
||||
json.freeform.inline = false
|
||||
return { result: json, errors }
|
||||
return {result: json, errors}
|
||||
}
|
||||
}
|
||||
json = JSON.parse(JSON.stringify(json))
|
||||
json.freeform.inline ??= true
|
||||
return { result: json, errors }
|
||||
return {result: json, errors}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -491,7 +491,7 @@ export class AddQuestionBox extends DesugaringStep<LayerConfigJson> {
|
|||
json.tagRenderings === undefined ||
|
||||
json.tagRenderings.some((tr) => tr["id"] === "leftover-questions")
|
||||
) {
|
||||
return { result: json }
|
||||
return {result: json}
|
||||
}
|
||||
json = JSON.parse(JSON.stringify(json))
|
||||
const allSpecials: Exclude<RenderingSpecification, string>[] = []
|
||||
|
@ -512,8 +512,8 @@ export class AddQuestionBox extends DesugaringStep<LayerConfigJson> {
|
|||
if (noLabels.length > 1) {
|
||||
errors.push(
|
||||
"At " +
|
||||
context +
|
||||
": multiple 'questions'-visualisations found which would show _all_ questions. Don't do this"
|
||||
context +
|
||||
": multiple 'questions'-visualisations found which would show _all_ questions. Don't do this"
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -537,24 +537,24 @@ export class AddQuestionBox extends DesugaringStep<LayerConfigJson> {
|
|||
if (blacklisted?.length > 0 && used?.length > 0) {
|
||||
errors.push(
|
||||
"At " +
|
||||
context +
|
||||
": the {questions()}-special rendering only supports either a blacklist OR a whitelist, but not both." +
|
||||
"\n Whitelisted: " +
|
||||
used.join(", ") +
|
||||
"\n Blacklisted: " +
|
||||
blacklisted.join(", ")
|
||||
context +
|
||||
": the {questions()}-special rendering only supports either a blacklist OR a whitelist, but not both." +
|
||||
"\n Whitelisted: " +
|
||||
used.join(", ") +
|
||||
"\n Blacklisted: " +
|
||||
blacklisted.join(", ")
|
||||
)
|
||||
}
|
||||
for (const usedLabel of used) {
|
||||
if (!allLabels.has(usedLabel)) {
|
||||
errors.push(
|
||||
"At " +
|
||||
context +
|
||||
": this layers specifies a special question element for label `" +
|
||||
usedLabel +
|
||||
"`, but this label doesn't exist.\n" +
|
||||
" Available labels are " +
|
||||
Array.from(allLabels).join(", ")
|
||||
context +
|
||||
": this layers specifies a special question element for label `" +
|
||||
usedLabel +
|
||||
"`, but this label doesn't exist.\n" +
|
||||
" Available labels are " +
|
||||
Array.from(allLabels).join(", ")
|
||||
)
|
||||
}
|
||||
seen.add(usedLabel)
|
||||
|
@ -583,6 +583,7 @@ export class AddQuestionBox extends DesugaringStep<LayerConfigJson> {
|
|||
|
||||
export class AddEditingElements extends DesugaringStep<LayerConfigJson> {
|
||||
private readonly _desugaring: DesugaringContext
|
||||
|
||||
constructor(desugaring: DesugaringContext) {
|
||||
super(
|
||||
"Add some editing elements, such as the delete button or the move button if they are configured. These used to be handled by the feature info box, but this has been replaced by special visualisation elements",
|
||||
|
@ -609,7 +610,7 @@ export class AddEditingElements extends DesugaringStep<LayerConfigJson> {
|
|||
if (json.allowSplit && !ValidationUtils.hasSpecialVisualisation(json, "split_button")) {
|
||||
json.tagRenderings.push({
|
||||
id: "split-button",
|
||||
render: { "*": "{split_button()}" },
|
||||
render: {"*": "{split_button()}"},
|
||||
})
|
||||
delete json.allowSplit
|
||||
}
|
||||
|
@ -617,13 +618,13 @@ export class AddEditingElements extends DesugaringStep<LayerConfigJson> {
|
|||
if (json.allowMove && !ValidationUtils.hasSpecialVisualisation(json, "move_button")) {
|
||||
json.tagRenderings.push({
|
||||
id: "move-button",
|
||||
render: { "*": "{move_button()}" },
|
||||
render: {"*": "{move_button()}"},
|
||||
})
|
||||
}
|
||||
if (json.deletion && !ValidationUtils.hasSpecialVisualisation(json, "delete_button")) {
|
||||
json.tagRenderings.push({
|
||||
id: "delete-button",
|
||||
render: { "*": "{delete_button()}" },
|
||||
render: {"*": "{delete_button()}"},
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -640,7 +641,7 @@ export class AddEditingElements extends DesugaringStep<LayerConfigJson> {
|
|||
if (!ValidationUtils.hasSpecialVisualisation(json, "all_tags")) {
|
||||
const trc: TagRenderingConfigJson = {
|
||||
id: "all-tags",
|
||||
render: { "*": "{all_tags()}" },
|
||||
render: {"*": "{all_tags()}"},
|
||||
|
||||
metacondition: {
|
||||
or: [
|
||||
|
@ -653,7 +654,7 @@ export class AddEditingElements extends DesugaringStep<LayerConfigJson> {
|
|||
json.tagRenderings?.push(trc)
|
||||
}
|
||||
|
||||
return { result: json }
|
||||
return {result: json}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1149,6 +1150,36 @@ class PreparePointRendering extends Fuse<PointRenderingConfigJson | LineRenderin
|
|||
}
|
||||
}
|
||||
|
||||
class SetFullNodeDatabase extends DesugaringStep<LayerConfigJson> {
|
||||
constructor() {
|
||||
super("sets the fullNodeDatabase-bit if needed",
|
||||
["fullNodeDatabase"],
|
||||
"SetFullNodeDatabase")
|
||||
}
|
||||
|
||||
convert(json: LayerConfigJson, context: string): {
|
||||
result: LayerConfigJson;
|
||||
errors?: string[];
|
||||
warnings?: string[];
|
||||
information?: string[]
|
||||
} {
|
||||
const needsSpecial = json.tagRenderings?.some(tr => {
|
||||
if (typeof tr === "string") {
|
||||
return false
|
||||
}
|
||||
const specs = ValidationUtils.getSpecialVisualisations(<TagRenderingConfigJson>tr)
|
||||
return specs?.some(sp => sp.needsNodeDatabase)
|
||||
}) ?? false
|
||||
if (!needsSpecial) {
|
||||
return {result: json}
|
||||
}
|
||||
return {
|
||||
result: {...json, fullNodeDatabase: true},
|
||||
information: ["Layer " + json.id + " needs the fullNodeDatabase"]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class AddMiniMap extends DesugaringStep<LayerConfigJson> {
|
||||
private readonly _state: DesugaringContext
|
||||
|
||||
|
@ -1163,19 +1194,19 @@ export class AddMiniMap extends DesugaringStep<LayerConfigJson> {
|
|||
|
||||
convert(layerConfig: LayerConfigJson, context: string): { result: LayerConfigJson } {
|
||||
if (!layerConfig.tagRenderings || layerConfig.source === "special") {
|
||||
return { result: layerConfig }
|
||||
return {result: layerConfig}
|
||||
}
|
||||
const state = this._state
|
||||
const hasMinimap = ValidationUtils.hasSpecialVisualisation(layerConfig, "minimap")
|
||||
if (!hasMinimap) {
|
||||
layerConfig = { ...layerConfig }
|
||||
layerConfig = {...layerConfig}
|
||||
layerConfig.tagRenderings = [...layerConfig.tagRenderings]
|
||||
const minimap = state.tagRenderings.get("minimap")
|
||||
if(minimap === undefined){
|
||||
if(state.tagRenderings.size > 0){
|
||||
if (minimap === undefined) {
|
||||
if (state.tagRenderings.size > 0) {
|
||||
throw "The 'minimap'-builtin tagrendering is not defined. As such, it cannot be added automatically"
|
||||
}
|
||||
}else{
|
||||
} else {
|
||||
layerConfig.tagRenderings.push(minimap)
|
||||
}
|
||||
}
|
||||
|
@ -1197,6 +1228,7 @@ export class PrepareLayer extends Fuse<LayerConfigJson> {
|
|||
new AddQuestionBox(),
|
||||
new AddMiniMap(state),
|
||||
new AddEditingElements(state),
|
||||
new SetFullNodeDatabase(),
|
||||
new On("mapRendering", new Concat(new ExpandRewrite()).andThenF(Utils.Flatten)),
|
||||
new On<(PointRenderingConfigJson | LineRenderingConfigJson)[], LayerConfigJson>(
|
||||
"mapRendering",
|
||||
|
|
|
@ -124,9 +124,6 @@ export interface LayerConfigJson {
|
|||
* If set, only features matching this extra tag will be shown.
|
||||
* This is useful to hide certain features from view.
|
||||
*
|
||||
* Important: hiding features does not work dynamically, but is only calculated when the data is first renders.
|
||||
* This implies that it is not possible to hide a feature after a tagging change
|
||||
*
|
||||
* The default value is 'yes'
|
||||
*/
|
||||
isShown?: TagConfigJson
|
||||
|
@ -404,4 +401,9 @@ export interface LayerConfigJson {
|
|||
* If set, open the selectedElementView in a floatOver instead of on the right
|
||||
*/
|
||||
popupInFloatover?: boolean
|
||||
|
||||
/**
|
||||
* _Set automatically by MapComplete, please ignore_
|
||||
*/
|
||||
fullNodeDatabase?: boolean
|
||||
}
|
||||
|
|
|
@ -68,7 +68,7 @@ export default class LayerConfig extends WithContextLoader {
|
|||
public readonly forceLoad: boolean
|
||||
public readonly syncSelection: typeof LayerConfig.syncSelectionAllowed[number] // this is a trick to conver a constant array of strings into a type union of these values
|
||||
|
||||
public readonly _needsFullNodeDatabase = false
|
||||
public readonly _needsFullNodeDatabase: boolean
|
||||
public readonly popupInFloatover
|
||||
|
||||
constructor(json: LayerConfigJson, context?: string, official: boolean = true) {
|
||||
|
@ -217,6 +217,7 @@ export default class LayerConfig extends WithContextLoader {
|
|||
this.doNotDownload = json.doNotDownload ?? false
|
||||
this.passAllFeatures = json.passAllFeatures ?? false
|
||||
this.minzoom = json.minzoom ?? 0
|
||||
this._needsFullNodeDatabase = json.fullNodeDatabase ?? false
|
||||
if (json["minZoom"] !== undefined) {
|
||||
throw "At " + context + ": minzoom is written all lowercase"
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ export default class WithContextLoader {
|
|||
*
|
||||
* The found value is interpreted as a tagrendering and fetched/parsed
|
||||
* */
|
||||
public tr(key: string, deflt: undefined, translationContext?: string) {
|
||||
public tr(key: string, deflt?: string, translationContext?: string) {
|
||||
const v = this._json[key]
|
||||
if (v === undefined || v === null) {
|
||||
if (deflt === undefined) {
|
||||
|
|
|
@ -146,7 +146,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
rasterInfo.defaultState ?? true,
|
||||
"Wether or not overlayer layer " + rasterInfo.id + " is shown"
|
||||
)
|
||||
const state = { isDisplayed }
|
||||
const state = {isDisplayed}
|
||||
overlayLayerStates.set(rasterInfo.id, state)
|
||||
new ShowOverlayRasterLayer(rasterInfo, this.map, this.mapProperties, state)
|
||||
}
|
||||
|
@ -158,18 +158,34 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
* A bit tricky, as this is heavily intertwined with the 'changes'-element, which generate a stream of new and changed features too
|
||||
*/
|
||||
|
||||
|
||||
if(this.layout.layers.some(l => l._needsFullNodeDatabase)){
|
||||
this.fullNodeDatabase = new FullNodeDatabaseSource()
|
||||
}
|
||||
|
||||
const layoutSource = new LayoutSource(
|
||||
layout.layers,
|
||||
this.featureSwitches,
|
||||
this.mapProperties,
|
||||
this.osmConnection.Backend(),
|
||||
(id) => self.layerState.filteredLayers.get(id).isDisplayed
|
||||
(id) => self.layerState.filteredLayers.get(id).isDisplayed,
|
||||
this.fullNodeDatabase
|
||||
)
|
||||
this.indexedFeatures = layoutSource
|
||||
const empty = []
|
||||
let currentViewIndex = 0
|
||||
this.currentView = new StaticFeatureSource(
|
||||
this.mapProperties.bounds.map((bbox) =>
|
||||
bbox === undefined ? empty : <Feature[]>[bbox.asGeoJson({ id: "current_view" })]
|
||||
this.mapProperties.bounds.map((bbox) => {
|
||||
if (!bbox) {
|
||||
return empty
|
||||
}
|
||||
currentViewIndex++
|
||||
return <Feature[]>[bbox.asGeoJson({
|
||||
zoom: this.mapProperties.zoom.data,
|
||||
...this.mapProperties.location.data,
|
||||
id: "current_view" }
|
||||
)];
|
||||
}
|
||||
)
|
||||
)
|
||||
this.featuresInView = new BBoxFeatureSource(layoutSource, this.mapProperties.bounds)
|
||||
|
@ -270,7 +286,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
for (const l of levels) {
|
||||
floors.add(l)
|
||||
}
|
||||
}else{
|
||||
} else {
|
||||
floors.add("0") // '0' is the default and is thus _always_ present
|
||||
}
|
||||
}
|
||||
|
@ -305,7 +321,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
this.drawSpecialLayers()
|
||||
this.initHotkeys()
|
||||
this.miscSetup()
|
||||
if(!Utils.runningFromConsole){
|
||||
if (!Utils.runningFromConsole) {
|
||||
console.log("State setup completed", this)
|
||||
}
|
||||
}
|
||||
|
@ -333,7 +349,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
|
||||
private initHotkeys() {
|
||||
Hotkeys.RegisterHotkey(
|
||||
{ nomod: "Escape", onUp: true },
|
||||
{nomod: "Escape", onUp: true},
|
||||
Translations.t.hotkeyDocumentation.closeSidebar,
|
||||
() => {
|
||||
this.selectedElement.setData(undefined)
|
||||
|
@ -354,7 +370,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
)
|
||||
|
||||
Hotkeys.RegisterHotkey(
|
||||
{ shift: "O" },
|
||||
{shift: "O"},
|
||||
Translations.t.hotkeyDocumentation.selectMapnik,
|
||||
() => {
|
||||
this.mapProperties.rasterLayer.setData(AvailableRasterLayers.osmCarto)
|
||||
|
@ -373,17 +389,17 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
}
|
||||
|
||||
Hotkeys.RegisterHotkey(
|
||||
{ nomod: "O" },
|
||||
{nomod: "O"},
|
||||
Translations.t.hotkeyDocumentation.selectOsmbasedmap,
|
||||
() => setLayerCategory("osmbasedmap")
|
||||
)
|
||||
|
||||
Hotkeys.RegisterHotkey({ nomod: "M" }, Translations.t.hotkeyDocumentation.selectMap, () =>
|
||||
Hotkeys.RegisterHotkey({nomod: "M"}, Translations.t.hotkeyDocumentation.selectMap, () =>
|
||||
setLayerCategory("map")
|
||||
)
|
||||
|
||||
Hotkeys.RegisterHotkey(
|
||||
{ nomod: "P" },
|
||||
{nomod: "P"},
|
||||
Translations.t.hotkeyDocumentation.selectAerial,
|
||||
() => setLayerCategory("photo")
|
||||
)
|
||||
|
@ -451,7 +467,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
),
|
||||
range: new StaticFeatureSource(
|
||||
this.mapProperties.maxbounds.map((bbox) =>
|
||||
bbox === undefined ? empty : <Feature[]>[bbox.asGeoJson({ id: "range" })]
|
||||
bbox === undefined ? empty : <Feature[]>[bbox.asGeoJson({id: "range"})]
|
||||
)
|
||||
),
|
||||
current_view: this.currentView
|
||||
|
@ -465,6 +481,14 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
this.featureSwitches.featureSwitchIsTesting
|
||||
)
|
||||
}
|
||||
const currentViewLayer = this.layout.layers.find(l => l.id === "current_view")
|
||||
if (currentViewLayer?.tagRenderings?.length > 0) {
|
||||
const params = MetaTagging.createExtraFuncParams(this)
|
||||
this.featureProperties.trackFeatureSource(specialLayers.current_view)
|
||||
specialLayers.current_view.features.addCallbackAndRunD(features => {
|
||||
MetaTagging.addMetatags(features, params, currentViewLayer, this.layout, this.osmObjectDownloader, this.featureProperties)
|
||||
})
|
||||
}
|
||||
|
||||
this.layerState.filteredLayers
|
||||
.get("range")
|
||||
|
@ -545,7 +569,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
}
|
||||
{
|
||||
this.selectedElement.addCallback(selected => {
|
||||
if(selected === undefined){
|
||||
if (selected === undefined) {
|
||||
// We did _unselect_ an item - we always remove the lastclick-object
|
||||
this.lastClickObject.features.setData([])
|
||||
this.selectedLayer.setData(undefined)
|
||||
|
|
|
@ -97,6 +97,16 @@ export class Tiles {
|
|||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a tilerange which (at least) contains the given coordinates.
|
||||
* This means that the actual iterated area might be a bit bigger then the the passed in coordinates
|
||||
* @param zoomlevel
|
||||
* @param lat0
|
||||
* @param lon0
|
||||
* @param lat1
|
||||
* @param lon1
|
||||
* @constructor
|
||||
*/
|
||||
static TileRangeBetween(
|
||||
zoomlevel: number,
|
||||
lat0: number,
|
||||
|
|
|
@ -78,6 +78,9 @@ export default class Table extends BaseUIElement {
|
|||
for (let j = 0; j < row.length; j++) {
|
||||
try {
|
||||
let elem = row[j]
|
||||
if(elem?.ConstructElement === undefined){
|
||||
continue
|
||||
}
|
||||
const htmlElem = elem?.ConstructElement()
|
||||
if (htmlElem === undefined) {
|
||||
continue
|
||||
|
|
|
@ -330,6 +330,7 @@ class LineRenderingLayer {
|
|||
})
|
||||
if (this._onClick) {
|
||||
map.on("click", polylayer, (e) => {
|
||||
console.log("Got polylayer click:", e)
|
||||
// polygon-layer-listener
|
||||
if(e.originalEvent["consumed"]){
|
||||
// This is a polygon beneath a marker, we can ignore it
|
||||
|
@ -469,8 +470,10 @@ export default class ShowDataLayer {
|
|||
if (this._options.zoomToFeatures) {
|
||||
const features = this._options.features.features.data
|
||||
const bbox = BBox.bboxAroundAll(features.map(BBox.get))
|
||||
map.resize()
|
||||
map.fitBounds(bbox.toLngLat(), {
|
||||
padding: {top: 10, bottom: 10, left: 10, right: 10},
|
||||
animate: false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,26 +1,26 @@
|
|||
import BaseUIElement from "../BaseUIElement"
|
||||
import { Stores, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { SubtleButton } from "../Base/SubtleButton"
|
||||
import {Stores, UIEventSource} from "../../Logic/UIEventSource"
|
||||
import {SubtleButton} from "../Base/SubtleButton"
|
||||
import Img from "../Base/Img"
|
||||
import { FixedUiElement } from "../Base/FixedUiElement"
|
||||
import {FixedUiElement} from "../Base/FixedUiElement"
|
||||
import Combine from "../Base/Combine"
|
||||
import Link from "../Base/Link"
|
||||
import { Utils } from "../../Utils"
|
||||
import {Utils} from "../../Utils"
|
||||
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
|
||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
import {VariableUiElement} from "../Base/VariableUIElement"
|
||||
import Loading from "../Base/Loading"
|
||||
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
||||
import {OsmConnection} from "../../Logic/Osm/OsmConnection"
|
||||
import Translations from "../i18n/Translations"
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import { Changes } from "../../Logic/Osm/Changes"
|
||||
import { UIElement } from "../UIElement"
|
||||
import {Changes} from "../../Logic/Osm/Changes"
|
||||
import {UIElement} from "../UIElement"
|
||||
import FilteredLayer from "../../Models/FilteredLayer"
|
||||
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"
|
||||
import Lazy from "../Base/Lazy"
|
||||
import List from "../Base/List"
|
||||
import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import { IndexedFeatureSource } from "../../Logic/FeatureSource/FeatureSource"
|
||||
import { MapLibreAdaptor } from "../Map/MapLibreAdaptor"
|
||||
import {SpecialVisualization, SpecialVisualizationState} from "../SpecialVisualization"
|
||||
import {IndexedFeatureSource} from "../../Logic/FeatureSource/FeatureSource"
|
||||
import {MapLibreAdaptor} from "../Map/MapLibreAdaptor"
|
||||
import ShowDataLayer from "../Map/ShowDataLayer"
|
||||
import SvelteUIElement from "../Base/SvelteUIElement"
|
||||
import MaplibreMap from "../Map/MaplibreMap.svelte"
|
||||
|
@ -54,6 +54,7 @@ class ApplyButton extends UIElement {
|
|||
>("idle")
|
||||
private readonly layer: FilteredLayer
|
||||
private readonly tagRenderingConfig: TagRenderingConfig
|
||||
private readonly appliedNumberOfFeatures = new UIEventSource<number>(0)
|
||||
|
||||
constructor(
|
||||
state: SpecialVisualizationState,
|
||||
|
@ -110,7 +111,7 @@ class ApplyButton extends UIElement {
|
|||
mla.allowZooming.setData(false)
|
||||
mla.allowMoving.setData(false)
|
||||
|
||||
const previewMap = new SvelteUIElement(MaplibreMap, { map: mlmap }).SetClass("h-48")
|
||||
const previewMap = new SvelteUIElement(MaplibreMap, {map: mlmap}).SetClass("h-48")
|
||||
|
||||
const features = this.target_feature_ids.map((id) =>
|
||||
this.state.indexedFeatures.featuresById.data.get(id)
|
||||
|
@ -131,7 +132,9 @@ class ApplyButton extends UIElement {
|
|||
return new FixedUiElement("All done!").SetClass("thanks")
|
||||
}
|
||||
if (st === "running") {
|
||||
return new Loading("Applying changes...")
|
||||
return new Loading(new VariableUiElement(this.appliedNumberOfFeatures.map(appliedTo => {
|
||||
return "Applying changes, currently at " + appliedTo + "/" + this.target_feature_ids.length
|
||||
})))
|
||||
}
|
||||
const error = st.error
|
||||
return new Combine([
|
||||
|
@ -142,11 +145,16 @@ class ApplyButton extends UIElement {
|
|||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Actually applies all the changes...
|
||||
*/
|
||||
private async Run() {
|
||||
try {
|
||||
console.log("Applying auto-action on " + this.target_feature_ids.length + " features")
|
||||
|
||||
for (const targetFeatureId of this.target_feature_ids) {
|
||||
for (let i = 0; i < this.target_feature_ids.length; i++) {
|
||||
const targetFeatureId = this.target_feature_ids[i];
|
||||
const feature = this.state.indexedFeatures.featuresById.data.get(targetFeatureId)
|
||||
const featureTags = this.state.featureProperties.getStore(targetFeatureId)
|
||||
const rendering = this.tagRenderingConfig.GetRenderValue(featureTags.data).txt
|
||||
const specialRenderings = Utils.NoNull(
|
||||
|
@ -156,8 +164,8 @@ class ApplyButton extends UIElement {
|
|||
if (specialRenderings.length == 0) {
|
||||
console.warn(
|
||||
"AutoApply: feature " +
|
||||
targetFeatureId +
|
||||
" got a rendering without supported auto actions:",
|
||||
targetFeatureId +
|
||||
" got a rendering without supported auto actions:",
|
||||
rendering
|
||||
)
|
||||
}
|
||||
|
@ -167,15 +175,19 @@ class ApplyButton extends UIElement {
|
|||
continue
|
||||
}
|
||||
const action = <AutoAction>specialRendering.func
|
||||
await action.applyActionOn(this.state, featureTags, specialRendering.args)
|
||||
await action.applyActionOn(feature, this.state, featureTags, specialRendering.args)
|
||||
}
|
||||
if( i % 50 === 0){
|
||||
await this.state.changes.flushChanges("Auto button: intermediate save")
|
||||
}
|
||||
this.appliedNumberOfFeatures.setData(i + 1)
|
||||
}
|
||||
console.log("Flushing changes...")
|
||||
await this.state.changes.flushChanges("Auto button")
|
||||
await this.state.changes.flushChanges("Auto button: done")
|
||||
this.buttonState.setData("done")
|
||||
} catch (e) {
|
||||
console.error("Error while running autoApply: ", e)
|
||||
this.buttonState.setData({ error: e })
|
||||
this.buttonState.setData({error: e})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -230,7 +242,7 @@ export default class AutoApplyButton implements SpecialVisualization {
|
|||
"To effectively use this button, you'll need some ingredients:",
|
||||
new List([
|
||||
"A target layer with features for which an action is defined in a tag rendering. The following special visualisations support an autoAction: " +
|
||||
supportedActions.join(", "),
|
||||
supportedActions.join(", "),
|
||||
"A host feature to place the auto-action on. This can be a big outline (such as a city). Another good option for this is the layer ",
|
||||
new Link("current_view", "./BuiltinLayers.md#current_view"),
|
||||
"Then, use a calculated tag on the host feature to determine the overlapping object ids",
|
||||
|
@ -250,7 +262,7 @@ export default class AutoApplyButton implements SpecialVisualization {
|
|||
!(
|
||||
state.featureSwitchIsTesting.data ||
|
||||
state.osmConnection._oauth_config.url ===
|
||||
OsmConnection.oauth_configs["osm-test"].url
|
||||
OsmConnection.oauth_configs["osm-test"].url
|
||||
)
|
||||
) {
|
||||
const t = Translations.t.general.add.import
|
||||
|
@ -274,21 +286,27 @@ export default class AutoApplyButton implements SpecialVisualization {
|
|||
}
|
||||
|
||||
return new Lazy(() => {
|
||||
const to_parse = new UIEventSource(undefined)
|
||||
const to_parse = new UIEventSource<string[]>(undefined)
|
||||
// Very ugly hack: read the value every 500ms
|
||||
Stores.Chronic(500, () => to_parse.data === undefined).addCallback(() => {
|
||||
const applicable = tagSource.data[argument[1]]
|
||||
to_parse.setData(applicable)
|
||||
let applicable = <string | string[]> tagSource.data[argument[1]]
|
||||
if(typeof applicable === "string"){
|
||||
applicable = JSON.parse(applicable)
|
||||
}
|
||||
to_parse.setData(<string[]> applicable)
|
||||
})
|
||||
|
||||
const loading = new Loading("Gathering which elements support auto-apply... ")
|
||||
return new VariableUiElement(
|
||||
to_parse.map((ids) => {
|
||||
Stores.ListStabilized(to_parse).map((ids) => {
|
||||
if (ids === undefined) {
|
||||
return loading
|
||||
}
|
||||
|
||||
return new ApplyButton(state, JSON.parse(ids), options)
|
||||
if (typeof ids === "string") {
|
||||
ids = JSON.parse(ids)
|
||||
}
|
||||
return new ApplyButton(state, ids, options)
|
||||
})
|
||||
)
|
||||
})
|
||||
|
|
|
@ -1,162 +0,0 @@
|
|||
import BaseUIElement from "../BaseUIElement"
|
||||
import {Store, UIEventSource} from "../../Logic/UIEventSource"
|
||||
import Translations from "../i18n/Translations"
|
||||
import {FixedUiElement} from "../Base/FixedUiElement"
|
||||
import OsmChangeAction from "../../Logic/Osm/Actions/OsmChangeAction"
|
||||
import {And} from "../../Logic/Tags/And"
|
||||
import {Tag} from "../../Logic/Tags/Tag"
|
||||
import {SpecialVisualization, SpecialVisualizationState} from "../SpecialVisualization"
|
||||
import {Feature} from "geojson"
|
||||
import {ImportFlowArguments, ImportFlowUtils} from "./ImportButtons/ImportFlow";
|
||||
import {MergePointConfig} from "../../Logic/Osm/Actions/CreateWayWithPointReuseAction";
|
||||
import {GeoOperations} from "../../Logic/GeoOperations";
|
||||
import ReplaceGeometryAction from "../../Logic/Osm/Actions/ReplaceGeometryAction";
|
||||
import {TagUtils} from "../../Logic/Tags/TagUtils";
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* A helper class for the various import-flows.
|
||||
* An import-flow always starts with a 'Import this'-button. Upon click, a custom confirmation panel is provided
|
||||
*/
|
||||
export abstract class AbstractImportButton implements SpecialVisualization {
|
||||
|
||||
public readonly funcName: string
|
||||
public readonly docs: string
|
||||
public readonly args: { name: string; defaultValue?: string; doc: string }[]
|
||||
private readonly showRemovedTags: boolean
|
||||
private readonly cannotBeImportedMessage: BaseUIElement | undefined
|
||||
|
||||
constructor(
|
||||
funcName: string,
|
||||
docsIntro: string,
|
||||
extraArgs: { name: string; doc: string; defaultValue?: string; required?: boolean }[],
|
||||
options?: { showRemovedTags?: true | boolean; cannotBeImportedMessage?: BaseUIElement }
|
||||
) {
|
||||
this.funcName = funcName
|
||||
this.showRemovedTags = options?.showRemovedTags ?? true
|
||||
this.cannotBeImportedMessage = options?.cannotBeImportedMessage
|
||||
this.docs = `${docsIntro}${ImportFlowUtils.documentationGeneral}`
|
||||
|
||||
this.args = [
|
||||
{
|
||||
name: "targetLayer",
|
||||
doc: "The id of the layer where this point should end up. This is not very strict, it will simply result in checking that this layer is shown preventing possible duplicate elements",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: "tags",
|
||||
doc: "The tags to add onto the new object - see specification above. If this is a key (a single word occuring in the properties of the object), the corresponding value is taken and expanded instead",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: "text",
|
||||
doc: "The text to show on the button",
|
||||
defaultValue: "Import this data into OpenStreetMap",
|
||||
},
|
||||
{
|
||||
name: "icon",
|
||||
doc: "A nice icon to show in the button",
|
||||
defaultValue: "./assets/svg/addSmall.svg",
|
||||
},
|
||||
...extraArgs,
|
||||
]
|
||||
}
|
||||
|
||||
abstract constructElement(
|
||||
state: SpecialVisualizationState,
|
||||
args: ImportFlowArguments,
|
||||
newTags: Store<Tag[]>,
|
||||
tagSource: UIEventSource<Record<string, string>>,
|
||||
feature: Feature,
|
||||
onCancelClicked: () => void
|
||||
): BaseUIElement
|
||||
|
||||
constr(
|
||||
state: SpecialVisualizationState,
|
||||
tagSource: UIEventSource<Record<string, string>>,
|
||||
argsRaw: string[]
|
||||
) {
|
||||
return new FixedUiElement("Deprecated")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class ConflateButton extends AbstractImportButton {
|
||||
needsNodeDatabase = true
|
||||
|
||||
constructor() {
|
||||
super(
|
||||
"conflate_button",
|
||||
"This button will modify the geometry of an existing OSM way to match the specified geometry. This can conflate OSM-ways with LineStrings and Polygons (only simple polygons with one single ring). An attempt is made to move points with special values to a decent new location (e.g. entrances)",
|
||||
[
|
||||
{
|
||||
name: "way_to_conflate",
|
||||
doc: "The key, of which the corresponding value is the id of the OSM-way that must be conflated; typically a calculatedTag",
|
||||
},
|
||||
],
|
||||
{
|
||||
cannotBeImportedMessage: Translations.t.general.add.import.wrongTypeToConflate,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
constructElement(
|
||||
state: SpecialVisualizationState,
|
||||
args: {
|
||||
max_snap_distance: string
|
||||
snap_onto_layers: string
|
||||
icon: string
|
||||
text: string
|
||||
tags: string
|
||||
newTags: UIEventSource<Tag[]>
|
||||
targetLayer: string
|
||||
},
|
||||
tagSource: UIEventSource<any>,
|
||||
feature: Feature,
|
||||
onCancelClicked: () => void
|
||||
): BaseUIElement {
|
||||
const nodesMustMatch = args.snap_onto_layers
|
||||
?.split(";")
|
||||
?.map((tag, i) => TagUtils.Tag(tag, "TagsSpec for import button " + i))
|
||||
|
||||
const mergeConfigs = []
|
||||
if (nodesMustMatch !== undefined && nodesMustMatch.length > 0) {
|
||||
const mergeConfig: MergePointConfig = {
|
||||
mode: args["point_move_mode"] == "move_osm" ? "move_osm_point" : "reuse_osm_point",
|
||||
ifMatches: new And(nodesMustMatch),
|
||||
withinRangeOfM: Number(args.max_snap_distance),
|
||||
}
|
||||
mergeConfigs.push(mergeConfig)
|
||||
}
|
||||
|
||||
const key = args["way_to_conflate"]
|
||||
const wayToConflate = tagSource.data[key]
|
||||
feature = GeoOperations.removeOvernoding(feature)
|
||||
const action: OsmChangeAction & { getPreview(): Promise<any> } = new ReplaceGeometryAction(
|
||||
state,
|
||||
feature,
|
||||
wayToConflate,
|
||||
{
|
||||
theme: state.layout.id,
|
||||
newTags: args.newTags.data,
|
||||
}
|
||||
)
|
||||
|
||||
return this.createConfirmPanelForWay(
|
||||
state,
|
||||
args,
|
||||
feature,
|
||||
tagSource,
|
||||
action,
|
||||
onCancelClicked
|
||||
)
|
||||
}
|
||||
|
||||
protected canBeImported(feature: Feature) {
|
||||
return (
|
||||
feature.geometry.type === "LineString" ||
|
||||
(feature.geometry.type === "Polygon" && feature.geometry.coordinates.length === 1)
|
||||
)
|
||||
}
|
||||
}
|
89
UI/Popup/ImportButtons/ConflateImportButtonViz.ts
Normal file
89
UI/Popup/ImportButtons/ConflateImportButtonViz.ts
Normal file
|
@ -0,0 +1,89 @@
|
|||
import {SpecialVisualization, SpecialVisualizationState} from "../../SpecialVisualization";
|
||||
import {UIEventSource} from "../../../Logic/UIEventSource";
|
||||
import {Feature, Geometry, LineString, Polygon} from "geojson";
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
|
||||
import BaseUIElement from "../../BaseUIElement";
|
||||
import {ImportFlowArguments, ImportFlowUtils} from "./ImportFlow";
|
||||
import Translations from "../../i18n/Translations";
|
||||
import {Utils} from "../../../Utils";
|
||||
import SvelteUIElement from "../../Base/SvelteUIElement";
|
||||
import WayImportFlow from "./WayImportFlow.svelte";
|
||||
import ConflateImportFlowState from "./ConflateImportFlowState";
|
||||
import {AutoAction} from "../AutoApplyButton";
|
||||
import {IndexedFeatureSource} from "../../../Logic/FeatureSource/FeatureSource";
|
||||
import {Changes} from "../../../Logic/Osm/Changes";
|
||||
import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig";
|
||||
import {OsmConnection} from "../../../Logic/Osm/OsmConnection";
|
||||
|
||||
export interface ConflateFlowArguments extends ImportFlowArguments {
|
||||
way_to_conflate: string
|
||||
point_move_mode?: "move_osm" | undefined;
|
||||
max_snap_distance?: string
|
||||
snap_onto_layers?: string,
|
||||
}
|
||||
|
||||
export default class ConflateImportButtonViz implements SpecialVisualization, AutoAction {
|
||||
supportsAutoAction: boolean = true;
|
||||
public readonly funcName: string = "conflate_button";
|
||||
public readonly args: { name: string; defaultValue?: string; doc: string; required?: boolean }[] = [
|
||||
...ImportFlowUtils.generalArguments,
|
||||
{
|
||||
name: "way_to_conflate",
|
||||
doc: "The key, of which the corresponding value is the id of the OSM-way that must be conflated; typically a calculatedTag",
|
||||
},
|
||||
|
||||
|
||||
];
|
||||
readonly docs: string = "This button will modify the geometry of an existing OSM way to match the specified geometry. This can conflate OSM-ways with LineStrings and Polygons (only simple polygons with one single ring). An attempt is made to move points with special values to a decent new location (e.g. entrances)" + ImportFlowUtils.documentationGeneral
|
||||
public readonly needsNodeDatabase = true
|
||||
|
||||
|
||||
async applyActionOn(feature: Feature<Geometry, { [name: string]: any; }>, state: {
|
||||
osmConnection: OsmConnection,
|
||||
layout: LayoutConfig;
|
||||
changes: Changes;
|
||||
indexedFeatures: IndexedFeatureSource;
|
||||
}, tagSource: UIEventSource<any>, argument: string[]): Promise<void> {
|
||||
|
||||
{
|
||||
// Small safety check to prevent duplicate imports
|
||||
const id = tagSource.data.id
|
||||
if (ImportFlowUtils.importedIds.has(id)) {
|
||||
return
|
||||
}
|
||||
ImportFlowUtils.importedIds.add(id)
|
||||
}
|
||||
|
||||
if (feature.geometry.type !== "LineString" && feature.geometry.type !== "Polygon") {
|
||||
return
|
||||
}
|
||||
|
||||
const args: ConflateFlowArguments = <any>Utils.ParseVisArgs(this.args, argument)
|
||||
const tagsToApply = ImportFlowUtils.getTagsToApply(tagSource, args)
|
||||
const idOfWayToReplaceGeometry = tagSource.data[args.way_to_conflate]
|
||||
const action = ConflateImportFlowState.createAction(<Feature<LineString | Polygon>>feature, args, state, idOfWayToReplaceGeometry, tagsToApply)
|
||||
tagSource.data["_imported"] = "yes"
|
||||
tagSource.ping()
|
||||
await state.changes.applyAction(action)
|
||||
}
|
||||
|
||||
constr(state: SpecialVisualizationState, tagSource: UIEventSource<Record<string, string>>, argument: string[], feature: Feature, layer: LayerConfig): BaseUIElement {
|
||||
|
||||
const canBeImported = feature.geometry.type === "LineString" ||
|
||||
(feature.geometry.type === "Polygon" && feature.geometry.coordinates.length === 1)
|
||||
if (!canBeImported) {
|
||||
return Translations.t.general.add.import.wrongTypeToConflate.SetClass("alert")
|
||||
}
|
||||
const args: ConflateFlowArguments = <any>Utils.ParseVisArgs(this.args, argument)
|
||||
const tagsToApply = ImportFlowUtils.getTagsToApply(tagSource, args)
|
||||
const idOfWayToReplaceGeometry = tagSource.data[args.way_to_conflate]
|
||||
const importFlow = new ConflateImportFlowState(state, <Feature<LineString | Polygon>>feature, args, tagsToApply, tagSource, idOfWayToReplaceGeometry)
|
||||
return new SvelteUIElement(WayImportFlow, {
|
||||
importFlow
|
||||
})
|
||||
}
|
||||
|
||||
getLayerDependencies(args: string[]) {
|
||||
return ImportFlowUtils.getLayerDependenciesWithSnapOnto(this.args, args)
|
||||
}
|
||||
}
|
83
UI/Popup/ImportButtons/ConflateImportFlowState.ts
Normal file
83
UI/Popup/ImportButtons/ConflateImportFlowState.ts
Normal file
|
@ -0,0 +1,83 @@
|
|||
import ImportFlow from "./ImportFlow";
|
||||
import {ConflateFlowArguments} from "./ConflateImportButtonViz";
|
||||
import {SpecialVisualizationState} from "../../SpecialVisualization";
|
||||
import {Feature, LineString, Polygon} from "geojson";
|
||||
import {Store, UIEventSource} from "../../../Logic/UIEventSource";
|
||||
import {Tag} from "../../../Logic/Tags/Tag";
|
||||
import OsmChangeAction from "../../../Logic/Osm/Actions/OsmChangeAction";
|
||||
import ReplaceGeometryAction from "../../../Logic/Osm/Actions/ReplaceGeometryAction";
|
||||
import {GeoOperations} from "../../../Logic/GeoOperations";
|
||||
import {TagUtils} from "../../../Logic/Tags/TagUtils";
|
||||
import {MergePointConfig} from "../../../Logic/Osm/Actions/CreateWayWithPointReuseAction";
|
||||
import {And} from "../../../Logic/Tags/And";
|
||||
import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig";
|
||||
import {Changes} from "../../../Logic/Osm/Changes";
|
||||
import {FeatureSource, IndexedFeatureSource} from "../../../Logic/FeatureSource/FeatureSource";
|
||||
import FullNodeDatabaseSource from "../../../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource";
|
||||
import {OsmConnection} from "../../../Logic/Osm/OsmConnection";
|
||||
|
||||
export default class ConflateImportFlowState extends ImportFlow<ConflateFlowArguments> {
|
||||
|
||||
public readonly originalFeature: Feature
|
||||
private readonly action: OsmChangeAction & { getPreview?(): Promise<FeatureSource>; newElementId?: string };
|
||||
constructor(state: SpecialVisualizationState, originalFeature: Feature<LineString | Polygon>, args: ConflateFlowArguments, tagsToApply: Store<Tag[]>, originalFeatureTags: UIEventSource<Record<string, string>>, idOfFeatureToReplaceGeometry: string) {
|
||||
super(state, args, tagsToApply, originalFeatureTags)
|
||||
this.originalFeature = originalFeature
|
||||
this.action = ConflateImportFlowState.createAction(originalFeature, args, state, idOfFeatureToReplaceGeometry, tagsToApply)
|
||||
|
||||
}
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
public GetPreview(): Promise<FeatureSource>{
|
||||
return this.action.getPreview()
|
||||
}
|
||||
|
||||
public async onConfirm() {
|
||||
const originalFeatureTags = this._originalFeatureTags
|
||||
originalFeatureTags.data["_imported"] = "yes"
|
||||
originalFeatureTags.ping() // will set isImported as per its definition
|
||||
const action = this.action
|
||||
await this.state.changes.applyAction(action)
|
||||
const newId = action.newElementId ?? action.mainObjectId
|
||||
this.state.selectedLayer.setData(this.targetLayer.layerDef)
|
||||
this.state.selectedElement.setData(this.state.indexedFeatures.featuresById.data.get(newId))
|
||||
}
|
||||
|
||||
public static createAction(feature: Feature<LineString | Polygon>,
|
||||
args: ConflateFlowArguments,
|
||||
state: {
|
||||
osmConnection: OsmConnection,
|
||||
layout: LayoutConfig;
|
||||
changes: Changes;
|
||||
indexedFeatures: IndexedFeatureSource,
|
||||
fullNodeDatabase?: FullNodeDatabaseSource
|
||||
},
|
||||
idOfFeatureToReplaceGeometry,
|
||||
tagsToApply: Store<Tag[]>
|
||||
): OsmChangeAction & { getPreview?(): Promise<FeatureSource>; newElementId?: string } {
|
||||
const nodesMustMatch = args.snap_onto_layers
|
||||
?.split(";")
|
||||
?.map((tag, i) => TagUtils.Tag(tag, "TagsSpec for import button " + i))
|
||||
|
||||
const mergeConfigs = []
|
||||
if (nodesMustMatch !== undefined && nodesMustMatch.length > 0) {
|
||||
const mergeConfig: MergePointConfig = {
|
||||
mode: args.point_move_mode == "move_osm" ? "move_osm_point" : "reuse_osm_point",
|
||||
ifMatches: new And(nodesMustMatch),
|
||||
withinRangeOfM: Number(args.max_snap_distance ?? 0),
|
||||
}
|
||||
mergeConfigs.push(mergeConfig)
|
||||
}
|
||||
|
||||
|
||||
return new ReplaceGeometryAction(
|
||||
state,
|
||||
GeoOperations.removeOvernoding(feature),
|
||||
idOfFeatureToReplaceGeometry,
|
||||
{
|
||||
theme: state.layout.id,
|
||||
newTags: tagsToApply.data,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
|
@ -15,6 +15,9 @@
|
|||
import TagHint from "../TagHint.svelte";
|
||||
import {TagsFilter} from "../../../Logic/Tags/TagsFilter";
|
||||
import {Store} from "../../../Logic/UIEventSource";
|
||||
import Svg from "../../../Svg";
|
||||
import ToSvelte from "../../Base/ToSvelte.svelte";
|
||||
import {EyeIcon, EyeOffIcon} from "@rgossiaux/svelte-heroicons/solid";
|
||||
|
||||
export let importFlow: ImportFlow
|
||||
let state = importFlow.state
|
||||
|
@ -24,16 +27,74 @@
|
|||
const isLoading = state.dataIsLoading
|
||||
const dispatch = createEventDispatcher<{ confirm }>()
|
||||
const canBeImported = importFlow.canBeImported()
|
||||
const tags : Store<TagsFilter> = importFlow.tagsToApply.map(tags => new And(tags))
|
||||
const tags: Store<TagsFilter> = importFlow.tagsToApply.map(tags => new And(tags))
|
||||
|
||||
const isDisplayed = importFlow.targetLayer.isDisplayed
|
||||
const hasFilter = importFlow.targetLayer.appliedFilters
|
||||
const hasFilter = importFlow.targetLayer.hasFilter
|
||||
|
||||
function abort() {
|
||||
state.selectedElement.setData(undefined)
|
||||
state.selectedLayer.setData(undefined)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<LoginToggle {state}>
|
||||
|
||||
{#if currentFlowStep === "start"}
|
||||
|
||||
{#if $canBeImported !== true && $canBeImported !== undefined}
|
||||
<Tr cls="alert w-full flex justify-center" t={$canBeImported.error}/>
|
||||
{#if $canBeImported.extraHelp}
|
||||
<Tr t={$canBeImported.extraHelp}/>
|
||||
{/if}
|
||||
{:else if !$isDisplayed}
|
||||
<!-- Check that the layer is enabled, so that we don't add a duplicate -->
|
||||
<div class="alert flex justify-center items-center">
|
||||
<EyeOffIcon class="w-8"/>
|
||||
<Tr t={Translations.t.general.add.layerNotEnabled
|
||||
.Subs({ layer: importFlow.targetLayer.layerDef.name })
|
||||
}/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap-reverse md:flex-nowrap">
|
||||
|
||||
<button class="flex w-full gap-x-1"
|
||||
on:click={() => {abort();state.guistate.openFilterView(importFlow.targetLayer.layerDef)}}>
|
||||
<ToSvelte construct={Svg.layers_svg().SetClass("w-12")}/>
|
||||
<Tr t={Translations.t.general.add.openLayerControl}/>
|
||||
</button>
|
||||
|
||||
<button class="flex w-full gap-x-1 primary" on:click={() => {isDisplayed.setData(true);abort()}}>
|
||||
<EyeIcon class="w-12"/>
|
||||
<Tr t={Translations.t.general.add.enableLayer.Subs({name: importFlow.targetLayer.layerDef.name})}/>
|
||||
</button>
|
||||
</div>
|
||||
{:else if $hasFilter}
|
||||
<!-- Some filters are enabled. The feature to add might already be mapped, but hidden -->
|
||||
<div class="alert flex justify-center items-center">
|
||||
<EyeOffIcon class="w-8"/>
|
||||
<Tr t={Translations.t.general.add.disableFiltersExplanation}/>
|
||||
</div>
|
||||
<div class="flex flex-wrap-reverse md:flex-nowrap">
|
||||
<button class="flex w-full gap-x-1 primary"
|
||||
on:click={() => {abort(); importFlow.targetLayer.disableAllFilters()}}>
|
||||
<EyeOffIcon class="w-12"/>
|
||||
<Tr t={Translations.t.general.add.disableFilters}/>
|
||||
</button>
|
||||
<button class="flex w-full gap-x-1"
|
||||
on:click={() => {abort();state.guistate.openFilterView(importFlow.targetLayer.layerDef)}}>
|
||||
<ToSvelte construct={Svg.layers_svg().SetClass("w-12")}/>
|
||||
<Tr t={Translations.t.general.add.openLayerControl}/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{:else if $isLoading}
|
||||
<Loading>
|
||||
<Tr t={Translations.t.general.add.stillLoading}/>
|
||||
</Loading>
|
||||
|
||||
{:else if currentFlowStep === "start"}
|
||||
<NextButton clss="primary w-full" on:click={() => currentFlowStep = "confirm"}>
|
||||
<slot name="start-flow-text">
|
||||
{#if importFlow?.args?.icon}
|
||||
|
@ -42,15 +103,6 @@
|
|||
{importFlow.args.text}
|
||||
</slot>
|
||||
</NextButton>
|
||||
{:else if $canBeImported !== true && $canBeImported !== undefined}
|
||||
<Tr cls="alert w-full flex justify-center" t={$canBeImported.error}/>
|
||||
{#if $canBeImported.extraHelp}
|
||||
<Tr t={$canBeImported.extraHelp}/>
|
||||
{/if}
|
||||
{:else if $isLoading}
|
||||
<Loading>
|
||||
<Tr t={Translations.t.general.add.stillLoading}/>
|
||||
</Loading>
|
||||
{:else if currentFlowStep === "confirm"}
|
||||
<div class="h-full w-full flex flex-col">
|
||||
<div class="w-full h-full">
|
||||
|
|
|
@ -149,11 +149,13 @@ export default abstract class ImportFlow<ArgT extends ImportFlowArguments> {
|
|||
public readonly args: ArgT;
|
||||
public readonly targetLayer: FilteredLayer;
|
||||
public readonly tagsToApply: Store<Tag[]>
|
||||
protected readonly _originalFeatureTags: UIEventSource<Record<string, string>>;
|
||||
|
||||
constructor(state: SpecialVisualizationState, args: ArgT, tagsToApply: Store<Tag[]>) {
|
||||
constructor(state: SpecialVisualizationState, args: ArgT, tagsToApply: Store<Tag[]>, originalTags: UIEventSource<Record<string, string>>) {
|
||||
this.state = state;
|
||||
this.args = args;
|
||||
this.tagsToApply = tagsToApply;
|
||||
this._originalFeatureTags = originalTags;
|
||||
this.targetLayer = state.layerState.filteredLayers.get(args.targetLayer)
|
||||
|
||||
|
||||
|
@ -166,6 +168,11 @@ export default abstract class ImportFlow<ArgT extends ImportFlowArguments> {
|
|||
const state = this.state
|
||||
return state.featureSwitchIsTesting.map(isTesting => {
|
||||
const t = Translations.t.general.add.import
|
||||
|
||||
if(this._originalFeatureTags.data["_imported"] === "yes"){
|
||||
return {error: t.hasBeenImported}
|
||||
}
|
||||
|
||||
const usesTestUrl = this.state.osmConnection._oauth_config.url === OsmConnection.oauth_configs["osm-test"].url
|
||||
if (!state.layout.official && !(isTesting || usesTestUrl)) {
|
||||
// Unofficial theme - imports not allowed
|
||||
|
@ -191,7 +198,7 @@ export default abstract class ImportFlow<ArgT extends ImportFlowArguments> {
|
|||
}
|
||||
|
||||
return undefined
|
||||
}, [state.mapProperties.zoom, state.dataIsLoading])
|
||||
}, [state.mapProperties.zoom, state.dataIsLoading, this._originalFeatureTags])
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ import Translations from "../../i18n/Translations";
|
|||
/**
|
||||
* The wrapper to make the special visualisation for the PointImportFlow
|
||||
*/
|
||||
export class ImportPointButtonViz implements SpecialVisualization {
|
||||
export class PointImportButtonViz implements SpecialVisualization {
|
||||
|
||||
public readonly funcName: string
|
||||
public readonly docs: string | BaseUIElement
|
||||
|
@ -51,7 +51,7 @@ export class ImportPointButtonViz implements SpecialVisualization {
|
|||
}
|
||||
const baseArgs: PointImportFlowArguments = <any> Utils.ParseVisArgs(this.args, argument)
|
||||
const tagsToApply = ImportFlowUtils.getTagsToApply(tagSource , baseArgs)
|
||||
const importFlow = new PointImportFlowState(state, <Feature<Point>> feature, baseArgs, tagsToApply)
|
||||
const importFlow = new PointImportFlowState(state, <Feature<Point>> feature, baseArgs, tagsToApply, tagSource)
|
||||
|
||||
return new SvelteUIElement(
|
||||
PointImportFlow, {
|
||||
|
@ -60,7 +60,5 @@ export class ImportPointButtonViz implements SpecialVisualization {
|
|||
)
|
||||
}
|
||||
|
||||
getLayerDependencies(argsRaw: string[]): string[] {
|
||||
return ImportFlowUtils.getLayerDependenciesWithSnapOnto(this.args, argsRaw)
|
||||
}
|
||||
|
||||
}
|
|
@ -20,12 +20,10 @@ export interface PointImportFlowArguments extends ImportFlowArguments {
|
|||
export class PointImportFlowState extends ImportFlow<PointImportFlowArguments> {
|
||||
public readonly startCoordinate: [number, number]
|
||||
private readonly _originalFeature: Feature<Point>;
|
||||
private readonly _originalFeatureTags: UIEventSource<Record<string, string>>
|
||||
|
||||
constructor(state: SpecialVisualizationState, originalFeature: Feature<Point>, args: PointImportFlowArguments, tagsToApply: Store<Tag[]>) {
|
||||
super(state, args, tagsToApply);
|
||||
constructor(state: SpecialVisualizationState, originalFeature: Feature<Point>, args: PointImportFlowArguments, tagsToApply: Store<Tag[]>, originalFeatureTags: UIEventSource<Record<string, string>>) {
|
||||
super(state, args, tagsToApply, originalFeatureTags);
|
||||
this._originalFeature = originalFeature;
|
||||
this._originalFeatureTags = state.featureProperties.getStore(originalFeature.properties.id)
|
||||
this.startCoordinate = GeoOperations.centerpointCoordinates(originalFeature)
|
||||
}
|
||||
|
||||
|
|
|
@ -22,49 +22,42 @@ import FullNodeDatabaseSource from "../../../Logic/FeatureSource/TiledFeatureSou
|
|||
export default class WayImportButtonViz implements AutoAction, SpecialVisualization {
|
||||
|
||||
|
||||
public readonly funcName: string
|
||||
public readonly docs: string | BaseUIElement
|
||||
public readonly example?: string
|
||||
public readonly args: { name: string; defaultValue?: string; doc: string }[]
|
||||
public readonly funcName: string= "import_way_button"
|
||||
public readonly docs: string = "This button will copy the data from an external dataset into OpenStreetMap, copying the geometry and adding it as a 'line'" + ImportFlowUtils.documentationGeneral
|
||||
public readonly args: { name: string; defaultValue?: string; doc: string }[]= [
|
||||
...ImportFlowUtils.generalArguments,
|
||||
{
|
||||
name: "snap_to_point_if",
|
||||
doc: "Points with the given tags will be snapped to or moved",
|
||||
},
|
||||
{
|
||||
name: "max_snap_distance",
|
||||
doc: "If the imported object is a LineString or (Multi)Polygon, already existing OSM-points will be reused to construct the geometry of the newly imported way",
|
||||
defaultValue: "0.05",
|
||||
},
|
||||
{
|
||||
name: "move_osm_point_if",
|
||||
doc: "Moves the OSM-point to the newly imported point if these conditions are met",
|
||||
},
|
||||
{
|
||||
name: "max_move_distance",
|
||||
doc: "If an OSM-point is moved, the maximum amount of meters it is moved. Capped on 20m",
|
||||
defaultValue: "0.05",
|
||||
},
|
||||
{
|
||||
name: "snap_onto_layers",
|
||||
doc: "If no existing nearby point exists, but a line of a specified layer is closeby, snap to this layer instead",
|
||||
},
|
||||
{
|
||||
name: "snap_to_layer_max_distance",
|
||||
doc: "Distance to distort the geometry to snap to this layer",
|
||||
defaultValue: "0.1",
|
||||
},
|
||||
]
|
||||
|
||||
public readonly supportsAutoAction = true
|
||||
public readonly needsNodeDatabase = true
|
||||
|
||||
constructor() {
|
||||
this.funcName = "import_way_button"
|
||||
this.docs = "This button will copy the data from an external dataset into OpenStreetMap, copying the geometry and adding it as a 'line'" + ImportFlowUtils.documentationGeneral
|
||||
this.args = [
|
||||
...ImportFlowUtils.generalArguments,
|
||||
{
|
||||
name: "snap_to_point_if",
|
||||
doc: "Points with the given tags will be snapped to or moved",
|
||||
},
|
||||
{
|
||||
name: "max_snap_distance",
|
||||
doc: "If the imported object is a LineString or (Multi)Polygon, already existing OSM-points will be reused to construct the geometry of the newly imported way",
|
||||
defaultValue: "0.05",
|
||||
},
|
||||
{
|
||||
name: "move_osm_point_if",
|
||||
doc: "Moves the OSM-point to the newly imported point if these conditions are met",
|
||||
},
|
||||
{
|
||||
name: "max_move_distance",
|
||||
doc: "If an OSM-point is moved, the maximum amount of meters it is moved. Capped on 20m",
|
||||
defaultValue: "0.05",
|
||||
},
|
||||
{
|
||||
name: "snap_onto_layers",
|
||||
doc: "If no existing nearby point exists, but a line of a specified layer is closeby, snap to this layer instead",
|
||||
},
|
||||
{
|
||||
name: "snap_to_layer_max_distance",
|
||||
doc: "Distance to distort the geometry to snap to this layer",
|
||||
defaultValue: "0.1",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
constr(state: SpecialVisualizationState, tagSource: UIEventSource<Record<string, string>>, argument: string[], feature: Feature, layer: LayerConfig): BaseUIElement {
|
||||
const geometry = feature.geometry
|
||||
if (!(geometry.type == "LineString" || geometry.type === "Polygon")) {
|
||||
|
@ -100,8 +93,10 @@ export default class WayImportButtonViz implements AutoAction, SpecialVisualizat
|
|||
|
||||
const args: WayImportFlowArguments = <any>Utils.ParseVisArgs(this.args, argument)
|
||||
const tagsToApply = ImportFlowUtils.getTagsToApply(tagSource, args)
|
||||
const mergeConfigs = WayImportFlowState.GetMergeConfig(args, tagsToApply)
|
||||
const mergeConfigs = WayImportFlowState.GetMergeConfig(args)
|
||||
const action = WayImportFlowState.CreateAction(<Feature<LineString | Polygon >>feature, args, state, tagsToApply, mergeConfigs)
|
||||
tagSource.data["_imported"] = "yes"
|
||||
tagSource.ping()
|
||||
await state.changes.applyAction(action)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
<script lang="ts">
|
||||
|
||||
/**
|
||||
* Can be used for both WayImportFlow and ConflateImportFlow
|
||||
*/
|
||||
import WayImportFlowState from "./WayImportFlowState";
|
||||
import ImportFlow from "./ImportFlow.svelte";
|
||||
import MapControlButton from "../../Base/MapControlButton.svelte";
|
||||
|
@ -12,7 +14,8 @@
|
|||
import StaticFeatureSource from "../../../Logic/FeatureSource/Sources/StaticFeatureSource";
|
||||
import {ImportFlowUtils} from "./ImportFlow";
|
||||
import {GeoOperations} from "../../../Logic/GeoOperations";
|
||||
export let importFlow: WayImportFlowState
|
||||
import ConflateImportFlowState from "./ConflateImportFlowState";
|
||||
export let importFlow: WayImportFlowState | ConflateImportFlowState
|
||||
|
||||
const state = importFlow.state
|
||||
const map = new UIEventSource<MlMap>(undefined)
|
||||
|
|
|
@ -8,14 +8,14 @@ import CreateWayWithPointReuseAction, {
|
|||
MergePointConfig
|
||||
} from "../../../Logic/Osm/Actions/CreateWayWithPointReuseAction";
|
||||
import {TagUtils} from "../../../Logic/Tags/TagUtils";
|
||||
import {OsmCreateAction} from "../../../Logic/Osm/Actions/OsmChangeAction";
|
||||
import {OsmCreateAction, PreviewableAction} from "../../../Logic/Osm/Actions/OsmChangeAction";
|
||||
import {FeatureSource, IndexedFeatureSource} from "../../../Logic/FeatureSource/FeatureSource";
|
||||
import CreateMultiPolygonWithPointReuseAction from "../../../Logic/Osm/Actions/CreateMultiPolygonWithPointReuseAction";
|
||||
import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig";
|
||||
import {Changes} from "../../../Logic/Osm/Changes";
|
||||
import FullNodeDatabaseSource from "../../../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource";
|
||||
|
||||
export interface WayImportFlowArguments extends ImportFlowArguments {
|
||||
export interface WayImportFlowArguments extends ImportFlowArguments {
|
||||
max_snap_distance: string
|
||||
snap_onto_layers: string,
|
||||
snap_to_layer_max_distance: string,
|
||||
|
@ -28,13 +28,11 @@ export default class WayImportFlowState extends ImportFlow<WayImportFlowArgument
|
|||
public readonly originalFeature: Feature<LineString | Polygon>;
|
||||
|
||||
private readonly action: OsmCreateAction & { getPreview?(): Promise<FeatureSource>; }
|
||||
private readonly _originalFeatureTags: UIEventSource<Record<string, string>>;
|
||||
|
||||
constructor(state: SpecialVisualizationState, originalFeature: Feature<LineString | Polygon>, args: WayImportFlowArguments, tagsToApply: Store<Tag[]>, originalFeatureTags: UIEventSource<Record<string, string>>) {
|
||||
super(state, args, tagsToApply);
|
||||
super(state, args, tagsToApply, originalFeatureTags);
|
||||
this.originalFeature = originalFeature;
|
||||
this._originalFeatureTags = originalFeatureTags;
|
||||
const mergeConfigs = WayImportFlowState.GetMergeConfig(args, tagsToApply)
|
||||
const mergeConfigs = WayImportFlowState.GetMergeConfig(args)
|
||||
this.action = WayImportFlowState.CreateAction(originalFeature, args, state, tagsToApply, mergeConfigs)
|
||||
}
|
||||
|
||||
|
@ -49,7 +47,7 @@ export default class WayImportFlowState extends ImportFlow<WayImportFlowArgument
|
|||
},
|
||||
tagsToApply: Store<Tag[]>,
|
||||
mergeConfigs: MergePointConfig[]
|
||||
): OsmCreateAction & { getPreview?(): Promise<FeatureSource>; newElementId?: string } {
|
||||
): OsmCreateAction & PreviewableAction & { newElementId?: string } {
|
||||
if (feature.geometry.type === "Polygon" && feature.geometry.coordinates.length > 1) {
|
||||
const coors = (<Polygon>feature.geometry).coordinates
|
||||
const outer = coors[0]
|
||||
|
@ -76,7 +74,7 @@ export default class WayImportFlowState extends ImportFlow<WayImportFlowArgument
|
|||
}
|
||||
}
|
||||
|
||||
public static GetMergeConfig(args: WayImportFlowArguments, newTags: Store<Tag[]>): MergePointConfig[] {
|
||||
public static GetMergeConfig(args: WayImportFlowArguments): MergePointConfig[] {
|
||||
const nodesMustMatch = args.snap_to_point_if
|
||||
?.split(";")
|
||||
?.map((tag, i) => TagUtils.Tag(tag, "TagsSpec for import button " + i))
|
||||
|
@ -108,6 +106,7 @@ export default class WayImportFlowState extends ImportFlow<WayImportFlowArgument
|
|||
return mergeConfigs
|
||||
}
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
public async onConfirm() {
|
||||
const originalFeatureTags = this._originalFeatureTags
|
||||
originalFeatureTags.data["_imported"] = "yes"
|
||||
|
|
|
@ -14,6 +14,9 @@ import { Tag } from "../../Logic/Tags/Tag"
|
|||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import { Changes } from "../../Logic/Osm/Changes"
|
||||
import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import {IndexedFeatureSource} from "../../Logic/FeatureSource/FeatureSource";
|
||||
import {Feature} from "geojson";
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
||||
|
||||
export default class TagApplyButton implements AutoAction, SpecialVisualization {
|
||||
public readonly funcName = "tag_apply"
|
||||
|
@ -111,13 +114,16 @@ export default class TagApplyButton implements AutoAction, SpecialVisualization
|
|||
})
|
||||
}
|
||||
|
||||
async applyActionOn(
|
||||
public async applyActionOn(
|
||||
feature: Feature,
|
||||
state: {
|
||||
layout: LayoutConfig
|
||||
changes: Changes
|
||||
indexedFeatures: IndexedFeatureSource
|
||||
},
|
||||
tags: UIEventSource<any>,
|
||||
args: string[]
|
||||
args: string[],
|
||||
|
||||
): Promise<void> {
|
||||
const tagsToApply = TagApplyButton.generateTagsToApply(args[0], tags)
|
||||
const targetIdKey = args[3]
|
||||
|
@ -138,7 +144,9 @@ export default class TagApplyButton implements AutoAction, SpecialVisualization
|
|||
public constr(
|
||||
state: SpecialVisualizationState,
|
||||
tags: UIEventSource<Record<string, string>>,
|
||||
args: string[]
|
||||
args: string[],
|
||||
feature: Feature,
|
||||
layer: LayerConfig
|
||||
): BaseUIElement {
|
||||
const tagsToApply = TagApplyButton.generateTagsToApply(args[0], tags)
|
||||
const msg = args[1]
|
||||
|
@ -167,7 +175,7 @@ export default class TagApplyButton implements AutoAction, SpecialVisualization
|
|||
new Combine([msg, tagsExplanation]).SetClass("flex flex-col")
|
||||
).onClick(async () => {
|
||||
applied.setData(true)
|
||||
await self.applyActionOn(state, tags, args)
|
||||
await self.applyActionOn(feature, state, tags, args)
|
||||
})
|
||||
|
||||
return new Toggle(
|
||||
|
|
|
@ -71,8 +71,9 @@ import SplitRoadWizard from "./Popup/SplitRoadWizard"
|
|||
import {ExportAsGpxViz} from "./Popup/ExportAsGpxViz"
|
||||
import WikipediaPanel from "./Wikipedia/WikipediaPanel.svelte"
|
||||
import TagRenderingEditable from "./Popup/TagRendering/TagRenderingEditable.svelte";
|
||||
import {ImportPointButtonViz} from "./Popup/ImportButtons/ImportPointButtonViz";
|
||||
import {PointImportButtonViz} from "./Popup/ImportButtons/PointImportButtonViz";
|
||||
import WayImportButtonViz from "./Popup/ImportButtons/WayImportButtonViz";
|
||||
import ConflateImportButtonViz from "./Popup/ImportButtons/ConflateImportButtonViz";
|
||||
|
||||
class NearbyImageVis implements SpecialVisualization {
|
||||
// Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests
|
||||
|
@ -619,9 +620,9 @@ export default class SpecialVisualizations {
|
|||
|
||||
new TagApplyButton(),
|
||||
|
||||
new ImportPointButtonViz(),
|
||||
new PointImportButtonViz(),
|
||||
new WayImportButtonViz(),
|
||||
// TODO new ConflateButton(),
|
||||
new ConflateImportButtonViz(),
|
||||
|
||||
new NearbyImageVis(),
|
||||
|
||||
|
|
|
@ -1,22 +1,38 @@
|
|||
import { FixedUiElement } from "../Base/FixedUiElement"
|
||||
import { Translation, TypedTranslation } from "./Translation"
|
||||
import {FixedUiElement} from "../Base/FixedUiElement"
|
||||
import {Translation, TypedTranslation} from "./Translation"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import CompiledTranslations from "../../assets/generated/CompiledTranslations"
|
||||
import LanguageUtils from "../../Utils/LanguageUtils"
|
||||
import {ClickableToggle} from "../Input/Toggle";
|
||||
|
||||
export default class Translations {
|
||||
static readonly t: Readonly<typeof CompiledTranslations.t> = CompiledTranslations.t
|
||||
private static knownLanguages = LanguageUtils.usedLanguages
|
||||
|
||||
constructor() {
|
||||
throw "Translations is static. If you want to intitialize a new translation, use the singular form"
|
||||
}
|
||||
|
||||
public static W(s: string | number | BaseUIElement): BaseUIElement {
|
||||
public static W(s: string | number | boolean | BaseUIElement): BaseUIElement {
|
||||
if (typeof s === "string") {
|
||||
return new FixedUiElement(s)
|
||||
}
|
||||
if (typeof s === "number") {
|
||||
return new FixedUiElement("" + s)
|
||||
return new FixedUiElement("" + s).SetClass("font-bold")
|
||||
}
|
||||
if (typeof s === "boolean") {
|
||||
return new FixedUiElement("" + s).SetClass("font-bold")
|
||||
}
|
||||
if (typeof s === "object") {
|
||||
if (s.ConstructElement) {
|
||||
return s
|
||||
}
|
||||
const v = JSON.stringify(s)
|
||||
if (v.length > 100) {
|
||||
const shortened = v.substring(0, 100) + "..."
|
||||
return new ClickableToggle(v, shortened).ToggleOnClick().SetClass("literal-code button")
|
||||
}
|
||||
return new FixedUiElement(v).SetClass("literal-code")
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
@ -57,7 +73,7 @@ export default class Translations {
|
|||
t = "" + t
|
||||
}
|
||||
if (typeof t === "string") {
|
||||
return new TypedTranslation<object>({ "*": t }, context)
|
||||
return new TypedTranslation<object>({"*": t}, context)
|
||||
}
|
||||
if (t["render"] !== undefined) {
|
||||
const msg =
|
||||
|
|
25
Utils.ts
25
Utils.ts
|
@ -216,29 +216,12 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
|||
}
|
||||
|
||||
/**
|
||||
* Converts a number to a string with one number after the comma
|
||||
* Converts a number to a number with precisely 7 decimals
|
||||
*
|
||||
* Utils.Round(15) // => "15.0"
|
||||
* Utils.Round(1) // => "1.0"
|
||||
* Utils.Round(1.5) // => "1.5"
|
||||
* Utils.Round(0.5) // => "0.5"
|
||||
* Utils.Round(1.6) // => "1.6"
|
||||
* Utils.Round(-15) // => "-15.0"
|
||||
* Utils.Round(-1) // => "-1.0"
|
||||
* Utils.Round(-1.5) // => "-1.5"
|
||||
* Utils.Round(-0.5) // => "-0.5"
|
||||
* Utils.Round(-1.6) // => "-1.6"
|
||||
* Utils.Round(-1531.63543) // =>"-1531.6"
|
||||
* Utils.Round7(12.123456789) // => 12.1234568
|
||||
*/
|
||||
public static Round(i: number): string {
|
||||
if (i < 0) {
|
||||
return "-" + Utils.Round(-i)
|
||||
}
|
||||
const j = "" + Math.floor(i * 10)
|
||||
if (j.length == 1) {
|
||||
return "0." + j
|
||||
}
|
||||
return j.substr(0, j.length - 1) + "." + j.substr(j.length - 1, j.length)
|
||||
public static Round7(i: number): number {
|
||||
return Math.round(i * 10000000) / 10000000
|
||||
}
|
||||
|
||||
public static Times(f: (i: number) => string, count: number): string {
|
||||
|
|
|
@ -46,9 +46,6 @@
|
|||
},
|
||||
"maxCacheAge": 0
|
||||
},
|
||||
"calculatedTags": [
|
||||
"_surface:strict:=get(feat)('_surface')"
|
||||
],
|
||||
"mapRendering": [
|
||||
{
|
||||
"width": {
|
||||
|
@ -293,24 +290,24 @@
|
|||
"name": "GRB geometries",
|
||||
"title": "GRB outline",
|
||||
"calculatedTags": [
|
||||
"_overlaps_with_buildings=overlapWith(feat)('osm-buildings').filter(f => f.feat.properties.id.indexOf('-') < 0)",
|
||||
"_overlaps_with_buildings=overlapWith(feat)('osm-buildings').filter(f => f.feat.properties.id.indexOf('-') < 0) ?? []",
|
||||
"_overlaps_with=get(feat)('_overlaps_with_buildings').find(f => f.overlap > 1 /* square meter */ )",
|
||||
"_osm_obj:source:ref=get(feat)('_overlaps_with')?.feat?.properties['source:geometry:ref']",
|
||||
"_osm_obj:id=get(feat)('_overlaps_with')?.feat?.properties?.id",
|
||||
"_osm_obj:source:date=(get(feat)('_overlaps_with')?.feat?.properties ?? {})['source:geometry:date']?.replace(/\\//g, '-')",
|
||||
"_osm_obj:source:date=(feat.properties['_overlaps_with']?.feat?.properties ?? {})['source:geometry:date']?.replace(/\\//g, '-')",
|
||||
"_osm_obj:building=get(feat)('_overlaps_with')?.feat?.properties?.building",
|
||||
"_osm_obj:addr:street=(get(feat)('_overlaps_with')?.feat?.properties ?? {})['addr:street']",
|
||||
"_osm_obj:addr:housenumber=(get(feat)('_overlaps_with')?.feat?.properties ?? {})['addr:housenumber']",
|
||||
"_osm_obj:surface=(get(feat)('_overlaps_with')?.feat?.properties ?? {})['_surface:strict']",
|
||||
"_osm_obj:surface=(get(feat)('_overlaps_with')?.feat?.properties ?? {})['_surface']",
|
||||
"_overlap_absolute=get(feat)('_overlaps_with')?.overlap",
|
||||
"_reverse_overlap_percentage=Math.round(100 * get(feat)('_overlap_absolute') / get(feat)('_surface'))",
|
||||
"_overlap_percentage=Math.round(100 * get(feat)('_overlap_absolute') / get(feat)('_osm_obj:surface'))",
|
||||
"_grb_ref=feat.properties['source:geometry:entity'] + '/' + feat.properties['source:geometry:oidn']",
|
||||
"_imported_osm_object_found= feat.properties['_osm_obj:source:ref'] == feat.properties._grb_ref",
|
||||
"_imported_osm_object_found=feat.properties['_osm_obj:source:ref'] === feat.properties['_grb_ref']",
|
||||
"_grb_date=feat.properties['source:geometry:date'].replace(/\\//g,'-')",
|
||||
"_imported_osm_still_fresh= feat.properties['_osm_obj:source:date'] == feat.properties._grb_date",
|
||||
"_imported_osm_still_fresh=feat.properties['_osm_obj:source:date'] == feat.properties._grb_date",
|
||||
"_target_building_type=feat.properties['_osm_obj:building'] === 'yes' ? feat.properties.building : (feat.properties['_osm_obj:building'] ?? feat.properties.building)",
|
||||
"_building:min_level= feat.properties['fixme']?.startsWith('verdieping, correct the building tag, add building:level and building:min_level before upload in JOSM!') ? '1' : ''",
|
||||
"_building:min_level=feat.properties['fixme']?.startsWith('verdieping, correct the building tag, add building:level and building:min_level before upload in JOSM!') ? '1' : ''",
|
||||
"_intersects_with_other_features=intersectionsWith(feat)('generic_osm_object').map(f => \"<a href='https://osm.org/\"+f.feat.properties.id+\"' target='_blank'>\" + f.feat.properties.id + \"</a>\").join(', ')"
|
||||
],
|
||||
"tagRenderings": [
|
||||
|
@ -358,7 +355,6 @@
|
|||
"and": [
|
||||
"_overlap_percentage>50",
|
||||
"_reverse_overlap_percentage>50",
|
||||
"_overlaps_with!=",
|
||||
"_osm_obj:addr:street=",
|
||||
"_osm_obj:addr:housenumber=",
|
||||
"addr:street~*",
|
||||
|
@ -373,8 +369,7 @@
|
|||
"if": {
|
||||
"and": [
|
||||
"_overlap_percentage>50",
|
||||
"_reverse_overlap_percentage>50",
|
||||
"_overlaps_with!="
|
||||
"_reverse_overlap_percentage>50"
|
||||
]
|
||||
},
|
||||
"then": "{conflate_button(osm-buildings,building=$_target_building_type; source:geometry:date=$_grb_date; source:geometry:ref=$_grb_ref, Replace the geometry in OpenStreetMap,,_osm_obj:id)}"
|
||||
|
@ -385,6 +380,7 @@
|
|||
"id": "Building info",
|
||||
"render": "This is a <b>{building}</b> <span class='subtle'>detected by {detection_method}</span>"
|
||||
},
|
||||
|
||||
{
|
||||
"id": "overlapping building address",
|
||||
"render": "The overlapping openstreetmap-building has no address information at all",
|
||||
|
@ -435,10 +431,20 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "overlapping building id",
|
||||
"render": "The overlapping <a href='https://osm.org/{_osm_obj:id}' target='_blank'>openstreetmap-building has id {_osm_obj:id}</a>",
|
||||
"condition": "_osm_obj:id~*"
|
||||
},
|
||||
{
|
||||
"id": "overlapping building type",
|
||||
"render": "<div>The overlapping <a href='https://osm.org/{_osm_obj:id}' target='_blank'>openstreetmap-building</a> is a <b>{_osm_obj:building}</b> and covers <b>{_overlap_percentage}%</b> of the GRB building.<br/>The GRB-building covers <b>{_reverse_overlap_percentage}%</b> of the OSM building<div><h3>GRB geometry:</h3>{minimap(21, id):height:10rem;border-radius:1rem;overflow:hidden}<h3>OSM geometry:</h3>{minimap(21,_osm_obj:id):height:10rem;border-radius:1rem;overflow:hidden}",
|
||||
"condition": "_overlaps_with!="
|
||||
"render": "The overlapping building is a <b>{_osm_obj:building}</b> and covers <b>{_overlap_percentage}%</b> of the GRB building. <br/>The GRB-building covers <b>{_reverse_overlap_percentage}%</b> of the OSM building<br/>The OSM-building is based on GRB-data from {_osm_obj:source:date}.",
|
||||
"condition": "_osm_obj:id~*"
|
||||
},
|
||||
{
|
||||
"id": "overlapping building map",
|
||||
"render": "<h3>GRB geometry:</h3>{minimap(21, id):height:10rem;border-radius:1rem;overflow:hidden}<h3>OSM geometry:</h3>{minimap(21,_osm_obj:id):height:10rem;border-radius:1rem;overflow:hidden}",
|
||||
"condition": "_osm_obj:id~*"
|
||||
},
|
||||
{
|
||||
"id": "apply-id",
|
||||
|
@ -717,15 +723,17 @@
|
|||
{
|
||||
"builtin": "current_view",
|
||||
"override": {
|
||||
"calculatedTags": [
|
||||
"calculatedTags+": [
|
||||
"_overlapping=Number(feat.properties.zoom) >= 16 ? overlapWith(feat)('grb').map(ff => ff.feat.properties) : undefined",
|
||||
"_applicable=get(feat)('_overlapping')?.filter(p => (p._imported_osm_object_found === 'true' || p._intersects_with_other_features === ''))?.map(p => p.id)",
|
||||
"_applicable_count=get(feat)('_applicable')?.length"
|
||||
"_applicable_conflate=get(feat)('_overlapping')?.filter(p => p._imported !== 'yes' && (!p['_imported_osm_still_fresh'] || !p['_imported_osm_object_found']) && p['_overlap_absolute'] > 10 && p['_overlap_percentage'] > 80 && p['_reverse_overlap_percentage'] > 80)?.map(p => p.id)",
|
||||
"_applicable=feat.properties._overlapping.filter(p => p._imported !== 'yes' && p._imported_osm_object_found === false && !(p['_overlap_absolute'] > 5) && !p._intersects_with_other_features)?.map(p => p.id)",
|
||||
"_applicable_count=get(feat)('_applicable')?.length",
|
||||
"_applicable_conflate_count=get(feat)('_applicable_conflate')?.length"
|
||||
],
|
||||
"tagRenderings+": [
|
||||
{
|
||||
"id": "hw",
|
||||
"render": "There are {_applicable_count} applicable elements in view",
|
||||
"render": "There are {_applicable_count} non-overlapping buildings in view and {_applicable_conflate_count} conflatable buildings",
|
||||
"mappings": [
|
||||
{
|
||||
"if": "zoom<14",
|
||||
|
@ -737,13 +745,23 @@
|
|||
},
|
||||
{
|
||||
"if": "_applicable_count=0",
|
||||
"then": "No importable buildins in view"
|
||||
"then": "No importable buildings in view"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "autoapply",
|
||||
"render": "{auto_apply(grb, _applicable, Import-button, Import or conflate all non-conflicting buildings in view)}",
|
||||
"render": "{auto_apply(grb, _applicable, Import-button, Import all non-conflicting buildings in view)}",
|
||||
"mappings": [
|
||||
{
|
||||
"if": "zoom<16",
|
||||
"then": "Zoom in more to import"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "autoapply_conflate",
|
||||
"render": "{auto_apply(grb, _applicable_conflate, Import-button, Conflate all non-conflicting buildings in view)}",
|
||||
"mappings": [
|
||||
{
|
||||
"if": "zoom<16",
|
||||
|
@ -762,6 +780,7 @@
|
|||
},
|
||||
"iconSize": "15,15,center"
|
||||
}
|
||||
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
12
package-lock.json
generated
12
package-lock.json
generated
|
@ -4475,9 +4475,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001486",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001486.tgz",
|
||||
"integrity": "sha512-uv7/gXuHi10Whlj0pp5q/tsK/32J2QSqVRKQhs2j8VsDCjgyruAh/eEXHF822VqO9yT6iZKw3nRwZRSPBE9OQg==",
|
||||
"version": "1.0.30001492",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001492.tgz",
|
||||
"integrity": "sha512-2efF8SAZwgAX1FJr87KWhvuJxnGJKOnctQa8xLOskAXNXq8oiuqgl6u1kk3fFpsp3GgvzlRjiK1sl63hNtFADw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
@ -15408,9 +15408,9 @@
|
|||
"integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="
|
||||
},
|
||||
"caniuse-lite": {
|
||||
"version": "1.0.30001486",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001486.tgz",
|
||||
"integrity": "sha512-uv7/gXuHi10Whlj0pp5q/tsK/32J2QSqVRKQhs2j8VsDCjgyruAh/eEXHF822VqO9yT6iZKw3nRwZRSPBE9OQg==",
|
||||
"version": "1.0.30001492",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001492.tgz",
|
||||
"integrity": "sha512-2efF8SAZwgAX1FJr87KWhvuJxnGJKOnctQa8xLOskAXNXq8oiuqgl6u1kk3fFpsp3GgvzlRjiK1sl63hNtFADw==",
|
||||
"dev": true
|
||||
},
|
||||
"canvg": {
|
||||
|
|
23
test.ts
23
test.ts
|
@ -32,30 +32,7 @@ async function testPdf() {
|
|||
await pdf.ConvertSvg("nl")
|
||||
}
|
||||
|
||||
function testImportButton() {
|
||||
const layout = new LayoutConfig(<any>theme, true) // qp.data === "" ? : new AllKnownLayoutsLazy().get(qp.data)
|
||||
const state = new ThemeViewState(layout)
|
||||
const originalFeature: Feature<Point> = {
|
||||
type: "Feature",
|
||||
properties: {
|
||||
id: "note/-1"
|
||||
},
|
||||
geometry: {
|
||||
type: "Point",
|
||||
coordinates: [3.2255, 51.2112]
|
||||
}
|
||||
}
|
||||
const importFlow = new PointImportFlowState(state, originalFeature, {
|
||||
text: "Import this point",
|
||||
newTags: undefined,
|
||||
targetLayer: "public_bookcase"
|
||||
}, tagsToApply)
|
||||
new SvelteUIElement(PointImportFlow, {
|
||||
importFlow
|
||||
}).SetClass("h-full").AttachTo("maindiv")
|
||||
}
|
||||
|
||||
testImportButton()
|
||||
// testPdf().then((_) => console.log("All done"))
|
||||
/*/
|
||||
testspecial()
|
||||
|
|
|
@ -85,7 +85,7 @@ describe("Code quality", () => {
|
|||
"innerText is not allowed as it is not testable with fakeDom. Use 'textContent' instead."
|
||||
)
|
||||
)
|
||||
|
||||
/*
|
||||
itAsync(
|
||||
"should not contain 'import * as name from \"xyz.json\"'",
|
||||
detectInCode(
|
||||
|
|
|
@ -6,6 +6,7 @@ import { describe, expect, it } from "vitest"
|
|||
import { OsmConnection } from "../../../../Logic/Osm/OsmConnection"
|
||||
import { ImmutableStore } from "../../../../Logic/UIEventSource"
|
||||
import { Changes } from "../../../../Logic/Osm/Changes"
|
||||
import FullNodeDatabaseSource from "../../../../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource";
|
||||
|
||||
describe("ReplaceGeometryAction", () => {
|
||||
const grbStripped = {
|
||||
|
@ -873,10 +874,6 @@ describe("ReplaceGeometryAction", () => {
|
|||
)
|
||||
|
||||
it("should move nodes accordingly", async () => {
|
||||
/**
|
||||
* TODO this is disabled - enable it again when the code works!
|
||||
*/
|
||||
return
|
||||
const layout = new LayoutConfig(<any>grbStripped)
|
||||
|
||||
const bbox = new BBox([
|
||||
|
@ -885,8 +882,8 @@ describe("ReplaceGeometryAction", () => {
|
|||
])
|
||||
const url = `https://www.openstreetmap.org/api/0.6/map.json?bbox=${bbox.minLon},${bbox.minLat},${bbox.maxLon},${bbox.maxLat}`
|
||||
const data = await Utils.downloadJson(url)
|
||||
const fullNodeDatabase = undefined // TODO new FullNodeDatabaseSource(undefined)
|
||||
// TODO fullNodeDatabase.handleOsmJson(data, 0)
|
||||
const fullNodeDatabase = new FullNodeDatabaseSource()
|
||||
fullNodeDatabase.handleOsmJson(data, 0, 0, 0)
|
||||
const changes = new Changes({
|
||||
dryRun: new ImmutableStore(true),
|
||||
osmConnection: new OsmConnection()
|
||||
|
|
Loading…
Reference in a new issue