├── .gitignore ├── .jshintrc ├── LICENSE.md ├── README.md ├── examples ├── Control.Geocoder.js ├── index.css ├── index.html └── index.js ├── package.json ├── scripts ├── dist.sh └── publish.sh └── src └── L.Routing.GraphHopper.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | /* 3 | * ENVIRONMENTS 4 | * ================= 5 | */ 6 | 7 | // Define globals exposed by modern browsers. 8 | "browser": true, 9 | 10 | // Define globals exposed by Node.js. 11 | "node": true, 12 | 13 | "globals": {"L": false}, 14 | 15 | /* 16 | * ENFORCING OPTIONS 17 | * ================= 18 | */ 19 | 20 | // Force all variable names to use either camelCase style or UPPER_CASE 21 | // with underscores. 22 | "camelcase": true, 23 | 24 | // Prohibit use of == and != in favor of === and !==. 25 | "eqeqeq": true, 26 | 27 | // Suppress warnings about == null comparisons. 28 | "eqnull": true, 29 | 30 | // Enforce tab width of 2 spaces. 31 | "indent": 2, 32 | 33 | "smarttabs": true, 34 | 35 | // Prohibit use of a variable before it is defined. 36 | "latedef": true, 37 | 38 | // Require capitalized names for constructor functions. 39 | "newcap": true, 40 | 41 | // Enforce use of single quotation marks for strings. 42 | "quotmark": "single", 43 | 44 | // Prohibit trailing whitespace. 45 | "trailing": true, 46 | 47 | // Prohibit use of explicitly undeclared variables. 48 | "undef": true, 49 | 50 | // Warn when variables are defined but never used. 51 | "unused": true, 52 | 53 | // All loops and conditionals should have braces. 54 | "curly": true 55 | } 56 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ## ISC License 2 | 3 | Copyright (c) 2015, Per Liedman (per@liedman.net) 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Leaflet Routing Machine / GraphHopper 2 | ===================================== 3 | 4 | ## Maintainers Wanted 5 | 6 | *I no longer use this module, and have not done so in a long while. If you use it, please consider taking over maintenance of this module (it's not a lot of work). Because of this situation, I have very limited time and expertise to answer any questions regarding this module* 7 | 8 | [![npm version](https://img.shields.io/npm/v/lrm-graphhopper.svg)](https://www.npmjs.com/package/lrm-graphhopper) 9 | 10 | Extends [Leaflet Routing Machine](https://github.com/perliedman/leaflet-routing-machine) with support for [GraphHopper](https://graphhopper.com/). 11 | 12 | Some brief instructions follow below, but the [Leaflet Routing Machine tutorial on alternative routers](http://www.liedman.net/leaflet-routing-machine/tutorials/alternative-routers/) is recommended. 13 | 14 | ## Installing 15 | 16 | Go to the [releases page](https://github.com/perliedman/lrm-graphhopper/releases) to get the script to include in your page. Put the script after Leaflet and Leaflet Routing Machine has been loaded. 17 | 18 | To use with for example Browserify: 19 | 20 | ```sh 21 | npm install --save lrm-graphhopper 22 | ``` 23 | 24 | ## Using 25 | 26 | There's a single class exported by this module, `L.Routing.GraphHopper`. It implements the [`IRouter`](http://www.liedman.net/leaflet-routing-machine/api/#irouter) interface. Use it to replace Leaflet Routing Machine's default OSRM router implementation: 27 | 28 | ```javascript 29 | var L = require('leaflet'); 30 | require('leaflet-routing-machine'); 31 | require('lrm-graphhopper'); // This will tack on the class to the L.Routing namespace 32 | 33 | L.Routing.control({ 34 | router: new L.Routing.GraphHopper('your GraphHopper API key'), 35 | }).addTo(map); 36 | ``` 37 | 38 | Note that you will need to pass a valid GraphHopper API key to the constructor. 39 | 40 | To keep track of the GraphHopper credits consumption, the application may listen to the `response` event fired by the Router object. This event holds the values from [GraphHopper's response HTTP headers](https://graphhopper.com/api/1/docs/#http-headers): 41 | * `status`: The HTTP status code (see [GraphHopper error codes](https://graphhopper.com/api/1/docs/#http-error-codes)) 42 | * `limit`: The `X-RateLimit-Limit` header 43 | * `remaining`: The `X-RateLimit-Remaining` header 44 | * `reset`: The `X-RateLimit-Reset` header 45 | * `credits`: The `X-RateLimit-Credits` header 46 | 47 | ```javascript 48 | var router = myRoutingControl.getRouter(); 49 | router.on('response',function(e){ 50 | console.log('This routing request consumed ' + e.credits + ' credit(s)'); 51 | console.log('You have ' + e.remaining + ' left'); 52 | }); 53 | ``` 54 | -------------------------------------------------------------------------------- /examples/Control.Geocoder.js: -------------------------------------------------------------------------------- 1 | (function (factory) { 2 | // Packaging/modules magic dance 3 | var L; 4 | if (typeof define === 'function' && define.amd) { 5 | // AMD 6 | define(['leaflet'], factory); 7 | } else if (typeof module !== 'undefined') { 8 | // Node/CommonJS 9 | L = require('leaflet'); 10 | module.exports = factory(L); 11 | } else { 12 | // Browser globals 13 | if (typeof window.L === 'undefined') 14 | throw 'Leaflet must be loaded first'; 15 | factory(window.L); 16 | } 17 | }(function (L) { 18 | 'use strict'; 19 | L.Control.Geocoder = L.Control.extend({ 20 | options: { 21 | showResultIcons: false, 22 | collapsed: true, 23 | expand: 'click', 24 | position: 'topright', 25 | placeholder: 'Search...', 26 | errorMessage: 'Nothing found.' 27 | }, 28 | 29 | _callbackId: 0, 30 | 31 | initialize: function (options) { 32 | L.Util.setOptions(this, options); 33 | if (!this.options.geocoder) { 34 | this.options.geocoder = new L.Control.Geocoder.Nominatim(); 35 | } 36 | }, 37 | 38 | onAdd: function (map) { 39 | var className = 'leaflet-control-geocoder', 40 | container = L.DomUtil.create('div', className), 41 | icon = L.DomUtil.create('div', 'leaflet-control-geocoder-icon', container), 42 | form = this._form = L.DomUtil.create('form', className + '-form', container), 43 | input; 44 | 45 | this._map = map; 46 | this._container = container; 47 | input = this._input = L.DomUtil.create('input'); 48 | input.type = 'text'; 49 | input.placeholder = this.options.placeholder; 50 | 51 | L.DomEvent.addListener(input, 'keydown', this._keydown, this); 52 | //L.DomEvent.addListener(input, 'onpaste', this._clearResults, this); 53 | //L.DomEvent.addListener(input, 'oninput', this._clearResults, this); 54 | 55 | this._errorElement = document.createElement('div'); 56 | this._errorElement.className = className + '-form-no-error'; 57 | this._errorElement.innerHTML = this.options.errorMessage; 58 | 59 | this._alts = L.DomUtil.create('ul', className + '-alternatives leaflet-control-geocoder-alternatives-minimized'); 60 | 61 | form.appendChild(input); 62 | form.appendChild(this._errorElement); 63 | container.appendChild(this._alts); 64 | 65 | L.DomEvent.addListener(form, 'submit', this._geocode, this); 66 | 67 | if (this.options.collapsed) { 68 | if (this.options.expand === 'click') { 69 | L.DomEvent.addListener(icon, 'click', function(e) { 70 | // TODO: touch 71 | if (e.button === 0 && e.detail === 1) { 72 | this._toggle(); 73 | } 74 | }, this); 75 | } else { 76 | L.DomEvent.addListener(icon, 'mouseover', this._expand, this); 77 | L.DomEvent.addListener(icon, 'mouseout', this._collapse, this); 78 | this._map.on('movestart', this._collapse, this); 79 | } 80 | } else { 81 | this._expand(); 82 | } 83 | 84 | L.DomEvent.disableClickPropagation(container); 85 | 86 | return container; 87 | }, 88 | 89 | _geocodeResult: function (results) { 90 | L.DomUtil.removeClass(this._container, 'leaflet-control-geocoder-throbber'); 91 | if (results.length === 1) { 92 | this._geocodeResultSelected(results[0]); 93 | } else if (results.length > 0) { 94 | this._alts.innerHTML = ''; 95 | this._results = results; 96 | L.DomUtil.removeClass(this._alts, 'leaflet-control-geocoder-alternatives-minimized'); 97 | for (var i = 0; i < results.length; i++) { 98 | this._alts.appendChild(this._createAlt(results[i], i)); 99 | } 100 | } else { 101 | L.DomUtil.addClass(this._errorElement, 'leaflet-control-geocoder-error'); 102 | } 103 | }, 104 | 105 | markGeocode: function(result) { 106 | this._map.fitBounds(result.bbox); 107 | 108 | if (this._geocodeMarker) { 109 | this._map.removeLayer(this._geocodeMarker); 110 | } 111 | 112 | this._geocodeMarker = new L.Marker(result.center) 113 | .bindPopup(result.html || result.name) 114 | .addTo(this._map) 115 | .openPopup(); 116 | 117 | return this; 118 | }, 119 | 120 | _geocode: function(event) { 121 | L.DomEvent.preventDefault(event); 122 | 123 | L.DomUtil.addClass(this._container, 'leaflet-control-geocoder-throbber'); 124 | this._clearResults(); 125 | this.options.geocoder.geocode(this._input.value, this._geocodeResult, this); 126 | 127 | return false; 128 | }, 129 | 130 | _geocodeResultSelected: function(result) { 131 | if (this.options.collapsed) { 132 | this._collapse(); 133 | } else { 134 | this._clearResults(); 135 | } 136 | this.markGeocode(result); 137 | }, 138 | 139 | _toggle: function() { 140 | if (this._container.className.indexOf('leaflet-control-geocoder-expanded') >= 0) { 141 | this._collapse(); 142 | } else { 143 | this._expand(); 144 | } 145 | }, 146 | 147 | _expand: function () { 148 | L.DomUtil.addClass(this._container, 'leaflet-control-geocoder-expanded'); 149 | this._input.select(); 150 | }, 151 | 152 | _collapse: function () { 153 | this._container.className = this._container.className.replace(' leaflet-control-geocoder-expanded', ''); 154 | L.DomUtil.addClass(this._alts, 'leaflet-control-geocoder-alternatives-minimized'); 155 | L.DomUtil.removeClass(this._errorElement, 'leaflet-control-geocoder-error'); 156 | }, 157 | 158 | _clearResults: function () { 159 | L.DomUtil.addClass(this._alts, 'leaflet-control-geocoder-alternatives-minimized'); 160 | this._selection = null; 161 | L.DomUtil.removeClass(this._errorElement, 'leaflet-control-geocoder-error'); 162 | }, 163 | 164 | _createAlt: function(result, index) { 165 | var li = document.createElement('li'), 166 | a = L.DomUtil.create('a', '', li), 167 | icon = this.options.showResultIcons && result.icon ? L.DomUtil.create('img', '', a) : null, 168 | text = result.html ? undefined : document.createTextNode(result.name); 169 | 170 | if (icon) { 171 | icon.src = result.icon; 172 | } 173 | 174 | a.href = '#'; 175 | a.setAttribute('data-result-index', index); 176 | 177 | if (result.html) { 178 | a.innerHTML = result.html; 179 | } else { 180 | a.appendChild(text); 181 | } 182 | 183 | L.DomEvent.addListener(li, 'click', function clickHandler(e) { 184 | L.DomEvent.preventDefault(e); 185 | this._geocodeResultSelected(result); 186 | }, this); 187 | 188 | return li; 189 | }, 190 | 191 | _keydown: function(e) { 192 | var _this = this, 193 | select = function select(dir) { 194 | if (_this._selection) { 195 | L.DomUtil.removeClass(_this._selection.firstChild, 'leaflet-control-geocoder-selected'); 196 | _this._selection = _this._selection[dir > 0 ? 'nextSibling' : 'previousSibling']; 197 | } 198 | if (!_this._selection) { 199 | _this._selection = _this._alts[dir > 0 ? 'firstChild' : 'lastChild']; 200 | } 201 | 202 | if (_this._selection) { 203 | L.DomUtil.addClass(_this._selection.firstChild, 'leaflet-control-geocoder-selected'); 204 | } 205 | }; 206 | 207 | switch (e.keyCode) { 208 | // Escape 209 | case 27: 210 | this._collapse(); 211 | break; 212 | // Up 213 | case 38: 214 | select(-1); 215 | L.DomEvent.preventDefault(e); 216 | break; 217 | // Up 218 | case 40: 219 | select(1); 220 | L.DomEvent.preventDefault(e); 221 | break; 222 | // Enter 223 | case 13: 224 | if (this._selection) { 225 | var index = parseInt(this._selection.firstChild.getAttribute('data-result-index'), 10); 226 | this._geocodeResultSelected(this._results[index]); 227 | this._clearResults(); 228 | L.DomEvent.preventDefault(e); 229 | } 230 | } 231 | return true; 232 | } 233 | }); 234 | 235 | L.Control.geocoder = function(id, options) { 236 | return new L.Control.Geocoder(id, options); 237 | }; 238 | 239 | L.Control.Geocoder.callbackId = 0; 240 | L.Control.Geocoder.jsonp = function(url, params, callback, context, jsonpParam) { 241 | var callbackId = '_l_geocoder_' + (L.Control.Geocoder.callbackId++); 242 | params[jsonpParam || 'callback'] = callbackId; 243 | window[callbackId] = L.Util.bind(callback, context); 244 | var script = document.createElement('script'); 245 | script.type = 'text/javascript'; 246 | script.src = url + L.Util.getParamString(params); 247 | script.id = callbackId; 248 | document.getElementsByTagName('head')[0].appendChild(script); 249 | }; 250 | L.Control.Geocoder.getJSON = function(url, params, callback) { 251 | var xmlHttp = new XMLHttpRequest(); 252 | xmlHttp.open( "GET", url + L.Util.getParamString(params), true); 253 | xmlHttp.send(null); 254 | xmlHttp.onreadystatechange = function () { 255 | if (xmlHttp.readyState != 4) return; 256 | if (xmlHttp.status != 200 && req.status != 304) return; 257 | callback(JSON.parse(xmlHttp.response)); 258 | }; 259 | }; 260 | 261 | L.Control.Geocoder.template = function (str, data, htmlEscape) { 262 | return str.replace(/\{ *([\w_]+) *\}/g, function (str, key) { 263 | var value = data[key]; 264 | if (value === undefined) { 265 | value = ''; 266 | } else if (typeof value === 'function') { 267 | value = value(data); 268 | } 269 | return L.Control.Geocoder.htmlEscape(value); 270 | }); 271 | }; 272 | 273 | // Adapted from handlebars.js 274 | // https://github.com/wycats/handlebars.js/ 275 | L.Control.Geocoder.htmlEscape = (function() { 276 | var badChars = /[&<>"'`]/g; 277 | var possible = /[&<>"'`]/; 278 | var escape = { 279 | '&': '&', 280 | '<': '<', 281 | '>': '>', 282 | '"': '"', 283 | '\'': ''', 284 | '`': '`' 285 | }; 286 | 287 | function escapeChar(chr) { 288 | return escape[chr]; 289 | } 290 | 291 | return function(string) { 292 | if (string == null) { 293 | return ''; 294 | } else if (!string) { 295 | return string + ''; 296 | } 297 | 298 | // Force a string conversion as this will be done by the append regardless and 299 | // the regex test will do this transparently behind the scenes, causing issues if 300 | // an object's to string has escaped characters in it. 301 | string = '' + string; 302 | 303 | if (!possible.test(string)) { 304 | return string; 305 | } 306 | return string.replace(badChars, escapeChar); 307 | }; 308 | })(); 309 | 310 | L.Control.Geocoder.Nominatim = L.Class.extend({ 311 | options: { 312 | serviceUrl: '//nominatim.openstreetmap.org/', 313 | geocodingQueryParams: {}, 314 | reverseQueryParams: {}, 315 | htmlTemplate: function(r) { 316 | var a = r.address, 317 | parts = []; 318 | if (a.road || a.building) { 319 | parts.push('{building} {road} {house_number}'); 320 | } 321 | 322 | if (a.city || a.town || a.village) { 323 | parts.push('{postcode} {city}{town}{village}'); 325 | } 326 | 327 | if (a.state || a.country) { 328 | parts.push('{state} {country}'); 330 | } 331 | 332 | return L.Control.Geocoder.template(parts.join('
'), a, true); 333 | } 334 | }, 335 | 336 | initialize: function(options) { 337 | L.Util.setOptions(this, options); 338 | }, 339 | 340 | geocode: function(query, cb, context) { 341 | L.Control.Geocoder.jsonp(this.options.serviceUrl + 'search/', L.extend({ 342 | q: query, 343 | limit: 5, 344 | format: 'json', 345 | addressdetails: 1 346 | }, this.options.geocodingQueryParams), 347 | function(data) { 348 | var results = []; 349 | for (var i = data.length - 1; i >= 0; i--) { 350 | var bbox = data[i].boundingbox; 351 | for (var j = 0; j < 4; j++) bbox[j] = parseFloat(bbox[j]); 352 | results[i] = { 353 | icon: data[i].icon, 354 | name: data[i].display_name, 355 | html: this.options.htmlTemplate ? 356 | this.options.htmlTemplate(data[i]) 357 | : undefined, 358 | bbox: L.latLngBounds([bbox[0], bbox[2]], [bbox[1], bbox[3]]), 359 | center: L.latLng(data[i].lat, data[i].lon), 360 | properties: data[i] 361 | }; 362 | } 363 | cb.call(context, results); 364 | }, this, 'json_callback'); 365 | }, 366 | 367 | reverse: function(location, scale, cb, context) { 368 | L.Control.Geocoder.jsonp(this.options.serviceUrl + 'reverse/', L.extend({ 369 | lat: location.lat, 370 | lon: location.lng, 371 | zoom: Math.round(Math.log(scale / 256) / Math.log(2)), 372 | addressdetails: 1, 373 | format: 'json' 374 | }, this.options.reverseQueryParams), function(data) { 375 | var result = [], 376 | loc; 377 | 378 | if (data && data.lat && data.lon) { 379 | loc = L.latLng(data.lat, data.lon); 380 | result.push({ 381 | name: data.display_name, 382 | html: this.options.htmlTemplate ? 383 | this.options.htmlTemplate(data) 384 | : undefined, 385 | center: loc, 386 | bounds: L.latLngBounds(loc, loc), 387 | properties: data 388 | }); 389 | } 390 | 391 | cb.call(context, result); 392 | }, this, 'json_callback'); 393 | } 394 | }); 395 | 396 | L.Control.Geocoder.nominatim = function(options) { 397 | return new L.Control.Geocoder.Nominatim(options); 398 | }; 399 | 400 | L.Control.Geocoder.Bing = L.Class.extend({ 401 | initialize: function(key) { 402 | this.key = key; 403 | }, 404 | 405 | geocode : function (query, cb, context) { 406 | L.Control.Geocoder.jsonp('//dev.virtualearth.net/REST/v1/Locations', { 407 | query: query, 408 | key : this.key 409 | }, function(data) { 410 | var results = []; 411 | for (var i = data.resourceSets[0].resources.length - 1; i >= 0; i--) { 412 | var resource = data.resourceSets[0].resources[i], 413 | bbox = resource.bbox; 414 | results[i] = { 415 | name: resource.name, 416 | bbox: L.latLngBounds([bbox[0], bbox[1]], [bbox[2], bbox[3]]), 417 | center: L.latLng(resource.point.coordinates) 418 | }; 419 | } 420 | cb.call(context, results); 421 | }, this, 'jsonp'); 422 | }, 423 | 424 | reverse: function(location, scale, cb, context) { 425 | L.Control.Geocoder.jsonp('//dev.virtualearth.net/REST/v1/Locations/' + location.lat + ',' + location.lng, { 426 | key : this.key 427 | }, function(data) { 428 | var results = []; 429 | for (var i = data.resourceSets[0].resources.length - 1; i >= 0; i--) { 430 | var resource = data.resourceSets[0].resources[i], 431 | bbox = resource.bbox; 432 | results[i] = { 433 | name: resource.name, 434 | bbox: L.latLngBounds([bbox[0], bbox[1]], [bbox[2], bbox[3]]), 435 | center: L.latLng(resource.point.coordinates) 436 | }; 437 | } 438 | cb.call(context, results); 439 | }, this, 'jsonp'); 440 | } 441 | }); 442 | 443 | L.Control.Geocoder.bing = function(key) { 444 | return new L.Control.Geocoder.Bing(key); 445 | }; 446 | 447 | L.Control.Geocoder.RaveGeo = L.Class.extend({ 448 | options: { 449 | querySuffix: '', 450 | deepSearch: true, 451 | wordBased: false 452 | }, 453 | 454 | jsonp: function(params, callback, context) { 455 | var callbackId = '_l_geocoder_' + (L.Control.Geocoder.callbackId++), 456 | paramParts = []; 457 | params.prepend = callbackId + '('; 458 | params.append = ')'; 459 | for (var p in params) { 460 | paramParts.push(p + '=' + escape(params[p])); 461 | } 462 | 463 | window[callbackId] = L.Util.bind(callback, context); 464 | var script = document.createElement('script'); 465 | script.type = 'text/javascript'; 466 | script.src = this._serviceUrl + '?' + paramParts.join('&'); 467 | script.id = callbackId; 468 | document.getElementsByTagName('head')[0].appendChild(script); 469 | }, 470 | 471 | initialize: function(serviceUrl, scheme, options) { 472 | L.Util.setOptions(this, options); 473 | 474 | this._serviceUrl = serviceUrl; 475 | this._scheme = scheme; 476 | }, 477 | 478 | geocode: function(query, cb, context) { 479 | L.Control.Geocoder.jsonp(this._serviceUrl, { 480 | address: query + this.options.querySuffix, 481 | scheme: this._scheme, 482 | outputFormat: 'jsonp', 483 | deepSearch: this.options.deepSearch, 484 | wordBased: this.options.wordBased 485 | }, function(data) { 486 | var results = []; 487 | for (var i = data.length - 1; i >= 0; i--) { 488 | var r = data[i], 489 | c = L.latLng(r.y, r.x); 490 | results[i] = { 491 | name: r.address, 492 | bbox: L.latLngBounds([c]), 493 | center: c 494 | }; 495 | } 496 | cb.call(context, results); 497 | }, this); 498 | } 499 | }); 500 | 501 | L.Control.Geocoder.raveGeo = function(serviceUrl, scheme, options) { 502 | return new L.Control.Geocoder.RaveGeo(serviceUrl, scheme, options); 503 | }; 504 | 505 | L.Control.Geocoder.MapQuest = L.Class.extend({ 506 | initialize: function(key) { 507 | // MapQuest seems to provide URI encoded API keys, 508 | // so to avoid encoding them twice, we decode them here 509 | this._key = decodeURIComponent(key); 510 | }, 511 | 512 | _formatName: function() { 513 | var r = [], 514 | i; 515 | for (i = 0; i < arguments.length; i++) { 516 | if (arguments[i]) { 517 | r.push(arguments[i]); 518 | } 519 | } 520 | 521 | return r.join(', '); 522 | }, 523 | 524 | geocode: function(query, cb, context) { 525 | L.Control.Geocoder.jsonp('//www.mapquestapi.com/geocoding/v1/address', { 526 | key: this._key, 527 | location: query, 528 | limit: 5, 529 | outFormat: 'json' 530 | }, function(data) { 531 | var results = [], 532 | loc, 533 | latLng; 534 | if (data.results && data.results[0].locations) { 535 | for (var i = data.results[0].locations.length - 1; i >= 0; i--) { 536 | loc = data.results[0].locations[i]; 537 | latLng = L.latLng(loc.latLng); 538 | results[i] = { 539 | name: this._formatName(loc.street, loc.adminArea4, loc.adminArea3, loc.adminArea1), 540 | bbox: L.latLngBounds(latLng, latLng), 541 | center: latLng 542 | }; 543 | } 544 | } 545 | 546 | cb.call(context, results); 547 | }, this); 548 | }, 549 | 550 | reverse: function(location, scale, cb, context) { 551 | L.Control.Geocoder.jsonp('//www.mapquestapi.com/geocoding/v1/reverse', { 552 | key: this._key, 553 | location: location.lat + ',' + location.lng, 554 | outputFormat: 'json' 555 | }, function(data) { 556 | var results = [], 557 | loc, 558 | latLng; 559 | if (data.results && data.results[0].locations) { 560 | for (var i = data.results[0].locations.length - 1; i >= 0; i--) { 561 | loc = data.results[0].locations[i]; 562 | latLng = L.latLng(loc.latLng); 563 | results[i] = { 564 | name: this._formatName(loc.street, loc.adminArea4, loc.adminArea3, loc.adminArea1), 565 | bbox: L.latLngBounds(latLng, latLng), 566 | center: latLng 567 | }; 568 | } 569 | } 570 | 571 | cb.call(context, results); 572 | }, this); 573 | } 574 | }); 575 | 576 | L.Control.Geocoder.mapQuest = function(key) { 577 | return new L.Control.Geocoder.MapQuest(key); 578 | }; 579 | 580 | L.Control.Geocoder.Mapbox = L.Class.extend({ 581 | options: { 582 | service_url: 'https://api.tiles.mapbox.com/v4/geocode/mapbox.places-v1/' 583 | }, 584 | 585 | initialize: function(access_token) { 586 | this._access_token = access_token; 587 | }, 588 | 589 | geocode: function(query, cb, context) { 590 | L.Control.Geocoder.getJSON(this.options.service_url + encodeURIComponent(query) + '.json', { 591 | access_token: this._access_token, 592 | }, function(data) { 593 | var results = [], 594 | loc, 595 | latLng, 596 | latLngBounds; 597 | if (data.features && data.features.length) { 598 | for (var i = 0; i <= data.features.length - 1; i++) { 599 | loc = data.features[i]; 600 | latLng = L.latLng(loc.center.reverse()); 601 | if(loc.hasOwnProperty('bbox')) 602 | { 603 | latLngBounds = L.latLngBounds(L.latLng(loc.bbox.slice(0, 2).reverse()), L.latLng(loc.bbox.slice(2, 4).reverse())); 604 | } 605 | else 606 | { 607 | latLngBounds = L.latLngBounds(latLng, latLng); 608 | } 609 | results[i] = { 610 | name: loc.place_name, 611 | bbox: latLngBounds, 612 | center: latLng 613 | }; 614 | } 615 | } 616 | 617 | cb.call(context, results); 618 | }); 619 | }, 620 | 621 | reverse: function(location, scale, cb, context) { 622 | L.Control.Geocoder.getJSON(this.options.service_url + encodeURIComponent(location.lng) + ',' + encodeURIComponent(location.lat) + '.json', { 623 | access_token: this._access_token, 624 | }, function(data) { 625 | var results = [], 626 | loc, 627 | latLng, 628 | latLngBounds; 629 | if (data.features && data.features.length) { 630 | for (var i = 0; i <= data.features.length - 1; i++) { 631 | loc = data.features[i]; 632 | latLng = L.latLng(loc.center.reverse()); 633 | if(loc.hasOwnProperty('bbox')) 634 | { 635 | latLngBounds = L.latLngBounds(L.latLng(loc.bbox.slice(0, 2).reverse()), L.latLng(loc.bbox.slice(2, 4).reverse())); 636 | } 637 | else 638 | { 639 | latLngBounds = L.latLngBounds(latLng, latLng); 640 | } 641 | results[i] = { 642 | name: loc.place_name, 643 | bbox: latLngBounds, 644 | center: latLng 645 | }; 646 | } 647 | } 648 | 649 | cb.call(context, results); 650 | }); 651 | } 652 | }); 653 | 654 | L.Control.Geocoder.mapbox = function(access_token) { 655 | return new L.Control.Geocoder.Mapbox(access_token); 656 | }; 657 | 658 | L.Control.Geocoder.Google = L.Class.extend({ 659 | options: { 660 | service_url: 'https://maps.googleapis.com/maps/api/geocode/json' 661 | }, 662 | 663 | initialize: function(key) { 664 | this._key = key; 665 | }, 666 | 667 | geocode: function(query, cb, context) { 668 | var params = { 669 | address: query, 670 | }; 671 | if(this._key && this._key.length) 672 | { 673 | params['key'] = this._key 674 | } 675 | 676 | L.Control.Geocoder.getJSON(this.options.service_url, params, function(data) { 677 | var results = [], 678 | loc, 679 | latLng, 680 | latLngBounds; 681 | if (data.results && data.results.length) { 682 | for (var i = 0; i <= data.results.length - 1; i++) { 683 | loc = data.results[i]; 684 | latLng = L.latLng(loc.geometry.location); 685 | latLngBounds = L.latLngBounds(L.latLng(loc.geometry.viewport.northeast), L.latLng(loc.geometry.viewport.southwest)); 686 | results[i] = { 687 | name: loc.formatted_address, 688 | bbox: latLngBounds, 689 | center: latLng 690 | }; 691 | } 692 | } 693 | 694 | cb.call(context, results); 695 | }); 696 | }, 697 | 698 | reverse: function(location, scale, cb, context) { 699 | var params = { 700 | latlng: encodeURIComponent(location.lat) + ',' + encodeURIComponent(location.lng) 701 | }; 702 | if(this._key && this._key.length) 703 | { 704 | params['key'] = this._key 705 | } 706 | L.Control.Geocoder.getJSON(this.options.service_url, params, function(data) { 707 | var results = [], 708 | loc, 709 | latLng, 710 | latLngBounds; 711 | if (data.results && data.results.length) { 712 | for (var i = 0; i <= data.results.length - 1; i++) { 713 | loc = data.results[i]; 714 | latLng = L.latLng(loc.geometry.location); 715 | latLngBounds = L.latLngBounds(L.latLng(loc.geometry.viewport.northeast), L.latLng(loc.geometry.viewport.southwest)); 716 | results[i] = { 717 | name: loc.formatted_address, 718 | bbox: latLngBounds, 719 | center: latLng 720 | }; 721 | } 722 | } 723 | 724 | cb.call(context, results); 725 | }); 726 | } 727 | }); 728 | 729 | L.Control.Geocoder.google = function(key) { 730 | return new L.Control.Geocoder.Google(key); 731 | }; 732 | return L.Control.Geocoder; 733 | })); 734 | -------------------------------------------------------------------------------- /examples/index.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-family: sans-serif; 3 | font-size: 11pt; 4 | } 5 | 6 | .map { 7 | position: fixed; 8 | top: 0px; 9 | left: 0px; 10 | right: 0px; 11 | bottom: 0px; 12 | } 13 | 14 | .results { 15 | background-color: white; 16 | opacity: 0.8; 17 | position: absolute; 18 | top: 12px; 19 | right: 12px; 20 | width: 320px; 21 | height: 480px; 22 | overflow-y: scroll; 23 | } 24 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Leaflet OSRM Example 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /examples/index.js: -------------------------------------------------------------------------------- 1 | var map = L.map('map'); 2 | 3 | L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', { 4 | attribution: '© OpenStreetMap contributors' 5 | }).addTo(map); 6 | 7 | var routingControl = L.Routing.control({ 8 | waypoints: [ 9 | L.latLng(57.74, 11.94), 10 | L.latLng(57.6792, 11.949) 11 | ], 12 | geocoder: L.Control.Geocoder.nominatim(), 13 | router: L.Routing.graphHopper('your-api-key'), 14 | routeWhileDragging: false 15 | }).addTo(map); 16 | 17 | var router = routingControl.getRouter(); 18 | router.on('response',function(e){ 19 | console.log('This request consumed ' + e.credits + ' credit(s)'); 20 | console.log('You have ' + e.remaining + ' left'); 21 | }); 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lrm-graphhopper", 3 | "version": "1.3.0", 4 | "description": "Support for GraphHopper in Leaflet Routing Machine", 5 | "main": "src/L.Routing.GraphHopper.js", 6 | "scripts": { 7 | "dist": "./scripts/dist.sh", 8 | "publish": "./scripts/publish.sh", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "keywords": [ 12 | "leaflet", 13 | "routing", 14 | "graphhopper" 15 | ], 16 | "author": "Per Liedman ", 17 | "license": "ISC", 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/perliedman/lrm-graphhopper.git" 21 | }, 22 | "homepage": "https://github.com/perliedman/lrm-graphhopper", 23 | "bugs": "https://github.com/perliedman/lrm-graphhopper/issues", 24 | "dependencies": { 25 | "corslite": "0.0.6", 26 | "polyline": "0.0.3" 27 | }, 28 | "browserify-shim": { 29 | "leaflet": "global:L" 30 | }, 31 | "devDependencies": { 32 | "browserify": "^8.1.3", 33 | "browserify-shim": "^3.8.2", 34 | "uglify-js": "^2.4.16" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /scripts/dist.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | VERSION=`echo "console.log(require('./package.json').version)" | node` 4 | 5 | echo Building dist files for $VERSION... 6 | mkdir -p dist 7 | browserify -t browserify-shim src/L.Routing.GraphHopper.js >dist/lrm-graphhopper.js 8 | uglifyjs dist/lrm-graphhopper.js >dist/lrm-graphhopper.min.js 9 | echo Done. 10 | -------------------------------------------------------------------------------- /scripts/publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | VERSION=`echo "console.log(require('./package.json').version)" | node` 4 | ORIGIN=`git remote -v|grep origin|head -n1|cut -f2|cut -d" " -f1` 5 | TMP=/tmp/.gh-pages-update 6 | CWD=`pwd` 7 | 8 | ./scripts/dist.sh 9 | 10 | echo Updating dist files on gh-pages... 11 | rm -rf $TMP 12 | git clone -b gh-pages . $TMP 13 | cd $TMP 14 | git remote set-url origin $ORIGIN 15 | git fetch origin gh-pages 16 | git rebase origin/gh-pages 17 | 18 | mkdir -p dist 19 | mkdir -p _data 20 | cp -a $CWD/dist/lrm-graphhopper.js dist/lrm-graphhopper-$VERSION.js 21 | cp -a $CWD/dist/lrm-graphhopper.min.js dist/lrm-graphhopper-$VERSION.min.js 22 | echo -e "- version: $VERSION\n" >>_data/versions.yml 23 | 24 | echo `pwd` 25 | git add -f dist/ _data/ 26 | git commit -m "Dist files $VERSION" 27 | git push origin gh-pages 28 | cd $CWD 29 | rm -rf $TMP 30 | -------------------------------------------------------------------------------- /src/L.Routing.GraphHopper.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var L = require('leaflet'); 5 | var corslite = require('corslite'); 6 | var polyline = require('polyline'); 7 | 8 | L.Routing = L.Routing || {}; 9 | 10 | L.Routing.GraphHopper = L.Evented.extend({ 11 | options: { 12 | serviceUrl: 'https://graphhopper.com/api/1/route', 13 | timeout: 30 * 1000, 14 | urlParameters: {} 15 | }, 16 | 17 | initialize: function(apiKey, options) { 18 | this._apiKey = apiKey; 19 | L.Util.setOptions(this, options); 20 | }, 21 | 22 | route: function(waypoints, callback, context, options) { 23 | var timedOut = false, 24 | wps = [], 25 | url, 26 | timer, 27 | wp, 28 | i; 29 | 30 | options = options || {}; 31 | url = this.buildRouteUrl(waypoints, options); 32 | 33 | timer = setTimeout(function() { 34 | timedOut = true; 35 | callback.call(context || callback, { 36 | status: -1, 37 | message: 'GraphHopper request timed out.' 38 | }); 39 | }, this.options.timeout); 40 | 41 | // Create a copy of the waypoints, since they 42 | // might otherwise be asynchronously modified while 43 | // the request is being processed. 44 | for (i = 0; i < waypoints.length; i++) { 45 | wp = waypoints[i]; 46 | wps.push({ 47 | latLng: wp.latLng, 48 | name: wp.name, 49 | options: wp.options 50 | }); 51 | } 52 | 53 | corslite(url, L.bind(function(err, resp) { 54 | var data; 55 | 56 | clearTimeout(timer); 57 | if (!timedOut) { 58 | var fired = err ? err : resp; 59 | this.fire("response", { 60 | status: fired.status, 61 | limit: Number(fired.getResponseHeader("X-RateLimit-Limit")), 62 | remaining: Number(fired.getResponseHeader("X-RateLimit-Remaining")), 63 | reset: Number(fired.getResponseHeader("X-RateLimit-Reset")), 64 | credits: Number(fired.getResponseHeader("X-RateLimit-Credits")) 65 | }); 66 | if (!err) { 67 | data = JSON.parse(resp.responseText); 68 | this._routeDone(data, wps, callback, context); 69 | } else { 70 | var finalResponse; 71 | var responseText = err && err.responseText; 72 | try { 73 | finalResponse = JSON.parse(responseText); 74 | } catch (e) { 75 | finalResponse = responseText; 76 | } 77 | 78 | callback.call(context || callback, { 79 | status: -1, 80 | message: 'HTTP request failed: ' + err, 81 | response: finalResponse 82 | }); 83 | } 84 | } 85 | }, this)); 86 | 87 | return this; 88 | }, 89 | 90 | _routeDone: function(response, inputWaypoints, callback, context) { 91 | var alts = [], 92 | mappedWaypoints, 93 | coordinates, 94 | i, 95 | path; 96 | 97 | context = context || callback; 98 | if (response.info && response.info.errors && response.info.errors.length) { 99 | callback.call(context, { 100 | // TODO: include all errors 101 | status: response.info.errors[0].details, 102 | message: response.info.errors[0].message 103 | }); 104 | return; 105 | } 106 | 107 | for (i = 0; i < response.paths.length; i++) { 108 | path = response.paths[i]; 109 | coordinates = this._decodePolyline(path.points); 110 | if (path.points_order) { 111 | var tempWaypoints = []; 112 | for (i = 0; i < path.points_order.length; i++) { 113 | tempWaypoints.push(inputWaypoints[path.points_order[i]]); 114 | } 115 | inputWaypoints = tempWaypoints; 116 | } 117 | mappedWaypoints = 118 | this._mapWaypointIndices(inputWaypoints, path.instructions, coordinates); 119 | 120 | alts.push({ 121 | name: '', 122 | coordinates: coordinates, 123 | instructions: this._convertInstructions(path.instructions), 124 | summary: { 125 | totalDistance: path.distance, 126 | totalTime: path.time / 1000, 127 | totalAscend: path.ascend, 128 | }, 129 | inputWaypoints: inputWaypoints, 130 | actualWaypoints: mappedWaypoints.waypoints, 131 | waypointIndices: mappedWaypoints.waypointIndices 132 | }); 133 | } 134 | 135 | callback.call(context, null, alts); 136 | }, 137 | 138 | _decodePolyline: function(geometry) { 139 | var coords = polyline.decode(geometry, 5), 140 | latlngs = new Array(coords.length), 141 | i; 142 | for (i = 0; i < coords.length; i++) { 143 | latlngs[i] = new L.LatLng(coords[i][0], coords[i][1]); 144 | } 145 | 146 | return latlngs; 147 | }, 148 | 149 | _toWaypoints: function(inputWaypoints, vias) { 150 | var wps = [], 151 | i; 152 | for (i = 0; i < vias.length; i++) { 153 | wps.push({ 154 | latLng: L.latLng(vias[i]), 155 | name: inputWaypoints[i].name, 156 | options: inputWaypoints[i].options 157 | }); 158 | } 159 | 160 | return wps; 161 | }, 162 | 163 | buildRouteUrl: function(waypoints, options) { 164 | var computeInstructions = 165 | /* Instructions are always needed, 166 | since we do not have waypoint indices otherwise */ 167 | true, 168 | //!(options && options.geometryOnly), 169 | locs = [], 170 | i, 171 | baseUrl; 172 | 173 | for (i = 0; i < waypoints.length; i++) { 174 | locs.push('point=' + waypoints[i].latLng.lat + ',' + waypoints[i].latLng.lng); 175 | } 176 | 177 | baseUrl = this.options.serviceUrl + '?' + 178 | locs.join('&'); 179 | 180 | return baseUrl + L.Util.getParamString(L.extend({ 181 | instructions: computeInstructions, 182 | type: 'json', 183 | key: this._apiKey 184 | }, this.options.urlParameters), baseUrl); 185 | }, 186 | 187 | _convertInstructions: function(instructions) { 188 | var signToType = { 189 | '-7': 'SlightLeft', 190 | '-3': 'SharpLeft', 191 | '-2': 'Left', 192 | '-1': 'SlightLeft', 193 | 0: 'Straight', 194 | 1: 'SlightRight', 195 | 2: 'Right', 196 | 3: 'SharpRight', 197 | 4: 'DestinationReached', 198 | 5: 'WaypointReached', 199 | 6: 'Roundabout', 200 | 7: 'SlightRight' 201 | }, 202 | result = [], 203 | type, 204 | i, 205 | instr; 206 | 207 | for (i = 0; instructions && i < instructions.length; i++) { 208 | instr = instructions[i]; 209 | if (i === 0) { 210 | type = 'Head'; 211 | } else { 212 | type = signToType[instr.sign]; 213 | } 214 | result.push({ 215 | type: type, 216 | modifier: type, 217 | text: instr.text, 218 | distance: instr.distance, 219 | time: instr.time / 1000, 220 | index: instr.interval[0], 221 | exit: instr.exit_number 222 | }); 223 | } 224 | 225 | return result; 226 | }, 227 | 228 | _mapWaypointIndices: function(waypoints, instructions, coordinates) { 229 | var wps = [], 230 | wpIndices = [], 231 | i, 232 | idx; 233 | 234 | wpIndices.push(0); 235 | wps.push(new L.Routing.Waypoint(coordinates[0], waypoints[0].name)); 236 | 237 | for (i = 0; instructions && i < instructions.length; i++) { 238 | if (instructions[i].sign === 5) { // VIA_REACHED 239 | idx = instructions[i].interval[0]; 240 | wpIndices.push(idx); 241 | wps.push({ 242 | latLng: coordinates[idx], 243 | name: waypoints[wps.length + 1].name 244 | }); 245 | } 246 | } 247 | 248 | wpIndices.push(coordinates.length - 1); 249 | wps.push({ 250 | latLng: coordinates[coordinates.length - 1], 251 | name: waypoints[waypoints.length - 1].name 252 | }); 253 | 254 | return { 255 | waypointIndices: wpIndices, 256 | waypoints: wps 257 | }; 258 | } 259 | }); 260 | 261 | L.Routing.graphHopper = function(apiKey, options) { 262 | return new L.Routing.GraphHopper(apiKey, options); 263 | }; 264 | 265 | module.exports = L.Routing.GraphHopper; 266 | })(); 267 | --------------------------------------------------------------------------------