Not yet working but a good start
This commit is contained in:
parent
f1ebcbcc1d
commit
64fd9ffb5b
213 changed files with 5501 additions and 4 deletions
31
src/geocsv_lite/core.cljs
Normal file
31
src/geocsv_lite/core.cljs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
(ns geocsv-lite.core
|
||||
(:require
|
||||
[clojure.browser.dom :as dom]
|
||||
[clojure.string :as cs]
|
||||
[geocsv-lite.data :refer [get-csv-url get-data get-query-part-as-map]]
|
||||
[geocsv-lite.gis :as gis]
|
||||
[geocsv-lite.map :as m]))
|
||||
|
||||
(enable-console-print!)
|
||||
|
||||
(println "This text is printed from src/geocsv-lite/core.cljs. Go ahead and edit it and see reloading in action.")
|
||||
|
||||
|
||||
|
||||
|
||||
;; define your app data so that it doesn't get over-written on reload
|
||||
|
||||
(defonce app-state (atom {:text "Hello world!"}))
|
||||
|
||||
(defn on-js-reload []
|
||||
;; optionally touch your app-state to force rerendering depending on
|
||||
;; your application
|
||||
;; (swap! app-state update-in [:__figwheel_counter] inc)
|
||||
(m/add-view "map" 55 -4 10)
|
||||
(let [query (get-query-part-as-map)
|
||||
uri (get-csv-url query)
|
||||
records (get-data :map)]
|
||||
(dom/set-text (.getElementById js/document "message")
|
||||
(str "Query was: " query "; uri was: " uri))))
|
||||
|
||||
|
||||
89
src/geocsv_lite/data.cljs
Normal file
89
src/geocsv_lite/data.cljs
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
(ns geocsv-lite.data
|
||||
(:require-macros [cljs.core.async.macros :refer [go]])
|
||||
(:require [clojure.browser.dom :as dom]
|
||||
[clojure.string :as cs]
|
||||
[cljs-http.client :as http]
|
||||
[cljs.core.async :refer [<!]]
|
||||
[geocsv-lite.gis :as gis]
|
||||
[geocsv-lite.map :refer [get-view]]
|
||||
))
|
||||
|
||||
;; function getQueryVariable(variable)
|
||||
;; {
|
||||
;; var query = window.location.search.substring(1);
|
||||
;; var vars = query.split("&");
|
||||
;; for (var i=0;i<vars.length;i++) {
|
||||
;; var pair = vars[i].split("=");
|
||||
;; if(pair[0] == variable){return pair[1];}
|
||||
;; }
|
||||
;; return(false);
|
||||
;; }
|
||||
|
||||
|
||||
(defn get-query-part-as-map
|
||||
"Returns the query part of the current document URL as a keyword-string map."
|
||||
[]
|
||||
(let [query-nvs (map #(cs/split % "=") (cs/split (subs js/window.location.search 1) "&"))]
|
||||
(when (every? #(= (count %) 2) query-nvs)
|
||||
(zipmap (map #(keyword (first %)) query-nvs)(map #(nth % 1) query-nvs)))))
|
||||
|
||||
|
||||
(defn get-csv-url
|
||||
"`query` is expected a keyword-string map which may have keys:
|
||||
|
||||
* `:docid` whose value is a Google Sheets document id;
|
||||
* `:uri` whose value is the URI of a JSON or CSV file.
|
||||
|
||||
If either of these keys is found, returns an appropriate URL, else nil."
|
||||
[query]
|
||||
(when (map? query)
|
||||
(cond
|
||||
(:docid query) (str
|
||||
"https://docs.google.com/spreadsheets/d/"
|
||||
(:docid query)
|
||||
"/export?format=csv")
|
||||
(:uri query) (:uri query))))
|
||||
|
||||
|
||||
(defn default-handler
|
||||
[response k]
|
||||
(if
|
||||
(= (:status response) 200)
|
||||
(let [content (:body response)
|
||||
data (js->clj (.-data (.parse js/Papa content)))
|
||||
cols (map
|
||||
#(let [n (cs/lower-case (cs/replace (cs/trim %) #"[^\w\d]+" "-"))]
|
||||
(keyword
|
||||
(if (empty? n)
|
||||
(gensym)
|
||||
n)))
|
||||
(first data))
|
||||
records (map
|
||||
(fn [r] (zipmap cols (map str r)))
|
||||
(rest data))
|
||||
]
|
||||
;; (println records)
|
||||
(gis/refresh-map-pins (get-view k) records))
|
||||
(println (str "Bad response from server: " (:status response)))))
|
||||
|
||||
|
||||
(defn get-data
|
||||
[k]
|
||||
(let
|
||||
[uri (get-csv-url (get-query-part-as-map))]
|
||||
(go (let [response (<! (http/get uri {:with-credentials? "false"
|
||||
:access-control-allow-credentials "true"
|
||||
:origin js/window.location.hostname}))]
|
||||
(println (cs/join " " ["tx:" uri "rx:" (:status response)]))
|
||||
(default-handler response k)))))
|
||||
|
||||
(defn get-data-with-uri-and-handler
|
||||
[uri handler-fn k]
|
||||
(go (let [response (<! (http/get uri))]
|
||||
(apply handler-fn (list response k)))))
|
||||
|
||||
|
||||
(go (let [uri "http://localhost:3449/data/data.csv"
|
||||
response (<! (http/get uri))]
|
||||
(when (= (:status response) 200)
|
||||
(default-handler response :map))))
|
||||
202
src/geocsv_lite/gis.cljs
Normal file
202
src/geocsv_lite/gis.cljs
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
(ns ^{:doc "geocsv app map stuff."
|
||||
:author "Simon Brooke"}
|
||||
geocsv-lite.gis
|
||||
(:require [cljs.reader :refer [read-string]]
|
||||
[clojure.string :as cs]))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;
|
||||
;;;; geocsv.gis: stuff to do with maps.
|
||||
;;;;
|
||||
;;;; This program is free software; you can redistribute it and/or
|
||||
;;;; modify it under the terms of the GNU General Public License
|
||||
;;;; as published by the Free Software Foundation; either version 2
|
||||
;;;; of the License, or (at your option) any later version.
|
||||
;;;;
|
||||
;;;; This program is distributed in the hope that it will be useful,
|
||||
;;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
;;;; GNU General Public License for more details.
|
||||
;;;;
|
||||
;;;; You should have received a copy of the GNU General Public License
|
||||
;;;; along with this program; if not, write to the Free Software
|
||||
;;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
|
||||
;;;; USA.
|
||||
;;;;
|
||||
;;;; Copyright (C) 2016 Simon Brooke for Radical Independence Campaign
|
||||
;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
;; map stuff.
|
||||
|
||||
(defn get-current-location [view]
|
||||
"Return the current location from the device, setting it in the database and
|
||||
returning the locality. `view` is expected to be a Leaflet view."
|
||||
(try
|
||||
(if (.-geolocation js/navigator)
|
||||
(.getCurrentPosition
|
||||
(.-geolocation js/navigator)
|
||||
(fn [position]
|
||||
(let [lat (.-latitude (.-coords position))
|
||||
lng (.-longitude (.-coords position))]
|
||||
(js/console.log (str "Current location is: " lat ", " lng))
|
||||
(if
|
||||
(and view (float? lat) (float? lng))
|
||||
(do
|
||||
(.panTo view (.latLng js/L lat lng)))
|
||||
(do
|
||||
(js/console.log
|
||||
(if view
|
||||
(str "Geolocation failed lat: '" lat "'; lng '" lng "'")
|
||||
"No value for subscription to [:view]"))
|
||||
0)))))
|
||||
(do
|
||||
(js/console.log "Geolocation not available")
|
||||
0))
|
||||
(catch js/Object any
|
||||
(js/console.log "Exception while trying to access location: " + any)
|
||||
0)))
|
||||
|
||||
|
||||
(defn map-pin-click-handler
|
||||
[id]
|
||||
(js/console.log (str "Click handler for record #" id)))
|
||||
|
||||
|
||||
(defn pin-image
|
||||
"Return the name of a suitable pin image for this `record`."
|
||||
[record]
|
||||
(let [n (cs/capitalize
|
||||
(cs/replace
|
||||
(cs/lower-case
|
||||
(str (:category record)))
|
||||
#"[^a-z0-9]" "-"))]
|
||||
(str
|
||||
(if
|
||||
(empty? n) "Unknown"
|
||||
n)
|
||||
"-pin")))
|
||||
|
||||
|
||||
(defn popup-content
|
||||
"Appropriate content for the popup of a map pin for this `record`."
|
||||
[record]
|
||||
(if
|
||||
(map? record) ;; which it should be!
|
||||
(str
|
||||
"<h5>"
|
||||
(:name record)
|
||||
"</h5><dl>"
|
||||
(apply
|
||||
str
|
||||
(map
|
||||
#(str "<dt>" (name %) "</dt><dd>" (record %) "</dd>")
|
||||
(filter #(record %) (keys record))))
|
||||
"</dl>")))
|
||||
|
||||
(defn popup-table-content
|
||||
"Appropriate content for the popup of a map pin for this `record`, as a
|
||||
table. Obviously this is semantically wrong, but for styling reasons it's
|
||||
worth trying."
|
||||
[record]
|
||||
(if
|
||||
(map? record) ;; which it should be!
|
||||
(str
|
||||
"<h5>"
|
||||
(:name record)
|
||||
"</h5><table>"
|
||||
(apply
|
||||
str
|
||||
(map
|
||||
#(str "<tr><th>" (name %) "</th><td>" (record %) "</td></tr>")
|
||||
(sort (filter #(record %) (keys record)))))
|
||||
"</table>")))
|
||||
|
||||
(defn add-map-pin
|
||||
"Add an appropriate map-pin for this `record` in this map `view`, if it
|
||||
has a valid `:latitude` and `:longitude`."
|
||||
[record index view]
|
||||
(let [lat (:latitude record)
|
||||
lng (:longitude record)]
|
||||
(if
|
||||
(and
|
||||
(number? lat)
|
||||
(number? lng)
|
||||
(not (zero? lat))
|
||||
(not (zero? lng)))
|
||||
(let [pin (.icon js/L
|
||||
(clj->js
|
||||
{:iconAnchor [16 41]
|
||||
:iconSize [32 42]
|
||||
:iconUrl (str "img/map-pins/" (pin-image record) ".png")
|
||||
:riseOnHover true
|
||||
:shadowAnchor [16 23]
|
||||
:shadowSize [57 24]
|
||||
:shadowUrl "img/map-pins/shadow_pin.png"}))
|
||||
marker (.marker js/L
|
||||
(.latLng js/L lat lng)
|
||||
(clj->js {:icon pin
|
||||
:title (:name record)}))]
|
||||
(.bindPopup marker (popup-table-content record))
|
||||
(.addTo marker view)
|
||||
(js/console.log (str "Added `"(:name record)"` in at " lat ", " lng))
|
||||
marker))))
|
||||
|
||||
(defn map-remove-pins
|
||||
"Remove all pins from this map `view`. Side-effecty; liable to be
|
||||
problematic."
|
||||
[view]
|
||||
(if view
|
||||
(.eachLayer view
|
||||
#(if
|
||||
(instance? js/L.Marker %)
|
||||
(.removeLayer view %)))
|
||||
view))
|
||||
|
||||
(defn compute-zoom
|
||||
"See [explanation here](https://leafletjs.com/examples/zoom-levels/). Brief
|
||||
summary: it's hard, but it doesn't need to be precise."
|
||||
[min-lat max-lat min-lng max-lng]
|
||||
(let [n (min (/ 360 (- max-lng min-lng)) (/ 180 (- max-lat min-lat)))]
|
||||
(first
|
||||
(remove
|
||||
nil?
|
||||
(map
|
||||
#(if (> (reduce * (repeat 2 %)) n) %)
|
||||
(range))))))
|
||||
|
||||
(defn compute-centre
|
||||
"Compute, and return as a map with keys `:latitude` and `:longitude`, the
|
||||
centre of the locations of these records as indicated by the values of their
|
||||
`:latitude` and `:longitude` keys."
|
||||
[records]
|
||||
(let [lats (filter number? (map :latitude records))
|
||||
min-lat (apply min lats)
|
||||
max-lat (apply max lats)
|
||||
lngs (filter number? (map :longitude records))
|
||||
min-lng (apply min lngs)
|
||||
max-lng (apply max lngs)]
|
||||
(if-not
|
||||
(or (empty? lats) (empty? lngs))
|
||||
{:latitude (+ min-lat (/ (- max-lat min-lat) 2))
|
||||
:longitude (+ min-lng (/ (- max-lng min-lng) 2))
|
||||
:zoom (compute-zoom min-lat max-lat min-lng max-lng)}
|
||||
{})))
|
||||
|
||||
(defn refresh-map-pins
|
||||
"Refresh the map pins on the current map. Side-effecty; liable to be
|
||||
problematic."
|
||||
[view records]
|
||||
(js/console.log "refresh-map-pins called")
|
||||
(let [view (map-remove-pins view)
|
||||
centre (compute-centre records)]
|
||||
(if
|
||||
view
|
||||
(let [added (remove nil? (map #(add-map-pin %1 %2 view) records (range)))]
|
||||
(js/console.log (str "Adding " (count added) " pins"))
|
||||
(if
|
||||
(:latitude centre)
|
||||
(do
|
||||
(js/console.log (str "computed centre: " centre))
|
||||
(.setView view (clj->js [(:latitude centre) (:longitude centre)]) (:zoom centre)))))
|
||||
(do (js/console.log "View is not yet ready")))))
|
||||
87
src/geocsv_lite/map.cljs
Normal file
87
src/geocsv_lite/map.cljs
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
(ns geocsv-lite.map
|
||||
(:require ))
|
||||
|
||||
;;; Cribbed heavily from
|
||||
;;; https://github.com/reagent-project/reagent-cookbook/tree/master/recipes/leaflet
|
||||
;;; but using OSM data because we can't afford commercial, so also cribbed from
|
||||
;;; https://switch2osm.org/using-tiles/getting-started-with-leaflet/
|
||||
;;; Note that this is raw reagent stylee; it should be refactoed into re-frame stylee
|
||||
;;; when I understand it better.
|
||||
|
||||
;; which provider to use
|
||||
(def ^:dynamic *map-provider* :osm)
|
||||
|
||||
(def osm-url "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png")
|
||||
(def osm-attrib "Map data © <a href='http://openstreetmap.org'>OpenStreetMap</a> contributors")
|
||||
|
||||
;; My gods mapbox is user-hostile!
|
||||
(defn map-did-mount-mapbox
|
||||
"Did-mount function loading map tile data from MapBox (proprietary).
|
||||
Arguments are:
|
||||
* `id` the element id of the HTML element to occupy (string);
|
||||
* `lat` the latitude of the centre of the view (real number);
|
||||
* `lng` the longitude of the centre of the view (real number);
|
||||
* `zoom` the initial zoom level of the view (real number)."
|
||||
[id lat lng zoom]
|
||||
(let [view (.setView
|
||||
(.map js/L id (clj->js {:zoomControl "false"}))
|
||||
#js [lat lng]
|
||||
zoom)]
|
||||
;; NEED TO REPLACE FIXME with your mapID!
|
||||
(.addTo (.tileLayer js/L "http://{s}.tiles.mapbox.com/v3/FIXME/{z}/{x}/{y}.png"
|
||||
(clj->js {:attribution "Map data © [...]"
|
||||
:maxZoom 18})))
|
||||
view))
|
||||
|
||||
(defn map-did-mount-osm
|
||||
"Did-mount function loading map tile data from Open Street Map.
|
||||
Arguments are:
|
||||
* `id` the element id of the HTML element to occupy (string);
|
||||
* `lat` the latitude of the centre of the view (real number);
|
||||
* `lng` the longitude of the centre of the view (real number);
|
||||
* `zoom` the initial zoom level of the view (real number)."
|
||||
[id lat lng zoom]
|
||||
(let [view (.setView
|
||||
(.map js/L
|
||||
id
|
||||
(clj->js {:zoomControl false}))
|
||||
#js [lat lng]
|
||||
zoom)]
|
||||
(.addTo (.tileLayer js/L osm-url
|
||||
(clj->js {:attribution osm-attrib
|
||||
:maxZoom 18}))
|
||||
view)
|
||||
view))
|
||||
|
||||
(defn map-did-mount
|
||||
"Select the actual map provider to use. Arguments are:
|
||||
* `id` the element id of the HTML element to occupy (string);
|
||||
* `lat` the latitude of the centre of the view (real number);
|
||||
* `lng` the longitude of the centre of the view (real number);
|
||||
* `zoom` the initial zoom level of the view (real number)."
|
||||
[id lat lng zoom]
|
||||
(case *map-provider*
|
||||
:mapbox (map-did-mount-mapbox id lat lng zoom)
|
||||
:osm (map-did-mount-osm id lat lng zoom)
|
||||
;; potentially others
|
||||
))
|
||||
|
||||
|
||||
(def views (atom {}))
|
||||
|
||||
|
||||
(defn add-view
|
||||
[id lat lng zoom]
|
||||
(let [k (keyword id)]
|
||||
(when-not
|
||||
(@views k)
|
||||
(swap! views assoc k (map-did-mount id lat lng zoom)))
|
||||
(views k)))
|
||||
|
||||
|
||||
(defn get-view
|
||||
[k]
|
||||
(@views k))
|
||||
|
||||
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue