From 882c4da93e6cbb683a518adac5887fd8261d5c05 Mon Sep 17 00:00:00 2001 From: Gautier Pelloux-Prayer Date: Tue, 6 Aug 2019 14:27:29 +0200 Subject: [PATCH] Add markers POI layer --- .gitignore | 1 + css/style.css | 3 + js/control/Export.js | 5 +- js/index.js | 44 ++++++++---- js/plugin/POIMarkers.js | 144 ++++++++++++++++++++++++++++++++++++++++ js/router/BRouter.js | 48 ++++++++++++-- locales/en.json | 6 +- package.json | 8 +++ yarn.lock | 5 ++ 9 files changed, 244 insertions(+), 20 deletions(-) create mode 100644 js/plugin/POIMarkers.js diff --git a/.gitignore b/.gitignore index 552abf86..3132bdfd 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ nbproject/ /dist brouter-web.*.zip yarn-error.log +package-lock.json diff --git a/css/style.css b/css/style.css index c0ec3592..05ec6240 100644 --- a/css/style.css +++ b/css/style.css @@ -167,6 +167,9 @@ input#trackname:focus:invalid { .routing-draw-enabled { cursor: crosshair; } +.pois-draw-enabled { + cursor: cell; +} #map { /* center error message horizontally */ diff --git a/js/control/Export.js b/js/control/Export.js index d876585f..97e89c12 100644 --- a/js/control/Export.js +++ b/js/control/Export.js @@ -1,8 +1,9 @@ BR.Export = L.Class.extend({ latLngs: [], - initialize: function(router) { + initialize: function(router, pois) { this.router = router; + this.pois = pois; this.exportButton = $('#exportButton'); var trackname = (this.trackname = document.getElementById('trackname')); this.tracknameAllowedChars = BR.conf.tracknameAllowedChars; @@ -38,7 +39,7 @@ BR.Export = L.Class.extend({ var name = encodeURIComponent(exportForm['trackname'].value); var includeWaypoints = exportForm['include-waypoints'].checked; - var uri = this.router.getUrl(this.latLngs, format, name, includeWaypoints); + var uri = this.router.getUrl(this.latLngs, this.pois.getMarkers(), format, name, includeWaypoints); var evt = document.createEvent('MouseEvents'); evt.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); diff --git a/js/index.js b/js/index.js index acca70e6..ef8582d9 100644 --- a/js/index.js +++ b/js/index.js @@ -32,7 +32,7 @@ sidebar, drawButton, deleteRouteButton, - drawToolbar, + pois, urlHash, saveWarningShown = false; @@ -93,7 +93,7 @@ function() { bootbox.prompt({ size: 'small', - title: i18next.t('map.delete-route-nogos'), + title: i18next.t('map.clear-route'), inputType: 'checkbox', inputOptions: [ { @@ -103,6 +103,10 @@ { text: i18next.t('map.delete-nogo-areas'), value: 'nogo' + }, + { + text: i18next.t('map.delete-pois'), + value: 'pois' } ], value: ['route'], @@ -114,13 +118,16 @@ if (result.indexOf('nogo') !== -1) { nogos.clear(); } + if (result.indexOf('pois') !== -1) { + pois.clear(); + } onUpdate(); urlHash.onMapMove(); } } }); }, - i18next.t('map.delete-route-nogos') + i18next.t('map.clear-route') ); function updateRoute(evt) { @@ -160,7 +167,6 @@ } else { stats = new BR.TrackStats(); } - exportRoute = new BR.Export(router); elevation = new BR.Elevation(); profile = new BR.Profile(); @@ -205,6 +211,12 @@ styles: BR.conf.routingStyles }); + pois = new BR.PoiMarkers({ + routing: routing + }); + + exportRoute = new BR.Export(router, pois); + routing.on('routing:routeWaypointEnd routing:setWaypointsEnd', function(evt) { search.clear(); onUpdate(evt && evt.err); @@ -245,6 +257,8 @@ routing.addTo(map); elevation.addBelow(map); + pois.addTo(map); + sidebar = BR.sidebar({ defaultTabId: BR.conf.transit ? 'tab_itinerary' : 'tab_profile', listeningTabs: { @@ -257,13 +271,7 @@ } nogos.addTo(map); - L.easyBar([ - drawButton, - reverseRouteButton, - nogos.getButton(), - deletePointButton, - deleteRouteButton - ]).addTo(map); + L.easyBar([drawButton, reverseRouteButton, nogos.getButton(), deletePointButton, deleteRouteButton]).addTo(map); nogos.preventRoutePointOnCreate(routing); if (BR.keys.strava) { @@ -304,6 +312,7 @@ return p; }; if (url == null) return; + var opts = router.parseUrlParams(url2params(url)); router.setOptions(opts); routingOptions.setOptions(opts); @@ -315,6 +324,10 @@ routing.clear(); routing.setWaypoints(opts.lonlats); } + + if (opts.pois) { + pois.setMarkers(opts.pois); + } }; var onInvalidHashChangeCb = function(params) { @@ -327,9 +340,13 @@ // do not initialize immediately urlHash = new L.Hash(null, null); + // this callback is used to append anything in URL after L.Hash wrote #map=zoom/lat/lng/layer urlHash.additionalCb = function() { - var url = router.getUrl(routing.getWaypoints(), null).substr('brouter?'.length + 1); + var url = router.getUrl(routing.getWaypoints(), pois.getMarkers(), null).substr('brouter?'.length + 1); + + // by default brouter use | as separator. To make URL more human-readable, we remplace them with ; for users url = url.replace(/\|/g, ';'); + return url.length > 0 ? '&' + url : null; }; urlHash.onHashChangeCb = onHashChangeCb; @@ -346,6 +363,7 @@ routingOptions.on('update', urlHash.onMapMove, urlHash); nogos.on('update', urlHash.onMapMove, urlHash); + pois.on('update', urlHash.onMapMove, urlHash); // waypoint add, move, delete (but last) routing.on('routing:routeWaypointEnd', urlHash.onMapMove, urlHash); // delete last waypoint @@ -394,6 +412,8 @@ }); } + L.AwesomeMarkers.Icon.prototype.options.prefix = 'fa'; + i18next .use(window.i18nextXHRBackend) .use(window.i18nextBrowserLanguageDetector) diff --git a/js/plugin/POIMarkers.js b/js/plugin/POIMarkers.js new file mode 100644 index 00000000..6ae7f084 --- /dev/null +++ b/js/plugin/POIMarkers.js @@ -0,0 +1,144 @@ +BR.PoiMarkers = L.Control.extend({ + markersLayer: null, + + options: { + routing: null, + shortcut: { + draw: { + enable: 80, // char code for 'p' + disable: 27 // char code for 'ESC' + } + } + }, + + onAdd: function(map) { + var self = this; + + this.map = map; + this.markersLayer = L.layerGroup([]).addTo(map); + + this.drawButton = L.easyButton({ + states: [ + { + stateName: 'activate-poi', + icon: 'fa-hand-o-right', + onClick: function() { + self.draw(true); + }, + title: i18next.t('map.draw-poi-start') + }, + { + stateName: 'deactivate-poi', + icon: 'fa-hand-o-right active', + onClick: function() { + self.draw(false); + }, + title: i18next.t('map.draw-poi-stop') + } + ] + }).addTo(map); + + map.on('routing:draw-start', function() { + self.draw(false); + }); + + var container = new L.DomUtil.create('div'); + // keys not working when map container does not have focus, use document instead + L.DomEvent.removeListener(container, 'keyup', this._keyupListener); + L.DomEvent.addListener(document, 'keyup', this._keyupListener, this); + + return container; + }, + + draw: function(enable) { + this.drawButton.state(enable ? 'deactivate-poi' : 'activate-poi'); + if (enable) { + this.options.routing.draw(false); + this.map.on('click', this.onMapClick, this); + L.DomUtil.addClass(this.map.getContainer(), 'pois-draw-enabled'); + } else { + this.map.off('click', this.onMapClick, this); + L.DomUtil.removeClass(this.map.getContainer(), 'pois-draw-enabled'); + } + }, + + _keyupListener: function(e) { + // Suppress shortcut handling when a text input field is focussed + if (document.activeElement.type == 'text' || document.activeElement.type == 'textarea') { + return; + } + if (e.keyCode === this.options.shortcut.draw.disable) { + this.draw(false); + } else if (e.keyCode === this.options.shortcut.draw.enable) { + this.draw(true); + } + }, + + onMapClick: function(e) { + var self = this; + bootbox.prompt({ + title: i18next.t('map.enter-poi-name'), + callback: function(result) { + if (result !== null) { + self.addMarker(e.latlng, result); + } + } + }); + }, + + addMarker: function(latlng, name) { + var sanitizeHTML = function(str) { + var temp = document.createElement('div'); + temp.textContent = str; + return temp.innerHTML; + }; + + var icon = L.AwesomeMarkers.icon({ + icon: 'star', + markerColor: 'cadetblue' + }); + var content = sanitizeHTML(name) + '
'; + content += ""; + + var self = this; + var marker = L.marker(latlng, { icon: icon, draggable: true, name: name }) + .bindPopup(content) + .on('dragend', function() { + self.fire('update'); + }) + .on('popupopen', function() { + $('#remove-poi-marker').on('click', function(e) { + self.markersLayer.removeLayer(marker); + e.preventDefault(); + self.fire('update'); + }); + }) + .addTo(this.markersLayer); + }, + + clear: function() { + this.markersLayer.clearLayers(); + }, + + setMarkers: function(latLngNames) { + this.clear(); + + if (!latLngNames) return; + + for (var i = 0; i < latLngNames.length; i++) { + var r = latLngNames[i]; + this.addMarker(r.latlng, r.name); + } + }, + + getMarkers: function() { + return this.markersLayer.getLayers().map(function(it) { + return { + latlng: it._latlng, + name: it.options.name + }; + }); + } +}); + +BR.PoiMarkers.include(L.Evented.prototype); diff --git a/js/router/BRouter.js b/js/router/BRouter.js index 71469fba..878bf960 100644 --- a/js/router/BRouter.js +++ b/js/router/BRouter.js @@ -38,7 +38,7 @@ L.BRouter = L.Class.extend({ L.setOptions(this, options); }, - getUrlParams: function(latLngs, format) { + getUrlParams: function(latLngs, pois, format) { params = {}; if (this._getLonLatsString(latLngs) != null) params.lonlats = this._getLonLatsString(latLngs); @@ -53,6 +53,8 @@ L.BRouter = L.Class.extend({ if (this.options.profile != null) params.profile = this.options.profile; + if (pois && this._getLonLatsNameString(pois) != null) params.pois = this._getLonLatsNameString(pois); + params.alternativeidx = this.options.alternative; if (format != null) { @@ -91,15 +93,18 @@ L.BRouter = L.Class.extend({ if (params.profile) { opts.profile = params.profile; } + if (params.pois) { + opts.pois = this._parseLonLatNames(params.pois); + } return opts; }, - getUrl: function(latLngs, format, trackname, exportWaypoints) { - var urlParams = this.getUrlParams(latLngs, format); - + getUrl: function(latLngs, pois, format, trackname, exportWaypoints) { + var urlParams = this.getUrlParams(latLngs, pois, format); var args = []; if (urlParams.lonlats != null && urlParams.lonlats.length > 0) args.push(L.Util.template('lonlats={lonlats}', urlParams)); + if (urlParams.pois != null && urlParams.pois.length > 0) args.push(L.Util.template('pois={pois}', urlParams)); if (urlParams.nogos != null) args.push(L.Util.template('nogos={nogos}', urlParams)); if (urlParams.polylines != null) args.push(L.Util.template('polylines={polylines}', urlParams)); if (urlParams.polygons != null) args.push(L.Util.template('polygons={polygons}', urlParams)); @@ -120,7 +125,7 @@ L.BRouter = L.Class.extend({ }, getRoute: function(latLngs, cb) { - var url = this.getUrl(latLngs, 'geojson'), + var url = this.getUrl(latLngs, null, 'geojson'), xhr = new XMLHttpRequest(); if (!url) { @@ -231,6 +236,39 @@ L.BRouter = L.Class.extend({ return lonlats; }, + _getLonLatsNameString: function(latLngNames) { + var s = ''; + for (var i = 0; i < latLngNames.length; i++) { + s += this._formatLatLng(latLngNames[i].latlng); + s += L.BRouter.NUMBER_SEPARATOR; + s += encodeURIComponent(latLngNames[i].name); + + if (i < latLngNames.length - 1) { + s += L.BRouter.GROUP_SEPARATOR; + } + } + return s; + }, + + _parseLonLatNames: function(s) { + var groups, + part, + lonlatnames = []; + + if (!s) { + return lonlatnames; + } + + groups = s.split(L.BRouter.GROUP_SEPARATOR); + for (var i = 0; i < groups.length; i++) { + // lng,lat,name + part = groups[i].split(L.BRouter.NUMBER_SEPARATOR); + lonlatnames.push({ latlng: L.latLng(part[1], part[0]), name: decodeURIComponent(part[2]) }); + } + + return lonlatnames; + }, + _getNogosString: function(nogos) { var s = ''; for (var i = 0, circle; i < nogos.length; i++) { diff --git a/locales/en.json b/locales/en.json index e4a6a10c..4fce696d 100644 --- a/locales/en.json +++ b/locales/en.json @@ -76,14 +76,18 @@ "map": { "attribution-osm-long": "OpenStreetMap contributors", "attribution-osm-short": "OpenStreetMap", + "clear-route": "Clear route data", "copyright": "Copyright", "cycling": "Cycling", "delete-last-point": "Delete last point", "delete-nogo-areas": "Delete all no-go areas", + "delete-pois": "Delete all points of interest", "delete-route": "Delete route", - "delete-route-nogos": "Delete route and nogos", + "draw-poi-start": "Draw points of interest (P key)", + "draw-poi-stop": "Stop drawing points of interest (ESC key)", "draw-route-start": "Draw route (D key)", "draw-route-stop": "Stop drawing route (ESC key)", + "enter-poi-name": "Enter Point of Interest name", "hikebike-hillshading": "Hillshading", "hiking": "Hiking", "layer": { diff --git a/package.json b/package.json index dc111638..c982062c 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "leaflet-routing": "nrenner/leaflet-routing#dev", "leaflet-sidebar-v2": "nrenner/leaflet-sidebar-v2#dev", "leaflet-triangle-marker": "^1.0.1", + "leaflet.awesome-markers": "^2.0.5", "leaflet.locatecontrol": "^0.60.0", "leaflet.snogylop": "^0.4.0", "leaflet.stravasegments": "2.3.2", @@ -175,6 +176,13 @@ "leaflet.stravasegments": { "main": "dist/index.js" }, + "leaflet.awesome-markers": { + "main": [ + "dist/leaflet.awesome-markers.js", + "dist/leaflet.awesome-markers.css", + "dist/images/*.png" + ] + }, "font-awesome": { "main": [ "css/font-awesome.css", diff --git a/yarn.lock b/yarn.lock index 70417ee6..05b97469 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4453,6 +4453,11 @@ leaflet-triangle-marker@^1.0.1: resolved "https://registry.yarnpkg.com/leaflet-triangle-marker/-/leaflet-triangle-marker-1.0.1.tgz#0775ee4903c6c0b71b20023dfb295dfc026bc23d" integrity sha512-nK2Wtp5tUPwg9STrE78oKLQJvcDZTMU5i+4la1zhHZKZcjoTl9oVVd0f6keMx+wN70IiHsoDkHCFzIiVcCs9eQ== +leaflet.awesome-markers@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/leaflet.awesome-markers/-/leaflet.awesome-markers-2.0.5.tgz#b7b0210d87e2566359bf478c1ab3ab07c7246f30" + integrity sha512-Ne/xDjkGyaujwNVVkv2tyXQUV0ZW7gZ0Mo0FuQY4jp2qWrvXi0hwDBvmZyF/8YOvybyMabTMM/mFWCTd1jZIQA== + leaflet.locatecontrol@^0.60.0: version "0.60.0" resolved "https://registry.yarnpkg.com/leaflet.locatecontrol/-/leaflet.locatecontrol-0.60.0.tgz#fc7be657ca9b7e8b8ba7263e52b0bb902b7cd965"