Skip to content

Instantly share code, notes, and snippets.

@proclamo-zz
Created October 4, 2015 15:35
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save proclamo-zz/0f97ea66f44fd9d8b341 to your computer and use it in GitHub Desktop.
Save proclamo-zz/0f97ea66f44fd9d8b341 to your computer and use it in GitHub Desktop.
Drawing a dataset from CartoDB's API without geo tools

My first approach to resolve this problem was focusing in GIS theory. But I was stuck with some cases and I was sure there was another way plus simple. Finally, I applied image drawing techniques and all was good. The only GIS thing is the conversion latitude / longitude to x / y pixels. In this case, I use an equirectangular projection, but we can use another projection.

In this moment, the application fetch the whole dataset from CartoDB API (70 MB) in GeoJSON format, extracts the geometries, calculates the bounds geometries, calculates the scale with these bounds, and draws the polygons. I implemented the panning and an zoom system that needs improve. All this in 4 seconds.

There is few points for improve performance. In server calls side:

  • simplify geometries with PostGIS or/and with TopoJSON, or codifying the data for reduce the space needed
  • parallel downloads with webworkers or similar
  • client caching, an simple memory hash or maybe a client database
  • call data within the view's bounds

In client rendering (needs monitoring):

  • now, every polygon starts a context.beginPath() ans ends with context.closePath(). Maybe I can call these functions with groups of polygons, or only one time globally.
  • maybe, ordering the polygons
  • maybe, filter only the polygons that will be within the bounds before drawing
  • maybe, draw groups of polygons in many canvas layers
  • maybe, draw the dataset in an in-memory canvas and batch the result as image in the main canvas

Of course, investigate the techniques in HTML5 gaming, HTML5 video rendering, streaming, database streaming (like pipelinedb), BI, Big Data (MapReduce), etc. And monitoring, monitoring, monitoring and deciding.

<!DOCTYPE html>
<html>
<head>
<title>Draw a dataset without geo tools</title>
<style type="text/css">
body, canvas {
background-color: #C6D6DD;
}
</style>
</head>
<body>
<script type="text/javascript" src="https://code.jquery.com/jquery-2.1.4.min.js"></script>
<script type="text/javascript">
(function($) {
// parameters
var width = 1000,
height = 800,
margin = 15,
zoom = 1,
panX = 0,
panY = 0,
url = 'https://rambo-test.cartodb.com/api/v2/sql?q=select cartodb_id, st_asgeojson(the_geom) from public.mnmappluto'
;
// private variables
var min = [10000, 10000],
max = [-10000, -100000],
geometries = [],
drawing = false,
canvas,
ctx,
startCoords = [0, 0],
last = [0,0],
dragged = false,
focus
;
canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
ctx = canvas.getContext('2d');
document.body.appendChild(canvas);
canvas.addEventListener('DOMMouseScroll',handleScroll,false);
canvas.addEventListener('mousewheel',handleScroll,false);
canvas.addEventListener('mousedown', handleMouseDown, false);
canvas.addEventListener('mousemove', handleMouseMove, false);
canvas.addEventListener('mouseup', handleMouseUp, false);
render();
function x(lon) {
return (lon + 180) / (360 / width);
}
function y(lat) {
return (90 - lat) / (180 / height);
}
function extractGeometries(geojsons) {
var geojson, coords, coord, point, points, i, j, l, ll;
for (i = 0, l = geojsons.length; i < l; i++) {
geojson = geojsons[i];
coords = geojson.coordinates[0][0];
points = [];
for (j = 0, ll = coords.length; j < ll; j++) {
coord = coords[j];
// convert longitude, latitude to x,y pixels
point = [x(coord[0]), y(coord[1])];
setMin(point);
points.push(point);
}
if (points.length) {
geometries.push(points);
}
}
for (i = 0, l = geometries.length; i < l; i++) {
points = geometries[i];
for (j = 0, ll = points.length; j < ll; j++) {
point = points[j];
setMax(point);
}
}
}
function setMin(point) {
min[0] = Math.min(min[0], point[0]);
min[1] = Math.min(min[1], point[1]);
}
function setMax(point) {
point[0] -= min[0];
point[1] -= min[1];
max[0] = Math.max(max[0], point[0]);
max[1] = Math.max(max[1], point[1]);
}
function getScale() {
var mapWidth = width - margin * 2;
var mapHeight = height - margin * 2;
return Math.min(mapWidth / max[0], mapHeight / max[1]) * zoom;
}
function getCenter(scale) {
// todo: translate center with focus point
var center = [
(width - (scale * max[0])) / 2,
(height - (scale * max[1])) / 2
];
center[0] += panX;
center[1] += panY;
return center;
}
function drawPolygons() {
drawing = true;
var scale = getScale();
// center image
var center = getCenter(scale);
var points, point, pointx, pointy, polygons;
// draw each polygon
for (var i = 0, l = geometries.length; i < l; i++) {
points = geometries[i];
polygons = [];
for (var j = 0, ll = points.length; j < ll; j++) {
point = points[j];
pointx = (center[0] + (point[0] * scale));
pointy = (center[1] + (point[1] * scale));
polygons.push([pointx, pointy]);
}
if (polygons.length) {
draw(polygons);
}
}
drawing = false;
}
function render() {
ctx.clearRect(0, 0, width, height);
$.getJSON(url).done(function(data) {
var geojsons = data.rows.map(function(row) {
return JSON.parse(row.st_asgeojson);
});
extractGeometries(geojsons);
drawPolygons();
}, this);
}
function draw(polygons) {
ctx.beginPath();
var data, i, l;
data = polygons[0];
ctx.moveTo(data[0], data[1]);
for (i = 1, l = polygons.length; i < l; i++) {
data = polygons[i];
ctx.lineTo(data[0], data[1]);
}
ctx.stroke();
ctx.closePath();
}
function handleScroll(ev){
ev.preventDefault();
ev.stopPropagation();
var delta = ev.wheelDelta ? ev.wheelDelta / 30 : ev.detail ? -ev.detail : 0;
if (delta){
focus = [ev.offsetX, ev.offsetY];
scaleTo(delta);
}
}
function scaleTo(scale) {
zoom += scale;
if (zoom <= 0) zoom = 0.1;
if (!drawing){
ctx.clearRect(0, 0, width, height);
drawPolygons();
}
}
function handleMouseDown(ev) {
dragged = true;
startCoords = [
ev.offsetX - last[0],
ev.offsetY - last[1]
];
}
function handleMouseUp(ev) {
dragged = false;
last = [
ev.offsetX - startCoords[0],
ev.offsetY - startCoords[1]
];
}
function handleMouseMove(ev) {
if (!dragged) return;
panX = ev.offsetX - startCoords[0];
panY = ev.offsetY - startCoords[1];
if (!drawing) {
ctx.clearRect(0, 0, width, height);
drawPolygons();
}
}
})(jQuery);
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment