├── .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 | d.value === mapType)]} 53 | clearable={false} 54 | options={mapTypes} 55 | labelKey="label" 56 | valueKey="value" 57 | onChange={({value}) => { 58 | setMapType(value[0].value); 59 | }} 60 | /> 61 | 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 | --------------------------------------------------------------------------------