Edit

Tiled Layer Rendering in an Offscreen Canvas

worker2 offscreencanvas2 tiles8

Simplified example of a tiled layer strategy using a map that lives inside a worker.

The tiles of the map in this example are rendered in a web worker, using OffscreenCanvas. This can be improved by using a pool of workers to enable parallel, multi-threaded rendering.

main.js
import Map from 'ol/Map.js';
import View from 'ol/View.js';
import TileLayer from 'ol/layer/Tile.js';
import ImageTileSource from 'ol/source/ImageTile.js';
const worker = new Worker('./worker.js', {type: 'module'});

const tileQueue = [];

new Map({
  layers: [
    new TileLayer({
      source: new ImageTileSource({
        tileSize: 512,
        loader: (z, x, y) => {
          return new Promise((resolve) => {
            const loadTile = () => {
              const handleMessage = ({data: {action, imageData}}) => {
                if (action !== 'rendered') {
                  return;
                }
                worker.removeEventListener('message', handleMessage);
                resolve(imageData);
                tileQueue.shift();
                const loadNextTile = tileQueue[0];
                loadNextTile?.();
              };
              worker.addEventListener('message', handleMessage);
              worker.postMessage({action: 'render', tile: [z, x, y]});
            };
            if (tileQueue.length === 0) {
              loadTile();
            }
            tileQueue.push(loadTile);
          });
        },
        attributions: [
          '<a href="https://www.openstreetmap.org/copyright" target="_blank">© OpenStreetMap contributors</a>',
        ],
      }),
    }),
  ],
  target: 'map',
  view: new View({
    center: [0, 0],
    zoom: 2,
  }),
});
index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>Tiled Layer Rendering in an Offscreen Canvas</title>
    <link rel="stylesheet" href="node_modules/ol/ol.css">
    <style>
      .map {
        width: 100%;
        height: 400px;
      }
      .map {
        background: rgba(232, 230, 223, 1);
        position: relative;
      }
      .map .ol-rotate {
        left: .5em;
        bottom: .5em;
        top: auto;
        right: auto;
      }

    </style>
  </head>
  <body>
    <div id="map" class="map"></div>

    <script type="module" src="main.js"></script>
  </body>
</html>
worker.js
import Map from 'ol/Map.js';
import View from 'ol/View.js';
import TileLayer from 'ol/layer/Tile.js';
import OSM from 'ol/source/OSM.js';
import {createXYZ} from 'ol/tilegrid.js';

const worker = self;

const tileGrid = createXYZ({
  tileSize: [512, 512],
});

const canvas = new OffscreenCanvas(512, 512);

const map = new Map({
  target: canvas,
  layers: [
    new TileLayer({
      source: new OSM({
        // No need to fade in tiles in the worker
        transition: 0,
      }),
    }),
  ],
});

worker.addEventListener('message', async ({data: {action, tile}}) => {
  if (action !== 'render') {
    return;
  }
  const view = new View({
    center: tileGrid.getTileCoordCenter(tile),
    resolution: tileGrid.getResolution(tile[0]),
  });
  map.setView(view);
  map.once('rendercomplete', () => {
    const imageData = canvas.transferToImageBitmap();
    worker.postMessage({action: 'rendered', imageData: imageData}, [imageData]);
  });
});
package.json
{
  "name": "tiled-layer-rendering-in-offscreen-canvas",
  "dependencies": {
    "ol": "10.9.0"
  },
  "devDependencies": {
    "vite": "^3.2.3"
  },
  "scripts": {
    "start": "vite",
    "build": "vite build"
  }
}