Merge tag 'geocsv-0.1.1'

This commit is contained in:
Simon Brooke 2020-01-29 20:46:24 +00:00
commit 576db9e88f
No known key found for this signature in database
GPG key ID: A7A4F18D1D4DF987
22 changed files with 329 additions and 805 deletions

View file

@ -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.

View file

@ -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]))

View file

@ -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 [& _]))

13
package-lock.json generated
View file

@ -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=="
}
}
}

View file

@ -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"]])

View file

@ -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.

View file

@ -6,27 +6,26 @@
<title>Welcome to geocsv</title>
</head>
<body>
<div id="app">
<div class="splash-screen">
<div class="sk-fading-circle">
<div class="sk-circle1 sk-circle"></div>
<div class="sk-circle2 sk-circle"></div>
<div class="sk-circle3 sk-circle"></div>
<div class="sk-circle4 sk-circle"></div>
<div class="sk-circle5 sk-circle"></div>
<div class="sk-circle6 sk-circle"></div>
<div class="sk-circle7 sk-circle"></div>
<div class="sk-circle8 sk-circle"></div>
<div class="sk-circle9 sk-circle"></div>
<div class="sk-circle10 sk-circle"></div>
<div class="sk-circle11 sk-circle"></div>
<div class="sk-circle12 sk-circle"></div>
<section class="section">
<div class="container is-fluid">
<div class="content">
<h4 class="title">Welcome to geocsv</h4>
<p>If you're seeing this message, that means you haven't yet compiled your ClojureScript!</p>
<p>Please run <code>lein figwheel</code> to start the ClojureScript compiler and reload the page.</p>
<h4>For better ClojureScript development experience in Chrome follow these steps:</h4>
<ul>
<li>Open DevTools
<li>Go to Settings ("three dots" icon in the upper right corner of DevTools > Menu > Settings F1 > General > Console)
<li>Check-in "Enable custom formatters"
<li>Close DevTools
<li>Open DevTools
</ul>
<p>See <a href="http://www.luminusweb.net/docs/clojurescript.md">ClojureScript</a> documentation for further details.</p>
</div>
</div>
<p class="footer">
<b>geocsv</b> is loading.
You must enable JavaScript to use <b>geocsv</b>.
</p>
</section>
</div>
{% block foot %}
@ -56,8 +55,8 @@
<!-- ATTENTION \/ -->
<!-- ATTENTION /\ -->
<!-- Leaflet -->
{% 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" %}
</body>
</html>

View file

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

View file

@ -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;
}

View file

@ -0,0 +1,3 @@
{
"lockfileVersion": 1
}

3
resources/public/js/package-lock.json generated Normal file
View file

@ -0,0 +1,3 @@
{
"lockfileVersion": 1
}

View file

@ -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))

View file

@ -22,10 +22,13 @@
(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))
;; (ring/create-resource-handler
;; {:path "/"})
(route/resources "/")
(route/not-found
(:body

View file

@ -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`."

View file

@ -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} %)))

View file

@ -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))

View file

@ -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})

View file

@ -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)]
(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 _]

View file

@ -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,33 +71,41 @@
(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]
(if
(map? record) ;; which it should be!
(str
"<h5>"
(:name record)
"</h5><dl>"
(apply
str
(map #(str "<dt>" (name %) "</dt><dd>" (record %) "</dd>") (keys record)))
"</dl>"))
(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)
@ -106,13 +114,13 @@
str
(map
#(str "<tr><th>" (name %) "</th><td>" (record %) "</td></tr>")
(sort (keys record))))
"</table>"))
(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]
[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"))
(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))))

View file

@ -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."