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>&lt;script&gt;geocsv_lite.core.initialise_map_element("map", "data/data.csv");&lt;/script&gt;</samp>
+            <code>geocsv_lite.core.initialiseMapElement(id, data-source)</code>: <br/>
+            <samp>&lt;script&gt;geocsv_lite.core.initialiseMapElement("map", "data/data.csv");&lt;/script&gt;</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);
+      }
     }
   }
 }