-
Notifications
You must be signed in to change notification settings - Fork 40
/
Polygon.hs
324 lines (264 loc) · 12.6 KB
/
Polygon.hs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
{-# LANGUAGE ScopedTypeVariables #-}
module Data.Geometry.Polygon where
import Control.Lens hiding (Simple)
import Data.Bifunctor
import qualified Data.CircularSeq as C
import Data.Ext
import qualified Data.Foldable as F
import Data.Geometry.Boundary
import Data.Geometry.Box
import Data.Geometry.Line
import Data.Geometry.LineSegment
import Data.Geometry.Point
import Data.Geometry.Properties
import Data.Geometry.Transformation
import Data.Geometry.Vector
import Data.Maybe (mapMaybe)
import Data.Proxy
import Data.Range
import Data.Semigroup
import Data.Util
import Frames.CoRec (asA)
-- import qualified Data.CircularList as C
--------------------------------------------------------------------------------
-- * Polygons
{- $setup
>>> :{
let simplePoly :: SimplePolygon () Rational
simplePoly = SimplePolygon . C.fromList . map ext $ [ point2 0 0
, point2 10 0
, point2 10 10
, point2 5 15
, point2 1 11
]
:} -}
-- | We distinguish between simple polygons (without holes) and Polygons with holes.
data PolygonType = Simple | Multi
data Polygon (t :: PolygonType) p r where
SimplePolygon :: C.CSeq (Point 2 r :+ p) -> Polygon Simple p r
MultiPolygon :: C.CSeq (Point 2 r :+ p) -> [Polygon Simple p r] -> Polygon Multi p r
type SimplePolygon = Polygon Simple
type MultiPolygon = Polygon Multi
-- | Polygons are per definition 2 dimensional
type instance Dimension (Polygon t p r) = 2
type instance NumType (Polygon t p r) = r
instance (Show p, Show r) => Show (Polygon t p r) where
show (SimplePolygon vs) = "SimplePolygon " <> show vs
show (MultiPolygon vs hs) = "MultiPolygon " <> show vs <> " " <> show hs
instance (Eq p, Eq r) => Eq (Polygon t p r) where
(SimplePolygon vs) == (SimplePolygon vs') = vs == vs'
(MultiPolygon vs hs) == (MultiPolygon vs' hs') = vs == vs' && hs == hs'
instance PointFunctor (Polygon t p) where
pmap f (SimplePolygon vs) = SimplePolygon (fmap (first f) vs)
pmap f (MultiPolygon vs hs) = MultiPolygon (fmap (first f) vs) (map (pmap f) hs)
instance Num r => IsTransformable (Polygon t p r) where
transformBy = transformPointFunctor
instance IsBoxable (Polygon t p r) where
boundingBox = boundingBoxList' . toListOf (outerBoundary.traverse.core)
-- * Functions on Polygons
outerBoundary :: forall t p r. Lens' (Polygon t p r) (C.CSeq (Point 2 r :+ p))
outerBoundary = lens g s
where
g :: Polygon t p r -> C.CSeq (Point 2 r :+ p)
g (SimplePolygon vs) = vs
g (MultiPolygon vs _) = vs
s :: Polygon t p r -> C.CSeq (Point 2 r :+ p)
-> Polygon t p r
s (SimplePolygon _) vs = SimplePolygon vs
s (MultiPolygon _ hs) vs = MultiPolygon vs hs
holes :: forall p r. Lens' (Polygon Multi p r) [Polygon Simple p r]
holes = lens g s
where
g :: Polygon Multi p r -> [Polygon Simple p r]
g (MultiPolygon _ hs) = hs
s :: Polygon Multi p r -> [Polygon Simple p r]
-> Polygon Multi p r
s (MultiPolygon vs _) = MultiPolygon vs
-- | Access the i^th vertex on the outer boundary
outerVertex :: Int -> Lens' (Polygon t p r) (Point 2 r :+ p)
outerVertex i = outerBoundary.C.item i
-- running time: $O(\log i)$
outerBoundaryEdge :: Int -> Polygon t p r -> LineSegment 2 p r
outerBoundaryEdge i p = let u = p^.outerVertex i
v = p^.outerVertex (i+1)
in LineSegment (Closed u) (Open v)
-- | Get all holes in a polygon
holeList :: Polygon t p r -> [Polygon Simple p r]
holeList (SimplePolygon _) = []
holeList (MultiPolygon _ hs) = hs
-- | The vertices in the polygon. No guarantees are given on the order in which
-- they appear!
vertices :: Polygon t p r -> [Point 2 r :+ p]
vertices (SimplePolygon vs) = F.toList vs
vertices (MultiPolygon vs hs) = F.toList vs ++ concatMap vertices hs
fromPoints :: [Point 2 r :+ p] -> SimplePolygon p r
fromPoints = SimplePolygon . C.fromList
-- | The edges along the outer boundary of the polygon. The edges are half open.
outerBoundaryEdges :: Polygon t p r -> C.CSeq (LineSegment 2 p r)
outerBoundaryEdges = toEdges . (^.outerBoundary)
-- | Gets the i^th edge on the outer boundary of the polygon, that is the edge
-- with vertices i and i+1 with respect to the current focus. All indices
-- modulo n.
--
-- | Given the vertices of the polygon. Produce a list of edges. The edges are
-- half-open.
toEdges :: C.CSeq (Point 2 r :+ p) -> C.CSeq (LineSegment 2 p r)
toEdges vs = C.zipLWith (\p q -> LineSegment (Closed p) (Open q)) vs (C.rotateR vs)
-- let vs' = F.toList vs in
-- C.fromList $ zipWith (\p q -> LineSegment (Closed p) (Open q)) vs' (tail vs' ++ vs')
-- | Test if q lies on the boundary of the polygon. Running time: O(n)
--
-- >>> point2 1 1 `onBoundary` simplePoly
-- False
-- >>> point2 0 0 `onBoundary` simplePoly
-- True
-- >>> point2 10 0 `onBoundary` simplePoly
-- True
-- >>> point2 5 13 `onBoundary` simplePoly
-- False
-- >>> point2 5 10 `onBoundary` simplePoly
-- False
-- >>> point2 10 5 `onBoundary` simplePoly
-- True
-- >>> point2 20 5 `onBoundary` simplePoly
-- False
--
-- TODO: testcases multipolygon
onBoundary :: (Fractional r, Ord r) => Point 2 r -> Polygon t p r -> Bool
q `onBoundary` pg = any (q `onSegment`) es
where
out = SimplePolygon $ pg^.outerBoundary
es = concatMap (F.toList . outerBoundaryEdges) $ out : holeList pg
-- | Check if a point lies inside a polygon, on the boundary, or outside of the polygon.
-- Running time: O(n).
--
-- >>> point2 1 1 `inPolygon` simplePoly
-- Inside
-- >>> point2 0 0 `inPolygon` simplePoly
-- OnBoundary
-- >>> point2 10 0 `inPolygon` simplePoly
-- OnBoundary
-- >>> point2 5 13 `inPolygon` simplePoly
-- Inside
-- >>> point2 5 10 `inPolygon` simplePoly
-- Inside
-- >>> point2 10 5 `inPolygon` simplePoly
-- OnBoundary
-- >>> point2 20 5 `inPolygon` simplePoly
-- Outside
--
-- TODO: Add some testcases with multiPolygons
-- TODO: Add some more onBoundary testcases
inPolygon :: forall t p r. (Fractional r, Ord r)
=> Point 2 r -> Polygon t p r
-> PointLocationResult
q `inPolygon` pg
| q `onBoundary` pg = OnBoundary
| odd kl && odd kr && not (any (q `inHole`) hs) = Inside
| otherwise = Outside
where
l = horizontalLine $ q^.yCoord
-- Given a line segment, compute the intersection point (if a point) with the
-- line l
intersectionPoint = asA (Proxy :: Proxy (Point 2 r)) . (`intersect` l)
-- Count the number of intersections that the horizontal line through q
-- maxes with the polygon, that are strictly to the left and strictly to
-- the right of q. If these numbers are both odd the point lies within the polygon.
--
--
-- note that: - by the asA (Point 2 r) we ignore horizontal segments (as desired)
-- - by the filtering, we effectively limit l to an open-half line, starting
-- at the (open) point q.
-- - by using half-open segments as edges we avoid double counting
-- intersections that coincide with vertices.
-- - If the point is outside, and on the same height as the
-- minimum or maximum coordinate of the polygon. The number of
-- intersections to the left or right may be one. Thus
-- incorrectly classifying the point as inside. To avoid this,
-- we count both the points to the left *and* to the right of
-- p. Only if both are odd the point is inside. so that if
-- the point is outside, and on the same y-coordinate as one
-- of the extermal vertices (one ofth)
--
-- See http://geomalgorithms.com/a03-_inclusion.html for more information.
SP kl kr = count (\p -> (p^.xCoord) `compare` (q^.xCoord))
. mapMaybe intersectionPoint . F.toList . outerBoundaryEdges $ pg
-- For multi polygons we have to test if we do not lie in a hole .
inHole = insidePolygon
hs = holeList pg
count :: (a -> Ordering) -> [a] -> SP Int Int
count f = foldr (\x (SP lts gts) -> case f x of
LT -> SP (lts + 1) gts
EQ -> SP lts gts
GT -> SP lts (gts + 1)) (SP 0 0)
-- | Test if a point lies strictly inside the polgyon.
insidePolygon :: (Fractional r, Ord r) => Point 2 r -> Polygon t p r -> Bool
q `insidePolygon` pg = q `inPolygon` pg == Inside
-- testQ = map (`inPolygon` testPoly) [ point2 1 1 -- Inside
-- , point2 0 0 -- OnBoundary
-- , point2 5 14 -- Inside
-- , point2 5 10 -- Inside
-- , point2 10 5 -- OnBoundary
-- , point2 20 5 -- Outside
-- ]
-- testPoly :: SimplePolygon () Rational
-- testPoly = SimplePolygon . C.fromList . map ext $ [ point2 0 0
-- , point2 10 0
-- , point2 10 10
-- , point2 5 15
-- , point2 1 11
-- ]
-- | Compute the area of a polygon
area :: Fractional r => Polygon t p r -> r
area poly@(SimplePolygon _) = abs $ signedArea poly
area (MultiPolygon vs hs) = area (SimplePolygon vs) - sum [area h | h <- hs]
-- | Compute the signed area of a simple polygon. The the vertices are in
-- clockwise order, the signed area will be negative, if the verices are given
-- in counter clockwise order, the area will be positive.
signedArea :: Fractional r => SimplePolygon p r -> r
signedArea poly = x / 2
where
x = sum [ p^.core.xCoord * q^.core.yCoord - q^.core.xCoord * p^.core.yCoord
| LineSegment' p q <- F.toList $ outerBoundaryEdges poly ]
-- | Compute the centroid of a simple polygon.
centroid :: Fractional r => SimplePolygon p r -> Point 2 r
centroid poly = Point $ sum' xs ^/ (6 * signedArea poly)
where
xs = [ (toVec p ^+^ toVec q) ^* (p^.xCoord * q^.yCoord - q^.xCoord * p^.yCoord)
| LineSegment' (p :+ _) (q :+ _) <- F.toList $ outerBoundaryEdges poly ]
sum' = F.foldl' (^+^) zero
-- | Test if the outer boundary of the polygon is in clockwise or counter
-- clockwise order.
--
-- running time: $O(1)$
--
isCounterClockwise :: (Eq r, Fractional r) => Polygon t p r -> Bool
isCounterClockwise = (\x -> x == abs x) . signedArea
. fromPoints . take 3 . F.toList . (^.outerBoundary)
-- | Orient the outer boundary to clockwise order
toClockwiseOrder :: (Eq r, Fractional r) => Polygon t p r -> Polygon t p r
toClockwiseOrder p
| isCounterClockwise p = p&outerBoundary %~ C.reverseDirection
| otherwise = p
-- | Orient the outer boundary to counter clockwise order
toCounterClockWiseOrder :: (Eq r, Fractional r) => Polygon t p r -> Polygon t p r
toCounterClockWiseOrder p
| not $ isCounterClockwise p = p&outerBoundary %~ C.reverseDirection
| otherwise = p
-- | Convert a Polygon to a simple polygon by forgetting about any holes.
asSimplePolygon :: Polygon t p r -> SimplePolygon p r
asSimplePolygon poly@(SimplePolygon _) = poly
asSimplePolygon (MultiPolygon vs _) = SimplePolygon vs
-- | Comparison that compares which point is 'larger' in the direction given by
-- the vector u.
cmpExtreme :: (Num r, Ord r)
=> Vector 2 r -> Point 2 r :+ p -> Point 2 r :+ q -> Ordering
cmpExtreme u p q = u `dot` (p^.core .-. q^.core) `compare` 0
-- | Finds the extreme points, minimum and maximum, in a given direction
--
-- running time: $O(n)$
extremesLinear :: (Ord r, Num r) => Vector 2 r -> Polygon t p r
-> (Point 2 r :+ p, Point 2 r :+ p)
extremesLinear u p = let vs = p^.outerBoundary
f = cmpExtreme u
in (F.minimumBy f vs, F.maximumBy f vs)