Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add 3D coordinates supports to GeoJSON reader and writer #1150

Merged
merged 1 commit into from
Aug 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions include/geos/io/GeoJSONWriter.h
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,33 @@ class GEOS_DLL GeoJSONWriter {

std::string write(const GeoJSONFeatureCollection& features);

/*
* \brief
* Returns the output dimension used by the
* <code>GeoJSONWriter</code>.
*/
int
getOutputDimension() const
{
return defaultOutputDimension;
}

/*
* Sets the output dimension used by the <code>GeoJSONWriter</code>.
*
* @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<double, double> convertCoordinate(const geom::CoordinateXY* c);
std::vector<double> convertCoordinate(const geom::Coordinate* c);

std::vector<std::pair<double, double>> convertCoordinateSequence(const geom::CoordinateSequence* c);
std::vector<std::vector<double>> convertCoordinateSequence(const geom::CoordinateSequence* c);

void encode(const geom::Geometry* g, GeoJSONType type, geos_nlohmann::ordered_json& j);

Expand Down
22 changes: 14 additions & 8 deletions src/io/GeoJSONReader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -210,13 +210,16 @@ geom::Coordinate GeoJSONReader::readCoordinate(
const std::vector<double>& 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");
}
}

Expand All @@ -225,7 +228,7 @@ std::unique_ptr<geom::Point> GeoJSONReader::readPoint(
{
const auto& coords = j.at("coordinates").get<std::vector<double>>();
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);
Expand All @@ -240,7 +243,8 @@ std::unique_ptr<geom::LineString> GeoJSONReader::readLineString(
const geos_nlohmann::json& j) const
{
const auto& coords = j.at("coordinates").get<std::vector<std::vector<double>>>();
auto coordinates = detail::make_unique<CoordinateSequence>(0u, 2u);
bool has_z = std::any_of(coords.begin(), coords.end(), [](auto v) { return v.size() > 2; });
auto coordinates = detail::make_unique<CoordinateSequence>(0u, has_z, false);
coordinates->reserve(coords.size());
for (const auto& coord : coords) {
const geom::Coordinate& c = readCoordinate(coord);
Expand All @@ -263,7 +267,8 @@ std::unique_ptr<geom::Polygon> GeoJSONReader::readPolygon(
std::vector<std::unique_ptr<geom::LinearRing>> rings;
rings.reserve(polygonCoords.size());
for (const auto& ring : polygonCoords) {
auto coordinates = detail::make_unique<CoordinateSequence>(0u, 2u);
bool has_z = std::any_of(ring.begin(), ring.end(), [](auto v) { return v.size() > 2; });
auto coordinates = detail::make_unique<CoordinateSequence>(0u, has_z, false);
coordinates->reserve(ring.size());
for (const auto& coord : ring) {
const geom::Coordinate& c = readCoordinate(coord);
Expand Down Expand Up @@ -307,7 +312,8 @@ std::unique_ptr<geom::MultiLineString> GeoJSONReader::readMultiLineString(
std::vector<std::unique_ptr<geom::LineString>> lines;
lines.reserve(listOfCoords.size());
for (const auto& coords : listOfCoords) {
auto coordinates = detail::make_unique<geom::CoordinateSequence>(0u, 2u);
bool has_z = std::any_of(coords.begin(), coords.end(), [](auto v) { return v.size() > 2; });
auto coordinates = detail::make_unique<geom::CoordinateSequence>(0u, has_z, false);
coordinates->reserve(coords.size());
for (const auto& coord : coords) {
const geom::Coordinate& c = readCoordinate(coord);
Expand Down
34 changes: 25 additions & 9 deletions src/io/GeoJSONWriter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
#include <ostream>
#include <sstream>
#include <cassert>
#include <cmath>

#include "geos/util.h"

Expand All @@ -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;
Expand Down Expand Up @@ -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();
Expand All @@ -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<std::vector<std::pair<double, double>>> rings;
std::vector<std::vector<std::vector<double>>> rings;
auto ring = poly->getExteriorRing();
rings.reserve(poly->getNumInteriorRing()+1);
rings.push_back(convertCoordinateSequence(ring->getCoordinates().get()));
Expand All @@ -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<std::vector<std::pair<double, double>>> lines;
std::vector<std::vector<std::vector<double>>> lines;
lines.reserve(multiLineString->getNumGeometries());
for (size_t i = 0; i < multiLineString->getNumGeometries(); i++) {
lines.push_back(convertCoordinateSequence(multiLineString->getGeometryN(i)->getCoordinates().get()));
Expand All @@ -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<std::vector<std::vector<std::pair<double, double>>>> polygons;
std::vector<std::vector<std::vector<std::vector<double>>>> polygons;
polygons.reserve(multiPolygon->getNumGeometries());
for (size_t i = 0; i < multiPolygon->getNumGeometries(); i++) {
const Polygon* polygon = multiPolygon->getGeometryN(i);
std::vector<std::vector<std::pair<double, double>>> rings;
std::vector<std::vector<std::vector<double>>> rings;
auto ring = polygon->getExteriorRing();
rings.reserve(polygon->getNumInteriorRing() + 1);
rings.push_back(convertCoordinateSequence(ring->getCoordinates().get()));
Expand All @@ -291,15 +304,18 @@ void GeoJSONWriter::encodeGeometryCollection(const geom::GeometryCollection* g,
j["geometries"] = geometryArray;
}

std::pair<double, double> GeoJSONWriter::convertCoordinate(const CoordinateXY* c)
std::vector<double> GeoJSONWriter::convertCoordinate(const Coordinate* c)
{
return std::make_pair(c->x, c->y);
if (std::isnan(c->z) || defaultOutputDimension == 2) {
return std::vector<double> { c->x, c->y };
}
return std::vector<double> { c->x, c->y, c->z };
}

std::vector<std::pair<double, double>> GeoJSONWriter::convertCoordinateSequence(const CoordinateSequence*
std::vector<std::vector<double>> GeoJSONWriter::convertCoordinateSequence(const CoordinateSequence*
coordinateSequence)
{
std::vector<std::pair<double, double>> coordinates;
std::vector<std::vector<double>> coordinates;
coordinates.reserve(coordinateSequence->size());
for (size_t i = 0; i<coordinateSequence->size(); i++) {
const geom::Coordinate& c = coordinateSequence->getAt(i);
Expand Down
100 changes: 95 additions & 5 deletions tests/unit/io/GeoJSONReaderTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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));
Expand All @@ -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
Expand Down Expand Up @@ -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<size_t>(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<size_t>(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<size_t>(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<size_t>(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<size_t>(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<size_t>(geom->getCoordinateDimension()), 3u);
}

}
Loading