├── .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 | [![Build Status](https://travis-ci.org/mapbox/mapbox-directions.js.png)](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 |
134 |
135 |
136 |
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 | 21 | 23 | 49 | 56 | 60 | 64 | 65 | 67 | 68 | 70 | image/svg+xml 71 | 73 | 74 | 75 | 76 | 77 | 82 | 88 | 93 | 99 | 105 | 110 | 116 | 122 | 127 | 133 | 137 | 140 | 144 | 149 | 155 | 160 | 166 | 173 | 181 | A 192 | 193 | 200 | 208 | B 219 | 220 | 221 | 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 | --------------------------------------------------------------------------------