diff --git a/Logic/Osm/Actions/ChangeLocationAction.ts b/Logic/Osm/Actions/ChangeLocationAction.ts new file mode 100644 index 000000000..56234e67e --- /dev/null +++ b/Logic/Osm/Actions/ChangeLocationAction.ts @@ -0,0 +1,17 @@ +import {ChangeDescription} from "./ChangeDescription"; +import OsmChangeAction from "./OsmChangeAction"; +import {Changes} from "../Changes"; + +export default class ChangeLocationAction extends OsmChangeAction { + constructor(id: string, newLonLat: [number, number], meta: { + theme: string, + reason: string + }) { + super(); + throw "TODO" + } + + protected CreateChangeDescriptions(changes: Changes): Promise { + return Promise.resolve([]); + } +} \ No newline at end of file diff --git a/UI/BigComponents/SimpleAddUI.ts b/UI/BigComponents/SimpleAddUI.ts index ad6a73ec5..3c39266b9 100644 --- a/UI/BigComponents/SimpleAddUI.ts +++ b/UI/BigComponents/SimpleAddUI.ts @@ -150,6 +150,7 @@ export default class SimpleAddUI extends Toggle { maxSnapDistance: preset.preciseInput.maxSnapDistance, bounds: mapBounds }) + preciseInput.installBounds(0.15, true) preciseInput.SetClass("h-32 rounded-xl overflow-hidden border border-gray").SetStyle("height: 12rem;") diff --git a/UI/Input/LocationInput.ts b/UI/Input/LocationInput.ts index 7e167a74e..53715562b 100644 --- a/UI/Input/LocationInput.ts +++ b/UI/Input/LocationInput.ts @@ -43,9 +43,13 @@ export default class LocationInput extends InputElement implements MinimapO private readonly _bounds: UIEventSource; public readonly _matching_layer: LayerConfig; private readonly map: BaseUIElement & MinimapObj; + public readonly leafletMap: UIEventSource + private readonly clickLocation: UIEventSource; + private readonly _minZoom: number; constructor(options: { + minZoom?: number, mapBackground?: UIEventSource, snapTo?: UIEventSource<{ feature: any }[]>, maxSnapDistance?: number, @@ -60,6 +64,7 @@ export default class LocationInput extends InputElement implements MinimapO this._centerLocation = options.centerLocation; this._snappedPointTags = options.snappedPointTags this._bounds = options.bounds; + this._minZoom = options.minZoom if (this._snapTo === undefined) { this._value = this._centerLocation; } else { @@ -142,6 +147,7 @@ export default class LocationInput extends InputElement implements MinimapO bounds: this._bounds } ) + this.leafletMap = this.map.leafletMap } GetValue(): UIEventSource { @@ -154,10 +160,9 @@ export default class LocationInput extends InputElement implements MinimapO protected InnerConstructElement(): HTMLElement { try { + const self = this; this.clickLocation.addCallbackAndRunD(location => this._centerLocation.setData(location)) - this.map.installBounds(0.15, true); - - if (this._snapTo !== undefined) { + if (this._snapTo !== undefined) { // Show the lines to snap to new ShowDataMultiLayer({ @@ -191,7 +196,7 @@ export default class LocationInput extends InputElement implements MinimapO } leaflet.setMaxZoom(layer.max_zoom) - leaflet.setMinZoom(layer.max_zoom - 2) + leaflet.setMinZoom(self._minZoom ?? layer.max_zoom - 2) leaflet.setZoom(layer.max_zoom - 1) }, [this.map.leafletMap]) @@ -223,8 +228,7 @@ export default class LocationInput extends InputElement implements MinimapO } } - readonly leafletMap: UIEventSource = this.map.leafletMap - + installBounds(factor: number | BBox, showRange?: boolean): void { this.map.installBounds(factor, showRange) } diff --git a/UI/Popup/MoveWizard.ts b/UI/Popup/MoveWizard.ts index f82e579ca..b59b56bc7 100644 --- a/UI/Popup/MoveWizard.ts +++ b/UI/Popup/MoveWizard.ts @@ -11,45 +11,47 @@ import BaseUIElement from "../BaseUIElement"; import LocationInput from "../Input/LocationInput"; import Loc from "../../Models/Loc"; import {GeoOperations} from "../../Logic/GeoOperations"; +import {OsmObject} from "../../Logic/Osm/OsmObject"; +import {Changes} from "../../Logic/Osm/Changes"; +import ChangeLocationAction from "../../Logic/Osm/Actions/ChangeLocationAction"; +import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; -interface MoveReason {text: Translation | string, +interface MoveReason { + text: Translation | string, icon: string | BaseUIElement, changesetCommentValue: string, lockBounds: true | boolean, background: undefined | "map" | "photo", - startZoom: number} + startZoom: number, + minZoom: number +} export default class MoveWizard extends Toggle { /** * The UI-element which helps moving a point */ constructor( - featureToMove : any, + featureToMove: any, state: { - osmConnection: OsmConnection, - featureSwitchUserbadge: UIEventSource - },options?: { - reasons?: {text: Translation | string, - icon: string | BaseUIElement, - changesetCommentValue: string, - lockBounds?: true | boolean, - background?: undefined | "map" | "photo", - startZoom?: number | 15 - }[] - disableDefaultReasons?: false | boolean - - }) { - //State.state.featureSwitchUserbadge - // state = State.state + osmConnection: OsmConnection, + featureSwitchUserbadge: UIEventSource, + changes: Changes, + layoutToUse: LayoutConfig + }, options?: { + reasons?: MoveReason[] + disableDefaultReasons?: false | boolean + + }) { options = options ?? {} - const t = Translations.t.move + + const t = Translations.t.move const loginButton = new Toggle( - t.loginToMove.Clone() .SetClass("btn").onClick(() => state.osmConnection.AttemptLogin()), + t.loginToMove.Clone().SetClass("btn").onClick(() => state.osmConnection.AttemptLogin()), undefined, state.featureSwitchUserbadge ) - - const currentStep = new UIEventSource<"start" | "reason" | "pick_location">("start") + + const currentStep = new UIEventSource<"start" | "reason" | "pick_location" | "moved">("start") const moveReason = new UIEventSource(undefined) const moveButton = new SubtleButton( Svg.move_ui(), @@ -57,26 +59,35 @@ export default class MoveWizard extends Toggle { ).onClick(() => { currentStep.setData("reason") }) - - - const reasons : MoveReason[] = [] - if(!options.disableDefaultReasons){ + + const moveAgainButton = new SubtleButton( + Svg.move_ui(), + t.inviteToMoveAgain.Clone() + ).onClick(() => { + currentStep.setData("reason") + }) + + + const reasons: MoveReason[] = [] + if (!options.disableDefaultReasons) { reasons.push({ text: t.reasonRelocation.Clone(), icon: Svg.relocation_svg(), changesetCommentValue: "relocated", lockBounds: false, background: undefined, - startZoom: 12 + startZoom: 12, + minZoom: 6 }) - + reasons.push({ text: t.reasonInaccurate.Clone(), icon: Svg.crosshair_svg(), changesetCommentValue: "improve_accuracy", lockBounds: true, background: "photo", - startZoom: 17 + startZoom: 17, + minZoom: 16 }) } for (const reason of options.reasons ?? []) { @@ -86,46 +97,112 @@ export default class MoveWizard extends Toggle { changesetCommentValue: reason.changesetCommentValue, lockBounds: reason.lockBounds ?? true, background: reason.background, - startZoom: reason.startZoom ?? 15 + startZoom: reason.startZoom ?? 15, + minZoom: reason.minZoom }) } - + const selectReason = new Combine(reasons.map(r => new SubtleButton(r.icon, r.text).onClick(() => { moveReason.setData(r) currentStep.setData("pick_location") }))) - + const cancelButton = new SubtleButton(Svg.close_svg(), t.cancel).onClick(() => currentStep.setData("start")) - - - + + const [lon, lat] = GeoOperations.centerpointCoordinates(featureToMove) - const locationInput = new LocationInput({ - centerLocation: new UIEventSource({ + const locationInput = moveReason.map(reason => { + if (reason === undefined) { + return undefined + } + const loc = new UIEventSource({ lon: lon, lat: lat, - zoom: moveReason.data.startZoom - }), - - }) - locationInput.SetStyle("height: 25rem") - super( + zoom: reason?.startZoom ?? 16 + }) + + + const locationInput = new LocationInput({ + minZoom: reason.minZoom, + centerLocation: loc + }) + + if (reason.lockBounds) { + locationInput.installBounds(0.05, true) + } + + locationInput.SetStyle("height: 17.5rem") + + const confirmMove = new SubtleButton(Svg.move_confirm_svg(), t.confirmMove) + confirmMove.onClick(() => { + state.changes.applyAction(new ChangeLocationAction(featureToMove.properties.id, [locationInput.GetValue().data.lon, locationInput.GetValue().data.lat], { + reason: Translations.WT(reason.text).textFor("en"), + theme: state.layoutToUse.icon + })) + currentStep.setData("moved") + }) + const zoomInFurhter = t.zoomInFurther.Clone().SetClass("alert block m-6") + return new Combine([ + locationInput, + new Toggle(confirmMove, zoomInFurhter, locationInput.GetValue().map(l => l.zoom >= 19)) + ]).SetClass("flex flex-col") + }); + + const dialogClasses = "p-2 md:p-4 m-2 border border-gray-400 rounded-xl flex flex-col" + + const moveFlow = new Toggle( new VariableUiElement(currentStep.map(currentStep => { - switch (currentStep){ + switch (currentStep) { case "start": return moveButton; case "reason": - return new Combine([selectReason, cancelButton]); + return new Combine([t.whyMove.Clone().SetClass("text-lg font-bold"), selectReason, cancelButton]).SetClass(dialogClasses); case "pick_location": - return new Combine([locationInput]) + return new Combine([t.moveTitle.Clone().SetClass("text-lg font-bold"), new VariableUiElement(locationInput), cancelButton]).SetClass(dialogClasses) + case "moved": + return new Combine([t.pointIsMoved.Clone().SetClass("thanks"), moveAgainButton]).SetClass("flex flex-col"); + } - - - - + + })), loginButton, state.osmConnection.isLoggedIn ) + let id = featureToMove.properties.id + const backend = state.osmConnection._oauth_config.url + if (id.startsWith(backend)) { + id = id.substring(backend.length) + } + + const moveDisallowedReason = new UIEventSource(undefined) + if (id.startsWith("way")) { + moveDisallowedReason.setData(t.isWay.Clone()) + } else if (id.startsWith("relation")) { + moveDisallowedReason.setData(t.isRelation.Clone()) + } else { + + OsmObject.DownloadReferencingWays(id).then(referencing => { + if (referencing.length > 0) { + console.log("Got a referencing way, move not allowed") + moveDisallowedReason.setData(t.partOfAWay.Clone()) + } + }) + OsmObject.DownloadReferencingRelations(id).then(partOf => { + if(partOf.length > 0){ + moveDisallowedReason.setData(t.partOfRelation.Clone()) + } + }) + } + super( + moveFlow, + new Combine([ + Svg.move_not_allowed_svg().SetStyle("height: 2rem"), + new Combine([t.cannotBeMoved.Clone(), + new VariableUiElement(moveDisallowedReason).SetClass("subtle") + ]).SetClass("flex flex-col") + ]).SetClass("flex"), + moveDisallowedReason.map(r => r === undefined) + ) } } \ No newline at end of file diff --git a/assets/svg/license_info.json b/assets/svg/license_info.json index cfbbfc32a..ce03d2a3e 100644 --- a/assets/svg/license_info.json +++ b/assets/svg/license_info.json @@ -965,6 +965,22 @@ "https://commons.wikimedia.org/wiki/File:Move_icon.svg" ] }, + { + "path": "move_confirm.svg", + "license": "CC0", + "authors": [ + "Pieter Vander Vennet" + ], + "sources": [] + }, + { + "path": "move_not_allowed.svg", + "license": "CC0", + "authors": [ + "Pieter Vander Vennet" + ], + "sources": [] + }, { "path": "no_checkmark.svg", "license": "CC0; trivial", diff --git a/assets/svg/move_confirm.svg b/assets/svg/move_confirm.svg new file mode 100644 index 000000000..5aa8ac888 --- /dev/null +++ b/assets/svg/move_confirm.svg @@ -0,0 +1,281 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + move + + + + + + + diff --git a/assets/svg/move_not_allowed.svg b/assets/svg/move_not_allowed.svg new file mode 100644 index 000000000..1abd15a03 --- /dev/null +++ b/assets/svg/move_not_allowed.svg @@ -0,0 +1,285 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + move + + + + + + + + diff --git a/css/index-tailwind-output.css b/css/index-tailwind-output.css index 054952756..e0b8bedad 100644 --- a/css/index-tailwind-output.css +++ b/css/index-tailwind-output.css @@ -820,6 +820,10 @@ video { margin: 0.75rem; } +.m-6 { + margin: 1.5rem; +} + .m-4 { margin: 1rem; } @@ -892,6 +896,10 @@ video { margin-left: 0.5rem; } +.mb-4 { + margin-bottom: 1rem; +} + .mt-4 { margin-top: 1rem; } @@ -912,10 +920,6 @@ video { margin-bottom: 0.25rem; } -.mb-4 { - margin-bottom: 1rem; -} - .box-border { box-sizing: border-box; } @@ -2365,6 +2369,10 @@ li::marker { padding: 0.5rem; } + .md\:p-4 { + padding: 1rem; + } + .md\:pt-4 { padding-top: 1rem; } diff --git a/langs/en.json b/langs/en.json index 78a21bdcf..2924b1e63 100644 --- a/langs/en.json +++ b/langs/en.json @@ -250,11 +250,20 @@ "move": { "loginToMove": "You must be logged in to move a point", "inviteToMove": "Move this point", + "inviteToMoveAgain": "Move this point again", + "moveTitle": "Move this point", + "whyMove": "Why do you want to move this point?", + "confirmMove": "Move here", + "pointIsMoved": "The point has been moved", + "zoomInFurther": "Zoom in further to confirm this move", "selectReason": "Why do you move this object?", "reasonRelocation": "The object has been relocated to a totally different location", "reasonInaccurate": "The location of this object is inaccurate and should be moved a few meter", - "onlyPoints": "Only points can be moved", - "isWay": "This feature is a way", + "cannotBeMoved": "This feature cannot be moved.", + "isWay": "This feature is a way. Use another OpenStreetMap editor to move it.", + "isRelation": "This feature is a relation and can not be moved", + "partOfAWay": "This feature is part of another way. Use another editor to move it", + "partOfRelation": "This feature is part of a relation. Use another editor to move it", "cancel": "Cancel move" } } \ No newline at end of file diff --git a/test.ts b/test.ts index e9b3388c9..8b7d6c065 100644 --- a/test.ts +++ b/test.ts @@ -7,7 +7,9 @@ import MinimapImplementation from "./UI/Base/MinimapImplementation"; State.state = new State(AllKnownLayouts.allKnownLayouts.get("bookcases")) const feature = { "type": "Feature", - "properties": {}, + "properties": { + id: "node/14925464" + }, "geometry": { "type": "Point", "coordinates": [