Skip to content

Commit 83da8bc

Browse files
committed
Normalize geometry structure; classify polygon rings
1 parent 3426fc4 commit 83da8bc

13 files changed

+246
-35
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
## vector-tile-js changelog
22

3+
### 2.0.0 (in progress)
4+
5+
- The structure of the return value of `loadGeometry()` was normalized. For point features, `loadGeometry()` now returns
6+
an array of points. For polygon features, `loadGeometry()` now classifies rings and groups them into outer and inner
7+
rings, using an extra level of array nesting. I.e., the return value for polygons is an array of arrays of arrays of
8+
Points. The structure for line features (array of arrays of points) is unchanged.
9+
- `toGeoJSON()` now classifies polygons into `Polygon` and `MultiPolygon` types based on ring structure.
10+
311
### 1.1.3 (2015-06-15)
412

513
- Workaround for https://github.com/mapbox/mapnik-vector-tile/issues/90

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,9 @@ An object that contains the data for a single feature.
9797

9898
#### Methods
9999

100-
- **loadGeometry()** — parses feature geometry and returns an array of
101-
[Point](https://github.com/mapbox/point-geometry) arrays (with each point having `x` and `y` properties)
100+
- **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:
101+
- For point features, the return value is an array of [`Point`](https://github.com/mapbox/point-geometry)s.
102+
- For line features, it is an array of arrays of `Point`s.
103+
- For polygon features, it is an array of arrays of arrays of `Point`s.
102104
- **bbox()** — calculates and returns the bounding box of the feature in the form `[x1, y1, x2, y2]`
103105
- **toGeoJSON(x, y, z)** — returns a GeoJSON representation of the feature. (`x`, `y`, and `z` refer to the containing tile's index.)

fixtures.js

Lines changed: 129 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ var mapnik = require('mapnik');
22
var path = require('path');
33
var fs = require('fs');
44

5-
mapnik.register_datasource(path.join(mapnik.settings.paths.input_plugins,'geojson.input'));
5+
mapnik.register_datasource(path.join(mapnik.settings.paths.input_plugins, 'geojson.input'));
66

77
var fixtures = {
88
"zero-point": {
@@ -31,6 +31,19 @@ var fixtures = {
3131
}
3232
]
3333
},
34+
"zero-polygon": {
35+
"type": "FeatureCollection",
36+
"features": [
37+
{
38+
"type": "Feature",
39+
"geometry": {
40+
"type": "MultiPolygon",
41+
"coordinates": []
42+
},
43+
"properties": {}
44+
}
45+
]
46+
},
3447
"singleton-multi-point": {
3548
"type": "FeatureCollection",
3649
"features": [
@@ -57,6 +70,19 @@ var fixtures = {
5770
}
5871
]
5972
},
73+
"singleton-multi-polygon": {
74+
"type": "FeatureCollection",
75+
"features": [
76+
{
77+
"type": "Feature",
78+
"geometry": {
79+
"type": "MultiPolygon",
80+
"coordinates": [[[[0, 0], [1, 0], [1, 1], [0, 0]]]]
81+
},
82+
"properties": {}
83+
}
84+
]
85+
},
6086
"multi-point": {
6187
"type": "FeatureCollection",
6288
"features": [
@@ -82,11 +108,112 @@ var fixtures = {
82108
"properties": {}
83109
}
84110
]
111+
},
112+
"multi-polygon": {
113+
"type": "FeatureCollection",
114+
"features": [
115+
{
116+
"type": "Feature",
117+
"geometry": {
118+
"type": "MultiPolygon",
119+
"coordinates": [[[[0, 0], [1, 0], [1, 1], [0, 0]]], [[[0, 0], [-1, 0], [-1, -1], [0, 0]]]]
120+
},
121+
"properties": {}
122+
}
123+
]
124+
},
125+
"polygon-with-inner": {
126+
"type": "FeatureCollection",
127+
"features": [
128+
{
129+
"type": "Feature",
130+
"geometry": {
131+
"type": "Polygon",
132+
"coordinates": [[[-2, 2], [2, 2], [2, -2], [-2, -2], [-2, 2]], [[-1, 1], [1, 1], [1, -1], [-1, -1], [-1, 1]]]
133+
},
134+
"properties": {}
135+
}
136+
]
137+
},
138+
"multipolygon": {
139+
"type": "FeatureCollection",
140+
"features": [
141+
{
142+
"type": "Feature",
143+
"properties": {},
144+
"geometry": {
145+
"type": "MultiPolygon",
146+
"coordinates": [
147+
[
148+
[
149+
[
150+
-65.91796875,
151+
-23.40276490540795
152+
],
153+
[
154+
-65.91796875,
155+
8.407168163601076
156+
],
157+
[
158+
-35.859375,
159+
8.407168163601076
160+
],
161+
[
162+
-35.859375,
163+
-23.40276490540795
164+
],
165+
[
166+
-65.91796875,
167+
-23.40276490540795
168+
]
169+
]
170+
],
171+
[
172+
[
173+
[
174+
-9.84375,
175+
12.897489183755892
176+
],
177+
[
178+
-9.84375,
179+
37.16031654673677
180+
],
181+
[
182+
18.28125,
183+
37.16031654673677
184+
],
185+
[
186+
18.28125,
187+
12.897489183755892
188+
],
189+
[
190+
-9.84375,
191+
12.897489183755892
192+
]
193+
]
194+
]
195+
]
196+
}
197+
}
198+
]
199+
},
200+
"stacked-multipolygon": {
201+
"type": "FeatureCollection",
202+
"features": [
203+
{
204+
"type": "Feature",
205+
"geometry": {
206+
"type": "MultiPolygon",
207+
"coordinates": [[[[-2, 2], [2, 2], [2, -2], [-2, -2], [-2, 2]]], [[[-1, 1], [1, 1], [1, -1], [-1, -1], [-1, 1]]]]
208+
},
209+
"properties": {}
210+
}
211+
]
85212
}
86213
}
87214

88215
for (var fixture in fixtures) {
89-
var vtile = new mapnik.VectorTile(0,0,0);
216+
var vtile = new mapnik.VectorTile(0, 0, 0);
90217
vtile.addGeoJSON(JSON.stringify(fixtures[fixture]), "geojson");
91218
fs.writeFileSync('./test/fixtures/' + fixture + '.pbf', vtile.getData());
92219
}

lib/vectortilefeature.js

Lines changed: 87 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ VectorTileFeature.prototype.loadGeometry = function() {
4747
length = 0,
4848
x = 0,
4949
y = 0,
50-
lines = [],
51-
line;
50+
line = [],
51+
lines = [line];
5252

5353
while (pbf.pos < end) {
5454
if (!length) {
@@ -59,21 +59,23 @@ VectorTileFeature.prototype.loadGeometry = function() {
5959

6060
length--;
6161

62-
if (cmd === 1 || cmd === 2) {
62+
if (cmd === 2 || (cmd === 1 && this.type === 1)) { // MLLLC
6363
x += pbf.readSVarint();
6464
y += pbf.readSVarint();
65+
line.push(new Point(x, y));
6566

66-
if (cmd === 1) { // moveTo
67-
if (line) lines.push(line);
67+
} else if (cmd === 1) {
68+
x += pbf.readSVarint();
69+
y += pbf.readSVarint();
70+
if (line.length) {
6871
line = [];
72+
lines.push(line);
6973
}
70-
7174
line.push(new Point(x, y));
7275

7376
} else if (cmd === 7) {
74-
7577
// Workaround for https://github.com/mapbox/mapnik-vector-tile/issues/90
76-
if (line) {
78+
if (line.length) {
7779
line.push(line[0].clone()); // closePolygon
7880
}
7981

@@ -82,9 +84,19 @@ VectorTileFeature.prototype.loadGeometry = function() {
8284
}
8385
}
8486

85-
if (line) lines.push(line);
86-
87-
return lines;
87+
switch (this.type) {
88+
case 1:
89+
return line;
90+
break;
91+
case 2:
92+
return lines;
93+
break;
94+
case 3:
95+
return classifyRings(lines);
96+
break;
97+
default:
98+
throw new Error('Unknown vector tile feature type: ' + this.type);
99+
}
88100
};
89101

90102
VectorTileFeature.prototype.bbox = function() {
@@ -133,8 +145,7 @@ VectorTileFeature.prototype.toGeoJSON = function(x, y, z) {
133145
coords = this.loadGeometry(),
134146
type = VectorTileFeature.types[this.type];
135147

136-
for (var i = 0; i < coords.length; i++) {
137-
var line = coords[i];
148+
function project(line) {
138149
for (var j = 0; j < line.length; j++) {
139150
var p = line[j], y2 = 180 - (p.y + y0) * 360 / size;
140151
line[j] = [
@@ -144,15 +155,30 @@ VectorTileFeature.prototype.toGeoJSON = function(x, y, z) {
144155
}
145156
}
146157

147-
if (type === 'Point' && coords.length === 1) {
148-
coords = coords[0][0];
149-
} else if (type === 'Point') {
150-
coords = coords[0];
151-
type = 'MultiPoint';
152-
} else if (type === 'LineString' && coords.length === 1) {
158+
switch (this.type) {
159+
case 1:
160+
project(coords);
161+
break;
162+
163+
case 2:
164+
for (var i = 0; i < coords.length; i++) {
165+
project(coords[i]);
166+
}
167+
break;
168+
169+
case 3:
170+
for (var i = 0; i < coords.length; i++) {
171+
for (var j = 0; j < coords[i].length; j++) {
172+
project(coords[i][j]);
173+
}
174+
}
175+
break;
176+
}
177+
178+
if (coords.length === 1) {
153179
coords = coords[0];
154-
} else if (type === 'LineString') {
155-
type = 'MultiLineString';
180+
} else {
181+
type = 'Multi' + type;
156182
}
157183

158184
return {
@@ -164,3 +190,43 @@ VectorTileFeature.prototype.toGeoJSON = function(x, y, z) {
164190
properties: this.properties
165191
};
166192
};
193+
194+
// classifies an array of rings into polygons with outer rings and holes
195+
196+
function classifyRings(rings) {
197+
var len = rings.length;
198+
199+
if (len <= 1) return [rings];
200+
201+
var polygons = [],
202+
polygon,
203+
ccw;
204+
205+
for (var i = 0; i < len; i++) {
206+
var area = signedArea(rings[i]);
207+
if (area === 0) continue;
208+
209+
if (!ccw) ccw = area < 0;
210+
211+
if (ccw === area < 0) {
212+
if (polygon) polygons.push(polygon);
213+
polygon = [rings[i]];
214+
215+
} else {
216+
polygon.push(rings[i]);
217+
}
218+
}
219+
if (polygon) polygons.push(polygon);
220+
221+
return polygons;
222+
}
223+
224+
function signedArea(ring) {
225+
var sum = 0;
226+
for (var i = 0, len = ring.length, j = len - 1, p1, p2; i < len; j = i++) {
227+
p1 = ring[i];
228+
p2 = ring[j];
229+
sum += (p2.x - p1.x) * (p1.y + p2.y);
230+
}
231+
return sum;
232+
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"benchmark": "^1.0.0",
1313
"coveralls": "~2.11.2",
1414
"istanbul": "~0.3.6",
15-
"mapnik": "^3.1.6",
15+
"mapnik": "^3.4.8",
1616
"jshint": "^2.6.3",
1717
"pbf": "^1.3.2",
1818
"tape": "~3.5.0"

test/fixtures/multi-point.pbf

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-

2-
geojson" � � .-(� x
1+

2+
geojson"� �.-(� x

test/fixtures/multi-polygon.pbf

44 Bytes
Binary file not shown.

test/fixtures/multipolygon.pbf

56 Bytes
Binary file not shown.
48 Bytes
Binary file not shown.
35 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)