├── .gitignore
├── dist
├── mapbox.directions.png
├── mapbox.directions.css
└── mapbox.directions.svg
├── .travis.yml
├── test
├── test.js
├── instructions_control.js
├── routes_control.js
├── input_control.js
├── layer.js
└── directions.js
├── .jshintrc
├── index.js
├── Makefile
├── src
├── request.js
├── errors_control.js
├── instructions_control.js
├── routes_control.js
├── format.js
├── input_control.js
├── directions.js
└── layer.js
├── LICENSE
├── CHANGELOG.md
├── package.json
├── README.md
├── API.md
├── index.html
└── lib
└── d3.js
/.gitignore:
--------------------------------------------------------------------------------
1 | dist/mapbox.directions.js
2 | node_modules
3 |
--------------------------------------------------------------------------------
/dist/mapbox.directions.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/YerkoPalma/mapbox-directions.js/mb-pages/dist/mapbox.directions.png
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | before_script:
2 | - export DISPLAY=:99.0
3 | - sh -e /etc/init.d/xvfb start
4 | sudo: false
5 | language: node_js
6 |
--------------------------------------------------------------------------------
/test/test.js:
--------------------------------------------------------------------------------
1 | var test = require('tape');
2 |
3 | require('mapbox.js');
4 | require('../');
5 |
6 | require('./directions.js');
7 | require('./input_control.js');
8 | require('./instructions_control.js');
9 | require('./layer.js');
10 | require('./routes_control.js');
11 |
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "globals": {
3 | "L": false,
4 | "require": false,
5 | "module": false,
6 | "console": false,
7 | "document": false,
8 | "window": false
9 | },
10 | "globalstrict": true,
11 | "loopfunc": true
12 | }
13 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | if (!L.mapbox) throw new Error('include mapbox.js before mapbox.directions.js');
4 |
5 | L.mapbox.directions = require('./src/directions');
6 | L.mapbox.directions.format = require('./src/format');
7 | L.mapbox.directions.layer = require('./src/layer');
8 | L.mapbox.directions.inputControl = require('./src/input_control');
9 | L.mapbox.directions.errorsControl = require('./src/errors_control');
10 | L.mapbox.directions.routesControl = require('./src/routes_control');
11 | L.mapbox.directions.instructionsControl = require('./src/instructions_control');
12 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | BROWSERIFY = node_modules/.bin/browserify
2 |
3 | all: dist/mapbox.directions.js
4 |
5 | node_modules/.install: package.json
6 | npm install && touch node_modules/.install
7 |
8 | dist:
9 | mkdir -p dist
10 |
11 | dist/mapbox.directions.js: node_modules/.install dist $(shell $(BROWSERIFY) --list index.js)
12 | npm run build
13 |
14 | clean:
15 | rm -rf dist/mapbox.directions.js
16 |
17 | D3_FILES = \
18 | node_modules/d3/src/start.js \
19 | node_modules/d3/src/selection/index.js \
20 | node_modules/d3/src/end.js
21 |
22 | lib/d3.js: $(D3_FILES)
23 | node_modules/.bin/smash $(D3_FILES) > $@
24 |
--------------------------------------------------------------------------------
/src/request.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var corslite = require('corslite');
4 |
5 | module.exports = function(url, callback) {
6 | return corslite(url, function (err, resp) {
7 | if (err && err.type === 'abort') {
8 | return;
9 | }
10 |
11 | if (err && !err.responseText) {
12 | return callback(err);
13 | }
14 |
15 | resp = resp || err;
16 |
17 | try {
18 | resp = JSON.parse(resp.responseText);
19 | } catch (e) {
20 | return callback(new Error(resp.responseText));
21 | }
22 |
23 | if (resp.error) {
24 | return callback(new Error(resp.error));
25 | }
26 |
27 | return callback(null, resp);
28 | });
29 | };
30 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | ISC License
2 |
3 | Copyright (c) 2014, Mapbox
4 |
5 | Permission to use, copy, modify, and/or distribute this software for any
6 | purpose with or without fee is hereby granted, provided that the above
7 | copyright notice and this permission notice appear in all copies.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
16 |
--------------------------------------------------------------------------------
/test/instructions_control.js:
--------------------------------------------------------------------------------
1 | var test = require('tape');
2 |
3 | test("Directions#instructionsControl", function (t) {
4 | var container, map, directions;
5 |
6 | function setup() {
7 | container = document.createElement('div');
8 | map = L.map(container).setView([0, 0], 0);
9 | directions = L.mapbox.directions();
10 | };
11 |
12 | t.test("on directions error", function (u) {
13 | setup();
14 |
15 | u.test("clears routes", function (v) {
16 | L.mapbox.directions.instructionsControl(container, directions).addTo(map);
17 | container.innerHTML = 'Instructions';
18 | directions.fire('error');
19 | v.equal(container.innerHTML,'');
20 | v.end();
21 | });
22 |
23 | u.end();
24 | });
25 |
26 | t.end();
27 | });
28 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## 0.4.0 (Oct 8 2015)
2 |
3 | #### Improvements
4 |
5 | * Add configurable units option ([a99abf](https://github.com/mapbox/mapbox-directions.js/commit/a99abf204adf3569cb19a0885bc24c66c71877cf))
6 | * Make route style configurable ([2f5ee4](https://github.com/mapbox/mapbox-directions.js/commit/2f5ee4dd281bd21706ee9c5d087c2c4b8da6f527))
7 |
8 | #### Bugfixes
9 |
10 | * Set inputControl `checked` profile on initialization ([83d8fb](https://github.com/mapbox/mapbox-directions.js/commit/83d8fbf28bc71867ab67159d957d6cd4d1a6ba7c))
11 | * Switch tests to smokestack + tape to avoid PhantomJS SSL bug on CI ([9e2711](https://github.com/mapbox/mapbox-directions.js/commit/9e2711b197854d915f6871be7d0781f33e6d8991))
12 |
13 | ## 0.3.0 (May 26 2015)
14 |
15 | #### Improvements
16 |
17 | * Add geocoding to `inputControl` (#73)
18 | * Add `mapbox.cycling` to available profiles
19 |
--------------------------------------------------------------------------------
/src/errors_control.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var d3 = require('../lib/d3');
4 |
5 | module.exports = function (container, directions) {
6 | var control = {}, map;
7 |
8 | control.addTo = function (_) {
9 | map = _;
10 | return control;
11 | };
12 |
13 | container = d3.select(L.DomUtil.get(container))
14 | .classed('mapbox-directions-errors', true);
15 |
16 | directions.on('load unload', function () {
17 | container
18 | .classed('mapbox-error-active', false)
19 | .html('');
20 | });
21 |
22 | directions.on('error', function (e) {
23 | container
24 | .classed('mapbox-error-active', true)
25 | .html('')
26 | .append('span')
27 | .attr('class', 'mapbox-directions-error')
28 | .text(e.error);
29 |
30 | container
31 | .insert('span', 'span')
32 | .attr('class', 'mapbox-directions-icon mapbox-error-icon');
33 | });
34 |
35 | return control;
36 | };
37 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@yerkopalma/mapbox-directions.js",
3 | "version": "0.5.1",
4 | "description": "Leaflet plugin for the Mapbox Directions API",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "jshint src && npm run build && browserify --debug test/test.js | tap-closer | smokestack -b firefox | tap-spec",
8 | "test:cr": "jshint src && npm run build && browserify --debug test/test.js | tap-closer | smokestack -b chrome | tap-spec",
9 | "build": "browserify --debug index.js > dist/mapbox.directions.js",
10 | "start": "budo index.js --live --serve=dist/mapbox.directions.js"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "git://github.com/mapbox/mapbox-directions.js.git"
15 | },
16 | "dependencies": {
17 | "corslite": "0.0.5",
18 | "d3": "3.4.1",
19 | "debounce": "0.0.3",
20 | "mapbox.js": "1.5.2",
21 | "polyline": "0.0.3",
22 | "queue-async": "^1.0.7"
23 | },
24 | "devDependencies": {
25 | "browserify": "^13.0.0",
26 | "budo": "^8.1.0",
27 | "jshint": "2",
28 | "sinon": "^1.17.1",
29 | "smash": "0.0",
30 | "smokestack": "^3.4.1",
31 | "tap-closer": "^1.0.0",
32 | "tap-spec": "^4.1.0",
33 | "tape": "^4.2.1"
34 | },
35 | "license": "BSD-2-Clause"
36 | }
37 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # mapbox-directions.js
2 |
3 | [](https://travis-ci.org/mapbox/mapbox-directions.js)
4 |
5 | **NOTE: This is a fork for mapbox-directions plugins, that includes language support for directions. Published as scoped package to npm at `@yerkopalma/mapbox-directions.js`**
6 |
7 | This is a Mapbox.js plugin for the Mapbox Directions API. Its main features include:
8 |
9 | * Input controls for origin and destination
10 | * Draggable origin and destination markers
11 | * Draggable intermediate waypoints
12 | * Display of turn-by-turn instructions
13 | * Selection of alternate routes
14 |
15 | ## [API](https://github.com/mapbox/mapbox-directions.js/blob/mb-pages/API.md)
16 |
17 | Managed as Markdown in `API.md`, following the standards in `DOCUMENTING.md`
18 |
19 | ## Development
20 |
21 | Run `npm install` to install dependencies.
22 |
23 | The `npm start` task will start up a live-reloading and regenerating development server.
24 |
25 | ## Building
26 |
27 | Requires [node.js](http://nodejs.org/) installed on your system.
28 |
29 | ``` sh
30 | git clone https://github.com/mapbox/mapbox-directions.js.git
31 | cd mapbox-directions.js
32 | npm install
33 | make
34 | ```
35 |
36 | This project uses [browserify](https://github.com/substack/node-browserify) to combine
37 | dependencies and installs a local copy when you run `npm install`.
38 | `make` will build the project in `dist/`.
39 |
40 | ### Tests
41 |
42 | Test with [smokestack](https://www.npmjs.com/package/smokestack):
43 |
44 | ``` sh
45 | npm test
46 | ```
47 |
--------------------------------------------------------------------------------
/src/instructions_control.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var d3 = require('../lib/d3'),
4 | format = require('./format');
5 |
6 | module.exports = function (container, directions) {
7 | var control = {}, map;
8 |
9 | control.addTo = function (_) {
10 | map = _;
11 | return control;
12 | };
13 |
14 | container = d3.select(L.DomUtil.get(container))
15 | .classed('mapbox-directions-instructions', true);
16 |
17 | directions.on('error', function () {
18 | container.html('');
19 | });
20 |
21 | directions.on('selectRoute', function (e) {
22 | var route = e.route;
23 |
24 | container.html('');
25 |
26 | var steps = container.append('ol')
27 | .attr('class', 'mapbox-directions-steps')
28 | .selectAll('li')
29 | .data(route.steps)
30 | .enter().append('li')
31 | .attr('class', 'mapbox-directions-step');
32 |
33 | steps.append('span')
34 | .attr('class', function (step) {
35 | return 'mapbox-directions-icon mapbox-' + step.maneuver.type.replace(/\s+/g, '-').toLowerCase() + '-icon';
36 | });
37 |
38 | steps.append('div')
39 | .attr('class', 'mapbox-directions-step-maneuver')
40 | .html(function (step) {
41 | return step.maneuver.instruction ? format[directions.options.language](step.maneuver.instruction) : ''
42 | });
43 |
44 | steps.append('div')
45 | .attr('class', 'mapbox-directions-step-distance')
46 | .text(function (step) {
47 | return step.distance ? format[directions.options.units](step.distance) : '';
48 | });
49 |
50 | steps.on('mouseover', function (step) {
51 | directions.highlightStep(step);
52 | });
53 |
54 | steps.on('mouseout', function () {
55 | directions.highlightStep(null);
56 | });
57 |
58 | steps.on('click', function (step) {
59 | map.panTo(L.GeoJSON.coordsToLatLng(step.maneuver.location.coordinates));
60 | });
61 | });
62 |
63 | return control;
64 | };
65 |
--------------------------------------------------------------------------------
/test/routes_control.js:
--------------------------------------------------------------------------------
1 | var test = require('tape');
2 |
3 | test("Directions#routesControl", function (t) {
4 | var container, map, directions;
5 |
6 | function setup(options) {
7 | options = options || {};
8 | container = document.createElement('div');
9 | map = L.map(container).setView([0, 0], 0);
10 | directions = L.mapbox.directions(options);
11 | };
12 |
13 | t.test("units options", function(u) {
14 | var response = {
15 | origin: {},
16 | destination: {},
17 | waypoints: [],
18 | routes: [{
19 | distance: 10,
20 | duration: 15,
21 | geometry: {type: "LineString", coordinates: []}
22 | }],
23 | steps: [{
24 | distance: 5
25 | }]
26 | };
27 |
28 | u.test("default: returns instructions in imperial units", function(v) {
29 | setup();
30 |
31 | L.mapbox.directions.routesControl(container, directions).addTo(map);
32 | directions.fire('load', response);
33 | v.equal(container.querySelector('.mapbox-directions-route-details').innerHTML.indexOf('33 ft,'), 0);
34 | v.end();
35 | });
36 |
37 | u.test("metric option returns instructions in metric", function(v) {
38 | setup({ units: 'metric' });
39 |
40 | L.mapbox.directions.routesControl(container, directions).addTo(map);
41 | directions.fire('load', response);
42 | v.equal(container.querySelector('.mapbox-directions-route-details').innerHTML.indexOf('10 m,'), 0);
43 | v.end();
44 | });
45 | });
46 |
47 | t.test("on directions error", function (u) {
48 | setup();
49 |
50 | u.test("clears routes", function (v) {
51 | L.mapbox.directions.routesControl(container, directions).addTo(map);
52 | container.innerHTML = 'Route 1';
53 | directions.fire('error');
54 | v.equal(container.innerHTML, '');
55 | v.end();
56 | });
57 |
58 | u.end();
59 | });
60 |
61 | t.end();
62 | });
63 |
--------------------------------------------------------------------------------
/src/routes_control.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var d3 = require('../lib/d3'),
4 | format = require('./format');
5 |
6 | module.exports = function (container, directions) {
7 | var control = {}, map, selection = 0;
8 |
9 | control.addTo = function (_) {
10 | map = _;
11 | return control;
12 | };
13 |
14 | container = d3.select(L.DomUtil.get(container))
15 | .classed('mapbox-directions-routes', true);
16 |
17 | directions.on('error', function () {
18 | container.html('');
19 | });
20 |
21 | directions.on('load', function (e) {
22 | container.html('');
23 |
24 | var routes = container.append('ul')
25 | .selectAll('li')
26 | .data(e.routes)
27 | .enter().append('li')
28 | .attr('class', 'mapbox-directions-route');
29 |
30 | routes.append('div')
31 | .attr('class','mapbox-directions-route-heading')
32 | .text(function (route) { return 'Route ' + (e.routes.indexOf(route) + 1); });
33 |
34 | routes.append('div')
35 | .attr('class', 'mapbox-directions-route-summary')
36 | .text(function (route) { return route.summary; });
37 |
38 | routes.append('div')
39 | .attr('class', 'mapbox-directions-route-details')
40 | .text(function (route) {
41 | return format[directions.options.units](route.distance) + ', ' + format.duration(route.duration);
42 | });
43 |
44 | routes.on('mouseover', function (route) {
45 | directions.highlightRoute(route);
46 | });
47 |
48 | routes.on('mouseout', function () {
49 | directions.highlightRoute(null);
50 | });
51 |
52 | routes.on('click', function (route) {
53 | directions.selectRoute(route);
54 | });
55 |
56 | directions.selectRoute(e.routes[0]);
57 | });
58 |
59 | directions.on('selectRoute', function (e) {
60 | container.selectAll('.mapbox-directions-route')
61 | .classed('mapbox-directions-route-active', function (route) { return route === e.route; });
62 | });
63 |
64 | return control;
65 | };
66 |
--------------------------------------------------------------------------------
/src/format.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = {
4 | duration: function (s) {
5 | var m = Math.floor(s / 60),
6 | h = Math.floor(m / 60);
7 | s %= 60;
8 | m %= 60;
9 | if (h === 0 && m === 0) return s + ' s';
10 | if (h === 0) return m + ' min';
11 | return h + ' h ' + m + ' min';
12 | },
13 |
14 | imperial: function (m) {
15 | var mi = m / 1609.344;
16 | if (mi >= 100) return mi.toFixed(0) + ' mi';
17 | if (mi >= 10) return mi.toFixed(1) + ' mi';
18 | if (mi >= 0.1) return mi.toFixed(2) + ' mi';
19 | return (mi * 5280).toFixed(0) + ' ft';
20 | },
21 |
22 | metric: function (m) {
23 | if (m >= 100000) return (m / 1000).toFixed(0) + ' km';
24 | if (m >= 10000) return (m / 1000).toFixed(1) + ' km';
25 | if (m >= 100) return (m / 1000).toFixed(2) + ' km';
26 | return m.toFixed(0) + ' m';
27 | },
28 | en: function (m) {
29 | return m;
30 | },
31 | es: function (m) {
32 | m = m.replace(/\b(N|n)?(orth)\b/g, "Norte");
33 | m = m.replace(/\b(N|n)?(ortheast)\b/g, "Noreste");
34 | m = m.replace(/\b(N|n)?(orthwest)\b/g, "Noroeste");
35 | m = m.replace(/\b(S|s)?(outh)\b/g, "Sur");
36 | m = m.replace(/\b(S|s)?(outheast)\b/g, "Sureste");
37 | m = m.replace(/\b(S|s)?(outhwest)\b/g, "Suroeste");
38 | m = m.replace(/\b(E|e)?(ast)\b/g, "Este");
39 | m = m.replace(/\b(W|w)?(est)\b/g, "Oeste");
40 | m = m.replace(/\b(O|o)?(nto)\b/g, "hacia");
41 | m = m.replace(/\b(O|o)?(n)\b/g, "en");
42 | m = m.replace(/\b(H|h)?(ead)\b/g, "Siga hacia el");
43 | m = m.replace(/Turn right/g, "Gire a la derecha");
44 | m = m.replace(/Bear right/g, "Vuelta a la derecha");
45 | m = m.replace(/Make a sharp right/g, "Curva brusca a la derecha");
46 | m = m.replace(/Make a slight right/g, "Curva ligera a la derecha");
47 | m = m.replace(/Turn left/g, "Gire a la izquierda");
48 | m = m.replace(/Bear left/g, "Encoste à esquerda");
49 | m = m.replace(/Go straight/g, "Siga derecho");
50 | m = m.replace(/Continue left/g, "Continue a la izquierda");
51 | m = m.replace(/Continue right/g, "Continue a la derecha");
52 | m = m.replace(/Keep left at the fork/g, "Mantengase a la izquierda en el cruce");
53 | m = m.replace(/Keep right at the fork/g, "Mantengase a la derecha en el cruce");
54 | m = m.replace(/Continue slightly right/g, "Continue ligeramente a la derecha");
55 | m = m.replace(/Continue slightly left/g, "Continue ligeramente a la izquierda");
56 | m = m.replace(/Make a sharp left/g, "Curva brusca a la izquierda");
57 | m = m.replace(/Make a slight left/g, "Curva ligera a la izquierda");
58 | m = m.replace(/Make a slight right/g, "Curva ligera a la derecha");
59 | m = m.replace(/Continue sharp left/g, "Curva brusca a la izquierda");
60 | m = m.replace(/Continue sharp right/g, "Curva brusca a la derecha");
61 | m = m.replace(/Make a sharp right/g, "Curva brusca a la derecha");
62 | m = m.replace(/Continue straight/g, "Continue derecho");
63 | m = m.replace(/The right/g, "La derecha");
64 | m = m.replace(/The left/g, "La izquierda");
65 | m = m.replace(/\bContinue/g, "Continue");
66 | m = m.replace(/Make a U-turn/g, "Gire en U");
67 | m = m.replace(/Enter the roundabout/g, "Entre a la rotonda");
68 | m = m.replace(/and take the exit/g, "tome esa salida");
69 | m = m.replace(/You have arrived at your destination/g, "Ha llegado a su destino");
70 | return m;
71 | }
72 | };
73 |
--------------------------------------------------------------------------------
/API.md:
--------------------------------------------------------------------------------
1 | # Directions
2 |
3 | ## L.mapbox.directions(options)
4 |
5 | _Extends_: `L.Class`
6 |
7 | | Options | Type | Description |
8 | | ---- | ---- | ---- |
9 | | options | object | [Directions options](#directions-options) object |
10 |
11 | ## Directions options
12 |
13 | | Option | Type | Default | Description |
14 | | ------ | ---- | ------- | ----------- |
15 | | `accessToken` | String | `null` | Required unless `L.mapbox.accessToken` is set globally |
16 | | `profile` | String | `mapbox.driving` | Routing profile to use. Options: `mapbox.driving`, `mapbox.walking`, `mapbox.cycling` |
17 | | `units` | String | `imperial` | Measurement system to be used in navigation instructions. Options: `imperial`, `metric` |
18 |
19 | ## Directions events
20 |
21 | | Event | Content |
22 | | ----- | ------- |
23 | | `origin` | Fired when the origin is selected. |
24 | | `destination` | Fired when the destination is selected. |
25 | | `profile` | Fired when a profile is selected. |
26 | | `selectRoute` | Fired when a route is selected. |
27 | | `highlightRoute` | Fired when a route is highlighted. |
28 | | `highlightStep` | Fired when a step is highlighted. |
29 | | `load` | Fired when directions load. |
30 | | `error` | Fired when remote requests result in an error. |
31 |
32 | ### directions.getOrigin()
33 |
34 | Returns the origin of the current route.
35 |
36 | _Returns_: the origin
37 |
38 | ### directions.setOrigin()
39 |
40 | Sets the origin of the current route.
41 |
42 | _Returns_: `this`
43 |
44 | ### directions.getDestination()
45 |
46 | Returns the destination of the current route.
47 |
48 | _Returns_: the destination
49 |
50 | ### directions.setDestination()
51 |
52 | Sets the destination of the current route.
53 |
54 | _Returns_: `this`
55 |
56 | ### directions.queryable()
57 |
58 | _Returns_: `boolean`, whether both the destination and the origin are set properly
59 | and directions can be retrieved at this time.
60 |
61 | ### directions.query(opts, callback)
62 |
63 | After you've set an origin and destination, `query` fires the query to geocoding
64 | and sets results in the controller.
65 |
66 | Options is an optional options object, which can specify:
67 |
68 | * `proximity`: a L.LatLng object that is fed into the geocoder and biases
69 | matches around a point
70 |
71 | Callback is an optional callback that will be called with `(err, results)`
72 |
73 | _Returns_: `this`
74 |
75 | ### directions.addWaypoint(index, waypoint)
76 |
77 | Add a waypoint to the route at the given index. `waypoint` can be a GeoJSON Point Feature or a `L.LatLng`.
78 |
79 | _Returns_: `this`
80 |
81 | ### directions.removeWaypoint(index)
82 |
83 | Remove the waypoint at the given index from the route.
84 |
85 | _Returns_: `this`
86 |
87 | ### directions.setWaypoint(index, waypoint)
88 |
89 | Change the waypoint at the given index. `waypoint` can be a GeoJSON Point Feature or a `L.LatLng`.
90 |
91 | _Returns_: `this`
92 |
93 | ### directions.reverse()
94 |
95 | Swap the origin and destination.
96 |
97 | _Returns_: `this`
98 |
99 | ### directions.query(opts)
100 |
101 | Send a directions query request. `opts` can contain a `proximity` LatLng object for geocoding origin/destination/waypoint strings.
102 |
103 | _Returns_: `this`
104 |
105 | ## L.mapbox.directions.layer(directions, options)
106 |
107 | _Extends_: `L.LayerGroup`
108 |
109 | Create a new layer that displays a given set of directions
110 | on a map.
111 |
112 | | Options | Value | Description |
113 | | ---- | ---- | ---- |
114 | | options | object | [Layer options](#layer-options) object |
115 |
116 | ## Layer options
117 |
118 | | Option | Type | Default | Description |
119 | | ------ | ---- | ------- | ----------- |
120 | | `readonly` | Boolean | `false` | Optional. If set to `true` marker and linestring interaction is disabled. |
121 | | `routeStyle` | Object | `{color: '#3BB2D0', weight: 4, opacity: .75}` | [GeoJSON style](http://leafletjs.com/reference.html#geojson-style) to specify `color`, `weight` and `opacity` of route polyline. |
122 |
123 | ## L.mapbox.directions.inputControl
124 |
125 | ### inputControl.addTo(map)
126 |
127 | Add this control to a given map object.
128 |
129 | _Returns_: `this`
130 |
131 | ## L.mapbox.directions.errorsControl
132 |
133 | ### errorsControl.addTo(map)
134 |
135 | Add this control to a given map object.
136 |
137 | _Returns_: `this`
138 |
139 | ## L.mapbox.directions.routesControl
140 |
141 | ### routesControl.addTo(map)
142 |
143 | Add this control to a given map object.
144 |
145 | _Returns_: `this`
146 |
147 | ## L.mapbox.directions.instructionsControl
148 |
--------------------------------------------------------------------------------
/test/input_control.js:
--------------------------------------------------------------------------------
1 | var test = require('tape');
2 |
3 | test("Directions#inputControl", function (t) {
4 | var container, map, directions;
5 |
6 | function setup () {
7 | container = document.createElement('div');
8 | map = L.map(container).setView([0, 0], 0);
9 | directions = L.mapbox.directions({accessToken: 'key'});
10 | };
11 |
12 | t.test("on directions origin", function (u) {
13 | setup();
14 |
15 | u.test("sets origin value (query)", function (v) {
16 | L.mapbox.directions.inputControl(container, directions).addTo(map);
17 | directions.setOrigin('San Francisco');
18 | v.equal(container.querySelector('#mapbox-directions-origin-input').value, 'San Francisco');
19 | v.end();
20 | });
21 |
22 | u.test("sets origin value (coordinates)", function (v) {
23 | L.mapbox.directions.inputControl(container, directions).addTo(map);
24 | directions.setOrigin(L.latLng(1, 2));
25 | v.equal(container.querySelector('#mapbox-directions-origin-input').value, '2, 1');
26 | v.end();
27 | });
28 |
29 | u.test("rounds to a zoom-appropriate precision", function (v) {
30 | L.mapbox.directions.inputControl(container, directions).addTo(map);
31 | map.setZoom(3);
32 | directions.setOrigin(L.latLng(0.12345678, 0.12345678));
33 | v.equal(container.querySelector('#mapbox-directions-origin-input').value, '0.12, 0.12');
34 | v.end();
35 | });
36 |
37 | u.test("clears origin value", function (v) {
38 | L.mapbox.directions.inputControl(container, directions).addTo(map);
39 | directions.setOrigin(L.latLng(1, 2));
40 | directions.setOrigin(undefined);
41 | v.equal(container.querySelector('#mapbox-directions-origin-input').value, '');
42 | v.end();
43 | });
44 |
45 | u.end();
46 | });
47 |
48 | t.test("on directions destination", function (u) {
49 | setup();
50 |
51 | u.test("sets destination value (query)", function (v) {
52 | L.mapbox.directions.inputControl(container, directions).addTo(map);
53 | directions.setDestination('San Francisco');
54 | v.equal(container.querySelector('#mapbox-directions-destination-input').value, 'San Francisco');
55 | v.end();
56 | });
57 |
58 | u.test("sets destination value (coordinates)", function (v) {
59 | L.mapbox.directions.inputControl(container, directions).addTo(map);
60 | directions.setDestination(L.latLng(1, 2));
61 | v.equal(container.querySelector('#mapbox-directions-destination-input').value, '2, 1');
62 | v.end();
63 | });
64 |
65 | u.test("rounds to a zoom-appropriate precision", function (v) {
66 | L.mapbox.directions.inputControl(container, directions).addTo(map);
67 | map.setZoom(3);
68 | directions.setDestination(L.latLng(0.12345678, 0.12345678));
69 | v.equal(container.querySelector('#mapbox-directions-destination-input').value, '0.12, 0.12');
70 | v.end();
71 | });
72 |
73 | u.test("clears origin value", function (v) {
74 | L.mapbox.directions.inputControl(container, directions).addTo(map);
75 | directions.setDestination(L.latLng(1, 2));
76 | directions.setDestination(undefined);
77 | v.equal(container.querySelector('#mapbox-directions-destination-input').value, '');
78 | v.end();
79 | });
80 |
81 | u.end();
82 | });
83 |
84 | t.test("on directions profile", function (u) {
85 | setup();
86 |
87 | u.test("checks the appropriate input", function (v) {
88 | L.mapbox.directions.inputControl(container, directions).addTo(map);
89 | directions.setProfile('mapbox.walking');
90 | v.equal(container.querySelector('#mapbox-directions-profile-driving').checked, false);
91 | v.equal(container.querySelector('#mapbox-directions-profile-walking').checked, true);
92 | v.end();
93 | });
94 |
95 | u.end();
96 | });
97 |
98 | t.test("directions profile set on initialization", function(u) {
99 | setup();
100 |
101 | u.test("checks the appropriate input", function(v) {
102 | directions = L.mapbox.directions({accessToken: 'key', profile: 'mapbox.cycling'});
103 | L.mapbox.directions.inputControl(container, directions).addTo(map);
104 | v.equal(container.querySelector('#mapbox-directions-profile-walking').checked, false);
105 | v.equal(container.querySelector('#mapbox-directions-profile-cycling').checked, true);
106 | v.end();
107 | });
108 | });
109 |
110 | t.end();
111 | });
112 |
--------------------------------------------------------------------------------
/src/input_control.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var d3 = require('../lib/d3');
4 |
5 | module.exports = function (container, directions) {
6 | var control = {}, map;
7 | var origChange = false,
8 | destChange = false;
9 |
10 | control.addTo = function (_) {
11 | map = _;
12 | return control;
13 | };
14 |
15 | container = d3.select(L.DomUtil.get(container))
16 | .classed('mapbox-directions-inputs', true);
17 |
18 | var form = container.append('form')
19 | .on('keypress', function () {
20 | if (d3.event.keyCode === 13) {
21 | d3.event.preventDefault();
22 |
23 | if (origChange)
24 | directions.setOrigin(originInput.property('value'));
25 | if (destChange)
26 | directions.setDestination(destinationInput.property('value'));
27 |
28 | if (directions.queryable())
29 | directions.query({ proximity: map.getCenter() });
30 |
31 | origChange = false;
32 | destChange = false;
33 | }
34 | });
35 |
36 | var origin = form.append('div')
37 | .attr('class', 'mapbox-directions-origin');
38 |
39 | origin.append('label')
40 | .attr('class', 'mapbox-form-label')
41 | .on('click', function () {
42 | if (directions.getOrigin() instanceof L.LatLng) {
43 | map.panTo(directions.getOrigin());
44 | }
45 | })
46 | .append('span')
47 | .attr('class', 'mapbox-directions-icon mapbox-depart-icon');
48 |
49 | var originInput = origin.append('input')
50 | .attr('type', 'text')
51 | .attr('required', 'required')
52 | .attr('id', 'mapbox-directions-origin-input')
53 | .attr('placeholder', 'Start')
54 | .on('input', function() {
55 | if (!origChange) origChange = true;
56 | });
57 |
58 | origin.append('div')
59 | .attr('class', 'mapbox-directions-icon mapbox-close-icon')
60 | .attr('title', 'Clear value')
61 | .on('click', function () {
62 | directions.setOrigin(undefined);
63 | });
64 |
65 | form.append('span')
66 | .attr('class', 'mapbox-directions-icon mapbox-reverse-icon mapbox-directions-reverse-input')
67 | .attr('title', 'Reverse origin & destination')
68 | .on('click', function () {
69 | directions.reverse().query();
70 | });
71 |
72 | var destination = form.append('div')
73 | .attr('class', 'mapbox-directions-destination');
74 |
75 | destination.append('label')
76 | .attr('class', 'mapbox-form-label')
77 | .on('click', function () {
78 | if (directions.getDestination() instanceof L.LatLng) {
79 | map.panTo(directions.getDestination());
80 | }
81 | })
82 | .append('span')
83 | .attr('class', 'mapbox-directions-icon mapbox-arrive-icon');
84 |
85 | var destinationInput = destination.append('input')
86 | .attr('type', 'text')
87 | .attr('required', 'required')
88 | .attr('id', 'mapbox-directions-destination-input')
89 | .attr('placeholder', 'End')
90 | .on('input', function() {
91 | if (!destChange) destChange = true;
92 | });
93 |
94 | destination.append('div')
95 | .attr('class', 'mapbox-directions-icon mapbox-close-icon')
96 | .attr('title', 'Clear value')
97 | .on('click', function () {
98 | directions.setDestination(undefined);
99 | });
100 |
101 | var profile = form.append('div')
102 | .attr('class', 'mapbox-directions-profile');
103 |
104 | var profiles = profile.selectAll('span')
105 | .data([
106 | ['mapbox.driving', 'driving', 'Driving'],
107 | ['mapbox.walking', 'walking', 'Walking'],
108 | ['mapbox.cycling', 'cycling', 'Cycling']])
109 | .enter()
110 | .append('span');
111 |
112 | profiles.append('input')
113 | .attr('type', 'radio')
114 | .attr('name', 'profile')
115 | .attr('id', function (d) { return 'mapbox-directions-profile-' + d[1]; })
116 | .property('checked', function (d, i) {
117 | if (directions.options.profile) return directions.options.profile === d[0];
118 | else return i === 0;
119 | })
120 | .on('change', function (d) {
121 | directions.setProfile(d[0]).query();
122 | });
123 |
124 | profiles.append('label')
125 | .attr('for', function (d) { return 'mapbox-directions-profile-' + d[1]; })
126 | .text(function (d) { return d[2]; });
127 |
128 | function format(waypoint) {
129 | if (!waypoint) {
130 | return '';
131 | } else if (waypoint.properties.name) {
132 | return waypoint.properties.name;
133 | } else if (waypoint.geometry.coordinates) {
134 | var precision = Math.max(0, Math.ceil(Math.log(map.getZoom()) / Math.LN2));
135 | return waypoint.geometry.coordinates[0].toFixed(precision) + ', ' +
136 | waypoint.geometry.coordinates[1].toFixed(precision);
137 | } else {
138 | return waypoint.properties.query || '';
139 | }
140 | }
141 |
142 | directions
143 | .on('origin', function (e) {
144 | originInput.property('value', format(e.origin));
145 | })
146 | .on('destination', function (e) {
147 | destinationInput.property('value', format(e.destination));
148 | })
149 | .on('profile', function (e) {
150 | profiles.selectAll('input')
151 | .property('checked', function (d) { return d[0] === e.profile; });
152 | })
153 | .on('load', function (e) {
154 | originInput.property('value', format(e.origin));
155 | destinationInput.property('value', format(e.destination));
156 | });
157 |
158 | return control;
159 | };
160 |
--------------------------------------------------------------------------------
/test/layer.js:
--------------------------------------------------------------------------------
1 | var test = require('tape');
2 |
3 | test("Directions#layer", function(t) {
4 | var container, map, directions;
5 |
6 | function setup () {
7 | container = document.createElement('div');
8 | map = L.map(container).setView([0, 0], 0);
9 | directions = L.mapbox.directions();
10 | };
11 |
12 | t.test("on map click", function (u) {
13 | u.plan(2);
14 |
15 | u.test("first sets origin", function(v) {
16 | setup();
17 | v.plan(1);
18 |
19 | L.mapbox.directions.layer(directions).addTo(map);
20 | map.fire('click', {latlng: L.latLng(1, 2)});
21 | v.deepEqual(directions.getOrigin().geometry.coordinates, [2, 1]);
22 | });
23 |
24 | u.test("then sets destination and queries", function(v) {
25 | setup();
26 | v.plan(1);
27 |
28 | L.mapbox.directions.layer(directions).addTo(map);
29 | map.fire('click', {latlng: L.latLng(1, 2)});
30 | directions.query = function() {};
31 | map.fire('click', {latlng: L.latLng(3, 4)});
32 | v.deepEqual(directions.getDestination().geometry.coordinates, [4, 3]);
33 | });
34 | });
35 |
36 | t.test("on directions origin", function (u) {
37 | u.plan(2);
38 |
39 | u.test("sets origin marker", function(v) {
40 | setup();
41 | v.plan(1);
42 |
43 | var layer = L.mapbox.directions.layer(directions).addTo(map);
44 | directions.fire('origin', {origin: directions._normalizeWaypoint(L.latLng(1, 2))});
45 | v.deepEqual(layer.originMarker.getLatLng(), L.latLng(1, 2));
46 | });
47 |
48 | u.test("updates origin marker", function(v) {
49 | setup();
50 | v.plan(1);
51 |
52 | var layer = L.mapbox.directions.layer(directions).addTo(map);
53 | directions.fire('origin', {origin: directions._normalizeWaypoint(L.latLng(1, 2))});
54 | directions.fire('origin', {origin: directions._normalizeWaypoint(L.latLng(3, 4))});
55 | v.deepEqual(layer.originMarker.getLatLng(), L.latLng(3, 4));
56 | });
57 | });
58 |
59 | t.test("on directions destination", function (u) {
60 | u.plan(2);
61 |
62 | u.test("sets destination marker", function(v) {
63 | setup();
64 | v.plan(1);
65 |
66 | var layer = L.mapbox.directions.layer(directions).addTo(map);
67 | directions.fire('destination', {destination: directions._normalizeWaypoint(L.latLng(1, 2))});
68 | v.deepEqual(layer.destinationMarker.getLatLng(), L.latLng(1, 2));
69 | });
70 |
71 | u.test("updates destination marker", function(v) {
72 | setup();
73 | v.plan(1);
74 |
75 | var layer = L.mapbox.directions.layer(directions).addTo(map);
76 | directions.fire('destination', {destination: directions._normalizeWaypoint(L.latLng(1, 2))});
77 | directions.fire('destination', {destination: directions._normalizeWaypoint(L.latLng(3, 4))});
78 | v.deepEqual(layer.destinationMarker.getLatLng(), L.latLng(3, 4));
79 | });
80 | });
81 |
82 | t.test("on directions load", function (u) {
83 | u.plan(1);
84 |
85 | var response = {
86 | origin: {
87 | type: "Feature",
88 | geometry: {
89 | "type": "Point",
90 | "coordinates": [0, 0]
91 | },
92 | properties: {}
93 | },
94 | destination: {
95 | type: "Feature",
96 | geometry: {
97 | "type": "Point",
98 | "coordinates": [0, 0]
99 | },
100 | properties: {}
101 | },
102 | waypoints: [],
103 | routes: [{
104 | geometry: {type: "LineString", coordinates: []}
105 | }]
106 | };
107 |
108 | u.test("shows route", function(v) {
109 | setup();
110 | v.plan(2);
111 |
112 | var layer = L.mapbox.directions.layer(directions).addTo(map);
113 | directions.fire('load', response);
114 | v.ok(layer.routeLayer, "shows route");
115 | v.deepEqual(layer.routeLayer.options.style, {
116 | color: '#3BB2D0',
117 | weight: 4,
118 | opacity: 0.75
119 | }, "displays route with default style options");
120 | });
121 | });
122 |
123 | t.test("options param", function (u) {
124 | u.plan(2);
125 |
126 | var response = {
127 | origin: {
128 | type: "Feature",
129 | geometry: {
130 | "type": "Point",
131 | "coordinates": [0, 0]
132 | },
133 | properties: {}
134 | },
135 | destination: {
136 | type: "Feature",
137 | geometry: {
138 | "type": "Point",
139 | "coordinates": [0, 0]
140 | },
141 | properties: {}
142 | },
143 | waypoints: [],
144 | routes: [{
145 | geometry: {type: "LineString", coordinates: []}
146 | }]
147 | };
148 |
149 | u.test("map clicking disabled in readonly mode", function(v) {
150 | v.plan(1);
151 |
152 | setup();
153 | L.mapbox.directions.layer(directions, {readonly:true}).addTo(map);
154 | map.fire('click', {latlng: L.latLng(1, 2)});
155 | v.equal(directions.getOrigin(), undefined);
156 | });
157 |
158 | u.test("shows route with custom style", function(v) {
159 | setup();
160 | v.plan(2);
161 |
162 | var routeStyle = {
163 | color: '#f00',
164 | weight: 2,
165 | opacity: 0.5
166 | };
167 | var layer = L.mapbox.directions.layer(directions, {routeStyle: routeStyle}).addTo(map);
168 | directions.fire('load', response);
169 | v.ok(layer.routeLayer, "shows route");
170 | v.deepEqual(layer.routeLayer.options.style, routeStyle, "displays route with custom style");
171 | });
172 | });
173 |
174 | t.end();
175 | });
176 |
--------------------------------------------------------------------------------
/dist/mapbox.directions.css:
--------------------------------------------------------------------------------
1 | /* Basics */
2 |
3 | .mapbox-directions-inputs,
4 | .mapbox-directions-errors,
5 | .mapbox-directions-routes,
6 | .mapbox-directions-instructions {
7 | font:15px/20px 'Helvetica Neue', Arial, Helvetica, sans-serif;
8 | }
9 |
10 | .mapbox-directions-inputs,
11 | .mapbox-directions-inputs *,
12 | .mapbox-directions-errors,
13 | .mapbox-directions-errors *,
14 | .mapbox-directions-routes,
15 | .mapbox-directions-routes *,
16 | .mapbox-directions-instructions,
17 | .mapbox-directions-instructions * {
18 | -webkit-box-sizing: border-box;
19 | -moz-box-sizing: border-box;
20 | box-sizing: border-box;
21 | }
22 |
23 | /* Inputs */
24 |
25 | .mapbox-directions-origin,
26 | .mapbox-directions-destination {
27 | background-color: white;
28 | position: relative;
29 | }
30 |
31 | .mapbox-form-label {
32 | cursor: pointer;
33 | position: absolute;
34 | left: 0;
35 | top: 0;
36 | background: #444;
37 | color: rgba(0,0,0,.75);
38 | font-weight: bold;
39 | text-align: center;
40 | padding: 10px;
41 | line-height: 20px;
42 | font-size: 12px;
43 | }
44 |
45 | .mapbox-directions-origin .mapbox-form-label {
46 | background-color: #3bb2d0;
47 | }
48 |
49 | .mapbox-directions-inputs input {
50 | font-size: 12px;
51 | width: 100%;
52 | border: 0;
53 | background-color: transparent;
54 | height: 40px;
55 | margin: 0;
56 | color: rgba(0,0,0,.5);
57 | padding: 10px 10px 10px 50px;
58 | }
59 |
60 | .mapbox-directions-inputs input:focus {
61 | color: rgba(0,0,0,.75);
62 | outline:0;
63 | box-shadow: none;
64 | outline: thin dotted\8;
65 | }
66 |
67 | .mapbox-directions-destination input {
68 | border-top: 1px solid rgba(0,0,0,.1);
69 | }
70 |
71 | .mapbox-directions-reverse-input {
72 | position: absolute;
73 | z-index: 10;
74 | background: white;
75 | left: 50px;
76 | top: 30px;
77 | cursor: pointer;
78 | }
79 |
80 | .mapbox-directions-inputs .mapbox-close-icon {
81 | opacity: .5;
82 | z-index: 2;
83 | position: absolute;
84 | right: 5px;
85 | top: 10px;
86 | cursor: pointer;
87 | }
88 |
89 | input:not(:valid) + .mapbox-close-icon {
90 | display: none;
91 | }
92 |
93 | .mapbox-close-icon:hover {
94 | opacity: .75;
95 | }
96 |
97 | .mapbox-directions-profile {
98 | margin-top: 5px;
99 | margin-bottom: 5px;
100 | padding: 2px;
101 | border-radius: 15px;
102 | vertical-align: middle;
103 | background: rgba(0,0,0,.1);
104 | }
105 |
106 | .mapbox-directions-profile label {
107 | cursor: pointer;
108 | vertical-align: top;
109 | display: inline-block;
110 | border-radius: 16px;
111 | padding: 3px 5px;
112 | font-size: 12px;
113 | color: rgba(0,0,0,0.5);
114 | line-height: 20px;
115 | text-align: center;
116 | width: 33.33%;
117 | }
118 |
119 | .mapbox-directions-profile input[type=radio] {
120 | display: none;
121 | }
122 |
123 | .mapbox-directions-profile input[type=radio]:checked + label {
124 | background: white;
125 | color: rgba(0,0,0,.5);
126 | }
127 |
128 | /* Errors */
129 |
130 | .mapbox-directions-error {
131 | color: white;
132 | display: inline-block;
133 | padding: 0 5px;
134 | }
135 |
136 | /* Routes */
137 |
138 | .mapbox-directions-routes ul {
139 | list-style: none;
140 | margin: 0;
141 | padding: 10px 10px 0 10px;
142 | border-bottom: 1px solid rgba(255,255,255,.25);
143 | }
144 |
145 | .mapbox-directions-routes li {
146 | font-size: 12px;
147 | padding: 10px 10px 10px 80px;
148 | display: block;
149 | position: relative;
150 | cursor: pointer;
151 | color: rgba(255,255,255,.5);
152 | min-height: 60px;
153 | }
154 |
155 | .mapbox-directions-routes li:hover,
156 | .mapbox-directions-routes .mapbox-directions-route-active {
157 | color: white;
158 | }
159 |
160 | .mapbox-directions-route-heading {
161 | position: absolute;
162 | left: 10px;
163 | top: 10px;
164 | }
165 |
166 | .mapbox-directions-route-summary {
167 | display: none;
168 | }
169 |
170 | .mapbox-directions-route-active .mapbox-directions-route-summary {
171 | display: block;
172 | }
173 |
174 | .mapbox-directions-route-details {
175 | font-size: 12px;
176 | color: rgba(255,255,255,.5);
177 | }
178 |
179 | /* Instructions */
180 |
181 | .mapbox-directions-steps {
182 | position: relative;
183 | list-style: none;
184 | margin: 0;
185 | padding: 0;
186 | }
187 |
188 | .mapbox-directions-step {
189 | position: relative;
190 | color: rgba(255,255,255,.75);
191 | cursor: pointer;
192 | padding: 20px 20px 20px 40px;
193 | font-size: 20px;
194 | line-height: 25px;
195 | }
196 |
197 | .mapbox-directions-step-distance {
198 | color: rgba(255,255,255,.5);
199 | position: absolute;
200 | padding: 5px 10px;
201 | font-size: 12px;
202 | left: 30px;
203 | bottom: -15px;
204 | }
205 |
206 | .mapbox-directions-step:hover {
207 | color: white;
208 | }
209 |
210 | .mapbox-directions-step:after {
211 | content: "";
212 | position: absolute;
213 | top: 50px;
214 | bottom: -20px;
215 | border-left: 2px dotted rgba(255,255,255,.2);
216 | left: 20px;
217 | }
218 |
219 | .mapbox-directions-step:last-child:after,
220 | .mapbox-directions-step:last-child .mapbox-directions-step-distance {
221 | display: none;
222 | }
223 |
224 | /* icons */
225 |
226 | .mapbox-directions-icon {
227 | background-image: url('mapbox.directions.png');
228 | -webkit-background-size: 280px 20px;
229 | background-size: 280px 20px;
230 | background-repeat: no-repeat;
231 | margin: 0;
232 | content: '';
233 | display: inline-block;
234 | vertical-align: top;
235 | width: 20px;
236 | height: 20px;
237 | }
238 |
239 | .mapbox-directions-instructions .mapbox-directions-icon {
240 | position: absolute;
241 | left: 10px;
242 | top: 25px;
243 | margin: auto;
244 | }
245 |
246 | .mapbox-continue-icon { background-position: 0 0; }
247 | .mapbox-sharp-right-icon { background-position: -20px 0; }
248 | .mapbox-turn-right-icon { background-position: -40px 0; }
249 | .mapbox-bear-right-icon { background-position: -60px 0; }
250 | .mapbox-u-turn-icon { background-position: -80px 0; }
251 | .mapbox-sharp-left-icon { background-position: -100px 0; }
252 | .mapbox-turn-left-icon { background-position: -120px 0; }
253 | .mapbox-bear-left-icon { background-position: -140px 0; }
254 | .mapbox-depart-icon { background-position: -160px 0; }
255 | .mapbox-enter-roundabout-icon { background-position: -180px 0; }
256 | .mapbox-arrive-icon { background-position: -200px 0; }
257 | .mapbox-close-icon { background-position: -220px 0; }
258 | .mapbox-reverse-icon { background-position: -240px 0; }
259 | .mapbox-error-icon { background-position: -260px 0; }
260 |
261 | .mapbox-marker-drag-icon {
262 | display: block;
263 | background-color: #444;
264 | border-radius: 50%;
265 | box-shadow: 0 0 5px 0 rgba(0,0,0,0.5);
266 | }
267 |
268 | .mapbox-marker-drag-icon-step {
269 | background-color: #3BB2D0;
270 | }
271 |
--------------------------------------------------------------------------------
/src/directions.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var request = require('./request'),
4 | polyline = require('polyline'),
5 | queue = require('queue-async');
6 |
7 | var Directions = L.Class.extend({
8 | includes: [L.Mixin.Events],
9 |
10 | options: {
11 | units: 'imperial',
12 | language: 'en'
13 | },
14 |
15 | statics: {
16 | URL_TEMPLATE: 'https://api.tiles.mapbox.com/v4/directions/{profile}/{waypoints}.json?instructions=html&geometry=polyline&access_token={token}',
17 | GEOCODER_TEMPLATE: 'https://api.tiles.mapbox.com/v4/geocode/mapbox.places/{query}.json?proximity={proximity}&access_token={token}'
18 | },
19 |
20 | initialize: function(options) {
21 | L.setOptions(this, options);
22 | this._waypoints = [];
23 | },
24 |
25 | getOrigin: function () {
26 | return this.origin;
27 | },
28 |
29 | getDestination: function () {
30 | return this.destination;
31 | },
32 |
33 | setOrigin: function (origin) {
34 | origin = this._normalizeWaypoint(origin);
35 |
36 | this.origin = origin;
37 | this.fire('origin', {origin: origin});
38 |
39 | if (!origin) {
40 | this._unload();
41 | }
42 |
43 | return this;
44 | },
45 |
46 | setDestination: function (destination) {
47 | destination = this._normalizeWaypoint(destination);
48 |
49 | this.destination = destination;
50 | this.fire('destination', {destination: destination});
51 |
52 | if (!destination) {
53 | this._unload();
54 | }
55 |
56 | return this;
57 | },
58 |
59 | getProfile: function() {
60 | return this.profile || this.options.profile || 'mapbox.driving';
61 | },
62 |
63 | setProfile: function (profile) {
64 | this.profile = profile;
65 | this.fire('profile', {profile: profile});
66 | return this;
67 | },
68 |
69 | getWaypoints: function() {
70 | return this._waypoints;
71 | },
72 |
73 | setWaypoints: function (waypoints) {
74 | this._waypoints = waypoints.map(this._normalizeWaypoint);
75 | return this;
76 | },
77 |
78 | addWaypoint: function (index, waypoint) {
79 | this._waypoints.splice(index, 0, this._normalizeWaypoint(waypoint));
80 | return this;
81 | },
82 |
83 | removeWaypoint: function (index) {
84 | this._waypoints.splice(index, 1);
85 | return this;
86 | },
87 |
88 | setWaypoint: function (index, waypoint) {
89 | this._waypoints[index] = this._normalizeWaypoint(waypoint);
90 | return this;
91 | },
92 |
93 | reverse: function () {
94 | var o = this.origin,
95 | d = this.destination;
96 |
97 | this.origin = d;
98 | this.destination = o;
99 | this._waypoints.reverse();
100 |
101 | this.fire('origin', {origin: this.origin})
102 | .fire('destination', {destination: this.destination});
103 |
104 | return this;
105 | },
106 |
107 | selectRoute: function (route) {
108 | this.fire('selectRoute', {route: route});
109 | },
110 |
111 | highlightRoute: function (route) {
112 | this.fire('highlightRoute', {route: route});
113 | },
114 |
115 | highlightStep: function (step) {
116 | this.fire('highlightStep', {step: step});
117 | },
118 |
119 | queryURL: function () {
120 | var template = Directions.URL_TEMPLATE,
121 | token = this.options.accessToken || L.mapbox.accessToken,
122 | profile = this.getProfile(),
123 | points = [this.origin].concat(this._waypoints).concat([this.destination]).map(function (point) {
124 | return point.geometry.coordinates;
125 | }).join(';');
126 |
127 | if (L.mapbox.feedback) {
128 | L.mapbox.feedback.record({directions: profile + ';' + points});
129 | }
130 |
131 | return L.Util.template(template, {
132 | token: token,
133 | profile: profile,
134 | waypoints: points
135 | });
136 | },
137 |
138 | queryable: function () {
139 | return this.getOrigin() && this.getDestination();
140 | },
141 |
142 | query: function (opts, callback) {
143 | if (!opts) opts = {};
144 | if (!this.queryable()) return this;
145 | if (callback === undefined) callback = function() {};
146 |
147 | if (this._query) {
148 | this._query.abort();
149 | }
150 |
151 | if (this._requests && this._requests.length) this._requests.forEach(function(request) {
152 | request.abort();
153 | });
154 | this._requests = [];
155 |
156 | var q = queue();
157 |
158 | var pts = [this.origin, this.destination].concat(this._waypoints);
159 | for (var i in pts) {
160 | if (!pts[i].geometry.coordinates) {
161 | q.defer(L.bind(this._geocode, this), pts[i], opts.proximity);
162 | }
163 | }
164 |
165 | q.await(L.bind(function(err) {
166 | if (err) {
167 | return this.fire('error', {error: err.message});
168 | }
169 |
170 | this._query = request(this.queryURL(), L.bind(function (err, resp) {
171 | this._query = null;
172 |
173 | if (err) {
174 | callback(err);
175 | return this.fire('error', {error: err.message});
176 | }
177 |
178 | this.directions = resp;
179 | this.directions.routes.forEach(function (route) {
180 | route.geometry = {
181 | type: "LineString",
182 | coordinates: polyline.decode(route.geometry, 6).map(function (c) { return c.reverse(); })
183 | };
184 | });
185 |
186 | if (!this.origin.properties.name) {
187 | this.origin = this.directions.origin;
188 | } else {
189 | this.directions.origin = this.origin;
190 | }
191 |
192 | if (!this.destination.properties.name) {
193 | this.destination = this.directions.destination;
194 | } else {
195 | this.directions.destination = this.destination;
196 | }
197 |
198 | callback(null, this.directions);
199 |
200 | this.fire('load', this.directions);
201 | }, this), this);
202 | }, this));
203 |
204 | return this;
205 | },
206 |
207 | _geocode: function(waypoint, proximity, cb) {
208 | if (!this._requests) this._requests = [];
209 | this._requests.push(request(L.Util.template(Directions.GEOCODER_TEMPLATE, {
210 | query: waypoint.properties.query,
211 | token: this.options.accessToken || L.mapbox.accessToken,
212 | proximity: proximity ? [proximity.lng, proximity.lat].join(',') : ''
213 | }), L.bind(function (err, resp) {
214 | if (err) {
215 | return cb(err);
216 | }
217 |
218 | if (!resp.features || !resp.features.length) {
219 | return cb(new Error("No results found for query " + waypoint.properties.query));
220 | }
221 |
222 | waypoint.geometry.coordinates = resp.features[0].center;
223 | waypoint.properties.name = resp.features[0].place_name;
224 |
225 | return cb();
226 | }, this)));
227 | },
228 |
229 | _unload: function () {
230 | this._waypoints = [];
231 | delete this.directions;
232 | this.fire('unload');
233 | },
234 |
235 | _normalizeWaypoint: function (waypoint) {
236 | if (!waypoint || waypoint.type === 'Feature') {
237 | return waypoint;
238 | }
239 |
240 | var coordinates,
241 | properties = {};
242 |
243 | if (waypoint instanceof L.LatLng) {
244 | waypoint = waypoint.wrap();
245 | coordinates = properties.query = [waypoint.lng, waypoint.lat];
246 | } else if (typeof waypoint === 'string') {
247 | properties.query = waypoint;
248 | }
249 |
250 | return {
251 | type: 'Feature',
252 | geometry: {
253 | type: 'Point',
254 | coordinates: coordinates
255 | },
256 | properties: properties
257 | };
258 | }
259 | });
260 |
261 | module.exports = function(options) {
262 | return new Directions(options);
263 | };
264 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Mapbox Directions
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
128 |
129 |
130 |
131 |
132 |
133 |
137 |
138 |
139 |
140 |
141 |
257 |
258 |
259 |
260 |
--------------------------------------------------------------------------------
/src/layer.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var debounce = require('debounce');
4 |
5 | var Layer = L.LayerGroup.extend({
6 | options: {
7 | readonly: false,
8 | routeStyle: {
9 | 'color': '#3BB2D0',
10 | 'weight': 4,
11 | 'opacity': 0.75
12 | }
13 | },
14 |
15 | initialize: function(directions, options) {
16 | L.setOptions(this, options);
17 | this._directions = directions || new L.Directions();
18 | L.LayerGroup.prototype.initialize.apply(this);
19 |
20 | this._drag = debounce(L.bind(this._drag, this), 100);
21 |
22 | this.originMarker = L.marker([0, 0], {
23 | draggable: !this.options.readonly,
24 | icon: L.mapbox.marker.icon({
25 | 'marker-size': 'medium',
26 | 'marker-color': '#3BB2D0',
27 | 'marker-symbol': 'a'
28 | })
29 | }).on('drag', this._drag, this);
30 |
31 | this.destinationMarker = L.marker([0, 0], {
32 | draggable: !this.options.readonly,
33 | icon: L.mapbox.marker.icon({
34 | 'marker-size': 'medium',
35 | 'marker-color': '#444',
36 | 'marker-symbol': 'b'
37 | })
38 | }).on('drag', this._drag, this);
39 |
40 | this.stepMarker = L.marker([0, 0], {
41 | icon: L.divIcon({
42 | className: 'mapbox-marker-drag-icon mapbox-marker-drag-icon-step',
43 | iconSize: new L.Point(12, 12)
44 | })
45 | });
46 |
47 | this.dragMarker = L.marker([0, 0], {
48 | draggable: !this.options.readonly,
49 | icon: this._waypointIcon()
50 | });
51 |
52 | this.dragMarker
53 | .on('dragstart', this._dragStart, this)
54 | .on('drag', this._drag, this)
55 | .on('dragend', this._dragEnd, this);
56 |
57 | this.routeLayer = L.geoJson(null, {style: this.options.routeStyle});
58 | this.routeHighlightLayer = L.geoJson(null, {style: this.options.routeStyle});
59 |
60 | this.waypointMarkers = [];
61 | },
62 |
63 | onAdd: function() {
64 | L.LayerGroup.prototype.onAdd.apply(this, arguments);
65 |
66 | if (!this.options.readonly) {
67 | this._map
68 | .on('click', this._click, this)
69 | .on('mousemove', this._mousemove, this);
70 | }
71 |
72 | this._directions
73 | .on('origin', this._origin, this)
74 | .on('destination', this._destination, this)
75 | .on('load', this._load, this)
76 | .on('unload', this._unload, this)
77 | .on('selectRoute', this._selectRoute, this)
78 | .on('highlightRoute', this._highlightRoute, this)
79 | .on('highlightStep', this._highlightStep, this);
80 | },
81 |
82 | onRemove: function() {
83 | this._directions
84 | .off('origin', this._origin, this)
85 | .off('destination', this._destination, this)
86 | .off('load', this._load, this)
87 | .off('unload', this._unload, this)
88 | .off('selectRoute', this._selectRoute, this)
89 | .off('highlightRoute', this._highlightRoute, this)
90 | .off('highlightStep', this._highlightStep, this);
91 |
92 | this._map
93 | .off('click', this._click, this)
94 | .off('mousemove', this._mousemove, this);
95 |
96 | L.LayerGroup.prototype.onRemove.apply(this, arguments);
97 | },
98 |
99 | _click: function(e) {
100 | if (!this._directions.getOrigin()) {
101 | this._directions.setOrigin(e.latlng);
102 | } else if (!this._directions.getDestination()) {
103 | this._directions.setDestination(e.latlng);
104 | }
105 |
106 | if (this._directions.queryable()) {
107 | this._directions.query();
108 | }
109 | },
110 |
111 | _mousemove: function(e) {
112 | if (!this.routeLayer || !this.hasLayer(this.routeLayer) || this._currentWaypoint !== undefined) {
113 | return;
114 | }
115 |
116 | var p = this._routePolyline().closestLayerPoint(e.layerPoint);
117 |
118 | if (!p || p.distance > 15) {
119 | return this.removeLayer(this.dragMarker);
120 | }
121 |
122 | var m = this._map.project(e.latlng),
123 | o = this._map.project(this.originMarker.getLatLng()),
124 | d = this._map.project(this.destinationMarker.getLatLng());
125 |
126 | if (o.distanceTo(m) < 15 || d.distanceTo(m) < 15) {
127 | return this.removeLayer(this.dragMarker);
128 | }
129 |
130 | for (var i = 0; i < this.waypointMarkers.length; i++) {
131 | var w = this._map.project(this.waypointMarkers[i].getLatLng());
132 | if (i !== this._currentWaypoint && w.distanceTo(m) < 15) {
133 | return this.removeLayer(this.dragMarker);
134 | }
135 | }
136 |
137 | this.dragMarker.setLatLng(this._map.layerPointToLatLng(p));
138 | this.addLayer(this.dragMarker);
139 | },
140 |
141 | _origin: function(e) {
142 | if (e.origin && e.origin.geometry.coordinates) {
143 | this.originMarker.setLatLng(L.GeoJSON.coordsToLatLng(e.origin.geometry.coordinates));
144 | this.addLayer(this.originMarker);
145 | } else {
146 | this.removeLayer(this.originMarker);
147 | }
148 | },
149 |
150 | _destination: function(e) {
151 | if (e.destination && e.destination.geometry.coordinates) {
152 | this.destinationMarker.setLatLng(L.GeoJSON.coordsToLatLng(e.destination.geometry.coordinates));
153 | this.addLayer(this.destinationMarker);
154 | } else {
155 | this.removeLayer(this.destinationMarker);
156 | }
157 | },
158 |
159 | _dragStart: function(e) {
160 | if (e.target === this.dragMarker) {
161 | this._currentWaypoint = this._findWaypointIndex(e.target.getLatLng());
162 | this._directions.addWaypoint(this._currentWaypoint, e.target.getLatLng());
163 | } else {
164 | this._currentWaypoint = this.waypointMarkers.indexOf(e.target);
165 | }
166 | },
167 |
168 | _drag: function(e) {
169 | var latLng = e.target.getLatLng();
170 |
171 | if (e.target === this.originMarker) {
172 | this._directions.setOrigin(latLng);
173 | } else if (e.target === this.destinationMarker) {
174 | this._directions.setDestination(latLng);
175 | } else {
176 | this._directions.setWaypoint(this._currentWaypoint, latLng);
177 | }
178 |
179 | if (this._directions.queryable()) {
180 | this._directions.query();
181 | }
182 | },
183 |
184 | _dragEnd: function() {
185 | this._currentWaypoint = undefined;
186 | },
187 |
188 | _removeWaypoint: function(e) {
189 | this._directions.removeWaypoint(this.waypointMarkers.indexOf(e.target)).query();
190 | },
191 |
192 | _load: function(e) {
193 | this._origin(e);
194 | this._destination(e);
195 |
196 | function waypointLatLng(i) {
197 | return L.GeoJSON.coordsToLatLng(e.waypoints[i].geometry.coordinates);
198 | }
199 |
200 | var l = Math.min(this.waypointMarkers.length, e.waypoints.length),
201 | i = 0;
202 |
203 | // Update existing
204 | for (; i < l; i++) {
205 | this.waypointMarkers[i].setLatLng(waypointLatLng(i));
206 | }
207 |
208 | // Add new
209 | for (; i < e.waypoints.length; i++) {
210 | var waypointMarker = L.marker(waypointLatLng(i), {
211 | draggable: !this.options.readonly,
212 | icon: this._waypointIcon()
213 | });
214 |
215 | waypointMarker
216 | .on('dragstart', this._dragStart, this)
217 | .on('drag', this._drag, this)
218 | .on('dragend', this._dragEnd, this);
219 |
220 | if(!this.options.readonly){
221 | waypointMarker.on('click', this._removeWaypoint, this);
222 | }
223 |
224 | this.waypointMarkers.push(waypointMarker);
225 | this.addLayer(waypointMarker);
226 | }
227 |
228 | // Remove old
229 | for (; i < this.waypointMarkers.length; i++) {
230 | this.removeLayer(this.waypointMarkers[i]);
231 | }
232 |
233 | this.waypointMarkers.length = e.waypoints.length;
234 | },
235 |
236 | _unload: function() {
237 | this.removeLayer(this.routeLayer);
238 | for (var i = 0; i < this.waypointMarkers.length; i++) {
239 | this.removeLayer(this.waypointMarkers[i]);
240 | }
241 | },
242 |
243 | _selectRoute: function(e) {
244 | this.routeLayer
245 | .clearLayers()
246 | .addData(e.route.geometry);
247 | this.addLayer(this.routeLayer);
248 | },
249 |
250 | _highlightRoute: function(e) {
251 | if (e.route) {
252 | this.routeHighlightLayer
253 | .clearLayers()
254 | .addData(e.route.geometry);
255 | this.addLayer(this.routeHighlightLayer);
256 | } else {
257 | this.removeLayer(this.routeHighlightLayer);
258 | }
259 | },
260 |
261 | _highlightStep: function(e) {
262 | if (e.step) {
263 | this.stepMarker.setLatLng(L.GeoJSON.coordsToLatLng(e.step.maneuver.location.coordinates));
264 | this.addLayer(this.stepMarker);
265 | } else {
266 | this.removeLayer(this.stepMarker);
267 | }
268 | },
269 |
270 | _routePolyline: function() {
271 | return this.routeLayer.getLayers()[0];
272 | },
273 |
274 | _findWaypointIndex: function(latLng) {
275 | var segment = this._findNearestRouteSegment(latLng);
276 |
277 | for (var i = 0; i < this.waypointMarkers.length; i++) {
278 | var s = this._findNearestRouteSegment(this.waypointMarkers[i].getLatLng());
279 | if (s > segment) {
280 | return i;
281 | }
282 | }
283 |
284 | return this.waypointMarkers.length;
285 | },
286 |
287 | _findNearestRouteSegment: function(latLng) {
288 | var min = Infinity,
289 | index,
290 | p = this._map.latLngToLayerPoint(latLng),
291 | positions = this._routePolyline()._originalPoints;
292 |
293 | for (var i = 1; i < positions.length; i++) {
294 | var d = L.LineUtil._sqClosestPointOnSegment(p, positions[i - 1], positions[i], true);
295 | if (d < min) {
296 | min = d;
297 | index = i;
298 | }
299 | }
300 |
301 | return index;
302 | },
303 |
304 | _waypointIcon: function() {
305 | return L.divIcon({
306 | className: 'mapbox-marker-drag-icon',
307 | iconSize: new L.Point(12, 12)
308 | });
309 | }
310 | });
311 |
312 | module.exports = function(directions, options) {
313 | return new Layer(directions, options);
314 | };
315 |
--------------------------------------------------------------------------------
/dist/mapbox.directions.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
222 |
--------------------------------------------------------------------------------
/test/directions.js:
--------------------------------------------------------------------------------
1 | var sinon = require('sinon');
2 | var test = require('tape');
3 |
4 | test("Directions", function(t) {
5 | t.test("#setOrigin", function(u) {
6 | u.plan(6);
7 |
8 | u.test("normalizes latLng", function(v) {
9 | var directions = L.mapbox.directions({accessToken: 'key'});
10 | directions.setOrigin(L.latLng(1, 2));
11 | v.deepEqual(directions.getOrigin().geometry.coordinates, [2, 1]);
12 | v.end();
13 | });
14 |
15 | u.test("wraps latLng", function (v) {
16 | var directions = L.mapbox.directions({accessToken: 'key'});
17 | directions.setOrigin(L.latLng(0, 190));
18 | v.deepEqual(directions.getOrigin().geometry.coordinates, [-170, 0]);
19 | v.end();
20 | });
21 |
22 | u.test("normalizes query string", function (v) {
23 | var directions = L.mapbox.directions({accessToken: 'key'});
24 | directions.setOrigin('San Francisco');
25 | v.equal(directions.getOrigin().properties.query, 'San Francisco');
26 | v.end();
27 | });
28 |
29 | u.test("fires event", function (v) {
30 | var directions = L.mapbox.directions({accessToken: 'key'});
31 | directions.on('origin', function (e) {
32 | v.deepEqual(e.origin.geometry.coordinates, [2, 1]);
33 | v.end();
34 | });
35 | directions.setOrigin(L.latLng(1, 2));
36 | });
37 |
38 | u.test("fires unload on falsy inputs", function (v) {
39 | var directions = L.mapbox.directions({accessToken: 'key'});
40 | directions.on('unload', function() { v.end(); });
41 | directions.setOrigin(L.latLng(1, 2));
42 | directions.setOrigin(undefined);
43 | });
44 |
45 | u.test("returns this", function (v) {
46 | var directions = L.mapbox.directions({accessToken: 'key'});
47 | v.deepEqual(directions.setOrigin(L.latLng(1, 2)), directions);
48 | v.end();
49 | });
50 | });
51 |
52 | t.test("#setDestination", function (u) {
53 | u.plan(6);
54 |
55 | u.test("normalizes latLng", function (v) {
56 | var directions = L.mapbox.directions({accessToken: 'key'});
57 | directions.setDestination(L.latLng(1, 2));
58 | v.deepEqual(directions.getDestination().geometry.coordinates, [2, 1]);
59 | v.end();
60 | });
61 |
62 | u.test("wraps latLng", function (v) {
63 | var directions = L.mapbox.directions({accessToken: 'key'});
64 | directions.setDestination(L.latLng(0, 190));
65 | v.deepEqual(directions.getDestination().geometry.coordinates, [-170, 0]);
66 | v.end();
67 | });
68 |
69 | u.test("normalizes query string", function (v) {
70 | var directions = L.mapbox.directions({accessToken: 'key'});
71 | directions.setDestination('San Francisco');
72 | v.equal(directions.getDestination().properties.query, 'San Francisco');
73 | v.end();
74 | });
75 |
76 | u.test("fires event", function (v) {
77 | var directions = L.mapbox.directions({accessToken: 'key'});
78 | directions.on('destination', function (e) {
79 | v.deepEqual(e.destination.geometry.coordinates, [2, 1]);
80 | v.end();
81 | });
82 | directions.setDestination(L.latLng(1, 2));
83 | });
84 |
85 | u.test("fires unload on falsy inputs", function (v) {
86 | var directions = L.mapbox.directions({accessToken: 'key'});
87 | directions.on('unload', function() { v.end(); });
88 | directions.setDestination(L.latLng(1, 2));
89 | directions.setDestination(undefined);
90 | });
91 |
92 | u.test("returns this", function (v) {
93 | var directions = L.mapbox.directions({accessToken: 'key'});
94 | v.skip(directions.setDestination(L.latLng(1, 2)), directions);
95 | v.end();
96 | });
97 | });
98 |
99 | t.test("#setProfile", function (u) {
100 | u.plan(2);
101 |
102 | u.test("fires event", function (v) {
103 | var directions = L.mapbox.directions({accessToken: 'key'});
104 | directions.on('profile', function (e) {
105 | v.equal(e.profile, 'mapbox.walking');
106 | v.end();
107 | });
108 | directions.setProfile('mapbox.walking');
109 | });
110 |
111 | u.test("returns this", function (v) {
112 | var directions = L.mapbox.directions({accessToken: 'key'});
113 | v.deepEqual(directions.setProfile('mapbox.walking'), directions);
114 | v.end();
115 | });
116 | });
117 |
118 | t.test("reverse", function (u) {
119 | u.plan(3);
120 |
121 | var a = {
122 | type: 'Feature',
123 | geometry: {
124 | type: 'Point',
125 | coordinates: [1, 2]
126 | },
127 | properties: {}
128 | }, b = {
129 | type: 'Feature',
130 | geometry: {
131 | type: 'Point',
132 | coordinates: [3, 4]
133 | },
134 | properties: {}
135 | };
136 |
137 | u.test("swaps origin and destination", function (v) {
138 | var directions = L.mapbox.directions({accessToken: 'key'});
139 | directions.setOrigin(a);
140 | directions.setDestination(b);
141 | directions.reverse();
142 | v.deepEqual(directions.getOrigin(), b);
143 | v.deepEqual(directions.getDestination(), a);
144 | v.end();
145 | });
146 |
147 | u.test("fires events", function (v) {
148 | var directions = L.mapbox.directions({accessToken: 'key'});
149 | directions.setOrigin(a);
150 | directions.setDestination(b);
151 |
152 | directions.on('origin', function (e) {
153 | v.deepEqual(e.origin, b);
154 | });
155 |
156 | directions.on('destination', function (e) {
157 | v.deepEqual(e.destination, a);
158 | v.end();
159 | });
160 |
161 | directions.reverse();
162 | });
163 |
164 | u.test("returns this", function (v) {
165 | var directions = L.mapbox.directions({accessToken: 'key'});
166 | v.deepEqual(directions.reverse(), directions);
167 | v.end();
168 | });
169 | });
170 |
171 | t.test("queryURL", function (u) {
172 | u.plan(3);
173 |
174 | u.test("constructs a URL with origin and destination", function (v) {
175 | var directions = L.mapbox.directions({accessToken: 'key'});
176 | directions.setOrigin(L.latLng(1, 2)).setDestination(L.latLng(3, 4));
177 | v.equal(directions.queryURL(), 'https://api.tiles.mapbox.com/v4/directions/mapbox.driving/2,1;4,3.json?instructions=html&geometry=polyline&access_token=key');
178 | v.end();
179 | });
180 |
181 | u.test("wraps coordinates", function (v) {
182 | var directions = L.mapbox.directions({accessToken: 'key'});
183 | directions.setOrigin(L.latLng(0, 190)).setDestination(L.latLng(0, -195));
184 | v.equal(directions.queryURL(), 'https://api.tiles.mapbox.com/v4/directions/mapbox.driving/-170,0;165,0.json?instructions=html&geometry=polyline&access_token=key');
185 | v.end();
186 | });
187 |
188 | u.test("sets profile", function (v) {
189 | var directions = L.mapbox.directions({accessToken: 'key', profile: 'mapbox.walking'});
190 | directions.setOrigin(L.latLng(1, 2)).setDestination(L.latLng(3, 4));
191 | v.equal(directions.queryURL(), 'https://api.tiles.mapbox.com/v4/directions/mapbox.walking/2,1;4,3.json?instructions=html&geometry=polyline&access_token=key');
192 | v.end();
193 | });
194 | });
195 |
196 | t.test("query", function (u) {
197 | u.plan(8);
198 |
199 | u.test("returns self", function (v) {
200 | var server = sinon.fakeServer.create();
201 | var directions = L.mapbox.directions({accessToken: 'key'});
202 | v.deepEqual(directions.query(), directions);
203 | v.end();
204 | server.restore();
205 | });
206 |
207 | u.test("fires error if response is an HTTP error", function (v) {
208 | var server = sinon.fakeServer.create();
209 | var directions = L.mapbox.directions({accessToken: 'key'});
210 |
211 | directions.on('error', function (e) {
212 | v.ok(e.error, 'error was in error event');
213 | v.ok(callback.called, 'callback was called');
214 | v.end();
215 | server.restore();
216 | });
217 |
218 | var callback = sinon.spy();
219 |
220 | directions
221 | .setOrigin(L.latLng(1, 2))
222 | .setDestination(L.latLng(3, 4))
223 | .query({}, callback);
224 |
225 | server.respondWith("GET", "https://api.tiles.mapbox.com/v4/directions/mapbox.driving/2,1;4,3.json?instructions=html&geometry=polyline&access_token=key",
226 | [400, { "Content-Type": "application/json" }, JSON.stringify({error: 'error'})]);
227 | server.respond();
228 | });
229 |
230 | u.test("fires error if response is an API error", function (v) {
231 | var server = sinon.fakeServer.create();
232 | var directions = L.mapbox.directions({accessToken: 'key'});
233 |
234 | directions.on('error', function (e) {
235 | v.equal(e.error, 'error');
236 | v.end();
237 | server.restore();
238 | });
239 |
240 | directions
241 | .setOrigin(L.latLng(1, 2))
242 | .setDestination(L.latLng(3, 4))
243 | .query();
244 |
245 | server.respondWith("GET", "https://api.tiles.mapbox.com/v4/directions/mapbox.driving/2,1;4,3.json?instructions=html&geometry=polyline&access_token=key",
246 | [200, { "Content-Type": "application/json" }, JSON.stringify({error: 'error'})]);
247 | server.respond();
248 | });
249 |
250 | u.test("fires load if response is successful", function (v) {
251 | var server = sinon.fakeServer.create();
252 | var directions = L.mapbox.directions({accessToken: 'key'});
253 |
254 | directions.on('load', function (e) {
255 | v.deepEqual(e.routes, []);
256 | v.end();
257 | server.restore();
258 | });
259 |
260 | directions
261 | .setOrigin(L.latLng(1, 2))
262 | .setDestination(L.latLng(3, 4))
263 | .query();
264 |
265 | server.respondWith("GET", "https://api.tiles.mapbox.com/v4/directions/mapbox.driving/2,1;4,3.json?instructions=html&geometry=polyline&access_token=key",
266 | [200, { "Content-Type": "application/json" }, JSON.stringify({routes: []})]);
267 | server.respond();
268 | });
269 |
270 | u.test("aborts currently pending request", function (v) {
271 | var server = sinon.fakeServer.create();
272 | var directions = L.mapbox.directions({accessToken: 'key'});
273 |
274 | directions
275 | .setOrigin(L.latLng(1, 2))
276 | .setDestination(L.latLng(3, 4))
277 | .query()
278 | .query();
279 |
280 | v.ok(server.requests[0].aborted);
281 | v.end();
282 | server.restore();
283 | });
284 |
285 | u.test("decodes polyline geometries", function (v) {
286 | var server = sinon.fakeServer.create();
287 | var directions = L.mapbox.directions({accessToken: 'key'});
288 |
289 | directions.on('load', function (e) {
290 | v.deepEqual(e.routes[0].geometry, {
291 | type: 'LineString',
292 | coordinates: [[-120.2, 38.5], [-120.95, 40.7], [-126.453, 43.252]]
293 | });
294 | v.end();
295 | server.restore();
296 | });
297 |
298 | directions
299 | .setOrigin(L.latLng(1, 2))
300 | .setDestination(L.latLng(3, 4))
301 | .query();
302 |
303 | server.respondWith("GET", "https://api.tiles.mapbox.com/v4/directions/mapbox.driving/2,1;4,3.json?instructions=html&geometry=polyline&access_token=key",
304 | [200, { "Content-Type": "application/json" }, JSON.stringify({routes: [{geometry: '_izlhA~rlgdF_{geC~ywl@_kwzCn`{nI'}]})]);
305 | server.respond();
306 | });
307 |
308 | u.test("replaces origin and destination with the response values if not set by geocoding", function (v) {
309 | var server = sinon.fakeServer.create();
310 | var directions = L.mapbox.directions({accessToken: 'key'}),
311 | response = {
312 | origin: {properties: {name: 'origin'}},
313 | destination: {properties: {name: 'destination'}},
314 | routes: []
315 | };
316 |
317 | directions.on('load', function () {
318 | v.deepEqual(directions.getOrigin(), response.origin);
319 | v.deepEqual(directions.getDestination(), response.destination);
320 | v.end();
321 | server.restore();
322 | });
323 |
324 | directions
325 | .setOrigin(L.latLng(1, 2))
326 | .setDestination(L.latLng(3, 4))
327 | .query();
328 |
329 | server.respondWith("GET", "https://api.tiles.mapbox.com/v4/directions/mapbox.driving/2,1;4,3.json?instructions=html&geometry=polyline&access_token=key",
330 | [200, { "Content-Type": "application/json" }, JSON.stringify(response)]);
331 | server.respond();
332 | });
333 |
334 | u.test("does not replaces origin and destination with the response values if set by geocoding", function (v) {
335 | var server = sinon.fakeServer.create();
336 | var directions = L.mapbox.directions({accessToken: 'key'}),
337 | origin = directions._normalizeWaypoint('somewhere'),
338 | response = {
339 | origin: {properties: {name: 'origin'}},
340 | destination: {properties: {name: 'destination'}},
341 | routes: []
342 | };
343 |
344 | // stub geocode
345 | origin.properties.name = 'Far far away';
346 | origin.geometry.coordinates = [2,1];
347 |
348 | directions.on('load', function () {
349 | v.deepEqual(directions.getOrigin(), origin);
350 | v.deepEqual(directions.getDestination(), response.destination);
351 | v.end();
352 | server.restore();
353 | });
354 |
355 | directions
356 | .setOrigin(origin)
357 | .setDestination(L.latLng(3, 4))
358 | .query();
359 |
360 | server.respondWith("GET", "https://api.tiles.mapbox.com/v4/directions/mapbox.driving/2,1;4,3.json?instructions=html&geometry=polyline&access_token=key",
361 | [200, { "Content-Type": "application/json" }, JSON.stringify(response)]);
362 | server.respond();
363 | });
364 | });
365 |
366 | t.test("geocode", function (u) {
367 | u.plan(3);
368 |
369 | var server;
370 |
371 | function run(runTest) {
372 | server = sinon.fakeServer.create();
373 |
374 | runTest(function() {
375 | server.restore();
376 | });
377 | }
378 |
379 | u.test("returns geocoded response", function (v) {
380 | var server = sinon.fakeServer.create();
381 | var directions = L.mapbox.directions({accessToken: 'key'}),
382 | response = {
383 | features:[{
384 | center:[3,3],
385 | place_name: 'San Francisco'
386 | }]
387 | };
388 |
389 | var wp = directions._normalizeWaypoint('San Francisco');
390 | v.equal(wp.geometry.coordinates, undefined);
391 |
392 | directions._geocode(wp, {lat: 2, lng: 2}, function(err) {
393 | v.ifError(err);
394 | v.deepEqual(wp.geometry.coordinates, [3,3]);
395 | v.end();
396 | server.restore();
397 | });
398 |
399 | server.respondWith("GET", "https://api.tiles.mapbox.com/v4/geocode/mapbox.places/San Francisco.json?proximity=2,2&access_token=key",
400 | [200, { "Content-Type": "application/json" }, JSON.stringify(response)]);
401 | server.respond();
402 | });
403 |
404 | u.test("handles no results found", function(v) {
405 | var server = sinon.fakeServer.create();
406 | var directions = L.mapbox.directions({accessToken: 'key'}),
407 | response = {
408 | features:[]
409 | };
410 |
411 | var wp = directions._normalizeWaypoint('asdfjkl');
412 | v.equal(wp.geometry.coordinates, undefined);
413 |
414 | directions._geocode(wp, {lat: 2, lng: 2}, function(err) {
415 | v.equal(err.message, 'No results found for query asdfjkl');
416 | v.equal(wp.geometry.coordinates, undefined);
417 | v.end();
418 | server.restore();
419 | });
420 |
421 | server.respondWith("GET", "https://api.tiles.mapbox.com/v4/geocode/mapbox.places/asdfjkl.json?proximity=2,2&access_token=key",
422 | [200, { "Content-Type": "application/json" }, JSON.stringify(response)]);
423 | server.respond();
424 | });
425 |
426 | u.test("bad geocoding cancels directions query", function(v) {
427 | var server = sinon.fakeServer.create();
428 | var directions = L.mapbox.directions({accessToken: 'key'}),
429 | response = {
430 | features:[]
431 | };
432 |
433 | directions.on('error', function (e) {
434 | v.equal(e.error, 'No results found for query asdfjkl');
435 | v.end();
436 | server.restore();
437 | });
438 |
439 | directions
440 | .setOrigin(directions._normalizeWaypoint('asdfjkl'))
441 | .setDestination(directions._normalizeWaypoint('San Rafael'))
442 | .query();
443 |
444 | server.respondWith("GET", "https://api.tiles.mapbox.com/v4/geocode/mapbox.places/asdfjkl.json?proximity=&access_token=key",
445 | [200, { "Content-Type": "application/json" }, JSON.stringify(response)]);
446 | server.respond();
447 | });
448 | });
449 |
450 | t.end();
451 | });
452 |
--------------------------------------------------------------------------------
/lib/d3.js:
--------------------------------------------------------------------------------
1 | !function(){
2 | var d3 = {version: "3.4.1"}; // semver
3 | var d3_arraySlice = [].slice,
4 | d3_array = function(list) { return d3_arraySlice.call(list); }; // conversion for NodeLists
5 |
6 | var d3_document = document,
7 | d3_documentElement = d3_document.documentElement,
8 | d3_window = window;
9 |
10 | // Redefine d3_array if the browser doesn’t support slice-based conversion.
11 | try {
12 | d3_array(d3_documentElement.childNodes)[0].nodeType;
13 | } catch(e) {
14 | d3_array = function(list) {
15 | var i = list.length, array = new Array(i);
16 | while (i--) array[i] = list[i];
17 | return array;
18 | };
19 | }
20 | var d3_subclass = {}.__proto__?
21 |
22 | // Until ECMAScript supports array subclassing, prototype injection works well.
23 | function(object, prototype) {
24 | object.__proto__ = prototype;
25 | }:
26 |
27 | // And if your browser doesn't support __proto__, we'll use direct extension.
28 | function(object, prototype) {
29 | for (var property in prototype) object[property] = prototype[property];
30 | };
31 |
32 | function d3_vendorSymbol(object, name) {
33 | if (name in object) return name;
34 | name = name.charAt(0).toUpperCase() + name.substring(1);
35 | for (var i = 0, n = d3_vendorPrefixes.length; i < n; ++i) {
36 | var prefixName = d3_vendorPrefixes[i] + name;
37 | if (prefixName in object) return prefixName;
38 | }
39 | }
40 |
41 | var d3_vendorPrefixes = ["webkit", "ms", "moz", "Moz", "o", "O"];
42 |
43 | function d3_selection(groups) {
44 | d3_subclass(groups, d3_selectionPrototype);
45 | return groups;
46 | }
47 |
48 | var d3_select = function(s, n) { return n.querySelector(s); },
49 | d3_selectAll = function(s, n) { return n.querySelectorAll(s); },
50 | d3_selectMatcher = d3_documentElement[d3_vendorSymbol(d3_documentElement, "matchesSelector")],
51 | d3_selectMatches = function(n, s) { return d3_selectMatcher.call(n, s); };
52 |
53 | // Prefer Sizzle, if available.
54 | if (typeof Sizzle === "function") {
55 | d3_select = function(s, n) { return Sizzle(s, n)[0] || null; };
56 | d3_selectAll = function(s, n) { return Sizzle.uniqueSort(Sizzle(s, n)); };
57 | d3_selectMatches = Sizzle.matchesSelector;
58 | }
59 |
60 | d3.selection = function() {
61 | return d3_selectionRoot;
62 | };
63 |
64 | var d3_selectionPrototype = d3.selection.prototype = [];
65 |
66 |
67 | d3_selectionPrototype.select = function(selector) {
68 | var subgroups = [],
69 | subgroup,
70 | subnode,
71 | group,
72 | node;
73 |
74 | selector = d3_selection_selector(selector);
75 |
76 | for (var j = -1, m = this.length; ++j < m;) {
77 | subgroups.push(subgroup = []);
78 | subgroup.parentNode = (group = this[j]).parentNode;
79 | for (var i = -1, n = group.length; ++i < n;) {
80 | if (node = group[i]) {
81 | subgroup.push(subnode = selector.call(node, node.__data__, i, j));
82 | if (subnode && "__data__" in node) subnode.__data__ = node.__data__;
83 | } else {
84 | subgroup.push(null);
85 | }
86 | }
87 | }
88 |
89 | return d3_selection(subgroups);
90 | };
91 |
92 | function d3_selection_selector(selector) {
93 | return typeof selector === "function" ? selector : function() {
94 | return d3_select(selector, this);
95 | };
96 | }
97 |
98 | d3_selectionPrototype.selectAll = function(selector) {
99 | var subgroups = [],
100 | subgroup,
101 | node;
102 |
103 | selector = d3_selection_selectorAll(selector);
104 |
105 | for (var j = -1, m = this.length; ++j < m;) {
106 | for (var group = this[j], i = -1, n = group.length; ++i < n;) {
107 | if (node = group[i]) {
108 | subgroups.push(subgroup = d3_array(selector.call(node, node.__data__, i, j)));
109 | subgroup.parentNode = node;
110 | }
111 | }
112 | }
113 |
114 | return d3_selection(subgroups);
115 | };
116 |
117 | function d3_selection_selectorAll(selector) {
118 | return typeof selector === "function" ? selector : function() {
119 | return d3_selectAll(selector, this);
120 | };
121 | }
122 | var d3_nsPrefix = {
123 | svg: "http://www.w3.org/2000/svg",
124 | xhtml: "http://www.w3.org/1999/xhtml",
125 | xlink: "http://www.w3.org/1999/xlink",
126 | xml: "http://www.w3.org/XML/1998/namespace",
127 | xmlns: "http://www.w3.org/2000/xmlns/"
128 | };
129 |
130 | d3.ns = {
131 | prefix: d3_nsPrefix,
132 | qualify: function(name) {
133 | var i = name.indexOf(":"),
134 | prefix = name;
135 | if (i >= 0) {
136 | prefix = name.substring(0, i);
137 | name = name.substring(i + 1);
138 | }
139 | return d3_nsPrefix.hasOwnProperty(prefix)
140 | ? {space: d3_nsPrefix[prefix], local: name}
141 | : name;
142 | }
143 | };
144 |
145 | d3_selectionPrototype.attr = function(name, value) {
146 | if (arguments.length < 2) {
147 |
148 | // For attr(string), return the attribute value for the first node.
149 | if (typeof name === "string") {
150 | var node = this.node();
151 | name = d3.ns.qualify(name);
152 | return name.local
153 | ? node.getAttributeNS(name.space, name.local)
154 | : node.getAttribute(name);
155 | }
156 |
157 | // For attr(object), the object specifies the names and values of the
158 | // attributes to set or remove. The values may be functions that are
159 | // evaluated for each element.
160 | for (value in name) this.each(d3_selection_attr(value, name[value]));
161 | return this;
162 | }
163 |
164 | return this.each(d3_selection_attr(name, value));
165 | };
166 |
167 | function d3_selection_attr(name, value) {
168 | name = d3.ns.qualify(name);
169 |
170 | // For attr(string, null), remove the attribute with the specified name.
171 | function attrNull() {
172 | this.removeAttribute(name);
173 | }
174 | function attrNullNS() {
175 | this.removeAttributeNS(name.space, name.local);
176 | }
177 |
178 | // For attr(string, string), set the attribute with the specified name.
179 | function attrConstant() {
180 | this.setAttribute(name, value);
181 | }
182 | function attrConstantNS() {
183 | this.setAttributeNS(name.space, name.local, value);
184 | }
185 |
186 | // For attr(string, function), evaluate the function for each element, and set
187 | // or remove the attribute as appropriate.
188 | function attrFunction() {
189 | var x = value.apply(this, arguments);
190 | if (x == null) this.removeAttribute(name);
191 | else this.setAttribute(name, x);
192 | }
193 | function attrFunctionNS() {
194 | var x = value.apply(this, arguments);
195 | if (x == null) this.removeAttributeNS(name.space, name.local);
196 | else this.setAttributeNS(name.space, name.local, x);
197 | }
198 |
199 | return value == null
200 | ? (name.local ? attrNullNS : attrNull) : (typeof value === "function"
201 | ? (name.local ? attrFunctionNS : attrFunction)
202 | : (name.local ? attrConstantNS : attrConstant));
203 | }
204 | function d3_collapse(s) {
205 | return s.trim().replace(/\s+/g, " ");
206 | }
207 | d3.requote = function(s) {
208 | return s.replace(d3_requote_re, "\\$&");
209 | };
210 |
211 | var d3_requote_re = /[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g;
212 |
213 | d3_selectionPrototype.classed = function(name, value) {
214 | if (arguments.length < 2) {
215 |
216 | // For classed(string), return true only if the first node has the specified
217 | // class or classes. Note that even if the browser supports DOMTokenList, it
218 | // probably doesn't support it on SVG elements (which can be animated).
219 | if (typeof name === "string") {
220 | var node = this.node(),
221 | n = (name = d3_selection_classes(name)).length,
222 | i = -1;
223 | if (value = node.classList) {
224 | while (++i < n) if (!value.contains(name[i])) return false;
225 | } else {
226 | value = node.getAttribute("class");
227 | while (++i < n) if (!d3_selection_classedRe(name[i]).test(value)) return false;
228 | }
229 | return true;
230 | }
231 |
232 | // For classed(object), the object specifies the names of classes to add or
233 | // remove. The values may be functions that are evaluated for each element.
234 | for (value in name) this.each(d3_selection_classed(value, name[value]));
235 | return this;
236 | }
237 |
238 | // Otherwise, both a name and a value are specified, and are handled as below.
239 | return this.each(d3_selection_classed(name, value));
240 | };
241 |
242 | function d3_selection_classedRe(name) {
243 | return new RegExp("(?:^|\\s+)" + d3.requote(name) + "(?:\\s+|$)", "g");
244 | }
245 |
246 | function d3_selection_classes(name) {
247 | return name.trim().split(/^|\s+/);
248 | }
249 |
250 | // Multiple class names are allowed (e.g., "foo bar").
251 | function d3_selection_classed(name, value) {
252 | name = d3_selection_classes(name).map(d3_selection_classedName);
253 | var n = name.length;
254 |
255 | function classedConstant() {
256 | var i = -1;
257 | while (++i < n) name[i](this, value);
258 | }
259 |
260 | // When the value is a function, the function is still evaluated only once per
261 | // element even if there are multiple class names.
262 | function classedFunction() {
263 | var i = -1, x = value.apply(this, arguments);
264 | while (++i < n) name[i](this, x);
265 | }
266 |
267 | return typeof value === "function"
268 | ? classedFunction
269 | : classedConstant;
270 | }
271 |
272 | function d3_selection_classedName(name) {
273 | var re = d3_selection_classedRe(name);
274 | return function(node, value) {
275 | if (c = node.classList) return value ? c.add(name) : c.remove(name);
276 | var c = node.getAttribute("class") || "";
277 | if (value) {
278 | re.lastIndex = 0;
279 | if (!re.test(c)) node.setAttribute("class", d3_collapse(c + " " + name));
280 | } else {
281 | node.setAttribute("class", d3_collapse(c.replace(re, " ")));
282 | }
283 | };
284 | }
285 |
286 | d3_selectionPrototype.style = function(name, value, priority) {
287 | var n = arguments.length;
288 | if (n < 3) {
289 |
290 | // For style(object) or style(object, string), the object specifies the
291 | // names and values of the attributes to set or remove. The values may be
292 | // functions that are evaluated for each element. The optional string
293 | // specifies the priority.
294 | if (typeof name !== "string") {
295 | if (n < 2) value = "";
296 | for (priority in name) this.each(d3_selection_style(priority, name[priority], value));
297 | return this;
298 | }
299 |
300 | // For style(string), return the computed style value for the first node.
301 | if (n < 2) return d3_window.getComputedStyle(this.node(), null).getPropertyValue(name);
302 |
303 | // For style(string, string) or style(string, function), use the default
304 | // priority. The priority is ignored for style(string, null).
305 | priority = "";
306 | }
307 |
308 | // Otherwise, a name, value and priority are specified, and handled as below.
309 | return this.each(d3_selection_style(name, value, priority));
310 | };
311 |
312 | function d3_selection_style(name, value, priority) {
313 |
314 | // For style(name, null) or style(name, null, priority), remove the style
315 | // property with the specified name. The priority is ignored.
316 | function styleNull() {
317 | this.style.removeProperty(name);
318 | }
319 |
320 | // For style(name, string) or style(name, string, priority), set the style
321 | // property with the specified name, using the specified priority.
322 | function styleConstant() {
323 | this.style.setProperty(name, value, priority);
324 | }
325 |
326 | // For style(name, function) or style(name, function, priority), evaluate the
327 | // function for each element, and set or remove the style property as
328 | // appropriate. When setting, use the specified priority.
329 | function styleFunction() {
330 | var x = value.apply(this, arguments);
331 | if (x == null) this.style.removeProperty(name);
332 | else this.style.setProperty(name, x, priority);
333 | }
334 |
335 | return value == null
336 | ? styleNull : (typeof value === "function"
337 | ? styleFunction : styleConstant);
338 | }
339 |
340 | d3_selectionPrototype.property = function(name, value) {
341 | if (arguments.length < 2) {
342 |
343 | // For property(string), return the property value for the first node.
344 | if (typeof name === "string") return this.node()[name];
345 |
346 | // For property(object), the object specifies the names and values of the
347 | // properties to set or remove. The values may be functions that are
348 | // evaluated for each element.
349 | for (value in name) this.each(d3_selection_property(value, name[value]));
350 | return this;
351 | }
352 |
353 | // Otherwise, both a name and a value are specified, and are handled as below.
354 | return this.each(d3_selection_property(name, value));
355 | };
356 |
357 | function d3_selection_property(name, value) {
358 |
359 | // For property(name, null), remove the property with the specified name.
360 | function propertyNull() {
361 | delete this[name];
362 | }
363 |
364 | // For property(name, string), set the property with the specified name.
365 | function propertyConstant() {
366 | this[name] = value;
367 | }
368 |
369 | // For property(name, function), evaluate the function for each element, and
370 | // set or remove the property as appropriate.
371 | function propertyFunction() {
372 | var x = value.apply(this, arguments);
373 | if (x == null) delete this[name];
374 | else this[name] = x;
375 | }
376 |
377 | return value == null
378 | ? propertyNull : (typeof value === "function"
379 | ? propertyFunction : propertyConstant);
380 | }
381 |
382 | d3_selectionPrototype.text = function(value) {
383 | return arguments.length
384 | ? this.each(typeof value === "function"
385 | ? function() { var v = value.apply(this, arguments); this.textContent = v == null ? "" : v; } : value == null
386 | ? function() { this.textContent = ""; }
387 | : function() { this.textContent = value; })
388 | : this.node().textContent;
389 | };
390 |
391 | d3_selectionPrototype.html = function(value) {
392 | return arguments.length
393 | ? this.each(typeof value === "function"
394 | ? function() { var v = value.apply(this, arguments); this.innerHTML = v == null ? "" : v; } : value == null
395 | ? function() { this.innerHTML = ""; }
396 | : function() { this.innerHTML = value; })
397 | : this.node().innerHTML;
398 | };
399 |
400 | d3_selectionPrototype.append = function(name) {
401 | name = d3_selection_creator(name);
402 | return this.select(function() {
403 | return this.appendChild(name.apply(this, arguments));
404 | });
405 | };
406 |
407 | function d3_selection_creator(name) {
408 | return typeof name === "function" ? name
409 | : (name = d3.ns.qualify(name)).local ? function() { return this.ownerDocument.createElementNS(name.space, name.local); }
410 | : function() { return this.ownerDocument.createElementNS(this.namespaceURI, name); };
411 | }
412 |
413 | d3_selectionPrototype.insert = function(name, before) {
414 | name = d3_selection_creator(name);
415 | before = d3_selection_selector(before);
416 | return this.select(function() {
417 | return this.insertBefore(name.apply(this, arguments), before.apply(this, arguments) || null);
418 | });
419 | };
420 |
421 | // TODO remove(selector)?
422 | // TODO remove(node)?
423 | // TODO remove(function)?
424 | d3_selectionPrototype.remove = function() {
425 | return this.each(function() {
426 | var parent = this.parentNode;
427 | if (parent) parent.removeChild(this);
428 | });
429 | };
430 | function d3_class(ctor, properties) {
431 | try {
432 | for (var key in properties) {
433 | Object.defineProperty(ctor.prototype, key, {
434 | value: properties[key],
435 | enumerable: false
436 | });
437 | }
438 | } catch (e) {
439 | ctor.prototype = properties;
440 | }
441 | }
442 |
443 | d3.map = function(object) {
444 | var map = new d3_Map;
445 | if (object instanceof d3_Map) object.forEach(function(key, value) { map.set(key, value); });
446 | else for (var key in object) map.set(key, object[key]);
447 | return map;
448 | };
449 |
450 | function d3_Map() {}
451 |
452 | d3_class(d3_Map, {
453 | has: d3_map_has,
454 | get: function(key) {
455 | return this[d3_map_prefix + key];
456 | },
457 | set: function(key, value) {
458 | return this[d3_map_prefix + key] = value;
459 | },
460 | remove: d3_map_remove,
461 | keys: d3_map_keys,
462 | values: function() {
463 | var values = [];
464 | this.forEach(function(key, value) { values.push(value); });
465 | return values;
466 | },
467 | entries: function() {
468 | var entries = [];
469 | this.forEach(function(key, value) { entries.push({key: key, value: value}); });
470 | return entries;
471 | },
472 | size: d3_map_size,
473 | empty: d3_map_empty,
474 | forEach: function(f) {
475 | for (var key in this) if (key.charCodeAt(0) === d3_map_prefixCode) f.call(this, key.substring(1), this[key]);
476 | }
477 | });
478 |
479 | var d3_map_prefix = "\0", // prevent collision with built-ins
480 | d3_map_prefixCode = d3_map_prefix.charCodeAt(0);
481 |
482 | function d3_map_has(key) {
483 | return d3_map_prefix + key in this;
484 | }
485 |
486 | function d3_map_remove(key) {
487 | key = d3_map_prefix + key;
488 | return key in this && delete this[key];
489 | }
490 |
491 | function d3_map_keys() {
492 | var keys = [];
493 | this.forEach(function(key) { keys.push(key); });
494 | return keys;
495 | }
496 |
497 | function d3_map_size() {
498 | var size = 0;
499 | for (var key in this) if (key.charCodeAt(0) === d3_map_prefixCode) ++size;
500 | return size;
501 | }
502 |
503 | function d3_map_empty() {
504 | for (var key in this) if (key.charCodeAt(0) === d3_map_prefixCode) return false;
505 | return true;
506 | }
507 |
508 | d3_selectionPrototype.data = function(value, key) {
509 | var i = -1,
510 | n = this.length,
511 | group,
512 | node;
513 |
514 | // If no value is specified, return the first value.
515 | if (!arguments.length) {
516 | value = new Array(n = (group = this[0]).length);
517 | while (++i < n) {
518 | if (node = group[i]) {
519 | value[i] = node.__data__;
520 | }
521 | }
522 | return value;
523 | }
524 |
525 | function bind(group, groupData) {
526 | var i,
527 | n = group.length,
528 | m = groupData.length,
529 | n0 = Math.min(n, m),
530 | updateNodes = new Array(m),
531 | enterNodes = new Array(m),
532 | exitNodes = new Array(n),
533 | node,
534 | nodeData;
535 |
536 | if (key) {
537 | var nodeByKeyValue = new d3_Map,
538 | dataByKeyValue = new d3_Map,
539 | keyValues = [],
540 | keyValue;
541 |
542 | for (i = -1; ++i < n;) {
543 | keyValue = key.call(node = group[i], node.__data__, i);
544 | if (nodeByKeyValue.has(keyValue)) {
545 | exitNodes[i] = node; // duplicate selection key
546 | } else {
547 | nodeByKeyValue.set(keyValue, node);
548 | }
549 | keyValues.push(keyValue);
550 | }
551 |
552 | for (i = -1; ++i < m;) {
553 | keyValue = key.call(groupData, nodeData = groupData[i], i);
554 | if (node = nodeByKeyValue.get(keyValue)) {
555 | updateNodes[i] = node;
556 | node.__data__ = nodeData;
557 | } else if (!dataByKeyValue.has(keyValue)) { // no duplicate data key
558 | enterNodes[i] = d3_selection_dataNode(nodeData);
559 | }
560 | dataByKeyValue.set(keyValue, nodeData);
561 | nodeByKeyValue.remove(keyValue);
562 | }
563 |
564 | for (i = -1; ++i < n;) {
565 | if (nodeByKeyValue.has(keyValues[i])) {
566 | exitNodes[i] = group[i];
567 | }
568 | }
569 | } else {
570 | for (i = -1; ++i < n0;) {
571 | node = group[i];
572 | nodeData = groupData[i];
573 | if (node) {
574 | node.__data__ = nodeData;
575 | updateNodes[i] = node;
576 | } else {
577 | enterNodes[i] = d3_selection_dataNode(nodeData);
578 | }
579 | }
580 | for (; i < m; ++i) {
581 | enterNodes[i] = d3_selection_dataNode(groupData[i]);
582 | }
583 | for (; i < n; ++i) {
584 | exitNodes[i] = group[i];
585 | }
586 | }
587 |
588 | enterNodes.update
589 | = updateNodes;
590 |
591 | enterNodes.parentNode
592 | = updateNodes.parentNode
593 | = exitNodes.parentNode
594 | = group.parentNode;
595 |
596 | enter.push(enterNodes);
597 | update.push(updateNodes);
598 | exit.push(exitNodes);
599 | }
600 |
601 | var enter = d3_selection_enter([]),
602 | update = d3_selection([]),
603 | exit = d3_selection([]);
604 |
605 | if (typeof value === "function") {
606 | while (++i < n) {
607 | bind(group = this[i], value.call(group, group.parentNode.__data__, i));
608 | }
609 | } else {
610 | while (++i < n) {
611 | bind(group = this[i], value);
612 | }
613 | }
614 |
615 | update.enter = function() { return enter; };
616 | update.exit = function() { return exit; };
617 | return update;
618 | };
619 |
620 | function d3_selection_dataNode(data) {
621 | return {__data__: data};
622 | }
623 |
624 | d3_selectionPrototype.datum = function(value) {
625 | return arguments.length
626 | ? this.property("__data__", value)
627 | : this.property("__data__");
628 | };
629 |
630 | d3_selectionPrototype.filter = function(filter) {
631 | var subgroups = [],
632 | subgroup,
633 | group,
634 | node;
635 |
636 | if (typeof filter !== "function") filter = d3_selection_filter(filter);
637 |
638 | for (var j = 0, m = this.length; j < m; j++) {
639 | subgroups.push(subgroup = []);
640 | subgroup.parentNode = (group = this[j]).parentNode;
641 | for (var i = 0, n = group.length; i < n; i++) {
642 | if ((node = group[i]) && filter.call(node, node.__data__, i, j)) {
643 | subgroup.push(node);
644 | }
645 | }
646 | }
647 |
648 | return d3_selection(subgroups);
649 | };
650 |
651 | function d3_selection_filter(selector) {
652 | return function() {
653 | return d3_selectMatches(this, selector);
654 | };
655 | }
656 |
657 | d3_selectionPrototype.order = function() {
658 | for (var j = -1, m = this.length; ++j < m;) {
659 | for (var group = this[j], i = group.length - 1, next = group[i], node; --i >= 0;) {
660 | if (node = group[i]) {
661 | if (next && next !== node.nextSibling) next.parentNode.insertBefore(node, next);
662 | next = node;
663 | }
664 | }
665 | }
666 | return this;
667 | };
668 | d3.ascending = function(a, b) {
669 | return a < b ? -1 : a > b ? 1 : a >= b ? 0 : NaN;
670 | };
671 |
672 | d3_selectionPrototype.sort = function(comparator) {
673 | comparator = d3_selection_sortComparator.apply(this, arguments);
674 | for (var j = -1, m = this.length; ++j < m;) this[j].sort(comparator);
675 | return this.order();
676 | };
677 |
678 | function d3_selection_sortComparator(comparator) {
679 | if (!arguments.length) comparator = d3.ascending;
680 | return function(a, b) {
681 | return a && b ? comparator(a.__data__, b.__data__) : !a - !b;
682 | };
683 | }
684 | function d3_noop() {}
685 |
686 | d3.dispatch = function() {
687 | var dispatch = new d3_dispatch,
688 | i = -1,
689 | n = arguments.length;
690 | while (++i < n) dispatch[arguments[i]] = d3_dispatch_event(dispatch);
691 | return dispatch;
692 | };
693 |
694 | function d3_dispatch() {}
695 |
696 | d3_dispatch.prototype.on = function(type, listener) {
697 | var i = type.indexOf("."),
698 | name = "";
699 |
700 | // Extract optional namespace, e.g., "click.foo"
701 | if (i >= 0) {
702 | name = type.substring(i + 1);
703 | type = type.substring(0, i);
704 | }
705 |
706 | if (type) return arguments.length < 2
707 | ? this[type].on(name)
708 | : this[type].on(name, listener);
709 |
710 | if (arguments.length === 2) {
711 | if (listener == null) for (type in this) {
712 | if (this.hasOwnProperty(type)) this[type].on(name, null);
713 | }
714 | return this;
715 | }
716 | };
717 |
718 | function d3_dispatch_event(dispatch) {
719 | var listeners = [],
720 | listenerByName = new d3_Map;
721 |
722 | function event() {
723 | var z = listeners, // defensive reference
724 | i = -1,
725 | n = z.length,
726 | l;
727 | while (++i < n) if (l = z[i].on) l.apply(this, arguments);
728 | return dispatch;
729 | }
730 |
731 | event.on = function(name, listener) {
732 | var l = listenerByName.get(name),
733 | i;
734 |
735 | // return the current listener, if any
736 | if (arguments.length < 2) return l && l.on;
737 |
738 | // remove the old listener, if any (with copy-on-write)
739 | if (l) {
740 | l.on = null;
741 | listeners = listeners.slice(0, i = listeners.indexOf(l)).concat(listeners.slice(i + 1));
742 | listenerByName.remove(name);
743 | }
744 |
745 | // add the new listener, if any
746 | if (listener) listeners.push(listenerByName.set(name, {on: listener}));
747 |
748 | return dispatch;
749 | };
750 |
751 | return event;
752 | }
753 |
754 | d3.event = null;
755 |
756 | function d3_eventPreventDefault() {
757 | d3.event.preventDefault();
758 | }
759 |
760 | function d3_eventSource() {
761 | var e = d3.event, s;
762 | while (s = e.sourceEvent) e = s;
763 | return e;
764 | }
765 |
766 | // Like d3.dispatch, but for custom events abstracting native UI events. These
767 | // events have a target component (such as a brush), a target element (such as
768 | // the svg:g element containing the brush) and the standard arguments `d` (the
769 | // target element's data) and `i` (the selection index of the target element).
770 | function d3_eventDispatch(target) {
771 | var dispatch = new d3_dispatch,
772 | i = 0,
773 | n = arguments.length;
774 |
775 | while (++i < n) dispatch[arguments[i]] = d3_dispatch_event(dispatch);
776 |
777 | // Creates a dispatch context for the specified `thiz` (typically, the target
778 | // DOM element that received the source event) and `argumentz` (typically, the
779 | // data `d` and index `i` of the target element). The returned function can be
780 | // used to dispatch an event to any registered listeners; the function takes a
781 | // single argument as input, being the event to dispatch. The event must have
782 | // a "type" attribute which corresponds to a type registered in the
783 | // constructor. This context will automatically populate the "sourceEvent" and
784 | // "target" attributes of the event, as well as setting the `d3.event` global
785 | // for the duration of the notification.
786 | dispatch.of = function(thiz, argumentz) {
787 | return function(e1) {
788 | try {
789 | var e0 =
790 | e1.sourceEvent = d3.event;
791 | e1.target = target;
792 | d3.event = e1;
793 | dispatch[e1.type].apply(thiz, argumentz);
794 | } finally {
795 | d3.event = e0;
796 | }
797 | };
798 | };
799 |
800 | return dispatch;
801 | }
802 |
803 | d3_selectionPrototype.on = function(type, listener, capture) {
804 | var n = arguments.length;
805 | if (n < 3) {
806 |
807 | // For on(object) or on(object, boolean), the object specifies the event
808 | // types and listeners to add or remove. The optional boolean specifies
809 | // whether the listener captures events.
810 | if (typeof type !== "string") {
811 | if (n < 2) listener = false;
812 | for (capture in type) this.each(d3_selection_on(capture, type[capture], listener));
813 | return this;
814 | }
815 |
816 | // For on(string), return the listener for the first node.
817 | if (n < 2) return (n = this.node()["__on" + type]) && n._;
818 |
819 | // For on(string, function), use the default capture.
820 | capture = false;
821 | }
822 |
823 | // Otherwise, a type, listener and capture are specified, and handled as below.
824 | return this.each(d3_selection_on(type, listener, capture));
825 | };
826 |
827 | function d3_selection_on(type, listener, capture) {
828 | var name = "__on" + type,
829 | i = type.indexOf("."),
830 | wrap = d3_selection_onListener;
831 |
832 | if (i > 0) type = type.substring(0, i);
833 | var filter = d3_selection_onFilters.get(type);
834 | if (filter) type = filter, wrap = d3_selection_onFilter;
835 |
836 | function onRemove() {
837 | var l = this[name];
838 | if (l) {
839 | this.removeEventListener(type, l, l.$);
840 | delete this[name];
841 | }
842 | }
843 |
844 | function onAdd() {
845 | var l = wrap(listener, d3_array(arguments));
846 | onRemove.call(this);
847 | this.addEventListener(type, this[name] = l, l.$ = capture);
848 | l._ = listener;
849 | }
850 |
851 | function removeAll() {
852 | var re = new RegExp("^__on([^.]+)" + d3.requote(type) + "$"),
853 | match;
854 | for (var name in this) {
855 | if (match = name.match(re)) {
856 | var l = this[name];
857 | this.removeEventListener(match[1], l, l.$);
858 | delete this[name];
859 | }
860 | }
861 | }
862 |
863 | return i
864 | ? listener ? onAdd : onRemove
865 | : listener ? d3_noop : removeAll;
866 | }
867 |
868 | var d3_selection_onFilters = d3.map({
869 | mouseenter: "mouseover",
870 | mouseleave: "mouseout"
871 | });
872 |
873 | d3_selection_onFilters.forEach(function(k) {
874 | if ("on" + k in d3_document) d3_selection_onFilters.remove(k);
875 | });
876 |
877 | function d3_selection_onListener(listener, argumentz) {
878 | return function(e) {
879 | var o = d3.event; // Events can be reentrant (e.g., focus).
880 | d3.event = e;
881 | argumentz[0] = this.__data__;
882 | try {
883 | listener.apply(this, argumentz);
884 | } finally {
885 | d3.event = o;
886 | }
887 | };
888 | }
889 |
890 | function d3_selection_onFilter(listener, argumentz) {
891 | var l = d3_selection_onListener(listener, argumentz);
892 | return function(e) {
893 | var target = this, related = e.relatedTarget;
894 | if (!related || (related !== target && !(related.compareDocumentPosition(target) & 8))) {
895 | l.call(target, e);
896 | }
897 | };
898 | }
899 |
900 | d3_selectionPrototype.each = function(callback) {
901 | return d3_selection_each(this, function(node, i, j) {
902 | callback.call(node, node.__data__, i, j);
903 | });
904 | };
905 |
906 | function d3_selection_each(groups, callback) {
907 | for (var j = 0, m = groups.length; j < m; j++) {
908 | for (var group = groups[j], i = 0, n = group.length, node; i < n; i++) {
909 | if (node = group[i]) callback(node, i, j);
910 | }
911 | }
912 | return groups;
913 | }
914 |
915 | d3_selectionPrototype.call = function(callback) {
916 | var args = d3_array(arguments);
917 | callback.apply(args[0] = this, args);
918 | return this;
919 | };
920 |
921 | d3_selectionPrototype.empty = function() {
922 | return !this.node();
923 | };
924 |
925 | d3_selectionPrototype.node = function() {
926 | for (var j = 0, m = this.length; j < m; j++) {
927 | for (var group = this[j], i = 0, n = group.length; i < n; i++) {
928 | var node = group[i];
929 | if (node) return node;
930 | }
931 | }
932 | return null;
933 | };
934 |
935 | d3_selectionPrototype.size = function() {
936 | var n = 0;
937 | this.each(function() { ++n; });
938 | return n;
939 | };
940 |
941 | function d3_selection_enter(selection) {
942 | d3_subclass(selection, d3_selection_enterPrototype);
943 | return selection;
944 | }
945 |
946 | var d3_selection_enterPrototype = [];
947 |
948 | d3.selection.enter = d3_selection_enter;
949 | d3.selection.enter.prototype = d3_selection_enterPrototype;
950 |
951 | d3_selection_enterPrototype.append = d3_selectionPrototype.append;
952 | d3_selection_enterPrototype.empty = d3_selectionPrototype.empty;
953 | d3_selection_enterPrototype.node = d3_selectionPrototype.node;
954 | d3_selection_enterPrototype.call = d3_selectionPrototype.call;
955 | d3_selection_enterPrototype.size = d3_selectionPrototype.size;
956 |
957 |
958 | d3_selection_enterPrototype.select = function(selector) {
959 | var subgroups = [],
960 | subgroup,
961 | subnode,
962 | upgroup,
963 | group,
964 | node;
965 |
966 | for (var j = -1, m = this.length; ++j < m;) {
967 | upgroup = (group = this[j]).update;
968 | subgroups.push(subgroup = []);
969 | subgroup.parentNode = group.parentNode;
970 | for (var i = -1, n = group.length; ++i < n;) {
971 | if (node = group[i]) {
972 | subgroup.push(upgroup[i] = subnode = selector.call(group.parentNode, node.__data__, i, j));
973 | subnode.__data__ = node.__data__;
974 | } else {
975 | subgroup.push(null);
976 | }
977 | }
978 | }
979 |
980 | return d3_selection(subgroups);
981 | };
982 |
983 | d3_selection_enterPrototype.insert = function(name, before) {
984 | if (arguments.length < 2) before = d3_selection_enterInsertBefore(this);
985 | return d3_selectionPrototype.insert.call(this, name, before);
986 | };
987 |
988 | function d3_selection_enterInsertBefore(enter) {
989 | var i0, j0;
990 | return function(d, i, j) {
991 | var group = enter[j].update,
992 | n = group.length,
993 | node;
994 | if (j != j0) j0 = j, i0 = 0;
995 | if (i >= i0) i0 = i + 1;
996 | while (!(node = group[i0]) && ++i0 < n);
997 | return node;
998 | };
999 | }
1000 |
1001 | // import "../transition/transition";
1002 |
1003 | d3_selectionPrototype.transition = function() {
1004 | var id = d3_transitionInheritId || ++d3_transitionId,
1005 | subgroups = [],
1006 | subgroup,
1007 | node,
1008 | transition = d3_transitionInherit || {time: Date.now(), ease: d3_ease_cubicInOut, delay: 0, duration: 250};
1009 |
1010 | for (var j = -1, m = this.length; ++j < m;) {
1011 | subgroups.push(subgroup = []);
1012 | for (var group = this[j], i = -1, n = group.length; ++i < n;) {
1013 | if (node = group[i]) d3_transitionNode(node, i, id, transition);
1014 | subgroup.push(node);
1015 | }
1016 | }
1017 |
1018 | return d3_transition(subgroups, id);
1019 | };
1020 | // import "../transition/transition";
1021 |
1022 | d3_selectionPrototype.interrupt = function() {
1023 | return this.each(d3_selection_interrupt);
1024 | };
1025 |
1026 | function d3_selection_interrupt() {
1027 | var lock = this.__transition__;
1028 | if (lock) ++lock.active;
1029 | }
1030 |
1031 | // TODO fast singleton implementation?
1032 | d3.select = function(node) {
1033 | var group = [typeof node === "string" ? d3_select(node, d3_document) : node];
1034 | group.parentNode = d3_documentElement;
1035 | return d3_selection([group]);
1036 | };
1037 |
1038 | d3.selectAll = function(nodes) {
1039 | var group = d3_array(typeof nodes === "string" ? d3_selectAll(nodes, d3_document) : nodes);
1040 | group.parentNode = d3_documentElement;
1041 | return d3_selection([group]);
1042 | };
1043 |
1044 | var d3_selectionRoot = d3.select(d3_documentElement);
1045 | if (typeof define === "function" && define.amd) {
1046 | define(d3);
1047 | } else if (typeof module === "object" && module.exports) {
1048 | module.exports = d3;
1049 | } else {
1050 | this.d3 = d3;
1051 | }
1052 | }();
1053 |
--------------------------------------------------------------------------------