moonlight-planar-1.1.0.0: test/native/Moonlight/Planar/ExactSegmentEventsSpec.hs
-- | Exact segment-event sweep against an independent quadratic oracle.
module Moonlight.Planar.ExactSegmentEventsSpec (tests) where
import Control.Monad ( foldM )
import Data.Foldable ( traverse_ )
import Moonlight.Planar.Exact ( SegmentRelation(..), ExactPoint, exactLineIntersection,
exactOnClosedSegment, exactSegment, exactSegmentEndpoints, exactSegmentRelation,
ExactIntersectionError, ExactSegment )
import Moonlight.Planar.Internal.ExactSegmentEvents ( ExactSegmentEventPlan,
ExactSweepSegmentId(..), exactSegmentEventPlan, exactSegmentRelationMap, exactSegmentSplitPoints,
exactSegmentSweepMaximumHeight )
import Support ( assertEqual, integerPoint, requireRight )
import qualified Data.Map.Strict as Map
import qualified Data.Set as Set
import qualified Data.Vector as V
tests :: IO ()
tests = testExactSegmentEventPlan
testExactSegmentEventPlan :: IO ()
testExactSegmentEventPlan = do
sourceFixture <-
traverse
(uncurry integerSegment)
[ ((0, 0), (4, 0))
, ((4, 0), (0, 0))
, ((2, 0), (6, 0))
, ((4, 0), (4, 4))
, ((1, -1), (1, 0))
, ((0, -1), (4, 1))
, ((10, 10), (11, 10))
]
verticalAndMultiway <-
traverse
(uncurry integerSegment)
[ ((0, -4), (0, 4))
, ((-4, 0), (4, 0))
, ((-3, -3), (3, 3))
, ((-3, 3), (3, -3))
, ((0, 1), (0, 5))
, ((0, 4), (0, 7))
]
grid <-
traverse
(uncurry integerSegment)
( [((-1, y), (5, y)) | y <- [0 .. 4]]
<> [((x, -1), (x, 5)) | x <- [0 .. 4]]
)
collinearOverlaps <-
traverse
(uncurry integerSegment)
[ ((0, 0), (8, 0))
, ((1, 0), (3, 0))
, ((2, 0), (6, 0))
, ((5, 0), (9, 0))
, ((8, 0), (10, 0))
, ((11, 0), (12, 0))
]
traverse_
compareEventPlanWithOracle
[sourceFixture, verticalAndMultiway, grid, collinearOverlaps]
let allRelations =
Set.fromList
[ exactSegmentRelation a b c d
| (leftIndex, left) <- zip [0 :: Int ..] sourceFixture
, right <- drop (leftIndex + 1) sourceFixture
, let (a, b) = exactSegmentEndpoints left
(c, d) = exactSegmentEndpoints right
]
assertEqual
"source fixture covers every segment relation"
( Set.fromList
[ SegmentsDisjoint
, SegmentsProperlyCross
, SegmentsShareEndpoint
, SegmentEndpointTouchesInterior
, SegmentsCollinearlyOverlap
, SegmentsDuplicate
]
)
allRelations
compareEventPlanWithOracle :: [ExactSegment] -> IO ()
compareEventPlanWithOracle segments = do
let vector = V.fromList segments
plan <- requireRight "exact event sweep" (exactSegmentEventPlan vector)
expectedSplits <- requireRight "quadratic split oracle" (quadraticSplitPoints segments)
assertEqual
"sweep relation map agrees with quadratic oracle"
(quadraticRelationMap segments)
(exactSegmentRelationMap plan)
traverse_
(assertSegmentSplits plan expectedSplits)
[0 .. length segments - 1]
let heightLimit = 2 * ceilingLog2 (length segments + 1)
if exactSegmentSweepMaximumHeight plan <= heightLimit
then pure ()
else
fail
( "AVL height exceeded conservative logarithmic bound: "
<> show (exactSegmentSweepMaximumHeight plan, heightLimit)
)
assertSegmentSplits
:: ExactSegmentEventPlan
-> Map.Map Int (Set.Set ExactPoint)
-> Int
-> IO ()
assertSegmentSplits plan expected segmentIndex =
assertEqual
("sweep split points agree for segment " <> show segmentIndex)
(Map.findWithDefault Set.empty segmentIndex expected)
(Set.fromList (exactSegmentSplitPoints plan (ExactSweepSegmentId segmentIndex)))
quadraticRelationMap
:: [ExactSegment]
-> Map.Map (ExactSweepSegmentId, ExactSweepSegmentId) SegmentRelation
quadraticRelationMap segments =
Map.fromList
[ ((ExactSweepSegmentId leftIndex, ExactSweepSegmentId rightIndex), relation)
| (leftIndex, left) <- zip [0 :: Int ..] segments
, (rightIndex, right) <- zip [leftIndex + 1 ..] (drop (leftIndex + 1) segments)
, let (a, b) = exactSegmentEndpoints left
(c, d) = exactSegmentEndpoints right
relation = exactSegmentRelation a b c d
, relation /= SegmentsDisjoint
]
quadraticSplitPoints
:: [ExactSegment]
-> Either ExactIntersectionError (Map.Map Int (Set.Set ExactPoint))
quadraticSplitPoints segments =
foldM addRelation initial (segmentPairs segments)
where
initial =
Map.fromList
[ (index, Set.fromList [from, to])
| (index, segment) <- zip [0 :: Int ..] segments
, let (from, to) = exactSegmentEndpoints segment
]
addRelation
:: Map.Map Int (Set.Set ExactPoint)
-> (Int, ExactSegment, Int, ExactSegment)
-> Either ExactIntersectionError (Map.Map Int (Set.Set ExactPoint))
addRelation splitPoints (leftIndex, left, rightIndex, right) = do
witnesses <- relationSplitWitnesses left right
pure
( Map.insertWith Set.union rightIndex (Set.fromList witnesses)
(Map.insertWith Set.union leftIndex (Set.fromList witnesses) splitPoints)
)
segmentPairs :: [value] -> [(Int, value, Int, value)]
segmentPairs values =
[ (leftIndex, left, rightIndex, right)
| (leftIndex, left) <- zip [0 :: Int ..] values
, (rightIndex, right) <- zip [leftIndex + 1 ..] (drop (leftIndex + 1) values)
]
relationSplitWitnesses
:: ExactSegment
-> ExactSegment
-> Either ExactIntersectionError [ExactPoint]
relationSplitWitnesses left right =
case exactSegmentRelation a b c d of
SegmentsDisjoint -> Right []
SegmentsProperlyCross -> (: []) <$> exactLineIntersection left right
SegmentsDuplicate -> Right []
_ ->
Right
( Set.toAscList
( Set.fromList
[ point
| point <- [a, b, c, d]
, exactOnClosedSegment a b point
, exactOnClosedSegment c d point
]
)
)
where
(a, b) = exactSegmentEndpoints left
(c, d) = exactSegmentEndpoints right
ceilingLog2 :: Int -> Int
ceilingLog2 target = length (takeWhile (< target) (iterate (* 2) 1))
integerSegment :: (Integer, Integer) -> (Integer, Integer) -> IO ExactSegment
integerSegment (fromX, fromY) (toX, toY) =
requireRight
"integer exact segment"
(exactSegment (integerPoint fromX fromY) (integerPoint toX toY))