File size: 3,160 Bytes
3accc99
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
"use strict";
/*
 * Offline map tiles via IndexedDB.
 *
 * A custom Leaflet tile layer that serves map tiles from IndexedDB when present
 * — so areas you've viewed once work fully offline afterwards. This works in the
 * Android WebView too, where a service worker can't run from a file:// page (the
 * service worker handles the same job for the hosted PWA).
 *
 * It degrades safely: on any IndexedDB/network/CORS error it falls back to the
 * plain tile URL, so the map can never be worse than an ordinary tile layer.
 * Tiles are only stored as you pan/zoom while online (never bulk pre-fetched),
 * in line with OpenStreetMap's tile usage policy.
 */
(function () {
  const DB_NAME = "mobis-tiles", STORE = "tiles";
  let dbp = null;

  function db() {
    if (dbp) return dbp;
    dbp = new Promise((resolve, reject) => {
      let req;
      try { req = indexedDB.open(DB_NAME, 1); } catch (e) { return reject(e); }
      req.onupgradeneeded = () => req.result.createObjectStore(STORE);
      req.onsuccess = () => resolve(req.result);
      req.onerror = () => reject(req.error);
    }).catch(() => null);
    return dbp;
  }

  async function idbGet(key) {
    const d = await db();
    if (!d) return null;
    return new Promise((res) => {
      try {
        const r = d.transaction(STORE, "readonly").objectStore(STORE).get(key);
        r.onsuccess = () => res(r.result || null);
        r.onerror = () => res(null);
      } catch (e) { res(null); }
    });
  }

  async function idbPut(key, blob) {
    const d = await db();
    if (!d) return;
    try { d.transaction(STORE, "readwrite").objectStore(STORE).put(blob, key); } catch (e) {}
  }

  if (!window.L || !L.TileLayer) return;

  const CachedTileLayer = L.TileLayer.extend({
    createTile(coords, done) {
      const tile = document.createElement("img");
      tile.setAttribute("role", "presentation");
      tile.alt = "";
      const url = this.getTileUrl(coords);

      let settled = false;
      const finish = () => { if (!settled) { settled = true; done(null, tile); } };
      tile.addEventListener("load", finish);
      tile.addEventListener("error", finish);

      const useBlob = (blob) => {
        const obj = URL.createObjectURL(blob);
        tile.addEventListener("load", () => { try { URL.revokeObjectURL(obj); } catch (e) {} }, { once: true });
        tile.src = obj;
      };

      idbGet(url).then((blob) => {
        if (blob) { useBlob(blob); return; }
        if (!navigator.onLine) { finish(); return; }   // offline + uncached → blank
        fetch(url, { mode: "cors" })
          .then((r) => (r && r.ok) ? r.blob() : Promise.reject())
          .then((b) => { idbPut(url, b); useBlob(b); })
          .catch(() => { tile.src = url; });            // CORS/network hiccup → direct
      }).catch(() => { tile.src = url; });

      return tile;
    },
  });

  // Drop-in factory: returns the caching layer, or a plain tile layer if the
  // browser lacks IndexedDB.
  window.makeTileLayer = function (template, opts) {
    if (!("indexedDB" in window)) return L.tileLayer(template, opts);
    return new CachedTileLayer(template, opts);
  };
})();