stickers with ability to drag them

This commit is contained in:
Fedor Katurov 2018-08-15 16:18:49 +07:00
parent 5f30df6f48
commit b8434c32e7
19 changed files with 460 additions and 39 deletions

5
package-lock.json generated
View file

@ -7413,6 +7413,11 @@
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.3.3.tgz",
"integrity": "sha512-R9Cu5s0bdEXb9zh0nU17pV00IEvRh4xpWR9g1Oqz17jEDuMtkhy6DoYN1Q5WjvoDMRmq389zDVueUs9A2uWZSg=="
},
"leaflet-editable": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/leaflet-editable/-/leaflet-editable-1.1.0.tgz",
"integrity": "sha1-93dZekCoGic/KHtIn9D+XM1gyNA="
},
"levn": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",

View file

@ -47,6 +47,7 @@
"clean-webpack-plugin": "^0.1.9",
"history": "^4.7.2",
"leaflet": "^1.3.3",
"leaflet-editable": "^1.1.0",
"react": "^16.3.2",
"react-dom": "^16.3.2",
"react-hot-loader": "^4.1.1",

5
src/constants/modes.js Normal file
View file

@ -0,0 +1,5 @@
export const MODES = {
POLY: 'POLY',
STICKERS: 'STICKERS',
NONE: 'NONE',
};

View file

@ -0,0 +1,12 @@
// Стили карт
export const providers = {
'watercolor': 'http://stamen-tiles-{s}.a.ssl.fastly.net/watercolor/{z}/{x}/{y}.jpg',
'darq': 'http://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png',
'2gis': 'https://tile1.maps.2gis.com/tiles?x={x}&y={y}&z={z}&v=1',
'default': 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
'hot': 'http://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png',
'blank': 'http://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png',
'sat': 'http://mt0.google.com/vt/lyrs=s,h&x={x}&y={y}&z={z}',
'ymap': 'https://vec03.maps.yandex.net/tiles?l=map&v=17.04.16-0&x={x}&y={y}&z={z}&scale=1&lang=ru_RU',
'ysat': 'https://sat02.maps.yandex.net/tiles?l=sat&v=3.330.0&x={x}&y={y}&z={z}&lang=ru_RU'
};

43
src/constants/stickers.js Normal file
View file

@ -0,0 +1,43 @@
// Стикеры
import L from "leaflet";
export const stickers = {
'objects': {},
'layers': L.layerGroup(),
'savedata': {},
'layer_to_object': {},
'src': [
{off: 5, title: 'Александр 3', title_long: 'Парк Городское Начало', latlng: [55.01275, 82.92368]},
{off: 9, title: 'пл.Калинина', title_long: "пл.Калинина", latlng: [55.06019, 82.91316]},
{off: 4, title: 'Мост', title_long: 'Мост', latlng: [55.00511, 82.93073]},
{off: 7, title: 'Икея', title_long: "Парковка ТЦ Мега", latlng: [54.96494, 82.93138]},
{off: 8, title: 'Бугринка', title_long: "Та самая коса\n(культовое место Усталых Педалек)", latlng: [54.97626, 82.95703]},
{off: 10, title: 'ГПНТБ', title_long: "ГПНТБ", latlng: [55.01665, 82.94629]}, // второй ряд
{off: 18, title: 'Оперный', title_long: "Оперный театр", latlng: [55.03027, 82.92292]},
{off: 1, title: 'Лес', title_long: 'Берёзовая роща', latlng: [55.04572, 82.95]}, // первый ряд
{off: 19, title: 'Пусто', title_long: "Пока что пусто 1"},
{off: 20, title: 'Пусто', title_long: "Пока что пусто 2"}, // третий ряд
{off: 2, title: 'Трасса', title_long: 'Дорога'},
{off: 3, title: 'Курочка', title_long: 'Курочка'},
{off: 6, title: 'Палатка', title_long: 'Палаточный лагерь'},
{off: 11, title: 'Фастфуд', title_long: "Двухколёсное ожирение"},
{off: 12, title: 'Пивко', title_long: "В Питере - пить!"},
{off: 13, title: 'Шаварма', title_long: "Вкусная шаурма"},
{off: 14, title: 'Камни', title_long: "Кааааммммуушшшки"},
{off: 15, title: 'Болото', title_long: "Пошла ты,\nтрясина грёбаная!"},
{off: 16, title: 'Роджер', title_long: "Может не надо?"},
{off: 17, title: 'Какашка', title_long: "Нехорошее место"},
{off: 21, title: 'Старт', title_long: "Старт здесь"},
{off: 22, title: '1', title_long: "Первая точка"},
{off: 23, title: '2', title_long: "Вторая точка"},
{off: 24, title: '3', title_long: "Третья точка"},
{off: 25, title: '4', title_long: "Четвёртая точка"},
{off: 26, title: '5', title_long: "Пятая точка"},
{off: 27, title: '7', title_long: "Шестая точка"},
{off: 28, title: 'Финиш', title_long: "Финиш здесь"},
{off: 29, title: 'Осторожно!', title_long: "Осторожно!"},
{off: 30, title: 'Вопрос', title_long: "Что тут?"}
]
};

View file

@ -1,16 +1,50 @@
import React from 'react';
import { Map } from '$modules/map';
import { MapScreen } from "$styles/mapScreen";
import { Editor } from '$modules/Editor';
import { MapScreen } from '$styles/mapScreen';
import { ControlsScreen } from '$styles/controlsScreen';
import { MODES } from '$constants/modes';
export class App extends React.Component {
state = {
mode: 'none',
};
componentDidMount() {
this.map = new Map('map');
const container = 'map';
const { mode } = this.state;
this.editor = new Editor({
container,
mode,
setMode: this.setMode,
});
}
setMode = mode => {
this.setState({ mode });
};
startPolyMode = () => this.editor.changeMode(MODES.POLY);
startStickerMode = () => this.editor.changeMode(MODES.STICKERS);
render() {
const { mode } = this.state;
return (
<div>
<MapScreen />
<ControlsScreen>
<button onClick={this.startPolyMode}>
{mode === MODES.POLY && '-->'}{MODES.POLY}
</button>
<button onClick={this.startStickerMode}>
{mode === MODES.STICKERS && '-->'}{MODES.STICKERS}
</button>
</ControlsScreen>
</div>
);
}
};
}

15
src/index.html Normal file
View file

@ -0,0 +1,15 @@
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="initial-scale=1, maximum-scale=0.8">
<link rel="icon" href="favicon.png?d" type="image/png" />
<title>@MAP</title>
<link rel="shortcut icon" href="/favicon.png?wd" type="image/png">
<meta property="og:image" content="/misc/vk_preview.png" />
<meta content="/misc/vk_preview.png">
</head>
<body>
<section id="index"></section>
</body>

57
src/modules/Editor.js Normal file
View file

@ -0,0 +1,57 @@
import { Map } from '$modules/Map';
import { Poly } from "$modules/Poly";
import { MODES } from '$constants/modes';
import { Stickers } from '$modules/Stickers';
export class Editor {
constructor({
container,
mode,
setMode
}) {
this.map = new Map({ container });
this.poly = new Poly({ map: this.map.map });
this.stickers = new Stickers({ map: this.map.map });
this.setMode = setMode;
this.mode = mode;
this.switches = {
[MODES.POLY]: {
start: this.poly.continue,
stop: this.poly.stop,
}
};
this.clickHandlers = {
[MODES.STICKERS]: this.stickers.createOnClick
};
this.map.map.on('click', this.onClick);
}
changeMode = mode => {
if (this.mode === mode) {
this.disableMode(mode);
this.setMode(MODES.NONE);
this.mode = MODES.NONE;
} else {
this.disableMode(this.mode);
this.setMode(mode);
this.mode = mode;
this.enableMode(mode);
}
};
enableMode = mode => {
if (this.switches[mode] && this.switches[mode].start) this.switches[mode].start();
};
disableMode = mode => {
if (this.switches[mode] && this.switches[mode].stop) this.switches[mode].stop();
};
onClick = e => {
if (this.clickHandlers[this.mode]) this.clickHandlers[this.mode](e);
};
}

19
src/modules/Map.js Normal file
View file

@ -0,0 +1,19 @@
import L from 'leaflet';
import { providers } from '$constants/providers';
import 'leaflet/dist/leaflet.css';
import 'leaflet-editable';
export class Map {
constructor({ container }) {
this.map = L.map(container, { editable: true }).setView([55.0153275, 82.9071235], 13);
this.tileLayer = L.tileLayer(providers.default, {
attribution: 'Независимое Велосообщество',
maxNativeZoom: 18,
maxZoom: 18,
});
this.tileLayer.addTo(this.map);
}
}

70
src/modules/Poly.js Normal file
View file

@ -0,0 +1,70 @@
import L from "leaflet";
const polyStyle = { color: '#ff3333', weight: '5' };
export class Poly {
constructor({ map }) {
this.poly = L.polyline([], polyStyle);
this.latlngs = [];
this.poly.addTo(map);
this.map = map;
this.bindEvents();
}
updateMarks = () => {
const coords = this.poly.toGeoJSON().geometry.coordinates;
this.latlngs = (coords && coords.length && coords.map(([lng, lat]) => ({ lng, lat }))) || [];
};
bindEvents = () => {
// Если на карте что-то меняется, пересчитать километражи
this.map.editTools.addEventListener('editable:drawing:mouseup', this.updateMarks);
this.map.editTools.addEventListener('editable:vertex:dragend', this.updateMarks);
this.map.editTools.addEventListener('editable:vertex:deleted', this.updateMarks);
this.map.editTools.addEventListener('editable:vertex:new', this.updateMarks);
// После удаления точки - продолжить рисование
this.map.editTools.addEventListener('editable:vertex:deleted', this.continueForward);
//
// 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);
};
continue = () => {
if (this.latlngs && this.latlngs.length) {
this.poly.enableEdit().continueForward();
this.poly.editor.options.skipMiddleMarkers = true;
this.poly.editor.reset();
} else {
this.poly = this.map.editTools.startPolyline();
this.poly.setStyle(polyStyle);
}
};
stop = () => {
if (this.map.editTools) this.map.editTools.stopDrawing();
};
continueForward = () => {
if (!this.poly.editor) return;
this.poly.editor.continueForward();
};
}

49
src/modules/Sticker.js Normal file
View file

@ -0,0 +1,49 @@
import L from 'leaflet';
import 'leaflet-editable';
import { DomMarker } from '$utils/leafletDomMarkers';
export class Sticker {
constructor({ latlng, deleteSticker }) {
this.isDragging = false;
this.deleteSticker = deleteSticker;
this.element = document.createElement('div');
const stickerImage = document.createElement('div');
stickerImage.innerHTML = '<div class="sticker-label" />';
const stickerArrow = document.createElement('div');
stickerArrow.innerHTML = '<div class="sticker-arrow" />';
this.element.appendChild(stickerArrow);
this.element.appendChild(stickerImage);
const marker = new DomMarker({
element: this.element,
});
this.sticker = L.marker(latlng, { icon: marker });
stickerImage.addEventListener('mousedown', this.onDragStart);
stickerImage.addEventListener('mouseup', this.onDragStop);
//
// this.sticker.addEventListener('click', this.onDelete);
}
onDelete = () => {
if (!this.isDragging) this.deleteSticker(this);
};
onDragStart = e => {
this.isDragging = true;
this.sticker.disableEdit();
console.log('dragStart');
};
onDragStop = e => {
this.isDragging = false;
this.sticker.enableEdit();
console.log('dragStop');
}
}

40
src/modules/Stickers.js Normal file
View file

@ -0,0 +1,40 @@
import L from 'leaflet';
import { Sticker } from '$modules/Sticker';
export class Stickers {
constructor({ map }) {
this.map = map;
this.layer = L.layerGroup();
this.stickers = [];
this.layer.addTo(this.map);
}
createOnClick = e => {
if (!e || !e.latlng) return;
const { latlng } = e;
this.createSticker({ latlng });
};
createSticker = ({ latlng }) => {
const sticker = new Sticker({
latlng,
deleteSticker: this.deleteStickerByReference,
});
this.stickers.push(sticker);
sticker.sticker.addTo(this.map);
sticker.sticker.enableEdit();
};
deleteStickerByReference = ref => {
const index = this.stickers.indexOf(ref);
if (index < 0) return;
this.map.removeLayer(ref.sticker);
this.stickers.splice(index, 1);
};
}

View file

@ -1,22 +0,0 @@
import L from "leaflet";
import { providers } from "$constants/providers";
import 'leaflet/dist/leaflet.css';
export class Map {
constructor(container) {
this.map = L.map(container, {
editable: true,
layers: [
]
}).setView([55.0153275, 82.9071235], 13);
this.tileLayer = L.tileLayer(providers.default, {
attribution: 'Независимое Велосообщество',
maxNativeZoom: 18,
maxZoom: 18,
}).addTo(this.map);
}
}

View file

@ -1,16 +1,16 @@
import L from "leaflet";
import L from 'leaflet';
import 'leaflet-editable';
import 'leaflet.markercluster';
import 'leaflet.markercluster.webpack';
import 'leaflet-geometryutil';
import { mapStyles } from "$constants/mapStyles";
import { mapStyles } from '$constants/mapStyles';
import { stickers } from "$constants/stickers";
import { stickers } from '$constants/stickers';
import { updateMarks } from "$utils/updater";
import { bindPolyEvents, preparePoly } from "$utils/poly";
import { updateMarks } from '$utils/updater';
import { bindPolyEvents, preparePoly } from '$utils/poly';
// В этой штуке мы храним точки и выноски, их связки и всё такое
const point_array = {
@ -25,8 +25,8 @@ const point_array = {
const points = L.layerGroup();
let mode = "none";
let current_map_style = 'default';
let mode = 'none';
const current_map_style = 'default';
// Интересные места;
// const places_layer;
@ -56,7 +56,6 @@ const prepareMapLayer = provider => {
};
const bindMapEvents = () => {
// при масштабировании карты масштабировать стрелки
// map.on('zoom', function (e) {
// $('.arr_mark > div').css('transform', 'scale(' + (map.getZoom()/13) + ')');

View file

@ -0,0 +1,13 @@
import styled from 'styled-components';
export const ControlsScreen = styled.div`
position: fixed;
right: 10px;
bottom: 10px;
height: 48px;
min-width: 120px;
background: #333333;
border-radius: 2px;
z-index: 2;
color: white;
`;

60
src/styles/mapScreen.js Normal file
View file

@ -0,0 +1,60 @@
import styled, { css } from 'styled-components';
const vertexMixin = css`
.leaflet-vertex-icon, .leaflet-middle-icon {
border-radius: 10px;
opacity :1;
border: none;
width: 16px !important;
height: 16px !important;margin-left:-8px !important;margin-top:-8px !important;
background: transparent;
}
.leaflet-vertex-icon::after, .leaflet-middle-icon::after {
content: ' ';
position:absolute;top:4px;left:4px;width:8px;height:8px;
background:white;border-radius: 8px;transform:scale(1);
transition:transform 150ms;
}
.leaflet-vertex-icon:hover, .leaflet-middle-icon:hover {
opacity: 1 !important;
}
.leaflet-vertex-icon:hover::after, .leaflet-middle-icon:hover::after,
.leaflet-vertex-icon:active::after, .leaflet-middle-icon:active::after {
transform: scale(2);
box-shadow: #999 0 0 5px 2px;
}
`;
const stickers = css`
.sticker-label {
width: 48px;
height: 48px;
position: absolute;
background: white;
border-radius: 32px;
left: 0;
top: 0;
}
.sticker-arrow {
width: 24px;
height: 24px;
position: absolute;
background: red;
}
`;
export const MapScreen = styled.div.attrs({ id: 'map' })`
width: 100%;
height: 100%;
position: absolute;
z-index: 1;
left: 0;
top: 0;
${vertexMixin}
${stickers}
`;

View file

@ -0,0 +1,21 @@
import L from 'leaflet';
export const DomMarker = L.DivIcon.extend({
initialize: function (options) {
this.options = options;
},
options: {
element: null // a initialized DOM element
// same options as divIcon except for html
},
createIcon: function() {
const { html, element } = this.options;
this._setIconStyles(element, 'icon');
return element;
}
});

View file

@ -13,7 +13,7 @@ const { join } = require('path');
const htmlPlugin = new HtmlWebPackPlugin({
template: './src/index.html',
filename: './index.html',
title: 'Ether Corners',
title: 'Map',
hash: false,
});