From c3059d95fed309e0a1b81b27434eb5fcbde1b3e5 Mon Sep 17 00:00:00 2001
From: John Kwening <kwening.john@gmail.com>
Date: Mon, 26 Mar 2018 20:01:11 -0400
Subject: [PATCH] feat: Prefetch and cache direction tiles

Add code logic for prefetching tiles at zoom level 18 for points for
polyline via waypoints from origin to destination.
---
 .../js/leaflet-tileLayer-pouchdb-cached.js    | 30 ++++++++++-
 client/src/js/lrm-google.js                   | 20 ++++++--
 client/src/js/osm-tile-name.js                | 51 +++++++++++++++++++
 client/src/routes/directions/index.js         |  2 +-
 4 files changed, 98 insertions(+), 5 deletions(-)
 create mode 100644 client/src/js/osm-tile-name.js

diff --git a/client/src/js/leaflet-tileLayer-pouchdb-cached.js b/client/src/js/leaflet-tileLayer-pouchdb-cached.js
index fc032dd..9184b83 100644
--- a/client/src/js/leaflet-tileLayer-pouchdb-cached.js
+++ b/client/src/js/leaflet-tileLayer-pouchdb-cached.js
@@ -287,8 +287,36 @@ L.TileLayer.include({
 				this._seedOneTile(tile, remaining, seedData);
 			}
 		}.bind(this));
-	}
+	},
+
+	cacheAhead: function(coords, done, zoom=18) {
+		var tile = document.createElement('img');
+
+		tile.onerror = L.bind(this._tileOnError, this, done, tile);
+
+		if (this.options.crossOrigin) {
+			tile.crossOrigin = '';
+		}
 
+		/*
+		 Alt tag is *set to empty string to keep screen readers from reading URL and for compliance reasons
+		 http://www.w3.org/TR/WCAG20-TECHS/H67
+		 */
+		tile.alt = '';
+
+		let tileUrl = this.getTileUrl(coords);
+		tileUrl = tileUrl.replace('NaN', zoom); // fix tileUrl with correct zoom value
+
+		// if available get cached tile image
+		this._db.get(tileUrl,
+			{
+				rev: true,
+				attachments: true,
+				binary: true,  // return attachment data as Blobs instead of as base64-encoded strings
+			},
+			this._onCacheLookup(tile, tileUrl, done)
+		);
+	}
 });
 
 export default L;
diff --git a/client/src/js/lrm-google.js b/client/src/js/lrm-google.js
index cf842cb..6d89230 100644
--- a/client/src/js/lrm-google.js
+++ b/client/src/js/lrm-google.js
@@ -3,6 +3,7 @@ const L = require('leaflet');
 const decodePolyline = require('decode-google-map-polyline');
 
 // app module imports
+const { latLngToPoint, pointToLatLng } = require('./osm-tile-name');
 const { makeRequest, BASE_ENDPOINTS } = require('./server-requests-utils')
 
 L.Routing = L.Routing || {};
@@ -12,8 +13,9 @@ L.Routing.Google = L.Class.extend({
 
   },
 
-  initialize: function(options) {
+  initialize: function(options, TILE_LAYER) {
     L.Util.setOptions(this, options);
+    this._TILE_LAYER = TILE_LAYER;
   },
 
   route: function(waypoints, callback, context, options) {
@@ -65,6 +67,18 @@ L.Routing.Google = L.Class.extend({
         });
       });
 
+      // prefetch/cache polylines
+      if (this._TILE_LAYER) {
+        route.coordinates.forEach((latLng) => {
+          const coord = latLngToPoint(latLng.lat, latLng.lng, 18);
+
+          // TODO - if needed cache all four corners by looping +1 for x/y
+          this._TILE_LAYER.cacheAhead(coord, () => {
+            // console.log('cached coord: ', coord);
+          });
+        });
+      }
+
       routes.push(route);
     });
 
@@ -72,6 +86,6 @@ L.Routing.Google = L.Class.extend({
   }
 });
 
-L.Routing.google = function(options={}) {
-  return new L.Routing.Google(options);
+L.Routing.google = function(options={}, TILE_LAYER=null) {
+  return new L.Routing.Google(options, TILE_LAYER);
 };
diff --git a/client/src/js/osm-tile-name.js b/client/src/js/osm-tile-name.js
new file mode 100644
index 0000000..b146ca5
--- /dev/null
+++ b/client/src/js/osm-tile-name.js
@@ -0,0 +1,51 @@
+/**
+ * Module for calculating tile name for slippy maps.
+ * 
+ * Source: https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames
+ * 
+ * Notes:
+ * This article describes the file naming conventions for the Slippy Map application.
+ * - Tiles are 256 × 256 pixel PNG files
+ * - Each zoom level is a directory, each column is a subdirectory, and each tile in that column is a file
+ * - Filename(url) format is /zoom/x/y.png
+ * The slippy map expects tiles to be served up at URLs following this scheme, so all tile server URLs look pretty similar.
+ */
+
+ // utility functions
+ const toRadians = (angle) => (angle * (Math.PI / 180));
+ const toDegrees = (radian) => (radian * (180/ Math.PI));
+ const getN = (zoom) => (Math.pow(2.0, zoom));
+ const getSec = (radian) => (1 / Math.cos(radian));
+
+ /**
+  * Calculate x and y coords for tile url.
+  * 
+  * @param {number} lat latitude in degrees
+  * @param {number} lng longitude in degrees
+  * @param {number} zoom zoom level
+  */
+ exports.latLngToPoint = (lat, lng, zoom) => {
+   const n = getN(zoom);
+   const latRadians = toRadians(lat);
+
+   const x = Math.floor(n * ((lng + 180) / 360));
+   
+   const secLat = getSec(latRadians);
+   const y = Math.floor(n * (1 - (Math.log(Math.tan(latRadians) + secLat) / Math.PI)) / 2);
+   return { x, y, z: zoom };
+ };
+
+ /**
+  * Estimate latitude and longitude for give tile url point.
+  * 
+  * @param {number} x point x
+  * @param {number} y point y
+  * @param {number} zoom zoom level
+  */
+exports.pointToLatLng = (x, y, zoom) => {
+  const n = getN(zoom);
+
+  const lng = (x / n) * 360 - 180;
+  const lat = Math.atan(Math.sinh(Math.PI - (y / n) * 2 * Math.PI)) * (180 / Math.PI);
+  return { lat, lng };
+};
diff --git a/client/src/routes/directions/index.js b/client/src/routes/directions/index.js
index 90be88b..457bcd0 100644
--- a/client/src/routes/directions/index.js
+++ b/client/src/routes/directions/index.js
@@ -77,7 +77,7 @@ export default class Directions extends Component {
       showAlternatives: true,
       show: false,
       collapsible: false,
-      router: L.Routing.google(),
+      router: L.Routing.google({}, OSM_TILE_LAYER),
     }).addTo(map);
 
     this.control.on('routeselected', (e) => {