-
Notifications
You must be signed in to change notification settings - Fork 17
/
spatial.clj
457 lines (384 loc) · 15.9 KB
/
spatial.clj
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
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
(ns geo.spatial
"Provides common interfaces for working with spatial objects and geodesics.
All units are in meters/radians/steradians unless otherwise specified.
Provides static spatial contexts for the earth, and some constants like the
earth's radii and circumferences, along with points like the poles.
Defines protocols for unified access to Points and Shapes, to allow different
geometry libraries to interoperate.
Basic utility functions for unit conversion; e.g. degrees<->radians.
Functions for computing earth radii, surface distances, and surface areas.
Constructors for shapes like bounding-boxes and circles, and utility
functions over shapes for their heights, centers, areas, etc. Can also
compute relationships between shapes: their intersections, contains, disjoint
statuses, etc."
(:use [clojure.math.numeric-tower :only [abs]])
(:require [geo.jts :as jts])
(:import (ch.hsr.geohash WGS84Point)
(ch.hsr.geohash.util VincentyGeodesy)
(org.locationtech.spatial4j.shape SpatialRelation
Shape
Rectangle)
(org.locationtech.jts.geom Geometry Coordinate)
(org.locationtech.spatial4j.shape.impl GeoCircle PointImpl RectangleImpl)
(org.locationtech.spatial4j.shape.jts JtsGeometry
JtsPoint
JtsShapeFactory)
(org.locationtech.spatial4j.distance DistanceCalculator DistanceUtils)
(org.locationtech.spatial4j.context SpatialContextFactory
SpatialContext)
(org.locationtech.spatial4j.context.jts JtsSpatialContext)))
(declare spatial4j-point)
(declare geohash-point)
(declare jts-point)
(defn square [x]
(Math/pow x 2))
(def ^SpatialContext earth
"The SpatialContext of the earth, as according to spatial4j."
(SpatialContextFactory/makeSpatialContext
{"geo" "true"
"datelineRule" "width180"
"spatialContextFactory" "org.locationtech.spatial4j.context.jts.JtsSpatialContextFactory"
"distCalculator" "vincentySphere"}
(.getClassLoader JtsSpatialContext)))
(def ^JtsShapeFactory jts-earth
"ShapeFactory for producing spatial4j Shapes from JTSGeometries based"
(->> (.getClassLoader JtsSpatialContext)
(SpatialContextFactory/makeSpatialContext
{"geo" "true"
"datelineRule" "width180"
"spatialContextFactory" "org.locationtech.spatial4j.context.jts.JtsSpatialContextFactory"
"distCalculator" "vincentySphere"})
(.getShapeFactory)))
(def earth-mean-radius
"Earth's mean radius, in meters."
(* 1000 DistanceUtils/EARTH_MEAN_RADIUS_KM))
(def earth-equatorial-radius
"Earth's radius at the equator, in meters."
6378137.0)
(def earth-polar-radius
"Earth's radius at the poles, in meters."
6356752.3)
(def earth-equatorial-radius-squared
(square earth-equatorial-radius))
(def earth-polar-radius-squared
(square earth-polar-radius))
(def earth-mean-circumference
"Earth's mean circumference, in meters."
(* 1000 40041))
(def earth-equatorial-circumference
"Earth's equatorial circumference, in meters"
(* 1000 40075))
(def earth-meridional-circumference
"Earth's circumference around a meridian, in meters."
(* 1000 40008))
(defn crosses-dateline? [^Geometry jts-geom]
(>= (.getWidth (.getEnvelopeInternal jts-geom))
180))
(defprotocol Shapelike
(^Shape to-shape [this] "Convert anything to a Shape.")
(^Geometry to-jts [this] [this srid] "Convert anything to a projected JTS Geometry."))
(extend-protocol Shapelike
GeoCircle
(to-shape [this] this)
(to-jts ([_] (throw (Exception. "Cannot cast GeoCircle to JTS.")))
([_ _] (throw (Exception. "Cannot cast GeoCircle to JTS."))))
RectangleImpl
(to-shape [this] this)
(to-jts ([this] (jts/set-srid (.getGeom ^JtsGeometry this) jts/default-srid))
([this srid] (to-jts (to-jts this) srid)))
PointImpl
(to-shape [this] this)
(to-jts ([this] (jts/set-srid (jts-point (.getY this) (.getX this)) jts/default-srid))
([this srid] (to-jts (to-jts this) srid)))
JtsGeometry
(to-shape [this] this)
(to-jts ([this] (jts/set-srid (.getGeom this) jts/default-srid))
([this srid] (to-jts (to-jts this) srid)))
JtsPoint
(to-shape [this] this)
(to-jts ([this] (jts/set-srid (jts-point (.getY this) (.getX this)) jts/default-srid))
([this srid] (to-jts (to-jts this) srid)))
Geometry
(to-shape [this] (.makeShape jts-earth (jts/transform-geom this jts/default-srid) true true))
(to-jts ([this] this)
([this srid] (jts/transform-geom this srid))))
(defprotocol Point
(latitude [this])
(longitude [this])
(to-spatial4j-point [this])
(to-geohash-point [this]))
(extend-protocol Point
WGS84Point
(latitude [this] (.getLatitude this))
(longitude [this] (.getLongitude this))
(to-spatial4j-point [this] (spatial4j-point this))
(to-geohash-point [this] this)
org.locationtech.jts.geom.Point
(latitude [this] (.getY (jts/transform-geom this jts/default-srid)))
(longitude [this] (.getX (jts/transform-geom this jts/default-srid)))
(to-spatial4j-point [this] (spatial4j-point this))
(to-geohash-point [this] (geohash-point this))
org.locationtech.jts.geom.Coordinate
(latitude [this] (.y this))
(longitude [this] (.x this))
(to-spatial4j-point [this] (spatial4j-point this))
(to-geohash-point [this] (geohash-point this))
org.locationtech.spatial4j.shape.Point
(latitude [this] (.getY this))
(longitude [this] (.getX this))
(to-spatial4j-point [this] this)
(to-geohash-point [this] (geohash-point this)))
(defn degrees->radians
[degrees]
(DistanceUtils/toRadians degrees))
(defn radians->degrees
[radians]
(DistanceUtils/toDegrees radians))
(defn earth-radius
"Returns an approximate radius for the earth, at some point. Based on the
geodetic model for an oblate spheroid."
[point]
(let [l (degrees->radians (latitude point))
a earth-equatorial-radius
a2 earth-equatorial-radius-squared
b earth-polar-radius
b2 earth-polar-radius-squared
cos (Math/cos l)
sin (Math/sin l)]
(Math/sqrt
(/ (+ (square (* a2 cos))
(square (* b2 sin)))
(+ (square (* a cos))
(square (* b sin)))))))
(defn distance->radians
"Converts distance, in meters on the surface of the earth, to radians.
Assumes earth mean radius."
([meters]
(distance->radians meters earth-mean-radius))
([meters radius]
(DistanceUtils/dist2Radians meters radius)))
(defn distance-at-point->radians
"Converts a distance near a point on the earth into radians; using a more
accurate model of the radius of the earth near that point."
[meters point]
(distance->radians meters (earth-radius point)))
(defn radians->distance
"Converts radians to meter distance on the surface of the earth. Assumes
earth mean radius."
([radians]
(radians->distance radians earth-mean-radius))
([radians radius]
(DistanceUtils/radians2Dist radians radius)))
(def square-degree-in-steradians
(/ (* 180 180) (* Math/PI Math/PI)))
(defn square-degrees->steradians
[steradians]
(/ steradians square-degree-in-steradians))
(defn jts-point
"Returns a Point used by JTS."
([point]
(jts/point (latitude point) (longitude point)))
([lat long]
(jts/point lat long)))
(defn steradians->area
"Converts steradians to square meters on the surface of the earth. Assumes
earth mean radius."
([steradians]
(steradians->area steradians earth-mean-radius))
([steradians radius]
(* steradians (square radius))))
(defn spatial4j-point
"A spatial4j point on the earth."
([point]
(.makePoint earth (longitude point) (latitude point)))
([lat long]
(.makePoint earth long lat)))
(defn geohash-point
"Returns a WGS84Point used by the geohash library."
([point]
(WGS84Point. (latitude point) (longitude point)))
([lat long]
(WGS84Point. lat long)))
(def point spatial4j-point)
(def north-pole (spatial4j-point 90 0))
(def south-pole (spatial4j-point -90 0))
(defn circle
"A spatial4j circle around the given point or lat,long, with radius in
meters."
([point meters]
; GeoCircle takes its radius in degrees, so we need to figure out how many
; degrees to use. For anything under 100 kilometers we use a local
; approximation; for bigger stuff we use the mean radius.
(let [point (to-spatial4j-point point)
radians (if (< 1e6 meters)
(distance->radians meters)
(distance-at-point->radians meters point))
degrees (radians->degrees radians)]
(.makeCircle earth point degrees))))
(defn distance
"Distance between two points, in meters"
[a b]
; There's a singularity in the geohash library's distance calculation
; algorithm, used here, which causes distances near the poles to return NaN.
(assert (not= 90.0 (abs (latitude a))))
(assert (not= 90.0 (abs (latitude b))))
(VincentyGeodesy/distanceInMeters
(to-geohash-point a)
(to-geohash-point b)))
(defn distance-in-degrees
"Distance between two points, in degrees."
[a b]
(-> earth
.getDistCalc
(.distance (to-spatial4j-point a)
(to-spatial4j-point b))))
(defn bounding-box
"Returns the bounding box of any shape."
^org.locationtech.spatial4j.shape.Rectangle [shape]
(.getBoundingBox (to-shape shape)))
(defn center
"Returns the centroid of a spatial4j shape. Note that .getCenter does bad
things for JTS shapes that cross the international dateline, so we use use
(center (bounding-box x)) for JTS stuff."
[shape]
(let [shape (to-shape shape)]
(if (instance? JtsGeometry shape)
(.getCenter (bounding-box shape))
(.getCenter (to-shape shape)))))
(defn height
"Returns the height of a shape, in degrees."
[shape]
(-> shape bounding-box .getHeight))
(defn width
"Returns the width of a shape, in degrees."
[shape]
(-> shape bounding-box .getWidth))
(defn area-in-square-degrees
"The area of a rectangle in square degrees."
[rect]
(.area (.getDistCalc earth)
^Rectangle (to-shape rect)))
(defn area
"The area of a rectangle in square meters. Note that spatial4j's term 'area'
actually refers to solid angle, not area; we convert by multiplying by the
earth's radius at the midpoint of the rectangle."
[rect]
(-> rect
area-in-square-degrees
square-degrees->steradians
steradians->area))
(defn relate
"The relationship between two shapes. Returns a keyword:
:contains a contains b
:within a falls within b
:intersects a and b have at least one point in common
:disjoint a and b have no points in common"
[a b]
(condp = (.relate (to-shape a) (to-shape b))
SpatialRelation/DISJOINT :disjoint
SpatialRelation/INTERSECTS :intersects
SpatialRelation/WITHIN :within
SpatialRelation/CONTAINS :contains))
(defn intersects?
"Do two shapes intersect in any way? Note that spatial4j's relate() considers
intersection *different* from containment, e.g. if A completely surrounds B,
their relation is not INTERSECTS. Spatial4j has a intersects() function on
relations (the one used here) which considers two shapes intersecting if
their intersection is non-empty; i.e. they are not disjoint."
[a b]
(.intersects (.relate (to-shape a) (to-shape b))))
(defn dist-at-idx
"Distance between the linestring's point at the given index and the subsequent point."
[linestring idx]
(distance (jts/point-n linestring idx)
(jts/point-n linestring (inc idx))))
(defn length
"Get geodesic length of a (jts) linestring by summing lengths of successive points"
[^org.locationtech.jts.geom.LineString linestring]
(let [num-points (.getNumPoints linestring)]
(if (= 0 num-points)
0
(loop [length 0.0
idx 0]
(if (= idx (dec num-points))
length
(recur (+ length (dist-at-idx linestring idx))
(inc idx)))))))
(defn within-dist? [p1 p2 dist]
(<= (distance p1 p2) dist))
(defn point-between [^Coordinate c1 ^Coordinate c2 dist]
(let [ratio (/ dist (distance c1 c2))
segment (org.locationtech.jts.geom.LineSegment. c1 c2)]
(.pointAlongOffset segment ratio 0)))
(defn- coord-list-length [coords]
(if (< (count coords) 2)
0
(length (jts/linestring coords))))
(defn- cut-point [current-segment next-coord segment-length]
(let [current-length (coord-list-length current-segment)
shortfall (- segment-length current-length)]
(point-between (last current-segment) next-coord shortfall)))
(defn- under-cap-with-next-point? [coords next-coord dist]
(< (length (jts/linestring (conj coords next-coord))) dist))
(defn- resegment-wgs84
"Performs the resegment operation used in (resegment),
with the assumption that a linestring is in WGS84 projection"
[linestring segment-length]
(loop [coords (jts/coords linestring)
segments []
current []]
(let [[next & remaining] coords]
(cond
(empty? coords) (map jts/linestring (conj segments current))
(empty? current) (recur remaining segments (conj current next))
(under-cap-with-next-point? current next segment-length) (recur remaining segments (conj current next))
:else (let [cut-point (cut-point current next segment-length)]
(recur coords
(conj segments (conj current cut-point))
[cut-point]))))))
(defn resegment
"Repartitions a JTS LineString into multiple contiguous linestrings, each up to the
provided length (in meters). Final segment may be less than the requested length.
Length of individual segments may vary a bit but total length should remain the same."
[linestring segment-length]
(let [srid (jts/get-srid linestring)]
(map #(jts/transform-geom % srid) (resegment-wgs84 (jts/transform-geom linestring 4326) segment-length))))
(def ^DistanceCalculator vincenty-distance-calculator (org.locationtech.spatial4j.distance.GeodesicSphereDistCalc$Vincenty.))
(defn rand-point-in-radius
"Get a random point around the given latitude and longitude within the given radius.
(rand-point-in-radius 34.05656 -118.41881 100)
(rand-point-in-radius 34.05656 -118.41881 100 :clustered)
(rand-point-in-radius 34.05656 -118.41881 100 (fn [] 1))
Returns org.locationtech.spatial4j.shape.jts.JtsPoint; Use geo.spatial/latitude and geo.spatial/longitude
to retrieve raw coords.
Accepts an optional 4th argument for customizing the distribution. Can be either :uniform or :clustered
for built-in distributions, or a custom fn.
Distribution fn should return a float between 0.0 and 1.0 when invoked.
The built-in :clustered distribution uses a linear distribution of radius, which results in points
clustered more heavily toward the center of the radius.
:uniform uses an exponential distribution of radius which results in points being spread evenly across
the circle.
Default distribution is :uniform."
([center-lat center-lon radius-meters]
(rand-point-in-radius center-lat center-lon radius-meters :uniform))
([center-lat center-lon radius-meters distribution]
(assert (or (= :uniform distribution)
(= :clustered distribution)
(fn? distribution))
"distribution must be :uniform, :clustered, or fn")
(let [center (point center-lat center-lon)
offset-fn (case distribution
:uniform (fn [] (Math/sqrt (rand)))
:clustered rand
distribution)
radius (* (offset-fn) radius-meters)
offset-degrees (-> radius
(distance-at-point->radians center)
(radians->degrees))
angle-degrees (rand 360)]
(.pointOnBearing vincenty-distance-calculator
center
offset-degrees
angle-degrees
earth
nil))))