From ce61840c38301c9f66de266520ff8f5031b32942 Mon Sep 17 00:00:00 2001 From: Young Hahn Date: Tue, 24 May 2016 19:18:32 -0400 Subject: [PATCH 1/8] Add unit tests for existing classifyRings functionality --- test/js/util/classify_rings.test.js | 79 +++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 test/js/util/classify_rings.test.js diff --git a/test/js/util/classify_rings.test.js b/test/js/util/classify_rings.test.js new file mode 100644 index 00000000000..e0d8dd9b224 --- /dev/null +++ b/test/js/util/classify_rings.test.js @@ -0,0 +1,79 @@ +'use strict'; + +var test = require('tap').test; +var fs = require('fs'); +var path = require('path'); +var Protobuf = require('pbf'); +var VectorTile = require('vector-tile').VectorTile; +var classifyRings = require('../../../js/util/classify_rings'); + +// Load a fill feature from fixture tile. +var vt = new VectorTile(new Protobuf(new Uint8Array(fs.readFileSync(path.join(__dirname, '/../../fixtures/mbsv5-6-18-23.vector.pbf'))))); +var feature = vt.layers.water.feature(0); + +test('classifyRings', function(assert) { + var geometry; + var classified; + + geometry = [ + [ + {x:0, y:0}, + {x:0, y:40}, + {x:40, y:40}, + {x:40, y:0}, + {x:0,y:0} + ] + ]; + classified = classifyRings(geometry); + assert.equal(classified.length, 1, '1 polygon'); + assert.equal(classified[0].length, 1, 'polygon 1 has 1 exterior'); + + geometry = [ + [ + {x:0, y:0}, + {x:0, y:40}, + {x:40, y:40}, + {x:40, y:0}, + {x:0,y:0} + ], + [ + {x:60, y:0}, + {x:60, y:40}, + {x:100, y:40}, + {x:100, y:0}, + {x:60,y:0} + ] + ]; + classified = classifyRings(geometry); + assert.equal(classified.length, 2, '2 polygons'); + assert.equal(classified[0].length, 1, 'polygon 1 has 1 exterior'); + assert.equal(classified[1].length, 1, 'polygon 2 has 1 exterior'); + + geometry = [ + [ + {x:0, y:0}, + {x:0, y:40}, + {x:40, y:40}, + {x:40, y:0}, + {x:0, y:0} + ], + [ + {x:10, y:10}, + {x:20, y:10}, + {x:20, y:20}, + {x:10, y:10} + ] + ]; + classified = classifyRings(geometry); + assert.equal(classified.length, 1, '1 polygon'); + assert.equal(classified[0].length, 2, 'polygon 1 has 1 exterior, 1 interior'); + + geometry = feature.loadGeometry(); + classified = classifyRings(geometry); + assert.equal(classified.length, 2, '2 polygons'); + assert.equal(classified[0].length, 1, 'polygon 1 has 1 exterior'); + assert.equal(classified[1].length, 10, 'polygon 2 has 1 exterior, 9 interior'); + + assert.end(); +}); + From 242ca206b468901bf70cb3974b71de4e65071740 Mon Sep 17 00:00:00 2001 From: Young Hahn Date: Tue, 24 May 2016 19:50:32 -0400 Subject: [PATCH 2/8] Add maxRings argument to classifyRings --- js/util/classify_rings.js | 18 ++++++++++- test/js/util/classify_rings.test.js | 47 +++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/js/util/classify_rings.js b/js/util/classify_rings.js index 53a28503385..876386f1afd 100644 --- a/js/util/classify_rings.js +++ b/js/util/classify_rings.js @@ -4,7 +4,7 @@ module.exports = classifyRings; // classifies an array of rings into polygons with outer rings and holes -function classifyRings(rings) { +function classifyRings(rings, maxRings) { var len = rings.length; if (len <= 1) return [rings]; @@ -17,6 +17,8 @@ function classifyRings(rings) { var area = signedArea(rings[i]); if (area === 0) continue; + rings[i].area = area; + if (ccw === undefined) ccw = area < 0; if (ccw === area < 0) { @@ -29,9 +31,23 @@ function classifyRings(rings) { } if (polygon) polygons.push(polygon); + if (maxRings > 1) { + len = polygons.length; + for (var j = 0; j < len; j++) { + var polygon = polygons[j]; + var truncated = [polygon.shift()]; + polygon.sort(sortByArea); + polygons[j] = truncated.concat(polygon.slice(0, maxRings-1)); + } + } + return polygons; } +function sortByArea(a, b) { + return b.area - a.area; +} + function signedArea(ring) { var sum = 0; for (var i = 0, len = ring.length, j = len - 1, p1, p2; i < len; j = i++) { diff --git a/test/js/util/classify_rings.test.js b/test/js/util/classify_rings.test.js index e0d8dd9b224..b3a330b921a 100644 --- a/test/js/util/classify_rings.test.js +++ b/test/js/util/classify_rings.test.js @@ -77,3 +77,50 @@ test('classifyRings', function(assert) { assert.end(); }); +test('classifyRings + maxRings', function(assert) { + var geometry; + var classified; + + geometry = [ + [ + {x:0, y:0}, + {x:0, y:40}, + {x:40, y:40}, + {x:40, y:0}, + {x:0, y:0} + ], + [ + {x:30, y:30}, + {x:32, y:30}, + {x:32, y:32}, + {x:30, y:30} + ], + [ + {x:10, y:10}, + {x:20, y:10}, + {x:20, y:20}, + {x:10, y:10} + ] + ]; + classified = classifyRings(geometry); + assert.equal(classified.length, 1, '1 polygon'); + assert.equal(classified[0].length, 3, 'polygon 1 has 1 exterior, 2 interior'); + assert.equal(classified[0][0].area, -3200, 'polygon 1 exterior ring has area=-3200'); + assert.equal(classified[0][1].area, 4, 'polygon 1 interior ring1 has area=4'); + assert.equal(classified[0][2].area, 100, 'polygon 1 interior ring2 has area=100'); + + classified = classifyRings(geometry, 2); + assert.equal(classified.length, 1, '1 polygon'); + assert.equal(classified[0].length, 2, 'polygon 1 has 1 exterior, 1 interior'); + assert.equal(classified[0][0].area, -3200, 'polygon 1 exterior ring has area=-3200'); + assert.equal(classified[0][1].area, 100, 'polygon 1 interior ring has area=100'); + + geometry = feature.loadGeometry(); + classified = classifyRings(geometry, 5); + assert.equal(classified.length, 2, '2 polygons'); + assert.equal(classified[0].length, 1, 'polygon 1 has 1 exterior'); + assert.equal(classified[1].length, 5, 'polygon 2 has 1 exterior, 4 interior'); + + assert.end(); +}); + From 915d72e94829577fd956f3dac6a5d9df29cd0831 Mon Sep 17 00:00:00 2001 From: Young Hahn Date: Tue, 24 May 2016 20:30:39 -0400 Subject: [PATCH 3/8] Winding order fix, set a max ring threshold to 100 (for now) --- js/data/bucket/fill_bucket.js | 3 ++- js/util/classify_rings.js | 2 +- test/js/util/classify_rings.test.js | 13 +++++++++++-- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/js/data/bucket/fill_bucket.js b/js/data/bucket/fill_bucket.js index 81e2bdf9940..0953ed129c0 100644 --- a/js/data/bucket/fill_bucket.js +++ b/js/data/bucket/fill_bucket.js @@ -5,6 +5,7 @@ var util = require('../../util/util'); var loadGeometry = require('../load_geometry'); var earcut = require('earcut'); var classifyRings = require('../../util/classify_rings'); +var EARCUT_MAX_RINGS = 100; module.exports = FillBucket; @@ -32,7 +33,7 @@ FillBucket.prototype.programInterfaces = { FillBucket.prototype.addFeature = function(feature) { var lines = loadGeometry(feature); - var polygons = classifyRings(lines); + var polygons = classifyRings(lines, EARCUT_MAX_RINGS); for (var i = 0; i < polygons.length; i++) { this.addPolygon(polygons[i]); } diff --git a/js/util/classify_rings.js b/js/util/classify_rings.js index 876386f1afd..c5c49244b7b 100644 --- a/js/util/classify_rings.js +++ b/js/util/classify_rings.js @@ -17,7 +17,7 @@ function classifyRings(rings, maxRings) { var area = signedArea(rings[i]); if (area === 0) continue; - rings[i].area = area; + rings[i].area = Math.abs(area); if (ccw === undefined) ccw = area < 0; diff --git a/test/js/util/classify_rings.test.js b/test/js/util/classify_rings.test.js index b3a330b921a..629d3b25922 100644 --- a/test/js/util/classify_rings.test.js +++ b/test/js/util/classify_rings.test.js @@ -105,14 +105,23 @@ test('classifyRings + maxRings', function(assert) { classified = classifyRings(geometry); assert.equal(classified.length, 1, '1 polygon'); assert.equal(classified[0].length, 3, 'polygon 1 has 1 exterior, 2 interior'); - assert.equal(classified[0][0].area, -3200, 'polygon 1 exterior ring has area=-3200'); + assert.equal(classified[0][0].area, 3200, 'polygon 1 exterior ring has area=3200'); assert.equal(classified[0][1].area, 4, 'polygon 1 interior ring1 has area=4'); assert.equal(classified[0][2].area, 100, 'polygon 1 interior ring2 has area=100'); classified = classifyRings(geometry, 2); assert.equal(classified.length, 1, '1 polygon'); assert.equal(classified[0].length, 2, 'polygon 1 has 1 exterior, 1 interior'); - assert.equal(classified[0][0].area, -3200, 'polygon 1 exterior ring has area=-3200'); + assert.equal(classified[0][0].area, 3200, 'polygon 1 exterior ring has area=3200'); + assert.equal(classified[0][1].area, 100, 'polygon 1 interior ring has area=100'); + + geometry[0].reverse(); + geometry[1].reverse(); + geometry[2].reverse(); + classified = classifyRings(geometry, 2); + assert.equal(classified.length, 1, '1 polygon'); + assert.equal(classified[0].length, 2, 'polygon 1 has 1 exterior, 1 interior'); + assert.equal(classified[0][0].area, 3200, 'polygon 1 exterior ring has area=3200'); assert.equal(classified[0][1].area, 100, 'polygon 1 interior ring has area=100'); geometry = feature.loadGeometry(); From 33bc6b2e0f4143eb2a1bae2567932529ab30038f Mon Sep 17 00:00:00 2001 From: Young Hahn Date: Tue, 24 May 2016 20:38:36 -0400 Subject: [PATCH 4/8] Satisfy the eslint beast --- js/util/classify_rings.js | 7 ++++--- test/js/util/classify_rings.test.js | 6 +++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/js/util/classify_rings.js b/js/util/classify_rings.js index c5c49244b7b..2c49d90a48c 100644 --- a/js/util/classify_rings.js +++ b/js/util/classify_rings.js @@ -11,6 +11,7 @@ function classifyRings(rings, maxRings) { var polygons = [], polygon, + truncated, ccw; for (var i = 0; i < len; i++) { @@ -34,10 +35,10 @@ function classifyRings(rings, maxRings) { if (maxRings > 1) { len = polygons.length; for (var j = 0; j < len; j++) { - var polygon = polygons[j]; - var truncated = [polygon.shift()]; + polygon = polygons[j]; + truncated = [polygon.shift()]; polygon.sort(sortByArea); - polygons[j] = truncated.concat(polygon.slice(0, maxRings-1)); + polygons[j] = truncated.concat(polygon.slice(0, maxRings - 1)); } } diff --git a/test/js/util/classify_rings.test.js b/test/js/util/classify_rings.test.js index 629d3b25922..d1c5917bdf7 100644 --- a/test/js/util/classify_rings.test.js +++ b/test/js/util/classify_rings.test.js @@ -21,7 +21,7 @@ test('classifyRings', function(assert) { {x:0, y:40}, {x:40, y:40}, {x:40, y:0}, - {x:0,y:0} + {x:0, y:0} ] ]; classified = classifyRings(geometry); @@ -34,14 +34,14 @@ test('classifyRings', function(assert) { {x:0, y:40}, {x:40, y:40}, {x:40, y:0}, - {x:0,y:0} + {x:0, y:0} ], [ {x:60, y:0}, {x:60, y:40}, {x:100, y:40}, {x:100, y:0}, - {x:60,y:0} + {x:60, y:0} ] ]; classified = classifyRings(geometry); From e2ac32e5f9a13f66a52836a9eaa4bfaa2d410065 Mon Sep 17 00:00:00 2001 From: jakepruitt Date: Wed, 25 May 2016 15:22:12 -0700 Subject: [PATCH 5/8] Bump EARCUT_MAX_RINGS to 500 --- js/data/bucket/fill_bucket.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/data/bucket/fill_bucket.js b/js/data/bucket/fill_bucket.js index 0953ed129c0..788177bc0bb 100644 --- a/js/data/bucket/fill_bucket.js +++ b/js/data/bucket/fill_bucket.js @@ -5,7 +5,7 @@ var util = require('../../util/util'); var loadGeometry = require('../load_geometry'); var earcut = require('earcut'); var classifyRings = require('../../util/classify_rings'); -var EARCUT_MAX_RINGS = 100; +var EARCUT_MAX_RINGS = 500; module.exports = FillBucket; From bb2c17a9d7b435d55b3c2c98b438a5e234a3c183 Mon Sep 17 00:00:00 2001 From: Lucas Wojciechowski Date: Thu, 26 May 2016 13:02:54 -0700 Subject: [PATCH 6/8] Use quickselect instead of sort in classifyRings --- js/util/classify_rings.js | 10 +++++----- package.json | 1 + 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/js/util/classify_rings.js b/js/util/classify_rings.js index 2c49d90a48c..5e58b68e1a9 100644 --- a/js/util/classify_rings.js +++ b/js/util/classify_rings.js @@ -1,5 +1,7 @@ 'use strict'; +var quickselect = require('quickselect'); + module.exports = classifyRings; // classifies an array of rings into polygons with outer rings and holes @@ -11,7 +13,6 @@ function classifyRings(rings, maxRings) { var polygons = [], polygon, - truncated, ccw; for (var i = 0; i < len; i++) { @@ -35,10 +36,9 @@ function classifyRings(rings, maxRings) { if (maxRings > 1) { len = polygons.length; for (var j = 0; j < len; j++) { - polygon = polygons[j]; - truncated = [polygon.shift()]; - polygon.sort(sortByArea); - polygons[j] = truncated.concat(polygon.slice(0, maxRings - 1)); + if (polygons[j].length <= maxRings) continue; + quickselect(polygons[j], maxRings - 1, 1, polygon.length - 1, sortByArea); + polygons[j] = polygon.slice(0, maxRings); } } diff --git a/package.json b/package.json index 0797025d69c..5e21aeaed4b 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "pbf": "^1.3.2", "pngjs": "^2.2.0", "point-geometry": "^0.0.0", + "quickselect": "^1.0.0", "request": "^2.39.0", "resolve-url": "^0.2.1", "shelf-pack": "^1.0.0", From 70104c682d612cf8a8809f54f8ce1ec8dc59a24b Mon Sep 17 00:00:00 2001 From: Lucas Wojciechowski Date: Thu, 26 May 2016 13:04:42 -0700 Subject: [PATCH 7/8] Refactoring --- js/util/classify_rings.js | 20 +++-- test/js/util/classify_rings.test.js | 114 ++++++++++++++++------------ 2 files changed, 74 insertions(+), 60 deletions(-) diff --git a/js/util/classify_rings.js b/js/util/classify_rings.js index 5e58b68e1a9..4bfd9b20147 100644 --- a/js/util/classify_rings.js +++ b/js/util/classify_rings.js @@ -2,11 +2,8 @@ var quickselect = require('quickselect'); -module.exports = classifyRings; - // classifies an array of rings into polygons with outer rings and holes - -function classifyRings(rings, maxRings) { +module.exports = function classifyRings(rings, maxRings) { var len = rings.length; if (len <= 1) return [rings]; @@ -16,7 +13,7 @@ function classifyRings(rings, maxRings) { ccw; for (var i = 0; i < len; i++) { - var area = signedArea(rings[i]); + var area = calculateSignedArea(rings[i]); if (area === 0) continue; rings[i].area = Math.abs(area); @@ -33,23 +30,24 @@ function classifyRings(rings, maxRings) { } if (polygon) polygons.push(polygon); + // Earcut performance degrages with the # of rings in a polygon. For this + // reason, we limit strip out all but the `maxRings` largest rings. if (maxRings > 1) { - len = polygons.length; - for (var j = 0; j < len; j++) { + for (var j = 0; j < polygons.length; j++) { if (polygons[j].length <= maxRings) continue; - quickselect(polygons[j], maxRings - 1, 1, polygon.length - 1, sortByArea); + quickselect(polygons[j], maxRings - 1, 1, polygon.length - 1, compareAreas); polygons[j] = polygon.slice(0, maxRings); } } return polygons; -} +}; -function sortByArea(a, b) { +function compareAreas(a, b) { return b.area - a.area; } -function signedArea(ring) { +function calculateSignedArea(ring) { var sum = 0; for (var i = 0, len = ring.length, j = len - 1, p1, p2; i < len; j = i++) { p1 = ring[i]; diff --git a/test/js/util/classify_rings.test.js b/test/js/util/classify_rings.test.js index d1c5917bdf7..d8452ebfa95 100644 --- a/test/js/util/classify_rings.test.js +++ b/test/js/util/classify_rings.test.js @@ -77,59 +77,75 @@ test('classifyRings', function(assert) { assert.end(); }); -test('classifyRings + maxRings', function(assert) { - var geometry; - var classified; +test('classifyRings + maxRings', function(t) { - geometry = [ - [ - {x:0, y:0}, - {x:0, y:40}, - {x:40, y:40}, - {x:40, y:0}, - {x:0, y:0} - ], - [ - {x:30, y:30}, - {x:32, y:30}, - {x:32, y:32}, - {x:30, y:30} - ], - [ - {x:10, y:10}, - {x:20, y:10}, - {x:20, y:20}, - {x:10, y:10} - ] - ]; - classified = classifyRings(geometry); - assert.equal(classified.length, 1, '1 polygon'); - assert.equal(classified[0].length, 3, 'polygon 1 has 1 exterior, 2 interior'); - assert.equal(classified[0][0].area, 3200, 'polygon 1 exterior ring has area=3200'); - assert.equal(classified[0][1].area, 4, 'polygon 1 interior ring1 has area=4'); - assert.equal(classified[0][2].area, 100, 'polygon 1 interior ring2 has area=100'); + function createGeometry(options) { + var geometry = [ + // Outer ring, area = 3200 + [ {x:0, y:0}, {x:0, y:40}, {x:40, y:40}, {x:40, y:0}, {x:0, y:0} ], + // Inner ring, area = 100 + [ {x:30, y:30}, {x:32, y:30}, {x:32, y:32}, {x:30, y:30} ], + // Inner ring, area = 4 + [ {x:10, y:10}, {x:20, y:10}, {x:20, y:20}, {x:10, y:10} ] + ]; + if (options && options.reverse) { + geometry[0].reverse(); + geometry[1].reverse(); + geometry[2].reverse(); + } + return geometry; + } - classified = classifyRings(geometry, 2); - assert.equal(classified.length, 1, '1 polygon'); - assert.equal(classified[0].length, 2, 'polygon 1 has 1 exterior, 1 interior'); - assert.equal(classified[0][0].area, 3200, 'polygon 1 exterior ring has area=3200'); - assert.equal(classified[0][1].area, 100, 'polygon 1 interior ring has area=100'); - geometry[0].reverse(); - geometry[1].reverse(); - geometry[2].reverse(); - classified = classifyRings(geometry, 2); - assert.equal(classified.length, 1, '1 polygon'); - assert.equal(classified[0].length, 2, 'polygon 1 has 1 exterior, 1 interior'); - assert.equal(classified[0][0].area, 3200, 'polygon 1 exterior ring has area=3200'); - assert.equal(classified[0][1].area, 100, 'polygon 1 interior ring has area=100'); + t.test('maxRings=undefined', function(t) { + var geometry = sortRings(classifyRings(createGeometry())); + t.equal(geometry.length, 1); + t.equal(geometry[0].length, 3); + t.equal(geometry[0][0].area, 3200); + t.equal(geometry[0][1].area, 100); + t.equal(geometry[0][2].area, 4); + t.end(); + }); - geometry = feature.loadGeometry(); - classified = classifyRings(geometry, 5); - assert.equal(classified.length, 2, '2 polygons'); - assert.equal(classified[0].length, 1, 'polygon 1 has 1 exterior'); - assert.equal(classified[1].length, 5, 'polygon 2 has 1 exterior, 4 interior'); + t.test('maxRings=2', function(t) { + var geometry = sortRings(classifyRings(createGeometry(), 2)); + t.equal(geometry.length, 1); + t.equal(geometry[0].length, 2); + t.equal(geometry[0][0].area, 3200); + t.equal(geometry[0][1].area, 100); + t.end(); + }); - assert.end(); + t.test('maxRings=2, reversed geometry', function(t) { + var geometry = sortRings(classifyRings(createGeometry({reverse: true}), 2)); + t.equal(geometry.length, 1); + t.equal(geometry[0].length, 2); + t.equal(geometry[0][0].area, 3200); + t.equal(geometry[0][1].area, 100); + t.end(); + }); + + t.test('maxRings=5, geometry from fixture', function(t) { + var geometry = sortRings(classifyRings(feature.loadGeometry(), 5)); + t.equal(geometry.length, 2); + t.equal(geometry[0].length, 1); + t.equal(geometry[1].length, 5); + + var areas = geometry[1].map(function(ring) { return ring.area; }); + t.deepEqual(areas, [2763951, 21600, 8298, 4758, 3411]); + t.end(); + }); + + t.end(); }); +function sortRings(geometry) { + for (var i = 0; i < geometry.length; i++) { + geometry[i] = geometry[i].sort(compareAreas); + } + return geometry; +} + +function compareAreas(a, b) { + return b.area - a.area; +} From fddba1b48e7e65b821ef7c7c3813f03baa8abf9d Mon Sep 17 00:00:00 2001 From: Lucas Wojciechowski Date: Thu, 26 May 2016 13:48:50 -0700 Subject: [PATCH 8/8] Respond to PR review --- js/util/classify_rings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/util/classify_rings.js b/js/util/classify_rings.js index 4bfd9b20147..74683eb0721 100644 --- a/js/util/classify_rings.js +++ b/js/util/classify_rings.js @@ -35,7 +35,7 @@ module.exports = function classifyRings(rings, maxRings) { if (maxRings > 1) { for (var j = 0; j < polygons.length; j++) { if (polygons[j].length <= maxRings) continue; - quickselect(polygons[j], maxRings - 1, 1, polygon.length - 1, compareAreas); + quickselect(polygons[j], maxRings, 1, polygon.length - 1, compareAreas); polygons[j] = polygon.slice(0, maxRings); } }