leaflet-mapbox-vector-tile
Version:
A Leaflet Plugin that renders Mapbox Vector Tiles on HTML5 Canvas.
427 lines (379 loc) • 11.5 kB
JavaScript
/**
* Created by Nicholas Hallahan <nhallahan@spatialdev.com>
* on 7/11/14.
*/
/**
* Using the JavaScript Topology Suite to detect unions of polygons.
*/
importScripts('../jsts/javascript.util.js', '../jsts/jsts-src.js');
reader = new jsts.io.GeoJSONReader();
parser = new jsts.io.GeoJSONParser();
onmessage = function(evt) {
var msg = evt.data;
for (var key in msg) {
var fn = calcPos[key];
if (typeof fn === 'function') fn(msg);
}
};
/**
* Computes the position for a dynamicLabel depending on
* the data type...
*/
calcPos = {
tilePoints: function(msg) { /* TODO */ },
tileLines: function(msg) { /* TODO */ },
tilePolys: function(msg) {
var tilePolys = msg.tilePolys;
var tileCls = classifyAndProjectTiles(tilePolys, msg.extent, msg.tileSize);
var largestPoly = mergeAndFindLargestPolygon(tilePolys, tileCls);
// if there are no polygons in the tiles, we dont have a center
if (!largestPoly.tid) {
postMessage({status: 'NO_POLY_IN_TILES'});
} else {
var center = centroid(tilePolys[largestPoly.tid][largestPoly.idx]);
if (!center || !center.x || !center.y) {
center = {
status: 'ERR',
description: 'No centroid calculated.'
}
} else {
center.status = 'OK';
center.z = parseInt(largestPoly.tid.split(':')[0]);
}
postMessage(center);
}
}
};
/**
* Goes through each tile and and classifies the paths...
*
* @param tiles
* @param extent
*/
function classifyAndProjectTiles(tilePaths, extent, tileSize) {
var tileCls = {};
for (var id in tilePaths) {
var t = tilePaths[id];
var cls = classifyAndProject(t, extent, tileSize, id);
tileCls[id] = cls;
}
return tileCls;
}
/**
* Checks through the coordinate arrays and classifies if they
* leave the bounds of the extent and where.
*
* @param coordsArray
*/
function classifyAndProject(coordsArray, extent, tileSize, id) {
// we need to know the tile address to project the coords
var zxy = id.split(':');
var x = parseInt(zxy[1]);
var y = parseInt(zxy[2]);
// the classification the the coords (what tile the intersect with)
var cls = {
internal: [],
topLeft: [],
left: [],
bottomLeft: [],
bottom: [],
bottomRight: [],
right: [],
topRight: [],
top: []
};
for (var i = 0, len = coordsArray.length; i < len; i++) {
var coords = coordsArray[i];
var overlapNeighbors = {};
var pathInExtentAtLeastOnce = false;
for (var j = 0, len2 = coords.length; j < len2; j++) {
var coord = coords[j];
var neighbor = checkCoordExtent(coord, extent);
// project the coord to the pixel space of the world (Spherical Mercator for Zoom Level)
coords[j] = project(coord, x, y, extent, tileSize);
// coord is outside tile
if (neighbor) {
overlapNeighbors[neighbor] = true;
}
// coord is inside tile
else {
pathInExtentAtLeastOnce = true;
}
}
// path is entirely inside of tile
if (Object.keys(overlapNeighbors).length === 0) {
cls.internal.push(i);
}
// path leaves the tile
else {
for (var neighbor in overlapNeighbors) {
// We don't want paths that are never in the extent at all...
if (pathInExtentAtLeastOnce) {
cls[neighbor].push(i);
}
}
}
}
return cls;
}
/**
* Checks to see if the path has left the extent of the vector tile.
* If so, we need to continue creating the polygon with coordinates
* from a neighboring tile...
*
* @param pbfFeature
* @param ctx
* @param coords
*/
function checkCoordExtent(coord, extent) {
var x = coord.x;
var y = coord.y;
// outside left side
if (x < 0) {
// in top left tile
if (y < 0) {
return 'topLeft';
}
// in bottom left tile
if (y > extent) {
return 'bottomLeft';
}
// in left tile
return 'left';
}
// outside right side
if (x > extent) {
// in top right tile
if (y < 0) {
return 'topRight';
}
// in bottom right tile
if (y > extent) {
return 'bottomRight';
}
// in right tile
return 'right';
}
// outside top side
if (y < 0) {
return 'top';
}
// outside bottom side
if (y > extent) {
return 'bottom';
}
return null;
}
var neighborFns = {
top: function(id) {
var zxy = id.split(':');
return zxy[0] + ':' + zxy[1] + ':' + (parseInt(zxy[2]) - 1);
},
topLeft: function(id) {
var zxy = id.split(':');
return zxy[0] + ':' + (parseInt(zxy[1]) - 1) + ':' + (parseInt(zxy[2]) - 1);
},
left: function(id) {
var zxy = id.split(':');
return zxy[0] + ':' + (parseInt(zxy[1]) - 1) + ':' + zxy[2];
},
bottomLeft: function(id) {
var zxy = id.split(':');
return zxy[0] + ':' + (parseInt(zxy[1]) - 1) + ':' + (parseInt(zxy[2]) + 1);
},
bottom: function(id) {
var zxy = id.split(':');
return zxy[0] + ':' + zxy[1] + ':' + (parseInt(zxy[2]) + 1);
},
bottomRight: function(id) {
var zxy = id.split(':');
return zxy[0] + ':' + (parseInt(zxy[1]) + 1) + ':' + (parseInt(zxy[2]) + 1);
},
right: function(id) {
var zxy = id.split(':');
return zxy[0] + ':' + (parseInt(zxy[1]) + 1) + ':' + zxy[2];
},
topRight: function(id) {
var zxy = id.split(':');
return zxy[0] + ':' + (parseInt(zxy[1]) + 1) + ':' + (parseInt(zxy[2]) - 1);
}
};
/**
* Projects a vector tile point to the Spherical Mercator pixel space for a given zoom level.
*
* @param vecPt
* @param tileX
* @param tileY
* @param extent
* @param tileSize
*/
function project(vecPt, tileX, tileY, extent, tileSize) {
var div = extent / tileSize;
var xOffset = tileX * tileSize;
var yOffset = tileY * tileSize;
return {
x: Math.floor(vecPt.x / div + xOffset),
y: Math.floor(vecPt.y / div + yOffset)
};
}
var inverseSides = {
top: 'bottom',
topLeft: 'bottomRight',
left: 'right',
bottomLeft: 'topRight',
bottom: 'top',
bottomRight: 'topLeft',
right: 'left',
topRight: 'bottomLeft'
};
function mergeAndFindLargestPolygon(tilePolys, tileCls) {
var largestPoly = {
tid: null, // tile id to get the tile in tilePolys
idx: null, // index of array of polys in tile
area: null // area of the poly
};
for (var id in tileCls) {
var cCls = tileCls[id]; // center tile classifications
var cPolys = tilePolys[id]; // center tile polygons
// Polygons internal to a tile do not need to be merged, but they may be the largest...
var internalClsArr = cCls.internal;
if (typeof internalClsArr === 'array') {
for (var i = 0, len = internalClsArr.length; i < len; i++) {
findLargestPoly(largestPoly, tilePolys, id, internalClsArr[i]);
}
}
for (var edge in neighborFns) {
var cClsEdgeArr = cCls[edge]; // poly idxs for center edge classification
// continue if there are no overlapping polys on a given edge...
if (cClsEdgeArr.length === 0) {
continue;
}
var nId = neighborFns[edge](id); // neighboring tile id
var nCls = tileCls[nId]; // neighboring tile classifications
var nPolys = tilePolys[nId]; // neighboring tile polygons
for (var j = 0, len2 = cClsEdgeArr.length; j < len2; j++) {
var cPolyIdx = cClsEdgeArr[j]; // a given center poly idx that overlaps the edge we are examining
// If we have a neighboring tile, we get the overlapping polygons that correspond with the center tile.
// We then try to union the polygons...
if (nCls) {
var inv = inverseSides[edge];
// We are just going to do 1 poly from center tile at a time so we can keep track of the one we are actually doing the merge on...
var cEdgePolys = polygonSetToGeoJson(cPolys, [cPolyIdx]); // 1 poly
var nEdgePolys = polygonSetToGeoJson(nPolys, nCls[inv]); // 1 or more polys
var ctrJsts = reader.read(cEdgePolys);
var nbrJsts = reader.read(nEdgePolys);
if (id === '5:18:16') {
console.log('b');
}
try {
// https://www.youtube.com/watch?v=RdSmokR0Enk
var union = ctrJsts.union(nbrJsts);
// the new merged polygon
var unionPoly = union.shell.points;
// Neighboring tile's inverse edge should be empty,
// because the corresponding shape has been merged.
nCls[inv] = [];
// Replace the polygon in the center tile with the new merged polygon
// so other tiles can merge this if needed.
cPolys[cPolyIdx] = unionPoly;
} catch (e) {
//NH TODO: WHY?
postMessage({
status: 'WARN',
details: 'union failed',
ctrJsts:ctrJsts,
nbrJsts:nbrJsts,
cEdgePolys:cEdgePolys,
nEdgePolys:nEdgePolys,
e:e,
tile: id
});
}
}
// Regardless of whether we unioned or not, we want to check to see if this polygon is the largest...
findLargestPoly(largestPoly, tilePolys, id, cPolyIdx);
}
}
}
return largestPoly;
}
/**
* Converts the array of arrays of {x,y} points to GeoJSON.
*
* Note that the GeoJSON we are using is in the projected pixel
* space. Normally GeoJSON is WGS84, but we don't care, because
* we are just doing a union topology check.
*
* @param polys
* @returns {{type: string, coordinates: Array}}
*/
function polygonSetToGeoJson(polys, idxArr) {
var coordinates = [];
var geojson = {
type: 'MultiPolygon',
coordinates: coordinates
};
for (var i = 0, len = idxArr.length; i < len; i++) {
var idx = idxArr[i];
var poly = polys[idx];
var geoPoly = [];
for (var j = 0, len2 = poly.length; j < len2; j++) {
var pt = poly[j];
var geoPt = [pt.x, pt.y];
geoPoly.push(geoPt);
}
geoPoly = [geoPoly]; // its an array in array, other items in the array would be rings
coordinates.push(geoPoly);
}
return geojson;
}
// http://en.wikipedia.org/wiki/Centroid#Centroid_of_polygon
function area(poly) {
var area = 0;
var len = poly.length;
for (var i = 0, j = len - 1; i < len; j=i, i++) {
var p1 = poly[j];
var p2 = poly[i];
area += p1.x * p2.y - p2.x * p1.y;
}
return Math.abs(area / 2);
}
/*
NH TODO: We are indeed getting the centroid, but ideally we
want to check if the centroid is actually within the polygon
for the polygons that bend like a boomarang. If it is outside,
we need to nudge it over until it is inside...
*/
function centroid(poly) {
var len = poly.length;
var x = 0;
var y = 0;
for (var i = 0, j = len - 1; i < len; j=i, i++) {
var p1 = poly[j];
var p2 = poly[i];
var f = p1.x * p2.y - p2.x * p1.y;
x += (p1.x + p2.x) * f;
y += (p1.y + p2.y) * f;
}
f = area(poly) * 6;
return {
x: Math.abs(x/f),
y: Math.abs(y/f)
};
}
function findLargestPoly(largestPoly, tilePolys, tid, idx) {
if (!largestPoly.tid) {
largestPoly.tid = tid;
largestPoly.idx = idx;
largestPoly.area = area(tilePolys[tid][idx]);
return largestPoly;
}
var largestArea = largestPoly.area;
var polyArea = area(tilePolys[tid][idx]);
if (polyArea > largestArea) {
largestPoly.tid = tid;
largestPoly.idx = idx;
largestPoly.area = polyArea;
}
return largestPoly;
}