├── .gitignore ├── .npmignore ├── README.md ├── demo.gif ├── example-overlay.js ├── example └── main.js ├── main.fragment.glsl ├── main.vertex.glsl ├── package.json └── pickups-1m.csv.binary /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | screenshot.png 2 | lng-lat-jitter.gif 3 | lng-lat-pan-no-jitter.giflng-lat-check-for-optimization.gif 4 | pickups-1m.csv.binary 5 | demo.gif 6 | example 7 | trip_data_1.csv 8 | trip_data_1.csv.binary 9 | save-as-binary.js 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-map-gl-stack-gl-overlay-example 2 | 3 | An experiment in using stackGL in a 4 | [react-map-gl](https://github.com/uber/react-map-gl) overlay. 5 | 6 | See a [live demo on gh-pages](http://vicapow.github.io/react-map-gl-stack-gl-overlay-example/) 7 | 8 | ![](demo.gif) 9 | 10 | This demo uses data from http://www.andresmh.com/nyctaxitrips/ 11 | 12 | ## Usage 13 | 14 | ````js 15 | render: function render() { 16 | return 17 | 22 | ; 23 | } 24 | ```` 25 | 26 | See example/main.js for a full example 27 | 28 | ## Passing Tile Coordinates to GPU 29 | 30 | This has evolved into a larger experiment in passing projected tile coordinates 31 | directly to the GPU as a performance optimization. It represents each tile 32 | coordinate as a double using two float values and uses the double-float 33 | functions from [Andrew Thall](http://andrewthall.org/)'s 34 | [Extended-Precision Floating-Point Numbers for GPU Computation](http://andrewthall.org/papers/df64_qf128.pdf). 35 | 36 | See also: 37 | 38 | 1. [Improving precision in your vertex transform](http://github.prideout.net/emulating-double-precision/) 39 | 2. [Double Precision in OpenGL and WebGL](http://blog.hvidtfeldts.net/index.php/2012/07/double-precision-in-opengl-and-webgl/) 40 | 3. [Heavy computing with GLSL – Part 2: Emulated double precision](https://www.thasler.com/blog/blog/glsl-part2-emu) 41 | 42 | ## To install 43 | 44 | npm install 45 | 46 | ## To run 47 | 48 | npm run start 49 | 50 | This will start a budo server running on localhost:9966. 51 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vicapow/react-map-gl-stack-gl-overlay-example/8e3270c8548265af79189585abc4136677e5998f/demo.gif -------------------------------------------------------------------------------- /example-overlay.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react/addons'); 4 | var PureRenderMixin = React.addons.PureRenderMixin; 5 | var window = require('global/window'); 6 | var r = require('r-dom'); 7 | var getContext = require('get-canvas-context'); 8 | var glslify = require('glslify'); 9 | var createShader = require('gl-shader'); 10 | var createVAO = require('gl-vao'); 11 | var createBuffer = require('gl-buffer'); 12 | 13 | var PI = Math.PI; 14 | var DEGREES_TO_RADIANS = PI / 180; 15 | 16 | function radians(degrees) { 17 | return degrees * DEGREES_TO_RADIANS; 18 | } 19 | 20 | // Convert a JavaScript double precision Number to the equivalent single 21 | // precision value. 22 | function float(value) { 23 | var array = new Float32Array(1); 24 | array[0] = value; 25 | return array[0]; 26 | } 27 | 28 | // Split a Number of double precision into two Numbers of single precision. 29 | function split(a) { 30 | var tHi = float(a); 31 | var tLo = a - tHi; 32 | return [tHi, tLo]; 33 | } 34 | 35 | var CanvasOverlay = React.createClass({ 36 | 37 | displayName: 'StackGLOverlay', 38 | 39 | mixins: [PureRenderMixin], 40 | 41 | propTypes: { 42 | width: React.PropTypes.number, 43 | height: React.PropTypes.number, 44 | zoom: React.PropTypes.number.isRequired, 45 | latitude: React.PropTypes.number.isRequired, 46 | longitude: React.PropTypes.number.isRequired, 47 | isDragging: React.PropTypes.bool, 48 | locations: React.PropTypes.array.isRequired, 49 | lngLatAccessor: React.PropTypes.func.isRequired 50 | }, 51 | 52 | getDefaultProps: function getDefaultProps() { 53 | return { 54 | lngLatAccessor: function defaultLocationAccessor(location) { 55 | return location; 56 | } 57 | }; 58 | }, 59 | 60 | componentWillReceiveProps: function componentWillReceiveProps(nextProps) { 61 | if (nextProps.locations !== this.props.locations) { 62 | this._updateVAO(); 63 | } 64 | }, 65 | 66 | _getLocationsBufferArray: function _getLocationsBufferArray() { 67 | var ret = []; 68 | var locations = this.props.locations || []; 69 | var location; 70 | var x; 71 | var y; 72 | var phi; 73 | var lamda; 74 | for (var i = 0; i < locations.length; i++) { 75 | location = this.props.lngLatAccessor(locations[i]); 76 | lamda = radians(location[0]); 77 | phi = radians(location[1]); 78 | // [0, 1] 79 | x = (lamda + Math.PI) / Math.PI * 0.5; 80 | // [0, 1] 81 | y = (PI + Math.log(Math.tan(PI * 0.25 + phi * 0.5))) / Math.PI * 0.5; 82 | ret.push(x); 83 | ret.push(y); 84 | } 85 | return ret; 86 | }, 87 | 88 | _updateVAO: function _updateVAO() { 89 | var gl = this._ctx; 90 | if (this._vao) { 91 | this._vao.dispose(); 92 | } 93 | this._vao = createVAO(gl, [{ 94 | buffer: createBuffer(gl, this._getLocationsBufferArray()), 95 | type: gl.FLOAT, 96 | size: 2 97 | }]); 98 | }, 99 | 100 | componentDidMount: function componentDidMount() { 101 | var gl = this._ctx = getContext('webgl', {canvas: this.getDOMNode()}); 102 | this._shader = createShader(gl, 103 | glslify('./main.vertex.glsl'), 104 | glslify('./main.fragment.glsl')); 105 | this._shader.attributes.coordinate.location = 0; 106 | this._updateVAO(); 107 | this._redraw(); 108 | }, 109 | 110 | componentDidUpdate: function componentDidUpdate() { 111 | this._redraw(); 112 | }, 113 | 114 | _redraw: function _redraw() { 115 | var gl = this._ctx; 116 | gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); 117 | gl.blendFunc(gl.ONE, gl.ONE); 118 | gl.enable(gl.BLEND); 119 | gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); 120 | this._shader.bind(); 121 | var lnglat = [this.props.longitude, this.props.latitude]; 122 | var zoom = this.props.zoom; 123 | var tileSize = 512; 124 | var scale = tileSize * Math.pow(2, zoom); 125 | var lambda = radians(lnglat[0]); 126 | var phi = radians(lnglat[1]); 127 | var x = (lambda + Math.PI) / Math.PI * 0.5; 128 | var y = (PI + Math.log(Math.tan(PI * 0.25 + phi * 0.5))) / Math.PI * 0.5; 129 | this._shader.uniforms.alpha = 0.001; 130 | var pointSize = 32 * Math.pow(2, zoom - 20); 131 | pointSize = Math.max(window.devicePixelRatio || 1, pointSize); 132 | this._shader.uniforms.pointSize = pointSize; 133 | this._shader.uniforms.scale = split(scale); 134 | this._shader.uniforms.center = split(x).concat(split(y)); 135 | this._shader.uniforms.dimensions = [this.props.width, this.props.height]; 136 | this._shader.uniforms.color = [231 / 255, 76 / 255, 60 / 255, 1]; 137 | this._vao.bind(); 138 | this._vao.draw(gl.POINTS, this.props.locations.length); 139 | this._vao.unbind(); 140 | }, 141 | 142 | render: function render() { 143 | var pixelRatio = window.devicePixelRatio || 1; 144 | return r.canvas({ 145 | ref: 'overlay', 146 | width: this.props.width * pixelRatio, 147 | height: this.props.height * pixelRatio, 148 | style: { 149 | width: this.props.width + 'px', 150 | height: this.props.height + 'px', 151 | position: 'absolute', 152 | pointerEvents: 'none', 153 | left: 0, 154 | top: 0 155 | } 156 | }); 157 | } 158 | }); 159 | 160 | module.exports = CanvasOverlay; 161 | -------------------------------------------------------------------------------- /example/main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var document = require('global/document'); 4 | var window = require('global/window'); 5 | var console = require('global/console'); 6 | var React = require('react'); 7 | var Immutable = require('immutable'); 8 | var r = require('r-dom'); 9 | var MapGL = require('react-map-gl'); 10 | var ExampleOverlay = require('../example-overlay'); 11 | var assign = require('object-assign'); 12 | var d3 = require('d3'); 13 | 14 | // Don't show tiles in the map. 15 | var mapStyle = Immutable.fromJS({ 16 | version: 8, 17 | sources: {}, 18 | layers: [ 19 | { 20 | id: 'background', 21 | type: 'background', 22 | minzoom: 0, 23 | maxzoom: 22, 24 | paint: { 25 | 'background-color': 'black' 26 | } 27 | } 28 | ] 29 | }); 30 | 31 | var App = React.createClass({ 32 | 33 | displayName: 'App', 34 | 35 | getInitialState: function getInitialState() { 36 | return { 37 | appWidth: window.innerWidth, 38 | appHeight: window.innerHeight, 39 | viewport: { 40 | latitude: 40.74, 41 | longitude: -73.97, 42 | zoom: 12 43 | } 44 | }; 45 | }, 46 | 47 | componentDidMount: function componentDidMount() { 48 | window.addEventListener('resize', function onResize() { 49 | this.setState({ 50 | appWidth: window.innerWidth, 51 | appHeight: window.innerHeight 52 | }); 53 | }.bind(this)); 54 | 55 | d3.xhr('./pickups-1m.csv.binary') 56 | .responseType('arraybuffer') 57 | .get(function response(error, xhr) { 58 | if (error) { 59 | throw error; 60 | } 61 | var payload = new Float32Array(xhr.response); 62 | var trips = []; 63 | var index = 0; 64 | while (index < payload.length) { 65 | trips.push([payload[index++], payload[index++]]); 66 | } 67 | /* eslint-disable no-console */ 68 | console.log('num trips', trips.length); 69 | /* eslint-enable no-console */ 70 | this.setState({trips: trips}); 71 | }.bind(this)); 72 | }, 73 | 74 | _onChangeViewport: function _onChangeViewport(viewport) { 75 | viewport.zoom = d3.round(viewport.zoom, 4); 76 | this.setState({viewport: viewport}); 77 | }, 78 | 79 | render: function render() { 80 | var props = assign({}, this.state.viewport, { 81 | width: this.state.appWidth, 82 | height: this.state.appHeight, 83 | mapStyle: mapStyle, 84 | onChangeViewport: this._onChangeViewport 85 | }); 86 | return r(MapGL, props, [ 87 | this.state.trips ? r(ExampleOverlay, { 88 | locations: this.state.trips, 89 | lngLatAccessor: this._tripLngLatAccessor, 90 | zoom: this.state.viewport.zoom, 91 | longitude: this.state.viewport.longitude, 92 | latitude: this.state.viewport.latitude 93 | }) : null 94 | ]); 95 | } 96 | }); 97 | document.body.style.margin = 0; 98 | React.render(r(App), document.body); 99 | -------------------------------------------------------------------------------- /main.fragment.glsl: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | 3 | varying vec4 fragColor; 4 | 5 | void main() { 6 | // Uncommon to draw points as circles instead of squares. 7 | if (distance(gl_PointCoord.st, vec2(0.5, 0.5)) > 0.5) { 8 | discard; 9 | } 10 | gl_FragColor = fragColor; 11 | } -------------------------------------------------------------------------------- /main.vertex.glsl: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | 3 | // A "double-float" is a double represented as two floats (hi and lo). 4 | 5 | attribute vec2 coordinate; 6 | 7 | // A double-float of the current scale on the tile. 8 | uniform vec2 scale; 9 | // A double-float of the current tile offset coordinates. 10 | uniform vec4 center; 11 | uniform vec2 dimensions; 12 | uniform vec4 color; 13 | uniform float alpha; 14 | uniform float pointSize; 15 | 16 | varying vec4 fragColor; 17 | 18 | // Originally from: http://andrewthall.org/papers/df64_qf128.pdf 19 | vec2 split(float a) { 20 | const float SPLIT = 4097.0; // (1 << 12) + 1; 21 | float t = a * SPLIT; 22 | float ahi = t - (t - a); 23 | // float ahi = - a; 24 | float alo = a - ahi; 25 | return vec2(ahi, alo); 26 | } 27 | 28 | vec2 twoProd(float a, float b) { 29 | float p = a * b; 30 | vec2 aS = split(a); 31 | vec2 bS = split(b); 32 | float err = ((aS.x * bS.x - p) + aS.x * bS.y + aS.y * bS.x) + aS.y * bS.y; 33 | return vec2(p, err); 34 | } 35 | 36 | vec2 twoSum(float a , float b) { 37 | float s = a + b; 38 | float v = s - a; 39 | float e = (a - (s - v)) + (b - v); 40 | return vec2(s, e); 41 | } 42 | 43 | vec2 quickTwoSum(float a, float b) { 44 | float s = a + b; 45 | float e = b - (s - a); 46 | return vec2(s, e); 47 | } 48 | 49 | vec2 df64mult(vec2 a, vec2 b) { 50 | vec2 p = twoProd(a.x, b.x); 51 | p.y += a.x * b.y; 52 | p.y += a.y * b.x; 53 | return quickTwoSum(p.x, p.y); 54 | } 55 | 56 | vec4 twoSumComp(vec2 ari, vec2 bri) { 57 | vec2 s = ari + bri; 58 | vec2 v = s - ari; 59 | vec2 e = (ari - (s - v)) + (bri - v); 60 | return vec4(s.x, e.x, s.y, e.y); 61 | } 62 | 63 | vec2 df64add(vec2 a, vec2 b) { 64 | vec4 st; 65 | st = twoSumComp(a, b); 66 | st.y += st.z; 67 | st.xy = quickTwoSum(st.x, st.y); 68 | st.y += st.w; 69 | st.xy = quickTwoSum(st.x, st.y); 70 | return st.xy; 71 | } 72 | 73 | void main() { 74 | vec2 centerX = center.xy; 75 | vec2 centerY = center.zw; 76 | 77 | vec2 coordX = df64mult(df64add(vec2(coordinate.x, 0), -centerX), scale); 78 | vec2 coordY = df64mult(df64add(vec2(coordinate.y, 0), -centerY), scale); 79 | 80 | vec2 point = vec2(coordX[0], coordY[0]); 81 | 82 | gl_Position = vec4(point / dimensions * 2.0, 0.0, 1.0); 83 | gl_PointSize = pointSize; 84 | fragColor = color; 85 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-map-gl-stack-gl-overlay-example", 3 | "version": "0.3.0", 4 | "description": "", 5 | "main": "example-overlay.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/vicapow/react-map-gl-stack-gl-overlay-example.git" 9 | }, 10 | "scripts": { 11 | "test": "echo \"Error: no test specified\" && exit 1", 12 | "start": "budo ./example/main.js --live -- -t envify | bistre", 13 | "lint": "uber-standard", 14 | "precommit": "npm run lint -s", 15 | "prepush": "npm run lint -s" 16 | }, 17 | "keywords": [ 18 | "react", 19 | "mapbox", 20 | "mapbox-gl", 21 | "react-map-gl", 22 | "map-gl", 23 | "stack-gl", 24 | "gl", 25 | "map", 26 | "overlay", 27 | "example" 28 | ], 29 | "author": "Victor Powell", 30 | "license": "ISC", 31 | "peerDependencies": { 32 | "react-map-gl": "^0.2.4", 33 | "r-dom": "^1.3.0", 34 | "react": "^0.13.3", 35 | "immutable": "^3.7.5" 36 | }, 37 | "dependencies": { 38 | "get-canvas-context": "^1.0.1", 39 | "gl-buffer": "^2.1.2", 40 | "gl-now": "^1.4.0", 41 | "gl-shader": "^4.0.6", 42 | "gl-texture2d": "^2.0.10", 43 | "gl-vao": "^1.2.1", 44 | "global": "^4.3.0", 45 | "glslify": "^2.3.1", 46 | "object-assign": "^4.0.1" 47 | }, 48 | "browserify": { 49 | "transform": [ 50 | "glslify" 51 | ] 52 | }, 53 | "devDependencies": { 54 | "bistre": "^1.0.1", 55 | "budo": "^6.0.1", 56 | "d3": "^3.5.9", 57 | "envify": "^3.4.0", 58 | "immutable": "^3.7.5", 59 | "r-dom": "^1.3.0", 60 | "react": "^0.13.3", 61 | "react-map-gl": "^0.2.4", 62 | "save": "^2.1.1", 63 | "uber-standard": "^5.1.0" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /pickups-1m.csv.binary: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vicapow/react-map-gl-stack-gl-overlay-example/8e3270c8548265af79189585abc4136677e5998f/pickups-1m.csv.binary --------------------------------------------------------------------------------