Skip to content

Commit 3dfeba4

Browse files
committed
encode MultiPolygon properly. do not split to multiple features.
#18
1 parent 4fb52ab commit 3dfeba4

File tree

3 files changed

+201
-80
lines changed

3 files changed

+201
-80
lines changed

src/main/java/no/ecc/vectortile/VectorTileDecoder.java

+20-5
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,13 @@
3030
import java.util.NoSuchElementException;
3131
import java.util.Set;
3232

33+
import com.vividsolutions.jts.algorithm.CGAlgorithms;
3334
import com.vividsolutions.jts.geom.Coordinate;
3435
import com.vividsolutions.jts.geom.Geometry;
3536
import com.vividsolutions.jts.geom.GeometryFactory;
3637
import com.vividsolutions.jts.geom.LineString;
3738
import com.vividsolutions.jts.geom.LinearRing;
39+
import com.vividsolutions.jts.geom.Polygon;
3840

3941
import vector_tile.VectorTile;
4042
import vector_tile.VectorTile.Tile.Layer;
@@ -314,18 +316,31 @@ private Feature parseFeature(VectorTile.Tile.Feature feature) {
314316
}
315317
break;
316318
case POLYGON:
317-
List<LinearRing> rings = new ArrayList<LinearRing>();
319+
List<List<LinearRing>> polygonRings = new ArrayList<List<LinearRing>>();
320+
List<LinearRing> ringsForCurrentPolygon = new ArrayList<LinearRing>();
318321
for (List<Coordinate> cs : coordsList) {
319322
// skip hole with too few coordinates
320-
if (rings.size() > 0 && cs.size() < 4) {
323+
if (ringsForCurrentPolygon.size() > 0 && cs.size() < 4) {
321324
continue;
322325
}
323-
rings.add(gf.createLinearRing(cs.toArray(new Coordinate[cs.size()])));
326+
LinearRing ring = gf.createLinearRing(cs.toArray(new Coordinate[cs.size()]));
327+
if (CGAlgorithms.isCCW(ring.getCoordinates())) {
328+
ringsForCurrentPolygon = new ArrayList<LinearRing>();
329+
polygonRings.add(ringsForCurrentPolygon);
330+
}
331+
ringsForCurrentPolygon.add(ring);
324332
}
325-
if (rings.size() > 0) {
333+
List<Polygon> polygons = new ArrayList<Polygon>();
334+
for (List<LinearRing> rings : polygonRings) {
326335
LinearRing shell = rings.get(0);
327336
LinearRing[] holes = rings.subList(1, rings.size()).toArray(new LinearRing[rings.size() - 1]);
328-
geometry = gf.createPolygon(shell, holes);
337+
polygons.add(gf.createPolygon(shell, holes));
338+
}
339+
if (polygons.size() == 1) {
340+
geometry = polygons.get(0);
341+
}
342+
if (polygons.size() > 1) {
343+
geometry = gf.createMultiPolygon(GeometryFactory.toPolygonArray(polygons));
329344
}
330345
break;
331346
case UNKNOWN:

src/main/java/no/ecc/vectortile/VectorTileEncoder.java

+68-64
Original file line numberDiff line numberDiff line change
@@ -24,24 +24,23 @@
2424
import java.util.List;
2525
import java.util.Map;
2626

27-
import com.vividsolutions.jts.geom.Point;
28-
import vector_tile.VectorTile;
29-
3027
import com.vividsolutions.jts.algorithm.CGAlgorithms;
3128
import com.vividsolutions.jts.geom.Coordinate;
3229
import com.vividsolutions.jts.geom.Geometry;
33-
import com.vividsolutions.jts.geom.GeometryCollection;
3430
import com.vividsolutions.jts.geom.GeometryFactory;
3531
import com.vividsolutions.jts.geom.LineString;
3632
import com.vividsolutions.jts.geom.LinearRing;
3733
import com.vividsolutions.jts.geom.MultiLineString;
3834
import com.vividsolutions.jts.geom.MultiPoint;
3935
import com.vividsolutions.jts.geom.MultiPolygon;
36+
import com.vividsolutions.jts.geom.Point;
4037
import com.vividsolutions.jts.geom.Polygon;
4138
import com.vividsolutions.jts.geom.TopologyException;
4239
import com.vividsolutions.jts.io.ParseException;
4340
import com.vividsolutions.jts.io.WKTReader;
4441

42+
import vector_tile.VectorTile;
43+
4544
public class VectorTileEncoder {
4645

4746
private final Map<String, Layer> layers = new LinkedHashMap<String, Layer>();
@@ -136,13 +135,11 @@ public void addFeature(String layerName, Map<String, ?> attributes, Geometry geo
136135
* @param id
137136
*/
138137
public void addFeature(String layerName, Map<String, ?> attributes, Geometry geometry, long id) {
139-
// split up MultiPolygon and GeometryCollection (without subclasses)
140-
if (geometry instanceof MultiPolygon || geometry.getClass().equals(GeometryCollection.class)) {
141-
splitAndAddFeatures(layerName, attributes, (GeometryCollection) geometry);
138+
139+
// skip small Polygon/LineString.
140+
if (geometry instanceof MultiPolygon && geometry.getArea() < 1.0d) {
142141
return;
143142
}
144-
145-
// skip small Polygon/LineString.
146143
if (geometry instanceof Polygon && geometry.getArea() < 1.0d) {
147144
return;
148145
}
@@ -159,12 +156,6 @@ public void addFeature(String layerName, Map<String, ?> attributes, Geometry geo
159156
geometry = clipGeometry(geometry);
160157
}
161158

162-
// if clipping result in MultiPolygon, then split once more
163-
if (geometry instanceof MultiPolygon || geometry.getClass().equals(GeometryCollection.class)) {
164-
splitAndAddFeatures(layerName, attributes, (GeometryCollection) geometry);
165-
return;
166-
}
167-
168159
// no need to add empty geometry
169160
if (geometry.isEmpty()) {
170161
return;
@@ -239,13 +230,6 @@ protected Geometry clipGeometry(Geometry geometry) {
239230
}
240231
}
241232

242-
private void splitAndAddFeatures(String layerName, Map<String, ?> attributes, GeometryCollection geometry) {
243-
for (int i = 0; i < geometry.getNumGeometries(); i++) {
244-
Geometry subGeometry = geometry.getGeometryN(i);
245-
addFeature(layerName, attributes, subGeometry);
246-
}
247-
}
248-
249233
/**
250234
* @return a byte array with the vector tile
251235
*/
@@ -296,6 +280,9 @@ public byte[] encode() {
296280
}
297281

298282
featureBuilder.setType(toGeomType(geometry));
283+
284+
x = 0;
285+
y = 0;
299286
featureBuilder.addAllGeometry(commands(geometry));
300287

301288
tileLayer.addFeatures(featureBuilder.build());
@@ -309,19 +296,22 @@ public byte[] encode() {
309296
}
310297

311298
static VectorTile.Tile.GeomType toGeomType(Geometry geometry) {
312-
if (geometry instanceof com.vividsolutions.jts.geom.Point) {
299+
if (geometry instanceof Point) {
313300
return VectorTile.Tile.GeomType.POINT;
314301
}
315-
if (geometry instanceof com.vividsolutions.jts.geom.MultiPoint) {
302+
if (geometry instanceof MultiPoint) {
316303
return VectorTile.Tile.GeomType.POINT;
317304
}
318-
if (geometry instanceof com.vividsolutions.jts.geom.LineString) {
305+
if (geometry instanceof LineString) {
319306
return VectorTile.Tile.GeomType.LINESTRING;
320307
}
321-
if (geometry instanceof com.vividsolutions.jts.geom.MultiLineString) {
308+
if (geometry instanceof MultiLineString) {
322309
return VectorTile.Tile.GeomType.LINESTRING;
323310
}
324-
if (geometry instanceof com.vividsolutions.jts.geom.Polygon) {
311+
if (geometry instanceof Polygon) {
312+
return VectorTile.Tile.GeomType.POLYGON;
313+
}
314+
if (geometry instanceof MultiPolygon) {
325315
return VectorTile.Tile.GeomType.POLYGON;
326316
}
327317
return VectorTile.Tile.GeomType.UNKNOWN;
@@ -332,48 +322,62 @@ static boolean shouldClosePath(Geometry geometry) {
332322
}
333323

334324
List<Integer> commands(Geometry geometry) {
335-
336-
x = 0;
337-
y = 0;
338-
325+
326+
if (geometry instanceof MultiLineString) {
327+
return commands((MultiLineString) geometry);
328+
}
339329
if (geometry instanceof Polygon) {
340-
Polygon polygon = (Polygon) geometry;
341-
List<Integer> commands = new ArrayList<Integer>();
342-
343-
// According to the vector tile specification, the exterior ring of a polygon
344-
// must be in clockwise order, while the interior ring in counter-clockwise order.
345-
// In the tile coordinate system, Y axis is positive down.
346-
//
347-
// However, in geographic coordinate system, Y axis is positive up.
348-
// Therefore, we must reverse the coordinates.
349-
// So, the code below will make sure that exterior ring is in counter-clockwise order
350-
// and interior ring in clockwise order.
351-
LineString exteriorRing = polygon.getExteriorRing();
352-
if (!CGAlgorithms.isCCW(exteriorRing.getCoordinates())) {
353-
exteriorRing = (LineString) exteriorRing.reverse();
354-
}
355-
commands.addAll(commands(exteriorRing.getCoordinates(), true));
356-
357-
for (int i = 0; i < polygon.getNumInteriorRing(); i++) {
358-
LineString interiorRing = polygon.getInteriorRingN(i);
359-
if (CGAlgorithms.isCCW(interiorRing.getCoordinates())) {
360-
interiorRing = (LineString) interiorRing.reverse();
361-
}
362-
commands.addAll(commands(interiorRing.getCoordinates(), true));
363-
}
364-
return commands;
330+
return commands((Polygon) geometry);
331+
}
332+
if (geometry instanceof MultiPolygon) {
333+
return commands((MultiPolygon) geometry);
334+
}
335+
336+
return commands(geometry.getCoordinates(), shouldClosePath(geometry), geometry instanceof MultiPoint);
337+
}
338+
339+
List<Integer> commands(MultiLineString mls) {
340+
List<Integer> commands = new ArrayList<Integer>();
341+
for (int i = 0; i < mls.getNumGeometries(); i++) {
342+
commands.addAll(commands(mls.getGeometryN(i).getCoordinates(), false));
343+
}
344+
return commands;
345+
}
346+
347+
List<Integer> commands(MultiPolygon mp) {
348+
List<Integer> commands = new ArrayList<Integer>();
349+
for (int i = 0; i < mp.getNumGeometries(); i++) {
350+
Polygon polygon = (Polygon) mp.getGeometryN(i);
351+
commands.addAll(commands(polygon));
365352
}
353+
return commands;
354+
}
355+
356+
List<Integer> commands(Polygon polygon) {
357+
List<Integer> commands = new ArrayList<Integer>();
358+
359+
// According to the vector tile specification, the exterior ring of a polygon
360+
// must be in clockwise order, while the interior ring in counter-clockwise order.
361+
// In the tile coordinate system, Y axis is positive down.
362+
//
363+
// However, in geographic coordinate system, Y axis is positive up.
364+
// Therefore, we must reverse the coordinates.
365+
// So, the code below will make sure that exterior ring is in counter-clockwise order
366+
// and interior ring in clockwise order.
367+
LineString exteriorRing = polygon.getExteriorRing();
368+
if (!CGAlgorithms.isCCW(exteriorRing.getCoordinates())) {
369+
exteriorRing = (LineString) exteriorRing.reverse();
370+
}
371+
commands.addAll(commands(exteriorRing.getCoordinates(), true));
366372

367-
if (geometry instanceof MultiLineString) {
368-
List<Integer> commands = new ArrayList<Integer>();
369-
GeometryCollection gc = (GeometryCollection) geometry;
370-
for (int i = 0; i < gc.getNumGeometries(); i++) {
371-
commands.addAll(commands(gc.getGeometryN(i).getCoordinates(), false));
373+
for (int i = 0; i < polygon.getNumInteriorRing(); i++) {
374+
LineString interiorRing = polygon.getInteriorRingN(i);
375+
if (CGAlgorithms.isCCW(interiorRing.getCoordinates())) {
376+
interiorRing = (LineString) interiorRing.reverse();
372377
}
373-
return commands;
378+
commands.addAll(commands(interiorRing.getCoordinates(), true));
374379
}
375-
376-
return commands(geometry.getCoordinates(), shouldClosePath(geometry), geometry instanceof MultiPoint);
380+
return commands;
377381
}
378382

379383
private int x = 0;

0 commit comments

Comments
 (0)