From 779c720b7d55574266fb61787033fb41c8e3d0c0 Mon Sep 17 00:00:00 2001 From: Gautier P Date: Tue, 1 Dec 2020 20:56:14 +0100 Subject: [PATCH] Add 20km allowed zone icon (#347) Co-authored-by: Gautier P --- css/style.css | 3 + index.html | 1 + js/control/Export.js | 2 +- js/control/Message.js | 10 +- js/index.js | 64 ++++++++++--- js/plugin/CircleGoArea.js | 191 ++++++++++++++++++++++++++++++++++++++ js/plugin/NogoAreas.js | 6 +- js/plugin/POIMarkers.js | 11 ++- js/router/BRouter.js | 24 ++++- locales/en.json | 4 + locales/fr.json | 4 +- 11 files changed, 296 insertions(+), 24 deletions(-) create mode 100644 js/plugin/CircleGoArea.js diff --git a/css/style.css b/css/style.css index aeea5d3..1c41f9f 100644 --- a/css/style.css +++ b/css/style.css @@ -197,6 +197,9 @@ input#trackname:focus:invalid { .pois-draw-enabled { cursor: cell; } +.circlego-draw-enabled { + cursor: pointer; +} #map { /* center error message horizontally */ diff --git a/index.html b/index.html index 1d15611..27566fd 100644 --- a/index.html +++ b/index.html @@ -373,6 +373,7 @@ Source

+
or
diff --git a/js/control/Export.js b/js/control/Export.js index fd6b69c..31c6b0e 100644 --- a/js/control/Export.js +++ b/js/control/Export.js @@ -47,7 +47,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, this.pois.getMarkers(), format, name, includeWaypoints); + var uri = this.router.getUrl(this.latLngs, this.pois.getMarkers(), null, format, name, includeWaypoints); e.preventDefault(); diff --git a/js/control/Message.js b/js/control/Message.js index b71c5aa..426ebde 100644 --- a/js/control/Message.js +++ b/js/control/Message.js @@ -40,8 +40,16 @@ BR.Message = L.Class.extend({ }, showError: function(err) { - if (err == 'Error: target island detected for section 0\n') { + if (err && err.message) err = err.message; + + if (err == 'target island detected for section 0\n') { err = i18next.t('warning.no-route-found'); + } else if (err == 'no track found at pass=0\n') { + err = i18next.t('warning.no-route-found'); + } else if (err == 'to-position not mapped in existing datafile\n') { + err = i18next.t('warning.invalid-route-to'); + } else if (err == 'from-position not mapped in existing datafile\n') { + err = i18next.t('warning.invalid-route-from'); } this._show(err, 'error'); }, diff --git a/js/index.js b/js/index.js index 476e588..c964db4 100644 --- a/js/index.js +++ b/js/index.js @@ -34,6 +34,7 @@ drawButton, deleteRouteButton, pois, + circleGo, urlHash; // By default bootstrap-select use glyphicons @@ -74,7 +75,10 @@ routing.draw(true); control.state('deactivate-draw'); }, - title: i18next.t('keyboard.generic-shortcut', { action: '$t(map.draw-route-start)', key: 'D' }) + title: i18next.t('keyboard.generic-shortcut', { + action: '$t(map.draw-route-start)', + key: 'D' + }) } ] }); @@ -84,7 +88,10 @@ function() { routing.reverse(); }, - i18next.t('keyboard.generic-shortcut', { action: '$t(map.reverse-route)', key: 'R' }) + i18next.t('keyboard.generic-shortcut', { + action: '$t(map.reverse-route)', + key: 'R' + }) ); var deletePointButton = L.easyButton( @@ -92,7 +99,10 @@ function() { routing.deleteLastPoint(); }, - i18next.t('keyboard.generic-shortcut', { action: '$t(map.delete-last-point)', key: 'Z' }) + i18next.t('keyboard.generic-shortcut', { + action: '$t(map.delete-last-point)', + key: 'Z' + }) ); deleteRouteButton = L.easyButton( @@ -100,7 +110,10 @@ function() { clearRoute(); }, - i18next.t('keyboard.generic-shortcut', { action: '$t(map.clear-route)', key: '$t(keyboard.backspace)' }) + i18next.t('keyboard.generic-shortcut', { + action: '$t(map.clear-route)', + key: '$t(keyboard.backspace)' + }) ); L.DomEvent.addListener( @@ -181,7 +194,10 @@ profile.update(evt.options); }); - BR.NogoAreas.MSG_BUTTON = i18next.t('keyboard.generic-shortcut', { action: '$t(map.nogo.draw)', key: 'N' }); + BR.NogoAreas.MSG_BUTTON = i18next.t('keyboard.generic-shortcut', { + action: '$t(map.nogo.draw)', + key: 'N' + }); BR.NogoAreas.MSG_BUTTON_CANCEL = i18next.t('keyboard.generic-shortcut', { action: '$t(map.nogo.cancel)', key: '$t(keyboard.escape)' @@ -243,9 +259,9 @@ styles: BR.conf.routingStyles }); - pois = new BR.PoiMarkers({ - routing: routing - }); + pois = new BR.PoiMarkers(routing); + circleGo = new BR.CircleGoArea(routing, nogos, pois); + pois.circlego = circleGo; exportRoute = new BR.Export(router, pois); @@ -305,7 +321,22 @@ } nogos.addTo(map); - L.easyBar([drawButton, reverseRouteButton, nogos.getButton(), deletePointButton, deleteRouteButton]).addTo(map); + + var shouldAddCircleGo = false; + var lang = i18next.languages.length && i18next.languages[0]; + + if (lang.startsWith('fr')) { + circleGo.options.radius = 20000; + shouldAddCircleGo = true; + } + + if (shouldAddCircleGo) circleGo.addTo(map); + + var buttons = [drawButton, reverseRouteButton, nogos.getButton()]; + if (shouldAddCircleGo) buttons.push(circleGo.getButton()); + buttons.push(deletePointButton, deleteRouteButton); + + L.easyBar(buttons).addTo(map); nogos.preventRoutePointOnCreate(routing); if (BR.keys.strava) { @@ -322,7 +353,10 @@ map.addControl( new BR.OpacitySliderControl({ id: 'route', - title: i18next.t('map.opacity-slider-shortcut', { action: '$t(map.opacity-slider)', key: 'M' }), + title: i18next.t('map.opacity-slider-shortcut', { + action: '$t(map.opacity-slider)', + key: 'M' + }), muteKeyCode: 77, // m callback: L.bind(routing.setOpacity, routing) }) @@ -358,6 +392,11 @@ var opts = router.parseUrlParams(url2params(url)); router.setOptions(opts); routingOptions.setOptions(opts); + if (opts.circlego) { + // must be done before nogos! + circleGo.options.radius = opts.circlego[2]; + circleGo.setCircle([opts.circlego[0], opts.circlego[1]]); + } nogos.setOptions(opts); profile.update(opts); @@ -366,7 +405,6 @@ routing.clear(); routing.setWaypoints(opts.lonlats); } - if (opts.pois) { pois.setMarkers(opts.pois); } @@ -384,7 +422,9 @@ 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(), pois.getMarkers(), null).substr('brouter?'.length + 1); + var url = router + .getUrl(routing.getWaypoints(), pois.getMarkers(), circleGo.getCircle(), 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, ';'); diff --git a/js/plugin/CircleGoArea.js b/js/plugin/CircleGoArea.js new file mode 100644 index 0000000..5fffebc --- /dev/null +++ b/js/plugin/CircleGoArea.js @@ -0,0 +1,191 @@ +BR.CircleGoArea = L.Control.extend({ + circleLayer: null, + + options: { + radius: 1000, // in meters + shortcut: { + draw: { + enable: 73, // char code for 'i' + disable: 27 // char code for 'ESC' + } + } + }, + initialize: function(routing, nogos, pois) { + this.routing = routing; + this.nogos = nogos; + this.pois = pois; + }, + + onAdd: function(map) { + var self = this; + + this.map = map; + this.circleLayer = L.layerGroup([]).addTo(map); + + var radiusKm = (this.options.radius / 1000).toFixed(); + this.drawButton = L.easyButton({ + states: [ + { + stateName: 'activate-circlego', + icon: 'fa-circle-o', + onClick: function() { + self.draw(true); + }, + title: i18next.t('keyboard.generic-shortcut', { + action: i18next.t('map.draw-circlego-start', { radius: radiusKm }), + key: 'I' + }) + }, + { + stateName: 'deactivate-circlego', + icon: 'fa-circle-o active', + onClick: function() { + self.draw(false); + }, + title: i18next.t('keyboard.generic-shortcut', { + action: i18next.t('map.draw-circlego-stop', { radius: radiusKm }), + key: '$t(keyboard.escape)' + }) + } + ] + }); + + map.on('routing:draw-start', function() { + self.draw(false); + }); + + L.DomEvent.addListener(document, 'keydown', this._keydownListener, this); + + var container = new L.DomUtil.create('div'); + return container; + }, + + draw: function(enable) { + this.drawButton.state(enable ? 'deactivate-circlego' : 'activate-circlego'); + if (enable) { + this.routing.draw(false); + this.pois.draw(false); + this.map.on('click', this.onMapClick, this); + L.DomUtil.addClass(this.map.getContainer(), 'circlego-draw-enabled'); + } else { + this.map.off('click', this.onMapClick, this); + L.DomUtil.removeClass(this.map.getContainer(), 'circlego-draw-enabled'); + } + }, + + _keydownListener: function(e) { + if (!BR.Util.keyboardShortcutsAllowed(e)) { + return; + } + if (e.keyCode === this.options.shortcut.draw.disable) { + this.draw(false); + } else if (e.keyCode === this.options.shortcut.draw.enable) { + this.draw(true); + } + }, + + setNogoCircle: function(center) { + if (center) { + var polygon = this.circleToPolygon(center, this.options.radius); + $('#nogoJSON').val(JSON.stringify(polygon)); + this.nogos.uploadNogos(); + } else { + this.nogos.clear(); + } + }, + + onMapClick: function(e) { + this.setCircle([e.latlng.lng, e.latlng.lat]); + }, + + setCircle: function(center) { + var self = this; + var icon = L.VectorMarkers.icon({ + icon: 'home', + markerColor: BR.conf.markerColors.circlego + }); + var marker = L.marker([center[1], center[0]], { icon: icon, draggable: true, name: name }) + .on('dragend', function(e) { + self.setNogoCircle([e.target.getLatLng().lng, e.target.getLatLng().lat]); + }) + .on('click', function() { + var drawing = self.drawButton.state() == 'deactivate-circlego'; + if (drawing) { + self.circleLayer.removeLayer(marker); + self.setNogoCircle(undefined); + } + }); + + this.clear(); + marker.addTo(this.circleLayer); + this.setNogoCircle(center); + this.draw(false); + }, + + clear: function() { + this.circleLayer.clearLayers(); + }, + + getButton: function() { + return this.drawButton; + }, + + getCircle: function() { + var circle = this.circleLayer.getLayers().map(function(it) { + return it.getLatLng(); + }); + if (circle && circle.length) { + return [circle[0].lng.toFixed(6), circle[0].lat.toFixed(6), this.options.radius].join(','); + } else { + return null; + } + }, + + toRadians: function(angleInDegrees) { + return (angleInDegrees * Math.PI) / 180; + }, + + toDegrees: function(angleInRadians) { + return (angleInRadians * 180) / Math.PI; + }, + + offset: function(c1, distance, bearing) { + var lon1 = this.toRadians(c1[0]); + var lat1 = this.toRadians(c1[1]); + var dByR = distance / 6378137; // distance divided by 6378137 (radius of the earth) wgs84 + var lat = Math.asin(Math.sin(lat1) * Math.cos(dByR) + Math.cos(lat1) * Math.sin(dByR) * Math.cos(bearing)); + var lon = + lon1 + + Math.atan2( + Math.sin(bearing) * Math.sin(dByR) * Math.cos(lat1), + Math.cos(dByR) - Math.sin(lat1) * Math.sin(lat) + ); + return [this.toDegrees(lon), this.toDegrees(lat)]; + }, + + circleToPolygon: function(center, radius, numberOfSegments) { + var n = numberOfSegments ? numberOfSegments : 64; + + var inner = []; + for (var i = 0; i < n; ++i) { + inner.push(this.offset(center, radius, (2 * Math.PI * -i) / n)); + } + inner.push(inner[0]); + + return { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + properties: {}, + geometry: { + type: 'LineString', + coordinates: inner + } + } + ] + }; + } +}); + +BR.CircleGoArea.include(L.Evented.prototype); diff --git a/js/plugin/NogoAreas.js b/js/plugin/NogoAreas.js index 6cff5ec..2c539bb 100644 --- a/js/plugin/NogoAreas.js +++ b/js/plugin/NogoAreas.js @@ -162,9 +162,13 @@ BR.NogoAreas = L.Control.extend({ var self = this; var geoJSONPromise; + var nogoJSON = $('#nogoJSON').val(); //hidden var nogoURL = $('#nogoURL').val(); var nogoFile = $('#nogoFile')[0].files[0]; - if (nogoURL) { + if (nogoJSON) { + geoJSONPromise = Promise.resolve(JSON.parse(nogoJSON)); + $('#nogoJSON').val(undefined); + } else if (nogoURL) { // TODO: Handle {{bbox}} geoJSONPromise = fetch(nogoURL).then(function(response) { response.json(); diff --git a/js/plugin/POIMarkers.js b/js/plugin/POIMarkers.js index c47990d..76d132a 100644 --- a/js/plugin/POIMarkers.js +++ b/js/plugin/POIMarkers.js @@ -1,8 +1,8 @@ BR.PoiMarkers = L.Control.extend({ markersLayer: null, + circlego: null, options: { - routing: null, shortcut: { draw: { enable: 80, // char code for 'p' @@ -10,6 +10,10 @@ BR.PoiMarkers = L.Control.extend({ } } }, + initialize: function(routing) { + this.routing = routing; + this.circlego = null; + }, onAdd: function(map) { var self = this; @@ -54,7 +58,8 @@ BR.PoiMarkers = L.Control.extend({ draw: function(enable) { this.drawButton.state(enable ? 'deactivate-poi' : 'activate-poi'); if (enable) { - this.options.routing.draw(false); + this.routing.draw(false); + this.circlego.draw(false); this.map.on('click', this.onMapClick, this); L.DomUtil.addClass(this.map.getContainer(), 'pois-draw-enabled'); } else { @@ -129,7 +134,7 @@ BR.PoiMarkers = L.Control.extend({ getMarkers: function() { return this.markersLayer.getLayers().map(function(it) { return { - latlng: it._latlng, + latlng: it.getLatLng(), name: it.options.name }; }); diff --git a/js/router/BRouter.js b/js/router/BRouter.js index d8985b1..f38ee1b 100644 --- a/js/router/BRouter.js +++ b/js/router/BRouter.js @@ -3,7 +3,7 @@ L.BRouter = L.Class.extend({ // NOTE: the routing API used here is not public! // /brouter?lonlats=1.1,1.2|2.1,2.2|3.1,3.2|4.1,4.2&nogos=-1.1,-1.2,1|-2.1,-2.2,2&profile=shortest&alternativeidx=1&format=kml URL_TEMPLATE: - '/brouter?lonlats={lonlats}&nogos={nogos}&polylines={polylines}&polygons={polygons}&profile={profile}&alternativeidx={alternativeidx}&format={format}', + '/brouter?lonlats={lonlats}&profile={profile}&alternativeidx={alternativeidx}&format={format}&nogos={nogos}&polylines={polylines}&polygons={polygons}', URL_PROFILE_UPLOAD: BR.conf.host + '/brouter/profile', PRECISION: 6, NUMBER_SEPARATOR: ',', @@ -42,7 +42,7 @@ L.BRouter = L.Class.extend({ L.setOptions(this, options); }, - getUrlParams: function(latLngs, pois, format) { + getUrlParams: function(latLngs, pois, circlego, format) { params = {}; if (this._getLonLatsString(latLngs) != null) params.lonlats = this._getLonLatsString(latLngs); @@ -59,6 +59,8 @@ L.BRouter = L.Class.extend({ if (pois && this._getLonLatsNameString(pois) != null) params.pois = this._getLonLatsNameString(pois); + if (circlego) params.circlego = circlego; + params.alternativeidx = this.options.alternative; if (format != null) { @@ -100,15 +102,27 @@ L.BRouter = L.Class.extend({ if (params.pois) { opts.pois = this._parseLonLatNames(params.pois); } + if (params.circlego) { + var circlego = params.circlego.split(','); + if (circlego.length == 3) { + circlego = [ + Number.parseFloat(circlego[0]), + Number.parseFloat(circlego[1]), + Number.parseInt(circlego[2]) + ]; + opts.circlego = circlego; + } + } return opts; }, - getUrl: function(latLngs, pois, format, trackname, exportWaypoints) { - var urlParams = this.getUrlParams(latLngs, pois, format); + getUrl: function(latLngs, pois, circlego, format, trackname, exportWaypoints) { + var urlParams = this.getUrlParams(latLngs, pois, circlego, 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.circlego != null) args.push(L.Util.template('circlego={circlego}', 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)); @@ -129,7 +143,7 @@ L.BRouter = L.Class.extend({ }, getRoute: function(latLngs, cb) { - var url = this.getUrl(latLngs, null, 'geojson'), + var url = this.getUrl(latLngs, null, null, 'geojson'), xhr = new XMLHttpRequest(); if (!url) { diff --git a/locales/en.json b/locales/en.json index 28189a0..5374acc 100644 --- a/locales/en.json +++ b/locales/en.json @@ -94,6 +94,8 @@ "delete-nogo-areas": "Delete all no-go areas", "delete-pois": "Delete all points of interest", "delete-route": "Delete route", + "draw-circlego-start": "Draw limited {{radius}}km go-to zone", + "draw-circlego-stop": "Stop drawing limited {{radius}}km go-to zone", "draw-poi-start": "Draw points of interest", "draw-poi-stop": "Stop drawing points of interest", "draw-route-start": "Draw route", @@ -267,6 +269,8 @@ }, "warning": { "cannot-get-route": "Error getting route URL", + "invalid-route-from": "Start marker is too far from a route.", + "invalid-route-to": "Destination marker is too far from a route.", "no-response": "no response from server", "no-route-found": "Error: cannot find a route for given points. Maybe try to move them closer to roads?", "profile-error": "Profile error: no or empty response from server", diff --git a/locales/fr.json b/locales/fr.json index 4e8cf81..8a00f3c 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -61,7 +61,7 @@ "keyboard": { "backspace": "Retour chariot", "escape": "Échap", - "generic-shortcut": "{{action}} (touche {{key}} )", + "generic-shortcut": "{{action}} (touche {{key}})", "shift": "Maj" }, "layers": { @@ -94,6 +94,8 @@ "delete-nogo-areas": "Supprimer toutes les zones interdites", "delete-pois": "Supprimer tous les points d'intérêt ", "delete-route": "Supprimer l'itinéraire", + "draw-circlego-start": "Ajouter une zone limite de {{radius}} km", + "draw-circlego-stop": "Arrêter l'ajout d'une zone limit de {{radius}} km", "draw-poi-start": "Ajouter des points d'intérêt", "draw-poi-stop": "Arrêter l'ajout de points d'intérêt", "draw-route-start": "Dessiner l'itinéraire",