/* done IMPORTANT: select closest point on drag instead of first done add touch hint poly done approx radius for dragFindNearest */ import { LatLngExpression, Marker, Polyline, PolylineOptions, marker, divIcon, LayerGroup, LatLng, LeafletMouseEvent, latLng, LatLngLiteral, } from 'leaflet'; import { distKm, distToSegment, getPolyLength, pointInArea } from "$utils/geom"; interface InteractivePolylineOptions extends PolylineOptions { maxMarkers?: number, constraintsStyle?: PolylineOptions, kmMarksEnabled?: boolean, kmMarksStep?: number, } export class Component extends Polyline { constructor(latlngs: LatLngExpression[] | LatLngExpression[][], options?: InteractivePolylineOptions) { super(latlngs, options); this.constraintsStyle = { ...this.constraintsStyle, ...options.constraintsStyle }; this.maxMarkers = options.maxMarkers || this.maxMarkers; // this.kmMarksEnabled = options.kmMarksEnabled || this.kmMarksEnabled; // this.kmMarksStep = options.kmMarksStep || this.kmMarksStep; this.constrLine = new Polyline([], this.constraintsStyle); this.startDragHinting(); } updateTouchHinter = ({ latlngs }: { latlngs: LatLngLiteral[] }): void => { this.touchHinter.setLatLngs(latlngs); }; setPoints = (latlngs: LatLng[]) => { this.setLatLngs(latlngs); this.recreateMarkers(); this.recalcDistance(); // this.recalcKmMarks(); this.touchHinter.setLatLngs(latlngs); this.fire('latlngschange', { latlngs }); }; createHintMarker = (latlng: LatLng): Marker => marker(latlng, { draggable: false, icon: divIcon({ className: 'leaflet-vertex-drag-helper', iconSize: [11, 11], iconAnchor: [6, 6] }) }); createMarker = (latlng: LatLng): Marker => marker(latlng, { draggable: true, icon: divIcon({ className: 'leaflet-vertex-icon', iconSize: [11, 11], iconAnchor: [6, 6] }) }) .on('contextmenu', this.dropMarker) .on('drag', this.onMarkerDrag) .on('dragstart', this.onMarkerDragStart) .on('dragend', this.onMarkerDragEnd) .addTo(this.markerLayer); recreateMarkers = () => { this.clearAllMarkers(); const latlngs = this.getLatLngs(); if (!latlngs || latlngs.length === 0) return; latlngs.forEach(latlng => this.markers.push(this.createMarker(latlng))); this.updateMarkers(); }; clearAllMarkers = (): void => { this.markerLayer.clearLayers(); this.markers = []; }; updateMarkers = (): void => { this.showVisibleMarkers(); }; showAllMarkers = (): void => { if (!this.is_editing) return; if (this._map.hasLayer(this.markerLayer)) return; this._map.addLayer(this.markerLayer); this.fire('allvertexshow'); console.log(); }; hideAllMarkers = (): void => { if (!this._map.hasLayer(this.markerLayer)) return; this._map.removeLayer(this.markerLayer); this.fire('allvertexhide'); }; showVisibleMarkers = (): void => { if (!this.is_editing) return; const northEast = this._map.getBounds().getNorthEast(); const southWest = this._map.getBounds().getSouthWest(); const { visible, hidden } = this.markers.reduce((obj, marker) => { const { lat, lng } = marker.getLatLng(); const is_hidden = (lat > northEast.lat || lng > northEast.lng || lat < southWest.lat || lng < southWest.lng); return is_hidden ? { ...obj, hidden: [...obj.hidden, marker] } : { ...obj, visible: [...obj.visible, marker] } }, { visible: [], hidden: [] } ); if (visible.length > this.maxMarkers) return this.hideAllMarkers(); this.showAllMarkers(); visible.forEach(marker => { if (!this.markerLayer.hasLayer(marker)) this.markerLayer.addLayer(marker); }); hidden.forEach(marker => { if (this.markerLayer.hasLayer(marker)) this.markerLayer.removeLayer(marker); }); }; editor = { disable: () => { this.hideAllMarkers(); this.is_editing = false; this.stopDragHinting(); this.stopDrawing(); this.touchHinter.removeFrom(this._map); this.fire('editordisable'); }, enable: () => { this.is_editing = true; this.showVisibleMarkers(); this.startDragHinting(); this.touchHinter.addTo(this._map); this.fire('editorenable'); }, continue: () => { this.is_drawing = true; this.drawing_direction = 'forward'; this.startDrawing(); }, prepend: () => { this.is_drawing = true; this.drawing_direction = 'backward'; this.startDrawing(); } }; moveDragHint = ({ latlng }: LeafletMouseEvent): void => { this.hintMarker.setLatLng(latlng); }; hideDragHint = (): void => { if (this._map.hasLayer(this.hintMarker)) this._map.removeLayer(this.hintMarker); }; showDragHint = (): void => { this._map.addLayer(this.hintMarker); }; startDragHinting = (): void => { this.touchHinter.on('mousemove', this.moveDragHint); this.touchHinter.on('mousedown', this.startDragHintMove); this.touchHinter.on('mouseover', this.showDragHint); this.touchHinter.on('mouseout', this.hideDragHint); }; stopDragHinting = (): void => { this.touchHinter.off('mousemove', this.moveDragHint); this.touchHinter.off('mousedown', this.startDragHintMove); this.touchHinter.off('mouseover', this.showDragHint); this.touchHinter.off('mouseout', this.hideDragHint); }; startDragHintMove = (event: LeafletMouseEvent): void => { event.originalEvent.stopPropagation(); event.originalEvent.preventDefault(); if (this.is_drawing) { this.stopDrawing(); this.is_drawing = true; } const prev = this.dragHintFindNearest(event.latlng); if (prev < 0) return; this.hint_prev_marker = prev; this.constrLine.setLatLngs([]).addTo(this._map); this._map.dragging.disable(); this.is_dragging = true; this._map.on('mousemove', this.dragHintMove); this._map.on('mouseup', this.dragHintAddMarker); this._map.on('mouseout', this.stopDragHintMove); }; stopDragHintMove = (): void => { this._map.dragging.enable(); this.constrLine.removeFrom(this._map); this._map.off('mousemove', this.dragHintMove); this._map.off('mouseup', this.dragHintAddMarker); this._map.off('mouseout', this.stopDragHintMove); if (this.is_drawing) this.startDrawing(); setTimeout(() => { this.is_dragging = false; }, 0); }; dragHintAddMarker = ({ latlng }: LeafletMouseEvent): void => { this.dragHintChangeDistance(this.hint_prev_marker, latlng); this.markers.splice((this.hint_prev_marker + 1), 0, this.createMarker(latlng)); this.insertLatLng(latlng, this.hint_prev_marker + 1); this.hideDragHint(); this.stopDragHintMove(); }; dragHintChangeDistance = (index: number, current: LatLngLiteral): void => { const prev = this.markers[index]; const next = this.markers[index + 1]; const initial_distance = distKm(prev.getLatLng(), next.getLatLng()); const current_distance = ( ((prev && distKm(prev.getLatLng(), current)) || 0) + ((next && distKm(next.getLatLng(), current)) || 0) ); this.distance += (current_distance - initial_distance); this.fire('distancechange', { distance: this.distance }); }; dragHintFindNearest = (latlng: LatLng): any => { const latlngs = this.getLatLngs() as LatLng[]; const neighbours = latlngs.filter((current, index) => { const next = latlngs[index + 1] as LatLng; return (next && pointInArea(current, next, latlng)); }) .map(el => latlngs.indexOf(el)) .sort((a, b) => ( distToSegment(latlngs[a], latlngs[a + 1], latlng) - distToSegment(latlngs[b], latlngs[b + 1], latlng) )); return neighbours.length > 0 ? neighbours[0] : -1; }; dragHintMove = (event: LeafletMouseEvent): void => { event.originalEvent.stopPropagation(); event.originalEvent.preventDefault(); this.setConstraints([ this.markers[this.hint_prev_marker].getLatLng(), event.latlng, this.markers[this.hint_prev_marker + 1].getLatLng(), ]); }; onMarkerDrag = ({ target }: { target: Marker}) => { const coords = new Array(0) .concat((this.vertex_index > 0 && this.markers[this.vertex_index - 1].getLatLng()) || []) .concat(target.getLatLng()) .concat((this.vertex_index < (this.markers.length - 1) && this.markers[this.vertex_index + 1].getLatLng()) || []); this.setConstraints(coords); this.fire('vertexdrag', { index: this.vertex_index, vertex: target }); }; onMarkerDragStart = ({ target }: { target: Marker}) => { if (this.is_drawing) { this.stopDrawing(); this.is_drawing = true; } if (this.is_dragging) this.stopDragHintMove(); this.vertex_index = this.markers.indexOf(target); this.is_dragging = true; this.constrLine.addTo(this._map); this.fire('vertexdragstart', { index: this.vertex_index, vertex: target }); }; onMarkerDragEnd = ({ target }: { target: Marker}): void => { const latlngs = this.getLatLngs() as LatLngLiteral[]; this.markerDragChangeDistance(this.vertex_index, latlngs[this.vertex_index], target.getLatLng()); this.replaceLatlng(target.getLatLng(), this.vertex_index); this.is_dragging = false; this.constrLine.removeFrom(this._map); this.vertex_index = null; if (this.is_drawing) this.startDrawing(); this.fire('vertexdragend', { index: this.vertex_index, vertex: target }); }; markerDragChangeDistance = (index: number, initial: LatLngLiteral, current: LatLngLiteral): void => { const prev = index > 0 ? this.markers[index - 1] : null; const next = index <= (this.markers.length + 1) ? this.markers[index + 1] : null; const initial_distance = ( ((prev && distKm(prev.getLatLng(), initial)) || 0) + ((next && distKm(next.getLatLng(), initial)) || 0) ); const current_distance = ( ((prev && distKm(prev.getLatLng(), current)) || 0) + ((next && distKm(next.getLatLng(), current)) || 0) ); this.distance += (current_distance - initial_distance); this.fire('distancechange', { distance: this.distance }); }; startDrawing = (): void => { this.is_drawing = true; this.setConstraints([]); this.constrLine.addTo(this._map); this._map.on('mousemove', this.onDrawingMove); this._map.on('click', this.onDrawingClick); }; stopDrawing = (): void => { this.constrLine.removeFrom(this._map); this._map.off('mousemove', this.onDrawingMove); this._map.off('click', this.onDrawingClick); this.is_drawing = false; }; onDrawingMove = ({ latlng }: LeafletMouseEvent): void => { if (this.markers.length === 0) { this.setConstraints([]); return; } if (!this._map.hasLayer(this.constrLine)) this._map.addLayer(this.constrLine); const marker = this.drawing_direction === 'forward' ? this.markers[this.markers.length - 1] : this.markers[0]; this.setConstraints([latlng, marker.getLatLng()]); }; onDrawingClick = (event: LeafletMouseEvent): void => { if (this.is_dragging) return; const { latlng } = event; this.stopDrawing(); const latlngs = this.getLatLngs() as any[]; this.drawingChangeDistance(latlng); if (this.drawing_direction === 'forward') { latlngs.push(latlng); this.markers.push(this.createMarker(latlng)); } else { latlngs.unshift(latlng); this.markers.unshift(this.createMarker(latlng)); } this.setLatLngs(latlngs); this.fire('latlngschange', { latlngs }); this.showVisibleMarkers(); this.startDrawing(); }; drawingChangeDistance = (latlng: LatLngLiteral): void => { const latlngs = this.getLatLngs() as LatLngLiteral[]; if (latlngs.length < 1) { this.distance = 0; this.fire('distancechange', { distance: this.distance }); return; } const point = this.drawing_direction === 'forward' ? latlngs[latlngs.length - 1] : latlngs[0]; this.distance += distKm(point, latlng); this.fire('distancechange', { distance: this.distance }); }; replaceLatlng = (latlng: LatLng, index: number): void => { const latlngs = this.getLatLngs() as LatLngLiteral[]; latlngs.splice(index, 1, latlng); this.setLatLngs(latlngs); this.fire('latlngschange', { latlngs }); }; insertLatLng = (latlng, index): void => { const latlngs = this.getLatLngs(); latlngs.splice(index, 0, latlng); this.setLatLngs(latlngs); this.fire('latlngschange', { latlngs }); }; setConstraints = (coords: LatLng[]) => { this.constrLine.setLatLngs(coords); }; dropMarker = ({ target }: LeafletMouseEvent): void => { const index = this.markers.indexOf(target); const latlngs = this.getLatLngs(); if (typeof index === 'undefined' || latlngs.length <= 2) return; this.dropMarkerDistanceChange(index); this._map.removeLayer(this.markers[index]); this.markers.splice(index, 1); latlngs.splice(index, 1); this.setLatLngs(latlngs); this.fire('latlngschange', { latlngs }); }; dropMarkerDistanceChange = (index: number): void => { const latlngs = this.getLatLngs() as LatLngLiteral[]; const prev = index > 0 ? latlngs[index - 1] : null; const current = latlngs[index]; const next = index <= (latlngs.length + 1) ? latlngs[index + 1] : null; const initial_distance = ( ((prev && distKm(prev, current)) || 0) + ((next && distKm(next, current)) || 0) ); const current_distance = (prev && next && distKm(prev, next)) || 0; this.distance += (current_distance - initial_distance); this.fire('distancechange', { distance: this.distance }); }; recalcDistance = () => { const latlngs = this.getLatLngs() as LatLngLiteral[]; this.distance = getPolyLength(latlngs); this.fire('distancechange', { distance: this.distance }); }; // // recalcKmMarks = () => { // if (!this.kmMarksEnabled) return; // // const latlngs = this.getLatLngs() as LatLngLiteral[]; // // this.kmMarks = { }; // // let last_km_mark = 0; // // latlngs.reduce((dist, latlng, index) => { // if (index >= latlngs.length - 1) return; // // const next = latlngs[index + 1]; // const sum = dist + distKm(latlng, next); // const rounded = Math.floor(dist / this.kmMarksStep) * this.kmMarksStep; // // if (rounded > last_km_mark) { // last_km_mark = rounded; // this.kmMarks[rounded] = latlng; // } // // return sum; // }, 0); // }; // kmMarksEnabled?: InteractivePolylineOptions['kmMarksEnabled'] = true; // kmMarksStep?: InteractivePolylineOptions['kmMarksStep'] = 5; // kmMarks?: { [x: number]: LatLngLiteral }; // kmMarksLayer?: LayerGroup = new LayerGroup(); markers: Marker[] = []; maxMarkers: InteractivePolylineOptions['maxMarkers'] = 2; markerLayer: LayerGroup = new LayerGroup(); constraintsStyle: InteractivePolylineOptions['constraintsStyle'] = { weight: 6, color: 'red', dashArray: '10, 12', opacity: 0.5, interactive: false, }; touchHinter: Polyline = new Polyline([], { weight: 24, smoothFactor: 3, className: 'touch-hinter-poly' // bubblingMouseEvents: false, }); hintMarker: Marker = this.createHintMarker(latLng({ lat: 0, lng: 0 })); constrLine: Polyline; is_editing: boolean = true; is_dragging: boolean = false; is_drawing: boolean = false; drawing_direction: 'forward' | 'backward' = 'forward'; vertex_index?: number = null; hint_prev_marker: number = null; distance: number = 0; } Component.addInitHook(function () { this.once('add', (event) => { if (event.target instanceof InteractivePoly) { this.map = event.target._map; this.map.on('touch', console.log); this.markerLayer.addTo(event.target._map); this.hintMarker.addTo(event.target._map); this.constrLine.addTo(event.target._map); this.touchHinter.addTo(event.target._map); this.map.on('moveend', this.updateMarkers); this.on('latlngschange', this.updateTouchHinter); if (window.innerWidth < 768) { this.touchHinter.setStyle({ weight: 32 }); } } }); this.once('remove', (event) => { if (event.target instanceof InteractivePoly) { this.markerLayer.removeFrom(this._map); this.hintMarker.removeFrom(this._map); this.constrLine.removeFrom(this._map); this.touchHinter.removeFrom(this._map); this.map.off('moveend', this.updateMarkers); } }); }); export const InteractivePoly = Component; /* events: vertexdragstart, vertexdragend, vertexdrag, allvertexhide allvertexshow editordisable editorenable distancechange latlngschange */