forked from MapComplete/MapComplete
		
	
		
			
				
	
	
		
			187 lines
		
	
	
	
		
			6.6 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			187 lines
		
	
	
	
		
			6.6 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
import { InputElement } from "./InputElement";
 | 
						|
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource";
 | 
						|
import Combine from "../Base/Combine";
 | 
						|
import Svg from "../../Svg";
 | 
						|
import Loc from "../../Models/Loc";
 | 
						|
import { GeoOperations } from "../../Logic/GeoOperations";
 | 
						|
import BaseUIElement from "../BaseUIElement";
 | 
						|
import { AvailableRasterLayers, RasterLayerPolygon } from "../../Models/RasterLayers";
 | 
						|
 | 
						|
/**
 | 
						|
 * Selects a length after clicking on the minimap, in meters
 | 
						|
 */
 | 
						|
export default class LengthInput extends InputElement<string> {
 | 
						|
    private readonly _location: Store<Loc>
 | 
						|
    private readonly value: UIEventSource<string>
 | 
						|
    private readonly background: Store<RasterLayerPolygon>
 | 
						|
 | 
						|
    constructor(
 | 
						|
        location: UIEventSource<Loc>,
 | 
						|
        mapBackground?: UIEventSource<RasterLayerPolygon>,
 | 
						|
        value?: UIEventSource<string>
 | 
						|
    ) {
 | 
						|
        super()
 | 
						|
        this._location = location
 | 
						|
        this.value = value ?? new UIEventSource<string>(undefined)
 | 
						|
        this.background = mapBackground ?? new ImmutableStore(AvailableRasterLayers.osmCarto)
 | 
						|
        this.SetClass("block")
 | 
						|
    }
 | 
						|
 | 
						|
    GetValue(): UIEventSource<string> {
 | 
						|
        return this.value
 | 
						|
    }
 | 
						|
 | 
						|
    IsValid(str: string): boolean {
 | 
						|
        const t = Number(str)
 | 
						|
        return !isNaN(t) && t >= 0
 | 
						|
    }
 | 
						|
 | 
						|
    protected InnerConstructElement(): HTMLElement {
 | 
						|
        let map: BaseUIElement = undefined
 | 
						|
        let layerControl: BaseUIElement = undefined
 | 
						|
        map = Minimap.createMiniMap({
 | 
						|
            background: this.background,
 | 
						|
            allowMoving: false,
 | 
						|
            location: this._location,
 | 
						|
            attribution: true,
 | 
						|
            leafletOptions: {
 | 
						|
                tap: true,
 | 
						|
            },
 | 
						|
        })
 | 
						|
 | 
						|
        const crosshair = new Combine([
 | 
						|
            Svg.length_crosshair_svg().SetStyle(
 | 
						|
                `position: absolute;top: 0;left: 0;transform:rotate(${this.value.data ?? 0}deg);`
 | 
						|
            ),
 | 
						|
        ])
 | 
						|
            .SetClass("block length-crosshair-svg relative pointer-events-none")
 | 
						|
            .SetStyle("z-index: 1000; visibility: hidden")
 | 
						|
 | 
						|
        const element = new Combine([
 | 
						|
            crosshair,
 | 
						|
            map?.SetClass("w-full h-full block absolute top-0 left-O overflow-hidden"),
 | 
						|
        ])
 | 
						|
            .SetClass("relative block bg-white border border-black rounded-xl overflow-hidden")
 | 
						|
            .ConstructElement()
 | 
						|
 | 
						|
        this.RegisterTriggers(
 | 
						|
            map?.ConstructElement(),
 | 
						|
            map?.leafletMap,
 | 
						|
            crosshair.ConstructElement()
 | 
						|
        )
 | 
						|
        element.style.overflow = "hidden"
 | 
						|
        element.style.display = "block"
 | 
						|
 | 
						|
        return element
 | 
						|
    }
 | 
						|
 | 
						|
    private RegisterTriggers(
 | 
						|
        htmlElement: HTMLElement,
 | 
						|
        leafletMap: UIEventSource<L.Map>,
 | 
						|
        measurementCrosshair: HTMLElement
 | 
						|
    ) {
 | 
						|
        let firstClickXY: [number, number] = undefined
 | 
						|
        let lastClickXY: [number, number] = undefined
 | 
						|
        const self = this
 | 
						|
 | 
						|
        function onPosChange(x: number, y: number, isDown: boolean, isUp?: boolean) {
 | 
						|
            if (x === undefined || y === undefined) {
 | 
						|
                // Touch end
 | 
						|
                firstClickXY = undefined
 | 
						|
                lastClickXY = undefined
 | 
						|
                return
 | 
						|
            }
 | 
						|
 | 
						|
            const rect = htmlElement.getBoundingClientRect()
 | 
						|
            // From the central part of location
 | 
						|
            const dx = x - rect.left
 | 
						|
            const dy = y - rect.top
 | 
						|
            if (isDown) {
 | 
						|
                if (lastClickXY === undefined && firstClickXY === undefined) {
 | 
						|
                    firstClickXY = [dx, dy]
 | 
						|
                } else if (firstClickXY !== undefined && lastClickXY === undefined) {
 | 
						|
                    lastClickXY = [dx, dy]
 | 
						|
                } else if (firstClickXY !== undefined && lastClickXY !== undefined) {
 | 
						|
                    // we measure again
 | 
						|
                    firstClickXY = [dx, dy]
 | 
						|
                    lastClickXY = undefined
 | 
						|
                }
 | 
						|
            }
 | 
						|
 | 
						|
            if (firstClickXY === undefined) {
 | 
						|
                measurementCrosshair.style.visibility = "hidden"
 | 
						|
                return
 | 
						|
            }
 | 
						|
 | 
						|
            const distance = Math.sqrt(
 | 
						|
                (dy - firstClickXY[1]) * (dy - firstClickXY[1]) +
 | 
						|
                    (dx - firstClickXY[0]) * (dx - firstClickXY[0])
 | 
						|
            )
 | 
						|
            if (isUp) {
 | 
						|
                if (distance > 15) {
 | 
						|
                    lastClickXY = [dx, dy]
 | 
						|
                }
 | 
						|
            } else if (lastClickXY !== undefined) {
 | 
						|
                return
 | 
						|
            }
 | 
						|
            measurementCrosshair.style.visibility = "unset"
 | 
						|
            measurementCrosshair.style.left = firstClickXY[0] + "px"
 | 
						|
            measurementCrosshair.style.top = firstClickXY[1] + "px"
 | 
						|
 | 
						|
            const angle = (180 * Math.atan2(firstClickXY[1] - dy, firstClickXY[0] - dx)) / Math.PI
 | 
						|
            const angleGeo = (angle + 270) % 360
 | 
						|
            const measurementCrosshairInner: HTMLElement = <HTMLElement>(
 | 
						|
                measurementCrosshair.firstChild
 | 
						|
            )
 | 
						|
            measurementCrosshairInner.style.transform = `rotate(${angleGeo}deg)`
 | 
						|
 | 
						|
            measurementCrosshairInner.style.width = distance * 2 + "px"
 | 
						|
            measurementCrosshairInner.style.marginLeft = -distance + "px"
 | 
						|
            measurementCrosshairInner.style.marginTop = -distance + "px"
 | 
						|
 | 
						|
            const leaflet = leafletMap?.data
 | 
						|
            if (leaflet) {
 | 
						|
                const first = leaflet.layerPointToLatLng(firstClickXY)
 | 
						|
                const last = leaflet.layerPointToLatLng([dx, dy])
 | 
						|
                const geoDist =
 | 
						|
                    Math.floor(
 | 
						|
                        GeoOperations.distanceBetween(
 | 
						|
                            [first.lng, first.lat],
 | 
						|
                            [last.lng, last.lat]
 | 
						|
                        ) * 10
 | 
						|
                    ) / 10
 | 
						|
                self.value.setData("" + geoDist)
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        htmlElement.ontouchstart = (ev: TouchEvent) => {
 | 
						|
            onPosChange(ev.touches[0].clientX, ev.touches[0].clientY, true)
 | 
						|
            ev.preventDefault()
 | 
						|
        }
 | 
						|
 | 
						|
        htmlElement.ontouchmove = (ev: TouchEvent) => {
 | 
						|
            onPosChange(ev.touches[0].clientX, ev.touches[0].clientY, false)
 | 
						|
            ev.preventDefault()
 | 
						|
        }
 | 
						|
 | 
						|
        htmlElement.ontouchend = (ev: TouchEvent) => {
 | 
						|
            onPosChange(undefined, undefined, false, true)
 | 
						|
            ev.preventDefault()
 | 
						|
        }
 | 
						|
 | 
						|
        htmlElement.onmousedown = (ev: MouseEvent) => {
 | 
						|
            onPosChange(ev.clientX, ev.clientY, true)
 | 
						|
            ev.preventDefault()
 | 
						|
        }
 | 
						|
 | 
						|
        htmlElement.onmouseup = (ev) => {
 | 
						|
            onPosChange(ev.clientX, ev.clientY, false, true)
 | 
						|
            ev.preventDefault()
 | 
						|
        }
 | 
						|
 | 
						|
        htmlElement.onmousemove = (ev: MouseEvent) => {
 | 
						|
            onPosChange(ev.clientX, ev.clientY, false)
 | 
						|
            ev.preventDefault()
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 |