forked from MapComplete/MapComplete
Refactoring of import button, various improvements
This commit is contained in:
parent
cabbdf96db
commit
a095af4f18
17 changed files with 527 additions and 328 deletions
|
@ -42,7 +42,6 @@ export class NewGeometryFromChangesFeatureSource implements FeatureSource {
|
||||||
feature: feature,
|
feature: feature,
|
||||||
freshness: now
|
freshness: now
|
||||||
})
|
})
|
||||||
console.warn("Added a new feature: ", JSON.stringify(feature))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const change of changes) {
|
for (const change of changes) {
|
||||||
|
|
102
Logic/Osm/Actions/CreateMultiPolygonWithPointReuseAction.ts
Normal file
102
Logic/Osm/Actions/CreateMultiPolygonWithPointReuseAction.ts
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
import {OsmCreateAction} from "./OsmChangeAction";
|
||||||
|
import {Tag} from "../../Tags/Tag";
|
||||||
|
import {Changes} from "../Changes";
|
||||||
|
import {ChangeDescription} from "./ChangeDescription";
|
||||||
|
import FeaturePipelineState from "../../State/FeaturePipelineState";
|
||||||
|
import FeatureSource from "../../FeatureSource/FeatureSource";
|
||||||
|
import CreateNewWayAction from "./CreateNewWayAction";
|
||||||
|
import CreateWayWithPointReuseAction, {MergePointConfig} from "./CreateWayWithPointReuseAction";
|
||||||
|
import {And} from "../../Tags/And";
|
||||||
|
import {TagUtils} from "../../Tags/TagUtils";
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* More or less the same as 'CreateNewWay', except that it'll try to reuse already existing points
|
||||||
|
*/
|
||||||
|
export default class CreateMultiPolygonWithPointReuseAction extends OsmCreateAction {
|
||||||
|
private readonly _tags: Tag[];
|
||||||
|
public newElementId: string = undefined;
|
||||||
|
public newElementIdNumber: number = undefined;
|
||||||
|
private readonly createOuterWay: CreateWayWithPointReuseAction
|
||||||
|
private readonly createInnerWays : CreateNewWayAction[]
|
||||||
|
private readonly geojsonPreview: any;
|
||||||
|
private readonly theme: string;
|
||||||
|
private readonly changeType: "import" | "create" | string;
|
||||||
|
constructor(tags: Tag[],
|
||||||
|
outerRingCoordinates: [number, number][],
|
||||||
|
innerRingsCoordinates: [number, number][][],
|
||||||
|
state: FeaturePipelineState,
|
||||||
|
config: MergePointConfig[],
|
||||||
|
changeType: "import" | "create" | string
|
||||||
|
) {
|
||||||
|
super(null,true);
|
||||||
|
this._tags = [...tags, new Tag("type","multipolygon")];
|
||||||
|
this.changeType = changeType;
|
||||||
|
this.theme = state.layoutToUse.id
|
||||||
|
this. createOuterWay = new CreateWayWithPointReuseAction([], outerRingCoordinates, state, config)
|
||||||
|
this. createInnerWays = innerRingsCoordinates.map(ringCoordinates =>
|
||||||
|
new CreateNewWayAction([],
|
||||||
|
ringCoordinates.map(([lon, lat] )=> ({lat, lon})),
|
||||||
|
{theme: state.layoutToUse.id}))
|
||||||
|
|
||||||
|
this.geojsonPreview = {
|
||||||
|
type: "Feature",
|
||||||
|
properties: TagUtils.changeAsProperties(new And(this._tags).asChange({})),
|
||||||
|
geometry:{
|
||||||
|
type: "Polygon",
|
||||||
|
coordinates: [
|
||||||
|
outerRingCoordinates,
|
||||||
|
...innerRingsCoordinates
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getPreview(): Promise<FeatureSource> {
|
||||||
|
const outerPreview = await this.createOuterWay.getPreview()
|
||||||
|
outerPreview.features.data.push({
|
||||||
|
freshness: new Date(),
|
||||||
|
feature: this.geojsonPreview
|
||||||
|
})
|
||||||
|
return outerPreview
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
|
||||||
|
console.log("Running CMPWPRA")
|
||||||
|
const descriptions: ChangeDescription[] = []
|
||||||
|
descriptions.push(...await this.createOuterWay.CreateChangeDescriptions(changes));
|
||||||
|
for (const innerWay of this.createInnerWays) {
|
||||||
|
descriptions.push(...await innerWay.CreateChangeDescriptions(changes))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
this.newElementIdNumber = changes.getNewID();
|
||||||
|
this.newElementId = "relation/"+this.newElementIdNumber
|
||||||
|
descriptions.push({
|
||||||
|
type:"relation",
|
||||||
|
id: this.newElementIdNumber,
|
||||||
|
tags: new And(this._tags).asChange({}),
|
||||||
|
meta: {
|
||||||
|
theme: this.theme,
|
||||||
|
changeType:this.changeType
|
||||||
|
},
|
||||||
|
changes: {
|
||||||
|
members: [
|
||||||
|
{
|
||||||
|
type: "way",
|
||||||
|
ref: this.createOuterWay.newElementIdNumber,
|
||||||
|
role: "outer"
|
||||||
|
},
|
||||||
|
// @ts-ignore
|
||||||
|
...this.createInnerWays.map(a => ({type: "way", ref: a.newElementIdNumber, role: "inner"}))
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
return descriptions
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -1,12 +1,12 @@
|
||||||
import {Tag} from "../../Tags/Tag";
|
import {Tag} from "../../Tags/Tag";
|
||||||
import OsmChangeAction from "./OsmChangeAction";
|
import OsmChangeAction, {OsmCreateAction} from "./OsmChangeAction";
|
||||||
import {Changes} from "../Changes";
|
import {Changes} from "../Changes";
|
||||||
import {ChangeDescription} from "./ChangeDescription";
|
import {ChangeDescription} from "./ChangeDescription";
|
||||||
import {And} from "../../Tags/And";
|
import {And} from "../../Tags/And";
|
||||||
import {OsmWay} from "../OsmObject";
|
import {OsmWay} from "../OsmObject";
|
||||||
import {GeoOperations} from "../../GeoOperations";
|
import {GeoOperations} from "../../GeoOperations";
|
||||||
|
|
||||||
export default class CreateNewNodeAction extends OsmChangeAction {
|
export default class CreateNewNodeAction extends OsmCreateAction {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maps previously created points onto their assigned ID, to reuse the point if uplaoded
|
* Maps previously created points onto their assigned ID, to reuse the point if uplaoded
|
||||||
|
@ -121,7 +121,6 @@ export default class CreateNewNodeAction extends OsmChangeAction {
|
||||||
reusedPointId = this._snapOnto.nodes[index + 1]
|
reusedPointId = this._snapOnto.nodes[index + 1]
|
||||||
}
|
}
|
||||||
if (reusedPointId !== undefined) {
|
if (reusedPointId !== undefined) {
|
||||||
console.log("Reusing an existing point:", reusedPointId)
|
|
||||||
this.setElementId(reusedPointId)
|
this.setElementId(reusedPointId)
|
||||||
return [{
|
return [{
|
||||||
tags: new And(this._basicTags).asChange(properties),
|
tags: new And(this._basicTags).asChange(properties),
|
||||||
|
@ -133,7 +132,6 @@ export default class CreateNewNodeAction extends OsmChangeAction {
|
||||||
|
|
||||||
const locations = [...this._snapOnto.coordinates]
|
const locations = [...this._snapOnto.coordinates]
|
||||||
locations.forEach(coor => coor.reverse())
|
locations.forEach(coor => coor.reverse())
|
||||||
console.log("Locations are: ", locations)
|
|
||||||
const ids = [...this._snapOnto.nodes]
|
const ids = [...this._snapOnto.nodes]
|
||||||
|
|
||||||
locations.splice(index + 1, 0, [this._lon, this._lat])
|
locations.splice(index + 1, 0, [this._lon, this._lat])
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
import {ChangeDescription} from "./ChangeDescription";
|
import {ChangeDescription} from "./ChangeDescription";
|
||||||
import OsmChangeAction from "./OsmChangeAction";
|
import {OsmCreateAction} 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";
|
||||||
import {And} from "../../Tags/And";
|
import {And} from "../../Tags/And";
|
||||||
|
|
||||||
export default class CreateNewWayAction extends OsmChangeAction {
|
export default class CreateNewWayAction extends OsmCreateAction {
|
||||||
public newElementId: string = undefined
|
public newElementId: string = undefined
|
||||||
|
public newElementIdNumber: number = undefined;
|
||||||
private readonly coordinates: ({ nodeId?: number, lat: number, lon: number })[];
|
private readonly coordinates: ({ nodeId?: number, lat: number, lon: number })[];
|
||||||
private readonly tags: Tag[];
|
private readonly tags: Tag[];
|
||||||
private readonly _options: {
|
private readonly _options: {
|
||||||
|
@ -55,7 +56,7 @@ export default class CreateNewWayAction extends OsmChangeAction {
|
||||||
|
|
||||||
|
|
||||||
const id = changes.getNewID()
|
const id = changes.getNewID()
|
||||||
|
this.newElementIdNumber = id
|
||||||
const newWay = <ChangeDescription>{
|
const newWay = <ChangeDescription>{
|
||||||
id,
|
id,
|
||||||
type: "way",
|
type: "way",
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import OsmChangeAction from "./OsmChangeAction";
|
import OsmChangeAction, {OsmCreateAction} 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";
|
||||||
|
@ -31,7 +31,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 OsmChangeAction {
|
export default class CreateWayWithPointReuseAction extends OsmCreateAction {
|
||||||
private readonly _tags: Tag[];
|
private readonly _tags: Tag[];
|
||||||
/**
|
/**
|
||||||
* lngLat-coordinates
|
* lngLat-coordinates
|
||||||
|
@ -40,6 +40,9 @@ export default class CreateWayWithPointReuseAction extends OsmChangeAction {
|
||||||
private _coordinateInfo: CoordinateInfo[];
|
private _coordinateInfo: CoordinateInfo[];
|
||||||
private _state: FeaturePipelineState;
|
private _state: FeaturePipelineState;
|
||||||
private _config: MergePointConfig[];
|
private _config: MergePointConfig[];
|
||||||
|
|
||||||
|
public newElementId: string = undefined;
|
||||||
|
public newElementIdNumber: number = undefined
|
||||||
|
|
||||||
constructor(tags: Tag[],
|
constructor(tags: Tag[],
|
||||||
coordinates: [number, number][],
|
coordinates: [number, number][],
|
||||||
|
@ -87,7 +90,8 @@ export default class CreateWayWithPointReuseAction extends OsmChangeAction {
|
||||||
properties: {
|
properties: {
|
||||||
"move": "yes",
|
"move": "yes",
|
||||||
"osm-id": reusedPoint.node.properties.id,
|
"osm-id": reusedPoint.node.properties.id,
|
||||||
"id": "new-geometry-move-existing" + i
|
"id": "new-geometry-move-existing" + i,
|
||||||
|
"distance":GeoOperations.distanceBetween(coordinateInfo.lngLat, reusedPoint.node.geometry.coordinates)
|
||||||
},
|
},
|
||||||
geometry: {
|
geometry: {
|
||||||
type: "LineString",
|
type: "LineString",
|
||||||
|
@ -97,8 +101,23 @@ export default class CreateWayWithPointReuseAction extends OsmChangeAction {
|
||||||
features.push(moveDescription)
|
features.push(moveDescription)
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// The geometry is moved
|
// The geometry is moved, the point is reused
|
||||||
geometryMoved = true
|
geometryMoved = true
|
||||||
|
|
||||||
|
const reuseDescription = {
|
||||||
|
type: "Feature",
|
||||||
|
properties: {
|
||||||
|
"move": "no",
|
||||||
|
"osm-id": reusedPoint.node.properties.id,
|
||||||
|
"id": "new-geometry-reuse-existing" + i,
|
||||||
|
"distance":GeoOperations.distanceBetween(coordinateInfo.lngLat, reusedPoint.node.geometry.coordinates)
|
||||||
|
},
|
||||||
|
geometry: {
|
||||||
|
type: "LineString",
|
||||||
|
coordinates: [coordinateInfo.lngLat, reusedPoint.node.geometry.coordinates]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
features.push(reuseDescription)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -138,11 +157,10 @@ export default class CreateWayWithPointReuseAction extends OsmChangeAction {
|
||||||
features.push(newGeometry)
|
features.push(newGeometry)
|
||||||
|
|
||||||
}
|
}
|
||||||
console.log("Preview:", features)
|
|
||||||
return new StaticFeatureSource(features, false)
|
return new StaticFeatureSource(features, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
|
public async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
|
||||||
const theme = this._state.layoutToUse.id
|
const theme = this._state.layoutToUse.id
|
||||||
const allChanges: ChangeDescription[] = []
|
const allChanges: ChangeDescription[] = []
|
||||||
const nodeIdsToUse: { lat: number, lon: number, nodeId?: number }[] = []
|
const nodeIdsToUse: { lat: number, lon: number, nodeId?: number }[] = []
|
||||||
|
@ -196,6 +214,8 @@ export default class CreateWayWithPointReuseAction extends OsmChangeAction {
|
||||||
})
|
})
|
||||||
|
|
||||||
allChanges.push(...(await newWay.CreateChangeDescriptions(changes)))
|
allChanges.push(...(await newWay.CreateChangeDescriptions(changes)))
|
||||||
|
this.newElementId = newWay.newElementId
|
||||||
|
this.newElementIdNumber = newWay.newElementIdNumber
|
||||||
return allChanges
|
return allChanges
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ export default abstract class OsmChangeAction {
|
||||||
public readonly trackStatistics: boolean;
|
public readonly trackStatistics: boolean;
|
||||||
/**
|
/**
|
||||||
* The ID of the object that is the center of this change.
|
* The ID of the object that is the center of this change.
|
||||||
* Null if the action creates a new object
|
* Null if the action creates a new object (at initialization)
|
||||||
* Undefined if such an id does not make sense
|
* Undefined if such an id does not make sense
|
||||||
*/
|
*/
|
||||||
public readonly mainObjectId: string;
|
public readonly mainObjectId: string;
|
||||||
|
@ -30,4 +30,11 @@ export default abstract class OsmChangeAction {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]>
|
protected abstract CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export abstract class OsmCreateAction extends OsmChangeAction{
|
||||||
|
|
||||||
|
public newElementId : string
|
||||||
|
public newElementIdNumber: number
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -46,6 +46,9 @@ export class Tag extends TagsFilter {
|
||||||
if (shorten) {
|
if (shorten) {
|
||||||
v = Utils.EllipsesAfter(v, 25);
|
v = Utils.EllipsesAfter(v, 25);
|
||||||
}
|
}
|
||||||
|
if(v === "" || v === undefined){
|
||||||
|
return "<span class='line-through'>"+this.key+"</span>"
|
||||||
|
}
|
||||||
if (linkToWiki) {
|
if (linkToWiki) {
|
||||||
return `<a href='https://wiki.openstreetmap.org/wiki/Key:${this.key}' target='_blank'>${this.key}</a>` +
|
return `<a href='https://wiki.openstreetmap.org/wiki/Key:${this.key}' target='_blank'>${this.key}</a>` +
|
||||||
`=` +
|
`=` +
|
||||||
|
|
|
@ -8,7 +8,7 @@ export abstract class TagsFilter {
|
||||||
|
|
||||||
abstract matchesProperties(properties: any): boolean;
|
abstract matchesProperties(properties: any): boolean;
|
||||||
|
|
||||||
abstract asHumanString(linkToWiki: boolean, shorten: boolean, properties: any);
|
abstract asHumanString(linkToWiki: boolean, shorten: boolean, properties: any) : string;
|
||||||
|
|
||||||
abstract usedKeys(): string[];
|
abstract usedKeys(): string[];
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import {Utils} from "../Utils";
|
||||||
|
|
||||||
export default class Constants {
|
export default class Constants {
|
||||||
|
|
||||||
public static vNumber = "0.13.0-alpha-4";
|
public static vNumber = "0.13.0-alpha-5";
|
||||||
public static ImgurApiKey = '7070e7167f0a25a'
|
public static ImgurApiKey = '7070e7167f0a25a'
|
||||||
public static readonly mapillary_client_token_v4 = "MLY|4441509239301885|b40ad2d3ea105435bd40c7e76993ae85"
|
public static readonly mapillary_client_token_v4 = "MLY|4441509239301885|b40ad2d3ea105435bd40c7e76993ae85"
|
||||||
|
|
||||||
|
|
|
@ -136,7 +136,7 @@ export interface LayerConfigJson {
|
||||||
titleIcons?: (string | TagRenderingConfigJson)[];
|
titleIcons?: (string | TagRenderingConfigJson)[];
|
||||||
|
|
||||||
|
|
||||||
mapRendering: (PointRenderingConfigJson | LineRenderingConfigJson)[]
|
mapRendering: null | (PointRenderingConfigJson | LineRenderingConfigJson)[]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If set, this layer will pass all the features it receives onto the next layer.
|
* If set, this layer will pass all the features it receives onto the next layer.
|
||||||
|
|
|
@ -7,17 +7,11 @@ import Translations from "../i18n/Translations";
|
||||||
import Constants from "../../Models/Constants";
|
import Constants from "../../Models/Constants";
|
||||||
import Toggle from "../Input/Toggle";
|
import Toggle from "../Input/Toggle";
|
||||||
import CreateNewNodeAction from "../../Logic/Osm/Actions/CreateNewNodeAction";
|
import CreateNewNodeAction from "../../Logic/Osm/Actions/CreateNewNodeAction";
|
||||||
import {Tag} from "../../Logic/Tags/Tag";
|
|
||||||
import Loading from "../Base/Loading";
|
import Loading from "../Base/Loading";
|
||||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
|
|
||||||
import {OsmConnection} from "../../Logic/Osm/OsmConnection";
|
import {OsmConnection} from "../../Logic/Osm/OsmConnection";
|
||||||
import {Changes} from "../../Logic/Osm/Changes";
|
|
||||||
import {ElementStorage} from "../../Logic/ElementStorage";
|
|
||||||
import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline";
|
|
||||||
import Lazy from "../Base/Lazy";
|
import Lazy from "../Base/Lazy";
|
||||||
import ConfirmLocationOfPoint from "../NewPoint/ConfirmLocationOfPoint";
|
import ConfirmLocationOfPoint from "../NewPoint/ConfirmLocationOfPoint";
|
||||||
import Img from "../Base/Img";
|
import Img from "../Base/Img";
|
||||||
import {Translation} from "../i18n/Translation";
|
|
||||||
import FilteredLayer from "../../Models/FilteredLayer";
|
import FilteredLayer from "../../Models/FilteredLayer";
|
||||||
import SpecialVisualizations from "../SpecialVisualizations";
|
import SpecialVisualizations from "../SpecialVisualizations";
|
||||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||||
|
@ -28,68 +22,29 @@ import ShowDataLayer from "../ShowDataLayer/ShowDataLayer";
|
||||||
import AllKnownLayers from "../../Customizations/AllKnownLayers";
|
import AllKnownLayers from "../../Customizations/AllKnownLayers";
|
||||||
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource";
|
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource";
|
||||||
import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer";
|
import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer";
|
||||||
import BaseLayer from "../../Models/BaseLayer";
|
|
||||||
import ReplaceGeometryAction from "../../Logic/Osm/Actions/ReplaceGeometryAction";
|
|
||||||
import CreateWayWithPointReuseAction, {MergePointConfig} from "../../Logic/Osm/Actions/CreateWayWithPointReuseAction";
|
import CreateWayWithPointReuseAction, {MergePointConfig} from "../../Logic/Osm/Actions/CreateWayWithPointReuseAction";
|
||||||
import OsmChangeAction from "../../Logic/Osm/Actions/OsmChangeAction";
|
import OsmChangeAction, {OsmCreateAction} from "../../Logic/Osm/Actions/OsmChangeAction";
|
||||||
import FeatureSource from "../../Logic/FeatureSource/FeatureSource";
|
import FeatureSource from "../../Logic/FeatureSource/FeatureSource";
|
||||||
import {OsmObject, OsmWay} from "../../Logic/Osm/OsmObject";
|
import {OsmObject, OsmWay} from "../../Logic/Osm/OsmObject";
|
||||||
import FeaturePipelineState from "../../Logic/State/FeaturePipelineState";
|
import FeaturePipelineState from "../../Logic/State/FeaturePipelineState";
|
||||||
import {DefaultGuiState} from "../DefaultGuiState";
|
import {DefaultGuiState} from "../DefaultGuiState";
|
||||||
import {PresetInfo} from "../BigComponents/SimpleAddUI";
|
import {PresetInfo} from "../BigComponents/SimpleAddUI";
|
||||||
|
import {TagUtils} from "../../Logic/Tags/TagUtils";
|
||||||
|
import {And} from "../../Logic/Tags/And";
|
||||||
export interface ImportButtonState {
|
import ReplaceGeometryAction from "../../Logic/Osm/Actions/ReplaceGeometryAction";
|
||||||
description?: Translation;
|
import CreateMultiPolygonWithPointReuseAction from "../../Logic/Osm/Actions/CreateMultiPolygonWithPointReuseAction";
|
||||||
image: () => BaseUIElement,
|
import {Tag} from "../../Logic/Tags/Tag";
|
||||||
message: string | BaseUIElement,
|
|
||||||
originalTags: UIEventSource<any>,
|
|
||||||
newTags: UIEventSource<Tag[]>,
|
|
||||||
targetLayer: FilteredLayer,
|
|
||||||
feature: any,
|
|
||||||
minZoom: number,
|
|
||||||
state: {
|
|
||||||
backgroundLayer: UIEventSource<BaseLayer>;
|
|
||||||
filteredLayers: UIEventSource<FilteredLayer[]>;
|
|
||||||
featureSwitchUserbadge: UIEventSource<boolean>;
|
|
||||||
featurePipeline: FeaturePipeline;
|
|
||||||
allElements: ElementStorage;
|
|
||||||
selectedElement: UIEventSource<any>;
|
|
||||||
layoutToUse: LayoutConfig,
|
|
||||||
osmConnection: OsmConnection,
|
|
||||||
changes: Changes,
|
|
||||||
locationControl: UIEventSource<{ zoom: number }>
|
|
||||||
},
|
|
||||||
guiState: { filterViewIsOpened: UIEventSource<boolean> },
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SnapSettings for newly imported points
|
|
||||||
*/
|
|
||||||
snapSettings?: {
|
|
||||||
snapToLayers: string[],
|
|
||||||
snapToLayersMaxDist?: number
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Settings if an imported feature must be conflated with an already existing feature
|
|
||||||
*/
|
|
||||||
conflationSettings?: {
|
|
||||||
conflateWayId: string
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Settings for newly created points which are part of a way: when to snap to already existing points?
|
|
||||||
*/
|
|
||||||
mergeConfigs: MergePointConfig[]
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
abstract class AbstractImportButton implements SpecialVisualizations {
|
abstract class AbstractImportButton implements SpecialVisualizations {
|
||||||
public readonly funcName: string
|
public readonly funcName: string
|
||||||
public readonly docs: string
|
public readonly docs: string
|
||||||
public readonly args: { name: string, defaultValue?: string, doc: string }[]
|
public readonly args: { name: string, defaultValue?: string, doc: string }[]
|
||||||
|
private readonly showRemovedTags: boolean;
|
||||||
|
|
||||||
constructor(funcName: string, docsIntro: string, extraArgs: { name: string, doc: string, defaultValue?: string }[]) {
|
constructor(funcName: string, docsIntro: string, extraArgs: { name: string, doc: string, defaultValue?: string }[], showRemovedTags = true) {
|
||||||
this.funcName = funcName
|
this.funcName = funcName
|
||||||
|
this.showRemovedTags = showRemovedTags;
|
||||||
|
|
||||||
this.docs = `${docsIntro}
|
this.docs = `${docsIntro}
|
||||||
|
|
||||||
|
@ -102,9 +57,7 @@ The argument \`tags\` of the import button takes a \`;\`-seperated list of tags
|
||||||
|
|
||||||
${Utils.Special_visualizations_tagsToApplyHelpText}
|
${Utils.Special_visualizations_tagsToApplyHelpText}
|
||||||
${Utils.special_visualizations_importRequirementDocs}
|
${Utils.special_visualizations_importRequirementDocs}
|
||||||
|
|
||||||
`
|
`
|
||||||
|
|
||||||
this.args = [
|
this.args = [
|
||||||
{
|
{
|
||||||
name: "targetLayer",
|
name: "targetLayer",
|
||||||
|
@ -128,11 +81,16 @@ ${Utils.special_visualizations_importRequirementDocs}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
abstract constructElement(state: FeaturePipelineState, args: { minzoom: string, max_snap_distance: string, snap_onto_layers: string, icon: string, text: string, tags: string, targetLayer: string },
|
abstract constructElement(state: FeaturePipelineState,
|
||||||
tagSource: UIEventSource<any>, guiState: DefaultGuiState): BaseUIElement;
|
args: { max_snap_distance: string, snap_onto_layers: string, icon: string, text: string, tags: string, newTags: UIEventSource<any>, targetLayer: string },
|
||||||
|
tagSource: UIEventSource<any>,
|
||||||
|
guiState: DefaultGuiState,
|
||||||
|
feature: any,
|
||||||
|
onCancelClicked: () => void): BaseUIElement;
|
||||||
|
|
||||||
|
|
||||||
constr(state, tagSource, argsRaw, guiState) {
|
constr(state, tagSource, argsRaw, guiState) {
|
||||||
|
const self = this;
|
||||||
/**
|
/**
|
||||||
* Some generic import button pre-validation is implemented here:
|
* Some generic import button pre-validation is implemented here:
|
||||||
* - Are we logged in?
|
* - Are we logged in?
|
||||||
|
@ -144,7 +102,7 @@ ${Utils.special_visualizations_importRequirementDocs}
|
||||||
|
|
||||||
const t = Translations.t.general.add.import;
|
const t = Translations.t.general.add.import;
|
||||||
const t0 = Translations.t.general.add;
|
const t0 = Translations.t.general.add;
|
||||||
const args = this.parseArgs(argsRaw)
|
const args = this.parseArgs(argsRaw, tagSource)
|
||||||
|
|
||||||
{
|
{
|
||||||
// Some initial validation
|
// Some initial validation
|
||||||
|
@ -171,26 +129,22 @@ ${Utils.special_visualizations_importRequirementDocs}
|
||||||
const id = tagSource.data.id;
|
const id = tagSource.data.id;
|
||||||
const feature = state.allElements.ContainingFeatures.get(id)
|
const feature = state.allElements.ContainingFeatures.get(id)
|
||||||
|
|
||||||
|
|
||||||
/**** THe actual panel showing the import guiding map ****/
|
|
||||||
const importGuidingPanel = this.constructElement(state, args, tagSource, guiState)
|
|
||||||
|
|
||||||
// Explanation of the tags that will be applied onto the imported/conflated object
|
// Explanation of the tags that will be applied onto the imported/conflated object
|
||||||
const newTags = SpecialVisualizations.generateTagsToApply(args.tags, tagSource)
|
const newTags = SpecialVisualizations.generateTagsToApply(args.tags, tagSource)
|
||||||
const appliedTags = new Toggle(
|
const appliedTags = new Toggle(
|
||||||
new VariableUiElement(
|
new VariableUiElement(
|
||||||
newTags.map(tgs => {
|
newTags.map(tgs => {
|
||||||
const parts = []
|
const filteredTags = tgs.filter(tg => self.showRemovedTags || (tg.value ?? "") !== "")
|
||||||
for (const tag of tgs) {
|
const asText = new And(filteredTags)
|
||||||
parts.push(tag.key + "=" + tag.value)
|
.asHumanString(true, true, {})
|
||||||
}
|
|
||||||
const txt = parts.join(" & ")
|
return t0.presetInfo.Subs({tags: asText}).SetClass("subtle");
|
||||||
return t0.presetInfo.Subs({tags: txt}).SetClass("subtle")
|
})),
|
||||||
})), undefined,
|
undefined,
|
||||||
state.osmConnection.userDetails.map(ud => ud.csCount >= Constants.userJourney.tagsVisibleAt)
|
state.osmConnection.userDetails.map(ud => ud.csCount >= Constants.userJourney.tagsVisibleAt)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const importClicked = new UIEventSource(false);
|
const importClicked = new UIEventSource(false);
|
||||||
inviteToImportButton.onClick(() => {
|
inviteToImportButton.onClick(() => {
|
||||||
|
@ -207,15 +161,17 @@ ${Utils.special_visualizations_importRequirementDocs}
|
||||||
|
|
||||||
const isImported = tagSource.map(tags => tags._imported === "yes")
|
const isImported = tagSource.map(tags => tags._imported === "yes")
|
||||||
|
|
||||||
|
|
||||||
|
/**** THe actual panel showing the import guiding map ****/
|
||||||
|
const importGuidingPanel = this.constructElement(state, args, tagSource, guiState, feature, () => importClicked.setData(false))
|
||||||
|
|
||||||
|
|
||||||
const importFlow = new Toggle(
|
const importFlow = new Toggle(
|
||||||
new Toggle(
|
new Toggle(
|
||||||
new Loading(t0.stillLoading),
|
new Loading(t0.stillLoading),
|
||||||
new Combine([importGuidingPanel, appliedTags]).SetClass("flex flex-col"),
|
new Combine([importGuidingPanel, appliedTags]).SetClass("flex flex-col"),
|
||||||
state.featurePipeline.runningQuery
|
state.featurePipeline.runningQuery
|
||||||
) ,
|
),
|
||||||
inviteToImportButton,
|
inviteToImportButton,
|
||||||
importClicked
|
importClicked
|
||||||
);
|
);
|
||||||
|
@ -239,12 +195,16 @@ ${Utils.special_visualizations_importRequirementDocs}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseArgs(argsRaw: string[]): { minzoom: string, max_snap_distance: string, snap_onto_layers: string, icon: string, text: string, tags: string, targetLayer: string } {
|
private parseArgs(argsRaw: string[], originalFeatureTags: UIEventSource<any>): { minzoom: string, max_snap_distance: string, snap_onto_layers: string, icon: string, text: string, tags: string, targetLayer: string, newTags: UIEventSource<Tag[]> } {
|
||||||
return Utils.ParseVisArgs(this.args, argsRaw)
|
const baseArgs = Utils.ParseVisArgs(this.args, argsRaw)
|
||||||
|
if (originalFeatureTags !== undefined) {
|
||||||
|
baseArgs["newTags"] = SpecialVisualizations.generateTagsToApply(baseArgs.tags, originalFeatureTags)
|
||||||
|
}
|
||||||
|
return baseArgs
|
||||||
}
|
}
|
||||||
|
|
||||||
getLayerDependencies(argsRaw: string[]) {
|
getLayerDependencies(argsRaw: string[]) {
|
||||||
const args = this.parseArgs(argsRaw)
|
const args = this.parseArgs(argsRaw, undefined)
|
||||||
|
|
||||||
const dependsOnLayers: string[] = []
|
const dependsOnLayers: string[] = []
|
||||||
|
|
||||||
|
@ -261,181 +221,31 @@ ${Utils.special_visualizations_importRequirementDocs}
|
||||||
|
|
||||||
|
|
||||||
protected abstract canBeImported(feature: any)
|
protected abstract canBeImported(feature: any)
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export class ImportButtonSpecialViz extends AbstractImportButton {
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super("import_button",
|
|
||||||
"This button will copy the data from an external dataset into OpenStreetMap",
|
|
||||||
[{
|
|
||||||
name: "snap_onto_layers",
|
|
||||||
doc: "If a way of the given layer(s) is closeby, will snap the new point onto this way (similar as preset might snap). To show multiple layers to snap onto, use a `;`-seperated list",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "max_snap_distance",
|
|
||||||
doc: "If the imported object is a point, the maximum distance that this point will be moved to snap onto a way in an already existing layer (in meters)",
|
|
||||||
defaultValue: "5"
|
|
||||||
}]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
canBeImported(feature: any) {
|
|
||||||
const type = feature.geometry.type
|
|
||||||
return type === "Point" || type === "LineString" || type === "Polygon"
|
|
||||||
}
|
|
||||||
|
|
||||||
constructElement(state, args,
|
|
||||||
tagSource,
|
|
||||||
guiState): BaseUIElement {
|
|
||||||
|
|
||||||
let snapSettings = undefined
|
|
||||||
{
|
|
||||||
// Configure the snapsettings (if applicable)
|
|
||||||
const snapToLayers = args.snap_onto_layers?.trim()?.split(";")?.filter(s => s !== "")
|
|
||||||
const snapToLayersMaxDist = Number(args.max_snap_distance ?? 5)
|
|
||||||
if (snapToLayers.length > 0) {
|
|
||||||
snapSettings = {
|
|
||||||
snapToLayers,
|
|
||||||
snapToLayersMaxDist
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const o =
|
|
||||||
{
|
|
||||||
state, guiState, image: img,
|
|
||||||
feature, newTags, message, minZoom: 18,
|
|
||||||
originalTags: tagSource,
|
|
||||||
targetLayer,
|
|
||||||
snapSettings,
|
|
||||||
conflationSettings: undefined,
|
|
||||||
mergeConfigs: undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
return ImportButton.createConfirmPanel(o, isImported, importClicked),
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class ImportButton {
|
|
||||||
|
|
||||||
public static createConfirmPanel(o: ImportButtonState,
|
|
||||||
isImported: UIEventSource<boolean>,
|
|
||||||
importClicked: UIEventSource<boolean>) {
|
|
||||||
const geometry = o.feature.geometry
|
|
||||||
if (geometry.type === "Point") {
|
|
||||||
return new Lazy(() => ImportButton.createConfirmPanelForPoint(o, isImported, importClicked))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (geometry.type === "Polygon" && geometry.coordinates.length > 1) {
|
|
||||||
return new Lazy(() => ImportButton.createConfirmForMultiPolygon(o, isImported, importClicked))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (geometry.type === "Polygon" || geometry.type == "LineString") {
|
|
||||||
return new Lazy(() => ImportButton.createConfirmForWay(o, isImported, importClicked))
|
|
||||||
}
|
|
||||||
console.error("Invalid type to import", geometry.type)
|
|
||||||
return new FixedUiElement("Invalid geometry type:" + geometry.type).SetClass("alert")
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public static createConfirmForMultiPolygon(o: ImportButtonState,
|
|
||||||
isImported: UIEventSource<boolean>,
|
|
||||||
importClicked: UIEventSource<boolean>): BaseUIElement {
|
|
||||||
if (o.conflationSettings !== undefined) {
|
|
||||||
return new FixedUiElement("Conflating multipolygons is not supported").SetClass("alert")
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// For every single linear ring, we create a new way
|
|
||||||
const createRings: (OsmChangeAction & { getPreview(): Promise<FeatureSource> })[] = []
|
|
||||||
|
|
||||||
for (const coordinateRing of o.feature.geometry.coordinates) {
|
|
||||||
createRings.push(new CreateWayWithPointReuseAction(
|
|
||||||
// The individual way doesn't receive any tags
|
|
||||||
[],
|
|
||||||
coordinateRing,
|
|
||||||
// @ts-ignore
|
|
||||||
o.state,
|
|
||||||
o.mergeConfigs
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return new FixedUiElement("Multipolygon! Here we come").SetClass("alert")
|
|
||||||
}
|
|
||||||
|
|
||||||
public static createConfirmForWay(o: ImportButtonState,
|
|
||||||
isImported: UIEventSource<boolean>,
|
|
||||||
importClicked: UIEventSource<boolean>): BaseUIElement {
|
|
||||||
|
|
||||||
|
protected createConfirmPanelForWay(
|
||||||
|
state: FeaturePipelineState,
|
||||||
|
args: { max_snap_distance: string, snap_onto_layers: string, icon: string, text: string, newTags: UIEventSource<Tag[]>, targetLayer: string },
|
||||||
|
feature: any,
|
||||||
|
originalFeatureTags: UIEventSource<any>,
|
||||||
|
action: (OsmChangeAction & { getPreview(): Promise<FeatureSource>, newElementId?: string }),
|
||||||
|
onCancel: () => void): BaseUIElement {
|
||||||
|
const self = this;
|
||||||
const confirmationMap = Minimap.createMiniMap({
|
const confirmationMap = Minimap.createMiniMap({
|
||||||
allowMoving: false,
|
allowMoving: false,
|
||||||
background: o.state.backgroundLayer
|
background: state.backgroundLayer
|
||||||
})
|
})
|
||||||
confirmationMap.SetStyle("height: 20rem; overflow: hidden").SetClass("rounded-xl")
|
confirmationMap.SetStyle("height: 20rem; overflow: hidden").SetClass("rounded-xl")
|
||||||
|
|
||||||
const relevantFeatures = Utils.NoNull([o.feature, o.state.allElements?.ContainingFeatures?.get(o.conflationSettings?.conflateWayId)])
|
|
||||||
// SHow all relevant data - including (eventually) the way of which the geometry will be replaced
|
// SHow all relevant data - including (eventually) the way of which the geometry will be replaced
|
||||||
new ShowDataMultiLayer({
|
new ShowDataMultiLayer({
|
||||||
leafletMap: confirmationMap.leafletMap,
|
leafletMap: confirmationMap.leafletMap,
|
||||||
enablePopups: false,
|
enablePopups: false,
|
||||||
zoomToFeatures: true,
|
zoomToFeatures: true,
|
||||||
features: new StaticFeatureSource(relevantFeatures, false),
|
features: new StaticFeatureSource([feature], false),
|
||||||
allElements: o.state.allElements,
|
allElements: state.allElements,
|
||||||
layers: o.state.filteredLayers
|
layers: state.filteredLayers
|
||||||
})
|
})
|
||||||
|
|
||||||
let action: OsmChangeAction & { getPreview(): Promise<FeatureSource> }
|
|
||||||
|
|
||||||
const changes = o.state.changes
|
|
||||||
let confirm: () => Promise<string>
|
|
||||||
if (o.conflationSettings !== undefined) {
|
|
||||||
// Conflate the way
|
|
||||||
action = new ReplaceGeometryAction(
|
|
||||||
o.state,
|
|
||||||
o.feature,
|
|
||||||
o.conflationSettings.conflateWayId,
|
|
||||||
{
|
|
||||||
theme: o.state.layoutToUse.id,
|
|
||||||
newTags: o.newTags.data
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
confirm = async () => {
|
|
||||||
changes.applyAction(action)
|
|
||||||
return o.feature.properties.id
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
// Upload the way to OSM
|
|
||||||
const geom = o.feature.geometry
|
|
||||||
let coordinates: [number, number][]
|
|
||||||
if (geom.type === "LineString") {
|
|
||||||
coordinates = geom.coordinates
|
|
||||||
} else if (geom.type === "Polygon") {
|
|
||||||
coordinates = geom.coordinates[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
action = new CreateWayWithPointReuseAction(
|
|
||||||
o.newTags.data,
|
|
||||||
coordinates,
|
|
||||||
// @ts-ignore
|
|
||||||
o.state,
|
|
||||||
o.mergeConfigs
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
confirm = async () => {
|
|
||||||
changes.applyAction(action)
|
|
||||||
return action.mainObjectId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
action.getPreview().then(changePreview => {
|
action.getPreview().then(changePreview => {
|
||||||
new ShowDataLayer({
|
new ShowDataLayer({
|
||||||
|
@ -443,89 +253,328 @@ export default class ImportButton {
|
||||||
enablePopups: false,
|
enablePopups: false,
|
||||||
zoomToFeatures: false,
|
zoomToFeatures: false,
|
||||||
features: changePreview,
|
features: changePreview,
|
||||||
allElements: o.state.allElements,
|
allElements: state.allElements,
|
||||||
layerToShow: AllKnownLayers.sharedLayers.get("conflation")
|
layerToShow: AllKnownLayers.sharedLayers.get("conflation")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const tagsExplanation = new VariableUiElement(o.newTags.map(tagsToApply => {
|
const tagsExplanation = new VariableUiElement(args.newTags.map(tagsToApply => {
|
||||||
const tagsStr = tagsToApply.map(t => t.asHumanString(false, true)).join("&");
|
const filteredTags = tagsToApply.filter(t => self.showRemovedTags || (t.value ?? "") !== "")
|
||||||
|
const tagsStr = new And(filteredTags).asHumanString(false, true, {})
|
||||||
return Translations.t.general.add.import.importTags.Subs({tags: tagsStr});
|
return Translations.t.general.add.import.importTags.Subs({tags: tagsStr});
|
||||||
}
|
}
|
||||||
)).SetClass("subtle")
|
)).SetClass("subtle")
|
||||||
|
|
||||||
const confirmButton = new SubtleButton(o.image(), new Combine([o.message, tagsExplanation]).SetClass("flex flex-col"))
|
const confirmButton = new SubtleButton(new Img(args.icon), new Combine([args.text, tagsExplanation]).SetClass("flex flex-col"))
|
||||||
confirmButton.onClick(async () => {
|
confirmButton.onClick(async () => {
|
||||||
{
|
{
|
||||||
if (isImported.data) {
|
originalFeatureTags.data["_imported"] = "yes"
|
||||||
return
|
originalFeatureTags.ping() // will set isImported as per its definition
|
||||||
}
|
state.changes.applyAction(action)
|
||||||
o.originalTags.data["_imported"] = "yes"
|
state.selectedElement.setData(state.allElements.ContainingFeatures.get(action.newElementId ?? action.mainObjectId))
|
||||||
o.originalTags.ping() // will set isImported as per its definition
|
|
||||||
|
|
||||||
const idToSelect = await confirm()
|
|
||||||
|
|
||||||
o.state.selectedElement.setData(o.state.allElements.ContainingFeatures.get(idToSelect))
|
|
||||||
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const cancel = new SubtleButton(Svg.close_ui(), Translations.t.general.cancel).onClick(() => importClicked.setData(false))
|
const cancel = new SubtleButton(Svg.close_ui(), Translations.t.general.cancel).onClick(onCancel)
|
||||||
|
|
||||||
|
|
||||||
return new Combine([confirmationMap, confirmButton, cancel]).SetClass("flex flex-col")
|
return new Combine([confirmationMap, confirmButton, cancel]).SetClass("flex flex-col")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static createConfirmPanelForPoint(
|
export class ConflateButton extends AbstractImportButton {
|
||||||
o: ImportButtonState,
|
|
||||||
isImported: UIEventSource<boolean>,
|
constructor() {
|
||||||
importClicked: UIEventSource<boolean>): BaseUIElement {
|
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"
|
||||||
|
}]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected canBeImported(feature: any) {
|
||||||
|
return feature.geometry.type === "LineString" || (feature.geometry.type === "Polygon" && feature.geometry.coordinates.length === 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
constructElement(state: FeaturePipelineState,
|
||||||
|
args: { max_snap_distance: string; snap_onto_layers: string; icon: string; text: string; tags: string; newTags: UIEventSource<Tag[]>; targetLayer: string },
|
||||||
|
tagSource: UIEventSource<any>, guiState: DefaultGuiState, feature: any, 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]
|
||||||
|
const action = new ReplaceGeometryAction(
|
||||||
|
state,
|
||||||
|
feature,
|
||||||
|
wayToConflate,
|
||||||
|
{
|
||||||
|
theme: state.layoutToUse.id,
|
||||||
|
newTags: args.newTags.data
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return this.createConfirmPanelForWay(
|
||||||
|
state,
|
||||||
|
args,
|
||||||
|
feature,
|
||||||
|
tagSource,
|
||||||
|
action,
|
||||||
|
onCancelClicked
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ImportWayButton extends AbstractImportButton {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super("import_way_button",
|
||||||
|
"This button will copy the data from an external dataset into OpenStreetMap",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: "snap_to_point_if",
|
||||||
|
doc: "Points with the given tags will be snapped to or moved",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "max_snap_distance",
|
||||||
|
doc: "If the imported object is a LineString or (Multi)Polygon, already existing OSM-points will be reused to construct the geometry of the newly imported way",
|
||||||
|
defaultValue: "5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "move_osm_point_if",
|
||||||
|
doc: "Moves the OSM-point to the newly imported point if these conditions are met",
|
||||||
|
},{
|
||||||
|
name:"max_move_distance",
|
||||||
|
doc: "If an OSM-point is moved, the maximum amount of meters it is moved. Capped on 20m",
|
||||||
|
defaultValue: "1"
|
||||||
|
},{
|
||||||
|
name:"snap_onto_layers",
|
||||||
|
doc:"If no existing nearby point exists, but a line of a specified layer is closeby, snap to this layer instead",
|
||||||
|
|
||||||
|
},{
|
||||||
|
name:"snap_to_layer_max_distance",
|
||||||
|
doc:"Distance to distort the geometry to snap to this layer",
|
||||||
|
defaultValue: "0.1"
|
||||||
|
}],
|
||||||
|
false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
canBeImported(feature: any) {
|
||||||
|
const type = feature.geometry.type
|
||||||
|
return type === "LineString" || type === "Polygon"
|
||||||
|
}
|
||||||
|
|
||||||
|
getLayerDependencies(argsRaw: string[]): string[] {
|
||||||
|
const deps = super.getLayerDependencies(argsRaw);
|
||||||
|
deps.push("type_node")
|
||||||
|
return deps
|
||||||
|
}
|
||||||
|
|
||||||
|
constructElement(state, args,
|
||||||
|
originalFeatureTags,
|
||||||
|
guiState,
|
||||||
|
feature,
|
||||||
|
onCancel): BaseUIElement {
|
||||||
|
|
||||||
|
|
||||||
|
const geometry = feature.geometry
|
||||||
|
|
||||||
|
if (!(geometry.type == "LineString" || geometry.type === "Polygon")) {
|
||||||
|
console.error("Invalid type to import", geometry.type)
|
||||||
|
return new FixedUiElement("Invalid geometry type:" + geometry.type).SetClass("alert")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Upload the way to OSM
|
||||||
|
const geom = feature.geometry
|
||||||
|
let coordinates: [number, number][]
|
||||||
|
if (geom.type === "LineString") {
|
||||||
|
coordinates = geom.coordinates
|
||||||
|
} else if (geom.type === "Polygon") {
|
||||||
|
coordinates = geom.coordinates[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodesMustMatch = args["snap_to_point_if"]?.split(";")?.map((tag, i) => TagUtils.Tag(tag, "TagsSpec for import button " + i))
|
||||||
|
|
||||||
|
const mergeConfigs = []
|
||||||
|
if (nodesMustMatch !== undefined && nodesMustMatch.length > 0) {
|
||||||
|
const mergeConfig: MergePointConfig = {
|
||||||
|
mode: "reuse_osm_point",
|
||||||
|
ifMatches: new And(nodesMustMatch),
|
||||||
|
withinRangeOfM: Number(args.max_snap_distance)
|
||||||
|
}
|
||||||
|
mergeConfigs.push(mergeConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const moveOsmPointIfTags = args["move_osm_point_if"]?.split(";")?.map((tag, i) => TagUtils.Tag(tag, "TagsSpec for import button " + i))
|
||||||
|
|
||||||
|
if (nodesMustMatch !== undefined && moveOsmPointIfTags.length > 0) {
|
||||||
|
const moveDistance = Math.min(20, Number(args["max_move_distance"]))
|
||||||
|
const mergeConfig: MergePointConfig = {
|
||||||
|
mode: "move_osm_point" ,
|
||||||
|
ifMatches: new And(moveOsmPointIfTags),
|
||||||
|
withinRangeOfM: moveDistance
|
||||||
|
}
|
||||||
|
mergeConfigs.push(mergeConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
let action: OsmCreateAction & { getPreview(): Promise<FeatureSource> };
|
||||||
|
|
||||||
|
const coors = feature.geometry.coordinates
|
||||||
|
if (feature.geometry.type === "Polygon" && coors.length > 1) {
|
||||||
|
const outer = coors[0]
|
||||||
|
const inner = [...coors]
|
||||||
|
inner.splice(0, 1)
|
||||||
|
action = new CreateMultiPolygonWithPointReuseAction(
|
||||||
|
args.newTags.data,
|
||||||
|
outer,
|
||||||
|
inner,
|
||||||
|
state,
|
||||||
|
mergeConfigs,
|
||||||
|
"import"
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
|
||||||
|
action = new CreateWayWithPointReuseAction(
|
||||||
|
args.newTags.data,
|
||||||
|
coordinates,
|
||||||
|
state,
|
||||||
|
mergeConfigs
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return this.createConfirmPanelForWay(
|
||||||
|
state,
|
||||||
|
args,
|
||||||
|
feature,
|
||||||
|
originalFeatureTags,
|
||||||
|
action,
|
||||||
|
onCancel
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ImportPointButton extends AbstractImportButton {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super("import_button",
|
||||||
|
"This button will copy the point from an external dataset into OpenStreetMap",
|
||||||
|
[{
|
||||||
|
name: "snap_onto_layers",
|
||||||
|
doc: "If a way of the given layer(s) is closeby, will snap the new point onto this way (similar as preset might snap). To show multiple layers to snap onto, use a `;`-seperated list"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "max_snap_distance",
|
||||||
|
doc: "The maximum distance that the imported point will be moved to snap onto a way in an already existing layer (in meters). This is previewed to the contributor, similar to the 'add new point'-action of MapComplete",
|
||||||
|
defaultValue: "5"
|
||||||
|
}],
|
||||||
|
false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
canBeImported(feature: any) {
|
||||||
|
return feature.geometry.type === "Point"
|
||||||
|
}
|
||||||
|
|
||||||
|
getLayerDependencies(argsRaw: string[]): string[] {
|
||||||
|
const deps = super.getLayerDependencies(argsRaw);
|
||||||
|
const layerSnap = argsRaw["snap_onto_layers"] ?? ""
|
||||||
|
if (layerSnap === "") {
|
||||||
|
return deps
|
||||||
|
}
|
||||||
|
|
||||||
|
deps.push(...layerSnap.split(";"))
|
||||||
|
return deps
|
||||||
|
}
|
||||||
|
|
||||||
|
private static createConfirmPanelForPoint(
|
||||||
|
args: { max_snap_distance: string, snap_onto_layers: string, icon: string, text: string, newTags: UIEventSource<any>, targetLayer: string },
|
||||||
|
state: FeaturePipelineState,
|
||||||
|
guiState: DefaultGuiState,
|
||||||
|
originalFeatureTags: UIEventSource<any>,
|
||||||
|
feature: any,
|
||||||
|
onCancel: () => void): BaseUIElement {
|
||||||
|
|
||||||
async function confirm(tags: any[], location: { lat: number, lon: number }, snapOntoWayId: string) {
|
async function confirm(tags: any[], location: { lat: number, lon: number }, snapOntoWayId: string) {
|
||||||
|
|
||||||
if (isImported.data) {
|
originalFeatureTags.data["_imported"] = "yes"
|
||||||
return
|
originalFeatureTags.ping() // will set isImported as per its definition
|
||||||
}
|
|
||||||
o.originalTags.data["_imported"] = "yes"
|
|
||||||
o.originalTags.ping() // will set isImported as per its definition
|
|
||||||
let snapOnto: OsmObject = undefined
|
let snapOnto: OsmObject = undefined
|
||||||
if (snapOntoWayId !== undefined) {
|
if (snapOntoWayId !== undefined) {
|
||||||
snapOnto = await OsmObject.DownloadObjectAsync(snapOntoWayId)
|
snapOnto = await OsmObject.DownloadObjectAsync(snapOntoWayId)
|
||||||
}
|
}
|
||||||
const newElementAction = new CreateNewNodeAction(tags, location.lat, location.lon, {
|
const newElementAction = new CreateNewNodeAction(tags, location.lat, location.lon, {
|
||||||
theme: o.state.layoutToUse.id,
|
theme: state.layoutToUse.id,
|
||||||
changeType: "import",
|
changeType: "import",
|
||||||
snapOnto: <OsmWay>snapOnto
|
snapOnto: <OsmWay>snapOnto
|
||||||
})
|
})
|
||||||
|
|
||||||
await o.state.changes.applyAction(newElementAction)
|
await state.changes.applyAction(newElementAction)
|
||||||
o.state.selectedElement.setData(o.state.allElements.ContainingFeatures.get(
|
state.selectedElement.setData(state.allElements.ContainingFeatures.get(
|
||||||
newElementAction.newElementId
|
newElementAction.newElementId
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancel() {
|
|
||||||
importClicked.setData(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const presetInfo = <PresetInfo>{
|
const presetInfo = <PresetInfo>{
|
||||||
tags: o.newTags.data,
|
tags: args.newTags.data,
|
||||||
icon: o.image,
|
icon: () => new Img(args.icon),
|
||||||
description: o.description,
|
description: Translations.WT(args.text),
|
||||||
layerToAddTo: o.targetLayer,
|
layerToAddTo: state.filteredLayers.data.filter(l => l.layerDef.id === args.targetLayer)[0],
|
||||||
name: o.message,
|
name: args.text,
|
||||||
title: o.message,
|
title: Translations.WT(args.text),
|
||||||
preciseInput: {
|
preciseInput: {
|
||||||
snapToLayers: o.snapSettings?.snapToLayers,
|
snapToLayers: args.snap_onto_layers?.split(";"),
|
||||||
maxSnapDistance: o.snapSettings?.snapToLayersMaxDist
|
maxSnapDistance: Number(args.max_snap_distance)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const [lon, lat] = o.feature.geometry.coordinates
|
const [lon, lat] = feature.geometry.coordinates
|
||||||
return new ConfirmLocationOfPoint(o.state, o.guiState.filterViewIsOpened, presetInfo, Translations.W(o.message), {
|
return new ConfirmLocationOfPoint(state, guiState.filterViewIsOpened, presetInfo, Translations.W(args.text), {
|
||||||
lon,
|
lon,
|
||||||
lat
|
lat
|
||||||
}, confirm, cancel)
|
}, confirm, onCancel)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
constructElement(state, args,
|
||||||
|
originalFeatureTags,
|
||||||
|
guiState,
|
||||||
|
feature,
|
||||||
|
onCancel): BaseUIElement {
|
||||||
|
|
||||||
|
|
||||||
|
const geometry = feature.geometry
|
||||||
|
|
||||||
|
if (geometry.type === "Point") {
|
||||||
|
return new Lazy(() => ImportPointButton.createConfirmPanelForPoint(
|
||||||
|
args,
|
||||||
|
state,
|
||||||
|
guiState,
|
||||||
|
originalFeatureTags,
|
||||||
|
feature,
|
||||||
|
onCancel
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
console.error("Invalid type to import", geometry.type)
|
||||||
|
return new FixedUiElement("Invalid geometry type:" + geometry.type).SetClass("alert")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,6 @@ import Histogram from "./BigComponents/Histogram";
|
||||||
import Loc from "../Models/Loc";
|
import Loc from "../Models/Loc";
|
||||||
import {Utils} from "../Utils";
|
import {Utils} from "../Utils";
|
||||||
import LayerConfig from "../Models/ThemeConfig/LayerConfig";
|
import LayerConfig from "../Models/ThemeConfig/LayerConfig";
|
||||||
import {ImportButtonSpecialViz} from "./BigComponents/ImportButton";
|
|
||||||
import {Tag} from "../Logic/Tags/Tag";
|
import {Tag} from "../Logic/Tags/Tag";
|
||||||
import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSource";
|
import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSource";
|
||||||
import ShowDataMultiLayer from "./ShowDataLayer/ShowDataMultiLayer";
|
import ShowDataMultiLayer from "./ShowDataLayer/ShowDataMultiLayer";
|
||||||
|
@ -39,6 +38,7 @@ import {DefaultGuiState} from "./DefaultGuiState";
|
||||||
import {GeoOperations} from "../Logic/GeoOperations";
|
import {GeoOperations} from "../Logic/GeoOperations";
|
||||||
import Hash from "../Logic/Web/Hash";
|
import Hash from "../Logic/Web/Hash";
|
||||||
import FeaturePipelineState from "../Logic/State/FeaturePipelineState";
|
import FeaturePipelineState from "../Logic/State/FeaturePipelineState";
|
||||||
|
import {ConflateButton, ImportPointButton, ImportWayButton} from "./Popup/ImportButton";
|
||||||
|
|
||||||
export interface SpecialVisualization {
|
export interface SpecialVisualization {
|
||||||
funcName: string,
|
funcName: string,
|
||||||
|
@ -478,8 +478,9 @@ export default class SpecialVisualizations {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
new ImportButtonSpecialViz(),
|
new ImportPointButton(),
|
||||||
|
new ImportWayButton(),
|
||||||
|
new ConflateButton(),
|
||||||
{
|
{
|
||||||
funcName: "multi_apply",
|
funcName: "multi_apply",
|
||||||
docs: "A button to apply the tagging of this object onto a list of other features. This is an advanced feature for which you'll need calculatedTags",
|
docs: "A button to apply the tagging of this object onto a list of other features. This is an advanced feature for which you'll need calculatedTags",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"id": "conflation",
|
"id": "conflation",
|
||||||
"description": "If the import-button is set to conflate two ways, a preview is shown. This layer defines how this preview is rendered. This layer cannot be included in a theme.",
|
"description": "If the import-button moves OSM points, the imported way points or conflates, a preview is shown. This layer defines how this preview is rendered. This layer cannot be included in a theme.",
|
||||||
"minzoom": 1,
|
"minzoom": 1,
|
||||||
"source": {
|
"source": {
|
||||||
"osmTags": {
|
"osmTags": {
|
||||||
|
@ -20,16 +20,36 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"location": "end",
|
"location": "end",
|
||||||
"icon": "circle:#0f0",
|
"icon": {
|
||||||
|
"render": "circle:#0f0",
|
||||||
|
"mappings":[ {
|
||||||
|
"if": "move=no",
|
||||||
|
"then": "ring:#0f0"
|
||||||
|
}]
|
||||||
|
},
|
||||||
"iconSize": "10,10,center"
|
"iconSize": "10,10,center"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"location": "start",
|
"location": "start",
|
||||||
"icon": "square:#f00",
|
"icon": "square:#f00",
|
||||||
"iconSize": "10,10,center"
|
"iconSize": {
|
||||||
|
"render":"10,10,center",
|
||||||
|
"mappings": [
|
||||||
|
{
|
||||||
|
"if": "distance<0.1",
|
||||||
|
"then": "0,0,center"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"width": "3",
|
"width": {
|
||||||
|
"render": "3",
|
||||||
|
"mappings": [{
|
||||||
|
"if": "distance<0.2",
|
||||||
|
"then": "0"
|
||||||
|
}]
|
||||||
|
},
|
||||||
"color": "#00f",
|
"color": "#00f",
|
||||||
"dasharray": {
|
"dasharray": {
|
||||||
"render": "",
|
"render": "",
|
||||||
|
|
|
@ -155,14 +155,14 @@
|
||||||
{
|
{
|
||||||
"if": "door=sliding",
|
"if": "door=sliding",
|
||||||
"then": {
|
"then": {
|
||||||
"en": "A door which rolls from overhead, typically seen for garages",
|
"en": "A sliding door where the door slides sidewards, typically parallel with a wall",
|
||||||
"nl": "Een poort die langs boven dichtrolt, typisch voor garages"
|
"nl": "Een schuifdeur or roldeur die bij het openen en sluiten zijwaarts beweegt"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"if": "door=overhead",
|
"if": "door=overhead",
|
||||||
"then": {
|
"then": {
|
||||||
"en": "This is an entrance without a physical door",
|
"en": "A door which rolls from overhead, typically seen for garages",
|
||||||
"nl": "Een poort die langs boven dichtrolt, typisch voor garages"
|
"nl": "Een poort die langs boven dichtrolt, typisch voor garages"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -34,15 +34,17 @@
|
||||||
"override": {
|
"override": {
|
||||||
"calculatedTags": [
|
"calculatedTags": [
|
||||||
"_is_part_of_building=feat.get('parent_ways')?.some(p => p.building !== undefined && p.building !== '') ?? false",
|
"_is_part_of_building=feat.get('parent_ways')?.some(p => p.building !== undefined && p.building !== '') ?? false",
|
||||||
|
"_is_part_of_grb_building=feat.get('parent_ways')?.some(p => p['source:geometry:ref'] !== undefined) ?? false",
|
||||||
"_is_part_of_building_passage=feat.get('parent_ways')?.some(p => p.tunnel === 'building_passage') ?? false",
|
"_is_part_of_building_passage=feat.get('parent_ways')?.some(p => p.tunnel === 'building_passage') ?? false",
|
||||||
"_is_part_of_highway=!feat.get('is_part_of_building_passage') && (feat.get('parent_ways')?.some(p => p.highway !== undefined && p.highway !== '') ?? false)",
|
"_is_part_of_highway=!feat.get('is_part_of_building_passage') && (feat.get('parent_ways')?.some(p => p.highway !== undefined && p.highway !== '') ?? false)",
|
||||||
"_is_part_of_landuse=feat.get('parent_ways')?.some(p => (p.landuse !== undefined && p.landuse !== '') || (p.natural !== undefined && p.natural !== '')) ?? false"
|
"_is_part_of_landuse=feat.get('parent_ways')?.some(p => (p.landuse !== undefined && p.landuse !== '') || (p.natural !== undefined && p.natural !== '')) ?? false",
|
||||||
|
"_moveable=feat.get('_is_part_of_building') && !feat.get('_is_part_of_grb_building')"
|
||||||
],
|
],
|
||||||
"mapRendering": [
|
"mapRendering": [
|
||||||
{
|
{
|
||||||
"icon": "square:#00f",
|
"icon": "square:#cc0",
|
||||||
"iconSize": "5,5,center",
|
"iconSize": "5,5,center",
|
||||||
"location": "point"
|
"location": ["point"]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"passAllFeatures": true
|
"passAllFeatures": true
|
||||||
|
@ -469,7 +471,7 @@
|
||||||
"tagRenderings": [
|
"tagRenderings": [
|
||||||
{
|
{
|
||||||
"id": "Import-button",
|
"id": "Import-button",
|
||||||
"render": "{import_button(OSM-buildings,building=$building;man_made=$man_made; source:geometry:date=$_grb_date; source:geometry:ref=$_grb_ref; addr:street=$addr:street; addr:housenumber=$addr:housenumber; building:min_level=$_building:min_level, Upload this building to OpenStreetMap)}",
|
"render": "{import_way_button(OSM-buildings,building=$building;man_made=$man_made; source:geometry:date=$_grb_date; source:geometry:ref=$_grb_ref; addr:street=$addr:street; addr:housenumber=$addr:housenumber; building:min_level=$_building:min_level, Upload this building to OpenStreetMap,,_is_part_of_building=true,1,_moveable=true)}",
|
||||||
"mappings": [
|
"mappings": [
|
||||||
{
|
{
|
||||||
"if": {
|
"if": {
|
||||||
|
@ -513,9 +515,12 @@
|
||||||
{
|
{
|
||||||
"if": "_osm_obj:addr:housenumber~*",
|
"if": "_osm_obj:addr:housenumber~*",
|
||||||
"then": "The overlapping building only has a housenumber known: {_osm_obj:addr:housenumber}"
|
"then": "The overlapping building only has a housenumber known: {_osm_obj:addr:housenumber}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"if": "_osm_obj:id=",
|
||||||
|
"then": "No overlapping OpenStreetMap-building found"
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
"conditon": "_osm_obj:id~*"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "grb_address_diff",
|
"id": "grb_address_diff",
|
||||||
|
|
|
@ -2755,9 +2755,6 @@
|
||||||
"4": {
|
"4": {
|
||||||
"then": "A door which rolls from overhead, typically seen for garages"
|
"then": "A door which rolls from overhead, typically seen for garages"
|
||||||
},
|
},
|
||||||
"5": {
|
|
||||||
"then": "This is an entrance without a physical door"
|
|
||||||
},
|
|
||||||
"5": {
|
"5": {
|
||||||
"then": "This is an entrance without a physical door"
|
"then": "This is an entrance without a physical door"
|
||||||
}
|
}
|
||||||
|
|
|
@ -2730,9 +2730,6 @@
|
||||||
"3": {
|
"3": {
|
||||||
"then": "Een schuifdeur or roldeur die bij het openen en sluiten zijwaarts beweegt"
|
"then": "Een schuifdeur or roldeur die bij het openen en sluiten zijwaarts beweegt"
|
||||||
},
|
},
|
||||||
"4": {
|
|
||||||
"then": "Een poort die langs boven dichtrolt, typisch voor garages"
|
|
||||||
},
|
|
||||||
"4": {
|
"4": {
|
||||||
"then": "Een poort die langs boven dichtrolt, typisch voor garages"
|
"then": "Een poort die langs boven dichtrolt, typisch voor garages"
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue