From a0e3650e45a86735de5fa6a00b3212b9a4027a4a Mon Sep 17 00:00:00 2001
From: "jeremy@jermolene.com" <jeremy@jermolene.com>
Date: Mon, 12 Dec 2022 08:54:40 +0000
Subject: [PATCH] Add basic support for traveltime.com isochrone API

---
 core/modules/startup/rootwidget.js            |   5 +
 core/modules/utils/dom/http.js                | 101 +++++++++++++++++-
 .../WidgetMessage_ tm-http-request.tid        |  43 ++++++++
 .../geospatial/demo/features/us-states.tid    |   1 +
 plugins/tiddlywiki/geospatial/demo/maps.tid   |  11 ++
 .../tiddlywiki/geospatial/demo/traveltime.tid |  92 ++++++++++++++++
 plugins/tiddlywiki/geospatial/plugin.info     |   2 +-
 plugins/tiddlywiki/geospatial/readme.tid      |  10 +-
 plugins/tiddlywiki/geospatial/settings.tid    |  15 +++
 .../tiddlywiki/geospatial/widgets/geomap.js   |  19 +++-
 10 files changed, 283 insertions(+), 16 deletions(-)
 create mode 100644 editions/tw5.com/tiddlers/messages/WidgetMessage_ tm-http-request.tid
 create mode 100644 plugins/tiddlywiki/geospatial/demo/maps.tid
 create mode 100644 plugins/tiddlywiki/geospatial/demo/traveltime.tid
 create mode 100644 plugins/tiddlywiki/geospatial/settings.tid

diff --git a/core/modules/startup/rootwidget.js b/core/modules/startup/rootwidget.js
index 41f3fe03f..73f1fda2d 100644
--- a/core/modules/startup/rootwidget.js
+++ b/core/modules/startup/rootwidget.js
@@ -20,6 +20,11 @@ exports.before = ["story"];
 exports.synchronous = true;
 
 exports.startup = function() {
+	// Install the HTTP client event handler
+	$tw.httpClient = new $tw.utils.HttpClient();
+	$tw.rootWidget.addEventListener("tm-http-request",function(event) {
+		$tw.httpClient.handleHttpRequest(event);
+	});
 	// Install the modal message mechanism
 	$tw.modal = new $tw.utils.Modal($tw.wiki);
 	$tw.rootWidget.addEventListener("tm-modal",function(event) {
diff --git a/core/modules/utils/dom/http.js b/core/modules/utils/dom/http.js
index 6e07b1040..3eca4985b 100644
--- a/core/modules/utils/dom/http.js
+++ b/core/modules/utils/dom/http.js
@@ -3,7 +3,7 @@ title: $:/core/modules/utils/dom/http.js
 type: application/javascript
 module-type: utils
 
-Browser HTTP support
+HTTP support
 
 \*/
 (function(){
@@ -13,11 +13,99 @@ Browser HTTP support
 "use strict";
 
 /*
-A quick and dirty HTTP function; to be refactored later. Options are:
+Manage tm-http-request events. Options are:
+wiki - the wiki object to use
+*/
+function HttpClient(options) {
+	options = options || {};
+	this.wiki = options.wiki || $tw.wiki;
+}
+
+HttpClient.prototype.handleHttpRequest = function(event) {
+	console.log("Making an HTTP request",event)
+	var self = this,
+		paramObject = event.paramObject || {},
+		url = paramObject.url,
+		completionActions = paramObject.oncompletion || "",
+		progressActions = paramObject.onprogress || "",
+		bindStatus = paramObject["bind-status"],
+		bindProgress = paramObject["bind-progress"],
+		method = paramObject.method || "GET",
+		HEADER_PARAMETER_PREFIX = "header-",
+		PASSWORD_HEADER_PARAMETER_PREFIX = "password-header-",
+		CONTEXT_VARIABLE_PARAMETER_PREFIX = "var-",
+		requestHeaders = {},
+		contextVariables = {},
+		setBinding = function(title,text) {
+			if(title) {
+				self.wiki.addTiddler(new $tw.Tiddler({title: title, text: text}));
+			}
+		};
+	if(url) {
+		setBinding(bindStatus,"pending");
+		setBinding(bindProgress,"0");
+		$tw.utils.each(paramObject,function(value,name) {
+			// Look for header- parameters
+			if(name.substr(0,HEADER_PARAMETER_PREFIX.length) === HEADER_PARAMETER_PREFIX) {
+				requestHeaders[name.substr(HEADER_PARAMETER_PREFIX.length)] = value;
+			}
+			// Look for password-header- parameters
+			if(name.substr(0,PASSWORD_HEADER_PARAMETER_PREFIX.length) === PASSWORD_HEADER_PARAMETER_PREFIX) {
+				requestHeaders[name.substr(PASSWORD_HEADER_PARAMETER_PREFIX.length)] = $tw.utils.getPassword(value) || "";
+			}
+			// Look for var- parameters
+			if(name.substr(0,CONTEXT_VARIABLE_PARAMETER_PREFIX.length) === CONTEXT_VARIABLE_PARAMETER_PREFIX) {
+				contextVariables[name.substr(CONTEXT_VARIABLE_PARAMETER_PREFIX.length)] = value;
+			}
+		});
+		$tw.utils.httpRequest({
+			url: url,
+			type: method,
+			headers: requestHeaders,
+			data: paramObject.body,
+			callback: function(err,data,xhr) {
+				var headers = {};
+				$tw.utils.each(xhr.getAllResponseHeaders().split("\r\n"),function(line) {
+					var parts = line.split(":");
+					if(parts.length === 2) {
+						headers[parts[0].toLowerCase()] = parts[1].trim();
+					}
+				});
+				setBinding(bindStatus,xhr.status === 200 ? "complete" : "error");
+				setBinding(bindProgress,"100");
+				var results = {
+					status: xhr.status.toString(),
+					statusText: xhr.statusText,
+					error: (err || "").toString(),
+					data: (data || "").toString(),
+					headers: JSON.stringify(headers)
+				};
+				$tw.rootWidget.invokeActionString(completionActions,undefined,undefined,$tw.utils.extend({},contextVariables,results));
+				// console.log("Back!",err,data,xhr);
+			},
+			progress: function(lengthComputable,loaded,total) {
+				if(lengthComputable) {
+					setBinding(bindProgress,"" + Math.floor((loaded/total) * 100))
+				}
+				$tw.rootWidget.invokeActionString(progressActions,undefined,undefined,{
+					lengthComputable: lengthComputable ? "yes" : "no",
+					loaded: loaded,
+					total: total
+				});
+			}
+		});
+	}
+};
+
+exports.HttpClient = HttpClient;
+
+/*
+Make an HTTP request. Options are:
 	url: URL to retrieve
 	headers: hashmap of headers to send
 	type: GET, PUT, POST etc
 	callback: function invoked with (err,data,xhr)
+	progress: optional function invoked with (lengthComputable,loaded,total)
 	returnProp: string name of the property to return as first argument of callback
 */
 exports.httpRequest = function(options) {
@@ -83,8 +171,16 @@ exports.httpRequest = function(options) {
 		options.callback($tw.language.getString("Error/XMLHttpRequest") + ": " + this.status,null,this);
 		}
 	};
+	// Handle progress
+	if(options.progress) {
+		request.onprogress = function(event) {
+			console.log("Progress event",event)
+			options.progress(event.lengthComputable,event.loaded,event.total);
+		};
+	}
 	// Make the request
 	request.open(type,url,true);
+	// Headers
 	if(headers) {
 		$tw.utils.each(headers,function(header,headerTitle,object) {
 			request.setRequestHeader(headerTitle,header);
@@ -96,6 +192,7 @@ exports.httpRequest = function(options) {
 	if(!hasHeader("X-Requested-With") && !isSimpleRequest(type,headers)) {
 		request.setRequestHeader("X-Requested-With","TiddlyWiki");
 	}
+	// Send data
 	try {
 		request.send(data);
 	} catch(e) {
diff --git a/editions/tw5.com/tiddlers/messages/WidgetMessage_ tm-http-request.tid b/editions/tw5.com/tiddlers/messages/WidgetMessage_ tm-http-request.tid
new file mode 100644
index 000000000..5befb6f1b
--- /dev/null
+++ b/editions/tw5.com/tiddlers/messages/WidgetMessage_ tm-http-request.tid	
@@ -0,0 +1,43 @@
+caption: tm-http-request
+created: 20220908161746341
+modified: 20220908161746341
+tags: Messages
+title: WidgetMessage: tm-http-request
+type: text/vnd.tiddlywiki
+
+The ''tm-http-request'' message is used to make an HTTP request to a server.
+
+It uses the following properties on the `event` object:
+
+|!Name |!Description |
+|param |Not used |
+|paramObject |Hashmap of parameters (see below) |
+
+The following parameters are used:
+
+|!Name |!Description |
+|method |HTTP method (eg "GET", "POST") |
+|body |String data to be sent with the request |
+|header-* |Headers with string values|
+|password-header-* |Headers with values taken from the password store |
+|var-* |Variables to be passed to the completion and progress handlers (without the "var-" prefix) |
+|bind-status |Title of tiddler to which the status of the request ("pending", "complete", "error") should be bound |
+|bind-progress |Title of tiddler to which the progress of the request (0 to 100) should be bound |
+|oncompletion |Action strings to be executed when the request completes |
+|onprogress |Action strings to be executed when progress is reported |
+
+The following variables are passed to the completion handler:
+
+|!Name |!Description |
+|status |HTTP result status code (see [[MDN|https://developer.mozilla.org/en-US/docs/Web/HTTP/Status]]) |
+|statusText |HTTP result status text |
+|error |Error string |
+|data |Returned data |
+|headers |Response headers as a JSON object |
+
+The following variables are passed to the progress handler:
+
+|!Name |!Description |
+|lengthComputable |Whether the progress loaded and total figures are valid - "yes" or "no" |
+|loaded |Number of bytes loaded so far |
+|total |Total number bytes to be loaded |
diff --git a/plugins/tiddlywiki/geospatial/demo/features/us-states.tid b/plugins/tiddlywiki/geospatial/demo/features/us-states.tid
index 5f3547b39..bb07278de 100644
--- a/plugins/tiddlywiki/geospatial/demo/features/us-states.tid
+++ b/plugins/tiddlywiki/geospatial/demo/features/us-states.tid
@@ -1,5 +1,6 @@
 title: $:/plugins/geospatial/demo/features/us-states
 type: application/json
+tags: $:/tags/GeoLayer
 
 {"type":"FeatureCollection","features":[
 	{"type":"Feature","id":"01","properties":{"name":"Alabama","density":94.65},"geometry":{"type":"Polygon","coordinates":[[[-87.359296,35.00118],[-85.606675,34.984749],[-85.431413,34.124869],[-85.184951,32.859696],[-85.069935,32.580372],[-84.960397,32.421541],[-85.004212,32.322956],[-84.889196,32.262709],[-85.058981,32.13674],[-85.053504,32.01077],[-85.141136,31.840985],[-85.042551,31.539753],[-85.113751,31.27686],[-85.004212,31.003013],[-85.497137,30.997536],[-87.600282,30.997536],[-87.633143,30.86609],[-87.408589,30.674397],[-87.446927,30.510088],[-87.37025,30.427934],[-87.518128,30.280057],[-87.655051,30.247195],[-87.90699,30.411504],[-87.934375,30.657966],[-88.011052,30.685351],[-88.10416,30.499135],[-88.137022,30.318396],[-88.394438,30.367688],[-88.471115,31.895754],[-88.241084,33.796253],[-88.098683,34.891641],[-88.202745,34.995703],[-87.359296,35.00118]]]}},
diff --git a/plugins/tiddlywiki/geospatial/demo/maps.tid b/plugins/tiddlywiki/geospatial/demo/maps.tid
new file mode 100644
index 000000000..671fa6ecf
--- /dev/null
+++ b/plugins/tiddlywiki/geospatial/demo/maps.tid
@@ -0,0 +1,11 @@
+title: $:/plugins/tiddlywiki/geospatial/demo/maps
+caption: Maps
+tags: $:/tags/GeospatialDemo
+
+! Map with Layers and Markers
+
+<$geomap
+	markers="[all[tiddlers+shadows]tag[$:/tags/GeoMarker]]"
+	layers="[all[tiddlers+shadows]tag[$:/tags/GeoLayer]]"
+/>
+
diff --git a/plugins/tiddlywiki/geospatial/demo/traveltime.tid b/plugins/tiddlywiki/geospatial/demo/traveltime.tid
new file mode 100644
index 000000000..6b20858b7
--- /dev/null
+++ b/plugins/tiddlywiki/geospatial/demo/traveltime.tid
@@ -0,0 +1,92 @@
+title: $:/plugins/tiddlywiki/geospatial/demo/traveltime
+caption: Traveltime
+tags: $:/tags/GeospatialDemo
+
+\define completion-actions()
+<$action-log/>
+<$action-setfield $tiddler="$:/temp/_StatusCode" text=<<status>>/>
+<$action-setfield $tiddler="$:/temp/_StatusText" text=<<statusText>>/>
+<$action-setfield $tiddler="$:/temp/_Error" text=<<error>>/>
+<$action-setfield $tiddler="$:/temp/_Result" text=<<data>>/>
+<$action-setfield $tiddler="$:/temp/_Headers" text=<<headers>>/>
+<$list filter="[<status>match[200]]" variable="ignore">
+<$action-setfield $tiddler="$:/temp/_IsochroneLayer" text={{{ [<data>] }}} tags="$:/tags/GeoLayer"/>
+</$list>
+\end
+
+\define progress-actions()
+<$action-log message="In progress-actions"/>
+<$action-log/>
+\end
+
+\define payload-source()
+\rules only transcludeinline transcludeblock filteredtranscludeinline filteredtranscludeblock
+{
+  "departure_searches": [
+    {
+      "id": "My first isochrone",
+      "coords": {
+        "lat": 51.507609,
+        "lng": -0.128315
+      },
+      "departure_time": "2021-09-27T08:00:00Z",
+      "travel_time": 3600,
+      "transportation": {
+        "type": "driving"
+      }
+    }
+  ]
+}
+\end
+
+\define get-traveltime-actions()
+<$wikify name="payload" text=<<payload-source>>>
+	<$action-log $message="Making payload"/>
+	<$action-log/>
+	<$action-sendmessage
+		$message="tm-http-request"
+		url="https://api.traveltimeapp.com/v4/time-map"
+		method="POST"
+		header-accept="application/geo+json"
+		header-Content-Type="application/json"
+		password-header-X-Api-Key="traveltime-secret-key"
+		password-header-X-Application-Id="traveltime-application-id"
+		body=<<payload>>
+		var-context="Context string"
+		bind-status="$:/temp/plugins/tiddlywiki/geospatial/demo/traveltime/status"
+		bind-progress="$:/temp/plugins/tiddlywiki/geospatial/demo/traveltime/progress"
+		oncompletion=<<completion-actions>>
+		onprogress=<<progress-actions>>
+	/>
+</$wikify>
+\end
+
+
+
+<$button actions=<<get-traveltime-actions>>>
+Call ~TravelTime
+</$button>
+
+Status:
+<pre><code><$text text={{$:/temp/plugins/tiddlywiki/geospatial/demo/traveltime/status}}/></code></pre>
+
+Progress:
+<pre><code><$text text={{$:/temp/plugins/tiddlywiki/geospatial/demo/traveltime/progress}}/></code></pre>
+
+Response
+
+~StatusCode:
+<pre><code><$text text={{$:/temp/_StatusCode}}/></code></pre>
+
+~StatusText:
+<pre><code><$text text={{$:/temp/_StatusText}}/></code></pre>
+
+Error:
+<pre><code><$text text={{$:/temp/_Error}}/></code></pre>
+
+Headers:
+<pre><code><$text text={{$:/temp/_Headers}}/></code></pre>
+
+Result:
+<pre><code><$text text={{$:/temp/_Result}}/></code></pre>
+
diff --git a/plugins/tiddlywiki/geospatial/plugin.info b/plugins/tiddlywiki/geospatial/plugin.info
index 33238c6c9..564cf38ed 100644
--- a/plugins/tiddlywiki/geospatial/plugin.info
+++ b/plugins/tiddlywiki/geospatial/plugin.info
@@ -2,5 +2,5 @@
 	"title": "$:/plugins/tiddlywiki/geospatial",
 	"name": "Geospatial Utilities",
 	"description": "Geospatial utilities",
-	"list": "readme license"
+	"list": "readme settings license"
 }
diff --git a/plugins/tiddlywiki/geospatial/readme.tid b/plugins/tiddlywiki/geospatial/readme.tid
index e6b223124..94963ea3d 100644
--- a/plugins/tiddlywiki/geospatial/readme.tid
+++ b/plugins/tiddlywiki/geospatial/readme.tid
@@ -1,11 +1,5 @@
 title: $:/plugins/tiddlywiki/geospatial/readme
 
-! Examples
+! Demos
 
-!! Simple Map
-
-<$geomap/>
-
-!! Map with Markers
-
-<$geomap markers="[all[tiddlers+shadows]tag[$:/tags/GeoMarker]]"/>
+<<tabs tabsList:"[all[tiddlers+shadows]tag[$:/tags/GeospatialDemo]]" default:"$:/plugins/tiddlywiki/geospatial/demo/traveltime">>
diff --git a/plugins/tiddlywiki/geospatial/settings.tid b/plugins/tiddlywiki/geospatial/settings.tid
new file mode 100644
index 000000000..dfcf4bfeb
--- /dev/null
+++ b/plugins/tiddlywiki/geospatial/settings.tid
@@ -0,0 +1,15 @@
+title: $:/plugins/tiddlywiki/geospatial/settings
+tags: $:/tags/ControlPanel
+caption: Geospatial Plugin
+
+<div class="tc-control-panel">
+
+! Geospatial Plugin Settings
+
+Register for a free account at https://traveltime.com/ and copy and paste the secrets below:
+
+~TravelTime Application ID: <$password name="traveltime-application-id"/>
+
+~TravelTime Secret Key: <$password name="traveltime-secret-key"/>
+
+</div>
diff --git a/plugins/tiddlywiki/geospatial/widgets/geomap.js b/plugins/tiddlywiki/geospatial/widgets/geomap.js
index 6c489d023..41e85e260 100644
--- a/plugins/tiddlywiki/geospatial/widgets/geomap.js
+++ b/plugins/tiddlywiki/geospatial/widgets/geomap.js
@@ -56,6 +56,8 @@ GeomapWidget.prototype.renderMap = function(domNode) {
 		maxZoom: 19,
 		attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
 	}).addTo(map);
+	// Disable Leaflet attribution
+	map.attributionControl.setPrefix("");
 	// Create default icon
 	const iconProportions = 365/560,
 		iconHeight = 50;
@@ -67,12 +69,18 @@ GeomapWidget.prototype.renderMap = function(domNode) {
 	});
 	// Add scale
 	L.control.scale().addTo(map);
-	// Add US states overlay
-	const layer = L.geoJSON($tw.utils.parseJSONSafe(self.wiki.getTiddlerText("$:/plugins/geospatial/demo/features/us-states"),[])).addTo(map);
-	// Create markers
+	// Add overlays
+	if(this.geomapLayerFilter) {
+		$tw.utils.each(this.wiki.filterTiddlers(this.geomapLayerFilter),function(title) {
+			var tiddler = self.wiki.getTiddler(title);
+			if(tiddler) {
+				var layer = L.geoJSON($tw.utils.parseJSONSafe(tiddler.fields.text || "[]",[])).addTo(map);
+			}
+		});
+	}
+	// Add markers
 	if(this.geomapMarkerFilter) {
-		var titles = this.wiki.filterTiddlers(this.geomapMarkerFilter);
-		$tw.utils.each(titles,function(title) {
+		$tw.utils.each(this.wiki.filterTiddlers(this.geomapMarkerFilter),function(title) {
 			var tiddler = self.wiki.getTiddler(title);
 			if(tiddler) {
 				var lat = $tw.utils.parseNumber(tiddler.fields.lat || "0"),
@@ -89,6 +97,7 @@ GeomapWidget.prototype.renderMap = function(domNode) {
 Compute the internal state of the widget
 */
 GeomapWidget.prototype.execute = function() {
+	this.geomapLayerFilter = this.getAttribute("layers");
 	this.geomapMarkerFilter = this.getAttribute("markers");
 };