diff --git a/include/geos/io/GeoJSONWriter.h b/include/geos/io/GeoJSONWriter.h index 8bb7e663ba..002a6f5018 100644 --- a/include/geos/io/GeoJSONWriter.h +++ b/include/geos/io/GeoJSONWriter.h @@ -74,11 +74,33 @@ class GEOS_DLL GeoJSONWriter { std::string write(const GeoJSONFeatureCollection& features); + /* + * \brief + * Returns the output dimension used by the + * GeoJSONWriter. + */ + int + getOutputDimension() const + { + return defaultOutputDimension; + } + + /* + * Sets the output dimension used by the GeoJSONWriter. + * + * @param newOutputDimension Supported values are 2 or 3. + * Default since GEOS 3.12 is 3. + * Note that 3 indicates up to 3 dimensions will be + * written but 2D GeoJSON is still produced for 2D geometries. + */ + void setOutputDimension(uint8_t newOutputDimension); + private: + uint8_t defaultOutputDimension = 3; - std::pair convertCoordinate(const geom::CoordinateXY* c); + std::vector convertCoordinate(const geom::Coordinate* c); - std::vector> convertCoordinateSequence(const geom::CoordinateSequence* c); + std::vector> convertCoordinateSequence(const geom::CoordinateSequence* c); void encode(const geom::Geometry* g, GeoJSONType type, geos_nlohmann::ordered_json& j); diff --git a/src/io/GeoJSONReader.cpp b/src/io/GeoJSONReader.cpp index 9daa5ac150..fe53b63c75 100644 --- a/src/io/GeoJSONReader.cpp +++ b/src/io/GeoJSONReader.cpp @@ -210,13 +210,16 @@ geom::Coordinate GeoJSONReader::readCoordinate( const std::vector& coords) const { if (coords.size() == 1) { - throw ParseException("Expected two coordinates found one"); + throw ParseException("Expected two or three coordinates found one"); } - else if (coords.size() > 2) { - throw ParseException("Expected two coordinates found more than two"); + else if (coords.size() == 2) { + return geom::Coordinate { coords[0], coords[1] }; + } + else if (coords.size() == 3) { + return geom::Coordinate { coords[0], coords[1], coords[2] }; } else { - return geom::Coordinate {coords[0], coords[1]}; + throw ParseException("Expected two or three coordinates found more than three"); } } @@ -225,7 +228,7 @@ std::unique_ptr GeoJSONReader::readPoint( { const auto& coords = j.at("coordinates").get>(); if (coords.size() == 1) { - throw ParseException("Expected two coordinates found one"); + throw ParseException("Expected two or three coordinates found one"); } else if (coords.size() < 2) { return geometryFactory.createPoint(2); @@ -240,7 +243,8 @@ std::unique_ptr GeoJSONReader::readLineString( const geos_nlohmann::json& j) const { const auto& coords = j.at("coordinates").get>>(); - auto coordinates = detail::make_unique(0u, 2u); + bool has_z = std::any_of(coords.begin(), coords.end(), [](auto v) { return v.size() > 2; }); + auto coordinates = detail::make_unique(0u, has_z, false); coordinates->reserve(coords.size()); for (const auto& coord : coords) { const geom::Coordinate& c = readCoordinate(coord); @@ -263,7 +267,8 @@ std::unique_ptr GeoJSONReader::readPolygon( std::vector> rings; rings.reserve(polygonCoords.size()); for (const auto& ring : polygonCoords) { - auto coordinates = detail::make_unique(0u, 2u); + bool has_z = std::any_of(ring.begin(), ring.end(), [](auto v) { return v.size() > 2; }); + auto coordinates = detail::make_unique(0u, has_z, false); coordinates->reserve(ring.size()); for (const auto& coord : ring) { const geom::Coordinate& c = readCoordinate(coord); @@ -307,7 +312,8 @@ std::unique_ptr GeoJSONReader::readMultiLineString( std::vector> lines; lines.reserve(listOfCoords.size()); for (const auto& coords : listOfCoords) { - auto coordinates = detail::make_unique(0u, 2u); + bool has_z = std::any_of(coords.begin(), coords.end(), [](auto v) { return v.size() > 2; }); + auto coordinates = detail::make_unique(0u, has_z, false); coordinates->reserve(coords.size()); for (const auto& coord : coords) { const geom::Coordinate& c = readCoordinate(coord); diff --git a/src/io/GeoJSONWriter.cpp b/src/io/GeoJSONWriter.cpp index 0fd4bc297c..bf3826d49a 100644 --- a/src/io/GeoJSONWriter.cpp +++ b/src/io/GeoJSONWriter.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include "geos/util.h" @@ -40,6 +41,17 @@ using json = geos_nlohmann::ordered_json; namespace geos { namespace io { // geos.io + +/* public */ +void +GeoJSONWriter::setOutputDimension(uint8_t dims) +{ + if(dims < 2 || dims > 3) { + throw util::IllegalArgumentException("GeoJSON output dimension must be 2 or 3"); + } + defaultOutputDimension = dims; +} + std::string GeoJSONWriter::write(const geom::Geometry* geometry, GeoJSONType type) { json j; @@ -217,7 +229,8 @@ void GeoJSONWriter::encodePoint(const geom::Point* point, geos_nlohmann::ordered { j["type"] = "Point"; if (!point->isEmpty()) { - j["coordinates"] = convertCoordinate(point->getCoordinate()); + auto as_coord = Coordinate { point->getX(), point->getY(), point->getZ()}; + j["coordinates"] = convertCoordinate(&as_coord); } else { j["coordinates"] = j.array(); @@ -233,7 +246,7 @@ void GeoJSONWriter::encodeLineString(const geom::LineString* line, geos_nlohmann void GeoJSONWriter::encodePolygon(const geom::Polygon* poly, geos_nlohmann::ordered_json& j) { j["type"] = "Polygon"; - std::vector>> rings; + std::vector>> rings; auto ring = poly->getExteriorRing(); rings.reserve(poly->getNumInteriorRing()+1); rings.push_back(convertCoordinateSequence(ring->getCoordinates().get())); @@ -252,7 +265,7 @@ void GeoJSONWriter::encodeMultiPoint(const geom::MultiPoint* multiPoint, geos_nl void GeoJSONWriter::encodeMultiLineString(const geom::MultiLineString* multiLineString, geos_nlohmann::ordered_json& j) { j["type"] = "MultiLineString"; - std::vector>> lines; + std::vector>> lines; lines.reserve(multiLineString->getNumGeometries()); for (size_t i = 0; i < multiLineString->getNumGeometries(); i++) { lines.push_back(convertCoordinateSequence(multiLineString->getGeometryN(i)->getCoordinates().get())); @@ -263,11 +276,11 @@ void GeoJSONWriter::encodeMultiLineString(const geom::MultiLineString* multiLine void GeoJSONWriter::encodeMultiPolygon(const geom::MultiPolygon* multiPolygon, geos_nlohmann::ordered_json& json) { json["type"] = "MultiPolygon"; - std::vector>>> polygons; + std::vector>>> polygons; polygons.reserve(multiPolygon->getNumGeometries()); for (size_t i = 0; i < multiPolygon->getNumGeometries(); i++) { const Polygon* polygon = multiPolygon->getGeometryN(i); - std::vector>> rings; + std::vector>> rings; auto ring = polygon->getExteriorRing(); rings.reserve(polygon->getNumInteriorRing() + 1); rings.push_back(convertCoordinateSequence(ring->getCoordinates().get())); @@ -291,15 +304,18 @@ void GeoJSONWriter::encodeGeometryCollection(const geom::GeometryCollection* g, j["geometries"] = geometryArray; } -std::pair GeoJSONWriter::convertCoordinate(const CoordinateXY* c) +std::vector GeoJSONWriter::convertCoordinate(const Coordinate* c) { - return std::make_pair(c->x, c->y); + if (std::isnan(c->z) || defaultOutputDimension == 2) { + return std::vector { c->x, c->y }; + } + return std::vector { c->x, c->y, c->z }; } -std::vector> GeoJSONWriter::convertCoordinateSequence(const CoordinateSequence* +std::vector> GeoJSONWriter::convertCoordinateSequence(const CoordinateSequence* coordinateSequence) { - std::vector> coordinates; + std::vector> coordinates; coordinates.reserve(coordinateSequence->size()); for (size_t i = 0; isize(); i++) { const geom::Coordinate& c = coordinateSequence->getAt(i); diff --git a/tests/unit/io/GeoJSONReaderTest.cpp b/tests/unit/io/GeoJSONReaderTest.cpp index bcbb9fead4..2a8e7c0309 100644 --- a/tests/unit/io/GeoJSONReaderTest.cpp +++ b/tests/unit/io/GeoJSONReaderTest.cpp @@ -336,7 +336,7 @@ void object::test<22> errorMessage = e.what(); } ensure(error == true); - ensure_equals(errorMessage, "ParseException: Expected two coordinates found one"); + ensure_equals(errorMessage, "ParseException: Expected two or three coordinates found one"); } // Throw ParseException for bad GeoJSON @@ -374,7 +374,7 @@ void object::test<24> errorMessage = e.what(); } ensure(error == true); - ensure_equals(errorMessage, "ParseException: Expected two coordinates found one"); + ensure_equals(errorMessage, "ParseException: Expected two or three coordinates found one"); } // Throw error when geometry type is unsupported @@ -412,7 +412,7 @@ void object::test<26> errorMessage = e.what(); } ensure(error == true); - ensure_equals(errorMessage, "ParseException: Expected two coordinates found one"); + ensure_equals(errorMessage, "ParseException: Expected two or three coordinates found one"); } // Read a GeoJSON empty Polygon with empty shell and empty inner rings @@ -446,7 +446,7 @@ void object::test<29> () { std::string errorMessage; - std::string geojson { "{\"type\":\"Point\",\"coordinates\":[1,2,3,4,5,6]}" }; + std::string geojson { "{\"type\":\"Point\",\"coordinates\":[1,2,3,4]}" }; bool error = false; try { GeomPtr geom(geojsonreader.read(geojson)); @@ -455,7 +455,7 @@ void object::test<29> errorMessage = e.what(); } ensure(error == true); - ensure_equals(errorMessage, "ParseException: Expected two coordinates found more than two"); + ensure_equals(errorMessage, "ParseException: Expected two or three coordinates found more than three"); } // Throw ParseException for bad GeoJSON @@ -505,5 +505,95 @@ void object::test<31> ensure_equals(features.getFeatures()[8].getId(), ""); } +// Read a point with all-null coordinates should fail +template<> +template<> +void object::test<32> +() +{ + std::string errorMessage; + std::string geojson { "{\"type\":\"Point\",\"coordinates\":[null,null]}" }; + bool error = false; + try { + GeomPtr geom(geojsonreader.read(geojson)); + } catch (geos::io::ParseException& e) { + error = true; + errorMessage = e.what(); + } + ensure(error == true); + ensure_equals(errorMessage, "ParseException: Error parsing JSON: '[json.exception.type_error.302] type must be number, but is null'"); +} + +// Read a GeoJSON Point with three dimensions +template<> +template<> +void object::test<33> +() +{ + std::string geojson { "{\"type\":\"Point\",\"coordinates\":[-117.0,33.0,10.0]}" }; + GeomPtr geom(geojsonreader.read(geojson)); + ensure_equals("POINT Z (-117 33 10)", geom->toText()); + ensure_equals(static_cast(geom->getCoordinateDimension()), 3u); +} + +// Read a GeoJSON MultiPoint with mixed dimensions +template<> +template<> +void object::test<34> +() +{ + std::string geojson { "{\"type\":\"MultiPoint\",\"coordinates\":[[-117.0,33.0,10.0],[-116.0,34.0]]}" }; + GeomPtr geom(geojsonreader.read(geojson)); + ensure_equals("MULTIPOINT Z ((-117 33 10), (-116 34 NaN))", geom->toText()); + ensure_equals(static_cast(geom->getCoordinateDimension()), 3u); +} + +// Read a GeoJSON LineString with three dimensions +template<> +template<> +void object::test<35> +() +{ + std::string geojson { "{\"type\":\"LineString\",\"coordinates\":[[-117, 33, 2], [-116, 34, 4]]}" }; + GeomPtr geom(geojsonreader.read(geojson)); + ensure_equals("LINESTRING Z (-117 33 2, -116 34 4)", geom->toText()); + ensure_equals(static_cast(geom->getCoordinateDimension()), 3u); +} + +// Read a GeoJSON LineString with mixed dimensions +template<> +template<> +void object::test<36> +() +{ + std::string geojson { "{\"type\":\"LineString\",\"coordinates\":[[-117, 33], [-116, 34, 4]]}" }; + GeomPtr geom(geojsonreader.read(geojson)); + ensure_equals("LINESTRING Z (-117 33 NaN, -116 34 4)", geom->toText()); + ensure_equals(static_cast(geom->getCoordinateDimension()), 3u); +} + +// Read a GeoJSON Polygon with three dimensions +template<> +template<> +void object::test<37> +() +{ + std::string geojson { "{\"type\":\"Polygon\",\"coordinates\":[[[30,10,1],[40,40,2],[20,40,4],[10,20,8],[30,10,16]]]}" }; + GeomPtr geom(geojsonreader.read(geojson)); + ensure_equals(geom->toText(), "POLYGON Z ((30 10 1, 40 40 2, 20 40 4, 10 20 8, 30 10 16))"); + ensure_equals(static_cast(geom->getCoordinateDimension()), 3u); +} + +// Read a GeoJSON Polygon with mixed dimensions +template<> +template<> +void object::test<38> +() +{ + std::string geojson { "{\"type\":\"Polygon\",\"coordinates\":[[[30,10],[40,40,2],[20,40],[10,20,8],[30,10]]]}" }; + GeomPtr geom(geojsonreader.read(geojson)); + ensure_equals(geom->toText(), "POLYGON Z ((30 10 NaN, 40 40 2, 20 40 NaN, 10 20 8, 30 10 NaN))"); + ensure_equals(static_cast(geom->getCoordinateDimension()), 3u); } +} diff --git a/tests/unit/io/GeoJSONWriterTest.cpp b/tests/unit/io/GeoJSONWriterTest.cpp index 26a2d85204..53d6e609ad 100644 --- a/tests/unit/io/GeoJSONWriterTest.cpp +++ b/tests/unit/io/GeoJSONWriterTest.cpp @@ -318,5 +318,106 @@ void object::test<20> ensure_equals(result, "{\"type\":\"Point\",\"coordinates\":[null,null]}"); } +// Write a Point Z to GeoJSON +template<> +template<> +void object::test<21> +() +{ + GeomPtr geom(wktreader.read("POINT Z (-117 33 10)")); + std::string result = geojsonwriter.write(geom.get()); + ensure_equals(result, "{\"type\":\"Point\",\"coordinates\":[-117.0,33.0,10.0]}"); +} + +// Write a Point Z with NaN to GeoJSON +template<> +template<> +void object::test<22> +() +{ + GeomPtr geom(wktreader.read("POINT Z (-117 33 NaN)")); + std::string result = geojsonwriter.write(geom.get()); + ensure_equals(result, "{\"type\":\"Point\",\"coordinates\":[-117.0,33.0]}"); +} + +// Write a Point M to GeoJSON ignores M +template<> +template<> +void object::test<23> +() +{ + GeomPtr geom(wktreader.read("POINT M (-117 33 10)")); + std::string result = geojsonwriter.write(geom.get()); + ensure_equals(result, "{\"type\":\"Point\",\"coordinates\":[-117.0,33.0]}"); +} + +// Write a Point ZM to GeoJSON ignores M +template<> +template<> +void object::test<24> +() +{ + GeomPtr geom(wktreader.read("POINT ZM (-117 33 10 2)")); + std::string result = geojsonwriter.write(geom.get()); + ensure_equals(result, "{\"type\":\"Point\",\"coordinates\":[-117.0,33.0,10.0]}"); +} + +// Write a LineString Z to GeoJSON +template<> +template<> +void object::test<25> +() +{ + GeomPtr geom(wktreader.read("LINESTRING Z (102 0 2, 103 1 4, 104 0 8, 105 1 16)")); + std::string result = geojsonwriter.write(geom.get()); + ensure_equals(result, "{\"type\":\"LineString\",\"coordinates\":[[102.0,0.0,2.0],[103.0,1.0,4.0],[104.0,0.0,8.0],[105.0,1.0,16.0]]}"); +} + +// Write a LineString Z with some NaN Z to GeoJSON +template<> +template<> +void object::test<26> +() +{ + GeomPtr geom(wktreader.read("LINESTRING Z (102 0 2, 103 1 NaN, 104 0 8, 105 1 NaN)")); + std::string result = geojsonwriter.write(geom.get()); + ensure_equals(result, "{\"type\":\"LineString\",\"coordinates\":[[102.0,0.0,2.0],[103.0,1.0],[104.0,0.0,8.0],[105.0,1.0]]}"); +} + + +// Setting outputs dimensions to an invalid value should raise +template<> +template<> +void object::test<27> +() +{ + std::string errorMessage; + bool error; + for (auto dims: { uint8_t{1}, uint8_t{4} }) { + errorMessage = ""; + error = false; + try { + geojsonwriter.setOutputDimension(dims); + } catch (geos::util::IllegalArgumentException& e) { + error = true; + errorMessage = e.what(); + } + ensure(error == true); + ensure_equals(errorMessage, "IllegalArgumentException: GeoJSON output dimension must be 2 or 3"); + } +} + + +// GeoJSONWriter without output dimensions set to 2 ignores Z and M values +template<> +template<> +void object::test<28> +() +{ + GeomPtr geom(wktreader.read("POINT ZM (-117 33 10 2)")); + geojsonwriter.setOutputDimension(2); + std::string result = geojsonwriter.write(geom.get()); + ensure_equals(result, "{\"type\":\"Point\",\"coordinates\":[-117.0,33.0]}"); +} }