Working on Firefox, including URL fetch.
URL data not working on Chrome or Safari. Bother.
This commit is contained in:
parent
47ca5b17ba
commit
a081358564
|
@ -2,6 +2,14 @@
|
||||||
|
|
||||||
An even more ultra-lightweight tool to show comma-separated value data on a map.
|
An even more ultra-lightweight tool to show comma-separated value data on a map.
|
||||||
|
|
||||||
|
## Other variants
|
||||||
|
|
||||||
|
This is a little project I've played about with, and there are now three variants:
|
||||||
|
|
||||||
|
1. [geocsv](https://github.com/simon-brooke/geocsv) is a fairly heavyweight web-app with both client-side and serverside components. It was the first version, and is the only version which meets the original requirement of being able to present data from [Google Sheets](https://www.google.co.uk/sheets/about/), but it's a remarkably heavyweight solution to what should be a simple problem.
|
||||||
|
2. [geocsv-lite](https://github.com/simon-brooke/geocsv-lite) is a much lighter, client-side only reworking of the problem, in ClojureScript. I still wasn't satisfied that this was light enough.
|
||||||
|
3. [geocsv-js](https://github.com/simon-brooke/geocsv-js) is a reworking in native JavaScript without any frameworks or heave libraries, except Leaflet. It is vastly lighter, and probably the one to use in most applications.
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
This is a third iteration of GeoCSV. The [original](https://github.com/simon-brooke/geocsv) was written quickly in Clojure and ClojureScript, with CSV parsing done server side and React (via [re-frame](https://github.com/day8/re-frame)) driving the client side. That's my comfort zone; but it had the benefit that my customer wanted to pull data from Google Sheets, which you can't do from client side (or at least I don't know how to) because of cross-site scripting protections.
|
This is a third iteration of GeoCSV. The [original](https://github.com/simon-brooke/geocsv) was written quickly in Clojure and ClojureScript, with CSV parsing done server side and React (via [re-frame](https://github.com/day8/re-frame)) driving the client side. That's my comfort zone; but it had the benefit that my customer wanted to pull data from Google Sheets, which you can't do from client side (or at least I don't know how to) because of cross-site scripting protections.
|
||||||
|
|
26
index.html
26
index.html
|
@ -122,8 +122,8 @@ crossorigin=""/>
|
||||||
<li>
|
<li>
|
||||||
For each <code>div</code> which you wish to contain a map view,
|
For each <code>div</code> which you wish to contain a map view,
|
||||||
an invocation of the function
|
an invocation of the function
|
||||||
<code>geocsv_lite.core.initialise_map_element(id, data-source)</code>: <br/>
|
<code>geocsv_lite.core.initialiseMapElement(id, data-source)</code>: <br/>
|
||||||
<samp><script>geocsv_lite.core.initialise_map_element("map", "data/data.csv");</script></samp>
|
<samp><script>geocsv_lite.core.initialiseMapElement("map", "data/data.csv");</script></samp>
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
<p>
|
<p>
|
||||||
|
@ -159,6 +159,12 @@ crossorigin=""/>
|
||||||
and you don't have an appropriate pin image for each value present,
|
and you don't have an appropriate pin image for each value present,
|
||||||
then you will get 'broken' pin images appearing on your map.
|
then you will get 'broken' pin images appearing on your map.
|
||||||
</p>
|
</p>
|
||||||
|
<h2>
|
||||||
|
GitHub repository
|
||||||
|
</h2>
|
||||||
|
<p>
|
||||||
|
Is <a href="https://github.com/simon-brooke/geocsv-js">here.</a>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<footer>
|
<footer>
|
||||||
|
@ -181,10 +187,10 @@ crossorigin=""></script>
|
||||||
<script src="js/geocsv.js" type="text/javascript"></script>
|
<script src="js/geocsv.js" type="text/javascript"></script>
|
||||||
<script>
|
<script>
|
||||||
/* Map using data from element content */
|
/* Map using data from element content */
|
||||||
GeoCSV.initialise_map_element("element-content-map",
|
GeoCSV.initialiseMapElement("element-content-map",
|
||||||
document.getElementById("element-content-map").innerText);
|
document.getElementById("element-content-map").innerText);
|
||||||
/* Map using inline CSV passed to the function */
|
/* Map using inline CSV passed to the function */
|
||||||
GeoCSV.initialise_map_element("inline-csv-map",
|
GeoCSV.initialiseMapElement("inline-csv-map",
|
||||||
"Country,Name,Latitude,Longitude,CountryCode,Continent,Category\n" +
|
"Country,Name,Latitude,Longitude,CountryCode,Continent,Category\n" +
|
||||||
"Somaliland,Hargeisa,9.55,44.05,NULL,Africa,\n" +
|
"Somaliland,Hargeisa,9.55,44.05,NULL,Africa,\n" +
|
||||||
"Western Sahara,El-Aaiún,27.153611,-13.203333,EH,Africa,EH\n" +
|
"Western Sahara,El-Aaiún,27.153611,-13.203333,EH,Africa,EH\n" +
|
||||||
|
@ -245,8 +251,16 @@ crossorigin=""></script>
|
||||||
"Zimbabwe,Harare,-17.8166666666667,31.033333,ZW,Africa,ZW\n" +
|
"Zimbabwe,Harare,-17.8166666666667,31.033333,ZW,Africa,ZW\n" +
|
||||||
"British Indian Ocean Territory,Diego Garcia,-7.3,72.4,IO,Africa,\n" );
|
"British Indian Ocean Territory,Diego Garcia,-7.3,72.4,IO,Africa,\n" );
|
||||||
/* Map using CSV from URL */
|
/* Map using CSV from URL */
|
||||||
var url = window.location.href.substring(0, window.location.href.length - "index.html".length) + "data/europe-capitals.csv";
|
console.log( "Window.location.href = `" + window.location.href + "`");
|
||||||
GeoCSV.initialise_map_element("url-map", url);
|
var url = window.location.href;
|
||||||
|
|
||||||
|
if (url.endsWith( "index.html")) {
|
||||||
|
url = url.substring(0, window.location.href.length - "index.html".length);
|
||||||
|
}
|
||||||
|
|
||||||
|
url = url + "data/europe-capitals.csv";
|
||||||
|
|
||||||
|
GeoCSV.initialiseMapElement("url-map", url);
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
118
js/geocsv.js
118
js/geocsv.js
|
@ -5,11 +5,17 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var GeoCSV = {
|
var GeoCSV = {
|
||||||
|
/**
|
||||||
|
* Methods for disentangling data.
|
||||||
|
*/
|
||||||
Data: {
|
Data: {
|
||||||
|
/**
|
||||||
|
* Prepare a single record (Object) from the keys `ks` and values `vs`.
|
||||||
|
*/
|
||||||
prepareRecord( ks, vs) {
|
prepareRecord( ks, vs) {
|
||||||
var record = new Object();
|
var record = new Object();
|
||||||
|
|
||||||
for ( i = 0; i < Math.min( ks.length, vs.length); i++) {
|
for ( i = 0; i < Math.min( ks.length, vs.length); i += 1) {
|
||||||
if ( ks[ i]) {
|
if ( ks[ i]) {
|
||||||
record[ ks[ i]] = vs[ i];
|
record[ ks[ i]] = vs[ i];
|
||||||
}
|
}
|
||||||
|
@ -18,6 +24,11 @@ var GeoCSV = {
|
||||||
return record;
|
return record;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare an array record objects from this `data`, assumed to be data
|
||||||
|
* as parsed by [PapaParse](https://www.papaparse.com/) from a CSV file
|
||||||
|
* with column headers in the first row.
|
||||||
|
*/
|
||||||
prepareRecords( data) {
|
prepareRecords( data) {
|
||||||
var cols = data[0].map( c => {
|
var cols = data[0].map( c => {
|
||||||
return c.trim().toLowerCase().replace( /[^\w\d]+/, "-");
|
return c.trim().toLowerCase().replace( /[^\w\d]+/, "-");
|
||||||
|
@ -39,13 +50,17 @@ var GeoCSV = {
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
|
||||||
getData( data_source) {
|
/**
|
||||||
var p = Papa.parse( data_source);
|
* Parse this `dataSource` and return it as record objects.
|
||||||
|
* Doesn't yet work for URLs.
|
||||||
|
*/
|
||||||
|
getData( dataSource) {
|
||||||
|
var p = Papa.parse( dataSource);
|
||||||
var data = p.data;
|
var data = p.data;
|
||||||
|
|
||||||
if ( p.errors.length > 0) {
|
if ( p.errors.length > 0) {
|
||||||
try {
|
try {
|
||||||
data = JSON.parse( data_source);
|
data = JSON.parse( dataSource);
|
||||||
}
|
}
|
||||||
catch( anything) {
|
catch( anything) {
|
||||||
data = null;
|
data = null;
|
||||||
|
@ -55,11 +70,19 @@ var GeoCSV = {
|
||||||
if ( data instanceof Array) {
|
if ( data instanceof Array) {
|
||||||
return this.prepareRecords( data);
|
return this.prepareRecords( data);
|
||||||
} else {
|
} else {
|
||||||
|
// this is where I should handle URLs.
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Methods related to locating and presenting data on the map
|
||||||
|
*/
|
||||||
GIS: {
|
GIS: {
|
||||||
|
/**
|
||||||
|
* Return an appropriate pin image name for this `record`.
|
||||||
|
*/
|
||||||
pinImage( record) {
|
pinImage( record) {
|
||||||
var c = record["category"];
|
var c = record["category"];
|
||||||
|
|
||||||
|
@ -73,6 +96,10 @@ var GeoCSV = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return appropriate HTML formatted popup content for this
|
||||||
|
* `record`.
|
||||||
|
*/
|
||||||
popupContent( record) {
|
popupContent( record) {
|
||||||
var c = "<h5>" + record[ "name"] + "</h5><table>";
|
var c = "<h5>" + record[ "name"] + "</h5><table>";
|
||||||
|
|
||||||
|
@ -87,7 +114,10 @@ var GeoCSV = {
|
||||||
return c + "</table>";
|
return c + "</table>";
|
||||||
},
|
},
|
||||||
|
|
||||||
addPin( record, index, view) {
|
/**
|
||||||
|
* Add an appropriate marker for this `record` on this `view`.
|
||||||
|
*/
|
||||||
|
addPin( record, view) {
|
||||||
var lat = Number( record[ "latitude"]);
|
var lat = Number( record[ "latitude"]);
|
||||||
var lng = Number( record[ "longitude"]);
|
var lng = Number( record[ "longitude"]);
|
||||||
|
|
||||||
|
@ -112,6 +142,9 @@ var GeoCSV = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove all pins from this map `view`.
|
||||||
|
*/
|
||||||
removePins( view) {
|
removePins( view) {
|
||||||
view.eachLayer( l => {
|
view.eachLayer( l => {
|
||||||
if ( l instanceof L.marker) {
|
if ( l instanceof L.marker) {
|
||||||
|
@ -122,6 +155,10 @@ var GeoCSV = {
|
||||||
return view;
|
return view;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pan and zoom this map `view` to focus these `records`.
|
||||||
|
* TODO: This isn't working *nearly* as well as the ClojureScript version.
|
||||||
|
*/
|
||||||
computeBounds( view, records) {
|
computeBounds( view, records) {
|
||||||
if ( records.length > 0) {
|
if ( records.length > 0) {
|
||||||
var minLng = 180;
|
var minLng = 180;
|
||||||
|
@ -152,23 +189,35 @@ var GeoCSV = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a marker to this map `view` for each record in these `records`
|
||||||
|
* which has valid `latitude`and `longitude` properties, first removing
|
||||||
|
* any existing markers.
|
||||||
|
*/
|
||||||
refreshPins( view, records) {
|
refreshPins( view, records) {
|
||||||
this.removePins( view);
|
this.removePins( view);
|
||||||
|
|
||||||
for ( i = 0; i < records.length; i++) {
|
records.forEach( r => {
|
||||||
if( records[i]) {
|
if( r) {
|
||||||
this.addPin( records[i], i, view);
|
this.addPin( r, view);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this.computeBounds( view, records);
|
this.computeBounds( view, records);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Methods related to displaying the map.
|
||||||
|
*/
|
||||||
Map: {
|
Map: {
|
||||||
views: new Object(),
|
views: new Object(),
|
||||||
|
|
||||||
didMount(id, lat, lng, zoom) {
|
/**
|
||||||
|
* Create a map overlaying the HTML element with this `id`, centered at
|
||||||
|
* these `lat` and `lng` coordinates, with this initial `zoom` value.
|
||||||
|
*/
|
||||||
|
createMap(id, lat, lng, zoom) {
|
||||||
var v = L.map( id).setView( {lat: lat, lon: lng}, zoom);
|
var v = L.map( id).setView( {lat: lat, lon: lng}, zoom);
|
||||||
|
|
||||||
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||||||
|
@ -179,22 +228,34 @@ var GeoCSV = {
|
||||||
return v;
|
return v;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a map view to my named views overlaying the HTML element with this
|
||||||
|
* `id` and centered at these `lat` and `lng` coordinates, with this
|
||||||
|
* initial `zoom` value, provided that it does not already exist. Return
|
||||||
|
* the view.
|
||||||
|
*/
|
||||||
addView( id, lat, lng, zoom) {
|
addView( id, lat, lng, zoom) {
|
||||||
/* can"t re-add a view to an element to which we"ve already added one */
|
/* can"t re-add a view to an element to which we"ve already added one */
|
||||||
if ( this.views[ id]) {
|
if ( this.views[ id]) {
|
||||||
return this.views[ id];
|
return this.views[ id];
|
||||||
} else {
|
} else {
|
||||||
var v = this.didMount( id, lat, lng, zoom);
|
var v = this.createMap( id, lat, lng, zoom);
|
||||||
this.views[ id] = v;
|
this.views[ id] = v;
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the view with this `id` from among my named views.
|
||||||
|
*/
|
||||||
getView( id) {
|
getView( id) {
|
||||||
return this.views[ id];
|
return this.views[ id];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Methods related to notification and logging.
|
||||||
|
*/
|
||||||
Notify: {
|
Notify: {
|
||||||
/**
|
/**
|
||||||
* Show this error `m` to the user and log it.
|
* Show this error `m` to the user and log it.
|
||||||
|
@ -213,17 +274,48 @@ var GeoCSV = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
initialise_map_element( id, data_source) {
|
/**
|
||||||
|
* Initialise a map view overlaying the HTML element with this `id`, and
|
||||||
|
* decorate it with markers as specified in the data from this source.
|
||||||
|
*/
|
||||||
|
initialiseMapElement( id, dataSource) {
|
||||||
this.Notify.message( "initialise_map_element called with arguments id = `" +
|
this.Notify.message( "initialise_map_element called with arguments id = `" +
|
||||||
id + "`");
|
id + "`");
|
||||||
var view = this.Map.addView( id, 0, 0, 0);
|
var view = this.Map.addView( id, 0, 0, 0);
|
||||||
var records = this.Data.getData( data_source);
|
var records = this.Data.getData( dataSource);
|
||||||
|
|
||||||
if ( records instanceof Array) {
|
if ( records instanceof Array) {
|
||||||
this.Notify.message( "Found " + records.length +
|
this.Notify.message( "Found " + records.length +
|
||||||
" records of inline data for map " + id);
|
" records of inline data for map " + id);
|
||||||
|
|
||||||
this.GIS.refreshPins( view, records);
|
this.GIS.refreshPins( view, records);
|
||||||
|
} else {
|
||||||
|
// is it a URL?
|
||||||
|
try {
|
||||||
|
fetch(dataSource)
|
||||||
|
.then((response) => {
|
||||||
|
console.debug( response.blob());
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
return response.text();
|
||||||
|
} else {
|
||||||
|
throw new Error( "Bad response from server: " + response.status);
|
||||||
|
}
|
||||||
|
}).then( text => {
|
||||||
|
var records = this.Data.getData( text);
|
||||||
|
|
||||||
|
if ( records instanceof Array) {
|
||||||
|
this.Notify.message( "Found " + records.length +
|
||||||
|
" records of data for map " + id);
|
||||||
|
|
||||||
|
this.GIS.refreshPins( view, records);
|
||||||
|
} else {
|
||||||
|
throw new Error( "No data?");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
this.Notify.error( error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue