Merge branch 'feature/3' into develop

This commit is contained in:
Simon Brooke 2020-05-30 23:02:10 +01:00
commit 75899f8a4d
No known key found for this signature in database
GPG key ID: A7A4F18D1D4DF987
17 changed files with 478 additions and 122 deletions

View file

@ -5,6 +5,7 @@
:output-path "docs/codox" :output-path "docs/codox"
:source-uri "https://github.com/simon-brooke/walkmap/blob/master/{filepath}#L{line}"} :source-uri "https://github.com/simon-brooke/walkmap/blob/master/{filepath}#L{line}"}
:dependencies [[org.clojure/clojure "1.8.0"] :dependencies [[org.clojure/clojure "1.8.0"]
[org.clojure/data.zip "1.0.0"]
[org.clojure/math.numeric-tower "0.0.4"] [org.clojure/math.numeric-tower "0.0.4"]
[org.clojure/math.combinatorics "0.1.6"] [org.clojure/math.combinatorics "0.1.6"]
[com.taoensso/timbre "4.10.0"] [com.taoensso/timbre "4.10.0"]

View file

@ -11,7 +11,7 @@
[v1 v2] [v1 v2]
(if (if
(and (vertex? v1) (vertex? v2)) (and (vertex? v1) (vertex? v2))
{:kind :edge :id (keyword (gensym "edge")) :start v1 :end v2} {:kind :edge :walkmap.id/id (keyword (gensym "edge")) :start v1 :end v2}
(throw (IllegalArgumentException. "Must be vertices.")))) (throw (IllegalArgumentException. "Must be vertices."))))
(defn edge? (defn edge?

8
src/walkmap/id.clj Normal file
View file

@ -0,0 +1,8 @@
(ns walkmap.id
"The namespace within which the privileged keyword `:walkmap.id/id` is defined.")
(def ^:const id
"The magic id key walkmap uses, to distinguish it from all other uses of
the unprotected keyword."
::id)

View file

@ -2,8 +2,10 @@
"Essentially the specification for things we shall consider to be path. "Essentially the specification for things we shall consider to be path.
**Note that** for these purposes `path` means any continuous linear **Note that** for these purposes `path` means any continuous linear
feature, where such features specifically include watercourses." feature, where such features specifically include watercourses."
(:require [walkmap.edge :as e] (:require [clojure.string :as s]
[walkmap.edge :as e]
[walkmap.polygon :refer [polygon?]] [walkmap.polygon :refer [polygon?]]
[walkmap.utils :refer [kind-type]]
[walkmap.vertex :refer [vertex?]])) [walkmap.vertex :refer [vertex?]]))
(defn path? (defn path?
@ -17,7 +19,7 @@
(seq? v) (seq? v)
(> (count v) 2) (> (count v) 2)
(every? vertex? v) (every? vertex? v)
(:id o) (:walkmap.id/id o)
(or (nil? (:kind o)) (= (:kind o) :path))))) (or (nil? (:kind o)) (= (:kind o) :path)))))
(defn path (defn path
@ -25,8 +27,11 @@
[& vertices] [& vertices]
(if (if
(every? vertex? vertices) (every? vertex? vertices)
{:vertices vertices :id (keyword (gensym "path")) :kind :path} {:vertices vertices :walkmap.id/id (keyword (gensym "path")) :kind :path}
(throw (IllegalArgumentException. "Each item on path must be a vertex.")))) (throw (IllegalArgumentException.
(str
"Each item on path must be a vertex: "
(s/join " " (map kind-type vertices)))))))
(defn polygon->path (defn polygon->path
"If `o` is a polygon, return an equivalent path. What's different about "If `o` is a polygon, return an equivalent path. What's different about

View file

@ -13,7 +13,7 @@
(coll? v) (coll? v)
(> (count v) 2) (> (count v) 2)
(every? vertex? v) (every? vertex? v)
(:id o) (:walkmap.id/id o)
(or (nil? (:kind o)) (= (:kind o) :polygon))))) (or (nil? (:kind o)) (= (:kind o) :polygon)))))

98
src/walkmap/read_svg.clj Normal file
View file

@ -0,0 +1,98 @@
(ns walkmap.read-svg
"Utility functions for scalable vector graphics (SVG) into walkmap
structures."
(:require [clojure.data.zip :as dz]
[clojure.data.zip.xml :as zx]
[clojure.java.io :as io]
[clojure.string :as s]
[clojure.xml :as x]
[clojure.zip :as z]
[taoensso.timbre :as l]
[walkmap.path :refer [path]]
;; [walkmap.polygon :refer [polygon]]
[walkmap.tag :refer [tag]]
[walkmap.utils :refer [kind-type truncate]]
[walkmap.vertex :refer [vertex vertex?]]))
(defn upper-case?
[s]
(every? #(Character/isUpperCase %) s))
(defn match->vertex
[match-vector x y]
(when-not (empty? match-vector)
(let [command (nth match-vector 1)
xcoord (read-string (nth match-vector 2))
ycoord (read-string (nth match-vector 3))
;; upper case command letters mean the coordinates that follow are
;; absolute; lower case, relative.
x' (if (upper-case? command) xcoord (+ x xcoord))
y' (if (upper-case? command) ycoord (+ y ycoord))]
(case (s/lower-case command)
("m" "l") {:vertex (vertex x' y') :x x' :y y'}
nil))))
(defn command-string->vertices
"Return the destination of each successive line (`l`, `L`) and move (`m`, `M`)
command in this string `s`, expected to be an SVG path command string."
[s]
(let [cmd-matcher ;; matches a 'command' in the string: a letter followed by
;;spaces and numbers
(re-matcher #"[a-zA-Z][^a-zA-Z]*" s)
seg-pattern ;; matches a command which initiates a move of the current
;; position.
#"([a-zA-Z]) +([-+]?[0-9]*\.?[0-9]+) +([-+]?[0-9]*\.?[0-9]+) +"]
(loop [match (re-find cmd-matcher)
result []
x 0
y 0]
(if-not match
(filter vertex? result)
(let [m (match->vertex (re-find seg-pattern match) x y)]
(recur (re-find cmd-matcher) ;loop with 2 new arguments
(conj result (:vertex m))
(or (:x m) x)
(or (:y m) y)))))))
(defn path-elt->path
"Given the SVG path element `elt`, return a walkmap path structure
representing the line (`l`, `L`) and move (`m`, `M`) commands in
that path."
[elt]
(if (= (-> elt :tag) :path)
(let [vs (command-string->vertices (-> elt :attrs :d))
p (when-not (empty? vs) (apply path vs))]
(if (and p (-> elt :attrs :class))
(tag p (map keyword (s/split (-> elt :attrs :class) #" ")))
p))
(throw (IllegalArgumentException.
(str "Must be an SVG `path` element: " elt)))))
(defn progeny
"Return all the nodes in the XML structure below this `elt` which match
this `predicate`."
;; the name `descendants` is bound in `clojure.core` for something quite
;; different, and I chose not to rebind it.
[elt predicate]
(if
(apply predicate (list elt))
(list elt)
(reduce
concat
(remove
empty?
(map
#(progeny % predicate)
(:content elt))))))
(defn read-svg
;; I tried to get this working with all the clever zip stuff in
;; `clojure.zip`, `clojure.data.zip`, and so on. It would probably have
;; been more elegant, but it kept crashing out of heap space on even
;; quite small XML files. So I've implemented my own solution.
([file-name]
(read-svg file-name nil))
([file-name map-kind]
(let [xml (x/parse (io/file file-name))
paths (progeny xml #(= (:tag %) :path))]
(remove nil? (map path-elt->path paths)))))

View file

@ -8,6 +8,7 @@
[walkmap.edge :as e] [walkmap.edge :as e]
[walkmap.polygon :refer [polygon?]] [walkmap.polygon :refer [polygon?]]
[walkmap.tag :refer [tag]] [walkmap.tag :refer [tag]]
[walkmap.utils :as u]
[walkmap.vertex :as v]) [walkmap.vertex :as v])
(:import org.clojars.smee.binary.core.BinaryIO (:import org.clojars.smee.binary.core.BinaryIO
java.io.DataInput)) java.io.DataInput))
@ -84,19 +85,19 @@
(when-not (when-not
(keyword? map-kind) (keyword? map-kind)
(throw (IllegalArgumentException. (throw (IllegalArgumentException.
(subs (str "Must be a keyword: " (or map-kind "nil")) 0 80)))) (u/truncate (str "Must be a keyword: " (or map-kind "nil")) 80))))
(cond (cond
(and (coll? o) (not (map? o))) (map #(canonicalise % map-kind) o) (and (coll? o) (not (map? o))) (map #(canonicalise % map-kind) o)
;; if it has :facets it's an STL structure, but it doesn't yet conform to `stl?` ;; if it has :facets it's an STL structure, but it doesn't yet conform to `stl?`
(:facets o) (assoc o (:facets o) (assoc o
:kind :stl :kind :stl
:id (or (:id o) (keyword (gensym "stl"))) :walkmap.id/id (or (:walkmap.id/id o) (keyword (gensym "stl")))
:facets (canonicalise (:facets o) map-kind)) :facets (canonicalise (:facets o) map-kind))
;; if it has :vertices it's a polygon, but it doesn't yet conform to `polygon?` ;; if it has :vertices it's a polygon, but it doesn't yet conform to `polygon?`
(:vertices o) (centre (:vertices o) (centre
(tag (tag
(assoc o (assoc o
:id (or (:id o) (keyword (gensym "poly"))) :walkmap.id/id (or (:walkmap.id/id o) (keyword (gensym "poly")))
:kind :polygon :kind :polygon
:vertices (canonicalise (:vertices o) map-kind)) :vertices (canonicalise (:vertices o) map-kind))
:facet map-kind)) :facet map-kind))
@ -120,7 +121,7 @@
(when-not (when-not
(keyword? map-kind) (keyword? map-kind)
(throw (IllegalArgumentException. (throw (IllegalArgumentException.
(subs (str "Must be a keyword: " (or map-kind "nil")) 0 80)))) (u/truncate (str "Must be a keyword: " (or map-kind "nil")) 80))))
(let [in (io/input-stream filename)] (let [in (io/input-stream filename)]
(canonicalise (b/decode binary-stl in) map-kind)))) (canonicalise (b/decode binary-stl in) map-kind))))

View file

@ -1,6 +1,8 @@
(ns walkmap.superstructure (ns walkmap.superstructure
"single indexing structure for walkmap objects" "single indexing structure for walkmap objects"
(:require [walkmap.path :as p] (:require [clojure.walk :refer [postwalk]]
[taoensso.timbre :as l]
[walkmap.path :as p]
[walkmap.polygon :as q] [walkmap.polygon :as q]
[walkmap.stl :as s] [walkmap.stl :as s]
[walkmap.utils :as u] [walkmap.utils :as u]
@ -22,28 +24,38 @@
;; superstructure - unless we replace the superstructure altogether with a ;; superstructure - unless we replace the superstructure altogether with a
;; database, which may be the Right Thing To Do. ;; database, which may be the Right Thing To Do.
(def vertex-index ::vertex-index)
(defn vertices
"If `o` is an object with vertices, return those vertices, else nil."
[o]
(cond
(v/vertex? o) (list o)
(q/polygon? o) (:vertices o)
(p/path? o) (:vertices o)))
(defn index-vertex (defn index-vertex
"Return a superstructure like `s` in which object `o` is indexed by vertex "Return a superstructure like `s` in which object `o` is indexed by vertex
`v`. It is an error (and an exception may be thrown) if `v`. It is an error (and an exception may be thrown) if
1. `s` is not a map; 1. `s` is not a map;
2. `o` is not a map; 2. `o` is not a map;
3. `o` does not have a value for the key `:id`; 3. `o` does not have a value for the key `:walkmap.id/id`;
4. `v` is not a vertex." 4. `v` is not a vertex."
[s o v] [s o v]
(if-not (v/vertex? o) (if-not (v/vertex? o)
(if (:id o) (if (:walkmap.id/id o)
(if (v/vertex? v) (if (v/vertex? v)
(let [vi (or (:vertex-index s) {}) (let [vi (or (::vertex-index s) {})
current (or (vi (:id v)) {})] current (or (vi (:walkmap.id/id v)) {})]
;; deep-merge doesn't merge sets, only maps; so at this ;; deep-merge doesn't merge sets, only maps; so at this
;; stage we need to build a map. ;; stage we need to build a map.
(assoc vi (:id v) (assoc current (:id o) (:id v)))) (assoc vi (:walkmap.id/id v) (assoc current (:walkmap.id/id o) (:walkmap.id/id v))))
(throw (IllegalArgumentException. "Not a vertex: " v))) (throw (IllegalArgumentException. "Not a vertex: " v)))
(throw (IllegalArgumentException. (subs (str "No `:id` value: " o) 0 80)))) (throw (IllegalArgumentException. (u/truncate (str "No `:walkmap.id/id` value: " o) 80))))
;; it shouldn't actually be an error to try to index a vertex, but it ;; it shouldn't actually be an error to try to index a vertex, but it
;; also isn't useful to do so, so I'd be inclined to ignore it. ;; also isn't useful to do so, so I'd be inclined to ignore it.
(:vertex-index s))) (::vertex-index s)))
(defn index-vertices (defn index-vertices
"Return a superstructure like `s` in which object `o` is indexed by its "Return a superstructure like `s` in which object `o` is indexed by its
@ -51,35 +63,106 @@
1. `s` is not a map; 1. `s` is not a map;
2. `o` is not a map; 2. `o` is not a map;
3. `o` does not have a value for the key `:id`." 3. `o` does not have a value for the key `:walkmap.id/id`."
[s o] [s o]
(assoc (u/deep-merge
s s
:vertex-index {::vertex-index
(reduce (reduce
u/deep-merge u/deep-merge
{}
(map (map
#(index-vertex s o %) #(index-vertex s o %)
(u/vertices o))))) (:vertices o)))}))
(defn add-to-superstructure (defn in-retrieve
"Return a superstructure like `s` with object `o` added. If `o` is a collection, "Internal guts of `retrieve`, q.v. `x` can be anything; `s` must be a
return a superstructure like `s` with each element of `o` added. If only one walkmap superstructure. TODO: recursive, quite likely to blow the fragile
Clojure stack. Probably better to do this with `walk`, but I don't yet
understand that."
[x s]
(cond
;; if it's a keyword identifying something in s, retrieve that something.
(keyword? x) (if (s x)
(in-retrieve (s x) s)
x)
;; if it's a map, for every key which is not `:walkmap.id/id`, recurse.
(map? x) (let [v (reduce
(fn [m k]
(assoc m k (in-retrieve (x k) s)))
{}
(keys (dissoc x :walkmap.id/id)))
id (:walkmap.id/id x)]
;; if it has an id, bind it to that id in the returned value.
(if id
(assoc
v
:walkmap.id/id
(:walkmap.id/id x))
v))
(set? x) x ;; TODO: should I search in sets for objects when storing?
(coll? x) (map #(in-retrieve % s) x)
:else x))
(defn retrieve
"Retrieve the canonical representation of the object with this `id` from the
superstructure `s`."
[id s]
(in-retrieve (id s) s))
(defn in-store-find-objects
"Return an id -> object map of every object within `o`. Internal to
`in-store`, q.v. Use at your own peril."
([o]
(in-store-find-objects o {}))
([o s]
(l/debug "Finding objects in:" o)
(cond
(set? o) s ;; TODO: should I search in sets for objects when storing?
(map? o) (if (:walkmap.id/id o)
(assoc
(in-store-find-objects (vals o) s)
(:walkmap.id/id o)
o)
(in-store-find-objects (vals o) s))
(coll? o) (reduce merge s (map #(in-store-find-objects % s) o))
:else s)))
(defn in-store-replace-with-keys
"Return a copy of `o` in which each reified walkmap object within `o` has
been replaced with the `:walkmap.id/id` of that object. Internal to
`in-store`, q.v. Use at your own peril."
[o]
(assoc
(postwalk #(or (:walkmap.id/id %) %) (dissoc o :walkmap.id/id))
:walkmap.id/id
(:walkmap.id/id o)))
;; (in-store-replace-with-keys (p/path (v/vertex 0 0 0) (v/vertex 0 1 2) (v/vertex 3 3 3)))
;; (in-store-find-objects (p/path (v/vertex 0 0 0) (v/vertex 0 1 2) (v/vertex 3 3 3)))
(defn store
"Return a superstructure like `s` with object `o` added. If only one
argument is supplied it will be assumed to represent `o` and a new argument is supplied it will be assumed to represent `o` and a new
superstructure will be returned. superstructure will be returned.
It is an error (and an exception may be thrown) if It is an error (and an exception may be thrown) if
1. `s` is not a map; 1. `s` is not a map;
2. `o` is not a map, or a sequence of maps." 2. `o` is not a recognisable walkmap object"
([o] ([o]
(add-to-superstructure {} o)) (store o {}))
([s o] ([o s]
(cond (when-not (:walkmap.id/id o)
(map? o) (let [o' (if (:id o) o (assoc o :id (keyword (gensym "obj"))))] (throw
(index-vertices (assoc s (:id o') o') o')) (IllegalArgumentException.
(coll? o) (reduce u/deep-merge (map #(add-to-superstructure s %) o)) (str "Not a walkmap object: no value for `:walkmap.id/id`: "
(nil? o) o (u/kind-type o)))))
:else (when-not (map? s)
(throw (IllegalArgumentException. (str "Don't know how to index " (or (type o) "nil"))))))) (throw
(IllegalArgumentException.
(str "Superstructure must be a map: " (u/kind-type s)))))
(assoc
(u/deep-merge s (in-store-find-objects o) (index-vertices s o))
(:walkmap.id/id o)
(in-store-replace-with-keys o))))

View file

@ -2,7 +2,9 @@
"Utility functions for writing stereolithography (STL) files (and possibly, "Utility functions for writing stereolithography (STL) files (and possibly,
later, other geometry files of interest to us) as scalable vector graphics later, other geometry files of interest to us) as scalable vector graphics
(SVG)." (SVG)."
(:require [clojure.string :as s] (:require [clojure.java.io :as io]
[clojure.string :as s]
[clojure.xml :as x]
[dali.io :as neatly-folded-clock] [dali.io :as neatly-folded-clock]
[hiccup.core :refer [html]] [hiccup.core :refer [html]]
[taoensso.timbre :as l :refer [info error spy]] [taoensso.timbre :as l :refer [info error spy]]
@ -106,3 +108,9 @@
:hiccup (spit out-filename (html s)) :hiccup (spit out-filename (html s))
(throw (Exception. "Unexpected renderer value: " *preferred-svg-render*))) (throw (Exception. "Unexpected renderer value: " *preferred-svg-render*)))
s))) s)))
(defn read-svg
([file-name]
(read-svg file-name nil))
([file-name map-kind]

View file

@ -2,7 +2,9 @@
"Code for tagging, untagging, and finding tags on objects. Note the use of "Code for tagging, untagging, and finding tags on objects. Note the use of
the namespaced keyword, `:walkmap.tag/tags`, denoted in this file `::tags`. the namespaced keyword, `:walkmap.tag/tags`, denoted in this file `::tags`.
This is in an attempt to avoid name clashes with other uses of this key." This is in an attempt to avoid name clashes with other uses of this key."
(:require [clojure.set :refer [difference union]])) (:require [clojure.set :refer [difference union]]
[taoensso.timbre :as l]
[walkmap.utils :refer [kind-type]]))
(defn tagged? (defn tagged?
"True if this `object` is tagged with each of these `tags`. It is an error "True if this `object` is tagged with each of these `tags`. It is an error
@ -20,9 +22,9 @@
(set? ot) (set? ot)
(every? ot tags))) (every? ot tags)))
(throw (IllegalArgumentException. (throw (IllegalArgumentException.
(str "Must be keyword(s): " (map type tags))))) (str "Must be keyword(s): " (map kind-type tags)))))
(throw (IllegalArgumentException. (throw (IllegalArgumentException.
(str "Must be a map: " (type object)))))) (str "Must be a map: " (kind-type object))))))
(defn tag (defn tag
"Return an object like this `object` but with these `tags` added to its tags, "Return an object like this `object` but with these `tags` added to its tags,
@ -30,17 +32,22 @@
thrown) if thrown) if
1. `object` is not a map; 1. `object` is not a map;
2. any of `tags` is not a keyword." 2. any of `tags` is not a keyword or sequence of keywords.
It's legal to include sequences of keywords in `tags`, so that users can do
useful things like `(tag obj (map keyword some-strings))`."
[object & tags] [object & tags]
(l/debug "Tagging" (kind-type object) "with" tags)
(let [tags' (flatten tags)]
(if (if
(map? object) (map? object)
(if (if
(every? keyword? tags) (every? keyword? tags')
(assoc object ::tags (union (set tags) (::tags object))) (assoc object ::tags (union (set tags') (::tags object)))
(throw (IllegalArgumentException. (throw (IllegalArgumentException.
(str "Must be keyword(s): " (map type tags))))) (str "Must be keyword(s): " (map kind-type tags')))))
(throw (IllegalArgumentException. (throw (IllegalArgumentException.
(str "Must be a map: " (type object)))))) (str "Must be a map: " (kind-type object)))))))
(defmacro tags (defmacro tags
"Return the tags of this object, if any." "Return the tags of this object, if any."
@ -60,6 +67,6 @@
(every? keyword? tags) (every? keyword? tags)
(assoc object ::tags (difference (::tags object) (set tags))) (assoc object ::tags (difference (::tags object) (set tags)))
(throw (IllegalArgumentException. (throw (IllegalArgumentException.
(str "Must be keywords: " (map type tags))))) (str "Must be keywords: " (map kind-type tags)))))
(throw (IllegalArgumentException. (throw (IllegalArgumentException.
(str "Must be a map: " (type object)))))) (str "Must be a map: " (kind-type object))))))

View file

@ -1,23 +1,29 @@
(ns walkmap.utils (ns walkmap.utils
"Miscellaneous utility functions." "Miscellaneous utility functions."
(:require [clojure.math.numeric-tower :as m] (:require [clojure.math.numeric-tower :as m]))
[walkmap.path :as p]
[walkmap.polygon :as q]
[walkmap.vertex :as v]))
(defn deep-merge (defn deep-merge
"Recursively merges maps. If vals are not maps, the last value wins." "Recursively merges maps. If vals are not maps, the last value wins."
;; TODO: not my implementation, not sure I entirely trust it. ;; TODO: not my implementation, not sure I entirely trust it.
;; TODO TODO: if we are to successfully merge walkmap objects, we must
;; return, on each object, the union of its tags if any.
[& vals] [& vals]
(if (every? map? vals) (if (every? map? vals)
(apply merge-with deep-merge vals) (apply merge-with deep-merge vals)
(last vals))) (last vals)))
(defn vertices (defn truncate
"If `o` is an object with vertices, return those vertices, else nil." "If string `s` is more than `n` characters long, return the first `n`
[o] characters; otherwise, return `s`."
(cond [s n]
(v/vertex? o) (list o) (if (and (string? s) (number? n) (> (count s) n))
(q/polygon? o) (:vertices o) (subs s 0 n)
(p/path? o) (:vertices o))) s))
(defn kind-type
"Identify the type of an `object`, e.g. for logging. If it has a `:kind` key,
it's one of ours, and that's what we want. Otherwise, we want its type; but
the type of `nil` is `nil`, which doesn't get printed when assembling error
,essages, so return \"nil\"."
[object]
(or (:kind object) (type object) "nil"))

View file

@ -5,7 +5,8 @@
two vertices, create an edge from them and use `walkmap.edge/length`." two vertices, create an edge from them and use `walkmap.edge/length`."
(:require [clojure.math.numeric-tower :as m] (:require [clojure.math.numeric-tower :as m]
[clojure.string :as s] [clojure.string :as s]
[walkmap.geometry :refer [=ish]])) [walkmap.geometry :refer [=ish]]
[walkmap.utils :refer [truncate]]))
(defn vertex-key (defn vertex-key
"Making sure we get the same key everytime we key a vertex with the same "Making sure we get the same key everytime we key a vertex with the same
@ -25,7 +26,7 @@
(str "vert_" (:x o) "_" (:y o)) (str "vert_" (:x o) "_" (:y o))
:else :else
(throw (IllegalArgumentException. (throw (IllegalArgumentException.
(subs (str "Not a vertex: " (or o "nil")) 0 80)))) (truncate (str "Not a vertex: " (or o "nil")) 80))))
"." "."
"-"))) "-")))
@ -41,7 +42,7 @@
[o] [o]
(and (and
(map? o) (map? o)
(:id o) (:walkmap.id/id o)
(number? (:x o)) (number? (:x o))
(number? (:y o)) (number? (:y o))
(or (nil? (:z o)) (number? (:z o))) (or (nil? (:z o)) (number? (:z o)))
@ -56,15 +57,15 @@
(defn vertex (defn vertex
"Make a vertex with this `x`, `y` and (if provided) `z` values. Returns a map "Make a vertex with this `x`, `y` and (if provided) `z` values. Returns a map
with those values, plus a unique `:id` value, and `:kind` set to `:vertex`. with those values, plus a unique `:walkmap.id/id` value, and `:kind` set to `:vertex`.
It's not necessary to use this function to create a vertex, but the `:id` It's not necessary to use this function to create a vertex, but the `:walkmap.id/id`
must be present and must be unique." must be present and must be unique."
([x y] ([x y]
(let [v {:x x :y y :kind :vertex}] (let [v {:x x :y y :kind :vertex}]
(assoc v :id (vertex-key v)))) (assoc v :walkmap.id/id (vertex-key v))))
([x y z] ([x y z]
(let [v (assoc (vertex x y) :z z)] (let [v (assoc (vertex x y) :z z)]
(assoc v :id (vertex-key v))))) (assoc v :walkmap.id/id (vertex-key v)))))
(defn canonicalise (defn canonicalise
"If `o` is a map with numeric values for `:x`, `:y` and optionally `:z`, "If `o` is a map with numeric values for `:x`, `:y` and optionally `:z`,
@ -76,13 +77,13 @@
(number? (:x o)) (number? (:x o))
(number? (:y o)) (number? (:y o))
(or (nil? (:z o)) (number? (:z o)))) (or (nil? (:z o)) (number? (:z o))))
(assoc o :kind :vertex :id (vertex-key o)) (assoc o :kind :vertex :walkmap.id/id (vertex-key o))
(throw (throw
(IllegalArgumentException. (IllegalArgumentException.
(subs (truncate
(str "Not a proto-vertex: must have numeric `:x` and `:y`: " (str "Not a proto-vertex: must have numeric `:x` and `:y`: "
(or o "nil")) (or o "nil"))
0 80))))) 80)))))
(def ensure3d (def ensure3d
"Given a vertex `o`, if `o` has a `:z` value, just return `o`; otherwise "Given a vertex `o`, if `o` has a `:z` value, just return `o`; otherwise
@ -98,7 +99,7 @@
(cond (cond
(not (vertex? o)) (throw (not (vertex? o)) (throw
(IllegalArgumentException. (IllegalArgumentException.
(subs (str "Not a vertex: " (or o "nil")) 0 80))) (truncate (str "Not a vertex: " (or o "nil")) 80)))
(:z o) o (:z o) o
:else (assoc o :z dflt)))))) :else (assoc o :z dflt))))))
@ -111,4 +112,4 @@
(assoc o :z 0.0) (assoc o :z 0.0)
(throw (throw
(IllegalArgumentException. (IllegalArgumentException.
(subs (str "Not a vertex: " (or o "nil")) 0 80))))))) (truncate (str "Not a vertex: " (or o "nil")) 80)))))))

View file

@ -8,32 +8,32 @@
(testing "identification of edges." (testing "identification of edges."
(is (edge? {:start (vertex 0.0 0.0 0.0) (is (edge? {:start (vertex 0.0 0.0 0.0)
:end (vertex 3 4 0.0)}) "It is.") :end (vertex 3 4 0.0)}) "It is.")
(is (not (edge? {:start {:y 0.0 :z 0.0 :id 'foo} (is (not (edge? {:start {:y 0.0 :z 0.0 :walkmap.id/id 'foo}
:end {:x 3 :y 4 :z 0.0 :id 'bar}})) "Start lacks :x key") :end {:x 3 :y 4 :z 0.0 :walkmap.id/id 'bar}})) "Start lacks :x key")
(is (not (edge? {:start {:x nil :y 0.0 :z 0.0 :id 'foo} (is (not (edge? {:start {:x nil :y 0.0 :z 0.0 :walkmap.id/id 'foo}
:end {:x 3 :y 4 :z 0.0 :id 'bar}})) "Start lacks :x value") :end {:x 3 :y 4 :z 0.0 :walkmap.id/id 'bar}})) "Start lacks :x value")
(is (not (edge? {:begin {:x nil :y 0.0 :z 0.0 :id 'foo} (is (not (edge? {:begin {:x nil :y 0.0 :z 0.0 :walkmap.id/id 'foo}
:end {:x 3 :y 4 :z 0.0 :id 'bar}})) "Lacks start key") :end {:x 3 :y 4 :z 0.0 :walkmap.id/id 'bar}})) "Lacks start key")
(is (not (edge? {:start {:x nil :y 0.0 :z 0.0 :id 'foo} (is (not (edge? {:start {:x nil :y 0.0 :z 0.0 :walkmap.id/id 'foo}
:finish {:x 3 :y 4 :z 0.0 :id 'bar}})) "Lacks end key") :finish {:x 3 :y 4 :z 0.0 :walkmap.id/id 'bar}})) "Lacks end key")
(is (not (edge? {:start {:x "zero" :y 0.0 :z 0.0 :id 'foo} (is (not (edge? {:start {:x "zero" :y 0.0 :z 0.0 :walkmap.id/id 'foo}
:end {:x 3 :y 4 :z 0.0 :id 'bar}})) "Value of x in start is not a number") :end {:x 3 :y 4 :z 0.0 :walkmap.id/id 'bar}})) "Value of x in start is not a number")
(is (false? (edge? "I am not an edge")) "Edge mustbe a map."))) (is (false? (edge? "I am not an edge")) "Edge mustbe a map.")))
(deftest collinear-test (deftest collinear-test
(testing "collinearity" (testing "collinearity"
(is (collinear? {:start {:x 0.0 :y 0.0 :z 0.0 :id 'foo} :end {:x 3.0 :y 4.0 :z 0.0 :id 'bar}} (is (collinear? {:start {:x 0.0 :y 0.0 :z 0.0 :walkmap.id/id 'foo} :end {:x 3.0 :y 4.0 :z 0.0 :walkmap.id/id 'bar}}
{:start {:x 3.0 :y 4.0 :z 0.0 :id 'foo} :end {:x 9.0 :y 12.0 :z 0.0 :id 'bar}}) {:start {:x 3.0 :y 4.0 :z 0.0 :walkmap.id/id 'foo} :end {:x 9.0 :y 12.0 :z 0.0 :walkmap.id/id 'bar}})
"Should be") "Should be")
(is (not (is (not
(collinear? {:start {:x 0.0 :y 0.0 :z 0.0 :id 'foo} :end {:x 3 :y 4 :z 0.0 :id 'bar}} (collinear? {:start {:x 0.0 :y 0.0 :z 0.0 :walkmap.id/id 'foo} :end {:x 3 :y 4 :z 0.0 :walkmap.id/id 'bar}}
{:start {:x 1.0 :y 2.0 :z 3.5 :id 'foo} :end {:x 4.0 :y 6.0 :z 3.5 :id 'bar}})) {:start {:x 1.0 :y 2.0 :z 3.5 :walkmap.id/id 'foo} :end {:x 4.0 :y 6.0 :z 3.5 :walkmap.id/id 'bar}}))
"Should not be!") "Should not be!")
(is (collinear? {:start {:x 0.0 :y 0.0 :z 0.0 :id 'foo} :end {:x 3.0 :y 4.0 :z 0.0 :id 'bar}} (is (collinear? {:start {:x 0.0 :y 0.0 :z 0.0 :walkmap.id/id 'foo} :end {:x 3.0 :y 4.0 :z 0.0 :walkmap.id/id 'bar}}
{:start {:x 0.0 :y 0.0 :z 0.0 :id 'foo} :end {:x 9.0 :y 12.0 :z 0.0 :id 'bar}}) {:start {:x 0.0 :y 0.0 :z 0.0 :walkmap.id/id 'foo} :end {:x 9.0 :y 12.0 :z 0.0 :walkmap.id/id 'bar}})
"Edge case: same start location") "Edge case: same start location")
(is (collinear? {:start {:x 0.0 :y 0.0 :z 0.0 :id 'foo} :end {:x 9.0 :y 12.0 :z 0.0 :id 'bar}} (is (collinear? {:start {:x 0.0 :y 0.0 :z 0.0 :walkmap.id/id 'foo} :end {:x 9.0 :y 12.0 :z 0.0 :walkmap.id/id 'bar}}
{:start {:x 3.0 :y 4.0 :z 0.0 :id 'foo} :end {:x 9.0 :y 12.0 :z 0.0 :id 'bar}}) {:start {:x 3.0 :y 4.0 :z 0.0 :walkmap.id/id 'foo} :end {:x 9.0 :y 12.0 :z 0.0 :walkmap.id/id 'bar}})
"Edge case: same end location") "Edge case: same end location")
)) ))
@ -89,7 +89,7 @@
(deftest length-test (deftest length-test
(testing "length of an edge" (testing "length of an edge"
(is (= (length {:start {:x 0.0 :y 0.0 :z 0.0 :id 'foo} :end {:x 3.0 :y 4.0 :z 0.0 :id 'bar}}) 5.0)))) (is (= (length {:start {:x 0.0 :y 0.0 :z 0.0 :walkmap.id/id 'foo} :end {:x 3.0 :y 4.0 :z 0.0 :walkmap.id/id 'bar}}) 5.0))))
(deftest minimad-test (deftest minimad-test
(testing "finding minimum and maximum coordinates of edges." (testing "finding minimum and maximum coordinates of edges."
@ -98,12 +98,12 @@
(deftest parallel-test (deftest parallel-test
(testing "parallelism" (testing "parallelism"
(is (parallel? {:start {:x 0.0 :y 0.0 :z 0.0 :id 'foo} :end {:x 3 :y 4 :z 0.0 :id 'bar}} (is (parallel? {:start {:x 0.0 :y 0.0 :z 0.0 :walkmap.id/id 'foo} :end {:x 3 :y 4 :z 0.0 :walkmap.id/id 'bar}}
{:start {:x 1.0 :y 2.0 :z 3.5 :id 'foo} :end {:x 4.0 :y 6.0 :z 3.5 :id 'bar}}) {:start {:x 1.0 :y 2.0 :z 3.5 :walkmap.id/id 'foo} :end {:x 4.0 :y 6.0 :z 3.5 :walkmap.id/id 'bar}})
"Should be") "Should be")
(is (not (is (not
(parallel? {:start {:x 0.0 :y 0.0 :z 0.0 :id 'foo} :end {:x 3 :y 4 :z 0.0 :id 'bar}} (parallel? {:start {:x 0.0 :y 0.0 :z 0.0 :walkmap.id/id 'foo} :end {:x 3 :y 4 :z 0.0 :walkmap.id/id 'bar}}
{:start {:x 1.0 :y 2.0 :z 3.5 :id 'foo} :end {:x 4.0 :y 6.0 :z 3.49 :id 'bar}})) {:start {:x 1.0 :y 2.0 :z 3.5 :walkmap.id/id 'foo} :end {:x 4.0 :y 6.0 :z 3.49 :walkmap.id/id 'bar}}))
"Should not be!"))) "Should not be!")))
(deftest overlaps2d-test (deftest overlaps2d-test
@ -114,8 +114,8 @@
(deftest unit-vector-test (deftest unit-vector-test
(testing "deriving the unit vector" (testing "deriving the unit vector"
(is (= (is (=
(unit-vector {:start {:x 0.0 :y 0.0 :z 0.0 :id 'foo} :end {:x 3 :y 4 :z 0.0 :id 'bar}}) (unit-vector {:start {:x 0.0 :y 0.0 :z 0.0 :walkmap.id/id 'foo} :end {:x 3 :y 4 :z 0.0 :walkmap.id/id 'bar}})
{:x 0.6, :y 0.8, :z 0.0})) {:x 0.6, :y 0.8, :z 0.0}))
(is (= (is (=
(unit-vector {:start {:x 1.0 :y 2.0 :z 3.5 :id 'foo} :end {:x 4.0 :y 6.0 :z 3.5 :id 'bar}}) (unit-vector {:start {:x 1.0 :y 2.0 :z 3.5 :walkmap.id/id 'foo} :end {:x 4.0 :y 6.0 :z 3.5 :walkmap.id/id 'bar}})
{:x 0.6, :y 0.8, :z 0.0})))) {:x 0.6, :y 0.8, :z 0.0}))))

View file

@ -11,4 +11,4 @@
(is (=ish 0 0.0) "Tricky conrer case!") (is (=ish 0 0.0) "Tricky conrer case!")
(is (=ish :foo :foo) "Fails over to plain old equals for non-numbers.") (is (=ish :foo :foo) "Fails over to plain old equals for non-numbers.")
(is (=ish 6 5 10000) "If tolerance is wide enough, anything can be equal.") (is (=ish 6 5 10000) "If tolerance is wide enough, anything can be equal.")
(is (=ish "hello" "goodbye" 10000) "Well, except non-numbers, of course."))) (is (not (=ish "hello" "goodbye" 10000)) "Well, except non-numbers, of course.")))

View file

@ -7,7 +7,7 @@
(deftest canonicalise-test (deftest canonicalise-test
(testing "Canonicalisation of objects read from STL: vertices." (testing "Canonicalisation of objects read from STL: vertices."
(is (vertex? (canonicalise {:x 3.0, :y 1.0, :z 1.0})) (is (vertex? (canonicalise {:x 3.0, :y 1.0, :z 1.0}))
"Vertex: should have an `:id` and `:kind` = `:vertex`.") "Vertex: should have an `:walkmap.id/id` and `:kind` = `:vertex`.")
(is (= (:x (canonicalise {:x 3.0, :y 1.0, :z 1.0})) 3.0) (is (= (:x (canonicalise {:x 3.0, :y 1.0, :z 1.0})) 3.0)
"`:x` value should be unchanged.") "`:x` value should be unchanged.")
(is (= (:y (canonicalise {:x 3.0, :y 1.0, :z 1.0})) 1.0) (is (= (:y (canonicalise {:x 3.0, :y 1.0, :z 1.0})) 1.0)
@ -28,7 +28,7 @@
:abc 0} :abc 0}
p' (canonicalise p)] p' (canonicalise p)]
(is (polygon? p') (is (polygon? p')
"Polygon: should have an `:id` and `:kind` = `:polygon`.") "Polygon: should have an `:walkmap.id/id` and `:kind` = `:polygon`.")
(is (= (count (:vertices p)) (count (:vertices p'))) (is (= (count (:vertices p)) (count (:vertices p')))
"Number of vertices should not change") "Number of vertices should not change")
(map (map
@ -93,4 +93,4 @@
{:x 51.0, :y 20.0, :z 1.0}], {:x 51.0, :y 20.0, :z 1.0}],
:abc 0}]} :abc 0}]}
stl' (canonicalise stl)] stl' (canonicalise stl)]
(is (stl? stl') "Stl: should have an `:id` and `:kind` = `:stl`.")))) (is (stl? stl') "Stl: should have an `:walkmap.id/id` and `:kind` = `:stl`."))))

View file

@ -0,0 +1,135 @@
(ns walkmap.superstructure-test
(:require [clojure.set :refer [subset?]]
[clojure.test :refer :all]
[walkmap.path :as p]
[walkmap.polygon :as q]
[walkmap.superstructure :refer :all]
[walkmap.tag :as t]
[walkmap.utils :as u]
[walkmap.vertex :as v]))
(deftest store-test
(testing "Object storage"
(let [p (p/path
(v/vertex (rand) (rand) (rand))
(v/vertex (rand) (rand) (rand))
(v/vertex (rand) (rand) (rand))
(v/vertex (rand) (rand) (rand)))
id (:walkmap.id/id p)
s (store p)
r (id s)]
(is (= (:walkmap.id/id r) id)
"A representation should be stored in `s` keyed by `id`, and the id of that representation should be `id`.")
(is (= (:kind r) (:kind p))
"The representation should have the same value for `:kind`.")
(is (= (count (:vertices p)) (count (:vertices r)))
"The representation of `p` in `s` should have the same number of vertices as `p`.")
(is (every? v/vertex? (:vertices p))
"Every vertex of `p` should be a vertex.")
(is (every? keyword? (:vertices r))
"Every vertex of the representation of `p` in `s` should be a keyword.")
(is (every? v/vertex? (map #(s %) (:vertices r)))
"The value in `s` of every vertex of the representation of `p` in `s`
should be a vertex.")
(is (subset? (set (:vertices r)) (set (keys (vertex-index s))))
"All the keys which are vertices of the representation of `p` in `s`
should be present as keys in the vertex-index of `s`.")
(is (every?
#(s (% id))
(map #(set (keys (% (vertex-index s)))) (:vertices r)))
"The value in the vertex-index in `s` for each keyword in the
vertexes of the representation of `p` in `s` should include,
as a key, the `id` of `p`."))))
(deftest retrieve-test
(testing "Object retrieval"
;; the value of `s` here is hand-typed; think of it as a specification
(let [s {:path1 {:walkmap.id/id :path1
:kind :path
:vertices '(:vert_0_0_0
:vert_0_0_1
:vert_1_0_0)}
:vert_0_0_0 {:walkmap.id/id :vert_0_0_0
:kind :vertex
:x 0
:y 0
:z 0}
:vert_0_0_1 {:walkmap.id/id :vert_0_0_1
:kind :vertex
:x 0
:y 0
:z 1}
:vert_1_0_0 {:walkmap.id/id :vert_1_0_0
:kind :vertex
:x 1
:y 0
:z 0}
:walkmap.superstructure/vertex-index {:vert_0_0_0 {:path1 :vert_0_0_0}
:vert_0_0_1 {:path1 :vert_0_0_1}
:vert_1_0_0 {:path1 :vert_1_0_0}}}
expected {:kind :path,
:vertices
'({:kind :vertex, :x 0, :y 0, :z 0, :walkmap.id/id :vert_0_0_0}
{:kind :vertex, :x 0, :y 0, :z 1, :walkmap.id/id :vert_0_0_1}
{:kind :vertex, :x 1, :y 0, :z 0, :walkmap.id/id :vert_1_0_0}),
:walkmap.id/id :path1}]
(is (= (retrieve :path1 s) expected)
"The object reconstructed from the superstructure."))))
(deftest round-trip-test
(testing "Roundtripping an object through the superstructure."
(let [p (p/path
(v/vertex (rand) (rand) (rand))
(v/vertex (rand) (rand) (rand))
(v/vertex (rand) (rand) (rand))
(v/vertex (rand) (rand) (rand)))
id (:walkmap.id/id p)
s (store p)
r (retrieve id s)]
(is (= p r) "As it was, so it shall be."))))
(deftest multi-object-round-trip-test
(testing "Roundtripping two different objects through a superstructure."
(let [p (p/path
(v/vertex (rand) (rand) (rand))
(v/vertex (rand) (rand) (rand))
(v/vertex (rand) (rand) (rand))
(v/vertex (rand) (rand) (rand)))
q (p/path
(v/vertex (rand) (rand) (rand))
(v/vertex (rand) (rand) (rand))
(v/vertex (rand) (rand) (rand))
(v/vertex (rand) (rand) (rand)))
pid (:walkmap.id/id p)
qid (:walkmap.id/id q)
s (store q (store p))
rp (retrieve pid s)
rq (retrieve qid s)]
(is (= p rp) "As `p` was, so it shall be.")
(is (= q rq) "As `q` was, so it shall be.")
(is (not= pid qid)
"It is not possible that the ids should be equal, since they are
gensymmed")
(is (not= rp rq)
"It is not possible that the paths should be equal, since at
minimum, their ids are gensymmed."))))
(deftest store-retrieve-edit-store-test
(testing "After editing a retrieved object and storing it again, a further
retrieve should return the new version."
(let [p (p/path
(v/vertex (rand) (rand) (rand))
(v/vertex (rand) (rand) (rand))
(v/vertex (rand) (rand) (rand))
(v/vertex (rand) (rand) (rand)))
id (:walkmap.id/id p)
o (store p)
r (retrieve id o)
p' (t/tag
(assoc r :vertices
(conj (:vertices id) (v/vertex (rand) (rand) (rand))))
:edited)
o' (store p' o)
r' (retrieve id o')]
(is (not= r r') "The value referenced by `id` should have changed.")
(is (= r' p') "The value referenced by `id` in `o'` should be equal to `p'`."))))

View file

@ -4,32 +4,32 @@
(deftest tag-tests (deftest tag-tests
(testing "Tagging" (testing "Tagging"
(is (set? (:walkmap.tag/tags (tag {} :foo :bar :ban :froboz))) (is (set? (:walkmap.tag/tags (tag {:kind :test-obj} :foo :bar :ban :froboz)))
"The value of `:walkmap.tag/tags` should be a set.") "The value of `:walkmap.tag/tags` should be a set.")
(is (= (count (:walkmap.tag/tags (tag {} :foo :bar :ban :froboz))) 4) (is (= (count (:walkmap.tag/tags (tag {:kind :test-obj} :foo :bar :ban :froboz))) 4)
"All the tags passed should be added.") "All the tags passed should be added.")
(is (:walkmap.tag/tags (tag {} :foo :bar :ban :froboz) :ban) (is (:walkmap.tag/tags (tag {:kind :test-obj} :foo :bar :ban :froboz) :ban)
"`:ban` should be present in the set, and, as it is a set, it "`:ban` should be present in the set, and, as it is a set, it
should be valid to apply it to a keyword.") should be valid to apply it to a keyword.")
(is (not ((:walkmap.tag/tags (tag {} :foo :bar :ban :froboz)) :cornflakes)) (is (not ((:walkmap.tag/tags (tag {:kind :test-obj} :foo :bar :ban :froboz)) :cornflakes))
"`:cornflakes should not be present.") "`:cornflakes should not be present.")
(is (true? (tagged? (tag {} :foo :bar :ban :froboz) :bar)) (is (true? (tagged? (tag {:kind :test-obj} :foo :bar :ban :froboz) :bar))
"`tagged?` should return an explicit `true`, not any other value.") "`tagged?` should return an explicit `true`, not any other value.")
(is (tagged? (tag {} :foo :bar :ban :froboz) :bar :froboz) (is (tagged? (tag {:kind :test-obj} :foo :bar :ban :froboz) :bar :froboz)
"We should be able to test for the presence of more than one tag") "We should be able to test for the presence of more than one tag")
(is (false? (tagged? {} :foo)) (is (false? (tagged? {:kind :test-obj} :foo))
"A missing `:walkmap.tag/tags` should not cause an error.") "A missing `:walkmap.tag/tags` should not cause an error.")
(is (= (tagged? (tag {} :foo :bar :ban :froboz) :bar :cornflakes) false) (is (= (tagged? (tag {:kind :test-obj} :foo :bar :ban :froboz) :bar :cornflakes) false)
"If any of the queried tags is missing, false should be returned") "If any of the queried tags is missing, false should be returned")
(is (tagged? (tag (tag {} :foo) :bar) :foo :bar) (is (tagged? (tag (tag {:kind :test-obj} :foo) :bar) :foo :bar)
"We should be able to add tags to an already tagged object") "We should be able to add tags to an already tagged object")
(is (false? (tagged? (tag {} :foo :bar) :cornflakes)) (is (false? (tagged? (tag {:kind :test-obj} :foo :bar) :cornflakes))
"`tagged?` should return an explicit `false` if a queried tag is missing.") "`tagged?` should return an explicit `false` if a queried tag is missing.")
(is (= (tags (tag {} :foo)) #{:foo}) (is (= (tags (tag {:kind :test-obj} :foo)) #{:foo})
"`tags` should return the tags on the object, if any.") "`tags` should return the tags on the object, if any.")
(is (every? nil? (map #(tags %) [1 :one "one" [:one] {:one 1}])) (is (every? nil? (map #(tags %) [1 :one "one" [:one] {:one 1}]))
"Things which don't have tags don't have tags, and that's not a problem.") "Things which don't have tags don't have tags, and that's not a problem.")
(let [object (tag {} :foo :bar :ban :froboz)] (let [object (tag {:kind :test-obj} :foo :bar :ban :froboz)]
(is (= (untag object :cornflakes) object) (is (= (untag object :cornflakes) object)
"Removing a missing tag should have no effect.") "Removing a missing tag should have no effect.")
(is (tagged? (untag object :foo) :bar :ban :froboz) (is (tagged? (untag object :foo) :bar :ban :froboz)
@ -42,10 +42,13 @@
"An exception should be thrown if `object` is not a map: `tagged?`.") "An exception should be thrown if `object` is not a map: `tagged?`.")
(is (thrown? IllegalArgumentException (untag [] :foo)) (is (thrown? IllegalArgumentException (untag [] :foo))
"An exception should be thrown if `object` is not a map: `untag`.") "An exception should be thrown if `object` is not a map: `untag`.")
(is (thrown? IllegalArgumentException (tag {} :foo "bar" :ban)) (is (thrown? IllegalArgumentException (tag {:kind :test-obj} :foo "bar" :ban))
"An exception should be thrown if any of `tags` is not a keyword: `tag`.") "An exception should be thrown if any of `tags` is not a keyword: `tag`.")
(is (thrown? IllegalArgumentException (tagged? {} :foo "bar" :ban)) (is (thrown? IllegalArgumentException (tagged? {:kind :test-obj} :foo "bar" :ban))
"An exception should be thrown if any of `tags` is not a keyword: `tagged?`.") "An exception should be thrown if any of `tags` is not a keyword: `tagged?`.")
(is (thrown? IllegalArgumentException (untag {} :foo "bar" :ban)) (is (thrown? IllegalArgumentException (untag {:kind :test-obj} :foo "bar" :ban))
"An exception should be thrown if any of `tags` is not a keywordp: `untag`."))) "An exception should be thrown if any of `tags` is not a keywordp: `untag`.")
(let [o (tag {:kind :test-obj} :foo '(:bar :ban) :froboz)]
(is (tagged? o :ban :bar :foo :froboz)
"It's now allowed to include lists of tags in the arg list for `tag`."))))