From ba0f65279d3e0ae14cfa3c9509b78e3a7da4d228 Mon Sep 17 00:00:00 2001 From: Yvee1 Date: Fri, 10 Apr 2026 16:46:01 +0200 Subject: [PATCH 01/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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/10] 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" )