Skip to content

Commit

Permalink
Allow GeoJSONWriter to force polygons to be CCW (#694)
Browse files Browse the repository at this point in the history
Allow GeoJSONWriter to force polygons to be CCW

Signed-off-by: Rui Brito <[email protected]>
  • Loading branch information
ruibritopt authored Feb 25, 2021
1 parent c7760fd commit 279ab3c
Show file tree
Hide file tree
Showing 3 changed files with 134 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,6 @@
*/
package org.locationtech.jts.io.geojson;

import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.json.simple.JSONAware;
import org.json.simple.JSONObject;
import org.locationtech.jts.geom.CoordinateSequence;
Expand All @@ -32,6 +24,14 @@
import org.locationtech.jts.geom.Polygon;
import org.locationtech.jts.util.Assert;

import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;


/**
* Writes {@link Geometry}s as JSON fragments in GeoJSON format.
Expand Down Expand Up @@ -59,6 +59,7 @@ public class GeoJsonWriter {

private double scale;
private boolean isEncodeCRS = true;
private boolean isForceCCW = false;

/**
* Constructs a GeoJsonWriter instance.
Expand Down Expand Up @@ -88,6 +89,16 @@ public void setEncodeCRS(boolean isEncodeCRS) {
this.isEncodeCRS = isEncodeCRS;
}

/**
* Sets whether the GeoJSON should be output following counter-clockwise orientation aka Right Hand Rule defined in RFC7946
* See <a href="https://tools.ietf.org/html/rfc7946#section-3.1.6">RFC 7946 Specification</a> for more context.
*
* @param isForceCCW true if the GeoJSON should be output following the RFC7946 counter-clockwise orientation aka Right Hand Rule
*/
public void setForceCCW(boolean isForceCCW) {
this.isForceCCW = isForceCCW;
}

/**
* Writes a {@link Geometry} in GeoJson format to a String.
*
Expand Down Expand Up @@ -158,6 +169,10 @@ public String toJSONString() {
} else if (geometry instanceof Polygon) {
Polygon polygon = (Polygon) geometry;

if (isForceCCW) {
polygon = (Polygon) OrientationTransformer.transformCCW(polygon);
}

result.put(GeoJsonConstants.NAME_COORDINATES, makeJsonAware(polygon));

} else if (geometry instanceof MultiPoint) {
Expand All @@ -173,6 +188,10 @@ public String toJSONString() {
} else if (geometry instanceof MultiPolygon) {
MultiPolygon multiPolygon = (MultiPolygon) geometry;

if (isForceCCW) {
multiPolygon = (MultiPolygon) OrientationTransformer.transformCCW(multiPolygon);
}

result.put(GeoJsonConstants.NAME_COORDINATES, makeJsonAware(multiPolygon));

} else if (geometry instanceof GeometryCollection) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package org.locationtech.jts.io.geojson;

import org.locationtech.jts.algorithm.Orientation;
import org.locationtech.jts.geom.*;

import java.util.ArrayList;
import java.util.List;

/**
* Utilities to modify the ring orientation of polygonal geometries.
*/
public class OrientationTransformer {

/**
* Transforms a geometry using the Right Hand Rule specifications defined
* in the latest GeoJSON specification.
* See <a href="https://tools.ietf.org/html/rfc7946#section-3.1.6">RFC-7946 Specification</a> for more context.
*
* @param geometry to be transformed
* @return Geometry under the Right Hand Rule specifications
*/
public static Geometry transformCCW(final Geometry geometry) {

if (geometry instanceof MultiPolygon) {
MultiPolygon multiPolygon = (MultiPolygon) geometry;

List<Polygon> polygons = new ArrayList<>();
for (int i = 0; i < multiPolygon.getNumGeometries(); i++) {
final Geometry polygon = multiPolygon.getGeometryN(i);
polygons.add((Polygon) transformCCW(polygon));
}

return new GeometryFactory().createMultiPolygon(polygons.toArray(new Polygon[0]));

} else if (geometry instanceof Polygon) {
return transformCCW((Polygon) geometry);

} else {
return geometry;
}
}

/**
* Transforms a polygon using the Right Hand Rule specifications defined
* in the latest GeoJSON specification.
* See <a href="https://tools.ietf.org/html/rfc7946#section-3.1.6">RFC-7946 Specification</a> for more context.
*
* @param polygon to be transformed
* @return Polygon under the Right Hand Rule specifications
*/
public static Polygon transformCCW(Polygon polygon) {
LinearRing exteriorRing = polygon.getExteriorRing();
LinearRing exteriorRingEnforced = transformCCW(exteriorRing, true);

List<LinearRing> interiorRings = new ArrayList<>();
for (int i = 0; i < polygon.getNumInteriorRing(); i++) {
interiorRings.add(transformCCW(polygon.getInteriorRingN(i), false));
}

return new GeometryFactory(polygon.getPrecisionModel(), polygon.getSRID())
.createPolygon(exteriorRingEnforced, interiorRings.toArray(new LinearRing[0]));
}

/**
* Transforms a polygon using the Right Hand Rule specifications defined
* in the latest GeoJSON specification.
* A linear ring MUST follow the right-hand rule with respect to the
* area it bounds, i.e., exterior rings are counterclockwise, and
* holes are clockwise.
*
* See <a href="https://tools.ietf.org/html/rfc7946#section-3.1.6">RFC 7946 Specification</a> for more context.
*
* @param ring the LinearRing, a constraint specific to Polygons
* @param isExteriorRing true if the LinearRing is the exterior polygon ring, the one that defines the boundary
* @return LinearRing under the Right Hand Rule specifications
*/
public static LinearRing transformCCW(LinearRing ring, boolean isExteriorRing) {
final boolean isRingClockWise = !Orientation.isCCW(ring.getCoordinateSequence());

final LinearRing rightHandRuleRing;
if (isExteriorRing) {
rightHandRuleRing = isRingClockWise? ring.reverse() : (LinearRing) ring.copy();
} else {
rightHandRuleRing = isRingClockWise? (LinearRing) ring.copy() : ring.reverse();
}
return rightHandRuleRing;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,16 @@ public void testPolygonWithHole() throws ParseException {
"{'type':'Polygon','coordinates':[[[0.0,0.0],[100,0.0],[100,100],[0.0,100],[0.0,0.0]],[[1,1],[1,10],[10,10],[10,1],[1,1]]]}");
}

public void testPolygonRightHandRule() throws ParseException {
runTest("POLYGON ((0 0, 0 100, 100 100, 100 0, 0 0))", true,
"{'type':'Polygon','coordinates':[[[0.0,0.0],[100,0.0],[100,100],[0.0,100],[0.0,0.0]]]}");
}

public void testPolygonWithHoleRightHandRule() throws ParseException {
runTest("POLYGON ((0 0, 0 100, 100 100, 100 0, 0 0), (1 1, 10 1, 10 10, 1 10, 1 1) )", true,
"{'type':'Polygon','coordinates':[[[0.0,0.0],[100,0.0],[100,100],[0.0,100],[0.0,0.0]],[[1,1],[1,10],[10,10],[10,1],[1,1]]]}");
}

public void testMultiPoint() throws ParseException {
runTest("MULTIPOINT ((0 0), (1 4), (100 200))",
"{'type':'MultiPoint','coordinates':[[0.0,0.0],[1,4],[100,200]]}");
Expand Down Expand Up @@ -125,20 +135,25 @@ private void runTest(String wkt) throws ParseException {
}

private void runTest(String wkt, String expectedGeojson) throws ParseException {
runTest(wkt, 0, false, expectedGeojson);
runTest(wkt, 0, false, false, expectedGeojson);
}

private void runTest(String wkt, int srid, String expectedGeojson) throws ParseException {
runTest(wkt, srid, true, expectedGeojson);
runTest(wkt, srid, true, false, expectedGeojson);
}

private void runTest(String wkt, boolean enforceRHR, String expectedGeojson) throws ParseException {
runTest(wkt, 0, false, enforceRHR, expectedGeojson);
}

private void runTest(String wkt, int srid, boolean encodeCRS, String expectedGeojson) throws ParseException {
private void runTest(String wkt, int srid, boolean encodeCRS, boolean enforceRHR, String expectedGeojson) throws ParseException {
Geometry geom = read(wkt);
geom.setSRID(srid);
geoJsonWriter.setEncodeCRS(encodeCRS);
geoJsonWriter.setForceCCW(enforceRHR);
String json = this.geoJsonWriter.write(geom);
json = json.replace('"', '\'');
assertEquals(json, expectedGeojson);
assertEquals(expectedGeojson, json);
}

}

0 comments on commit 279ab3c

Please sign in to comment.