From edbd911de654891bcec26b91b32487ab65564996 Mon Sep 17 00:00:00 2001 From: muerwre Date: Thu, 21 Feb 2019 18:15:20 +0700 Subject: [PATCH] InteractivePoly: working marker dragging --- src/modules/Editor.ts | 3 +- src/modules/InteractivePoly.ts | 191 +++++++++++++++++++++++++++ src/modules/Poly.ts | 133 ++++++++++--------- src/parts/map.js | 81 ------------ src/parts/poly.js | 228 --------------------------------- src/utils/geom.ts | 11 ++ 6 files changed, 277 insertions(+), 370 deletions(-) create mode 100644 src/modules/InteractivePoly.ts delete mode 100644 src/parts/map.js delete mode 100644 src/parts/poly.js diff --git a/src/modules/Editor.ts b/src/modules/Editor.ts index c27d009..fa2131e 100644 --- a/src/modules/Editor.ts +++ b/src/modules/Editor.ts @@ -358,7 +358,8 @@ export class Editor { this.setInitialData(); this.owner = { id }; - if (this.poly.latlngs && this.poly.latlngs.length > 1) this.poly.poly.editor.enable(); + // todo: implement + // if (this.poly.latlngs && this.poly.latlngs.length > 1) this.poly.poly.editor.enable(); this.stickers.startEditing(); }; diff --git a/src/modules/InteractivePoly.ts b/src/modules/InteractivePoly.ts new file mode 100644 index 0000000..e6468ef --- /dev/null +++ b/src/modules/InteractivePoly.ts @@ -0,0 +1,191 @@ +import { LatLngExpression, Marker, Polyline, PolylineOptions, marker, divIcon, LayerGroup, LatLng } from 'leaflet'; + +interface InteractivePolylineOptions extends PolylineOptions { + maxMarkers?: number, + constraintsStyle?: PolylineOptions, +} + +export class InteractivePoly extends Polyline { + constructor(latlngs: LatLngExpression[] | LatLngExpression[][], options?: InteractivePolylineOptions) { + super(latlngs, options); + + this.constraintsStyle = { ...this.constraintsStyle, ...options.constraintsStyle }; + this.maxMarkers = options.maxMarkers || this.maxMarkers; + + this.constr1 = new Polyline([], this.constraintsStyle).addTo(this.constraintsLayer); + this.constr2 = new Polyline([], this.constraintsStyle).addTo(this.constraintsLayer); + } + + setPoints = (latlngs: LatLng[]) => { + this.setLatLngs(latlngs); + this.recreateMarkers(); + }; + + createMarker = (latlng: LatLng): Marker => marker(latlng, { + draggable: true, + icon: divIcon({ + className: 'leaflet-vertex-icon', + iconSize: [11, 11], + iconAnchor: [6, 6] + }) + }) + .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._map.hasLayer(this.markerLayer)) return; + + this._map.addLayer(this.markerLayer); + this.fire('allvertexshow'); + }; + + hideAllMarkers = (): void => { + if (!this._map.hasLayer(this.markerLayer)) return; + + this._map.removeLayer(this.markerLayer); + this.fire('allvertexhide'); + }; + + showVisibleMarkers = (): void => { + 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 = { + + }; + + onMarkerDrag = ({ target }: { target: Marker}) => { + console.log(this.vertex_index, this.markers.length); + + this.setConstraints( + this.vertex_index > 0 && this.markers[this.vertex_index - 1].getLatLng(), + target.getLatLng(), + this.vertex_index < (this.markers.length - 1) && this.markers[this.vertex_index + 1].getLatLng(), + ); + + this.fire('vertexdrag', { index: this.vertex_index, vertex: target }); + }; + + onMarkerDragStart = ({ target }: { target: Marker}) => { + this.vertex_index = this.markers.indexOf(target); + + this.is_dragging = true; + this.constraintsLayer.addTo(this._map); + + this.fire('vertexdragstart', { index: this.vertex_index, vertex: target }); + }; + + onMarkerDragEnd = ({ target }: { target: Marker}): void => { + this.replaceLatlng(target.getLatLng(), this.vertex_index); + + this.is_dragging = false; + this.constraintsLayer.removeFrom(this._map); + + this.fire('vertexdragend', { index: this.vertex_index, vertex: target }); + this.vertex_index = null; + }; + + replaceLatlng = (latlng: LatLng, index: number): void => { + const latlngs = this.getLatLngs(); + latlngs.splice(index, 1, latlng); + this.setLatLngs(latlngs); + }; + + setConstraints = (prev?: LatLng, marker?: LatLng, next?: LatLng) => { + if (prev) this.constr1.setLatLngs([prev, marker]); + if (next) this.constr2.setLatLngs([next, marker]); + }; + + markers: Marker[] = []; + maxMarkers: InteractivePolylineOptions['maxMarkers'] = 2; + markerLayer: LayerGroup = new LayerGroup(); + constraintsLayer: LayerGroup = new LayerGroup(); + constraintsStyle: InteractivePolylineOptions['constraintsStyle'] = { + weight: 6, + color: 'red', + dashArray: '2, 10', + opacity: 0.5, + }; + + constr1: Polyline; + constr2: Polyline; + + is_dragging: boolean = false; + vertex_index?: number = null; + markers_visible: boolean = true; +} + +InteractivePoly.addInitHook(function () { + this.once('add', (event) => { + if (event.target instanceof InteractivePoly) { + this.map = event.target._map; + this.markerLayer.addTo(event.target._map); + + this.map.on('moveend', this.updateMarkers); + } + }); + + this.once('remove', () => { + if (event.target instanceof InteractivePoly) { + this.markerLayer.removeFrom(this.map); + this.map.off('moveend', this.updateMarkers); + } + }); +}); + +/* + events: + vertexdragstart, + vertexdragend, + vertexdrag, + + allvertexhide + allvertexshow + */ diff --git a/src/modules/Poly.ts b/src/modules/Poly.ts index deba6dc..dfb6311 100644 --- a/src/modules/Poly.ts +++ b/src/modules/Poly.ts @@ -1,12 +1,12 @@ -import { Map, LayerGroup } from 'leaflet'; -import 'leaflet-geometryutil'; +import { Map, LayerGroup, Polyline } from 'leaflet'; import { EditablePolyline } from '$utils/EditablePolyline'; import { simplify } from '$utils/simplify'; -import { findDistance, middleCoord } from '$utils/geom'; +import { findDistance, getPolyLength, middleCoord } from '$utils/geom'; import { CLIENT } from '$config/frontend'; import { MODES } from '$constants/modes'; import { editor, Editor } from "$modules/Editor"; import { ILatLng } from "$modules/Stickers"; +import { InteractivePoly } from "$modules/InteractivePoly"; const polyStyle = { color: 'url(#activePathGradient)', @@ -27,22 +27,28 @@ export class Poly { constructor({ map, routerMoveStart, lockMapClicks, setDistance, triggerOnChange, editor, }: Props) { - this.poly = new EditablePolyline([], { - ...polyStyle, - maxMarkers: 100, - - onPointsSet: this.updateMarks, - onMarkerDragEnd: this.updateMarks, - onPointAdded: this.updateMarks, - onPointDropped: this.updateMarks, - onContinueDrawing: this.setModeOnDrawing, - - onMarkersHide: () => editor.setMarkersShown(false), - onMarkersShow: () => editor.setMarkersShown(true), - }).addTo(map); + this.poly = new InteractivePoly([], { + color: 'url(#activePathGradient)', + weight: 6, + maxMarkers: 300, + }); + // this.poly = new EditablePolyline([], { + // ...polyStyle, + // maxMarkers: 100, + // + // onPointsSet: this.updateMarks, + // onMarkerDragEnd: this.updateMarks, + // onPointAdded: this.updateMarks, + // onPointDropped: this.updateMarks, + // onContinueDrawing: this.setModeOnDrawing, + // + // onMarkersHide: () => editor.setMarkersShown(false), + // onMarkersShow: () => editor.setMarkersShown(true), + // }).addTo(map); this.poly.addTo(map); - this.poly._reloadPolyline(); + // todo: uncomment + // this.poly._reloadPolyline(); this.editor = editor; this.map = map; @@ -61,45 +67,46 @@ export class Poly { drawArrows = () => { // todo: fix this - // this.arrows.clearLayers(); - // const { latlngs } = this; - // - // if (!latlngs || latlngs.length <= 1) return; - // - // latlngs.forEach((latlng, i) => { - // if (i === 0) return; - // - // const mid = middleCoord(latlngs[i], latlngs[i - 1]); - // const dist = findDistance(latlngs[i - 1].lat, latlngs[i - 1].lng, latlngs[i].lat, latlngs[i].lng); - // - // if (dist <= 1) return; - // - // const slide = new Polyline( - // [ - // latlngs[i - 1], - // [mid.lat, mid.lng] - // ], - // { color: 'none', weight: CLIENT.STROKE_WIDTH } - // ).addTo(this.arrows); - // - // // todo: uncomment and fix this: - // // slide._path.setAttribute('marker-end', 'url(#long-arrow)'); - // }); + this.arrows.clearLayers(); + const { latlngs } = this; + + if (!latlngs || latlngs.length <= 1) return; + + latlngs.forEach((latlng, i) => { + if (i === 0) return; + + const mid = middleCoord(latlngs[i], latlngs[i - 1]); + const dist = findDistance(latlngs[i - 1].lat, latlngs[i - 1].lng, latlngs[i].lat, latlngs[i].lng); + + if (dist <= 1) return; + + const slide = new Polyline( + [ + latlngs[i - 1], + [mid.lat, mid.lng] + ], + { color: 'none', weight: CLIENT.STROKE_WIDTH } + ).addTo(this.arrows) as any; + + // todo: uncomment and fix this: + slide._path.setAttribute('marker-end', 'url(#long-arrow)'); + }); }; updateMarks = () => { - // todo: fix this - // const coords = this.poly.toGeoJSON().geometry.coordinates; - // - // const meters = (this.poly && (L.GeometryUtil.length(this.poly) / 1000)) || 0; - // const kilometers = (meters && meters.toFixed(1)) || 0; - // - // this.setTotalDist(kilometers); - // this.routerMoveStart(); - // - // this.drawArrows(); // <-- uncomment - // - // if (coords.length > 1) this.triggerOnChange(); + const coords = this.poly.toGeoJSON().geometry.coordinates; + + // const meters = (this.latlngs && this.latlngs.length > 1 && getPolyLength(this.latlngs)) || 0; + // const kilometers = (meters && Number(meters.toFixed(1))) || 0; + + const kilometers = ((this.latlngs && this.latlngs.length > 1 && getPolyLength(this.latlngs)) || 0); + + this.setDistance(parseFloat(kilometers.toFixed(2))); + this.routerMoveStart(); + + this.drawArrows(); // <-- uncomment + + if (coords.length > 1) this.triggerOnChange(); }; preventMissClicks = (e): void => { @@ -113,15 +120,18 @@ export class Poly { }; continue = (): void => { - this.poly.editor.continueForward(); + // todo: implement + // this.poly.editor.continueForward(); }; stop = (): void => { - if (this.poly) this.poly.editor.stopDrawing(); + // todo: implement + // if (this.poly) this.poly.editor.stopDrawing(); }; continueForward = (): void => { - this.poly.continueForward(); + // todo: implement + // this.poly.continueForward(); }; lockMap = (): void => { @@ -129,7 +139,10 @@ export class Poly { }; setPoints = (latlngs: Array): void => { + console.log('setP'); + if (!latlngs || latlngs.length <= 1) return; + // todo: implement this.poly.setPoints(latlngs); }; @@ -141,15 +154,15 @@ export class Poly { ...simplified, ]; - this.poly.setLatLngs(summary); - this.poly.editor.enable(); - this.poly.editor.reset(); + this.poly.setPoints(summary); this.updateMarks(); }; clearAll = (): void => { // this.poly.setLatLngs([]); - this.poly.editor.clear(); + // todo: implement + // this.poly.editor.clear(); + this.poly.setPoints([]); this.updateMarks(); }; diff --git a/src/parts/map.js b/src/parts/map.js deleted file mode 100644 index d4a6d58..0000000 --- a/src/parts/map.js +++ /dev/null @@ -1,81 +0,0 @@ -import L from 'leaflet'; - -// import 'leaflet-editable'; -// import 'leaflet.markercluster'; -// import 'leaflet.markercluster.webpack'; -import 'leaflet-geometryutil'; - -import { mapStyles } from '$constants/mapStyles'; - -import { stickers } from '$constants/stickers'; - -import { updateMarks } from '$utils/updater'; -import { bindPolyEvents, preparePoly } from '$utils/poly'; - -// В этой штуке мы храним точки и выноски, их связки и всё такое -const point_array = { - points: L.layerGroup(), - vectors: L.layerGroup(), - handles: L.layerGroup(), - pairs: {}, - point_to_id: {}, - id_to_point: {}, - savedata: {} -}; - -const points = L.layerGroup(); - -// let mode = 'none'; -const current_map_style = 'dgis'; - -// Интересные места; -// const places_layer; - -export const map = L.map('map', { - editable: true, - layers: [points, point_array.points, point_array.vectors, stickers.layers] -}).setView([55.0153275, 82.9071235], 13); - -map.editTools.skipMiddleMarkers = true; - -// Слой с интересными местами -// const places_layer = L.markerClusterGroup({maxClusterRadius: 20}); - -// const poly = preparePoly(map); // начинаем новую полилинию - -// const updateOverlays = () => updateMarks({ map, poly, km_marks }); -const updateOverlays = e => console.log(); - -const prepareMapLayer = provider => { - L.tileLayer(provider, { - attribution: 'Независимое Велосообщество', - maxNativeZoom: 18, - maxZoom: 18, - // minZoom: 11 - }).addTo(map); -}; - -const bindMapEvents = () => { - // при масштабировании карты масштабировать стрелки - // map.on('zoom', function (e) { - // $('.arr_mark > div').css('transform', 'scale(' + (map.getZoom()/13) + ')'); - // }); - - map.on('click', updateOverlays); -}; - -export const setMode = variant => { - mode = variant; -}; - -// prepareMap(); - -export const prepareMap = () => { - // Эта функция создаёт саму карту и наносит на неё маршруты в самом начале работы - // создаём объект с картой - map.doubleClickZoom.disable(); - prepareMapLayer(mapStyles[current_map_style]); - - bindMapEvents(); - // bindPolyEvents({ poly, map, updateOverlays, clearKmMarks }); -}; diff --git a/src/parts/poly.js b/src/parts/poly.js deleted file mode 100644 index 98a6043..0000000 --- a/src/parts/poly.js +++ /dev/null @@ -1,228 +0,0 @@ -import L from "leaflet"; -import { map } from "$utils/map"; -import { findDistance, middle_latlng } from "../js/common"; - -let poly = null; -export const km_marks = L.layerGroup(); - -// const updateOverlays = () => updateMarks({ map, poly, km_marks }); - -const getRouteArray = poly => poly.toGeoJSON().geometry.coordinates; - - -export const writeReduxData = ({ e, updatePolyCoords }) => { - const route = getRouteArray(poly); - const latlngs = route.map(([lng, lat]) => ({lat, lng})); - - updatePolyCoords({ latlngs }); -}; - -const endMarker = ({ end_latlng, length }) => L.marker( - [end_latlng[1], end_latlng[0]], - { - icon: L.divIcon( - { - html: `${length} км`, - className: 'end_mark' - } - ) - } -); - -const startMarker = ({ start_latlng, map }) => L.marker( - [start_latlng[1], start_latlng[0]], - { - icon: L.divIcon({ - html: `
`, - className: 'arr_mark' - }) - } -); - -export const updateMarks = () => { - km_marks.clearLayers(); - const route = getRouteArray(poly); - const latlngs = poly.getLatLngs(); - let start_latlng; - let end_latlng; - let i; - let rotation; - let middle; - let distance; - - if (route.length > 0) { - start_latlng = route[0]; - end_latlng = route[route.length - 1]; - km_marks.addLayer(startMarker({ start_latlng, map })); - if (route.length > 1) { - const segs = L.GeometryUtil.accumulatedLengths(poly); - const length = Math.round(segs[segs.length - 1] / 1000); - // end mark - km_marks.addLayer(endMarker({ end_latlng, length })); - - //and also length to panel: - // $('#text_route_length').text(length + 'км'); - - for (i = 1; i < latlngs.length; i += 1) { - rotation = L.GeometryUtil.bearing(latlngs[i - 1], latlngs[i]); - middle = middle_latlng(latlngs[i], latlngs[i - 1]); - distance = findDistance(latlngs[i - 1].lat, latlngs[i - 1].lng, latlngs[i].lat, latlngs[i].lng); - - if (distance > 1) { - km_marks.addLayer(L.marker([middle.lat, middle.lng], { icon: L.divIcon({ html: '
', className: 'arr_mark' }) })); - } - - } - } else { - // $('#text_route_length').text('0 км'); - } - } - // updatePolyCoords({ latlngs: route }); - // local_store_data(); -}; - -const clearKmMarks = () => km_marks.clearLayers(); - -const insertVertex = ({ e, updatePolyCoords }) => { - // Добавляет редактирующую ручку по щелчку - // если щелчок по кривой, а не по ручке И если ломаная в режиме редактирования. Иначе - перейти к редактированию - if (e.originalEvent.target.tagName === 'path' && poly.editor._enabled) { - // если щелкнуть по кривой во время редактирования, editable не должно рисовать новую точку - if (e.type === 'editable:drawing:click') e.cancel(); - - let latlngs = poly.getLatLngs(); // набор точек ломанной - let best = 10000; - let pos = []; // переменные для определения принадлежности точки отрезку на ломанной - - for(let i=0; ix1 && xx2 && (xx2)) - || - (x1 === x2 && Math.abs(x-x1)>0.001) - ) && ( - (y1y1 && yy2 && (yy2)) - || - (y1 === y2 && Math.abs(y-y1)>0.001) - ) - ) { - // если да, то проверяем, далеко ли точка от самого отрезка между двумя точками - let dx1 = x2 - x1; - let dy1 = y2 - y1; - let dx = x - x1; - let dy = y - y1; - let result = Math.abs((dx1 * dy) - (dx * dy1)); - if (result < best) { - // это - не очень-то точная функция. Но по клику она определяет, по какому отрезку мы кликнули - best = result; - pos = [i, i+1]; - } - } - } - // если точка найдена, добавляем её в отрезок - if (pos.length>1) { - // poly.editor.disable(); - latlngs.splice(pos[1],0,e.latlng); - poly.setLatLngs(latlngs); - poly.editor.initVertexMarkers(); - poly.editor.enable(); - poly.editor.continueForward(); - - writeReduxData({ e, updatePolyCoords }); - } - } else { - // Рисование! Очистим буфер отмен :-) - // redoBuffer = []; - // если ломаная не в режиме редактирования или если мы, всё-таки, кликнули по ручке, просто активируем редактор - // route_state('active'); - } -}; - -export const bindPolyEvents = ({ updatePolyCoords }) => { - // Если на карте что-то меняется, пересчитать километражи - map.editTools.addEventListener('editable:drawing:mouseup', updateMarks); - map.editTools.addEventListener('editable:vertex:dragend', updateMarks); - - map.editTools.addEventListener('editable:vertex:dragend', e => writeReduxData({ e, updatePolyCoords })); - map.editTools.addEventListener('editable:vertex:new', e => writeReduxData({ e, updatePolyCoords })); - map.editTools.addEventListener('editable:vertex:deleted', e => writeReduxData({ e, updatePolyCoords })); - - // Продолжить рисование после удаления точки - map.editTools.addEventListener('editable:vertex:deleted', e => { - poly.editor.continueForward(); - updateMarks(); - }); - - // Добавлять точек в полилинию по щелчку - map.editTools.addEventListener('editable:drawing:click', e => insertVertex({ e, updatePolyCoords })); - map.editTools.addEventListener('editable:drawing:clicked', () => updateMarks({ updatePolyCoords })); - - // Это для точек. При перетаскивании конца указателя тащим точку - // map.editTools.addEventListener('editable:vertex:drag', on_vertex_drag); - - // при перетаскивании ручек убирать все отметки километров - map.editTools.addEventListener('editable:vertex:dragstart', clearKmMarks); -}; - -export const updatePoly = (latlngs) => { - // const route = latlngs.map(([lat, lng]) => new L.latLng(lat, lng)); - if (!latlngs || latlngs.length < 2) return; - - poly.setLatLngs(createLatLngs(latlngs)); - poly.addTo(map); - poly.setStyle({ color: '#ff3333', weight: '5' }); - poly.editor.options.skipMiddleMarkers = true; - poly.editor.disable().enable(); - poly.editor.continueForward(); - // -}; - -export const createLatLngs = latlngs => latlngs.map(({ lat, lng }) => new L.LatLng(lat, lng)); - -const createPoly = () => { - const result = map.editTools.startPolyline(); - - result.editor.enable(); - - return result; -}; - -const restorePoly = latlngs => { - const result = L.polyline(createLatLngs(latlngs), { color: 'red' }).addTo(map); - - result.enableEdit().continueForward(); - result.editor.options.skipMiddleMarkers = true; - - result.editor.reset(); - - return result; -}; - -export const preparePoly = ({ updatePolyCoords, latlngs }) => { - map.addLayer(km_marks); - - poly = (latlngs && latlngs.length) - ? restorePoly(latlngs) - : createPoly(); - - updateMarks(); - - poly.setStyle({ color: '#ff3333', weight: '5' }); - - bindPolyEvents({ updatePolyCoords }); - - return poly; -}; - diff --git a/src/utils/geom.ts b/src/utils/geom.ts index 24ce715..c1f29fb 100644 --- a/src/utils/geom.ts +++ b/src/utils/geom.ts @@ -34,3 +34,14 @@ export const findDistance = (t1: number, n1: number, t2: number, n2: number): nu export const getLabelDirection = (angle: number): 'left' | 'right' => ( ((angle % Math.PI) >= -(Math.PI / 2) && (angle % Math.PI) <= (Math.PI / 2)) ? 'left' : 'right' ); + +// export const getPolyLength = (latlngs: ILatLng[]): number => latlngs.reduce((distance, latlng, i) => ( +// i < latlngs.length +// ? distance + findDistance(latlng.lat, latlng.lng, latlngs[i + 1].lat, latlngs[i + 1].lng) +// : distance +// ), 0); +export const getPolyLength = (latlngs: ILatLng[]): number => { + console.log('latlngs', latlngs); + + return 0; +};