packages feed

geometry-simple-0.1.0.0: src/Data/Geometry/Topology/Snapping.hs

{- | Tolerance-based noding for precision retries.

Nearby vertices share one stored position. Segments receive shared intersection
nodes and nearby vertices before exact overlay. The algorithm follows the
vertex and near-segment snapping strategy used by GEOS SnappingNoder.
-}
module Data.Geometry.Topology.Snapping (snapPlanars) where

import Data.Geometry.Internal (TopologicalDimension (..))
import Data.Geometry.Topology.Planar
import Data.List (minimumBy, sortOn)
import qualified Data.List as List
import qualified Data.Map.Strict as Map
import Data.Ord (comparing)

{- | Snap two inputs together within a positive distance tolerance.
Process stored vertices before generated intersections. Select representatives
in XY order so operand order does not determine the coordinate choice.
-}
snapPlanars :: Rational -> Planar -> Planar -> (Planar, Planar)
snapPlanars tolerance first second = (finish a, finish b)
  where
    stored = representatives tolerance (unique (allPositions first ++ allPositions second))
    a = mapPositions (stored Map.!) first
    b = mapPositions (stored Map.!) second
    edges = segments a ++ segments b
    queryEdges = segmentQuery edges
    noded = nodeSegments edges []
    nodes = representatives tolerance (unique (allPositions a ++ allPositions b) ++ unique (concatMap (\(p, q) -> [p, q]) noded))
    candidates = segmentQuery [(p, p) | p <- unique (Map.elems nodes)]
    squaredTolerance = tolerance * tolerance
    finish shape = Planar (planarPoints shape ++ collapsed) curves polygons
      where
        paths = map path (planarLines shape)
        collapsed = [p | [p] <- paths]
        curves = [line | line@(_ : _ : _) <- paths]
        polygons = [shell : filter hasArea holes | rings <- planarPolygons shape, shell : holes <- [map path rings], hasArea shell]
    hasArea ring = planarDimension (Planar [] [] [[ring]]) == SurfaceDimension
    path [] = []
    path values@(start : _) = start : concatMap insert (lineSegments values)
    insert edge@(p@(px, py), q@(qx, qy)) = sortOn parameter (filter between (unique (crossings ++ nearby))) ++ [q]
      where
        direction = subtractPosition q p
        parameter point = dot (subtractPosition point p) direction
        between point = parameter point > 0 && parameter point < squaredLength direction
        crossings = [nodes Map.! point | other <- queryEdges edge, point <- segmentIntersection edge other]
        expanded = ((min px qx - tolerance, min py qy - tolerance), (max px qx + tolerance, max py qy + tolerance))
        nearby =
            [ point
            | (point, _) <- candidates expanded
            , squaredLength (subtractPosition point p) >= squaredTolerance
            , squaredLength (subtractPosition point q) >= squaredTolerance
            , squaredLength (segmentOffset point edge) < squaredTolerance
            ]

-- | Transform every position without changing the component structure.
mapPositions :: (Position -> Position) -> Planar -> Planar
mapPositions f (Planar points lines' polygons) = Planar (map f points) (map (map f) lines') (map (map (map f)) polygons)

{- | Select an existing nearby representative or store a rounded position.
The spatial buckets contain only representatives. A chain of nearby inputs
therefore cannot move an endpoint farther than the tolerance.
-}
representatives :: Rational -> [Position] -> Map.Map Position Position
representatives tolerance = snd . List.foldl' add (Map.empty, Map.empty)
  where
    squaredTolerance = tolerance * tolerance
    bucket (x, y) = (floor (x / tolerance) :: Integer, floor (y / tolerance) :: Integer)
    add state@(buckets, assigned) point
        | Map.member point assigned = state
        | otherwise = case nearby of
            [] ->
                let chosen = rounded point
                 in (Map.insertWith (++) (bucket chosen) [chosen] buckets, Map.insert point chosen assigned)
            _ -> (buckets, Map.insert point (minimumBy (comparing (\p -> (distance p, p))) nearby) assigned)
      where
        (i, j) = bucket point
        distance p = squaredLength (subtractPosition p point)
        nearby = [p | x <- [i - 1 .. i + 1], y <- [j - 1 .. j + 1], p <- Map.findWithDefault [] (x, y) buckets, distance p <= squaredTolerance]
    rounded (x, y) = (toRational (fromRational x :: Double), toRational (fromRational y :: Double))