001  (ns walkmap.path
002    "Essentially the specification for things we shall consider to be path.
003    **Note that** for these purposes `path` means any continuous linear
004    feature, where such features specifically include watercourses."
005    (:require [clojure.string :as s]
006              [walkmap.edge :as e]
007              [walkmap.polygon :refer [polygon?]]
008              [walkmap.utils :refer [kind-type]]
009              [walkmap.vertex :refer [vertex?]]))
010  
011  (defn path?
012    "True if `o` satisfies the conditions for a path. A path shall be a map
013    having the key `:vertices`, whose value shall be a sequence of vertices as
014    defined in `walkmap.vertex`."
015    [o]
016    (let
017      [v (:vertices o)]
018      (and
019        (seq? v)
020        (> (count v) 2)
021        (every? vertex? v)
022        (:walkmap.id/id o)
023        (or (nil? (:kind o)) (= (:kind o) :path)))))
024  
025  (defn path
026    "Return a path constructed from these `vertices`."
027    [& vertices]
028    (when-not (every? vertex? vertices)
029      (throw (IllegalArgumentException.
030               (str
031                 "Each item on path must be a vertex: "
032                 (s/join " " (map kind-type (remove vertex? vertices)))))))
033    {:vertices vertices :walkmap.id/id (keyword (gensym "path")) :kind :path})
034  
035  (defn polygon->path
036    "If `o` is a polygon, return an equivalent path. What's different about
037    a path is that in polygons there is an implicit edge between the first
038    vertex and the last. In paths, there isn't, so we need to add that
039    edge explicitly.
040  
041    If `o` is not a polygon, will throw an exception."
042    [o]
043    (when-not (polygon? o)
044      (throw (IllegalArgumentException. (str "Not a polygon: " (kind-type o)))))
045    (assoc (dissoc o :vertices)
046      :kind :path
047      ;; `concat` rather than `conj` because order matters.
048      :vertices (concat (:vertices o) (list (first (:vertices o))))))
049  
050  (defn path->edges
051    "if `o` is a path, a polygon, or a sequence of vertices, return a sequence of
052    edges representing that path, polygon or sequence.
053  
054    Throws `IllegalArgumentException` if `o` is not a path, a polygon, or
055    sequence of vertices."
056    [o]
057    (cond
058      (seq? o)
059      (when
060        (and
061          (vertex? (first o))
062          (vertex? (first (rest o))))
063        (cons
064          ;; TODO: think about: when constructing an edge from a path, should the
065          ;; constructed edge be tagged with the tags of the path?
066          (e/edge (first o) (rest o))
067          (path->edges (rest o))))
068      (path? o)
069      (path->edges (:vertices o))
070      :else
071      (throw (IllegalArgumentException.
072               "Not a path or sequence of vertices!"))))
073  
074  (defn length
075    "Return the length of this path, in metres. **Note that**
076    1. This is not the same as the distance from the start to the end of the
077    path, which, except for absolutely straight paths, will be shorter;
078    2. It is not even quite the same as the length of the path *as rendered*,
079    since paths will generally be rendered as spline curves."
080    [path]
081    (if
082      (path? path)
083      (reduce + (map e/length (path->edges path)))
084      (throw (IllegalArgumentException. "Not a path!"))))