Skip to content

File straightSkeleton.cpp

File List > algorithm > straightSkeleton.cpp

Go to the documentation of this file

// Copyright (c) 2012-2013, IGN France.
// Copyright (c) 2012-2022, Oslandia.
// SPDX-License-Identifier: LGPL-2.0-or-later

#include "SFCGAL/algorithm/straightSkeleton.h"
#include "SFCGAL/algorithm/distance.h"

#include "SFCGAL/Envelope.h"
#include "SFCGAL/LineString.h"
#include "SFCGAL/MultiLineString.h"
#include "SFCGAL/MultiPolygon.h"
#include "SFCGAL/Polygon.h"
#include "SFCGAL/PolyhedralSurface.h"
#include "SFCGAL/Solid.h"
#include "SFCGAL/Triangle.h"

#include "SFCGAL/Exception.h"

#include "SFCGAL/algorithm/intersection.h"
#include "SFCGAL/algorithm/isValid.h"
#include "SFCGAL/algorithm/orientation.h"
#include "SFCGAL/algorithm/tesselate.h"
#include "SFCGAL/algorithm/translate.h"

#include <CGAL/Arr_segment_traits_2.h>
#include <CGAL/Arrangement_2.h>
#include <CGAL/Straight_skeleton_converter_2.h>
#include <CGAL/Surface_mesh.h>
#include <CGAL/create_straight_skeleton_from_polygon_with_holes_2.h>
#include <CGAL/extrude_skeleton.h>

#include <memory>

namespace SFCGAL::algorithm {

using Point_2              = Kernel::Point_2;
using Point_3              = Kernel::Point_3;
using Polygon_2            = CGAL::Polygon_2<Kernel>;
using Polygon_with_holes_2 = CGAL::Polygon_with_holes_2<Kernel>;
using Straight_skeleton_2  = CGAL::Straight_skeleton_2<Kernel>;
using Mesh                 = CGAL::Surface_mesh<Point_3>;
using Segment_2            = Kernel::Segment_2;
using Arrangement_2 = CGAL::Arrangement_2<CGAL::Arr_segment_traits_2<Kernel>>;

#if CGAL_VERSION_MAJOR < 6
template <class T>
using SHARED_PTR = boost::shared_ptr<T>;
#else
template <class T>
using SHARED_PTR = std::shared_ptr<T>;
#endif

namespace { // anonymous

template <class K, bool outputDistanceInM>
void
straightSkeletonToMultiLineString(const CGAL::Straight_skeleton_2<K> &ss,
                                  MultiLineString &result, bool innerOnly,
                                  Kernel::Vector_2 &translate,
                                  const double     &toleranceAbs)
{
  using Ss = CGAL::Straight_skeleton_2<K>;

  using Vertex_const_handle     = typename Ss::Vertex_const_handle;
  using Halfedge_const_handle   = typename Ss::Halfedge_const_handle;
  using Halfedge_const_iterator = typename Ss::Halfedge_const_iterator;

  Halfedge_const_handle const null_halfedge;
  Vertex_const_handle const   null_vertex;

  for (Halfedge_const_iterator it = ss.halfedges_begin();
       it != ss.halfedges_end(); ++it) {
    // skip contour edge
    if (!it->is_bisector()) {
      continue;
    }

    // Skip non-inner edges if requested
    if (innerOnly && !it->is_inner_bisector()) {
      continue;
    }

    // avoid duplicates
    if (it->opposite() < it) {
      continue;
    }

    LineString *ls = nullptr;
    Point       pa(it->opposite()->vertex()->point());
    Point       pb(it->vertex()->point());
    // avoid degenerate cases.https://gitlab.com/Oslandia/SFCGAL/-/issues/143
    if (pa != pb && distancePointPoint(pa, pb) > toleranceAbs) {
      if (outputDistanceInM) {
        pa.setM(CGAL::to_double(it->opposite()->vertex()->time()));
        pb.setM(CGAL::to_double(it->vertex()->time()));
        ls = new LineString(pa, pb);
      } else {
        ls = new LineString(pa, pb);
      }

      algorithm::translate(*ls, translate);
      result.addGeometry(ls);
    }
  }
}

auto
angle(const Point &a, const Point &b, const Point &c) -> double
{
  Point const ab(CGAL::to_double(b.x() - a.x()),
                 CGAL::to_double(b.y() - a.y()));
  Point const cb(CGAL::to_double(b.x() - c.x()),
                 CGAL::to_double(b.y() - c.y()));

  double const dot =
      (CGAL::to_double(ab.x() * cb.x() + ab.y() * cb.y())); /* dot product */
  double const cross =
      (CGAL::to_double(ab.x() * cb.y() - ab.y() * cb.x())); /* cross product */

  double const alpha = std::atan2(cross, dot);

  return alpha;
}

template <class K>
void
straightSkeletonToMedialAxis(const CGAL::Straight_skeleton_2<K> &ss,
                             MultiLineString                    &result,
                             Kernel::Vector_2                   &translate)
{
  using Ss = CGAL::Straight_skeleton_2<K>;

  using Halfedge_const_handle   = typename Ss::Halfedge_const_handle;
  using Halfedge_const_iterator = typename Ss::Halfedge_const_iterator;

  // Maximum angle for touching bisectors to be
  // retained in the ouptput. The value is in
  // radians and represents the angle between the
  // bisector and the defining edge.
  //
  // The angle between the incident edges will be double
  // this value, so CGAL_PI / 8.0 means 45 degrees between
  // defining edges
  //
  // TODO: take as parameter ?
  //
  // NOTE: I'm adding some tolerance here to include those angles
  //       of exactly 45 degrees that are otherwise cut out due
  //       to rounding precision
  //
  const double maxTouchingAngle = CGAL_PI / 8.0 + 1e-13;

  for (Halfedge_const_iterator it = ss.halfedges_begin();
       it != ss.halfedges_end(); ++it) {
    // skip contour edge
    if (!it->is_bisector()) {
      continue;
    }

    // avoid duplicates
    if (it->opposite() < it) {
      continue;
    }

    // Skip non-inner edges unless they bisect
    // a low angle pair of segments
    if (!it->is_inner_bisector()) {

      const Halfedge_const_handle &de1 = it->defining_contour_edge();

      // We need to check the angle formed by:
      const Point &p1 = it->vertex()->point();
      const Point &p2 = de1->vertex()->point();
      const Point &p3 = de1->opposite()->vertex()->point();

      double const ang = angle(p1, p2, p3);

      if (ang > maxTouchingAngle) {
        continue;
      }
    }

    std::unique_ptr<LineString> ls(
        new LineString(Point(it->opposite()->vertex()->point()),
                       Point(it->vertex()->point())));
    algorithm::translate(*ls, translate);
    result.addGeometry(ls.release());
  }
}

auto
straightSkeleton(const Polygon_with_holes_2 &poly)
    -> SHARED_PTR<Straight_skeleton_2>
{
  SHARED_PTR<CGAL::Straight_skeleton_2<CGAL::Epick>> const sk =
      CGAL::create_interior_straight_skeleton_2(
          poly.outer_boundary().vertices_begin(),
          poly.outer_boundary().vertices_end(), poly.holes_begin(),
          poly.holes_end(), CGAL::Epick());
  SHARED_PTR<Straight_skeleton_2> ret;
  if (sk) {
    ret = CGAL::convert_straight_skeleton_2<Straight_skeleton_2>(*sk);
  }
  return ret;
}

// Throw an exception if any two polygon rings touch,
// since CGAL segfaults in that case.
// @todo find upstream reference to find out exact case
// See https://github.com/Oslandia/SFCGAL/issues/75
void
checkNoTouchingHoles(const Polygon &g)
{
  const size_t numRings = g.numRings();

  for (size_t ri = 0; ri < numRings - 1; ++ri) {
    for (size_t rj = ri + 1; rj < numRings; ++rj) {
      std::unique_ptr<Geometry> inter =
          g.is3D() ? intersection3D(g.ringN(ri), g.ringN(rj))
                   : intersection(g.ringN(ri), g.ringN(rj));

      // @note this check would accept rings touching at
      //       more than a single point, which may be
      //       still dangerous. @todo find out !
      if (!inter->isEmpty() && inter->is<Point>()) {
        BOOST_THROW_EXCEPTION(NotImplementedException(
            std::string("straight skeleton of Polygon with point ") +
            "touching rings is not implemented. " + "Error at " +
            inter->asText()));
      }
    }
  }
}

auto
preparePolygon(const Polygon &poly, Kernel::Vector_2 &trans)
    -> Polygon_with_holes_2
{
  checkNoTouchingHoles(poly);
  Envelope const env = poly.envelope();
  trans              = Kernel::Vector_2(-env.xMin(), -env.yMin());

  // @todo: avoid cloning !
  std::unique_ptr<Polygon> cloned(poly.clone());
  algorithm::translate(*cloned, trans);
  Polygon_with_holes_2 ret = cloned->toPolygon_with_holes_2();
  trans                    = -trans;

  return ret;
}

void
extractPolygons(const Geometry &g, std::vector<Polygon> &vect)
{
  switch (g.geometryTypeId()) {
  case TYPE_TRIANGLE:
    vect.push_back(g.as<Triangle>().toPolygon());
    break;
  case TYPE_POLYGON:
    vect.push_back(g.as<Polygon>());
    break;
  case TYPE_MULTIPOLYGON: {
    const auto &mp = g.as<MultiPolygon>();
    for (size_t i = 0; i < mp.numGeometries(); i++) {
      vect.push_back(mp.polygonN(i));
    }
    break;
  }
  default:
    break;
  }
}

} // namespace

auto
straightSkeleton(const Geometry &g, bool          autoOrientation,
                 NoValidityCheck /*unused*/, bool innerOnly,
                 bool outputDistanceInM, const double & /*toleranceAbs*/)
    -> std::unique_ptr<MultiLineString>
{
  switch (g.geometryTypeId()) {
  case TYPE_TRIANGLE:
    return straightSkeleton(g.as<Triangle>().toPolygon(), autoOrientation,
                            innerOnly, outputDistanceInM);

  case TYPE_POLYGON:
    return straightSkeleton(g.as<Polygon>(), autoOrientation, innerOnly,
                            outputDistanceInM);

  case TYPE_MULTIPOLYGON:
    return straightSkeleton(g.as<MultiPolygon>(), autoOrientation, innerOnly,
                            outputDistanceInM);

  default:
    return std::make_unique<MultiLineString>();
  }
}

auto
straightSkeleton(const Geometry &g, bool autoOrientation, bool innerOnly,
                 bool outputDistanceInM, const double & /*toleranceAbs*/)
    -> std::unique_ptr<MultiLineString>
{
  SFCGAL_ASSERT_GEOMETRY_VALIDITY_2D(g);

  std::unique_ptr<MultiLineString> result(straightSkeleton(
      g, autoOrientation, NoValidityCheck(), innerOnly, outputDistanceInM));
  propagateValidityFlag(*result, true);
  return result;
}
auto
straightSkeleton(const Polygon &g, bool /*autoOrientation*/, bool innerOnly,
                 bool outputDistanceInM, const double &toleranceAbs)
    -> std::unique_ptr<MultiLineString>
{
  std::unique_ptr<MultiLineString> result(new MultiLineString);

  if (g.isEmpty()) {
    return result;
  }

  Kernel::Vector_2                      trans;
  Polygon_with_holes_2 const            polygon  = preparePolygon(g, trans);
  SHARED_PTR<Straight_skeleton_2> const skeleton = straightSkeleton(polygon);

  if (skeleton == nullptr) {
    BOOST_THROW_EXCEPTION(Exception("CGAL failed to create straightSkeleton"));
  }

  if (outputDistanceInM) {
    straightSkeletonToMultiLineString<Kernel, true>(
        *skeleton, *result, innerOnly, trans, toleranceAbs);
  } else {
    straightSkeletonToMultiLineString<Kernel, false>(
        *skeleton, *result, innerOnly, trans, toleranceAbs);
  }
  return result;
}

auto
straightSkeleton(const MultiPolygon &g, bool /*autoOrientation*/,
                 bool innerOnly, bool outputDistanceInM,
                 const double &toleranceAbs) -> std::unique_ptr<MultiLineString>
{
  std::unique_ptr<MultiLineString> result(new MultiLineString);

  for (size_t i = 0; i < g.numGeometries(); i++) {
    Kernel::Vector_2           trans;
    Polygon_with_holes_2 const polygon = preparePolygon(g.polygonN(i), trans);
    SHARED_PTR<Straight_skeleton_2> const skeleton = straightSkeleton(polygon);

    if (skeleton == nullptr) {
      BOOST_THROW_EXCEPTION(
          Exception("CGAL failed to create straightSkeleton"));
    }

    if (outputDistanceInM) {
      straightSkeletonToMultiLineString<Kernel, true>(
          *skeleton, *result, innerOnly, trans, toleranceAbs);
    } else {
      straightSkeletonToMultiLineString<Kernel, false>(
          *skeleton, *result, innerOnly, trans, toleranceAbs);
    }
  }

  return result;
}

auto
approximateMedialAxis(const Geometry &g) -> std::unique_ptr<MultiLineString>
{
  SFCGAL_ASSERT_GEOMETRY_VALIDITY_2D(g);

  std::unique_ptr<MultiLineString> mx(new MultiLineString);

  std::vector<Polygon> polys;
  extractPolygons(g, polys);

  for (auto &poly : polys) {
    Kernel::Vector_2                      trans;
    Polygon_with_holes_2 const            polygon = preparePolygon(poly, trans);
    SHARED_PTR<Straight_skeleton_2> const skeleton = straightSkeleton(polygon);

    if (skeleton == nullptr) {
      BOOST_THROW_EXCEPTION(
          Exception("CGAL failed to create straightSkeleton"));
    }

    straightSkeletonToMedialAxis(*skeleton, *mx, trans);
  }

  propagateValidityFlag(*mx, true);
  return mx;
}

auto
extrudeStraightSkeleton(const Polygon &g, double height)
    -> std::unique_ptr<PolyhedralSurface>
{

  Mesh sm;
  CGAL::extrude_skeleton(g.toPolygon_with_holes_2(), sm,
                         CGAL::parameters::maximum_height(height));
  std::unique_ptr<PolyhedralSurface> polys(new PolyhedralSurface(sm));

  return polys;
}

auto
extrudeStraightSkeleton(const Geometry &g, double height)
    -> std::unique_ptr<PolyhedralSurface>
{
  SFCGAL_ASSERT_GEOMETRY_VALIDITY_2D(g);

  if (g.geometryTypeId() != TYPE_POLYGON) {
    BOOST_THROW_EXCEPTION(Exception("Geometry must be a Polygon"));
  }
  std::unique_ptr<PolyhedralSurface> result(
      extrudeStraightSkeleton(g.as<Polygon>(), height));
  propagateValidityFlag(*result, true);
  return result;
}

auto
extrudeStraightSkeleton(const Geometry &g, double building_height,
                        double roof_height)
    -> std::unique_ptr<PolyhedralSurface>
{
  std::unique_ptr<PolyhedralSurface> const roof{
      extrudeStraightSkeleton(g, roof_height)};
  translate(*roof, 0.0, 0.0, building_height);
  std::unique_ptr<Geometry> building(extrude(g.as<Polygon>(), building_height));
  std::unique_ptr<PolyhedralSurface> result{
      new PolyhedralSurface(building->as<Solid>().exteriorShell())};
  result->addPolygons(*roof);
  return result;
}

auto
straightSkeletonPartition(const Geometry &g, bool autoOrientation)
    -> std::unique_ptr<MultiPolygon>
{
  SFCGAL_ASSERT_GEOMETRY_VALIDITY_2D(g);

  std::unique_ptr<MultiPolygon> result(new MultiPolygon);

  switch (g.geometryTypeId()) {
  case TYPE_TRIANGLE:
    return straightSkeletonPartition(g.as<Triangle>().toPolygon(),
                                     autoOrientation);
  case TYPE_POLYGON:
    return straightSkeletonPartition(g.as<Polygon>(), autoOrientation);
  case TYPE_MULTIPOLYGON:
    return straightSkeletonPartition(g.as<MultiPolygon>(), autoOrientation);
  default:
    BOOST_THROW_EXCEPTION(
        Exception("Geometry must be a Polygon or MultiPolygon"));
  }

  return result;
}

auto
straightSkeletonPartition(const MultiPolygon &g, bool autoOrientation)
    -> std::unique_ptr<MultiPolygon>
{
  std::unique_ptr<MultiPolygon> result(new MultiPolygon);

  for (size_t i = 0; i < g.numGeometries(); i++) {
    std::unique_ptr<MultiPolygon> partitioned =
        straightSkeletonPartition(g.polygonN(i), autoOrientation);
    for (size_t j = 0; j < partitioned->numGeometries(); j++) {
      result->addGeometry(partitioned->geometryN(j));
    }
  }

  return result;
}

auto
straightSkeletonPartition(const Polygon &g, bool /*autoOrientation*/)
    -> std::unique_ptr<MultiPolygon>
{
  std::unique_ptr<MultiPolygon> result(new MultiPolygon);

  if (g.isEmpty()) {
    return result;
  }

  Kernel::Vector_2                      trans;
  Polygon_with_holes_2 const            polygon  = preparePolygon(g, trans);
  SHARED_PTR<Straight_skeleton_2> const skeleton = straightSkeleton(polygon);

  if (skeleton == nullptr) {
    BOOST_THROW_EXCEPTION(Exception("CGAL failed to create straightSkeleton"));
  }

  // Function to create a polygon from a face
  auto create_polygon_from_face =
      [&trans](const Straight_skeleton_2::Face_const_handle &face) {
        std::vector<Point>                         points;
        Straight_skeleton_2::Halfedge_const_handle start   = face->halfedge();
        Straight_skeleton_2::Halfedge_const_handle current = start;
        do {
          const Point_2 &p = current->vertex()->point();
          points.emplace_back(CGAL::to_double(p.x()), CGAL::to_double(p.y()));
          current = current->next();
        } while (current != start);

        SFCGAL::Polygon poly(SFCGAL::LineString(std::move(points)));
        algorithm::translate(poly, trans);
        return poly;
      };

  // Function to check if a face corresponds to a hole
  auto is_hole_face = [&polygon](
                          const Straight_skeleton_2::Face_const_handle &face) {
    Point_2 test_point = face->halfedge()->vertex()->point();
    for (auto hit = polygon.holes_begin(); hit != polygon.holes_end(); ++hit) {
      if (hit->bounded_side(test_point) == CGAL::ON_BOUNDED_SIDE) {
        return true;
      }
    }
    return false;
  };

  // Iterate through the faces of the skeleton
  for (auto face = skeleton->faces_begin(); face != skeleton->faces_end();
       ++face) {
    // Skip the faces that correspond to holes
    if (is_hole_face(face))
      continue;

    result->addGeometry(create_polygon_from_face(face));
  }

  return result;
}
} // namespace SFCGAL::algorithm