This is an automated email from the ASF dual-hosted git repository. jiayu pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/sedona.git
The following commit(s) were added to refs/heads/master by this push: new 4e40828598 [GH-2304]add Multipolygon encode / decode (#2303) 4e40828598 is described below commit 4e4082859857ddbb045088a295ed3294e236f300 Author: Zhuocheng Shang <122398181+zhuochengsh...@users.noreply.github.com> AuthorDate: Wed Aug 20 11:19:46 2025 -0700 [GH-2304]add Multipolygon encode / decode (#2303) * add Multipolygon encode / decode * more solid streaming read in --- .../sedona/common/S2Geography/Geography.java | 4 +- .../common/S2Geography/GeographyCollection.java | 2 +- .../common/S2Geography/MultiPolygonGeography.java | 61 ++++++++++++++++++++++ .../common/S2Geography/PolygonGeographyTest.java | 42 +++++++++++++++ .../sedona/common/S2Geography/TestHelper.java | 8 +++ 5 files changed, 115 insertions(+), 2 deletions(-) diff --git a/common/src/main/java/org/apache/sedona/common/S2Geography/Geography.java b/common/src/main/java/org/apache/sedona/common/S2Geography/Geography.java index da2c0a5dbc..bfc5d6df35 100644 --- a/common/src/main/java/org/apache/sedona/common/S2Geography/Geography.java +++ b/common/src/main/java/org/apache/sedona/common/S2Geography/Geography.java @@ -244,9 +244,11 @@ public abstract class Geography { geo = PolylineGeography.decode(in, tag); break; case POLYGON: - case MULTIPOLYGON: geo = PolygonGeography.decode(in, tag); break; + case MULTIPOLYGON: + geo = MultiPolygonGeography.decode(in, tag); + break; case GEOGRAPHY_COLLECTION: geo = GeographyCollection.decode(in, tag); break; diff --git a/common/src/main/java/org/apache/sedona/common/S2Geography/GeographyCollection.java b/common/src/main/java/org/apache/sedona/common/S2Geography/GeographyCollection.java index d384101c29..71e9f84624 100644 --- a/common/src/main/java/org/apache/sedona/common/S2Geography/GeographyCollection.java +++ b/common/src/main/java/org/apache/sedona/common/S2Geography/GeographyCollection.java @@ -152,7 +152,7 @@ public class GeographyCollection extends Geography { return geo; } - private void countShapes() { + void countShapes() { numShapesList.clear(); totalShapes = 0; for (Geography geo : features) { diff --git a/common/src/main/java/org/apache/sedona/common/S2Geography/MultiPolygonGeography.java b/common/src/main/java/org/apache/sedona/common/S2Geography/MultiPolygonGeography.java index ec71252874..32090b5c94 100644 --- a/common/src/main/java/org/apache/sedona/common/S2Geography/MultiPolygonGeography.java +++ b/common/src/main/java/org/apache/sedona/common/S2Geography/MultiPolygonGeography.java @@ -18,11 +18,19 @@ */ package org.apache.sedona.common.S2Geography; +import com.esotericsoftware.kryo.io.Input; +import com.esotericsoftware.kryo.io.UnsafeInput; +import com.esotericsoftware.kryo.io.UnsafeOutput; import com.google.common.geometry.S2Polygon; +import java.io.EOFException; +import java.io.IOException; +import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.logging.Logger; public class MultiPolygonGeography extends GeographyCollection { + private static final Logger logger = Logger.getLogger(MultiPolygonGeography.class.getName()); /** * Wrap each raw S2Polygon in a PolygonGeography, then hand it off to GeographyCollection to do * the rest (including serialization). @@ -44,4 +52,57 @@ public class MultiPolygonGeography extends GeographyCollection { // every child PolygonGeography return 2; } + + @Override + public void encode(UnsafeOutput out, EncodeOptions opts) throws IOException { + out.writeInt(features.size()); + for (Geography feature : features) { + ((PolygonGeography) feature).polygon.encode(out); + } + out.flush(); + } + + /** This is what decodeTagged() actually calls */ + public static MultiPolygonGeography decode(Input in, EncodeTag tag) throws IOException { + // cast to UnsafeInput—will work if you always pass a Kryo-backed stream + if (!(in instanceof UnsafeInput)) { + throw new IllegalArgumentException("Expected UnsafeInput"); + } + return decode((UnsafeInput) in, tag); + } + + /** Decodes a GeographyCollection from a tagged input stream. */ + public static MultiPolygonGeography decode(UnsafeInput in, EncodeTag tag) + throws IOException, EOFException { + // Handle EMPTY flag + if ((tag.getFlags() & EncodeTag.FLAG_EMPTY) != 0) { + logger.fine("Decoded empty Multipolygon."); + return new MultiPolygonGeography(); + } + + // Skip any covering data + tag.skipCovering(in); + + MultiPolygonGeography geo; + try { + int count = in.readInt(); + if (count < 0) { + throw new IOException("MultiPolygon.decodeTagged error: negative polygon count: " + count); + } + + List<S2Polygon> polygons = new ArrayList<>(); + for (int i = 0; i < count; i++) { + polygons.add(S2Polygon.decode(in)); + } + + geo = new MultiPolygonGeography(GeographyKind.MULTIPOLYGON, polygons); + geo.countShapes(); + + } catch (EOFException e) { + throw new IOException( + "MultiPolygon.decodeTagged error: insufficient data to decode all parts of the geography.", + e); + } + return geo; + } } diff --git a/common/src/test/java/org/apache/sedona/common/S2Geography/PolygonGeographyTest.java b/common/src/test/java/org/apache/sedona/common/S2Geography/PolygonGeographyTest.java index 5a1ace7976..a86865a27f 100644 --- a/common/src/test/java/org/apache/sedona/common/S2Geography/PolygonGeographyTest.java +++ b/common/src/test/java/org/apache/sedona/common/S2Geography/PolygonGeographyTest.java @@ -18,7 +18,11 @@ */ package org.apache.sedona.common.S2Geography; +import static org.junit.Assert.assertEquals; + import com.google.common.geometry.*; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -42,4 +46,42 @@ public class PolygonGeographyTest { TestHelper.assertRoundTrip(geo, new EncodeOptions()); } + + @Test + public void testEncodeMultiPolygon() throws IOException { + S2Point pt = S2LatLng.fromDegrees(45, -64).toPoint(); + S2Point pt_mid = S2LatLng.fromDegrees(45, 0).toPoint(); + S2Point pt_end = S2LatLng.fromDegrees(0, 0).toPoint(); + // Build a single polygon and wrap in geography + List<S2Point> points = new ArrayList<>(); + points.add(pt); + points.add(pt_mid); + points.add(pt_end); + points.add(pt); + S2Loop polyline = new S2Loop(points); + S2Polygon poly = new S2Polygon(polyline); + + S2Point pt2 = S2LatLng.fromDegrees(30, 10).toPoint(); + S2Point pt_mid2 = S2LatLng.fromDegrees(35, 20).toPoint(); + S2Point pt_end2 = S2LatLng.fromDegrees(32, 15).toPoint(); + // Build a single polygon and wrap in geography + List<S2Point> points2 = new ArrayList<>(); + points2.add(pt2); + points2.add(pt_mid2); + points2.add(pt_end2); + points2.add(pt2); + S2Loop polyline2 = new S2Loop(points2); + S2Polygon poly2 = new S2Polygon(polyline2); + + MultiPolygonGeography geo = + new MultiPolygonGeography(Geography.GeographyKind.MULTIPOLYGON, List.of(poly2, poly)); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + geo.encodeTagged(baos, new EncodeOptions()); + byte[] data = baos.toByteArray(); + + ByteArrayInputStream in = new ByteArrayInputStream(data); + MultiPolygonGeography decoded = (MultiPolygonGeography) Geography.decodeTagged(in); + assertEquals(0, TestHelper.compareTo(geo, decoded)); + } } diff --git a/common/src/test/java/org/apache/sedona/common/S2Geography/TestHelper.java b/common/src/test/java/org/apache/sedona/common/S2Geography/TestHelper.java index bc75e8a27a..dee589ab9d 100644 --- a/common/src/test/java/org/apache/sedona/common/S2Geography/TestHelper.java +++ b/common/src/test/java/org/apache/sedona/common/S2Geography/TestHelper.java @@ -220,6 +220,14 @@ public class TestHelper { Geography g2 = (Geography) ((GeographyCollection) geo2).features.get(i); compareTo(g1, g2); } + } else if (geo1 instanceof MultiPolygonGeography && geo2 instanceof MultiPolygonGeography) { + if (S2_isEmpty(geo1) && S2_isEmpty(geo2)) return 0; + assertEquals(geo1.numShapes(), geo2.numShapes()); + for (int i = 0; i < geo1.numShapes(); i++) { + Geography g1 = (Geography) ((MultiPolygonGeography) geo1).features.get(i); + Geography g2 = (Geography) ((MultiPolygonGeography) geo2).features.get(i); + compareTo(g1, g2); + } } return 0; }