Skip to content

Commit

Permalink
Add 3D supports to GeoJSON reader and writer (#1150)
Browse files Browse the repository at this point in the history
  • Loading branch information
Oreilles authored Aug 22, 2024
1 parent e802897 commit 0296a92
Show file tree
Hide file tree
Showing 5 changed files with 259 additions and 24 deletions.
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

0 comments on commit 0296a92

Please sign in to comment.