mw-ui

0.1.0-SNAPSHOT


Web-based user interface for MicroWorld

dependencies

org.clojure/clojure
1.6.0
mw-engine
0.1.0-SNAPSHOT
mw-parser
0.1.0-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]
            [hiccup.core :refer [html]]
            [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 "State " (:state cell) "; altitude: " (:altitude cell) "; fertility: " (:fertility 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)}
            [:img {:alt (world/format-cell 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 session as a complete HTML page.

(defn render-world-table
  []
  (let [world (or (session/get :world)
                  (engine/transform-world
                   (heightmap/apply-heightmap
                    (world/make-world 20 20)
                    "resources/public/img/20x20/hill.png")
                   rules/init-rules))
        rules (or (session/get :rules) rules/natural-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)]]))

Render the world implied by the session as a complete HTML page.

(defn render-world
  []
  (html
   [:html
    [:head
     [:title "MicroWorld demo"]
     [:link {:media "only screen and (max-device-width: 480px)" :href "css/phone.css" :type  "text/css" :rel "stylesheet"}]
     [:link {:media "only screen and (min-device-width: 481px) and (max-device-width: 1024px)" :href "css/tablet.css" :type "text/css" :rel "stylesheet"}]
     [:link {:media "screen and (min-device-width: 1025px)" :href "css/standard.css" :type "text/css" :rel "stylesheet"}]
     [:link {:media "print" :href "css/print.css" :type "text/css" :rel "stylesheet"}]
     [:link {:href "css/states.css" :type "text/css" :rel "stylesheet"}]
     [:meta {:http-equiv "refresh" :content "5"}]]
    [:body
     (render-world-table)
     ]]))
 
(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))
 
(ns mw-ui.routes.home
  (:use 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.session :as session]))
(defn home-page []
  (layout/render "world.html" {:title "Watch your world grow" 
                               :content (html (world/render-world-table)) 
                               :seconds (or (session/get :seconds) 5) 
                               :maybe-refresh "refresh"}))
(defn world-page []
  (layout/render "world.html" {:title "Watch your world grow" 
                               :content (html (world/render-world-table)) 
                               :seconds (or (session/get :seconds) 5) 
                               :maybe-refresh "refresh"}))
(defn about-page []
  (layout/render "about.html" {:title "About MicroWorld" :content (util/md->html "/md/about.md")}))
(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 (list-states)
                              :components ["mw-engine" "mw-parser" "mw-ui"]}))
(defn rules-page 
  ([request]
    (let [rule-text (:src request)
          error 
          (try 
            (do
              (if rule-text
                (session/put! :rules (compiler/compile-string rule-text)))
              (session/put! :rule-text rule-text)
              nil)
            (catch Exception e (.getMessage e)))]
      (layout/render "rules.html" {:title "Edit Rules" 
                                   :rule-text (or (session/get :rule-text) (slurp "resources/public/rulesets/basic.txt"))
                                   :error error})))
  ([]
    (rules-page nil)))
(defroutes home-routes
  (GET "/" [] (home-page))
  (GET "/about" [] (about-page))
  (GET "/docs"  [] (docs-page))
  (GET "/world"  [] (world-page))
  (GET "/rules" request (rules-page request))
  (POST "/rules" request (rules-page request)))
 
(ns mw-ui.util
  (:require [noir.io :as io]
            [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)))