mw-ui0.1.1-SNAPSHOTWeb-based user interface for MicroWorld dependencies
| (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 session as a complete HTML page. | (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])) | ||||||||||||||||||||||||||||||||||
(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 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 | (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])) | ||||||||||||||||||||||||||||||||||
(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 If | (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))))))) | ||||||||||||||||||||||||||||||||||