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)
 
(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]))
 
(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))
 
(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))
 
(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))])
 
(ns mw-ui.repl
  (:use mw-ui.handler
        ring.server.standalone
        [ring.middleware file-info file])
  (:gen-class))
(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))
(defn -main []
  (start-server))
 
(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)))
 
(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)})))))))
 
(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)))
 
(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)))))))