packages feed

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

-- | Select points and curve portions by their M coordinate.
module Data.Geometry.Topology.Measures (locateAlong, locateBetween) where

import Data.Geometry.Internal
import qualified Data.List as List
import qualified Data.Vector as V
import qualified Data.Vector.Unboxed as U

{- | Select the points and curve portions whose measure equals the argument.
Constant-measure curve portions remain curves. See 'locateBetween' for the
empty-input, layout, and polygon rules.
-}
locateAlong :: Double -> Geometry -> Maybe Geometry
locateAlong value = locateBetween value value

{- | Select the inclusive interval between two M values. Interpolate X, Y,
and Z linearly within each segment. Retain the source coordinate layouts.
Consecutive selected portions stay connected within each input curve.

Return 'Nothing' for an empty input. Return an empty Point for no match or a
reversed interval. An input without M returns an empty XY Point.
Point-only results are MultiPoints. Curve-only results are MultiLineStrings.
A mixed result contains a MultiLineString and a MultiPoint.

For polygons, apply the operation to each boundary ring. This is the
implementation-defined surface rule permitted by OGC SFA 1.2.1, 6.1.2.6.5.
Do not interpolate measures across a polygon interior or between members.
Segments with nonfinite M values do not interpolate. Their matching endpoints
can still contribute points. A NaN interval bound matches nothing.
-}

{- HLINT ignore locateBetween "Use >" -}
locateBetween :: Double -> Double -> Geometry -> Maybe Geometry
locateBetween lower upper geometry
    | geometryEmpty geometry = Nothing
    | not (measured layout) = Just (PointGeometry (EmptyPoint DimXY))
    -- A NaN bound fails this comparison, so it selects nothing.
    | not (lower <= upper) = Just empty
    | otherwise = Just result
  where
    layout = geometryDimensions geometry
    empty = PointGeometry (EmptyPoint layout)
    (points, curves) = select lower upper geometry
    result = case (points, curves) of
        ([], []) -> empty
        (_, []) -> MultiPoint (U.fromList points)
        ([], _) -> MultiLineString (V.fromList curves)
        _ -> GeometryCollection (V.fromList [MultiLineString (V.fromList curves), MultiPoint (U.fromList points)])

-- | Whether a coordinate layout includes M.
measured :: Dimensions -> Bool
measured layout = layout == DimXYM || layout == DimXYZM

-- | Select each atomic member without joining distinct curves.
select :: Double -> Double -> Geometry -> ([Point], [Coordinates])
select lower upper geometry = case geometry of
    PointGeometry point -> ([point | selectedPoint point], [])
    LineString coordinates -> selectCoordinates lower upper coordinates
    Polygon (PolygonRings shell holes) -> selectCoordinates lower upper shell <> foldMap (selectCoordinates lower upper) holes
    MultiPoint points -> (filter selectedPoint (U.toList points), [])
    MultiLineString curves -> foldMap (selectCoordinates lower upper) curves
    MultiPolygon polygons -> foldMap (select lower upper . Polygon) polygons
    GeometryCollection children -> foldMap (select lower upper) children
  where
    selectedPoint point = measured (pointDimensions point) && maybe False (inRange lower upper . measure) (withPoint coordinateComponents point)

-- | Read the M ordinate from the common coordinate representation.
measure :: (Double, Double, Double, Double) -> Double
measure (_, _, _, value) = value

-- | Test the closed measure interval. NaN values do not match.
inRange :: Double -> Double -> Double -> Bool
inRange lower upper value = lower <= value && value <= upper

-- | Clip a measured sequence and retain its concrete coordinate layout.
selectCoordinates :: Double -> Double -> Coordinates -> ([Point], [Coordinates])
selectCoordinates lower upper coordinates = case coordinates of
    CoordinatesXYM values -> build PointXYM CoordinatesXYM values
    CoordinatesXYZM values -> build PointXYZM CoordinatesXYZM values
    _ -> ([], [])
  where
    build wrapPoint wrapCurve values =
        let runs = selectedRuns lower upper (U.toList values)
         in ( [wrapPoint value | [value] <- runs]
            , [wrapCurve (U.fromList run) | run@(_ : _ : _) <- runs]
            )

-- | Join consecutive selected pieces. An excluded segment separates runs.
selectedRuns :: (Coordinate c) => Double -> Double -> [c] -> [[c]]
selectedRuns _ _ [] = []
selectedRuns lower upper [value] = [[value] | inRange lower upper (measure (coordinateComponents value))]
selectedRuns lower upper values@(start : _) = joinEnds (reverse (finish (List.foldl' append ([], []) pieces)))
  where
    pieces = concat [clipSegment lower upper a b | (a, b) <- zip values (drop 1 values)]
    finish ([], completed) = completed
    finish (current, completed) = reverse current : completed
    append state [] = ([], finish state)
    append ([], completed) piece = (reverse piece, completed)
    append state@(current@(lastValue : _), completed) piece@(firstValue : rest)
        | sameCoordinate lastValue firstValue = (reverse rest ++ current, completed)
        | otherwise = (reverse piece, finish state)
    joinEnds runs@(initial@(first : _) : rest)
        | sameCoordinate start (last values) = case reverse rest of
            final : middle | sameCoordinate first (last final) -> (final ++ drop 1 initial) : reverse middle
            _ -> runs
    joinEnds runs = runs

-- | Clip one segment in measure space and retain its original direction.
clipSegment :: (Coordinate c) => Double -> Double -> c -> c -> [[c]]
clipSegment lower upper a b
    | not (finite start && finite end) = [[a] | inRange lower upper start] ++ [[]] ++ [[b] | inRange lower upper end]
    | start == end && not (inRange lower upper start) = [[]]
    | start == end = [if sameCoordinate a b then [a] else [a, b]]
    | selectedStart > selectedEnd = [[]]
    | selectedStart == selectedEnd = [[at selectedStart]]
    | start < end = [[at selectedStart, at selectedEnd]]
    | otherwise = [[at selectedEnd, at selectedStart]]
  where
    start = measure (coordinateComponents a)
    end = measure (coordinateComponents b)
    selectedStart = max lower (min start end)
    selectedEnd = min upper (max start end)
    at value
        | value == start = a
        | value == end = b
        | otherwise = interpolate ((toRational value - toRational start) / (toRational end - toRational start)) value a b

-- | Interpolate coordinates exactly before their final conversion to Double.
interpolate :: (Coordinate c) => Rational -> Double -> c -> c -> c
interpolate fraction value a b = coordinateFromComponents (along x u, along y v, along z w, value)
  where
    (x, y, z, _) = coordinateComponents a
    (u, v, w, _) = coordinateComponents b
    along first second
        | first == second = first
        | finite first && finite second = fromRational ((1 - fraction) * toRational first + fraction * toRational second)
        | otherwise = (1 - fromRational fraction) * first + fromRational fraction * second

-- | Compare shared endpoints while retaining unknown Z values.
sameCoordinate :: (Coordinate c) => c -> c -> Bool
sameCoordinate a b = equal x u && equal y v && equal z w && equal m n
  where
    (x, y, z, m) = coordinateComponents a
    (u, v, w, n) = coordinateComponents b
    equal first second = first == second || (isNaN first && isNaN second)