forked from MapComplete/MapComplete
		
	
		
			
				
	
	
		
			243 lines
		
	
	
	
		
			9.6 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			243 lines
		
	
	
	
		
			9.6 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
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<boolean>
 | 
						|
 | 
						|
    /**
 | 
						|
     * 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<FilteredLayer[]>
 | 
						|
            backgroundLayer: UIEventSource<BaseLayer>
 | 
						|
            featureSwitchIsTesting: UIEventSource<boolean>
 | 
						|
            featureSwitchIsDebugging: UIEventSource<boolean>
 | 
						|
            featureSwitchShowAllQuestions: UIEventSource<boolean>
 | 
						|
            osmConnection: OsmConnection
 | 
						|
            featureSwitchUserbadge: UIEventSource<boolean>
 | 
						|
            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<boolean>(false)
 | 
						|
 | 
						|
        const leafletMap = new UIEventSource<BaseUIElement>(
 | 
						|
            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<FilteredLayer[]>
 | 
						|
            backgroundLayer: UIEventSource<BaseLayer>
 | 
						|
            featureSwitchIsTesting: UIEventSource<boolean>
 | 
						|
            featureSwitchIsDebugging: UIEventSource<boolean>
 | 
						|
            featureSwitchShowAllQuestions: UIEventSource<boolean>
 | 
						|
            osmConnection: OsmConnection
 | 
						|
            featureSwitchUserbadge: UIEventSource<boolean>
 | 
						|
            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(<any>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
 | 
						|
    }
 | 
						|
}
 |