diff --git a/cartocrow/core/CMakeLists.txt b/cartocrow/core/CMakeLists.txt index c741a27c..26089cd3 100644 --- a/cartocrow/core/CMakeLists.txt +++ b/cartocrow/core/CMakeLists.txt @@ -10,6 +10,8 @@ set(SOURCES polyline_set.cpp segment_delaunay_graph_helpers.cpp stopwatch.cpp + polygon_set_raw.cpp + point_set.cpp ) set(HEADERS core.h @@ -35,6 +37,8 @@ set(HEADERS polyline_set.h stopwatch.h polygon_helpers.h + polygon_set_raw.h + point_set.h ) add_library(core ${SOURCES}) 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/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/core/geometric_feature.h b/cartocrow/core/geometric_feature.h new file mode 100644 index 00000000..027ac961 --- /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/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/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..2ad43eb5 --- /dev/null +++ b/cartocrow/core/polygon_set_raw.h @@ -0,0 +1,58 @@ +#pragma once + +#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(cartocrow::transform(trans, pgn)); + } + return transformed; + } + + Box bbox() const { + return CGAL::bbox_2(polygons_with_holes.begin(), polygons_with_holes.end()); + } + + PolygonSet polygonSet() const { + PolygonSet polygonSet; + 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; + } + + 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); +PolygonSetRaw pretendExact(const PolygonSetRaw& pgs); +} \ No newline at end of file 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/core/straight_geometry.h b/cartocrow/core/straight_geometry.h new file mode 100644 index 00000000..91df6d3f --- /dev/null +++ b/cartocrow/core/straight_geometry.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 "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 +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 fabf72fe..554fc1c6 100644 --- a/cartocrow/reader/CMakeLists.txt +++ b/cartocrow/reader/CMakeLists.txt @@ -1,5 +1,4 @@ set(SOURCES - ipe_reader.cpp gdal_conversion.cpp boundary_map_reader.cpp region_map_reader.cpp diff --git a/cartocrow/reader/gdal_conversion.cpp b/cartocrow/reader/gdal_conversion.cpp index b1f980d1..c4724e12 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,40 @@ 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()); + 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()}; } -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()}); @@ -90,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 dda667fa..bc5526bc 100644 --- a/cartocrow/reader/gdal_conversion.h +++ b/cartocrow/reader/gdal_conversion.h @@ -20,14 +20,19 @@ 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" +#include "cartocrow/core/point_set.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); +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/reader/geometry_reader.h b/cartocrow/reader/geometry_reader.h new file mode 100644 index 00000000..48cb3c9d --- /dev/null +++ b/cartocrow/reader/geometry_reader.h @@ -0,0 +1,65 @@ +/* +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/geometric_feature.h" + +#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. + /// \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. + /// \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. + /// \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.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..883ca9f0 100644 --- a/cartocrow/reader/ipe_reader.h +++ b/cartocrow/reader/ipe_reader.h @@ -17,42 +17,624 @@ along with this program. If not, see . #pragma once -#include -#include +#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 "../core/geometric_feature.h" -#include "cartocrow/renderer/render_path.h" - -#include "cartocrow/core/core.h" -#include "cartocrow/core/cubic_bezier.h" +#include #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, Segment, + Point, PointSet, CubicBezierCurve, CubicBezierSpline, Circle, 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; + } 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; + } + } + } + } + + 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) { + if (polyline.num_edges() == 1) { + *out++ = polyline.edge(0); + } else { + *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) { + 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 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(); + + 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) { + 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)); + } -/// Various utility methods for reading Ipe files. + if (spline.numCurves() == 1) { + *out++ = spline.curve(0); + } else { + *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; + + 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: + /// 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: - /// 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); + } + + 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). + 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; + } + + 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 + 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"; + } + + /// Returns all geometries in the provided file that are convertible to Geometry. + /// \pre canRead(path) + template < + class Geometry, + class OutputIterator, + 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; + + 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); + + std::vector gs; + Traits::convert(*object, std::back_inserter(gs)); + for (auto& g : gs) { + *out++ = GeometricFeature(std::move(g), attributes); + } + 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; + } + + /// 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 cartocrow +namespace { +static_assert(GeometryReader); +using Out = std::back_insert_iterator>>; +static_assert(GeometryReaderFor, Out>); +} +} \ No newline at end of file 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/cartocrow/renderer/geometry_renderer.cpp b/cartocrow/renderer/geometry_renderer.cpp index 7b560d13..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; @@ -103,6 +109,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..87720ccf 100644 --- a/cartocrow/renderer/geometry_renderer.h +++ b/cartocrow/renderer/geometry_renderer.h @@ -20,6 +20,8 @@ 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/point_set.h" #include "../core/polyline.h" #include "../core/polyline_set.h" #include "../core/halfplane.h" @@ -113,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 @@ -131,6 +135,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. diff --git a/data/test_ipe_reader.ipe b/data/test_ipe_reader.ipe new file mode 100644 index 00000000..f8f53943 --- /dev/null +++ b/data/test_ipe_reader.ipe @@ -0,0 +1,585 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +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 + + + + + + +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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +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/CMakeLists.txt b/test/CMakeLists.txt index 060fae5c..ffec8d6c 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -5,6 +5,7 @@ set(TEST_SOURCES "cartocrow_test.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..e16c2b1d --- /dev/null +++ b/test/reader/ipe_reader.cpp @@ -0,0 +1,195 @@ +/* +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" +#include "cartocrow/renderer/ipe_renderer.h" + +using namespace cartocrow; +using namespace renderer; + +TEST_CASE("Reading points") { + IpeReader ipeReader; + + auto points = ipeReader.readV>("data/test_ipe_reader.ipe"); + 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); +} + +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("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("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.save("test_ipe_reader_saved.ipe"); +//} \ No newline at end of file