001 (ns walkmap.edge
002 "Essentially the specification for things we shall consider to be an edge.
003 An edge is a line segment having just a start and an end, with no intervening
004 nodes."
005 (:require [clojure.math.numeric-tower :as m]
006 [walkmap.utils :as u]
007 [walkmap.vertex :refer [canonicalise check-vertex ensure2d ensure3d vertex vertex= vertex?]]))
008
009 (defn edge
010 "Return an edge between vertices `v1` and `v2`."
011 [v1 v2]
012 {:kind :edge
013 :walkmap.id/id (keyword (gensym "edge"))
014 :start (check-vertex v1)
015 :end (check-vertex v2)})
016
017 (defn edge?
018 "True if `o` satisfies the conditions for a edge. An edge shall be a map
019 having the keys `:start` and `:end`, such that the values of each of those
020 keys shall be a vertex."
021 [o]
022 (and
023 (map? o)
024 (vertex? (:start o))
025 (vertex? (:end o))))
026
027 (defn length
028 "Return the length of the edge `e`."
029 [e]
030 (let [start (ensure3d (:start e))
031 end (ensure3d (:end e))]
032 (m/sqrt
033 (reduce
034 +
035 (map
036 #(m/expt (- (% end) (% start)) 2)
037 [:x :y :z])))))
038
039 (defn centre
040 "Return the vertex that represents the centre of this `edge`."
041 [edge]
042 (let [s (ensure3d (:start edge))
043 e (ensure3d (:end edge))]
044 (vertex
045 (+ (:x s) (/ (- (:x e) (:x s)) 2))
046 (+ (:y s) (/ (- (:y e) (:y s)) 2))
047 (+ (:z s) (/ (- (:z e) (:z s)) 2)))))
048
049 (defn unit-vector
050 "Return an vertex parallel to `e` starting from the coordinate origin. Two
051 edges which are parallel will have the same unit vector."
052 [e]
053 (let [e' {:start (ensure3d (:start e)) :end (ensure3d (:end e))}
054 l (length e')]
055 (canonicalise
056 (reduce
057 merge
058 {}
059 (map
060 (fn [k]
061 {k (/ (- (k (:end e')) (k (:start e'))) l)})
062 [:x :y :z])))))
063
064 (defn parallel?
065 "True if all `edges` passed are parallel with one another."
066 [& edges]
067 (let [uvs (map unit-vector edges)]
068 (every?
069 #(vertex= % (first uvs))
070 (rest uvs))))
071
072 (defn collinear?
073 "True if edges `e1` and `e2` are collinear with one another."
074 [e1 e2]
075 (parallel?
076 e1
077 e2
078 (if (vertex= (:start e1) (:start e2))
079 {:start (:start e1) :end (:end e2)}
080 {:start (:start e1) :end (:start e2)})))
081
082 (defn collinear2d?
083 "True if the projections of edges `e1`, `e2` onto the x, y plane are
084 collinear."
085 [e1 e2]
086 (collinear? {:start (ensure2d (:start e1)) :end (ensure2d (:end e1))}
087 {:start (ensure2d (:start e2)) :end (ensure2d (:end e2))}))
088
089 (defn minimaxd
090 "Apply function `f` to `coord` of the vertices at start and end of `edge`
091 and return the result. Intended use case is `f` = `min` or `max`, `coord`
092 is `:x`, `:y` or `:z`. No checks are made for sane arguments."
093 [edge coord f]
094 (apply f (list (coord (:start edge)) (coord (:end edge)))))
095
096 (defn on?
097 "True if the vertex `v` is on the edge `e`."
098 [e v]
099 (let [p (ensure3d (:start e))
100 q (ensure3d v)
101 r (ensure3d (:end e))]
102 (and
103 (collinear? (edge p q) (edge q r))
104 (<= (:x q) (max (:x p) (:x r)))
105 (>= (:x q) (min (:x p) (:x r)))
106 (<= (:y q) (max (:y p) (:y r)))
107 (>= (:y q) (min (:y p) (:y r)))
108 (<= (:z q) (max (:z p) (:z r)))
109 (>= (:z q) (min (:z p) (:z r))))))
110
111 (defn on2d?
112 "True if vertex `v` is on edge `e` when projected onto the x, y plane."
113 [e v]
114 (on? (edge (ensure2d (:start e)) (ensure2d (:end e))) v))
115
116 (defn overlaps2d?
117 "True if the recangle in the x,y plane bisected by edge `e1` overlaps that
118 bisected by edge `e2`. It is an error if either `e1` or `e2` is not an edge."
119 [e1 e2]
120 (when (and (edge? e1) (edge? e2))
121 (and
122 (> (minimaxd e1 :x max) (minimaxd e2 :x min))
123 (< (minimaxd e1 :x min) (minimaxd e2 :x max))
124 (> (minimaxd e1 :y max) (minimaxd e2 :y min))
125 (< (minimaxd e1 :y min) (minimaxd e2 :y max)))))
126
127 (defn intersection2d
128 "The probability of two lines intersecting in 3d space is low, and actually
129 that is mostly not something we're interested in. We're interested in
130 intersection in the `x,y` plane. This function returns a vertex representing
131 a point vertically over the intersection of edges `e1`, `e2` in the `x,y`
132 plane, whose `z` coordinate is
133
134 * 0 if both edges are 2d (i.e. have missing or zero `z` coordinates);
135 * if one edge is 2d, then the point on the other edge over the intersection;
136 * otherwise, the average of the z coordinates of the points on the two
137 edges over the intersection.
138
139 If no such intersection exists, `nil` is returned.
140
141 It is an error, and an exception will be thrown, if either `e1` or `e2` is
142 not an edge."
143 [e1 e2]
144 (if (and (edge? e1) (edge? e2))
145 (when
146 (overlaps2d? e1 e2) ;; relatively cheap check
147 (if
148 (collinear2d? e1 e2)
149 ;; any point within the overlap will do, but we'll pick the end of e1
150 ;; which is on e2
151 (if (on2d? e2 (:start e1)) (:start e1) (:end e1))
152 ;; blatantly stolen from
153 ;; https://gist.github.com/cassiel/3e725b49670356a9b936
154 (let [x1 (:x (:start e1))
155 x2 (:x (:end e1))
156 x3 (:x (:start e2))
157 x4 (:x (:end e2))
158 y1 (:y (:start e1))
159 y2 (:y (:end e1))
160 y3 (:y (:start e2))
161 y4 (:y (:end e2))
162 denom (- (* (- x1 x2) (- y3 y4))
163 (* (- y1 y2) (- x3 x4)))
164 x1y2-y1x2 (- (* x1 y2) (* y1 x2))
165 x3y4-y3x4 (- (* x3 y4) (* y3 x4))
166 px-num (- (* x1y2-y1x2 (- x3 x4))
167 (* (- x1 x2) x3y4-y3x4))
168 py-num (- (* x1y2-y1x2 (- y3 y4))
169 (* (- y1 y2) x3y4-y3x4))
170 result (when-not (zero? denom)
171 (vertex (/ px-num denom) (/ py-num denom)))]
172 (when (and result (on2d? e1 result) (on2d? e2 result)) result))))
173 (throw (IllegalArgumentException.
174 (str
175 "Both `e1` and `e2` must be edges."
176 (map #(or (:kind %) (type %)) [e1 e2]))))))
177