Skip to content

Instantly share code, notes, and snippets.

@migurski
Last active December 14, 2015 18:38
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save migurski/5130639 to your computer and use it in GitHub Desktop.
GL-Solar, Rainbow Road edition
//
// features_list() is used via a Web Worker, called from Map class in map.js.
//
self.addEventListener('message', onmessage);
var pi = Math.PI;
var rpm = 1/6378137;
function onmessage(e)
{
var start = (new Date()).getTime();
var node_id = e.data.node_id,
features = e.data.features,
zoom = e.data.zoom,
list = features_list(features, zoom);
var end = (new Date()).getTime();
self.postMessage({node_id: node_id, list: list, elapsed: end - start});
}
function features_list(features, zoom)
{
var reds = {motorway: 1, motorway_link: 1, trunk: 1, trunk_link: 1, primary: 1};
var pixel = 2 * pi / (1 << (zoom + 8)),
floats = [];
for(var i in features)
{
var props = features[i]['properties'],
geometry = features[i]['geometry'],
parts = (geometry['type'] == 'LineString') ? [geometry['coordinates']] : geometry['coordinates'];
if(zoom < 14 && props['kind'] != 'major_road' && props['kind'] != 'highway')
{
continue;
}
var widths = highway_widths(props['highway'], props['kind'], zoom),
inner = widths[0],
outer = widths[1],
incap = inner/7,
outcap = inner/20;
var layer = highway_layer(props['highway'], props['explicit_layer'], props['is_bridge'], props['is_tunnel']);
for(var j in parts)
{
for(var k = parts[j].length - 2; k >= 0; k--)
{
// Positions of line segment start and end in mercator
var loc1 = {lon: parts[j][k][0], lat: parts[j][k][1]},
loc2 = {lon: parts[j][k+1][0], lat: parts[j][k+1][1]},
pA = project(loc1),
pZ = project(loc2),
len = hypot(pA, pZ);
// replace end point
parts[j].splice(k+1, 1, pZ);
var pieces = Math.floor(len / (10 * pixel)),
length = len / pieces;
for(var l = 1; l < pieces; l++)
{
var p = lerp(pA, pZ, 1 - l/pieces);
// splice in new point
parts[j].splice(k+1, 0, p);
}
}
// use the final pA from above
parts[j][0] = pA;
for(var k = 0; k < parts[j].length - 1; k++)
{
// Positions of line segment start and end in mercator
var p1 = parts[j][k],
p2 = parts[j][k+1];
// Offsets to the front, back and sides of line segment in mercator
var θ = Math.atan2(p2.y - p1.y, p2.x - p1.x),
ux = Math.cos(θ),
uy = Math.sin(θ),
vx = Math.cos(θ + pi/2),
vy = Math.sin(θ + pi/2);
// Positions of outer corners of line segment capped line in mercator
var pa = {x: p1.x - vx*inner - ux*incap, y: p1.y - vy*inner - uy*incap},
pb = {x: p2.x - vx*inner + ux*incap, y: p2.y - vy*inner + uy*incap},
pc = {x: p2.x + vx*inner + ux*incap, y: p2.y + vy*inner + uy*incap},
pd = {x: p1.x + vx*inner - ux*incap, y: p1.y + vy*inner - uy*incap},
z = layer;
// Render colors, including alpha value based on is_tunnel
var r = (props['highway'] in reds) ? 211/255 : 147/255,
g = (props['highway'] in reds) ? 54/255 : 161/255,
b = (props['highway'] in reds) ? 130/255 : 161/255,
a = (props['is_tunnel'] == 'yes') ? .4 : 1;
// Two triangles covering this line segment, with (x, y, z, r, g, b, a) values.
floats = floats.concat([pa.x, pa.y, z, 1, 0, 0, a, k, pb.x, pb.y, z, 1,.5, 0, a, k, pc.x, pc.y, z, 1, 1, 0, a, k]);
floats = floats.concat([pa.x, pa.y, z, 1, 0, 0, a, k, pc.x, pc.y, z, 1, 1, 0, a, k, pd.x, pd.y, z, 1,.5, 0, a, k]);
// Two additional triangles for bridge casings.
if(zoom >= 15 && props['is_bridge'] == 'yes')
{
// Positions of outer corners of line segment capped line in mercator
var pa = {x: p1.x - vx*outer - ux*outcap, y: p1.y - vy*outer - uy*outcap},
pb = {x: p2.x - vx*outer + ux*outcap, y: p2.y - vy*outer + uy*outcap},
pc = {x: p2.x + vx*outer + ux*outcap, y: p2.y + vy*outer + uy*outcap},
pd = {x: p1.x + vx*outer - ux*outcap, y: p1.y + vy*outer - uy*outcap},
z = layer - 10;
// Render colors for map background and adjusted z-index.
var r = 0 /* 253/255 */,
g = 0 /* 246/255 */,
b = 0 /* 227/255 */;
floats = floats.concat([pa.x, pa.y, z, r, g, b, a, -1, pb.x, pb.y, z, r, g, b, a, -1, pc.x, pc.y, z, r, g, b, a, -1]);
floats = floats.concat([pa.x, pa.y, z, r, g, b, a, -1, pc.x, pc.y, z, r, g, b, a, -1, pd.x, pd.y, z, r, g, b, a, -1]);
}
}
}
}
return floats;
}
function project(loc)
{
var λ = pi * loc.lon / 180,
φ = pi * loc.lat / 180;
var x = λ,
y = Math.log(Math.tan(pi/4 + φ/2));
return {x: x, y: y};
}
function hypot(pA, pB)
{
return Math.sqrt(Math.pow(pA.x - pB.x, 2) + Math.pow(pA.y - pB.y, 2));
}
function lerp(pA, pB, amt)
{
return {x: pA.x * (1-amt) + pB.x * amt, y: pA.y * (1-amt) + pB.y * amt};
}
//
// Larger numbers cause roads to shrink faster on zoom out.
//
var highway_coefficients = {
motorway: .6, trunk: .6, primary: .6, secondary: .6, tertiary: .6,
motorway_link: .7, trunk_link: .7, primary_link: .7, secondary_link: .7, tertiary_link: .7
};
//
// Get highway width in mercator radians.
//
function highway_widths(highway, kind, zoom)
{
var pixel = 2 * pi / (1 << (zoom + 8)),
coeff = (highway in highway_coefficients) ? highway_coefficients[highway] : .8,
coeff = (kind == 'path' ? .9 : coeff),
scale = Math.pow(2, coeff * (zoom - 18));
if(highway == 'motorway') {
var inner = 14;
} else if(kind == 'path' || kind == 'rail' || highway == 'service') {
var inner = 3;
} else {
var inner = 6.5;
}
return [inner * pixel * scale, (inner + 4) * pixel * scale];
}
//
// Smaller numbers prioritize roads in front of other roads.
//
var highway_priorities = {
motorway: 0, trunk: 1, primary: 2, secondary: 3, tertiary: 4,
motorway_link: 5, trunk_link: 5, primary_link: 5, secondary_link: 5, tertiary_link: 5,
residential: 6, unclassified: 6, road: 6,
unclassified: 7, service: 7, minor: 7
};
//
// Get highway layer (z-index) as an integer.
//
function highway_layer(highway, explicit_layer, is_bridge, is_tunnel)
{
// explicit layering mostly wins
var layer = (explicit_layer == undefined) ? 0 : explicit_layer * 1000;
// implicit layering less important.
if(is_bridge == 'yes')
{
layer += 100;
}
if(is_tunnel == 'yes')
{
layer -= 100;
}
// leave the +/-10 order of magnitude open for bridge casings.
// adjust slightly based on priority derived from highway type
layer -= (highway in highway_priorities) ? highway_priorities[highway] : 9;
return layer;
}
function linkProgram(gl, vsource, fsource)
{
if(gl == undefined)
{
alert("Your browser does not support WebGL, try Google Chrome? Sorry.");
throw "Your browser does not support WebGL, try Google Chrome? Sorry.";
}
var program = gl.createProgram(),
vshader = createShader(gl, vsource, gl.VERTEX_SHADER),
fshader = createShader(gl, fsource, gl.FRAGMENT_SHADER);
gl.attachShader(program, vshader);
gl.attachShader(program, fshader);
gl.linkProgram(program);
if(!gl.getProgramParameter(program, gl.LINK_STATUS))
{
throw gl.getProgramInfoLog(program);
}
return program;
}
function createShader(gl, source, type)
{
var shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if(!gl.getShaderParameter(shader, gl.COMPILE_STATUS))
{
throw gl.getShaderInfoLog(shader);
}
return shader;
}
// http://paulirish.com/2011/requestanimationframe-for-smart-animating/
// http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating
// requestAnimationFrame polyfill by Erik Möller
// fixes from Paul Irish and Tino Zijdel
(function() {
var lastTime = 0;
var vendors = ['ms', 'moz', 'webkit', 'o'];
for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame'];
window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame']
|| window[vendors[x]+'CancelRequestAnimationFrame'];
}
if (!window.requestAnimationFrame)
window.requestAnimationFrame = function(callback, element) {
var currTime = new Date().getTime();
var timeToCall = Math.max(0, 16 - (currTime - lastTime));
var id = window.setTimeout(function() { callback(currTime + timeToCall); },
timeToCall);
lastTime = currTime + timeToCall;
return id;
};
if (!window.cancelAnimationFrame)
window.cancelAnimationFrame = function(id) {
clearTimeout(id);
};
}());
function endianness()
{
if(window.ArrayBuffer == undefined)
{
alert("Your browser does not support ArrayBuffer, try Google Chrome? Sorry.");
throw "Your browser does not support ArrayBuffer, try Google Chrome? Sorry.";
}
var b = new ArrayBuffer(4),
f = new Float32Array(b),
u = new Uint32Array(b);
f[0] = 1.0;
if(u[0] == 32831) {
return 'big';
} else {
return 'little';
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>GL-Solar (Squares demo)</title>
<script src="http://teczno.com/squares/Squares-D3-0.0.5.min.js" type="application/javascript"></script>
<script src="gl-boilerplate.js" type="application/javascript"></script>
<script src="tile-queue.js" type="application/javascript"></script>
<script src="map.js" type="application/javascript"></script>
<link rel="stylesheet" href="http://www.openstreetmap.us/~migurski/style.css">
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="map"></div>
<p>
<a href="http://mike.teczno.com">Michal Migurski</a>, Feb 2013.
</p>
<script id="shader-vertex" type="x-shader/x-vertex">
const float pi = 3.14159265359;
const mat4 view = mat4 (2.0/{CANVAS_WIDTH}, 0, 0, 0, 0, -2.0/{CANVAS_HEIGHT}, 0, 0, 0, 0, -0.0001, 0, -1, 1, 0, 1);
uniform mat4 panzoom;
uniform float frame;
attribute vec3 xyz;
attribute vec4 rgba;
attribute float seg;
varying float bounce;
varying vec4 color;
varying mat4 bloop;
varying vec4 pos;
void main()
{
pos = view * panzoom * vec4(xyz, 1);
bounce = log(xyz.z + 1.) * atan(frame/5000.) * .002;
bloop = mat4(1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
sin(pos.x * 5.0) * sin(frame / 10.) * bounce,
sin(pos.y * 9.6) * cos(frame / 10.) * bounce,
0, 1);
gl_Position = bloop * pos;
if(seg >= 0.) {
color = vec4(.5 * cos(frame/4. - seg/2. + 0. * pi/3.) + .5,
.5 * cos(frame/4. - seg/2. + 2. * pi/3.) + .5,
.5 * cos(frame/4. - seg/2. + 4. * pi/3.) + .5,
rgba.a);
} else {
color = rgba;
}
}
</script>
<script id="shader-fragment" type="x-shader/x-fragment">
precision mediump float;
const vec3 bg = vec3(0, 0, 0); // vec3(0.992, 0.965, 0.890);
varying vec4 color;
void main()
{
// instead of using the full RGBA, do a linear mix with background color
gl_FragColor = vec4(mix(bg.rgb, color.rgb, color.a), 1);
}
</script>
<script type="application/javascript">
<!--
var ctx = get_webgl_context();
var geo = new sq.Geo.Mercator();
var map = new Map(document.getElementById('map'), geo, {lat: 37.73570, lon: -122.40806}, 16.5);
//
// Return a pair of functions to call from Map class in map.js, one to
// push new data into xyz-rgba buffer and the other to trigger redraw.
//
function get_webgl_context(matrix)
{
var map = document.getElementById('map'),
c = document.createElement('canvas');
c.width = map.clientWidth;
c.height = map.clientHeight;
c.style.position = 'absolute';
map.insertBefore(c, null);
var gl = c.getContext('experimental-webgl'),
vsource = document.getElementById('shader-vertex').text,
vsource = vsource.replace('{CANVAS_WIDTH}', c.width.toFixed(1)),
vsource = vsource.replace('{CANVAS_HEIGHT}', c.height.toFixed(1)),
fsource = document.getElementById('shader-fragment').text,
program = linkProgram(gl, vsource, fsource);
gl.useProgram(program);
var xyzrgba_buffer = gl.createBuffer(),
xyz_attrib = gl.getAttribLocation(program, 'xyz'),
rgba_attrib = gl.getAttribLocation(program, 'rgba'),
seg_attrib = gl.getAttribLocation(program, 'seg'),
panzoom = gl.getUniformLocation(program, 'panzoom'),
frameuni = gl.getUniformLocation(program, 'frame'),
length = 0;
gl.enableVertexAttribArray(xyz_attrib);
gl.enableVertexAttribArray(rgba_attrib);
gl.enableVertexAttribArray(seg_attrib);
gl.bindBuffer(gl.ARRAY_BUFFER, xyzrgba_buffer);
function data(xys)
{
gl.bufferData(gl.ARRAY_BUFFER, xys, gl.DYNAMIC_DRAW);
length = xys.length/8;
}
function draw(size, ul, lr)
{
// mx+b style transformation.
var mx = size.x / (lr.x - ul.x), bx = -mx * ul.x,
my = size.y / (lr.y - ul.y), by = -my * ul.y;
var matrix = new Float32Array([mx, 0, 0, 0, 0, my, 0, 0, 0, 0, 1, 0, bx, by, 0, 1]);
gl.uniformMatrix4fv(panzoom, false, matrix);
}
var frame = 1;
paint();
function paint()
{
gl.clearColor(0, 0, 0, 1); // gl.clearColor(253/255, 246/255, 227/255, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.enable(gl.DEPTH_TEST);
gl.uniform1f(frameuni, frame);
gl.vertexAttribPointer(xyz_attrib, 3, gl.FLOAT, false, 4*8, 0);
gl.vertexAttribPointer(rgba_attrib, 4, gl.FLOAT, false, 4*8, 4*3);
gl.vertexAttribPointer(seg_attrib, 1, gl.FLOAT, false, 4*8, 4*7);
gl.drawArrays(gl.TRIANGLES, 0, length);
/*
gl.drawArrays(gl.TRIANGLES, 0, Math.min(length/2));
gl.drawArrays(gl.TRIANGLES, Math.min(length/2), length - Math.min(length/2));
*/
frame += 1;
requestAnimationFrame(paint);
}
return {draw: draw, data: data};
}
//-->
</script>
<iframe width="40" height="30" src="http://www.youtube.com/embed/eJF-u9xWIH8?rel=0&autoplay=1" frameborder="0" allowfullscreen></iframe>
</body>
</html>
//
// Sample Map class for use with Squares.
// Implements the Map interface from v0.0.5:
// https://github.com/migurski/Squares/blob/315e37bc/src/Base.ts
//
// Renders data with WebGL, and relies on Web Worker in feature-arrayer.js
// and global ctx variable from get_webgl_context() in index.html.
//
function Map(parent, proj, loc, zoom)
{
this.queue = new Queue();
this.timeout = false;
this.workers = [new Worker('feature-arrayer.js'), new Worker('feature-arrayer.js')];
this.selection = d3.select(parent);
this.parent = parent;
var size = sq.Mouse.element_size(this.parent), coord = proj.locationCoordinate(loc).zoomTo(zoom);
this.grid = new sq.Grid.Grid(size.x, size.y, coord, 0);
this.projection = proj;
sq.Mouse.link_control(this.selection, new sq.Mouse.Control(this, false));
sq.Hash.link_control(this);
var map = this;
d3.select(window).on('resize.map', function() { map.update_gridsize() });
this.workers[0].addEventListener('message', function(e) { map.new_data(e.data.node_id, e.data.list, e.data.elapsed) }, false);
this.workers[1].addEventListener('message', function(e) { map.new_data(e.data.node_id, e.data.list, e.data.elapsed) }, false);
this.selection.selectAll('div.tile').remove();
this.redraw(false);
}
Map.prototype = {
update_gridsize: function()
{
var size = sq.Mouse.element_size(this.parent);
this.grid.resize(size.x, size.y);
},
pointLocation: function(point)
{
var coord = this.grid.pointCoordinate(point ? point : this.grid.center);
return this.projection.coordinateLocation(coord);
},
locationPoint: function(loc)
{
var coord = this.projection.locationCoordinate(loc);
return this.grid.coordinatePoint(coord);
},
setCenterZoom: function(loc, zoom)
{
this.grid.setCenter(this.projection.locationCoordinate(loc, zoom));
this.redraw(true);
},
onMoved: function(callback)
{
this.moved_callback = callback;
},
redraw: function(moved)
{
var tiles = this.grid.visibleTiles(),
join = this.selection.selectAll('div.tile').data(tiles, tile_key);
var map = this;
join.exit()
.remove()
.each(function(tile, i) { map.exit_handler(tile, this) });
join.enter()
.append('div')
.attr('class', 'tile')
.style('position', 'absolute')
.style('margin', '0')
.style('padding', '0')
.style('border', '0')
.style('-webkit-transform-origin', '0px 0px')
.each(function(tile, i) { map.enter_handler(tile, this) });
this.selection.selectAll('div.tile')
.style('left', tile_left)
.style('top', tile_top)
.style('width', tile_width)
.style('height', tile_height);
if(this.moved_callback)
{
this.moved_callback(this);
}
this.queue.process();
this.render();
},
update: function()
{
var len = 0,
offs = [];
// get the total length of all arrays
this.selection.selectAll('div.tile')
.each(function() { if(this.array) { len += this.array.length } });
var xys = new Float32Array(len),
off = 0;
// concatenate all arrays to xys
this.selection.selectAll('div.tile')
.each(function() { if(this.array) { xys.set(this.array, off); offs.push(off); off += this.array.length } });
ctx.data(xys);
var map = this;
if(map.timeout) {
clearTimeout(map.timeout);
}
map.timeout = setTimeout(function() { map.redraw() }, 100);
},
render: function()
{
var keys = [];
for(var key in this.arrays)
{
keys.push(key);
}
var size = sq.Mouse.element_size(this.parent),
nw = this.pointLocation({x: 0, y: 0}),
se = this.pointLocation(size),
ul = this.projection.project(nw),
lr = this.projection.project(se);
ctx.draw(size, ul, lr);
},
exit_handler: function(tile, node)
{
this.queue.cancel(node);
var map = this;
if(map.timeout) {
clearTimeout(map.timeout);
}
map.timeout = setTimeout(function() { map.update() }, 25);
},
enter_handler: function(tile, node)
{
if(tile.coord.zoom < 12)
{
return;
}
var map = this,
worker = map.workers[Math.floor(Math.random() * 2)];
var callback = function(data)
{
map.queue.close(node);
worker.postMessage({node_id: node.id, features: data['features'], zoom: tile.coord.zoom});
}
node.id = this.next_int().toString();
node.onjson = callback;
this.queue.append(node, 'http://tile.openstreetmap.us/vectiles-highroad/'+tile.toKey()+'.json');
},
new_data: function(node_id, list, elapsed)
{
var f32array = new Float32Array(list);
this.selection.selectAll('div.tile')
.each(function() { if(this.id == node_id) { this.array = f32array } });
var map = this;
if(map.timeout) {
clearTimeout(map.timeout);
}
map.timeout = setTimeout(function() { map.update() }, 25);
},
next_int: function()
{
if(this.number == undefined)
{
this.number = 0;
}
return ++this.number;
}
}
function tile_key(tile) { return tile.toKey() }
function tile_left(tile) { return tile.left() }
function tile_top(tile) { return tile.top() }
function tile_width(tile) { return tile.width() }
function tile_height(tile) { return tile.height() }
function tile_xform(tile) { return tile.transform() }
body
{
background-color: #EEE8D5;
margin: 0 !important;
}
#map
{
width: 960px;
height: 500px;
position: relative;
overflow: hidden;
margin: 0;
padding: 0 0 0 0;
}
div.tile
{
color: #839496;
display: block;
}
iframe
{
width: 40px !important;
}
function Queue()
{
this.queue = [];
this.queue_by_id = {};
this.open_request_count = 0;
this.requests_by_id = {};
}
Queue.prototype = {
append: function(node, href)
{
var request = new Request(node, href);
this.queue.push(request);
this.queue_by_id[request.id] = request;
},
cancel: function(node)
{
this.close(node);
var request = this.queue_by_id[node.id];
if(request)
{
request.deny();
delete this.queue_by_id[node.id];
}
},
close: function(node)
{
var request = this.requests_by_id[node.id];
if(request)
{
request.deny();
delete this.requests_by_id[node.id];
this.open_request_count--;
}
},
process: function()
{
//this.queue.sort(Request.prototype.compare);
//console.log('processing', this.open_request_count, 'open req count', this.queue.length, 'queue');
while(this.open_request_count < 4 && this.queue.length > 0)
{
var request = this.queue.shift(),
loading = request.load();
if(loading)
{
this.requests_by_id[request.id] = request;
this.open_request_count++;
}
delete this.queue_by_id[request.id];
}
}
};
function Request(node, href)
{
this.id = node.id;
this.sort = node.sort;
this.node = node;
this.href = href;
}
Request.prototype = {
deny: function()
{
this.node = null;
},
load: function()
{
if(this.node && this.node.parentNode)
{
d3.json(this.href, this.node.onjson);
return true;
}
return false;
},
compare: function(a, b)
{
return b.sort - a.sort;
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment