InteractivePoly: working marker dragging

This commit is contained in:
muerwre 2019-02-21 18:15:20 +07:00
parent 4dfba4839d
commit edbd911de6
6 changed files with 277 additions and 370 deletions

View file

@ -358,7 +358,8 @@ export class Editor {
this.setInitialData(); this.setInitialData();
this.owner = { id }; 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(); this.stickers.startEditing();
}; };

View file

@ -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
*/

View file

@ -1,12 +1,12 @@
import { Map, LayerGroup } from 'leaflet'; import { Map, LayerGroup, Polyline } from 'leaflet';
import 'leaflet-geometryutil';
import { EditablePolyline } from '$utils/EditablePolyline'; import { EditablePolyline } from '$utils/EditablePolyline';
import { simplify } from '$utils/simplify'; import { simplify } from '$utils/simplify';
import { findDistance, middleCoord } from '$utils/geom'; import { findDistance, getPolyLength, middleCoord } from '$utils/geom';
import { CLIENT } from '$config/frontend'; import { CLIENT } from '$config/frontend';
import { MODES } from '$constants/modes'; import { MODES } from '$constants/modes';
import { editor, Editor } from "$modules/Editor"; import { editor, Editor } from "$modules/Editor";
import { ILatLng } from "$modules/Stickers"; import { ILatLng } from "$modules/Stickers";
import { InteractivePoly } from "$modules/InteractivePoly";
const polyStyle = { const polyStyle = {
color: 'url(#activePathGradient)', color: 'url(#activePathGradient)',
@ -27,22 +27,28 @@ export class Poly {
constructor({ constructor({
map, routerMoveStart, lockMapClicks, setDistance, triggerOnChange, editor, map, routerMoveStart, lockMapClicks, setDistance, triggerOnChange, editor,
}: Props) { }: Props) {
this.poly = new EditablePolyline([], { this.poly = new InteractivePoly([], {
...polyStyle, color: 'url(#activePathGradient)',
maxMarkers: 100, weight: 6,
maxMarkers: 300,
onPointsSet: this.updateMarks, });
onMarkerDragEnd: this.updateMarks, // this.poly = new EditablePolyline([], {
onPointAdded: this.updateMarks, // ...polyStyle,
onPointDropped: this.updateMarks, // maxMarkers: 100,
onContinueDrawing: this.setModeOnDrawing, //
// onPointsSet: this.updateMarks,
onMarkersHide: () => editor.setMarkersShown(false), // onMarkerDragEnd: this.updateMarks,
onMarkersShow: () => editor.setMarkersShown(true), // onPointAdded: this.updateMarks,
}).addTo(map); // onPointDropped: this.updateMarks,
// onContinueDrawing: this.setModeOnDrawing,
//
// onMarkersHide: () => editor.setMarkersShown(false),
// onMarkersShow: () => editor.setMarkersShown(true),
// }).addTo(map);
this.poly.addTo(map); this.poly.addTo(map);
this.poly._reloadPolyline(); // todo: uncomment
// this.poly._reloadPolyline();
this.editor = editor; this.editor = editor;
this.map = map; this.map = map;
@ -61,45 +67,46 @@ export class Poly {
drawArrows = () => { drawArrows = () => {
// todo: fix this // todo: fix this
// this.arrows.clearLayers(); this.arrows.clearLayers();
// const { latlngs } = this; const { latlngs } = this;
//
// if (!latlngs || latlngs.length <= 1) return; if (!latlngs || latlngs.length <= 1) return;
//
// latlngs.forEach((latlng, i) => { latlngs.forEach((latlng, i) => {
// if (i === 0) return; if (i === 0) return;
//
// const mid = middleCoord(latlngs[i], latlngs[i - 1]); 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); const dist = findDistance(latlngs[i - 1].lat, latlngs[i - 1].lng, latlngs[i].lat, latlngs[i].lng);
//
// if (dist <= 1) return; if (dist <= 1) return;
//
// const slide = new Polyline( const slide = new Polyline(
// [ [
// latlngs[i - 1], latlngs[i - 1],
// [mid.lat, mid.lng] [mid.lat, mid.lng]
// ], ],
// { color: 'none', weight: CLIENT.STROKE_WIDTH } { color: 'none', weight: CLIENT.STROKE_WIDTH }
// ).addTo(this.arrows); ).addTo(this.arrows) as any;
//
// // todo: uncomment and fix this: // todo: uncomment and fix this:
// // slide._path.setAttribute('marker-end', 'url(#long-arrow)'); slide._path.setAttribute('marker-end', 'url(#long-arrow)');
// }); });
}; };
updateMarks = () => { updateMarks = () => {
// todo: fix this const coords = this.poly.toGeoJSON().geometry.coordinates;
// const coords = this.poly.toGeoJSON().geometry.coordinates;
// // const meters = (this.latlngs && this.latlngs.length > 1 && getPolyLength(this.latlngs)) || 0;
// const meters = (this.poly && (L.GeometryUtil.length(this.poly) / 1000)) || 0; // const kilometers = (meters && Number(meters.toFixed(1))) || 0;
// const kilometers = (meters && meters.toFixed(1)) || 0;
// const kilometers = ((this.latlngs && this.latlngs.length > 1 && getPolyLength(this.latlngs)) || 0);
// this.setTotalDist(kilometers);
// this.routerMoveStart(); this.setDistance(parseFloat(kilometers.toFixed(2)));
// this.routerMoveStart();
// this.drawArrows(); // <-- uncomment
// this.drawArrows(); // <-- uncomment
// if (coords.length > 1) this.triggerOnChange();
if (coords.length > 1) this.triggerOnChange();
}; };
preventMissClicks = (e): void => { preventMissClicks = (e): void => {
@ -113,15 +120,18 @@ export class Poly {
}; };
continue = (): void => { continue = (): void => {
this.poly.editor.continueForward(); // todo: implement
// this.poly.editor.continueForward();
}; };
stop = (): void => { stop = (): void => {
if (this.poly) this.poly.editor.stopDrawing(); // todo: implement
// if (this.poly) this.poly.editor.stopDrawing();
}; };
continueForward = (): void => { continueForward = (): void => {
this.poly.continueForward(); // todo: implement
// this.poly.continueForward();
}; };
lockMap = (): void => { lockMap = (): void => {
@ -129,7 +139,10 @@ export class Poly {
}; };
setPoints = (latlngs: Array<ILatLng>): void => { setPoints = (latlngs: Array<ILatLng>): void => {
console.log('setP');
if (!latlngs || latlngs.length <= 1) return; if (!latlngs || latlngs.length <= 1) return;
// todo: implement
this.poly.setPoints(latlngs); this.poly.setPoints(latlngs);
}; };
@ -141,15 +154,15 @@ export class Poly {
...simplified, ...simplified,
]; ];
this.poly.setLatLngs(summary); this.poly.setPoints(summary);
this.poly.editor.enable();
this.poly.editor.reset();
this.updateMarks(); this.updateMarks();
}; };
clearAll = (): void => { clearAll = (): void => {
// this.poly.setLatLngs([]); // this.poly.setLatLngs([]);
this.poly.editor.clear(); // todo: implement
// this.poly.editor.clear();
this.poly.setPoints([]);
this.updateMarks(); this.updateMarks();
}; };

View file

@ -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 });
};

View file

@ -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}&nbsp;км`,
className: 'end_mark'
}
)
}
);
const startMarker = ({ start_latlng, map }) => L.marker(
[start_latlng[1], start_latlng[0]],
{
icon: L.divIcon({
html: `<div style="transform: scale(${(map.getZoom() / 13)});"><div class="arr_start"></div></div>`,
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: '<div style="transform: scale(' + (map.getZoom() / 13) + ');"><img src="misc/arr.png" style="transform: translateX(-4px) translateY(-4px) rotate(' + (270 + rotation) + 'deg);"></div>', 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; i<latlngs.length-1; i++) {
// Дальше определяем, лежит ли точка на отрезке ломаной перебором этих отрезков
const x = e.latlng['lat'];
const x1 = latlngs[i]['lat'];
const x2 = latlngs[i+1]['lat'];
const y = e.latlng['lng'];
const y1 = latlngs[i]['lng'];
const y2 = latlngs[i+1]['lng'];
// эта странная конструкция определяет, лежит ли вообще точка между двумя соседями на отрезке
if ((
(x1<x2 && (x>x1 && x<x2))
||
(x1>x2 && (x<x1 && x>x2))
||
(x1 === x2 && Math.abs(x-x1)>0.001)
) && (
(y1<y2 && (y>y1 && y<y2))
||
(y1>y2 && (y<y1 && y>y2))
||
(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;
};

View file

@ -34,3 +34,14 @@ export const findDistance = (t1: number, n1: number, t2: number, n2: number): nu
export const getLabelDirection = (angle: number): 'left' | 'right' => ( export const getLabelDirection = (angle: number): 'left' | 'right' => (
((angle % Math.PI) >= -(Math.PI / 2) && (angle % Math.PI) <= (Math.PI / 2)) ? '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;
};