WebGL rendering of live OpenStreetMap data using the OSM-Solar color scheme.
Built with Squares, rendered with false colors to show line structure.
self.addEventListener('message', onmessage); | |
var pi = Math.PI; | |
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 = 0; k < parts[j].length - 1; 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]}, | |
p1 = project(loc1), | |
p2 = project(loc2); | |
// 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, .5, 0, a, pb.x, pb.y, z, 1, 0, 0, a, pc.x, pc.y, z, 1, .5, 0, a]); | |
floats = floats.concat([pa.x, pa.y, z, 1, .5, 0, a, pc.x, pc.y, z, 1, .5, 0, a, pd.x, pd.y, z, 1, 1, 0, a]); | |
// 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 = 253/255, | |
g = 246/255, | |
b = 227/255; | |
floats = floats.concat([pa.x, pa.y, z, 1, .5, 0, a, pb.x, pb.y, z, 1, 0, 0, a, pc.x, pc.y, z, 1, .5, 0, a]); | |
floats = floats.concat([pa.x, pa.y, z, 1, .5, 0, a, pc.x, pc.y, z, 1, 0, 0, a, pd.x, pd.y, z, 1, 1, 0, a]); | |
} | |
} | |
} | |
} | |
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}; | |
} | |
// | |
// 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 False-Color Edition</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="style.css"> | |
</head> | |
<body> | |
<div id="map"></div> | |
<script id="shader-vertex" type="x-shader/x-vertex"> | |
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; | |
attribute vec3 xyz; | |
attribute vec4 rgba; | |
varying vec4 color; | |
void main() | |
{ | |
gl_Position = view * panzoom * vec4(xyz, 1); | |
color = rgba; | |
} | |
</script> | |
<script id="shader-fragment" type="x-shader/x-fragment"> | |
precision mediump float; | |
const vec3 bg = 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.8043, lon: -122.2712}, 15); | |
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'), | |
panzoom = gl.getUniformLocation(program, 'panzoom'), | |
length = 0; | |
gl.enableVertexAttribArray(xyz_attrib); | |
gl.enableVertexAttribArray(rgba_attrib); | |
gl.bindBuffer(gl.ARRAY_BUFFER, xyzrgba_buffer); | |
function data(xys) | |
{ | |
gl.bufferData(gl.ARRAY_BUFFER, xys, gl.DYNAMIC_DRAW); | |
length = xys.length/7; | |
} | |
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.clearColor(253/255, 246/255, 227/255, 1); | |
gl.clear(gl.COLOR_BUFFER_BIT); | |
gl.enable(gl.DEPTH_TEST); | |
gl.uniformMatrix4fv(panzoom, false, matrix); | |
gl.vertexAttribPointer(xyz_attrib, 3, gl.FLOAT, false, 4*7, 0); | |
gl.vertexAttribPointer(rgba_attrib, 4, gl.FLOAT, false, 4*7, 4*3); | |
gl.drawArrays(gl.TRIANGLES, 0, length); | |
} | |
return {draw: draw, data: data}; | |
} | |
//--> | |
</script> | |
</body> | |
</html> |
function Map(parent, proj, loc, zoom) | |
{ | |
this.queue = new Queue(); | |
this.timeout = false; | |
this.worker = 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.worker.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; | |
var callback = function(data) | |
{ | |
map.queue.close(node); | |
map.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) | |
{ | |
console.log('node', node_id, list.length, 'array in', elapsed, 'msec'); | |
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; | |
} | |
#map | |
{ | |
width: 960px; | |
height: 500px; | |
position: relative; | |
overflow: hidden; | |
margin: 0; | |
padding: 0; | |
} | |
div.tile | |
{ | |
color: #839496; | |
display: block; | |
} |
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; | |
} | |
}; |