├── .gitignore ├── .jshintrc ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── ISSUE_TEMPLATE.md ├── LICENSE.md ├── README.md ├── bower.json ├── dist ├── leaflet-routing-machine.css ├── leaflet.routing.icons.png ├── leaflet.routing.icons.svg └── routing-icon.png ├── examples ├── Control.Geocoder.js ├── config.js ├── index.css ├── index.html ├── index.js ├── localized.html ├── localized.js ├── mapbox.html └── mapbox.js ├── package-lock.json ├── package.json ├── scripts └── publish.sh ├── src ├── autocomplete.js ├── control.js ├── error-control.js ├── formatter.js ├── geocoder-element.js ├── index.js ├── itinerary-builder.js ├── itinerary.js ├── line.js ├── localization.js ├── mapbox.js ├── osrm-v1.js ├── plan.js └── waypoint.js └── test ├── SpecRunner.html ├── lib └── jasmine-2.0.0 │ ├── boot.js │ ├── console.js │ ├── jasmine-html.js │ ├── jasmine.css │ ├── jasmine.js │ └── jasmine_favicon.png └── spec ├── formatter-spec.js └── osrmv1-spec.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/*.js 3 | .grunt/ 4 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .grunt/ 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | before_install: 2 | - npm install -g grunt-cli 3 | before_deploy: 4 | - grunt before-deploy 5 | after_deploy: 6 | - grunt after-deploy 7 | language: node_js 8 | node_js: 9 | - 6 10 | notifications: 11 | email: false 12 | deploy: 13 | provider: npm 14 | email: "per@liedman.net" 15 | "on": 16 | all_branches: true 17 | tags: true 18 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 2.2.0 (2015-03-03) 2 | 3 | 4 | #### Bug Fixes 5 | 6 | * **OSRM:** make sure waypoint options exist before checking them. ([2dd9d778]([object Object]/commit/2dd9d778baae30f0ef7f2d535a9d12ee7283a500)) 7 | 8 | 9 | #### Features 10 | 11 | * **plan:** routeWhileDragging also used when adding a new waypoint by dragging the route li ([6ab58fb2]([object Object]/commit/6ab58fb25d56f650ab0af4e168748e2997070582)) 12 | 13 | 14 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Hi there, 2 | before submitting a new issue, please consider the following: 3 | 4 | * [ ] This is actually an issue with Leaflet Routing Machine, not a question 5 | * [ ] You have read [the Leaflet Routing Machine documentation](http://www.liedman.net/leaflet-routing-machine/api/) and [tutorials](http://www.liedman.net/leaflet-routing-machine/tutorials/) 6 | * [ ] You have looked through existing issues 7 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ## ISC License 2 | 3 | Copyright (c) 2014, Per Liedman (per@liedman.net) 4 | Turn instruction icons Copyright (c) 2014, Mapbox (mapbox.com) 5 | 6 | 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. 7 | 8 | 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. 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [Leaflet Routing Machine]((https://www.liedman.net/leaflet-routing-machine/)) [![NPM version](https://img.shields.io/npm/v/leaflet-routing-machine.svg)](https://www.npmjs.com/package/leaflet-routing-machine) ![Leaflet 1.0 compatible!](https://img.shields.io/badge/Leaflet%201.0-%E2%9C%93-1EB300.svg?style=flat) [![Join the chat at https://gitter.im/leaflet-routing-machine/Lobby](https://badges.gitter.im/leaflet-routing-machine/Lobby.svg)](https://gitter.im/leaflet-routing-machine/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 2 | ======================= 3 | 4 | **This plugin is barely maintained!** It has been a good long while since I had any reason to work with routing or Leaflet, and it does not look like that will change. I do not spend time on maintaining this plugin, and only rarely can I find time to respond to issues. If you want to take over as maintainer, feel free to contact me at per@liedman.net. 5 | 6 | Find the way from A to B on a Leaflet map. The plugin supports multiple backends: 7 | 8 | * [OSRM](http://project-osrm.org/) - builtin and used by default (version 5) 9 | * [Mapbox Directions API](https://www.mapbox.com/developers/api/directions/) - builtin with the class `L.Routing.Mapbox` 10 | * [GraphHopper](https://graphhopper.com/) - through plugin [lrm-graphopper](https://github.com/perliedman/lrm-graphhopper) 11 | * [Mapzen Valhalla](https://mapzen.com/projects/valhalla/) - through plugin [lrm-valhalla](https://github.com/valhalla/lrm-valhalla) 12 | * [TomTom Online Routing API](http://developer.tomtom.com/io-docs) - through plugin [lrm-tomtom](https://github.com/mrohnstock/lrm-tomtom) by [Mathias Rohnstock](https://github.com/mrohnstock) 13 | * Legacy support for OSRM version 4 through plugin [lrm-osrm4](https://github.com/perliedman/lrm-osrm4) 14 | * Esri - through plugin [lrm-esri](https://github.com/jgravois/lrm-esri) 15 | 16 | ## Features 17 | 18 | * Standard Leaflet control, with Leaflet look and feel 19 | * Routing from start to destination, with possibility of via points 20 | * Add, edit and remove waypoints through both address input and using the map 21 | * Multiple language support 22 | * Highly customizable for advanced use 23 | * Customizable look (theming / skins) 24 | * Open Source released under ISC License (more or less equivalent with the MIT license) 25 | 26 | __Go to the [Leaflet Routing Machine site](https://www.liedman.net/leaflet-routing-machine/) for more information, demos, tutorials and more.__ 27 | 28 | **Update 2020-04-06:** Out of the box, Leaflet Routing Machine relies on OSRM's demo server. At this moment, the demo server (which is outside scope of the plugin and outside control of the plugin's author) is [no longer maintained, and its SSL certificate has expired](https://github.com/Project-OSRM/osrm-backend/issues/5655). The plugin will not work unless you [configure a routing backend](https://www.liedman.net/leaflet-routing-machine/tutorials/alternative-routers/) yourself. I am sorry to say that, to my knowledge, there is no good default. 29 | 30 | ## Support and New Features 31 | 32 | Leaflet Routing Machine is in many ways already a feature complete routing UI. Most likely, your requirements are already covered and require very little adaptation. 33 | 34 | For questions and discussions, you might want to look at [the Leaflet Routing Machine gitter](https://gitter.im/leaflet-routing-machine/Lobby). 35 | 36 | ## Building 37 | 38 | ```sh 39 | npm install 40 | ``` 41 | 42 | This requires [Node and npm](http://nodejs.org/), as well as `grunt`. 43 | 44 | # Usage 45 | 46 | Download [latest release](https://github.com/perliedman/leaflet-routing-machine/releases), or obtain the latest release via [unpkg.com](https://unpkg.com/). 47 | 48 | ```html 49 | 50 | 51 | ``` 52 | 53 | or via npm: 54 | 55 | ```sh 56 | npm install --save leaflet-routing-machine 57 | ``` 58 | 59 | ```javascript 60 | var L = require('leaflet'); 61 | require('leaflet-routing-machine'); 62 | 63 | ... 64 | ``` 65 | 66 | LRM attaches itself onto `L`. 67 | 68 | __Go to the [Leaflet Routing Machine site](http://www.liedman.net/leaflet-routing-machine/) for more information, demos, tutorials and more.__ 69 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "leaflet-routing-machine", 3 | "version": "3.2.5", 4 | "homepage": "https://github.com/perliedman/leaflet-routing-machine", 5 | "authors": [ 6 | "Per Liedman (http://www.liedman.net/)" 7 | ], 8 | "description": "Find the way from A to B on a Leaflet map, using OSRM as backend.", 9 | "keywords": [ 10 | "leaflet", 11 | "route" 12 | ], 13 | "license": "MIT", 14 | "main": [ 15 | "dist/leaflet.routing.icons.png", 16 | "dist/leaflet-routing-machine.css", 17 | "dist/leaflet.routing.icons.svg", 18 | "dist/leaflet-routing-machine.js", 19 | "dist/routing-icon.png" 20 | ], 21 | "ignore": [ 22 | "**/.*", 23 | "node_modules", 24 | "bower_components", 25 | "test", 26 | "tests", 27 | "src", 28 | "css", 29 | "Gruntfile.js", 30 | "package.json", 31 | "build.sh" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /dist/leaflet-routing-machine.css: -------------------------------------------------------------------------------- 1 | .leaflet-routing-container, .leaflet-routing-error { 2 | width: 320px; 3 | background-color: white; 4 | padding-top: 4px; 5 | transition: all 0.2s ease; 6 | box-sizing: border-box; 7 | } 8 | 9 | .leaflet-control-container .leaflet-routing-container-hide { 10 | width: 32px; 11 | height: 32px; 12 | } 13 | 14 | .leaflet-routing-container h2 { 15 | font-size: 14px; 16 | } 17 | 18 | .leaflet-routing-container h3 { 19 | font-size: 12px; 20 | font-weight: normal; 21 | } 22 | 23 | .leaflet-routing-collapsible .leaflet-routing-geocoders { 24 | margin-top: 20px; 25 | } 26 | 27 | .leaflet-routing-alt, .leaflet-routing-geocoders, .leaflet-routing-error { 28 | padding: 6px; 29 | margin-top: 2px; 30 | margin-bottom: 6px; 31 | border-bottom: 1px solid #ccc; 32 | max-height: 320px; 33 | overflow-y: auto; 34 | transition: all 0.2s ease; 35 | } 36 | 37 | .leaflet-control-container .leaflet-routing-container-hide .leaflet-routing-alt, 38 | .leaflet-control-container .leaflet-routing-container-hide .leaflet-routing-geocoders { 39 | display: none; 40 | } 41 | 42 | .leaflet-bar .leaflet-routing-alt:last-child { 43 | border-bottom: none; 44 | } 45 | 46 | .leaflet-routing-alt-minimized { 47 | color: #888; 48 | max-height: 64px; 49 | overflow: hidden; 50 | cursor: pointer; 51 | } 52 | 53 | .leaflet-routing-alt table { 54 | border-collapse: collapse; 55 | } 56 | 57 | .leaflet-routing-alt tr:hover { 58 | background-color: #eee; 59 | cursor: pointer; 60 | } 61 | 62 | .leaflet-routing-alt::-webkit-scrollbar { 63 | width: 8px; 64 | } 65 | 66 | .leaflet-routing-alt::-webkit-scrollbar-track { 67 | border-radius: 2px; 68 | background-color: #eee; 69 | } 70 | 71 | .leaflet-routing-alt::-webkit-scrollbar-thumb { 72 | border-radius: 2px; 73 | background-color: #888; 74 | } 75 | 76 | .leaflet-routing-icon { 77 | background-image: url('leaflet.routing.icons.png'); 78 | -webkit-background-size: 240px 20px; 79 | background-size: 240px 20px; 80 | background-repeat: no-repeat; 81 | margin: 0; 82 | content: ''; 83 | display: inline-block; 84 | vertical-align: top; 85 | width: 20px; 86 | height: 20px; 87 | } 88 | 89 | .leaflet-routing-icon-continue { background-position: 0 0; } 90 | .leaflet-routing-icon-sharp-right { background-position: -20px 0; } 91 | .leaflet-routing-icon-turn-right { background-position: -40px 0; } 92 | .leaflet-routing-icon-bear-right { background-position: -60px 0; } 93 | .leaflet-routing-icon-u-turn { background-position: -80px 0; } 94 | .leaflet-routing-icon-sharp-left { background-position: -100px 0; } 95 | .leaflet-routing-icon-turn-left { background-position: -120px 0; } 96 | .leaflet-routing-icon-bear-left { background-position: -140px 0; } 97 | .leaflet-routing-icon-depart { background-position: -160px 0; } 98 | .leaflet-routing-icon-enter-roundabout { background-position: -180px 0; } 99 | .leaflet-routing-icon-arrive { background-position: -200px 0; } 100 | .leaflet-routing-icon-via { background-position: -220px 0; } 101 | 102 | .leaflet-routing-geocoders div { 103 | padding: 4px 0px 4px 0px; 104 | } 105 | 106 | .leaflet-routing-geocoders input { 107 | width: 303px; 108 | width: calc(100% - 4px); 109 | line-height: 1.67; 110 | border: 1px solid #ccc; 111 | } 112 | 113 | .leaflet-routing-geocoders button { 114 | font: bold 18px 'Lucida Console', Monaco, monospace; 115 | border: 1px solid #ccc; 116 | border-radius: 4px; 117 | background-color: white; 118 | margin: 0; 119 | margin-right: 3px; 120 | float: right; 121 | cursor: pointer; 122 | transition: background-color 0.2s ease; 123 | } 124 | 125 | .leaflet-routing-add-waypoint:after { 126 | content: '+'; 127 | } 128 | 129 | .leaflet-routing-reverse-waypoints:after { 130 | font-weight: normal; 131 | content: '\21c5'; 132 | } 133 | 134 | .leaflet-routing-geocoders button:hover { 135 | background-color: #eee; 136 | } 137 | 138 | .leaflet-routing-geocoders input,.leaflet-routing-remove-waypoint,.leaflet-routing-geocoder { 139 | position: relative; 140 | } 141 | 142 | .leaflet-routing-geocoder-result { 143 | font: 12px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif; 144 | position: absolute; 145 | max-height: 0; 146 | overflow: hidden; 147 | transition: all 0.5s ease; 148 | z-index: 1000; /* Arbitrary, but try to be above "most" things. */ 149 | visibility: hidden; 150 | } 151 | 152 | .leaflet-routing-geocoder-result table { 153 | width: 100%; 154 | border: 1px solid #ccc; 155 | border-radius: 0 0 4px 4px; 156 | background-color: white; 157 | cursor: pointer; 158 | } 159 | 160 | .leaflet-routing-geocoder-result-open { 161 | max-height: 800px; 162 | visibility: visible; 163 | } 164 | 165 | .leaflet-routing-geocoder-selected, .leaflet-routing-geocoder-result tr:hover { 166 | background-color: #eee; 167 | } 168 | 169 | .leaflet-routing-geocoder-no-results { 170 | font-style: italic; 171 | color: #888; 172 | } 173 | 174 | .leaflet-routing-remove-waypoint { 175 | background-color: transparent; 176 | display: inline-block; 177 | vertical-align: middle; 178 | cursor: pointer; 179 | } 180 | 181 | .leaflet-routing-remove-waypoint:after { 182 | position: absolute; 183 | display: block; 184 | width: 15px; 185 | height: 1px; 186 | z-index: 1; 187 | right: 1px; 188 | top: 4px; 189 | bottom: 0; 190 | margin: auto; 191 | padding: 2px; 192 | font-size: 18px; 193 | font-weight: bold; 194 | content: "\00d7"; 195 | text-align: center; 196 | cursor: pointer; 197 | color: #ccc; 198 | background: white; 199 | padding-bottom: 16px; 200 | margin-top: -16px; 201 | padding-right: 4px; 202 | line-height: 1; 203 | } 204 | 205 | .leaflet-routing-remove-waypoint:hover { 206 | color: black; 207 | } 208 | 209 | .leaflet-routing-instruction-distance { 210 | width: 48px; 211 | } 212 | 213 | .leaflet-routing-collapse-btn { 214 | position: absolute; 215 | top: 0; 216 | right: 6px; 217 | font-size: 24px; 218 | color: #ccc; 219 | font-weight: bold; 220 | } 221 | 222 | .leaflet-routing-collapse-btn:after { 223 | content: '\00d7'; 224 | } 225 | 226 | .leaflet-routing-container-hide .leaflet-routing-collapse-btn { 227 | position: relative; 228 | left: 4px; 229 | top: 4px; 230 | display: block; 231 | width: 26px; 232 | height: 23px; 233 | background-image: url('routing-icon.png'); 234 | } 235 | 236 | .leaflet-routing-container-hide .leaflet-routing-collapse-btn:after { 237 | content: none; 238 | } 239 | 240 | .leaflet-top .leaflet-routing-container.leaflet-routing-container-hide { 241 | margin-top: 10px !important; 242 | } 243 | .leaflet-right .leaflet-routing-container.leaflet-routing-container-hide { 244 | margin-right: 10px !important; 245 | } 246 | .leaflet-bottom .leaflet-routing-container.leaflet-routing-container-hide { 247 | margin-bottom: 10px !important; 248 | } 249 | .leaflet-left .leaflet-routing-container.leaflet-routing-container-hide { 250 | margin-left: 10px !important; 251 | } 252 | 253 | @media only screen and (max-width: 640px) { 254 | .leaflet-routing-container { 255 | margin: 0 !important; 256 | padding: 0 !important; 257 | width: 100%; 258 | height: 100%; 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /dist/leaflet.routing.icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perliedman/leaflet-routing-machine/8dc939a314425684883dc6f016b3aa9d04c8b7ca/dist/leaflet.routing.icons.png -------------------------------------------------------------------------------- /dist/leaflet.routing.icons.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 21 | 23 | 49 | 56 | 60 | 64 | 68 | 72 | 73 | 75 | 76 | 78 | image/svg+xml 79 | 81 | 82 | 83 | 84 | 85 | 90 | 96 | 101 | 107 | 113 | 118 | 124 | 130 | 135 | 141 | 145 | 148 | 152 | 159 | 167 | A 178 | 179 | 186 | 194 | B 205 | 206 | 213 | V 226 | ia 238 | 239 | 240 | -------------------------------------------------------------------------------- /dist/routing-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perliedman/leaflet-routing-machine/8dc939a314425684883dc6f016b3aa9d04c8b7ca/dist/routing-icon.png -------------------------------------------------------------------------------- /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: 'https://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/config.js: -------------------------------------------------------------------------------- 1 | window.lrmConfig = { 2 | // serviceUrl: 'https://api.mapbox.com/directions/v5', 3 | // profile: 'mapbox/driving', 4 | }; 5 | -------------------------------------------------------------------------------- /examples/index.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-family: sans-serif; 3 | font-size: 11pt; 4 | } 5 | 6 | .map { 7 | position: fixed; 8 | top: 0; 9 | left: 0; 10 | right: 0; 11 | bottom: 0; 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 control = L.Routing.control(L.extend(window.lrmConfig, { 8 | waypoints: [ 9 | L.latLng(57.74, 11.94), 10 | L.latLng(57.6792, 11.949) 11 | ], 12 | geocoder: L.Control.Geocoder.nominatim(), 13 | routeWhileDragging: true, 14 | reverseWaypoints: true, 15 | showAlternatives: true, 16 | altLineOptions: { 17 | styles: [ 18 | {color: 'black', opacity: 0.15, weight: 9}, 19 | {color: 'white', opacity: 0.8, weight: 6}, 20 | {color: 'blue', opacity: 0.5, weight: 2} 21 | ] 22 | } 23 | })).addTo(map); 24 | 25 | L.Routing.errorControl(control).addTo(map); -------------------------------------------------------------------------------- /examples/localized.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Leaflet OSRM Example 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /examples/localized.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 control = L.Routing.control(L.extend(window.lrmConfig, { 8 | waypoints: [ 9 | L.latLng(57.74, 11.94), 10 | L.latLng(57.6792, 11.949) 11 | ], 12 | language: 'de', 13 | geocoder: L.Control.Geocoder.nominatim(), 14 | routeWhileDragging: true, 15 | reverseWaypoints: true, 16 | showAlternatives: true, 17 | altLineOptions: { 18 | styles: [ 19 | {color: 'black', opacity: 0.15, weight: 9}, 20 | {color: 'white', opacity: 0.8, weight: 6}, 21 | {color: 'blue', opacity: 0.5, weight: 2} 22 | ] 23 | } 24 | })).addTo(map); 25 | 26 | L.Routing.errorControl(control).addTo(map); -------------------------------------------------------------------------------- /examples/mapbox.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Leaflet OSRM Example 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/mapbox.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 control = 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 | routeWhileDragging: true, 14 | reverseWaypoints: true, 15 | showAlternatives: true, 16 | altLineOptions: { 17 | styles: [ 18 | {color: 'black', opacity: 0.15, weight: 9}, 19 | {color: 'white', opacity: 0.8, weight: 6}, 20 | {color: 'blue', opacity: 0.5, weight: 2} 21 | ] 22 | }, 23 | router: L.Routing.mapbox('your-token-here') 24 | }).addTo(map); 25 | 26 | L.Routing.errorControl(control).addTo(map); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "leaflet-routing-machine", 3 | "version": "3.3.0", 4 | "description": "Routing for Leaflet", 5 | "directories": { 6 | "example": "examples", 7 | "dist": "dist" 8 | }, 9 | "scripts": { 10 | "prepare": "browserify -t browserify-shim -p browserify-derequire -o dist/leaflet-routing-machine.js src/index.js && uglifyjs dist/leaflet-routing-machine.js >dist/leaflet-routing-machine.min.js", 11 | "publish": "scripts/publish.sh", 12 | "test": "echo \"Error: no test specified\" && exit 1" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git://github.com/perliedman/leaflet-routing-machine.git" 17 | }, 18 | "keywords": [ 19 | "leaflet", 20 | "routing", 21 | "osrm" 22 | ], 23 | "author": { 24 | "name": "Per Liedman", 25 | "email": "per@liedman.net" 26 | }, 27 | "license": "ISC", 28 | "bugs": { 29 | "url": "https://github.com/perliedman/leaflet-routing-machine/issues" 30 | }, 31 | "homepage": "https://github.com/perliedman/leaflet-routing-machine", 32 | "browserify-shim": { 33 | "leaflet": "global:L" 34 | }, 35 | "main": "./dist/leaflet-routing-machine.js", 36 | "devDependencies": { 37 | "browserify": "^14.1.0", 38 | "browserify-derequire": "^0.9.4", 39 | "browserify-shim": "^3.8.13", 40 | "derequire": "^2.0.6", 41 | "uglify-js": "^2.8.2" 42 | }, 43 | "dependencies": { 44 | "@mapbox/corslite": "0.0.7", 45 | "@mapbox/polyline": "^0.2.0", 46 | "osrm-text-instructions": "^0.13.2" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /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 | git checkout -b build 9 | 10 | echo Building dist files for $VERSION... 11 | npm install 12 | echo Done. 13 | 14 | git add dist/* -f 15 | git add bower.json -f 16 | 17 | git commit -m "v$VERSION" 18 | 19 | git tag v$VERSION -f 20 | git push origin build --tags -f 21 | 22 | echo Updating dist files on gh-pages... 23 | rm -rf $TMP 24 | git clone -b gh-pages . $TMP 25 | cd $TMP 26 | git remote set-url origin $ORIGIN 27 | git fetch origin gh-pages 28 | git rebase origin/gh-pages 29 | 30 | cp -a $CWD/dist $TMP 31 | git add -f dist/ 32 | git commit -m "Dist files $VERSION" 33 | git push origin gh-pages 34 | cd $CWD 35 | rm -rf $TMP 36 | 37 | git checkout master 38 | git branch -D build 39 | -------------------------------------------------------------------------------- /src/autocomplete.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var L = require('leaflet'); 5 | 6 | module.exports = L.Class.extend({ 7 | options: { 8 | timeout: 500, 9 | blurTimeout: 100, 10 | noResultsMessage: 'No results found.', 11 | attachResultsToContainer: false, 12 | }, 13 | 14 | initialize: function(elem, callback, context, options) { 15 | L.setOptions(this, options); 16 | 17 | this._elem = elem; 18 | this._resultFn = options.resultFn ? L.Util.bind(options.resultFn, options.resultContext) : null; 19 | this._autocomplete = options.autocompleteFn ? L.Util.bind(options.autocompleteFn, options.autocompleteContext) : null; 20 | this._selectFn = L.Util.bind(callback, context); 21 | this._container = L.DomUtil.create('div', 'leaflet-routing-geocoder-result', options.attachResultsToContainer ? this._elem.parentElement : undefined); 22 | this._resultTable = L.DomUtil.create('table', '', this._container); 23 | 24 | // TODO: looks a bit like a kludge to register same for input and keypress - 25 | // browsers supporting both will get duplicate events; just registering 26 | // input will not catch enter, though. 27 | L.DomEvent.addListener(this._elem, 'input', this._keyPressed, this); 28 | L.DomEvent.addListener(this._elem, 'keypress', this._keyPressed, this); 29 | L.DomEvent.addListener(this._elem, 'keydown', this._keyDown, this); 30 | L.DomEvent.addListener(this._elem, 'blur', function() { 31 | if (this._isOpen) { 32 | this.close(); 33 | } 34 | }, this); 35 | }, 36 | 37 | close: function() { 38 | L.DomUtil.removeClass(this._container, 'leaflet-routing-geocoder-result-open'); 39 | if (this.options.attachResultsToContainer) { 40 | this._container.style.position = 'absolute'; 41 | } 42 | this._isOpen = false; 43 | }, 44 | 45 | _open: function() { 46 | var rect = this._elem.getBoundingClientRect(); 47 | if (!this._container.parentElement) { 48 | // See notes section under https://developer.mozilla.org/en-US/docs/Web/API/Window/scrollX 49 | // This abomination is required to support all flavors of IE 50 | var scrollX = (window.pageXOffset !== undefined) ? window.pageXOffset 51 | : (document.documentElement || document.body.parentNode || document.body).scrollLeft; 52 | var scrollY = (window.pageYOffset !== undefined) ? window.pageYOffset 53 | : (document.documentElement || document.body.parentNode || document.body).scrollTop; 54 | this._container.style.left = (rect.left + scrollX) + 'px'; 55 | this._container.style.top = (rect.bottom + scrollY) + 'px'; 56 | this._container.style.width = (rect.right - rect.left) + 'px'; 57 | document.body.appendChild(this._container); 58 | } else if (this.options.attachResultsToContainer) { 59 | this._container.style.position = 'relative'; 60 | } 61 | 62 | L.DomUtil.addClass(this._container, 'leaflet-routing-geocoder-result-open'); 63 | this._isOpen = true; 64 | }, 65 | 66 | _setResults: function(results) { 67 | var i, 68 | tr, 69 | td, 70 | text; 71 | 72 | delete this._selection; 73 | this._results = results; 74 | 75 | while (this._resultTable.firstChild) { 76 | this._resultTable.removeChild(this._resultTable.firstChild); 77 | } 78 | 79 | for (i = 0; i < results.length; i++) { 80 | tr = L.DomUtil.create('tr', '', this._resultTable); 81 | tr.setAttribute('data-result-index', i); 82 | td = L.DomUtil.create('td', '', tr); 83 | 84 | if (this.options.formatGeocoderResult) { 85 | text = this.options.formatGeocoderResult(results[i]); 86 | } else { 87 | text = document.createTextNode(results[i].name); 88 | } 89 | 90 | td.appendChild(text); 91 | // mousedown + click because: 92 | // http://stackoverflow.com/questions/10652852/jquery-fire-click-before-blur-event 93 | L.DomEvent.addListener(td, 'mousedown', L.DomEvent.preventDefault); 94 | L.DomEvent.addListener(td, 'click', this._createClickListener(results[i])); 95 | } 96 | 97 | if (!i) { 98 | tr = L.DomUtil.create('tr', '', this._resultTable); 99 | td = L.DomUtil.create('td', 'leaflet-routing-geocoder-no-results', tr); 100 | td.innerHTML = this.options.noResultsMessage; 101 | } 102 | 103 | this._open(); 104 | 105 | if (results.length > 0) { 106 | // Select the first entry 107 | this._select(1); 108 | } 109 | }, 110 | 111 | _createClickListener: function(r) { 112 | var resultSelected = this._resultSelected(r); 113 | return L.bind(function() { 114 | this._elem.blur(); 115 | resultSelected(); 116 | }, this); 117 | }, 118 | 119 | _resultSelected: function(r) { 120 | return L.bind(function() { 121 | this.close(); 122 | this._elem.value = r.name; 123 | this._lastCompletedText = r.name; 124 | this._selectFn(r); 125 | }, this); 126 | }, 127 | 128 | _keyPressed: function(e) { 129 | var index; 130 | 131 | if (this._isOpen && e.keyCode === 13 && this._selection) { 132 | index = parseInt(this._selection.getAttribute('data-result-index'), 10); 133 | this._resultSelected(this._results[index])(); 134 | L.DomEvent.preventDefault(e); 135 | return; 136 | } 137 | 138 | if (e.keyCode === 13) { 139 | L.DomEvent.preventDefault(e); 140 | this._complete(this._resultFn, true); 141 | return; 142 | } 143 | 144 | if (this._autocomplete && document.activeElement === this._elem) { 145 | if (this._timer) { 146 | clearTimeout(this._timer); 147 | } 148 | this._timer = setTimeout(L.Util.bind(function() { this._complete(this._autocomplete); }, this), 149 | this.options.timeout); 150 | return; 151 | } 152 | 153 | this._unselect(); 154 | }, 155 | 156 | _select: function(dir) { 157 | var sel = this._selection; 158 | if (sel) { 159 | L.DomUtil.removeClass(sel.firstChild, 'leaflet-routing-geocoder-selected'); 160 | sel = sel[dir > 0 ? 'nextSibling' : 'previousSibling']; 161 | } 162 | if (!sel) { 163 | sel = this._resultTable[dir > 0 ? 'firstChild' : 'lastChild']; 164 | } 165 | 166 | if (sel) { 167 | L.DomUtil.addClass(sel.firstChild, 'leaflet-routing-geocoder-selected'); 168 | this._selection = sel; 169 | } 170 | }, 171 | 172 | _unselect: function() { 173 | if (this._selection) { 174 | L.DomUtil.removeClass(this._selection.firstChild, 'leaflet-routing-geocoder-selected'); 175 | } 176 | delete this._selection; 177 | }, 178 | 179 | _keyDown: function(e) { 180 | if (this._isOpen) { 181 | switch (e.keyCode) { 182 | // Escape 183 | case 27: 184 | this.close(); 185 | L.DomEvent.preventDefault(e); 186 | return; 187 | // Up 188 | case 38: 189 | this._select(-1); 190 | L.DomEvent.preventDefault(e); 191 | return; 192 | // Down 193 | case 40: 194 | this._select(1); 195 | L.DomEvent.preventDefault(e); 196 | return; 197 | } 198 | } 199 | }, 200 | 201 | _complete: function(completeFn, trySelect) { 202 | var v = this._elem.value; 203 | function completeResults(results) { 204 | this._lastCompletedText = v; 205 | if (trySelect && results.length === 1) { 206 | this._resultSelected(results[0])(); 207 | } else { 208 | this._setResults(results); 209 | } 210 | } 211 | 212 | if (!v) { 213 | return; 214 | } 215 | 216 | if (v !== this._lastCompletedText) { 217 | completeFn(v, completeResults, this); 218 | } else if (trySelect) { 219 | completeResults.call(this, this._results); 220 | } 221 | } 222 | }); 223 | })(); 224 | -------------------------------------------------------------------------------- /src/control.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var L = require('leaflet'); 5 | 6 | var Itinerary = require('./itinerary'); 7 | var Line = require('./line'); 8 | var Plan = require('./plan'); 9 | var OSRMv1 = require('./osrm-v1'); 10 | 11 | module.exports = Itinerary.extend({ 12 | options: { 13 | fitSelectedRoutes: 'smart', 14 | routeLine: function(route, options) { return new Line(route, options); }, 15 | autoRoute: true, 16 | routeWhileDragging: false, 17 | routeDragInterval: 500, 18 | waypointMode: 'connect', 19 | showAlternatives: false, 20 | defaultErrorHandler: function(e) { 21 | console.error('Routing error:', e.error); 22 | } 23 | }, 24 | 25 | initialize: function(options) { 26 | L.Util.setOptions(this, options); 27 | 28 | this._router = this.options.router || new OSRMv1(options); 29 | this._plan = this.options.plan || new Plan(this.options.waypoints, options); 30 | this._requestCount = 0; 31 | 32 | Itinerary.prototype.initialize.call(this, options); 33 | 34 | this.on('routeselected', this._routeSelected, this); 35 | if (this.options.defaultErrorHandler) { 36 | this.on('routingerror', this.options.defaultErrorHandler); 37 | } 38 | this._plan.on('waypointschanged', this._onWaypointsChanged, this); 39 | if (options.routeWhileDragging) { 40 | this._setupRouteDragging(); 41 | } 42 | }, 43 | 44 | _onZoomEnd: function() { 45 | if (!this._selectedRoute || 46 | !this._router.requiresMoreDetail) { 47 | return; 48 | } 49 | 50 | var map = this._map; 51 | if (this._router.requiresMoreDetail(this._selectedRoute, 52 | map.getZoom(), map.getBounds())) { 53 | this.route({ 54 | callback: L.bind(function(err, routes) { 55 | var i; 56 | if (!err) { 57 | for (i = 0; i < routes.length; i++) { 58 | this._routes[i].properties = routes[i].properties; 59 | } 60 | this._updateLineCallback(err, routes); 61 | } 62 | 63 | }, this), 64 | simplifyGeometry: false, 65 | geometryOnly: true 66 | }); 67 | } 68 | }, 69 | 70 | onAdd: function(map) { 71 | if (this.options.autoRoute) { 72 | this.route(); 73 | } 74 | 75 | var container = Itinerary.prototype.onAdd.call(this, map); 76 | 77 | this._map = map; 78 | this._map.addLayer(this._plan); 79 | 80 | this._map.on('zoomend', this._onZoomEnd, this); 81 | 82 | if (this._plan.options.geocoder) { 83 | container.insertBefore(this._plan.createGeocoders(), container.firstChild); 84 | } 85 | 86 | return container; 87 | }, 88 | 89 | onRemove: function(map) { 90 | map.off('zoomend', this._onZoomEnd, this); 91 | if (this._line) { 92 | map.removeLayer(this._line); 93 | } 94 | map.removeLayer(this._plan); 95 | if (this._alternatives && this._alternatives.length > 0) { 96 | for (var i = 0, len = this._alternatives.length; i < len; i++) { 97 | map.removeLayer(this._alternatives[i]); 98 | } 99 | } 100 | return Itinerary.prototype.onRemove.call(this, map); 101 | }, 102 | 103 | getWaypoints: function() { 104 | return this._plan.getWaypoints(); 105 | }, 106 | 107 | setWaypoints: function(waypoints) { 108 | this._plan.setWaypoints(waypoints); 109 | return this; 110 | }, 111 | 112 | spliceWaypoints: function() { 113 | var removed = this._plan.spliceWaypoints.apply(this._plan, arguments); 114 | return removed; 115 | }, 116 | 117 | getPlan: function() { 118 | return this._plan; 119 | }, 120 | 121 | getRouter: function() { 122 | return this._router; 123 | }, 124 | 125 | _routeSelected: function(e) { 126 | var route = this._selectedRoute = e.route, 127 | alternatives = this.options.showAlternatives && e.alternatives, 128 | fitMode = this.options.fitSelectedRoutes, 129 | fitBounds = 130 | (fitMode === 'smart' && !this._waypointsVisible()) || 131 | (fitMode !== 'smart' && fitMode); 132 | 133 | this._updateLines({route: route, alternatives: alternatives}); 134 | 135 | if (fitBounds) { 136 | this._map.fitBounds(this._line.getBounds()); 137 | } 138 | 139 | if (this.options.waypointMode === 'snap') { 140 | this._plan.off('waypointschanged', this._onWaypointsChanged, this); 141 | this.setWaypoints(route.waypoints); 142 | this._plan.on('waypointschanged', this._onWaypointsChanged, this); 143 | } 144 | }, 145 | 146 | _waypointsVisible: function() { 147 | var wps = this.getWaypoints(), 148 | mapSize, 149 | bounds, 150 | boundsSize, 151 | i, 152 | p; 153 | 154 | try { 155 | mapSize = this._map.getSize(); 156 | 157 | for (i = 0; i < wps.length; i++) { 158 | p = this._map.latLngToLayerPoint(wps[i].latLng); 159 | 160 | if (bounds) { 161 | bounds.extend(p); 162 | } else { 163 | bounds = L.bounds([p]); 164 | } 165 | } 166 | 167 | boundsSize = bounds.getSize(); 168 | return (boundsSize.x > mapSize.x / 5 || 169 | boundsSize.y > mapSize.y / 5) && this._waypointsInViewport(); 170 | 171 | } catch (e) { 172 | return false; 173 | } 174 | }, 175 | 176 | _waypointsInViewport: function() { 177 | var wps = this.getWaypoints(), 178 | mapBounds, 179 | i; 180 | 181 | try { 182 | mapBounds = this._map.getBounds(); 183 | } catch (e) { 184 | return false; 185 | } 186 | 187 | for (i = 0; i < wps.length; i++) { 188 | if (mapBounds.contains(wps[i].latLng)) { 189 | return true; 190 | } 191 | } 192 | 193 | return false; 194 | }, 195 | 196 | _updateLines: function(routes) { 197 | var addWaypoints = this.options.addWaypoints !== undefined ? 198 | this.options.addWaypoints : true; 199 | this._clearLines(); 200 | 201 | // add alternatives first so they lie below the main route 202 | this._alternatives = []; 203 | if (routes.alternatives) routes.alternatives.forEach(function(alt, i) { 204 | this._alternatives[i] = this.options.routeLine(alt, 205 | L.extend({ 206 | isAlternative: true 207 | }, this.options.altLineOptions || this.options.lineOptions)); 208 | this._alternatives[i].addTo(this._map); 209 | this._hookAltEvents(this._alternatives[i]); 210 | }, this); 211 | 212 | this._line = this.options.routeLine(routes.route, 213 | L.extend({ 214 | addWaypoints: addWaypoints, 215 | extendToWaypoints: this.options.waypointMode === 'connect' 216 | }, this.options.lineOptions)); 217 | this._line.addTo(this._map); 218 | this._hookEvents(this._line); 219 | }, 220 | 221 | _hookEvents: function(l) { 222 | l.on('linetouched', function(e) { 223 | if (e.afterIndex < this.getWaypoints().length - 1) { 224 | this._plan.dragNewWaypoint(e); 225 | } 226 | }, this); 227 | }, 228 | 229 | _hookAltEvents: function(l) { 230 | l.on('linetouched', function(e) { 231 | var alts = this._routes.slice(); 232 | var selected = alts.splice(e.target._route.routesIndex, 1)[0]; 233 | this.fire('routeselected', {route: selected, alternatives: alts}); 234 | }, this); 235 | }, 236 | 237 | _onWaypointsChanged: function(e) { 238 | if (this.options.autoRoute) { 239 | this.route({}); 240 | } 241 | if (!this._plan.isReady()) { 242 | this._clearLines(); 243 | this._clearAlts(); 244 | } 245 | this.fire('waypointschanged', {waypoints: e.waypoints}); 246 | }, 247 | 248 | _setupRouteDragging: function() { 249 | var timer = 0, 250 | waypoints; 251 | 252 | this._plan.on('waypointdrag', L.bind(function(e) { 253 | waypoints = e.waypoints; 254 | 255 | if (!timer) { 256 | timer = setTimeout(L.bind(function() { 257 | this.route({ 258 | waypoints: waypoints, 259 | geometryOnly: true, 260 | callback: L.bind(this._updateLineCallback, this) 261 | }); 262 | timer = undefined; 263 | }, this), this.options.routeDragInterval); 264 | } 265 | }, this)); 266 | this._plan.on('waypointdragend', function() { 267 | if (timer) { 268 | clearTimeout(timer); 269 | timer = undefined; 270 | } 271 | this.route(); 272 | }, this); 273 | }, 274 | 275 | _updateLineCallback: function(err, routes) { 276 | if (!err) { 277 | routes = routes.slice(); 278 | var selected = routes.splice(this._selectedRoute.routesIndex, 1)[0]; 279 | this._updateLines({ 280 | route: selected, 281 | alternatives: this.options.showAlternatives ? routes : [] 282 | }); 283 | } else if (err.type !== 'abort') { 284 | this._clearLines(); 285 | } 286 | }, 287 | 288 | route: function(options) { 289 | var ts = ++this._requestCount, 290 | wps; 291 | 292 | if (this._pendingRequest && this._pendingRequest.abort) { 293 | this._pendingRequest.abort(); 294 | this._pendingRequest = null; 295 | } 296 | 297 | options = options || {}; 298 | 299 | if (this._plan.isReady()) { 300 | if (this.options.useZoomParameter) { 301 | options.z = this._map && this._map.getZoom(); 302 | } 303 | 304 | wps = options && options.waypoints || this._plan.getWaypoints(); 305 | this.fire('routingstart', {waypoints: wps}); 306 | this._pendingRequest = this._router.route(wps, function(err, routes) { 307 | this._pendingRequest = null; 308 | 309 | if (options.callback) { 310 | return options.callback.call(this, err, routes); 311 | } 312 | 313 | // Prevent race among multiple requests, 314 | // by checking the current request's count 315 | // against the last request's; ignore result if 316 | // this isn't the last request. 317 | if (ts === this._requestCount) { 318 | this._clearLines(); 319 | this._clearAlts(); 320 | if (err && err.type !== 'abort') { 321 | this.fire('routingerror', {error: err}); 322 | return; 323 | } 324 | 325 | routes.forEach(function(route, i) { route.routesIndex = i; }); 326 | 327 | if (!options.geometryOnly) { 328 | this.fire('routesfound', {waypoints: wps, routes: routes}); 329 | this.setAlternatives(routes); 330 | } else { 331 | var selectedRoute = routes.splice(0,1)[0]; 332 | this._routeSelected({route: selectedRoute, alternatives: routes}); 333 | } 334 | } 335 | }, this, options); 336 | } 337 | }, 338 | 339 | _clearLines: function() { 340 | if (this._line) { 341 | this._map.removeLayer(this._line); 342 | delete this._line; 343 | } 344 | if (this._alternatives && this._alternatives.length) { 345 | for (var i in this._alternatives) { 346 | this._map.removeLayer(this._alternatives[i]); 347 | } 348 | this._alternatives = []; 349 | } 350 | } 351 | }); 352 | })(); 353 | -------------------------------------------------------------------------------- /src/error-control.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var L = require('leaflet'); 5 | 6 | module.exports = L.Control.extend({ 7 | options: { 8 | header: 'Routing error', 9 | formatMessage: function(error) { 10 | if (error.status < 0) { 11 | return 'Calculating the route caused an error. Technical description follows:
' +
12 | 						error.message + '
= 1000) { 38 | data = { 39 | value: round(d / 1609.344, sensitivity), 40 | unit: un.miles 41 | }; 42 | } else { 43 | data = { 44 | value: round(yards, sensitivity), 45 | unit: un.yards 46 | }; 47 | } 48 | } else { 49 | v = round(d, sensitivity); 50 | data = { 51 | value: v >= 1000 ? (v / 1000) : v, 52 | unit: v >= 1000 ? un.kilometers : un.meters 53 | }; 54 | } 55 | 56 | if (simpleRounding) { 57 | data.value = data.value.toFixed(-sensitivity); 58 | } 59 | 60 | return L.Util.template(this.options.distanceTemplate, data); 61 | }, 62 | 63 | _round: function(d, sensitivity) { 64 | var s = sensitivity || this.options.roundingSensitivity, 65 | pow10 = Math.pow(10, (Math.floor(d / s) + '').length - 1), 66 | r = Math.floor(d / pow10), 67 | p = (r > 5) ? pow10 : pow10 / 2; 68 | 69 | return Math.round(d / p) * p; 70 | }, 71 | 72 | formatTime: function(t /* Number (seconds) */) { 73 | var un = this.options.unitNames || this._localization.localize('units'); 74 | // More than 30 seconds precision looks ridiculous 75 | t = Math.round(t / 30) * 30; 76 | 77 | if (t > 86400) { 78 | return Math.round(t / 3600) + ' ' + un.hours; 79 | } else if (t > 3600) { 80 | return Math.floor(t / 3600) + ' ' + un.hours + ' ' + 81 | Math.round((t % 3600) / 60) + ' ' + un.minutes; 82 | } else if (t > 300) { 83 | return Math.round(t / 60) + ' ' + un.minutes; 84 | } else if (t > 60) { 85 | return Math.floor(t / 60) + ' ' + un.minutes + 86 | (t % 60 !== 0 ? ' ' + (t % 60) + ' ' + un.seconds : ''); 87 | } else { 88 | return t + ' ' + un.seconds; 89 | } 90 | }, 91 | 92 | formatInstruction: function(instr, i) { 93 | if (instr.text === undefined) { 94 | return this.capitalize(L.Util.template(this._getInstructionTemplate(instr, i), 95 | L.extend({}, instr, { 96 | exitStr: instr.exit ? this._localization.localize('formatOrder')(instr.exit) : '', 97 | dir: this._localization.localize(['directions', instr.direction]), 98 | modifier: this._localization.localize(['directions', instr.modifier]) 99 | }))); 100 | } else { 101 | return instr.text; 102 | } 103 | }, 104 | 105 | getIconName: function(instr, i) { 106 | switch (instr.type) { 107 | case 'Head': 108 | if (i === 0) { 109 | return 'depart'; 110 | } 111 | break; 112 | case 'WaypointReached': 113 | return 'via'; 114 | case 'Roundabout': 115 | return 'enter-roundabout'; 116 | case 'DestinationReached': 117 | return 'arrive'; 118 | } 119 | 120 | switch (instr.modifier) { 121 | case 'Straight': 122 | return 'continue'; 123 | case 'SlightRight': 124 | return 'bear-right'; 125 | case 'Right': 126 | return 'turn-right'; 127 | case 'SharpRight': 128 | return 'sharp-right'; 129 | case 'TurnAround': 130 | case 'Uturn': 131 | return 'u-turn'; 132 | case 'SharpLeft': 133 | return 'sharp-left'; 134 | case 'Left': 135 | return 'turn-left'; 136 | case 'SlightLeft': 137 | return 'bear-left'; 138 | } 139 | }, 140 | 141 | capitalize: function(s) { 142 | return s.charAt(0).toUpperCase() + s.substring(1); 143 | }, 144 | 145 | _getInstructionTemplate: function(instr, i) { 146 | var type = instr.type === 'Straight' ? (i === 0 ? 'Head' : 'Continue') : instr.type, 147 | strings = this._localization.localize(['instructions', type]); 148 | 149 | if (!strings) { 150 | strings = [ 151 | this._localization.localize(['directions', type]), 152 | ' ' + this._localization.localize(['instructions', 'Onto']) 153 | ]; 154 | } 155 | 156 | return strings[0] + (strings.length > 1 && instr.road ? strings[1] : ''); 157 | } 158 | }); 159 | })(); 160 | -------------------------------------------------------------------------------- /src/geocoder-element.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var L = require('leaflet'); 5 | var Autocomplete = require('./autocomplete'); 6 | var Localization = require('./localization'); 7 | 8 | function selectInputText(input) { 9 | if (input.setSelectionRange) { 10 | // On iOS, select() doesn't work 11 | input.setSelectionRange(0, 9999); 12 | } else { 13 | // On at least IE8, setSeleectionRange doesn't exist 14 | input.select(); 15 | } 16 | } 17 | 18 | module.exports = L.Class.extend({ 19 | includes: ((typeof L.Evented !== 'undefined' && L.Evented.prototype) || L.Mixin.Events), 20 | 21 | options: { 22 | createGeocoder: function(i, nWps, options) { 23 | var container = L.DomUtil.create('div', 'leaflet-routing-geocoder'), 24 | input = L.DomUtil.create('input', '', container), 25 | remove = options.addWaypoints ? L.DomUtil.create('span', 'leaflet-routing-remove-waypoint', container) : undefined; 26 | 27 | input.disabled = !options.addWaypoints; 28 | 29 | return { 30 | container: container, 31 | input: input, 32 | closeButton: remove 33 | }; 34 | }, 35 | geocoderPlaceholder: function(i, numberWaypoints, geocoderElement) { 36 | var l = new Localization(geocoderElement.options.language).localize('ui'); 37 | return i === 0 ? 38 | l.startPlaceholder : 39 | (i < numberWaypoints - 1 ? 40 | L.Util.template(l.viaPlaceholder, {viaNumber: i}) : 41 | l.endPlaceholder); 42 | }, 43 | 44 | geocoderClass: function() { 45 | return ''; 46 | }, 47 | 48 | waypointNameFallback: function(latLng) { 49 | var ns = latLng.lat < 0 ? 'S' : 'N', 50 | ew = latLng.lng < 0 ? 'W' : 'E', 51 | lat = (Math.round(Math.abs(latLng.lat) * 10000) / 10000).toString(), 52 | lng = (Math.round(Math.abs(latLng.lng) * 10000) / 10000).toString(); 53 | return ns + lat + ', ' + ew + lng; 54 | }, 55 | maxGeocoderTolerance: 200, 56 | autocompleteOptions: {}, 57 | language: 'en', 58 | }, 59 | 60 | initialize: function(wp, i, nWps, options) { 61 | L.setOptions(this, options); 62 | 63 | var g = this.options.createGeocoder(i, nWps, this.options), 64 | closeButton = g.closeButton, 65 | geocoderInput = g.input; 66 | geocoderInput.setAttribute('placeholder', this.options.geocoderPlaceholder(i, nWps, this)); 67 | geocoderInput.className = this.options.geocoderClass(i, nWps); 68 | 69 | this._element = g; 70 | this._waypoint = wp; 71 | 72 | this.update(); 73 | // This has to be here, or geocoder's value will not be properly 74 | // initialized. 75 | // TODO: look into why and make _updateWaypointName fix this. 76 | geocoderInput.value = wp.name; 77 | 78 | L.DomEvent.addListener(geocoderInput, 'click', function() { 79 | selectInputText(this); 80 | }, geocoderInput); 81 | 82 | if (closeButton) { 83 | L.DomEvent.addListener(closeButton, 'click', function() { 84 | this.fire('delete', { waypoint: this._waypoint }); 85 | }, this); 86 | } 87 | 88 | if (typeof this.options.formatGeocoderResult == 'function') { 89 | this.options.autocompleteOptions.formatGeocoderResult = this.options.formatGeocoderResult; 90 | } 91 | 92 | new Autocomplete(geocoderInput, function(r) { 93 | geocoderInput.value = r.name; 94 | wp.name = r.name; 95 | wp.latLng = r.center; 96 | this.fire('geocoded', { waypoint: wp, value: r }); 97 | }, this, L.extend({ 98 | resultFn: this.options.geocoder.geocode, 99 | resultContext: this.options.geocoder, 100 | autocompleteFn: this.options.geocoder.suggest, 101 | autocompleteContext: this.options.geocoder 102 | }, this.options.autocompleteOptions)); 103 | }, 104 | 105 | getContainer: function() { 106 | return this._element.container; 107 | }, 108 | 109 | setValue: function(v) { 110 | this._element.input.value = v; 111 | }, 112 | 113 | update: function(force) { 114 | var wp = this._waypoint, 115 | wpCoords; 116 | 117 | wp.name = wp.name || ''; 118 | 119 | if (wp.latLng && (force || !wp.name)) { 120 | wpCoords = this.options.waypointNameFallback(wp.latLng); 121 | if (this.options.geocoder && this.options.geocoder.reverse) { 122 | this.options.geocoder.reverse(wp.latLng, 67108864 /* zoom 18 */, function(rs) { 123 | if (rs.length > 0 && rs[0].center.distanceTo(wp.latLng) < this.options.maxGeocoderTolerance) { 124 | wp.name = rs[0].name; 125 | } else { 126 | wp.name = wpCoords; 127 | } 128 | this._update(); 129 | }, this); 130 | } else { 131 | wp.name = wpCoords; 132 | this._update(); 133 | } 134 | } 135 | }, 136 | 137 | focus: function() { 138 | var input = this._element.input; 139 | input.focus(); 140 | selectInputText(input); 141 | }, 142 | 143 | _update: function() { 144 | var wp = this._waypoint, 145 | value = wp && wp.name ? wp.name : ''; 146 | this.setValue(value); 147 | this.fire('reversegeocoded', {waypoint: wp, value: value}); 148 | } 149 | }); 150 | })(); 151 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | var L = require('leaflet'), 2 | Control = require('./control'), 3 | Itinerary = require('./itinerary'), 4 | Line = require('./line'), 5 | OSRMv1 = require('./osrm-v1'), 6 | Plan = require('./plan'), 7 | Waypoint = require('./waypoint'), 8 | Autocomplete = require('./autocomplete'), 9 | Formatter = require('./formatter'), 10 | GeocoderElement = require('./geocoder-element'), 11 | Localization = require('./localization'), 12 | ItineraryBuilder = require('./itinerary-builder'), 13 | Mapbox = require('./mapbox'), 14 | ErrorControl = require('./error-control'); 15 | 16 | L.routing = { 17 | control: function(options) { return new Control(options); }, 18 | itinerary: function(options) { 19 | return Itinerary(options); 20 | }, 21 | line: function(route, options) { 22 | return new Line(route, options); 23 | }, 24 | plan: function(waypoints, options) { 25 | return new Plan(waypoints, options); 26 | }, 27 | waypoint: function(latLng, name, options) { 28 | return new Waypoint(latLng, name, options); 29 | }, 30 | osrmv1: function(options) { 31 | return new OSRMv1(options); 32 | }, 33 | localization: function(options) { 34 | return new Localization(options); 35 | }, 36 | formatter: function(options) { 37 | return new Formatter(options); 38 | }, 39 | geocoderElement: function(wp, i, nWps, plan) { 40 | return new L.Routing.GeocoderElement(wp, i, nWps, plan); 41 | }, 42 | itineraryBuilder: function(options) { 43 | return new ItineraryBuilder(options); 44 | }, 45 | mapbox: function(accessToken, options) { 46 | return new Mapbox(accessToken, options); 47 | }, 48 | errorControl: function(routingControl, options) { 49 | return new ErrorControl(routingControl, options); 50 | }, 51 | autocomplete: function(elem, callback, context, options) { 52 | return new Autocomplete(elem, callback, context, options); 53 | } 54 | }; 55 | 56 | module.exports = L.Routing = { 57 | Control: Control, 58 | Itinerary: Itinerary, 59 | Line: Line, 60 | OSRMv1: OSRMv1, 61 | Plan: Plan, 62 | Waypoint: Waypoint, 63 | Autocomplete: Autocomplete, 64 | Formatter: Formatter, 65 | GeocoderElement: GeocoderElement, 66 | Localization: Localization, 67 | ItineraryBuilder: ItineraryBuilder, 68 | 69 | // Legacy; remove these in next major release 70 | control: L.routing.control, 71 | itinerary: L.routing.itinerary, 72 | line: L.routing.line, 73 | plan: L.routing.plan, 74 | waypoint: L.routing.waypoint, 75 | osrmv1: L.routing.osrmv1, 76 | geocoderElement: L.routing.geocoderElement, 77 | mapbox: L.routing.mapbox, 78 | errorControl: L.routing.errorControl, 79 | }; 80 | -------------------------------------------------------------------------------- /src/itinerary-builder.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var L = require('leaflet'); 5 | 6 | module.exports = L.Class.extend({ 7 | options: { 8 | containerClassName: '' 9 | }, 10 | 11 | initialize: function(options) { 12 | L.setOptions(this, options); 13 | }, 14 | 15 | createContainer: function(className) { 16 | var table = L.DomUtil.create('table', (className || '') + ' ' + this.options.containerClassName), 17 | colgroup = L.DomUtil.create('colgroup', '', table); 18 | 19 | L.DomUtil.create('col', 'leaflet-routing-instruction-icon', colgroup); 20 | L.DomUtil.create('col', 'leaflet-routing-instruction-text', colgroup); 21 | L.DomUtil.create('col', 'leaflet-routing-instruction-distance', colgroup); 22 | 23 | return table; 24 | }, 25 | 26 | createStepsContainer: function() { 27 | return L.DomUtil.create('tbody', ''); 28 | }, 29 | 30 | createStep: function(text, distance, icon, steps) { 31 | var row = L.DomUtil.create('tr', '', steps), 32 | span, 33 | td; 34 | td = L.DomUtil.create('td', '', row); 35 | span = L.DomUtil.create('span', 'leaflet-routing-icon leaflet-routing-icon-'+icon, td); 36 | td.appendChild(span); 37 | td = L.DomUtil.create('td', '', row); 38 | td.appendChild(document.createTextNode(text)); 39 | td = L.DomUtil.create('td', '', row); 40 | td.appendChild(document.createTextNode(distance)); 41 | return row; 42 | } 43 | }); 44 | })(); 45 | -------------------------------------------------------------------------------- /src/itinerary.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var L = require('leaflet'); 5 | var Formatter = require('./formatter'); 6 | var ItineraryBuilder = require('./itinerary-builder'); 7 | 8 | module.exports = L.Control.extend({ 9 | includes: ((typeof L.Evented !== 'undefined' && L.Evented.prototype) || L.Mixin.Events), 10 | 11 | options: { 12 | pointMarkerStyle: { 13 | radius: 5, 14 | color: '#03f', 15 | fillColor: 'white', 16 | opacity: 1, 17 | fillOpacity: 0.7 18 | }, 19 | summaryTemplate: '

{name}

{distance}, {time}

', 20 | timeTemplate: '{time}', 21 | containerClassName: '', 22 | alternativeClassName: '', 23 | minimizedClassName: '', 24 | itineraryClassName: '', 25 | totalDistanceRoundingSensitivity: -1, 26 | show: true, 27 | collapsible: undefined, 28 | collapseBtn: function(itinerary) { 29 | var collapseBtn = L.DomUtil.create('span', itinerary.options.collapseBtnClass); 30 | L.DomEvent.on(collapseBtn, 'click', itinerary._toggle, itinerary); 31 | itinerary._container.insertBefore(collapseBtn, itinerary._container.firstChild); 32 | }, 33 | collapseBtnClass: 'leaflet-routing-collapse-btn' 34 | }, 35 | 36 | initialize: function(options) { 37 | L.setOptions(this, options); 38 | this._formatter = this.options.formatter || new Formatter(this.options); 39 | this._itineraryBuilder = this.options.itineraryBuilder || new ItineraryBuilder({ 40 | containerClassName: this.options.itineraryClassName 41 | }); 42 | }, 43 | 44 | onAdd: function(map) { 45 | var collapsible = this.options.collapsible; 46 | 47 | collapsible = collapsible || (collapsible === undefined && map.getSize().x <= 640); 48 | 49 | this._container = L.DomUtil.create('div', 'leaflet-routing-container leaflet-bar ' + 50 | (!this.options.show ? 'leaflet-routing-container-hide ' : '') + 51 | (collapsible ? 'leaflet-routing-collapsible ' : '') + 52 | this.options.containerClassName); 53 | this._altContainer = this.createAlternativesContainer(); 54 | this._container.appendChild(this._altContainer); 55 | L.DomEvent.disableClickPropagation(this._container); 56 | L.DomEvent.addListener(this._container, 'mousewheel', function(e) { 57 | L.DomEvent.stopPropagation(e); 58 | }); 59 | 60 | if (collapsible) { 61 | this.options.collapseBtn(this); 62 | } 63 | 64 | return this._container; 65 | }, 66 | 67 | onRemove: function() { 68 | }, 69 | 70 | createAlternativesContainer: function() { 71 | return L.DomUtil.create('div', 'leaflet-routing-alternatives-container'); 72 | }, 73 | 74 | setAlternatives: function(routes) { 75 | var i, 76 | alt, 77 | altDiv; 78 | 79 | this._clearAlts(); 80 | 81 | this._routes = routes; 82 | 83 | for (i = 0; i < this._routes.length; i++) { 84 | alt = this._routes[i]; 85 | altDiv = this._createAlternative(alt, i); 86 | this._altContainer.appendChild(altDiv); 87 | this._altElements.push(altDiv); 88 | } 89 | 90 | this._selectRoute({route: this._routes[0], alternatives: this._routes.slice(1)}); 91 | 92 | return this; 93 | }, 94 | 95 | show: function() { 96 | L.DomUtil.removeClass(this._container, 'leaflet-routing-container-hide'); 97 | }, 98 | 99 | hide: function() { 100 | L.DomUtil.addClass(this._container, 'leaflet-routing-container-hide'); 101 | }, 102 | 103 | _toggle: function() { 104 | var collapsed = L.DomUtil.hasClass(this._container, 'leaflet-routing-container-hide'); 105 | this[collapsed ? 'show' : 'hide'](); 106 | }, 107 | 108 | _createAlternative: function(alt, i) { 109 | var altDiv = L.DomUtil.create('div', 'leaflet-routing-alt ' + 110 | this.options.alternativeClassName + 111 | (i > 0 ? ' leaflet-routing-alt-minimized ' + this.options.minimizedClassName : '')), 112 | template = this.options.summaryTemplate, 113 | data = L.extend({ 114 | name: alt.name, 115 | distance: this._formatter.formatDistance(alt.summary.totalDistance, this.options.totalDistanceRoundingSensitivity), 116 | time: this._formatter.formatTime(alt.summary.totalTime) 117 | }, alt); 118 | altDiv.innerHTML = typeof(template) === 'function' ? template(data) : L.Util.template(template, data); 119 | L.DomEvent.addListener(altDiv, 'click', this._onAltClicked, this); 120 | this.on('routeselected', this._selectAlt, this); 121 | 122 | altDiv.appendChild(this._createItineraryContainer(alt)); 123 | return altDiv; 124 | }, 125 | 126 | _clearAlts: function() { 127 | var el = this._altContainer; 128 | while (el && el.firstChild) { 129 | el.removeChild(el.firstChild); 130 | } 131 | 132 | this._altElements = []; 133 | }, 134 | 135 | _createItineraryContainer: function(r) { 136 | var container = this._itineraryBuilder.createContainer(), 137 | steps = this._itineraryBuilder.createStepsContainer(), 138 | i, 139 | instr, 140 | step, 141 | distance, 142 | text, 143 | icon; 144 | 145 | container.appendChild(steps); 146 | 147 | for (i = 0; i < r.instructions.length; i++) { 148 | instr = r.instructions[i]; 149 | text = this._formatter.formatInstruction(instr, i); 150 | distance = this._formatter.formatDistance(instr.distance); 151 | icon = this._formatter.getIconName(instr, i); 152 | step = this._itineraryBuilder.createStep(text, distance, icon, steps); 153 | 154 | if(instr.index) { 155 | this._addRowListeners(step, r.coordinates[instr.index]); 156 | } 157 | } 158 | 159 | return container; 160 | }, 161 | 162 | _addRowListeners: function(row, coordinate) { 163 | L.DomEvent.addListener(row, 'mouseover', function() { 164 | this._marker = L.circleMarker(coordinate, 165 | this.options.pointMarkerStyle).addTo(this._map); 166 | }, this); 167 | L.DomEvent.addListener(row, 'mouseout', function() { 168 | if (this._marker) { 169 | this._map.removeLayer(this._marker); 170 | delete this._marker; 171 | } 172 | }, this); 173 | L.DomEvent.addListener(row, 'click', function(e) { 174 | this._map.panTo(coordinate); 175 | L.DomEvent.stopPropagation(e); 176 | }, this); 177 | }, 178 | 179 | _onAltClicked: function(e) { 180 | var altElem = e.target || window.event.srcElement; 181 | while (!L.DomUtil.hasClass(altElem, 'leaflet-routing-alt')) { 182 | altElem = altElem.parentElement; 183 | } 184 | 185 | var j = this._altElements.indexOf(altElem); 186 | var alts = this._routes.slice(); 187 | var route = alts.splice(j, 1)[0]; 188 | 189 | this.fire('routeselected', { 190 | route: route, 191 | alternatives: alts 192 | }); 193 | }, 194 | 195 | _selectAlt: function(e) { 196 | var altElem, 197 | j, 198 | n, 199 | classFn; 200 | 201 | altElem = this._altElements[e.route.routesIndex]; 202 | 203 | if (L.DomUtil.hasClass(altElem, 'leaflet-routing-alt-minimized')) { 204 | for (j = 0; j < this._altElements.length; j++) { 205 | n = this._altElements[j]; 206 | classFn = j === e.route.routesIndex ? 'removeClass' : 'addClass'; 207 | L.DomUtil[classFn](n, 'leaflet-routing-alt-minimized'); 208 | if (this.options.minimizedClassName) { 209 | L.DomUtil[classFn](n, this.options.minimizedClassName); 210 | } 211 | 212 | if (j !== e.route.routesIndex) n.scrollTop = 0; 213 | } 214 | } 215 | 216 | L.DomEvent.stop(e); 217 | }, 218 | 219 | _selectRoute: function(routes) { 220 | if (this._marker) { 221 | this._map.removeLayer(this._marker); 222 | delete this._marker; 223 | } 224 | this.fire('routeselected', routes); 225 | } 226 | }); 227 | })(); 228 | -------------------------------------------------------------------------------- /src/line.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var L = require('leaflet'); 5 | 6 | module.exports = L.LayerGroup.extend({ 7 | includes: ((typeof L.Evented !== 'undefined' && L.Evented.prototype) || L.Mixin.Events), 8 | 9 | options: { 10 | styles: [ 11 | {color: 'black', opacity: 0.15, weight: 9}, 12 | {color: 'white', opacity: 0.8, weight: 6}, 13 | {color: 'red', opacity: 1, weight: 2} 14 | ], 15 | missingRouteStyles: [ 16 | {color: 'black', opacity: 0.15, weight: 7}, 17 | {color: 'white', opacity: 0.6, weight: 4}, 18 | {color: 'gray', opacity: 0.8, weight: 2, dashArray: '7,12'} 19 | ], 20 | addWaypoints: true, 21 | extendToWaypoints: true, 22 | missingRouteTolerance: 10 23 | }, 24 | 25 | initialize: function(route, options) { 26 | L.setOptions(this, options); 27 | L.LayerGroup.prototype.initialize.call(this, options); 28 | this._route = route; 29 | 30 | if (this.options.extendToWaypoints) { 31 | this._extendToWaypoints(); 32 | } 33 | 34 | this._addSegment( 35 | route.coordinates, 36 | this.options.styles, 37 | this.options.addWaypoints); 38 | }, 39 | 40 | getBounds: function() { 41 | return L.latLngBounds(this._route.coordinates); 42 | }, 43 | 44 | _findWaypointIndices: function() { 45 | var wps = this._route.inputWaypoints, 46 | indices = [], 47 | i; 48 | for (i = 0; i < wps.length; i++) { 49 | indices.push(this._findClosestRoutePoint(wps[i].latLng)); 50 | } 51 | 52 | return indices; 53 | }, 54 | 55 | _findClosestRoutePoint: function(latlng) { 56 | var minDist = Number.MAX_VALUE, 57 | minIndex, 58 | i, 59 | d; 60 | 61 | for (i = this._route.coordinates.length - 1; i >= 0 ; i--) { 62 | // TODO: maybe do this in pixel space instead? 63 | d = latlng.distanceTo(this._route.coordinates[i]); 64 | if (d < minDist) { 65 | minIndex = i; 66 | minDist = d; 67 | } 68 | } 69 | 70 | return minIndex; 71 | }, 72 | 73 | _extendToWaypoints: function() { 74 | var wps = this._route.inputWaypoints, 75 | wpIndices = this._getWaypointIndices(), 76 | i, 77 | wpLatLng, 78 | routeCoord; 79 | 80 | for (i = 0; i < wps.length; i++) { 81 | wpLatLng = wps[i].latLng; 82 | routeCoord = L.latLng(this._route.coordinates[wpIndices[i]]); 83 | if (wpLatLng.distanceTo(routeCoord) > 84 | this.options.missingRouteTolerance) { 85 | this._addSegment([wpLatLng, routeCoord], 86 | this.options.missingRouteStyles); 87 | } 88 | } 89 | }, 90 | 91 | _addSegment: function(coords, styles, mouselistener) { 92 | var i, 93 | pl; 94 | 95 | for (i = 0; i < styles.length; i++) { 96 | pl = L.polyline(coords, styles[i]); 97 | this.addLayer(pl); 98 | if (mouselistener) { 99 | pl.on('mousedown', this._onLineTouched, this); 100 | } 101 | } 102 | }, 103 | 104 | _findNearestWpBefore: function(i) { 105 | var wpIndices = this._getWaypointIndices(), 106 | j = wpIndices.length - 1; 107 | while (j >= 0 && wpIndices[j] > i) { 108 | j--; 109 | } 110 | 111 | return j; 112 | }, 113 | 114 | _onLineTouched: function(e) { 115 | var afterIndex = this._findNearestWpBefore(this._findClosestRoutePoint(e.latlng)); 116 | this.fire('linetouched', { 117 | afterIndex: afterIndex, 118 | latlng: e.latlng 119 | }); 120 | L.DomEvent.stop(e); 121 | }, 122 | 123 | _getWaypointIndices: function() { 124 | if (!this._wpIndices) { 125 | this._wpIndices = this._route.waypointIndices || this._findWaypointIndices(); 126 | } 127 | 128 | return this._wpIndices; 129 | } 130 | }); 131 | })(); 132 | -------------------------------------------------------------------------------- /src/localization.js: -------------------------------------------------------------------------------- 1 | /* 2 | NOTICE 3 | Since version 3.2.5, the functionality in this file is by 4 | default NOT used for localizing OSRM instructions. 5 | Instead, we rely on the module osrm-text-instructions (https://github.com/Project-OSRM/osrm-text-instructions/). 6 | 7 | This file can still be used for other routing backends, or if you specify the 8 | stepToText option in the OSRMv1 class. 9 | */ 10 | 11 | (function() { 12 | 'use strict'; 13 | 14 | var spanish = { 15 | directions: { 16 | N: 'norte', 17 | NE: 'noreste', 18 | E: 'este', 19 | SE: 'sureste', 20 | S: 'sur', 21 | SW: 'suroeste', 22 | W: 'oeste', 23 | NW: 'noroeste', 24 | SlightRight: 'leve giro a la derecha', 25 | Right: 'derecha', 26 | SharpRight: 'giro pronunciado a la derecha', 27 | SlightLeft: 'leve giro a la izquierda', 28 | Left: 'izquierda', 29 | SharpLeft: 'giro pronunciado a la izquierda', 30 | Uturn: 'media vuelta' 31 | }, 32 | instructions: { 33 | // instruction, postfix if the road is named 34 | 'Head': 35 | ['Derecho {dir}', ' sobre {road}'], 36 | 'Continue': 37 | ['Continuar {dir}', ' en {road}'], 38 | 'TurnAround': 39 | ['Dar vuelta'], 40 | 'WaypointReached': 41 | ['Llegó a un punto del camino'], 42 | 'Roundabout': 43 | ['Tomar {exitStr} salida en la rotonda', ' en {road}'], 44 | 'DestinationReached': 45 | ['Llegada a destino'], 46 | 'Fork': ['En el cruce gira a {modifier}', ' hacia {road}'], 47 | 'Merge': ['Incorpórate {modifier}', ' hacia {road}'], 48 | 'OnRamp': ['Gira {modifier} en la salida', ' hacia {road}'], 49 | 'OffRamp': ['Toma la salida {modifier}', ' hacia {road}'], 50 | 'EndOfRoad': ['Gira {modifier} al final de la carretera', ' hacia {road}'], 51 | 'Onto': 'hacia {road}' 52 | }, 53 | formatOrder: function(n) { 54 | return n + 'º'; 55 | }, 56 | ui: { 57 | startPlaceholder: 'Inicio', 58 | viaPlaceholder: 'Via {viaNumber}', 59 | endPlaceholder: 'Destino' 60 | }, 61 | units: { 62 | meters: 'm', 63 | kilometers: 'km', 64 | yards: 'yd', 65 | miles: 'mi', 66 | hours: 'h', 67 | minutes: 'min', 68 | seconds: 's' 69 | } 70 | }; 71 | 72 | L.Routing = L.Routing || {}; 73 | 74 | var Localization = L.Class.extend({ 75 | initialize: function(langs) { 76 | this._langs = L.Util.isArray(langs) ? langs.slice() : [langs, 'en']; 77 | 78 | for (var i = 0, l = this._langs.length; i < l; i++) { 79 | var generalizedCode = /([A-Za-z]+)/.exec(this._langs[i])[1] 80 | if (!Localization[this._langs[i]]) { 81 | if (Localization[generalizedCode]) { 82 | this._langs[i] = generalizedCode; 83 | } else { 84 | throw new Error('No localization for language "' + this._langs[i] + '".'); 85 | } 86 | } 87 | } 88 | }, 89 | 90 | localize: function(keys) { 91 | var dict, 92 | key, 93 | value; 94 | 95 | keys = L.Util.isArray(keys) ? keys : [keys]; 96 | 97 | for (var i = 0, l = this._langs.length; i < l; i++) { 98 | dict = Localization[this._langs[i]]; 99 | for (var j = 0, nKeys = keys.length; dict && j < nKeys; j++) { 100 | key = keys[j]; 101 | value = dict[key]; 102 | dict = value; 103 | } 104 | 105 | if (value) { 106 | return value; 107 | } 108 | } 109 | } 110 | }); 111 | 112 | module.exports = L.extend(Localization, { 113 | 'en': { 114 | directions: { 115 | N: 'north', 116 | NE: 'northeast', 117 | E: 'east', 118 | SE: 'southeast', 119 | S: 'south', 120 | SW: 'southwest', 121 | W: 'west', 122 | NW: 'northwest', 123 | SlightRight: 'slight right', 124 | Right: 'right', 125 | SharpRight: 'sharp right', 126 | SlightLeft: 'slight left', 127 | Left: 'left', 128 | SharpLeft: 'sharp left', 129 | Uturn: 'Turn around' 130 | }, 131 | instructions: { 132 | // instruction, postfix if the road is named 133 | 'Head': 134 | ['Head {dir}', ' on {road}'], 135 | 'Continue': 136 | ['Continue {dir}'], 137 | 'TurnAround': 138 | ['Turn around'], 139 | 'WaypointReached': 140 | ['Waypoint reached'], 141 | 'Roundabout': 142 | ['Take the {exitStr} exit in the roundabout', ' onto {road}'], 143 | 'DestinationReached': 144 | ['Destination reached'], 145 | 'Fork': ['At the fork, turn {modifier}', ' onto {road}'], 146 | 'Merge': ['Merge {modifier}', ' onto {road}'], 147 | 'OnRamp': ['Turn {modifier} on the ramp', ' onto {road}'], 148 | 'OffRamp': ['Take the ramp on the {modifier}', ' onto {road}'], 149 | 'EndOfRoad': ['Turn {modifier} at the end of the road', ' onto {road}'], 150 | 'Onto': 'onto {road}' 151 | }, 152 | formatOrder: function(n) { 153 | var i = n % 10 - 1, 154 | suffix = ['st', 'nd', 'rd']; 155 | 156 | return suffix[i] ? n + suffix[i] : n + 'th'; 157 | }, 158 | ui: { 159 | startPlaceholder: 'Start', 160 | viaPlaceholder: 'Via {viaNumber}', 161 | endPlaceholder: 'End' 162 | }, 163 | units: { 164 | meters: 'm', 165 | kilometers: 'km', 166 | yards: 'yd', 167 | miles: 'mi', 168 | hours: 'h', 169 | minutes: 'min', 170 | seconds: 's' 171 | } 172 | }, 173 | 174 | 'de': { 175 | directions: { 176 | N: 'Norden', 177 | NE: 'Nordosten', 178 | E: 'Osten', 179 | SE: 'Südosten', 180 | S: 'Süden', 181 | SW: 'Südwesten', 182 | W: 'Westen', 183 | NW: 'Nordwesten', 184 | SlightRight: 'leicht rechts', 185 | Right: 'rechts', 186 | SharpRight: 'scharf rechts', 187 | SlightLeft: 'leicht links', 188 | Left: 'links', 189 | SharpLeft: 'scharf links', 190 | Uturn: 'Wenden' 191 | }, 192 | instructions: { 193 | // instruction, postfix if the road is named 194 | 'Head': 195 | ['Richtung {dir}', ' auf {road}'], 196 | 'Continue': 197 | ['Geradeaus Richtung {dir}', ' auf {road}'], 198 | 'SlightRight': 199 | ['Leicht rechts abbiegen', ' auf {road}'], 200 | 'Right': 201 | ['Rechts abbiegen', ' auf {road}'], 202 | 'SharpRight': 203 | ['Scharf rechts abbiegen', ' auf {road}'], 204 | 'TurnAround': 205 | ['Wenden'], 206 | 'SharpLeft': 207 | ['Scharf links abbiegen', ' auf {road}'], 208 | 'Left': 209 | ['Links abbiegen', ' auf {road}'], 210 | 'SlightLeft': 211 | ['Leicht links abbiegen', ' auf {road}'], 212 | 'WaypointReached': 213 | ['Zwischenhalt erreicht'], 214 | 'Roundabout': 215 | ['Nehmen Sie die {exitStr} Ausfahrt im Kreisverkehr', ' auf {road}'], 216 | 'DestinationReached': 217 | ['Sie haben ihr Ziel erreicht'], 218 | 'Fork': ['An der Kreuzung {modifier}', ' auf {road}'], 219 | 'Merge': ['Fahren Sie {modifier} weiter', ' auf {road}'], 220 | 'OnRamp': ['Fahren Sie {modifier} auf die Auffahrt', ' auf {road}'], 221 | 'OffRamp': ['Nehmen Sie die Ausfahrt {modifier}', ' auf {road}'], 222 | 'EndOfRoad': ['Fahren Sie {modifier} am Ende der Straße', ' auf {road}'], 223 | 'Onto': 'auf {road}' 224 | }, 225 | formatOrder: function(n) { 226 | return n + '.'; 227 | }, 228 | ui: { 229 | startPlaceholder: 'Start', 230 | viaPlaceholder: 'Via {viaNumber}', 231 | endPlaceholder: 'Ziel' 232 | } 233 | }, 234 | 235 | 'sv': { 236 | directions: { 237 | N: 'norr', 238 | NE: 'nordost', 239 | E: 'öst', 240 | SE: 'sydost', 241 | S: 'syd', 242 | SW: 'sydväst', 243 | W: 'väst', 244 | NW: 'nordväst', 245 | SlightRight: 'svagt höger', 246 | Right: 'höger', 247 | SharpRight: 'skarpt höger', 248 | SlightLeft: 'svagt vänster', 249 | Left: 'vänster', 250 | SharpLeft: 'skarpt vänster', 251 | Uturn: 'Vänd' 252 | }, 253 | instructions: { 254 | // instruction, postfix if the road is named 255 | 'Head': 256 | ['Åk åt {dir}', ' till {road}'], 257 | 'Continue': 258 | ['Fortsätt {dir}'], 259 | 'SlightRight': 260 | ['Svagt höger', ' till {road}'], 261 | 'Right': 262 | ['Sväng höger', ' till {road}'], 263 | 'SharpRight': 264 | ['Skarpt höger', ' till {road}'], 265 | 'TurnAround': 266 | ['Vänd'], 267 | 'SharpLeft': 268 | ['Skarpt vänster', ' till {road}'], 269 | 'Left': 270 | ['Sväng vänster', ' till {road}'], 271 | 'SlightLeft': 272 | ['Svagt vänster', ' till {road}'], 273 | 'WaypointReached': 274 | ['Viapunkt nådd'], 275 | 'Roundabout': 276 | ['Tag {exitStr} avfarten i rondellen', ' till {road}'], 277 | 'DestinationReached': 278 | ['Framme vid resans mål'], 279 | 'Fork': ['Tag av {modifier}', ' till {road}'], 280 | 'Merge': ['Anslut {modifier} ', ' till {road}'], 281 | 'OnRamp': ['Tag påfarten {modifier}', ' till {road}'], 282 | 'OffRamp': ['Tag avfarten {modifier}', ' till {road}'], 283 | 'EndOfRoad': ['Sväng {modifier} vid vägens slut', ' till {road}'], 284 | 'Onto': 'till {road}' 285 | }, 286 | formatOrder: function(n) { 287 | return ['första', 'andra', 'tredje', 'fjärde', 'femte', 288 | 'sjätte', 'sjunde', 'åttonde', 'nionde', 'tionde' 289 | /* Can't possibly be more than ten exits, can there? */][n - 1]; 290 | }, 291 | ui: { 292 | startPlaceholder: 'Från', 293 | viaPlaceholder: 'Via {viaNumber}', 294 | endPlaceholder: 'Till' 295 | } 296 | }, 297 | 298 | 'es': spanish, 299 | 'sp': spanish, 300 | 301 | 'nl': { 302 | directions: { 303 | N: 'noordelijke', 304 | NE: 'noordoostelijke', 305 | E: 'oostelijke', 306 | SE: 'zuidoostelijke', 307 | S: 'zuidelijke', 308 | SW: 'zuidewestelijke', 309 | W: 'westelijke', 310 | NW: 'noordwestelijke' 311 | }, 312 | instructions: { 313 | // instruction, postfix if the road is named 314 | 'Head': 315 | ['Vertrek in {dir} richting', ' de {road} op'], 316 | 'Continue': 317 | ['Ga in {dir} richting', ' de {road} op'], 318 | 'SlightRight': 319 | ['Volg de weg naar rechts', ' de {road} op'], 320 | 'Right': 321 | ['Ga rechtsaf', ' de {road} op'], 322 | 'SharpRight': 323 | ['Ga scherpe bocht naar rechts', ' de {road} op'], 324 | 'TurnAround': 325 | ['Keer om'], 326 | 'SharpLeft': 327 | ['Ga scherpe bocht naar links', ' de {road} op'], 328 | 'Left': 329 | ['Ga linksaf', ' de {road} op'], 330 | 'SlightLeft': 331 | ['Volg de weg naar links', ' de {road} op'], 332 | 'WaypointReached': 333 | ['Aangekomen bij tussenpunt'], 334 | 'Roundabout': 335 | ['Neem de {exitStr} afslag op de rotonde', ' de {road} op'], 336 | 'DestinationReached': 337 | ['Aangekomen op eindpunt'], 338 | }, 339 | formatOrder: function(n) { 340 | if (n === 1 || n >= 20) { 341 | return n + 'ste'; 342 | } else { 343 | return n + 'de'; 344 | } 345 | }, 346 | ui: { 347 | startPlaceholder: 'Vertrekpunt', 348 | viaPlaceholder: 'Via {viaNumber}', 349 | endPlaceholder: 'Bestemming' 350 | } 351 | }, 352 | 'fr': { 353 | directions: { 354 | N: 'nord', 355 | NE: 'nord-est', 356 | E: 'est', 357 | SE: 'sud-est', 358 | S: 'sud', 359 | SW: 'sud-ouest', 360 | W: 'ouest', 361 | NW: 'nord-ouest' 362 | }, 363 | instructions: { 364 | // instruction, postfix if the road is named 365 | 'Head': 366 | ['Tout droit au {dir}', ' sur {road}'], 367 | 'Continue': 368 | ['Continuer au {dir}', ' sur {road}'], 369 | 'SlightRight': 370 | ['Légèrement à droite', ' sur {road}'], 371 | 'Right': 372 | ['A droite', ' sur {road}'], 373 | 'SharpRight': 374 | ['Complètement à droite', ' sur {road}'], 375 | 'TurnAround': 376 | ['Faire demi-tour'], 377 | 'SharpLeft': 378 | ['Complètement à gauche', ' sur {road}'], 379 | 'Left': 380 | ['A gauche', ' sur {road}'], 381 | 'SlightLeft': 382 | ['Légèrement à gauche', ' sur {road}'], 383 | 'WaypointReached': 384 | ['Point d\'étape atteint'], 385 | 'Roundabout': 386 | ['Au rond-point, prenez la {exitStr} sortie', ' sur {road}'], 387 | 'DestinationReached': 388 | ['Destination atteinte'], 389 | }, 390 | formatOrder: function(n) { 391 | return n + 'º'; 392 | }, 393 | ui: { 394 | startPlaceholder: 'Départ', 395 | viaPlaceholder: 'Intermédiaire {viaNumber}', 396 | endPlaceholder: 'Arrivée' 397 | } 398 | }, 399 | 'it': { 400 | directions: { 401 | N: 'nord', 402 | NE: 'nord-est', 403 | E: 'est', 404 | SE: 'sud-est', 405 | S: 'sud', 406 | SW: 'sud-ovest', 407 | W: 'ovest', 408 | NW: 'nord-ovest' 409 | }, 410 | instructions: { 411 | // instruction, postfix if the road is named 412 | 'Head': 413 | ['Dritto verso {dir}', ' su {road}'], 414 | 'Continue': 415 | ['Continuare verso {dir}', ' su {road}'], 416 | 'SlightRight': 417 | ['Mantenere la destra', ' su {road}'], 418 | 'Right': 419 | ['A destra', ' su {road}'], 420 | 'SharpRight': 421 | ['Strettamente a destra', ' su {road}'], 422 | 'TurnAround': 423 | ['Fare inversione di marcia'], 424 | 'SharpLeft': 425 | ['Strettamente a sinistra', ' su {road}'], 426 | 'Left': 427 | ['A sinistra', ' sur {road}'], 428 | 'SlightLeft': 429 | ['Mantenere la sinistra', ' su {road}'], 430 | 'WaypointReached': 431 | ['Punto di passaggio raggiunto'], 432 | 'Roundabout': 433 | ['Alla rotonda, prendere la {exitStr} uscita'], 434 | 'DestinationReached': 435 | ['Destinazione raggiunta'], 436 | }, 437 | formatOrder: function(n) { 438 | return n + 'º'; 439 | }, 440 | ui: { 441 | startPlaceholder: 'Partenza', 442 | viaPlaceholder: 'Intermedia {viaNumber}', 443 | endPlaceholder: 'Destinazione' 444 | } 445 | }, 446 | 'pt': { 447 | directions: { 448 | N: 'norte', 449 | NE: 'nordeste', 450 | E: 'leste', 451 | SE: 'sudeste', 452 | S: 'sul', 453 | SW: 'sudoeste', 454 | W: 'oeste', 455 | NW: 'noroeste', 456 | SlightRight: 'curva ligeira a direita', 457 | Right: 'direita', 458 | SharpRight: 'curva fechada a direita', 459 | SlightLeft: 'ligeira a esquerda', 460 | Left: 'esquerda', 461 | SharpLeft: 'curva fechada a esquerda', 462 | Uturn: 'Meia volta' 463 | }, 464 | instructions: { 465 | // instruction, postfix if the road is named 466 | 'Head': 467 | ['Siga {dir}', ' na {road}'], 468 | 'Continue': 469 | ['Continue {dir}', ' na {road}'], 470 | 'SlightRight': 471 | ['Curva ligeira a direita', ' na {road}'], 472 | 'Right': 473 | ['Curva a direita', ' na {road}'], 474 | 'SharpRight': 475 | ['Curva fechada a direita', ' na {road}'], 476 | 'TurnAround': 477 | ['Retorne'], 478 | 'SharpLeft': 479 | ['Curva fechada a esquerda', ' na {road}'], 480 | 'Left': 481 | ['Curva a esquerda', ' na {road}'], 482 | 'SlightLeft': 483 | ['Curva ligueira a esquerda', ' na {road}'], 484 | 'WaypointReached': 485 | ['Ponto de interesse atingido'], 486 | 'Roundabout': 487 | ['Pegue a {exitStr} saída na rotatória', ' na {road}'], 488 | 'DestinationReached': 489 | ['Destino atingido'], 490 | 'Fork': ['Na encruzilhada, vire a {modifier}', ' na {road}'], 491 | 'Merge': ['Entre à {modifier}', ' na {road}'], 492 | 'OnRamp': ['Vire {modifier} na rampa', ' na {road}'], 493 | 'OffRamp': ['Entre na rampa na {modifier}', ' na {road}'], 494 | 'EndOfRoad': ['Vire {modifier} no fim da rua', ' na {road}'], 495 | 'Onto': 'na {road}' 496 | }, 497 | formatOrder: function(n) { 498 | return n + 'º'; 499 | }, 500 | ui: { 501 | startPlaceholder: 'Origem', 502 | viaPlaceholder: 'Intermédio {viaNumber}', 503 | endPlaceholder: 'Destino' 504 | } 505 | }, 506 | 'sk': { 507 | directions: { 508 | N: 'sever', 509 | NE: 'serverovýchod', 510 | E: 'východ', 511 | SE: 'juhovýchod', 512 | S: 'juh', 513 | SW: 'juhozápad', 514 | W: 'západ', 515 | NW: 'serverozápad' 516 | }, 517 | instructions: { 518 | // instruction, postfix if the road is named 519 | 'Head': 520 | ['Mierte na {dir}', ' na {road}'], 521 | 'Continue': 522 | ['Pokračujte na {dir}', ' na {road}'], 523 | 'SlightRight': 524 | ['Mierne doprava', ' na {road}'], 525 | 'Right': 526 | ['Doprava', ' na {road}'], 527 | 'SharpRight': 528 | ['Prudko doprava', ' na {road}'], 529 | 'TurnAround': 530 | ['Otočte sa'], 531 | 'SharpLeft': 532 | ['Prudko doľava', ' na {road}'], 533 | 'Left': 534 | ['Doľava', ' na {road}'], 535 | 'SlightLeft': 536 | ['Mierne doľava', ' na {road}'], 537 | 'WaypointReached': 538 | ['Ste v prejazdovom bode.'], 539 | 'Roundabout': 540 | ['Odbočte na {exitStr} výjazde', ' na {road}'], 541 | 'DestinationReached': 542 | ['Prišli ste do cieľa.'], 543 | }, 544 | formatOrder: function(n) { 545 | var i = n % 10 - 1, 546 | suffix = ['.', '.', '.']; 547 | 548 | return suffix[i] ? n + suffix[i] : n + '.'; 549 | }, 550 | ui: { 551 | startPlaceholder: 'Začiatok', 552 | viaPlaceholder: 'Cez {viaNumber}', 553 | endPlaceholder: 'Koniec' 554 | } 555 | }, 556 | 'el': { 557 | directions: { 558 | N: 'βόρεια', 559 | NE: 'βορειοανατολικά', 560 | E: 'ανατολικά', 561 | SE: 'νοτιοανατολικά', 562 | S: 'νότια', 563 | SW: 'νοτιοδυτικά', 564 | W: 'δυτικά', 565 | NW: 'βορειοδυτικά' 566 | }, 567 | instructions: { 568 | // instruction, postfix if the road is named 569 | 'Head': 570 | ['Κατευθυνθείτε {dir}', ' στην {road}'], 571 | 'Continue': 572 | ['Συνεχίστε {dir}', ' στην {road}'], 573 | 'SlightRight': 574 | ['Ελαφρώς δεξιά', ' στην {road}'], 575 | 'Right': 576 | ['Δεξιά', ' στην {road}'], 577 | 'SharpRight': 578 | ['Απότομη δεξιά στροφή', ' στην {road}'], 579 | 'TurnAround': 580 | ['Κάντε αναστροφή'], 581 | 'SharpLeft': 582 | ['Απότομη αριστερή στροφή', ' στην {road}'], 583 | 'Left': 584 | ['Αριστερά', ' στην {road}'], 585 | 'SlightLeft': 586 | ['Ελαφρώς αριστερά', ' στην {road}'], 587 | 'WaypointReached': 588 | ['Φτάσατε στο σημείο αναφοράς'], 589 | 'Roundabout': 590 | ['Ακολουθήστε την {exitStr} έξοδο στο κυκλικό κόμβο', ' στην {road}'], 591 | 'DestinationReached': 592 | ['Φτάσατε στον προορισμό σας'], 593 | }, 594 | formatOrder: function(n) { 595 | return n + 'º'; 596 | }, 597 | ui: { 598 | startPlaceholder: 'Αφετηρία', 599 | viaPlaceholder: 'μέσω {viaNumber}', 600 | endPlaceholder: 'Προορισμός' 601 | } 602 | }, 603 | 'ca': { 604 | directions: { 605 | N: 'nord', 606 | NE: 'nord-est', 607 | E: 'est', 608 | SE: 'sud-est', 609 | S: 'sud', 610 | SW: 'sud-oest', 611 | W: 'oest', 612 | NW: 'nord-oest', 613 | SlightRight: 'lleu gir a la dreta', 614 | Right: 'dreta', 615 | SharpRight: 'gir pronunciat a la dreta', 616 | SlightLeft: 'gir pronunciat a l\'esquerra', 617 | Left: 'esquerra', 618 | SharpLeft: 'lleu gir a l\'esquerra', 619 | Uturn: 'mitja volta' 620 | }, 621 | instructions: { 622 | 'Head': 623 | ['Recte {dir}', ' sobre {road}'], 624 | 'Continue': 625 | ['Continuar {dir}'], 626 | 'TurnAround': 627 | ['Donar la volta'], 628 | 'WaypointReached': 629 | ['Ha arribat a un punt del camí'], 630 | 'Roundabout': 631 | ['Agafar {exitStr} sortida a la rotonda', ' a {road}'], 632 | 'DestinationReached': 633 | ['Arribada al destí'], 634 | 'Fork': ['A la cruïlla gira a la {modifier}', ' cap a {road}'], 635 | 'Merge': ['Incorpora\'t {modifier}', ' a {road}'], 636 | 'OnRamp': ['Gira {modifier} a la sortida', ' cap a {road}'], 637 | 'OffRamp': ['Pren la sortida {modifier}', ' cap a {road}'], 638 | 'EndOfRoad': ['Gira {modifier} al final de la carretera', ' cap a {road}'], 639 | 'Onto': 'cap a {road}' 640 | }, 641 | formatOrder: function(n) { 642 | return n + 'º'; 643 | }, 644 | ui: { 645 | startPlaceholder: 'Origen', 646 | viaPlaceholder: 'Via {viaNumber}', 647 | endPlaceholder: 'Destí' 648 | }, 649 | units: { 650 | meters: 'm', 651 | kilometers: 'km', 652 | yards: 'yd', 653 | miles: 'mi', 654 | hours: 'h', 655 | minutes: 'min', 656 | seconds: 's' 657 | } 658 | }, 659 | 'ru': { 660 | directions: { 661 | N: 'север', 662 | NE: 'северовосток', 663 | E: 'восток', 664 | SE: 'юговосток', 665 | S: 'юг', 666 | SW: 'югозапад', 667 | W: 'запад', 668 | NW: 'северозапад', 669 | SlightRight: 'плавно направо', 670 | Right: 'направо', 671 | SharpRight: 'резко направо', 672 | SlightLeft: 'плавно налево', 673 | Left: 'налево', 674 | SharpLeft: 'резко налево', 675 | Uturn: 'развернуться' 676 | }, 677 | instructions: { 678 | 'Head': 679 | ['Начать движение на {dir}', ' по {road}'], 680 | 'Continue': 681 | ['Продолжать движение на {dir}', ' по {road}'], 682 | 'SlightRight': 683 | ['Плавный поворот направо', ' на {road}'], 684 | 'Right': 685 | ['Направо', ' на {road}'], 686 | 'SharpRight': 687 | ['Резкий поворот направо', ' на {road}'], 688 | 'TurnAround': 689 | ['Развернуться'], 690 | 'SharpLeft': 691 | ['Резкий поворот налево', ' на {road}'], 692 | 'Left': 693 | ['Поворот налево', ' на {road}'], 694 | 'SlightLeft': 695 | ['Плавный поворот налево', ' на {road}'], 696 | 'WaypointReached': 697 | ['Точка достигнута'], 698 | 'Roundabout': 699 | ['{exitStr} съезд с кольца', ' на {road}'], 700 | 'DestinationReached': 701 | ['Окончание маршрута'], 702 | 'Fork': ['На развилке поверните {modifier}', ' на {road}'], 703 | 'Merge': ['Перестройтесь {modifier}', ' на {road}'], 704 | 'OnRamp': ['Поверните {modifier} на съезд', ' на {road}'], 705 | 'OffRamp': ['Съезжайте на {modifier}', ' на {road}'], 706 | 'EndOfRoad': ['Поверните {modifier} в конце дороги', ' на {road}'], 707 | 'Onto': 'на {road}' 708 | }, 709 | formatOrder: function(n) { 710 | return n + '-й'; 711 | }, 712 | ui: { 713 | startPlaceholder: 'Начало', 714 | viaPlaceholder: 'Через {viaNumber}', 715 | endPlaceholder: 'Конец' 716 | }, 717 | units: { 718 | meters: 'м', 719 | kilometers: 'км', 720 | yards: 'ярд', 721 | miles: 'ми', 722 | hours: 'ч', 723 | minutes: 'м', 724 | seconds: 'с' 725 | } 726 | }, 727 | 728 | 'pl': { 729 | directions: { 730 | N: 'północ', 731 | NE: 'północny wschód', 732 | E: 'wschód', 733 | SE: 'południowy wschód', 734 | S: 'południe', 735 | SW: 'południowy zachód', 736 | W: 'zachód', 737 | NW: 'północny zachód', 738 | SlightRight: 'lekko w prawo', 739 | Right: 'w prawo', 740 | SharpRight: 'ostro w prawo', 741 | SlightLeft: 'lekko w lewo', 742 | Left: 'w lewo', 743 | SharpLeft: 'ostro w lewo', 744 | Uturn: 'zawróć' 745 | }, 746 | instructions: { 747 | // instruction, postfix if the road is named 748 | 'Head': 749 | ['Kieruj się na {dir}', ' na {road}'], 750 | 'Continue': 751 | ['Jedź dalej przez {dir}'], 752 | 'TurnAround': 753 | ['Zawróć'], 754 | 'WaypointReached': 755 | ['Punkt pośredni'], 756 | 'Roundabout': 757 | ['Wyjedź {exitStr} zjazdem na rondzie', ' na {road}'], 758 | 'DestinationReached': 759 | ['Dojechano do miejsca docelowego'], 760 | 'Fork': ['Na rozwidleniu {modifier}', ' na {road}'], 761 | 'Merge': ['Zjedź {modifier}', ' na {road}'], 762 | 'OnRamp': ['Wjazd {modifier}', ' na {road}'], 763 | 'OffRamp': ['Zjazd {modifier}', ' na {road}'], 764 | 'EndOfRoad': ['Skręć {modifier} na końcu drogi', ' na {road}'], 765 | 'Onto': 'na {road}' 766 | }, 767 | formatOrder: function(n) { 768 | return n + '.'; 769 | }, 770 | ui: { 771 | startPlaceholder: 'Początek', 772 | viaPlaceholder: 'Przez {viaNumber}', 773 | endPlaceholder: 'Koniec' 774 | }, 775 | units: { 776 | meters: 'm', 777 | kilometers: 'km', 778 | yards: 'yd', 779 | miles: 'mi', 780 | hours: 'godz', 781 | minutes: 'min', 782 | seconds: 's' 783 | } 784 | }, 785 | 'uk': { 786 | directions: { 787 | N: 'північ', 788 | NE: 'північний схід', 789 | E: 'схід', 790 | SE: 'південний схід', 791 | S: 'південь', 792 | SW: 'південний захід', 793 | W: 'захід', 794 | NW: 'північний захід', 795 | SlightRight: 'плавно направо', 796 | Right: 'направо', 797 | SharpRight: 'різко направо', 798 | SlightLeft: 'плавно наліво', 799 | Left: 'наліво', 800 | SharpLeft: 'різко наліво', 801 | Uturn: 'розвернутися', 802 | }, 803 | instructions: { 804 | 'Head': 805 | [ 'Почати рух на {dir}', 'по {road}'], 806 | 'Continue': 807 | [ 'Продовжувати рух на {dir}', 'по {road}'], 808 | 'SlightRight': 809 | [ 'Плавний поворот направо', 'на {road}'], 810 | 'Right': 811 | [ 'Направо', 'на {road}'], 812 | 'SharpRight': 813 | [ 'Різкий поворот направо', 'на {road}'], 814 | 'TurnAround': 815 | [ 'Розгорнутися'], 816 | 'SharpLeft': 817 | [ 'Різкий поворот наліво', 'на {road}'], 818 | 'Left': 819 | [ 'Поворот наліво', 'на {road}'], 820 | 'SlightLeft': 821 | [ 'Плавний поворот наліво', 'на {road}'], 822 | 'WaypointReached': 823 | [ 'Точка досягнута'], 824 | 'Roundabout': 825 | [ "{ExitStr} з'їзд з кільця", 'на {road}'], 826 | 'DestinationReached': 827 | [ 'Закінчення маршруту'], 828 | 'Fork': [ 'На розвилці поверніть {modifier}', 'на {road}'], 829 | 'Merge': [ 'Візьміть {modifier}', 'на {road}'], 830 | 'OnRamp': [ "Поверніть {modifier} на з'їзд", 'на {road}'], 831 | 'OffRamp': [ "З'їжджайте на {modifier}", 'на {road}'], 832 | 'EndOfRoad': [ 'Поверніть {modifier} в кінці дороги', 'на {road}'], 833 | 'Onto': 'на {road}' 834 | }, 835 | formatOrder: function(n) { 836 | return n + '-й'; 837 | }, 838 | ui: { 839 | startPlaceholder: 'Початок', 840 | viaPlaceholder: 'Через {viaNumber}', 841 | endPlaceholder: 'Кінець' 842 | }, 843 | units: { 844 | meters: 'м', 845 | kilometers: 'км', 846 | yards: 'ярд', 847 | miles: 'ми', 848 | hours: 'г', 849 | minutes: 'хв', 850 | seconds: 'сек' 851 | } 852 | } 853 | }); 854 | })(); 855 | -------------------------------------------------------------------------------- /src/mapbox.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var L = require('leaflet'); 5 | 6 | var OSRMv1 = require('./osrm-v1'); 7 | 8 | /** 9 | * Works against OSRM's new API in version 5.0; this has 10 | * the API version v1. 11 | */ 12 | module.exports = OSRMv1.extend({ 13 | options: { 14 | serviceUrl: 'https://api.mapbox.com/directions/v5', 15 | profile: 'mapbox/driving', 16 | useHints: false 17 | }, 18 | 19 | initialize: function(accessToken, options) { 20 | L.Routing.OSRMv1.prototype.initialize.call(this, options); 21 | this.options.requestParameters = this.options.requestParameters || {}; 22 | /* jshint camelcase: false */ 23 | this.options.requestParameters.access_token = accessToken; 24 | /* jshint camelcase: true */ 25 | } 26 | }); 27 | })(); 28 | -------------------------------------------------------------------------------- /src/osrm-v1.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var L = require('leaflet'), 5 | corslite = require('@mapbox/corslite'), 6 | polyline = require('@mapbox/polyline'), 7 | osrmTextInstructions = require('osrm-text-instructions')('v5'); 8 | 9 | // Ignore camelcase naming for this file, since OSRM's API uses 10 | // underscores. 11 | /* jshint camelcase: false */ 12 | 13 | var Waypoint = require('./waypoint'); 14 | 15 | /** 16 | * Works against OSRM's new API in version 5.0; this has 17 | * the API version v1. 18 | */ 19 | module.exports = L.Class.extend({ 20 | options: { 21 | serviceUrl: 'https://router.project-osrm.org/route/v1', 22 | profile: 'driving', 23 | timeout: 30 * 1000, 24 | routingOptions: { 25 | alternatives: true, 26 | steps: true 27 | }, 28 | polylinePrecision: 5, 29 | useHints: true, 30 | suppressDemoServerWarning: false, 31 | language: 'en' 32 | }, 33 | 34 | initialize: function(options) { 35 | L.Util.setOptions(this, options); 36 | this._hints = { 37 | locations: {} 38 | }; 39 | 40 | if (!this.options.suppressDemoServerWarning && 41 | this.options.serviceUrl.indexOf('//router.project-osrm.org') >= 0) { 42 | console.warn('You are using OSRM\'s demo server. ' + 43 | 'Please note that it is **NOT SUITABLE FOR PRODUCTION USE**.\n' + 44 | 'Refer to the demo server\'s usage policy: ' + 45 | 'https://github.com/Project-OSRM/osrm-backend/wiki/Api-usage-policy\n\n' + 46 | 'To change, set the serviceUrl option.\n\n' + 47 | 'Please do not report issues with this server to neither ' + 48 | 'Leaflet Routing Machine or OSRM - it\'s for\n' + 49 | 'demo only, and will sometimes not be available, or work in ' + 50 | 'unexpected ways.\n\n' + 51 | 'Please set up your own OSRM server, or use a paid service ' + 52 | 'provider for production.'); 53 | } 54 | }, 55 | 56 | route: function(waypoints, callback, context, options) { 57 | var timedOut = false, 58 | wps = [], 59 | url, 60 | timer, 61 | wp, 62 | i, 63 | xhr; 64 | 65 | options = L.extend({}, this.options.routingOptions, options); 66 | url = this.buildRouteUrl(waypoints, options); 67 | if (this.options.requestParameters) { 68 | url += L.Util.getParamString(this.options.requestParameters, url); 69 | } 70 | 71 | timer = setTimeout(function() { 72 | timedOut = true; 73 | callback.call(context || callback, { 74 | status: -1, 75 | message: 'OSRM request timed out.' 76 | }); 77 | }, this.options.timeout); 78 | 79 | // Create a copy of the waypoints, since they 80 | // might otherwise be asynchronously modified while 81 | // the request is being processed. 82 | for (i = 0; i < waypoints.length; i++) { 83 | wp = waypoints[i]; 84 | wps.push(new Waypoint(wp.latLng, wp.name, wp.options)); 85 | } 86 | 87 | return xhr = corslite(url, L.bind(function(err, resp) { 88 | var data, 89 | error = {}; 90 | 91 | clearTimeout(timer); 92 | if (!timedOut) { 93 | if (!err) { 94 | try { 95 | data = JSON.parse(resp.responseText); 96 | try { 97 | return this._routeDone(data, wps, options, callback, context); 98 | } catch (ex) { 99 | error.status = -3; 100 | error.message = ex.toString(); 101 | } 102 | } catch (ex) { 103 | error.status = -2; 104 | error.message = 'Error parsing OSRM response: ' + ex.toString(); 105 | } 106 | } else { 107 | var message = err.type + (err.target && err.target.status ? ' HTTP ' + err.target.status + ': ' + err.target.statusText : ''); 108 | if (err.responseText) { 109 | try { 110 | data = JSON.parse(err.responseText); 111 | if (data.message) 112 | message = data.message; 113 | } catch (ex) { 114 | } 115 | } 116 | error.message = 'HTTP request failed: ' + message; 117 | error.url = url; 118 | error.status = -1; 119 | error.target = err; 120 | } 121 | 122 | callback.call(context || callback, error); 123 | } else { 124 | xhr.abort(); 125 | } 126 | }, this)); 127 | }, 128 | 129 | requiresMoreDetail: function(route, zoom, bounds) { 130 | if (!route.properties.isSimplified) { 131 | return false; 132 | } 133 | 134 | var waypoints = route.inputWaypoints, 135 | i; 136 | for (i = 0; i < waypoints.length; ++i) { 137 | if (!bounds.contains(waypoints[i].latLng)) { 138 | return true; 139 | } 140 | } 141 | 142 | return false; 143 | }, 144 | 145 | _routeDone: function(response, inputWaypoints, options, callback, context) { 146 | var alts = [], 147 | actualWaypoints, 148 | i, 149 | route; 150 | 151 | context = context || callback; 152 | if (response.code !== 'Ok') { 153 | callback.call(context, { 154 | status: response.code 155 | }); 156 | return; 157 | } 158 | 159 | actualWaypoints = this._toWaypoints(inputWaypoints, response.waypoints); 160 | 161 | for (i = 0; i < response.routes.length; i++) { 162 | route = this._convertRoute(response.routes[i]); 163 | route.inputWaypoints = inputWaypoints; 164 | route.waypoints = actualWaypoints; 165 | route.properties = {isSimplified: !options || !options.geometryOnly || options.simplifyGeometry}; 166 | alts.push(route); 167 | } 168 | 169 | this._saveHintData(response.waypoints, inputWaypoints); 170 | 171 | callback.call(context, null, alts); 172 | }, 173 | 174 | _convertRoute: function(responseRoute) { 175 | var result = { 176 | name: '', 177 | coordinates: [], 178 | instructions: [], 179 | summary: { 180 | totalDistance: responseRoute.distance, 181 | totalTime: responseRoute.duration 182 | } 183 | }, 184 | legNames = [], 185 | waypointIndices = [], 186 | index = 0, 187 | legCount = responseRoute.legs.length, 188 | hasSteps = responseRoute.legs[0].steps.length > 0, 189 | i, 190 | j, 191 | leg, 192 | step, 193 | geometry, 194 | type, 195 | modifier, 196 | text, 197 | stepToText; 198 | 199 | if (this.options.stepToText) { 200 | stepToText = this.options.stepToText; 201 | } else { 202 | stepToText = L.bind(osrmTextInstructions.compile, osrmTextInstructions, this.options.language); 203 | } 204 | 205 | for (i = 0; i < legCount; i++) { 206 | leg = responseRoute.legs[i]; 207 | legNames.push(leg.summary && leg.summary.charAt(0).toUpperCase() + leg.summary.substring(1)); 208 | for (j = 0; j < leg.steps.length; j++) { 209 | step = leg.steps[j]; 210 | geometry = this._decodePolyline(step.geometry); 211 | result.coordinates.push.apply(result.coordinates, geometry); 212 | type = this._maneuverToInstructionType(step.maneuver, i === legCount - 1); 213 | modifier = this._maneuverToModifier(step.maneuver); 214 | text = stepToText(step, {legCount: legCount, legIndex: i}); 215 | 216 | if (type) { 217 | if ((i == 0 && step.maneuver.type == 'depart') || step.maneuver.type == 'arrive') { 218 | waypointIndices.push(index); 219 | } 220 | 221 | result.instructions.push({ 222 | type: type, 223 | distance: step.distance, 224 | time: step.duration, 225 | road: step.name, 226 | direction: this._bearingToDirection(step.maneuver.bearing_after), 227 | exit: step.maneuver.exit, 228 | index: index, 229 | mode: step.mode, 230 | modifier: modifier, 231 | text: text 232 | }); 233 | } 234 | 235 | index += geometry.length; 236 | } 237 | } 238 | 239 | result.name = legNames.join(', '); 240 | if (!hasSteps) { 241 | result.coordinates = this._decodePolyline(responseRoute.geometry); 242 | } else { 243 | result.waypointIndices = waypointIndices; 244 | } 245 | 246 | return result; 247 | }, 248 | 249 | _bearingToDirection: function(bearing) { 250 | var oct = Math.round(bearing / 45) % 8; 251 | return ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW'][oct]; 252 | }, 253 | 254 | _maneuverToInstructionType: function(maneuver, lastLeg) { 255 | switch (maneuver.type) { 256 | case 'new name': 257 | return 'Continue'; 258 | case 'depart': 259 | return 'Head'; 260 | case 'arrive': 261 | return lastLeg ? 'DestinationReached' : 'WaypointReached'; 262 | case 'roundabout': 263 | case 'rotary': 264 | return 'Roundabout'; 265 | case 'merge': 266 | case 'fork': 267 | case 'on ramp': 268 | case 'off ramp': 269 | case 'end of road': 270 | return this._camelCase(maneuver.type); 271 | // These are all reduced to the same instruction in the current model 272 | //case 'turn': 273 | //case 'ramp': // deprecated in v5.1 274 | default: 275 | return this._camelCase(maneuver.modifier); 276 | } 277 | }, 278 | 279 | _maneuverToModifier: function(maneuver) { 280 | var modifier = maneuver.modifier; 281 | 282 | switch (maneuver.type) { 283 | case 'merge': 284 | case 'fork': 285 | case 'on ramp': 286 | case 'off ramp': 287 | case 'end of road': 288 | modifier = this._leftOrRight(modifier); 289 | } 290 | 291 | return modifier && this._camelCase(modifier); 292 | }, 293 | 294 | _camelCase: function(s) { 295 | var words = s.split(' '), 296 | result = ''; 297 | for (var i = 0, l = words.length; i < l; i++) { 298 | result += words[i].charAt(0).toUpperCase() + words[i].substring(1); 299 | } 300 | 301 | return result; 302 | }, 303 | 304 | _leftOrRight: function(d) { 305 | return d.indexOf('left') >= 0 ? 'Left' : 'Right'; 306 | }, 307 | 308 | _decodePolyline: function(routeGeometry) { 309 | var cs = polyline.decode(routeGeometry, this.options.polylinePrecision), 310 | result = new Array(cs.length), 311 | i; 312 | for (i = cs.length - 1; i >= 0; i--) { 313 | result[i] = L.latLng(cs[i]); 314 | } 315 | 316 | return result; 317 | }, 318 | 319 | _toWaypoints: function(inputWaypoints, vias) { 320 | var wps = [], 321 | i, 322 | viaLoc; 323 | for (i = 0; i < vias.length; i++) { 324 | viaLoc = vias[i].location; 325 | wps.push(new Waypoint(L.latLng(viaLoc[1], viaLoc[0]), 326 | inputWaypoints[i].name, 327 | inputWaypoints[i].options)); 328 | } 329 | 330 | return wps; 331 | }, 332 | 333 | buildRouteUrl: function(waypoints, options) { 334 | var locs = [], 335 | hints = [], 336 | wp, 337 | latLng, 338 | computeInstructions, 339 | computeAlternative = true; 340 | 341 | for (var i = 0; i < waypoints.length; i++) { 342 | wp = waypoints[i]; 343 | latLng = wp.latLng; 344 | locs.push(latLng.lng + ',' + latLng.lat); 345 | hints.push(this._hints.locations[this._locationKey(latLng)] || ''); 346 | } 347 | 348 | computeInstructions = 349 | true; 350 | 351 | return this.options.serviceUrl + '/' + this.options.profile + '/' + 352 | locs.join(';') + '?' + 353 | (options.geometryOnly ? (options.simplifyGeometry ? '' : 'overview=full') : 'overview=false') + 354 | '&alternatives=' + computeAlternative.toString() + 355 | '&steps=' + computeInstructions.toString() + 356 | (this.options.useHints ? '&hints=' + hints.join(';') : '') + 357 | (options.allowUTurns ? '&continue_straight=' + !options.allowUTurns : ''); 358 | }, 359 | 360 | _locationKey: function(location) { 361 | return location.lat + ',' + location.lng; 362 | }, 363 | 364 | _saveHintData: function(actualWaypoints, waypoints) { 365 | var loc; 366 | this._hints = { 367 | locations: {} 368 | }; 369 | for (var i = actualWaypoints.length - 1; i >= 0; i--) { 370 | loc = waypoints[i].latLng; 371 | this._hints.locations[this._locationKey(loc)] = actualWaypoints[i].hint; 372 | } 373 | }, 374 | }); 375 | })(); 376 | -------------------------------------------------------------------------------- /src/plan.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var L = require('leaflet'); 5 | var GeocoderElement = require('./geocoder-element'); 6 | var Waypoint = require('./waypoint'); 7 | 8 | module.exports = (L.Layer || L.Class).extend({ 9 | includes: ((typeof L.Evented !== 'undefined' && L.Evented.prototype) || L.Mixin.Events), 10 | 11 | options: { 12 | dragStyles: [ 13 | {color: 'black', opacity: 0.15, weight: 9}, 14 | {color: 'white', opacity: 0.8, weight: 6}, 15 | {color: 'red', opacity: 1, weight: 2, dashArray: '7,12'} 16 | ], 17 | draggableWaypoints: true, 18 | routeWhileDragging: false, 19 | addWaypoints: true, 20 | reverseWaypoints: false, 21 | addButtonClassName: '', 22 | language: 'en', 23 | createGeocoderElement: function(wp, i, nWps, plan) { 24 | return new GeocoderElement(wp, i, nWps, plan); 25 | }, 26 | createMarker: function(i, wp) { 27 | var options = { 28 | draggable: this.draggableWaypoints 29 | }, 30 | marker = L.marker(wp.latLng, options); 31 | 32 | return marker; 33 | }, 34 | geocodersClassName: '' 35 | }, 36 | 37 | initialize: function(waypoints, options) { 38 | L.Util.setOptions(this, options); 39 | this._waypoints = []; 40 | this.setWaypoints(waypoints); 41 | }, 42 | 43 | isReady: function() { 44 | var i; 45 | for (i = 0; i < this._waypoints.length; i++) { 46 | if (!this._waypoints[i].latLng) { 47 | return false; 48 | } 49 | } 50 | 51 | return true; 52 | }, 53 | 54 | getWaypoints: function() { 55 | var i, 56 | wps = []; 57 | 58 | for (i = 0; i < this._waypoints.length; i++) { 59 | wps.push(this._waypoints[i]); 60 | } 61 | 62 | return wps; 63 | }, 64 | 65 | setWaypoints: function(waypoints) { 66 | var args = [0, this._waypoints.length].concat(waypoints); 67 | this.spliceWaypoints.apply(this, args); 68 | return this; 69 | }, 70 | 71 | spliceWaypoints: function() { 72 | var args = [arguments[0], arguments[1]], 73 | i; 74 | 75 | for (i = 2; i < arguments.length; i++) { 76 | args.push(arguments[i] && arguments[i].hasOwnProperty('latLng') ? arguments[i] : new Waypoint(arguments[i])); 77 | } 78 | 79 | [].splice.apply(this._waypoints, args); 80 | 81 | // Make sure there's always at least two waypoints 82 | while (this._waypoints.length < 2) { 83 | this.spliceWaypoints(this._waypoints.length, 0, null); 84 | } 85 | 86 | this._updateMarkers(); 87 | this._fireChanged.apply(this, args); 88 | }, 89 | 90 | onAdd: function(map) { 91 | this._map = map; 92 | this._updateMarkers(); 93 | }, 94 | 95 | onRemove: function() { 96 | var i; 97 | this._removeMarkers(); 98 | 99 | if (this._newWp) { 100 | for (i = 0; i < this._newWp.lines.length; i++) { 101 | this._map.removeLayer(this._newWp.lines[i]); 102 | } 103 | } 104 | 105 | delete this._map; 106 | }, 107 | 108 | createGeocoders: function() { 109 | var container = L.DomUtil.create('div', 'leaflet-routing-geocoders ' + this.options.geocodersClassName), 110 | waypoints = this._waypoints, 111 | addWpBtn, 112 | reverseBtn; 113 | 114 | this._geocoderContainer = container; 115 | this._geocoderElems = []; 116 | 117 | 118 | if (this.options.addWaypoints) { 119 | addWpBtn = L.DomUtil.create('button', 'leaflet-routing-add-waypoint ' + this.options.addButtonClassName, container); 120 | addWpBtn.setAttribute('type', 'button'); 121 | L.DomEvent.addListener(addWpBtn, 'click', function() { 122 | this.spliceWaypoints(waypoints.length, 0, null); 123 | }, this); 124 | } 125 | 126 | if (this.options.reverseWaypoints) { 127 | reverseBtn = L.DomUtil.create('button', 'leaflet-routing-reverse-waypoints', container); 128 | reverseBtn.setAttribute('type', 'button'); 129 | L.DomEvent.addListener(reverseBtn, 'click', function() { 130 | this._waypoints.reverse(); 131 | this.setWaypoints(this._waypoints); 132 | }, this); 133 | } 134 | 135 | this._updateGeocoders(); 136 | this.on('waypointsspliced', this._updateGeocoders); 137 | 138 | return container; 139 | }, 140 | 141 | _createGeocoder: function(i) { 142 | var geocoder = this.options.createGeocoderElement(this._waypoints[i], i, this._waypoints.length, this.options); 143 | geocoder 144 | .on('delete', function() { 145 | if (i > 0 || this._waypoints.length > 2) { 146 | this.spliceWaypoints(i, 1); 147 | } else { 148 | this.spliceWaypoints(i, 1, new Waypoint()); 149 | } 150 | }, this) 151 | .on('geocoded', function(e) { 152 | this._updateMarkers(); 153 | this._fireChanged(); 154 | this._focusGeocoder(i + 1); 155 | this.fire('waypointgeocoded', { 156 | waypointIndex: i, 157 | waypoint: e.waypoint 158 | }); 159 | }, this) 160 | .on('reversegeocoded', function(e) { 161 | this.fire('waypointgeocoded', { 162 | waypointIndex: i, 163 | waypoint: e.waypoint 164 | }); 165 | }, this); 166 | 167 | return geocoder; 168 | }, 169 | 170 | _updateGeocoders: function() { 171 | var elems = [], 172 | i, 173 | geocoderElem; 174 | 175 | for (i = 0; i < this._geocoderElems.length; i++) { 176 | this._geocoderContainer.removeChild(this._geocoderElems[i].getContainer()); 177 | } 178 | 179 | for (i = this._waypoints.length - 1; i >= 0; i--) { 180 | geocoderElem = this._createGeocoder(i); 181 | this._geocoderContainer.insertBefore(geocoderElem.getContainer(), this._geocoderContainer.firstChild); 182 | elems.push(geocoderElem); 183 | } 184 | 185 | this._geocoderElems = elems.reverse(); 186 | }, 187 | 188 | _removeMarkers: function() { 189 | var i; 190 | if (this._markers) { 191 | for (i = 0; i < this._markers.length; i++) { 192 | if (this._markers[i]) { 193 | this._map.removeLayer(this._markers[i]); 194 | } 195 | } 196 | } 197 | this._markers = []; 198 | }, 199 | 200 | _updateMarkers: function() { 201 | var i, 202 | m; 203 | 204 | if (!this._map) { 205 | return; 206 | } 207 | 208 | this._removeMarkers(); 209 | 210 | for (i = 0; i < this._waypoints.length; i++) { 211 | if (this._waypoints[i].latLng) { 212 | m = this.options.createMarker(i, this._waypoints[i], this._waypoints.length); 213 | if (m) { 214 | m.addTo(this._map); 215 | if (this.options.draggableWaypoints) { 216 | this._hookWaypointEvents(m, i); 217 | } 218 | } 219 | } else { 220 | m = null; 221 | } 222 | this._markers.push(m); 223 | } 224 | }, 225 | 226 | _fireChanged: function() { 227 | this.fire('waypointschanged', {waypoints: this.getWaypoints()}); 228 | 229 | if (arguments.length >= 2) { 230 | this.fire('waypointsspliced', { 231 | index: Array.prototype.shift.call(arguments), 232 | nRemoved: Array.prototype.shift.call(arguments), 233 | added: arguments 234 | }); 235 | } 236 | }, 237 | 238 | _hookWaypointEvents: function(m, i, trackMouseMove) { 239 | var eventLatLng = function(e) { 240 | return trackMouseMove ? e.latlng : e.target.getLatLng(); 241 | }, 242 | dragStart = L.bind(function(e) { 243 | this.fire('waypointdragstart', {index: i, latlng: eventLatLng(e)}); 244 | }, this), 245 | drag = L.bind(function(e) { 246 | this._waypoints[i].latLng = eventLatLng(e); 247 | this.fire('waypointdrag', {index: i, latlng: eventLatLng(e)}); 248 | }, this), 249 | dragEnd = L.bind(function(e) { 250 | this._waypoints[i].latLng = eventLatLng(e); 251 | this._waypoints[i].name = ''; 252 | if (this._geocoderElems) { 253 | this._geocoderElems[i].update(true); 254 | } 255 | this.fire('waypointdragend', {index: i, latlng: eventLatLng(e)}); 256 | this._fireChanged(); 257 | }, this), 258 | mouseMove, 259 | mouseUp; 260 | 261 | if (trackMouseMove) { 262 | mouseMove = L.bind(function(e) { 263 | this._markers[i].setLatLng(e.latlng); 264 | drag(e); 265 | }, this); 266 | mouseUp = L.bind(function(e) { 267 | this._map.dragging.enable(); 268 | this._map.off('mouseup', mouseUp); 269 | this._map.off('mousemove', mouseMove); 270 | dragEnd(e); 271 | }, this); 272 | this._map.dragging.disable(); 273 | this._map.on('mousemove', mouseMove); 274 | this._map.on('mouseup', mouseUp); 275 | dragStart({latlng: this._waypoints[i].latLng}); 276 | } else { 277 | m.on('dragstart', dragStart); 278 | m.on('drag', drag); 279 | m.on('dragend', dragEnd); 280 | } 281 | }, 282 | 283 | dragNewWaypoint: function(e) { 284 | var newWpIndex = e.afterIndex + 1; 285 | if (this.options.routeWhileDragging) { 286 | this.spliceWaypoints(newWpIndex, 0, e.latlng); 287 | this._hookWaypointEvents(this._markers[newWpIndex], newWpIndex, true); 288 | } else { 289 | this._dragNewWaypoint(newWpIndex, e.latlng); 290 | } 291 | }, 292 | 293 | _dragNewWaypoint: function(newWpIndex, initialLatLng) { 294 | newWpIndex = (newWpIndex === 0) ? 1 : newWpIndex; 295 | var wp = new Waypoint(initialLatLng), 296 | prevWp = this._waypoints[newWpIndex - 1], 297 | nextWp = this._waypoints[newWpIndex], 298 | marker = this.options.createMarker(newWpIndex, wp, this._waypoints.length + 1), 299 | lines = [], 300 | draggingEnabled = this._map.dragging.enabled(), 301 | mouseMove = L.bind(function(e) { 302 | var i, 303 | latLngs; 304 | if (marker) { 305 | marker.setLatLng(e.latlng); 306 | } 307 | for (i = 0; i < lines.length; i++) { 308 | latLngs = lines[i].getLatLngs(); 309 | latLngs.splice(1, 1, e.latlng); 310 | lines[i].setLatLngs(latLngs); 311 | } 312 | 313 | L.DomEvent.stop(e); 314 | }, this), 315 | mouseUp = L.bind(function(e) { 316 | var i; 317 | if (marker) { 318 | this._map.removeLayer(marker); 319 | } 320 | for (i = 0; i < lines.length; i++) { 321 | this._map.removeLayer(lines[i]); 322 | } 323 | this._map.off('mousemove', mouseMove); 324 | this._map.off('mouseup', mouseUp); 325 | this.spliceWaypoints(newWpIndex, 0, e.latlng); 326 | if (draggingEnabled) { 327 | this._map.dragging.enable(); 328 | } 329 | 330 | L.DomEvent.stop(e); 331 | }, this), 332 | i; 333 | 334 | if (marker) { 335 | marker.addTo(this._map); 336 | } 337 | 338 | for (i = 0; i < this.options.dragStyles.length; i++) { 339 | lines.push(L.polyline([prevWp.latLng, initialLatLng, nextWp.latLng], 340 | this.options.dragStyles[i]).addTo(this._map)); 341 | } 342 | 343 | if (draggingEnabled) { 344 | this._map.dragging.disable(); 345 | } 346 | 347 | this._map.on('mousemove', mouseMove); 348 | this._map.on('mouseup', mouseUp); 349 | }, 350 | 351 | _focusGeocoder: function(i) { 352 | if (this._geocoderElems[i]) { 353 | this._geocoderElems[i].focus(); 354 | } else { 355 | document.activeElement.blur(); 356 | } 357 | } 358 | }); 359 | })(); 360 | -------------------------------------------------------------------------------- /src/waypoint.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var L = require('leaflet'); 5 | 6 | module.exports = L.Class.extend({ 7 | options: { 8 | allowUTurn: false, 9 | }, 10 | initialize: function(latLng, name, options) { 11 | L.Util.setOptions(this, options); 12 | this.latLng = L.latLng(latLng); 13 | this.name = name; 14 | } 15 | }); 16 | })(); 17 | -------------------------------------------------------------------------------- /test/SpecRunner.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Jasmine Spec Runner v2.0.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /test/lib/jasmine-2.0.0/boot.js: -------------------------------------------------------------------------------- 1 | /** 2 | Starting with version 2.0, this file "boots" Jasmine, performing all of the necessary initialization before executing the loaded environment and all of a project's specs. This file should be loaded after `jasmine.js`, but before any project source files or spec files are loaded. Thus this file can also be used to customize Jasmine for a project. 3 | 4 | If a project is using Jasmine via the standalone distribution, this file can be customized directly. If a project is using Jasmine via the [Ruby gem][jasmine-gem], this file can be copied into the support directory via `jasmine copy_boot_js`. Other environments (e.g., Python) will have different mechanisms. 5 | 6 | The location of `boot.js` can be specified and/or overridden in `jasmine.yml`. 7 | 8 | [jasmine-gem]: http://github.com/pivotal/jasmine-gem 9 | */ 10 | 11 | (function() { 12 | 13 | /** 14 | * ## Require & Instantiate 15 | * 16 | * Require Jasmine's core files. Specifically, this requires and attaches all of Jasmine's code to the `jasmine` reference. 17 | */ 18 | window.jasmine = jasmineRequire.core(jasmineRequire); 19 | 20 | /** 21 | * Since this is being run in a browser and the results should populate to an HTML page, require the HTML-specific Jasmine code, injecting the same reference. 22 | */ 23 | jasmineRequire.html(jasmine); 24 | 25 | /** 26 | * Create the Jasmine environment. This is used to run all specs in a project. 27 | */ 28 | var env = jasmine.getEnv(); 29 | 30 | /** 31 | * ## The Global Interface 32 | * 33 | * Build up the functions that will be exposed as the Jasmine public interface. A project can customize, rename or alias any of these functions as desired, provided the implementation remains unchanged. 34 | */ 35 | var jasmineInterface = { 36 | describe: function(description, specDefinitions) { 37 | return env.describe(description, specDefinitions); 38 | }, 39 | 40 | xdescribe: function(description, specDefinitions) { 41 | return env.xdescribe(description, specDefinitions); 42 | }, 43 | 44 | it: function(desc, func) { 45 | return env.it(desc, func); 46 | }, 47 | 48 | xit: function(desc, func) { 49 | return env.xit(desc, func); 50 | }, 51 | 52 | beforeEach: function(beforeEachFunction) { 53 | return env.beforeEach(beforeEachFunction); 54 | }, 55 | 56 | afterEach: function(afterEachFunction) { 57 | return env.afterEach(afterEachFunction); 58 | }, 59 | 60 | expect: function(actual) { 61 | return env.expect(actual); 62 | }, 63 | 64 | pending: function() { 65 | return env.pending(); 66 | }, 67 | 68 | spyOn: function(obj, methodName) { 69 | return env.spyOn(obj, methodName); 70 | }, 71 | 72 | jsApiReporter: new jasmine.JsApiReporter({ 73 | timer: new jasmine.Timer() 74 | }) 75 | }; 76 | 77 | /** 78 | * Add all of the Jasmine global/public interface to the proper global, so a project can use the public interface directly. For example, calling `describe` in specs instead of `jasmine.getEnv().describe`. 79 | */ 80 | if (typeof window == "undefined" && typeof exports == "object") { 81 | extend(exports, jasmineInterface); 82 | } else { 83 | extend(window, jasmineInterface); 84 | } 85 | 86 | /** 87 | * Expose the interface for adding custom equality testers. 88 | */ 89 | jasmine.addCustomEqualityTester = function(tester) { 90 | env.addCustomEqualityTester(tester); 91 | }; 92 | 93 | /** 94 | * Expose the interface for adding custom expectation matchers 95 | */ 96 | jasmine.addMatchers = function(matchers) { 97 | return env.addMatchers(matchers); 98 | }; 99 | 100 | /** 101 | * Expose the mock interface for the JavaScript timeout functions 102 | */ 103 | jasmine.clock = function() { 104 | return env.clock; 105 | }; 106 | 107 | /** 108 | * ## Runner Parameters 109 | * 110 | * More browser specific code - wrap the query string in an object and to allow for getting/setting parameters from the runner user interface. 111 | */ 112 | 113 | var queryString = new jasmine.QueryString({ 114 | getWindowLocation: function() { return window.location; } 115 | }); 116 | 117 | var catchingExceptions = queryString.getParam("catch"); 118 | env.catchExceptions(typeof catchingExceptions === "undefined" ? true : catchingExceptions); 119 | 120 | /** 121 | * ## Reporters 122 | * The `HtmlReporter` builds all of the HTML UI for the runner page. This reporter paints the dots, stars, and x's for specs, as well as all spec names and all failures (if any). 123 | */ 124 | var htmlReporter = new jasmine.HtmlReporter({ 125 | env: env, 126 | onRaiseExceptionsClick: function() { queryString.setParam("catch", !env.catchingExceptions()); }, 127 | getContainer: function() { return document.body; }, 128 | createElement: function() { return document.createElement.apply(document, arguments); }, 129 | createTextNode: function() { return document.createTextNode.apply(document, arguments); }, 130 | timer: new jasmine.Timer() 131 | }); 132 | 133 | /** 134 | * The `jsApiReporter` also receives spec results, and is used by any environment that needs to extract the results from JavaScript. 135 | */ 136 | env.addReporter(jasmineInterface.jsApiReporter); 137 | env.addReporter(htmlReporter); 138 | 139 | /** 140 | * Filter which specs will be run by matching the start of the full name against the `spec` query param. 141 | */ 142 | var specFilter = new jasmine.HtmlSpecFilter({ 143 | filterString: function() { return queryString.getParam("spec"); } 144 | }); 145 | 146 | env.specFilter = function(spec) { 147 | return specFilter.matches(spec.getFullName()); 148 | }; 149 | 150 | /** 151 | * Setting up timing functions to be able to be overridden. Certain browsers (Safari, IE 8, phantomjs) require this hack. 152 | */ 153 | window.setTimeout = window.setTimeout; 154 | window.setInterval = window.setInterval; 155 | window.clearTimeout = window.clearTimeout; 156 | window.clearInterval = window.clearInterval; 157 | 158 | /** 159 | * ## Execution 160 | * 161 | * Replace the browser window's `onload`, ensure it's called, and then run all of the loaded specs. This includes initializing the `HtmlReporter` instance and then executing the loaded Jasmine environment. All of this will happen after all of the specs are loaded. 162 | */ 163 | var currentWindowOnload = window.onload; 164 | 165 | window.onload = function() { 166 | if (currentWindowOnload) { 167 | currentWindowOnload(); 168 | } 169 | htmlReporter.initialize(); 170 | env.execute(); 171 | }; 172 | 173 | /** 174 | * Helper function for readability above. 175 | */ 176 | function extend(destination, source) { 177 | for (var property in source) destination[property] = source[property]; 178 | return destination; 179 | } 180 | 181 | }()); 182 | -------------------------------------------------------------------------------- /test/lib/jasmine-2.0.0/console.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2008-2013 Pivotal Labs 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the 6 | "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | */ 23 | function getJasmineRequireObj() { 24 | if (typeof module !== "undefined" && module.exports) { 25 | return exports; 26 | } else { 27 | window.jasmineRequire = window.jasmineRequire || {}; 28 | return window.jasmineRequire; 29 | } 30 | } 31 | 32 | getJasmineRequireObj().console = function(jRequire, j$) { 33 | j$.ConsoleReporter = jRequire.ConsoleReporter(); 34 | }; 35 | 36 | getJasmineRequireObj().ConsoleReporter = function() { 37 | 38 | var noopTimer = { 39 | start: function(){}, 40 | elapsed: function(){ return 0; } 41 | }; 42 | 43 | function ConsoleReporter(options) { 44 | var print = options.print, 45 | showColors = options.showColors || false, 46 | onComplete = options.onComplete || function() {}, 47 | timer = options.timer || noopTimer, 48 | specCount, 49 | failureCount, 50 | failedSpecs = [], 51 | pendingCount, 52 | ansi = { 53 | green: '\x1B[32m', 54 | red: '\x1B[31m', 55 | yellow: '\x1B[33m', 56 | none: '\x1B[0m' 57 | }; 58 | 59 | this.jasmineStarted = function() { 60 | specCount = 0; 61 | failureCount = 0; 62 | pendingCount = 0; 63 | print("Started"); 64 | printNewline(); 65 | timer.start(); 66 | }; 67 | 68 | this.jasmineDone = function() { 69 | printNewline(); 70 | for (var i = 0; i < failedSpecs.length; i++) { 71 | specFailureDetails(failedSpecs[i]); 72 | } 73 | 74 | printNewline(); 75 | var specCounts = specCount + " " + plural("spec", specCount) + ", " + 76 | failureCount + " " + plural("failure", failureCount); 77 | 78 | if (pendingCount) { 79 | specCounts += ", " + pendingCount + " pending " + plural("spec", pendingCount); 80 | } 81 | 82 | print(specCounts); 83 | 84 | printNewline(); 85 | var seconds = timer.elapsed() / 1000; 86 | print("Finished in " + seconds + " " + plural("second", seconds)); 87 | 88 | printNewline(); 89 | 90 | onComplete(failureCount === 0); 91 | }; 92 | 93 | this.specDone = function(result) { 94 | specCount++; 95 | 96 | if (result.status == "pending") { 97 | pendingCount++; 98 | print(colored("yellow", "*")); 99 | return; 100 | } 101 | 102 | if (result.status == "passed") { 103 | print(colored("green", '.')); 104 | return; 105 | } 106 | 107 | if (result.status == "failed") { 108 | failureCount++; 109 | failedSpecs.push(result); 110 | print(colored("red", 'F')); 111 | } 112 | }; 113 | 114 | return this; 115 | 116 | function printNewline() { 117 | print("\n"); 118 | } 119 | 120 | function colored(color, str) { 121 | return showColors ? (ansi[color] + str + ansi.none) : str; 122 | } 123 | 124 | function plural(str, count) { 125 | return count == 1 ? str : str + "s"; 126 | } 127 | 128 | function repeat(thing, times) { 129 | var arr = []; 130 | for (var i = 0; i < times; i++) { 131 | arr.push(thing); 132 | } 133 | return arr; 134 | } 135 | 136 | function indent(str, spaces) { 137 | var lines = (str || '').split("\n"); 138 | var newArr = []; 139 | for (var i = 0; i < lines.length; i++) { 140 | newArr.push(repeat(" ", spaces).join("") + lines[i]); 141 | } 142 | return newArr.join("\n"); 143 | } 144 | 145 | function specFailureDetails(result) { 146 | printNewline(); 147 | print(result.fullName); 148 | 149 | for (var i = 0; i < result.failedExpectations.length; i++) { 150 | var failedExpectation = result.failedExpectations[i]; 151 | printNewline(); 152 | print(indent(failedExpectation.stack, 2)); 153 | } 154 | 155 | printNewline(); 156 | } 157 | } 158 | 159 | return ConsoleReporter; 160 | }; 161 | -------------------------------------------------------------------------------- /test/lib/jasmine-2.0.0/jasmine-html.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2008-2013 Pivotal Labs 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the 6 | "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | */ 23 | jasmineRequire.html = function(j$) { 24 | j$.ResultsNode = jasmineRequire.ResultsNode(); 25 | j$.HtmlReporter = jasmineRequire.HtmlReporter(j$); 26 | j$.QueryString = jasmineRequire.QueryString(); 27 | j$.HtmlSpecFilter = jasmineRequire.HtmlSpecFilter(); 28 | }; 29 | 30 | jasmineRequire.HtmlReporter = function(j$) { 31 | 32 | var noopTimer = { 33 | start: function() {}, 34 | elapsed: function() { return 0; } 35 | }; 36 | 37 | function HtmlReporter(options) { 38 | var env = options.env || {}, 39 | getContainer = options.getContainer, 40 | createElement = options.createElement, 41 | createTextNode = options.createTextNode, 42 | onRaiseExceptionsClick = options.onRaiseExceptionsClick || function() {}, 43 | timer = options.timer || noopTimer, 44 | results = [], 45 | specsExecuted = 0, 46 | failureCount = 0, 47 | pendingSpecCount = 0, 48 | htmlReporterMain, 49 | symbols; 50 | 51 | this.initialize = function() { 52 | htmlReporterMain = createDom("div", {className: "html-reporter"}, 53 | createDom("div", {className: "banner"}, 54 | createDom("span", {className: "title"}, "Jasmine"), 55 | createDom("span", {className: "version"}, j$.version) 56 | ), 57 | createDom("ul", {className: "symbol-summary"}), 58 | createDom("div", {className: "alert"}), 59 | createDom("div", {className: "results"}, 60 | createDom("div", {className: "failures"}) 61 | ) 62 | ); 63 | getContainer().appendChild(htmlReporterMain); 64 | 65 | symbols = find(".symbol-summary"); 66 | }; 67 | 68 | var totalSpecsDefined; 69 | this.jasmineStarted = function(options) { 70 | totalSpecsDefined = options.totalSpecsDefined || 0; 71 | timer.start(); 72 | }; 73 | 74 | var summary = createDom("div", {className: "summary"}); 75 | 76 | var topResults = new j$.ResultsNode({}, "", null), 77 | currentParent = topResults; 78 | 79 | this.suiteStarted = function(result) { 80 | currentParent.addChild(result, "suite"); 81 | currentParent = currentParent.last(); 82 | }; 83 | 84 | this.suiteDone = function(result) { 85 | if (currentParent == topResults) { 86 | return; 87 | } 88 | 89 | currentParent = currentParent.parent; 90 | }; 91 | 92 | this.specStarted = function(result) { 93 | currentParent.addChild(result, "spec"); 94 | }; 95 | 96 | var failures = []; 97 | this.specDone = function(result) { 98 | if (result.status != "disabled") { 99 | specsExecuted++; 100 | } 101 | 102 | symbols.appendChild(createDom("li", { 103 | className: result.status, 104 | id: "spec_" + result.id, 105 | title: result.fullName 106 | } 107 | )); 108 | 109 | if (result.status == "failed") { 110 | failureCount++; 111 | 112 | var failure = 113 | createDom("div", {className: "spec-detail failed"}, 114 | createDom("div", {className: "description"}, 115 | createDom("a", {title: result.fullName, href: specHref(result)}, result.fullName) 116 | ), 117 | createDom("div", {className: "messages"}) 118 | ); 119 | var messages = failure.childNodes[1]; 120 | 121 | for (var i = 0; i < result.failedExpectations.length; i++) { 122 | var expectation = result.failedExpectations[i]; 123 | messages.appendChild(createDom("div", {className: "result-message"}, expectation.message)); 124 | messages.appendChild(createDom("div", {className: "stack-trace"}, expectation.stack)); 125 | } 126 | 127 | failures.push(failure); 128 | } 129 | 130 | if (result.status == "pending") { 131 | pendingSpecCount++; 132 | } 133 | }; 134 | 135 | this.jasmineDone = function() { 136 | var banner = find(".banner"); 137 | banner.appendChild(createDom("span", {className: "duration"}, "finished in " + timer.elapsed() / 1000 + "s")); 138 | 139 | var alert = find(".alert"); 140 | 141 | alert.appendChild(createDom("span", { className: "exceptions" }, 142 | createDom("label", { className: "label", 'for': "raise-exceptions" }, "raise exceptions"), 143 | createDom("input", { 144 | className: "raise", 145 | id: "raise-exceptions", 146 | type: "checkbox" 147 | }) 148 | )); 149 | var checkbox = find("input"); 150 | 151 | checkbox.checked = !env.catchingExceptions(); 152 | checkbox.onclick = onRaiseExceptionsClick; 153 | 154 | if (specsExecuted < totalSpecsDefined) { 155 | var skippedMessage = "Ran " + specsExecuted + " of " + totalSpecsDefined + " specs - run all"; 156 | alert.appendChild( 157 | createDom("span", {className: "bar skipped"}, 158 | createDom("a", {href: "?", title: "Run all specs"}, skippedMessage) 159 | ) 160 | ); 161 | } 162 | var statusBarMessage = "" + pluralize("spec", specsExecuted) + ", " + pluralize("failure", failureCount); 163 | if (pendingSpecCount) { statusBarMessage += ", " + pluralize("pending spec", pendingSpecCount); } 164 | 165 | var statusBarClassName = "bar " + ((failureCount > 0) ? "failed" : "passed"); 166 | alert.appendChild(createDom("span", {className: statusBarClassName}, statusBarMessage)); 167 | 168 | var results = find(".results"); 169 | results.appendChild(summary); 170 | 171 | summaryList(topResults, summary); 172 | 173 | function summaryList(resultsTree, domParent) { 174 | var specListNode; 175 | for (var i = 0; i < resultsTree.children.length; i++) { 176 | var resultNode = resultsTree.children[i]; 177 | if (resultNode.type == "suite") { 178 | var suiteListNode = createDom("ul", {className: "suite", id: "suite-" + resultNode.result.id}, 179 | createDom("li", {className: "suite-detail"}, 180 | createDom("a", {href: specHref(resultNode.result)}, resultNode.result.description) 181 | ) 182 | ); 183 | 184 | summaryList(resultNode, suiteListNode); 185 | domParent.appendChild(suiteListNode); 186 | } 187 | if (resultNode.type == "spec") { 188 | if (domParent.getAttribute("class") != "specs") { 189 | specListNode = createDom("ul", {className: "specs"}); 190 | domParent.appendChild(specListNode); 191 | } 192 | specListNode.appendChild( 193 | createDom("li", { 194 | className: resultNode.result.status, 195 | id: "spec-" + resultNode.result.id 196 | }, 197 | createDom("a", {href: specHref(resultNode.result)}, resultNode.result.description) 198 | ) 199 | ); 200 | } 201 | } 202 | } 203 | 204 | if (failures.length) { 205 | alert.appendChild( 206 | createDom('span', {className: "menu bar spec-list"}, 207 | createDom("span", {}, "Spec List | "), 208 | createDom('a', {className: "failures-menu", href: "#"}, "Failures"))); 209 | alert.appendChild( 210 | createDom('span', {className: "menu bar failure-list"}, 211 | createDom('a', {className: "spec-list-menu", href: "#"}, "Spec List"), 212 | createDom("span", {}, " | Failures "))); 213 | 214 | find(".failures-menu").onclick = function() { 215 | setMenuModeTo('failure-list'); 216 | }; 217 | find(".spec-list-menu").onclick = function() { 218 | setMenuModeTo('spec-list'); 219 | }; 220 | 221 | setMenuModeTo('failure-list'); 222 | 223 | var failureNode = find(".failures"); 224 | for (var i = 0; i < failures.length; i++) { 225 | failureNode.appendChild(failures[i]); 226 | } 227 | } 228 | }; 229 | 230 | return this; 231 | 232 | function find(selector) { 233 | return getContainer().querySelector(selector); 234 | } 235 | 236 | function createDom(type, attrs, childrenVarArgs) { 237 | var el = createElement(type); 238 | 239 | for (var i = 2; i < arguments.length; i++) { 240 | var child = arguments[i]; 241 | 242 | if (typeof child === 'string') { 243 | el.appendChild(createTextNode(child)); 244 | } else { 245 | if (child) { 246 | el.appendChild(child); 247 | } 248 | } 249 | } 250 | 251 | for (var attr in attrs) { 252 | if (attr == "className") { 253 | el[attr] = attrs[attr]; 254 | } else { 255 | el.setAttribute(attr, attrs[attr]); 256 | } 257 | } 258 | 259 | return el; 260 | } 261 | 262 | function pluralize(singular, count) { 263 | var word = (count == 1 ? singular : singular + "s"); 264 | 265 | return "" + count + " " + word; 266 | } 267 | 268 | function specHref(result) { 269 | return "?spec=" + encodeURIComponent(result.fullName); 270 | } 271 | 272 | function setMenuModeTo(mode) { 273 | htmlReporterMain.setAttribute("class", "html-reporter " + mode); 274 | } 275 | } 276 | 277 | return HtmlReporter; 278 | }; 279 | 280 | jasmineRequire.HtmlSpecFilter = function() { 281 | function HtmlSpecFilter(options) { 282 | var filterString = options && options.filterString() && options.filterString().replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); 283 | var filterPattern = new RegExp(filterString); 284 | 285 | this.matches = function(specName) { 286 | return filterPattern.test(specName); 287 | }; 288 | } 289 | 290 | return HtmlSpecFilter; 291 | }; 292 | 293 | jasmineRequire.ResultsNode = function() { 294 | function ResultsNode(result, type, parent) { 295 | this.result = result; 296 | this.type = type; 297 | this.parent = parent; 298 | 299 | this.children = []; 300 | 301 | this.addChild = function(result, type) { 302 | this.children.push(new ResultsNode(result, type, this)); 303 | }; 304 | 305 | this.last = function() { 306 | return this.children[this.children.length - 1]; 307 | }; 308 | } 309 | 310 | return ResultsNode; 311 | }; 312 | 313 | jasmineRequire.QueryString = function() { 314 | function QueryString(options) { 315 | 316 | this.setParam = function(key, value) { 317 | var paramMap = queryStringToParamMap(); 318 | paramMap[key] = value; 319 | options.getWindowLocation().search = toQueryString(paramMap); 320 | }; 321 | 322 | this.getParam = function(key) { 323 | return queryStringToParamMap()[key]; 324 | }; 325 | 326 | return this; 327 | 328 | function toQueryString(paramMap) { 329 | var qStrPairs = []; 330 | for (var prop in paramMap) { 331 | qStrPairs.push(encodeURIComponent(prop) + "=" + encodeURIComponent(paramMap[prop])); 332 | } 333 | return "?" + qStrPairs.join('&'); 334 | } 335 | 336 | function queryStringToParamMap() { 337 | var paramStr = options.getWindowLocation().search.substring(1), 338 | params = [], 339 | paramMap = {}; 340 | 341 | if (paramStr.length > 0) { 342 | params = paramStr.split('&'); 343 | for (var i = 0; i < params.length; i++) { 344 | var p = params[i].split('='); 345 | var value = decodeURIComponent(p[1]); 346 | if (value === "true" || value === "false") { 347 | value = JSON.parse(value); 348 | } 349 | paramMap[decodeURIComponent(p[0])] = value; 350 | } 351 | } 352 | 353 | return paramMap; 354 | } 355 | 356 | } 357 | 358 | return QueryString; 359 | }; 360 | -------------------------------------------------------------------------------- /test/lib/jasmine-2.0.0/jasmine.css: -------------------------------------------------------------------------------- 1 | body { background-color: #eeeeee; padding: 0; margin: 5px; overflow-y: scroll; } 2 | 3 | .html-reporter { font-size: 11px; font-family: Monaco, "Lucida Console", monospace; line-height: 14px; color: #333333; } 4 | .html-reporter a { text-decoration: none; } 5 | .html-reporter a:hover { text-decoration: underline; } 6 | .html-reporter p, .html-reporter h1, .html-reporter h2, .html-reporter h3, .html-reporter h4, .html-reporter h5, .html-reporter h6 { margin: 0; line-height: 14px; } 7 | .html-reporter .banner, .html-reporter .symbol-summary, .html-reporter .summary, .html-reporter .result-message, .html-reporter .spec .description, .html-reporter .spec-detail .description, .html-reporter .alert .bar, .html-reporter .stack-trace { padding-left: 9px; padding-right: 9px; } 8 | .html-reporter .banner .version { margin-left: 14px; } 9 | .html-reporter #jasmine_content { position: fixed; right: 100%; } 10 | .html-reporter .version { color: #aaaaaa; } 11 | .html-reporter .banner { margin-top: 14px; } 12 | .html-reporter .duration { color: #aaaaaa; float: right; } 13 | .html-reporter .symbol-summary { overflow: hidden; *zoom: 1; margin: 14px 0; } 14 | .html-reporter .symbol-summary li { display: inline-block; height: 8px; width: 14px; font-size: 16px; } 15 | .html-reporter .symbol-summary li.passed { font-size: 14px; } 16 | .html-reporter .symbol-summary li.passed:before { color: #5e7d00; content: "\02022"; } 17 | .html-reporter .symbol-summary li.failed { line-height: 9px; } 18 | .html-reporter .symbol-summary li.failed:before { color: #b03911; content: "x"; font-weight: bold; margin-left: -1px; } 19 | .html-reporter .symbol-summary li.disabled { font-size: 14px; } 20 | .html-reporter .symbol-summary li.disabled:before { color: #bababa; content: "\02022"; } 21 | .html-reporter .symbol-summary li.pending { line-height: 17px; } 22 | .html-reporter .symbol-summary li.pending:before { color: #ba9d37; content: "*"; } 23 | .html-reporter .exceptions { color: #fff; float: right; margin-top: 5px; margin-right: 5px; } 24 | .html-reporter .bar { line-height: 28px; font-size: 14px; display: block; color: #eee; } 25 | .html-reporter .bar.failed { background-color: #b03911; } 26 | .html-reporter .bar.passed { background-color: #a6b779; } 27 | .html-reporter .bar.skipped { background-color: #bababa; } 28 | .html-reporter .bar.menu { background-color: #fff; color: #aaaaaa; } 29 | .html-reporter .bar.menu a { color: #333333; } 30 | .html-reporter .bar a { color: white; } 31 | .html-reporter.spec-list .bar.menu.failure-list, .html-reporter.spec-list .results .failures { display: none; } 32 | .html-reporter.failure-list .bar.menu.spec-list, .html-reporter.failure-list .summary { display: none; } 33 | .html-reporter .running-alert { background-color: #666666; } 34 | .html-reporter .results { margin-top: 14px; } 35 | .html-reporter.showDetails .summaryMenuItem { font-weight: normal; text-decoration: inherit; } 36 | .html-reporter.showDetails .summaryMenuItem:hover { text-decoration: underline; } 37 | .html-reporter.showDetails .detailsMenuItem { font-weight: bold; text-decoration: underline; } 38 | .html-reporter.showDetails .summary { display: none; } 39 | .html-reporter.showDetails #details { display: block; } 40 | .html-reporter .summaryMenuItem { font-weight: bold; text-decoration: underline; } 41 | .html-reporter .summary { margin-top: 14px; } 42 | .html-reporter .summary ul { list-style-type: none; margin-left: 14px; padding-top: 0; padding-left: 0; } 43 | .html-reporter .summary ul.suite { margin-top: 7px; margin-bottom: 7px; } 44 | .html-reporter .summary li.passed a { color: #5e7d00; } 45 | .html-reporter .summary li.failed a { color: #b03911; } 46 | .html-reporter .summary li.pending a { color: #ba9d37; } 47 | .html-reporter .description + .suite { margin-top: 0; } 48 | .html-reporter .suite { margin-top: 14px; } 49 | .html-reporter .suite a { color: #333333; } 50 | .html-reporter .failures .spec-detail { margin-bottom: 28px; } 51 | .html-reporter .failures .spec-detail .description { background-color: #b03911; } 52 | .html-reporter .failures .spec-detail .description a { color: white; } 53 | .html-reporter .result-message { padding-top: 14px; color: #333333; white-space: pre; } 54 | .html-reporter .result-message span.result { display: block; } 55 | .html-reporter .stack-trace { margin: 5px 0 0 0; max-height: 224px; overflow: auto; line-height: 18px; color: #666666; border: 1px solid #ddd; background: white; white-space: pre; } 56 | -------------------------------------------------------------------------------- /test/lib/jasmine-2.0.0/jasmine_favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/perliedman/leaflet-routing-machine/8dc939a314425684883dc6f016b3aa9d04c8b7ca/test/lib/jasmine-2.0.0/jasmine_favicon.png -------------------------------------------------------------------------------- /test/spec/formatter-spec.js: -------------------------------------------------------------------------------- 1 | describe('L.Routing.Formatter', function() { 2 | describe('._round', function() { 3 | it('rounds 0 < x < 5 to multiple of 0.5', function() { 4 | var p = new L.Routing.Formatter(undefined); 5 | expect(p._round(1)).toBe(1); 6 | expect(p._round(1.4)).toBe(1.5); 7 | expect(p._round(1.75)).toBe(2); 8 | }); 9 | it('rounds 5 < x < 10 to nearest integer', function() { 10 | var p = new L.Routing.Formatter(undefined); 11 | expect(p._round(7)).toBe(7); 12 | expect(p._round(9.4)).toBe(9); 13 | expect(p._round(9.8)).toBe(10); 14 | }); 15 | it('rounds 10 < x < 50 values to multiples of 5', function() { 16 | var p = new L.Routing.Formatter(undefined); 17 | expect(p._round(11.5)).toBe(10); 18 | expect(p._round(14)).toBe(15); 19 | expect(p._round(42)).toBe(40); 20 | expect(p._round(43)).toBe(45); 21 | }); 22 | it('rounds 50 < x < 100 to multiples of 10', function() { 23 | var p = new L.Routing.Formatter(undefined); 24 | expect(p._round(72)).toBe(70); 25 | expect(p._round(76)).toBe(80); 26 | expect(p._round(97.6)).toBe(100); 27 | }); 28 | it('rounds 100 < x < 150 to multiples of 50', function() { 29 | var p = new L.Routing.Formatter(undefined); 30 | expect(p._round(105)).toBe(100); 31 | expect(p._round(125)).toBe(150); 32 | }); 33 | it('rounds large values to multiples of 100', function() { 34 | var p = new L.Routing.Formatter(undefined); 35 | expect(p._round(686)).toBe(700); 36 | expect(p._round(860)).toBe(900); 37 | }); 38 | it('considers rounding sensitivity', function() { 39 | var p = new L.Routing.Formatter({roundingSensitivity: 5}); 40 | expect(p._round(24)).toBe(24); 41 | expect(p._round(52)).toBe(50); 42 | }); 43 | }); 44 | 45 | describe('.formatDistance', function() { 46 | it('rounds long distances reasonably', function() { 47 | var p = new L.Routing.Formatter({ 48 | distanceTemplate: '{value}' 49 | }); 50 | expect(parseInt(p.formatDistance(22000), 10)).toBe(20); 51 | expect(parseInt(p.formatDistance(24000), 10)).toBe(25); 52 | expect(parseInt(p.formatDistance(86000), 10)).toBe(90); 53 | }); 54 | it('formats imperial units properly', function() { 55 | var p = new L.Routing.Formatter({ 56 | distanceTemplate: '{value}', 57 | units: 'imperial' 58 | }); 59 | expect(parseInt(p.formatDistance(800), 10)).toBe(900); 60 | expect(parseInt(p.formatDistance(22000), 10)).toBe(15); 61 | expect(parseInt(p.formatDistance(24500), 10)).toBe(15); 62 | expect(parseInt(p.formatDistance(86000), 10)).toBe(55); 63 | }); 64 | }); 65 | 66 | describe('.formatTime', function() { 67 | it('rounds whole minutes without seconds', function() { 68 | var p = new L.Routing.Formatter(); 69 | expect(p.formatTime(240)).toBe('4 min'); 70 | }) 71 | it('rounds just under five minutes to five minutes without seconds', function() { 72 | var p = new L.Routing.Formatter(); 73 | expect(p.formatTime(299.10000000005)).toBe('5 min'); 74 | }) 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /test/spec/osrmv1-spec.js: -------------------------------------------------------------------------------- 1 | describe('L.Routing.OSRMv1', function() { 2 | describe('#route', function() { 3 | var waypoints = [ 4 | new L.Routing.Waypoint([57.73, 11.94]), 5 | new L.Routing.Waypoint([57.7, 11.9]) 6 | ]; 7 | it('returns correct waypoints', function(done) { 8 | var router = new L.Routing.OSRMv1(); 9 | router.route(waypoints, function(err, routes) { 10 | if (err) { 11 | return done(err); 12 | } 13 | 14 | if (!routes.length) { 15 | return done('No routes :('); 16 | } 17 | 18 | waypoints.forEach(function(wp, i) { 19 | var returnedWp = routes[0].waypoints[i]; 20 | expect(Math.abs(returnedWp.latLng.lat - wp.latLng.lat)).to.be.lessThan(0.1); 21 | expect(Math.abs(returnedWp.latLng.lng - wp.latLng.lng)).to.be.lessThan(0.1); 22 | }); 23 | 24 | done(); 25 | }); 26 | }); 27 | }); 28 | }); 29 | --------------------------------------------------------------------------------