Offline areas

Quick start

The example below, running in the Mappia calculator - click it to run it live. Full-size picture

  1. Run it

    Click the picture: the map opens right here and runs the example. Nothing is saved, and nothing to install.

  2. Try it
    • Press “How many tiles is this view?”: the label counts the tiles of this view over three zoom levels.
    • Pan or zoom the map and press it again: the estimate follows the current view.
    • Use zoom + 4 instead of zoom + 2 in estimate: the count grows about fourfold per extra level.
  3. Make it yours

    Copy the query, change it - the key parameters are below - and run it again in the playground, or paste it into the Mappia editor to save it as your map.

Key parameters

ParameterExampleWhat it does
getCacheableLayersExtjsUtils.OFFLINE.getCacheableLayers()Lists the layers on the map whose tiles can be saved. Pass the list, or part of it, as layers.
estimateTileCountForLayersExtjsUtils.OFFLINE.estimateTileCountForLayers(map.getExtent(), 10, 13, layers)Counts the tiles an area would need, without downloading anything. Check it before every download.
downloadAreaExtjsUtils.OFFLINE.downloadArea({ name: "Farm 4711", extent: map.getExtent(), zoomMin: 10, zoomMax: 13 })Downloads the tiles of an extent and zoom range into the browser; resolves with the area’s record.
setServingPolicyExtjsUtils.OFFLINE.setServingPolicy({ mode: "CacheOnly" })Chooses whether tiles come from the download or the network. The map reloads its tiles at once.
listAreasExtjsUtils.OFFLINE.listAreas()Resolves with every area saved in this browser: id, name, status, tile count and size.
deleteAreaExtjsUtils.OFFLINE.deleteArea(area.id)Removes an area’s tiles and record from the browser.

Every parameter, with its type and default, is in the reference at the end of this page.

Complete example

The query 26 lines · runs as is
// Offline areas: ExtjsUtils.OFFLINE downloads an area's tiles for use without a connection.
// Estimating first costs nothing - it only counts the tiles of the chosen layers and zooms.
[
  {
    title: "Offline areas",
    name: "CSR:estados",
    source: "calculate",
    opacity: 0.5,
    visibility: true,
    paramsButtonConfig: [{ type: "query", pressed: true }],
    descriptionHtml:
      "{{button|id=estimate_area|text=How many tiles is this view?|handler=estimate}}" +
      "{{label|id=estimate_label|text=Press the button to estimate.}}",
    functions: {
      estimate: function () {
        var map = ExtjsUtils.JS.getMap();
        var zoom = map.getZoom();
        var layers = ExtjsUtils.OFFLINE.getCacheableLayers();
        var tiles = ExtjsUtils.OFFLINE.estimateTileCountForLayers(map.getExtent(), zoom, zoom + 2, layers);
        Ext.getCmp("estimate_label").setText(
          "This view at zoom " + zoom + "-" + (zoom + 2) + ": " + tiles + " tiles for " + layers.length + " layer(s)."
        );
      },
    },
  },
];

Customize it

The steps

  1. Choose the layers: getCacheableLayers() lists the ones that can be saved.
  2. Estimate: estimateTileCountForLayers(extent, zoomMin, zoomMax, layers) counts the tiles, and checkStorageQuota() tells how much space the browser grants.
  3. Download: downloadArea({...}) saves the tiles in the browser.
var map = ExtjsUtils.JS.getMap();
ExtjsUtils.OFFLINE.downloadArea({
  name: "Farm 4711",
  extent: map.getExtent(),
  zoomMin: map.getZoom(),
  zoomMax: map.getZoom() + 3,
  layers: ExtjsUtils.OFFLINE.getCacheableLayers(),
  onProgress: function (p) { console.log(p.done + " / " + p.total); },
}).then(function (area) {
  console.log(area.status, area.tileCount + " tiles");
});

Run this from a button handler or another callback, after the map has loaded.

Which layers can be saved

Tiled catalogue maps and tile basemaps such as OpenStreetMap and XYZ layers. Calculated layers that use an operation cannot, because their images depend on the view rather than on a tile grid, and neither can Google or Bing basemaps, whose terms forbid it. Call getCacheableLayers after the query’s layers are on the map; in code that runs right after loading, wait for ExtjsUtils.OFFLINE.whenLayersReady() first.

Size limits

Each extra zoom level multiplies the tiles by about four. An area may hold at most 20,000 tiles: above that, downloadArea refuses with “Area too large” before downloading anything. Pass maxTiles to change the limit for one call.

How saved tiles are served

A policy chosen by the page; changing it reloads the tiles on screen, so the difference shows at once:

modeBehaviour
CacheFirst (default)The saved tile if there is one, the network otherwise
NetworkFirstThe live tile; the saved one only when the network fails
CacheOnlyOnly saved tiles - never the network
NetworkOnlyOnly the network - ignore saved tiles

When a tile at the current zoom was not saved, the nearest coarser saved zoom is cropped to fill it, so zooming past the saved range blurs instead of leaving holes.

Managing areas

  • Download again with the same id to resume or extend an area: tiles already saved are not fetched again.
  • An aborted download, or one with failed tiles, ends with status partial.
  • listAreas() and deleteArea(id) list and remove areas.
  • exportAreaAsZip(id) packs an area into one file, and importAreaFromZip(blob) rebuilds it on another device without downloading the tiles again.

Check that it really works offline

Saved tiles are served by the browser’s service worker, which only takes control of a page from its next load. verifyAreaServed(id) re-requests a sample of an area’s tiles and reports whether they really came from the download.

Reference: the parameters used here

Generated from the platform source. Every entry, searchable, is in the API reference; the raw data is api.json. Open an entry for its description, parameters and example; # links to it.

OFFLINE · offline areas

OFFLINE13 entries
Offline area engine: one IndexedDB record and one Cache Storage bucket per area.
Usage: ExtjsUtils.OFFLINE
Runnable example: the offline-areas demo in the repository (examples/offline-areas-demo).

checkStorageQuota

function() : Promise.<Object>helper# Reports the browser's storage quota for this origin (navigator.storage.estimate): how much is used, how much the browser grants and the difference.

Written as ExtjsUtils.OFFLINE.checkStorageQuota

Reports the browser's storage quota for this origin (navigator.storage.estimate): how much is used, how much the browser grants and the difference. It says nothing about a particular area — compare available with estimateAreaBytes before a download. Older browsers without the Storage API report {unknown: true}.

Returns {unknown: false, usage, quota, available} in bytes, or {unknown: true}.

ExtjsUtils.OFFLINE.checkStorageQuota().then(function(q) {
    if (!q.unknown) console.log(Math.round(q.available / 1048576) + " MB free of " + Math.round(q.quota / 1048576));
});

deleteArea

function(id, options) : Promise.<undefined>helper# Deletes an area completely: its Cache Storage bucket (all its tiles) and its IndexedDB record, and tells the service worker.

Written as ExtjsUtils.OFFLINE.deleteArea

Deletes an area completely: its Cache Storage bucket (all its tiles) and its IndexedDB record, and tells the service worker. The shared definitions bucket is not touched (other areas of the same query may use it; resetAllOfflineData clears it). Waits for the worker to confirm it rebuilt its index without the deleted bucket, then asks the map to request its tiles again (reloadMapTiles), so the screen shows the difference at once instead of at the next pan or zoom. Pass {reloadTiles: false} to skip that (e.g. deleting several areas right before an import replaces them — see importAreaFromZip's replaceAreaIds, which does this itself). Safe to call for an id that no longer exists.

id String
The area id.
options Object
{reloadTiles} — defaults to true.

Returns Resolves once both the cache and the record are gone.

ExtjsUtils.OFFLINE.deleteArea(areaId).then(function() { console.log("deleted"); });

downloadArea

function(options) : Promise.<Object>helper# Downloads (or resumes) one offline area: every tile of the chosen layers over the extent and zoom range, one HTTP request per tile with a bounded concurrency, into the area's own Cache Storage bucket …

Written as ExtjsUtils.OFFLINE.downloadArea

Downloads (or resumes) one offline area: every tile of the chosen layers over the extent and zoom range, one HTTP request per tile with a bounded concurrency, into the area's own Cache Storage bucket — plus, unless includeDefinitions: false, the query's definitions (pruned GetCapabilities + legends) into their shared bucket, in parallel (see downloadDefinitions). The area record is saved as 'downloading' first and finished as 'ready', or 'partial' when aborted or when a tile/definition failed. Resumable: pass the same id again and tiles already in the cache are not re-fetched (with refresh: true they are, each replacing its stored copy — never a second one); with a different extent, zoom range or maps the area grows, and its record widens to cover both (e.g. one id per imóvel, downloaded again after it is re-drawn). The serving policy (see setServingPolicy) is forced to "NetworkOnly" for the download's duration — so every request really goes to the network, never gets answered by the worker from a bucket — and put back to whatever it was, once it settles. Rejects without any request when the estimated tile count exceeds maxTiles ("Area too large"). The engine works without a query id: queryId defaults to the page's ?queryid parameter and is only stored on the record. The service worker serves the cached tiles when the page is offline; use verifyAreaServed to check.

options Object
Area definition and download options.
options.name String
Display name of the area (defaults to the id).
options.extent OpenLayers.Bounds|Array.<Number>|Object
The area, in map units: an OpenLayers.Bounds, a [left, bottom, right, top] array or any {left, bottom, right, top} object (e.g. map.getExtent()).
options.zoomMin Number
The first zoom level to download.
options.zoomMax Number
The last zoom level to download (inclusive).
options.queryId String
Query id recorded on the area; defaults to the ?queryid URL parameter (may be empty — not required).
options.id String
Id of an existing area to resume/refresh; omitted ⇒ createAreaId().
options.layers Array.<Object>
Targets from getCacheableLayers to include; omitted ⇒ layerNames, or every cacheable map. Without it the download first waits for the query's layers (whenLayersReady).
options.layerNames Array.<String>
Map names to include (WMS LAYERS, or "OSM Mapnik") — see selectCacheableLayers. Rejects, before any request, when one of them is not on the map.
options.maxTiles Number
Tile-count cap for this call; defaults to MAX_TILES_PER_AREA.
options.concurrency Number
Parallel tile requests; defaults to CONCURRENCY.
options.signal AbortSignal
Aborts the download; what was fetched stays cached and the area ends 'partial'.
options.includeDefinitions Boolean
false to download tiles only.
options.refresh Boolean
true to fetch the tiles already stored again, overwriting them (e.g. updated imagery).
options.onProgress function
Called after each tile with {done, total, failed, bytes} (tiles only).

Returns The saved area record (see listAreas), with status 'ready' or 'partial'.

var map = ExtjsUtils.JS.getMap();
var controller = new AbortController();
ExtjsUtils.OFFLINE.downloadArea({
    name: "Fazenda Santa Clara",
    extent: map.getExtent(),
    zoomMin: map.getZoom(),
    zoomMax: map.getZoom() + 3,
    layers: ExtjsUtils.OFFLINE.getCacheableLayers(),
    signal: controller.signal,
    onProgress: function(p) { console.log(p.done + "/" + p.total + " tiles, " + p.failed + " failed"); }
}).then(function(area) {
    console.log("area " + area.id + " is " + area.status);
}).catch(function(err) {
    alert(err.message); // e.g. "Area too large: ~35000 tiles exceeds the 20000 cap..."
});
// controller.abort() cancels; the partial area can be resumed later with {id: area.id}

estimateTileCountForLayers

function(extent, zoomMin, zoomMax, layers) : Numberhelper# Upper bound on the tile requests an area would need for these layers over the zoom range, honouring each layer's maxZoom (zooms above it count once, at the limit).

Written as ExtjsUtils.OFFLINE.estimateTileCountForLayers

Upper bound on the tile requests an area would need for these layers over the zoom range, honouring each layer's maxZoom (zooms above it count once, at the limit). Still pure arithmetic — no URL is built — so run it first, before enumerating or fetching anything, to reject an oversized area cheaply: it is the same number downloadArea compares against MAX_TILES_PER_AREA.

extent OpenLayers.Bounds|Array.<Number>|Object
The area, in map units: an OpenLayers.Bounds, a [left, bottom, right, top] array or any {left, bottom, right, top} object.
zoomMin Number
The first zoom level to cover.
zoomMax Number
The last zoom level to cover (inclusive).
layers Array.<Object>
Targets from getCacheableLayers; omitted ⇒ every cacheable layer on the map.

Returns The estimated tile count, 0 without a map.

var extent = ExtjsUtils.JS.getMap().getExtent();
var rough = ExtjsUtils.OFFLINE.estimateTileCountForLayers(extent, 10, 14, selectedLayers);
if (rough > ExtjsUtils.OFFLINE.MAX_TILES_PER_AREA) {
    alert("Too large: ~" + rough + " tiles. Narrow the extent or the zoom range.");
}

exportAreaAsZip

function(id, options) : Promise.<Blob>helper# Packs an existing area — every tile currently in its cache — into one .zip Blob that is self-describing: a manifest.json (entries {url, path, map, contentType, status} plus an area block with …

Written as ExtjsUtils.OFFLINE.exportAreaAsZip

Packs an existing area — every tile currently in its cache — into one .zip Blob that is self-describing: a manifest.json (entries {url, path, map, contentType, status} plus an area block with the record's id, name, extent, zoom range, layer names and query id) and one file per tile, laid out one folder per map (<map>/<host>/<path>, e.g. mapnik/tile.openstreetmap.org/16/23456/37891.png). importAreaFromZip/importAreaFromUrl rebuild the area from it on another device without re-downloading a single tile — host the file anywhere. Tiles are stored uncompressed (they are PNG/JPEG already) and the zip is assembled in memory: fine for the thousands of tiles an area holds, not for hundreds of MB. Pair it with exportAreaFilename and saveBlobAsFile to hand the pack to the user.

id String
The area id.
options Object
Export options.
options.onProgress function
Called per tile with {phase: 'packing', done, total, failed, bytes}.

Returns The zip file; rejects when no area has that id.

ExtjsUtils.OFFLINE.getArea(areaId).then(function(area) {
    return ExtjsUtils.OFFLINE.exportAreaAsZip(areaId).then(function(blob) {
        ExtjsUtils.OFFLINE.saveBlobAsFile(blob, ExtjsUtils.OFFLINE.exportAreaFilename(area));
    });
});

getCacheableLayers

function() : Array.<Object>helper# Lists the layers currently on the map whose tiles can be cached for offline use, as {layer, kind} "targets" — the objects every other function here accepts as layers.

Written as ExtjsUtils.OFFLINE.getCacheableLayers

Lists the layers currently on the map whose tiles can be cached for offline use, as {layer, kind} "targets" — the objects every other function here accepts as layers. Two kinds: 'storage' (a CustomWMS layer served from a hosted layer store — addRemoteWMSServer with storage — recursing into a Composed layer's inner layers) and 'grid' (any other tiled OpenLayers.Layer.Grid: XYZ/OSM basemaps and plain named WMS layers such as CSR:cultivo_cafe_cafe). Excluded: calculate layers with an operation but no store (their requests depend on the viewport, not on a tile grid) and Google/Bing basemaps (their terms forbid caching). Filter the result to let the user pick which maps to include; the WMS name of a target is target.layer.params.LAYERS (target.layer.name for XYZ/OSM). Wait for whenLayersReady before calling it, or a {name: "CSR:..."} layer may not be on the map yet; selectCacheableLayers picks maps by name.

Returns Targets {layer: OpenLayers.Layer, kind: 'storage'|'grid'}, or [] without a map.

var targets = ExtjsUtils.OFFLINE.getCacheableLayers();
var selected = targets.filter(function(t) {
    var key = (t.layer.params && t.layer.params.LAYERS) || t.layer.name;
    return ["CSR:cultivo_cafe_cafe", "mapnik"].indexOf(key) !== -1;
});

getServingPolicy

function() : Promise.<Object>helper# How the service worker answers map requests that hit (or miss) an offline bucket: {mode} with one of "CacheFirst" (default: bucket first, network on miss), "NetworkFirst" (live first, bucket …

Written as ExtjsUtils.OFFLINE.getServingPolicy

How the service worker answers map requests that hit (or miss) an offline bucket: {mode} with one of "CacheFirst" (default: bucket first, network on miss), "NetworkFirst" (live first, bucket when the network fails), "CacheOnly" (bucket / soft crop only — never the network), "NetworkOnly" (live only — never the bucket). Legacy kebab-case values (cache-first, …) are accepted and normalized. forceOffline: true makes the worker treat the network as down no matter what navigator.onLine says — every mode then behaves as it would with no network at all (an "Offline" preset is {mode: "CacheOnly", forceOffline: true}). Stored in Cache Storage (OfflineCacheKey.SETTINGS_CACHE_NAME); resetAllOfflineData clears it.

Returns {mode, forceOffline, updatedAt} — the default (forceOffline: false, updatedAt: null) when nothing was set yet.

ExtjsUtils.OFFLINE.getServingPolicy().then(function(policy) { console.log(policy.mode); });

importAreaFromZip

function(zipBlob, options) : Promise.<Object>helper# Rebuilds an offline area from a pack produced by exportAreaAsZip (or any zip following the manifest contract) — "load the tile cache from this file instead of downloading every tile".

Written as ExtjsUtils.OFFLINE.importAreaFromZip

Rebuilds an offline area from a pack produced by exportAreaAsZip (or any zip following the manifest contract) — "load the tile cache from this file instead of downloading every tile". The area record comes from the pack's manifest.area and can be overridden by options; a pack without an area block still imports with whatever the options give. Re-importing a pack with the same id refreshes that area in place (entries are overwritten, never duplicated). Definitions are not part of a pack; run downloadDefinitions separately when the device needs them. See importAreaFromUrl for packs hosted on a server.

zipBlob Blob
The pack, e.g. from an <input type="file"> or a fetch().blob().
options Object
Import options.
options.id String
Area id to create/refresh; defaults to the pack's, else a new id.
options.name String
Area name; defaults to the existing/packed name, else the id.
options.extent Array.<Number>
[left, bottom, right, top] in map units; defaults to the packed extent.
options.zoomMin Number
Defaults to the packed value.
options.zoomMax Number
Defaults to the packed value.
options.layerNames Array.<String>
Map names the pack covers; defaults to the packed list.
options.queryId String
Defaults to the packed value.
options.replaceAreaIds Array.<String>
Ids of other areas to delete (cache + record) BEFORE unpacking — the "one property open at a time" tool: {id: 'current', replaceAreaIds: ['current']} never keeps more than one area's tiles on disk, freeing space before the new pack lands.
options.signal AbortSignal
Aborts the unpacking; the area ends 'partial'.
options.onProgress function
Called per entry with {phase: 'unpacking', done, total, failed, bytes}.
options.sourceUrl String
Recorded as importedFrom (set automatically by importAreaFromUrl).

Returns The saved area record (see listAreas).

// <input type="file" id="pack"> in the host page, forwarded as a Blob
ExtjsUtils.OFFLINE.importAreaFromZip(fileBlob, {
    name: "Fazenda Santa Clara",
    onProgress: function(p) { console.log(p.done + "/" + p.total); }
}).then(function(area) { console.log("imported", area.id, area.tileCount + " tiles"); });

listAreas

function() : Promise.<Array.<Object>>helper# Lists every offline area saved on this browser, as area records.

Written as ExtjsUtils.OFFLINE.listAreas

Lists every offline area saved on this browser, as area records. An area record is a plain object with id, name, extent ([left, bottom, right, top] in map units), zoomMin, zoomMax, layerNames (the WMS map names / basemap names it covers), queryId, status ('downloading', 'ready' or 'partial' — partial means aborted or some tile/definition failed), tileCount, failedCount, bytes, createdAt, updatedAt and, when definitions were downloaded, definitionsId, definitionsBytes, definitionsFailedCount; imported packs also carry importedFrom. This is what an "offline areas" list in a host page shows.

Returns The area records, in store order.

ExtjsUtils.OFFLINE.listAreas().then(function(areas) {
    areas.forEach(function(area) {
        console.log(area.name, area.status, area.tileCount + " tiles", Math.round(area.bytes / 1048576) + " MB");
    });
});

reloadMapTiles

function(options) : Numberhelper# Asks the tiled layers on the map for their tiles again, so what the screen shows follows the current serving policy and bucket contents right away instead of at the next pan or zoom.

Written as ExtjsUtils.OFFLINE.reloadMapTiles

Asks the tiled layers on the map for their tiles again, so what the screen shows follows the current serving policy and bucket contents right away instead of at the next pan or zoom. Every tile URL gets a fresh _dc value, so the browser cannot answer from its own memory/HTTP cache and the service worker sees each request; OfflineCacheKey.normalize drops _dc, so the offline buckets still match. WMS layers (and the maps inside a Composed layer) carry it as a request parameter, XYZ/OSM basemaps on their URL template. Hidden layers only get the new URLs (they request them when shown). setServingPolicy calls this for you.

options Object
{layers} — the OpenLayers.Layers, or getCacheableLayers() targets, to reload; default: every layer on the map.

Returns How many layers were asked to reload.

ExtjsUtils.OFFLINE.reloadMapTiles();
// only the downloaded-area layers
ExtjsUtils.OFFLINE.reloadMapTiles({ layers: ExtjsUtils.OFFLINE.getCacheableLayers() });

setServingPolicy

function(policy) : Promise.<Object>helper# Sets the serving policy (see getServingPolicy), tells the worker — which applies it from the next request on, no page reload — and then asks the map to request its tiles again (reloadMapTiles), …

Written as ExtjsUtils.OFFLINE.setServingPolicy

Sets the serving policy (see getServingPolicy), tells the worker — which applies it from the next request on, no page reload — and then asks the map to request its tiles again (reloadMapTiles), so the screen shows the new mode at once instead of at the next pan or zoom. Pass reloadTiles: false to only store the policy (e.g. right before a download).

policy Object
{mode, forceOffline, reloadTiles} — mode is "CacheFirst", "NetworkFirst", "CacheOnly" or "NetworkOnly" (legacy kebab-case accepted); forceOffline (default false) makes the worker treat the network as down regardless of navigator.onLine — any call without it turns simulated offline back off; reloadTiles defaults to true.

Returns The stored policy {mode, forceOffline, updatedAt}.

ExtjsUtils.OFFLINE.setServingPolicy({ mode: "CacheFirst" });
// store only; the tiles on screen keep what they show
ExtjsUtils.OFFLINE.setServingPolicy({ mode: "NetworkOnly", reloadTiles: false });
// simulate being offline, without DevTools throttling
ExtjsUtils.OFFLINE.setServingPolicy({ mode: "CacheOnly", forceOffline: true });

suggestMaxZoom

function(target) : Object|nullhelper# Suggests the finest zoom at which a raster layer still adds real detail, from the pixel size its GetCapabilities abstract declares (the platform's rasters carry a generated "Cell Width : N" line; …

Written as ExtjsUtils.OFFLINE.suggestMaxZoom

Suggests the finest zoom at which a raster layer still adds real detail, from the pixel size its GetCapabilities abstract declares (the platform's rasters carry a generated "Cell Width : N" line; e.g. a 29.04 m cell size gives zoom 13 on the Web Mercator series, so zoom 14+ is pure upsampling). Cell sizes below 1 are read as degrees and converted at the equator. It is a suggestion, not a setting: pass result.maxZoom to layer.setMaxZoom() (or put maxZoom in the layer config) to actually apply it, which shrinks the offline area (see effectiveZoom). The layer's capabilities record must be loaded (wait for the "local" source).

target Object|OpenLayers.Layer
A {layer, kind} entry from getCacheableLayers, or the layer itself.

Returns {cellSize, metersPerCell, maxZoom}, or null when the abstract declares no cell size.

ExtjsUtils.OFFLINE.getCacheableLayers().forEach(function(t) {
    var suggestion = ExtjsUtils.OFFLINE.suggestMaxZoom(t);
    if (suggestion && typeof t.layer.setMaxZoom === "function") t.layer.setMaxZoom(suggestion.maxZoom);
});

verifyAreaServed

function(id, options) : Promise.<Object>helper# Proves an area is really served offline.

Written as ExtjsUtils.OFFLINE.verifyAreaServed

Proves an area is really served offline. It re-requests a sample of the URLs in the area's cache through the page's normal fetch path (bypassing the HTTP cache), so the service worker sees them like any tile request, and counts the responses the worker stamped as coming from this area's bucket. Without a controlling worker nothing can be served from the cache at all — controlled: false says so (on localhost the worker is off unless the service-worker config enables it, and a freshly registered worker only controls the page from its next load). It also checks coverage against the bucket's own metadata: re-enumerates the tiles the recorded extent, zoom range and layers imply — with the live layers, so the same query must be loaded — and reports which are missing from the cache.

id String
The area id.
options Object
Verification options.
options.sampleSize Number
How many cached URLs to re-request; 0 checks every entry.
options.signal AbortSignal
Aborts the sample requests.
options.coverage Boolean
false to skip the coverage re-enumeration.

Returns {id, cacheName, controlled, metadata, cachedEntries, checked, servedFromCache, servedByNetwork, failed, entries: [{url, status, servedFrom}], coverage: {layersFound, expected, present, missing, missingSample} | null}.

ExtjsUtils.OFFLINE.verifyAreaServed(areaId, {sampleSize: 30}).then(function(result) {
    if (!result.controlled) console.warn("no service worker controls this page yet");
    console.log(result.servedFromCache + "/" + result.checked + " served from the area cache");
    if (result.coverage) console.log(result.coverage.missing + " expected tiles missing");
});