From ba0f65279d3e0ae14cfa3c9509b78e3a7da4d228 Mon Sep 17 00:00:00 2001 From: Yvee1 Date: Fri, 10 Apr 2026 16:46:01 +0200 Subject: [PATCH 01/17] Add PolygonSetRaw --- cartocrow/core/CMakeLists.txt | 2 + cartocrow/core/polygon_set_raw.cpp | 19 +++++++++ cartocrow/core/polygon_set_raw.h | 24 +++++++++++ cartocrow/reader/gdal_conversion.cpp | 63 +++++++++++----------------- cartocrow/reader/gdal_conversion.h | 14 ++++--- 5 files changed, 77 insertions(+), 45 deletions(-) create mode 100644 cartocrow/core/polygon_set_raw.cpp create mode 100644 cartocrow/core/polygon_set_raw.h diff --git a/cartocrow/core/CMakeLists.txt b/cartocrow/core/CMakeLists.txt index c741a27c..9851b373 100644 --- a/cartocrow/core/CMakeLists.txt +++ b/cartocrow/core/CMakeLists.txt @@ -10,6 +10,7 @@ set(SOURCES polyline_set.cpp segment_delaunay_graph_helpers.cpp stopwatch.cpp + polygon_set_raw.cpp ) set(HEADERS core.h @@ -35,6 +36,7 @@ set(HEADERS polyline_set.h stopwatch.h polygon_helpers.h + polygon_set_raw.h ) add_library(core ${SOURCES}) diff --git a/cartocrow/core/polygon_set_raw.cpp b/cartocrow/core/polygon_set_raw.cpp new file mode 100644 index 00000000..0f7c98a9 --- /dev/null +++ b/cartocrow/core/polygon_set_raw.cpp @@ -0,0 +1,19 @@ +#include "polygon_set_raw.h" + +namespace cartocrow { +PolygonSetRaw approximate(const PolygonSetRaw& pgs) { + PolygonSetRaw approximated; + for (const PolygonWithHoles& pgn : pgs.polygons_with_holes) { + approximated.polygons_with_holes.push_back(cartocrow::approximate(pgn)); + } + return approximated; +} + +PolygonSetRaw pretendExact(const PolygonSetRaw& pgs) { + PolygonSetRaw exact; + for (const auto& pgn : pgs.polygons_with_holes) { + exact.polygons_with_holes.push_back(cartocrow::pretendExact(pgn)); + } + return exact; +} +} \ No newline at end of file diff --git a/cartocrow/core/polygon_set_raw.h b/cartocrow/core/polygon_set_raw.h new file mode 100644 index 00000000..a9e5c433 --- /dev/null +++ b/cartocrow/core/polygon_set_raw.h @@ -0,0 +1,24 @@ +#include +#include + +namespace cartocrow { +template +struct PolygonSetRaw { + std::vector> polygons_with_holes; + + PolygonSetRaw transform(const CGAL::Aff_transformation_2& trans) const { + PolygonSetRaw transformed; + for (const auto& pgn : polygons_with_holes) { + transformed.polygons_with_holes.push_back(transform(trans, pgn)); + } + return transformed; + } + + Box bbox() const { + return CGAL::bbox_2(polygons_with_holes.begin(), polygons_with_holes.end()); + } +}; + +PolygonSetRaw approximate(const PolygonSetRaw& pgs); +PolygonSetRaw pretendExact(const PolygonSetRaw& pgs); +} \ No newline at end of file diff --git a/cartocrow/reader/gdal_conversion.cpp b/cartocrow/reader/gdal_conversion.cpp index b1f980d1..12bdbdf6 100644 --- a/cartocrow/reader/gdal_conversion.cpp +++ b/cartocrow/reader/gdal_conversion.cpp @@ -18,22 +18,17 @@ along with this program. If not, see . #include "gdal_conversion.h" namespace cartocrow { -PolygonSet ogrMultiPolygonToPolygonSet(const OGRMultiPolygon& multiPolygon) { - PolygonSet polygonSet; - for (auto& poly: multiPolygon) { - for (auto& linearRing: *poly) { - auto polygon = ogrLinearRingToPolygon(*linearRing); - if (polygon.is_clockwise_oriented()) { - polygon.reverse_orientation(); - } - polygonSet.symmetric_difference(polygon); - } +PolygonSetRaw ogrMultiPolygonToPolygonSetRaw(const OGRMultiPolygon& multiPolygon) { + PolygonSetRaw polygonSet; + for (const auto& poly : multiPolygon) { + auto pgnWH = ogrPolygonToPolygonWithHoles(*poly); + polygonSet.polygons_with_holes.push_back(pgnWH); } return polygonSet; } -Polygon ogrLinearRingToPolygon(const OGRLinearRing& ogrLinearRing) { - Polygon polygon; +Polygon ogrLinearRingToPolygon(const OGRLinearRing& ogrLinearRing) { + Polygon polygon; for (auto &pt: ogrLinearRing) { polygon.push_back({pt.getX(), pt.getY()}); } @@ -44,44 +39,34 @@ Polygon ogrLinearRingToPolygon(const OGRLinearRing& ogrLinearRing) { return polygon; } -PolygonSet ogrPolygonToPolygonSet(const OGRPolygon& ogrPolygon) { - PolygonSet polygonSet; - for (auto& linearRing : ogrPolygon) { - Polygon polygon; - for (auto& pt : *linearRing) { - polygon.push_back({pt.getX(), pt.getY()}); - } - // if the begin and end vertices are equal, remove one of them - if (polygon.container().front() == polygon.container().back()) { - polygon.container().pop_back(); - } - if (polygon.is_clockwise_oriented()) { - polygon.reverse_orientation(); - } - polygonSet.symmetric_difference(polygon); - } +PolygonSetRaw ogrPolygonToPolygonSetRaw(const OGRPolygon& ogrPolygon) { + PolygonSetRaw polygonSet; + auto polygon = ogrPolygonToPolygonWithHoles(ogrPolygon); + polygonSet.polygons_with_holes.emplace_back(polygon); return polygonSet; } -PolygonWithHoles ogrPolygonToPolygonWithHoles(const OGRPolygon& ogrPolygon) { - std::vector> pgns; - ogrPolygonToPolygonSet(ogrPolygon).polygons_with_holes(std::back_inserter(pgns)); - assert(pgns.size() == 1); - return pgns.front(); +PolygonWithHoles ogrPolygonToPolygonWithHoles(const OGRPolygon& ogrPolygon) { + auto outer = ogrLinearRingToPolygon(*ogrPolygon.getExteriorRing()); + std::vector> holes; + for (int i = 0; i < ogrPolygon.getNumInteriorRings(); ++i) { + holes.push_back(ogrLinearRingToPolygon(*ogrPolygon.getInteriorRing(i))); + } + return {outer, holes.begin(), holes.end()}; } -std::vector> ogrMultiLineStringToMultiPolyline(const OGRMultiLineString& ogrMultiLineString) { - std::vector> pls; +PolylineSet ogrMultiLineStringToPolylineSet(const OGRMultiLineString& ogrMultiLineString) { + PolylineSet polylineSet; for (const auto& lineString : ogrMultiLineString) { - pls.push_back(ogrLineStringToPolyline(*lineString)); + polylineSet.polylines.push_back(ogrLineStringToPolyline(*lineString)); } - return pls; + return polylineSet; } -Polyline ogrLineStringToPolyline(const OGRLineString& ogrLineString) { - Polyline pl; +Polyline ogrLineStringToPolyline(const OGRLineString& ogrLineString) { + Polyline pl; for (const auto& pt : ogrLineString) { pl.push_back({pt.getX(), pt.getY()}); diff --git a/cartocrow/reader/gdal_conversion.h b/cartocrow/reader/gdal_conversion.h index dda667fa..21b3aa57 100644 --- a/cartocrow/reader/gdal_conversion.h +++ b/cartocrow/reader/gdal_conversion.h @@ -20,14 +20,16 @@ along with this program. If not, see . #include #include "cartocrow/core/core.h" #include "cartocrow/core/polyline.h" +#include "cartocrow/core/polyline_set.h" +#include "cartocrow/core/polygon_set_raw.h" namespace cartocrow { -PolygonSet ogrMultiPolygonToPolygonSet(const OGRMultiPolygon& multiPolygon); -PolygonSet ogrPolygonToPolygonSet(const OGRPolygon& ogrPolygon); -Polygon ogrLinearRingToPolygon(const OGRLinearRing& ogrLinearRing); -std::vector> ogrMultiLineStringToMultiPolyline(const OGRMultiLineString& ogrMultiLineString); -Polyline ogrLineStringToPolyline(const OGRLineString& ogrLineString); -PolygonWithHoles ogrPolygonToPolygonWithHoles(const OGRPolygon& ogrPolygon); +PolygonSetRaw ogrMultiPolygonToPolygonSetRaw(const OGRMultiPolygon& multiPolygon); +PolygonSetRaw ogrPolygonToPolygonSetRaw(const OGRPolygon& ogrPolygon); +Polygon ogrLinearRingToPolygon(const OGRLinearRing& ogrLinearRing); +PolylineSet ogrMultiLineStringToPolylineSet(const OGRMultiLineString& ogrMultiLineString); +Polyline ogrLineStringToPolyline(const OGRLineString& ogrLineString); +PolygonWithHoles ogrPolygonToPolygonWithHoles(const OGRPolygon& ogrPolygon); OGRLinearRing polygonToOGRLinearRing(const Polygon& polygon); OGRPolygon polygonWithHolesToOGRPolygon(const PolygonWithHoles& polygon); OGRMultiPolygon polygonSetToOGRMultiPolygon(const PolygonSet& polygonSet); From 044efbb71de3d23272b2433bc704dace8f83e9fe Mon Sep 17 00:00:00 2001 From: Yvee1 Date: Mon, 13 Apr 2026 08:11:53 +0200 Subject: [PATCH 02/17] PolygonSetRaw: add draw and convert to PolygonSet, fix transform --- cartocrow/core/polygon_set_raw.h | 12 +++++++++++- cartocrow/renderer/geometry_renderer.cpp | 8 ++++++++ cartocrow/renderer/geometry_renderer.h | 3 +++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/cartocrow/core/polygon_set_raw.h b/cartocrow/core/polygon_set_raw.h index a9e5c433..013c5fc7 100644 --- a/cartocrow/core/polygon_set_raw.h +++ b/cartocrow/core/polygon_set_raw.h @@ -1,3 +1,5 @@ +#pragma once + #include #include @@ -9,7 +11,7 @@ struct PolygonSetRaw { PolygonSetRaw transform(const CGAL::Aff_transformation_2& trans) const { PolygonSetRaw transformed; for (const auto& pgn : polygons_with_holes) { - transformed.polygons_with_holes.push_back(transform(trans, pgn)); + transformed.polygons_with_holes.push_back(cartocrow::transform(trans, pgn)); } return transformed; } @@ -17,6 +19,14 @@ struct PolygonSetRaw { Box bbox() const { return CGAL::bbox_2(polygons_with_holes.begin(), polygons_with_holes.end()); } + + PolygonSet polygonSet() const { + PolygonSet polygonSet; + for (const auto& pgn : polygons_with_holes) { + polygonSet.join(pgn); + } + return polygonSet; + } }; PolygonSetRaw approximate(const PolygonSetRaw& pgs); diff --git a/cartocrow/renderer/geometry_renderer.cpp b/cartocrow/renderer/geometry_renderer.cpp index 7b560d13..e9433187 100644 --- a/cartocrow/renderer/geometry_renderer.cpp +++ b/cartocrow/renderer/geometry_renderer.cpp @@ -103,6 +103,14 @@ void GeometryRenderer::draw(const PolygonSet& ps) { draw(path); } +void GeometryRenderer::draw(const PolygonSetRaw& ps) { + RenderPath path; + for (const auto& p : ps.polygons_with_holes) { + path << p; + } + draw(path); +} + void GeometryRenderer::draw(const CubicBezierCurve& c) { CubicBezierSpline spline; spline.appendCurve(c); diff --git a/cartocrow/renderer/geometry_renderer.h b/cartocrow/renderer/geometry_renderer.h index a35ea135..74489628 100644 --- a/cartocrow/renderer/geometry_renderer.h +++ b/cartocrow/renderer/geometry_renderer.h @@ -20,6 +20,7 @@ along with this program. If not, see . #include "../core/core.h" #include "../core/cubic_bezier.h" #include "../core/ellipse.h" +#include "../core/polygon_set_raw.h" #include "../core/polyline.h" #include "../core/polyline_set.h" #include "../core/halfplane.h" @@ -131,6 +132,8 @@ class GeometryRenderer { void draw(const PolygonWithHoles& p); /// Draws a polygon set with the currently set style. void draw(const PolygonSet& p); + /// Draws a polygon set with the currently set style. + void draw(const PolygonSetRaw& p); /// Draws a circle with the currently set style. virtual void draw(const Circle& c) = 0; /// Draws an ellipse with the currently set style. From affb0f688d0713b0c20a38f3c4f927413203058d Mon Sep 17 00:00:00 2001 From: Yvee1 Date: Wed, 15 Apr 2026 08:37:44 +0200 Subject: [PATCH 03/17] Ensure correct orientation of polygons when reading from GDAL --- cartocrow/reader/gdal_conversion.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cartocrow/reader/gdal_conversion.cpp b/cartocrow/reader/gdal_conversion.cpp index 12bdbdf6..413fa745 100644 --- a/cartocrow/reader/gdal_conversion.cpp +++ b/cartocrow/reader/gdal_conversion.cpp @@ -48,9 +48,15 @@ PolygonSetRaw ogrPolygonToPolygonSetRaw(const OGRPolygon& ogrPolygon) { PolygonWithHoles ogrPolygonToPolygonWithHoles(const OGRPolygon& ogrPolygon) { auto outer = ogrLinearRingToPolygon(*ogrPolygon.getExteriorRing()); + if (!outer.is_counterclockwise_oriented()) { + outer.reverse_orientation(); + } std::vector> holes; for (int i = 0; i < ogrPolygon.getNumInteriorRings(); ++i) { holes.push_back(ogrLinearRingToPolygon(*ogrPolygon.getInteriorRing(i))); + if (!holes.back().is_clockwise_oriented()) { + holes.back().reverse_orientation(); + } } return {outer, holes.begin(), holes.end()}; } From 544aa847f52f4ee8844330f27dd8273e1a51573d Mon Sep 17 00:00:00 2001 From: Yvee1 Date: Wed, 22 Apr 2026 17:45:51 +0200 Subject: [PATCH 04/17] Add PointSet --- cartocrow/core/CMakeLists.txt | 2 ++ cartocrow/core/point_set.cpp | 13 +++++++++++++ cartocrow/core/point_set.h | 24 ++++++++++++++++++++++++ cartocrow/reader/gdal_conversion.cpp | 12 ++++++++++++ cartocrow/reader/gdal_conversion.h | 3 +++ cartocrow/renderer/geometry_renderer.cpp | 6 ++++++ cartocrow/renderer/geometry_renderer.h | 3 +++ 7 files changed, 63 insertions(+) create mode 100644 cartocrow/core/point_set.cpp create mode 100644 cartocrow/core/point_set.h diff --git a/cartocrow/core/CMakeLists.txt b/cartocrow/core/CMakeLists.txt index 9851b373..26089cd3 100644 --- a/cartocrow/core/CMakeLists.txt +++ b/cartocrow/core/CMakeLists.txt @@ -11,6 +11,7 @@ set(SOURCES segment_delaunay_graph_helpers.cpp stopwatch.cpp polygon_set_raw.cpp + point_set.cpp ) set(HEADERS core.h @@ -37,6 +38,7 @@ set(HEADERS stopwatch.h polygon_helpers.h polygon_set_raw.h + point_set.h ) add_library(core ${SOURCES}) diff --git a/cartocrow/core/point_set.cpp b/cartocrow/core/point_set.cpp new file mode 100644 index 00000000..8efff828 --- /dev/null +++ b/cartocrow/core/point_set.cpp @@ -0,0 +1,13 @@ +#include "point_set.h" + +namespace cartocrow { +PointSet approximate(const PointSet& ps) { + std::vector> psInexact; + + for (const auto& p : ps.points) { + psInexact.push_back(approximate(p)); + } + + return {psInexact}; +} +} diff --git a/cartocrow/core/point_set.h b/cartocrow/core/point_set.h new file mode 100644 index 00000000..2093d64b --- /dev/null +++ b/cartocrow/core/point_set.h @@ -0,0 +1,24 @@ +#pragma once + +#include "core.h" + +namespace cartocrow { +template +struct PointSet { + std::vector> points; + + PointSet transform(const CGAL::Aff_transformation_2& trans) const { + PointSet transformed; + for (const auto& p : points) { + transformed.points.push_back(p.transform(trans)); + } + return transformed; + } + + Box bbox() const { + return CGAL::bbox_2(points.begin(), points.end()); + } +}; + +PointSet approximate(const PointSet& ps); +} // namespace cartocrow diff --git a/cartocrow/reader/gdal_conversion.cpp b/cartocrow/reader/gdal_conversion.cpp index 413fa745..c4724e12 100644 --- a/cartocrow/reader/gdal_conversion.cpp +++ b/cartocrow/reader/gdal_conversion.cpp @@ -81,6 +81,18 @@ Polyline ogrLineStringToPolyline(const OGRLineString& ogrLineString) { return pl; } +Point ogrPointToPoint(const OGRPoint& ogrPoint) { + return {ogrPoint.getX(), ogrPoint.getY()}; +} + +PointSet ogrMultiPointToPointSet(const OGRMultiPoint& ogrMultiPoint) { + std::vector> pts; + for (const OGRPoint* p : ogrMultiPoint) { + pts.emplace_back(p->getX(), p->getY()); + } + return {pts}; +} + OGRLinearRing polygonToOGRLinearRing(const Polygon& polygon) { OGRLinearRing ring; for (const auto& v : polygon.vertices()) { diff --git a/cartocrow/reader/gdal_conversion.h b/cartocrow/reader/gdal_conversion.h index 21b3aa57..bc5526bc 100644 --- a/cartocrow/reader/gdal_conversion.h +++ b/cartocrow/reader/gdal_conversion.h @@ -22,6 +22,7 @@ along with this program. If not, see . #include "cartocrow/core/polyline.h" #include "cartocrow/core/polyline_set.h" #include "cartocrow/core/polygon_set_raw.h" +#include "cartocrow/core/point_set.h" namespace cartocrow { PolygonSetRaw ogrMultiPolygonToPolygonSetRaw(const OGRMultiPolygon& multiPolygon); @@ -30,6 +31,8 @@ Polygon ogrLinearRingToPolygon(const OGRLinearRing& ogrLinearRing); PolylineSet ogrMultiLineStringToPolylineSet(const OGRMultiLineString& ogrMultiLineString); Polyline ogrLineStringToPolyline(const OGRLineString& ogrLineString); PolygonWithHoles ogrPolygonToPolygonWithHoles(const OGRPolygon& ogrPolygon); +PointSet ogrMultiPointToPointSet(const OGRMultiPoint& ogrMultiPoint); +Point ogrPointToPoint(const OGRPoint& ogrPoint); OGRLinearRing polygonToOGRLinearRing(const Polygon& polygon); OGRPolygon polygonWithHolesToOGRPolygon(const PolygonWithHoles& polygon); OGRMultiPolygon polygonSetToOGRMultiPolygon(const PolygonSet& polygonSet); diff --git a/cartocrow/renderer/geometry_renderer.cpp b/cartocrow/renderer/geometry_renderer.cpp index e9433187..e499c3a4 100644 --- a/cartocrow/renderer/geometry_renderer.cpp +++ b/cartocrow/renderer/geometry_renderer.cpp @@ -87,6 +87,12 @@ void GeometryRenderer::draw(const PolylineSet& ps) { draw(path); } +void GeometryRenderer::draw(const PointSet& ps) { + for (const auto& p : ps.points) { + draw(p); + } +} + void GeometryRenderer::draw(const PolygonWithHoles& p) { RenderPath path; path << p; diff --git a/cartocrow/renderer/geometry_renderer.h b/cartocrow/renderer/geometry_renderer.h index 74489628..87720ccf 100644 --- a/cartocrow/renderer/geometry_renderer.h +++ b/cartocrow/renderer/geometry_renderer.h @@ -21,6 +21,7 @@ along with this program. If not, see . #include "../core/cubic_bezier.h" #include "../core/ellipse.h" #include "../core/polygon_set_raw.h" +#include "../core/point_set.h" #include "../core/polyline.h" #include "../core/polyline_set.h" #include "../core/halfplane.h" @@ -114,6 +115,8 @@ class GeometryRenderer { /// Draws a single point with the currently set style. virtual void draw(const Point& p) = 0; + /// Draws a point set with the currently set style. + void draw(const PointSet& ps); /// Draws a single line segment with the currently set style. void draw(const Segment& s); /// Draws a rectangle From 1b818fcfac451cd4a51459c1ef4a69e846c9223b Mon Sep 17 00:00:00 2001 From: Yvee1 Date: Fri, 26 Jun 2026 18:17:04 +0200 Subject: [PATCH 05/17] Start on new readers. Implemented first version of ipe reader. Ipe reader has not been tested thoroughly --- cartocrow/core/geometric_feature.h | 32 ++ cartocrow/core/straight_geometry.h | 31 ++ cartocrow/reader/CMakeLists.txt | 3 - cartocrow/reader/geometry_reader.h | 51 ++++ cartocrow/reader/ipe_reader.cpp | 201 ------------- cartocrow/reader/ipe_reader.h | 424 ++++++++++++++++++++++++-- data/test_ipe_reader.ipe | 461 +++++++++++++++++++++++++++++ test/CMakeLists.txt | 3 +- test/reader/ipe_reader.cpp | 48 +++ 9 files changed, 1019 insertions(+), 235 deletions(-) create mode 100644 cartocrow/core/geometric_feature.h create mode 100644 cartocrow/core/straight_geometry.h create mode 100644 cartocrow/reader/geometry_reader.h delete mode 100644 cartocrow/reader/ipe_reader.cpp create mode 100644 data/test_ipe_reader.ipe create mode 100644 test/reader/ipe_reader.cpp diff --git a/cartocrow/core/geometric_feature.h b/cartocrow/core/geometric_feature.h new file mode 100644 index 00000000..451e679c --- /dev/null +++ b/cartocrow/core/geometric_feature.h @@ -0,0 +1,32 @@ +/* +Copyright (C) 2026 TU Eindhoven + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +#pragma once + +#include "core.h" + +namespace cartocrow { +using GeometryAttribute = std::variant, double, std::vector, + std::string, std::vector, int64_t>; + +using GeometryAttributes = std::unordered_map; + +template struct GeometricFeature { + Geometry geometry; + GeometryAttributes attributes; +} +} \ No newline at end of file diff --git a/cartocrow/core/straight_geometry.h b/cartocrow/core/straight_geometry.h new file mode 100644 index 00000000..a871834e --- /dev/null +++ b/cartocrow/core/straight_geometry.h @@ -0,0 +1,31 @@ +/* +Copyright (C) 2026 TU Eindhoven + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +#pragma once + +#include "polyline.h" +#include "polyline_set.h" +#include "point_set.h" +#include "polygon_set_raw.h" + +namespace cartocrow { +/// Type that serves as an internal representation of the straight (i.e. linear) features of the OGC Simple Feature Access. +/// OGC name: MultiPolygon Polygon LinearRing MultiLineString LineString Point MultiPoint +template +using StraightGeometry = std::variant, PolygonWithHoles, Polygon, + PolylineSet, Polyline, Point, PointSet>; +} \ No newline at end of file diff --git a/cartocrow/reader/CMakeLists.txt b/cartocrow/reader/CMakeLists.txt index fabf72fe..4e7ff4d5 100644 --- a/cartocrow/reader/CMakeLists.txt +++ b/cartocrow/reader/CMakeLists.txt @@ -1,14 +1,11 @@ set(SOURCES - ipe_reader.cpp gdal_conversion.cpp boundary_map_reader.cpp - region_map_reader.cpp ) set(HEADERS ipe_reader.h gdal_conversion.h boundary_map_reader.h - region_map_reader.h ) add_library(reader ${SOURCES}) diff --git a/cartocrow/reader/geometry_reader.h b/cartocrow/reader/geometry_reader.h new file mode 100644 index 00000000..48523c97 --- /dev/null +++ b/cartocrow/reader/geometry_reader.h @@ -0,0 +1,51 @@ +/* +Copyright (C) 2026 TU Eindhoven + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +#pragma once + +#include +#include +#include + +namespace cartocrow { +template concept GeometryReader = requires(R reader, std::filesystem::path path) { + /// If it exists, return the well-known text representation (WKT) of the coordinate reference system + {reader.readSpatialReference(path)}->std::same_as>; + + /// Returns whether the reader can parse the given file. + {reader.canRead(path)}->std::same_as; +}; + +//GeometryReader R +template +concept GeometryReaderFor = + GeometryReader && requires(R reader, std::filesystem::path path, OutputIterator out) { + + /// Returns all geometries in the provided file that are convertible to Geometry. + /// precondition: canRead(path) + reader.template read(path, out); + + /// Returns the first geometry in the provided file that is convertible to Geometry. + /// precondition: canRead(path) + {reader.template readSingle(path)}->std::same_as>; + + /// Returns geometries in the provided file that are convertible to Geometry including their attributes. + /// Outputs Feature. + /// precondition: canRead(path) + reader.template readWithAttributes(path, out); +}; +} \ No newline at end of file diff --git a/cartocrow/reader/ipe_reader.cpp b/cartocrow/reader/ipe_reader.cpp deleted file mode 100644 index 50d0b42e..00000000 --- a/cartocrow/reader/ipe_reader.cpp +++ /dev/null @@ -1,201 +0,0 @@ -/* -Copyright (C) 2026 TU Eindhoven - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -*/ - -#include "ipe_reader.h" - -#include "cartocrow/core/cubic_bezier.h" - -#include - -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -using namespace cartocrow::renderer; - -namespace cartocrow { - -std::shared_ptr IpeReader::loadIpeFile(const std::filesystem::path& filename) { - std::fstream fin(filename); - std::string input; - if (fin) { - using Iterator = std::istreambuf_iterator; - input.assign(Iterator(fin), Iterator()); - } - - ipe::Platform::initLib(ipe::IPELIB_VERSION); - int load_reason = 0; - ipe::Buffer buffer(input.c_str(), input.size()); - ipe::BufferSource bufferSource(buffer); - ipe::FileFormat format = ipe::Document::fileFormat(bufferSource); - ipe::Document* document = ipe::Document::load(bufferSource, format, load_reason); - - if (load_reason > 0) { - throw std::runtime_error("Unable to load Ipe file: parse error at position " + - std::to_string(load_reason)); - } else if (load_reason == ipe::Document::EVersionTooOld) { - throw std::runtime_error("Unable to load Ipe file: the version of the file is too old"); - } else if (load_reason == ipe::Document::EVersionTooRecent) { - throw std::runtime_error("Unable to load Ipe file: the file version is newer than Ipelib"); - } else if (load_reason == ipe::Document::EFileOpenError) { - throw std::runtime_error("Unable to load Ipe file: error opening the file"); - } else if (load_reason == ipe::Document::ENotAnIpeFile) { - throw std::runtime_error( - "Unable to load Ipe file: the file does not exist or was not created by Ipe"); - } - - return std::shared_ptr(document); -} - -Color IpeReader::convertIpeColor(ipe::Color color) { - return Color{static_cast(color.iRed.toDouble() * 255), - static_cast(color.iGreen.toDouble() * 255), - static_cast(color.iBlue.toDouble() * 255)}; -} - -PolygonSet IpeReader::convertShapeToPolygonSet(const ipe::Shape& shape, - const ipe::Matrix& matrix) { - PolygonSet set; - for (int i = 0; i < shape.countSubPaths(); ++i) { - Polygon polygon; - if (shape.subPath(i)->type() != ipe::SubPath::ECurve) { - throw std::runtime_error("Encountered shape with a non-polygonal boundary"); - } - const ipe::Curve* curve = shape.subPath(i)->asCurve(); - for (int j = 0; j < curve->countSegments(); ++j) { - ipe::CurveSegment segment = curve->segment(j); - if (segment.type() != ipe::CurveSegment::ESegment) { - throw std::runtime_error("Encountered shape with a non-polygonal boundary"); - } - if (j == 0) { - ipe::Vector v = matrix * segment.cp(0); - polygon.push_back(Point(v.x, v.y)); - } - ipe::Vector v = matrix * segment.last(); - Point p(v.x, v.y); - if (p != polygon.container().back()) { - polygon.push_back(Point(v.x, v.y)); - } - } - // if the begin and end vertices are equal, remove one of them - if (polygon.container().front() == polygon.container().back()) { - polygon.container().pop_back(); - } - if (!polygon.is_simple()) { -// std::cerr << "Encountered non-simple polygon" << std::endl; -// continue; - for (const auto v : polygon.vertices()) { - std::cout << v << std::endl; - } - throw std::runtime_error("Encountered non-simple polygon"); - } - if (polygon.is_clockwise_oriented()) { - polygon.reverse_orientation(); - } - set.symmetric_difference(PolygonWithHoles(polygon)); - } - return set; -} - -CubicBezierSpline IpeReader::convertPathToSpline(const ipe::SubPath& path, const ipe::Matrix& matrix) { - CubicBezierSpline spline; - if (path.type() == ipe::SubPath::EClosedSpline) { - std::vector beziers; - path.asClosedSpline()->beziers(beziers); - for (auto bezier : beziers) { - spline.appendCurve( - Point(bezier.iV[0].x, bezier.iV[0].y), Point(bezier.iV[1].x, bezier.iV[1].y), - Point(bezier.iV[2].x, bezier.iV[2].y), Point(bezier.iV[3].x, bezier.iV[3].y)); - } - } else { - throw std::runtime_error("Only closed splines are supported for spline conversion"); - } - return spline; -} - -RenderPath IpeReader::convertShapeToRenderPath(const ipe::Shape& shape, const ipe::Matrix& matrix) { - RenderPath renderPath; - for (int i = 0; i < shape.countSubPaths(); ++i) { - if (shape.subPath(i)->type() != ipe::SubPath::ECurve) { - throw std::runtime_error("Encountered closed ellipse or B-spline; unimplemented"); - } - const ipe::Curve* curve = shape.subPath(i)->asCurve(); - for (int j = 0; j < curve->countSegments(); ++j) { - ipe::CurveSegment segment = curve->segment(j); - Point last; - if (segment.type() == ipe::CurveSegment::ESegment || segment.type() == ipe::CurveSegment::EArc) { - if (j == 0) { - ipe::Vector v = matrix * segment.cp(0); - Point pt(v.x, v.y); - last = pt; - renderPath.moveTo(pt); - } - ipe::Vector v = matrix * segment.last(); - Point pt(v.x, v.y); - if (pt != last) { - last = pt; - if (segment.type() == ipe::CurveSegment::ESegment) { - renderPath.lineTo(pt); - } else { - auto m = segment.matrix(); - auto clockwise = m.a[3] < 0; - auto center = matrix * ipe::Vector(m.a[4], m.a[5]); - renderPath.arcTo({center.x, center.y}, clockwise, pt); - } - } - } - } - - renderPath.close(); - } - return renderPath; -} - -RenderPath IpeReader::loadIpePath(const std::filesystem::path& ipeFile) { - std::shared_ptr document = IpeReader::loadIpeFile(ipeFile); - - if (document->countPages() == 0) { - throw std::runtime_error("Cannot read map from an Ipe file with no pages"); - } else if (document->countPages() > 1) { - throw std::runtime_error("Cannot read map from an Ipe file with more than one page"); - } - - ipe::Page* page = document->page(0); - - for (int i = 0; i < page->count(); ++i) { - ipe::Object* object = page->object(i); - ipe::Object::Type type = object->type(); - if (type != ipe::Object::Type::EPath) { - continue; - } - ipe::Path* path = object->asPath(); - ipe::Matrix matrix = path->matrix(); - ipe::Shape ipeShape = path->shape(); - auto renderPath = convertShapeToRenderPath(ipeShape, matrix); - return renderPath; - } - - throw std::runtime_error("Could not find a path in the ipe file"); -} -} // namespace cartocrow diff --git a/cartocrow/reader/ipe_reader.h b/cartocrow/reader/ipe_reader.h index cd5866cf..9b12e14a 100644 --- a/cartocrow/reader/ipe_reader.h +++ b/cartocrow/reader/ipe_reader.h @@ -17,42 +17,408 @@ along with this program. If not, see . #pragma once -#include -#include - -#include "cartocrow/renderer/render_path.h" - -#include "cartocrow/core/core.h" -#include "cartocrow/core/cubic_bezier.h" +#include "geometry_reader.h" +#include "../core/core.h" +#include "../core/ellipse.h" +#include "../core/polyline.h" +#include "../core/polyline_set.h" +#include "../core/point_set.h" +#include "../core/polygon_set_raw.h" +#include "../core/cubic_bezier.h" #include #include +#include #include +#include +#include +#include +#include namespace cartocrow { +template +concept IpeReaderTraits = requires(ipe::Object& o, OutputIterator out) { + { Traits::template convert(o, out) }; +}; + +// todo: make a RenderPath reader traits that parses everything to a render path? + +using IntermediateIpeGeometry = std::variant< + PolygonSetRaw, PolygonWithHoles, Polygon, PolylineSet, Polyline, Point, PointSet, + CubicBezierCurve, CubicBezierSpline, Ellipse>; + +template +concept IpeReaderIntermediateGeometryConverter = requires(const IntermediateIpeGeometry& g, OutputIterator out) { + { Traits::template convert(g, out) }; +}; + +/// is a model of IpeReaderTraits +template +requires IpeReaderIntermediateGeometryConverter>, Converter> +struct IpeReaderIntermediateGeometryTraits { + template + static void convertToIntermediate(ipe::Object& o, OutputIterator out) { + // We (currently) don't have a proper intermediate type for a group, so instead just ungroup everything. + if (o.type() == ipe::Object::EGroup) { + auto* group = o.asGroup(); + for (auto* obj : *group) { + convertToIntermediate(*obj, out); + } + return; + } + + if (o.type() == ipe::Object::EReference) { + // Convert to point if it is a mark + auto* ref = o.asReference(); + auto type = ref->type(); + if (ref->flags() & ipe::Reference::EIsMark) { + auto matrix = o.matrix(); + auto realPos = matrix * ref->position(); + *out++ = Point(realPos.x, realPos.y); + } + return; + } + + // Only the path object type remains to be parsed. + if (o.type() != ipe::Object::Type::EPath) { + return; + } + + const ipe::Path* path = o.asPath(); + ipe::Matrix matrix = path->matrix(); + ipe::Shape shape = path->shape(); + + // If all subpaths are closed curves with straight segments then make PolygonSetRaw (or Polygon). + // If all subpaths are open curves with straight segments then make PolylineSet (or Polyline) + // Otherwise, parse and output the subpaths separately + bool allStraightSegments = true; + bool allClosed = true; + bool allOpen = true; + for (int i = 0; i < shape.countSubPaths(); ++i) { + + auto* ssp = shape.subPath(i); + if (!ssp->closed()) { + allClosed = false; + } else { + allOpen = false; + } + if (ssp->type() != ipe::SubPath::ECurve) { + allStraightSegments = false; + } + + const ipe::Curve* curve = ssp->asCurve(); + for (int j = 0; j < curve->countSegments() && allStraightSegments; ++j) { + ipe::CurveSegment segment = curve->segment(j); + if (segment.type() != ipe::CurveSegment::ESegment) { + allStraightSegments = false; + } + } + } + + if (allStraightSegments && allClosed) { + PolygonSetRaw ps; + for (int i = 0; i < shape.countSubPaths(); ++i) { + Polygon polygon; + const ipe::Curve* curve = shape.subPath(i)->asCurve(); + for (int j = 0; j < curve->countSegments(); ++j) { + ipe::CurveSegment segment = curve->segment(j); + if (j == 0) { + ipe::Vector v = matrix * segment.cp(0); + polygon.push_back(Point(v.x, v.y)); + } + ipe::Vector v = matrix * segment.last(); + Point p(v.x, v.y); + if (p != polygon.container().back()) { + polygon.push_back(Point(v.x, v.y)); + } + } + // if the begin and end vertices are equal, remove one of them + if (polygon.container().front() == polygon.container().back()) { + polygon.container().pop_back(); + } + if (shape.countSubPaths() == 1) { + *out++ = polygon; + } else { + ps.polygons_with_holes.emplace_back(polygon); + } + } + if (shape.countSubPaths() > 1) { + *out++ = ps; + } + + return; + } else if (allStraightSegments && allOpen) { + PolylineSet ps; + for (int i = 0; i < shape.countSubPaths(); ++i) { + Polyline polyline; + const ipe::Curve* curve = shape.subPath(i)->asCurve(); + for (int j = 0; j < curve->countSegments(); ++j) { + ipe::CurveSegment segment = curve->segment(j); + if (j == 0) { + ipe::Vector v = matrix * segment.cp(0); + polyline.push_back(Point(v.x, v.y)); + } + ipe::Vector v = matrix * segment.last(); + Point p(v.x, v.y); + polyline.push_back(Point(v.x, v.y)); + } + if (shape.countSubPaths() == 1) { + *out++ = polyline; + } else { + ps.polylines.push_back(polyline); + } + } + if (shape.countSubPaths() > 1) { + *out++ = ps; + } + + return; + } + + // not all straight or not all closed or not all open, so parse separately + for (int i = 0; i < shape.countSubPaths(); ++i) { + auto* ssp = shape.subPath(i); + if (ssp->type() == ipe::SubPath::EClosedSpline) { + CubicBezierSpline spline; + std::vector beziers; + ssp->asClosedSpline()->beziers(beziers); + for (auto bezier : beziers) { + spline.appendCurve(Point(bezier.iV[0].x, bezier.iV[0].y), + Point(bezier.iV[1].x, bezier.iV[1].y), + Point(bezier.iV[2].x, bezier.iV[2].y), + Point(bezier.iV[3].x, bezier.iV[3].y)); + } + *out++ = spline; + } else if (ssp->type() == ipe::SubPath::EEllipse) { + auto matrix = ssp->asEllipse()->matrix(); + + Ellipse ellipse(matrix.a[0], matrix.a[1], matrix.a[2], matrix.a[3], matrix.a[4], + matrix.a[5]); + *out++ = ellipse; + } else if (ssp->type() == ipe::SubPath::ECurve) { + auto* curve = ssp->asCurve(); + + bool allSegments = true; + for (int j = 0; j < curve->countSegments(); ++j) { + ipe::CurveSegment segment = curve->segment(j); + if (segment.type() != ipe::CurveSegment::ESegment) { + allSegments = false; + break; + } + } + if (allSegments) { + if (ssp->closed()) { + // make polygon + Polygon polygon; + for (int j = 0; j < curve->countSegments(); ++j) { + ipe::CurveSegment segment = curve->segment(j); + if (j == 0) { + ipe::Vector v = matrix * segment.cp(0); + polygon.push_back(Point(v.x, v.y)); + } + ipe::Vector v = matrix * segment.last(); + Point p(v.x, v.y); + if (p != polygon.container().back()) { + polygon.push_back(Point(v.x, v.y)); + } + } + // if the begin and end vertices are equal, remove one of them + if (polygon.container().front() == polygon.container().back()) { + polygon.container().pop_back(); + } + *out++ = polygon; + } else { + // make polyline + Polyline polyline; + for (int j = 0; j < curve->countSegments(); ++j) { + ipe::CurveSegment segment = curve->segment(j); + if (j == 0) { + ipe::Vector v = matrix * segment.cp(0); + polyline.push_back(Point(v.x, v.y)); + } + ipe::Vector v = matrix * segment.last(); + Point p(v.x, v.y); + polyline.push_back(Point(v.x, v.y)); + } + *out++ = polyline; + } + } else { + // make spline + for (int j = 0; j < curve->countSegments(); ++j) { + ipe::CurveSegment segment = curve->segment(j); + std::vector beziers; + segment.beziers(beziers); + + // todo test if .beziers also converts circular arcs + CubicBezierSpline spline; + for (auto bezier : beziers) { + spline.appendCurve(Point(bezier.iV[0].x, bezier.iV[0].y), + Point(bezier.iV[1].x, bezier.iV[1].y), + Point(bezier.iV[2].x, bezier.iV[2].y), + Point(bezier.iV[3].x, bezier.iV[3].y)); + } + *out++ = spline; + } + } + } + } + return; + } + + template + static void convert(ipe::Object& o, OutputIterator out) { + std::vector intermediates; + convertToIntermediate(o, std::back_inserter(intermediates)); + + for (const auto& intermediate : intermediates) { + Converter::convert(intermediate, out); + } + } +}; + +// is a model of IpeReaderIntermediateGeometryConverter +template +struct BasicIpeReaderTraitsConverter { + template + static void convert(const IntermediateIpeGeometry& g, OutputIterator out) { + std::visit( + [&](auto&& g) { + using T = std::decay_t; -/// Various utility methods for reading Ipe files. + if constexpr (std::is_convertible_v) { + *out++ = Geometry{g}; + } + }, + g); + } +}; + +template +using BasicIpeReaderTraits = IpeReaderIntermediateGeometryTraits>; + +namespace { + using Out = std::back_insert_iterator>>; + + static_assert(IpeReaderTraits, Out, BasicIpeReaderTraits>>); +} + +// models GeometryReader and GeometryReaderFor every Geometry class IpeReader { + private: + int m_pageNumber = 0; + public: - /// Loads the given Ipe file into an Ipe document. - /** - * This encapsulates the things necessary in Ipelib to load from the file. - * It throws an exception if the file could not be read correctly. - */ - static std::shared_ptr loadIpeFile(const std::filesystem::path& filename); - /// Converts an Ipe color to a CartoCrow color. - static Color convertIpeColor(ipe::Color color); - /// Converts an Ipe shape to a polygon set. - /** - * Throws if the shape contains a non-polygonal boundary. - */ - static PolygonSet convertShapeToPolygonSet(const ipe::Shape& shape, - const ipe::Matrix& matrix); - /// Converts an Ipe path to a Bézier spline. - static CubicBezierSpline convertPathToSpline(const ipe::SubPath& path, const ipe::Matrix& matrix); - - static renderer::RenderPath convertShapeToRenderPath(const ipe::Shape& shape, const ipe::Matrix& matrix); - static renderer::RenderPath loadIpePath(const std::filesystem::path& ipeFile); -}; + // ===== Static Ipe helpers ===== + static std::shared_ptr loadIpeFile(const std::filesystem::path& filename) { + std::fstream fin(filename); + std::string input; + if (fin) { + using Iterator = std::istreambuf_iterator; + input.assign(Iterator(fin), Iterator()); + } else { + std::stringstream ss; + ss << "Cannot open file " << filename; + throw std::runtime_error(ss.str()); + } + + ipe::Platform::initLib(ipe::IPELIB_VERSION); + int load_reason = 0; + ipe::Buffer buffer(input.c_str(), input.size()); + ipe::BufferSource bufferSource(buffer); + ipe::FileFormat format = ipe::Document::fileFormat(bufferSource); + ipe::Document* document = ipe::Document::load(bufferSource, format, load_reason); + + if (load_reason > 0) { + throw std::runtime_error("Unable to load Ipe file: parse error at position " + + std::to_string(load_reason)); + } else if (load_reason == ipe::Document::EVersionTooOld) { + throw std::runtime_error("Unable to load Ipe file: the version of the file is too old"); + } else if (load_reason == ipe::Document::EVersionTooRecent) { + throw std::runtime_error( + "Unable to load Ipe file: the file version is newer than Ipelib"); + } else if (load_reason == ipe::Document::EFileOpenError) { + throw std::runtime_error("Unable to load Ipe file: error opening the file"); + } else if (load_reason == ipe::Document::ENotAnIpeFile) { + throw std::runtime_error( + "Unable to load Ipe file: the file does not exist or was not created by Ipe"); + } + + return std::shared_ptr(document); + } + + Color convertIpeColor(ipe::Color color) { + return Color{static_cast(color.iRed.toDouble() * 255), + static_cast(color.iGreen.toDouble() * 255), + static_cast(color.iBlue.toDouble() * 255)}; + } + + // ===== Reader methods ===== -} // namespace cartocrow + /// If it exists, return the well-known text representation (WKT) of the coordinate reference system + std::optional readSpatialReference(std::filesystem::path path) { + return std::nullopt; + } + + /// Returns whether the reader can parse the given file. + /// Currently only checks the file extension. + // todo: actually try to parse to ipe document? + bool canRead(std::filesystem::path path) { + return path.extension() == ".ipe"; + } + + template < + class Geometry, + class OutputIterator, + class Traits = BasicIpeReaderTraits + > + requires IpeReaderTraits + void read(std::filesystem::path path, OutputIterator out) { + std::shared_ptr document = IpeReader::loadIpeFile(path); + + if (m_pageNumber >= document->countPages()) { + std::cerr << "Current page number exceeds document page count." << std::endl; + std::cerr << "Setting page number to last page." << std::endl; + m_pageNumber = document->countPages() - 1; + } else if (m_pageNumber < 0) { + std::cerr << "Current page number is negative." << std::endl; + std::cerr << "Setting page number to first page." << std::endl; + m_pageNumber = 0; + } + + ipe::Page* page = document->page(m_pageNumber); + + for (int i = 0; i < page->count(); ++i) { + ipe::Object* object = page->object(i); + Traits::convert(*object, out); + } + } + + template > + requires IpeReaderTraits>, Traits> + std::optional readSingle(std::filesystem::path path) { + std::shared_ptr document = IpeReader::loadIpeFile(path); + + if (m_pageNumber >= document->countPages()) { + std::cerr << "Current page number exceeds document page count." << std::endl; + std::cerr << "Setting page number to last page." << std::endl; + m_pageNumber = document->countPages() - 1; + } else if (m_pageNumber < 0) { + std::cerr << "Current page number is negative." << std::endl; + std::cerr << "Setting page number to first page." << std::endl; + m_pageNumber = 0; + } + + ipe::Page* page = document->page(m_pageNumber); + + std::vector gs; + for (int i = 0; i < page->count(); ++i) { + ipe::Object* object = page->object(i); + Traits::convert(*object, std::back_inserter(gs)); + if (!gs.empty()) + return gs[0]; + } + + return std::nullopt; + } +}; +} \ No newline at end of file diff --git a/data/test_ipe_reader.ipe b/data/test_ipe_reader.ipe new file mode 100644 index 00000000..fed30e2f --- /dev/null +++ b/data/test_ipe_reader.ipe @@ -0,0 +1,461 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +0.5 0 0 0.5 0 0 e + + + + + +0.5 0 0 0.5 0 0 e + + +0.5 0 0 0.5 0 0 e + + + + + + +0.5 0 0 0.5 0 0 e + + +0.5 0 0 0.5 0 0 e + + + + + +-0.5 -0.5 m +0.5 -0.5 l +0.5 0.5 l +-0.5 0.5 l +h + + + + +-0.5 -0.5 m +0.5 -0.5 l +0.5 0.5 l +-0.5 0.5 l +h + + + + + +-0.5 -0.5 m +0.5 -0.5 l +0.5 0.5 l +-0.5 0.5 l +h + + +-0.5 -0.5 m +0.5 -0.5 l +0.5 0.5 l +-0.5 0.5 l +h + + + + + + +-0.5 -0.5 m +0.5 0.5 l +h + + +-0.5 0.5 m +0.5 -0.5 l +h + + + + + +0 -0.5 m +0 0.5 l +h + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +0 0 m +-1 0.333 l +-1 -0.333 l +h + + + + +0 0 m +-1 0.333 l +-1 -0.333 l +h + + + + +0 0 m +-1 0.333 l +-0.8 0 l +-1 -0.333 l +h + + + + +0 0 m +-1 0.333 l +-0.8 0 l +-1 -0.333 l +h + + + + +0.6 0 0 0.6 0 0 e +0.4 0 0 0.4 0 0 e + + + + +0.6 0 0 0.6 0 0 e + + + + + +0.5 0 0 0.5 0 0 e + + +0.6 0 0 0.6 0 0 e +0.4 0 0 0.4 0 0 e + + + + + +-0.6 -0.6 m +0.6 -0.6 l +0.6 0.6 l +-0.6 0.6 l +h +-0.4 -0.4 m +0.4 -0.4 l +0.4 0.4 l +-0.4 0.4 l +h + + + + +-0.6 -0.6 m +0.6 -0.6 l +0.6 0.6 l +-0.6 0.6 l +h + + + + + +-0.5 -0.5 m +0.5 -0.5 l +0.5 0.5 l +-0.5 0.5 l +h + + +-0.6 -0.6 m +0.6 -0.6 l +0.6 0.6 l +-0.6 0.6 l +h +-0.4 -0.4 m +0.4 -0.4 l +0.4 0.4 l +-0.4 0.4 l +h + + + + + + +-0.43 -0.57 m +0.57 0.43 l +0.43 0.57 l +-0.57 -0.43 l +h + + +-0.43 0.57 m +0.57 -0.43 l +0.43 -0.57 l +-0.57 0.43 l +h + + + + + +0 0 m +-1 0.333 l +-1 -0.333 l +h + + + + +0 0 m +-1 0.333 l +-0.8 0 l +-1 -0.333 l +h + + + + +0 0 m +-1 0.333 l +-0.8 0 l +-1 -0.333 l +h + + + + +-1 0.333 m +0 0 l +-1 -0.333 l + + + + +0 0 m +-1 0.333 l +-1 -0.333 l +h +-1 0 m +-2 0.333 l +-2 -0.333 l +h + + + + +0 0 m +-1 0.333 l +-1 -0.333 l +h +-1 0 m +-2 0.333 l +-2 -0.333 l +h + + + + + + + + +-0.7 -0.4 m +0.7 -0.4 l +0 0.8124 l +h + + + + + + + + + + + + + + + + + + + + + + + + + +144.544 155.39 m +123.907 113.135 l +178.446 67.44 l +249.199 124.927 l +h + + + diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 060fae5c..a04a7d1a 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -2,9 +2,8 @@ set(TEST_SOURCES "cartocrow_test.cpp" "core/cubic_bezier.cpp" "core/core.cpp" "core/polygon_helpers.cpp" - "core/region_arrangement.cpp" - "core/region_map.cpp" "renderer/ipe_renderer.cpp" + "reader/ipe_reader.cpp" ) diff --git a/test/reader/ipe_reader.cpp b/test/reader/ipe_reader.cpp new file mode 100644 index 00000000..bef1f681 --- /dev/null +++ b/test/reader/ipe_reader.cpp @@ -0,0 +1,48 @@ +/* +Copyright (C) 2026 TU Eindhoven + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +#include "../catch.hpp" + +#include "cartocrow/reader/ipe_reader.h" + +using namespace cartocrow; + +TEST_CASE("Reading points") { + IpeReader ipeReader; + + std::vector> points; + ipeReader.read>("data/test_ipe_reader.ipe", std::back_inserter(points)); + CHECK(points.size() == 4); + + auto exists = [&](Point point) { + return std::find(points.begin(), points.end(), point) != points.end(); + }; + + CHECK(exists({0, 0})); + CHECK(exists({64, 64})); + CHECK(exists({64, 0})); + CHECK(exists({0, 64})); +} + +TEST_CASE("Reading a polygon") { + std::vector> points({{144.544, 155.39}, {123.907, 113.135}, {178.446, 67.44}, {249.199, 124.927}}); + Polygon expectedPolygon(points.begin(), points.end()); + + IpeReader ipeReader; + auto parsedPolygon = ipeReader.readSingle>("data/test_ipe_reader.ipe"); + CHECK(parsedPolygon == expectedPolygon); +} \ No newline at end of file From e0baf9ca2c9328bd733b2337b3cfd0a5e5c9e95d Mon Sep 17 00:00:00 2001 From: Yvee1 Date: Mon, 29 Jun 2026 14:20:08 +0200 Subject: [PATCH 06/17] Test and polish ipe reader --- cartocrow/core/cubic_bezier.cpp | 15 +++++ cartocrow/core/cubic_bezier.h | 12 ++++ cartocrow/core/polygon_set_raw.h | 10 +++ cartocrow/core/polyline.h | 2 + cartocrow/reader/ipe_reader.h | 111 ++++++++++++++++++++++++++----- data/test_ipe_reader.ipe | 107 ++++++++++++++++++++++++++++- test/reader/ipe_reader.cpp | 102 +++++++++++++++++++++++++++- 7 files changed, 338 insertions(+), 21 deletions(-) diff --git a/cartocrow/core/cubic_bezier.cpp b/cartocrow/core/cubic_bezier.cpp index 553c757b..c009e6cf 100644 --- a/cartocrow/core/cubic_bezier.cpp +++ b/cartocrow/core/cubic_bezier.cpp @@ -42,6 +42,8 @@ CubicBezierCurve::CubicBezierCurve(Point source, Point target) : source + (target - source) * 2.0 / 3.0, target) {} +CubicBezierCurve::CubicBezierCurve(Segment seg) : CubicBezierCurve(seg.source(), seg.target()) {} + Point CubicBezierCurve::source() const { return m_p0; } @@ -346,6 +348,19 @@ bool CubicBezierCurve::selfIntersects(double threshold) const { CubicBezierSpline::CubicBezierSpline() {}; +CubicBezierSpline::CubicBezierSpline(Segment segment) { + appendCurve(segment); +} + +CubicBezierSpline::CubicBezierSpline(CubicBezierCurve curve) + : m_c({curve.source(), curve.sourceControl(), curve.targetControl(), curve.target()}) {}; + +CubicBezierSpline::CubicBezierSpline(Polyline polyline) { + for (int i = 0; i < polyline.num_edges(); ++i) { + appendCurve(polyline.vertex(i), polyline.vertex(i + 1)); + } +} + void CubicBezierSpline::appendCurve(const Curve& curve) { for (int i = 0; i < 4; ++i) { if (i == 0 && !m_c.empty()) { diff --git a/cartocrow/core/cubic_bezier.h b/cartocrow/core/cubic_bezier.h index eb92f568..f04b0a50 100644 --- a/cartocrow/core/cubic_bezier.h +++ b/cartocrow/core/cubic_bezier.h @@ -94,6 +94,9 @@ class CubicBezierCurve { /// Construct a cubic Bézier curve from two endpoints. CubicBezierCurve(Point source, Point target); + /// Construct a cubic Bézier curve from a line segment. + CubicBezierCurve(Segment seg); + /// Returns the source of this curve. Point source() const; /// Returns the control point on the source side of this curve. @@ -322,6 +325,15 @@ class CubicBezierSpline { CubicBezierSpline(InputIterator begin, InputIterator end) : m_c(begin, end) { assert(m_c.size() == 0 || (m_c.size() - 1) % 3 == 0);} + /// Create a spline from a polyline. + CubicBezierSpline(Segment segment); + + /// Create a spline from a single curve; + CubicBezierSpline(CubicBezierCurve curve); + + /// Create a spline from a polyline. + CubicBezierSpline(Polyline polyline); + /// Append a cubic Bézier curve. void appendCurve(const Curve& curve); /// Append a cubic Bézier curve from its two endpoints and two control points. diff --git a/cartocrow/core/polygon_set_raw.h b/cartocrow/core/polygon_set_raw.h index 013c5fc7..9cc39d02 100644 --- a/cartocrow/core/polygon_set_raw.h +++ b/cartocrow/core/polygon_set_raw.h @@ -27,6 +27,16 @@ struct PolygonSetRaw { } return polygonSet; } + + PolygonSetRaw() = default; + + PolygonSetRaw(Polygon polygon) { + polygons_with_holes.emplace_back(std::move(polygon)); + } + + PolygonSetRaw(PolygonWithHoles polygon) { + polygons_with_holes.push_back(std::move(polygon)); + } }; PolygonSetRaw approximate(const PolygonSetRaw& pgs); diff --git a/cartocrow/core/polyline.h b/cartocrow/core/polyline.h index a081f642..f506fa34 100644 --- a/cartocrow/core/polyline.h +++ b/cartocrow/core/polyline.h @@ -100,6 +100,8 @@ template class Polyline { std::copy(begin, end, std::back_inserter(m_points)); } + Polyline(Segment segment) : m_points({segment.source(), segment.target()}) {}; + explicit Polyline(std::vector> points): m_points(points) {}; void push_back(const CGAL::Point_2& p) { diff --git a/cartocrow/reader/ipe_reader.h b/cartocrow/reader/ipe_reader.h index 9b12e14a..efa97f57 100644 --- a/cartocrow/reader/ipe_reader.h +++ b/cartocrow/reader/ipe_reader.h @@ -44,8 +44,8 @@ concept IpeReaderTraits = requires(ipe::Object& o, OutputIterator out) { // todo: make a RenderPath reader traits that parses everything to a render path? using IntermediateIpeGeometry = std::variant< - PolygonSetRaw, PolygonWithHoles, Polygon, PolylineSet, Polyline, Point, PointSet, - CubicBezierCurve, CubicBezierSpline, Ellipse>; + PolygonSetRaw, PolygonWithHoles, Polygon, PolylineSet, Polyline, Segment, + Point, PointSet, CubicBezierCurve, CubicBezierSpline, Ellipse>; template concept IpeReaderIntermediateGeometryConverter = requires(const IntermediateIpeGeometry& g, OutputIterator out) { @@ -95,7 +95,6 @@ struct IpeReaderIntermediateGeometryTraits { bool allClosed = true; bool allOpen = true; for (int i = 0; i < shape.countSubPaths(); ++i) { - auto* ssp = shape.subPath(i); if (!ssp->closed()) { allClosed = false; @@ -163,7 +162,11 @@ struct IpeReaderIntermediateGeometryTraits { polyline.push_back(Point(v.x, v.y)); } if (shape.countSubPaths() == 1) { - *out++ = polyline; + if (polyline.num_edges() == 1) { + *out++ = polyline.edge(0); + } else { + *out++ = polyline; + } } else { ps.polylines.push_back(polyline); } @@ -257,7 +260,12 @@ struct IpeReaderIntermediateGeometryTraits { Point(bezier.iV[2].x, bezier.iV[2].y), Point(bezier.iV[3].x, bezier.iV[3].y)); } - *out++ = spline; + + if (spline.numCurves() == 1) { + *out++ = spline.curve(0); + } else { + *out++ = spline; + } } } } @@ -305,7 +313,10 @@ namespace { // models GeometryReader and GeometryReaderFor every Geometry class IpeReader { private: + /// The page to read from int m_pageNumber = 0; + /// The layer to read from. If std::nullopt then it reads from all layers. + std::optional> m_layer = std::nullopt; public: // ===== Static Ipe helpers ===== @@ -316,9 +327,9 @@ class IpeReader { using Iterator = std::istreambuf_iterator; input.assign(Iterator(fin), Iterator()); } else { - std::stringstream ss; - ss << "Cannot open file " << filename; - throw std::runtime_error(ss.str()); + std::stringstream ss; + ss << "Cannot open file " << filename; + throw std::runtime_error(ss.str()); } ipe::Platform::initLib(ipe::IPELIB_VERSION); @@ -330,28 +341,82 @@ class IpeReader { if (load_reason > 0) { throw std::runtime_error("Unable to load Ipe file: parse error at position " + - std::to_string(load_reason)); + std::to_string(load_reason)); } else if (load_reason == ipe::Document::EVersionTooOld) { throw std::runtime_error("Unable to load Ipe file: the version of the file is too old"); } else if (load_reason == ipe::Document::EVersionTooRecent) { throw std::runtime_error( - "Unable to load Ipe file: the file version is newer than Ipelib"); + "Unable to load Ipe file: the file version is newer than Ipelib"); } else if (load_reason == ipe::Document::EFileOpenError) { throw std::runtime_error("Unable to load Ipe file: error opening the file"); } else if (load_reason == ipe::Document::ENotAnIpeFile) { throw std::runtime_error( - "Unable to load Ipe file: the file does not exist or was not created by Ipe"); + "Unable to load Ipe file: the file does not exist or was not created by Ipe"); } return std::shared_ptr(document); } Color convertIpeColor(ipe::Color color) { - return Color{static_cast(color.iRed.toDouble() * 255), - static_cast(color.iGreen.toDouble() * 255), - static_cast(color.iBlue.toDouble() * 255)}; + return Color{ static_cast(color.iRed.toDouble() * 255), + static_cast(color.iGreen.toDouble() * 255), + static_cast(color.iBlue.toDouble() * 255) }; + } + + // ===== Ipe reader specific functions ===== + /// Set the page to read from. + /// Note! Page indices start at zero (so pass one integer smaller than the one the ipe GUI shows). + void setPage(int pageNumber) { + m_pageNumber = pageNumber; + } + + /// Removes the layer filter so that objects are read from all layers. + void removeLayerFilter() { + m_layer = std::nullopt; + } + + /// Read objects only from the specified layer + void setLayerFilter(int layerNumber) { + m_layer = layerNumber; + } + + /// Read objects only from the specified layer + void setLayerFilter(std::string layerName) { + m_layer = layerName; + } + + /// Return the number of pages in the ipe document + int numberOfPages(std::filesystem::path path) { + auto doc = loadIpeFile(path); + return doc->countPages(); + } + + /// Number of layers + int numberOfLayer(std::filesystem::path path, int pageIndex) { + auto doc = loadIpeFile(path); + auto page = doc->page(pageIndex); + return page->countLayers(); + } + + private: + /// Whether to skip the ipe object with index i in the given page. + bool skipObject(ipe::Page* page, int i) const { + if (m_layer.has_value()) { + auto layerIndex = page->layerOf(i); + if (auto* layerIndexP = std::get_if(&*m_layer)) { + if (layerIndex != *layerIndexP) + return true; // object is not on layer so we skip + } else if (auto* layerNameP = std::get_if(&*m_layer)) { + if (*page->layer(layerIndex).data() != *layerNameP->c_str()) { + return true; + } + } + } + return false; } + public: + // ===== Reader methods ===== /// If it exists, return the well-known text representation (WKT) of the coordinate reference system @@ -371,7 +436,7 @@ class IpeReader { class OutputIterator, class Traits = BasicIpeReaderTraits > - requires IpeReaderTraits + requires IpeReaderTraits void read(std::filesystem::path path, OutputIterator out) { std::shared_ptr document = IpeReader::loadIpeFile(path); @@ -379,7 +444,8 @@ class IpeReader { std::cerr << "Current page number exceeds document page count." << std::endl; std::cerr << "Setting page number to last page." << std::endl; m_pageNumber = document->countPages() - 1; - } else if (m_pageNumber < 0) { + } + else if (m_pageNumber < 0) { std::cerr << "Current page number is negative." << std::endl; std::cerr << "Setting page number to first page." << std::endl; m_pageNumber = 0; @@ -388,11 +454,22 @@ class IpeReader { ipe::Page* page = document->page(m_pageNumber); for (int i = 0; i < page->count(); ++i) { + if (skipObject(page, i)) + continue; ipe::Object* object = page->object(i); Traits::convert(*object, out); } } + /// Convenience function that calls read and stores the results in a vector. + template > + requires IpeReaderTraits>, Traits> + std::vector readV(std::filesystem::path path) { + std::vector gs; + read>, Traits>(path, std::back_inserter(gs)); + return gs; + } + template > requires IpeReaderTraits>, Traits> std::optional readSingle(std::filesystem::path path) { @@ -412,6 +489,8 @@ class IpeReader { std::vector gs; for (int i = 0; i < page->count(); ++i) { + if (skipObject(page, i)) + continue; ipe::Object* object = page->object(i); Traits::convert(*object, std::back_inserter(gs)); if (!gs.empty()) diff --git a/data/test_ipe_reader.ipe b/data/test_ipe_reader.ipe index fed30e2f..d5671b2c 100644 --- a/data/test_ipe_reader.ipe +++ b/data/test_ipe_reader.ipe @@ -1,7 +1,7 @@ - + @@ -450,7 +450,7 @@ h - + 144.544 155.39 m 123.907 113.135 l 178.446 67.44 l @@ -458,4 +458,107 @@ h h + + + + +114.865 425.25 m +63.4295 398.019 l +83.0962 364.359 l +119.026 388.564 l +139.827 353.013 l +191.641 404.071 l +h +126.754 413.814 m +170.227 401.938 l +145.129 368.101 l +121.824 404.178 l +h + + +109.155 363.72 m +98.5656 347.457 l +125.418 339.893 l +118.61 360.694 l +h +133.558 349.21 m +133.73 339.896 l +149.254 337.826 l +149.772 344.898 l +h + + +126.754 413.814 m +121.824 404.178 l +145.129 368.101 l +170.227 401.938 l +h + + + + + + +25.5402 46.4026 m +35.4382 72.8944 l +77.6505 53.3894 l +85.8018 80.7546 l +113.749 67.072 l +110.256 48.7315 l +156.544 62.7053 l +134.419 88.6149 l + + +27.578 37.0868 m +39.5139 47.8582 +60.4745 28.3532 +76.1949 33.5933 +92.2065 48.1493 +120.445 57.174 +135.583 37.9601 +155.088 54.5539 c + + +37.8869 109.428 m +61.4676 126.313 l + + +43.3035 58.7575 m +57.7755 73.3623 +78.0893 59.4214 +95.7478 72.8312 c + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/reader/ipe_reader.cpp b/test/reader/ipe_reader.cpp index bef1f681..0ec8facf 100644 --- a/test/reader/ipe_reader.cpp +++ b/test/reader/ipe_reader.cpp @@ -18,14 +18,15 @@ along with this program. If not, see . #include "../catch.hpp" #include "cartocrow/reader/ipe_reader.h" +#include "cartocrow/renderer/ipe_renderer.h" using namespace cartocrow; +using namespace renderer; TEST_CASE("Reading points") { IpeReader ipeReader; - std::vector> points; - ipeReader.read>("data/test_ipe_reader.ipe", std::back_inserter(points)); + auto points = ipeReader.readV>("data/test_ipe_reader.ipe"); CHECK(points.size() == 4); auto exists = [&](Point point) { @@ -45,4 +46,99 @@ TEST_CASE("Reading a polygon") { IpeReader ipeReader; auto parsedPolygon = ipeReader.readSingle>("data/test_ipe_reader.ipe"); CHECK(parsedPolygon == expectedPolygon); -} \ No newline at end of file +} + +TEST_CASE("Reading points and a polygon") { + std::vector> points( + {{144.544, 155.39}, {123.907, 113.135}, {178.446, 67.44}, {249.199, 124.927}}); + Polygon expectedPolygon(points.begin(), points.end()); + + using PointOrPoly = std::variant, Polygon>; + + IpeReader ipeReader; + auto pointOrPolys = ipeReader.readV("data/test_ipe_reader.ipe"); + + CHECK(pointOrPolys.size() == 5); + + auto exists = [&](PointOrPoly&& p) { + return std::find(pointOrPolys.begin(), pointOrPolys.end(), p) != pointOrPolys.end(); + }; + + CHECK(exists(expectedPolygon)); + CHECK(exists(Point{0, 0})); + CHECK(exists(Point{64, 64})); + CHECK(exists(Point{64, 0})); + CHECK(exists(Point{0, 64})); +} + +TEST_CASE("Reading polygon sets") { + // Test whether polygon is automatically converted to PolygonSetRaw. + // The file contains 2 polygon sets and 1 polygon. + IpeReader ipeReader; + ipeReader.setPage(1); + + auto psrs = ipeReader.readV>("data/test_ipe_reader.ipe"); + auto pgns = ipeReader.readV>("data/test_ipe_reader.ipe"); + + CHECK(psrs.size() == 3); + CHECK(pgns.size() == 1); +} + +TEST_CASE("Reading polylines, segments, Bézier curves and splines") { + std::filesystem::path fn("data/test_ipe_reader.ipe"); + + // Test open geometries. + // The file contains a line segment, a polyline, a cubic Bézier curve, a cubic Bézier spline. + IpeReader ipeReader; + ipeReader.setPage(2); + + auto ls = ipeReader.readV>(fn); + CHECK(ls.size() == 1); // should only return the line segment + + auto pls = ipeReader.readV>(fn); + CHECK(pls.size() == 2); // should return the polyline and the line segment + + auto cbcs = ipeReader.readV(fn); + CHECK(cbcs.size() == 2); // should return the cubic Bézier curve and the line segment + + auto cbss = ipeReader.readV(fn); + CHECK(cbss.size() == 4); // should return all +} + +TEST_CASE("Read points from specific layer") { + std::filesystem::path fn("data/test_ipe_reader.ipe"); + + IpeReader ipeReader; + ipeReader.setPage(3); + + ipeReader.setLayerFilter("red"); + auto redPoints = ipeReader.readV>(fn); + ipeReader.setLayerFilter(0); + auto bluePoints = ipeReader.readV>(fn); + ipeReader.setLayerFilter("green"); + auto greenPoints = ipeReader.readV>(fn); + + CHECK(bluePoints.size() == 7); + CHECK(redPoints.size() == 11); + CHECK(greenPoints.size() == 8); +} + +//TEST_CASE("Manual check: load and save test_ipe_reader.ipe; geometries should be equivalent") { +// IpeReader ipeReader; +// IpeRenderer ipeRenderer; +// +// auto fn = "data/test_ipe_reader.ipe"; +// +// for (int pageIndex = 0; pageIndex < ipeReader.numberOfPages(fn); ++pageIndex) { +// ipeReader.setPage(pageIndex); +// auto geoms = ipeReader.readV(fn); +// ipeRenderer.addPainting([geoms](GeometryRenderer& r) { +// for (const auto& g : geoms) { +// std::visit([&](auto& someG) { r.draw(someG); }, g); +// } +// }); +// ipeRenderer.nextPage(); +// } +// +// ipeRenderer.save("test_ipe_reader_saved.ipe"); +//} \ No newline at end of file From aeb28fa05ff283255d3bf86a8f548d994dcd2a3d Mon Sep 17 00:00:00 2001 From: Yvee1 Date: Tue, 30 Jun 2026 10:46:02 +0200 Subject: [PATCH 07/17] Implement attribute reading methods in ipe reader --- cartocrow/core/geometric_feature.h | 4 +- cartocrow/reader/geometry_reader.h | 20 +++- cartocrow/reader/ipe_reader.h | 166 ++++++++++++++++++++++------- test/reader/ipe_reader.cpp | 35 ++++++ 4 files changed, 180 insertions(+), 45 deletions(-) diff --git a/cartocrow/core/geometric_feature.h b/cartocrow/core/geometric_feature.h index 451e679c..027ac961 100644 --- a/cartocrow/core/geometric_feature.h +++ b/cartocrow/core/geometric_feature.h @@ -23,10 +23,10 @@ namespace cartocrow { using GeometryAttribute = std::variant, double, std::vector, std::string, std::vector, int64_t>; -using GeometryAttributes = std::unordered_map; +using GeometryAttributes = std::unordered_map; template struct GeometricFeature { Geometry geometry; GeometryAttributes attributes; -} +}; } \ No newline at end of file diff --git a/cartocrow/reader/geometry_reader.h b/cartocrow/reader/geometry_reader.h index 48523c97..48cb3c9d 100644 --- a/cartocrow/reader/geometry_reader.h +++ b/cartocrow/reader/geometry_reader.h @@ -17,6 +17,8 @@ along with this program. If not, see . #pragma once +#include "../core/geometric_feature.h" + #include #include #include @@ -36,16 +38,28 @@ concept GeometryReaderFor = GeometryReader && requires(R reader, std::filesystem::path path, OutputIterator out) { /// Returns all geometries in the provided file that are convertible to Geometry. - /// precondition: canRead(path) + /// \pre canRead(path) reader.template read(path, out); + /// Returns a vector with all geometries in the provided file that are convertible to Geometry. + /// \pre canRead(path) + {reader.template readV(path)}->std::same_as>; + /// Returns the first geometry in the provided file that is convertible to Geometry. - /// precondition: canRead(path) + /// \pre canRead(path) {reader.template readSingle(path)}->std::same_as>; /// Returns geometries in the provided file that are convertible to Geometry including their attributes. /// Outputs Feature. - /// precondition: canRead(path) + /// \pre canRead(path) reader.template readWithAttributes(path, out); + + /// Returns a vector with all geometries in the provided file that are convertible to Geometry including their attributes. + /// \pre canRead(path) + {reader.template readWithAttributesV(path)}->std::same_as>>; + + /// Returns the first feature in the provided file that is convertible to Geometry. + /// \pre canRead(path) + {reader.template readSingleWithAttributes(path)}->std::same_as>>; }; } \ No newline at end of file diff --git a/cartocrow/reader/ipe_reader.h b/cartocrow/reader/ipe_reader.h index efa97f57..3607bd87 100644 --- a/cartocrow/reader/ipe_reader.h +++ b/cartocrow/reader/ipe_reader.h @@ -25,6 +25,9 @@ along with this program. If not, see . #include "../core/point_set.h" #include "../core/polygon_set_raw.h" #include "../core/cubic_bezier.h" +#include "../core/geometric_feature.h" + +#include #include #include @@ -415,11 +418,64 @@ class IpeReader { return false; } + GeometryAttributes getAttributes(ipe::Page* page, int i) const { + // There is no nice way to get all attributes that are relevant for an object, the logic is all in the saveAsXml function. + // So for now we convert to xml and parse that. This is not so efficient because the entire geometry is also exported to xml. + + ipe::String ipeString; + ipe::StringStream ipeSS(ipeString); + page->object(i)->saveAsXml(ipeSS, page->layer(page->layerOf(i))); + std::string input = ipeString.data(); + + GeometryAttributes result; + + // Regex for key="value" + std::regex attr_regex("(\\w+)\\s*=\\s*\"([^\"]*)\""); + + for (auto it = std::sregex_iterator(input.begin(), input.end(), attr_regex); + it != std::sregex_iterator(); ++it) { + std::string key = (*it)[1].str(); + std::string value = (*it)[2].str(); + + // Ignore matrix attributes for paths and position attributes for references + if (key == "matrix" || key == "pos") + continue; + + result[key] = value; + } + + return result; + } + + /// If handle returns true the parsing stops. + void readHelper(std::filesystem::path path, std::function handle) { + std::shared_ptr document = IpeReader::loadIpeFile(path); + + if (m_pageNumber >= document->countPages()) { + std::cerr << "Current page number exceeds document page count." << std::endl; + std::cerr << "Setting page number to last page." << std::endl; + m_pageNumber = document->countPages() - 1; + } else if (m_pageNumber < 0) { + std::cerr << "Current page number is negative." << std::endl; + std::cerr << "Setting page number to first page." << std::endl; + m_pageNumber = 0; + } + + ipe::Page* page = document->page(m_pageNumber); + + for (int i = 0; i < page->count(); ++i) { + if (skipObject(page, i)) + continue; + if (handle(page, i)) + break; + } + } + public: // ===== Reader methods ===== - /// If it exists, return the well-known text representation (WKT) of the coordinate reference system + /// If it exists, return the well-known text representation (WKT) of the coordinate reference system std::optional readSpatialReference(std::filesystem::path path) { return std::nullopt; } @@ -431,6 +487,8 @@ class IpeReader { return path.extension() == ".ipe"; } + /// Returns all geometries in the provided file that are convertible to Geometry. + /// \pre canRead(path) template < class Geometry, class OutputIterator, @@ -438,30 +496,15 @@ class IpeReader { > requires IpeReaderTraits void read(std::filesystem::path path, OutputIterator out) { - std::shared_ptr document = IpeReader::loadIpeFile(path); - - if (m_pageNumber >= document->countPages()) { - std::cerr << "Current page number exceeds document page count." << std::endl; - std::cerr << "Setting page number to last page." << std::endl; - m_pageNumber = document->countPages() - 1; - } - else if (m_pageNumber < 0) { - std::cerr << "Current page number is negative." << std::endl; - std::cerr << "Setting page number to first page." << std::endl; - m_pageNumber = 0; - } - - ipe::Page* page = document->page(m_pageNumber); - - for (int i = 0; i < page->count(); ++i) { - if (skipObject(page, i)) - continue; + readHelper(path, [&](ipe::Page* page, int i) { ipe::Object* object = page->object(i); Traits::convert(*object, out); - } + return false; + }); } - /// Convenience function that calls read and stores the results in a vector. + /// Returns a vector with all geometries in the provided file that are convertible to Geometry. + /// \pre canRead(path) template > requires IpeReaderTraits>, Traits> std::vector readV(std::filesystem::path path) { @@ -470,34 +513,77 @@ class IpeReader { return gs; } + /// Returns the first geometry in the provided file that is convertible to Geometry. + /// \pre canRead(path) template > requires IpeReaderTraits>, Traits> std::optional readSingle(std::filesystem::path path) { - std::shared_ptr document = IpeReader::loadIpeFile(path); + std::vector gs; - if (m_pageNumber >= document->countPages()) { - std::cerr << "Current page number exceeds document page count." << std::endl; - std::cerr << "Setting page number to last page." << std::endl; - m_pageNumber = document->countPages() - 1; - } else if (m_pageNumber < 0) { - std::cerr << "Current page number is negative." << std::endl; - std::cerr << "Setting page number to first page." << std::endl; - m_pageNumber = 0; - } + readHelper(path, [&](ipe::Page* page, int i) { + ipe::Object* object = page->object(i); + Traits::convert(*object, std::back_inserter(gs)); + return !gs.empty(); // stop if a geometry is found + }); - ipe::Page* page = document->page(m_pageNumber); + return gs.empty() ? std::nullopt : std::optional(gs[0]); + } - std::vector gs; - for (int i = 0; i < page->count(); ++i) { - if (skipObject(page, i)) - continue; + /// Returns geometries in the provided file that are convertible to Geometry including their attributes. + /// Outputs Feature. + /// \pre canRead(path) + template > + requires IpeReaderTraits>, Traits> + void readWithAttributes(std::filesystem::path path, OutputIterator out) { + readHelper(path, [&](ipe::Page* page, int i) { ipe::Object* object = page->object(i); + auto attributes = getAttributes(page, i); + + std::vector gs; Traits::convert(*object, std::back_inserter(gs)); - if (!gs.empty()) - return gs[0]; - } + for (auto& g : gs) { + *out++ = GeometricFeature(std::move(g), attributes); + } + return false; + }); + } - return std::nullopt; + /// Returns a vector with all geometries in the provided file that are convertible to Geometry including their attributes. + /// \pre canRead(path) + template > + requires IpeReaderTraits>, Traits> + std::vector> readWithAttributesV(std::filesystem::path path) { + std::vector> gs; + readWithAttributes>>, Traits>(path, std::back_inserter(gs)); + return gs; + } + + /// Returns the first feature in the provided file that is convertible to Geometry. + /// \pre canRead(path) + template > + requires IpeReaderTraits>, Traits> + std::optional> readSingleWithAttributes(std::filesystem::path path) { + std::vector> fs; + + readHelper(path, [&](ipe::Page* page, int i) { + ipe::Object* object = page->object(i); + auto attributes = getAttributes(page, i); + + std::vector gs; + Traits::convert(*object, std::back_inserter(gs)); + for (auto& g : gs) { + fs.emplace_back(std::move(g), attributes); + } + return !gs.empty(); // stop if a geometry is found + }); + + return fs.empty() ? std::nullopt : fs[0]; } }; + +namespace { +static_assert(GeometryReader); +using Out = std::back_insert_iterator>>; +static_assert(GeometryReaderFor, Out>); +} } \ No newline at end of file diff --git a/test/reader/ipe_reader.cpp b/test/reader/ipe_reader.cpp index 0ec8facf..bc03a827 100644 --- a/test/reader/ipe_reader.cpp +++ b/test/reader/ipe_reader.cpp @@ -123,6 +123,41 @@ TEST_CASE("Read points from specific layer") { CHECK(greenPoints.size() == 8); } +TEST_CASE("Read attributes") { + std::filesystem::path fn("data/test_ipe_reader.ipe"); + + IpeReader ipeReader; + ipeReader.setPage(3); + + ipeReader.setLayerFilter("red"); + auto redPoints = ipeReader.readWithAttributesV>(fn); + ipeReader.setLayerFilter(0); + auto bluePoints = ipeReader.readWithAttributesV>(fn); + ipeReader.setLayerFilter("green"); + auto greenPoints = ipeReader.readWithAttributesV>(fn); + + CHECK(bluePoints.size() == 7); + for (const auto& bp : bluePoints) { + CHECK(bp.attributes.contains("fill")); + CHECK(std::holds_alternative(bp.attributes.at("fill"))); + CHECK(std::get(bp.attributes.at("fill")) == "CB light blue"); + } + + CHECK(redPoints.size() == 11); + for (const auto& rp : redPoints) { + CHECK(rp.attributes.contains("fill")); + CHECK(std::holds_alternative(rp.attributes.at("fill"))); + CHECK(std::get(rp.attributes.at("fill")) == "CB light red"); + } + + CHECK(greenPoints.size() == 8); + for (const auto& gp : greenPoints) { + CHECK(gp.attributes.contains("fill")); + CHECK(std::holds_alternative(gp.attributes.at("fill"))); + CHECK(std::get(gp.attributes.at("fill")) == "CB light green"); + } +} + //TEST_CASE("Manual check: load and save test_ipe_reader.ipe; geometries should be equivalent") { // IpeReader ipeReader; // IpeRenderer ipeRenderer; From 806831d5f9cfb5049a6c7eb6ae78ff30570adf8b Mon Sep 17 00:00:00 2001 From: Yvee1 Date: Tue, 7 Jul 2026 12:32:45 +0200 Subject: [PATCH 08/17] Add/fix circles and ellipses in ipe reader --- cartocrow/core/ellipse.h | 3 ++ cartocrow/reader/ipe_reader.h | 80 ++++++++++++++++++++++++++--------- data/test_ipe_reader.ipe | 23 +++++++++- test/reader/ipe_reader.cpp | 63 ++++++++++++++++++--------- 4 files changed, 128 insertions(+), 41 deletions(-) diff --git a/cartocrow/core/ellipse.h b/cartocrow/core/ellipse.h index fc6bc8f0..76911571 100644 --- a/cartocrow/core/ellipse.h +++ b/cartocrow/core/ellipse.h @@ -24,6 +24,9 @@ class Ellipse { Ellipse() : A(1), B(), C(1), D(), E(), F(-1) {} Ellipse(double a, double b, double c, double d, double e, double f); + Ellipse(Circle c) + : A(1), B(0), C(1), D(-2 * c.center().x()), E(-2 * c.center().y()), + F(c.center().x() * c.center().x() + c.center().y() * c.center().y() - c.squared_radius()) {} double angle() const; Point center() const; diff --git a/cartocrow/reader/ipe_reader.h b/cartocrow/reader/ipe_reader.h index 3607bd87..a4d7ed71 100644 --- a/cartocrow/reader/ipe_reader.h +++ b/cartocrow/reader/ipe_reader.h @@ -48,7 +48,7 @@ concept IpeReaderTraits = requires(ipe::Object& o, OutputIterator out) { using IntermediateIpeGeometry = std::variant< PolygonSetRaw, PolygonWithHoles, Polygon, PolylineSet, Polyline, Segment, - Point, PointSet, CubicBezierCurve, CubicBezierSpline, Ellipse>; + Point, PointSet, CubicBezierCurve, CubicBezierSpline, Circle, Ellipse>; template concept IpeReaderIntermediateGeometryConverter = requires(const IntermediateIpeGeometry& g, OutputIterator out) { @@ -106,13 +106,13 @@ struct IpeReaderIntermediateGeometryTraits { } if (ssp->type() != ipe::SubPath::ECurve) { allStraightSegments = false; - } - - const ipe::Curve* curve = ssp->asCurve(); - for (int j = 0; j < curve->countSegments() && allStraightSegments; ++j) { - ipe::CurveSegment segment = curve->segment(j); - if (segment.type() != ipe::CurveSegment::ESegment) { - allStraightSegments = false; + } else { + const ipe::Curve* curve = ssp->asCurve(); + for (int j = 0; j < curve->countSegments() && allStraightSegments; ++j) { + ipe::CurveSegment segment = curve->segment(j); + if (segment.type() != ipe::CurveSegment::ESegment) { + allStraightSegments = false; + } } } } @@ -189,18 +189,52 @@ struct IpeReaderIntermediateGeometryTraits { std::vector beziers; ssp->asClosedSpline()->beziers(beziers); for (auto bezier : beziers) { - spline.appendCurve(Point(bezier.iV[0].x, bezier.iV[0].y), - Point(bezier.iV[1].x, bezier.iV[1].y), - Point(bezier.iV[2].x, bezier.iV[2].y), - Point(bezier.iV[3].x, bezier.iV[3].y)); + auto c0T = matrix * bezier.iV[0]; + auto c1T = matrix * bezier.iV[1]; + auto c2T = matrix * bezier.iV[2]; + auto c3T = matrix * bezier.iV[3]; + spline.appendCurve(Point(c0T.x, c0T.y), + Point(c1T.x, c1T.y), + Point(c2T.x, c2T.y), + Point(c3T.x, c3T.y)); } *out++ = spline; } else if (ssp->type() == ipe::SubPath::EEllipse) { - auto matrix = ssp->asEllipse()->matrix(); - - Ellipse ellipse(matrix.a[0], matrix.a[1], matrix.a[2], matrix.a[3], matrix.a[4], - matrix.a[5]); - *out++ = ellipse; + auto ellipseMatrix = ssp->asEllipse()->matrix(); + auto finalMatrix = matrix * ellipseMatrix; + // Transposed linear map + auto a = finalMatrix.a[0]; + auto b = finalMatrix.a[2]; + auto c = finalMatrix.a[1]; + auto d = finalMatrix.a[3]; + // Translation + auto e = finalMatrix.a[4]; + auto f = finalMatrix.a[5]; + double det = a * d - b * c; + double ia = d / det; + double ib = -b / det; + double ic = -c / det; + double id = a / det; + double q11 = ia * ia + ic * ic; + double q12 = ia * ib + ic * id; + double q22 = ib * ib + id * id; + double tx = e; + double ty = f; + double A = q11; + double B = 2 * q12; + double C = q22; + double D = -2 * (q11 * tx + q12 * ty); + double E = -2 * (q12 * tx + q22 * ty); + double F = q11 * tx * tx + 2 * q12 * tx * ty + q22 * ty * ty - 1; + + if (std::abs(B) < M_EPSILON && std::abs(A - C) < M_EPSILON) { + Point center(-D / (2 * A), -E / (2 * A)); + Circle circle(center, center.x() * center.x() + center.y() * center.y() - F / A); + *out++ = circle; + } else { + Ellipse ellipse(A, B, C, D, E, F); + *out++ = ellipse; + } } else if (ssp->type() == ipe::SubPath::ECurve) { auto* curve = ssp->asCurve(); @@ -258,10 +292,14 @@ struct IpeReaderIntermediateGeometryTraits { // todo test if .beziers also converts circular arcs CubicBezierSpline spline; for (auto bezier : beziers) { - spline.appendCurve(Point(bezier.iV[0].x, bezier.iV[0].y), - Point(bezier.iV[1].x, bezier.iV[1].y), - Point(bezier.iV[2].x, bezier.iV[2].y), - Point(bezier.iV[3].x, bezier.iV[3].y)); + auto c0T = matrix * bezier.iV[0]; + auto c1T = matrix * bezier.iV[1]; + auto c2T = matrix * bezier.iV[2]; + auto c3T = matrix * bezier.iV[3]; + spline.appendCurve(Point(c0T.x, c0T.y), + Point(c1T.x, c1T.y), + Point(c2T.x, c2T.y), + Point(c3T.x, c3T.y)); } if (spline.numCurves() == 1) { diff --git a/data/test_ipe_reader.ipe b/data/test_ipe_reader.ipe index d5671b2c..f8f53943 100644 --- a/data/test_ipe_reader.ipe +++ b/data/test_ipe_reader.ipe @@ -1,7 +1,7 @@ - + @@ -561,4 +561,25 @@ h + + + + +24.6994 0 0 24.6994 78.1795 429.032 e + + +19.898 0 0 19.898 95.5769 405.205 e + + +12.7644 5.00189 17.7555 12.1402 121.05 80.6025 e + + + +5.95769 0 0 5.95769 112.634 55.5553 e + + +2.58731 0 0 2.58731 113.152 25.715 e + + + diff --git a/test/reader/ipe_reader.cpp b/test/reader/ipe_reader.cpp index bc03a827..10fd4d7c 100644 --- a/test/reader/ipe_reader.cpp +++ b/test/reader/ipe_reader.cpp @@ -158,22 +158,47 @@ TEST_CASE("Read attributes") { } } -//TEST_CASE("Manual check: load and save test_ipe_reader.ipe; geometries should be equivalent") { -// IpeReader ipeReader; -// IpeRenderer ipeRenderer; -// -// auto fn = "data/test_ipe_reader.ipe"; -// -// for (int pageIndex = 0; pageIndex < ipeReader.numberOfPages(fn); ++pageIndex) { -// ipeReader.setPage(pageIndex); -// auto geoms = ipeReader.readV(fn); -// ipeRenderer.addPainting([geoms](GeometryRenderer& r) { -// for (const auto& g : geoms) { -// std::visit([&](auto& someG) { r.draw(someG); }, g); -// } -// }); -// ipeRenderer.nextPage(); -// } -// -// ipeRenderer.save("test_ipe_reader_saved.ipe"); -//} \ No newline at end of file +TEST_CASE("Read ellipses and circles") { + std::filesystem::path fn("data/test_ipe_reader.ipe"); + + IpeReader ipeReader; + ipeReader.setPage(4); + + // The page has 5 ellipses, three of which are circles (two are grouped), one of which is a skewed circle (the object has a transformation matrix), the last is a 'proper' ipe ellipse. + // The two grouped circles should automatically be ungrouped. + + auto circles = ipeReader.readV>(fn); + CHECK(circles.size() == 3); + + auto ellipses = ipeReader.readV(fn); + CHECK(ellipses.size() == 5); +} + +TEST_CASE("Manual check: load and save test_ipe_reader.ipe; geometries should be equivalent") { + IpeReader ipeReader; + IpeRenderer ipeRenderer; + + auto fn = "data/test_ipe_reader.ipe"; + + for (int pageIndex = 0; pageIndex < ipeReader.numberOfPages(fn); ++pageIndex) { + ipeReader.setPage(pageIndex); + auto geoms = ipeReader.readV(fn); + ipeRenderer.addPainting([geoms](GeometryRenderer& r) { + for (const auto& g : geoms) { + std::visit([&](auto& someG) { r.draw(someG); }, g); + } + }); + ipeRenderer.nextPage(); + } + + ipeRenderer.addPainting([](GeometryRenderer& r) { + //Ellipse e; + //auto e_ = e.stretch(1/2.0, 1/7.0); + //auto e__ = e_.translateTo({12.0, 14.0}); + //r.draw(e__); + Ellipse e(0.3, 0.1, 0.1, -1, 0.6, -1); + r.draw(e); + }); + + ipeRenderer.save("test_ipe_reader_saved.ipe"); +} \ No newline at end of file From 3d74b6845087e74415a12ae4b6097209cfcdd6d6 Mon Sep 17 00:00:00 2001 From: Yvee1 Date: Tue, 7 Jul 2026 12:34:53 +0200 Subject: [PATCH 09/17] Forgot to press save... --- test/reader/ipe_reader.cpp | 47 +++++++++++++++----------------------- 1 file changed, 19 insertions(+), 28 deletions(-) diff --git a/test/reader/ipe_reader.cpp b/test/reader/ipe_reader.cpp index 10fd4d7c..e16c2b1d 100644 --- a/test/reader/ipe_reader.cpp +++ b/test/reader/ipe_reader.cpp @@ -174,31 +174,22 @@ TEST_CASE("Read ellipses and circles") { CHECK(ellipses.size() == 5); } -TEST_CASE("Manual check: load and save test_ipe_reader.ipe; geometries should be equivalent") { - IpeReader ipeReader; - IpeRenderer ipeRenderer; - - auto fn = "data/test_ipe_reader.ipe"; - - for (int pageIndex = 0; pageIndex < ipeReader.numberOfPages(fn); ++pageIndex) { - ipeReader.setPage(pageIndex); - auto geoms = ipeReader.readV(fn); - ipeRenderer.addPainting([geoms](GeometryRenderer& r) { - for (const auto& g : geoms) { - std::visit([&](auto& someG) { r.draw(someG); }, g); - } - }); - ipeRenderer.nextPage(); - } - - ipeRenderer.addPainting([](GeometryRenderer& r) { - //Ellipse e; - //auto e_ = e.stretch(1/2.0, 1/7.0); - //auto e__ = e_.translateTo({12.0, 14.0}); - //r.draw(e__); - Ellipse e(0.3, 0.1, 0.1, -1, 0.6, -1); - r.draw(e); - }); - - ipeRenderer.save("test_ipe_reader_saved.ipe"); -} \ No newline at end of file +//TEST_CASE("Manual check: load and save test_ipe_reader.ipe; geometries should be equivalent") { +// IpeReader ipeReader; +// IpeRenderer ipeRenderer; +// +// auto fn = "data/test_ipe_reader.ipe"; +// +// for (int pageIndex = 0; pageIndex < ipeReader.numberOfPages(fn); ++pageIndex) { +// ipeReader.setPage(pageIndex); +// auto geoms = ipeReader.readV(fn); +// ipeRenderer.addPainting([geoms](GeometryRenderer& r) { +// for (const auto& g : geoms) { +// std::visit([&](auto& someG) { r.draw(someG); }, g); +// } +// }); +// ipeRenderer.nextPage(); +// } +// +// ipeRenderer.save("test_ipe_reader_saved.ipe"); +//} \ No newline at end of file From fe05a7b953f274b78b6cdc96537dffe015602272 Mon Sep 17 00:00:00 2001 From: Yvee1 Date: Tue, 7 Jul 2026 13:53:08 +0200 Subject: [PATCH 10/17] Get region map reader to work with new ipe reader --- cartocrow/core/polygon_set_raw.h | 18 ++++++++- cartocrow/core/straight_geometry.h | 3 +- cartocrow/reader/CMakeLists.txt | 2 + cartocrow/reader/ipe_reader.h | 15 +++++++- cartocrow/reader/region_map_reader.cpp | 52 +++++++------------------- cartocrow/reader/region_map_reader.h | 19 ++++++++++ test/CMakeLists.txt | 2 + 7 files changed, 69 insertions(+), 42 deletions(-) diff --git a/cartocrow/core/polygon_set_raw.h b/cartocrow/core/polygon_set_raw.h index 9cc39d02..2ad43eb5 100644 --- a/cartocrow/core/polygon_set_raw.h +++ b/cartocrow/core/polygon_set_raw.h @@ -22,8 +22,22 @@ struct PolygonSetRaw { PolygonSet polygonSet() const { PolygonSet polygonSet; - for (const auto& pgn : polygons_with_holes) { - polygonSet.join(pgn); + for (auto pgn : polygons_with_holes) { + if (!pgn.outer_boundary().is_simple()) { + throw std::runtime_error("Encountered non-simple polygon"); + } + if (pgn.outer_boundary().is_clockwise_oriented()) { + pgn.outer_boundary().reverse_orientation(); + } + for (auto& hole : pgn.holes()) { + if (!hole.is_simple()) { + throw std::runtime_error("Encountered non-simple polygon"); + } + if (hole.is_counterclockwise_oriented()) { + hole.reverse_orientation(); + } + } + polygonSet.symmetric_difference(pgn); } return polygonSet; } diff --git a/cartocrow/core/straight_geometry.h b/cartocrow/core/straight_geometry.h index a871834e..91df6d3f 100644 --- a/cartocrow/core/straight_geometry.h +++ b/cartocrow/core/straight_geometry.h @@ -24,8 +24,9 @@ along with this program. If not, see . namespace cartocrow { /// Type that serves as an internal representation of the straight (i.e. linear) features of the OGC Simple Feature Access. -/// OGC name: MultiPolygon Polygon LinearRing MultiLineString LineString Point MultiPoint +/// OGC name: MultiPolygon Polygon LinearRing template using StraightGeometry = std::variant, PolygonWithHoles, Polygon, +// MultiLineString LineString Point MultiPoint PolylineSet, Polyline, Point, PointSet>; } \ No newline at end of file diff --git a/cartocrow/reader/CMakeLists.txt b/cartocrow/reader/CMakeLists.txt index 4e7ff4d5..554fc1c6 100644 --- a/cartocrow/reader/CMakeLists.txt +++ b/cartocrow/reader/CMakeLists.txt @@ -1,11 +1,13 @@ set(SOURCES gdal_conversion.cpp boundary_map_reader.cpp + region_map_reader.cpp ) set(HEADERS ipe_reader.h gdal_conversion.h boundary_map_reader.h + region_map_reader.h ) add_library(reader ${SOURCES}) diff --git a/cartocrow/reader/ipe_reader.h b/cartocrow/reader/ipe_reader.h index a4d7ed71..883ca9f0 100644 --- a/cartocrow/reader/ipe_reader.h +++ b/cartocrow/reader/ipe_reader.h @@ -398,12 +398,25 @@ class IpeReader { return std::shared_ptr(document); } - Color convertIpeColor(ipe::Color color) { + static Color convertIpeColor(ipe::Color color) { return Color{ static_cast(color.iRed.toDouble() * 255), static_cast(color.iGreen.toDouble() * 255), static_cast(color.iBlue.toDouble() * 255) }; } + static Color convertStringToColor(std::string s, std::filesystem::path path) { + std::istringstream iss(s); + double r, g, b; + if (!(iss >> r >> g >> b)) { + auto doc = loadIpeFile(path); + ipe::Attribute attr(true, ipe::String(s.data())); + return convertIpeColor(doc->cascade()->find(ipe::Kind::EColor, attr).color()); + } + + return {static_cast(std::lround(r * 255)), static_cast(std::lround(g * 255)), + static_cast(std::lround(b * 255))}; + } + // ===== Ipe reader specific functions ===== /// Set the page to read from. /// Note! Page indices start at zero (so pass one integer smaller than the one the ipe GUI shows). diff --git a/cartocrow/reader/region_map_reader.cpp b/cartocrow/reader/region_map_reader.cpp index d0045832..e5479e09 100644 --- a/cartocrow/reader/region_map_reader.cpp +++ b/cartocrow/reader/region_map_reader.cpp @@ -33,46 +33,24 @@ namespace cartocrow { RegionMap ipeToRegionMap(const std::filesystem::path& file, bool labelAtCentroid) { RegionMap regions; - std::shared_ptr document = IpeReader::loadIpeFile(file); + IpeReader reader; - if (document->countPages() == 0) { + int numPages = reader.numberOfPages(file); + if (numPages == 0) { throw std::runtime_error("Cannot read map from an Ipe file with no pages"); - } else if (document->countPages() > 1) { + } else if (numPages > 1) { throw std::runtime_error("Cannot read map from an Ipe file with more than one page"); } - ipe::Page* page = document->page(0); - // step 1: find labels - std::vector labels; - - for (int i = 0; i < page->count(); ++i) { - ipe::Object* object = page->object(i); - ipe::Object::Type type = object->type(); - if (type != ipe::Object::Type::EText) { - continue; - } - ipe::Matrix matrix = object->matrix(); - ipe::Vector translation = matrix * object->asText()->position(); - Point position(translation.x, translation.y); - ipe::String ipeString = object->asText()->text(); - std::string text(ipeString.data(), ipeString.size()); - labels.push_back(detail::RegionLabel{position, text, false}); - } - + auto labels = reader.readV(file); + // step 2: find regions - for (int i = 0; i < page->count(); ++i) { - ipe::Object* object = page->object(i); - int layer = page->layerOf(i); - ipe::Object::Type type = object->type(); - if (type != ipe::Object::Type::EPath) { - continue; - } - ipe::Path* path = object->asPath(); - ipe::Matrix matrix = path->matrix(); - ipe::Shape ipeShape = path->shape(); - // interpret filled paths as regions - PolygonSet shape = cartocrow::IpeReader::convertShapeToPolygonSet(ipeShape, matrix); + // interpret filled paths as regions + auto features = reader.readWithAttributesV>(file); + + for (auto& feature : features) { + auto shape = pretendExact(feature.geometry).polygonSet(); std::string name; if (labelAtCentroid) { auto& label = findLabelAtCentroid(shape, labels); @@ -112,11 +90,9 @@ RegionMap ipeToRegionMap(const std::filesystem::path& file, bool labelAtCentroid } else { Region region; region.name = name; - if (path->fill().isSymbolic()) { - region.color = IpeReader::convertIpeColor( - document->cascade()->find(ipe::Kind::EColor, path->fill()).color()); - } else { - region.color = IpeReader::convertIpeColor(path->fill().color()); + if (feature.attributes.contains("fill")) { + auto colorString = std::get(feature.attributes["fill"]); + region.color = IpeReader::convertStringToColor(colorString, file); } region.shape = shape; regions[name] = region; diff --git a/cartocrow/reader/region_map_reader.h b/cartocrow/reader/region_map_reader.h index 08e5fbec..19a1b1b5 100644 --- a/cartocrow/reader/region_map_reader.h +++ b/cartocrow/reader/region_map_reader.h @@ -17,9 +17,28 @@ along with this program. If not, see . #pragma once +#include "ipe_reader.h" #include "cartocrow/core/region_map.h" namespace cartocrow { +namespace { +struct RegionLabelReaderTraits { + template + static void convert(ipe::Object& object, OutputIterator out) { + ipe::Object::Type type = object.type(); + if (type != ipe::Object::Type::EText) { + return; + } + ipe::Matrix matrix = object.matrix(); + ipe::Vector translation = matrix * object.asText()->position(); + Point position(translation.x, translation.y); + ipe::String ipeString = object.asText()->text(); + std::string text(ipeString.data(), ipeString.size()); + *out++ = detail::RegionLabel{position, text, false}; + } +}; +} + /// Creates a \ref RegionMap from a region map in Ipe format. /// /// The Ipe figure to be read needs to contain a single page. This page diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index a04a7d1a..ffec8d6c 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -2,6 +2,8 @@ set(TEST_SOURCES "cartocrow_test.cpp" "core/cubic_bezier.cpp" "core/core.cpp" "core/polygon_helpers.cpp" + "core/region_arrangement.cpp" + "core/region_map.cpp" "renderer/ipe_renderer.cpp" "reader/ipe_reader.cpp" ) From e7c13423fd0a31cf6f9625c7edd16ed257cb1a7d Mon Sep 17 00:00:00 2001 From: Yvee1 Date: Mon, 10 Aug 2026 15:59:17 +0200 Subject: [PATCH 11/17] Use template params to specify cardinality and attribute options in GeometryReader --- cartocrow/reader/geometry_reader.h | 57 ++++++++-- cartocrow/reader/ipe_reader.h | 146 ++++++++++++------------- cartocrow/reader/region_map_reader.cpp | 4 +- cartocrow/reader/region_map_reader.h | 5 +- test/reader/ipe_reader.cpp | 53 ++++++--- 5 files changed, 159 insertions(+), 106 deletions(-) diff --git a/cartocrow/reader/geometry_reader.h b/cartocrow/reader/geometry_reader.h index 48cb3c9d..2e02634c 100644 --- a/cartocrow/reader/geometry_reader.h +++ b/cartocrow/reader/geometry_reader.h @@ -32,34 +32,75 @@ template concept GeometryReader = requires(R reader, std::filesystem:: {reader.canRead(path)}->std::same_as; }; +struct Single {}; +struct Multiple {}; +struct WithoutAttributes {}; +struct WithAttributes{}; + +template +struct ElementType { + using type = Geometry; +}; + +template +struct ElementType { + using type = GeometricFeature; +}; + +template +using ElementTypeT = typename ElementType::type; + +template +struct CardinalityType { + using type = std::vector; +}; + +template +struct CardinalityType { + using type = std::optional; +}; + +template +using CardinalityTypeT = + typename CardinalityType::type; + +template +using ReadResultT = + CardinalityTypeT< + ElementTypeT, + Cardinality>; + //GeometryReader R -template +template concept GeometryReaderFor = - GeometryReader && requires(R reader, std::filesystem::path path, OutputIterator out) { + GeometryReader && requires(R reader, std::filesystem::path path, std::back_insert_iterator> outG, + std::back_insert_iterator>> outGF) { /// Returns all geometries in the provided file that are convertible to Geometry. /// \pre canRead(path) - reader.template read(path, out); + reader.template read(path, outG); /// Returns a vector with all geometries in the provided file that are convertible to Geometry. /// \pre canRead(path) - {reader.template readV(path)}->std::same_as>; + {reader.template read(path)}->std::same_as>; /// Returns the first geometry in the provided file that is convertible to Geometry. /// \pre canRead(path) - {reader.template readSingle(path)}->std::same_as>; + {reader.template read(path)}->std::same_as>; /// Returns geometries in the provided file that are convertible to Geometry including their attributes. /// Outputs Feature. /// \pre canRead(path) - reader.template readWithAttributes(path, out); + reader.template read(path, outGF); /// Returns a vector with all geometries in the provided file that are convertible to Geometry including their attributes. /// \pre canRead(path) - {reader.template readWithAttributesV(path)}->std::same_as>>; + {reader.template read(path)}->std::same_as>>; /// Returns the first feature in the provided file that is convertible to Geometry. /// \pre canRead(path) - {reader.template readSingleWithAttributes(path)}->std::same_as>>; + {reader.template read(path)}->std::same_as>>; }; } \ No newline at end of file diff --git a/cartocrow/reader/ipe_reader.h b/cartocrow/reader/ipe_reader.h index 883ca9f0..68f22eb3 100644 --- a/cartocrow/reader/ipe_reader.h +++ b/cartocrow/reader/ipe_reader.h @@ -41,7 +41,7 @@ along with this program. If not, see . namespace cartocrow { template concept IpeReaderTraits = requires(ipe::Object& o, OutputIterator out) { - { Traits::template convert(o, out) }; + { Traits::template convert(o, out) }->std::same_as; }; // todo: make a RenderPath reader traits that parses everything to a render path? @@ -52,7 +52,7 @@ using IntermediateIpeGeometry = std::variant< template concept IpeReaderIntermediateGeometryConverter = requires(const IntermediateIpeGeometry& g, OutputIterator out) { - { Traits::template convert(g, out) }; + { Traits::template convert(g, out) }->std::same_as; }; /// is a model of IpeReaderTraits @@ -315,13 +315,15 @@ struct IpeReaderIntermediateGeometryTraits { } template - static void convert(ipe::Object& o, OutputIterator out) { + static bool convert(ipe::Object& o, OutputIterator out) { std::vector intermediates; convertToIntermediate(o, std::back_inserter(intermediates)); for (const auto& intermediate : intermediates) { Converter::convert(intermediate, out); } + + return !intermediates.empty(); } }; @@ -329,16 +331,19 @@ struct IpeReaderIntermediateGeometryTraits { template struct BasicIpeReaderTraitsConverter { template - static void convert(const IntermediateIpeGeometry& g, OutputIterator out) { + static bool convert(const IntermediateIpeGeometry& g, OutputIterator out) { + bool convertedSomething = false; std::visit( [&](auto&& g) { using T = std::decay_t; if constexpr (std::is_convertible_v) { *out++ = Geometry{g}; + convertedSomething = true; } }, g); + return convertedSomething; } }; @@ -538,103 +543,90 @@ class IpeReader { return path.extension() == ".ipe"; } - /// Returns all geometries in the provided file that are convertible to Geometry. + /// Returns geometries in the provided file that are convertible to Geometry. /// \pre canRead(path) template < + class Cardinality, class Geometry, - class OutputIterator, + class AttrMode, class Traits = BasicIpeReaderTraits > - requires IpeReaderTraits - void read(std::filesystem::path path, OutputIterator out) { - readHelper(path, [&](ipe::Page* page, int i) { - ipe::Object* object = page->object(i); - Traits::convert(*object, out); - return false; - }); - } - - /// Returns a vector with all geometries in the provided file that are convertible to Geometry. - /// \pre canRead(path) - template > - requires IpeReaderTraits>, Traits> - std::vector readV(std::filesystem::path path) { - std::vector gs; - read>, Traits>(path, std::back_inserter(gs)); - return gs; - } - - /// Returns the first geometry in the provided file that is convertible to Geometry. - /// \pre canRead(path) - template > - requires IpeReaderTraits>, Traits> - std::optional readSingle(std::filesystem::path path) { - std::vector gs; + requires IpeReaderTraits>, Traits> + ReadResultT read(std::filesystem::path path) { + std::vector> gs; readHelper(path, [&](ipe::Page* page, int i) { ipe::Object* object = page->object(i); - Traits::convert(*object, std::back_inserter(gs)); - return !gs.empty(); // stop if a geometry is found - }); - - return gs.empty() ? std::nullopt : std::optional(gs[0]); - } - - /// Returns geometries in the provided file that are convertible to Geometry including their attributes. - /// Outputs Feature. - /// \pre canRead(path) - template > - requires IpeReaderTraits>, Traits> - void readWithAttributes(std::filesystem::path path, OutputIterator out) { - readHelper(path, [&](ipe::Page* page, int i) { - ipe::Object* object = page->object(i); - auto attributes = getAttributes(page, i); + if constexpr (std::same_as) { + Traits::convert(*object, std::back_inserter(gs)); + if constexpr (std::same_as) { + return !gs.empty(); // stop if a geometry is found + } else { + return false; + } + } else { + auto attributes = getAttributes(page, i); - std::vector gs; - Traits::convert(*object, std::back_inserter(gs)); - for (auto& g : gs) { - *out++ = GeometricFeature(std::move(g), attributes); + std::vector temps; + Traits::convert(*object, std::back_inserter(temps)); + for (auto& t : temps) { + gs.emplace_back(std::move(t), attributes); + } + if constexpr (std::same_as) { + return !gs.empty(); // stop if a geometry is found + } else { + return false; + } } - return false; }); - } - /// Returns a vector with all geometries in the provided file that are convertible to Geometry including their attributes. - /// \pre canRead(path) - template > - requires IpeReaderTraits>, Traits> - std::vector> readWithAttributesV(std::filesystem::path path) { - std::vector> gs; - readWithAttributes>>, Traits>(path, std::back_inserter(gs)); - return gs; + if constexpr (std::same_as) { + return gs.empty() ? std::nullopt : std::optional>(gs.front()); + } else { + return gs; + } } - /// Returns the first feature in the provided file that is convertible to Geometry. + /// Returns geometries in the provided file that are convertible to Geometry. /// \pre canRead(path) - template > - requires IpeReaderTraits>, Traits> - std::optional> readSingleWithAttributes(std::filesystem::path path) { - std::vector> fs; - + template < + class Cardinality, + class Geometry, + class AttrMode, + class OutputIterator, + class Traits = BasicIpeReaderTraits + > + requires IpeReaderTraits>, Traits> + void read(std::filesystem::path path, OutputIterator out) { readHelper(path, [&](ipe::Page* page, int i) { ipe::Object* object = page->object(i); - auto attributes = getAttributes(page, i); + if constexpr (std::same_as) { + auto convertedSomething = Traits::convert(*object, out); + if constexpr (std::same_as) { + return convertedSomething; // stop if a geometry is found + } else { + return false; + } + } else { + auto attributes = getAttributes(page, i); - std::vector gs; - Traits::convert(*object, std::back_inserter(gs)); - for (auto& g : gs) { - fs.emplace_back(std::move(g), attributes); + std::vector temps; + Traits::convert(*object, std::back_inserter(temps)); + for (auto& t : temps) { + *out++ = GeometricFeature(std::move(t), attributes); + } + if constexpr (std::same_as) { + return !temps.empty(); // stop if a geometry is found + } else { + return false; + } } - return !gs.empty(); // stop if a geometry is found }); - - return fs.empty() ? std::nullopt : fs[0]; } }; namespace { static_assert(GeometryReader); -using Out = std::back_insert_iterator>>; -static_assert(GeometryReaderFor, Out>); +static_assert(GeometryReaderFor>); } } \ No newline at end of file diff --git a/cartocrow/reader/region_map_reader.cpp b/cartocrow/reader/region_map_reader.cpp index e5479e09..116a06da 100644 --- a/cartocrow/reader/region_map_reader.cpp +++ b/cartocrow/reader/region_map_reader.cpp @@ -43,11 +43,11 @@ RegionMap ipeToRegionMap(const std::filesystem::path& file, bool labelAtCentroid } // step 1: find labels - auto labels = reader.readV(file); + auto labels = reader.read(file); // step 2: find regions // interpret filled paths as regions - auto features = reader.readWithAttributesV>(file); + auto features = reader.read, WithAttributes>(file); for (auto& feature : features) { auto shape = pretendExact(feature.geometry).polygonSet(); diff --git a/cartocrow/reader/region_map_reader.h b/cartocrow/reader/region_map_reader.h index 19a1b1b5..2a2cc7eb 100644 --- a/cartocrow/reader/region_map_reader.h +++ b/cartocrow/reader/region_map_reader.h @@ -24,10 +24,10 @@ namespace cartocrow { namespace { struct RegionLabelReaderTraits { template - static void convert(ipe::Object& object, OutputIterator out) { + static bool convert(ipe::Object& object, OutputIterator out) { ipe::Object::Type type = object.type(); if (type != ipe::Object::Type::EText) { - return; + return false; } ipe::Matrix matrix = object.matrix(); ipe::Vector translation = matrix * object.asText()->position(); @@ -35,6 +35,7 @@ struct RegionLabelReaderTraits { ipe::String ipeString = object.asText()->text(); std::string text(ipeString.data(), ipeString.size()); *out++ = detail::RegionLabel{position, text, false}; + return true; } }; } diff --git a/test/reader/ipe_reader.cpp b/test/reader/ipe_reader.cpp index e16c2b1d..26c3a10f 100644 --- a/test/reader/ipe_reader.cpp +++ b/test/reader/ipe_reader.cpp @@ -26,7 +26,7 @@ using namespace renderer; TEST_CASE("Reading points") { IpeReader ipeReader; - auto points = ipeReader.readV>("data/test_ipe_reader.ipe"); + auto points = ipeReader.read, WithoutAttributes>("data/test_ipe_reader.ipe"); CHECK(points.size() == 4); auto exists = [&](Point point) { @@ -44,7 +44,7 @@ TEST_CASE("Reading a polygon") { Polygon expectedPolygon(points.begin(), points.end()); IpeReader ipeReader; - auto parsedPolygon = ipeReader.readSingle>("data/test_ipe_reader.ipe"); + auto parsedPolygon = ipeReader.read, WithoutAttributes>("data/test_ipe_reader.ipe"); CHECK(parsedPolygon == expectedPolygon); } @@ -56,7 +56,7 @@ TEST_CASE("Reading points and a polygon") { using PointOrPoly = std::variant, Polygon>; IpeReader ipeReader; - auto pointOrPolys = ipeReader.readV("data/test_ipe_reader.ipe"); + auto pointOrPolys = ipeReader.read("data/test_ipe_reader.ipe"); CHECK(pointOrPolys.size() == 5); @@ -77,8 +77,8 @@ TEST_CASE("Reading polygon sets") { IpeReader ipeReader; ipeReader.setPage(1); - auto psrs = ipeReader.readV>("data/test_ipe_reader.ipe"); - auto pgns = ipeReader.readV>("data/test_ipe_reader.ipe"); + auto psrs = ipeReader.read, WithoutAttributes>("data/test_ipe_reader.ipe"); + auto pgns = ipeReader.read, WithoutAttributes>("data/test_ipe_reader.ipe"); CHECK(psrs.size() == 3); CHECK(pgns.size() == 1); @@ -92,16 +92,16 @@ TEST_CASE("Reading polylines, segments, Bézier curves and splines") { IpeReader ipeReader; ipeReader.setPage(2); - auto ls = ipeReader.readV>(fn); + auto ls = ipeReader.read, WithoutAttributes>(fn); CHECK(ls.size() == 1); // should only return the line segment - auto pls = ipeReader.readV>(fn); + auto pls = ipeReader.read, WithoutAttributes>(fn); CHECK(pls.size() == 2); // should return the polyline and the line segment - auto cbcs = ipeReader.readV(fn); + auto cbcs = ipeReader.read(fn); CHECK(cbcs.size() == 2); // should return the cubic Bézier curve and the line segment - auto cbss = ipeReader.readV(fn); + auto cbss = ipeReader.read(fn); CHECK(cbss.size() == 4); // should return all } @@ -112,17 +112,36 @@ TEST_CASE("Read points from specific layer") { ipeReader.setPage(3); ipeReader.setLayerFilter("red"); - auto redPoints = ipeReader.readV>(fn); + auto redPoints = ipeReader.read, WithoutAttributes>(fn); ipeReader.setLayerFilter(0); - auto bluePoints = ipeReader.readV>(fn); + auto bluePoints = ipeReader.read, WithoutAttributes>(fn); ipeReader.setLayerFilter("green"); - auto greenPoints = ipeReader.readV>(fn); + auto greenPoints = ipeReader.read, WithoutAttributes>(fn); CHECK(bluePoints.size() == 7); CHECK(redPoints.size() == 11); CHECK(greenPoints.size() == 8); } +TEST_CASE("Read with output iterator") { + std::filesystem::path fn("data/test_ipe_reader.ipe"); + + IpeReader ipeReader; + ipeReader.setPage(3); + + std::vector> allPoints; + ipeReader.setLayerFilter("red"); + ipeReader.read, WithoutAttributes>(fn, std::back_inserter(allPoints)); + ipeReader.setLayerFilter(0); + ipeReader.read, WithoutAttributes>(fn, std::back_inserter(allPoints)); + ipeReader.setLayerFilter("green"); + ipeReader.read, WithoutAttributes>(fn, std::back_inserter(allPoints)); + + ipeReader.read, WithoutAttributes>(fn, std::back_inserter(allPoints)); + + CHECK(allPoints.size() == 27); +} + TEST_CASE("Read attributes") { std::filesystem::path fn("data/test_ipe_reader.ipe"); @@ -130,11 +149,11 @@ TEST_CASE("Read attributes") { ipeReader.setPage(3); ipeReader.setLayerFilter("red"); - auto redPoints = ipeReader.readWithAttributesV>(fn); + auto redPoints = ipeReader.read, WithAttributes>(fn); ipeReader.setLayerFilter(0); - auto bluePoints = ipeReader.readWithAttributesV>(fn); + auto bluePoints = ipeReader.read, WithAttributes>(fn); ipeReader.setLayerFilter("green"); - auto greenPoints = ipeReader.readWithAttributesV>(fn); + auto greenPoints = ipeReader.read, WithAttributes>(fn); CHECK(bluePoints.size() == 7); for (const auto& bp : bluePoints) { @@ -167,10 +186,10 @@ TEST_CASE("Read ellipses and circles") { // The page has 5 ellipses, three of which are circles (two are grouped), one of which is a skewed circle (the object has a transformation matrix), the last is a 'proper' ipe ellipse. // The two grouped circles should automatically be ungrouped. - auto circles = ipeReader.readV>(fn); + auto circles = ipeReader.read, WithoutAttributes>(fn); CHECK(circles.size() == 3); - auto ellipses = ipeReader.readV(fn); + auto ellipses = ipeReader.read(fn); CHECK(ellipses.size() == 5); } From 308a27169f3d82a45589c456fd5ea4840ed4256f Mon Sep 17 00:00:00 2001 From: Yvee1 Date: Thu, 13 Aug 2026 16:43:35 +0200 Subject: [PATCH 12/17] Require file path in constructor of reader rather than in the read method --- cartocrow/reader/geometry_reader.h | 18 ++--- cartocrow/reader/ipe_reader.h | 102 ++++++++++++++++++------- cartocrow/reader/region_map_reader.cpp | 8 +- test/reader/ipe_reader.cpp | 77 +++++++++---------- 4 files changed, 124 insertions(+), 81 deletions(-) diff --git a/cartocrow/reader/geometry_reader.h b/cartocrow/reader/geometry_reader.h index 2e02634c..6cfaebb7 100644 --- a/cartocrow/reader/geometry_reader.h +++ b/cartocrow/reader/geometry_reader.h @@ -26,10 +26,10 @@ along with this program. If not, see . namespace cartocrow { template concept GeometryReader = requires(R reader, std::filesystem::path path) { /// If it exists, return the well-known text representation (WKT) of the coordinate reference system - {reader.readSpatialReference(path)}->std::same_as>; + {reader.readSpatialReference()}->std::same_as>; /// Returns whether the reader can parse the given file. - {reader.canRead(path)}->std::same_as; + {R::canRead(path)}->std::same_as; }; struct Single {}; @@ -75,32 +75,32 @@ using ReadResultT = //GeometryReader R template concept GeometryReaderFor = - GeometryReader && requires(R reader, std::filesystem::path path, std::back_insert_iterator> outG, + GeometryReader && requires(R reader, std::back_insert_iterator> outG, std::back_insert_iterator>> outGF) { /// Returns all geometries in the provided file that are convertible to Geometry. /// \pre canRead(path) - reader.template read(path, outG); + reader.template read(outG); /// Returns a vector with all geometries in the provided file that are convertible to Geometry. /// \pre canRead(path) - {reader.template read(path)}->std::same_as>; + {reader.template read()}->std::same_as>; /// Returns the first geometry in the provided file that is convertible to Geometry. /// \pre canRead(path) - {reader.template read(path)}->std::same_as>; + {reader.template read()}->std::same_as>; /// Returns geometries in the provided file that are convertible to Geometry including their attributes. /// Outputs Feature. /// \pre canRead(path) - reader.template read(path, outGF); + reader.template read(outGF); /// Returns a vector with all geometries in the provided file that are convertible to Geometry including their attributes. /// \pre canRead(path) - {reader.template read(path)}->std::same_as>>; + {reader.template read()}->std::same_as>>; /// Returns the first feature in the provided file that is convertible to Geometry. /// \pre canRead(path) - {reader.template read(path)}->std::same_as>>; + {reader.template read()}->std::same_as>>; }; } \ No newline at end of file diff --git a/cartocrow/reader/ipe_reader.h b/cartocrow/reader/ipe_reader.h index 68f22eb3..d56f485d 100644 --- a/cartocrow/reader/ipe_reader.h +++ b/cartocrow/reader/ipe_reader.h @@ -359,6 +359,8 @@ namespace { // models GeometryReader and GeometryReaderFor every Geometry class IpeReader { private: + /// The current file that is being read. + std::shared_ptr m_document; /// The page to read from int m_pageNumber = 0; /// The layer to read from. If std::nullopt then it reads from all layers. @@ -426,6 +428,18 @@ class IpeReader { /// Set the page to read from. /// Note! Page indices start at zero (so pass one integer smaller than the one the ipe GUI shows). void setPage(int pageNumber) { + if (m_pageNumber >= m_document->countPages()) { + std::cerr << "Page number exceeds document page count." << std::endl; + std::cerr << "Setting page number to last page." << std::endl; + m_pageNumber = m_document->countPages() - 1; + return; + } else if (m_pageNumber < 0) { + std::cerr << "Ppage number is negative." << std::endl; + std::cerr << "Setting page number to first page." << std::endl; + m_pageNumber = 0; + return; + } + m_pageNumber = pageNumber; } @@ -445,18 +459,58 @@ class IpeReader { } /// Return the number of pages in the ipe document - int numberOfPages(std::filesystem::path path) { - auto doc = loadIpeFile(path); - return doc->countPages(); + int numberOfPages() const { + return m_document->countPages(); } - /// Number of layers - int numberOfLayer(std::filesystem::path path, int pageIndex) { - auto doc = loadIpeFile(path); - auto page = doc->page(pageIndex); + /// Number of layers of a page. + int numberOfLayers(int pageIndex) const { + auto page = m_document->page(pageIndex); return page->countLayers(); } + /// Number of layers of the current page. + int numberOfLayers() const { + return numberOfLayers(m_pageNumber); + } + + /// Returns the name of a layer of a page. + std::string layerName(int pageIndex, int layerIndex) const { + auto ln = m_document->page(pageIndex)->layer(layerIndex); + return std::string(ln.data(), ln.size()); + } + + /// Returns the name of a layer of the current page. + std::string layerName(int layerIndex) const { + return layerName(m_pageNumber, layerIndex); + } + + /// Outputs the names of the layers of a page. + template + void layerNames(int pageIndex, OutputIterator out) const { + for (int i = 0; i < numberOfLayers(pageIndex); ++i) { + *out++ = layerName(i); + } + } + + /// Returns the names of the layers of a page. + std::vector layerNames(int pageIndex) const { + std::vector names; + layerNames(pageIndex, std::back_inserter(names)); + return names; + } + + /// Outputs the names of the layers of the current page. + template + void layerNames(OutputIterator out) const { + return layerNames(m_pageNumber, out); + } + + /// Returns the names of the layers of the current page. + std::vector layerNames() const { + return layerNames(m_pageNumber); + } + private: /// Whether to skip the ipe object with index i in the given page. bool skipObject(ipe::Page* page, int i) const { @@ -466,7 +520,8 @@ class IpeReader { if (layerIndex != *layerIndexP) return true; // object is not on layer so we skip } else if (auto* layerNameP = std::get_if(&*m_layer)) { - if (*page->layer(layerIndex).data() != *layerNameP->c_str()) { + auto ln = page->layer(layerIndex); + if (std::string(ln.data(), ln.size()) != *layerNameP) { return true; } } @@ -504,20 +559,8 @@ class IpeReader { } /// If handle returns true the parsing stops. - void readHelper(std::filesystem::path path, std::function handle) { - std::shared_ptr document = IpeReader::loadIpeFile(path); - - if (m_pageNumber >= document->countPages()) { - std::cerr << "Current page number exceeds document page count." << std::endl; - std::cerr << "Setting page number to last page." << std::endl; - m_pageNumber = document->countPages() - 1; - } else if (m_pageNumber < 0) { - std::cerr << "Current page number is negative." << std::endl; - std::cerr << "Setting page number to first page." << std::endl; - m_pageNumber = 0; - } - - ipe::Page* page = document->page(m_pageNumber); + void readHelper(std::function handle) { + ipe::Page* page = m_document->page(m_pageNumber); for (int i = 0; i < page->count(); ++i) { if (skipObject(page, i)) @@ -530,16 +573,19 @@ class IpeReader { public: // ===== Reader methods ===== + IpeReader(const std::filesystem::path& filename) { + m_document = loadIpeFile(filename); + } /// If it exists, return the well-known text representation (WKT) of the coordinate reference system - std::optional readSpatialReference(std::filesystem::path path) { + std::optional readSpatialReference() { return std::nullopt; } /// Returns whether the reader can parse the given file. /// Currently only checks the file extension. // todo: actually try to parse to ipe document? - bool canRead(std::filesystem::path path) { + static bool canRead(std::filesystem::path path) { return path.extension() == ".ipe"; } @@ -552,10 +598,10 @@ class IpeReader { class Traits = BasicIpeReaderTraits > requires IpeReaderTraits>, Traits> - ReadResultT read(std::filesystem::path path) { + ReadResultT read() { std::vector> gs; - readHelper(path, [&](ipe::Page* page, int i) { + readHelper([&](ipe::Page* page, int i) { ipe::Object* object = page->object(i); if constexpr (std::same_as) { Traits::convert(*object, std::back_inserter(gs)); @@ -597,8 +643,8 @@ class IpeReader { class Traits = BasicIpeReaderTraits > requires IpeReaderTraits>, Traits> - void read(std::filesystem::path path, OutputIterator out) { - readHelper(path, [&](ipe::Page* page, int i) { + void read(OutputIterator out) { + readHelper([&](ipe::Page* page, int i) { ipe::Object* object = page->object(i); if constexpr (std::same_as) { auto convertedSomething = Traits::convert(*object, out); diff --git a/cartocrow/reader/region_map_reader.cpp b/cartocrow/reader/region_map_reader.cpp index 116a06da..13df45b6 100644 --- a/cartocrow/reader/region_map_reader.cpp +++ b/cartocrow/reader/region_map_reader.cpp @@ -33,9 +33,9 @@ namespace cartocrow { RegionMap ipeToRegionMap(const std::filesystem::path& file, bool labelAtCentroid) { RegionMap regions; - IpeReader reader; + IpeReader reader(file); - int numPages = reader.numberOfPages(file); + int numPages = reader.numberOfPages(); if (numPages == 0) { throw std::runtime_error("Cannot read map from an Ipe file with no pages"); } else if (numPages > 1) { @@ -43,11 +43,11 @@ RegionMap ipeToRegionMap(const std::filesystem::path& file, bool labelAtCentroid } // step 1: find labels - auto labels = reader.read(file); + auto labels = reader.read(); // step 2: find regions // interpret filled paths as regions - auto features = reader.read, WithAttributes>(file); + auto features = reader.read, WithAttributes>(); for (auto& feature : features) { auto shape = pretendExact(feature.geometry).polygonSet(); diff --git a/test/reader/ipe_reader.cpp b/test/reader/ipe_reader.cpp index 26c3a10f..97746fc3 100644 --- a/test/reader/ipe_reader.cpp +++ b/test/reader/ipe_reader.cpp @@ -24,9 +24,9 @@ using namespace cartocrow; using namespace renderer; TEST_CASE("Reading points") { - IpeReader ipeReader; + IpeReader ipeReader("data/test_ipe_reader.ipe"); - auto points = ipeReader.read, WithoutAttributes>("data/test_ipe_reader.ipe"); + auto points = ipeReader.read, WithoutAttributes>(); CHECK(points.size() == 4); auto exists = [&](Point point) { @@ -43,8 +43,8 @@ TEST_CASE("Reading a polygon") { std::vector> points({{144.544, 155.39}, {123.907, 113.135}, {178.446, 67.44}, {249.199, 124.927}}); Polygon expectedPolygon(points.begin(), points.end()); - IpeReader ipeReader; - auto parsedPolygon = ipeReader.read, WithoutAttributes>("data/test_ipe_reader.ipe"); + IpeReader ipeReader("data/test_ipe_reader.ipe"); + auto parsedPolygon = ipeReader.read, WithoutAttributes>(); CHECK(parsedPolygon == expectedPolygon); } @@ -55,8 +55,8 @@ TEST_CASE("Reading points and a polygon") { using PointOrPoly = std::variant, Polygon>; - IpeReader ipeReader; - auto pointOrPolys = ipeReader.read("data/test_ipe_reader.ipe"); + IpeReader ipeReader("data/test_ipe_reader.ipe"); + auto pointOrPolys = ipeReader.read(); CHECK(pointOrPolys.size() == 5); @@ -74,49 +74,45 @@ TEST_CASE("Reading points and a polygon") { TEST_CASE("Reading polygon sets") { // Test whether polygon is automatically converted to PolygonSetRaw. // The file contains 2 polygon sets and 1 polygon. - IpeReader ipeReader; + IpeReader ipeReader("data/test_ipe_reader.ipe"); ipeReader.setPage(1); - auto psrs = ipeReader.read, WithoutAttributes>("data/test_ipe_reader.ipe"); - auto pgns = ipeReader.read, WithoutAttributes>("data/test_ipe_reader.ipe"); + auto psrs = ipeReader.read, WithoutAttributes>(); + auto pgns = ipeReader.read, WithoutAttributes>(); CHECK(psrs.size() == 3); CHECK(pgns.size() == 1); } TEST_CASE("Reading polylines, segments, Bézier curves and splines") { - std::filesystem::path fn("data/test_ipe_reader.ipe"); - // Test open geometries. // The file contains a line segment, a polyline, a cubic Bézier curve, a cubic Bézier spline. - IpeReader ipeReader; + IpeReader ipeReader("data/test_ipe_reader.ipe"); ipeReader.setPage(2); - auto ls = ipeReader.read, WithoutAttributes>(fn); + auto ls = ipeReader.read, WithoutAttributes>(); CHECK(ls.size() == 1); // should only return the line segment - auto pls = ipeReader.read, WithoutAttributes>(fn); + auto pls = ipeReader.read, WithoutAttributes>(); CHECK(pls.size() == 2); // should return the polyline and the line segment - auto cbcs = ipeReader.read(fn); + auto cbcs = ipeReader.read(); CHECK(cbcs.size() == 2); // should return the cubic Bézier curve and the line segment - auto cbss = ipeReader.read(fn); + auto cbss = ipeReader.read(); CHECK(cbss.size() == 4); // should return all } TEST_CASE("Read points from specific layer") { - std::filesystem::path fn("data/test_ipe_reader.ipe"); - - IpeReader ipeReader; + IpeReader ipeReader("data/test_ipe_reader.ipe"); ipeReader.setPage(3); ipeReader.setLayerFilter("red"); - auto redPoints = ipeReader.read, WithoutAttributes>(fn); + auto redPoints = ipeReader.read, WithoutAttributes>(); ipeReader.setLayerFilter(0); - auto bluePoints = ipeReader.read, WithoutAttributes>(fn); + auto bluePoints = ipeReader.read, WithoutAttributes>(); ipeReader.setLayerFilter("green"); - auto greenPoints = ipeReader.read, WithoutAttributes>(fn); + auto greenPoints = ipeReader.read, WithoutAttributes>(); CHECK(bluePoints.size() == 7); CHECK(redPoints.size() == 11); @@ -124,36 +120,39 @@ TEST_CASE("Read points from specific layer") { } TEST_CASE("Read with output iterator") { - std::filesystem::path fn("data/test_ipe_reader.ipe"); - - IpeReader ipeReader; + IpeReader ipeReader("data/test_ipe_reader.ipe"); ipeReader.setPage(3); std::vector> allPoints; ipeReader.setLayerFilter("red"); - ipeReader.read, WithoutAttributes>(fn, std::back_inserter(allPoints)); + ipeReader.read, WithoutAttributes>(std::back_inserter(allPoints)); ipeReader.setLayerFilter(0); - ipeReader.read, WithoutAttributes>(fn, std::back_inserter(allPoints)); + ipeReader.read, WithoutAttributes>(std::back_inserter(allPoints)); ipeReader.setLayerFilter("green"); - ipeReader.read, WithoutAttributes>(fn, std::back_inserter(allPoints)); + ipeReader.read, WithoutAttributes>(std::back_inserter(allPoints)); - ipeReader.read, WithoutAttributes>(fn, std::back_inserter(allPoints)); + ipeReader.read, WithoutAttributes>(std::back_inserter(allPoints)); CHECK(allPoints.size() == 27); } -TEST_CASE("Read attributes") { - std::filesystem::path fn("data/test_ipe_reader.ipe"); +TEST_CASE("Read layer names") { + IpeReader ipeReader("data/test_ipe_reader.ipe"); + ipeReader.setPage(3); + auto names = ipeReader.layerNames(); + CHECK(names == std::vector({"blue", "red", "green"})); +} - IpeReader ipeReader; +TEST_CASE("Read attributes") { + IpeReader ipeReader("data/test_ipe_reader.ipe"); ipeReader.setPage(3); ipeReader.setLayerFilter("red"); - auto redPoints = ipeReader.read, WithAttributes>(fn); + auto redPoints = ipeReader.read, WithAttributes>(); ipeReader.setLayerFilter(0); - auto bluePoints = ipeReader.read, WithAttributes>(fn); + auto bluePoints = ipeReader.read, WithAttributes>(); ipeReader.setLayerFilter("green"); - auto greenPoints = ipeReader.read, WithAttributes>(fn); + auto greenPoints = ipeReader.read, WithAttributes>(); CHECK(bluePoints.size() == 7); for (const auto& bp : bluePoints) { @@ -178,18 +177,16 @@ TEST_CASE("Read attributes") { } TEST_CASE("Read ellipses and circles") { - std::filesystem::path fn("data/test_ipe_reader.ipe"); - - IpeReader ipeReader; + IpeReader ipeReader("data/test_ipe_reader.ipe"); ipeReader.setPage(4); // The page has 5 ellipses, three of which are circles (two are grouped), one of which is a skewed circle (the object has a transformation matrix), the last is a 'proper' ipe ellipse. // The two grouped circles should automatically be ungrouped. - auto circles = ipeReader.read, WithoutAttributes>(fn); + auto circles = ipeReader.read, WithoutAttributes>(); CHECK(circles.size() == 3); - auto ellipses = ipeReader.read(fn); + auto ellipses = ipeReader.read(); CHECK(ellipses.size() == 5); } From 0daa17898d2b20c219d456eba0cbf5447a02eee7 Mon Sep 17 00:00:00 2001 From: Yvee1 Date: Thu, 13 Aug 2026 17:39:40 +0200 Subject: [PATCH 13/17] Extract read methods to a separate class so they can be reused --- cartocrow/reader/CMakeLists.txt | 1 + cartocrow/reader/ipe_reader.h | 122 ++++++------------------ cartocrow/reader/linear_object_reader.h | 99 +++++++++++++++++++ cartocrow/reader/region_map_reader.h | 4 +- 4 files changed, 132 insertions(+), 94 deletions(-) create mode 100644 cartocrow/reader/linear_object_reader.h diff --git a/cartocrow/reader/CMakeLists.txt b/cartocrow/reader/CMakeLists.txt index 554fc1c6..e14d7c6f 100644 --- a/cartocrow/reader/CMakeLists.txt +++ b/cartocrow/reader/CMakeLists.txt @@ -8,6 +8,7 @@ set(HEADERS gdal_conversion.h boundary_map_reader.h region_map_reader.h + linear_object_reader.h ) add_library(reader ${SOURCES}) diff --git a/cartocrow/reader/ipe_reader.h b/cartocrow/reader/ipe_reader.h index d56f485d..2e2554b8 100644 --- a/cartocrow/reader/ipe_reader.h +++ b/cartocrow/reader/ipe_reader.h @@ -18,6 +18,7 @@ along with this program. If not, see . #pragma once #include "geometry_reader.h" +#include "linear_object_reader.h" #include "../core/core.h" #include "../core/ellipse.h" #include "../core/polyline.h" @@ -39,10 +40,19 @@ along with this program. If not, see . #include namespace cartocrow { +namespace { +/// We use this as the object type: page and object index. +/// We need a reference to the page to determine whether the layer of an object. +using IpeObject = std::pair; +} + template -concept IpeReaderTraits = requires(ipe::Object& o, OutputIterator out) { - { Traits::template convert(o, out) }->std::same_as; -}; +concept IpeReaderTraits = + LinearObjectReaderTraits< + IpeObject, + Geometry, + OutputIterator, + Traits>; // todo: make a RenderPath reader traits that parses everything to a render path? @@ -315,9 +325,11 @@ struct IpeReaderIntermediateGeometryTraits { } template - static bool convert(ipe::Object& o, OutputIterator out) { + static bool convert(const IpeObject& ipeObject, OutputIterator out) { std::vector intermediates; - convertToIntermediate(o, std::back_inserter(intermediates)); + + auto [page, index] = ipeObject; + convertToIntermediate(*page->object(index), std::back_inserter(intermediates)); for (const auto& intermediate : intermediates) { Converter::convert(intermediate, out); @@ -357,7 +369,7 @@ namespace { } // models GeometryReader and GeometryReaderFor every Geometry -class IpeReader { +class IpeReader : public LinearObjectReader, BasicIpeReaderTraits> { private: /// The current file that is being read. std::shared_ptr m_document; @@ -443,6 +455,8 @@ class IpeReader { m_pageNumber = pageNumber; } + // Todo: make the layer filters more flexible, so that layers can be toggled separately to be read or ignored. + /// Removes the layer filter so that objects are read from all layers. void removeLayerFilter() { m_layer = std::nullopt; @@ -513,7 +527,8 @@ class IpeReader { private: /// Whether to skip the ipe object with index i in the given page. - bool skipObject(ipe::Page* page, int i) const { + bool skipObject(const IpeObject& obj) const override { + auto [page, i] = obj; if (m_layer.has_value()) { auto layerIndex = page->layerOf(i); if (auto* layerIndexP = std::get_if(&*m_layer)) { @@ -529,7 +544,9 @@ class IpeReader { return false; } - GeometryAttributes getAttributes(ipe::Page* page, int i) const { + GeometryAttributes getAttributes(const IpeObject& obj) const override { + auto [page, i] = obj; + // There is no nice way to get all attributes that are relevant for an object, the logic is all in the saveAsXml function. // So for now we convert to xml and parse that. This is not so efficient because the entire geometry is also exported to xml. @@ -559,19 +576,19 @@ class IpeReader { } /// If handle returns true the parsing stops. - void readHelper(std::function handle) { + void readHelper(std::function handle) override { ipe::Page* page = m_document->page(m_pageNumber); for (int i = 0; i < page->count(); ++i) { - if (skipObject(page, i)) + IpeObject obj(page, i); + if (skipObject(obj)) continue; - if (handle(page, i)) + if (handle(obj)) break; } } public: - // ===== Reader methods ===== IpeReader(const std::filesystem::path& filename) { m_document = loadIpeFile(filename); @@ -588,87 +605,6 @@ class IpeReader { static bool canRead(std::filesystem::path path) { return path.extension() == ".ipe"; } - - /// Returns geometries in the provided file that are convertible to Geometry. - /// \pre canRead(path) - template < - class Cardinality, - class Geometry, - class AttrMode, - class Traits = BasicIpeReaderTraits - > - requires IpeReaderTraits>, Traits> - ReadResultT read() { - std::vector> gs; - - readHelper([&](ipe::Page* page, int i) { - ipe::Object* object = page->object(i); - if constexpr (std::same_as) { - Traits::convert(*object, std::back_inserter(gs)); - if constexpr (std::same_as) { - return !gs.empty(); // stop if a geometry is found - } else { - return false; - } - } else { - auto attributes = getAttributes(page, i); - - std::vector temps; - Traits::convert(*object, std::back_inserter(temps)); - for (auto& t : temps) { - gs.emplace_back(std::move(t), attributes); - } - if constexpr (std::same_as) { - return !gs.empty(); // stop if a geometry is found - } else { - return false; - } - } - }); - - if constexpr (std::same_as) { - return gs.empty() ? std::nullopt : std::optional>(gs.front()); - } else { - return gs; - } - } - - /// Returns geometries in the provided file that are convertible to Geometry. - /// \pre canRead(path) - template < - class Cardinality, - class Geometry, - class AttrMode, - class OutputIterator, - class Traits = BasicIpeReaderTraits - > - requires IpeReaderTraits>, Traits> - void read(OutputIterator out) { - readHelper([&](ipe::Page* page, int i) { - ipe::Object* object = page->object(i); - if constexpr (std::same_as) { - auto convertedSomething = Traits::convert(*object, out); - if constexpr (std::same_as) { - return convertedSomething; // stop if a geometry is found - } else { - return false; - } - } else { - auto attributes = getAttributes(page, i); - - std::vector temps; - Traits::convert(*object, std::back_inserter(temps)); - for (auto& t : temps) { - *out++ = GeometricFeature(std::move(t), attributes); - } - if constexpr (std::same_as) { - return !temps.empty(); // stop if a geometry is found - } else { - return false; - } - } - }); - } }; namespace { diff --git a/cartocrow/reader/linear_object_reader.h b/cartocrow/reader/linear_object_reader.h new file mode 100644 index 00000000..041110be --- /dev/null +++ b/cartocrow/reader/linear_object_reader.h @@ -0,0 +1,99 @@ +#pragma once + +#include "geometry_reader.h" + +namespace cartocrow { +template +concept LinearObjectReaderTraits = requires(const Object& o, OutputIterator out) { + { Traits::template convert(o, out) }->std::same_as; +}; + +/// A GeometryReader that iterates over objects and converts them. +/// This is a helper class for implementing e.g. the Ipe and GDAL readers. +template class DefaultReaderTraits> +class LinearObjectReader { + virtual bool skipObject(const Object& obj) const = 0; + virtual void readHelper(std::function handle) = 0; + virtual GeometryAttributes getAttributes(const Object& obj) const = 0; + + public: + /// Returns geometries in the provided file that are convertible to Geometry. + /// \pre canRead(path) + template < + class Cardinality, + class Geometry, + class AttrMode, + class Traits = DefaultReaderTraits + > + requires LinearObjectReaderTraits>, Traits> + ReadResultT read() { + std::vector> gs; + + readHelper([&](const Object& object) { + if constexpr (std::same_as) { + Traits::convert(object, std::back_inserter(gs)); + if constexpr (std::same_as) { + return !gs.empty(); // stop if a geometry is found + } else { + return false; + } + } else { + auto attributes = getAttributes(object); + + std::vector temps; + Traits::convert(object, std::back_inserter(temps)); + for (auto& t : temps) { + gs.emplace_back(std::move(t), attributes); + } + if constexpr (std::same_as) { + return !gs.empty(); // stop if a geometry is found + } else { + return false; + } + } + }); + + if constexpr (std::same_as) { + return gs.empty() ? std::nullopt : std::optional>(gs.front()); + } else { + return gs; + } + } + + /// Returns geometries in the provided file that are convertible to Geometry. + /// \pre canRead(path) + template < + class Cardinality, + class Geometry, + class AttrMode, + class OutputIterator, + class Traits = DefaultReaderTraits + > + requires LinearObjectReaderTraits>, Traits> + void read(OutputIterator out) { + readHelper([&](const Object& object) { + if constexpr (std::same_as) { + auto convertedSomething = Traits::convert(object, out); + if constexpr (std::same_as) { + return convertedSomething; // stop if a geometry is found + } else { + return false; + } + } else { + auto attributes = getAttributes(object); + + std::vector temps; + Traits::convert(object, std::back_inserter(temps)); + for (auto& t : temps) { + *out++ = GeometricFeature(std::move(t), attributes); + } + if constexpr (std::same_as) { + return !temps.empty(); // stop if a geometry is found + } else { + return false; + } + } + }); + } +}; +} \ No newline at end of file diff --git a/cartocrow/reader/region_map_reader.h b/cartocrow/reader/region_map_reader.h index 2a2cc7eb..93c3e26f 100644 --- a/cartocrow/reader/region_map_reader.h +++ b/cartocrow/reader/region_map_reader.h @@ -24,7 +24,9 @@ namespace cartocrow { namespace { struct RegionLabelReaderTraits { template - static bool convert(ipe::Object& object, OutputIterator out) { + static bool convert(const IpeObject& ipeObject, OutputIterator out) { + auto [page, index] = ipeObject; + auto& object = *page->object(index); ipe::Object::Type type = object.type(); if (type != ipe::Object::Type::EText) { return false; From 58b6b3b51234474eb3025b13c29208572eef3624 Mon Sep 17 00:00:00 2001 From: Yvee1 Date: Mon, 17 Aug 2026 11:25:00 +0200 Subject: [PATCH 14/17] Add basic GDAL reader --- cartocrow/reader/gdal_conversion.cpp | 34 ++++ cartocrow/reader/gdal_conversion.h | 2 + cartocrow/reader/gdal_reader.h | 226 +++++++++++++++++++++++++++ cartocrow/reader/ipe_reader.h | 2 +- data/test_gdal_reader.gpkg | Bin 0 -> 126976 bytes test/CMakeLists.txt | 1 + test/reader/gdal_reader.cpp | 99 ++++++++++++ test/reader/ipe_reader.cpp | 25 +-- 8 files changed, 377 insertions(+), 12 deletions(-) create mode 100644 cartocrow/reader/gdal_reader.h create mode 100644 data/test_gdal_reader.gpkg create mode 100644 test/reader/gdal_reader.cpp diff --git a/cartocrow/reader/gdal_conversion.cpp b/cartocrow/reader/gdal_conversion.cpp index c4724e12..86d65a5b 100644 --- a/cartocrow/reader/gdal_conversion.cpp +++ b/cartocrow/reader/gdal_conversion.cpp @@ -18,6 +18,40 @@ along with this program. If not, see . #include "gdal_conversion.h" namespace cartocrow { +StraightGeometry ogrGeometryToStraightGeometry(const OGRGeometry& geometry) { + switch (wkbFlatten(geometry.getGeometryType())) { + case wkbMultiPolygon: { + const OGRMultiPolygon *poMultiPolygon = geometry.toMultiPolygon(); + return ogrMultiPolygonToPolygonSetRaw(*poMultiPolygon); + } + case wkbPolygon: { + const OGRPolygon *poly = geometry.toPolygon(); + return ogrPolygonToPolygonWithHoles(*poly); + } + case wkbLinearRing: { + const OGRLinearRing *poly = geometry.toLinearRing(); + return ogrLinearRingToPolygon(*poly); + } + case wkbLineString: { + const OGRLineString *pl = geometry.toLineString(); + return ogrLineStringToPolyline(*pl); + } + case wkbMultiLineString: { + const OGRMultiLineString *pl = geometry.toMultiLineString(); + return ogrMultiLineStringToPolylineSet(*pl); + } + case wkbPoint: { + const OGRPoint* p = geometry.toPoint(); + return ogrPointToPoint(*p); + } + case wkbMultiPoint: { + const OGRMultiPoint* mp = geometry.toMultiPoint(); + return ogrMultiPointToPointSet(*mp); + } + default: throw std::runtime_error("Unhandled geometry type: " + std::string(geometry.getGeometryName())); + } +} + PolygonSetRaw ogrMultiPolygonToPolygonSetRaw(const OGRMultiPolygon& multiPolygon) { PolygonSetRaw polygonSet; for (const auto& poly : multiPolygon) { diff --git a/cartocrow/reader/gdal_conversion.h b/cartocrow/reader/gdal_conversion.h index bc5526bc..7f55666f 100644 --- a/cartocrow/reader/gdal_conversion.h +++ b/cartocrow/reader/gdal_conversion.h @@ -23,8 +23,10 @@ along with this program. If not, see . #include "cartocrow/core/polyline_set.h" #include "cartocrow/core/polygon_set_raw.h" #include "cartocrow/core/point_set.h" +#include "cartocrow/core/straight_geometry.h" namespace cartocrow { +StraightGeometry ogrGeometryToStraightGeometry(const OGRGeometry& geometry); PolygonSetRaw ogrMultiPolygonToPolygonSetRaw(const OGRMultiPolygon& multiPolygon); PolygonSetRaw ogrPolygonToPolygonSetRaw(const OGRPolygon& ogrPolygon); Polygon ogrLinearRingToPolygon(const OGRLinearRing& ogrLinearRing); diff --git a/cartocrow/reader/gdal_reader.h b/cartocrow/reader/gdal_reader.h new file mode 100644 index 00000000..de2cf812 --- /dev/null +++ b/cartocrow/reader/gdal_reader.h @@ -0,0 +1,226 @@ +#pragma once + +#include +#include "geometry_reader.h" +#include "linear_object_reader.h" +#include "gdal_conversion.h" +#include "../core/straight_geometry.h" + +namespace cartocrow { +namespace { +using GDALObject = OGRFeature; +} + +template +concept GDALReaderTraits = + LinearObjectReaderTraits< + GDALObject, + Geometry, + OutputIterator, + Traits>; + +using IntermediateGDALGeometry = StraightGeometry; + +template +concept GDALReaderIntermediateGeometryConverter = requires(const IntermediateGDALGeometry& g, OutputIterator out) { + { Traits::template convert(g, out) }->std::same_as; +}; + +/// is a model of GDALReaderTraits +template +requires GDALReaderIntermediateGeometryConverter>, Converter> +struct GDALReaderIntermediateGeometryTraits { + template + static void convertToIntermediate(const GDALObject& o, OutputIterator out) { + const OGRGeometry *poGeometry; + poGeometry = o.GetGeometryRef(); + *out++ = ogrGeometryToStraightGeometry(*poGeometry); + return; + } + + template + static bool convert(const GDALObject& o, OutputIterator out) { + std::vector intermediates; + convertToIntermediate(o, std::back_inserter(intermediates)); + + for (const auto& intermediate : intermediates) { + Converter::convert(intermediate, out); + } + + return !intermediates.empty(); + } +}; + +// is a model of GDALReaderIntermediateGeometryConverter +template +struct BasicGDALReaderTraitsConverter { + template + static bool convert(const IntermediateGDALGeometry& g, OutputIterator out) { + bool convertedSomething = false; + std::visit( + [&](auto&& g) { + using T = std::decay_t; + + if constexpr (std::is_convertible_v) { + *out++ = Geometry{g}; + convertedSomething = true; + } + }, + g); + return convertedSomething; + } +}; + +template +using BasicGDALReaderTraits = GDALReaderIntermediateGeometryTraits>; + +namespace { + using Out = std::back_insert_iterator>>; + + static_assert(GDALReaderTraits, Out, BasicGDALReaderTraits>>); +} + +class GDALReader : public LinearObjectReader { + private: + // Layer index or name. + std::variant m_layer = 0; + GDALDataset* m_dataset; + + public: + GDALReader(const std::filesystem::path& path) { + GDALAllRegister(); + GDALDataset *poDS; + + m_dataset = (GDALDataset*) GDALOpenEx( path.string().c_str(), GDAL_OF_VECTOR, nullptr, nullptr, nullptr ); + if( m_dataset == nullptr ) { + printf( "GDAL open failed.\n" ); + exit( 1 ); + } + } + + /// Set layer to read from + void setLayer(int layerNumber) { + if (m_dataset->GetLayer(layerNumber) != nullptr) { + m_layer = layerNumber; + } else { + std::cerr << "No layer with index: " << layerNumber << std::endl; + } + } + + /// Set layer to read from. + void setLayer(std::string layerName) { + if (m_dataset->GetLayerByName(layerName.c_str()) != nullptr) { + m_layer = layerName; + } else { + std::cerr << "No layer named: " << layerName << std::endl; + } + } + + /// Number of layers. + int numberOfLayers() const { + return m_dataset->GetLayerCount(); + } + + /// Returns the name of a layer. + std::string layerName(int layerIndex) const { + return m_dataset->GetLayer(layerIndex)->GetName(); + } + + /// Outputs the names of the layers of a page. + template + void layerNames(OutputIterator out) const { + for (int i = 0; i < numberOfLayers(); ++i) { + *out++ = layerName(i); + } + } + + /// Returns the names of the layers of a page. + std::vector layerNames() const { + std::vector names; + layerNames(std::back_inserter(names)); + return names; + } + private: + OGRLayer* getLayer() { + OGRLayer* poLayer; + + if (auto* layerIndexP = std::get_if(&m_layer)) { + poLayer = m_dataset->GetLayer(*layerIndexP); + } else if (auto* layerNameP = std::get_if(&m_layer)) { + poLayer = m_dataset->GetLayerByName(layerNameP->c_str()); + } + + return poLayer; + } + + private: + /// If handle returns true the parsing stops. + void readHelper(std::function handle) override { + auto* poLayer = getLayer(); + + for (const auto& poFeature : *poLayer) { + if (handle(*poFeature)) { + break; + } + } + } + + GeometryAttributes getAttributes(const GDALObject& obj) const override { + GeometryAttributes attributes; + + int i = 0; + for( auto&& oField : obj ) { + std::string name = obj.GetDefnRef()->GetFieldDefn(i)->GetNameRef(); + if (oField.IsNull()) { continue; } + switch (oField.GetType()) { + case OFTInteger: + attributes[name] = static_cast(oField.GetInteger()); + break; + case OFTReal: + attributes[name] = oField.GetDouble(); + break; + case OFTInteger64: + attributes[name] = static_cast(oField.GetInteger64()); + break; + case OFTString: { + attributes[name] = static_cast(oField.GetString()); + break; + } + default: + std::cout << "Did not handle this type of attribute: " << oField.GetType() << std::endl; + break; + } + ++i; + } + + return attributes; + } + + bool skipObject(const GDALObject& obj) const override { + return false; + } + public: + + + // ===== Reader methods ===== + /// If it exists, return the well-known text representation (WKT) of the coordinate reference system + std::optional readSpatialReference() { + auto* poLayer = getLayer(); + poLayer->ResetReading(); + return poLayer->GetSpatialRef()->exportToWkt(); + } + + /// Returns whether the reader can parse the given file. + static bool canRead(std::filesystem::path path) { + GDALDataset *poDS; + + poDS = (GDALDataset*) GDALOpenEx( path.string().c_str(), GDAL_OF_VECTOR, nullptr, nullptr, nullptr ); + return poDS != nullptr; + } +}; + +namespace { +static_assert(GeometryReader); +static_assert(GeometryReaderFor>); +} +} \ No newline at end of file diff --git a/cartocrow/reader/ipe_reader.h b/cartocrow/reader/ipe_reader.h index 2e2554b8..60cd89c8 100644 --- a/cartocrow/reader/ipe_reader.h +++ b/cartocrow/reader/ipe_reader.h @@ -369,7 +369,7 @@ namespace { } // models GeometryReader and GeometryReaderFor every Geometry -class IpeReader : public LinearObjectReader, BasicIpeReaderTraits> { +class IpeReader : public LinearObjectReader { private: /// The current file that is being read. std::shared_ptr m_document; diff --git a/data/test_gdal_reader.gpkg b/data/test_gdal_reader.gpkg new file mode 100644 index 0000000000000000000000000000000000000000..d516fede25fe029493bf336527b91e2fec0518c7 GIT binary patch literal 126976 zcmeI5dvF`adB6_KjKW9IEmOyobhzxD32zMliG1-+EFt}Jh3ya zTRUT?yY~R@4iAB(WXbM_A&9%(Z+E}_?YFzPd%%6+W0Sndp+q*Hj*4i2+CrJl)V&B% z6tx|{cjCAD>c$5R>IwcdQ55|=IrmK;^=ZPjhuf*1^Jg1zh{g6Q3Eye^gY7ljtDB!_ znQQ%O>ly1qEjt?*tOpzRn}1{ayfz^mmk2CRY_WFCbT?nvD(3lQlFKJ^XOgVQr#Kcj zTAp82KNk47ILGoCfy;|VnehJHV9PK zaXmmaDpbPRr^l(09zlHy%#n5(W@8;N*J+WZauu5{!o0!uSW>K_HP(S|Wqh;M-ra3_ zWJu}@MmsZ}x2cBbJv7N6V=Qt?nXs8?nnMxhz6c5g@q1=+(yh_dABZrd=NIF2y`);~ z9LFc;L_7c(5=*0x+#xH+RZE!T7jgGmWCi{_hbDqEq(ViNkNYID&_iKm_H^ zu%eho(;@$qH#CcmGP7C+ZcP_+xi!fWI9WQ)YG{$s&F>2sb8w*$a~KaNfpI1**%Y2h+338OmCjjXGqE0HNc+lrH(Bk2gQknJPR4jv z;L65v64qMN9EgM#hu$Q#(A+{^VEK4?>!$e(yI9sbBy_1fG`d(x7KfJ0>f0hIriE+P z7>0J1^2XI8vs0;s<@%Plu8@n0d^E-8xdbaL3CO7kx_qro_EUp(w=gDO-pZssHeaBu zR{Oqvrbl;{I((ALra3Xc#Ky9z`E*8*n%@}MT;A43k?M`@mUFH&G88irmvUSsfoisK z9!30tS<(xO-8Nkwq-kl~SxI4}mV(y$v}(KYd`}oiis<_6JWd_cTFFene{2R%^rc>` zlgE9W$Y=BouMw`RYa6X8#%|n}{4GuP`}ftfrDCHf7Qyycw24jr=Cx-fk3Pl{S_<16 zt@hqtQ{hy}Ftf=#ZY_DhlN8VfG?g2qHd@mzH3psp7ZvC894=)(Q<_~Sn8V(gNqOpW zd7GN-Grcu&Y0Fjmoi@n2a;Mp9r)ks0g;L#g^-@nP<#ke{w1!rrLilPi%95_6mvQrP zJZta?p3BP-vh}*-oDj?NIg!U#!_thcR-LBZsi+{bF??B@#1kaGW{mi!N>e0K1)ccn zI3e#Q(OCrM+!S8~$%;`vTRD|FzHI6L`lgvkRb9@YiL*YO$z|5<0#RIL7v|i*h_?{NR zo19#p%fvW@*N-BXKJ`cSWj#P`GwnQ6DrG#H$M@oRe1}ab~X(EgtO!F~Px1A}`9+}@eUkzmLlnLXtoIXm2reSHIcj{EPg z5VLone=mt4cM=#}nlI0BnFT&J=WzE`5Z^!4=ON;F7bkMc5$BRv9831}dG_rY9OxhF zAM^|j?HQ<~!;q)HU((?|Kd$5?Uf_%Kahy1SZ;l?ps>#9GOi~GFyx~YCjrYhJr&P&a ze7jUif_G5ymVng60^&0$&c*n2G=)EjmV$JfaE?O?dm%T+<+FU8tpClne#-VZ{s%80 z00e*l5C8%|00;m9AOHk_01yBIKtLf7Gq-l`=xjS^#V<479TC|a{=%OqC|_y8J1WJX z{=WXb-F^GJJ$oabq2d0%;l81sp*=$9Zg z9J`Dzd^Yj=u`O4UrdE0ftuG|Cl*HeOHWVWV>I=eU4SL9)#L3kwFFp6>Gf!P!eevms zotHoL{Majpf=@m8rS`EapN=g&HvGnxE1Phw%(j;(+e^0JK@$T3AOHk_01yBIKmZ5; z0U!VbfB+Bx0`EuyHcO{Tvms&LVxe1f00e*l5C8%|00;m9 zAOHkzG6Hpd|DW1&lW7g=00AHX1b_e#00KY&2mk>f00e*l5C8(#lmNN^59|MHiWD*f z0zd!=00AHX1b_e#00KY&2mk>faMKYW{{LY8f73M!Cv*AypY1_Xcr5C8%|00;m9AOHk_01yBIK;Wh%(ANHYYK!UfRQns7Uu^$} z_6IkcHb2oa*ZS4gGuDS%b~b*kaiL+q`8QZ#st)y+BU`NJYt?tCb@hvcP7b- ze2Qb!Q5>|Wek|~DagLqO#iJr;lo<~(-Ux#tA-~VZgb;0vuapOk4l##=AqLG%PmsjH z!$>JmDhUMx<@wMEr$CH%{0IsKPv8uE{s0OylgxMog}nYSy?N_GnuT2PID;52?7%5q8a3&UFgIS25asN zFeiF+N{=9q>i}W`6U!4@WUO23!pdg^E-xCftqrT3M-i((5N1M=4Pun@ynz_sXR~%3 z!`8X08piY4Y?@7R=eX2*ZL=(gB0|M7vk6p^*&xvM+oeW@N>~` z*;oh6by{SpT=wR&$C6@bo%qJun(X0j6UArZ+@f$c#fuypofosxIcw~@tj8GAzB0bq zYVYngJu)PXNyd2Nd7Ik(y@w_lWQ;{lDHApmO>-#1+!sNCAb!tGPP#Q!#V%zSG9nk_ zbfZJ zL{RPwD~f3}9r90kL$l~8Gpl9b*7Q-ATazq-lcm$Fh8CH1{JwxO2Nw!4hw-o;7-zyV zZagiB9DWwB2DQtz^4?8W`{1DIqMW6%U08uD8^=jlYt7aq65876TU1+Tw~!ZDK3?89 zX+Fa)mcb>VOXZ=_#Y(a`v|Lt?GAX8oYsS*hOj6#sdSsF>HIQ82^2QNzQIU_P*gThD zg(U$w6+xG;waI>JuHP*$sb-#*i$J4^kQJb#%J^Gj?jo0?B&1gZIr zfz9P@Z4{~A*ls!JN*%kHiMW*GDhX7xjq@nt56qHIRP1%>@*qu1$_Y_-$0>Ec4E zZn}D@CzkR$sZm-(t5G4ml8LgUE9qt2d>jw@e1hlla)fNX?l>pJ@_bI@@uEVSaMh~Q zv^y0QL^g(32T44?;nhsUKUJFHkSgfJi;RTGr#UCxHQU{l?(T|5x{eHYO$~P?PSS2V zlU<-)F1L0hcO&fx?oMk4ZXBfdq|?G3w@clta8@Fy_#l*>4C+p{ z>Xb zDoU)4t&&J%r?=H*rwW%1*{Lp%BHJ6hZ+0zN&faS6IDt3MhN|^>Wj8=xwy)PvD`iro zS+=>WY!TcjQ`IS3aVMzq_O7CKX@B>~EizkseQeihs5*6TvJq9M+Xhfll<7a#dk12r zb(ghcif&dvFLo=SUEF?Gn2*ZL2RoYV$9C6T_;ZVRw_L!B zU_sf~DFxhCSa^@szI(T6rAw-mv`Q~U8%}T45j8}XZSIt9n7WG@SK1@hEVT8vO@vss zY%55!C7Q(3NsL3~Tba6jP&d*S?s84v(L^dfE7e=MZ6t7K=ebObGo0R{tG6N2NtVlQ zoYF;p|8HpzQnnAYU&bHs0s=q)2mk>f00e*l5C8%|00;m9AW#p1Ke04Yg>$$1rYXx6 z>{af_p8wkZtFQfJb#wo>U%T|3UyeQVmCioP$A3Kb#_(ecu}@zaGg)w^Cj2IM>BVaRII!(%#)9}yHg@&yr3#|so zU(>gc^?yTqin9HcZMgjh_yAr&00;m9AOHk_01yBIKmZ5;0U!VbHb@}a(A2rD`|K{w zl9c?6Tl!AE{0ju>@keu_m>cfxU07J?!O^*B>`XMt_26H>_X;^K=IQV2?JRvSUP`x+ z$Inmd&Sp|eyrt;?MKA1<^285UiAv99z>js|@Q+qv^0VgD;5C(_Cc;0Mj(1DtvF0Ya ztSDW%8!RAZYN9o`|Iz;Bi?+_i|W#u?{37K z2Q9is(^M)``u)G{Rm%1U+iUm(UO)f{00AHX1b_e#00KY&2mk>f00e-*yMchkVxd|r zX7Ud|ZJ>p$|4psk6#j!35C8%|00;m9AOHk_01yBIS^^)QHZ@S6xC;l7=a!L|X?5>g zbKewywsPs^pIE;6KyJSrOqoZYjNCJ}e9J?Z;^UtlEo3K@aLc7HJnJ2OZO8wPe)6NY zkL~%+ok}=-00AHX1m1Q6Wc^R!CpE$g2mk>f00e*l5C8%|00;m9AOHk_01$Zh5@>6= zm1?2BLA5{L)?&TVbXUXg8ye03Y5FJX8=K$UJlFcO*5Q_am1_UgO~atcMZ6nng#MS>z3mOMY~T zQ(bpD&Q@^nh$fiB-kHe=qRB%i1Wu$SK{1+%NAq!ZKF=e)lgKom#(p4|jE*8p`H1*A z7rl$=6wXL_l!ZQrk|ga~?x3w5QS7}~b;yT)pj38&%R8lpmmG7tu&f+L>#9R5UW7aDu1rVvrM+P~SI`)!QGI zKmo=UEx+l!!gDtv+G{8zDAuZHcYT5BgfU>1<2w**BvT z^?Mw$&7Y~S1^YMBg8jx8JdCc1OZ)3({JKWO0|dUi`&!2o-F&gTK3JZO!17!tSlI5D z-`{KP@O3s{n5|R~@*`B1{34W<=PsoZB)H~?ZcL}@uUda;fFqI)GF{`rCBrS04d>p# zgmg(2j<9}#N$12RXMj1Oh`C%y9`4nAYxb(5QVw+7J2}IIk(^b6k2~Br|0zCmpEh$h z89%3@i~2xWaJG`5E>Ip73-9TXozV|SS0TD-uc(jW;J#+$dsW^QysH~Cs_BeA7sU@b z6N%?{SE0SzY9Ajr6*_AvM?|ZrMxu7qr5KT{sTR2!b`@@MTkQ;ED$unQlKk0M(MXEB zvo4h+QHN9dC|=!buIAlD=S{{|;=cJM1 zZR}jj2_#*SUf?Q?T^gPAqn8|^+K9S@(HT^E+Ug^iEc-T%VH(BWM$m1x9FV3NONW#euCJvK6 zq;pzS&%A=*LBH!G&je`5~*l%G!ab++(@q>R1(Hj)WVb6 zqE{h1B0XkV#0@ke+3V=04+SjPtatDDD62Q?WIUQ8PiQV$vv0yXIW?aW`H5^g%4epO z?Q1D}?FnvysR=(i!z~?j_@v4?Uoe~eMw{P5?6c2178Px6} zk{Cinu+$4kAPLd*7LA;-gA}DJMPSe*C$-ZQWwil{4CzG0c=be$BmJoqsh%mZYR4)H zXaiIYr2a-ElmNNcX#z;6E9Rh{D8g#zE5>UB6d7zeN^WwUb;ebpXBLHJaojVDxhuyw zWAY*Zr9N7WFP>>6r0z;2`lO~Sp6W#SxK=_w(+J4jxL8c-R3oG5%|$^&m`1v+pBK|q zjM1btcJN}tN)eiL`kqcwNkf1Hr94hbCoTdGg&hPvT3*za{hJ^49AxPagC6!|ZV;6!r%Lj#0zq-da0g zo9#KuV(T^89=Dyg_1bzja?uNEfdCKy0zd!=00AHX1b_e#00KY&2)s)Ocp97SofLJ@ zL`h#`(R^vDzpsC9ci;YQ&)$ef00e*l5C8%| z00;m9AOHk_Ks^L@H#Aa*ZC2kjWx9f&tY3lTQygwadC{lu1Mo?4xLJ@MHK=en0Z@ zXP;O-bgwxfy>^?*g@5?ymtV2{;)T^$UZ39nmiK#O-m#zmC3!s|um69m`2Vy0ww?-t z3=jYUKmZ5;0U!VbfB+Bx0zd!=00AIy^AfN$5U+pW|L^8)AXes?M14d42a;~(++(!r0h|Bc_TDdF$|1b_e#xNZc%|KD{(3mF3eAOHk_01yBI zKmZ5;0U!VbfB+D)jd!$I!@7~tI%1mlG z%&U{h)v{2Ts2+rjo{8R8e|s7xg$MRXewmh|m3*5JLD?^p&et9ZHX4V{uZc08TKj4i zv2HJW#eyWiDA*NaB_DfT1J({d?h1VED(XG#t)Y%r^{z+qQXIDw9owPn>|3uAnX7o# z8|arfJq1h4ysDGU6;q#t=;E$=5UC#Y){Lkw?|ExR zv^ERBdCe63|AGHM@c)M=4aUcWwBEcc`Tskes^SW<@>P~{Eq2@=ip+Q?QKXjdMhBV+ z`vX2C<%AuMyHT3YEaLysMe?7|EJ?>pt^&HpYVYhcEnAIGm(wf}I@HTRxkycn138g{ zn0zIlhsZk<3Hk##@e~t?lzf@1zFDVnWWEDvGw5(I q#Q1#y$-TU@l&K4YK8#(Q2gb1*Qyn90I. +*/ + +#include "../catch.hpp" + +#include "cartocrow/reader/gdal_reader.h" + +using namespace cartocrow; + +// Test case names need to be unique, so we prefix with GDALReader. +#define TEST_CASE_(name) TEST_CASE("[GDALReader] " name, "[GDALReader]") + +TEST_CASE_("Reading points") { + GDALReader gdalReader("data/test_gdal_reader.gpkg"); + gdalReader.setLayer("test_points"); + + auto points = gdalReader.read, WithAttributes>(); + CHECK(points.size() == 4); + + auto exists = [&](const Point& point, std::string name, double weight) { + return std::find_if(points.begin(), points.end(), [&](const auto& f) { + const auto& attrs = f.attributes; + const auto& pt = f.geometry; + + return CGAL::squared_distance(pt, point) < M_EPSILON && + attrs.contains("name") && std::holds_alternative(attrs.at("name")) && std::get(attrs.at("name")) == name && + attrs.contains("weight") && std::holds_alternative(attrs.at("weight")) && std::get(attrs.at("weight")) == weight; + }) != points.end(); + }; + + CHECK(exists({-0.450809, 0.212951}, "A", 0.4)); + CHECK(exists({0.0124533, -0.0261519}, "B", 2.3)); + CHECK(exists({-0.0772105, 0.235367}, "C", 8)); + CHECK(exists({-0.102117, 0.0709838}, "D", -1.1111)); +} + +TEST_CASE_("Reading polygons") { + GDALReader gdalReader("data/test_gdal_reader.gpkg"); + gdalReader.setLayer("test_polygons"); + + auto polygons = gdalReader.read, WithAttributes>(); + CHECK(polygons.size() == 3); + + auto exists = [&](std::string name) { + return std::find_if(polygons.begin(), polygons.end(), [&](const auto& f) { + const auto& attrs = f.attributes; + + return attrs.contains("name") && std::holds_alternative(attrs.at("name")) && std::get(attrs.at("name")) == name; + }) != polygons.end(); + }; + + CHECK(exists("Alice")); + CHECK(exists("Bob")); + CHECK(exists("Eve")); +} + +TEST_CASE_("Reading points and polygons as StraightGeometry") { + GDALReader gdalReader("data/test_gdal_reader.gpkg"); + + std::vector>> features; + + gdalReader.setLayer("test_points"); + gdalReader.read, WithAttributes>(std::back_inserter(features)); + + gdalReader.setLayer("test_polygons"); + gdalReader.read, WithAttributes>(std::back_inserter(features)); + + CHECK(features.size() == 7); +} + +TEST_CASE_("Read layer names") { + GDALReader gdalReader("data/test_gdal_reader.gpkg"); + auto names = gdalReader.layerNames(); + std::sort(names.begin(), names.end()); + std::vector expected({"test_points", "test_polygons"}); + std::sort(expected.begin(), expected.end()); + CHECK(names == expected); +} + +TEST_CASE_("Read spatial reference") { + GDALReader gdalReader("data/test_gdal_reader.gpkg"); + auto sRef = gdalReader.readSpatialReference(); + CHECK(sRef.has_value()); + CHECK(*sRef == R"(GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AXIS["Latitude",NORTH],AXIS["Longitude",EAST],AUTHORITY["EPSG","4326"]])"); +} \ No newline at end of file diff --git a/test/reader/ipe_reader.cpp b/test/reader/ipe_reader.cpp index 97746fc3..8e5a872e 100644 --- a/test/reader/ipe_reader.cpp +++ b/test/reader/ipe_reader.cpp @@ -23,7 +23,10 @@ along with this program. If not, see . using namespace cartocrow; using namespace renderer; -TEST_CASE("Reading points") { +// Test case names need to be unique, so we prefix with IpeReader. +#define TEST_CASE_(name) TEST_CASE("[IpeReader] " name, "[IpeReader]") + +TEST_CASE_("Reading points") { IpeReader ipeReader("data/test_ipe_reader.ipe"); auto points = ipeReader.read, WithoutAttributes>(); @@ -39,7 +42,7 @@ TEST_CASE("Reading points") { CHECK(exists({0, 64})); } -TEST_CASE("Reading a polygon") { +TEST_CASE_("Reading a polygon") { std::vector> points({{144.544, 155.39}, {123.907, 113.135}, {178.446, 67.44}, {249.199, 124.927}}); Polygon expectedPolygon(points.begin(), points.end()); @@ -48,7 +51,7 @@ TEST_CASE("Reading a polygon") { CHECK(parsedPolygon == expectedPolygon); } -TEST_CASE("Reading points and a polygon") { +TEST_CASE_("Reading points and a polygon") { std::vector> points( {{144.544, 155.39}, {123.907, 113.135}, {178.446, 67.44}, {249.199, 124.927}}); Polygon expectedPolygon(points.begin(), points.end()); @@ -71,7 +74,7 @@ TEST_CASE("Reading points and a polygon") { CHECK(exists(Point{0, 64})); } -TEST_CASE("Reading polygon sets") { +TEST_CASE_("Reading polygon sets") { // Test whether polygon is automatically converted to PolygonSetRaw. // The file contains 2 polygon sets and 1 polygon. IpeReader ipeReader("data/test_ipe_reader.ipe"); @@ -84,7 +87,7 @@ TEST_CASE("Reading polygon sets") { CHECK(pgns.size() == 1); } -TEST_CASE("Reading polylines, segments, Bézier curves and splines") { +TEST_CASE_("Reading polylines, segments, Bézier curves and splines") { // Test open geometries. // The file contains a line segment, a polyline, a cubic Bézier curve, a cubic Bézier spline. IpeReader ipeReader("data/test_ipe_reader.ipe"); @@ -103,7 +106,7 @@ TEST_CASE("Reading polylines, segments, Bézier curves and splines") { CHECK(cbss.size() == 4); // should return all } -TEST_CASE("Read points from specific layer") { +TEST_CASE_("Read points from specific layer") { IpeReader ipeReader("data/test_ipe_reader.ipe"); ipeReader.setPage(3); @@ -119,7 +122,7 @@ TEST_CASE("Read points from specific layer") { CHECK(greenPoints.size() == 8); } -TEST_CASE("Read with output iterator") { +TEST_CASE_("Read with output iterator") { IpeReader ipeReader("data/test_ipe_reader.ipe"); ipeReader.setPage(3); @@ -136,14 +139,14 @@ TEST_CASE("Read with output iterator") { CHECK(allPoints.size() == 27); } -TEST_CASE("Read layer names") { +TEST_CASE_("Read layer names") { IpeReader ipeReader("data/test_ipe_reader.ipe"); ipeReader.setPage(3); auto names = ipeReader.layerNames(); CHECK(names == std::vector({"blue", "red", "green"})); } -TEST_CASE("Read attributes") { +TEST_CASE_("Read attributes") { IpeReader ipeReader("data/test_ipe_reader.ipe"); ipeReader.setPage(3); @@ -176,7 +179,7 @@ TEST_CASE("Read attributes") { } } -TEST_CASE("Read ellipses and circles") { +TEST_CASE_("Read ellipses and circles") { IpeReader ipeReader("data/test_ipe_reader.ipe"); ipeReader.setPage(4); @@ -190,7 +193,7 @@ TEST_CASE("Read ellipses and circles") { CHECK(ellipses.size() == 5); } -//TEST_CASE("Manual check: load and save test_ipe_reader.ipe; geometries should be equivalent") { +//TEST_CASE_("Manual check: load and save test_ipe_reader.ipe; geometries should be equivalent") { // IpeReader ipeReader; // IpeRenderer ipeRenderer; // From 90da3904e86fdabe4e00f35d25055c5793f83dc0 Mon Sep 17 00:00:00 2001 From: Yvee1 Date: Mon, 17 Aug 2026 13:55:23 +0200 Subject: [PATCH 15/17] Remove degeneracy from .gpkg test input to circumvent precondition crash in debug mode --- data/test_gdal_reader.gpkg | Bin 126976 -> 126976 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/data/test_gdal_reader.gpkg b/data/test_gdal_reader.gpkg index d516fede25fe029493bf336527b91e2fec0518c7..018649f2f9a139effbf224398b777bfb87abbc6b 100644 GIT binary patch delta 595 zcmZp8z~1nHT_!ltC$l6~AuYcsH?c&)m_dMnk&(ecL4kpRL42Z&Go$#%gnWH{^AJNr zD-%mALt{M)L$j#;&-;}@D)!$feQf{#|9|_plNL7s`SwqG;~jwX=70JL_v%Z^uNJ;b zy|6!)OD?)f?1BB8?f*X|Rh+e7vT31NeD4!`MrNQ=Mj&PZVwhr>Y5G#X#gpFJYxM4T zwbJg${ xoT)PV6u$=AO|I&L2s1G6Yt}EZXV`q%F8i>Uy-L=v&4L0AEQ<~ZEGuAa003@{0|Wp7 delta 588 zcmZp8z~1nHT_!ltC$l6~AuYcsH?c&)m_dMniHX5ML4kpRL2ROoGo#qXgna$!d*(Cp zC>xqb?SFRv%-OD-ph`rcx zWj~M6!EgHx-?KlmMcIIP(k1&}R=wrPGoINqG6PL#1Y#B-hAH0Glw|30?y-GBPRjQc zRx99|V1~dnqpNoNvCiTl~a~7rGY^4qyd3Agso2N^AWCdzd6lGfWfA5Qc+|% Date: Tue, 18 Aug 2026 12:06:14 +0200 Subject: [PATCH 16/17] Add MultiReader --- .github/workflows/build-linux.yml | 4 +- cartocrow/reader/CMakeLists.txt | 1 + cartocrow/reader/gdal_reader.h | 13 +- cartocrow/reader/geometry_reader.h | 5 + cartocrow/reader/ipe_reader.h | 8 ++ cartocrow/reader/linear_object_reader.h | 3 + cartocrow/reader/multi_reader.h | 155 ++++++++++++++++++++++++ test/CMakeLists.txt | 1 + test/reader/multi_reader.cpp | 45 +++++++ 9 files changed, 230 insertions(+), 5 deletions(-) create mode 100644 cartocrow/reader/multi_reader.h create mode 100644 test/reader/multi_reader.cpp diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index 25769c76..e4743c40 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -66,7 +66,7 @@ jobs: run: | cmake -DCMAKE_BUILD_TYPE=Release -S . -B build-release cmake --build build-release - - name: Test CartoCrow core in debug mode + - name: Test CartoCrow core in Debug mode run: build-debug/test/cartocrow_test - - name: Test CartoCrow core in debug mode + - name: Test CartoCrow core in Release mode run: build-release/test/cartocrow_test diff --git a/cartocrow/reader/CMakeLists.txt b/cartocrow/reader/CMakeLists.txt index e14d7c6f..376d256f 100644 --- a/cartocrow/reader/CMakeLists.txt +++ b/cartocrow/reader/CMakeLists.txt @@ -9,6 +9,7 @@ set(HEADERS boundary_map_reader.h region_map_reader.h linear_object_reader.h + multi_reader.h ) add_library(reader ${SOURCES}) diff --git a/cartocrow/reader/gdal_reader.h b/cartocrow/reader/gdal_reader.h index de2cf812..44d38fe8 100644 --- a/cartocrow/reader/gdal_reader.h +++ b/cartocrow/reader/gdal_reader.h @@ -89,7 +89,6 @@ class GDALReader : public LinearObjectReader public: GDALReader(const std::filesystem::path& path) { GDALAllRegister(); - GDALDataset *poDS; m_dataset = (GDALDataset*) GDALOpenEx( path.string().c_str(), GDAL_OF_VECTOR, nullptr, nullptr, nullptr ); if( m_dataset == nullptr ) { @@ -200,9 +199,17 @@ class GDALReader : public LinearObjectReader return false; } public: - - // ===== Reader methods ===== + /// Load a different file. + /// Resets the page number and layer focus. + void load(const std::filesystem::path& path) { + m_dataset = (GDALDataset*) GDALOpenEx( path.string().c_str(), GDAL_OF_VECTOR, nullptr, nullptr, nullptr ); + if( m_dataset == nullptr ) { + printf( "GDAL open failed.\n" ); + exit( 1 ); + } + } + /// If it exists, return the well-known text representation (WKT) of the coordinate reference system std::optional readSpatialReference() { auto* poLayer = getLayer(); diff --git a/cartocrow/reader/geometry_reader.h b/cartocrow/reader/geometry_reader.h index 6cfaebb7..d01209cc 100644 --- a/cartocrow/reader/geometry_reader.h +++ b/cartocrow/reader/geometry_reader.h @@ -30,6 +30,9 @@ template concept GeometryReader = requires(R reader, std::filesystem:: /// Returns whether the reader can parse the given file. {R::canRead(path)}->std::same_as; + + {R(path)}->std::same_as; + {reader.load(path)}; }; struct Single {}; @@ -77,6 +80,8 @@ template concept GeometryReaderFor = GeometryReader && requires(R reader, std::back_insert_iterator> outG, std::back_insert_iterator>> outGF) { + /// The reader needs to have a templated default traits type-alias. + typename R::template DefaultTraits; /// Returns all geometries in the provided file that are convertible to Geometry. /// \pre canRead(path) diff --git a/cartocrow/reader/ipe_reader.h b/cartocrow/reader/ipe_reader.h index 60cd89c8..9151ceca 100644 --- a/cartocrow/reader/ipe_reader.h +++ b/cartocrow/reader/ipe_reader.h @@ -594,6 +594,14 @@ class IpeReader : public LinearObjectReader { m_document = loadIpeFile(filename); } + /// Load a different file. + /// Resets the page number and layer focus. + void load(const std::filesystem::path& filename) { + m_document = loadIpeFile(filename); + m_pageNumber = 0; + m_layer = std::nullopt; + } + /// If it exists, return the well-known text representation (WKT) of the coordinate reference system std::optional readSpatialReference() { return std::nullopt; diff --git a/cartocrow/reader/linear_object_reader.h b/cartocrow/reader/linear_object_reader.h index 041110be..876b3c58 100644 --- a/cartocrow/reader/linear_object_reader.h +++ b/cartocrow/reader/linear_object_reader.h @@ -17,6 +17,9 @@ class LinearObjectReader { virtual GeometryAttributes getAttributes(const Object& obj) const = 0; public: + template + using DefaultTraits = DefaultReaderTraits; + /// Returns geometries in the provided file that are convertible to Geometry. /// \pre canRead(path) template < diff --git a/cartocrow/reader/multi_reader.h b/cartocrow/reader/multi_reader.h new file mode 100644 index 00000000..c8c4f5e0 --- /dev/null +++ b/cartocrow/reader/multi_reader.h @@ -0,0 +1,155 @@ +#include "geometry_reader.h" + +namespace cartocrow { +template +struct DefaultMultiReaderTraits +{ + template + using ReaderTraits = typename Reader::template DefaultTraits; +}; + +template +class MultiReader; + +template <> +class MultiReader<> { + public: + template + using DefaultTraits = DefaultMultiReaderTraits; + + MultiReader(std::filesystem::path path) {} + + void load(std::filesystem::path path) {} + + static bool canRead(std::filesystem::path path) { + return false; + } + + template < + class Cardinality, + class Geometry, + class AttrMode, + class Traits = DefaultMultiReaderTraits + > + ReadResultT read() { + throw std::runtime_error("Cannot read this file!"); + } + + /// Returns geometries in the provided file that are convertible to Geometry. + /// \pre canRead(path) + template < + class Cardinality, + class Geometry, + class AttrMode, + class OutputIterator, + class Traits = DefaultMultiReaderTraits + > + void read(OutputIterator out) { + throw std::runtime_error("Cannot read this file!"); + } + + std::optional readSpatialReference() { return std::nullopt; } +}; + +template +class MultiReader { +private: + template + friend class MultiReader; + + using NextReader = MultiReader; + + using ReaderVariant = + std::variant; + + ReaderVariant m_reader; + +protected: + template + static Variant makeReaderImpl(std::filesystem::path path) + { + if (Reader::canRead(path)) + return Variant{Reader(path)}; + + if constexpr (sizeof...(Rest) > 0) + return makeReaderImpl(path); + else + throw std::runtime_error("Cannot read this file!"); + } + + static ReaderVariant makeReader(std::filesystem::path path) + { + return makeReaderImpl< + ReaderVariant, + FirstReader, + OtherReaders... + >(path); + } +public: + template + using DefaultTraits = DefaultMultiReaderTraits; + + MultiReader(std::filesystem::path path) + : m_reader(makeReader(path)) {} + + void load(std::filesystem::path path) { + m_reader = makeReader(path); + } + + static bool canRead(std::filesystem::path path) { + return FirstReader::canRead(path) || NextReader::canRead(path); + } + + ReaderVariant& getReader() { + return m_reader; + } + + std::optional readSpatialReference() { + return std::visit([](auto& reader) { + return reader.readSpatialReference(); + }, m_reader); + } + + template < + class Cardinality, + class Geometry, + class AttrMode, + class Traits = DefaultMultiReaderTraits + > + ReadResultT read() { + return std::visit([](auto& reader) { + using Reader = std::decay_t; + return reader.read>(); + }, m_reader); + } + + /// Returns geometries in the provided file that are convertible to Geometry. + /// \pre canRead(path) + template < + class Cardinality, + class Geometry, + class AttrMode, + class OutputIterator, + class Traits = DefaultMultiReaderTraits + > + void read(OutputIterator out) { + return std::visit([&out](auto& reader) { + using Reader = std::decay_t; + return reader.read>(out); + }, m_reader); + } +}; +} + +// For testing purposes (not ideal that ipe and gdal readers are included here) +#include "ipe_reader.h" +#include "gdal_reader.h" + +namespace cartocrow { +static_assert(GeometryReader>); +static_assert(GeometryReader>); +static_assert(GeometryReader>); +static_assert(GeometryReaderFor, Point>); +static_assert(GeometryReaderFor, Point>); +static_assert(GeometryReaderFor, Point>); +} \ No newline at end of file diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 24894554..c9d47a44 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -7,6 +7,7 @@ set(TEST_SOURCES "cartocrow_test.cpp" "renderer/ipe_renderer.cpp" "reader/ipe_reader.cpp" "reader/gdal_reader.cpp" + "reader/multi_reader.cpp" ) diff --git a/test/reader/multi_reader.cpp b/test/reader/multi_reader.cpp new file mode 100644 index 00000000..08dcf1c6 --- /dev/null +++ b/test/reader/multi_reader.cpp @@ -0,0 +1,45 @@ +/* +Copyright (C) 2026 TU Eindhoven + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +#include "../catch.hpp" + +#include "cartocrow/reader/multi_reader.h" + +using namespace cartocrow; + +// Test case names need to be unique, so we add a prefix. +#define TEST_CASE_(name) TEST_CASE("[MultiReader] " name, "[MultiReader]") + +TEST_CASE_("Reading points from .ipe and .gpkg") { + using Reader = MultiReader; + + std::vector> points; + + CHECK(Reader::canRead("data/test_gdal_reader.gpkg")); + + Reader reader("data/test_gdal_reader.gpkg"); + std::get(reader.getReader()).setLayer("test_points"); + + reader.read, WithoutAttributes>(std::back_inserter(points)); + CHECK(points.size() == 4); + + CHECK(Reader::canRead("data/test_ipe_reader.ipe")); + + reader.load("data/test_ipe_reader.ipe"); + reader.read, WithoutAttributes>(std::back_inserter(points)); + CHECK(points.size() == 8); +} \ No newline at end of file From 7388ceb0c861981845f4fae322f6daa41d18a19f Mon Sep 17 00:00:00 2001 From: Yvee1 Date: Tue, 18 Aug 2026 12:27:56 +0200 Subject: [PATCH 17/17] Add missing template keywords --- cartocrow/reader/multi_reader.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cartocrow/reader/multi_reader.h b/cartocrow/reader/multi_reader.h index c8c4f5e0..8040f1fb 100644 --- a/cartocrow/reader/multi_reader.h +++ b/cartocrow/reader/multi_reader.h @@ -119,7 +119,7 @@ class MultiReader { ReadResultT read() { return std::visit([](auto& reader) { using Reader = std::decay_t; - return reader.read>(); + return reader.template read>(); }, m_reader); } @@ -135,7 +135,7 @@ class MultiReader { void read(OutputIterator out) { return std::visit([&out](auto& reader) { using Reader = std::decay_t; - return reader.read>(out); + return reader.template read>(out); }, m_reader); } };