001 (ns walkmap.vertex
002 "Essentially the specification for things we shall consider to be vertices.
003
004 Note that there's no `distance` function here; to find the distance between
005 two vertices, create an edge from them and use `walkmap.edge/length`."
006 (:require [clojure.math.numeric-tower :as m]
007 [clojure.string :as s]
008 [taoensso.timbre :as l]
009 [walkmap.utils :refer [=ish kind-type truncate]]))
010
011 (defn vertex-key
012 "Making sure we get the same key everytime we key a vertex with the same
013 coordinates. `o` must have numeric values for `:x`, `:y`, and optionally
014 `:z`; it is an error and an exception will be thrown if `o` does not
015 conform to this specification.
016
017 **Note:** these keys can be quite long. No apology is made: it is required
018 that the same key can *never* refer to two different locations in space."
019 [o]
020 (keyword
021 (s/replace
022 (cond
023 (and (:x o) (:y o) (:z o))
024 (str "vert_" (:x o) "_" (:y o) "_" (:z o))
025 (and (:x o) (:y o))
026 (str "vert_" (:x o) "_" (:y o))
027 :else
028 (throw (IllegalArgumentException.
029 (truncate (str "Not a vertex: " (or o "nil")) 80))))
030 "."
031 "-")))
032
033 (defn vertex?
034 "True if `o` satisfies the conditions for a vertex. That is, essentially,
035 that it must rerpresent a two- or three- dimensional vector. A vertex is
036 shall be a map having at least the keys `:x` and `:y`, where the value of
037 those keys is a number. If the key `:z` is also present, its value must also
038 be a number.
039
040 The name `vector?` was not used as that would clash with a function of that
041 name in `clojure.core` whose semantics are entirely different."
042 [o]
043 (and
044 (map? o)
045 (:walkmap.id/id o)
046 (number? (:x o))
047 (number? (:y o))
048 (or (nil? (:z o)) (number? (:z o)))
049 (or (nil? (:kind o)) (= (:kind o) :vertex))))
050
051 (defn vertex=
052 "True if vertices `v1`, `v2` represent the same vertex."
053 [v1 v2]
054 (every?
055 #(=ish (% v1) (% v2))
056 [:x :y :z]))
057
058 (defn vertex*
059 "Return a vertex like `v1`, but with each of its coordinates multiplied
060 by the equivalent vertex in `v2`."
061 [v1 v2]
062 (if
063 (and (vertex? v1) (vertex? v2))
064 (let [f (fn [v1 v2 coord]
065 (* (or (coord v1) 0)
066 ;; one here is deliberate!
067 (or (coord v2) 1)))]
068 (assoc v1 :x (f v1 v2 :x)
069 :y (f v1 v2 :y)
070 :z (f v1 v2 :z)))
071 (do (l/warn
072 (s/join
073 " "
074 ["in `vertex-multiply`, both must be vectors. v1:" v1 "v2:" v2]))
075 v1)))
076
077 (defn vertex
078 "Make a vertex with this `x`, `y` and (if provided) `z` values. Returns a map
079 with those values, plus a unique `:walkmap.id/id` value, and `:kind` set to `:vertex`.
080 It's not necessary to use this function to create a vertex, but the `:walkmap.id/id`
081 must be present and must be unique."
082 ([x y]
083 (let [v {:x x :y y :kind :vertex}]
084 (assoc v :walkmap.id/id (vertex-key v))))
085 ([x y z]
086 (let [v (assoc (vertex x y) :z z)]
087 (assoc v :walkmap.id/id (vertex-key v)))))
088
089 (defn canonicalise
090 "If `o` is a map with numeric values for `:x`, `:y` and optionally `:z`,
091 upgrade it to something we will recognise as a vertex."
092 [o]
093 (if
094 (and
095 (map? o)
096 (number? (:x o))
097 (number? (:y o))
098 (or (nil? (:z o)) (number? (:z o))))
099 (assoc o :kind :vertex :walkmap.id/id (vertex-key o))
100 (throw
101 (IllegalArgumentException.
102 (truncate
103 (str "Not a proto-vertex: must have numeric `:x` and `:y`: "
104 (or o "nil"))
105 80)))))
106
107 (def ensure3d
108 "Given a vertex `o`, if `o` has a `:z` value, just return `o`; otherwise
109 return a vertex like `o` but having thie `dflt` value as the value of its
110 `:z` key, or zero as the value of its `:z` key if `dflt` is not specified.
111
112 If `o` is not a vertex, throws an exception."
113 (memoize
114 (fn
115 ([o]
116 (ensure3d o 0.0))
117 ([o dflt]
118 (cond
119 (not (vertex? o)) (throw
120 (IllegalArgumentException.
121 (truncate (str "Not a vertex: " (or o "nil")) 80)))
122 (:z o) o
123 :else (assoc o :z dflt))))))
124
125 (def ensure2d
126 "If `o` is a vertex, set its `:z` value to zero; else throw an exception."
127 (memoize
128 (fn [o]
129 (if
130 (vertex? o)
131 (assoc o :z 0.0)
132 (throw
133 (IllegalArgumentException.
134 (truncate (str "Not a vertex: " (or o "nil")) 80)))))))
135
136 (defn within-box?
137 "True if `target` is within the box defined by `minv` and `maxv`. All
138 arguments must be vertices; additionally, both `minv` and `maxv` must
139 have `:z` coordinates."
140 [target minv maxv]
141 (when-not (and (vertex? target) (vertex? minv) (vertex? maxv))
142 (throw (IllegalArgumentException.
143 (s/join " " ["Arguments to `within-box?` must be vertices:"
144 (map kind-type [target minv maxv])]))))
145 (every?
146 (map
147 #(< (% minv) (or (% target) 0) (% maxv))
148 [:x :y :z])))