Офлайн режим

Офлайн-режим в OpenLayers строится вокруг идеи отделения источников данных от слоя отображения карты. Основная сложность заключается не в самой библиотеке, а в организации локального хранения тайлов, векторных данных и стратегии их подгрузки при отсутствии сетевого соединения.

OpenLayers изначально не предоставляет единой «кнопки офлайн-режима», вместо этого он предлагает набор инструментов: источники (source), слои (layer), форматтеры (format) и механизмы перехвата запросов. Комбинация этих компонентов позволяет построить устойчивую систему работы без сети.

Ключевые элементы офлайн-архитектуры:

  • кеширование тайлов (raster tiles)
  • локальное хранение в IndexedDB или Cache Storage
  • использование service worker
  • предварительная загрузка регионов карты
  • управление стратегией подгрузки источников

Кеширование тайловых слоёв

Наиболее распространённый сценарий офлайн-карт — работа с тайлами XYZ. OpenLayers использует ol/source/XYZ, который делает HTTP-запросы к тайловому серверу.

Базовая конфигурация слоя

import Map from 'ol/Map';
import View from 'ol/View';
import TileLayer from 'ol/layer/Tile';
import XYZ from 'ol/source/XYZ';

const layer = new TileLayer({
  source: new XYZ({
    url: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
    crossOrigin: 'anonymous'
  })
});

const map = new Map({
  target: 'map',
  layers: [layer],
  view: new View({
    center: [0, 0],
    zoom: 2
  })
});

В онлайн-режиме всё работает напрямую, однако для офлайн-доступа требуется перехват сетевых запросов и их сохранение.


Service Worker как основа офлайн-доступа

Service Worker становится центральным компонентом системы кеширования. Он перехватывает запросы тайлов и решает, отдавать ли их из сети или локального хранилища.

Регистрация service worker

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js');
}

Перехват запросов тайлов

self.addEventListener('fetch', (event) => {
  const url = new URL(event.request.url);

  if (url.pathname.includes('/tile/')) {
    event.respondWith(
      caches.match(event.request).then((cached) => {
        if (cached) {
          return cached;
        }

        return fetch(event.request).then((response) => {
          const clone = response.clone();
          caches.open('tile-cache').then((cache) => {
            cache.put(event.request, clone);
          });
          return response;
        });
      })
    );
  }
});

В данном подходе реализуется стратегия cache-first, где приоритет отдаётся локальным данным.


Предзагрузка тайлов (preloading strategy)

Офлайн-карты требуют заранее подготовленного набора данных. Подход зависит от масштаба:

  • фиксированные регионы (город, область)
  • динамически выбранные bounding box
  • уровни масштабирования

Пример обхода тайлов в заданном регионе

function generateTileUrls(z, xStart, xEnd, yStart, yEnd) {
  const urls = [];

  for (let x = xStart; x <= xEnd; x++) {
    for (let y = yStart; y <= yEnd; y++) {
      urls.push(`https://tile.openstreetmap.org/${z}/${x}/${y}.png`);
    }
  }

  return urls;
}

Далее эти URL можно прогреть через fetch, что позволит service worker сохранить их в кеш.


Использование Cache Storage

Cache Storage API обеспечивает долговременное хранение ресурсов. В контексте OpenLayers это чаще всего тайлы, стили и GeoJSON.

Сохранение тайлов вручную

async function cacheTile(url) {
  const cache = await caches.open('tile-cache');
  const response = await fetch(url);
  await cache.put(url, response);
}

Проверка наличия тайла

async function getCachedTile(url) {
  const cache = await caches.open('tile-cache');
  const match = await cache.match(url);
  return match || null;
}

Cache Storage используется как второй уровень после service worker, обеспечивая устойчивость даже при повторных перезапусках приложения.


Офлайн работа с векторными данными

Помимо тайлов, OpenLayers активно используется для отображения векторных данных через ol/source/Vector.

Загрузка GeoJSON и локальное хранение

import VectorSource from 'ol/source/Vector';
import GeoJSON from 'ol/format/GeoJSON';

const source = new VectorSource({
  format: new GeoJSON(),
  url: '/data/regions.geojson'
});

Для офлайн-режима данные должны быть заранее сохранены.

Хранение GeoJSON в IndexedDB

function saveGeoJSON(key, data) {
  const request = indexedDB.open('map-db', 1);

  request.onupgradenee ded = () => {
    const db = request.result;
    db.createObjectStore('geojson');
  };

  request.onsucc ess = () => {
    const db = request.result;
    const tx = db.transaction('geojson', 'readwrite');
    tx.objectStore('geojson').put(data, key);
  };
}

Загрузка данных из IndexedDB

function loadGeoJSON(key) {
  return new Promise((resolve) => {
    const request = indexedDB.open('map-db', 1);

    request.onsucc ess = () => {
      const db = request.result;
      const tx = db.transaction('geojson', 'readonly');
      const store = tx.objectStore('geojson');
      const getRequest = store.get(key);

      getRequest.onsucc ess = () => {
        resolve(getRequest.result);
      };
    };
  });
}

После загрузки данные могут быть переданы в OpenLayers:

loadGeoJSON('regions').then((data) => {
  const features = new GeoJSON().readFeatures(data);

  source.addFeatures(features);
});

Стратегии деградации при отсутствии сети

Офлайн-режим требует корректного поведения при частичной доступности данных.

Многоуровневая стратегия источников:

  1. Cache Storage (быстрый доступ)
  2. IndexedDB (структурированные данные)
  3. Network fallback (при наличии сети)

Пример логики выбора источника

async function fetchTile(url) {
  const cached = await caches.match(url);
  if (cached) return cached;

  try {
    const response = await fetch(url);
    const cache = await caches.open('tile-cache');
    cache.put(url, response.clone());
    return response;
  } catch (e) {
    return null;
  }
}

Работа с стилями в офлайн-режиме

Если используется ol/style/Style и динамические ресурсы (иконки, SVG), они также должны быть закешированы.

Пример использования локальных иконок

import Icon from 'ol/style/Icon';
import Style from 'ol/style/Style';

const style = new Style({
  image: new Icon({
    src: '/offline/icons/marker.png'
  })
});

Service Worker должен перехватывать запрос /offline/icons/* и обеспечивать их локальную доставку.


Предварительное построение офлайн-пакета

Для сложных приложений используется стратегия сборки офлайн-пакета:

  • список тайлов по zoom-уровням
  • GeoJSON слои
  • стили и изображения
  • метаданные регионов

Генерация списка ресурсов

function buildOfflineManifest(region) {
  const manifest = [];

  for (let z = region.minZoom; z <= region.maxZoom; z++) {
    const tiles = generateTileUrls(
      z,
      region.xMin[z],
      region.xMax[z],
      region.yMin[z],
      region.yMax[z]
    );

    manifest.push(...tiles);
  }

  return manifest;
}

Контроль актуальности кеша

Офлайн-данные устаревают, поэтому вводится версия кеша.

const CACHE_VERSION = 'v3';

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(`tile-cache-${CACHE_VERSION}`)
  );
});

Удаление старых кешей:

self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then((keys) =>
      Promise.all(
        keys.map((key) => {
          if (!key.includes(CACHE_VERSION)) {
            return caches.delete(key);
          }
        })
      )
    )
  );
});

Ограничения офлайн-режима в OpenLayers

При работе без сети сохраняются структурные ограничения:

  • невозможность динамической генерации тайлов
  • рост объёма локального хранилища
  • необходимость точного контроля zoom-уровней
  • зависимость от заранее подготовленных данных
  • отсутствие актуализации внешних слоёв

Эти ограничения формируют архитектурный компромисс между автономностью и актуальностью данных.