From 5cbcaa85a1655f5be245e3d2c4bffffaa71ffb12 Mon Sep 17 00:00:00 2001 From: Simon Brooke <simon@journeyman.cc> Date: Tue, 25 Feb 2020 14:17:02 +0000 Subject: [PATCH] Working, including URL fetch. --- README.md | 8 ++++ index.html | 26 +++++++++--- js/geocsv.js | 118 +++++++++++++++++++++++++++++++++++++++++++++------ 3 files changed, 133 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 2a9d156..adc97d4 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,14 @@ 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 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. diff --git a/index.html b/index.html index d8ec296..075b763 100644 --- a/index.html +++ b/index.html @@ -122,8 +122,8 @@ crossorigin=""/> <li> For each <code>div</code> which you wish to contain a map view, an invocation of the function - <code>geocsv_lite.core.initialise_map_element(id, data-source)</code>: <br/> - <samp><script>geocsv_lite.core.initialise_map_element("map", "data/data.csv");</script></samp> + <code>geocsv_lite.core.initialiseMapElement(id, data-source)</code>: <br/> + <samp><script>geocsv_lite.core.initialiseMapElement("map", "data/data.csv");</script></samp> </li> </ol> <p> @@ -159,6 +159,12 @@ crossorigin=""/> and you don't have an appropriate pin image for each value present, then you will get 'broken' pin images appearing on your map. </p> + <h2> + GitHub repository + </h2> + <p> + Is <a href="https://github.com/simon-brooke/geocsv-js">here.</a> + </p> </div> </div> <footer> @@ -181,10 +187,10 @@ crossorigin=""></script> <script src="js/geocsv.js" type="text/javascript"></script> <script> /* Map using data from element content */ - GeoCSV.initialise_map_element("element-content-map", + GeoCSV.initialiseMapElement("element-content-map", document.getElementById("element-content-map").innerText); /* 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" + "Somaliland,Hargeisa,9.55,44.05,NULL,Africa,\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" + "British Indian Ocean Territory,Diego Garcia,-7.3,72.4,IO,Africa,\n" ); /* Map using CSV from URL */ - var url = window.location.href.substring(0, window.location.href.length - "index.html".length) + "data/europe-capitals.csv"; - GeoCSV.initialise_map_element("url-map", url); + console.log( "Window.location.href = `" + window.location.href + "`"); + 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> </body> </html> diff --git a/js/geocsv.js b/js/geocsv.js index 8fc7d84..7822f2c 100644 --- a/js/geocsv.js +++ b/js/geocsv.js @@ -5,11 +5,17 @@ */ var GeoCSV = { + /** + * Methods for disentangling data. + */ Data: { + /** + * Prepare a single record (Object) from the keys `ks` and values `vs`. + */ prepareRecord( ks, vs) { 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]) { record[ ks[ i]] = vs[ i]; } @@ -18,6 +24,11 @@ var GeoCSV = { 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) { var cols = data[0].map( c => { return c.trim().toLowerCase().replace( /[^\w\d]+/, "-"); @@ -39,13 +50,17 @@ var GeoCSV = { 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; if ( p.errors.length > 0) { try { - data = JSON.parse( data_source); + data = JSON.parse( dataSource); } catch( anything) { data = null; @@ -55,11 +70,19 @@ var GeoCSV = { if ( data instanceof Array) { return this.prepareRecords( data); } else { + // this is where I should handle URLs. return null; } } }, + + /** + * Methods related to locating and presenting data on the map + */ GIS: { + /** + * Return an appropriate pin image name for this `record`. + */ pinImage( record) { var c = record["category"]; @@ -73,6 +96,10 @@ var GeoCSV = { } }, + /** + * Return appropriate HTML formatted popup content for this + * `record`. + */ popupContent( record) { var c = "<h5>" + record[ "name"] + "</h5><table>"; @@ -87,7 +114,10 @@ var GeoCSV = { 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 lng = Number( record[ "longitude"]); @@ -112,6 +142,9 @@ var GeoCSV = { } }, + /** + * Remove all pins from this map `view`. + */ removePins( view) { view.eachLayer( l => { if ( l instanceof L.marker) { @@ -122,6 +155,10 @@ var GeoCSV = { 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) { if ( records.length > 0) { 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) { this.removePins( view); - for ( i = 0; i < records.length; i++) { - if( records[i]) { - this.addPin( records[i], i, view); + records.forEach( r => { + if( r) { + this.addPin( r, view); } - } + }); this.computeBounds( view, records); } }, + /** + * Methods related to displaying the map. + */ Map: { 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); L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { @@ -179,22 +228,34 @@ var GeoCSV = { 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) { /* can"t re-add a view to an element to which we"ve already added one */ if ( this.views[ id]) { return this.views[ id]; } else { - var v = this.didMount( id, lat, lng, zoom); + var v = this.createMap( id, lat, lng, zoom); this.views[ id] = v; return v; } }, + /** + * Get the view with this `id` from among my named views. + */ getView( id) { return this.views[ id]; } }, + /** + * Methods related to notification and logging. + */ Notify: { /** * 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 = `" + id + "`"); 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) { this.Notify.message( "Found " + records.length + " records of inline data for map " + id); 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); + } } } }