import { SubtleButton } from "../Base/SubtleButton" import Combine from "../Base/Combine" import Svg from "../../Svg" import Toggle from "../Input/Toggle" import { UIEventSource } from "../../Logic/UIEventSource" import Translations from "../i18n/Translations" import { VariableUiElement } from "../Base/VariableUIElement" import { Translation } from "../i18n/Translation" import BaseUIElement from "../BaseUIElement" import { GeoOperations } from "../../Logic/GeoOperations" import ChangeLocationAction from "../../Logic/Osm/Actions/ChangeLocationAction" import MoveConfig from "../../Models/ThemeConfig/MoveConfig" import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction" import { And } from "../../Logic/Tags/And" import { Tag } from "../../Logic/Tags/Tag" import { LoginToggle } from "./LoginButton" import { SpecialVisualizationState } from "../SpecialVisualization" import { Feature, Point } from "geojson" import { OsmTags } from "../../Models/OsmFeature" import SvelteUIElement from "../Base/SvelteUIElement" import { MapProperties } from "../../Models/MapProperties" import LocationInput from "../InputElement/Helpers/LocationInput.svelte" import Geosearch from "../BigComponents/Geosearch.svelte" import Constants from "../../Models/Constants" import OpenBackgroundSelectorButton from "../BigComponents/OpenBackgroundSelectorButton.svelte" interface MoveReason { text: Translation | string invitingText: Translation | string icon: BaseUIElement changesetCommentValue: string lockBounds: true | boolean includeSearch: false | boolean background: undefined | "map" | "photo" | string | string[] startZoom: number minZoom: number eraseAddressFields: false | boolean } export default class MoveWizard extends Toggle { /** * The UI-element which helps moving a point */ constructor( featureToMove: Feature, tags: UIEventSource, state: SpecialVisualizationState, options: MoveConfig ) { const t = Translations.t.move const reasons: MoveReason[] = [] if (options.enableRelocation) { reasons.push({ text: t.reasons.reasonRelocation, invitingText: t.inviteToMove.reasonRelocation, icon: Svg.relocation_svg(), changesetCommentValue: "relocated", lockBounds: false, background: undefined, includeSearch: true, startZoom: 12, minZoom: 6, eraseAddressFields: true, }) } if (options.enableImproveAccuracy) { reasons.push({ text: t.reasons.reasonInaccurate, invitingText: t.inviteToMove.reasonInaccurate, icon: Svg.crosshair_svg(), changesetCommentValue: "improve_accuracy", lockBounds: true, includeSearch: false, background: "photo", startZoom: 18, minZoom: 16, eraseAddressFields: false, }) } const currentStep = new UIEventSource<"start" | "reason" | "pick_location" | "moved">( "start" ) const moveReason = new UIEventSource(undefined) let moveButton: BaseUIElement if (reasons.length === 1) { const reason = reasons[0] moveReason.setData(reason) moveButton = new SubtleButton( reason.icon.SetStyle("height: 1.5rem; width: 1.5rem;"), Translations.T(reason.invitingText) ).onClick(() => { currentStep.setData("pick_location") }) } else { moveButton = new SubtleButton( Svg.move_svg().SetStyle("width: 1.5rem; height: 1.5rem"), t.inviteToMove.generic ).onClick(() => { currentStep.setData("reason") }) } const moveAgainButton = new SubtleButton(Svg.move_svg(), t.inviteToMoveAgain).onClick( () => { currentStep.setData("reason") } ) 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 = moveReason.map((reason) => { if (reason === undefined) { return undefined } const mapProperties: Partial = { minzoom: new UIEventSource(reason.minZoom), zoom: new UIEventSource(reason?.startZoom ?? 16), location: new UIEventSource({ lon, lat }), bounds: new UIEventSource(undefined), rasterLayer: state.mapProperties.rasterLayer, } const value = new UIEventSource<{ lon: number; lat: number }>(undefined) const locationInput = new Combine([ new SvelteUIElement(LocationInput, { mapProperties, value, initialCoordinate: { lon, lat }, }).SetClass("w-full h-full"), new SvelteUIElement(OpenBackgroundSelectorButton, { state }).SetClass( "absolute bottom-0 left-0" ), ]).SetClass("relative w-full h-full") let searchPanel: BaseUIElement = undefined if (reason.includeSearch) { searchPanel = new SvelteUIElement(Geosearch, { bounds: mapProperties.bounds, clearAfterView: false, }) } locationInput.SetStyle("height: 17.5rem") const confirmMove = new SubtleButton(Svg.move_confirm_svg(), t.confirmMove) confirmMove.onClick(async () => { const loc = value.data await state.changes.applyAction( new ChangeLocationAction(featureToMove.properties.id, [loc.lon, loc.lat], { reason: reason.changesetCommentValue, theme: state.layout.id, }) ) featureToMove.properties._lat = loc.lat featureToMove.properties._lon = loc.lon featureToMove.geometry.coordinates = [loc.lon, loc.lat] if (reason.eraseAddressFields) { await state.changes.applyAction( new ChangeTagAction( featureToMove.properties.id, new And([ new Tag("addr:housenumber", ""), new Tag("addr:street", ""), new Tag("addr:city", ""), new Tag("addr:postcode", ""), ]), featureToMove.properties, { changeType: "relocated", theme: state.layout.id, } ) ) } state.featureProperties.getStore(id).ping() currentStep.setData("moved") state.mapProperties.location.setData(loc) }) const zoomInFurhter = t.zoomInFurther.SetClass("alert block m-6") return new Combine([ searchPanel, locationInput, new Toggle( confirmMove, zoomInFurhter, mapProperties.zoom.map((zoom) => zoom >= Constants.minZoomLevelToAddNewPoint) ), ]).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 LoginToggle( new VariableUiElement( currentStep.map((currentStep) => { switch (currentStep) { case "start": return moveButton case "reason": return new Combine([ t.whyMove.SetClass("text-lg font-bold"), selectReason, cancelButton, ]).SetClass(dialogClasses) case "pick_location": return new Combine([ t.moveTitle.SetClass("text-lg font-bold"), new VariableUiElement(locationInput), cancelButton, ]).SetClass(dialogClasses) case "moved": return new Combine([ t.pointIsMoved.SetClass("thanks"), moveAgainButton, ]).SetClass("flex flex-col") } }) ), undefined, state ) 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) } else if (id.startsWith("relation")) { moveDisallowedReason.setData(t.isRelation) } else if (id.indexOf("-") < 0) { state.osmObjectDownloader.DownloadReferencingWays(id).then((referencing) => { if (referencing.length > 0) { console.log("Got a referencing way, move not allowed") moveDisallowedReason.setData(t.partOfAWay) } }) state.osmObjectDownloader.DownloadReferencingRelations(id).then((partOf) => { if (partOf.length > 0) { moveDisallowedReason.setData(t.partOfRelation) } }) } super( moveFlow, new Combine([ Svg.move_not_allowed_svg().SetStyle("height: 2rem").SetClass("m-2"), new Combine([ t.cannotBeMoved, new VariableUiElement(moveDisallowedReason).SetClass("subtle"), ]).SetClass("flex flex-col"), ]).SetClass("flex m-2 p-2 rounded-lg bg-gray-200"), moveDisallowedReason.map((r) => r === undefined) ) const self = this currentStep.addCallback((state) => { if (state === "start") { return } self.ScrollIntoView() }) } }