From 8a8f615e23a0b269e3e952d1cd99cf931d6b85d6 Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Fri, 9 Oct 2015 14:47:22 -0700 Subject: [PATCH 1/2] Normalize geometry structure; classify polygon rings --- CHANGELOG.md | 8 ++ README.md | 6 +- fixtures.js | 131 +++++++++++++++++++++- lib/vectortilefeature.js | 108 ++++++++++++++---- package.json | 2 +- test/fixtures/multi-point.pbf | 4 +- test/fixtures/multi-polygon.pbf | Bin 0 -> 44 bytes test/fixtures/multipolygon.pbf | Bin 0 -> 56 bytes test/fixtures/polygon-with-inner.pbf | Bin 0 -> 48 bytes test/fixtures/singleton-multi-polygon.pbf | Bin 0 -> 35 bytes test/fixtures/stacked-multipolygon.pbf | Bin 0 -> 37 bytes test/fixtures/zero-polygon.pbf | 0 test/parse.test.js | 22 ++-- 13 files changed, 245 insertions(+), 36 deletions(-) create mode 100644 test/fixtures/multi-polygon.pbf create mode 100644 test/fixtures/multipolygon.pbf create mode 100644 test/fixtures/polygon-with-inner.pbf create mode 100644 test/fixtures/singleton-multi-polygon.pbf create mode 100644 test/fixtures/stacked-multipolygon.pbf create mode 100644 test/fixtures/zero-polygon.pbf diff --git a/CHANGELOG.md b/CHANGELOG.md index c55538f..3de10fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ ## vector-tile-js changelog +### 2.0.0 (in progress) + +- The structure of the return value of `loadGeometry()` was normalized. For point features, `loadGeometry()` now returns + an array of points. For polygon features, `loadGeometry()` now classifies rings and groups them into outer and inner + rings, using an extra level of array nesting. I.e., the return value for polygons is an array of arrays of arrays of + Points. The structure for line features (array of arrays of points) is unchanged. +- `toGeoJSON()` now classifies polygons into `Polygon` and `MultiPolygon` types based on ring structure. + ### 1.2.0 (2015-12-10) - Added "id" property to toGeoJSON() output diff --git a/README.md b/README.md index e9c4dfc..c5ef39e 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,9 @@ An object that contains the data for a single feature. #### Methods -- **loadGeometry()** — parses feature geometry and returns an array of - [Point](https://github.com/mapbox/point-geometry) arrays (with each point having `x` and `y` properties) +- **loadGeometry()** — returns feature geometry. Since the vector tile specification does not distinguish between Point and MultiPoint, LineString and MultiLineString, or Polygon and MultiPolygon, the result is always treated as a "Multi" variant: + - For point features, the return value is an array of [`Point`](https://github.com/mapbox/point-geometry)s. + - For line features, it is an array of arrays of `Point`s. + - For polygon features, it is an array of arrays of arrays of `Point`s. - **bbox()** — calculates and returns the bounding box of the feature in the form `[x1, y1, x2, y2]` - **toGeoJSON(x, y, z)** — returns a GeoJSON representation of the feature. (`x`, `y`, and `z` refer to the containing tile's index.) diff --git a/fixtures.js b/fixtures.js index 45e4372..db7bc03 100644 --- a/fixtures.js +++ b/fixtures.js @@ -2,7 +2,7 @@ var mapnik = require('mapnik'); var path = require('path'); var fs = require('fs'); -mapnik.register_datasource(path.join(mapnik.settings.paths.input_plugins,'geojson.input')); +mapnik.register_datasource(path.join(mapnik.settings.paths.input_plugins, 'geojson.input')); var fixtures = { "zero-point": { @@ -31,6 +31,19 @@ var fixtures = { } ] }, + "zero-polygon": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "MultiPolygon", + "coordinates": [] + }, + "properties": {} + } + ] + }, "singleton-multi-point": { "type": "FeatureCollection", "features": [ @@ -57,6 +70,19 @@ var fixtures = { } ] }, + "singleton-multi-polygon": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "MultiPolygon", + "coordinates": [[[[0, 0], [1, 0], [1, 1], [0, 0]]]] + }, + "properties": {} + } + ] + }, "multi-point": { "type": "FeatureCollection", "features": [ @@ -82,11 +108,112 @@ var fixtures = { "properties": {} } ] + }, + "multi-polygon": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "MultiPolygon", + "coordinates": [[[[0, 0], [1, 0], [1, 1], [0, 0]]], [[[0, 0], [-1, 0], [-1, -1], [0, 0]]]] + }, + "properties": {} + } + ] + }, + "polygon-with-inner": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [[[-2, 2], [2, 2], [2, -2], [-2, -2], [-2, 2]], [[-1, 1], [1, 1], [1, -1], [-1, -1], [-1, 1]]] + }, + "properties": {} + } + ] + }, + "multipolygon": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "MultiPolygon", + "coordinates": [ + [ + [ + [ + -65.91796875, + -23.40276490540795 + ], + [ + -65.91796875, + 8.407168163601076 + ], + [ + -35.859375, + 8.407168163601076 + ], + [ + -35.859375, + -23.40276490540795 + ], + [ + -65.91796875, + -23.40276490540795 + ] + ] + ], + [ + [ + [ + -9.84375, + 12.897489183755892 + ], + [ + -9.84375, + 37.16031654673677 + ], + [ + 18.28125, + 37.16031654673677 + ], + [ + 18.28125, + 12.897489183755892 + ], + [ + -9.84375, + 12.897489183755892 + ] + ] + ] + ] + } + } + ] + }, + "stacked-multipolygon": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "MultiPolygon", + "coordinates": [[[[-2, 2], [2, 2], [2, -2], [-2, -2], [-2, 2]]], [[[-1, 1], [1, 1], [1, -1], [-1, -1], [-1, 1]]]] + }, + "properties": {} + } + ] } } for (var fixture in fixtures) { - var vtile = new mapnik.VectorTile(0,0,0); + var vtile = new mapnik.VectorTile(0, 0, 0); vtile.addGeoJSON(JSON.stringify(fixtures[fixture]), "geojson"); fs.writeFileSync('./test/fixtures/' + fixture + '.pbf', vtile.getData()); } diff --git a/lib/vectortilefeature.js b/lib/vectortilefeature.js index f8cdfc2..8c19168 100644 --- a/lib/vectortilefeature.js +++ b/lib/vectortilefeature.js @@ -47,8 +47,8 @@ VectorTileFeature.prototype.loadGeometry = function() { length = 0, x = 0, y = 0, - lines = [], - line; + line = [], + lines = [line]; while (pbf.pos < end) { if (!length) { @@ -59,21 +59,23 @@ VectorTileFeature.prototype.loadGeometry = function() { length--; - if (cmd === 1 || cmd === 2) { + if (cmd === 2 || (cmd === 1 && this.type === 1)) { // MLLLC x += pbf.readSVarint(); y += pbf.readSVarint(); + line.push(new Point(x, y)); - if (cmd === 1) { // moveTo - if (line) lines.push(line); + } else if (cmd === 1) { + x += pbf.readSVarint(); + y += pbf.readSVarint(); + if (line.length) { line = []; + lines.push(line); } - line.push(new Point(x, y)); } else if (cmd === 7) { - // Workaround for https://github.com/mapbox/mapnik-vector-tile/issues/90 - if (line) { + if (line.length) { line.push(line[0].clone()); // closePolygon } @@ -82,9 +84,16 @@ VectorTileFeature.prototype.loadGeometry = function() { } } - if (line) lines.push(line); - - return lines; + switch (this.type) { + case 1: + return line; + case 2: + return lines; + case 3: + return classifyRings(lines); + default: + throw new Error('Unknown vector tile feature type: ' + this.type); + } }; VectorTileFeature.prototype.bbox = function() { @@ -131,10 +140,10 @@ VectorTileFeature.prototype.toGeoJSON = function(x, y, z) { x0 = this.extent * x, y0 = this.extent * y, coords = this.loadGeometry(), - type = VectorTileFeature.types[this.type]; + type = VectorTileFeature.types[this.type], + i, j; - for (var i = 0; i < coords.length; i++) { - var line = coords[i]; + function project(line) { for (var j = 0; j < line.length; j++) { var p = line[j], y2 = 180 - (p.y + y0) * 360 / size; line[j] = [ @@ -144,15 +153,30 @@ VectorTileFeature.prototype.toGeoJSON = function(x, y, z) { } } - if (type === 'Point' && coords.length === 1) { - coords = coords[0][0]; - } else if (type === 'Point') { - coords = coords[0]; - type = 'MultiPoint'; - } else if (type === 'LineString' && coords.length === 1) { + switch (this.type) { + case 1: + project(coords); + break; + + case 2: + for (i = 0; i < coords.length; i++) { + project(coords[i]); + } + break; + + case 3: + for (i = 0; i < coords.length; i++) { + for (j = 0; j < coords[i].length; j++) { + project(coords[i][j]); + } + } + break; + } + + if (coords.length === 1) { coords = coords[0]; - } else if (type === 'LineString') { - type = 'MultiLineString'; + } else { + type = 'Multi' + type; } var result = { @@ -170,3 +194,43 @@ VectorTileFeature.prototype.toGeoJSON = function(x, y, z) { return result; }; + +// classifies an array of rings into polygons with outer rings and holes + +function classifyRings(rings) { + var len = rings.length; + + if (len <= 1) return [rings]; + + var polygons = [], + polygon, + ccw; + + for (var i = 0; i < len; i++) { + var area = signedArea(rings[i]); + if (area === 0) continue; + + if (!ccw) ccw = area < 0; + + if (ccw === area < 0) { + if (polygon) polygons.push(polygon); + polygon = [rings[i]]; + + } else { + polygon.push(rings[i]); + } + } + if (polygon) polygons.push(polygon); + + return polygons; +} + +function signedArea(ring) { + var sum = 0; + for (var i = 0, len = ring.length, j = len - 1, p1, p2; i < len; j = i++) { + p1 = ring[i]; + p2 = ring[j]; + sum += (p2.x - p1.x) * (p1.y + p2.y); + } + return sum; +} diff --git a/package.json b/package.json index e9b2235..7d059b9 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "benchmark": "^1.0.0", "coveralls": "~2.11.2", "istanbul": "~0.3.6", - "mapnik": "^3.1.6", + "mapnik": "^3.4.8", "jshint": "^2.6.3", "pbf": "^1.3.2", "tape": "~3.5.0", diff --git a/test/fixtures/multi-point.pbf b/test/fixtures/multi-point.pbf index f2c7030..4a5e123 100644 --- a/test/fixtures/multi-point.pbf +++ b/test/fixtures/multi-point.pbf @@ -1,2 +1,2 @@ - -geojson" – Ò .-(€ x \ No newline at end of file + +geojson "– Ò.-(€ x \ No newline at end of file diff --git a/test/fixtures/multi-polygon.pbf b/test/fixtures/multi-polygon.pbf new file mode 100644 index 0000000000000000000000000000000000000000..7b9cae1a79f29ccf967b683b015dbaea56fd65af GIT binary patch literal 44 zcmb2r;$lxv&Ce>%&l8g3V3c5165)I$-=HAGASNct&&j|bB*wrX%CFI&P{9ZQzy1e% literal 0 HcmV?d00001 diff --git a/test/fixtures/multipolygon.pbf b/test/fixtures/multipolygon.pbf new file mode 100644 index 0000000000000000000000000000000000000000..171c073b1da39d4f83079e9de804ba1c60e468fb GIT binary patch literal 56 zcmb0#<6=)w&Ce>%&l6JPV3c51Qs7)7azIXs;R)*+RtAR0to)o8SkALbF}z@D0P>%+ L@M|%&l8g4V3c51lHj}~e@R}7Atr`_A)234+(k;8fk9h`fnTFRp@I%&l3{lV3c51;^u5nXiyLmV_*>F*Jx0vU<3euhz5)R literal 0 HcmV?d00001 diff --git a/test/fixtures/stacked-multipolygon.pbf b/test/fixtures/stacked-multipolygon.pbf new file mode 100644 index 0000000000000000000000000000000000000000..3e8ef30ce258c76cbe9445dd3c6e3f3d535c9d69 GIT binary patch literal 37 scmb1A=3-Az&Ce>%&l3{nV3c51;^n*~e@R}7Atr`_A(~&KL7{>X0Gw_JBme*a literal 0 HcmV?d00001 diff --git a/test/fixtures/zero-polygon.pbf b/test/fixtures/zero-polygon.pbf new file mode 100644 index 0000000..e69de29 diff --git a/test/parse.test.js b/test/parse.test.js index 66727fc..2102708 100644 --- a/test/parse.test.js +++ b/test/parse.test.js @@ -36,7 +36,7 @@ test('parsing vector tiles', function(t) { t.equal(park.properties.type, 'Park'); // Check point geometry - t.deepEqual(park.loadGeometry(), [ [ { x: 3898, y: 1731 } ] ]); + t.deepEqual(park.loadGeometry(), [ { x: 3898, y: 1731 } ]); // Check line geometry t.deepEqual(tile.layers.road.feature(656).loadGeometry(), [ [ { x: 1988, y: 306 }, { x: 1808, y: 321 }, { x: 1506, y: 347 } ] ]); @@ -47,12 +47,12 @@ test('parsing vector tiles', function(t) { var tile = new VectorTile(new Protobuf(data)); var building = tile.layers.building.feature(0).loadGeometry(); - t.deepEqual(building, [ [ { x: 2039, y: -32 }, { x: 2035, y: -31 }, { x: 2032, y: -31 }, { x: 2032, y: -32 }, { x: 2039, y: -32 } ] ]); - building[0][0].x = 1; - building[0][0].y = 2; - building[0][1].x = 3; - building[0][1].y = 4; - t.deepEqual(building, [ [ { x: 1, y: 2 }, { x: 3, y: 4 }, { x: 2032, y: -31 }, { x: 2032, y: -32 }, { x: 2039, y: -32 } ] ]); + t.deepEqual(building, [ [ [ { x: 2039, y: -32 }, { x: 2035, y: -31 }, { x: 2032, y: -31 }, { x: 2032, y: -32 }, { x: 2039, y: -32 } ] ] ]); + building[0][0][0].x = 1; + building[0][0][0].y = 2; + building[0][0][1].x = 3; + building[0][0][1].y = 4; + t.deepEqual(building, [ [ [ { x: 1, y: 2 }, { x: 3, y: 4 }, { x: 2032, y: -31 }, { x: 2032, y: -32 }, { x: 2039, y: -32 } ] ] ]); t.end(); }); @@ -120,9 +120,17 @@ test('parsing vector tiles', function(t) { // https://github.com/mapbox/vector-tile-spec/issues/30 t.equal(geoJSONFromFixture("singleton-multi-point").geometry.type, 'Point'); t.equal(geoJSONFromFixture("singleton-multi-line").geometry.type, 'LineString'); + t.equal(geoJSONFromFixture("singleton-multi-polygon").geometry.type, 'Polygon'); t.equal(geoJSONFromFixture("multi-point").geometry.type, 'MultiPoint'); t.equal(geoJSONFromFixture("multi-line").geometry.type, 'MultiLineString'); + t.equal(geoJSONFromFixture("multi-polygon").geometry.type, 'MultiPolygon'); + + // https://github.com/mapbox/vector-tile-js/issues/32 + t.equal(geoJSONFromFixture("polygon-with-inner").geometry.type, 'Polygon'); + t.equal(geoJSONFromFixture("polygon-with-inner").geometry.coordinates.length, 2); + t.equal(geoJSONFromFixture("multipolygon").geometry.type, 'MultiPolygon'); + t.equal(geoJSONFromFixture("multipolygon").geometry.coordinates.length, 2); t.end(); }) From e4ec4c65ce8443a8921976095c52ba947a1131f2 Mon Sep 17 00:00:00 2001 From: Vladimir Agafonkin Date: Wed, 16 Mar 2016 14:52:09 +0200 Subject: [PATCH 2/2] fix ring classification --- lib/vectortilefeature.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/vectortilefeature.js b/lib/vectortilefeature.js index 8c19168..8262a01 100644 --- a/lib/vectortilefeature.js +++ b/lib/vectortilefeature.js @@ -210,7 +210,7 @@ function classifyRings(rings) { var area = signedArea(rings[i]); if (area === 0) continue; - if (!ccw) ccw = area < 0; + if (ccw === undefined) ccw = area < 0; if (ccw === area < 0) { if (polygon) polygons.push(polygon);