diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index 25769c76..e4743c40 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -66,7 +66,7 @@ jobs: run: | cmake -DCMAKE_BUILD_TYPE=Release -S . -B build-release cmake --build build-release - - name: Test CartoCrow core in debug mode + - name: Test CartoCrow core in Debug mode run: build-debug/test/cartocrow_test - - name: Test CartoCrow core in debug mode + - name: Test CartoCrow core in Release mode run: build-release/test/cartocrow_test diff --git a/cartocrow/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..376d256f 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 @@ -9,6 +8,8 @@ set(HEADERS gdal_conversion.h boundary_map_reader.h region_map_reader.h + linear_object_reader.h + multi_reader.h ) add_library(reader ${SOURCES}) diff --git a/cartocrow/reader/gdal_conversion.cpp b/cartocrow/reader/gdal_conversion.cpp index b1f980d1..86d65a5b 100644 --- a/cartocrow/reader/gdal_conversion.cpp +++ b/cartocrow/reader/gdal_conversion.cpp @@ -18,22 +18,51 @@ 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); +StraightGeometry ogrGeometryToStraightGeometry(const OGRGeometry& geometry) { + switch (wkbFlatten(geometry.getGeometryType())) { + case wkbMultiPolygon: { + const OGRMultiPolygon *poMultiPolygon = geometry.toMultiPolygon(); + return ogrMultiPolygonToPolygonSetRaw(*poMultiPolygon); } + case wkbPolygon: { + const OGRPolygon *poly = geometry.toPolygon(); + return ogrPolygonToPolygonWithHoles(*poly); + } + case wkbLinearRing: { + const OGRLinearRing *poly = geometry.toLinearRing(); + return ogrLinearRingToPolygon(*poly); + } + case wkbLineString: { + const OGRLineString *pl = geometry.toLineString(); + return ogrLineStringToPolyline(*pl); + } + case wkbMultiLineString: { + const OGRMultiLineString *pl = geometry.toMultiLineString(); + return ogrMultiLineStringToPolylineSet(*pl); + } + case wkbPoint: { + const OGRPoint* p = geometry.toPoint(); + return ogrPointToPoint(*p); + } + case wkbMultiPoint: { + const OGRMultiPoint* mp = geometry.toMultiPoint(); + return ogrMultiPointToPointSet(*mp); + } + default: throw std::runtime_error("Unhandled geometry type: " + std::string(geometry.getGeometryName())); + } +} + +PolygonSetRaw ogrMultiPolygonToPolygonSetRaw(const OGRMultiPolygon& multiPolygon) { + PolygonSetRaw polygonSet; + for (const auto& poly : multiPolygon) { + 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 +73,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 +115,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..7f55666f 100644 --- a/cartocrow/reader/gdal_conversion.h +++ b/cartocrow/reader/gdal_conversion.h @@ -20,14 +20,21 @@ 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" +#include "cartocrow/core/straight_geometry.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); +StraightGeometry ogrGeometryToStraightGeometry(const OGRGeometry& geometry); +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/gdal_reader.h b/cartocrow/reader/gdal_reader.h new file mode 100644 index 00000000..44d38fe8 --- /dev/null +++ b/cartocrow/reader/gdal_reader.h @@ -0,0 +1,233 @@ +#pragma once + +#include +#include "geometry_reader.h" +#include "linear_object_reader.h" +#include "gdal_conversion.h" +#include "../core/straight_geometry.h" + +namespace cartocrow { +namespace { +using GDALObject = OGRFeature; +} + +template +concept GDALReaderTraits = + LinearObjectReaderTraits< + GDALObject, + Geometry, + OutputIterator, + Traits>; + +using IntermediateGDALGeometry = StraightGeometry; + +template +concept GDALReaderIntermediateGeometryConverter = requires(const IntermediateGDALGeometry& g, OutputIterator out) { + { Traits::template convert(g, out) }->std::same_as; +}; + +/// is a model of GDALReaderTraits +template +requires GDALReaderIntermediateGeometryConverter>, Converter> +struct GDALReaderIntermediateGeometryTraits { + template + static void convertToIntermediate(const GDALObject& o, OutputIterator out) { + const OGRGeometry *poGeometry; + poGeometry = o.GetGeometryRef(); + *out++ = ogrGeometryToStraightGeometry(*poGeometry); + return; + } + + template + static bool convert(const GDALObject& o, OutputIterator out) { + std::vector intermediates; + convertToIntermediate(o, std::back_inserter(intermediates)); + + for (const auto& intermediate : intermediates) { + Converter::convert(intermediate, out); + } + + return !intermediates.empty(); + } +}; + +// is a model of GDALReaderIntermediateGeometryConverter +template +struct BasicGDALReaderTraitsConverter { + template + static bool convert(const IntermediateGDALGeometry& g, OutputIterator out) { + bool convertedSomething = false; + std::visit( + [&](auto&& g) { + using T = std::decay_t; + + if constexpr (std::is_convertible_v) { + *out++ = Geometry{g}; + convertedSomething = true; + } + }, + g); + return convertedSomething; + } +}; + +template +using BasicGDALReaderTraits = GDALReaderIntermediateGeometryTraits>; + +namespace { + using Out = std::back_insert_iterator>>; + + static_assert(GDALReaderTraits, Out, BasicGDALReaderTraits>>); +} + +class GDALReader : public LinearObjectReader { + private: + // Layer index or name. + std::variant m_layer = 0; + GDALDataset* m_dataset; + + public: + GDALReader(const std::filesystem::path& path) { + GDALAllRegister(); + + m_dataset = (GDALDataset*) GDALOpenEx( path.string().c_str(), GDAL_OF_VECTOR, nullptr, nullptr, nullptr ); + if( m_dataset == nullptr ) { + printf( "GDAL open failed.\n" ); + exit( 1 ); + } + } + + /// Set layer to read from + void setLayer(int layerNumber) { + if (m_dataset->GetLayer(layerNumber) != nullptr) { + m_layer = layerNumber; + } else { + std::cerr << "No layer with index: " << layerNumber << std::endl; + } + } + + /// Set layer to read from. + void setLayer(std::string layerName) { + if (m_dataset->GetLayerByName(layerName.c_str()) != nullptr) { + m_layer = layerName; + } else { + std::cerr << "No layer named: " << layerName << std::endl; + } + } + + /// Number of layers. + int numberOfLayers() const { + return m_dataset->GetLayerCount(); + } + + /// Returns the name of a layer. + std::string layerName(int layerIndex) const { + return m_dataset->GetLayer(layerIndex)->GetName(); + } + + /// Outputs the names of the layers of a page. + template + void layerNames(OutputIterator out) const { + for (int i = 0; i < numberOfLayers(); ++i) { + *out++ = layerName(i); + } + } + + /// Returns the names of the layers of a page. + std::vector layerNames() const { + std::vector names; + layerNames(std::back_inserter(names)); + return names; + } + private: + OGRLayer* getLayer() { + OGRLayer* poLayer; + + if (auto* layerIndexP = std::get_if(&m_layer)) { + poLayer = m_dataset->GetLayer(*layerIndexP); + } else if (auto* layerNameP = std::get_if(&m_layer)) { + poLayer = m_dataset->GetLayerByName(layerNameP->c_str()); + } + + return poLayer; + } + + private: + /// If handle returns true the parsing stops. + void readHelper(std::function handle) override { + auto* poLayer = getLayer(); + + for (const auto& poFeature : *poLayer) { + if (handle(*poFeature)) { + break; + } + } + } + + GeometryAttributes getAttributes(const GDALObject& obj) const override { + GeometryAttributes attributes; + + int i = 0; + for( auto&& oField : obj ) { + std::string name = obj.GetDefnRef()->GetFieldDefn(i)->GetNameRef(); + if (oField.IsNull()) { continue; } + switch (oField.GetType()) { + case OFTInteger: + attributes[name] = static_cast(oField.GetInteger()); + break; + case OFTReal: + attributes[name] = oField.GetDouble(); + break; + case OFTInteger64: + attributes[name] = static_cast(oField.GetInteger64()); + break; + case OFTString: { + attributes[name] = static_cast(oField.GetString()); + break; + } + default: + std::cout << "Did not handle this type of attribute: " << oField.GetType() << std::endl; + break; + } + ++i; + } + + return attributes; + } + + bool skipObject(const GDALObject& obj) const override { + return false; + } + public: + // ===== Reader methods ===== + /// Load a different file. + /// Resets the page number and layer focus. + void load(const std::filesystem::path& path) { + m_dataset = (GDALDataset*) GDALOpenEx( path.string().c_str(), GDAL_OF_VECTOR, nullptr, nullptr, nullptr ); + if( m_dataset == nullptr ) { + printf( "GDAL open failed.\n" ); + exit( 1 ); + } + } + + /// If it exists, return the well-known text representation (WKT) of the coordinate reference system + std::optional readSpatialReference() { + auto* poLayer = getLayer(); + poLayer->ResetReading(); + return poLayer->GetSpatialRef()->exportToWkt(); + } + + /// Returns whether the reader can parse the given file. + static bool canRead(std::filesystem::path path) { + GDALDataset *poDS; + + poDS = (GDALDataset*) GDALOpenEx( path.string().c_str(), GDAL_OF_VECTOR, nullptr, nullptr, nullptr ); + return poDS != nullptr; + } +}; + +namespace { +static_assert(GeometryReader); +static_assert(GeometryReaderFor>); +} +} \ No newline at end of file diff --git a/cartocrow/reader/geometry_reader.h b/cartocrow/reader/geometry_reader.h new file mode 100644 index 00000000..d01209cc --- /dev/null +++ b/cartocrow/reader/geometry_reader.h @@ -0,0 +1,111 @@ +/* +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()}->std::same_as>; + + /// Returns whether the reader can parse the given file. + {R::canRead(path)}->std::same_as; + + {R(path)}->std::same_as; + {reader.load(path)}; +}; + +struct Single {}; +struct Multiple {}; +struct WithoutAttributes {}; +struct WithAttributes{}; + +template +struct ElementType { + using type = Geometry; +}; + +template +struct ElementType { + using type = GeometricFeature; +}; + +template +using ElementTypeT = typename ElementType::type; + +template +struct CardinalityType { + using type = std::vector; +}; + +template +struct CardinalityType { + using type = std::optional; +}; + +template +using CardinalityTypeT = + typename CardinalityType::type; + +template +using ReadResultT = + CardinalityTypeT< + ElementTypeT, + Cardinality>; + +//GeometryReader R +template +concept GeometryReaderFor = + GeometryReader && requires(R reader, std::back_insert_iterator> outG, + std::back_insert_iterator>> outGF) { + /// The reader needs to have a templated default traits type-alias. + typename R::template DefaultTraits; + + /// Returns all geometries in the provided file that are convertible to Geometry. + /// \pre canRead(path) + reader.template read(outG); + + /// Returns a vector with all geometries in the provided file that are convertible to Geometry. + /// \pre canRead(path) + {reader.template read()}->std::same_as>; + + /// Returns the first geometry in the provided file that is convertible to Geometry. + /// \pre canRead(path) + {reader.template read()}->std::same_as>; + + /// Returns geometries in the provided file that are convertible to Geometry including their attributes. + /// Outputs Feature. + /// \pre canRead(path) + reader.template read(outGF); + + /// Returns a vector with all geometries in the provided file that are convertible to Geometry including their attributes. + /// \pre canRead(path) + {reader.template read()}->std::same_as>>; + + /// Returns the first feature in the provided file that is convertible to Geometry. + /// \pre canRead(path) + {reader.template read()}->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..9151ceca 100644 --- a/cartocrow/reader/ipe_reader.h +++ b/cartocrow/reader/ipe_reader.h @@ -17,42 +17,606 @@ along with this program. If not, see . #pragma once -#include -#include +#include "geometry_reader.h" +#include "linear_object_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 { +namespace { +/// We use this as the object type: page and object index. +/// We need a reference to the page to determine whether the layer of an object. +using IpeObject = std::pair; +} + +template +concept IpeReaderTraits = + LinearObjectReaderTraits< + IpeObject, + Geometry, + OutputIterator, + Traits>; + +// 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) }->std::same_as; +}; + +/// 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)); + } + + if (spline.numCurves() == 1) { + *out++ = spline.curve(0); + } else { + *out++ = spline; + } + } + } + } + } + return; + } + + template + static bool convert(const IpeObject& ipeObject, OutputIterator out) { + std::vector intermediates; + + auto [page, index] = ipeObject; + convertToIntermediate(*page->object(index), std::back_inserter(intermediates)); + + for (const auto& intermediate : intermediates) { + Converter::convert(intermediate, out); + } + + return !intermediates.empty(); + } +}; + +// is a model of IpeReaderIntermediateGeometryConverter +template +struct BasicIpeReaderTraitsConverter { + template + static bool convert(const IntermediateIpeGeometry& g, OutputIterator out) { + bool convertedSomething = false; + std::visit( + [&](auto&& g) { + using T = std::decay_t; + + if constexpr (std::is_convertible_v) { + *out++ = Geometry{g}; + convertedSomething = true; + } + }, + g); + return convertedSomething; + } +}; + +template +using BasicIpeReaderTraits = IpeReaderIntermediateGeometryTraits>; + +namespace { + using Out = std::back_insert_iterator>>; + + static_assert(IpeReaderTraits, Out, BasicIpeReaderTraits>>); +} + +// models GeometryReader and GeometryReaderFor every Geometry +class IpeReader : public LinearObjectReader { + private: + /// The current file that is being read. + std::shared_ptr m_document; + /// The page to read from + int m_pageNumber = 0; + /// The layer to read from. If std::nullopt then it reads from all layers. + std::optional> m_layer = std::nullopt; -/// Various utility methods for reading Ipe files. -class IpeReader { 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) { + if (m_pageNumber >= m_document->countPages()) { + std::cerr << "Page number exceeds document page count." << std::endl; + std::cerr << "Setting page number to last page." << std::endl; + m_pageNumber = m_document->countPages() - 1; + return; + } else if (m_pageNumber < 0) { + std::cerr << "Ppage number is negative." << std::endl; + std::cerr << "Setting page number to first page." << std::endl; + m_pageNumber = 0; + return; + } + + m_pageNumber = pageNumber; + } + + // Todo: make the layer filters more flexible, so that layers can be toggled separately to be read or ignored. + + /// Removes the layer filter so that objects are read from all layers. + void removeLayerFilter() { + m_layer = std::nullopt; + } + + /// 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() const { + return m_document->countPages(); + } + + /// Number of layers of a page. + int numberOfLayers(int pageIndex) const { + auto page = m_document->page(pageIndex); + return page->countLayers(); + } + + /// Number of layers of the current page. + int numberOfLayers() const { + return numberOfLayers(m_pageNumber); + } + + /// Returns the name of a layer of a page. + std::string layerName(int pageIndex, int layerIndex) const { + auto ln = m_document->page(pageIndex)->layer(layerIndex); + return std::string(ln.data(), ln.size()); + } + + /// Returns the name of a layer of the current page. + std::string layerName(int layerIndex) const { + return layerName(m_pageNumber, layerIndex); + } + + /// Outputs the names of the layers of a page. + template + void layerNames(int pageIndex, OutputIterator out) const { + for (int i = 0; i < numberOfLayers(pageIndex); ++i) { + *out++ = layerName(i); + } + } + + /// Returns the names of the layers of a page. + std::vector layerNames(int pageIndex) const { + std::vector names; + layerNames(pageIndex, std::back_inserter(names)); + return names; + } + + /// Outputs the names of the layers of the current page. + template + void layerNames(OutputIterator out) const { + return layerNames(m_pageNumber, out); + } + + /// Returns the names of the layers of the current page. + std::vector layerNames() const { + return layerNames(m_pageNumber); + } + + private: + /// Whether to skip the ipe object with index i in the given page. + bool skipObject(const IpeObject& obj) const override { + auto [page, i] = obj; + if (m_layer.has_value()) { + auto layerIndex = page->layerOf(i); + if (auto* layerIndexP = std::get_if(&*m_layer)) { + if (layerIndex != *layerIndexP) + return true; // object is not on layer so we skip + } else if (auto* layerNameP = std::get_if(&*m_layer)) { + auto ln = page->layer(layerIndex); + if (std::string(ln.data(), ln.size()) != *layerNameP) { + return true; + } + } + } + return false; + } + + GeometryAttributes getAttributes(const IpeObject& obj) const override { + auto [page, i] = obj; + + // There is no nice way to get all attributes that are relevant for an object, the logic is all in the saveAsXml function. + // So for now we convert to xml and parse that. This is not so efficient because the entire geometry is also exported to xml. + + 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::function handle) override { + ipe::Page* page = m_document->page(m_pageNumber); + + for (int i = 0; i < page->count(); ++i) { + IpeObject obj(page, i); + if (skipObject(obj)) + continue; + if (handle(obj)) + break; + } + } + + public: + // ===== Reader methods ===== + IpeReader(const std::filesystem::path& filename) { + m_document = loadIpeFile(filename); + } + + /// Load a different file. + /// Resets the page number and layer focus. + void load(const std::filesystem::path& filename) { + m_document = loadIpeFile(filename); + m_pageNumber = 0; + m_layer = std::nullopt; + } + + /// If it exists, return the well-known text representation (WKT) of the coordinate reference system + std::optional readSpatialReference() { + return std::nullopt; + } + + /// Returns whether the reader can parse the given file. + /// Currently only checks the file extension. + // todo: actually try to parse to ipe document? + static bool canRead(std::filesystem::path path) { + return path.extension() == ".ipe"; + } }; -} // namespace cartocrow +namespace { +static_assert(GeometryReader); +static_assert(GeometryReaderFor>); +} +} \ No newline at end of file diff --git a/cartocrow/reader/linear_object_reader.h b/cartocrow/reader/linear_object_reader.h new file mode 100644 index 00000000..876b3c58 --- /dev/null +++ b/cartocrow/reader/linear_object_reader.h @@ -0,0 +1,102 @@ +#pragma once + +#include "geometry_reader.h" + +namespace cartocrow { +template +concept LinearObjectReaderTraits = requires(const Object& o, OutputIterator out) { + { Traits::template convert(o, out) }->std::same_as; +}; + +/// A GeometryReader that iterates over objects and converts them. +/// This is a helper class for implementing e.g. the Ipe and GDAL readers. +template class DefaultReaderTraits> +class LinearObjectReader { + virtual bool skipObject(const Object& obj) const = 0; + virtual void readHelper(std::function handle) = 0; + virtual GeometryAttributes getAttributes(const Object& obj) const = 0; + + public: + template + using DefaultTraits = DefaultReaderTraits; + + /// Returns geometries in the provided file that are convertible to Geometry. + /// \pre canRead(path) + template < + class Cardinality, + class Geometry, + class AttrMode, + class Traits = DefaultReaderTraits + > + requires LinearObjectReaderTraits>, Traits> + ReadResultT read() { + std::vector> gs; + + readHelper([&](const Object& object) { + if constexpr (std::same_as) { + Traits::convert(object, std::back_inserter(gs)); + if constexpr (std::same_as) { + return !gs.empty(); // stop if a geometry is found + } else { + return false; + } + } else { + auto attributes = getAttributes(object); + + std::vector temps; + Traits::convert(object, std::back_inserter(temps)); + for (auto& t : temps) { + gs.emplace_back(std::move(t), attributes); + } + if constexpr (std::same_as) { + return !gs.empty(); // stop if a geometry is found + } else { + return false; + } + } + }); + + if constexpr (std::same_as) { + return gs.empty() ? std::nullopt : std::optional>(gs.front()); + } else { + return gs; + } + } + + /// Returns geometries in the provided file that are convertible to Geometry. + /// \pre canRead(path) + template < + class Cardinality, + class Geometry, + class AttrMode, + class OutputIterator, + class Traits = DefaultReaderTraits + > + requires LinearObjectReaderTraits>, Traits> + void read(OutputIterator out) { + readHelper([&](const Object& object) { + if constexpr (std::same_as) { + auto convertedSomething = Traits::convert(object, out); + if constexpr (std::same_as) { + return convertedSomething; // stop if a geometry is found + } else { + return false; + } + } else { + auto attributes = getAttributes(object); + + std::vector temps; + Traits::convert(object, std::back_inserter(temps)); + for (auto& t : temps) { + *out++ = GeometricFeature(std::move(t), attributes); + } + if constexpr (std::same_as) { + return !temps.empty(); // stop if a geometry is found + } else { + return false; + } + } + }); + } +}; +} \ No newline at end of file diff --git a/cartocrow/reader/multi_reader.h b/cartocrow/reader/multi_reader.h new file mode 100644 index 00000000..8040f1fb --- /dev/null +++ b/cartocrow/reader/multi_reader.h @@ -0,0 +1,155 @@ +#include "geometry_reader.h" + +namespace cartocrow { +template +struct DefaultMultiReaderTraits +{ + template + using ReaderTraits = typename Reader::template DefaultTraits; +}; + +template +class MultiReader; + +template <> +class MultiReader<> { + public: + template + using DefaultTraits = DefaultMultiReaderTraits; + + MultiReader(std::filesystem::path path) {} + + void load(std::filesystem::path path) {} + + static bool canRead(std::filesystem::path path) { + return false; + } + + template < + class Cardinality, + class Geometry, + class AttrMode, + class Traits = DefaultMultiReaderTraits + > + ReadResultT read() { + throw std::runtime_error("Cannot read this file!"); + } + + /// Returns geometries in the provided file that are convertible to Geometry. + /// \pre canRead(path) + template < + class Cardinality, + class Geometry, + class AttrMode, + class OutputIterator, + class Traits = DefaultMultiReaderTraits + > + void read(OutputIterator out) { + throw std::runtime_error("Cannot read this file!"); + } + + std::optional readSpatialReference() { return std::nullopt; } +}; + +template +class MultiReader { +private: + template + friend class MultiReader; + + using NextReader = MultiReader; + + using ReaderVariant = + std::variant; + + ReaderVariant m_reader; + +protected: + template + static Variant makeReaderImpl(std::filesystem::path path) + { + if (Reader::canRead(path)) + return Variant{Reader(path)}; + + if constexpr (sizeof...(Rest) > 0) + return makeReaderImpl(path); + else + throw std::runtime_error("Cannot read this file!"); + } + + static ReaderVariant makeReader(std::filesystem::path path) + { + return makeReaderImpl< + ReaderVariant, + FirstReader, + OtherReaders... + >(path); + } +public: + template + using DefaultTraits = DefaultMultiReaderTraits; + + MultiReader(std::filesystem::path path) + : m_reader(makeReader(path)) {} + + void load(std::filesystem::path path) { + m_reader = makeReader(path); + } + + static bool canRead(std::filesystem::path path) { + return FirstReader::canRead(path) || NextReader::canRead(path); + } + + ReaderVariant& getReader() { + return m_reader; + } + + std::optional readSpatialReference() { + return std::visit([](auto& reader) { + return reader.readSpatialReference(); + }, m_reader); + } + + template < + class Cardinality, + class Geometry, + class AttrMode, + class Traits = DefaultMultiReaderTraits + > + ReadResultT read() { + return std::visit([](auto& reader) { + using Reader = std::decay_t; + return reader.template read>(); + }, m_reader); + } + + /// Returns geometries in the provided file that are convertible to Geometry. + /// \pre canRead(path) + template < + class Cardinality, + class Geometry, + class AttrMode, + class OutputIterator, + class Traits = DefaultMultiReaderTraits + > + void read(OutputIterator out) { + return std::visit([&out](auto& reader) { + using Reader = std::decay_t; + return reader.template read>(out); + }, m_reader); + } +}; +} + +// For testing purposes (not ideal that ipe and gdal readers are included here) +#include "ipe_reader.h" +#include "gdal_reader.h" + +namespace cartocrow { +static_assert(GeometryReader>); +static_assert(GeometryReader>); +static_assert(GeometryReader>); +static_assert(GeometryReaderFor, Point>); +static_assert(GeometryReaderFor, Point>); +static_assert(GeometryReaderFor, Point>); +} \ No newline at end of file diff --git a/cartocrow/reader/region_map_reader.cpp b/cartocrow/reader/region_map_reader.cpp index d0045832..13df45b6 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(file); - if (document->countPages() == 0) { + int numPages = reader.numberOfPages(); + 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.read(); + // 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.read, WithAttributes>(); + + 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..93c3e26f 100644 --- a/cartocrow/reader/region_map_reader.h +++ b/cartocrow/reader/region_map_reader.h @@ -17,9 +17,31 @@ 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 bool convert(const IpeObject& ipeObject, OutputIterator out) { + auto [page, index] = ipeObject; + auto& object = *page->object(index); + ipe::Object::Type type = object.type(); + if (type != ipe::Object::Type::EText) { + return false; + } + 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}; + return true; + } +}; +} + /// 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_gdal_reader.gpkg b/data/test_gdal_reader.gpkg new file mode 100644 index 00000000..018649f2 Binary files /dev/null and b/data/test_gdal_reader.gpkg differ 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..c9d47a44 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -5,6 +5,9 @@ set(TEST_SOURCES "cartocrow_test.cpp" "core/region_arrangement.cpp" "core/region_map.cpp" "renderer/ipe_renderer.cpp" + "reader/ipe_reader.cpp" + "reader/gdal_reader.cpp" + "reader/multi_reader.cpp" ) diff --git a/test/reader/gdal_reader.cpp b/test/reader/gdal_reader.cpp new file mode 100644 index 00000000..732e9162 --- /dev/null +++ b/test/reader/gdal_reader.cpp @@ -0,0 +1,99 @@ +/* +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/gdal_reader.h" + +using namespace cartocrow; + +// Test case names need to be unique, so we prefix with GDALReader. +#define TEST_CASE_(name) TEST_CASE("[GDALReader] " name, "[GDALReader]") + +TEST_CASE_("Reading points") { + GDALReader gdalReader("data/test_gdal_reader.gpkg"); + gdalReader.setLayer("test_points"); + + auto points = gdalReader.read, WithAttributes>(); + CHECK(points.size() == 4); + + auto exists = [&](const Point& point, std::string name, double weight) { + return std::find_if(points.begin(), points.end(), [&](const auto& f) { + const auto& attrs = f.attributes; + const auto& pt = f.geometry; + + return CGAL::squared_distance(pt, point) < M_EPSILON && + attrs.contains("name") && std::holds_alternative(attrs.at("name")) && std::get(attrs.at("name")) == name && + attrs.contains("weight") && std::holds_alternative(attrs.at("weight")) && std::get(attrs.at("weight")) == weight; + }) != points.end(); + }; + + CHECK(exists({-0.450809, 0.212951}, "A", 0.4)); + CHECK(exists({0.0124533, -0.0261519}, "B", 2.3)); + CHECK(exists({-0.0772105, 0.235367}, "C", 8)); + CHECK(exists({-0.102117, 0.0709838}, "D", -1.1111)); +} + +TEST_CASE_("Reading polygons") { + GDALReader gdalReader("data/test_gdal_reader.gpkg"); + gdalReader.setLayer("test_polygons"); + + auto polygons = gdalReader.read, WithAttributes>(); + CHECK(polygons.size() == 3); + + auto exists = [&](std::string name) { + return std::find_if(polygons.begin(), polygons.end(), [&](const auto& f) { + const auto& attrs = f.attributes; + + return attrs.contains("name") && std::holds_alternative(attrs.at("name")) && std::get(attrs.at("name")) == name; + }) != polygons.end(); + }; + + CHECK(exists("Alice")); + CHECK(exists("Bob")); + CHECK(exists("Eve")); +} + +TEST_CASE_("Reading points and polygons as StraightGeometry") { + GDALReader gdalReader("data/test_gdal_reader.gpkg"); + + std::vector>> features; + + gdalReader.setLayer("test_points"); + gdalReader.read, WithAttributes>(std::back_inserter(features)); + + gdalReader.setLayer("test_polygons"); + gdalReader.read, WithAttributes>(std::back_inserter(features)); + + CHECK(features.size() == 7); +} + +TEST_CASE_("Read layer names") { + GDALReader gdalReader("data/test_gdal_reader.gpkg"); + auto names = gdalReader.layerNames(); + std::sort(names.begin(), names.end()); + std::vector expected({"test_points", "test_polygons"}); + std::sort(expected.begin(), expected.end()); + CHECK(names == expected); +} + +TEST_CASE_("Read spatial reference") { + GDALReader gdalReader("data/test_gdal_reader.gpkg"); + auto sRef = gdalReader.readSpatialReference(); + CHECK(sRef.has_value()); + CHECK(*sRef == R"(GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AXIS["Latitude",NORTH],AXIS["Longitude",EAST],AUTHORITY["EPSG","4326"]])"); +} \ No newline at end of file diff --git a/test/reader/ipe_reader.cpp b/test/reader/ipe_reader.cpp new file mode 100644 index 00000000..8e5a872e --- /dev/null +++ b/test/reader/ipe_reader.cpp @@ -0,0 +1,214 @@ +/* +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 names need to be unique, so we prefix with IpeReader. +#define TEST_CASE_(name) TEST_CASE("[IpeReader] " name, "[IpeReader]") + +TEST_CASE_("Reading points") { + IpeReader ipeReader("data/test_ipe_reader.ipe"); + + auto points = ipeReader.read, WithoutAttributes>(); + 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("data/test_ipe_reader.ipe"); + auto parsedPolygon = ipeReader.read, WithoutAttributes>(); + 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("data/test_ipe_reader.ipe"); + auto pointOrPolys = ipeReader.read(); + + 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("data/test_ipe_reader.ipe"); + ipeReader.setPage(1); + + auto psrs = ipeReader.read, WithoutAttributes>(); + auto pgns = ipeReader.read, WithoutAttributes>(); + + CHECK(psrs.size() == 3); + CHECK(pgns.size() == 1); +} + +TEST_CASE_("Reading polylines, segments, Bézier curves and splines") { + // Test open geometries. + // The file contains a line segment, a polyline, a cubic Bézier curve, a cubic Bézier spline. + IpeReader ipeReader("data/test_ipe_reader.ipe"); + ipeReader.setPage(2); + + auto ls = ipeReader.read, WithoutAttributes>(); + CHECK(ls.size() == 1); // should only return the line segment + + auto pls = ipeReader.read, WithoutAttributes>(); + CHECK(pls.size() == 2); // should return the polyline and the line segment + + auto cbcs = ipeReader.read(); + CHECK(cbcs.size() == 2); // should return the cubic Bézier curve and the line segment + + auto cbss = ipeReader.read(); + CHECK(cbss.size() == 4); // should return all +} + +TEST_CASE_("Read points from specific layer") { + IpeReader ipeReader("data/test_ipe_reader.ipe"); + ipeReader.setPage(3); + + ipeReader.setLayerFilter("red"); + auto redPoints = ipeReader.read, WithoutAttributes>(); + ipeReader.setLayerFilter(0); + auto bluePoints = ipeReader.read, WithoutAttributes>(); + ipeReader.setLayerFilter("green"); + auto greenPoints = ipeReader.read, WithoutAttributes>(); + + CHECK(bluePoints.size() == 7); + CHECK(redPoints.size() == 11); + CHECK(greenPoints.size() == 8); +} + +TEST_CASE_("Read with output iterator") { + IpeReader ipeReader("data/test_ipe_reader.ipe"); + ipeReader.setPage(3); + + std::vector> allPoints; + ipeReader.setLayerFilter("red"); + ipeReader.read, WithoutAttributes>(std::back_inserter(allPoints)); + ipeReader.setLayerFilter(0); + ipeReader.read, WithoutAttributes>(std::back_inserter(allPoints)); + ipeReader.setLayerFilter("green"); + ipeReader.read, WithoutAttributes>(std::back_inserter(allPoints)); + + ipeReader.read, WithoutAttributes>(std::back_inserter(allPoints)); + + CHECK(allPoints.size() == 27); +} + +TEST_CASE_("Read layer names") { + IpeReader ipeReader("data/test_ipe_reader.ipe"); + ipeReader.setPage(3); + auto names = ipeReader.layerNames(); + CHECK(names == std::vector({"blue", "red", "green"})); +} + +TEST_CASE_("Read attributes") { + IpeReader ipeReader("data/test_ipe_reader.ipe"); + ipeReader.setPage(3); + + ipeReader.setLayerFilter("red"); + auto redPoints = ipeReader.read, WithAttributes>(); + ipeReader.setLayerFilter(0); + auto bluePoints = ipeReader.read, WithAttributes>(); + ipeReader.setLayerFilter("green"); + auto greenPoints = ipeReader.read, WithAttributes>(); + + 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") { + IpeReader ipeReader("data/test_ipe_reader.ipe"); + ipeReader.setPage(4); + + // The page has 5 ellipses, three of which are circles (two are grouped), one of which is a skewed circle (the object has a transformation matrix), the last is a 'proper' ipe ellipse. + // The two grouped circles should automatically be ungrouped. + + auto circles = ipeReader.read, WithoutAttributes>(); + CHECK(circles.size() == 3); + + auto ellipses = ipeReader.read(); + 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 diff --git a/test/reader/multi_reader.cpp b/test/reader/multi_reader.cpp new file mode 100644 index 00000000..08dcf1c6 --- /dev/null +++ b/test/reader/multi_reader.cpp @@ -0,0 +1,45 @@ +/* +Copyright (C) 2026 TU Eindhoven + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +#include "../catch.hpp" + +#include "cartocrow/reader/multi_reader.h" + +using namespace cartocrow; + +// Test case names need to be unique, so we add a prefix. +#define TEST_CASE_(name) TEST_CASE("[MultiReader] " name, "[MultiReader]") + +TEST_CASE_("Reading points from .ipe and .gpkg") { + using Reader = MultiReader; + + std::vector> points; + + CHECK(Reader::canRead("data/test_gdal_reader.gpkg")); + + Reader reader("data/test_gdal_reader.gpkg"); + std::get(reader.getReader()).setLayer("test_points"); + + reader.read, WithoutAttributes>(std::back_inserter(points)); + CHECK(points.size() == 4); + + CHECK(Reader::canRead("data/test_ipe_reader.ipe")); + + reader.load("data/test_ipe_reader.ipe"); + reader.read, WithoutAttributes>(std::back_inserter(points)); + CHECK(points.size() == 8); +} \ No newline at end of file