From 2ac80044f3f5faeedee540deef9d92337bf18239 Mon Sep 17 00:00:00 2001 From: James Cardona Date: Wed, 16 Sep 2015 14:17:20 -0400 Subject: [PATCH 01/13] new VectorTileClipper class adds logic to clip geometries that are in overzoomed tiles --- lib/vectortilefeature.js | 408 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 408 insertions(+) diff --git a/lib/vectortilefeature.js b/lib/vectortilefeature.js index 740c66e..dd02adf 100644 --- a/lib/vectortilefeature.js +++ b/lib/vectortilefeature.js @@ -4,6 +4,364 @@ var Point = require('point-geometry'); module.exports = VectorTileFeature; +function VectorTileClipper(feature) { + // the ratio we'll need when producing the final result to extend back (or still reduce) to 4096 + this.finalRatio = 4096 / feature.extent * Math.pow(2, feature.dz); + + var margin = 64; // 8px times 4096/512 + margin /= this.finalRatio; + + var clipExtent = feature.extent >> feature.dz; + if (margin > clipExtent) + margin = clipExtent; + + this.dz = feature.dz; + this.margin = margin; + this.xmin = clipExtent * feature.xPos - margin; + this.ymin = clipExtent * feature.yPos - margin; + this.xmax = this.xmin + clipExtent + 2 * margin; + this.ymax = this.ymin + clipExtent + 2 * margin; + this.lines = []; + + this._prevIsIn = false; + this._isLine = feature.type !== 3; // consider points like line (ignore evertyhing outside) +} + +VectorTileClipper.prototype.moveTo = function(x, y) { + if (this.line) this.lines.push(this.line); + this.line = []; + //this.line.push(new Point(x, y)); + this._prevIsIn = this._isIn(x, y); + this._moveTo(x, y, this._prevIsIn); + + this._prevPt = new Point(x, y); + this._firstPt = new Point(x, y); +}; + +VectorTileClipper.prototype.lineTo = function(x, y) { + var isIn = this._isIn(x, y), + outPt, inPt, midPt, + pt1, pt2, ratio, + intercept, intercepts, + xpos, ypos, + xtest, ytest; + if (isIn) { + if (this._prevIsIn){ + // both in: just push + this._lineTo(x, y, true); + } + else { + outPt = this._prevPt; + inPt = new Point(x, y); + midPt = this._intersect(inPt, outPt); + this._lineTo(midPt.x, midPt.y, true); + this._lineTo(inPt.x, inPt.y, true); + } + } + else { + if (this._prevIsIn) { + inPt = this._prevPt; + outPt = new Point(x, y); + midPt = this._intersect(inPt, outPt); + this._lineTo(midPt.x, midPt.y, true); + this._lineTo(outPt.x, outPt.y, false); + } + else { + // going from pt1 to pt2 + pt1 = this._prevPt; + pt2 = new Point(x, y); + + // both points are outside but we could have two intersection points + // first, rule out obvious non intersecting cases + if ((pt1.x <= this.xmin && pt2.x <= this.xmin) || + (pt1.x >= this.xmax && pt2.x >= this.xmax) || + (pt1.y <= this.ymin && pt2.y <= this.ymin) || + (pt1.y >= this.ymax && pt2.y >= this.ymax)) { + this._lineTo(pt2.x, pt2.y, false); + } + else { + // figure out various intercepts, store them if they are on the extent boundary + intercepts = []; + + // xpos and ypos are bool to indicate if below min (false) or above max (true) + if ((pt1.x < this.xmin && pt2.x > this.xmin) || (pt1.x > this.xmin && pt2.x < this.xmin)) { + ratio = (this.xmin - pt1.x) / (pt2.x - pt1.x); + ytest = pt1.y + ratio * (pt2.y - pt1.y); + if (ytest <= this.ymin) + ypos = false; + else if (ytest >= this.ymax) + ypos = true; + else { + intercept = {}; + intercept.ratio = ratio; + intercept.x = this.xmin; + intercept.y = ytest; + intercepts.push(intercept); + } + } + if ((pt1.x < this.xmax && pt2.x > this.xmax) || (pt1.x > this.xmax && pt2.x < this.xmax)) { + ratio = (this.xmax - pt1.x) / (pt2.x - pt1.x); + ytest = pt1.y + ratio * (pt2.y - pt1.y); + if (ytest <= this.ymin) + ypos = false; + else if (ytest >= this.ymax) + ypos = true; + else { + intercept = {}; + intercept.ratio = ratio; + intercept.x = this.xmax; + intercept.y = ytest; + intercepts.push(intercept); + } + } + if ((pt1.y < this.ymin && pt2.y > this.ymin) || (pt1.y > this.ymin && pt2.y < this.ymin)) { + ratio = (this.ymin - pt1.y) / (pt2.y - pt1.y); + xtest = pt1.x + ratio * (pt2.x - pt1.x); + if (xtest <= this.xmin) + xpos = false; + else if (xtest >= this.xmax) + xpos = true; + else { + intercept = {}; + intercept.ratio = ratio; + intercept.x = xtest; + intercept.y = this.ymin; + intercepts.push(intercept); + } + } + if ((pt1.y < this.ymax && pt2.y > this.ymax) || (pt1.y > this.ymax && pt2.y < this.ymax)) { + ratio = (this.ymax - pt1.y) / (pt2.y - pt1.y); + xtest = pt1.x + ratio * (pt2.x - pt1.x); + if (xtest <= this.xmin) + xpos = false; + else if (xtest >= this.xmax) + xpos = true; + else { + intercept = {}; + intercept.ratio = ratio; + intercept.x = xtest; + intercept.y = this.ymax; + intercepts.push(intercept); + } + } + // intercepts has no more than two elements + if (intercepts.length === 0) { + // add the corresponding corner + if (xpos) { + if (ypos) { + this._lineTo(this.xmax, this.ymax, true); + } + else { + this._lineTo(this.xmax, this.ymin, true); + } + } + else { + if (ypos) { + this._lineTo(this.xmin, this.ymax, true); + } + else { + this._lineTo(this.xmin, this.ymin, true); + } + } + } + else if ((intercepts.length > 1) && (intercepts[0].ratio > intercepts[1].ratio)) { + this._lineTo(intercepts[1].x, intercepts[1].y, true); + this._lineTo(intercepts[0].x, intercepts[0].y, true); + } + else { + for (var i = 0; i < intercepts.length; i++) + this._lineTo(intercepts[i].x, intercepts[i].y, true); + } + this._lineTo(pt2.x, pt2.y, false); + } + } + } + this._prevIsIn = isIn; + this._prevPt = new Point(x, y); +}; + +VectorTileClipper.prototype.closePolygon = function() { + var firstPt, lastPt; + if (this.line.length > 0) { + firstPt = this._firstPt; + lastPt = this._prevPt; + if (firstPt.x !== lastPt.x || firstPt.y !== lastPt.y) + this.lineTo(firstPt.x, firstPt.y); + } +}; + +VectorTileClipper.prototype.result = function() { + // add current line + if (this.line){ + this.lines.push(this.line); + } + return this.lines; + + /*// need to transform coordinates to go back to the expected 4096 extent + for (var i = 0; i < this.lines.length; i++) + { + var line = this.lines[i]; + for (var j = 0; j < line.length; j++) + { + var pt = line[j]; + + // snap points outside of extent + if (pt.x < this.xmin) + pt.x = this.xmin; + if (pt.x > this.xmax) + pt.x = this.xmax; + if (pt.y < this.ymin) + pt.y = this.ymin; + if (pt.y > this.ymax) + pt.y = this.ymax; + + // transform + pt.x = (pt.x - (this.xmin + this.margin)) * this.finalRatio; + pt.y = (pt.y - (this.ymin + this.margin)) * this.finalRatio; + } + } + + // done + return this.lines;*/ +}; + +VectorTileClipper.prototype._isIn = function(x, y) { + return x >= this.xmin && x <= this.xmax && y >= this.ymin && y <= this.ymax; +}; + +VectorTileClipper.prototype._intersect = function(inPt, outPt) { + var x, y; + + if (inPt.x == outPt.x && inPt.y == outPt.y) + { + x=0; + } + + if (outPt.x >= this.xmin && outPt.x <= this.xmax) + { + y = outPt.y <= this.ymin ? this.ymin : this.ymax; + x = inPt.x + (y - inPt.y) / (outPt.y - inPt.y) * (outPt.x - inPt.x); + } + else if (outPt.y >= this.ymin && outPt.y <= this.ymax) + { + x = outPt.x <= this.xmin ? this.xmin : this.xmax; + y = inPt.y + (x - inPt.x) / (outPt.x - inPt.x) * (outPt.y - inPt.y); + } + else + { + y = outPt.y <= this.ymin ? this.ymin : this.ymax; + x = outPt.x <= this.xmin ? this.xmin : this.xmax; + + var xRatio = (x - inPt.x) / (outPt.x - inPt.x); + var yRatio = (y - inPt.y) / (outPt.y - inPt.y); + if (xRatio < yRatio) + { + y = inPt.y + xRatio * (outPt.y - inPt.y); + } + else + { + x = inPt.x + yRatio * (outPt.x - inPt.x); + } + } + return new Point(x, y); +}; + +VectorTileClipper.prototype._moveTo = function(x, y, isIn) { + if (this._isLine) { + if (isIn) { + x = (x - (this.xmin + this.margin)) * this.finalRatio; + y = (y - (this.ymin + this.margin)) * this.finalRatio; + this.line.push(new Point(x, y)); + } + } + else { + // snap points outside of extent + if (x < this.xmin) + x = this.xmin; + if (x > this.xmax) + x = this.xmax; + if (y < this.ymin) + y = this.ymin; + if (y > this.ymax) + y = this.ymax; + + // transform + x = (x - (this.xmin + this.margin)) * this.finalRatio; + y = (y - (this.ymin + this.margin)) * this.finalRatio; + + this.line.push(new Point(x, y)); + + this._is_h = false; + this._is_v = false; + } +}; + +VectorTileClipper.prototype._lineTo = function(x, y, isIn) { + var lastPt, prevPt; + + if (this._isLine) { + if (isIn) { + x = (x - (this.xmin + this.margin)) * this.finalRatio; + y = (y - (this.ymin + this.margin)) * this.finalRatio; + if (this.line.length > 0) { + lastPt = this.line[this.line.length - 1]; + if (lastPt.x === x && lastPt.y === y) + return; + } + this.line.push(new Point(x, y)); + } + else if (this.line && this.line.length > 0) { + this.lines.push(this.line); + this.line = []; + } + } + else { + // snap points outside of extent + if (x < this.xmin) + x = this.xmin; + if (x > this.xmax) + x = this.xmax; + if (y < this.ymin) + y = this.ymin; + if (y > this.ymax) + y = this.ymax; + + // transform + x = (x - (this.xmin + this.margin)) * this.finalRatio; + y = (y - (this.ymin + this.margin)) * this.finalRatio; + + if (this.line && this.line.length > 0) { + lastPt = this.line[this.line.length - 1]; + var is_h = lastPt.x === x; + var is_v = lastPt.y === y; + if (is_h && is_v) + return; + + if (this._is_h && is_h) { + lastPt.x = x; + lastPt.y = y; + prevPt = this.line[this.line.length - 2]; // valid if this._is_h is true + this._is_h = prevPt.x === x; + this._is_v = prevPt.y === y; + } + else if (this._is_v && is_v) { + lastPt.x = x; + lastPt.y = y; + prevPt = this.line[this.line.length - 2]; // valid if this._is_v is true + this._is_h = prevPt.x === x; + this._is_v = prevPt.y === y; + } + else { + this.line.push(new Point(x, y)); + this._is_h = is_h; + this._is_v = is_v; + } + } + else { + this.line.push(new Point(x, y)); // should never happen actually + } + } +}; function VectorTileFeature(pbf, end, extent, keys, values) { // Public this.properties = {}; @@ -39,6 +397,10 @@ function readTag(pbf, feature) { VectorTileFeature.types = ['Unknown', 'Point', 'LineString', 'Polygon']; VectorTileFeature.prototype.loadGeometry = function() { + // Test if the tile is overzoomed. We should use the clipping approach in this case + if (this.dz) + return this.loadClippedGeometry(); + var pbf = this._pbf; pbf.pos = this._geometry; @@ -87,6 +449,52 @@ VectorTileFeature.prototype.loadGeometry = function() { return lines; }; +VectorTileFeature.prototype.loadClippedGeometry = function() { + if (!this.dz && !this.xPos && !this.yPos) + return []; + + var pbf = this._pbf; + pbf.pos = this._geometry; + + var clipper = new VectorTileClipper(this); + + var end = pbf.readVarint() + pbf.pos, + cmd = 1, + length = 0, + x = 0, + y = 0, + lines = [], + line; + + while (pbf.pos < end) { + if (!length) { + var cmdLen = pbf.readVarint(); + cmd = cmdLen & 0x7; + length = cmdLen >> 3; + } + + length--; + + if (cmd === 1 || cmd === 2) { + x += pbf.readSVarint(); + y += pbf.readSVarint(); + + if (cmd === 1) { // moveTo + clipper.moveTo(x, y); + } + else { // lineTo + clipper.lineTo(x, y); + } + } else if (cmd === 7) { + clipper.closePolygon(); + } else { + throw new Error('unknown command ' + cmd); + } + } + + return clipper.result(); +}; + VectorTileFeature.prototype.bbox = function() { var pbf = this._pbf; pbf.pos = this._geometry; From fd3ebda41499039b7eb7851c3ce7b53950171dfd Mon Sep 17 00:00:00 2001 From: marc4025 Date: Thu, 17 Sep 2015 10:57:44 -0700 Subject: [PATCH 02/13] very minor updates from runtime implementation --- lib/vectortilefeature.js | 88 ++++++++++++++-------------------------- 1 file changed, 31 insertions(+), 57 deletions(-) diff --git a/lib/vectortilefeature.js b/lib/vectortilefeature.js index dd02adf..1a3e1c0 100644 --- a/lib/vectortilefeature.js +++ b/lib/vectortilefeature.js @@ -24,7 +24,7 @@ function VectorTileClipper(feature) { this.lines = []; this._prevIsIn = false; - this._isLine = feature.type !== 3; // consider points like line (ignore evertyhing outside) + this._isLine = feature.type !== 3; // consider points like line (ignore everything outside) } VectorTileClipper.prototype.moveTo = function(x, y) { @@ -74,10 +74,10 @@ VectorTileClipper.prototype.lineTo = function(x, y) { // both points are outside but we could have two intersection points // first, rule out obvious non intersecting cases if ((pt1.x <= this.xmin && pt2.x <= this.xmin) || - (pt1.x >= this.xmax && pt2.x >= this.xmax) || - (pt1.y <= this.ymin && pt2.y <= this.ymin) || - (pt1.y >= this.ymax && pt2.y >= this.ymax)) { - this._lineTo(pt2.x, pt2.y, false); + (pt1.x >= this.xmax && pt2.x >= this.xmax) || + (pt1.y <= this.ymin && pt2.y <= this.ymin) || + (pt1.y >= this.ymax && pt2.y >= this.ymax)) { + this._lineTo(pt2.x, pt2.y, false); } else { // figure out various intercepts, store them if they are on the extent boundary @@ -196,33 +196,6 @@ VectorTileClipper.prototype.result = function() { this.lines.push(this.line); } return this.lines; - - /*// need to transform coordinates to go back to the expected 4096 extent - for (var i = 0; i < this.lines.length; i++) - { - var line = this.lines[i]; - for (var j = 0; j < line.length; j++) - { - var pt = line[j]; - - // snap points outside of extent - if (pt.x < this.xmin) - pt.x = this.xmin; - if (pt.x > this.xmax) - pt.x = this.xmax; - if (pt.y < this.ymin) - pt.y = this.ymin; - if (pt.y > this.ymax) - pt.y = this.ymax; - - // transform - pt.x = (pt.x - (this.xmin + this.margin)) * this.finalRatio; - pt.y = (pt.y - (this.ymin + this.margin)) * this.finalRatio; - } - } - - // done - return this.lines;*/ }; VectorTileClipper.prototype._isIn = function(x, y) { @@ -230,12 +203,7 @@ VectorTileClipper.prototype._isIn = function(x, y) { }; VectorTileClipper.prototype._intersect = function(inPt, outPt) { - var x, y; - - if (inPt.x == outPt.x && inPt.y == outPt.y) - { - x=0; - } + var x, y, xRatio, yRatio; if (outPt.x >= this.xmin && outPt.x <= this.xmax) { @@ -252,8 +220,8 @@ VectorTileClipper.prototype._intersect = function(inPt, outPt) { y = outPt.y <= this.ymin ? this.ymin : this.ymax; x = outPt.x <= this.xmin ? this.xmin : this.xmax; - var xRatio = (x - inPt.x) / (outPt.x - inPt.x); - var yRatio = (y - inPt.y) / (outPt.y - inPt.y); + xRatio = (x - inPt.x) / (outPt.x - inPt.x); + yRatio = (y - inPt.y) / (outPt.y - inPt.y); if (xRatio < yRatio) { y = inPt.y + xRatio * (outPt.y - inPt.y); @@ -276,15 +244,18 @@ VectorTileClipper.prototype._moveTo = function(x, y, isIn) { } else { // snap points outside of extent - if (x < this.xmin) - x = this.xmin; - if (x > this.xmax) - x = this.xmax; - if (y < this.ymin) - y = this.ymin; - if (y > this.ymax) - y = this.ymax; - + if (!isIn) + { + if (x < this.xmin) + x = this.xmin; + if (x > this.xmax) + x = this.xmax; + if (y < this.ymin) + y = this.ymin; + if (y > this.ymax) + y = this.ymax; + } + // transform x = (x - (this.xmin + this.margin)) * this.finalRatio; y = (y - (this.ymin + this.margin)) * this.finalRatio; @@ -317,14 +288,17 @@ VectorTileClipper.prototype._lineTo = function(x, y, isIn) { } else { // snap points outside of extent - if (x < this.xmin) - x = this.xmin; - if (x > this.xmax) - x = this.xmax; - if (y < this.ymin) - y = this.ymin; - if (y > this.ymax) - y = this.ymax; + if (!isIn) + { + if (x < this.xmin) + x = this.xmin; + if (x > this.xmax) + x = this.xmax; + if (y < this.ymin) + y = this.ymin; + if (y > this.ymax) + y = this.ymax; + } // transform x = (x - (this.xmin + this.margin)) * this.finalRatio; From b6c6f1ecc2f919631b7692b57d10fc6a56388160 Mon Sep 17 00:00:00 2001 From: James Cardona Date: Thu, 17 Sep 2015 20:33:08 -0400 Subject: [PATCH 03/13] temporary workaround for extent different than 4096 --- lib/vectortilefeature.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/vectortilefeature.js b/lib/vectortilefeature.js index 1a3e1c0..5627b22 100644 --- a/lib/vectortilefeature.js +++ b/lib/vectortilefeature.js @@ -372,8 +372,9 @@ VectorTileFeature.types = ['Unknown', 'Point', 'LineString', 'Polygon']; VectorTileFeature.prototype.loadGeometry = function() { // Test if the tile is overzoomed. We should use the clipping approach in this case - if (this.dz) + if (this.dz || this.extent > 4096) { return this.loadClippedGeometry(); + } var pbf = this._pbf; pbf.pos = this._geometry; @@ -424,8 +425,9 @@ VectorTileFeature.prototype.loadGeometry = function() { }; VectorTileFeature.prototype.loadClippedGeometry = function() { - if (!this.dz && !this.xPos && !this.yPos) - return []; + if (!this.dz && !this.xPos && !this.yPos) { + this.dz = this.xPos = this.yPos = 0; + } var pbf = this._pbf; pbf.pos = this._geometry; From 8d42bc60e9029936d3db0c15b778b6f693de113e Mon Sep 17 00:00:00 2001 From: marc4025 Date: Fri, 18 Sep 2015 13:56:03 -0700 Subject: [PATCH 04/13] prevent empty line arrays - do not clip leaf tiles with extent > 4096 --- lib/vectortilefeature.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/vectortilefeature.js b/lib/vectortilefeature.js index 5627b22..a52e26e 100644 --- a/lib/vectortilefeature.js +++ b/lib/vectortilefeature.js @@ -28,7 +28,7 @@ function VectorTileClipper(feature) { } VectorTileClipper.prototype.moveTo = function(x, y) { - if (this.line) this.lines.push(this.line); + if (this.line && this.line.length > 0) this.lines.push(this.line); this.line = []; //this.line.push(new Point(x, y)); this._prevIsIn = this._isIn(x, y); @@ -192,7 +192,7 @@ VectorTileClipper.prototype.closePolygon = function() { VectorTileClipper.prototype.result = function() { // add current line - if (this.line){ + if (this.line && this.line.length > 0) { this.lines.push(this.line); } return this.lines; @@ -372,7 +372,7 @@ VectorTileFeature.types = ['Unknown', 'Point', 'LineString', 'Polygon']; VectorTileFeature.prototype.loadGeometry = function() { // Test if the tile is overzoomed. We should use the clipping approach in this case - if (this.dz || this.extent > 4096) { + if (this.dz) { return this.loadClippedGeometry(); } @@ -426,7 +426,7 @@ VectorTileFeature.prototype.loadGeometry = function() { VectorTileFeature.prototype.loadClippedGeometry = function() { if (!this.dz && !this.xPos && !this.yPos) { - this.dz = this.xPos = this.yPos = 0; + return []; // this should not happen } var pbf = this._pbf; From d75bb23ac1e4b049e37af806acd3ecf7b05a04c2 Mon Sep 17 00:00:00 2001 From: marc4025 Date: Fri, 18 Sep 2015 15:01:02 -0700 Subject: [PATCH 05/13] return null instead of empty arrays --- lib/vectortilefeature.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/vectortilefeature.js b/lib/vectortilefeature.js index a52e26e..2b9d4b3 100644 --- a/lib/vectortilefeature.js +++ b/lib/vectortilefeature.js @@ -195,6 +195,8 @@ VectorTileClipper.prototype.result = function() { if (this.line && this.line.length > 0) { this.lines.push(this.line); } + if (this.lines.length === 0) + return null; return this.lines; }; @@ -426,7 +428,7 @@ VectorTileFeature.prototype.loadGeometry = function() { VectorTileFeature.prototype.loadClippedGeometry = function() { if (!this.dz && !this.xPos && !this.yPos) { - return []; // this should not happen + return null; // this should not happen } var pbf = this._pbf; From ccfaec8b94b33e32aba085b2b5096ab9836389c2 Mon Sep 17 00:00:00 2001 From: marc4025 Date: Mon, 21 Sep 2015 11:11:42 -0700 Subject: [PATCH 06/13] make tileExtent>4096 work --- lib/vectortilefeature.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/vectortilefeature.js b/lib/vectortilefeature.js index 2b9d4b3..0ac145d 100644 --- a/lib/vectortilefeature.js +++ b/lib/vectortilefeature.js @@ -378,6 +378,14 @@ VectorTileFeature.prototype.loadGeometry = function() { return this.loadClippedGeometry(); } + // couldn't get extent>4096 working without overflows so scale it down here + var dz = 0; + var ext = this.extent; + while (ext > 4096) { + dz += 1; + ext = ext >> 1; + } + var pbf = this._pbf; pbf.pos = this._geometry; @@ -407,7 +415,7 @@ VectorTileFeature.prototype.loadGeometry = function() { line = []; } - line.push(new Point(x, y)); + line.push(new Point(x >> dz, y >> dz)); } else if (cmd === 7) { @@ -427,10 +435,6 @@ VectorTileFeature.prototype.loadGeometry = function() { }; VectorTileFeature.prototype.loadClippedGeometry = function() { - if (!this.dz && !this.xPos && !this.yPos) { - return null; // this should not happen - } - var pbf = this._pbf; pbf.pos = this._geometry; From 7c64934266b23abbc59fbe910661e4624eca7a07 Mon Sep 17 00:00:00 2001 From: James Cardona Date: Wed, 30 Sep 2015 16:45:39 -0400 Subject: [PATCH 07/13] fix jshint errors --- lib/vectortilefeature.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/vectortilefeature.js b/lib/vectortilefeature.js index 0ac145d..304560d 100644 --- a/lib/vectortilefeature.js +++ b/lib/vectortilefeature.js @@ -444,9 +444,7 @@ VectorTileFeature.prototype.loadClippedGeometry = function() { cmd = 1, length = 0, x = 0, - y = 0, - lines = [], - line; + y = 0; while (pbf.pos < end) { if (!length) { From 4bb629c328e3d2f4e9bfb132c5158c0b9e717ddc Mon Sep 17 00:00:00 2001 From: marc4025 Date: Wed, 30 Sep 2015 16:52:30 -0700 Subject: [PATCH 08/13] Initial clipping test --- package.json | 2 +- test/clip.test.js | 146 +++++++++++++++++++++++++++++++++++++++++ test/fixtures/clip.pbf | Bin 0 -> 119 bytes 3 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 test/clip.test.js create mode 100644 test/fixtures/clip.pbf diff --git a/package.json b/package.json index faf4b3f..0d8378a 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "node": true }, "scripts": { - "test": "jshint lib && tape test/parse.test.js", + "test": "jshint lib && tape test/parse.test.js && tape test/clip.test.js", "cov": "istanbul cover ./node_modules/.bin/tape test/parse.test.js && coveralls < ./coverage/lcov.info" } } diff --git a/test/clip.test.js b/test/clip.test.js new file mode 100644 index 0000000..740e43b --- /dev/null +++ b/test/clip.test.js @@ -0,0 +1,146 @@ +var test = require('tape'), + fs = require('fs'), + Protobuf = require('pbf'), + VectorTile = require('..').VectorTile, + VectorTileLayer = require('..').VectorTileLayer, + VectorTileFeature = require('..').VectorTileFeature; + +test('check geometry clipping', function(t) { + var data = fs.readFileSync(__dirname + '/fixtures/clip.pbf'); + + //t.test('should have all layers', function(t) { + // var tile = new VectorTile(new Protobuf(data)); + + // t.deepEqual(Object.keys(tile.layers), [ + // 'polygon', 'line', 'point' ]); + + // t.end(); + //}); + + t.test('should return expected polygon', function(t) { + var tile = new VectorTile(new Protobuf(data)); + + var feature = tile.layers.polygon.feature(0); + //t.deepEqual(feature.extent, 32768); + //t.deepEqual(feature.type, 3); + + // define child tile + feature.dz = 2; + feature.xPos = 1; + feature.yPos = 1; + + // load geometry (should be clipped) + var geom = feature.loadGeometry(); + + // check result + t.deepEqual(geom, [[{ x: -64, y: 3072 }, { x: -64, y: -64 }, { x: 2611.2, y: -64 }, { x: 4160, y: 1872 }, { x: 4160, y: 3072 }, { x: -64, y: 3072 }]]); + + t.end(); + }); + + t.test('should return expected line', function (t) { + var tile = new VectorTile(new Protobuf(data)); + + var feature = tile.layers.line.feature(0); + //t.deepEqual(feature.extent, 32768); + //t.deepEqual(feature.type, 2); + + // define child tile + feature.dz = 2; + feature.xPos = 1; + feature.yPos = 1; + + // load geometry (should be clipped) + var geom = feature.loadGeometry(); + + // check result + t.deepEqual(geom, [[{ x: 2611.2, y: -64 }, { x: 4160, y: 1872 }]]); + + t.end(); + }); + + t.test('should return expected point', function (t) { + var tile = new VectorTile(new Protobuf(data)); + + var feature = tile.layers.point.feature(0); + //t.deepEqual(feature.extent, 32768); + //t.deepEqual(feature.type, 1); + + // define child tile + feature.dz = 2; + feature.xPos = 1; + feature.yPos = 1; + + // load geometry (should be clipped) + var geom = feature.loadGeometry(); + + // check result + t.deepEqual(geom, [[{ x: 1024, y: 3072 }, { x: 4096, y: 1024 }, { x: 4906, y: 4096 }]]); + + t.end(); + }); + + + t.test('should return null polygon', function (t) { + var tile = new VectorTile(new Protobuf(data)); + + var feature = tile.layers.polygon.feature(0); + //t.deepEqual(feature.extent, 32768); + //t.deepEqual(feature.type, 3); + + // define child tile + feature.dz = 2; + feature.xPos = 2; + feature.yPos = 2; + + // load geometry (should be clipped) + var geom = feature.loadGeometry(); + + // check result + t.deepEqual(geom, null); + + t.end(); + }); + + t.test('should return null line', function (t) { + var tile = new VectorTile(new Protobuf(data)); + + var feature = tile.layers.line.feature(0); + //t.deepEqual(feature.extent, 32768); + //t.deepEqual(feature.type, 2); + + // define child tile + feature.dz = 2; + feature.xPos = 2; + feature.yPos = 2; + + // load geometry (should be clipped) + var geom = feature.loadGeometry(); + + // check result + t.deepEqual(geom, null); + + t.end(); + }); + + t.test('should return null point', function (t) { + var tile = new VectorTile(new Protobuf(data)); + + var feature = tile.layers.point.feature(0); + //t.deepEqual(feature.extent, 32768); + //t.deepEqual(feature.type, 1); + + // define child tile + feature.dz = 2; + feature.xPos = 2; + feature.yPos = 2; + + // load geometry (should be clipped) + var geom = feature.loadGeometry(); + + // check result + t.deepEqual(geom, null); + + t.end(); + }); +}); diff --git a/test/fixtures/clip.pbf b/test/fixtures/clip.pbf new file mode 100644 index 0000000000000000000000000000000000000000..2173c8bae392c2bd4c83c4f9e71815c2a81e18bb GIT binary patch literal 119 zcmb2r;9@Vxq$&l8fAU{(_4Y;b6Jz$nG=-@YN?e|$p&W5WVQevO6(CMgvzmYmGI vR3QloCM6MU%D}2rxmXMGGxJJ-${3YI6oI;d#sQ78{~r%DE}`K7BS;wlQYt1V literal 0 HcmV?d00001 From b156f0ed335dd5fed3f2dc63716310a8ab5c68b0 Mon Sep 17 00:00:00 2001 From: marc4025 Date: Thu, 1 Oct 2015 10:41:41 -0700 Subject: [PATCH 09/13] fixed degenerated cases after clipping --- lib/vectortilefeature.js | 39 +++++++++++++++++++++++++++------------ test/clip.test.js | 4 ++-- 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/lib/vectortilefeature.js b/lib/vectortilefeature.js index 304560d..641db84 100644 --- a/lib/vectortilefeature.js +++ b/lib/vectortilefeature.js @@ -24,13 +24,12 @@ function VectorTileClipper(feature) { this.lines = []; this._prevIsIn = false; - this._isLine = feature.type !== 3; // consider points like line (ignore everything outside) + this.type = feature.type; } VectorTileClipper.prototype.moveTo = function(x, y) { - if (this.line && this.line.length > 0) this.lines.push(this.line); - this.line = []; - //this.line.push(new Point(x, y)); + this._push_line(); + this._prevIsIn = this._isIn(x, y); this._moveTo(x, y, this._prevIsIn); @@ -192,9 +191,8 @@ VectorTileClipper.prototype.closePolygon = function() { VectorTileClipper.prototype.result = function() { // add current line - if (this.line && this.line.length > 0) { - this.lines.push(this.line); - } + this._push_line(); + if (this.lines.length === 0) return null; return this.lines; @@ -236,8 +234,26 @@ VectorTileClipper.prototype._intersect = function(inPt, outPt) { return new Point(x, y); }; -VectorTileClipper.prototype._moveTo = function(x, y, isIn) { - if (this._isLine) { +VectorTileClipper.prototype._push_line = function () { + if (this.line) { + if (this.type === 1) { // point + if (this.line.length > 0) + this.lines.push(this.line); + } + else if (this.type === 2) { // line + if (this.line.length > 1) + this.lines.push(this.line); + } + else if (this.type === 3) { // polygon + if (this.line.length > 3) + this.lines.push(this.line); + } + } + this.line = []; +}; + +VectorTileClipper.prototype._moveTo = function (x, y, isIn) { + if (this.type !== 3) { if (isIn) { x = (x - (this.xmin + this.margin)) * this.finalRatio; y = (y - (this.ymin + this.margin)) * this.finalRatio; @@ -272,7 +288,7 @@ VectorTileClipper.prototype._moveTo = function(x, y, isIn) { VectorTileClipper.prototype._lineTo = function(x, y, isIn) { var lastPt, prevPt; - if (this._isLine) { + if (this.type !== 3) { if (isIn) { x = (x - (this.xmin + this.margin)) * this.finalRatio; y = (y - (this.ymin + this.margin)) * this.finalRatio; @@ -284,8 +300,7 @@ VectorTileClipper.prototype._lineTo = function(x, y, isIn) { this.line.push(new Point(x, y)); } else if (this.line && this.line.length > 0) { - this.lines.push(this.line); - this.line = []; + this._push_line(); } } else { diff --git a/test/clip.test.js b/test/clip.test.js index 740e43b..ff6e429 100644 --- a/test/clip.test.js +++ b/test/clip.test.js @@ -132,8 +132,8 @@ test('check geometry clipping', function(t) { // define child tile feature.dz = 2; - feature.xPos = 2; - feature.yPos = 2; + feature.xPos = 3; + feature.yPos = 3; // load geometry (should be clipped) var geom = feature.loadGeometry(); From cbf24627b7772ca11d7502285bce90af29788f5b Mon Sep 17 00:00:00 2001 From: marc4025 Date: Thu, 1 Oct 2015 10:54:39 -0700 Subject: [PATCH 10/13] missed that change --- test/clip.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/clip.test.js b/test/clip.test.js index ff6e429..b72610a 100644 --- a/test/clip.test.js +++ b/test/clip.test.js @@ -74,8 +74,8 @@ test('check geometry clipping', function(t) { // load geometry (should be clipped) var geom = feature.loadGeometry(); - // check result - t.deepEqual(geom, [[{ x: 1024, y: 3072 }, { x: 4096, y: 1024 }, { x: 4906, y: 4096 }]]); + // check result + t.deepEqual(geom, [[{ x: 1024, y: 3072 }], [{ x: 4096, y: 1024 }], [{ x: 4906, y: 4096 }]]); t.end(); }); From 2d1f8a43be76117d9805ced314615f166efd6ff1 Mon Sep 17 00:00:00 2001 From: marc4025 Date: Thu, 1 Oct 2015 10:55:54 -0700 Subject: [PATCH 11/13] fixed typo --- test/clip.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/clip.test.js b/test/clip.test.js index b72610a..f50b9aa 100644 --- a/test/clip.test.js +++ b/test/clip.test.js @@ -75,7 +75,7 @@ test('check geometry clipping', function(t) { var geom = feature.loadGeometry(); // check result - t.deepEqual(geom, [[{ x: 1024, y: 3072 }], [{ x: 4096, y: 1024 }], [{ x: 4906, y: 4096 }]]); + t.deepEqual(geom, [[{ x: 1024, y: 3072 }], [{ x: 4096, y: 1024 }], [{ x: 4096, y: 4096 }]]); t.end(); }); From 9e67b5f2fc1e3c48cc420bfb615d48575fed0a3a Mon Sep 17 00:00:00 2001 From: James Cardona Date: Fri, 2 Oct 2015 18:30:27 -0400 Subject: [PATCH 12/13] moved VectorTileClipper to new module --- lib/vectortileclipper.js | 396 ++++++++++++++++++++++++++++++++ lib/vectortilefeature.js | 484 ++++----------------------------------- test/clip.test.js | 27 +-- 3 files changed, 449 insertions(+), 458 deletions(-) create mode 100644 lib/vectortileclipper.js diff --git a/lib/vectortileclipper.js b/lib/vectortileclipper.js new file mode 100644 index 0000000..46cdb63 --- /dev/null +++ b/lib/vectortileclipper.js @@ -0,0 +1,396 @@ +'use strict'; + +var Point = require('point-geometry'); + +module.exports = VectorTileClipper; + +function VectorTileClipper(feature) { + this.feature = feature; + + // the ratio we'll need when producing the final result to extend back (or still reduce) to 4096 + this.finalRatio = 4096 / feature.extent * Math.pow(2, feature.dz); + + var margin = 64; // 8px times 4096/512 + margin /= this.finalRatio; + + var clipExtent = feature.extent >> feature.dz; + if (margin > clipExtent) + margin = clipExtent; + + this.dz = feature.dz; + this.margin = margin; + this.xmin = clipExtent * feature.xPos - margin; + this.ymin = clipExtent * feature.yPos - margin; + this.xmax = this.xmin + clipExtent + 2 * margin; + this.ymax = this.ymin + clipExtent + 2 * margin; + this.lines = []; + + this._prevIsIn = false; + this.type = feature.type; +} + +VectorTileClipper.prototype.loadGeometry = function() { + var pbf = this.feature._pbf; + pbf.pos = this.feature._geometry; + + var end = pbf.readVarint() + pbf.pos, + cmd = 1, + length = 0, + x = 0, + y = 0; + + while (pbf.pos < end) { + if (!length) { + var cmdLen = pbf.readVarint(); + cmd = cmdLen & 0x7; + length = cmdLen >> 3; + } + + length--; + + if (cmd === 1 || cmd === 2) { + x += pbf.readSVarint(); + y += pbf.readSVarint(); + + if (cmd === 1) { // moveTo + this.moveTo(x, y); + } + else { // lineTo + this.lineTo(x, y); + } + } else if (cmd === 7) { + this.closePolygon(); + } else { + throw new Error('unknown command ' + cmd); + } + } + + return this.result(); +}; + +VectorTileClipper.prototype.moveTo = function(x, y) { + this._push_line(); + + this._prevIsIn = this._isIn(x, y); + this._moveTo(x, y, this._prevIsIn); + + this._prevPt = new Point(x, y); + this._firstPt = new Point(x, y); +}; + +VectorTileClipper.prototype.lineTo = function(x, y) { + var isIn = this._isIn(x, y), + outPt, inPt, midPt, + pt1, pt2, ratio, + intercept, intercepts, + xpos, ypos, + xtest, ytest; + if (isIn) { + if (this._prevIsIn){ + // both in: just push + this._lineTo(x, y, true); + } + else { + outPt = this._prevPt; + inPt = new Point(x, y); + midPt = this._intersect(inPt, outPt); + this._lineTo(midPt.x, midPt.y, true); + this._lineTo(inPt.x, inPt.y, true); + } + } + else { + if (this._prevIsIn) { + inPt = this._prevPt; + outPt = new Point(x, y); + midPt = this._intersect(inPt, outPt); + this._lineTo(midPt.x, midPt.y, true); + this._lineTo(outPt.x, outPt.y, false); + } + else { + // going from pt1 to pt2 + pt1 = this._prevPt; + pt2 = new Point(x, y); + + // both points are outside but we could have two intersection points + // first, rule out obvious non intersecting cases + if ((pt1.x <= this.xmin && pt2.x <= this.xmin) || + (pt1.x >= this.xmax && pt2.x >= this.xmax) || + (pt1.y <= this.ymin && pt2.y <= this.ymin) || + (pt1.y >= this.ymax && pt2.y >= this.ymax)) { + this._lineTo(pt2.x, pt2.y, false); + } + else { + // figure out various intercepts, store them if they are on the extent boundary + intercepts = []; + + // xpos and ypos are bool to indicate if below min (false) or above max (true) + if ((pt1.x < this.xmin && pt2.x > this.xmin) || (pt1.x > this.xmin && pt2.x < this.xmin)) { + ratio = (this.xmin - pt1.x) / (pt2.x - pt1.x); + ytest = pt1.y + ratio * (pt2.y - pt1.y); + if (ytest <= this.ymin) + ypos = false; + else if (ytest >= this.ymax) + ypos = true; + else { + intercept = {}; + intercept.ratio = ratio; + intercept.x = this.xmin; + intercept.y = ytest; + intercepts.push(intercept); + } + } + if ((pt1.x < this.xmax && pt2.x > this.xmax) || (pt1.x > this.xmax && pt2.x < this.xmax)) { + ratio = (this.xmax - pt1.x) / (pt2.x - pt1.x); + ytest = pt1.y + ratio * (pt2.y - pt1.y); + if (ytest <= this.ymin) + ypos = false; + else if (ytest >= this.ymax) + ypos = true; + else { + intercept = {}; + intercept.ratio = ratio; + intercept.x = this.xmax; + intercept.y = ytest; + intercepts.push(intercept); + } + } + if ((pt1.y < this.ymin && pt2.y > this.ymin) || (pt1.y > this.ymin && pt2.y < this.ymin)) { + ratio = (this.ymin - pt1.y) / (pt2.y - pt1.y); + xtest = pt1.x + ratio * (pt2.x - pt1.x); + if (xtest <= this.xmin) + xpos = false; + else if (xtest >= this.xmax) + xpos = true; + else { + intercept = {}; + intercept.ratio = ratio; + intercept.x = xtest; + intercept.y = this.ymin; + intercepts.push(intercept); + } + } + if ((pt1.y < this.ymax && pt2.y > this.ymax) || (pt1.y > this.ymax && pt2.y < this.ymax)) { + ratio = (this.ymax - pt1.y) / (pt2.y - pt1.y); + xtest = pt1.x + ratio * (pt2.x - pt1.x); + if (xtest <= this.xmin) + xpos = false; + else if (xtest >= this.xmax) + xpos = true; + else { + intercept = {}; + intercept.ratio = ratio; + intercept.x = xtest; + intercept.y = this.ymax; + intercepts.push(intercept); + } + } + // intercepts has no more than two elements + if (intercepts.length === 0) { + // add the corresponding corner + if (xpos) { + if (ypos) { + this._lineTo(this.xmax, this.ymax, true); + } + else { + this._lineTo(this.xmax, this.ymin, true); + } + } + else { + if (ypos) { + this._lineTo(this.xmin, this.ymax, true); + } + else { + this._lineTo(this.xmin, this.ymin, true); + } + } + } + else if ((intercepts.length > 1) && (intercepts[0].ratio > intercepts[1].ratio)) { + this._lineTo(intercepts[1].x, intercepts[1].y, true); + this._lineTo(intercepts[0].x, intercepts[0].y, true); + } + else { + for (var i = 0; i < intercepts.length; i++) + this._lineTo(intercepts[i].x, intercepts[i].y, true); + } + this._lineTo(pt2.x, pt2.y, false); + } + } + } + this._prevIsIn = isIn; + this._prevPt = new Point(x, y); +}; + +VectorTileClipper.prototype.closePolygon = function() { + var firstPt, lastPt; + if (this.line.length > 0) { + firstPt = this._firstPt; + lastPt = this._prevPt; + if (firstPt.x !== lastPt.x || firstPt.y !== lastPt.y) + this.lineTo(firstPt.x, firstPt.y); + } +}; + +VectorTileClipper.prototype.result = function() { + // add current line + this._push_line(); + + if (this.lines.length === 0) + return null; + return this.lines; +}; + +VectorTileClipper.prototype._isIn = function(x, y) { + return x >= this.xmin && x <= this.xmax && y >= this.ymin && y <= this.ymax; +}; + +VectorTileClipper.prototype._intersect = function(inPt, outPt) { + var x, y, xRatio, yRatio; + + if (outPt.x >= this.xmin && outPt.x <= this.xmax) + { + y = outPt.y <= this.ymin ? this.ymin : this.ymax; + x = inPt.x + (y - inPt.y) / (outPt.y - inPt.y) * (outPt.x - inPt.x); + } + else if (outPt.y >= this.ymin && outPt.y <= this.ymax) + { + x = outPt.x <= this.xmin ? this.xmin : this.xmax; + y = inPt.y + (x - inPt.x) / (outPt.x - inPt.x) * (outPt.y - inPt.y); + } + else + { + y = outPt.y <= this.ymin ? this.ymin : this.ymax; + x = outPt.x <= this.xmin ? this.xmin : this.xmax; + + xRatio = (x - inPt.x) / (outPt.x - inPt.x); + yRatio = (y - inPt.y) / (outPt.y - inPt.y); + if (xRatio < yRatio) + { + y = inPt.y + xRatio * (outPt.y - inPt.y); + } + else + { + x = inPt.x + yRatio * (outPt.x - inPt.x); + } + } + return new Point(x, y); +}; + +VectorTileClipper.prototype._push_line = function () { + if (this.line) { + if (this.type === 1) { // point + if (this.line.length > 0) + this.lines.push(this.line); + } + else if (this.type === 2) { // line + if (this.line.length > 1) + this.lines.push(this.line); + } + else if (this.type === 3) { // polygon + if (this.line.length > 3) + this.lines.push(this.line); + } + } + this.line = []; +}; + +VectorTileClipper.prototype._moveTo = function (x, y, isIn) { + if (this.type !== 3) { + if (isIn) { + x = (x - (this.xmin + this.margin)) * this.finalRatio; + y = (y - (this.ymin + this.margin)) * this.finalRatio; + this.line.push(new Point(x, y)); + } + } + else { + // snap points outside of extent + if (!isIn) + { + if (x < this.xmin) + x = this.xmin; + if (x > this.xmax) + x = this.xmax; + if (y < this.ymin) + y = this.ymin; + if (y > this.ymax) + y = this.ymax; + } + + // transform + x = (x - (this.xmin + this.margin)) * this.finalRatio; + y = (y - (this.ymin + this.margin)) * this.finalRatio; + + this.line.push(new Point(x, y)); + + this._is_h = false; + this._is_v = false; + } +}; + +VectorTileClipper.prototype._lineTo = function(x, y, isIn) { + var lastPt, prevPt; + + if (this.type !== 3) { + if (isIn) { + x = (x - (this.xmin + this.margin)) * this.finalRatio; + y = (y - (this.ymin + this.margin)) * this.finalRatio; + if (this.line.length > 0) { + lastPt = this.line[this.line.length - 1]; + if (lastPt.x === x && lastPt.y === y) + return; + } + this.line.push(new Point(x, y)); + } + else if (this.line && this.line.length > 0) { + this._push_line(); + } + } + else { + // snap points outside of extent + if (!isIn) + { + if (x < this.xmin) + x = this.xmin; + if (x > this.xmax) + x = this.xmax; + if (y < this.ymin) + y = this.ymin; + if (y > this.ymax) + y = this.ymax; + } + + // transform + x = (x - (this.xmin + this.margin)) * this.finalRatio; + y = (y - (this.ymin + this.margin)) * this.finalRatio; + + if (this.line && this.line.length > 0) { + lastPt = this.line[this.line.length - 1]; + var is_h = lastPt.x === x; + var is_v = lastPt.y === y; + if (is_h && is_v) + return; + + if (this._is_h && is_h) { + lastPt.x = x; + lastPt.y = y; + prevPt = this.line[this.line.length - 2]; // valid if this._is_h is true + this._is_h = prevPt.x === x; + this._is_v = prevPt.y === y; + } + else if (this._is_v && is_v) { + lastPt.x = x; + lastPt.y = y; + prevPt = this.line[this.line.length - 2]; // valid if this._is_v is true + this._is_h = prevPt.x === x; + this._is_v = prevPt.y === y; + } + else { + this.line.push(new Point(x, y)); + this._is_h = is_h; + this._is_v = is_v; + } + } + else { + this.line.push(new Point(x, y)); // should never happen actually + } + } +}; diff --git a/lib/vectortilefeature.js b/lib/vectortilefeature.js index 641db84..777ae2a 100644 --- a/lib/vectortilefeature.js +++ b/lib/vectortilefeature.js @@ -1,358 +1,10 @@ 'use strict'; var Point = require('point-geometry'); +var VectorTileClipper = require('./vectortileclipper'); module.exports = VectorTileFeature; -function VectorTileClipper(feature) { - // the ratio we'll need when producing the final result to extend back (or still reduce) to 4096 - this.finalRatio = 4096 / feature.extent * Math.pow(2, feature.dz); - - var margin = 64; // 8px times 4096/512 - margin /= this.finalRatio; - - var clipExtent = feature.extent >> feature.dz; - if (margin > clipExtent) - margin = clipExtent; - - this.dz = feature.dz; - this.margin = margin; - this.xmin = clipExtent * feature.xPos - margin; - this.ymin = clipExtent * feature.yPos - margin; - this.xmax = this.xmin + clipExtent + 2 * margin; - this.ymax = this.ymin + clipExtent + 2 * margin; - this.lines = []; - - this._prevIsIn = false; - this.type = feature.type; -} - -VectorTileClipper.prototype.moveTo = function(x, y) { - this._push_line(); - - this._prevIsIn = this._isIn(x, y); - this._moveTo(x, y, this._prevIsIn); - - this._prevPt = new Point(x, y); - this._firstPt = new Point(x, y); -}; - -VectorTileClipper.prototype.lineTo = function(x, y) { - var isIn = this._isIn(x, y), - outPt, inPt, midPt, - pt1, pt2, ratio, - intercept, intercepts, - xpos, ypos, - xtest, ytest; - if (isIn) { - if (this._prevIsIn){ - // both in: just push - this._lineTo(x, y, true); - } - else { - outPt = this._prevPt; - inPt = new Point(x, y); - midPt = this._intersect(inPt, outPt); - this._lineTo(midPt.x, midPt.y, true); - this._lineTo(inPt.x, inPt.y, true); - } - } - else { - if (this._prevIsIn) { - inPt = this._prevPt; - outPt = new Point(x, y); - midPt = this._intersect(inPt, outPt); - this._lineTo(midPt.x, midPt.y, true); - this._lineTo(outPt.x, outPt.y, false); - } - else { - // going from pt1 to pt2 - pt1 = this._prevPt; - pt2 = new Point(x, y); - - // both points are outside but we could have two intersection points - // first, rule out obvious non intersecting cases - if ((pt1.x <= this.xmin && pt2.x <= this.xmin) || - (pt1.x >= this.xmax && pt2.x >= this.xmax) || - (pt1.y <= this.ymin && pt2.y <= this.ymin) || - (pt1.y >= this.ymax && pt2.y >= this.ymax)) { - this._lineTo(pt2.x, pt2.y, false); - } - else { - // figure out various intercepts, store them if they are on the extent boundary - intercepts = []; - - // xpos and ypos are bool to indicate if below min (false) or above max (true) - if ((pt1.x < this.xmin && pt2.x > this.xmin) || (pt1.x > this.xmin && pt2.x < this.xmin)) { - ratio = (this.xmin - pt1.x) / (pt2.x - pt1.x); - ytest = pt1.y + ratio * (pt2.y - pt1.y); - if (ytest <= this.ymin) - ypos = false; - else if (ytest >= this.ymax) - ypos = true; - else { - intercept = {}; - intercept.ratio = ratio; - intercept.x = this.xmin; - intercept.y = ytest; - intercepts.push(intercept); - } - } - if ((pt1.x < this.xmax && pt2.x > this.xmax) || (pt1.x > this.xmax && pt2.x < this.xmax)) { - ratio = (this.xmax - pt1.x) / (pt2.x - pt1.x); - ytest = pt1.y + ratio * (pt2.y - pt1.y); - if (ytest <= this.ymin) - ypos = false; - else if (ytest >= this.ymax) - ypos = true; - else { - intercept = {}; - intercept.ratio = ratio; - intercept.x = this.xmax; - intercept.y = ytest; - intercepts.push(intercept); - } - } - if ((pt1.y < this.ymin && pt2.y > this.ymin) || (pt1.y > this.ymin && pt2.y < this.ymin)) { - ratio = (this.ymin - pt1.y) / (pt2.y - pt1.y); - xtest = pt1.x + ratio * (pt2.x - pt1.x); - if (xtest <= this.xmin) - xpos = false; - else if (xtest >= this.xmax) - xpos = true; - else { - intercept = {}; - intercept.ratio = ratio; - intercept.x = xtest; - intercept.y = this.ymin; - intercepts.push(intercept); - } - } - if ((pt1.y < this.ymax && pt2.y > this.ymax) || (pt1.y > this.ymax && pt2.y < this.ymax)) { - ratio = (this.ymax - pt1.y) / (pt2.y - pt1.y); - xtest = pt1.x + ratio * (pt2.x - pt1.x); - if (xtest <= this.xmin) - xpos = false; - else if (xtest >= this.xmax) - xpos = true; - else { - intercept = {}; - intercept.ratio = ratio; - intercept.x = xtest; - intercept.y = this.ymax; - intercepts.push(intercept); - } - } - // intercepts has no more than two elements - if (intercepts.length === 0) { - // add the corresponding corner - if (xpos) { - if (ypos) { - this._lineTo(this.xmax, this.ymax, true); - } - else { - this._lineTo(this.xmax, this.ymin, true); - } - } - else { - if (ypos) { - this._lineTo(this.xmin, this.ymax, true); - } - else { - this._lineTo(this.xmin, this.ymin, true); - } - } - } - else if ((intercepts.length > 1) && (intercepts[0].ratio > intercepts[1].ratio)) { - this._lineTo(intercepts[1].x, intercepts[1].y, true); - this._lineTo(intercepts[0].x, intercepts[0].y, true); - } - else { - for (var i = 0; i < intercepts.length; i++) - this._lineTo(intercepts[i].x, intercepts[i].y, true); - } - this._lineTo(pt2.x, pt2.y, false); - } - } - } - this._prevIsIn = isIn; - this._prevPt = new Point(x, y); -}; - -VectorTileClipper.prototype.closePolygon = function() { - var firstPt, lastPt; - if (this.line.length > 0) { - firstPt = this._firstPt; - lastPt = this._prevPt; - if (firstPt.x !== lastPt.x || firstPt.y !== lastPt.y) - this.lineTo(firstPt.x, firstPt.y); - } -}; - -VectorTileClipper.prototype.result = function() { - // add current line - this._push_line(); - - if (this.lines.length === 0) - return null; - return this.lines; -}; - -VectorTileClipper.prototype._isIn = function(x, y) { - return x >= this.xmin && x <= this.xmax && y >= this.ymin && y <= this.ymax; -}; - -VectorTileClipper.prototype._intersect = function(inPt, outPt) { - var x, y, xRatio, yRatio; - - if (outPt.x >= this.xmin && outPt.x <= this.xmax) - { - y = outPt.y <= this.ymin ? this.ymin : this.ymax; - x = inPt.x + (y - inPt.y) / (outPt.y - inPt.y) * (outPt.x - inPt.x); - } - else if (outPt.y >= this.ymin && outPt.y <= this.ymax) - { - x = outPt.x <= this.xmin ? this.xmin : this.xmax; - y = inPt.y + (x - inPt.x) / (outPt.x - inPt.x) * (outPt.y - inPt.y); - } - else - { - y = outPt.y <= this.ymin ? this.ymin : this.ymax; - x = outPt.x <= this.xmin ? this.xmin : this.xmax; - - xRatio = (x - inPt.x) / (outPt.x - inPt.x); - yRatio = (y - inPt.y) / (outPt.y - inPt.y); - if (xRatio < yRatio) - { - y = inPt.y + xRatio * (outPt.y - inPt.y); - } - else - { - x = inPt.x + yRatio * (outPt.x - inPt.x); - } - } - return new Point(x, y); -}; - -VectorTileClipper.prototype._push_line = function () { - if (this.line) { - if (this.type === 1) { // point - if (this.line.length > 0) - this.lines.push(this.line); - } - else if (this.type === 2) { // line - if (this.line.length > 1) - this.lines.push(this.line); - } - else if (this.type === 3) { // polygon - if (this.line.length > 3) - this.lines.push(this.line); - } - } - this.line = []; -}; - -VectorTileClipper.prototype._moveTo = function (x, y, isIn) { - if (this.type !== 3) { - if (isIn) { - x = (x - (this.xmin + this.margin)) * this.finalRatio; - y = (y - (this.ymin + this.margin)) * this.finalRatio; - this.line.push(new Point(x, y)); - } - } - else { - // snap points outside of extent - if (!isIn) - { - if (x < this.xmin) - x = this.xmin; - if (x > this.xmax) - x = this.xmax; - if (y < this.ymin) - y = this.ymin; - if (y > this.ymax) - y = this.ymax; - } - - // transform - x = (x - (this.xmin + this.margin)) * this.finalRatio; - y = (y - (this.ymin + this.margin)) * this.finalRatio; - - this.line.push(new Point(x, y)); - - this._is_h = false; - this._is_v = false; - } -}; - -VectorTileClipper.prototype._lineTo = function(x, y, isIn) { - var lastPt, prevPt; - - if (this.type !== 3) { - if (isIn) { - x = (x - (this.xmin + this.margin)) * this.finalRatio; - y = (y - (this.ymin + this.margin)) * this.finalRatio; - if (this.line.length > 0) { - lastPt = this.line[this.line.length - 1]; - if (lastPt.x === x && lastPt.y === y) - return; - } - this.line.push(new Point(x, y)); - } - else if (this.line && this.line.length > 0) { - this._push_line(); - } - } - else { - // snap points outside of extent - if (!isIn) - { - if (x < this.xmin) - x = this.xmin; - if (x > this.xmax) - x = this.xmax; - if (y < this.ymin) - y = this.ymin; - if (y > this.ymax) - y = this.ymax; - } - - // transform - x = (x - (this.xmin + this.margin)) * this.finalRatio; - y = (y - (this.ymin + this.margin)) * this.finalRatio; - - if (this.line && this.line.length > 0) { - lastPt = this.line[this.line.length - 1]; - var is_h = lastPt.x === x; - var is_v = lastPt.y === y; - if (is_h && is_v) - return; - - if (this._is_h && is_h) { - lastPt.x = x; - lastPt.y = y; - prevPt = this.line[this.line.length - 2]; // valid if this._is_h is true - this._is_h = prevPt.x === x; - this._is_v = prevPt.y === y; - } - else if (this._is_v && is_v) { - lastPt.x = x; - lastPt.y = y; - prevPt = this.line[this.line.length - 2]; // valid if this._is_v is true - this._is_h = prevPt.x === x; - this._is_v = prevPt.y === y; - } - else { - this.line.push(new Point(x, y)); - this._is_h = is_h; - this._is_v = is_v; - } - } - else { - this.line.push(new Point(x, y)); // should never happen actually - } - } -}; function VectorTileFeature(pbf, end, extent, keys, values) { // Public this.properties = {}; @@ -388,106 +40,70 @@ function readTag(pbf, feature) { VectorTileFeature.types = ['Unknown', 'Point', 'LineString', 'Polygon']; VectorTileFeature.prototype.loadGeometry = function() { + var lines; + // Test if the tile is overzoomed. We should use the clipping approach in this case if (this.dz) { - return this.loadClippedGeometry(); - } - - // couldn't get extent>4096 working without overflows so scale it down here - var dz = 0; - var ext = this.extent; - while (ext > 4096) { - dz += 1; - ext = ext >> 1; - } - - var pbf = this._pbf; - pbf.pos = this._geometry; - - var end = pbf.readVarint() + pbf.pos, - cmd = 1, - length = 0, - x = 0, - y = 0, - lines = [], - line; - - while (pbf.pos < end) { - if (!length) { - var cmdLen = pbf.readVarint(); - cmd = cmdLen & 0x7; - length = cmdLen >> 3; - } - - length--; - - if (cmd === 1 || cmd === 2) { - x += pbf.readSVarint(); - y += pbf.readSVarint(); - - if (cmd === 1) { // moveTo - if (line) lines.push(line); - line = []; + var clipper = new VectorTileClipper(this); + lines = clipper.loadGeometry(); + } else { + // couldn't get extent>4096 working without overflows so scale it down here + var dz = 0; + var ext = this.extent; + while (ext > 4096) { + dz += 1; + ext = ext >> 1; + } + + var pbf = this._pbf; + pbf.pos = this._geometry; + + var end = pbf.readVarint() + pbf.pos, + cmd = 1, + length = 0, + x = 0, + y = 0, + line; + + lines = []; + while (pbf.pos < end) { + if (!length) { + var cmdLen = pbf.readVarint(); + cmd = cmdLen & 0x7; + length = cmdLen >> 3; } - line.push(new Point(x >> dz, y >> dz)); + length--; - } else if (cmd === 7) { + if (cmd === 1 || cmd === 2) { + x += pbf.readSVarint(); + y += pbf.readSVarint(); - // Workaround for https://github.com/mapbox/mapnik-vector-tile/issues/90 - if (line) { - line.push(line[0].clone()); // closePolygon - } - - } else { - throw new Error('unknown command ' + cmd); - } - } - - if (line) lines.push(line); - - return lines; -}; - -VectorTileFeature.prototype.loadClippedGeometry = function() { - var pbf = this._pbf; - pbf.pos = this._geometry; - - var clipper = new VectorTileClipper(this); - - var end = pbf.readVarint() + pbf.pos, - cmd = 1, - length = 0, - x = 0, - y = 0; + if (cmd === 1) { // moveTo + if (line) lines.push(line); + line = []; + } - while (pbf.pos < end) { - if (!length) { - var cmdLen = pbf.readVarint(); - cmd = cmdLen & 0x7; - length = cmdLen >> 3; - } + line.push(new Point(x >> dz, y >> dz)); - length--; + } + else if (cmd === 7) { - if (cmd === 1 || cmd === 2) { - x += pbf.readSVarint(); - y += pbf.readSVarint(); + // Workaround for https://github.com/mapbox/mapnik-vector-tile/issues/90 + if (line) { + line.push(line[0].clone()); // closePolygon + } - if (cmd === 1) { // moveTo - clipper.moveTo(x, y); } - else { // lineTo - clipper.lineTo(x, y); + else { + throw new Error('unknown command ' + cmd); } - } else if (cmd === 7) { - clipper.closePolygon(); - } else { - throw new Error('unknown command ' + cmd); } + + if (line) lines.push(line); } - return clipper.result(); + return lines; }; VectorTileFeature.prototype.bbox = function() { diff --git a/test/clip.test.js b/test/clip.test.js index f50b9aa..07879de 100644 --- a/test/clip.test.js +++ b/test/clip.test.js @@ -8,21 +8,10 @@ var test = require('tape'), test('check geometry clipping', function(t) { var data = fs.readFileSync(__dirname + '/fixtures/clip.pbf'); - //t.test('should have all layers', function(t) { - // var tile = new VectorTile(new Protobuf(data)); - - // t.deepEqual(Object.keys(tile.layers), [ - // 'polygon', 'line', 'point' ]); - - // t.end(); - //}); - t.test('should return expected polygon', function(t) { var tile = new VectorTile(new Protobuf(data)); var feature = tile.layers.polygon.feature(0); - //t.deepEqual(feature.extent, 32768); - //t.deepEqual(feature.type, 3); // define child tile feature.dz = 2; @@ -42,8 +31,6 @@ test('check geometry clipping', function(t) { var tile = new VectorTile(new Protobuf(data)); var feature = tile.layers.line.feature(0); - //t.deepEqual(feature.extent, 32768); - //t.deepEqual(feature.type, 2); // define child tile feature.dz = 2; @@ -63,8 +50,6 @@ test('check geometry clipping', function(t) { var tile = new VectorTile(new Protobuf(data)); var feature = tile.layers.point.feature(0); - //t.deepEqual(feature.extent, 32768); - //t.deepEqual(feature.type, 1); // define child tile feature.dz = 2; @@ -85,8 +70,6 @@ test('check geometry clipping', function(t) { var tile = new VectorTile(new Protobuf(data)); var feature = tile.layers.polygon.feature(0); - //t.deepEqual(feature.extent, 32768); - //t.deepEqual(feature.type, 3); // define child tile feature.dz = 2; @@ -97,7 +80,7 @@ test('check geometry clipping', function(t) { var geom = feature.loadGeometry(); // check result - t.deepEqual(geom, null); + t.equal(geom, null); t.end(); }); @@ -106,8 +89,6 @@ test('check geometry clipping', function(t) { var tile = new VectorTile(new Protobuf(data)); var feature = tile.layers.line.feature(0); - //t.deepEqual(feature.extent, 32768); - //t.deepEqual(feature.type, 2); // define child tile feature.dz = 2; @@ -118,7 +99,7 @@ test('check geometry clipping', function(t) { var geom = feature.loadGeometry(); // check result - t.deepEqual(geom, null); + t.equal(geom, null); t.end(); }); @@ -127,8 +108,6 @@ test('check geometry clipping', function(t) { var tile = new VectorTile(new Protobuf(data)); var feature = tile.layers.point.feature(0); - //t.deepEqual(feature.extent, 32768); - //t.deepEqual(feature.type, 1); // define child tile feature.dz = 2; @@ -139,7 +118,7 @@ test('check geometry clipping', function(t) { var geom = feature.loadGeometry(); // check result - t.deepEqual(geom, null); + t.equal(geom, null); t.end(); }); From 255616ad3be638869cf936c48e31c3bba6d53b58 Mon Sep 17 00:00:00 2001 From: James Cardona Date: Wed, 4 Nov 2015 09:21:56 -0500 Subject: [PATCH 13/13] changed test script to run all test.js as batch --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0d8378a..61333f3 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "node": true }, "scripts": { - "test": "jshint lib && tape test/parse.test.js && tape test/clip.test.js", + "test": "jshint lib && tape test/*.test.js", "cov": "istanbul cover ./node_modules/.bin/tape test/parse.test.js && coveralls < ./coverage/lcov.info" } }