Skip to content

Commit 3429fd1

Browse files
authored
Add Leaflet demo for GitHub Pages (#15)
* got slider running * missing files * layer control * grouped layers * plotting * fix date format in toolip * rename to docs for gh pages * add title etc * rename again and display geojson * make note about the docs folder
1 parent b739849 commit 3429fd1

9 files changed

Lines changed: 726 additions & 0 deletions

File tree

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,9 @@ A typical workflow is:
5858
- Use our login end point to exchange your credentials for an access token
5959
- Pass the access token in the `Authorization` header of your http requests to
6060
our APIs' endpoints
61+
62+
63+
## The docs folder
64+
65+
This contains a simple demo of accessing the API's to create a map using the [Leaflet](https://leafletjs.com/) library.
66+
This is served up as a github page at [https://cibolabs.github.io/api-docs](https://cibolabs.github.io/api-docs).

docs/README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Leaflet Demo
2+
3+
## Obtaining a token
4+
5+
A token needs to be obtained as described in the [Quickstart](../quickstart.md). The website
6+
requests this token when opened.
7+
8+
A `property_id` is also needed but this is set to a default value for testing.
9+
10+
## To run locally
11+
12+
Open a terminal in this directory and run the following command:
13+
14+
```
15+
python3 -m http.server
16+
```
17+
18+
Then in a browser, navigate to [http://localhost:8000/](http://localhost:8000/). Changes to your code
19+
are reloaded when you refresh.
20+
21+
## Github Pages
22+
23+
These pages are automatically serverd by github pages.

docs/index.html

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<!DOCTYPE html>
2+
<html lang="en-GB">
3+
<head>
4+
<!-- Leaflet -->
5+
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
6+
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
7+
crossorigin=""/>
8+
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
9+
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
10+
crossorigin=""></script>
11+
12+
<!-- From https://github.com/fifogipo/leaflet-custom-headers -->
13+
<script src="static/js/leaflet-custom-headers/main.js"></script>
14+
15+
<!-- From https://github.com/Eclipse1979/leaflet-slider -->
16+
<link rel="stylesheet" href="/static/js/leaflet-slider/leaflet-slider.css" />
17+
<script src="static/js/leaflet-slider/leaflet-slider.js"></script>
18+
19+
<!-- From https://github.com/ismyrnow/leaflet-groupedlayercontrol -->
20+
<link rel="stylesheet" href="/static/js/leaflet-groupedlayercontrol/leaflet.groupedlayercontrol.min.css" />
21+
<script src="static/js/leaflet-groupedlayercontrol/leaflet.groupedlayercontrol.min.js"></script>
22+
23+
<!-- JSChart -->
24+
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
25+
<script src="https://cdn.jsdelivr.net/npm/chart.js/dist/chart.min.js"></script>
26+
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
27+
28+
<!-- our javascript (last) -->
29+
<script src="static/js/index.js"></script>
30+
</head>
31+
<body onload="init();">
32+
<dialog id="myDialog">
33+
<h2>Enter your Access Token</h2>
34+
<form method="dialog">
35+
<label for="token">Token:</label>
36+
<input type="text" id="token" name="token" required>
37+
<label for="property_id">Property Id:</label>
38+
<input type="text" id="property_id" name="property_id" value="434b1601-200d-428c-9c6d-1446b05ecc3b" required>
39+
<button type="submit">Submit</button>
40+
</form>
41+
</dialog>
42+
<div id="map" style="height: 60vh;width: 100vw"></div>
43+
<h3 id="plottitle" hidden>Paddocks' Total Standing Dry Matter</h3>
44+
<canvas id="plot" style="height: 35vh;width: 100vw"></canvas>
45+
</body>
46+
</html>

docs/static/js/index.js

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
"use strict";
2+
3+
// globals
4+
let g_tsdmlayer = null;
5+
let g_nbarlayer = null;
6+
const g_tileurl = 'https://tiles.pasturekey.cibolabs.com';
7+
const g_pkeyurl = 'https://data.pasturekey.cibolabs.com';
8+
let g_chart = null;
9+
10+
function init()
11+
{
12+
const myDialog = document.getElementById('myDialog');
13+
myDialog.addEventListener('close', () => {
14+
const form = myDialog.querySelector('form');
15+
const token = form.querySelector('#token');
16+
const property_id = form.querySelector('#property_id');
17+
console.log('User entered:', token.value, property_id.value);
18+
19+
// create map and zoom in on farm
20+
// TODO: can we get extents programmatically?
21+
let map = L.map('map').setView([-31.349912533186938, 150.13404649761717], 14);
22+
23+
// background layer
24+
const OSMLayer = L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
25+
maxZoom: 19,
26+
attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
27+
});
28+
OSMLayer.addTo(map);
29+
30+
31+
// get dates for this property_id
32+
fetch(g_tileurl + '/getimagedates/' + property_id.value, {
33+
headers: {
34+
'Authorization': 'Bearer ' + token.value,
35+
'Accept': 'application/json'
36+
},
37+
method: "GET"})
38+
.then(response => response.json())
39+
.then(function(data) {
40+
const last_date = data.dates[data.dates.length - 1];
41+
g_tsdmlayer = new L.TileLayerHeaders(g_tileurl + '/tsdm/' + last_date + '/' + property_id.value + '/{z}/{x}/{y}', {
42+
attribution: '&copy; <a href="https://www.cibolabs.com.au/">CiboLabs</a>',
43+
customHeaders: {
44+
'Authorization': 'Bearer ' + token.value,
45+
'Accept': 'image/png'
46+
}
47+
});
48+
g_nbarlayer = new L.TileLayerHeaders(g_tileurl + '/nbar/' + last_date + '/' + property_id.value + '/{z}/{x}/{y}', {
49+
attribution: '&copy; <a href="https://www.cibolabs.com.au/">CiboLabs</a>',
50+
customHeaders: {
51+
'Authorization': 'Bearer ' + token.value,
52+
'Accept': 'image/png'
53+
}
54+
});
55+
g_nbarlayer.addTo(map); // displayed by default
56+
57+
// and empty geojson layer to be populated later
58+
let geojsonLayer = L.geoJSON().addTo(map);
59+
60+
// start a fetch do the property boundaries can be displayed when ready
61+
fetch(g_pkeyurl + '/geom/' + property_id.value, {
62+
headers: {
63+
'Authorization': 'Bearer ' + token.value,
64+
'Accept': 'application/json'
65+
},
66+
method: "POST"})
67+
.then(response => response.json())
68+
.then(function(data) {
69+
geojsonLayer.addData(data);
70+
});
71+
72+
// dates - make a slider
73+
let slider = L.control.slider(function(value) {
74+
newLabel(data.dates[value], map);
75+
}, {'position': 'bottomright', 'max': data.dates.length - 1,
76+
'value': data.dates.length - 1,
77+
'orientation': 'horizontal', 'size': '1000px', 'collapsed': false,
78+
'getValue': function(value){return data.dates[value]}});
79+
slider.addTo(map);
80+
81+
// layer control
82+
let groupedOverlay = {
83+
"CiboLabs": {
84+
"TSDM": g_tsdmlayer,
85+
"NBAR": g_nbarlayer
86+
},
87+
"Other": {
88+
"Property Boundaries": geojsonLayer
89+
}
90+
};
91+
let basemap = {"OpenStreetMap": OSMLayer};
92+
let options = {
93+
exclusiveGroups: ["CiboLabs"]
94+
}
95+
let layerControl = L.control.groupedLayers(basemap, groupedOverlay, options).addTo(map);
96+
97+
// do plot when the result of this fetch comes in
98+
fetch(g_pkeyurl + '/gettsdmstats/' + property_id.value, {
99+
headers: {
100+
'Authorization': 'Bearer ' + token.value,
101+
'Accept': 'application/json'
102+
},
103+
method: "POST"})
104+
.then(response => response.json())
105+
.then(function(data) {
106+
107+
// pull out the data for every paddock
108+
let paddock_data = [];
109+
let labels = null;
110+
for(const paddock of data.paddocks)
111+
{
112+
const paddock_obj = {
113+
'label': paddock.paddock_name,
114+
'data': paddock.stats[0].median
115+
};
116+
paddock_data.push(paddock_obj);
117+
118+
if(!labels)
119+
{
120+
labels = [];
121+
for(const datestr of paddock.stats[0].dates)
122+
{
123+
const year = datestr.substring(0,4);
124+
const month = datestr.substring(4,6);
125+
const day = datestr.substring(6,8);
126+
labels.push(new Date(year, month-1, day));
127+
}
128+
}
129+
}
130+
131+
if(g_chart)
132+
{
133+
g_chart.destroy();
134+
}
135+
136+
// display this now there is data
137+
document.getElementById('plottitle').removeAttribute("hidden");
138+
139+
const ctx = document.getElementById('plot');
140+
g_chart = new Chart(ctx, {
141+
'type': 'line',
142+
'data': {
143+
'labels': labels,
144+
'datasets': paddock_data
145+
},
146+
'options': {
147+
'plugins': {
148+
'legend': {
149+
'display': false // This hides the legend
150+
}
151+
},
152+
'scales': {
153+
'x': {
154+
'type': 'time',
155+
'time': {
156+
'tooltipFormat':'dd/MM/yyyy',
157+
'displayFormats': {
158+
'unit': 'day',
159+
'day': 'd MMM yyyy'
160+
}
161+
}
162+
}
163+
}
164+
}
165+
});
166+
167+
});
168+
});
169+
});
170+
myDialog.showModal();
171+
}
172+
173+
function newLabel(label, map)
174+
{
175+
const form = myDialog.querySelector('form');
176+
const property_id = form.querySelector('#property_id');
177+
if( g_tsdmlayer )
178+
{
179+
g_tsdmlayer.setUrl(g_tileurl + '/tsdm/' + label + '/' + property_id.value + '/{z}/{x}/{y}');
180+
}
181+
if( g_nbarlayer )
182+
{
183+
g_nbarlayer.setUrl(g_tileurl + '/nbar/' + label + '/' + property_id.value + '/{z}/{x}/{y}');
184+
}
185+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
(function (global, factory) {
2+
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('leaflet')) :
3+
typeof define === 'function' && define.amd ? define(['leaflet'], factory) :
4+
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, (global.L = global.L || {}, global.L.TileLayerHeaders = factory(global.L)));
5+
})(this, (function (L) { 'use strict';
6+
7+
function _interopNamespaceDefault(e) {
8+
var n = Object.create(null);
9+
if (e) {
10+
Object.keys(e).forEach(function (k) {
11+
if (k !== 'default') {
12+
var d = Object.getOwnPropertyDescriptor(e, k);
13+
Object.defineProperty(n, k, d.get ? d : {
14+
enumerable: true,
15+
get: function () { return e[k]; }
16+
});
17+
}
18+
});
19+
}
20+
n.default = e;
21+
return Object.freeze(n);
22+
}
23+
24+
var L__namespace = /*#__PURE__*/_interopNamespaceDefault(L);
25+
26+
/**
27+
* TileLayerHeaders extends Leaflet's TileLayer to allow custom headers in tile requests.
28+
*/
29+
class TileLayerHeaders extends L__namespace.TileLayer {
30+
/**
31+
* Creates an instance of TileLayerHeaders.
32+
* @param urlTemplate - The URL template for fetching tiles.
33+
* @param options - Tile layer options including custom headers.
34+
*/
35+
constructor(urlTemplate, options) {
36+
super(urlTemplate, options);
37+
}
38+
/**
39+
* Creates a tile element and fetches the tile image with custom headers.
40+
* @param coords - Tile coordinates.
41+
* @param done - Callback function to signal completion.
42+
* @returns The created HTMLImageElement.
43+
*/
44+
createTile(coords, done) {
45+
const tile = document.createElement('img');
46+
L.DomEvent.on(tile, 'load', this._tileOnLoad.bind(this, done, tile));
47+
L.DomEvent.on(tile, 'error', this._tileOnError.bind(this, done, tile, new Error()));
48+
if (this.options.crossOrigin || this.options.crossOrigin === '') {
49+
tile.crossOrigin = this.options.crossOrigin === true ? '' : this.options.crossOrigin;
50+
}
51+
if (typeof this.options.referrerPolicy === 'string') {
52+
tile.referrerPolicy = this.options.referrerPolicy;
53+
}
54+
tile.alt = '';
55+
const tileUrl = this.getTileUrl(coords);
56+
fetch(tileUrl, {
57+
headers: this.options.customHeaders,
58+
})
59+
.then(response => response.blob())
60+
.then(blob => {
61+
const objectUrl = URL.createObjectURL(blob);
62+
tile.src = objectUrl;
63+
tile.onload = () => URL.revokeObjectURL(objectUrl);
64+
tile.onerror = () => URL.revokeObjectURL(objectUrl);
65+
done(undefined, tile);
66+
})
67+
.catch(error => done(error, undefined));
68+
return tile;
69+
}
70+
}
71+
72+
return TileLayerHeaders;
73+
74+
}));

docs/static/js/leaflet-groupedlayercontrol/leaflet.groupedlayercontrol.min.css

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/static/js/leaflet-groupedlayercontrol/leaflet.groupedlayercontrol.min.js

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)