Working on Firefox, including URL fetch.

URL data not working on Chrome or Safari. Bother.
This commit is contained in:
Simon Brooke 2020-02-25 14:17:02 +00:00
parent 47ca5b17ba
commit a081358564
No known key found for this signature in database
GPG key ID: A7A4F18D1D4DF987
3 changed files with 133 additions and 19 deletions

View file

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

View file

@ -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>&lt;script&gt;geocsv_lite.core.initialise_map_element("map", "data/data.csv");&lt;/script&gt;</samp> <samp>&lt;script&gt;geocsv_lite.core.initialiseMapElement("map", "data/data.csv");&lt;/script&gt;</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>

View file

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