# mw-ui

## 0.1.2-SNAPSHOT

Web-based user interface for MicroWorld

### dependencies

org.clojure/clojure

1.6.0

mw-engine

0.1.2-SNAPSHOT

mw-parser

0.1.2-SNAPSHOT

lib-noir

0.8.4

ring-server

0.3.1

selmer

0.6.8

com.taoensso/timbre

3.2.1

com.taoensso/tower

2.0.2

markdown-clj

0.9.44

environ

0.5.0

noir-exception

0.2.2

(this space intentionally left almost blank)

# mw-ui.handler

toc

    (ns mw-ui.handler
      (:require [compojure.core :refer [defroutes]]
                [mw-ui.routes.home :refer [home-routes]]
                [mw-ui.middleware :refer [load-middleware]]
                [noir.response :refer [redirect]]
                [noir.util.middleware :refer [app-handler]]
                [compojure.route :as route]
                [taoensso.timbre :as timbre]
                [taoensso.timbre.appenders.rotor :as rotor]
                [selmer.parser :as parser]
                [environ.core :refer [env]]))

    (defroutes app-routes
      (route/resources "/")
      (route/not-found "Not Found"))

init will be called once when app is deployed as a servlet on an app server such as Tomcat put any initialization code here

    (defn init
      []
      (timbre/set-config!
        [:appenders :rotor]
        {:min-level :info
         :enabled? true
         :async? false ; should be always false for rotor
         :max-message-per-msecs nil
         :fn rotor/appender-fn})
      (timbre/set-config!
        [:shared-appender-config :rotor]
        {:path "mw_ui.log" :max-size (* 512 1024) :backlog 10})
      (if (env :dev) (parser/cache-off!))
      (timbre/info "mw-ui started successfully"))

destroy will be called when your application shuts down, put any clean up code here

    (defn destroy
      []
      (timbre/info "mw-ui is shutting down..."))

    (def app (app-handler
               ;; add your application routes here
               [home-routes app-routes]
               ;; add custom middleware here
               :middleware (load-middleware)
               ;; timeout sessions after 30 minutes
               :session-options {:timeout (* 60 30)
                                 :timeout-response (redirect "/")}
               ;; add access rules here
               :access-rules []
               ;; serialize/deserialize the following data formats
               ;; available formats:
               ;; :json :json-kw :yaml :yaml-kw :edn :yaml-in-html
               :formats [:json-kw :edn]))

# mw-ui.layout

toc

    (ns mw-ui.layout
      (:require [selmer.parser :as parser]
                [clojure.string :as s]
                [ring.util.response :refer [content-type response]]
                [compojure.response :refer [Renderable]]))

    (def template-path "templates/")

    (deftype RenderableTemplate [template params]
      Renderable
      (render [this request]
        (content-type
          (->> (assoc params
                      (keyword (s/replace template #".html" "-selected")) "active"
                      :servlet-context
                      (if-let [context (:servlet-context request)]
                        (.getContextPath context)))
            (parser/render-file (str template-path template))
            response)
          "text/html; charset=utf-8")))

    (defn render [template & [params]]
      (RenderableTemplate. template params))

# mw-ui.middleware

toc

    (ns mw-ui.middleware
      (:require [taoensso.timbre :as timbre]
                [selmer.parser :as parser]
                [environ.core :refer [env]]
                [selmer.middleware :refer [wrap-error-page]]
                [noir-exception.core
                  :refer [wrap-internal-error wrap-exceptions]]))

    (defn log-request [handler]
      (fn [req]
        (timbre/debug req)
        (handler req)))

    (def development-middleware
      [log-request
       wrap-error-page
       wrap-exceptions])

    (def production-middleware
      [#(wrap-internal-error % :log (fn [e] (timbre/error e)))])

    (defn load-middleware []
      (concat (when (env :dev) development-middleware)
              production-middleware))

# mw-ui.render-world

toc

    (ns mw-ui.render-world
      (:require [mw-engine.core :as engine]
                [mw-engine.world :as world]
                [mw-engine.heightmap :as heightmap]
                [mw-engine.natural-rules :as rules]
                [mw-parser.bulk :as compiler]
                [hiccup.core :refer [html]]
                [noir.io :as io]
                [noir.session :as session]))

    (defn format-css-class [statekey]
      "Format this statekey, assumed to be a keyword indicating a state in the
       world, into a CSS class"
      (subs (str statekey) 1))

Render this statekey, assumed to be a keyword indicating a state in the world, into a path which should recover the corresponding image file.

    (defn format-image-path
      [statekey]
      (format "img/tiles/%s.png" (format-css-class statekey)))

    (defn format-mouseover [cell]
      (str cell))

Render this world cell as a Hiccup table cell.

    (defn render-cell
      [cell]
      (let [state (:state cell)]
        [:td {:class (format-css-class state) :title (format-mouseover cell)}
         [:a {:href (format "inspect?x=%d&y=%d" (:x cell) (:y cell))}
          [:img {:alt (:state cell) :src (format-image-path state)}]]]))

Render this world row as a Hiccup table row.

    (defn render-world-row
      [row]
      (apply vector (cons :tr (map render-cell row))))

Render the world implied by the current session as a complete HTML table in a DIV.

    (defn render-world-table
      []
      (let [world (or (session/get :world)
                      (engine/transform-world
                       (heightmap/apply-heightmap
                         (io/get-resource "/img/heightmaps/small_hill.png")
                         ;; "resources/public/img/heightmaps/great_britain_and_ireland_small.png")
                       rules/init-rules))
            rules (or (session/get :rules)
                      (do (session/put! :rules
                                        (compiler/compile-file
                                          (io/get-resource "/rulesets/basic.txt")))
                        (session/get :rules)))
            generation (+ (or (session/get :generation) 0) 1)
            w2 (engine/transform-world world rules)
            ]
        (session/put! :world w2)
        (session/put! :generation generation)
        [:div {:class "world"}
          (apply vector
                     (cons :table
                           (map render-world-row w2)))
          [:p
           (str "Generation " generation)]]))

    (defn render-inspector
      [cell table]
      [:table {:class "music-ruled"}
       [:tr
        [:td {:colspan 2 :style "text-align: center;"}
         [:img {:src (str "img/tiles/" (name (:state cell)) ".png")
                :width 64
                :height 64}]]]
       [:tr [:th "Key"][:th "Value"]]
       (map #(vector :tr (vector :th %)(vector :td (cell %))) (keys cell))])

# mw-ui.repl

toc

    (ns mw-ui.repl
      (:use mw-ui.handler
            ring.server.standalone
            [ring.middleware file-info file]))

    (defonce server (atom nil))

    (defn get-handler []
      ;; #'app expands to (var app) so that when we reload our code,
      ;; the server is forced to re-resolve the symbol in the var
      ;; rather than having its own copy. When the root binding
      ;; changes, the server picks it up without having to restart.
      (-> #'app
          ; Makes static assets in $PROJECT_DIR/resources/public/ available.
          (wrap-file "resources")
          ; Content-Type, Content-Length, and Last Modified headers for files in body
          (wrap-file-info)))

used for starting the server in development mode from REPL

    (defn start-server
      [& [port]]
      (let [port (if port (Integer/parseInt port) 3000)]
        (reset! server
                (serve (get-handler)
                       {:port port
                        :init init
                        :auto-reload? true
                        :destroy destroy
                        :join? false}))
        (println (str "You can view the site at http://localhost:" port))))

    (defn stop-server []
      (.stop @server)
      (reset! server nil))

# mw-ui.routes.home

toc

    (ns mw-ui.routes.home
      (:use clojure.walk
            compojure.core
            [mw-engine.utils :as engine-utils]
            [mw-ui.routes.rules :as rules]
            [mw-ui.routes.params :as params])
      (:require [hiccup.core :refer [html]]
                [mw-ui.layout :as layout]
                [mw-ui.util :as util]
                [mw-ui.render-world :as world]
                [noir.session :as session]
                [ring.util.response :as response]))

    (defn home-page []
      (layout/render "trusted-content.html" {:title "Welcome to MicroWorld"
                                  :content (util/md->html "/md/mw-ui.md")}))

    (defn inspect-page [request]
      (let [params (keywordize-keys (:params request))
            xs (:x params)
            ys (:y params)
            x (if (not (empty? xs)) (read-string xs) 0)
            y (if (not (empty? ys)) (read-string ys) 0)
            world (session/get :world)
            cell (engine-utils/get-cell world x y)
            state (:state params)]
        (cond state
          (do
            (session/put! :world (engine-utils/set-property world cell :state (keyword state)))
            (response/redirect "world"))
          true
          (layout/render "inspector.html"
                         {:title (format "Inspect cell at %d, %d" x y)
                          :content (html (world/render-inspector cell world))
                          :cell cell
                          :x (:x cell)
                          :y (:y cell)
                          :states (util/list-resources
                                    "/img/tiles" #"([0-9a-z-_]+).png")}))))

    (defn world-page []
      (layout/render "trusted-content.html"
                     {:title "Watch your world grow"
                     :world-selected "active"
                     :content (html (world/render-world-table))
                     :pause (or (session/get :pause) 5)
                     :maybe-refresh "refresh"}))

    (defn about-page []
      (layout/render "trusted-content.html"
                     {:title "About MicroWorld"
                      :about-selected "active"
                      :content (util/md->html "/md/about.md")}))

    (defn md-page [request]
      (let [params (keywordize-keys (:params request))
            content (or (:content params) "missing.md")]
        (layout/render "trusted-content.html"
                       {:title "Welcome to MicroWorld"
                        :content (util/md->html (str "/md/" content))})))

    (defn list-states []
      (sort
        (filter #(not (nil? %))
                (map #(first (rest (re-matches #"([0-9a-z-]+).png" (.getName %))))
                     (file-seq (clojure.java.io/file "resources/public/img/tiles"))))))

    (defn docs-page []
      (layout/render "docs.html" {:title "Documentation"
                                  :parser (util/md->html "/md/mw-parser.md" )
                                  :states (util/list-resources "/img/tiles" #"([0-9a-z-_]+).png")
                                  :lessons (util/list-resources "/md/lesson-plans"  #"([0-9a-z-_]+).md")
                                  :components ["mw-engine" "mw-parser" "mw-ui"]}))

    (defroutes home-routes
      (GET "/" [] (home-page))
      (GET "/about" [] (about-page))
      (GET "/docs"  [] (docs-page))
      (GET "/world"  [] (world-page))
      (GET "/params" [] (params/params-page))
      (GET "/md" request (md-page request))
      (POST "/params" request (params/params-page request))
      (GET "/rules" request (rules/rules-page request))
      (POST "/rules" request (rules/rules-page request))
      (GET "/inspect" request (inspect-page request))
      (POST "/inspect" request (inspect-page request)))

# mw-ui.routes.params

toc

    (ns mw-ui.routes.params
      (:use clojure.walk
            clojure.java.io
            compojure.core)
      (:require [hiccup.core :refer [html]]
                [mw-engine.heightmap :as heightmap]
                [mw-parser.bulk :as compiler]
                [mw-ui.layout :as layout]
                [mw-ui.util :as util]
                [mw-ui.render-world :as world]
                [noir.io :as io]
                [noir.session :as session]))

    (defn- send-params []
      {:title "Choose your world"
       :heightmaps (util/list-resources "/img/heightmaps" #"([0-9a-z-_]+).png")
       :pause (or (session/get :pause) 5)
       :rulesets (util/list-resources "/rulesets" #"([0-9a-z-_]+).txt")
       })

Handler for params request. If no `request` passed, show empty params form. If `request` is passed, put parameters from request into session and show the world page.

    (defn params-page
      ([]
        (layout/render "params.html" (send-params)))
      ([request]
        (try
          (let [params (keywordize-keys (:form-params request))
                map (:heightmap params)
                pause (:pause params)
                rulefile (:ruleset params)
                rulepath (str "/rulesets/" rulefile ".txt")]
            (if (not (= map ""))
              (session/put! :world
                            (heightmap/apply-heightmap
                              (io/get-resource (str "/img/heightmaps/" map ".png")))))
            (if (not (= rulefile ""))
              (do
                (session/put! :rule-text (io/slurp-resource rulepath))
                (session/put! :rules (compiler/compile-file (io/get-resource rulepath)))))
            (if (not (= pause ""))
              (session/put! :pause pause))
            (layout/render "params.html"
                           (merge (send-params)
                                  {:r rulefile
                                   :h map
                                   :message "Your parameters are saved, now look at your world"})))
          (catch Exception e
            (let [params (keywordize-keys (:form-params request))]
              (layout/render "params.html"
                             (merge (send-params)
                                    {:title "Choose your world"
                                     :r (:ruleset params)
                                     :h (:heightmap params)
                                     :message "Your paramters are not saved"
                                     :error (str (.getName (.getClass e)) ": " (.getMessage e) "; " params)})))))))

# mw-ui.routes.rules

toc

    (ns mw-ui.routes.rules
      (:use clojure.walk
            compojure.core)
      (:require [hiccup.core :refer [html]]
                [mw-parser.bulk :as compiler]
                [mw-ui.layout :as layout]
                [mw-ui.util :as util]
                [mw-ui.render-world :as world]
                [noir.io :as io]
                [noir.session :as session]
                [ring.util.response :as response]))

    (defn process-rules-request
      [request]
      (let [src (:src (keywordize-keys (:form-params request)))]
          (try
            (cond src
              (let [rules (compiler/compile-string src)]
                {:rule-text src
                 :rules rules
                 :message (str "Successfully compiled "
                               (count rules)
                               " rules")           })
              true {:rule-text (or
                                 (session/get :rule-text)
                                 (io/slurp-resource "/rulesets/basic.txt"))
                    :message "No rules found in request; loading defaults"})
            (catch Exception e
              {:rule-text src
               :message "An error occurred during compilation"
               :error (str (.getName (.getClass e)) ": " (.getMessage e))}))))

Request handler for the `rules` request. If the `request` contains a value for `:src`, treat that as rule source and try to compile it. If compilation succeeds, stash the compiled rules and the rule text on the session, and provide feedback; if not, provide feedback.

If `request` doesn't contain a value for `:src`, load basic rule source from the session or from `resources/rulesets/basic.txt` and pass that back.

    (defn rules-page
      ([request]
        (let [processed (process-rules-request request)]
          (if (:rules processed)
            (session/put! :rules (:rules processed)))
          (if (:rule-text processed)
            (session/put! :rule-text (:rule-text processed)))
          (layout/render "rules.html"
                         (merge {:title "Edit Rules"} processed))))
      ([]
        (rules-page nil)))

# mw-ui.util

toc

    (ns mw-ui.util
      (:require [noir.io :as io]
                [noir.session :as session]
                [markdown.core :as md]))

reads a markdown file from public/md and returns an HTML string

    (defn md->html
      [filename]
      (->>
        (io/slurp-resource filename)
        (md/md-to-html-string)))

    (defn list-resources [directory pattern]
      "List resource files matching `pattern` in `directory`."
      (let
        [path (str (io/resource-path) directory)]
        (session/put! :list-resources-path path)
        (sort
          (filter #(not (nil? %))
                (map #(first (rest (re-matches pattern (.getName %))))
                     (file-seq (clojure.java.io/file path)))))))