diff --git a/README.md b/README.md index 2e12b74..3e2cb03 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # geocsv -GeoCSV is a wee tool to show comma-separated value data on a map. +A wee tool to show comma-separated value data on a map. The CSV file must have @@ -20,18 +20,32 @@ If you run the server running **geocsv**, the simplest way to add CSV files is s https://geocsv.example.com/ -and the file you want to view is `myfile.csv`, then you would specify this as +and the file you want to view is `myfile.csv`, then you would specify this as the value of `file` in the query part of the URL. https://geocsv.example.com/?file=myfile.csv +### Loading CSV file onto another public server + +If you're not running the **geocsv** server yourself, you can upload the CSV to another server which is accessible by the **geocsv** server. You can then map data from the CSV file by specifying the URL of the file as the value of `uri` in the query part of the URL: + + https://geocsv.example.com/?uri=http://my.other.server/path/to/myfile.csv + ### Using a Google spreadsheet -If you use [Google Sheets](https://www.google.co.uk/sheets/about/), then every sheet has a 'document id', a long string of characters which uniquely identifies that sheet. Suppose your Google spreadsheet has a document id of `abcdefghijklmnopqrstuvwxyz-12345`, then you could pull data from this spreadsheet by specifying: +If you use [Google Sheets](https://www.google.co.uk/sheets/about/), then every sheet has a 'document id', a long string of characters which uniquely identifies that sheet. Suppose your Google spreadsheet has a document id of `abcdefghijklmnopqrstuvwxyz-12345`, then you could pull data from this spreadsheet by specifying this as the value of `docid` in the query part of the URL: https://geocsv.example.com/?docid=abcdefghijklmnopqrstuvwxyz-12345 The spreadsheet **must** be publicly readable. +### Precedence + +Nothing, of course, stops you from specifying multiple arguments in the query part of the URL, but only one will be used. The precedence is in this order: + +1. `docid` is considered first, and overrides anything else; +2. `uri` is considered next, and overrides `file`; +3. the value of `file` is considered only if neither of the other two are present. + ## Not yet working GeoCSV is at an early stage of development, and some features are not yet working. @@ -40,10 +54,6 @@ GeoCSV is at an early stage of development, and some features are not yet workin At the current stage of development, if no appropriate image exists in the `resources/public/img/map-pins` folder, that's your problem. **TODO:** I intend at some point to make missing pin images default to `unknown-pin.png`, which does exist. -### Doesn't scale and centre the map to show the data in the sheet - -Currently the map is initially centred roughly on the centre of Scotland, and scaled arbitrarily. It should compute an appropriate centre and scale from the data provided, but currently doesn't. - ## Prerequisites You will need [Leiningen][1] 2.0 or above installed. diff --git a/env/dev/cljs/geocsv/app.cljs b/env/dev/cljs/geocsv/app.cljs index 4283167..9e65331 100644 --- a/env/dev/cljs/geocsv/app.cljs +++ b/env/dev/cljs/geocsv/app.cljs @@ -1,6 +1,6 @@ (ns^:figwheel-no-load geocsv.app (:require - [geocsv.core :as core] + [geocsv.client.core :as core] [cljs.spec.alpha :as s] [expound.alpha :as expound] [devtools.core :as devtools])) diff --git a/env/prod/cljs/geocsv/app.cljs b/env/prod/cljs/geocsv/app.cljs index 15b798d..5e4c2f4 100644 --- a/env/prod/cljs/geocsv/app.cljs +++ b/env/prod/cljs/geocsv/app.cljs @@ -1,5 +1,5 @@ (ns geocsv.app - (:require [geocsv.core :as core])) + (:require [geocsv.client.core :as core])) ;;ignore println statements in prod (set! *print-fn* (fn [& _])) diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index a16f310..0000000 --- a/package-lock.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "geocsv", - "version": "0.1.0-SNAPSHOT", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "leaflet": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.3.1.tgz", - "integrity": "sha512-adQOIzh+bfdridLM1xIgJ9VnJbAUY3wqs/ueF+ITla+PLQ1z47USdBKUf+iD9FuUA8RtlT6j6hZBfZoA6mW+XQ==" - } - } -} diff --git a/project.clj b/project.clj index a18d6b7..e4a5781 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject geocsv "0.1.0" +(defproject geocsv "0.1.1" :description "A wee tool to show comma-separated value data on a map." :url "http://example.com/FIXME" @@ -12,8 +12,9 @@ [com.cemerick/url "0.1.1"] [com.cognitect/transit-clj "0.8.319"] [compojure "1.6.1"] + [cpath-clj "0.1.2"] [cprop "0.1.15"] - [csv2edn "0.1.5"] + [csv2edn "0.1.6"] [day8.re-frame/http-fx "0.1.6"] [expound "0.8.3"] [funcool/struct "1.4.0"] @@ -32,6 +33,7 @@ [org.clojure/tools.cli "0.4.2"] [org.clojure/tools.logging "0.5.0"] [org.webjars.npm/bulma "0.8.0"] + [org.webjars.npm/leaflet "1.6.0"] [org.webjars.npm/material-icons "0.3.1"] [org.webjars/webjars-locator "0.38"] [re-frame "0.10.9"] @@ -43,7 +45,6 @@ [selmer "1.12.18"]] :min-lein-version "2.0.0" - :npm {:dependencies [[leaflet "1.3.1"]]} :source-paths ["src/clj" "src/cljs" "src/cljc"] :test-paths ["test/clj"] @@ -53,9 +54,11 @@ :plugins [[lein-cljsbuild "1.1.7"] [lein-codox "0.10.7"] - [lein-npm "0.6.2"] [lein-release "1.0.5"]] + :deploy-repositories [["releases" :clojars] + ["snapshots" :clojars]] + :clean-targets ^{:protect false} [:target-path [:cljsbuild :builds :app :compiler :output-dir] [:cljsbuild :builds :app :compiler :output-to]] :figwheel @@ -108,7 +111,7 @@ :cljsbuild{:builds {:app {:source-paths ["src/cljs" "src/cljc" "env/dev/cljs"] - :figwheel {:on-jsload "geocsv.core/mount-components"} + :figwheel {:on-jsload "geocsv.client.core/mount-components"} :compiler {:output-dir "target/cljsbuild/public/js/out" :closure-defines {"re_frame.trace.trace_enabled_QMARK_" true} @@ -141,4 +144,17 @@ } :profiles/dev {} - :profiles/test {}}) + :profiles/test {}} + + ;; `lein release` doesn't play nice with `git flow release`. Run `lein release` in the + ;; `develop` branch, then reset the `master` branch to the release tag. + + :release-tasks [["vcs" "assert-committed"] + ["clean"] + ["codox"] + ["change" "version" "leiningen.release/bump-version" "release"] + ["vcs" "commit"] + ["uberjar"] + ["deploy" "clojars"] + ["change" "version" "leiningen.release/bump-version"] + ["vcs" "commit"]]) diff --git a/resources/docs/docs.md b/resources/docs/docs.md index af49f3d..3e2cb03 100644 --- a/resources/docs/docs.md +++ b/resources/docs/docs.md @@ -1,6 +1,6 @@ # geocsv -GeoCSV is a wee tool to show comma-separated value data on a map. +A wee tool to show comma-separated value data on a map. The CSV file must have @@ -12,42 +12,48 @@ The CSV file must have Additionally, the value of the column `category`, if present, will be used to select map pins from the map pins folder, if a suitable pin is present. Thus is the value of `category` is `foo`, a map pin image with the name `Foo-pin.png` will be selected. +## Passing CSV files to the app + +### Loading them onto the server + +If you run the server running **geocsv**, the simplest way to add CSV files is simply to copy them into the directory `resourcs/data`. The default file is the one named `data.csv`, which is the one that will be served if nothing else is specified. Other files can be specifiec by appending `?file=filename` to the URL; so if the URL of your geocsv service is + + https://geocsv.example.com/ + +and the file you want to view is `myfile.csv`, then you would specify this as the value of `file` in the query part of the URL. + + https://geocsv.example.com/?file=myfile.csv + +### Loading CSV file onto another public server + +If you're not running the **geocsv** server yourself, you can upload the CSV to another server which is accessible by the **geocsv** server. You can then map data from the CSV file by specifying the URL of the file as the value of `uri` in the query part of the URL: + + https://geocsv.example.com/?uri=http://my.other.server/path/to/myfile.csv + +### Using a Google spreadsheet + +If you use [Google Sheets](https://www.google.co.uk/sheets/about/), then every sheet has a 'document id', a long string of characters which uniquely identifies that sheet. Suppose your Google spreadsheet has a document id of `abcdefghijklmnopqrstuvwxyz-12345`, then you could pull data from this spreadsheet by specifying this as the value of `docid` in the query part of the URL: + + https://geocsv.example.com/?docid=abcdefghijklmnopqrstuvwxyz-12345 + +The spreadsheet **must** be publicly readable. + +### Precedence + +Nothing, of course, stops you from specifying multiple arguments in the query part of the URL, but only one will be used. The precedence is in this order: + +1. `docid` is considered first, and overrides anything else; +2. `uri` is considered next, and overrides `file`; +3. the value of `file` is considered only if neither of the other two are present. + ## Not yet working GeoCSV is at an early stage of development, and some features are not yet working. -### Doesn't actually interpret CSV - -I haven't yet found an easy way to parse CSV into EDN client side, so I've written a [separate library](https://github.com/simon-brooke/csv2edn) to do it server side. However, that library is not yet integrated. Currently the client side actually interprets JSON. - ### Missing map pin images At the current stage of development, if no appropriate image exists in the `resources/public/img/map-pins` folder, that's your problem. **TODO:** I intend at some point to make missing pin images default to `unknown-pin.png`, which does exist. -### Doesn't scale and centre the map to show the data in the sheet - -Currently the map is initially centred roughly on the centre of Scotland, and scaled arbitrarily. It should compute an appropriate centre and scale from the data provided, but currently doesn't. - -### There's no way of linking your own data feed - -Currently, the data is taken from the file `resources/public/data/data.json`. What I intend is that you should have a form which allows you to either - -1. enter [the `DOCID` of your own (publicly readable) Google Sheets spreadsheet](https://stackoverflow.com/questions/33713084/download-link-for-google-spreadsheets-csv-export-with-multiple-sheets); -2. enter the URL of a CSV file publicly available on the web; -3. upload a CSV file to the server. - -### There's no way of shareing the map of your own data with other people - -Currently, the data that is shared is just the data that's present when the app is compiled. Ideally, there should be a way of generating a URL, which might take the form: - - https://server.name/geocsv/docid/564747867 - -To show data from the first sheet of the Google Sheets spreadsheet whose `DOCID` is 564747867; or - - https://server.name/geocsv?uri=https://address.of.another.server/path/to/csv-file.csv - -to show the content of a publicly available CSV file. - ## Prerequisites You will need [Leiningen][1] 2.0 or above installed. diff --git a/resources/html/home.html b/resources/html/home.html index 9cb562e..74314ba 100644 --- a/resources/html/home.html +++ b/resources/html/home.html @@ -1,32 +1,31 @@ - - - Welcome to geocsv + + + Welcome to geocsv +
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+

Welcome to geocsv

+

If you're seeing this message, that means you haven't yet compiled your ClojureScript!

+

Please run lein figwheel to start the ClojureScript compiler and reload the page.

+

For better ClojureScript development experience in Chrome follow these steps:

+
    +
  • Open DevTools +
  • Go to Settings ("three dots" icon in the upper right corner of DevTools > Menu > Settings F1 > General > Console) +
  • Check-in "Enable custom formatters" +
  • Close DevTools +
  • Open DevTools +
+

See ClojureScript documentation for further details.

+
-
- +
{% block foot %} @@ -50,14 +49,14 @@ {% style "/css/geocsv.css" %} - {% style "js/lib/node_modules/leaflet/dist/leaflet.css" %} - {% script "/js/lib/node_modules/leaflet/dist/leaflet.js" %} + {% style "/assets/leaflet/dist/leaflet.css" %} + {% script "/assets/leaflet/dist/leaflet.js" %} {% script "/js/app.js" %} diff --git a/resources/public/img/map-pins/unknown-pin.png b/resources/public/img/map-pins/Unknown-pin.png similarity index 100% rename from resources/public/img/map-pins/unknown-pin.png rename to resources/public/img/map-pins/Unknown-pin.png diff --git a/resources/public/img/map-pins/unknown-pin.svg b/resources/public/img/map-pins/Unknown-pin.svg similarity index 100% rename from resources/public/img/map-pins/unknown-pin.svg rename to resources/public/img/map-pins/Unknown-pin.svg diff --git a/resources/public/img/map-pins/unknown-pin.xcf b/resources/public/img/map-pins/unknown-pin.xcf deleted file mode 100644 index b55b9f8..0000000 Binary files a/resources/public/img/map-pins/unknown-pin.xcf and /dev/null differ diff --git a/resources/public/js/lib/node_modules/leaflet/dist/leaflet.css b/resources/public/js/lib/node_modules/leaflet/dist/leaflet.css deleted file mode 100644 index d1b47a1..0000000 --- a/resources/public/js/lib/node_modules/leaflet/dist/leaflet.css +++ /dev/null @@ -1,636 +0,0 @@ -/* required styles */ - -.leaflet-pane, -.leaflet-tile, -.leaflet-marker-icon, -.leaflet-marker-shadow, -.leaflet-tile-container, -.leaflet-pane > svg, -.leaflet-pane > canvas, -.leaflet-zoom-box, -.leaflet-image-layer, -.leaflet-layer { - position: absolute; - left: 0; - top: 0; - } -.leaflet-container { - overflow: hidden; - } -.leaflet-tile, -.leaflet-marker-icon, -.leaflet-marker-shadow { - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; - -webkit-user-drag: none; - } -/* Safari renders non-retina tile on retina better with this, but Chrome is worse */ -.leaflet-safari .leaflet-tile { - image-rendering: -webkit-optimize-contrast; - } -/* hack that prevents hw layers "stretching" when loading new tiles */ -.leaflet-safari .leaflet-tile-container { - width: 1600px; - height: 1600px; - -webkit-transform-origin: 0 0; - } -.leaflet-marker-icon, -.leaflet-marker-shadow { - display: block; - } -/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */ -/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */ -.leaflet-container .leaflet-overlay-pane svg, -.leaflet-container .leaflet-marker-pane img, -.leaflet-container .leaflet-shadow-pane img, -.leaflet-container .leaflet-tile-pane img, -.leaflet-container img.leaflet-image-layer { - max-width: none !important; - max-height: none !important; - } - -.leaflet-container.leaflet-touch-zoom { - -ms-touch-action: pan-x pan-y; - touch-action: pan-x pan-y; - } -.leaflet-container.leaflet-touch-drag { - -ms-touch-action: pinch-zoom; - /* Fallback for FF which doesn't support pinch-zoom */ - touch-action: none; - touch-action: pinch-zoom; -} -.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom { - -ms-touch-action: none; - touch-action: none; -} -.leaflet-container { - -webkit-tap-highlight-color: transparent; -} -.leaflet-container a { - -webkit-tap-highlight-color: rgba(51, 181, 229, 0.4); -} -.leaflet-tile { - filter: inherit; - visibility: hidden; - } -.leaflet-tile-loaded { - visibility: inherit; - } -.leaflet-zoom-box { - width: 0; - height: 0; - -moz-box-sizing: border-box; - box-sizing: border-box; - z-index: 800; - } -/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */ -.leaflet-overlay-pane svg { - -moz-user-select: none; - } - -.leaflet-pane { z-index: 400; } - -.leaflet-tile-pane { z-index: 200; } -.leaflet-overlay-pane { z-index: 400; } -.leaflet-shadow-pane { z-index: 500; } -.leaflet-marker-pane { z-index: 600; } -.leaflet-tooltip-pane { z-index: 650; } -.leaflet-popup-pane { z-index: 700; } - -.leaflet-map-pane canvas { z-index: 100; } -.leaflet-map-pane svg { z-index: 200; } - -.leaflet-vml-shape { - width: 1px; - height: 1px; - } -.lvml { - behavior: url(#default#VML); - display: inline-block; - position: absolute; - } - - -/* control positioning */ - -.leaflet-control { - position: relative; - z-index: 800; - pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */ - pointer-events: auto; - } -.leaflet-top, -.leaflet-bottom { - position: absolute; - z-index: 1000; - pointer-events: none; - } -.leaflet-top { - top: 0; - } -.leaflet-right { - right: 0; - } -.leaflet-bottom { - bottom: 0; - } -.leaflet-left { - left: 0; - } -.leaflet-control { - float: left; - clear: both; - } -.leaflet-right .leaflet-control { - float: right; - } -.leaflet-top .leaflet-control { - margin-top: 10px; - } -.leaflet-bottom .leaflet-control { - margin-bottom: 10px; - } -.leaflet-left .leaflet-control { - margin-left: 10px; - } -.leaflet-right .leaflet-control { - margin-right: 10px; - } - - -/* zoom and fade animations */ - -.leaflet-fade-anim .leaflet-tile { - will-change: opacity; - } -.leaflet-fade-anim .leaflet-popup { - opacity: 0; - -webkit-transition: opacity 0.2s linear; - -moz-transition: opacity 0.2s linear; - -o-transition: opacity 0.2s linear; - transition: opacity 0.2s linear; - } -.leaflet-fade-anim .leaflet-map-pane .leaflet-popup { - opacity: 1; - } -.leaflet-zoom-animated { - -webkit-transform-origin: 0 0; - -ms-transform-origin: 0 0; - transform-origin: 0 0; - } -.leaflet-zoom-anim .leaflet-zoom-animated { - will-change: transform; - } -.leaflet-zoom-anim .leaflet-zoom-animated { - -webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1); - -moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1); - -o-transition: -o-transform 0.25s cubic-bezier(0,0,0.25,1); - transition: transform 0.25s cubic-bezier(0,0,0.25,1); - } -.leaflet-zoom-anim .leaflet-tile, -.leaflet-pan-anim .leaflet-tile { - -webkit-transition: none; - -moz-transition: none; - -o-transition: none; - transition: none; - } - -.leaflet-zoom-anim .leaflet-zoom-hide { - visibility: hidden; - } - - -/* cursors */ - -.leaflet-interactive { - cursor: pointer; - } -.leaflet-grab { - cursor: -webkit-grab; - cursor: -moz-grab; - } -.leaflet-crosshair, -.leaflet-crosshair .leaflet-interactive { - cursor: crosshair; - } -.leaflet-popup-pane, -.leaflet-control { - cursor: auto; - } -.leaflet-dragging .leaflet-grab, -.leaflet-dragging .leaflet-grab .leaflet-interactive, -.leaflet-dragging .leaflet-marker-draggable { - cursor: move; - cursor: -webkit-grabbing; - cursor: -moz-grabbing; - } - -/* marker & overlays interactivity */ -.leaflet-marker-icon, -.leaflet-marker-shadow, -.leaflet-image-layer, -.leaflet-pane > svg path, -.leaflet-tile-container { - pointer-events: none; - } - -.leaflet-marker-icon.leaflet-interactive, -.leaflet-image-layer.leaflet-interactive, -.leaflet-pane > svg path.leaflet-interactive { - pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */ - pointer-events: auto; - } - -/* visual tweaks */ - -.leaflet-container { - background: #ddd; - outline: 0; - } -.leaflet-container a { - color: #0078A8; - } -.leaflet-container a.leaflet-active { - outline: 2px solid orange; - } -.leaflet-zoom-box { - border: 2px dotted #38f; - background: rgba(255,255,255,0.5); - } - - -/* general typography */ -.leaflet-container { - font: 12px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif; - } - - -/* general toolbar styles */ - -.leaflet-bar { - box-shadow: 0 1px 5px rgba(0,0,0,0.65); - border-radius: 4px; - } -.leaflet-bar a, -.leaflet-bar a:hover { - background-color: #fff; - border-bottom: 1px solid #ccc; - width: 26px; - height: 26px; - line-height: 26px; - display: block; - text-align: center; - text-decoration: none; - color: black; - } -.leaflet-bar a, -.leaflet-control-layers-toggle { - background-position: 50% 50%; - background-repeat: no-repeat; - display: block; - } -.leaflet-bar a:hover { - background-color: #f4f4f4; - } -.leaflet-bar a:first-child { - border-top-left-radius: 4px; - border-top-right-radius: 4px; - } -.leaflet-bar a:last-child { - border-bottom-left-radius: 4px; - border-bottom-right-radius: 4px; - border-bottom: none; - } -.leaflet-bar a.leaflet-disabled { - cursor: default; - background-color: #f4f4f4; - color: #bbb; - } - -.leaflet-touch .leaflet-bar a { - width: 30px; - height: 30px; - line-height: 30px; - } -.leaflet-touch .leaflet-bar a:first-child { - border-top-left-radius: 2px; - border-top-right-radius: 2px; - } -.leaflet-touch .leaflet-bar a:last-child { - border-bottom-left-radius: 2px; - border-bottom-right-radius: 2px; - } - -/* zoom control */ - -.leaflet-control-zoom-in, -.leaflet-control-zoom-out { - font: bold 18px 'Lucida Console', Monaco, monospace; - text-indent: 1px; - } - -.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out { - font-size: 22px; - } - - -/* layers control */ - -.leaflet-control-layers { - box-shadow: 0 1px 5px rgba(0,0,0,0.4); - background: #fff; - border-radius: 5px; - } -.leaflet-control-layers-toggle { - background-image: url(images/layers.png); - width: 36px; - height: 36px; - } -.leaflet-retina .leaflet-control-layers-toggle { - background-image: url(images/layers-2x.png); - background-size: 26px 26px; - } -.leaflet-touch .leaflet-control-layers-toggle { - width: 44px; - height: 44px; - } -.leaflet-control-layers .leaflet-control-layers-list, -.leaflet-control-layers-expanded .leaflet-control-layers-toggle { - display: none; - } -.leaflet-control-layers-expanded .leaflet-control-layers-list { - display: block; - position: relative; - } -.leaflet-control-layers-expanded { - padding: 6px 10px 6px 6px; - color: #333; - background: #fff; - } -.leaflet-control-layers-scrollbar { - overflow-y: scroll; - overflow-x: hidden; - padding-right: 5px; - } -.leaflet-control-layers-selector { - margin-top: 2px; - position: relative; - top: 1px; - } -.leaflet-control-layers label { - display: block; - } -.leaflet-control-layers-separator { - height: 0; - border-top: 1px solid #ddd; - margin: 5px -10px 5px -6px; - } - -/* Default icon URLs */ -.leaflet-default-icon-path { - background-image: url(images/marker-icon.png); - } - - -/* attribution and scale controls */ - -.leaflet-container .leaflet-control-attribution { - background: #fff; - background: rgba(255, 255, 255, 0.7); - margin: 0; - } -.leaflet-control-attribution, -.leaflet-control-scale-line { - padding: 0 5px; - color: #333; - } -.leaflet-control-attribution a { - text-decoration: none; - } -.leaflet-control-attribution a:hover { - text-decoration: underline; - } -.leaflet-container .leaflet-control-attribution, -.leaflet-container .leaflet-control-scale { - font-size: 11px; - } -.leaflet-left .leaflet-control-scale { - margin-left: 5px; - } -.leaflet-bottom .leaflet-control-scale { - margin-bottom: 5px; - } -.leaflet-control-scale-line { - border: 2px solid #777; - border-top: none; - line-height: 1.1; - padding: 2px 5px 1px; - font-size: 11px; - white-space: nowrap; - overflow: hidden; - -moz-box-sizing: border-box; - box-sizing: border-box; - - background: #fff; - background: rgba(255, 255, 255, 0.5); - } -.leaflet-control-scale-line:not(:first-child) { - border-top: 2px solid #777; - border-bottom: none; - margin-top: -2px; - } -.leaflet-control-scale-line:not(:first-child):not(:last-child) { - border-bottom: 2px solid #777; - } - -.leaflet-touch .leaflet-control-attribution, -.leaflet-touch .leaflet-control-layers, -.leaflet-touch .leaflet-bar { - box-shadow: none; - } -.leaflet-touch .leaflet-control-layers, -.leaflet-touch .leaflet-bar { - border: 2px solid rgba(0,0,0,0.2); - background-clip: padding-box; - } - - -/* popup */ - -.leaflet-popup { - position: absolute; - text-align: center; - margin-bottom: 20px; - } -.leaflet-popup-content-wrapper { - padding: 1px; - text-align: left; - border-radius: 12px; - } -.leaflet-popup-content { - margin: 13px 19px; - line-height: 1.4; - } -.leaflet-popup-content p { - margin: 18px 0; - } -.leaflet-popup-tip-container { - width: 40px; - height: 20px; - position: absolute; - left: 50%; - margin-left: -20px; - overflow: hidden; - pointer-events: none; - } -.leaflet-popup-tip { - width: 17px; - height: 17px; - padding: 1px; - - margin: -10px auto 0; - - -webkit-transform: rotate(45deg); - -moz-transform: rotate(45deg); - -ms-transform: rotate(45deg); - -o-transform: rotate(45deg); - transform: rotate(45deg); - } -.leaflet-popup-content-wrapper, -.leaflet-popup-tip { - background: white; - color: #333; - box-shadow: 0 3px 14px rgba(0,0,0,0.4); - } -.leaflet-container a.leaflet-popup-close-button { - position: absolute; - top: 0; - right: 0; - padding: 4px 4px 0 0; - border: none; - text-align: center; - width: 18px; - height: 14px; - font: 16px/14px Tahoma, Verdana, sans-serif; - color: #c3c3c3; - text-decoration: none; - font-weight: bold; - background: transparent; - } -.leaflet-container a.leaflet-popup-close-button:hover { - color: #999; - } -.leaflet-popup-scrolled { - overflow: auto; - border-bottom: 1px solid #ddd; - border-top: 1px solid #ddd; - } - -.leaflet-oldie .leaflet-popup-content-wrapper { - zoom: 1; - } -.leaflet-oldie .leaflet-popup-tip { - width: 24px; - margin: 0 auto; - - -ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)"; - filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678); - } -.leaflet-oldie .leaflet-popup-tip-container { - margin-top: -1px; - } - -.leaflet-oldie .leaflet-control-zoom, -.leaflet-oldie .leaflet-control-layers, -.leaflet-oldie .leaflet-popup-content-wrapper, -.leaflet-oldie .leaflet-popup-tip { - border: 1px solid #999; - } - - -/* div icon */ - -.leaflet-div-icon { - background: #fff; - border: 1px solid #666; - } - - -/* Tooltip */ -/* Base styles for the element that has a tooltip */ -.leaflet-tooltip { - position: absolute; - padding: 6px; - background-color: #fff; - border: 1px solid #fff; - border-radius: 3px; - color: #222; - white-space: nowrap; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - pointer-events: none; - box-shadow: 0 1px 3px rgba(0,0,0,0.4); - } -.leaflet-tooltip.leaflet-clickable { - cursor: pointer; - pointer-events: auto; - } -.leaflet-tooltip-top:before, -.leaflet-tooltip-bottom:before, -.leaflet-tooltip-left:before, -.leaflet-tooltip-right:before { - position: absolute; - pointer-events: none; - border: 6px solid transparent; - background: transparent; - content: ""; - } - -/* Directions */ - -.leaflet-tooltip-bottom { - margin-top: 6px; -} -.leaflet-tooltip-top { - margin-top: -6px; -} -.leaflet-tooltip-bottom:before, -.leaflet-tooltip-top:before { - left: 50%; - margin-left: -6px; - } -.leaflet-tooltip-top:before { - bottom: 0; - margin-bottom: -12px; - border-top-color: #fff; - } -.leaflet-tooltip-bottom:before { - top: 0; - margin-top: -12px; - margin-left: -6px; - border-bottom-color: #fff; - } -.leaflet-tooltip-left { - margin-left: -6px; -} -.leaflet-tooltip-right { - margin-left: 6px; -} -.leaflet-tooltip-left:before, -.leaflet-tooltip-right:before { - top: 50%; - margin-top: -6px; - } -.leaflet-tooltip-left:before { - right: 0; - margin-right: -12px; - border-left-color: #fff; - } -.leaflet-tooltip-right:before { - left: 0; - margin-left: -12px; - border-right-color: #fff; - } diff --git a/resources/public/js/lib/package-lock.json b/resources/public/js/lib/package-lock.json new file mode 100644 index 0000000..48e341a --- /dev/null +++ b/resources/public/js/lib/package-lock.json @@ -0,0 +1,3 @@ +{ + "lockfileVersion": 1 +} diff --git a/resources/public/js/package-lock.json b/resources/public/js/package-lock.json new file mode 100644 index 0000000..48e341a --- /dev/null +++ b/resources/public/js/package-lock.json @@ -0,0 +1,3 @@ +{ + "lockfileVersion": 1 +} diff --git a/src/clj/geocsv/handler serves resources.clj b/src/clj/geocsv/handler serves resources.clj new file mode 100644 index 0000000..e86d51b --- /dev/null +++ b/src/clj/geocsv/handler serves resources.clj @@ -0,0 +1,40 @@ +(ns geocsv.handler + (:require [compojure.core :refer [routes wrap-routes]] + [geocsv.env :refer [defaults]] + [geocsv.middleware :as middleware] + [geocsv.layout :refer [error-page]] + [geocsv.routes.home :refer [home-routes]] + [geocsv.routes.json :refer [json-routes]] + [reitit.ring :as ring] + [ring.middleware.content-type :refer [wrap-content-type]] + [ring.middleware.webjars :refer [wrap-webjars]] + [mount.core :as mount])) + +(mount/defstate init-app + :start ((or (:init defaults) (fn []))) + :stop ((or (:stop defaults) (fn [])))) + +(mount/defstate app-routes + :start + (ring/ring-handler + (ring/router + [(home-routes) +;; (-> #'json-routes +;; (wrap-routes middleware/wrap-csrf) +;; (wrap-routes middleware/wrap-formats)) + ]) + (ring/routes + (ring/create-resource-handler + {:path "/"}) + (wrap-content-type + (wrap-webjars (constantly nil))) + (ring/create-default-handler + {:not-found + (constantly (error-page {:status 404, :title "404 - Page not found"})) + :method-not-allowed + (constantly (error-page {:status 405, :title "405 - Not allowed"})) + :not-acceptable + (constantly (error-page {:status 406, :title "406 - Not acceptable"}))})))) + +(defn app [] + (middleware/wrap-base #'app-routes)) diff --git a/src/clj/geocsv/handler.clj b/src/clj/geocsv/handler.clj index 001fbf6..8c0e65a 100644 --- a/src/clj/geocsv/handler.clj +++ b/src/clj/geocsv/handler.clj @@ -22,11 +22,14 @@ (routes (-> #'home-routes (wrap-routes middleware/wrap-csrf) - (wrap-routes middleware/wrap-formats)) + (wrap-routes middleware/wrap-formats) + wrap-webjars) (-> #'rest-routes (wrap-routes middleware/wrap-csrf) (wrap-routes middleware/wrap-formats)) - (route/resources "/") +;; (ring/create-resource-handler +;; {:path "/"}) + (route/resources "/") (route/not-found (:body (error-page {:status 404 diff --git a/src/clj/geocsv/routes/rest.clj b/src/clj/geocsv/routes/rest.clj index 26d0446..165dc15 100644 --- a/src/clj/geocsv/routes/rest.clj +++ b/src/clj/geocsv/routes/rest.clj @@ -6,6 +6,7 @@ [clojure.java.io :as io] [clojure.string :as s] [clojure.tools.logging :as log] + [cpath-clj.core :as cp] [compojure.core :refer [defroutes GET POST]] [csv2edn.csv2edn :refer :all] [noir.response :as nresponse] @@ -37,19 +38,18 @@ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defn get-pin-image-names + "Return the category names for which we have pin images; `request` is ignored. + + This looks odd - why not file-seq over the directory? - but the answer is we + may be running in a jar file, and if we are that will fail." [request] (ar/do-or-server-fail (map - #(s/replace (.getName %) #"-pin\.png$" "") - (let [grammar-matcher (.getPathMatcher - (java.nio.file.FileSystems/getDefault) - "glob:*-pin.png")] + #(s/replace (s/replace (str %) #"-pin\.png$" "") "/" "") (->> "public/img/map-pins" - io/resource - io/file - file-seq - (filter #(.isFile %)) - (filter #(.matches grammar-matcher (.getFileName (.toPath %))))))) + cp/resources + keys + (filter #(re-find #".*-pin.png" %)))) 200)) (defn get-data-uri @@ -72,7 +72,7 @@ "Return JSON formatted data taken from the CSV file with the name `filename` in the directory `resources/public/data`." [filename] - (-> (str "public/data/" filename) io/resource io/file io/reader csv->json)) + (-> (str "public/data/" filename) io/resource io/reader csv->json)) (defn get-data "Return JSON formatted data from the source implied by this `request`." diff --git a/src/cljs/geocsv/ajax.cljs b/src/cljs/geocsv/client/ajax.cljs similarity index 83% rename from src/cljs/geocsv/ajax.cljs rename to src/cljs/geocsv/client/ajax.cljs index 4605fd6..c1adc58 100644 --- a/src/cljs/geocsv/ajax.cljs +++ b/src/cljs/geocsv/client/ajax.cljs @@ -1,4 +1,4 @@ -(ns geocsv.ajax +(ns geocsv.client.ajax (:require [ajax.core :as ajax] [luminus-transit.time :as time] @@ -6,11 +6,9 @@ [re-frame.core :as rf])) (defn local-uri? [{:keys [uri]}] - (js/console.log (str "local-uri?: received `" (str uri) "` (type " (type uri) ") as uri")) (not (re-find #"^\w+?://" (str uri)))) (defn default-headers [request] - (js/console.log (str "default-headers: received `" request "` as request")) (if (local-uri? request) (-> request (update :headers #(merge {"x-csrf-token" js/csrfToken} %))) diff --git a/src/cljs/geocsv/core.cljs b/src/cljs/geocsv/client/core.cljs similarity index 72% rename from src/cljs/geocsv/core.cljs rename to src/cljs/geocsv/client/core.cljs index 26bed9c..b67025d 100644 --- a/src/cljs/geocsv/core.cljs +++ b/src/cljs/geocsv/client/core.cljs @@ -1,17 +1,18 @@ -(ns geocsv.core +(ns geocsv.client.core (:require [day8.re-frame.http-fx] [reagent.core :as r] [re-frame.core :as rf] - [geocsv.views.map :as mv] + [geocsv.client.gis :as gis] + [geocsv.client.views.map :as mv] [goog.events :as events] [goog.history.EventType :as HistoryEventType] [markdown.core :refer [md->html]] - [geocsv.ajax :as ajax] - [geocsv.events] + [geocsv.client.ajax :as ajax] + [geocsv.client.events] [reitit.core :as reitit] [reitit.frontend.easy :as rfe] - [clojure.string :as string]) + [clojure.string :as s]) (:import goog.History)) (defn nav-link [uri title page] @@ -39,7 +40,31 @@ (defn about-page [] [:section.section>div.container>div.content - [:img {:src "/img/warning_clojure.png"}]]) + [:img {:src "/img/warning_clojure.png"}] + (when-let [images @(rf/subscribe [:available-pin-images])] + [:div + [:h2 "The following pin images are available on this server"] + (apply + vector + (cons + :ol + (map + #(vector + :ol + [:img + {:src + (str + "img/map-pins/" + (s/capitalize + (s/replace + (s/lower-case + (str %)) + #"[^a-z0-9]" "-")) + "-pin.png") + :alt %}] + " " + %) + (sort images))))])]) (defn home-page [] [:section.section>div.container>div.content @@ -90,6 +115,7 @@ (defn init! [] (rf/dispatch-sync [:initialise-db]) + (rf/dispatch [:fetch-pin-image-names]) (start-router!) (ajax/load-interceptors!) (mount-components)) diff --git a/src/cljs/geocsv/db.cljs b/src/cljs/geocsv/client/db.cljs similarity index 71% rename from src/cljs/geocsv/db.cljs rename to src/cljs/geocsv/client/db.cljs index fa530d9..b6cb488 100644 --- a/src/cljs/geocsv/db.cljs +++ b/src/cljs/geocsv/client/db.cljs @@ -1,6 +1,6 @@ (ns ^{:doc "geocsv app initial database." :author "Simon Brooke"} - geocsv.db) + geocsv.client.db) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;; @@ -28,6 +28,14 @@ (def default-db {:page :home - :available-pin-images #{"Planning authority" "unknown"} ;; need to be fetched from server side - :map {:map-centre [56 -4] - :map-zoom 6}}) + :available-pin-images #{"Planning-authority" + "Landowner" + "Unknown" + "Anchor-customer" + "Investor" + "Broadband-supplier" + "Operator" + "Other-key-customers" + "Power-supplier"} ;; need to be fetched from server side + :latitude 56 + :longitude -4}) diff --git a/src/cljs/geocsv/events.cljs b/src/cljs/geocsv/client/events.cljs similarity index 76% rename from src/cljs/geocsv/events.cljs rename to src/cljs/geocsv/client/events.cljs index f461a71..5b6da4a 100644 --- a/src/cljs/geocsv/events.cljs +++ b/src/cljs/geocsv/client/events.cljs @@ -1,9 +1,9 @@ -(ns geocsv.events +(ns geocsv.client.events (:require [ajax.core :as ajax] [ajax.json :refer [json-request-format json-response-format]] [cemerick.url :refer [url url-encode]] - [geocsv.db :refer [default-db]] - [geocsv.gis :refer [refresh-map-pins]] + [geocsv.client.db :refer [default-db]] + [geocsv.client.gis :refer [compute-centre refresh-map-pins]] [re-frame.core :as rf] [reitit.frontend.easy :as rfe] [reitit.frontend.controllers :as rfc])) @@ -40,7 +40,6 @@ :query nil :anchor nil)) - ;;dispatchers: keep in alphabetical order, please. (rf/reg-event-fx :bad-data @@ -73,6 +72,24 @@ :on-failure [:bad-data]} :db db}))) +(rf/reg-event-fx + :fetch-pin-image-names + (fn [{db :db} _] + (let [uri (assoc source-host + :path "/get-pin-image-names")] + (js/console.log + (str + "Fetching data: " uri)) + ;; we return a map of (side) effects + {:http-xhrio {:method :get + :uri uri + :format (json-request-format) + :response-format (json-response-format {:keywords? true}) + :on-success [:process-pin-image-names] + ;; ignore :on-failure for now + } + :db db}))) + (rf/reg-event-fx :fetch-docs (fn [_ _] @@ -120,12 +137,26 @@ ;; TODO: why is this an `-fx`? Does it need to be? (fn [{db :db} [_ response]] - (let [data (js->clj response)] - (js/console.log (str "processing fetched JSON data")) + (let [db' (assoc db :data (js->clj response))] + (js/console.log (str "processing fetched JSON data")) + {:db (if-let [data (:data db')] + (let [centre (compute-centre data)] + (if + (:view db') + (refresh-map-pins (merge db' centre)) + db) + db))}))) + +(rf/reg-event-fx + :process-pin-image-names + (fn + [{db :db} [_ response]] + (let [db' (assoc db :available-pin-images (set response))] + (js/console.log (str "processing pin images")) {:db (if - (:view db) - (refresh-map-pins (assoc db :data data)) - db)}))) + (:view db') + (refresh-map-pins db') + db')}))) (rf/reg-event-db :set-docs @@ -167,11 +198,6 @@ (js/console.log (str "Fetching longitude" v)) v))) -(rf/reg-sub - :map - (fn [db _] - (:map db))) - (rf/reg-sub :route (fn [db _] diff --git a/src/cljs/geocsv/gis.cljs b/src/cljs/geocsv/client/gis.cljs similarity index 64% rename from src/cljs/geocsv/gis.cljs rename to src/cljs/geocsv/client/gis.cljs index 4e23769..298fbfa 100644 --- a/src/cljs/geocsv/gis.cljs +++ b/src/cljs/geocsv/client/gis.cljs @@ -1,6 +1,6 @@ (ns ^{:doc "geocsv app map stuff." :author "Simon Brooke"} - geocsv.gis + geocsv.client.gis (:require [ajax.core :refer [GET]] [ajax.json :refer [json-request-format json-response-format]] [cljs.reader :refer [read-string]] @@ -71,48 +71,56 @@ (defn pin-image "Return the name of a suitable pin image for this `record`." - [record] - (let [available @(subscribe [:available-pin-images])] - (js/console.log (str "pin-image: available is of type `" (type available) "`; `(fn? available)` returns " (set? available))) + [db record] + (let [available (:available-pin-images db) + category (s/capitalize + (s/replace + (s/lower-case + (str (:category record))) + #"[^a-z0-9]" "-"))] (if - (contains? available (:category record)) - (str - (s/capitalize - (s/replace (s/lower-case (str (:category record))) #"[^a-z0-9]" "-")) "-pin") - "unknown-pin"))) + (available category) + (str category "-pin") + "Unknown-pin"))) (defn popup-content "Appropriate content for the popup of a map pin for this `record`." [record] - (str - "
" - (:name record) - "
" - (apply - str - (map #(str "
" (name %) "
" (record %) "
") (keys record))) - "
")) + (if + (map? record) ;; which it should be! + (str + "
" + (:name record) + "
" + (apply + str + (map + #(str "
" (name %) "
" (record %) "
") + (filter #(record %) (keys record)))) + "
"))) (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] - (str - "
" - (:name record) - "
" - (apply - str - (map - #(str "") - (sort (keys record)))) - "
" (name %) "" (record %) "
")) + (if + (map? record) ;; which it should be! + (str + "
" + (:name record) + "
" + (apply + str + (map + #(str "") + (sort (filter #(record %) (keys record))))) + "
" (name %) "" (record %) "
"))) (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] + [db record index view] (let [lat (:latitude record) lng (:longitude record)] (if @@ -125,7 +133,7 @@ (clj->js {:iconAnchor [16 41] :iconSize [32 42] - :iconUrl (str "img/map-pins/" (pin-image record) ".png") + :iconUrl (str "img/map-pins/" (pin-image db record) ".png") :riseOnHover true :shadowAnchor [16 23] :shadowSize [57 24] @@ -150,17 +158,52 @@ (.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." [db] (let [view (map-remove-pins @(subscribe [:view])) - data (:data db)] + data (:data db) + centre (compute-centre data)] (if view - (let [added (remove nil? (map #(add-map-pin %1 %2 view) data (range)))] - (js/console.log (str "Adding " (count added) " pins"))) - (js/console.log "View is not yet ready")) - db)) - + (let [added (remove nil? (map #(add-map-pin db %1 %2 view) data (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)) + (merge db centre)) + db)) + (do (js/console.log "View is not yet ready") db)))) diff --git a/src/cljs/geocsv/views/map.cljs b/src/cljs/geocsv/client/views/map.cljs similarity index 84% rename from src/cljs/geocsv/views/map.cljs rename to src/cljs/geocsv/client/views/map.cljs index 8d9d3b5..27b5945 100644 --- a/src/cljs/geocsv/views/map.cljs +++ b/src/cljs/geocsv/client/views/map.cljs @@ -1,11 +1,11 @@ (ns ^{:doc "a map onto which to project CSV data." :author "Simon Brooke"} - geocsv.views.map + geocsv.client.views.map (:require [cljsjs.leaflet] [re-frame.core :refer [reg-sub subscribe dispatch dispatch-sync]] [reagent.core :as reagent] [recalcitrant.core :refer [error-boundary]] - [geocsv.gis :refer [refresh-map-pins get-current-location]])) + [geocsv.client.gis :refer [refresh-map-pins get-current-location]])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;; @@ -61,22 +61,14 @@ (let [view (.setView (.map js/L "map" - ;; (clj->js {:zoomControl false}) - ) - #js [56 -4] ;;[@(subscribe [:latitude]) @(subscribe [:longitude])] + (clj->js {:zoomControl false})) + #js [@(subscribe [:latitude]) @(subscribe [:longitude])] @(subscribe [:zoom]))] (.addTo (.tileLayer js/L osm-url (clj->js {:attribution osm-attrib :maxZoom 18})) view) (dispatch-sync [:set-view view]) -;; (.on view "moveend" -;; (fn [_] (let [c (.getCenter view)] -;; (js/console.log (str "Moving centre to " c)) -;; (dispatch-sync [:set-latitude (.-lat c)]) -;; (dispatch-sync [:set-longitude (.-lng c)]) -;; (dispatch [:fetch-data])))) -;; (refresh-map-pins) view)) (defn map-did-mount @@ -91,7 +83,7 @@ (defn map-render "Render the actual div containing the map." [] - [:div#map {:style {:height "1000px"}}]) + [:div#map {:style {:height "800px"}}]) (defn panel "A reagent class for the map object."