diff --git a/package-lock.json b/package-lock.json index 1f57e5c..241d7ff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index bad5f57..d57b7dd 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/constants/modes.js b/src/constants/modes.js new file mode 100644 index 0000000..efa6a0c --- /dev/null +++ b/src/constants/modes.js @@ -0,0 +1,5 @@ +export const MODES = { + POLY: 'POLY', + STICKERS: 'STICKERS', + NONE: 'NONE', +}; diff --git a/src/constants/providers.js b/src/constants/providers.js new file mode 100644 index 0000000..7035606 --- /dev/null +++ b/src/constants/providers.js @@ -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' +}; diff --git a/src/constants/stickers.js b/src/constants/stickers.js new file mode 100644 index 0000000..385d040 --- /dev/null +++ b/src/constants/stickers.js @@ -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: "Что тут?"} + ] +}; diff --git a/src/containers/App.jsx b/src/containers/App.jsx index a4a55d8..0defa4e 100644 --- a/src/containers/App.jsx +++ b/src/containers/App.jsx @@ -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 ( - +
+ + + + + +
); } -}; +} diff --git a/src/index.html b/src/index.html new file mode 100644 index 0000000..444bdef --- /dev/null +++ b/src/index.html @@ -0,0 +1,15 @@ + + + + + + + @MAP + + + + + + +
+ diff --git a/src/modules/Editor.js b/src/modules/Editor.js new file mode 100644 index 0000000..3ed571c --- /dev/null +++ b/src/modules/Editor.js @@ -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); + }; +} diff --git a/src/modules/Map.js b/src/modules/Map.js new file mode 100644 index 0000000..ee001d2 --- /dev/null +++ b/src/modules/Map.js @@ -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); + } +} diff --git a/src/modules/Poly.js b/src/modules/Poly.js new file mode 100644 index 0000000..dd7fabb --- /dev/null +++ b/src/modules/Poly.js @@ -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(); + }; +} diff --git a/src/modules/Sticker.js b/src/modules/Sticker.js new file mode 100644 index 0000000..3714514 --- /dev/null +++ b/src/modules/Sticker.js @@ -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 = '
'; + + const stickerArrow = document.createElement('div'); + stickerArrow.innerHTML = '
'; + + 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'); + } +} diff --git a/src/modules/Stickers.js b/src/modules/Stickers.js new file mode 100644 index 0000000..532c7dc --- /dev/null +++ b/src/modules/Stickers.js @@ -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); + }; +} diff --git a/src/modules/map.js b/src/modules/map.js deleted file mode 100644 index 5eface5..0000000 --- a/src/modules/map.js +++ /dev/null @@ -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); - } -} diff --git a/src/parts/map.js b/src/parts/map.js index 462bd7f..8bffded 100644 --- a/src/parts/map.js +++ b/src/parts/map.js @@ -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,15 +25,15 @@ 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; export const map = L.map('map', { editable: true, - layers: [ points, point_array.points, point_array.vectors, stickers.layers ] + layers: [points, point_array.points, point_array.vectors, stickers.layers] }).setView([55.0153275, 82.9071235], 13); map.editTools.skipMiddleMarkers = true; @@ -51,12 +51,11 @@ const prepareMapLayer = provider => { attribution: 'Независимое Велосообщество', maxNativeZoom: 18, maxZoom: 18, - //minZoom: 11 + // minZoom: 11 }).addTo(map); }; const bindMapEvents = () => { - // при масштабировании карты масштабировать стрелки // map.on('zoom', function (e) { // $('.arr_mark > div').css('transform', 'scale(' + (map.getZoom()/13) + ')'); diff --git a/src/parts/poly.js b/src/parts/poly.js index 6e26768..beac0c0 100644 --- a/src/parts/poly.js +++ b/src/parts/poly.js @@ -200,7 +200,7 @@ const createPoly = () => { }; const restorePoly = latlngs => { - const result = L.polyline(createLatLngs(latlngs), {color: 'red'}).addTo(map); + const result = L.polyline(createLatLngs(latlngs), { color: 'red' }).addTo(map); result.enableEdit().continueForward(); result.editor.options.skipMiddleMarkers = true; diff --git a/src/styles/controlsScreen.js b/src/styles/controlsScreen.js new file mode 100644 index 0000000..8d07ef4 --- /dev/null +++ b/src/styles/controlsScreen.js @@ -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; +`; diff --git a/src/styles/mapScreen.js b/src/styles/mapScreen.js new file mode 100644 index 0000000..5fe381f --- /dev/null +++ b/src/styles/mapScreen.js @@ -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} +`; diff --git a/src/utils/leafletDomMarkers.js b/src/utils/leafletDomMarkers.js new file mode 100644 index 0000000..19b27e7 --- /dev/null +++ b/src/utils/leafletDomMarkers.js @@ -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; + } +}); + diff --git a/webpack.config.js b/webpack.config.js index 25a3c5a..9a81f1d 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -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, });