Refactoring: port import flow

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

View file

@ -1,3 +1,4 @@
[//]: # (WARNING: this file is automatically generated. Please find the sources at the bottom and edit those sources) [//]: # (WARNING: this file is automatically generated. Please find the sources at the bottom and edit those sources)
Special tag renderings Special tag renderings

View file

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

View file

@ -10,23 +10,25 @@ import { UIEventSource } from "../../UIEventSource"
*/ */
export default class TileLocalStorage<T> { export default class TileLocalStorage<T> {
private static perLayer: Record<string, TileLocalStorage<any>> = {} private static perLayer: Record<string, TileLocalStorage<any>> = {}
private static readonly useIndexedDb = typeof indexedDB !== "undefined"
private readonly _layername: string private readonly _layername: string
private readonly inUse = new UIEventSource(false) private readonly inUse = new UIEventSource(false)
private readonly cachedSources: Record<number, UIEventSource<T> & { flush: () => void }> = {} private readonly cachedSources: Record<number, UIEventSource<T> & { flush: () => void }> = {}
private readonly _maxAgeSeconds: number;
private static readonly useIndexedDb = typeof indexedDB !== "undefined" private constructor(layername: string, maxAgeSeconds: number) {
private constructor(layername: string) {
this._layername = layername 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 key = backend + "_" + layername
const cached = TileLocalStorage.perLayer[key] const cached = TileLocalStorage.perLayer[key]
if (cached) { if (cached) {
return cached return cached
} }
const tls = new TileLocalStorage<T>(key) const tls = new TileLocalStorage<T>(key, maxAgeS)
TileLocalStorage.perLayer[key] = tls TileLocalStorage.perLayer[key] = tls
return tls return tls
} }
@ -57,6 +59,8 @@ export default class TileLocalStorage<T> {
await this.inUse.AsPromise((inUse) => !inUse) await this.inUse.AsPromise((inUse) => !inUse)
this.inUse.setData(true) this.inUse.setData(true)
await IdbLocalStorage.SetDirectly(this._layername + "_" + tileIndex, data) await IdbLocalStorage.SetDirectly(this._layername + "_" + tileIndex, data)
await IdbLocalStorage.SetDirectly(this._layername + "_" + tileIndex + "_date", Date.now())
this.inUse.setData(false) this.inUse.setData(false)
} catch (e) { } catch (e) {
console.error( console.error(
@ -72,10 +76,24 @@ export default class TileLocalStorage<T> {
} }
} }
private GetIdb(tileIndex: number): Promise<any> { private async GetIdb(tileIndex: number): Promise<any> {
if (!TileLocalStorage.useIndexedDb) { if (!TileLocalStorage.useIndexedDb) {
return undefined return undefined
} }
return IdbLocalStorage.GetDirectly(this._layername + "_" + tileIndex) const date = <any>await IdbLocalStorage.GetDirectly(this._layername + "_" + tileIndex + "_date")
const maxAge = this._maxAgeSeconds
const timeDiff = Date.now() - date
if (timeDiff >= maxAge) {
console.log("Dropping cache for", this._layername, tileIndex, "out of date")
await IdbLocalStorage.SetDirectly(this._layername + "_" + tileIndex, undefined)
return undefined
}
const data = await IdbLocalStorage.GetDirectly(this._layername + "_" + tileIndex)
return <any>data
}
invalidate(zoomlevel: number, tileIndex) {
this.getTileSource(tileIndex).setData(undefined)
} }
} }

View file

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

View file

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

View file

@ -7,6 +7,7 @@ import { TagsFilter } from "../../Tags/TagsFilter"
import { Feature } from "geojson" import { Feature } from "geojson"
import FeatureSourceMerger from "../Sources/FeatureSourceMerger" import FeatureSourceMerger from "../Sources/FeatureSourceMerger"
import OsmObjectDownloader from "../../Osm/OsmObjectDownloader" 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' * 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 isActive: Store<boolean>
private readonly _backend: string private readonly _backend: string
private readonly allowedTags: TagsFilter 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 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 _downloadedTiles: Set<number> = new Set<number>()
private readonly _downloadedData: Feature[][] = [] 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' * If given: this featureSwitch will not update if the store contains 'false'
*/ */
isActive?: Store<boolean>, isActive?: Store<boolean>,
patchRelations?: true | boolean patchRelations?: true | boolean,
fullNodeDatabase?: FullNodeDatabaseSource
}) { }) {
super() super()
this.options = options;
this._bounds = options.bounds this._bounds = options.bounds
this.allowedTags = options.allowedFeatures this.allowedTags = options.allowedFeatures
this.isActive = options.isActive ?? new ImmutableStore(true) this.isActive = options.isActive ?? new ImmutableStore(true)
@ -119,7 +132,7 @@ export default class OsmFeatureSource extends FeatureSourceMerger {
return feature 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) console.log("OsmFeatureSource: loading ", z, x, y, "from", this._backend)
if (z >= 22) { if (z >= 22) {
throw "This is an absurd high zoom level" throw "This is an absurd high zoom level"
@ -141,9 +154,7 @@ export default class OsmFeatureSource extends FeatureSourceMerger {
try { try {
const osmJson = await Utils.downloadJsonCached(url, 2000) const osmJson = await Utils.downloadJsonCached(url, 2000)
try { try {
this.rawDataHandlers.forEach((handler) => this.options?.fullNodeDatabase?.handleOsmJson(osmJson, z, x, y)
handler(osmJson, Tiles.tile_index(z, x, y))
)
let features = <Feature<any, { id: string }>[]>OsmToGeoJson( let features = <Feature<any, { id: string }>[]>OsmToGeoJson(
osmJson, osmJson,
// @ts-ignore // @ts-ignore
@ -181,7 +192,7 @@ export default class OsmFeatureSource extends FeatureSourceMerger {
y, y,
"due to", "due to",
e, e,
"; retrying with smaller bounds" e === "rate limited" ? "; stopping now" : "; retrying with smaller bounds"
) )
if (e === "rate limited") { if (e === "rate limited") {
return return

View file

@ -1,31 +1,25 @@
import {FeatureSource, FeatureSourceForLayer, Tiled } from "../FeatureSource"
import {OsmNode, OsmObject, OsmWay} from "../../Osm/OsmObject" import {OsmNode, OsmObject, OsmWay} from "../../Osm/OsmObject"
import SimpleFeatureSource from "../Sources/SimpleFeatureSource"
import FilteredLayer from "../../../Models/FilteredLayer"
import {UIEventSource} from "../../UIEventSource" import {UIEventSource} from "../../UIEventSource"
import { OsmTags } from "../../../Models/OsmFeature";
import {BBox} from "../../BBox"; import {BBox} from "../../BBox";
import { Feature, Point } from "geojson"; import StaticFeatureSource from "../Sources/StaticFeatureSource";
import {Tiles} from "../../../Models/TileRange";
export default class FullNodeDatabaseSource { export default class FullNodeDatabaseSource {
public readonly loadedTiles = new Map<number, FeatureSource & Tiled>()
private readonly onTileLoaded: (tile: Tiled & FeatureSourceForLayer) => void private readonly loadedTiles = new Map<number, Map<number, OsmNode>>()
private readonly layer: FilteredLayer
private readonly nodeByIds = new Map<number, OsmNode>() private readonly nodeByIds = new Map<number, OsmNode>()
private readonly parentWays = new Map<number, UIEventSource<OsmWay[]>>() private readonly parentWays = new Map<number, UIEventSource<OsmWay[]>>()
constructor(layer: FilteredLayer, onTileLoaded: (tile: Tiled & FeatureSourceForLayer) => void) { private smallestZoom = 99
this.onTileLoaded = onTileLoaded private largestZoom = 0
this.layer = layer
if (this.layer === undefined) {
throw "Layer is undefined"
}
}
public handleOsmJson(osmJson: any, tileId: number) { public handleOsmJson(osmJson: any, z: number, x: number, y: number) : void {
const allObjects = OsmObject.ParseObjects(osmJson.elements) const allObjects = OsmObject.ParseObjects(osmJson.elements)
const nodesById = new Map<number, OsmNode>() 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) { for (const osmObj of allObjects) {
if (osmObj.type !== "node") { if (osmObj.type !== "node") {
continue continue
@ -59,10 +53,9 @@ export default class FullNodeDatabaseSource {
osmNode.asGeoJson() osmNode.asGeoJson()
) )
const featureSource = new SimpleFeatureSource(this.layer, tileId) const featureSource = new StaticFeatureSource(asGeojsonFeatures)
featureSource.features.setData(asGeojsonFeatures) const tileId = Tiles.tile_index(z, x, y)
this.loadedTiles.set(tileId, featureSource) this.loadedTiles.set(tileId, nodesById)
this.onTileLoaded(featureSource)
} }
/** /**
@ -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 * @param nodeId
* @constructor * @constructor
*/ */
@ -84,8 +77,20 @@ export default class FullNodeDatabaseSource {
return this.parentWays.get(nodeId) return this.parentWays.get(nodeId)
} }
getNodesWithin(bBox: BBox) : Feature<Point, OsmTags>[]{ /**
// 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
throw "TODO" * @param bbox
*/
getNodesWithin(bbox: BBox) : Map<number, OsmNode>{
const allById = new Map<number, OsmNode>()
for (let z = this.smallestZoom; z < this.largestZoom; z++) {
const range = Tiles.tileRangeFrom(bbox, z)
Tiles.MapRange(range, (x, y ) => {
const tileId = Tiles.tile_index(z, x, y)
const nodesById = this.loadedTiles.get(tileId)
nodesById?.forEach((v,k) => allById.set(k,v))
})
}
return allById
} }
} }

View file

@ -4,6 +4,8 @@ import { BBox } from "../../BBox"
import TileLocalStorage from "../Actors/TileLocalStorage" import TileLocalStorage from "../Actors/TileLocalStorage"
import {Feature} from "geojson" import {Feature} from "geojson"
import StaticFeatureSource from "../Sources/StaticFeatureSource" import StaticFeatureSource from "../Sources/StaticFeatureSource"
import {constants} from "http2";
import HTTP_STATUS_CONTINUE = module
export default class LocalStorageFeatureSource extends DynamicTileSource { export default class LocalStorageFeatureSource extends DynamicTileSource {
constructor( constructor(
@ -15,18 +17,25 @@ export default class LocalStorageFeatureSource extends DynamicTileSource {
zoom: Store<number> zoom: Store<number>
}, },
options?: { 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( super(
zoomlevel, zoomlevel,
(tileIndex) => (tileIndex) =>
new StaticFeatureSource( new StaticFeatureSource(
storage storage
.getTileSource(tileIndex) .getTileSource(tileIndex)
.map((features) => .mapD((features) => {
features?.filter((f) => !f.properties.id.match(/(node|way)\/-[0-9]+/)) 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, mapProperties,

View file

@ -9,6 +9,7 @@ import {IndexedFeatureSource} from "./FeatureSource/FeatureSource"
import OsmObjectDownloader from "./Osm/OsmObjectDownloader" import OsmObjectDownloader from "./Osm/OsmObjectDownloader"
import {Utils} from "../Utils"; import {Utils} from "../Utils";
import {GeoJSONFeature} from "maplibre-gl"; import {GeoJSONFeature} from "maplibre-gl";
import {UIEventSource} from "./UIEventSource";
/** /**
* Metatagging adds various tags to the elements, e.g. lat, lon, surface area, ... * 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 { export default class MetaTagging {
private static errorPrintCount = 0 private static errorPrintCount = 0
private static readonly stopErrorOutputAt = 10 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: { constructor(state: {
layout: LayoutConfig layout: LayoutConfig
@ -27,19 +28,7 @@ export default class MetaTagging {
indexedFeatures: IndexedFeatureSource indexedFeatures: IndexedFeatureSource
featureProperties: FeaturePropertiesStore featureProperties: FeaturePropertiesStore
}) { }) {
const params: ExtraFuncParams = { const params = MetaTagging.createExtraFuncParams(state)
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)];
},
}
for (const layer of state.layout.layers) { for (const layer of state.layout.layers) {
if (layer.source === null) { if (layer.source === null) {
continue continue
@ -117,15 +106,7 @@ export default class MetaTagging {
const tags = featurePropertiesStores?.getStore(feature.properties.id) const tags = featurePropertiesStores?.getStore(feature.properties.id)
let somethingChanged = false let somethingChanged = false
let definedTags = new Set(Object.getOwnPropertyNames(feature.properties)) 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) { for (const metatag of metatagsToApply) {
try { try {
if (!metatag.keys.some((key) => !(key in feature.properties))) { if (!metatag.keys.some((key) => !(key in feature.properties))) {
@ -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) { if (somethingChanged) {
try { try {
featurePropertiesStores?.getStore(feature.properties.id)?.ping() tags?.ping()
} catch (e) { } catch (e) {
console.error("Could not ping a store for a changed property due to", 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 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 * Creates a function that implements that calculates a property and adds this property onto the feature properties
* @param specification * @param specification
@ -200,25 +206,25 @@ export default class MetaTagging {
private static createFunctionForFeature([key, code, isStrict]: [string, string, boolean], private static createFunctionForFeature([key, code, isStrict]: [string, string, boolean],
helperFunctions: Record<ExtraFuncType, (feature: Feature) => Function>, helperFunctions: Record<ExtraFuncType, (feature: Feature) => Function>,
layerId: string = "unkown layer" layerId: string = "unkown layer"
): ((feature: GeoJSONFeature) => void) | undefined { ): ((feature: GeoJSONFeature, propertiesStore?: UIEventSource<any>) => void) | undefined {
if (code === undefined) { if (code === undefined) {
return undefined return undefined
} }
const calculateAndAssign: ((feat: GeoJSONFeature, store?: UIEventSource<any>) => string | any) = (feat, store) => {
const calculateAndAssign: ((feat: GeoJSONFeature) => (string | undefined)) = (feat) => {
try { 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 === "") { if (result === "") {
result = undefined result = undefined
} }
if (result !== undefined && typeof result !== "string") { const oldValue= feat.properties[key]
// Make sure it is a string! if(oldValue == result){
result = JSON.stringify(result) return oldValue
} }
delete feat.properties[key] delete feat.properties[key]
feat.properties[key] = result feat.properties[key] = result
store?.ping()
return result return result
} catch (e) { } catch (e) {
if (MetaTagging.errorPrintCount < MetaTagging.stopErrorOutputAt) { if (MetaTagging.errorPrintCount < MetaTagging.stopErrorOutputAt) {
@ -253,10 +259,9 @@ export default class MetaTagging {
if (isStrict) { if (isStrict) {
return calculateAndAssign return calculateAndAssign
} }
return (feature: any) => { return (feature: any, store?: UIEventSource<any>) => {
delete feature.properties[key] delete feature.properties[key]
Utils.AddLazyProperty(feature.properties, key, () => calculateAndAssign(feature)) Utils.AddLazyProperty(feature.properties, key, () => calculateAndAssign(feature, store))
return undefined
} }
} }
@ -266,19 +271,19 @@ export default class MetaTagging {
private static createRetaggingFunc( private static createRetaggingFunc(
layer: LayerConfig, layer: LayerConfig,
helpers: Record<ExtraFuncType, (feature: Feature) => Function> helpers: Record<ExtraFuncType, (feature: Feature) => Function>
): (feature: any) => boolean { ): (feature: Feature, tags: UIEventSource<Record<string, any>>) => boolean {
const calculatedTags: [string, string, boolean][] = layer.calculatedTags const calculatedTags: [string, string, boolean][] = layer.calculatedTags
if (calculatedTags === undefined || calculatedTags.length === 0) { if (calculatedTags === undefined || calculatedTags.length === 0) {
return undefined 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) { if (functions === undefined) {
functions = calculatedTags.map(spec => this.createFunctionForFeature(spec, helpers, layer.id)) functions = calculatedTags.map(spec => this.createFunctionForFeature(spec, helpers, layer.id))
MetaTagging.retaggingFuncCache.set(layer.id, functions) MetaTagging.retaggingFuncCache.set(layer.id, functions)
} }
return (feature: Feature) => { return (feature: Feature, store: UIEventSource<Record<string, any>>) => {
const tags = feature.properties const tags = feature.properties
if (tags === undefined) { if (tags === undefined) {
return return
@ -286,7 +291,7 @@ export default class MetaTagging {
try { try {
for (const f of functions) { for (const f of functions) {
f(feature) f(feature, store)
} }
} catch (e) { } catch (e) {
console.error("Invalid syntax in calculated tags or some other error: ", e) console.error("Invalid syntax in calculated tags or some other error: ", e)

View file

@ -1,4 +1,4 @@
import {OsmCreateAction} from "./OsmChangeAction" import {OsmCreateAction, PreviewableAction} from "./OsmChangeAction"
import {Tag} from "../../Tags/Tag" import {Tag} from "../../Tags/Tag"
import {Changes} from "../Changes" import {Changes} from "../Changes"
import {ChangeDescription} from "./ChangeDescription" import {ChangeDescription} from "./ChangeDescription"
@ -6,7 +6,7 @@ import CreateNewWayAction from "./CreateNewWayAction"
import CreateWayWithPointReuseAction, {MergePointConfig} from "./CreateWayWithPointReuseAction" import CreateWayWithPointReuseAction, {MergePointConfig} from "./CreateWayWithPointReuseAction"
import {And} from "../../Tags/And" import {And} from "../../Tags/And"
import {TagUtils} from "../../Tags/TagUtils" import {TagUtils} from "../../Tags/TagUtils"
import {IndexedFeatureSource} from "../../FeatureSource/FeatureSource" import {FeatureSource, IndexedFeatureSource} from "../../FeatureSource/FeatureSource"
import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig"; import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig";
import {Position} from "geojson"; import {Position} from "geojson";
import FullNodeDatabaseSource from "../../FeatureSource/TiledFeatureSource/FullNodeDatabaseSource"; 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 * 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 newElementId: string = undefined
public newElementIdNumber: number = undefined public newElementIdNumber: number = undefined
private readonly _tags: Tag[] private readonly _tags: Tag[]
@ -67,7 +67,6 @@ export default class CreateMultiPolygonWithPointReuseAction extends OsmCreateAct
} }
protected async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> { protected async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
console.log("Running CMPWPRA")
const descriptions: ChangeDescription[] = [] const descriptions: ChangeDescription[] = []
descriptions.push(...(await this.createOuterWay.CreateChangeDescriptions(changes))) descriptions.push(...(await this.createOuterWay.CreateChangeDescriptions(changes)))
for (const innerWay of this.createInnerWays) { for (const innerWay of this.createInnerWays) {
@ -103,4 +102,8 @@ export default class CreateMultiPolygonWithPointReuseAction extends OsmCreateAct
return descriptions return descriptions
} }
getPreview(): Promise<FeatureSource> {
return undefined
}
} }

View file

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

View file

@ -1,4 +1,4 @@
import {OsmCreateAction} from "./OsmChangeAction" import {OsmCreateAction, PreviewableAction} from "./OsmChangeAction"
import {Tag} from "../../Tags/Tag" import {Tag} from "../../Tags/Tag"
import {Changes} from "../Changes" import {Changes} from "../Changes"
import {ChangeDescription} from "./ChangeDescription" 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 * 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 newElementId: string = undefined
public newElementIdNumber: number = undefined public newElementIdNumber: number = undefined
private readonly _tags: Tag[] private readonly _tags: Tag[]

View file

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

View file

@ -1,4 +1,4 @@
import OsmChangeAction from "./OsmChangeAction" import OsmChangeAction, {PreviewableAction} from "./OsmChangeAction"
import {Changes} from "../Changes" import {Changes} from "../Changes"
import {ChangeDescription} from "./ChangeDescription" import {ChangeDescription} from "./ChangeDescription"
import {Tag} from "../../Tags/Tag" import {Tag} from "../../Tags/Tag"
@ -15,7 +15,7 @@ import { Feature } from "@turf/turf"
import {Geometry, LineString, Point} from "geojson" import {Geometry, LineString, Point} from "geojson"
import FullNodeDatabaseSource from "../../FeatureSource/TiledFeatureSource/FullNodeDatabaseSource" 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 * The target feature - mostly used for the metadata
*/ */
@ -38,9 +38,14 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
private readonly identicalTo: number[] private readonly identicalTo: number[]
private readonly newTags: Tag[] | undefined 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( constructor(
state: { state: {
osmConnection: OsmConnection osmConnection: OsmConnection,
fullNodeDatabase?: FullNodeDatabaseSource fullNodeDatabase?: FullNodeDatabaseSource
}, },
feature: any, feature: any,
@ -55,6 +60,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
this.feature = feature this.feature = feature
this.wayToReplaceId = wayToReplaceId this.wayToReplaceId = wayToReplaceId
this.theme = options.theme this.theme = options.theme
this.newElementId = wayToReplaceId
const geom = this.feature.geometry const geom = this.feature.geometry
let coordinates: [number, number][] let coordinates: [number, number][]
@ -81,7 +87,6 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
this.newTags = options.newTags this.newTags = options.newTags
} }
// noinspection JSUnusedGlobalSymbols
public async getPreview(): Promise<FeatureSource> { public async getPreview(): Promise<FeatureSource> {
const { closestIds, allNodesById, detachedNodes, reprojectedNodes } = const { closestIds, allNodesById, detachedNodes, reprojectedNodes } =
await this.GetClosestIds() 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) { if (this.newTags !== undefined && this.newTags.length > 0) {
const addExtraTags = new ChangeTagAction( const addExtraTags = new ChangeTagAction(
this.wayToReplaceId, this.wayToReplaceId,

View file

@ -25,9 +25,9 @@ export class Changes {
public readonly state: { allElements?: IndexedFeatureSource; osmConnection: OsmConnection } public readonly state: { allElements?: IndexedFeatureSource; osmConnection: OsmConnection }
public readonly extraComment: UIEventSource<string> = new UIEventSource(undefined) public readonly extraComment: UIEventSource<string> = new UIEventSource(undefined)
public readonly backend: string public readonly backend: string
public readonly isUploading = new UIEventSource(false)
private readonly historicalUserLocations?: FeatureSource private readonly historicalUserLocations?: FeatureSource
private _nextId: number = -1 // Newly assigned ID's are negative private _nextId: number = -1 // Newly assigned ID's are negative
public readonly isUploading = new UIEventSource(false)
private readonly previouslyCreated: OsmObject[] = [] private readonly previouslyCreated: OsmObject[] = []
private readonly _leftRightSensitive: boolean private readonly _leftRightSensitive: boolean
private readonly _changesetHandler: ChangesetHandler private readonly _changesetHandler: ChangesetHandler
@ -246,11 +246,12 @@ export class Changes {
switch (change.type) { switch (change.type) {
case "node": case "node":
// @ts-ignore // @ts-ignore
const nlat = change.changes.lat const nlat = Utils.Round7(change.changes.lat)
// @ts-ignore // @ts-ignore
const nlon = change.changes.lon const nlon = Utils.Round7(change.changes.lon)
const n = <OsmNode>obj const n = <OsmNode>obj
if (n.lat !== nlat || n.lon !== nlon) { if (n.lat !== nlat || n.lon !== nlon) {
console.log("Node moved:", n.lat, nlat, n.lon, nlon)
n.lat = nlat n.lat = nlat
n.lon = nlon n.lon = nlon
changed = true changed = true

View file

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

View file

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

View file

@ -15,7 +15,20 @@ export class RegexTag extends TagsFilter {
this.matchesEmpty = RegexTag.doesMatch("", this.value) 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) { if (fromTag === undefined) {
return return
} }
@ -25,11 +38,8 @@ export class RegexTag extends TagsFilter {
if (typeof possibleRegex === "string") { if (typeof possibleRegex === "string") {
return fromTag === possibleRegex return fromTag === possibleRegex
} }
if (typeof fromTag.match !== "function") { return possibleRegex.test(fromTag)
console.error("Error: fromTag is not a regex: ", fromTag, possibleRegex)
throw "Error: fromTag is not a regex: " + fromTag + possibleRegex
}
return fromTag.match(possibleRegex) !== null
} }
private static source(r: string | RegExp) { 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").matchesProperties({"otherkey":"value"}) // => false
* new RegexTag("key","value",true).matchesProperties({"otherkey":"something"}) // => true * 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 { matchesProperties(tags: Record<string, string>): boolean {
if (typeof this.key === "string") { 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 return RegexTag.doesMatch(value, this.value) != this.invert
} }

View file

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

View file

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

View file

@ -583,6 +583,7 @@ export class AddQuestionBox extends DesugaringStep<LayerConfigJson> {
export class AddEditingElements extends DesugaringStep<LayerConfigJson> { export class AddEditingElements extends DesugaringStep<LayerConfigJson> {
private readonly _desugaring: DesugaringContext private readonly _desugaring: DesugaringContext
constructor(desugaring: DesugaringContext) { constructor(desugaring: DesugaringContext) {
super( 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", "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",
@ -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> { export class AddMiniMap extends DesugaringStep<LayerConfigJson> {
private readonly _state: DesugaringContext private readonly _state: DesugaringContext
@ -1197,6 +1228,7 @@ export class PrepareLayer extends Fuse<LayerConfigJson> {
new AddQuestionBox(), new AddQuestionBox(),
new AddMiniMap(state), new AddMiniMap(state),
new AddEditingElements(state), new AddEditingElements(state),
new SetFullNodeDatabase(),
new On("mapRendering", new Concat(new ExpandRewrite()).andThenF(Utils.Flatten)), new On("mapRendering", new Concat(new ExpandRewrite()).andThenF(Utils.Flatten)),
new On<(PointRenderingConfigJson | LineRenderingConfigJson)[], LayerConfigJson>( new On<(PointRenderingConfigJson | LineRenderingConfigJson)[], LayerConfigJson>(
"mapRendering", "mapRendering",

View file

@ -124,9 +124,6 @@ export interface LayerConfigJson {
* If set, only features matching this extra tag will be shown. * If set, only features matching this extra tag will be shown.
* This is useful to hide certain features from view. * 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' * The default value is 'yes'
*/ */
isShown?: TagConfigJson isShown?: TagConfigJson
@ -404,4 +401,9 @@ export interface LayerConfigJson {
* If set, open the selectedElementView in a floatOver instead of on the right * If set, open the selectedElementView in a floatOver instead of on the right
*/ */
popupInFloatover?: boolean popupInFloatover?: boolean
/**
* _Set automatically by MapComplete, please ignore_
*/
fullNodeDatabase?: boolean
} }

View file

@ -68,7 +68,7 @@ export default class LayerConfig extends WithContextLoader {
public readonly forceLoad: boolean 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 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 public readonly popupInFloatover
constructor(json: LayerConfigJson, context?: string, official: boolean = true) { constructor(json: LayerConfigJson, context?: string, official: boolean = true) {
@ -217,6 +217,7 @@ export default class LayerConfig extends WithContextLoader {
this.doNotDownload = json.doNotDownload ?? false this.doNotDownload = json.doNotDownload ?? false
this.passAllFeatures = json.passAllFeatures ?? false this.passAllFeatures = json.passAllFeatures ?? false
this.minzoom = json.minzoom ?? 0 this.minzoom = json.minzoom ?? 0
this._needsFullNodeDatabase = json.fullNodeDatabase ?? false
if (json["minZoom"] !== undefined) { if (json["minZoom"] !== undefined) {
throw "At " + context + ": minzoom is written all lowercase" throw "At " + context + ": minzoom is written all lowercase"
} }

View file

@ -15,7 +15,7 @@ export default class WithContextLoader {
* *
* The found value is interpreted as a tagrendering and fetched/parsed * 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] const v = this._json[key]
if (v === undefined || v === null) { if (v === undefined || v === null) {
if (deflt === undefined) { if (deflt === undefined) {

View file

@ -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 * 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( const layoutSource = new LayoutSource(
layout.layers, layout.layers,
this.featureSwitches, this.featureSwitches,
this.mapProperties, this.mapProperties,
this.osmConnection.Backend(), this.osmConnection.Backend(),
(id) => self.layerState.filteredLayers.get(id).isDisplayed (id) => self.layerState.filteredLayers.get(id).isDisplayed,
this.fullNodeDatabase
) )
this.indexedFeatures = layoutSource this.indexedFeatures = layoutSource
const empty = [] const empty = []
let currentViewIndex = 0
this.currentView = new StaticFeatureSource( this.currentView = new StaticFeatureSource(
this.mapProperties.bounds.map((bbox) => this.mapProperties.bounds.map((bbox) => {
bbox === undefined ? empty : <Feature[]>[bbox.asGeoJson({ id: "current_view" })] 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) this.featuresInView = new BBoxFeatureSource(layoutSource, this.mapProperties.bounds)
@ -465,6 +481,14 @@ export default class ThemeViewState implements SpecialVisualizationState {
this.featureSwitches.featureSwitchIsTesting 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 this.layerState.filteredLayers
.get("range") .get("range")

View file

@ -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( static TileRangeBetween(
zoomlevel: number, zoomlevel: number,
lat0: number, lat0: number,

View file

@ -78,6 +78,9 @@ export default class Table extends BaseUIElement {
for (let j = 0; j < row.length; j++) { for (let j = 0; j < row.length; j++) {
try { try {
let elem = row[j] let elem = row[j]
if(elem?.ConstructElement === undefined){
continue
}
const htmlElem = elem?.ConstructElement() const htmlElem = elem?.ConstructElement()
if (htmlElem === undefined) { if (htmlElem === undefined) {
continue continue

View file

@ -330,6 +330,7 @@ class LineRenderingLayer {
}) })
if (this._onClick) { if (this._onClick) {
map.on("click", polylayer, (e) => { map.on("click", polylayer, (e) => {
console.log("Got polylayer click:", e)
// polygon-layer-listener // polygon-layer-listener
if(e.originalEvent["consumed"]){ if(e.originalEvent["consumed"]){
// This is a polygon beneath a marker, we can ignore it // This is a polygon beneath a marker, we can ignore it
@ -469,8 +470,10 @@ export default class ShowDataLayer {
if (this._options.zoomToFeatures) { if (this._options.zoomToFeatures) {
const features = this._options.features.features.data const features = this._options.features.features.data
const bbox = BBox.bboxAroundAll(features.map(BBox.get)) const bbox = BBox.bboxAroundAll(features.map(BBox.get))
map.resize()
map.fitBounds(bbox.toLngLat(), { map.fitBounds(bbox.toLngLat(), {
padding: {top: 10, bottom: 10, left: 10, right: 10}, padding: {top: 10, bottom: 10, left: 10, right: 10},
animate: false
}) })
} }
} }

View file

@ -54,6 +54,7 @@ class ApplyButton extends UIElement {
>("idle") >("idle")
private readonly layer: FilteredLayer private readonly layer: FilteredLayer
private readonly tagRenderingConfig: TagRenderingConfig private readonly tagRenderingConfig: TagRenderingConfig
private readonly appliedNumberOfFeatures = new UIEventSource<number>(0)
constructor( constructor(
state: SpecialVisualizationState, state: SpecialVisualizationState,
@ -131,7 +132,9 @@ class ApplyButton extends UIElement {
return new FixedUiElement("All done!").SetClass("thanks") return new FixedUiElement("All done!").SetClass("thanks")
} }
if (st === "running") { 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 const error = st.error
return new Combine([ return new Combine([
@ -142,11 +145,16 @@ class ApplyButton extends UIElement {
) )
} }
/**
* Actually applies all the changes...
*/
private async Run() { private async Run() {
try { try {
console.log("Applying auto-action on " + this.target_feature_ids.length + " features") 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 featureTags = this.state.featureProperties.getStore(targetFeatureId)
const rendering = this.tagRenderingConfig.GetRenderValue(featureTags.data).txt const rendering = this.tagRenderingConfig.GetRenderValue(featureTags.data).txt
const specialRenderings = Utils.NoNull( const specialRenderings = Utils.NoNull(
@ -167,11 +175,15 @@ class ApplyButton extends UIElement {
continue continue
} }
const action = <AutoAction>specialRendering.func 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...") console.log("Flushing changes...")
await this.state.changes.flushChanges("Auto button") await this.state.changes.flushChanges("Auto button: done")
this.buttonState.setData("done") this.buttonState.setData("done")
} catch (e) { } catch (e) {
console.error("Error while running autoApply: ", e) console.error("Error while running autoApply: ", e)
@ -274,21 +286,27 @@ export default class AutoApplyButton implements SpecialVisualization {
} }
return new Lazy(() => { return new Lazy(() => {
const to_parse = new UIEventSource(undefined) const to_parse = new UIEventSource<string[]>(undefined)
// Very ugly hack: read the value every 500ms // Very ugly hack: read the value every 500ms
Stores.Chronic(500, () => to_parse.data === undefined).addCallback(() => { Stores.Chronic(500, () => to_parse.data === undefined).addCallback(() => {
const applicable = tagSource.data[argument[1]] let applicable = <string | string[]> tagSource.data[argument[1]]
to_parse.setData(applicable) if(typeof applicable === "string"){
applicable = JSON.parse(applicable)
}
to_parse.setData(<string[]> applicable)
}) })
const loading = new Loading("Gathering which elements support auto-apply... ") const loading = new Loading("Gathering which elements support auto-apply... ")
return new VariableUiElement( return new VariableUiElement(
to_parse.map((ids) => { Stores.ListStabilized(to_parse).map((ids) => {
if (ids === undefined) { if (ids === undefined) {
return loading return loading
} }
return new ApplyButton(state, JSON.parse(ids), options) if (typeof ids === "string") {
ids = JSON.parse(ids)
}
return new ApplyButton(state, ids, options)
}) })
) )
}) })

View file

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

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

View 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,
}
)
}
}

View file

@ -15,6 +15,9 @@
import TagHint from "../TagHint.svelte"; import TagHint from "../TagHint.svelte";
import {TagsFilter} from "../../../Logic/Tags/TagsFilter"; import {TagsFilter} from "../../../Logic/Tags/TagsFilter";
import {Store} from "../../../Logic/UIEventSource"; 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 export let importFlow: ImportFlow
let state = importFlow.state let state = importFlow.state
@ -27,13 +30,71 @@
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 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> </script>
<LoginToggle {state}> <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"}> <NextButton clss="primary w-full" on:click={() => currentFlowStep = "confirm"}>
<slot name="start-flow-text"> <slot name="start-flow-text">
{#if importFlow?.args?.icon} {#if importFlow?.args?.icon}
@ -42,15 +103,6 @@
{importFlow.args.text} {importFlow.args.text}
</slot> </slot>
</NextButton> </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"} {:else if currentFlowStep === "confirm"}
<div class="h-full w-full flex flex-col"> <div class="h-full w-full flex flex-col">
<div class="w-full h-full"> <div class="w-full h-full">

View file

@ -149,11 +149,13 @@ export default abstract class ImportFlow<ArgT extends ImportFlowArguments> {
public readonly args: ArgT; public readonly args: ArgT;
public readonly targetLayer: FilteredLayer; public readonly targetLayer: FilteredLayer;
public readonly tagsToApply: Store<Tag[]> 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.state = state;
this.args = args; this.args = args;
this.tagsToApply = tagsToApply; this.tagsToApply = tagsToApply;
this._originalFeatureTags = originalTags;
this.targetLayer = state.layerState.filteredLayers.get(args.targetLayer) this.targetLayer = state.layerState.filteredLayers.get(args.targetLayer)
@ -166,6 +168,11 @@ export default abstract class ImportFlow<ArgT extends ImportFlowArguments> {
const state = this.state const state = this.state
return state.featureSwitchIsTesting.map(isTesting => { return state.featureSwitchIsTesting.map(isTesting => {
const t = Translations.t.general.add.import 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 const usesTestUrl = this.state.osmConnection._oauth_config.url === OsmConnection.oauth_configs["osm-test"].url
if (!state.layout.official && !(isTesting || usesTestUrl)) { if (!state.layout.official && !(isTesting || usesTestUrl)) {
// Unofficial theme - imports not allowed // Unofficial theme - imports not allowed
@ -191,7 +198,7 @@ export default abstract class ImportFlow<ArgT extends ImportFlowArguments> {
} }
return undefined return undefined
}, [state.mapProperties.zoom, state.dataIsLoading]) }, [state.mapProperties.zoom, state.dataIsLoading, this._originalFeatureTags])
} }

View file

@ -13,7 +13,7 @@ import Translations from "../../i18n/Translations";
/** /**
* The wrapper to make the special visualisation for the PointImportFlow * 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 funcName: string
public readonly docs: string | BaseUIElement public readonly docs: string | BaseUIElement
@ -51,7 +51,7 @@ export class ImportPointButtonViz implements SpecialVisualization {
} }
const baseArgs: PointImportFlowArguments = <any> Utils.ParseVisArgs(this.args, argument) const baseArgs: PointImportFlowArguments = <any> Utils.ParseVisArgs(this.args, argument)
const tagsToApply = ImportFlowUtils.getTagsToApply(tagSource , baseArgs) 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( return new SvelteUIElement(
PointImportFlow, { PointImportFlow, {
@ -60,7 +60,5 @@ export class ImportPointButtonViz implements SpecialVisualization {
) )
} }
getLayerDependencies(argsRaw: string[]): string[] {
return ImportFlowUtils.getLayerDependenciesWithSnapOnto(this.args, argsRaw)
}
} }

View file

@ -20,12 +20,10 @@ export interface PointImportFlowArguments extends ImportFlowArguments {
export class PointImportFlowState extends ImportFlow<PointImportFlowArguments> { export class PointImportFlowState extends ImportFlow<PointImportFlowArguments> {
public readonly startCoordinate: [number, number] public readonly startCoordinate: [number, number]
private readonly _originalFeature: Feature<Point>; private readonly _originalFeature: Feature<Point>;
private readonly _originalFeatureTags: UIEventSource<Record<string, string>>
constructor(state: SpecialVisualizationState, originalFeature: Feature<Point>, args: PointImportFlowArguments, tagsToApply: Store<Tag[]>) { constructor(state: SpecialVisualizationState, originalFeature: Feature<Point>, args: PointImportFlowArguments, tagsToApply: Store<Tag[]>, originalFeatureTags: UIEventSource<Record<string, string>>) {
super(state, args, tagsToApply); super(state, args, tagsToApply, originalFeatureTags);
this._originalFeature = originalFeature; this._originalFeature = originalFeature;
this._originalFeatureTags = state.featureProperties.getStore(originalFeature.properties.id)
this.startCoordinate = GeoOperations.centerpointCoordinates(originalFeature) this.startCoordinate = GeoOperations.centerpointCoordinates(originalFeature)
} }

View file

@ -22,18 +22,9 @@ import FullNodeDatabaseSource from "../../../Logic/FeatureSource/TiledFeatureSou
export default class WayImportButtonViz implements AutoAction, SpecialVisualization { export default class WayImportButtonViz implements AutoAction, SpecialVisualization {
public readonly funcName: string public readonly funcName: string= "import_way_button"
public readonly docs: string | BaseUIElement 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 example?: string public readonly args: { name: string; defaultValue?: string; doc: string }[]= [
public readonly args: { name: string; defaultValue?: string; doc: string }[]
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, ...ImportFlowUtils.generalArguments,
{ {
name: "snap_to_point_if", name: "snap_to_point_if",
@ -63,7 +54,9 @@ export default class WayImportButtonViz implements AutoAction, SpecialVisualizat
defaultValue: "0.1", defaultValue: "0.1",
}, },
] ]
}
public readonly supportsAutoAction = true
public readonly needsNodeDatabase = true
constr(state: SpecialVisualizationState, tagSource: UIEventSource<Record<string, string>>, argument: string[], feature: Feature, layer: LayerConfig): BaseUIElement { constr(state: SpecialVisualizationState, tagSource: UIEventSource<Record<string, string>>, argument: string[], feature: Feature, layer: LayerConfig): BaseUIElement {
const geometry = feature.geometry const geometry = feature.geometry
@ -100,8 +93,10 @@ export default class WayImportButtonViz implements AutoAction, SpecialVisualizat
const args: WayImportFlowArguments = <any>Utils.ParseVisArgs(this.args, argument) const args: WayImportFlowArguments = <any>Utils.ParseVisArgs(this.args, argument)
const tagsToApply = ImportFlowUtils.getTagsToApply(tagSource, args) 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) const action = WayImportFlowState.CreateAction(<Feature<LineString | Polygon >>feature, args, state, tagsToApply, mergeConfigs)
tagSource.data["_imported"] = "yes"
tagSource.ping()
await state.changes.applyAction(action) await state.changes.applyAction(action)
} }

View file

@ -1,5 +1,7 @@
<script lang="ts"> <script lang="ts">
/**
* Can be used for both WayImportFlow and ConflateImportFlow
*/
import WayImportFlowState from "./WayImportFlowState"; import WayImportFlowState from "./WayImportFlowState";
import ImportFlow from "./ImportFlow.svelte"; import ImportFlow from "./ImportFlow.svelte";
import MapControlButton from "../../Base/MapControlButton.svelte"; import MapControlButton from "../../Base/MapControlButton.svelte";
@ -12,7 +14,8 @@
import StaticFeatureSource from "../../../Logic/FeatureSource/Sources/StaticFeatureSource"; import StaticFeatureSource from "../../../Logic/FeatureSource/Sources/StaticFeatureSource";
import {ImportFlowUtils} from "./ImportFlow"; import {ImportFlowUtils} from "./ImportFlow";
import {GeoOperations} from "../../../Logic/GeoOperations"; import {GeoOperations} from "../../../Logic/GeoOperations";
export let importFlow: WayImportFlowState import ConflateImportFlowState from "./ConflateImportFlowState";
export let importFlow: WayImportFlowState | ConflateImportFlowState
const state = importFlow.state const state = importFlow.state
const map = new UIEventSource<MlMap>(undefined) const map = new UIEventSource<MlMap>(undefined)

View file

@ -8,7 +8,7 @@ import CreateWayWithPointReuseAction, {
MergePointConfig MergePointConfig
} from "../../../Logic/Osm/Actions/CreateWayWithPointReuseAction"; } from "../../../Logic/Osm/Actions/CreateWayWithPointReuseAction";
import {TagUtils} from "../../../Logic/Tags/TagUtils"; 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 {FeatureSource, IndexedFeatureSource} from "../../../Logic/FeatureSource/FeatureSource";
import CreateMultiPolygonWithPointReuseAction from "../../../Logic/Osm/Actions/CreateMultiPolygonWithPointReuseAction"; import CreateMultiPolygonWithPointReuseAction from "../../../Logic/Osm/Actions/CreateMultiPolygonWithPointReuseAction";
import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig"; import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig";
@ -28,13 +28,11 @@ export default class WayImportFlowState extends ImportFlow<WayImportFlowArgument
public readonly originalFeature: Feature<LineString | Polygon>; public readonly originalFeature: Feature<LineString | Polygon>;
private readonly action: OsmCreateAction & { getPreview?(): Promise<FeatureSource>; } 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>>) { 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.originalFeature = originalFeature;
this._originalFeatureTags = originalFeatureTags; const mergeConfigs = WayImportFlowState.GetMergeConfig(args)
const mergeConfigs = WayImportFlowState.GetMergeConfig(args, tagsToApply)
this.action = WayImportFlowState.CreateAction(originalFeature, args, state, tagsToApply, mergeConfigs) this.action = WayImportFlowState.CreateAction(originalFeature, args, state, tagsToApply, mergeConfigs)
} }
@ -49,7 +47,7 @@ export default class WayImportFlowState extends ImportFlow<WayImportFlowArgument
}, },
tagsToApply: Store<Tag[]>, tagsToApply: Store<Tag[]>,
mergeConfigs: MergePointConfig[] mergeConfigs: MergePointConfig[]
): OsmCreateAction & { getPreview?(): Promise<FeatureSource>; newElementId?: string } { ): OsmCreateAction & PreviewableAction & { newElementId?: string } {
if (feature.geometry.type === "Polygon" && feature.geometry.coordinates.length > 1) { if (feature.geometry.type === "Polygon" && feature.geometry.coordinates.length > 1) {
const coors = (<Polygon>feature.geometry).coordinates const coors = (<Polygon>feature.geometry).coordinates
const outer = coors[0] 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 const nodesMustMatch = args.snap_to_point_if
?.split(";") ?.split(";")
?.map((tag, i) => TagUtils.Tag(tag, "TagsSpec for import button " + i)) ?.map((tag, i) => TagUtils.Tag(tag, "TagsSpec for import button " + i))
@ -108,6 +106,7 @@ export default class WayImportFlowState extends ImportFlow<WayImportFlowArgument
return mergeConfigs return mergeConfigs
} }
// noinspection JSUnusedGlobalSymbols
public async onConfirm() { public async onConfirm() {
const originalFeatureTags = this._originalFeatureTags const originalFeatureTags = this._originalFeatureTags
originalFeatureTags.data["_imported"] = "yes" originalFeatureTags.data["_imported"] = "yes"

View file

@ -14,6 +14,9 @@ import { Tag } from "../../Logic/Tags/Tag"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
import { Changes } from "../../Logic/Osm/Changes" import { Changes } from "../../Logic/Osm/Changes"
import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization" 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 { export default class TagApplyButton implements AutoAction, SpecialVisualization {
public readonly funcName = "tag_apply" public readonly funcName = "tag_apply"
@ -111,13 +114,16 @@ export default class TagApplyButton implements AutoAction, SpecialVisualization
}) })
} }
async applyActionOn( public async applyActionOn(
feature: Feature,
state: { state: {
layout: LayoutConfig layout: LayoutConfig
changes: Changes changes: Changes
indexedFeatures: IndexedFeatureSource
}, },
tags: UIEventSource<any>, tags: UIEventSource<any>,
args: string[] args: string[],
): Promise<void> { ): Promise<void> {
const tagsToApply = TagApplyButton.generateTagsToApply(args[0], tags) const tagsToApply = TagApplyButton.generateTagsToApply(args[0], tags)
const targetIdKey = args[3] const targetIdKey = args[3]
@ -138,7 +144,9 @@ export default class TagApplyButton implements AutoAction, SpecialVisualization
public constr( public constr(
state: SpecialVisualizationState, state: SpecialVisualizationState,
tags: UIEventSource<Record<string, string>>, tags: UIEventSource<Record<string, string>>,
args: string[] args: string[],
feature: Feature,
layer: LayerConfig
): BaseUIElement { ): BaseUIElement {
const tagsToApply = TagApplyButton.generateTagsToApply(args[0], tags) const tagsToApply = TagApplyButton.generateTagsToApply(args[0], tags)
const msg = args[1] const msg = args[1]
@ -167,7 +175,7 @@ export default class TagApplyButton implements AutoAction, SpecialVisualization
new Combine([msg, tagsExplanation]).SetClass("flex flex-col") new Combine([msg, tagsExplanation]).SetClass("flex flex-col")
).onClick(async () => { ).onClick(async () => {
applied.setData(true) applied.setData(true)
await self.applyActionOn(state, tags, args) await self.applyActionOn(feature, state, tags, args)
}) })
return new Toggle( return new Toggle(

View file

@ -71,8 +71,9 @@ import SplitRoadWizard from "./Popup/SplitRoadWizard"
import {ExportAsGpxViz} from "./Popup/ExportAsGpxViz" import {ExportAsGpxViz} from "./Popup/ExportAsGpxViz"
import WikipediaPanel from "./Wikipedia/WikipediaPanel.svelte" import WikipediaPanel from "./Wikipedia/WikipediaPanel.svelte"
import TagRenderingEditable from "./Popup/TagRendering/TagRenderingEditable.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 WayImportButtonViz from "./Popup/ImportButtons/WayImportButtonViz";
import ConflateImportButtonViz from "./Popup/ImportButtons/ConflateImportButtonViz";
class NearbyImageVis implements SpecialVisualization { class NearbyImageVis implements SpecialVisualization {
// Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests // 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 TagApplyButton(),
new ImportPointButtonViz(), new PointImportButtonViz(),
new WayImportButtonViz(), new WayImportButtonViz(),
// TODO new ConflateButton(), new ConflateImportButtonViz(),
new NearbyImageVis(), new NearbyImageVis(),

View file

@ -3,20 +3,36 @@ import { Translation, TypedTranslation } from "./Translation"
import BaseUIElement from "../BaseUIElement" import BaseUIElement from "../BaseUIElement"
import CompiledTranslations from "../../assets/generated/CompiledTranslations" import CompiledTranslations from "../../assets/generated/CompiledTranslations"
import LanguageUtils from "../../Utils/LanguageUtils" import LanguageUtils from "../../Utils/LanguageUtils"
import {ClickableToggle} from "../Input/Toggle";
export default class Translations { export default class Translations {
static readonly t: Readonly<typeof CompiledTranslations.t> = CompiledTranslations.t static readonly t: Readonly<typeof CompiledTranslations.t> = CompiledTranslations.t
private static knownLanguages = LanguageUtils.usedLanguages private static knownLanguages = LanguageUtils.usedLanguages
constructor() { constructor() {
throw "Translations is static. If you want to intitialize a new translation, use the singular form" 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") { if (typeof s === "string") {
return new FixedUiElement(s) return new FixedUiElement(s)
} }
if (typeof s === "number") { 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 return s
} }

View file

@ -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.Round7(12.123456789) // => 12.1234568
* 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"
*/ */
public static Round(i: number): string { public static Round7(i: number): number {
if (i < 0) { return Math.round(i * 10000000) / 10000000
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 Times(f: (i: number) => string, count: number): string { public static Times(f: (i: number) => string, count: number): string {

View file

@ -46,9 +46,6 @@
}, },
"maxCacheAge": 0 "maxCacheAge": 0
}, },
"calculatedTags": [
"_surface:strict:=get(feat)('_surface')"
],
"mapRendering": [ "mapRendering": [
{ {
"width": { "width": {
@ -293,20 +290,20 @@
"name": "GRB geometries", "name": "GRB geometries",
"title": "GRB outline", "title": "GRB outline",
"calculatedTags": [ "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 */ )", "_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:source:ref=get(feat)('_overlaps_with')?.feat?.properties['source:geometry:ref']",
"_osm_obj:id=get(feat)('_overlaps_with')?.feat?.properties?.id", "_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:building=get(feat)('_overlaps_with')?.feat?.properties?.building",
"_osm_obj:addr:street=(get(feat)('_overlaps_with')?.feat?.properties ?? {})['addr:street']", "_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: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", "_overlap_absolute=get(feat)('_overlaps_with')?.overlap",
"_reverse_overlap_percentage=Math.round(100 * get(feat)('_overlap_absolute') / get(feat)('_surface'))", "_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'))", "_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']", "_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,'-')", "_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)", "_target_building_type=feat.properties['_osm_obj:building'] === 'yes' ? feat.properties.building : (feat.properties['_osm_obj:building'] ?? feat.properties.building)",
@ -358,7 +355,6 @@
"and": [ "and": [
"_overlap_percentage>50", "_overlap_percentage>50",
"_reverse_overlap_percentage>50", "_reverse_overlap_percentage>50",
"_overlaps_with!=",
"_osm_obj:addr:street=", "_osm_obj:addr:street=",
"_osm_obj:addr:housenumber=", "_osm_obj:addr:housenumber=",
"addr:street~*", "addr:street~*",
@ -373,8 +369,7 @@
"if": { "if": {
"and": [ "and": [
"_overlap_percentage>50", "_overlap_percentage>50",
"_reverse_overlap_percentage>50", "_reverse_overlap_percentage>50"
"_overlaps_with!="
] ]
}, },
"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)}" "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", "id": "Building info",
"render": "This is a <b>{building}</b> <span class='subtle'>detected by {detection_method}</span>" "render": "This is a <b>{building}</b> <span class='subtle'>detected by {detection_method}</span>"
}, },
{ {
"id": "overlapping building address", "id": "overlapping building address",
"render": "The overlapping openstreetmap-building has no address information at all", "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", "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}", "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": "_overlaps_with!=" "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", "id": "apply-id",
@ -717,15 +723,17 @@
{ {
"builtin": "current_view", "builtin": "current_view",
"override": { "override": {
"calculatedTags": [ "calculatedTags+": [
"_overlapping=Number(feat.properties.zoom) >= 16 ? overlapWith(feat)('grb').map(ff => ff.feat.properties) : undefined", "_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_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_count=get(feat)('_applicable')?.length" "_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+": [ "tagRenderings+": [
{ {
"id": "hw", "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": [ "mappings": [
{ {
"if": "zoom<14", "if": "zoom<14",
@ -737,13 +745,23 @@
}, },
{ {
"if": "_applicable_count=0", "if": "_applicable_count=0",
"then": "No importable buildins in view" "then": "No importable buildings in view"
} }
] ]
}, },
{ {
"id": "autoapply", "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": [ "mappings": [
{ {
"if": "zoom<16", "if": "zoom<16",
@ -762,6 +780,7 @@
}, },
"iconSize": "15,15,center" "iconSize": "15,15,center"
} }
] ]
} }
} }

12
package-lock.json generated
View file

@ -4475,9 +4475,9 @@
} }
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001486", "version": "1.0.30001492",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001486.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001492.tgz",
"integrity": "sha512-uv7/gXuHi10Whlj0pp5q/tsK/32J2QSqVRKQhs2j8VsDCjgyruAh/eEXHF822VqO9yT6iZKw3nRwZRSPBE9OQg==", "integrity": "sha512-2efF8SAZwgAX1FJr87KWhvuJxnGJKOnctQa8xLOskAXNXq8oiuqgl6u1kk3fFpsp3GgvzlRjiK1sl63hNtFADw==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -15408,9 +15408,9 @@
"integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==" "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="
}, },
"caniuse-lite": { "caniuse-lite": {
"version": "1.0.30001486", "version": "1.0.30001492",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001486.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001492.tgz",
"integrity": "sha512-uv7/gXuHi10Whlj0pp5q/tsK/32J2QSqVRKQhs2j8VsDCjgyruAh/eEXHF822VqO9yT6iZKw3nRwZRSPBE9OQg==", "integrity": "sha512-2efF8SAZwgAX1FJr87KWhvuJxnGJKOnctQa8xLOskAXNXq8oiuqgl6u1kk3fFpsp3GgvzlRjiK1sl63hNtFADw==",
"dev": true "dev": true
}, },
"canvg": { "canvg": {

23
test.ts
View file

@ -32,30 +32,7 @@ async function testPdf() {
await pdf.ConvertSvg("nl") 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")) // testPdf().then((_) => console.log("All done"))
/*/ /*/
testspecial() testspecial()

View file

@ -85,7 +85,7 @@ describe("Code quality", () => {
"innerText is not allowed as it is not testable with fakeDom. Use 'textContent' instead." "innerText is not allowed as it is not testable with fakeDom. Use 'textContent' instead."
) )
) )
/*
itAsync( itAsync(
"should not contain 'import * as name from \"xyz.json\"'", "should not contain 'import * as name from \"xyz.json\"'",
detectInCode( detectInCode(

View file

@ -6,6 +6,7 @@ import { describe, expect, it } from "vitest"
import { OsmConnection } from "../../../../Logic/Osm/OsmConnection" import { OsmConnection } from "../../../../Logic/Osm/OsmConnection"
import { ImmutableStore } from "../../../../Logic/UIEventSource" import { ImmutableStore } from "../../../../Logic/UIEventSource"
import { Changes } from "../../../../Logic/Osm/Changes" import { Changes } from "../../../../Logic/Osm/Changes"
import FullNodeDatabaseSource from "../../../../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource";
describe("ReplaceGeometryAction", () => { describe("ReplaceGeometryAction", () => {
const grbStripped = { const grbStripped = {
@ -873,10 +874,6 @@ describe("ReplaceGeometryAction", () => {
) )
it("should move nodes accordingly", async () => { 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 layout = new LayoutConfig(<any>grbStripped)
const bbox = new BBox([ 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 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 data = await Utils.downloadJson(url)
const fullNodeDatabase = undefined // TODO new FullNodeDatabaseSource(undefined) const fullNodeDatabase = new FullNodeDatabaseSource()
// TODO fullNodeDatabase.handleOsmJson(data, 0) fullNodeDatabase.handleOsmJson(data, 0, 0, 0)
const changes = new Changes({ const changes = new Changes({
dryRun: new ImmutableStore(true), dryRun: new ImmutableStore(true),
osmConnection: new OsmConnection() osmConnection: new OsmConnection()