#3: Good progress towards getting SVG reading going, but not there yet.

This commit is contained in:
Simon Brooke 2020-05-28 17:48:01 +01:00
parent 1ab35dbe7d
commit 9892af65e3
No known key found for this signature in database
GPG key ID: A7A4F18D1D4DF987
10 changed files with 155 additions and 32 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

@ -2,7 +2,8 @@
"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.vertex :refer [vertex?]])) [walkmap.vertex :refer [vertex?]]))
@ -26,7 +27,10 @@
(if (if
(every? vertex? vertices) (every? vertex? vertices)
{:vertices vertices :id (keyword (gensym "path")) :kind :path} {:vertices vertices :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 #(or (:kind %) (type %) "nil") 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

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

@ -0,0 +1,95 @@
(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 :refer [info error spy]]
[walkmap.path :refer [path]]
;; [walkmap.polygon :refer [polygon]]
[walkmap.tag :refer [tag]]
[walkmap.vertex :refer [vertex vertex?]]))
(defn upper-case?
[s]
(every? #(Character/isUpperCase %) s))
(defn match->vertex
[match-vector x y]
(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'})))
(defn command-string->vertices
[s]
(let [matcher (re-matcher #"([a-zA-Z]) +([-+]?[0-9]*\.?[0-9]+) +([-+]?[0-9]*\.?[0-9]+) +" s)]
(loop [match (re-find matcher) ;loop starts with 2 set arguments
result []
x 0
y 0]
(if-not match
(filter vertex? result)
(let [m (match->vertex match x y)]
(recur (re-find matcher) ;loop with 2 new arguments
(conj result (:vertex m))
(:x m)
(:y m)))))))
(defn path-elt->path
[elt]
(tag
(path (command-string->vertices (-> elt :attrs :d)))
(when (-> elt :attrs :class)
(map keyword (s/split (-> elt :attrs :class) #" ")))))
(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))]
(map path-elt->path paths))))
(read-svg "resources/iom/manual_roads.svg")
;; (def xx (z/xml-zip (x/parse (io/file "resources/iom/manual_roads.svg"))))
;; (type xx)
;; (first xx)
;; (zx/xml-> xx :svg :g :path)
;; (def xxx (x/parse (io/file "resources/iom/manual_roads.svg")))

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,7 +85,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))))
(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?`
@ -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

@ -40,7 +40,7 @@
;; 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 (:id v) (assoc current (:id o) (: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 `: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)))

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

@ -30,17 +30,21 @@
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]
(if (let [tags' (flatten tags)]
(map? object)
(if (if
(every? keyword? tags) (map? object)
(assoc object ::tags (union (set tags) (::tags object))) (if
(every? keyword? tags')
(assoc object ::tags (union (set tags') (::tags object)))
(throw (IllegalArgumentException.
(str "Must be keyword(s): " (map type tags')))))
(throw (IllegalArgumentException. (throw (IllegalArgumentException.
(str "Must be keyword(s): " (map type tags))))) (str "Must be a map: " (type object)))))))
(throw (IllegalArgumentException.
(str "Must be a map: " (type object))))))
(defmacro tags (defmacro tags
"Return the tags of this object, if any." "Return the tags of this object, if any."

View file

@ -1,9 +1,6 @@
(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."
@ -13,11 +10,20 @@
(apply merge-with deep-merge vals) (apply merge-with deep-merge vals)
(last vals))) (last vals)))
(defn vertices ;; (defn vertices
"If `o` is an object with vertices, return those vertices, else nil." ;; "If `o` is an object with vertices, return those vertices, else nil."
[o] ;; [o]
(cond ;; (cond
(v/vertex? o) (list o) ;; (v/vertex? o) (list o)
(q/polygon? o) (:vertices o) ;; (q/polygon? o) (:vertices o)
(p/path? o) (:vertices o))) ;; (p/path? o) (:vertices o)))
(defn truncate
"If string `s` is more than `n` characters long, return the first `n`
characters; otherwise, return `s`."
[s n]
(if (and (string? s) (number? n) (> (count s) n))
(subs s 0 n)
s))

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))))
"." "."
"-"))) "-")))
@ -79,10 +80,10 @@
(assoc o :kind :vertex :id (vertex-key o)) (assoc o :kind :vertex :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

@ -47,5 +47,8 @@
(is (thrown? IllegalArgumentException (tagged? {} :foo "bar" :ban)) (is (thrown? IllegalArgumentException (tagged? {} :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 {} :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 {} :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`."))))