From 5f454c4bbf49227ee155688a2f728524741e072e Mon Sep 17 00:00:00 2001 From: Paul Asmuth Date: Sat, 1 Feb 2020 22:32:52 +0100 Subject: [PATCH] load GeoJSON polygons and multi polygons --- src/data.cc | 46 +++++++++ src/data.h | 4 + src/graphics/geometry.cc | 21 ++++ src/graphics/geometry.h | 29 ++++++ src/utils/geojson.cc | 206 ++++++++++++++++++++++++++++++++++++--- src/utils/geojson.h | 10 +- 6 files changed, 301 insertions(+), 15 deletions(-) diff --git a/src/data.cc b/src/data.cc index 04e07b8a1..8f7516672 100644 --- a/src/data.cc +++ b/src/data.cc @@ -14,6 +14,7 @@ #include "data.h" #include "utils/fileutil.h" #include "utils/csv.h" +#include "utils/geojson.h" #include "sexpr_conv.h" #include "sexpr_util.h" #include @@ -184,6 +185,51 @@ ReturnCode data_load_strings( return expr_to_strings(expr, values); } +ReturnCode data_load_polys2_geojson( + const Expr* expr, + std::vector* data) { + if (!expr || !expr_is_value(expr)) { + return errorf( + ERROR, + "argument error; expected a filename, got: {}", + expr_inspect(expr)); + } + + const auto& path = expr_get_value(expr); + + GeoJSONReader reader; + reader.on_polygons = [data] (const Poly3* polys, size_t poly_count) { + for (size_t i = 0; i < poly_count; ++i) { + data->emplace_back(poly3_to_poly2(polys[i])); + } + + return OK; + }; + + return geojson_read_file(path, reader); +} + +ReturnCode data_load_polys2( + const Expr* expr, + std::vector* data) { + if (!expr || !expr_is_list(expr) || !expr_get_list(expr)) { + return errorf( + ERROR, + "argument error; expected a list, got: {}", + expr_inspect(expr)); + } + + auto args = expr_get_list(expr); + + if (args && expr_is_value_literal(args, "geojson")) { + return data_load_polys2_geojson(expr_next(args), data); + } + + return err_invalid_value(expr_inspect(expr), { + "geojson" + }); +} + ReturnCode data_load( const Expr* expr, std::vector* values) { diff --git a/src/data.h b/src/data.h index 784b1b02c..d7fae1d13 100644 --- a/src/data.h +++ b/src/data.h @@ -52,6 +52,10 @@ ReturnCode data_load_strings( const Expr* expr, std::vector* values); +ReturnCode data_load_polys2( + const Expr* expr, + std::vector* data); + ReturnCode data_load( const Expr* expr, std::vector* values); diff --git a/src/graphics/geometry.cc b/src/graphics/geometry.cc index 8187ebe4a..15a6882bf 100644 --- a/src/graphics/geometry.cc +++ b/src/graphics/geometry.cc @@ -48,5 +48,26 @@ std::ostream& operator <<(std::ostream& os, const Rectangle& r) { return os; } +PolyLine2 polyline3_to_polyline2(const PolyLine3& p) { + PolyLine2 p2; + + for (const auto& x : p.vertices) { + p2.vertices.emplace_back(x); + } + + return p2; +} + +Poly2 poly3_to_poly2(const Poly3& p) { + Poly2 p2; + p2.boundary = polyline3_to_polyline2(p.boundary); + + for (const auto& x : p.holes) { + p2.holes.emplace_back(polyline3_to_polyline2(x)); + } + + return p2; +} + } // namespace clip diff --git a/src/graphics/geometry.h b/src/graphics/geometry.h index 8d9801617..7e572c12c 100644 --- a/src/graphics/geometry.h +++ b/src/graphics/geometry.h @@ -16,6 +16,7 @@ #include #include #include +#include #include "vmath.h" namespace clip { @@ -33,5 +34,33 @@ struct Rectangle { std::ostream& operator <<(std::ostream& os, const Rectangle& c); +struct PolyLine2 { + std::vector vertices; +}; + +struct PolyLine3 { + std::vector vertices; +}; + +/** + * - The poly lines are assumed to form closed 'rings'. + */ +struct Poly2 { + PolyLine2 boundary; + std::vector holes; +}; + +/** + * - The poly lines are assumed to form closed 'rings'. + */ +struct Poly3 { + PolyLine3 boundary; + std::vector holes; +}; + +PolyLine2 polyline3_to_polyline2(const PolyLine3& p); + +Poly2 poly3_to_poly2(const Poly3& p); + } // namespace clip diff --git a/src/utils/geojson.cc b/src/utils/geojson.cc index 936186d4f..cb56ce8f1 100644 --- a/src/utils/geojson.cc +++ b/src/utils/geojson.cc @@ -14,15 +14,171 @@ #include "geojson.h" #include #include +#include namespace clip { -ReturnCode geojson_read_object(std::istream* input); -ReturnCode geojson_read_objects(std::istream* input); +struct GeoJSONCoord { + double value; + int rlevel; +}; + +ReturnCode geojson_read_object(const GeoJSONReader& reader, std::istream* input); +ReturnCode geojson_read_objects(const GeoJSONReader& reader, std::istream* input); + +ReturnCode geojson_read_coords( + std::istream* input, + std::vector* coords) { + if (auto rc = json_read_array_begin(input); !rc) { + return rc; + } + + for (int level = 0, rlevel = 0; level >= 0; ) { + TokenType token; + std::string token_data; + if (auto rc = json_parse(input, &token, &token_data); !rc) { + return rc; + } + + switch (token) { + case JSON_NUMBER: { + double value; + try { + value = std::stod(token_data); + } catch (...) { + return errorf(ERROR, "invalid number: '{}'", token_data); + } + + coords->emplace_back(GeoJSONCoord { + value: value, + rlevel: rlevel + }); + + rlevel = level; + break; + } + + case JSON_ARRAY_BEGIN: + ++level; + continue; + + case JSON_ARRAY_END: + rlevel = --level; + continue; + + default: + return {ERROR, "coordinates must be (nested) arrays of numbers"}; + } + } + + return OK; +} + +ReturnCode geojson_read_polygon( + const GeoJSONReader& reader, + const std::vector& coords) { + std::vector rings; + for (size_t i = 0; i < coords.size(); i += 2) { + if (coords[i].rlevel == 0) { + rings.emplace_back(); + } + + if (i + 2 > coords.size() || coords[i + 1].rlevel != 2 || rings.empty()) { + return {ERROR, "invalid coordinate format for 'Polygon' objects"}; + } + + if (i + 2 < coords.size() && coords[i + 2].rlevel == 2) { + rings.back().vertices.emplace_back( + coords[i + 0].value, + coords[i + 2].value, + coords[i + 2].value); + } else { + rings.back().vertices.emplace_back( + coords[i + 0].value, + coords[i + 1].value, + 0); + } + } + + if (rings.empty()) { + return {ERROR, "invalid coordinate format for 'Polygon' objects"}; + } + + Poly3 poly; + poly.boundary = rings[0]; + if (rings.size() > 1) { + poly.holes = std::vector(rings.begin() + 1, rings.end()); + } + + if (reader.on_polygons) { + if (auto rc = reader.on_polygons(&poly, 1); !rc) { + return rc; + } + } + + return OK; +} + +ReturnCode geojson_read_multi_polygon( + const GeoJSONReader& reader, + const std::vector& coords) { + std::vector> rings; + for (size_t i = 0; i < coords.size(); i += 2) { + switch (coords[i].rlevel) { + case 0: + rings.emplace_back(); + /* fallthrough */ + case 1: + rings.back().emplace_back(); + break; + } + + if (i + 2 > coords.size() || coords[i + 1].rlevel != 3 || rings.empty()) { + return {ERROR, "invalid coordinate format for 'MultiPolygon' objects"}; + } + + if (i + 2 < coords.size() && coords[i + 2].rlevel == 3) { + rings.back().back().vertices.emplace_back( + coords[i + 0].value, + coords[i + 2].value, + coords[i + 2].value); + } else { + rings.back().back().vertices.emplace_back( + coords[i + 0].value, + coords[i + 1].value, + 0); + } + } + + std::vector polys; + for (const auto& r : rings) { + if (r.empty()) { + return {ERROR, "invalid coordinate format for 'MultiPolygon' objects"}; + } + + Poly3 poly; + poly.boundary = r[0]; + if (r.size() > 1) { + poly.holes = std::vector(r.begin() + 1, r.end()); + } + + polys.emplace_back(std::move(poly)); + } + + if (reader.on_polygons) { + if (auto rc = reader.on_polygons(polys.data(), polys.size()); !rc) { + return rc; + } + } + + return OK; +} ReturnCode geojson_read_object_data( + const GeoJSONReader& reader, std::istream* input) { - std::string obj_type; + std::string type; + std::vector coords; for (TokenType token = JSON_OBJECT_BEGIN; token != JSON_OBJECT_END; ) { std::string token_data; @@ -40,7 +196,7 @@ ReturnCode geojson_read_object_data( } if (token_data == "type") { - if (auto rc = json_read_string(input, &obj_type); !rc) { + if (auto rc = json_read_string(input, &type); !rc) { return rc; } @@ -48,7 +204,7 @@ ReturnCode geojson_read_object_data( } if (token_data == "features") { - if (auto rc = geojson_read_objects(input); !rc) { + if (auto rc = geojson_read_objects(reader, input); !rc) { return rc; } @@ -56,7 +212,15 @@ ReturnCode geojson_read_object_data( } if (token_data == "geometry") { - if (auto rc = geojson_read_object(input); !rc) { + if (auto rc = geojson_read_object(reader, input); !rc) { + return rc; + } + + continue; + } + + if (token_data == "coordinates") { + if (auto rc = geojson_read_coords(input, &coords); !rc) { return rc; } @@ -66,11 +230,25 @@ ReturnCode geojson_read_object_data( json_skip(input); } - std::cerr << "read obj: " << obj_type << std::endl; - return OK; + if (type == "Feature" || + type == "FeatureCollection" || + type == "GeometryCollection") { + return OK; + } + + if (type == "Polygon") { + return geojson_read_polygon(reader, coords); + } + + if (type == "MultiPolygon") { + return geojson_read_multi_polygon(reader, coords); + } + + return errorf(ERROR, "invalid object type: {}", type); } ReturnCode geojson_read_objects( + const GeoJSONReader& reader, std::istream* input) { if (auto rc = json_read_array_begin(input); !rc) { return rc; @@ -84,7 +262,7 @@ ReturnCode geojson_read_objects( switch (token) { case JSON_OBJECT_BEGIN: - if (auto rc = geojson_read_object_data(input); !rc) { + if (auto rc = geojson_read_object_data(reader, input); !rc) { return rc; } @@ -100,26 +278,28 @@ ReturnCode geojson_read_objects( } ReturnCode geojson_read_object( + const GeoJSONReader& reader, std::istream* input) { if (auto rc = json_read_object_begin(input); !rc) { return rc; } - if (auto rc = geojson_read_object_data(input); !rc) { + if (auto rc = geojson_read_object_data(reader, input); !rc) { return rc; } return OK; } -ReturnCode geojson_parse_file( - const std::string& path) { +ReturnCode geojson_read_file( + const std::string& path, + const GeoJSONReader& reader) { std::ifstream input(path, std::ios::binary); if (!input) { return errorf(ERROR, "unable to open file '{}': {}", path, std::strerror(errno)); } - return geojson_read_object(&input); + return geojson_read_object(reader, &input); } } // namespace clip diff --git a/src/utils/geojson.h b/src/utils/geojson.h index 2a818dd16..4e9c59ec4 100644 --- a/src/utils/geojson.h +++ b/src/utils/geojson.h @@ -16,13 +16,19 @@ #include #include "return_code.h" #include "utils/json.h" +#include "graphics/polygon.h" namespace clip { +struct GeoJSONReader { + std::function on_polygons; +}; + // Parse a RFC7946 compliant GEOJSON file // https://tools.ietf.org/html/rfc4180 -ReturnCode geojson_parse_file( - const std::string& path); +ReturnCode geojson_read_file( + const std::string& path, + const GeoJSONReader& reader); } // namespace clip