diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ac3297e
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+bower_components/
+nbproject/
+
diff --git a/bower.json b/bower.json
new file mode 100644
index 0000000..a7f3db1
--- /dev/null
+++ b/bower.json
@@ -0,0 +1,16 @@
+{
+ "name": "brouter-web",
+ "version": "0.1.0",
+ "main": "js/index.js",
+ "ignore": [
+ "**/.*",
+ "bower_components"
+ ],
+ "dependencies": {
+ "normalize-css": "*",
+ "leaflet-gpx": "mpetazzoni/leaflet-gpx",
+ "leaflet-search": "*",
+ "leaflet-plugins": "*",
+ "leaflet-routing": "Turistforeningen/leaflet-routing#gh-pages"
+ }
+}
diff --git a/css/leaflet-routing.css b/css/leaflet-routing.css
new file mode 100644
index 0000000..cb0b24f
--- /dev/null
+++ b/css/leaflet-routing.css
@@ -0,0 +1,5 @@
+div.line-mouse-marker {
+ background-color: #ffffff;
+ border: 2px solid black;
+ border-radius: 10px;
+}
diff --git a/css/style.css b/css/style.css
new file mode 100644
index 0000000..2efa6da
--- /dev/null
+++ b/css/style.css
@@ -0,0 +1,125 @@
+html, body, #map {
+ height: 100%;
+}
+
+.info {
+ padding: 6px 8px;
+ font: 14px/16px "Helvetica Neue", Arial, Helvetica, sans-serif;
+ box-shadow: 0px 0px 0px 3px rgba(70,130,180,0.2), 0 1px 5px rgba(0,0,0,0.4);
+ background: rgba(255,255,255,0.9);
+ border-radius: 5px;
+}
+.leaflet-control.elevation .background {
+ box-shadow: 0px 0px 0px 3px rgba(70,130,180,0.2);
+}
+div.elevation {
+ margin-bottom: -2px !important;
+}
+/*
+.info, div.elevation {
+ display:table-row;
+}
+*/
+
+.hidden {
+ display: none;
+}
+
+#message {
+ position: absolute;
+ left: 446px; /* 400 + 10 + 26 + 10 */
+ top: 0px;
+ margin-top: 10px;
+ z-index: 1000;
+ box-shadow: 0 1px 5px rgba(0,0,0,0.4);
+}
+
+#header {
+ width: 100%;
+ margin: auto;
+ text-align: center;
+}
+#header, .heading {
+ color: #333;
+}
+.title {
+ padding-top: 4px;
+}
+.title-name {
+ font-size: larger;
+ text-shadow: 1px 1px 2px rgba(0,0,0,0.4);
+}
+.version {
+ font-size: x-small;
+}
+.header-text {
+ font-size: small;
+ margin-top: 0.5em;
+ line-height: 1.4em;
+}
+.hint {
+ color: orangered;
+}
+
+.heading {
+ font-weight: bold;
+}
+.heading, .content, .content > .label, .content > .value {
+ float: left;
+}
+
+table {
+ border-collapse: separate;
+ border-spacing: 4px;
+ /* spacing between cells only */
+ margin: -4px;
+}
+
+td {
+ vertical-align: top;
+ padding: 0;
+}
+#stats td:nth-child(2) {
+ text-align: right;
+}
+
+.heading, tr > td:first-child, .label {
+ /* 1/4 of net info control width (370), so that values start at 50% */
+ width: 92.5px;
+}
+
+.leaflet-container {
+ cursor: auto;
+}
+
+/* left sidebar as additional control position */
+
+.leaflet-left {
+ left: 400px !important;
+}
+
+.leaflet-leftpane {
+ left: 5px;
+ top: 7px;
+ bottom: 7px;
+
+ /*
+ height: 100%;
+ display:table;
+ */
+ position: absolute;
+ z-index: 1000;
+ pointer-events: none;
+}
+
+.leaflet-leftpane .leaflet-control {
+ margin: 3px 5px;
+ width: 370px;
+}
+
+/* TODO hack to maximize last div, further table-row tests, check out flex box */
+.leaflet-leftpane .leaflet-control:last-child {
+ position: absolute;
+ top: 432px;
+ bottom: 0px;
+}
\ No newline at end of file
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..e0aeb5f
--- /dev/null
+++ b/index.html
@@ -0,0 +1,100 @@
+
+
+
+
+ BRouter desktop web client
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Requires a local desktop installation of BRouter
+
+
+
+
+ | Profile: |
+ |
+ | Alternative: |
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/js/control/Control.js b/js/control/Control.js
new file mode 100644
index 0000000..0f89042
--- /dev/null
+++ b/js/control/Control.js
@@ -0,0 +1,37 @@
+BR.Control = L.Control.extend({
+ options: {
+ position: 'leftpane'
+ },
+
+ onAdd: function (map) {
+ var container = L.DomUtil.create('div', 'info'),
+ heading,
+ div;
+
+ if (this.options.heading) {
+ heading = L.DomUtil.create('div', 'heading', container);
+ heading.innerHTML = this.options.heading;
+ this._content = L.DomUtil.create('div', 'content', container);
+ } else {
+ this._content = container;
+ }
+
+ if (this.options.divId) {
+ div = L.DomUtil.get(this.options.divId);
+ L.DomUtil.removeClass(div, 'hidden');
+ this._content.appendChild(div);
+ }
+
+ var stop = L.DomEvent.stopPropagation;
+ L.DomEvent
+ .on(container, 'click', stop)
+ .on(container, 'mousedown', stop)
+ .on(container, 'dblclick', stop);
+ // disabled because links not working, remove?
+ //L.DomEvent.on(container, 'click', L.DomEvent.preventDefault);
+
+ return container;
+ }
+});
+
+
diff --git a/js/control/Download.js b/js/control/Download.js
new file mode 100644
index 0000000..c583dc8
--- /dev/null
+++ b/js/control/Download.js
@@ -0,0 +1,20 @@
+BR.Download = BR.Control.extend({
+ options: {
+ heading: 'Download'
+ },
+
+ onAdd: function (map) {
+ var container = BR.Control.prototype.onAdd.call(this, map);
+ return container;
+ },
+
+ update: function (urls) {
+ var html = '
';
+ if (urls.gpx) {
+ html += '
GPX · ';
+ html += '
KML';
+ }
+ html += '
'
+ this._content.innerHTML = html;
+ }
+});
diff --git a/js/control/Profile.js b/js/control/Profile.js
new file mode 100644
index 0000000..e87ffe1
--- /dev/null
+++ b/js/control/Profile.js
@@ -0,0 +1,11 @@
+BR.Profile = BR.Control.extend({
+ options: {
+ heading: ''
+ },
+
+ onAdd: function (map) {
+ var container = BR.Control.prototype.onAdd.call(this, map);
+ container.innerHTML = " ";
+ return container;
+ }
+});
diff --git a/js/control/RoutingOptions.js b/js/control/RoutingOptions.js
new file mode 100644
index 0000000..7a85b5b
--- /dev/null
+++ b/js/control/RoutingOptions.js
@@ -0,0 +1,28 @@
+BR.RoutingOptions = BR.Control.extend({
+ options: {
+ heading: 'Options',
+ divId: 'route_options'
+ },
+
+ onAdd: function (map) {
+ L.DomUtil.get('profile').onchange = this._getChangeHandler();
+ L.DomUtil.get('alternative').onchange = this._getChangeHandler();
+
+ return BR.Control.prototype.onAdd.call(this, map);
+ },
+
+ getOptions: function() {
+ return {
+ profile: L.DomUtil.get('profile').value,
+ alternative: L.DomUtil.get('alternative').value
+ };
+ },
+
+ _getChangeHandler: function() {
+ return L.bind(function(evt) {
+ this.fire('update', {options: this.getOptions()});
+ }, this);
+ }
+});
+
+BR.RoutingOptions.include(L.Mixin.Events);
diff --git a/js/control/TrackStats.js b/js/control/TrackStats.js
new file mode 100644
index 0000000..82a899a
--- /dev/null
+++ b/js/control/TrackStats.js
@@ -0,0 +1,49 @@
+BR.TrackStats = BR.Control.extend({
+ options: {
+ heading: 'Route'
+ },
+
+ onAdd: function (map) {
+ var container = BR.Control.prototype.onAdd.call(this, map);
+ this.update();
+ return container;
+ },
+
+ update: function (polyline) {
+ var stats = this.calcStats(polyline),
+ html = '';
+
+ html += '';
+ html += '| Length: | ' + L.Util.formatNum(stats.distance/1000,1) + ' | km |
';
+ html += '| Ascent: | ' + Math.round(stats.elevationGain) + ' | m |
';
+ html += '| Descent: | ' + Math.round(stats.elevationLoss) + ' | m |
';
+ html += '
';
+
+ this._content.innerHTML = html;
+ },
+
+ calcStats: function(polyline) {
+ var stats = {
+ distance: 0,
+ elevationGain: 0,
+ elevationLoss: 0
+ };
+
+ var latLngs = polyline ? polyline.getLatLngs() : [];
+ for (var i = 0, current, next, eleDiff; i < latLngs.length - 1; i++) {
+ current = latLngs[i];
+ next = latLngs[i + 1];
+ stats.distance += current.distanceTo(next);
+
+ // from Leaflet.gpx plugin (writes to LatLng.meta.ele, LatLng now supports ele)
+ eleDiff = (next.ele || next.meta.ele) - (current.ele || current.meta.ele);
+ if (eleDiff > 0) {
+ stats.elevationGain += eleDiff;
+ } else {
+ stats.elevationLoss += Math.abs(eleDiff);
+ }
+ }
+
+ return stats;
+ }
+});
diff --git a/js/index.js b/js/index.js
new file mode 100644
index 0000000..03e4971
--- /dev/null
+++ b/js/index.js
@@ -0,0 +1,124 @@
+/*
+ BRouter web - web client for BRouter bike routing engine
+
+ Licensed under the MIT license.
+*/
+
+(function() {
+
+ function initMap() {
+ var odblAttribution = 'data © OpenStreetMap contributors '
+ + '(ODbL)';
+
+ var landscape = L.tileLayer('http://{s}.tile.thunderforest.com/landscape/{z}/{x}/{y}.png', {
+ maxZoom: 18,
+ attribution: 'tiles © Thunderforest '
+ + '(CC-BY-SA 2.0)' + odblAttribution
+ });
+
+ var osm = L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
+ maxZoom: 19,
+ attribution: 'tiles © OpenStreetMap contributors'
+ });
+
+ var map = new L.Map('map', {
+ layers: [osm],
+ center: new L.LatLng(50.99, 9.86),
+ zoom: 6
+ });
+ map.attributionControl.addAttribution(
+ 'BRouter © Arndt Brenschede, '
+ + 'routing + map data © OpenStreetMap contributors '
+ + '(ODbL)');
+
+ L.control.layers({
+ 'OpenStreetMap': osm,
+ 'Landscape (Thunderforest)': landscape
+ }, {
+ /*
+ 'Hiking (Waymarked Trails)': hiking
+ */
+ }).addTo(map);
+
+ map.addControl(new L.Control.Permalink({text: 'Permalink', position: 'bottomright'})); //, layers: layersControl
+ map.addControl(new BR.Search());
+
+ return map;
+ }
+
+ function initApp(map) {
+ var router,
+ routing,
+ routesLayer,
+ routingOptions,
+ nogos,
+ stats,
+ elevation,
+ download,
+ profile,
+ leftPaneId = 'leftpane';
+
+ // left sidebar as additional control position
+ map._controlCorners[leftPaneId] = L.DomUtil.create('div', 'leaflet-' + leftPaneId, map._controlContainer);
+
+ router = L.bRouter(); //brouterCgi dummyRouter
+
+ function updateRoute(evt) {
+ router.setOptions(evt.options);
+ routing.routeAllSegments(onUpdate);
+ }
+
+ routingOptions = new BR.RoutingOptions();
+ routingOptions.on('update', updateRoute);
+
+ nogos = new BR.NogoAreas();
+ nogos.on('update', updateRoute);
+
+ // initial option settings
+ router.setOptions(nogos.getOptions());
+ router.setOptions(routingOptions.getOptions());
+
+ stats = new BR.TrackStats();
+ download = new BR.Download();
+ elevation = new BR.Elevation();
+ profile = new BR.Profile();
+
+ routing = new BR.Routing({routing: {
+ router: L.bind(router.getRouteSegment, router)
+ }});
+ routing.on('routing:routeWaypointEnd', onUpdate);
+
+ function onUpdate() {
+ var track = routing.toPolyline(),
+ latLngs = routing.getWaypoints(),
+ urls = {};
+
+ elevation.update(track);
+ stats.update(track);
+
+ if (latLngs.length > 1) {
+ urls.gpx = router.getUrl(latLngs, 'gpx');
+ urls.kml = router.getUrl(latLngs, 'kml');
+ }
+
+ download.update(urls);
+ };
+
+ map.addControl(new BR.Control({
+ heading: '',
+ divId: 'header'
+ }));
+ routingOptions.addTo(map);
+ stats.addTo(map);
+ download.addTo(map);
+ elevation.addTo(map);
+ profile.addTo(map);
+
+ nogos.addTo(map);
+ routing.addTo(map);
+ }
+
+ map = initMap();
+ initApp(map);
+
+})();
diff --git a/js/plugin/Elevation.js b/js/plugin/Elevation.js
new file mode 100644
index 0000000..2231da4
--- /dev/null
+++ b/js/plugin/Elevation.js
@@ -0,0 +1,35 @@
+BR.Elevation = L.Control.Elevation.extend({
+ options: {
+ position: "leftpane",
+ width: 385,
+ margins: {
+ top: 20,
+ right: 20,
+ bottom: 30,
+ left: 50
+ },
+ theme: "steelblue-theme" //purple
+ },
+
+ clear: function() {
+ this._data = [];
+ this._dist = 0;
+ this._maxElevation = 0;
+
+ // workaround for 'Error: Problem parsing d=""' in Webkit when empty data
+ // https://groups.google.com/d/msg/d3-js/7rFxpXKXFhI/HzIO_NPeDuMJ
+ //this._areapath.datum(this._data).attr("d", this._area);
+ this._areapath.attr("d", "M0 0");
+
+ this._x.domain([0,1]);
+ this._y.domain([0,1]);
+ this._updateAxis();
+ },
+
+ update: function(track) {
+ this.clear();
+ if (track && track.getLatLngs().length > 0) {
+ this.addData(track);
+ }
+ }
+});
\ No newline at end of file
diff --git a/js/plugin/NogoAreas.js b/js/plugin/NogoAreas.js
new file mode 100644
index 0000000..66fad17
--- /dev/null
+++ b/js/plugin/NogoAreas.js
@@ -0,0 +1,66 @@
+L.drawLocal.draw.toolbar.buttons.circle = 'Draw no-go area (circle)';
+L.drawLocal.edit.toolbar.buttons.edit = 'Edit no-go areas';
+L.drawLocal.edit.toolbar.buttons.remove = 'Delete no-go areas';
+
+BR.NogoAreas = L.Control.Draw.extend({
+ initialize: function () {
+ this.drawnItems = new L.FeatureGroup();
+
+ L.Control.Draw.prototype.initialize.call(this, {
+ draw: {
+ position: 'topleft',
+ polyline: false,
+ polygon: false,
+ circle: true,
+ rectangle: false,
+ marker: false
+ },
+ edit: {
+ featureGroup: this.drawnItems,
+ //edit: false,
+ edit: {
+ selectedPathOptions: {
+ //opacity: 0.8
+ }
+ },
+ remove: true
+ }
+ });
+ },
+
+ onAdd: function (map) {
+ map.addLayer(this.drawnItems);
+
+ map.on('draw:created', function (e) {
+ var layer = e.layer;
+ this.drawnItems.addLayer(layer);
+ this._fireUpdate();
+ }, this);
+
+ map.on('draw:editstart', function (e) {
+ this.drawnItems.eachLayer(function (layer) {
+ layer.on('edit', function(e) {
+ this._fireUpdate();
+ }, this);
+ }, this);
+ }, this);
+
+ map.on('draw:deleted', function (e) {
+ this._fireUpdate();
+ }, this);
+
+ return L.Control.Draw.prototype.onAdd.call(this, map);
+ },
+
+ getOptions: function() {
+ return {
+ nogos: this.drawnItems.getLayers()
+ };
+ },
+
+ _fireUpdate: function () {
+ this.fire('update', {options: this.getOptions()});
+ }
+});
+
+BR.NogoAreas.include(L.Mixin.Events);
\ No newline at end of file
diff --git a/js/plugin/Routing.js b/js/plugin/Routing.js
new file mode 100644
index 0000000..a4bf9c1
--- /dev/null
+++ b/js/plugin/Routing.js
@@ -0,0 +1,20 @@
+BR.Routing = L.Routing.extend({
+ options: {
+ /* not implemented yet
+ icons: {
+ start: new L.Icon.Default({iconUrl: 'bower_components/leaflet-gpx/pin-icon-start.png'}),
+ end: new L.Icon.Default(),
+ normal: new L.Icon.Default()
+ },*/
+ snapping: null
+ },
+
+ onAdd: function (map) {
+ var container = L.Routing.prototype.onAdd.call(this, map);
+
+ // enable drawing mode
+ this.draw(true);
+
+ return container;
+ }
+});
diff --git a/js/plugin/Search.js b/js/plugin/Search.js
new file mode 100644
index 0000000..daf4661
--- /dev/null
+++ b/js/plugin/Search.js
@@ -0,0 +1,17 @@
+BR.Search = L.Control.Search.extend({
+ options: {
+ //url: 'http://nominatim.openstreetmap.org/search?format=json&q={s}',
+ url: 'http://open.mapquestapi.com/nominatim/v1/search.php?format=json&q={s}',
+ jsonpParam: 'json_callback',
+ propertyName: 'display_name',
+ propertyLoc: ['lat','lon'],
+ markerLocation: false,
+ autoType: false,
+ autoCollapse: true,
+ minLength: 2,
+ zoom: 12
+ },
+
+ // patch: interferes with draw plugin (adds all layers twice to map?)
+ _onLayerAddRemove: function() {}
+});
diff --git a/js/router/BRouter.js b/js/router/BRouter.js
new file mode 100644
index 0000000..0cae7a7
--- /dev/null
+++ b/js/router/BRouter.js
@@ -0,0 +1,104 @@
+L.BRouter = L.Class.extend({
+ statics: {
+ // http://localhost:17777/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: 'http://localhost:17777/brouter?lonlats={lonlats}&nogos={nogos}&profile={profile}&alternativeidx={alternativeidx}&format={format}',
+ PRECISION: 6,
+ NUMBER_SEPARATOR: ',',
+ GROUP_SEPARATOR: '|'
+ },
+
+ options: {
+ format: 'gpx'
+ },
+
+ initialize: function (options) {
+ L.setOptions(this, options);
+ },
+
+ setOptions: function(options) {
+ L.setOptions(this, options);
+ },
+
+ getUrl: function(latLngs, format) {
+ var urlParams = {
+ lonlats: this._getLonLatsString(latLngs),
+ nogos: this._getNogosString(this.options.nogos),
+ profile: this.options.profile,
+ alternativeidx: this.options.alternative,
+ format: format || this.options.format
+ };
+ var url = L.Util.template(L.BRouter.URL_TEMPLATE, urlParams);
+ return url;
+ },
+
+ getRoute: function(latLngs, cb) {
+ var url = this.getUrl(latLngs);
+ if (!url) {
+ return cb(new Error('Error getting route URL'));
+ }
+
+ var gpxLayer = new L.GPX(url, {
+ async: true,
+ polyline_options: {
+ opacity: 0.6
+ },
+ marker_options: {
+ startIconUrl: null,
+ endIconUrl: null
+ }
+ }).on('loaded', function(e) {
+ // leaflet.spin
+ gpxLayer.fire('data:loaded');
+ var gpx = e.target;
+
+ return cb(null, gpx.getLayers()[0]);
+ })/* TODO no error handling in leaflet-gpx
+ .on('error', function(e){
+ console.error('error');
+ gpxLayer.fire('data:loaded');
+ return cb(new Error('Routing failed'));
+ })*/;
+ },
+
+ getRouteSegment: function(l1, l2, cb) {
+ return this.getRoute([l1, l2], cb);
+ },
+
+ _getLonLatsString: function(latLngs) {
+ var s = '';
+ for (var i = 0; i < latLngs.length; i++) {
+ s += this._formatLatLng(latLngs[i]);
+ if (i < (latLngs.length - 1)) {
+ s += L.BRouter.GROUP_SEPARATOR;
+ }
+ }
+ return s;
+ },
+
+ _getNogosString: function(nogos) {
+ var s = '';
+ for (var i = 0, circle; i < nogos.length; i++) {
+ circle = nogos[i];
+ s += this._formatLatLng(circle.getLatLng());
+ s += L.BRouter.NUMBER_SEPARATOR;
+ s += Math.round(circle.getRadius());
+ if (i < (nogos.length - 1)) {
+ s += L.BRouter.GROUP_SEPARATOR;
+ }
+ }
+ return s;
+ },
+
+ // formats L.LatLng object as lng,lat string
+ _formatLatLng: function(latLng) {
+ var s = '';
+ s += L.Util.formatNum(latLng.lng, L.BRouter.PRECISION);
+ s += L.BRouter.NUMBER_SEPARATOR;
+ s += L.Util.formatNum(latLng.lat, L.BRouter.PRECISION);
+ return s;
+ }
+});
+
+L.bRouter = function (options) {
+ return new L.BRouter(options);
+};
\ No newline at end of file
diff --git a/js/router/brouterCgi.js b/js/router/brouterCgi.js
new file mode 100644
index 0000000..a0a8023
--- /dev/null
+++ b/js/router/brouterCgi.js
@@ -0,0 +1,29 @@
+// BRouter online demo interface
+// TODO remove or adopt to new structure (only supports two waypoints!)
+var brouterCgi = (function() {
+ // http://h2096617.stratoserver.net/cgi-bin/brouter.sh?coords=13.404681_52.520185_13.340278_52.512356_trekking_0
+ //var URL_TEMPLATE = '/cgi-bin/proxy.cgi?url=' + 'http://h2096617.stratoserver.net/cgi-bin/brouter.sh?coords={fromLng}_{fromLat}_{toLng}_{toLat}_{profile}_{alt}';
+ var URL_TEMPLATE = '/proxy.php?url=' + 'cgi-bin/brouter.sh?coords={fromLng}_{fromLat}_{toLng}_{toLat}_{profile}_{alt}';
+ var PRECISION = 6;
+
+ function getUrl(polyline) {
+ var latLngs = polyline.getLatLngs();
+ var urlParams = {
+ fromLat: L.Util.formatNum(latLngs[0].lat, PRECISION),
+ fromLng: L.Util.formatNum(latLngs[0].lng, PRECISION),
+ toLat: L.Util.formatNum(latLngs[1].lat, PRECISION),
+ toLng: L.Util.formatNum(latLngs[1].lng, PRECISION),
+ profile: 'trekking',
+ alt: '0'
+ };
+ var url = L.Util.template(URL_TEMPLATE, urlParams);
+ //console.log(url);
+ //return 'test/test.gpx';
+ return url;
+ }
+
+ return {
+ getUrl: getUrl
+ }
+
+})();
\ No newline at end of file