import Toggle from "../Input/Toggle" import Svg from "../../Svg" import { UIEventSource } from "../../Logic/UIEventSource" import { SubtleButton } from "../Base/SubtleButton" import Minimap from "../Base/Minimap" import ShowDataLayer from "../ShowDataLayer/ShowDataLayer" import { GeoOperations } from "../../Logic/GeoOperations" import { LeafletMouseEvent } from "leaflet" import Combine from "../Base/Combine" import { Button } from "../Base/Button" import Translations from "../i18n/Translations" import SplitAction from "../../Logic/Osm/Actions/SplitAction" import Title from "../Base/Title" import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource" import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer" import LayerConfig from "../../Models/ThemeConfig/LayerConfig" import { BBox } from "../../Logic/BBox" import * as split_point from "../../assets/layers/split_point/split_point.json" import { OsmConnection } from "../../Logic/Osm/OsmConnection" import { Changes } from "../../Logic/Osm/Changes" import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" import { ElementStorage } from "../../Logic/ElementStorage" import BaseLayer from "../../Models/BaseLayer" import FilteredLayer from "../../Models/FilteredLayer" import BaseUIElement from "../BaseUIElement" import { VariableUiElement } from "../Base/VariableUIElement" export default class SplitRoadWizard extends Combine { // @ts-ignore private static splitLayerStyling = new LayerConfig( split_point, "(BUILTIN) SplitRoadWizard.ts", true ) public dialogIsOpened: UIEventSource /** * A UI Element used for splitting roads * * @param id: The id of the road to remove * @param state: the state of the application */ constructor( id: string, state: { filteredLayers: UIEventSource backgroundLayer: UIEventSource featureSwitchIsTesting: UIEventSource featureSwitchIsDebugging: UIEventSource featureSwitchShowAllQuestions: UIEventSource osmConnection: OsmConnection featureSwitchUserbadge: UIEventSource changes: Changes layoutToUse: LayoutConfig allElements: ElementStorage } ) { const t = Translations.t.split // Contains the points on the road that are selected to split on - contains geojson points with extra properties such as 'location' with the distance along the linestring const splitPoints = new UIEventSource<{ feature: any; freshness: Date }[]>([]) const hasBeenSplit = new UIEventSource(false) // Toggle variable between show split button and map const splitClicked = new UIEventSource(false) const leafletMap = new UIEventSource( SplitRoadWizard.setupMapComponent(id, splitPoints, state) ) // Toggle between splitmap const splitButton = new SubtleButton( Svg.scissors_ui().SetStyle("height: 1.5rem; width: auto"), new Toggle( t.splitAgain.Clone().SetClass("text-lg font-bold"), t.inviteToSplit.Clone().SetClass("text-lg font-bold"), hasBeenSplit ) ) splitButton.onClick(() => { splitClicked.setData(true) }) // Only show the splitButton if logged in, else show login prompt const loginBtn = t.loginToSplit .Clone() .onClick(() => state.osmConnection.AttemptLogin()) .SetClass("login-button-friendly") const splitToggle = new Toggle(splitButton, loginBtn, state.osmConnection.isLoggedIn) // Save button const saveButton = new Button(t.split.Clone(), async () => { hasBeenSplit.setData(true) splitClicked.setData(false) const splitAction = new SplitAction( id, splitPoints.data.map((ff) => ff.feature.geometry.coordinates), { theme: state?.layoutToUse?.id, }, 5, (coordinates) => { state.allElements.ContainingFeatures.get(id).geometry["coordinates"] = coordinates } ) await state.changes.applyAction(splitAction) // We throw away the old map and splitpoints, and create a new map from scratch splitPoints.setData([]) leafletMap.setData(SplitRoadWizard.setupMapComponent(id, splitPoints, state)) }) saveButton.SetClass("btn btn-primary mr-3") const disabledSaveButton = new Button("Split", undefined) disabledSaveButton.SetClass("btn btn-disabled mr-3") // Only show the save button if there are split points defined const saveToggle = new Toggle( disabledSaveButton, saveButton, splitPoints.map((data) => data.length === 0) ) const cancelButton = Translations.t.general.cancel .Clone() // Not using Button() element to prevent full width button .SetClass("btn btn-secondary mr-3") .onClick(() => { splitPoints.setData([]) splitClicked.setData(false) }) cancelButton.SetClass("btn btn-secondary block") const splitTitle = new Title(t.splitTitle) const mapView = new Combine([ splitTitle, new VariableUiElement(leafletMap), new Combine([cancelButton, saveToggle]).SetClass("flex flex-row"), ]) mapView.SetClass("question") super([ Toggle.If(hasBeenSplit, () => t.hasBeenSplit.Clone().SetClass("font-bold thanks block w-full") ), new Toggle(mapView, splitToggle, splitClicked), ]) this.dialogIsOpened = splitClicked } private static setupMapComponent( id: string, splitPoints: UIEventSource<{ feature: any; freshness: Date }[]>, state: { filteredLayers: UIEventSource backgroundLayer: UIEventSource featureSwitchIsTesting: UIEventSource featureSwitchIsDebugging: UIEventSource featureSwitchShowAllQuestions: UIEventSource osmConnection: OsmConnection featureSwitchUserbadge: UIEventSource changes: Changes layoutToUse: LayoutConfig allElements: ElementStorage } ): BaseUIElement { // Load the road with given id on the minimap const roadElement = state.allElements.ContainingFeatures.get(id) // Minimap on which you can select the points to be splitted const miniMap = Minimap.createMiniMap({ background: state.backgroundLayer, allowMoving: true, leafletOptions: { minZoom: 14, }, }) miniMap.SetStyle("width: 100%; height: 24rem").SetClass("rounded-xl overflow-hidden") miniMap.installBounds(BBox.get(roadElement).pad(0.25), false) // Define how a cut is displayed on the map // Datalayer displaying the road and the cut points (if any) new ShowDataMultiLayer({ features: StaticFeatureSource.fromGeojson([roadElement]), layers: state.filteredLayers, leafletMap: miniMap.leafletMap, zoomToFeatures: true, state, }) new ShowDataLayer({ features: new StaticFeatureSource(splitPoints), leafletMap: miniMap.leafletMap, zoomToFeatures: false, layerToShow: SplitRoadWizard.splitLayerStyling, state, }) /** * Handles a click on the overleaf map. * Finds the closest intersection with the road and adds a point there, ready to confirm the cut. * @param coordinates Clicked location, [lon, lat] */ function onMapClick(coordinates) { // First, we check if there is another, already existing point nearby const points = splitPoints.data .map((f, i) => [f.feature, i]) .filter( (p) => GeoOperations.distanceBetween(p[0].geometry.coordinates, coordinates) < 5 ) .map((p) => p[1]) .sort((a, b) => a - b) .reverse(/*Copy/derived list, inplace reverse is fine*/) if (points.length > 0) { for (const point of points) { splitPoints.data.splice(point, 1) } splitPoints.ping() return } // Get nearest point on the road const pointOnRoad = GeoOperations.nearestPoint(roadElement, coordinates) // pointOnRoad is a geojson // Update point properties to let it match the layer pointOnRoad.properties["_split_point"] = "yes" // Add it to the list of all points and notify observers splitPoints.data.push({ feature: pointOnRoad, freshness: new Date() }) // show the point on the data layer splitPoints.ping() // not updated using .setData, so manually ping observers } // When clicked, pass clicked location coordinates to onMapClick function miniMap.leafletMap.addCallbackAndRunD((leafletMap) => leafletMap.on("click", (mouseEvent: LeafletMouseEvent) => { onMapClick([mouseEvent.latlng.lng, mouseEvent.latlng.lat]) }) ) return miniMap } }