diff --git a/modules/core/src/main/java/org/locationtech/jts/geom/Geometry.java b/modules/core/src/main/java/org/locationtech/jts/geom/Geometry.java index 104a885c8e..7876d8b472 100644 --- a/modules/core/src/main/java/org/locationtech/jts/geom/Geometry.java +++ b/modules/core/src/main/java/org/locationtech/jts/geom/Geometry.java @@ -563,6 +563,34 @@ public Point getInteriorPoint() */ public abstract int getDimension(); + /** + * Returns the coordinate dimension of this geometry, + * based on the dimension of its {@link CoordinateSequence}s + * (i.e. 2 for XY, 3 for XYZ or XYM, or 4 for XYZM). + * A coordinate dimension of 3 does not distinguish XYZ from XYM; + * use {@link CoordinateSequence#hasZ()} and {@link CoordinateSequence#hasM()} + * to determine which ordinates are present. + * For a {@link GeometryCollection} this is the maximum coordinate dimension + * over all of its elements. + * An empty collection has coordinate dimension 2. + *
+ * Empty geometries report the dimension of their (empty)
+ * coordinate sequences, so e.g. POINT Z EMPTY read from WKT
+ * has coordinate dimension 3.
+ * Note that empty geometries created by {@link GeometryFactory}
+ * (e.g. via {@link GeometryFactory#createPoint()}) may report
+ * a coordinate dimension of 3, since that is the default
+ * dimension of their coordinate sequences.
+ *
+ * Note that this is a different concept to the topological + * dimension returned by {@link #getDimension()}. + * + * @return the coordinate dimension of this geometry (2, 3 or 4) + * + * @see #getDimension() + */ + public abstract int getCoordinateDimension(); + /** * Tests whether an atomic geometry or any element of a collection * has the specified dimension. diff --git a/modules/core/src/main/java/org/locationtech/jts/geom/GeometryCollection.java b/modules/core/src/main/java/org/locationtech/jts/geom/GeometryCollection.java index f89c84c4b5..b127a0ec73 100644 --- a/modules/core/src/main/java/org/locationtech/jts/geom/GeometryCollection.java +++ b/modules/core/src/main/java/org/locationtech/jts/geom/GeometryCollection.java @@ -113,6 +113,14 @@ public int getDimension() { //*/ } + public int getCoordinateDimension() { + int dimension = 2; + for (int i = 0; i < geometries.length; i++) { + dimension = Math.max(dimension, geometries[i].getCoordinateDimension()); + } + return dimension; + } + public boolean hasDimension(int dim) { if (geomCollDim == null) { geomCollDim = new GeometryCollectionDimension(this); diff --git a/modules/core/src/main/java/org/locationtech/jts/geom/LineString.java b/modules/core/src/main/java/org/locationtech/jts/geom/LineString.java index bf79badbc6..8ca835592a 100644 --- a/modules/core/src/main/java/org/locationtech/jts/geom/LineString.java +++ b/modules/core/src/main/java/org/locationtech/jts/geom/LineString.java @@ -113,6 +113,10 @@ public int getDimension() { return 1; } + public int getCoordinateDimension() { + return points.getDimension(); + } + public int getBoundaryDimension() { if (isClosed()) { return Dimension.FALSE; diff --git a/modules/core/src/main/java/org/locationtech/jts/geom/Point.java b/modules/core/src/main/java/org/locationtech/jts/geom/Point.java index cbe145416f..d8e511e510 100644 --- a/modules/core/src/main/java/org/locationtech/jts/geom/Point.java +++ b/modules/core/src/main/java/org/locationtech/jts/geom/Point.java @@ -91,6 +91,10 @@ public int getDimension() { return 0; } + public int getCoordinateDimension() { + return coordinates.getDimension(); + } + public int getBoundaryDimension() { return Dimension.FALSE; } diff --git a/modules/core/src/main/java/org/locationtech/jts/geom/Polygon.java b/modules/core/src/main/java/org/locationtech/jts/geom/Polygon.java index 94a8bc17fa..324de53a99 100644 --- a/modules/core/src/main/java/org/locationtech/jts/geom/Polygon.java +++ b/modules/core/src/main/java/org/locationtech/jts/geom/Polygon.java @@ -165,6 +165,14 @@ public int getDimension() { return 2; } + public int getCoordinateDimension() { + int dimension = shell.getCoordinateDimension(); + for (int i = 0; i < holes.length; i++) { + dimension = Math.max(dimension, holes[i].getCoordinateDimension()); + } + return dimension; + } + public int getBoundaryDimension() { return 1; } diff --git a/modules/core/src/test/java/org/locationtech/jts/geom/GeometryCoordinateDimensionTest.java b/modules/core/src/test/java/org/locationtech/jts/geom/GeometryCoordinateDimensionTest.java new file mode 100644 index 0000000000..8bae0b86db --- /dev/null +++ b/modules/core/src/test/java/org/locationtech/jts/geom/GeometryCoordinateDimensionTest.java @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2026 Matthew de Detrich. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * and Eclipse Distribution License v. 1.0 which accompanies this distribution. + * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html + * and the Eclipse Distribution License is available at + * + * http://www.eclipse.org/org/documents/edl-v10.php. + */ + +package org.locationtech.jts.geom; + +import org.locationtech.jts.geom.impl.CoordinateArraySequenceFactory; +import org.locationtech.jts.geom.impl.PackedCoordinateSequenceFactory; +import org.locationtech.jts.io.WKTReader; + +import junit.textui.TestRunner; +import test.jts.GeometryTestCase; + +/** + * Tests for {@link Geometry#getCoordinateDimension()}. + */ +public class GeometryCoordinateDimensionTest extends GeometryTestCase { + public static void main(String args[]) { + TestRunner.run(GeometryCoordinateDimensionTest.class); + } + + static GeometryFactory geomFact = new GeometryFactory(PackedCoordinateSequenceFactory.DOUBLE_FACTORY); + + /** + * A reader with the legacy "unlabelled 3rd ordinate is Z" syntax disabled, + * so that WKT lacking a Z or M tag is parsed as genuinely 2D. + */ + static WKTReader strictReader = strictReader(); + + private static WKTReader strictReader() { + WKTReader reader = new WKTReader(geomFact); + reader.setIsOldJtsCoordinateSyntaxAllowed(false); + return reader; + } + + static GeometryFactory arrayGeomFact = new GeometryFactory(CoordinateArraySequenceFactory.instance()); + + static WKTReader arrayStrictReader = arrayStrictReader(); + + private static WKTReader arrayStrictReader() { + WKTReader reader = new WKTReader(arrayGeomFact); + reader.setIsOldJtsCoordinateSyntaxAllowed(false); + return reader; + } + + public GeometryCoordinateDimensionTest(String name) { super(name); } + + public void testXY() { + checkDimension("POINT (1 2)", 2); + } + + public void testXYZ() { + checkDimension("POINT Z (1 2 3)", 3); + } + + public void testXYM() { + checkDimension("POINT M (1 2 3)", 3); + } + + public void testXYZM() { + checkDimension("POINT ZM (1 2 3 4)", 4); + } + + public void testLineStringXYZ() { + checkDimension("LINESTRING Z (1 1 1, 2 2 2, 3 3 3)", 3); + } + + public void testPolygonXYZ() { + checkDimension("POLYGON Z ((1 9 2, 9 9 2, 9 1 2, 1 1 2, 1 9 2))", 3); + } + + public void testEmptyPoint() { + checkDimension("POINT EMPTY", 2); + } + + public void testEmptyPointZ() { + checkDimension("POINT Z EMPTY", 3); + } + + public void testEmptyPointZM() { + checkDimension("POINT ZM EMPTY", 4); + } + + public void testEmptyLineStringZ() { + checkDimension("LINESTRING Z EMPTY", 3); + } + + public void testGeometryCollectionEmptyZ() { + checkDimension("GEOMETRYCOLLECTION (POINT Z EMPTY, LINESTRING EMPTY)", 3); + } + + public void testMultiPolygonXYZM() { + checkDimension("MULTIPOLYGON ZM (((1 9 2 3, 9 9 2 3, 9 1 2 3, 1 1 2 3, 1 9 2 3)))", 4); + } + + public void testGeometryCollectionMixedDimension() { + checkDimension("GEOMETRYCOLLECTION (POINT (1 1), LINESTRING Z (1 1 1, 2 2 2))", 3); + } + + public void testGeometryCollectionAllEmpty() { + checkDimension("GEOMETRYCOLLECTION (POINT EMPTY, LINESTRING EMPTY)", 2); + } + + public void testArraySequenceXY() { + checkDimension(arrayStrictReader, "POINT (1 2)", 2); + } + + public void testArraySequenceEmptyPointZ() { + checkDimension(arrayStrictReader, "POINT Z EMPTY", 3); + } + + /** + * Empty geometries created by the factory use the + * default coordinate sequence dimension (3). + */ + public void testArraySequenceFactoryEmptyPoint() { + assertEquals(3, arrayGeomFact.createPoint().getCoordinateDimension()); + } + + private void checkDimension(String wkt, int expectedDimension) { + checkDimension(strictReader, wkt, expectedDimension); + } + + private void checkDimension(WKTReader reader, String wkt, int expectedDimension) { + Geometry geom = read(reader, wkt); + int actual = geom.getCoordinateDimension(); + assertEquals(expectedDimension, actual); + } +}