Edit

Map Export

export3 png1 geotiff5 worldfile1

Download

Export a map as a GeoTIFF or PNG image.

Example of exporting a map, using a HTMLCanvasElement as temporary map target. PNG is easy to export, without using an external library. The GeoTIFF export is georeferenced with the correct projection. The PNG + worldfile ZIP is also georeferenced, but users of the image will have to know (or guess) the projection.

main.js
import {zip} from 'fflate';
import {writeArrayBuffer as writeGeotiff} from 'geotiff';
import Map from 'ol/Map.js';
import View from 'ol/View.js';
import {asArray} from 'ol/color.js';
import GeoJSON from 'ol/format/GeoJSON.js';
import HeatmapLayer from 'ol/layer/Heatmap.js';
import VectorLayer from 'ol/layer/Vector.js';
import VectorSource from 'ol/source/Vector.js';
import Fill from 'ol/style/Fill.js';
import Style from 'ol/style/Style.js';

const style = new Style({
  fill: new Fill({
    color: '#eeeeee',
  }),
});

const map = new Map({
  layers: [
    new VectorLayer({
      source: new VectorSource({
        url: 'https://openlayers.org/data/vector/ecoregions.json',
        format: new GeoJSON(),
      }),
      background: 'white',
      style: function (feature) {
        const color = asArray(feature.get('COLOR_NNH') || '#eeeeee');
        color[3] = 0.75;
        style.getFill().setColor(color);
        return style;
      },
    }),
    new HeatmapLayer({
      source: new VectorSource({
        url: 'data/geojson/world-cities.geojson',
        format: new GeoJSON(),
      }),
      weight: function (feature) {
        return feature.get('population') / 1e7;
      },
      radius: 15,
      blur: 15,
      opacity: 0.75,
    }),
  ],
  target: 'map',
  view: new View({
    center: [0, 0],
    zoom: 2,
  }),
});

document.getElementById('export-map').addEventListener('click', () => {
  const format = document.getElementById('export-format').value;
  const mapCanvas = document.createElement('canvas');
  const size = map.getSize();
  mapCanvas.width = size[0];
  mapCanvas.height = size[1];

  map.setTarget(mapCanvas);
  map.once('rendercomplete', () => {
    const view = map.getView();
    const extent = view.calculateExtent(size);
    const resolution = view.getResolution();
    const projection = view.getProjection();

    if (format === 'geotiff') {
      exportGeoTIFF(mapCanvas, size, extent, resolution, projection);
    } else if (format === 'png') {
      exportPNG(mapCanvas);
    } else if (format === 'png-world') {
      exportPNGWithWorldfile(mapCanvas, extent, resolution);
    }

    map.setTarget('map');
  });
});

function exportGeoTIFF(canvas, size, extent, resolution, projection) {
  const context = canvas.getContext('2d');
  const imageData = context.getImageData(0, 0, size[0], size[1]);
  const epsgCode = projection.getCode().split(':')[1];

  const tiff = writeGeotiff(imageData.data, {
    width: size[0],
    height: size[1],
    ModelPixelScale: [resolution, resolution, 0],
    ModelTiepoint: [0, 0, 0, extent[0], extent[3], 0],
    GTRasterTypeGeoKey: 1,
    ProjectedCSTypeGeoKey: parseInt(epsgCode),
  });

  const blob = new Blob([tiff], {type: 'image/tiff'});
  downloadFile(blob, 'map-export.tiff');
}

function exportPNG(canvas) {
  const link = document.createElement('a');
  link.href = canvas.toDataURL('image/png');
  link.download = 'map-export.png';
  document.body.appendChild(link);
  link.click();
  document.body.removeChild(link);
}

function exportPNGWithWorldfile(canvas, extent, resolution) {
  // Create worldfile content
  const worldfileContent = [
    resolution.toFixed(6), // pixel width
    '0.000000', // rotation
    '0.000000', // rotation
    (-resolution).toFixed(6), // pixel height (negative)
    extent[0].toFixed(6), // upper-left X
    extent[3].toFixed(6), // upper-left Y
  ].join('\n');

  // Convert canvas to blob and create zip
  canvas.toBlob((pngBlob) => {
    pngBlob.arrayBuffer().then((pngBuffer) => {
      const files = {
        'map-export.png': [new Uint8Array(pngBuffer), {level: 0}], // level 0 = no compression for PNG
        'map-export.pgw': [
          new TextEncoder().encode(worldfileContent),
          {level: 6},
        ],
      };

      zip(files, (err, data) => {
        if (err) {
          alert('Error creating zip:', err);
          return;
        }
        const zipBlob = new Blob([data], {type: 'application/zip'});
        downloadFile(zipBlob, 'map-export.zip');
      });
    });
  });
}

function downloadFile(blob, filename) {
  const link = document.createElement('a');
  link.href = URL.createObjectURL(blob);
  link.download = filename;
  document.body.appendChild(link);
  link.click();
  document.body.removeChild(link);
}
index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>Map Export</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css">
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/[email protected]/css/fontawesome.min.css">
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/[email protected]/css/solid.css">
    <link rel="stylesheet" href="node_modules/ol/ol.css">
    <style>
      .map {
        width: 100%;
        height: 400px;
      }
    </style>
  </head>
  <body>
    <div id="map" class="map"></div>
    <div class="btn-group">
      <a id="export-map" class="btn btn-outline-dark" role="button"><i class="fa fa-download"></i> Download</a>
      <select id="export-format" class="form-select" style="max-width: 180px;">
        <option value="geotiff">GeoTIFF</option>
        <option value="png">PNG</option>
        <option value="png-world">PNG + Worldfile (ZIP)</option>
      </select>
    </div>
    <a id="image-download" download="map.png"></a>

    <script type="module" src="main.js"></script>
  </body>
</html>
package.json
{
  "name": "export-map",
  "dependencies": {
    "ol": "10.9.0",
    "fflate": "^0.8.2",
    "geotiff": "^3.0.5 || ^3.1.0-beta.0"
  },
  "devDependencies": {
    "vite": "^3.2.3"
  },
  "scripts": {
    "start": "vite",
    "build": "vite build"
  }
}