geometry-simple-0.1.0.0: src/Data/Geometry/Topology/Planar.hs
{-# LANGUAGE BangPatterns #-}
-- | Exact planar primitives shared by the topology operations.
module Data.Geometry.Topology.Planar where
import Data.Geometry.Internal
import Data.List (sortBy, sortOn)
import qualified Data.List as List
import qualified Data.Map.Strict as Map
import qualified Data.Set as Set
import qualified Data.Vector as V
import qualified Data.Vector.Unboxed as U
-- | An exact position in the XY plane.
type Position = (Rational, Rational)
-- | A closed straight segment.
type Segment = (Position, Position)
-- | A balanced tree of segments with exact bounding boxes on its branches.
data SegmentIndex
= -- | One segment, which can have coincident endpoints.
SegmentLeaf !Segment
| -- | The enclosing box and two nonempty subtrees.
SegmentBranch !Segment !SegmentIndex !SegmentIndex
-- | Enclose the indexed segments. Leaf endpoints also specify their bounds.
indexBounds :: SegmentIndex -> Segment
indexBounds (SegmentLeaf edge) = edge
indexBounds (SegmentBranch bounds _ _) = bounds
-- | Divide segments at the median, alternating X and Y at each level.
indexSegments :: [Segment] -> Maybe SegmentIndex
indexSegments = build fst snd
where
build _ _ [] = Nothing
build _ _ [edge] = Just (SegmentLeaf edge)
build coordinate other edges = do
let (first, second) = splitAt (length edges `div` 2) (sortOn (\(a, b) -> coordinate a + coordinate b) edges)
left <- build other coordinate first
right <- build other coordinate second
let ((ax, ay), (bx, by)) = indexBounds left
((cx, cy), (dx, dy)) = indexBounds right
bounds = ((minimum [ax, bx, cx, dx], minimum [ay, by, cy, dy]), (maximum [ax, bx, cx, dx], maximum [ay, by, cy, dy]))
pure (SegmentBranch bounds left right)
-- | Build one index and select segments whose closed bounds meet each query box.
segmentQuery :: [Segment] -> Segment -> [Segment]
segmentQuery edges = maybe (const []) querySegments (indexSegments edges)
-- | Select leaves whose closed bounds intersect the supplied box.
querySegments :: SegmentIndex -> Segment -> [Segment]
querySegments tree bounds
| not (overlapsBounds bounds (indexBounds tree)) = []
| otherwise = case tree of
SegmentLeaf edge -> [edge]
SegmentBranch _ left right -> querySegments left bounds ++ querySegments right bounds
-- | Attach values to indexed bounds, retaining values with equal boxes.
boundsQuery :: [(Segment, a)] -> Segment -> [a]
boundsQuery entries = concatMap (table Map.!) . query
where
table = Map.fromListWith (++) [(bounds, [value]) | (bounds, value) <- entries]
query = segmentQuery (Map.keys table)
-- | Enumerate unordered pairs with intersecting bounds, including equal boxes.
overlappingPairs :: [(Segment, a)] -> [(a, a)]
overlappingPairs entries = [(a, b) | (bounds, (i, a)) <- indexed, (j, b) <- query bounds, i < j]
where
indexed = zipWith (\i (bounds, value) -> (bounds, (i, value))) [0 :: Int ..] entries
query = boundsQuery indexed
-- | Prepare exact winding queries over directed edges, retaining duplicate edges.
prepareWinding :: [Segment] -> Position -> Int
prepareWinding edges = maybe (const 0) windingIndex (indexSegments edges)
{- | Count crossings without visiting branches wholly to the right of the point.
Each branch stores cumulative changes at endpoint Y values. An upward edge
adds one between its endpoints; a downward edge subtracts one. Shared vertices
cancel when branches combine. Branches that contain the query X still use exact
orientation tests at their leaves.
-}
windingIndex :: SegmentIndex -> Position -> Int
windingIndex = snd . build
where
build (SegmentLeaf (a@(_, ay), b@(_, by))) =
( Map.filter (/= 0) (Map.fromListWith (+) [(ay, 1), (by, -1)])
, \point@(_, y) ->
if ay <= y && by > y && orientation a b point == GT
then 1
else if by <= y && ay > y && orientation a b point == LT then -1 else 0
)
build (SegmentBranch ((ax, ay), (bx, by)) left right) = (changes, classify)
where
(leftChanges, leftWinding) = build left
(rightChanges, rightWinding) = build right
changes = Map.filter (/= 0) (Map.unionWith (+) leftChanges rightChanges)
cumulative = snd (Map.mapAccum (\total delta -> let next = total + delta in (next, next)) 0 changes)
classify point@(x, y)
| x >= bx || y < ay || y >= by = 0
| x < ax = maybe 0 snd (Map.lookupLE y cumulative)
| otherwise = leftWinding point + rightWinding point
-- | A point's location relative to a geometry.
data Location = Exterior | Boundary | Interior deriving (Eq, Ord, Show, Read)
-- | The atomic components of a geometry, projected into the XY plane.
data Planar = Planar
{ planarPoints :: [Position]
, planarLines :: [[Position]]
, planarPolygons :: [[[Position]]]
}
deriving (Eq, Show, Read)
-- | Convert finite coordinates without rounding their binary values.
positions :: Coordinates -> [Position]
positions = withCoordinates (map position . U.toList)
-- | Project one finite coordinate into the plane.
position :: (Coordinate c) => c -> Position
position coordinate = let (x, y, _, _) = coordinateComponents coordinate in (toRational x, toRational y)
-- | Flatten collections while retaining polygon and line boundaries.
planar :: Geometry -> Planar
planar geometry =
let Planar points lines' polygons = collect (Planar [] [] []) geometry
in Planar (reverse points) (reverse lines') (reverse polygons)
where
collect rest@(Planar points lines' polygons) shape = case shape of
PointGeometry point -> maybe rest (\p -> Planar (p : points) lines' polygons) (withPoint position point)
LineString line -> Planar points (positions line : lines') polygons
Polygon (PolygonRings shell holes) -> Planar points lines' (map positions (shell : V.toList holes) : polygons)
MultiPoint values -> U.foldl' (\acc point -> collect acc (PointGeometry point)) rest values
MultiLineString values -> V.foldl' (\acc line -> collect acc (LineString line)) rest values
MultiPolygon values -> V.foldl' (\acc rings -> collect acc (Polygon rings)) rest values
GeometryCollection children -> V.foldl' collect rest children
-- | Concatenate atomic components without changing their coordinates.
combinePlanar :: [Planar] -> Planar
combinePlanar parts = Planar (concatMap planarPoints parts) (concatMap planarLines parts) (concatMap planarPolygons parts)
{- | Find the dimension of a valid point set, ignoring empty components.
Collapsed lines and collinear rings contribute only their stored point set.
-}
planarDimension :: Planar -> TopologicalDimension
planarDimension shape
| any (any spansArea) (planarPolygons shape) = SurfaceDimension
| not (null (segments shape)) = CurveDimension
| not (null (allPositions shape)) = PointDimension
| otherwise = NoDimension
-- | Test whether the positions do not all lie on one line.
spansArea :: [Position] -> Bool
spansArea (first : rest) = case dropWhile (== first) rest of
second : remaining -> any ((/= EQ) . orientation first second) remaining
[] -> False
spansArea [] = False
-- | Remove duplicates and return values in ascending order.
unique :: (Ord a) => [a] -> [a]
unique = Set.toAscList . Set.fromList
-- | List adjacent pairs, excluding segments with zero length.
lineSegments :: [Position] -> [Segment]
lineSegments points = [(a, b) | (a, b) <- zip points (drop 1 points), a /= b]
-- | List the segments of a closed ring.
ringSegments :: [Position] -> [Segment]
ringSegments [] = []
ringSegments points = lineSegments (points ++ take 1 points)
-- | List every line segment and polygon boundary segment.
segments :: Planar -> [Segment]
segments shape = concatMap lineSegments (planarLines shape) ++ concatMap ringSegments (concat (planarPolygons shape))
-- | List distinct input coordinates, including collapsed lines.
vertices :: Planar -> [Position]
vertices = unique . allPositions
-- | Traverse coordinates without sorting or removing duplicates.
allPositions :: Planar -> [Position]
allPositions shape = planarPoints shape ++ concat (planarLines shape) ++ concat (concat (planarPolygons shape))
-- | Enclose a nonempty point set in an exact axis-aligned rectangle.
pointBounds :: [Position] -> Maybe Segment
pointBounds [] = Nothing
pointBounds (first : rest) = Just (List.foldl' extend (first, first) rest)
where
extend ((!ax, !ay), (!bx, !by)) (x, y) = ((min ax x, min ay y), (max bx x, max by y))
-- | Test strict separation of the input envelopes. Empty inputs are disjoint.
disjointBounds :: [Position] -> [Position] -> Bool
disjointBounds a b = case (pointBounds a, pointBounds b) of
(Just first, Just second) -> not (overlapsBounds first second)
_ -> True
-- | Test whether the first nonempty envelope contains the second one.
enclosesBounds :: [Position] -> [Position] -> Bool
enclosesBounds a b = case (pointBounds a, pointBounds b) of
(Just ((ax, ay), (bx, by)), Just ((cx, cy), (dx, dy))) -> ax <= cx && ay <= cy && bx >= dx && by >= dy
_ -> False
-- | Choose one point from each connected component before testing its edges.
componentPoints :: Planar -> [Position]
componentPoints shape =
planarPoints shape
++ [point | point : _ <- planarLines shape]
++ [point | (point : _) : _ <- planarPolygons shape]
-- | Test segment contacts using both coordinate bounds and stop at the first hit.
segmentsIntersect :: [Segment] -> [Segment] -> Bool
segmentsIntersect first second = any (\edge -> not (all (null . segmentIntersection edge) (query edge))) first
where
query = segmentQuery second
-- | The displacement from the second position to the first.
subtractPosition :: Position -> Position -> Position
subtractPosition (x, y) (u, v) = (x - u, y - v)
-- | Translate a position by an XY displacement.
addPosition :: Position -> Position -> Position
addPosition (x, y) (u, v) = (x + u, y + v)
-- | Scale both XY components by an exact factor.
scalePosition :: Rational -> Position -> Position
scalePosition t (x, y) = (t * x, t * y)
-- | The signed area of the parallelogram spanned by two vectors.
cross :: Position -> Position -> Rational
cross (x, y) (u, v) = x * v - y * u
-- | The turn from the first segment to the second: GT is counterclockwise.
orientation :: Position -> Position -> Position -> Ordering
orientation a b c = compare (cross (subtractPosition b a) (subtractPosition c a)) 0
-- | Test membership of a closed segment with exact arithmetic.
pointOnSegment :: Position -> Segment -> Bool
pointOnSegment point@(x, y) (a@(ax, ay), b@(bx, by)) =
x >= min ax bx && x <= max ax bx && y >= min ay by && y <= max ay by && orientation a b point == EQ
-- | Return no point, one intersection, or the two ends of an overlap.
segmentIntersection :: Segment -> Segment -> [Position]
segmentIntersection first@(a, b) second@(c, d)
| not (overlapsBounds first second) = []
| denominator == 0 = case unique [p | p <- [a, b, c, d], pointOnSegment p first, pointOnSegment p second] of
[] -> []
[p] -> [p]
firstPoint : rest -> [firstPoint, last rest]
| t >= 0 && t <= 1 && u >= 0 && u <= 1 = [addPosition a (scalePosition t ab)]
| otherwise = []
where
ab = subtractPosition b a
cd = subtractPosition d c
ac = subtractPosition c a
denominator = cross ab cd
t = cross ac cd / denominator
u = cross ac ab / denominator
-- | Test whether two segment bounding boxes meet.
overlapsBounds :: Segment -> Segment -> Bool
overlapsBounds ((ax, ay), (bx, by)) ((cx, cy), (dx, dy)) =
max ax bx >= min cx dx && max cx dx >= min ax bx && max ay by >= min cy dy && max cy dy >= min ay by
-- | Split segments at every intersection and supplied point. Return unique edges.
nodeSegments :: [Segment] -> [Position] -> [Segment]
nodeSegments input points = unique (concatMap split edges)
where
edges = unique [if a < b then (a, b) else (b, a) | (a, b) <- input, a /= b]
query = segmentQuery edges
-- Segment intersections already account for every endpoint.
endpoints = Set.fromList (concatMap (\(a, b) -> [a, b]) edges)
extraPoints = Set.toList (Set.fromList points `Set.difference` endpoints)
queryPoints = segmentQuery [(p, p) | p <- extraPoints]
split edge@(a, b) = lineSegments (unique (a : b : [p | (p, _) <- queryPoints edge, pointOnSegment p edge] ++ concatMap (segmentIntersection edge) (query edge)))
-- | The exact midpoint of a segment.
midpoint :: Segment -> Position
midpoint (a, b) = scalePosition (1 / 2) (addPosition a b)
-- | Build one edge index for repeated exact location queries in a ring.
prepareRing :: [Position] -> Position -> Location
prepareRing ring = case indexSegments (ringSegments ring) of
Nothing -> const Exterior
Just tree ->
let winding = windingIndex tree
in \point ->
if any (pointOnSegment point) (querySegments tree (point, point))
then Boundary
else if odd (winding point) then Interior else Exterior
-- | Index the shell and holes once. Skip holes whose bounds exclude the point.
preparePolygon :: [[Position]] -> Position -> Location
preparePolygon [] = const Exterior
preparePolygon [shell] = prepareRing shell
preparePolygon (shell : holes) = classify
where
locateShell = prepareRing shell
queryHoles = boundsQuery [(bounds, prepareRing hole) | hole <- holes, Just bounds <- [pointBounds hole]]
classify point = case locateShell point of
Exterior -> Exterior
shellLocation
| Interior `elem` holesHere -> Exterior
| Boundary `elem` holesHere -> Boundary
| otherwise -> shellLocation
where
holesHere = map ($ point) (queryHoles (point, point))
-- | Index polygon components and retain the boundary rules for their union.
prepareSurface :: [[[Position]]] -> Position -> Location
prepareSurface [] = const Exterior
prepareSurface [polygon] = preparePolygon polygon
prepareSurface polygons = classify
where
queryPolygons = boundsQuery [(bounds, preparePolygon polygon) | polygon <- polygons, Just bounds <- [pointBounds (concat polygon)]]
queryEdges = segmentQuery (concatMap ringSegments (concat polygons))
locations point = map ($ point) (queryPolygons (point, point))
classify point
| Interior `elem` here = Interior
| otherwise = case filter (== Boundary) here of
_ : _ : _ | all (elem Interior . locations) (sectorSamples queryEdges point) -> Interior
_ : _ -> Boundary
[] -> Exterior
where
here = locations point
-- | Prepare point locations and surface membership for repeated arrangement queries.
prepareLocations :: Planar -> (Position -> Location, Position -> Bool)
prepareLocations shape = (classify, (== Interior) . locateSurface)
where
locateSurface = prepareSurface (planarPolygons shape)
queryLines = segmentQuery (concatMap lineSegments lines')
lines' = planarLines shape
linePoints = Set.fromList (concat lines')
points = Set.fromList (planarPoints shape)
endpoints = Map.fromListWith (+) [(p, 1 :: Int) | line@(first : _) <- lines', p <- [first, last line]]
classify point = case locateSurface point of
Exterior
| Set.member point linePoints || any (pointOnSegment point) (queryLines (point, point)) ->
if odd (Map.findWithDefault 0 point endpoints) then Boundary else Interior
| Set.member point points -> Interior
| otherwise -> Exterior
result -> result
-- | Select a nearby point without crossing any segment after the start point.
nearPoint :: (Segment -> [Segment]) -> Position -> Position -> Position
nearPoint query origin direction = addPosition origin (scalePosition step direction)
where
-- Only crossings before t=2 can reduce the initial step of one.
reach = (origin, addPosition origin (scalePosition 2 direction))
step = minimum (1 : [t / 2 | edge <- query reach, t <- rayParameters edge, t > 0])
rayParameters (a, b)
| determinant /= 0 = [t | u >= 0 && u <= 1]
| cross offset direction /= 0 = []
| otherwise = map parameter [a, b]
where
edge = subtractPosition b a
offset = subtractPosition a origin
determinant = cross direction edge
t = cross offset edge / determinant
u = cross offset direction / determinant
parameter point =
let (dx, dy) = direction
(x, y) = subtractPosition point origin
in if dx /= 0 then x / dx else y / dy
-- | Sample the faces immediately to the left and right of a noded edge.
sidePoints :: (Segment -> [Segment]) -> Segment -> (Position, Position)
sidePoints query segment@(a, b) = (nearPoint query middle normal, nearPoint query middle (scalePosition (-1) normal))
where
middle = midpoint segment
(dx, dy) = subtractPosition b a
normal = (-dy, dx)
-- | Sort nonzero directions counterclockwise from the positive X axis.
compareDirection :: Position -> Position -> Ordering
compareDirection a@(x, y) b@(u, v) = case compare (half x y) (half u v) of
EQ -> compare 0 (cross a b)
result -> result
where
half p q = not (q > 0 || (q == 0 && p >= 0))
-- | Sample boundary sectors using an existing edge index.
sectorSamples :: (Segment -> [Segment]) -> Position -> [Position]
sectorSamples query point = [nearPoint query point (addPosition a b) | (a, b) <- zip directions (drop 1 directions ++ take 1 directions)]
where
directions = sortBy compareDirection (unique ([(1, 0), (0, 1), (-1, 0), (0, -1)] ++ incident))
incident = [normalize direction | edge@(a, b) <- query (point, point), pointOnSegment point edge, q <- [a, b], q /= point, let direction = subtractPosition q point]
normalize (x, y) = let size = abs x + abs y in (x / size, y / size)
-- | Return the vector from the closest point on a segment to a point.
segmentOffset :: Position -> Segment -> Position
segmentOffset point (a, b)
| a == b = offset
| otherwise = subtractPosition offset (scalePosition fraction direction)
where
offset = subtractPosition point a
direction = subtractPosition b a
fraction = max 0 (min 1 (dot offset direction / squaredLength direction))
-- | The exact scalar product of two vectors.
dot :: Position -> Position -> Rational
dot (x, y) (u, v) = x * u + y * v
-- | The exact squared length of a vector.
squaredLength :: Position -> Rational
squaredLength vector = dot vector vector
{- | Scale before the square root, then round the complete length to Double.
Keep the scale exact so subnormal projections do not round to zero too early.
-}
vectorLength :: Position -> Double
vectorLength = fromRational . vectorMagnitude
-- | Approximate a norm with exact scaling and a Double square root.
vectorMagnitude :: Position -> Rational
vectorMagnitude (x, y)
| scale == 0 = 0
| otherwise = scale * toRational (sqrt (1 + fromRational (ratio * ratio)) :: Double)
where
scale = max (abs x) (abs y)
ratio = min (abs x) (abs y) / scale