├── .gitignore
├── README.md
├── package.json
├── public
├── favicon.ico
├── index.html
└── manifest.json
├── src
├── Main.js
├── city-settings.json
├── controls.js
├── graph-layer
│ ├── constants.js
│ ├── edge-attributes-transform.js
│ ├── edge-layer.js
│ ├── graph-layer.js
│ ├── node-attributes-transform.js
│ ├── shortest-path-transform.js
│ └── utils.js
├── index.css
├── index.js
├── load-data.js
├── map.js
└── old
│ ├── binary-heap.js
│ └── compute-graph.js
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | build/
2 | dist/
3 | node_modules/
4 | coverage/
5 | test/
6 | data/
7 | TODO
8 |
9 | */**/yarn.lock
10 | yarn-error.log
11 | package-lock.json
12 |
13 | .vscode/
14 | .project
15 | .idea
16 | .DS_Store
17 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Isochronic Map (GPU Version)
2 |
3 | [Demo](http://pessimistress.github.io/isochronic-map/)
4 |
5 | ## Run app locally
6 |
7 | ```bash
8 | yarn
9 | yarn start
10 | ```
11 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vis-hackathon",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "baseui": "^7.4.1",
7 | "deck.gl": "^7.2.0-alpha.4",
8 | "react": "^16.8.6",
9 | "react-dom": "^16.8.6",
10 | "react-scripts": "3.0.1",
11 | "styletron-engine-atomic": "^1.3.0",
12 | "styletron-react": "^5.1.3"
13 | },
14 | "scripts": {
15 | "start": "react-scripts start",
16 | "build": "react-scripts build",
17 | "test": "react-scripts test",
18 | "eject": "react-scripts eject"
19 | },
20 | "eslintConfig": {
21 | "extends": "react-app"
22 | },
23 | "browserslist": {
24 | "production": [
25 | ">0.2%",
26 | "not dead",
27 | "not op_mini all"
28 | ],
29 | "development": [
30 | "last 1 chrome version",
31 | "last 1 firefox version",
32 | "last 1 safari version"
33 | ]
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Pessimistress/isochronic-map-gpu/98a009a3423664331edb1dd748c07887c58fb67d/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
22 | React App
23 |
24 |
25 |
26 |
27 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/src/Main.js:
--------------------------------------------------------------------------------
1 | import React, {useState, useEffect} from "react";
2 | import {Client as Styletron} from 'styletron-engine-atomic';
3 | import {Provider as StyletronProvider} from 'styletron-react';
4 | import {LightTheme, BaseProvider} from 'baseui';
5 | import {Spinner} from 'baseui/spinner';
6 | import citySettings from "./city-settings.json";
7 | import Controls from "./controls";
8 | import Map from "./map";
9 | import loadData from './load-data';
10 |
11 | const engine = new Styletron();
12 |
13 | const initialState = {
14 | graph: {},
15 | city: "san-francisco",
16 | ...citySettings["san-francisco"],
17 | hour: 0,
18 | mapType: 1,
19 | loaded: false
20 | };
21 |
22 | function App() {
23 | const [data, setData] = useState(initialState);
24 | const loadCity = async city => {
25 | try {
26 | const graph = await loadData(city);
27 | setData(state => ({...state, graph, loaded: true}));
28 | } catch (e) {
29 | console.error(e);
30 | }
31 | };
32 | useEffect(() => {
33 | if (!data.loaded) {
34 | loadCity(data.city);
35 | }
36 | });
37 | const setCity = city => {
38 | setData(state => ({
39 | ...state,
40 | city,
41 | ...citySettings[city],
42 | loaded: false
43 | }));
44 | };
45 | const setHour = hour => {
46 | setData(state => ({...state, hour}));
47 | };
48 | const setMapType = mapType => {
49 | setData(state => ({...state, mapType}));
50 | };
51 | const setSourceIndex = sourceIndex => {
52 | setData(state => ({...state, sourceIndex}));
53 | };
54 | const setViewState = viewState => {
55 | setData(state => ({...state, viewState}));
56 | };
57 |
58 | return (
59 |
60 |
61 |
62 |
70 |
71 | {data.loaded ? (
72 |
81 | ) : (
82 |
83 | )}
84 |
85 |
86 |
87 | );
88 | }
89 |
90 | export default App;
91 |
--------------------------------------------------------------------------------
/src/city-settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "san-francisco": {
3 | "viewState": {
4 | "longitude": -122.4192016,
5 | "latitude": 37.7751543,
6 | "zoom": 13,
7 | "pitch": 0,
8 | "bearing": 0
9 | },
10 | "sourceIndex": 311
11 | },
12 | "new-york": {
13 | "sourceIndex": 58689,
14 | "viewState": {
15 | "longitude": -73.9963128,
16 | "latitude": 40.7512821,
17 | "zoom": 13,
18 | "pitch": 0,
19 | "bearing": 0
20 | }
21 | },
22 | "seattle": {
23 | "sourceIndex": 164774,
24 | "viewState": {
25 | "latitude": 47.6098543,
26 | "longitude": -122.3350119,
27 | "zoom": 13,
28 | "pitch": 0,
29 | "bearing": 0
30 | }
31 | },
32 | "nairobi": {
33 | "sourceIndex": 66128,
34 | "viewState": {
35 | "longitude": 36.8382651,
36 | "latitude": -1.2575046,
37 | "zoom": 13,
38 | "pitch": 0,
39 | "bearing": 0
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/controls.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {Select} from 'baseui/select';
3 | import {Block} from 'baseui/block';
4 | import {FormControl} from 'baseui/form-control';
5 | import {Slider} from 'baseui/slider';
6 |
7 | const cities = [
8 | {value: 'cincinnati', label: 'Cincinnati'},
9 | {value: 'london', label: 'London'},
10 | {value: 'nairobi', label: 'Nairobi'},
11 | {value: 'new-york', label: 'New York'},
12 | {value: 'san-francisco', label: 'San Francisco'},
13 | {value: 'seattle', label: 'Seattle'}
14 | ];
15 |
16 | const mapTypes = [
17 | {value: 0, label: 'Base Map'},
18 | {value: 1, label: 'Node Distance'},
19 | {value: 2, label: 'Average Speed'},
20 | {value: 3, label: 'Isochronic Map'}
21 | ]
22 |
23 | const Controls = ({
24 | city,
25 | mapType,
26 | setMapType,
27 | hour,
28 | setHour,
29 | setCity
30 | }) => (
31 |
40 |
41 |
51 |
52 |
62 |
63 |
64 | setHour(Number(value))}
70 | />
71 |
72 |
73 | );
74 |
75 | export default Controls;
76 |
--------------------------------------------------------------------------------
/src/graph-layer/constants.js:
--------------------------------------------------------------------------------
1 |
2 | export const TRANSITION_FRAMES = 60;
3 | export const ISOCHRONIC_SCALE = 6; // seconds per meter
4 |
5 | export const ISOCHRONIC_RINGS = [
6 | 120, // 2 min
7 | 300, // 5 min
8 | 600, // 10 min
9 | 900, // 15 min
10 | 1200, // 20 min
11 | 1800, // 30 min
12 | 2700, // 45 min
13 | 3600 // 60 min
14 | ];
15 |
--------------------------------------------------------------------------------
/src/graph-layer/edge-attributes-transform.js:
--------------------------------------------------------------------------------
1 | import {Buffer, Transform} from '@luma.gl/core';
2 | import {getFloatTexture, getTextureSize} from './utils';
3 |
4 | export default class EdgePositionTransform {
5 | constructor(gl) {
6 | this._sourcePositionsBuffer = [
7 | new Buffer(gl, {accessor: {size: 3}, byteLength: 12}),
8 | new Buffer(gl, {accessor: {size: 3}, byteLength: 12})
9 | ];
10 | this._targetPositionsBuffer = [
11 | new Buffer(gl, {accessor: {size: 3}, byteLength: 12}),
12 | new Buffer(gl, {accessor: {size: 3}, byteLength: 12})
13 | ];
14 | this._validityBuffer = [
15 | new Buffer(gl, {accessor: {size: 1}, byteLength: 4}),
16 | new Buffer(gl, {accessor: {size: 1}, byteLength: 4})
17 | ];
18 |
19 | this.nodePositionsTexture = getFloatTexture(gl, 4);
20 |
21 | this._swapBuffer = false;
22 | this._transform = this._getTransform(gl);
23 | this._bufferChanged = false;
24 |
25 | this.gl = gl;
26 | }
27 |
28 | get sourcePositionsBuffer() {
29 | return this._sourcePositionsBuffer[this._swapBuffer ? 1 : 0];
30 | }
31 |
32 | get targetPositionsBuffer() {
33 | return this._targetPositionsBuffer[this._swapBuffer ? 1 : 0];
34 | }
35 |
36 | get validityBuffer() {
37 | return this._validityBuffer[this._swapBuffer ? 1 : 0];
38 | }
39 |
40 | reset(sourceIndex) {
41 | if (this._bufferChanged) {
42 | this.update();
43 | }
44 | }
45 |
46 | update({nodeCount = this._nodeCount, edgeCount = this._edgeCount, attributes = this._attributes} = {}) {
47 | this._swapBuffer = !this._swapBuffer;
48 | this._nodeCount = nodeCount;
49 | this._edgeCount = edgeCount;
50 | this._attributes = attributes;
51 |
52 | const {nodePositionsTexture, sourcePositionsBuffer, targetPositionsBuffer, validityBuffer} = this;
53 | nodePositionsTexture.resize(getTextureSize(nodeCount));
54 |
55 | sourcePositionsBuffer.reallocate(edgeCount * 3 * 4);
56 | targetPositionsBuffer.reallocate(edgeCount * 3 * 4);
57 | validityBuffer.reallocate(edgeCount * 4);
58 |
59 | this._transform.update({
60 | sourceBuffers: {
61 | edgeSourceIndices: attributes.edgeSourceIndices,
62 | edgeTargetIndices: attributes.edgeTargetIndices,
63 | },
64 | feedbackBuffers: {
65 | sourcePositions: this.sourcePositionsBuffer,
66 | targetPositions: this.targetPositionsBuffer,
67 | isValid: this.validityBuffer
68 | },
69 | elementCount: edgeCount
70 | });
71 |
72 | this._bufferChanged = false;
73 | }
74 |
75 | run({nodePositionsBuffer}) {
76 | const {_transform, nodePositionsTexture} = this;
77 | nodePositionsTexture.setSubImageData({data: nodePositionsBuffer});
78 |
79 | _transform.run({
80 | uniforms: {
81 | nodePositions: nodePositionsTexture,
82 | textureDims: [nodePositionsTexture.width, nodePositionsTexture.height]
83 | }
84 | });
85 |
86 | this._bufferChanged = true;
87 | // console.log(this.validityBuffer.getData());
88 | }
89 |
90 | _getTransform(gl) {
91 | return new Transform(gl, {
92 | vs: `\
93 | #version 300 es
94 | in float edgeSourceIndices;
95 | in float edgeTargetIndices;
96 |
97 | uniform sampler2D nodePositions;
98 | uniform vec2 textureDims;
99 |
100 | out vec3 sourcePositions;
101 | out vec3 targetPositions;
102 | out float isValid;
103 |
104 | ivec2 getVexelCoord(float index) {
105 | float y = floor(index / textureDims.x);
106 | float x = index - textureDims.x * y;
107 | return ivec2(x, y);
108 | }
109 |
110 | void main() {
111 | ivec2 sourceCoord = getVexelCoord(edgeSourceIndices);
112 | ivec2 targetoord = getVexelCoord(edgeTargetIndices);
113 |
114 | vec4 source = texelFetch(nodePositions, sourceCoord, 0);
115 | vec4 target = texelFetch(nodePositions, targetoord, 0);
116 |
117 | sourcePositions = source.rgb;
118 | targetPositions = target.rgb;
119 |
120 | isValid = source.a * target.a;
121 | }
122 | `,
123 | varyings: ['sourcePositions', 'targetPositions', 'isValid'],
124 | elementCount: 1,
125 |
126 | feedbackBuffers: {
127 | sourcePositions: this.sourcePositionsBuffer,
128 | targetPositions: this.targetPositionsBuffer,
129 | isValid: this.validityBuffer
130 | }
131 | });
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/src/graph-layer/edge-layer.js:
--------------------------------------------------------------------------------
1 | import {LineLayer} from '@deck.gl/layers';
2 |
3 | export default class EdgeLayer extends LineLayer {
4 | getShaders() {
5 | const shaders = super.getShaders();
6 | shaders.inject = {
7 | 'vs:#decl': `
8 | attribute float instanceValid;
9 | `,
10 | 'vs:#main-end': `
11 | vColor.a *= instanceValid + 0.1;
12 | `
13 | };
14 | return shaders;
15 | }
16 |
17 | initializeState(context) {
18 | super.initializeState(context);
19 |
20 | // TODO: deck.gl's Attribute's update does not respect external buffer's offset or stride
21 | this.getAttributeManager().addInstanced({
22 | instanceValid: {size: 1, accessor: 'getIsValid', transition: true}
23 | });
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/graph-layer/graph-layer.js:
--------------------------------------------------------------------------------
1 | import {CompositeLayer, COORDINATE_SYSTEM} from '@deck.gl/core';
2 | import Attribute from '@deck.gl/core/dist/esm/lib/attribute';
3 |
4 | import GL from '@luma.gl/constants';
5 |
6 | import ShortestPathTransform from './shortest-path-transform';
7 | import EdgeAttributesTransform from './edge-attributes-transform';
8 | import NodeAttributesTransform from './node-attributes-transform';
9 |
10 | import {ScatterplotLayer, TextLayer} from '@deck.gl/layers';
11 | import EdgeLayer from './edge-layer';
12 |
13 | import {TRANSITION_FRAMES, ISOCHRONIC_SCALE, ISOCHRONIC_RINGS} from './constants';
14 |
15 | const MODE = {
16 | NONE: 0,
17 | NODE_DISTANCE: 1,
18 | TRAFFIC: 2,
19 | ISOCHRONIC: 3
20 | };
21 |
22 | export default class GraphLayer extends CompositeLayer {
23 |
24 | initializeState({gl}) {
25 | this.setState({
26 | attributes: this._getAttributes(gl),
27 | nodeAttributesTransform: new NodeAttributesTransform(gl),
28 | edgeAttributesTransform: new EdgeAttributesTransform(gl),
29 | shortestPathTransform: new ShortestPathTransform(gl),
30 | transitionDuration: 0,
31 | iteration: Infinity,
32 | lastAttributeChange: -1,
33 | animation: requestAnimationFrame(this.animate.bind(this))
34 | });
35 | }
36 |
37 | updateState({props, oldProps, changeFlags}) {
38 | const dataChanged = changeFlags.dataChanged || changeFlags.updateTriggersChanged;
39 | const {attributes, shortestPathTransform, nodeAttributesTransform, edgeAttributesTransform} = this.state;
40 |
41 | if (props.data && dataChanged) {
42 | const nodeCount = props.data.nodes.length;
43 | const edgeCount = props.data.edges.length;
44 |
45 | for (const attributeName in attributes) {
46 | const attribute = attributes[attributeName];
47 |
48 | if (changeFlags.dataChanged ||
49 | (changeFlags.updateTriggersChanged && changeFlags.updateTriggersChanged[attribute.userData.accessor])) {
50 | attribute.setNeedsUpdate();
51 | const isNode = attributeName.startsWith('node');
52 | const numInstances = isNode ? nodeCount : edgeCount;
53 | attribute.allocate(numInstances);
54 | attribute.updateBuffer({
55 | numInstances,
56 | data: isNode ? props.data.nodes : props.data.edges,
57 | props,
58 | context: this
59 | })
60 | }
61 | }
62 |
63 | // Reset model
64 | shortestPathTransform.update({nodeCount, edgeCount, attributes});
65 | nodeAttributesTransform.update({nodeCount, edgeCount, attributes});
66 | edgeAttributesTransform.update({nodeCount, edgeCount, attributes});
67 |
68 | this.setState({
69 | transitionDuration: edgeCount,
70 | maxIterations: Math.ceil(Math.sqrt(nodeCount)) + TRANSITION_FRAMES
71 | });
72 | }
73 |
74 | if (dataChanged || props.sourceIndex !== oldProps.sourceIndex) {
75 | shortestPathTransform.reset(props.sourceIndex);
76 | nodeAttributesTransform.reset(props.sourceIndex);
77 | edgeAttributesTransform.reset(props.sourceIndex);
78 | this.setState({iteration: 0, lastAttributeChange: 0});
79 | } else if (props.mode !== oldProps.mode) {
80 | nodeAttributesTransform.update();
81 | edgeAttributesTransform.update();
82 | if (this.state.iteration >= this.state.maxIterations) {
83 | this._updateAttributes();
84 | }
85 | this.setState({lastAttributeChange: this.state.iteration});
86 | }
87 | }
88 |
89 | finalizeState() {
90 | super.finalizeState();
91 |
92 | cancelAnimationFrame(this.state.animation);
93 | }
94 |
95 | animate() {
96 | if (this.state.iteration < this.state.maxIterations) {
97 | const {shortestPathTransform} = this.state;
98 |
99 | shortestPathTransform.run();
100 |
101 | this._updateAttributes();
102 | }
103 | this.state.iteration++;
104 | // Try bind the callback to the latest version of the layer
105 | this.state.animation = requestAnimationFrame(this.animate.bind(this));
106 | }
107 |
108 | _updateAttributes() {
109 | const {shortestPathTransform, nodeAttributesTransform, edgeAttributesTransform, iteration} = this.state;
110 | const props = this.getCurrentLayer().props;
111 |
112 | const moduleParameters = Object.assign(Object.create(props), {
113 | viewport: this.context.viewport
114 | });
115 |
116 | nodeAttributesTransform.run({
117 | moduleParameters,
118 | mode: props.mode,
119 | nodeValueTexture: shortestPathTransform.nodeValueTexture,
120 | distortion: Math.min(iteration / TRANSITION_FRAMES, 1)
121 | });
122 | edgeAttributesTransform.run({
123 | nodePositionsBuffer: nodeAttributesTransform.nodePositionsBuffer
124 | });
125 | }
126 |
127 | _getAttributes(gl) {
128 | return {
129 | nodePositions: new Attribute(gl, {
130 | size: 2,
131 | accessor: 'getNodePosition'
132 | }),
133 | nodeIndices: new Attribute(gl, {
134 | size: 1,
135 | accessor: 'getNodeIndex'
136 | }),
137 | edgeSourceIndices: new Attribute(gl, {
138 | size: 1,
139 | type: GL.INT,
140 | accessor: 'getEdgeSource'
141 | }),
142 | edgeTargetIndices: new Attribute(gl, {
143 | size: 1,
144 | type: GL.INT,
145 | accessor: 'getEdgeTarget'
146 | }),
147 | edgeValues: new Attribute(gl, {
148 | size: 3,
149 | accessor: 'getEdgeValue'
150 | })
151 | };
152 | }
153 |
154 | // Hack: we're using attribute transition with a moving target, so instead of
155 | // interpolating linearly within duration we make duration really long and
156 | // hijack the progress calculation with this easing function
157 | // Can probably remove when constant speed transition is implemented
158 | _transitionEasing(t) {
159 | const {iteration, lastAttributeChange} = this.state;
160 |
161 | const ticks = iteration - lastAttributeChange;
162 | if (ticks <= TRANSITION_FRAMES) {
163 | return ticks / TRANSITION_FRAMES;
164 | }
165 | return 1;
166 | }
167 |
168 | _getIsochronicRings() {
169 | const {data, getNodePosition, sourceIndex, mode} = this.props;
170 |
171 | const sourcePosition = getNodePosition(data.nodes[sourceIndex]);
172 |
173 | return mode === MODE.ISOCHRONIC && [
174 | new ScatterplotLayer(this.getSubLayerProps({
175 | id: 'isochronic-rings-circle',
176 | data: ISOCHRONIC_RINGS,
177 | filled: false,
178 | stroked: true,
179 | lineWidthMinPixels: 1,
180 |
181 | coordinateSystem: COORDINATE_SYSTEM.METER_OFFSETS,
182 | coordinateOrigin: sourcePosition,
183 |
184 | getPosition: d => [0, 0],
185 | getRadius: d => d * ISOCHRONIC_SCALE,
186 | getLineColor: [0, 128, 255]
187 | })),
188 | new TextLayer(this.getSubLayerProps({
189 | id: 'isochronic-rings-legend',
190 | data: ISOCHRONIC_RINGS,
191 |
192 | coordinateSystem: COORDINATE_SYSTEM.METER_OFFSETS,
193 | coordinateOrigin: sourcePosition,
194 |
195 | getTextAnchor: 'start',
196 | getPosition: d => [d * ISOCHRONIC_SCALE, 0],
197 | getText: d => ` ${d / 60} min`,
198 | getSize: 20,
199 | getColor: [0, 128, 255]
200 | }))
201 | ];
202 | }
203 |
204 | renderLayers() {
205 | const {data, getNodePosition} = this.props;
206 | const {nodeAttributesTransform, edgeAttributesTransform, transitionDuration} = this.state;
207 |
208 | const transition = this.props.transition && {
209 | duration: transitionDuration,
210 | easing: this._transitionEasing.bind(this)
211 | };
212 |
213 | return [
214 | new EdgeLayer(this.getSubLayerProps({
215 | id: 'edges',
216 | data: data.edges,
217 | getSourcePosition: d => [0, 0],
218 | getTargetPosition: d => [0, 0],
219 | getColor: [200, 200, 200],
220 | widthScale: 3,
221 |
222 | instanceSourcePositions: edgeAttributesTransform.sourcePositionsBuffer,
223 | instanceTargetPositions: edgeAttributesTransform.targetPositionsBuffer,
224 | instanceValid: edgeAttributesTransform.validityBuffer,
225 |
226 | transitions: transition && {
227 | getSourcePosition: transition,
228 | getTargetPosition: transition,
229 | getIsValid: transition
230 | }
231 | })),
232 |
233 | new ScatterplotLayer(this.getSubLayerProps({
234 | id: 'nodes',
235 | data: data.nodes,
236 | getPosition: getNodePosition,
237 |
238 | instancePositions: {buffer: nodeAttributesTransform.nodePositionsBuffer, size: 4},
239 | instanceFillColors: nodeAttributesTransform.nodeColorsBuffer,
240 | instanceRadius: nodeAttributesTransform.nodeRadiusBuffer,
241 |
242 | transitions: transition && {
243 | getPosition: transition,
244 | getFillColor: transition,
245 | getRadius: transition
246 | },
247 |
248 | pickable: true,
249 | autoHighlight: true,
250 | highlightColor: [0, 200, 255, 200]
251 | })),
252 |
253 | this._getIsochronicRings()
254 | ]
255 | }
256 | }
257 |
258 | GraphLayer.defaultProps = {
259 | mode: MODE.NODE_DISTANCE,
260 | getNodePosition: {type: 'accessor'},
261 | getNodeIndex: {type: 'accessor'},
262 | getEdgeSource: {type: 'accessor'},
263 | getEdgeTarget: {type: 'accessor'},
264 | getEdgeValue: {type: 'accessor'}
265 | };
266 |
--------------------------------------------------------------------------------
/src/graph-layer/node-attributes-transform.js:
--------------------------------------------------------------------------------
1 | import {Buffer, Transform} from '@luma.gl/core';
2 | import {getTextureSize} from './utils';
3 |
4 | import {ISOCHRONIC_SCALE} from './constants';
5 |
6 | export default class NodePositionTransform {
7 | constructor(gl) {
8 | this._nodePositionsBuffer = [
9 | new Buffer(gl, {accessor: {size: 4}, byteLength: 12}),
10 | new Buffer(gl, {accessor: {size: 4}, byteLength: 12})
11 | ];
12 | this._nodeColorsBuffer = [
13 | new Buffer(gl, {accessor: {size: 4}, byteLength: 16}),
14 | new Buffer(gl, {accessor: {size: 4}, byteLength: 16})
15 | ];
16 | this._nodeRadiusBuffer = [
17 | new Buffer(gl, {accessor: {size: 1}, byteLength: 4}),
18 | new Buffer(gl, {accessor: {size: 1}, byteLength: 4})
19 | ];
20 |
21 | this._swapBuffer = false;
22 | this._bufferChanged = false;
23 |
24 | this._transform = this._getTransform(gl);
25 |
26 | this.gl = gl;
27 | }
28 |
29 | get nodePositionsBuffer() {
30 | return this._nodePositionsBuffer[this._swapBuffer ? 1 : 0];
31 | }
32 |
33 | get nodeColorsBuffer() {
34 | return this._nodeColorsBuffer[this._swapBuffer ? 1 : 0];
35 | }
36 |
37 | get nodeRadiusBuffer() {
38 | return this._nodeRadiusBuffer[this._swapBuffer ? 1 : 0];
39 | }
40 |
41 | reset(sourceIndex) {
42 | this._sourcePosition = this._transform.sourceBuffers[0].nodePositions.value.slice(sourceIndex * 2, sourceIndex * 2 + 2);
43 |
44 | if (this._bufferChanged) {
45 | this.update();
46 | }
47 | }
48 |
49 | update({nodeCount = this._nodeCount, attributes = this._attributes} = {}) {
50 | this._swapBuffer = !this._swapBuffer;
51 | this._nodeCount = nodeCount;
52 | this._attributes = attributes;
53 |
54 | const textureSize = getTextureSize(nodeCount);
55 | this.nodePositionsBuffer.reallocate(textureSize.width * textureSize.height * 4 * 4);
56 | this.nodeColorsBuffer.reallocate(textureSize.width * textureSize.height * 4 * 4);
57 | this.nodeRadiusBuffer.reallocate(textureSize.width * textureSize.height * 4);
58 |
59 | this._transform.update({
60 | sourceBuffers: {
61 | nodePositions: attributes.nodePositions,
62 | nodeIndices: attributes.nodeIndices
63 | },
64 | feedbackBuffers: {
65 | position: this.nodePositionsBuffer,
66 | color: this.nodeColorsBuffer,
67 | radius: this.nodeRadiusBuffer
68 | },
69 | elementCount: nodeCount
70 | });
71 | this._bufferChanged = false;
72 | }
73 |
74 | run({moduleParameters, mode, nodeValueTexture, distortion}) {
75 | this._transform.model.updateModuleSettings(moduleParameters);
76 | this._transform.run({
77 | uniforms: {
78 | sourcePosition: this._sourcePosition,
79 | nodeValues: nodeValueTexture,
80 | mode,
81 | distortion,
82 | textureDims: [nodeValueTexture.width, nodeValueTexture.height]
83 | }
84 | });
85 | this._bufferChanged = true;
86 |
87 | // console.log(this.nodeColorsBuffer.getData());
88 | }
89 |
90 | _getTransform(gl) {
91 | return new Transform(gl, {
92 | vs: `\
93 | #version 300 es
94 |
95 | #define MODE_NONE 0
96 | #define MODE_NODE_DISTANCE 1
97 | #define MODE_TRAFFIC 2
98 | #define MODE_ISOCHRONIC 3
99 |
100 | in vec2 nodePositions;
101 | in float nodeIndices;
102 |
103 | uniform int mode;
104 | uniform sampler2D nodeValues;
105 | uniform vec2 textureDims;
106 | uniform vec2 sourcePosition;
107 | uniform float distortion;
108 |
109 | out vec4 position;
110 | out vec4 color;
111 | out float radius;
112 |
113 | const vec4 GREEN = vec4(0., 255., 0., 255.);
114 | const vec4 YELLOW = vec4(255., 255., 0., 255.);
115 | const vec4 RED = vec4(255., 0., 0., 255.);
116 | const vec4 BLACK = vec4(0., 0., 0., 255.);
117 | const vec4 GRAY = vec4(200., 200., 200., 100.);
118 |
119 | ivec2 getVexelCoord(float index) {
120 | float y = floor(index / textureDims.x);
121 | float x = index - textureDims.x * y;
122 | return ivec2(x, y);
123 | }
124 |
125 | vec4 colorScale(float r) {
126 | vec4 c = mix(GREEN, YELLOW, r);
127 | c = mix(c, RED, max(r - 1.0, 0.0));
128 | c = mix(c, BLACK, min(1.0, max(r - 2.0, 0.0)));
129 | return c;
130 | }
131 |
132 | void main() {
133 | vec4 valuePixel = texelFetch(nodeValues, getVexelCoord(nodeIndices), 0);
134 | float travelTime = valuePixel.r;
135 | float streetDistance = valuePixel.g;
136 | float nodeDistance = valuePixel.b;
137 | float isValid = valuePixel.a;
138 |
139 | position = vec4(nodePositions, 0.0, isValid);
140 | color = GRAY;
141 | radius = 10.;
142 |
143 | if (mode == MODE_NODE_DISTANCE) {
144 |
145 | color = mix(color, BLACK, isValid);
146 | radius = mix(radius, sqrt(nodeDistance) * 5., isValid);
147 |
148 | } else if (mode == MODE_TRAFFIC) {
149 |
150 | float r = travelTime / streetDistance * 12.;
151 | r = mix(0.0, r, distortion * isValid);
152 | position.z = r * 400.;
153 | radius = mix(radius, radius * 3.0, isValid);
154 | color = mix(GRAY, colorScale(r), isValid);
155 |
156 | } else if (mode == MODE_ISOCHRONIC) {
157 |
158 | float geoDistance = length((nodePositions - sourcePosition) * project_uCommonUnitsPerWorldUnit.xy / project_uCommonUnitsPerMeter.xy);
159 | float r = travelTime / geoDistance * ${ISOCHRONIC_SCALE.toFixed(1)};
160 | r = mix(1.0, r, distortion * isValid);
161 | position.xy = mix(sourcePosition, nodePositions, r);
162 | radius = mix(radius, radius * 3.0, isValid);
163 | color = mix(GRAY, colorScale(r), isValid);
164 |
165 | }
166 | }
167 | `,
168 | varyings: ['position', 'color', 'radius'],
169 | elementCount: 1,
170 |
171 | feedbackBuffers: {
172 | position: this.nodePositionsBuffer,
173 | color: this.nodeColorsBuffer,
174 | radius: this.nodeRadiusBuffer
175 | }
176 | });
177 | }
178 | }
179 |
--------------------------------------------------------------------------------
/src/graph-layer/shortest-path-transform.js:
--------------------------------------------------------------------------------
1 | import {
2 | Buffer,
3 | Model,
4 | Framebuffer,
5 | clear,
6 | // readPixelsToArray,
7 | readPixelsToBuffer
8 | } from '@luma.gl/core';
9 | import GL from '@luma.gl/constants';
10 |
11 | import {getFloatTexture, getTextureSize, getTexelCoord} from './utils';
12 |
13 | export default class ShortestPathTransform {
14 |
15 | constructor(gl) {
16 | const nodeValueTextures = [
17 | getFloatTexture(gl, 4),
18 | getFloatTexture(gl, 4)
19 | ];
20 | // Mirrors nodeValueTexture
21 | const nodeValueBuffer = new Buffer(gl, {byteLength: 4, accessor: {size: 1, type: GL.FLOAT}});
22 |
23 | const nodeValueFramebuffer = new Framebuffer(gl, {
24 | id: `${this.id || 'transform'}-framebuffer-0`,
25 | width: 1,
26 | height: 1,
27 | attachments: {
28 | [GL.COLOR_ATTACHMENT0]: nodeValueTextures[0]
29 | }
30 | });
31 |
32 | this.gl = gl;
33 | this.nodeValueTexture = null;
34 | this.nodeValueTextures = nodeValueTextures;
35 | this.nodeValueFramebuffer = nodeValueFramebuffer;
36 | this.nodeValueBuffer = nodeValueBuffer;
37 |
38 | this._model = this._getModel(gl);
39 | this._swapTexture = false;
40 | }
41 |
42 | update({nodeCount, edgeCount, attributes}) {
43 | const textureSize = getTextureSize(nodeCount);
44 | this.nodeValueFramebuffer.resize(textureSize);
45 | // 1 float per channel, 4 changels per pixel
46 | this.nodeValueBuffer.reallocate(textureSize.width * textureSize.height * 4 * 4);
47 |
48 | this._model.setVertexCount(edgeCount);
49 |
50 | this._model.setAttributes({
51 | edgeSourceIndices: attributes.edgeSourceIndices,
52 | edgeTargetIndices: attributes.edgeTargetIndices,
53 | edgeValues: attributes.edgeValues
54 | });
55 |
56 | this._model.setUniforms({
57 | textureDims: [textureSize.width, textureSize.height]
58 | })
59 | }
60 |
61 | reset(sourceIndex) {
62 | const {gl, nodeValueFramebuffer, nodeValueTextures} = this;
63 |
64 | for (const texture of nodeValueTextures) {
65 | nodeValueFramebuffer.attach({
66 | [GL.COLOR_ATTACHMENT0]: texture
67 | });
68 | clear(gl, {framebuffer: nodeValueFramebuffer, color: [1e6, 1e6, 1e6, 0]});
69 | texture.setSubImageData({
70 | data: new Float32Array([0, 0, 0, 0]),
71 | ...getTexelCoord(sourceIndex),
72 | width: 1,
73 | height: 1
74 | });
75 | }
76 | }
77 |
78 | run() {
79 | const {_swapTexture, nodeValueFramebuffer, nodeValueTextures, nodeValueBuffer, _model} = this;
80 |
81 | const sourceTexture = nodeValueTextures[_swapTexture ? 0 : 1];
82 | const targetTexture = nodeValueTextures[_swapTexture ? 1 : 0];
83 | nodeValueFramebuffer.attach({
84 | [GL.COLOR_ATTACHMENT0]: targetTexture
85 | });
86 |
87 | _model.draw({
88 | framebuffer: nodeValueFramebuffer,
89 | parameters: {
90 | viewport: [0, 0, targetTexture.width, targetTexture.height],
91 | blend: true,
92 | blendFunc: [GL.ONE, GL.ONE, GL.ONE, GL.ONE],
93 | blendEquation: [GL.MIN, GL.MAX],
94 | depthMask: false,
95 | depthTest: false
96 | },
97 | uniforms: {
98 | nodeValueSampler: sourceTexture,
99 | }
100 | });
101 |
102 | // Copy texture to buffer
103 | // console.log(readPixelsToArray(nodeValueFramebuffer, {sourceFormat: GL.RGBA}));
104 | readPixelsToBuffer(nodeValueFramebuffer, {target: nodeValueBuffer, sourceFormat: GL.RGBA, sourceType: GL.FLOAT});
105 |
106 | this.nodeValueTexture = targetTexture;
107 | this._swapTexture = !this._swapTexture;
108 | }
109 |
110 | _getModel(gl) {
111 | return new Model(gl, {
112 | vs: `#version 300 es
113 |
114 | uniform vec2 textureDims;
115 | uniform sampler2D nodeValueSampler;
116 |
117 | in float edgeSourceIndices;
118 | in float edgeTargetIndices;
119 | in vec3 edgeValues;
120 |
121 | out vec3 value;
122 |
123 | ivec2 getVexelCoord(float index) {
124 | float y = floor(index / textureDims.x);
125 | float x = index - textureDims.x * y;
126 | return ivec2(x, y);
127 | }
128 |
129 | vec2 getTexCoord(float index) {
130 | vec2 texCoord = vec2(getVexelCoord(index)) + 0.5;
131 | texCoord /= textureDims;
132 |
133 | return texCoord;
134 | }
135 |
136 | void main() {
137 | vec2 texCoord = getTexCoord(edgeTargetIndices);
138 | gl_Position = vec4(texCoord * 2.0 - 1.0, 0.0, 1.0);
139 |
140 | vec3 sourceValue = texelFetch(nodeValueSampler, getVexelCoord(edgeSourceIndices), 0).rgb;
141 | value = sourceValue + edgeValues;
142 | }
143 | `,
144 | fs: `#version 300 es
145 | precision highp float;
146 | #define MAX_VALUE 1000000.0
147 |
148 | in vec3 value;
149 | out vec4 color;
150 |
151 | void main() {
152 | if (value.r >= MAX_VALUE) {
153 | discard;
154 | }
155 | color = vec4(value, 1.0);
156 | }
157 | `,
158 | drawMode: GL.POINTS
159 | });
160 | }
161 |
162 | }
163 |
--------------------------------------------------------------------------------
/src/graph-layer/utils.js:
--------------------------------------------------------------------------------
1 | import GL from '@luma.gl/constants';
2 | import {Texture2D} from '@luma.gl/core';
3 |
4 | const TEXTURE_WIDTH = 512;
5 |
6 | const TEXTURE_FORMATS = {
7 | 1: GL.R32F,
8 | 2: GL.RG32F,
9 | 3: GL.RGB32F,
10 | 4: GL.RGBA32F
11 | }
12 | const DATA_FORMATS = {
13 | 1: GL.RED,
14 | 2: GL.RG,
15 | 3: GL.RGB,
16 | 4: GL.RGBA
17 | }
18 |
19 | function getNextPOT(x) {
20 | return Math.pow(2, Math.max(0, Math.ceil(Math.log2(x))));
21 | }
22 |
23 | export function getTexelCoord(index) {
24 | return {
25 | x: index % TEXTURE_WIDTH,
26 | y: Math.floor(index / TEXTURE_WIDTH)
27 | }
28 | }
29 |
30 | export function getTextureSize(nodeCount) {
31 | const width = TEXTURE_WIDTH;
32 | const height = getNextPOT(nodeCount / TEXTURE_WIDTH);
33 | return {width, height};
34 | }
35 |
36 | export function getFloatTexture(gl, size) {
37 | return new Texture2D(gl, {
38 | data: null,
39 | format: TEXTURE_FORMATS[size],
40 | type: GL.FLOAT,
41 | border: 0,
42 | mipmaps: false,
43 | parameters: {
44 | [GL.TEXTURE_MAG_FILTER]: GL.NEAREST,
45 | [GL.TEXTURE_MIN_FILTER]: GL.NEAREST
46 | },
47 | dataFormat: DATA_FORMATS[size],
48 | width: 1,
49 | height: 1
50 | });
51 | }
52 |
53 | export function walk(graph, startNode, maxDepth = 10, depth = 0) {
54 | if (startNode.visited || depth > maxDepth) {
55 | return;
56 | }
57 | startNode.visited = true;
58 | for (const edgeId in graph.edges) {
59 | const edge = graph.edges[edgeId];
60 | if (edge.hours[0] && edge.start_junction_id === startNode.id) {
61 | walk(graph, graph.nodesById[edge.end_junction_id], maxDepth, depth + 1);
62 | }
63 | }
64 | };
65 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
4 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | overflow: hidden;
9 | }
10 |
11 | code {
12 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
13 | monospace;
14 | }
15 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './Main';
5 |
6 | ReactDOM.render(, document.getElementById('root'));
7 |
--------------------------------------------------------------------------------
/src/load-data.js:
--------------------------------------------------------------------------------
1 | const AWS_PREFIX =
2 | "https://uber-common-public.s3-us-west-2.amazonaws.com/svc-vis-prototype/vis-hackathon-isochronic-map";
3 |
4 | export default async function loadData(city) {
5 | const edgesText = await fetch(`${AWS_PREFIX}/${city}/edges-sm.csv`).then(res => res.text());
6 | const nodesText = await fetch(`${AWS_PREFIX}/${city}/nodes-sm.csv`).then(res => res.text());
7 |
8 | const nodes = parseCSV(nodesText);
9 | const edges = parseCSV(edgesText);
10 |
11 | console.log(`Loaded ${city}. ${nodes.length} nodes, ${edges.length} edges`);
12 | return {nodes, edges};
13 | }
14 |
15 | function parseCSV(text) {
16 | const lines = text.split('\n').filter(Boolean);
17 | const headers = lines.shift().split(',');
18 | return lines.map(line => {
19 | const values = line.split(',');
20 | const row = {};
21 | for (let i = 0; i < headers.length; i++) {
22 | const name = headers[i];
23 | row[name] = name === 'times_by_hour' ? values[i].split('\t').map(Number) : Number(values[i]);
24 | }
25 | return row;
26 | });
27 | }
28 |
--------------------------------------------------------------------------------
/src/map.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import DeckGL from "deck.gl";
3 | import GraphLayer from './graph-layer/graph-layer.js';
4 |
5 | const Map = ({viewState, graph, mode, setSourceIndex, setViewState, hour, sourceIndex}) => (
6 |
7 | {
12 | setViewState(viewState)
13 | }}
14 | style={{
15 | left: '280px',
16 | width: 'calc(100vw - 280px)'
17 | }}
18 | layers={[
19 | new GraphLayer({
20 | data: graph,
21 | sourceIndex,
22 | onClick: ({index}) => {
23 | setSourceIndex(index);
24 | },
25 | getNodePosition: d => [d.lon, d.lat],
26 | getNodeIndex: (d, {index}) => index,
27 | getEdgeSource: d => d.start,
28 | getEdgeTarget: d => d.end,
29 | getEdgeValue: d => [
30 | d.times_by_hour[hour] || 1e6,
31 | d.distance,
32 | 1
33 | ],
34 |
35 | mode,
36 |
37 | transition: true,
38 |
39 | updateTriggers: {
40 | getEdgeValue: hour
41 | }
42 | })
43 | ]}
44 | />
45 |
46 | );
47 |
48 | export default Map;
49 |
--------------------------------------------------------------------------------
/src/old/binary-heap.js:
--------------------------------------------------------------------------------
1 | class BinaryHeap {
2 | constructor(scoreFunction = n => n) {
3 | this.content = [];
4 | this.scoreFunction = scoreFunction;
5 | }
6 |
7 | bubbleUp = N => {
8 | // Fetch the element that has to be moved.
9 | const element = this.content[N];
10 | const score = this.scoreFunction(element);
11 | let n = N;
12 | // When at 0, an element can not go up any further.
13 | while (n > 0) {
14 | // Compute the parent element's index, and fetch it.
15 | const parentN = Math.floor((n + 1) / 2) - 1;
16 | const parent = this.content[parentN];
17 | // If the parent has a lesser score, things are in order and we
18 | // are done.
19 | if (score < this.scoreFunction(parent)) {
20 | // Otherwise, swap the parent with the current element and
21 | // continue.
22 | this.content[parentN] = element;
23 | this.content[n] = parent;
24 | n = parentN;
25 | } else {
26 | // exit the loop;
27 | n = 0;
28 | }
29 | }
30 | }
31 |
32 | sinkDown = N =>{
33 | // Look up the target element and its score.
34 | const length = this.content.length;
35 | const element = this.content[N];
36 | const elemScore = this.scoreFunction(element);
37 | let swap = element;
38 | let n = N;
39 | while(swap) {
40 | // Compute the indices of the child elements.
41 | const child2N = (n + 1) * 2;
42 | const child1N = child2N - 1;
43 | // This is used to store the new position of the element,
44 | // if any.
45 | swap = null;
46 | // If the first child exists (is inside the array)...
47 | let child1Score;
48 | if (child1N < length) {
49 | // Look it up and compute its score.
50 | const child1 = this.content[child1N],
51 | child1Score = this.scoreFunction(child1);
52 | // If the score is less than our element's, we need to swap.
53 | if (child1Score < elemScore)
54 | swap = child1N;
55 | }
56 | // Do the same checks for the other child.
57 | if (child2N < length) {
58 | const child2 = this.content[child2N],
59 | child2Score = this.scoreFunction(child2);
60 | if (child2Score < (swap === null ? elemScore : child1Score))
61 | swap = child2N;
62 | }
63 |
64 | // if there is a swappable element
65 | if (swap) {
66 | // swap and continue.
67 | this.content[n] = this.content[swap];
68 | this.content[swap] = element;
69 | n = swap;
70 | }
71 | // else we'll exit the loop.
72 | }
73 | };
74 | size = () => this.content.length;
75 | push = element => {
76 | this.content.push(element);
77 | this.bubbleUp(this.content.length - 1);
78 | };
79 | pop = () => {
80 | // Store the first element so we can return it later.
81 | const result = this.content[0];
82 | // Get the element at the end of the array.
83 | const end = this.content.pop();
84 | // If there are any elements left, put the end element at the
85 | // start, and let it sink down.
86 | if (this.content.length > 0) {
87 | this.content[0] = end;
88 | this.sinkDown(0);
89 | }
90 | return result;
91 | };
92 | remove = value => {
93 | const length = this.content.length;
94 | const i = this.content.indexOf(value);
95 | const end = this.content.pop();
96 | if (i !== length - 1) {
97 | this.content[i] = end;
98 | this.bubbleUp(i);
99 | this.sinkDown(i);
100 | }
101 | }
102 | }
103 |
104 | export default BinaryHeap;
105 |
--------------------------------------------------------------------------------
/src/old/compute-graph.js:
--------------------------------------------------------------------------------
1 | import BinaryHeap from "./binary-heap";
2 |
3 | const R = 6371e3; // metres
4 | const k = 0.44704 * 30;
5 |
6 | export function computeGraph({hour, startNode, edges, nodes}) {
7 | const nodeHash = nodes.reduce((prev, curr, idx) => {
8 | const {id, lat, lon} = curr;
9 | prev[id] = {
10 | idx,
11 | lat: Number(lat),
12 | lon: Number(lon),
13 | timeToReach: Infinity,
14 | neighbors: []
15 | };
16 | return prev;
17 | }, {});
18 |
19 | const segmentLookup = Object.values(edges).reduce((prev, curr) => {
20 | const {start_junction_id, end_junction_id, hours} = curr;
21 | if (hours[hour]) {
22 | if (!prev[start_junction_id]) {
23 | addAngle(start_junction_id, nodeHash, startNode);
24 | prev[start_junction_id] = {};
25 | }
26 | addAngle(end_junction_id, nodeHash, startNode);
27 | nodeHash[start_junction_id].neighbors.push(end_junction_id);
28 | prev[start_junction_id][end_junction_id] = {...hours[hour]};
29 | }
30 | return prev;
31 | }, {});
32 |
33 | const Queue = new BinaryHeap(node => nodeHash[node].timeToReach);
34 |
35 | nodes.forEach(n => {
36 | nodeHash[n.id].timeToReach = n.id === startNode ? 0 : Infinity;
37 | Queue.push(n.id);
38 | });
39 |
40 | while (Queue.size()) {
41 | const current = Queue.pop();
42 | const timeCurrent = nodeHash[current].timeToReach;
43 | if (timeCurrent !== Infinity) {
44 | nodeHash[current].neighbors.forEach(neighbor => {
45 | const segment = segmentLookup[current][neighbor];
46 | const timeSegment = segment.time;
47 | const tentativeTimeToReach = timeCurrent + timeSegment;
48 | if (tentativeTimeToReach < nodeHash[neighbor].timeToReach) {
49 | Queue.remove(neighbor);
50 | nodeHash[neighbor].timeToReach = tentativeTimeToReach;
51 | Queue.push(neighbor);
52 | }
53 | });
54 | }
55 | }
56 |
57 | nodes.forEach(n => {
58 | const node = nodeHash[n.id];
59 | if (node.timeToReach < Infinity) {
60 | node.position = destination(
61 | nodeHash[startNode],
62 | node.bearing,
63 | k * node.timeToReach
64 | )
65 | }
66 | });
67 | return {nodes, segmentLookup};
68 | }
69 |
70 | function toRadians(deg) {
71 | return (deg * Math.PI) / 180;
72 | }
73 | function toDegrees(rad) {
74 | return (rad * 180) / Math.PI;
75 | }
76 | function addAngle(nodeId, hash, startNode) {
77 | const bearing = getBearing(hash[startNode], hash[nodeId]);
78 | hash[nodeId].bearing = bearing;
79 | }
80 |
81 | function getBearing(a, b) {
82 | const λ1 = toRadians(a.lon);
83 | const λ2 = toRadians(b.lon);
84 | const φ1 = toRadians(a.lat);
85 | const φ2 = toRadians(b.lat);
86 | const y = Math.sin(λ2 - λ1) * Math.cos(φ2);
87 | const x =
88 | Math.cos(φ1) * Math.sin(φ2) -
89 | Math.sin(φ1) * Math.cos(φ2) * Math.cos(λ2 - λ1);
90 | return Math.atan2(y, x);
91 | }
92 |
93 | function destination(source, bearing, distance) {
94 | const lat = toRadians(source.lat);
95 | const lon = toRadians(source.lon);
96 |
97 | const destLat = Math.asin(
98 | Math.sin(lat) * Math.cos(distance / R) +
99 | Math.cos(lat) * Math.sin(distance / R) * Math.cos(bearing)
100 | );
101 | const destLon =
102 | lon +
103 | Math.atan2(
104 | Math.sin(bearing) * Math.sin(distance / R) * Math.cos(lat),
105 | Math.cos(distance / R) - Math.sin(lat) * Math.sin(destLat)
106 | );
107 | return {lat: toDegrees(destLat), lon: toDegrees(destLon)};
108 | }
109 |
--------------------------------------------------------------------------------