diff --git a/Logic/FeatureSource/Sources/NewGeometryFromChangesFeatureSource.ts b/Logic/FeatureSource/Sources/NewGeometryFromChangesFeatureSource.ts index b7347b8ac..1c7ab3f39 100644 --- a/Logic/FeatureSource/Sources/NewGeometryFromChangesFeatureSource.ts +++ b/Logic/FeatureSource/Sources/NewGeometryFromChangesFeatureSource.ts @@ -42,7 +42,6 @@ export class NewGeometryFromChangesFeatureSource implements FeatureSource { feature: feature, freshness: now }) - console.warn("Added a new feature: ", JSON.stringify(feature)) } for (const change of changes) { diff --git a/Logic/Osm/Actions/CreateMultiPolygonWithPointReuseAction.ts b/Logic/Osm/Actions/CreateMultiPolygonWithPointReuseAction.ts new file mode 100644 index 000000000..4723a2b11 --- /dev/null +++ b/Logic/Osm/Actions/CreateMultiPolygonWithPointReuseAction.ts @@ -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 { + const outerPreview = await this.createOuterWay.getPreview() + outerPreview.features.data.push({ + freshness: new Date(), + feature: this.geojsonPreview + }) + return outerPreview + } + + protected async CreateChangeDescriptions(changes: Changes): Promise { + 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 + } + + +} \ No newline at end of file diff --git a/Logic/Osm/Actions/CreateNewNodeAction.ts b/Logic/Osm/Actions/CreateNewNodeAction.ts index 304db374f..569ffa761 100644 --- a/Logic/Osm/Actions/CreateNewNodeAction.ts +++ b/Logic/Osm/Actions/CreateNewNodeAction.ts @@ -1,12 +1,12 @@ import {Tag} from "../../Tags/Tag"; -import OsmChangeAction from "./OsmChangeAction"; +import OsmChangeAction, {OsmCreateAction} from "./OsmChangeAction"; import {Changes} from "../Changes"; import {ChangeDescription} from "./ChangeDescription"; import {And} from "../../Tags/And"; import {OsmWay} from "../OsmObject"; 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 @@ -121,7 +121,6 @@ export default class CreateNewNodeAction extends OsmChangeAction { reusedPointId = this._snapOnto.nodes[index + 1] } if (reusedPointId !== undefined) { - console.log("Reusing an existing point:", reusedPointId) this.setElementId(reusedPointId) return [{ tags: new And(this._basicTags).asChange(properties), @@ -133,7 +132,6 @@ export default class CreateNewNodeAction extends OsmChangeAction { const locations = [...this._snapOnto.coordinates] locations.forEach(coor => coor.reverse()) - console.log("Locations are: ", locations) const ids = [...this._snapOnto.nodes] locations.splice(index + 1, 0, [this._lon, this._lat]) diff --git a/Logic/Osm/Actions/CreateNewWayAction.ts b/Logic/Osm/Actions/CreateNewWayAction.ts index ef10d417f..ad8d2a2a3 100644 --- a/Logic/Osm/Actions/CreateNewWayAction.ts +++ b/Logic/Osm/Actions/CreateNewWayAction.ts @@ -1,12 +1,13 @@ import {ChangeDescription} from "./ChangeDescription"; -import OsmChangeAction from "./OsmChangeAction"; +import {OsmCreateAction} from "./OsmChangeAction"; import {Changes} from "../Changes"; import {Tag} from "../../Tags/Tag"; import CreateNewNodeAction from "./CreateNewNodeAction"; import {And} from "../../Tags/And"; -export default class CreateNewWayAction extends OsmChangeAction { +export default class CreateNewWayAction extends OsmCreateAction { public newElementId: string = undefined + public newElementIdNumber: number = undefined; private readonly coordinates: ({ nodeId?: number, lat: number, lon: number })[]; private readonly tags: Tag[]; private readonly _options: { @@ -55,7 +56,7 @@ export default class CreateNewWayAction extends OsmChangeAction { const id = changes.getNewID() - + this.newElementIdNumber = id const newWay = { id, type: "way", diff --git a/Logic/Osm/Actions/CreateWayWithPointReuseAction.ts b/Logic/Osm/Actions/CreateWayWithPointReuseAction.ts index 2d81fa80f..66ec02785 100644 --- a/Logic/Osm/Actions/CreateWayWithPointReuseAction.ts +++ b/Logic/Osm/Actions/CreateWayWithPointReuseAction.ts @@ -1,4 +1,4 @@ -import OsmChangeAction from "./OsmChangeAction"; +import OsmChangeAction, {OsmCreateAction} from "./OsmChangeAction"; import {Tag} from "../../Tags/Tag"; import {Changes} from "../Changes"; 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 */ -export default class CreateWayWithPointReuseAction extends OsmChangeAction { +export default class CreateWayWithPointReuseAction extends OsmCreateAction { private readonly _tags: Tag[]; /** * lngLat-coordinates @@ -40,6 +40,9 @@ export default class CreateWayWithPointReuseAction extends OsmChangeAction { private _coordinateInfo: CoordinateInfo[]; private _state: FeaturePipelineState; private _config: MergePointConfig[]; + + public newElementId: string = undefined; + public newElementIdNumber: number = undefined constructor(tags: Tag[], coordinates: [number, number][], @@ -87,7 +90,8 @@ export default class CreateWayWithPointReuseAction extends OsmChangeAction { properties: { "move": "yes", "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: { type: "LineString", @@ -97,8 +101,23 @@ export default class CreateWayWithPointReuseAction extends OsmChangeAction { features.push(moveDescription) } else { - // The geometry is moved + // The geometry is moved, the point is reused 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) } - console.log("Preview:", features) return new StaticFeatureSource(features, false) } - protected async CreateChangeDescriptions(changes: Changes): Promise { + public async CreateChangeDescriptions(changes: Changes): Promise { const theme = this._state.layoutToUse.id const allChanges: ChangeDescription[] = [] const nodeIdsToUse: { lat: number, lon: number, nodeId?: number }[] = [] @@ -196,6 +214,8 @@ export default class CreateWayWithPointReuseAction extends OsmChangeAction { }) allChanges.push(...(await newWay.CreateChangeDescriptions(changes))) + this.newElementId = newWay.newElementId + this.newElementIdNumber = newWay.newElementIdNumber return allChanges } diff --git a/Logic/Osm/Actions/OsmChangeAction.ts b/Logic/Osm/Actions/OsmChangeAction.ts index 13a31a76a..dc1a8e591 100644 --- a/Logic/Osm/Actions/OsmChangeAction.ts +++ b/Logic/Osm/Actions/OsmChangeAction.ts @@ -11,7 +11,7 @@ export default abstract class OsmChangeAction { public readonly trackStatistics: boolean; /** * 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 */ public readonly mainObjectId: string; @@ -30,4 +30,11 @@ export default abstract class OsmChangeAction { } protected abstract CreateChangeDescriptions(changes: Changes): Promise -} \ No newline at end of file +} + +export abstract class OsmCreateAction extends OsmChangeAction{ + + public newElementId : string + public newElementIdNumber: number + +} diff --git a/Logic/Tags/Tag.ts b/Logic/Tags/Tag.ts index f2539e1bc..a6f4fa91c 100644 --- a/Logic/Tags/Tag.ts +++ b/Logic/Tags/Tag.ts @@ -46,6 +46,9 @@ export class Tag extends TagsFilter { if (shorten) { v = Utils.EllipsesAfter(v, 25); } + if(v === "" || v === undefined){ + return ""+this.key+"" + } if (linkToWiki) { return `${this.key}` + `=` + diff --git a/Logic/Tags/TagsFilter.ts b/Logic/Tags/TagsFilter.ts index da8fa94c1..cffaf99ca 100644 --- a/Logic/Tags/TagsFilter.ts +++ b/Logic/Tags/TagsFilter.ts @@ -8,7 +8,7 @@ export abstract class TagsFilter { 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[]; diff --git a/Models/Constants.ts b/Models/Constants.ts index 729d27f61..7147d4af1 100644 --- a/Models/Constants.ts +++ b/Models/Constants.ts @@ -2,7 +2,7 @@ import {Utils} from "../Utils"; 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 readonly mapillary_client_token_v4 = "MLY|4441509239301885|b40ad2d3ea105435bd40c7e76993ae85" diff --git a/Models/ThemeConfig/Json/LayerConfigJson.ts b/Models/ThemeConfig/Json/LayerConfigJson.ts index 796ef91fc..06c94304f 100644 --- a/Models/ThemeConfig/Json/LayerConfigJson.ts +++ b/Models/ThemeConfig/Json/LayerConfigJson.ts @@ -136,7 +136,7 @@ export interface LayerConfigJson { 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. diff --git a/UI/Popup/ImportButton.ts b/UI/Popup/ImportButton.ts index 10f297f7d..53567668c 100644 --- a/UI/Popup/ImportButton.ts +++ b/UI/Popup/ImportButton.ts @@ -7,17 +7,11 @@ import Translations from "../i18n/Translations"; import Constants from "../../Models/Constants"; import Toggle from "../Input/Toggle"; import CreateNewNodeAction from "../../Logic/Osm/Actions/CreateNewNodeAction"; -import {Tag} from "../../Logic/Tags/Tag"; import Loading from "../Base/Loading"; -import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; 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 ConfirmLocationOfPoint from "../NewPoint/ConfirmLocationOfPoint"; import Img from "../Base/Img"; -import {Translation} from "../i18n/Translation"; import FilteredLayer from "../../Models/FilteredLayer"; import SpecialVisualizations from "../SpecialVisualizations"; import {FixedUiElement} from "../Base/FixedUiElement"; @@ -28,68 +22,29 @@ import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"; import AllKnownLayers from "../../Customizations/AllKnownLayers"; import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"; 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 OsmChangeAction from "../../Logic/Osm/Actions/OsmChangeAction"; +import OsmChangeAction, {OsmCreateAction} from "../../Logic/Osm/Actions/OsmChangeAction"; import FeatureSource from "../../Logic/FeatureSource/FeatureSource"; import {OsmObject, OsmWay} from "../../Logic/Osm/OsmObject"; import FeaturePipelineState from "../../Logic/State/FeaturePipelineState"; import {DefaultGuiState} from "../DefaultGuiState"; import {PresetInfo} from "../BigComponents/SimpleAddUI"; - - -export interface ImportButtonState { - description?: Translation; - image: () => BaseUIElement, - message: string | BaseUIElement, - originalTags: UIEventSource, - newTags: UIEventSource, - targetLayer: FilteredLayer, - feature: any, - minZoom: number, - state: { - backgroundLayer: UIEventSource; - filteredLayers: UIEventSource; - featureSwitchUserbadge: UIEventSource; - featurePipeline: FeaturePipeline; - allElements: ElementStorage; - selectedElement: UIEventSource; - layoutToUse: LayoutConfig, - osmConnection: OsmConnection, - changes: Changes, - locationControl: UIEventSource<{ zoom: number }> - }, - guiState: { filterViewIsOpened: UIEventSource }, - - /** - * 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[] -} +import {TagUtils} from "../../Logic/Tags/TagUtils"; +import {And} from "../../Logic/Tags/And"; +import ReplaceGeometryAction from "../../Logic/Osm/Actions/ReplaceGeometryAction"; +import CreateMultiPolygonWithPointReuseAction from "../../Logic/Osm/Actions/CreateMultiPolygonWithPointReuseAction"; +import {Tag} from "../../Logic/Tags/Tag"; abstract class AbstractImportButton implements SpecialVisualizations { public readonly funcName: string public readonly docs: 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.showRemovedTags = showRemovedTags; 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_importRequirementDocs} - ` - this.args = [ { 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 }, - tagSource: UIEventSource, guiState: DefaultGuiState): BaseUIElement; + abstract constructElement(state: FeaturePipelineState, + args: { max_snap_distance: string, snap_onto_layers: string, icon: string, text: string, tags: string, newTags: UIEventSource, targetLayer: string }, + tagSource: UIEventSource, + guiState: DefaultGuiState, + feature: any, + onCancelClicked: () => void): BaseUIElement; + constr(state, tagSource, argsRaw, guiState) { - + const self = this; /** * Some generic import button pre-validation is implemented here: * - Are we logged in? @@ -144,7 +102,7 @@ ${Utils.special_visualizations_importRequirementDocs} const t = Translations.t.general.add.import; const t0 = Translations.t.general.add; - const args = this.parseArgs(argsRaw) + const args = this.parseArgs(argsRaw, tagSource) { // Some initial validation @@ -171,26 +129,22 @@ ${Utils.special_visualizations_importRequirementDocs} const id = tagSource.data.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 const newTags = SpecialVisualizations.generateTagsToApply(args.tags, tagSource) const appliedTags = new Toggle( new VariableUiElement( newTags.map(tgs => { - const parts = [] - for (const tag of tgs) { - parts.push(tag.key + "=" + tag.value) - } - const txt = parts.join(" & ") - return t0.presetInfo.Subs({tags: txt}).SetClass("subtle") - })), undefined, + const filteredTags = tgs.filter(tg => self.showRemovedTags || (tg.value ?? "") !== "") + const asText = new And(filteredTags) + .asHumanString(true, true, {}) + + return t0.presetInfo.Subs({tags: asText}).SetClass("subtle"); + })), + undefined, state.osmConnection.userDetails.map(ud => ud.csCount >= Constants.userJourney.tagsVisibleAt) ) - - + const importClicked = new UIEventSource(false); inviteToImportButton.onClick(() => { @@ -207,15 +161,17 @@ ${Utils.special_visualizations_importRequirementDocs} 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( new Toggle( - new Loading(t0.stillLoading), - new Combine([importGuidingPanel, appliedTags]).SetClass("flex flex-col"), - state.featurePipeline.runningQuery - ) , + new Loading(t0.stillLoading), + new Combine([importGuidingPanel, appliedTags]).SetClass("flex flex-col"), + state.featurePipeline.runningQuery + ), inviteToImportButton, 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 } { - return Utils.ParseVisArgs(this.args, argsRaw) + private parseArgs(argsRaw: string[], originalFeatureTags: UIEventSource): { minzoom: string, max_snap_distance: string, snap_onto_layers: string, icon: string, text: string, tags: string, targetLayer: string, newTags: UIEventSource } { + const baseArgs = Utils.ParseVisArgs(this.args, argsRaw) + if (originalFeatureTags !== undefined) { + baseArgs["newTags"] = SpecialVisualizations.generateTagsToApply(baseArgs.tags, originalFeatureTags) + } + return baseArgs } getLayerDependencies(argsRaw: string[]) { - const args = this.parseArgs(argsRaw) + const args = this.parseArgs(argsRaw, undefined) const dependsOnLayers: string[] = [] @@ -261,181 +221,31 @@ ${Utils.special_visualizations_importRequirementDocs} 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, - importClicked: UIEventSource) { - 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, - importClicked: UIEventSource): 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 })[] = [] - - 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, - importClicked: UIEventSource): BaseUIElement { + protected createConfirmPanelForWay( + state: FeaturePipelineState, + args: { max_snap_distance: string, snap_onto_layers: string, icon: string, text: string, newTags: UIEventSource, targetLayer: string }, + feature: any, + originalFeatureTags: UIEventSource, + action: (OsmChangeAction & { getPreview(): Promise, newElementId?: string }), + onCancel: () => void): BaseUIElement { + const self = this; const confirmationMap = Minimap.createMiniMap({ allowMoving: false, - background: o.state.backgroundLayer + background: state.backgroundLayer }) 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 new ShowDataMultiLayer({ leafletMap: confirmationMap.leafletMap, enablePopups: false, zoomToFeatures: true, - features: new StaticFeatureSource(relevantFeatures, false), - allElements: o.state.allElements, - layers: o.state.filteredLayers + features: new StaticFeatureSource([feature], false), + allElements: state.allElements, + layers: state.filteredLayers }) - let action: OsmChangeAction & { getPreview(): Promise } - - const changes = o.state.changes - let confirm: () => Promise - 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 => { new ShowDataLayer({ @@ -443,89 +253,328 @@ export default class ImportButton { enablePopups: false, zoomToFeatures: false, features: changePreview, - allElements: o.state.allElements, + allElements: state.allElements, layerToShow: AllKnownLayers.sharedLayers.get("conflation") }) }) - const tagsExplanation = new VariableUiElement(o.newTags.map(tagsToApply => { - const tagsStr = tagsToApply.map(t => t.asHumanString(false, true)).join("&"); + const tagsExplanation = new VariableUiElement(args.newTags.map(tagsToApply => { + 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}); } )).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 () => { { - if (isImported.data) { - return - } - o.originalTags.data["_imported"] = "yes" - o.originalTags.ping() // will set isImported as per its definition - - const idToSelect = await confirm() - - o.state.selectedElement.setData(o.state.allElements.ContainingFeatures.get(idToSelect)) + originalFeatureTags.data["_imported"] = "yes" + originalFeatureTags.ping() // will set isImported as per its definition + state.changes.applyAction(action) + state.selectedElement.setData(state.allElements.ContainingFeatures.get(action.newElementId ?? action.mainObjectId)) } }) - 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") } +} - public static createConfirmPanelForPoint( - o: ImportButtonState, - isImported: UIEventSource, - importClicked: UIEventSource): BaseUIElement { +export class ConflateButton extends AbstractImportButton { + + 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" + }] + ); + } + + 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; targetLayer: string }, + tagSource: UIEventSource, 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 }; + + 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, targetLayer: string }, + state: FeaturePipelineState, + guiState: DefaultGuiState, + originalFeatureTags: UIEventSource, + feature: any, + onCancel: () => void): BaseUIElement { async function confirm(tags: any[], location: { lat: number, lon: number }, snapOntoWayId: string) { - if (isImported.data) { - return - } - o.originalTags.data["_imported"] = "yes" - o.originalTags.ping() // will set isImported as per its definition + originalFeatureTags.data["_imported"] = "yes" + originalFeatureTags.ping() // will set isImported as per its definition let snapOnto: OsmObject = undefined if (snapOntoWayId !== undefined) { snapOnto = await OsmObject.DownloadObjectAsync(snapOntoWayId) } const newElementAction = new CreateNewNodeAction(tags, location.lat, location.lon, { - theme: o.state.layoutToUse.id, + theme: state.layoutToUse.id, changeType: "import", snapOnto: snapOnto }) - await o.state.changes.applyAction(newElementAction) - o.state.selectedElement.setData(o.state.allElements.ContainingFeatures.get( + await state.changes.applyAction(newElementAction) + state.selectedElement.setData(state.allElements.ContainingFeatures.get( newElementAction.newElementId )) } - function cancel() { - importClicked.setData(false) - } - const presetInfo = { - tags: o.newTags.data, - icon: o.image, - description: o.description, - layerToAddTo: o.targetLayer, - name: o.message, - title: o.message, + tags: args.newTags.data, + icon: () => new Img(args.icon), + description: Translations.WT(args.text), + layerToAddTo: state.filteredLayers.data.filter(l => l.layerDef.id === args.targetLayer)[0], + name: args.text, + title: Translations.WT(args.text), preciseInput: { - snapToLayers: o.snapSettings?.snapToLayers, - maxSnapDistance: o.snapSettings?.snapToLayersMaxDist + snapToLayers: args.snap_onto_layers?.split(";"), + maxSnapDistance: Number(args.max_snap_distance) } } - const [lon, lat] = o.feature.geometry.coordinates - return new ConfirmLocationOfPoint(o.state, o.guiState.filterViewIsOpened, presetInfo, Translations.W(o.message), { + const [lon, lat] = feature.geometry.coordinates + return new ConfirmLocationOfPoint(state, guiState.filterViewIsOpened, presetInfo, Translations.W(args.text), { lon, 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") } diff --git a/UI/SpecialVisualizations.ts b/UI/SpecialVisualizations.ts index 08adfedd9..7ffdae034 100644 --- a/UI/SpecialVisualizations.ts +++ b/UI/SpecialVisualizations.ts @@ -20,7 +20,6 @@ import Histogram from "./BigComponents/Histogram"; import Loc from "../Models/Loc"; import {Utils} from "../Utils"; import LayerConfig from "../Models/ThemeConfig/LayerConfig"; -import {ImportButtonSpecialViz} from "./BigComponents/ImportButton"; import {Tag} from "../Logic/Tags/Tag"; import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSource"; import ShowDataMultiLayer from "./ShowDataLayer/ShowDataMultiLayer"; @@ -39,6 +38,7 @@ import {DefaultGuiState} from "./DefaultGuiState"; import {GeoOperations} from "../Logic/GeoOperations"; import Hash from "../Logic/Web/Hash"; import FeaturePipelineState from "../Logic/State/FeaturePipelineState"; +import {ConflateButton, ImportPointButton, ImportWayButton} from "./Popup/ImportButton"; export interface SpecialVisualization { funcName: string, @@ -478,8 +478,9 @@ export default class SpecialVisualizations { ) } }, - new ImportButtonSpecialViz(), - + new ImportPointButton(), + new ImportWayButton(), + new ConflateButton(), { 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", diff --git a/assets/layers/conflation/conflation.json b/assets/layers/conflation/conflation.json index 75220077e..41ceed600 100644 --- a/assets/layers/conflation/conflation.json +++ b/assets/layers/conflation/conflation.json @@ -1,6 +1,6 @@ { "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, "source": { "osmTags": { @@ -20,16 +20,36 @@ }, { "location": "end", - "icon": "circle:#0f0", + "icon": { + "render": "circle:#0f0", + "mappings":[ { + "if": "move=no", + "then": "ring:#0f0" + }] + }, "iconSize": "10,10,center" }, { "location": "start", "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", "dasharray": { "render": "", diff --git a/assets/layers/entrance/entrance.json b/assets/layers/entrance/entrance.json index 354970edf..aaf36e0d1 100644 --- a/assets/layers/entrance/entrance.json +++ b/assets/layers/entrance/entrance.json @@ -155,14 +155,14 @@ { "if": "door=sliding", "then": { - "en": "A door which rolls from overhead, typically seen for garages", - "nl": "Een poort die langs boven dichtrolt, typisch voor garages" + "en": "A sliding door where the door slides sidewards, typically parallel with a wall", + "nl": "Een schuifdeur or roldeur die bij het openen en sluiten zijwaarts beweegt" } }, { "if": "door=overhead", "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" } }, diff --git a/assets/themes/grb_import/grb.json b/assets/themes/grb_import/grb.json index 73371a91f..a072e5e3a 100644 --- a/assets/themes/grb_import/grb.json +++ b/assets/themes/grb_import/grb.json @@ -34,15 +34,17 @@ "override": { "calculatedTags": [ "_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_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": [ { - "icon": "square:#00f", + "icon": "square:#cc0", "iconSize": "5,5,center", - "location": "point" + "location": ["point"] } ], "passAllFeatures": true @@ -469,7 +471,7 @@ "tagRenderings": [ { "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": [ { "if": { @@ -513,9 +515,12 @@ { "if": "_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", diff --git a/langs/layers/en.json b/langs/layers/en.json index 1ce141178..113699c54 100644 --- a/langs/layers/en.json +++ b/langs/layers/en.json @@ -2755,9 +2755,6 @@ "4": { "then": "A door which rolls from overhead, typically seen for garages" }, - "5": { - "then": "This is an entrance without a physical door" - }, "5": { "then": "This is an entrance without a physical door" } diff --git a/langs/layers/nl.json b/langs/layers/nl.json index 20e4f54a8..03a8e7a83 100644 --- a/langs/layers/nl.json +++ b/langs/layers/nl.json @@ -2730,9 +2730,6 @@ "3": { "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": { "then": "Een poort die langs boven dichtrolt, typisch voor garages" }