diff --git a/.gitignore b/.gitignore
index 552abf8..3132bdf 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 c0ec359..05ec624 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 d876585..97e89c1 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 acca70e..ef8582d 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 0000000..3052818
--- /dev/null
+++ b/js/plugin/POIMarkers.js
@@ -0,0 +1,147 @@
+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) {
+ // this method must only be used to sanitize for textContent.
+ // do NOT use it to sanitize any attribute,
+ // see https://web.archive.org/web/20121208091505/http://benv.ca/2012/10/4/you-are-probably-misusing-DOM-text-methods/
+ var sanitizeHTMLContent = function(str) {
+ var temp = document.createElement('div');
+ temp.textContent = str;
+ return temp.innerHTML;
+ };
+
+ var icon = L.AwesomeMarkers.icon({
+ icon: 'star',
+ markerColor: 'cadetblue'
+ });
+ var content = sanitizeHTMLContent(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 71469fb..878bf96 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 e4a6a10..4fce696 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 0a1449a..93b6ab2 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 70417ee..05b9746 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"