inkmap
a library for generating high-quality, printable maps. Go to the API documentation

Give inkmap a JSON spec and it will use it to generate a PNG image in the background.

Use the print() API to begin printing a spec. This function returns a Promise which resolves to a Blob containing the map image.

Generate!
import { downloadBlob, print } from '@camptocamp/inkmap';

const root = document.getElementById('example-01');
const btn = /** @type {CustomButton} */ root.querySelector('custom-button');
const spec = /** @type {PrintSpecEditor} */ root.querySelector('print-spec');

// make sure the spec is valid to allow printing
spec.onValidityCheck((valid) => (btn.enabled = valid));

btn.addEventListener('click', async () => {
  btn.showSpinner();

  // create a job, get a promise that resolves when the job is finished
  const blob = await print(spec.value);

  btn.hideSpinner();

  downloadBlob(blob, 'inkmap.png');
});

Showing progress

inkmap also enables finer monitoring by making the job status observable.

Use the queuePrint() API to start a print job asynchronously. This function returns a Promise which resolves to a job id. You can then feed the job id to getJobStatus() which will return an observable that you can subscribe() to, emitting updates on the given print job status.

Generate!
import { downloadBlob, getJobStatus, queuePrint } from '@camptocamp/inkmap';

const root = document.getElementById('example-02');
const btn = /** @type {CustomButton} */ root.querySelector('custom-button');
const bar = /** @type {CustomProgress} */ root.querySelector('custom-progress');
const spec = /** @type {PrintSpecEditor} */ root.querySelector('print-spec');

// make sure the spec is valid to allow printing
spec.onValidityCheck((valid) => (btn.enabled = valid));

btn.addEventListener('click', async () => {
  btn.showSpinner();

  // display the job progress
  bar.progress = 0;
  bar.status = 'pending';

  // create a job, get a promise that resolves with the job id
  const jobId = await queuePrint(spec.value);

  getJobStatus(jobId).subscribe((printStatus) => {
    // update the job progress
    bar.progress = printStatus.progress;
    bar.status = printStatus.status;

    // job is finished
    if (printStatus.progress === 1) {
      btn.hideSpinner();
      downloadBlob(printStatus.imageBlob, 'inkmap.png');
    }
  });
});

Canceling a job

inkmap also enables job cancellation.

Use cancelJob() after having started a job with queuePrint().

Generate!
import {
  cancelJob,
  downloadBlob,
  getJobStatus,
  queuePrint,
} from '@camptocamp/inkmap';

const root = document.getElementById('example-03');
const btn = /** @type {CustomButton} */ root.querySelector('custom-button');
const bar = /** @type {CustomProgress} */ root.querySelector('custom-progress');
const spec = /** @type {PrintSpecEditor} */ root.querySelector('print-spec');
const btnCancel = /** @type {Button} */ root.querySelector('.cancel-btn');

// make sure the spec is valid to allow printing
spec.onValidityCheck((valid) => (btn.enabled = valid));

let jobId;

btn.addEventListener('click', async () => {
  btn.showSpinner();

  // display the job progress
  bar.progress = 0;
  bar.status = 'pending';

  // create a job, get a promise that resolves with the job id
  jobId = await queuePrint(spec.value);

  getJobStatus(jobId).subscribe((printStatus) => {
    // update the job progress
    bar.progress = printStatus.progress;
    bar.status = printStatus.status;

    // job is finished or canceled
    if (printStatus.progress === 1 || printStatus.progress === -1) {
      btn.hideSpinner();
    }

    // job is finished
    if (printStatus.progress === 1) {
      downloadBlob(printStatus.imageBlob, 'inkmap.png');
    }
  });
});

btnCancel.addEventListener('click', async () => {
  // cancel job based on job id
  cancelJob(jobId);
});

Monitoring multiple jobs

Multiple jobs can be monitored at once through a long-lived observable.

Use getJobsStatus() to receive a long-lived observable that you can subscribe() to and which will regularly give you status updates on all running jobs.

Add to the queue
import {
  getJobsStatus,
  queuePrint,
  downloadBlob,
  getJobStatus,
} from '@camptocamp/inkmap';

const root = document.getElementById('example-04');
const btn = /** @type {CustomButton} */ root.querySelector('custom-button');
const bars = /** @type {ProgressBars} */ root.querySelector('progress-bars');
const spec = /** @type {PrintSpecEditor} */ root.querySelector('print-spec');

// make sure the spec is valid to allow printing
spec.onValidityCheck((valid) => (btn.enabled = valid));

// subscribe to the jobs status updates
// ATTENTION! subscriptions to long-running observables might cause memory leaks!
getJobsStatus().subscribe((jobs) => {
  bars.jobsStatus = jobs;
});

btn.addEventListener('click', async () => {
  // create a job, get a promise that resolves with the job id
  const jobId = await queuePrint(spec.value);

  getJobStatus(jobId).subscribe((printStatus) => {
    // job is finished
    if (printStatus.progress === 1) {
      downloadBlob(printStatus.imageBlob, `inkmap-${jobId}.png`);
    }
  });
});

Printing to PDF

inkmap jobs give back a Blob which can be inserted into a PDF. This is for example supported by jsPDF, as showcased here.

Generate!
import { print, getAttributionsText, getNorthArrow } from '@camptocamp/inkmap';
import { jsPDF } from 'jspdf';
import { getScaleBar } from '../../src/main/index.js';

const root = document.getElementById('example-05');
const btn = /** @type {CustomButton} */ root.querySelector('custom-button');
const spec = /** @type {PrintSpecEditor} */ root.querySelector('print-spec');

// make sure the spec is valid to allow printing
spec.onValidityCheck((valid) => (btn.enabled = valid));

btn.addEventListener('click', async () => {
  btn.showSpinner();

  const mapWidth = 277; // mm
  const mapHeight = 150; // mm

  // Force map size to fit the PDF document
  const specValue = {
    ...spec.value,
    size: [mapWidth, mapHeight, 'mm'],
    attributions: null, // do not print widgets on the map
    northArrow: false,
    scaleBar: false,
  };

  // create a job, get a promise that resolves when the job is finished
  const blob = await print(specValue);

  btn.hideSpinner();

  // initializes the PDF document
  const doc = new jsPDF({
    orientation: 'landscape',
    unit: 'mm',
    format: 'a4', // 210 by 297mm
    putOnlyUsedFonts: true,
  });

  // create an Object URL from the map image blob and add it to the PDF
  const imgUrl = URL.createObjectURL(blob);
  doc.addImage(imgUrl, 'JPEG', 10, 40, mapWidth, mapHeight);

  // add a title
  doc.setFont('times', 'bold');
  doc.setFontSize(20);
  doc.text('A fantastic map.', 148.5, 13, null, null, 'center');

  // add north arrow
  const arrow = getNorthArrow(specValue, [16, 'mm']);
  const arrowSizeMm = arrow.getRealWorldDimensions('mm');
  doc.addImage(
    arrow.getImage(),
    'PNG',
    140,
    21,
    arrowSizeMm[0],
    arrowSizeMm[1],
  );

  // add scalebar next to the north arrow
  const scalebar = getScaleBar(specValue, [30, 'mm']);
  const scalebarSizeMm = scalebar.getRealWorldDimensions('mm');
  doc.addImage(
    scalebar.getImage(),
    'PNG',
    287 - scalebarSizeMm[0],
    37 - scalebarSizeMm[1],
    scalebarSizeMm[0],
    scalebarSizeMm[1],
  );

  // add a creation date
  doc.setFont('courier', 'normal');
  doc.setFontSize(12);
  doc.text(
    `Print date : ${new Date().toLocaleString()}`,
    10,
    200,
    null,
    null,
    'left',
  );

  // add attribution
  doc.setFont('courier', 'normal');
  doc.setFontSize(12);
  doc.text(getAttributionsText(spec.value), 287, 200, null, null, 'right');

  // download the result
  doc.save('inkmap.pdf');
});

Using projections

inkmap supports rendering maps in many different projections. Projections can be defined using the projectionDefinitions field of a spec. If a projection is not defined in the spec but still needed for rendering, inkmap will look it up on epsg.io. This only works for projections with an EPSG code!

Under the hood, inkmap uses proj4js.

Generate!
import { downloadBlob, print } from '@camptocamp/inkmap';

const root = document.getElementById('example-06');
const btn = /** @type {CustomButton} */ root.querySelector('custom-button');
const spec = /** @type {PrintSpecEditor} */ root.querySelector('print-spec');

// make sure the spec is valid to allow printing
spec.onValidityCheck((valid) => (btn.enabled = valid));

btn.addEventListener('click', async () => {
  btn.showSpinner();

  // create a job, get a promise that resolves when the job is finished
  const blob = await print(spec.value);

  btn.hideSpinner();

  downloadBlob(blob, 'inkmap.png');
});

Error handling

When errors are encountered while loading data for one or several layers, inkmap includes a list of error objects with urls in the status updates.

Generate!
import { downloadBlob, getJobStatus, queuePrint } from '@camptocamp/inkmap';

const root = document.getElementById('example-07');
const btn = /** @type {CustomButton} */ root.querySelector('custom-button');
const spec = /** @type {PrintSpecEditor} */ root.querySelector('print-spec');
const errors = root.querySelector('#errors');

// make sure the spec is valid to allow printing
spec.onValidityCheck((valid) => (btn.enabled = valid));

btn.addEventListener('click', async () => {
  btn.showSpinner();

  // create a job, get a promise that resolves with the job id
  const jobId = await queuePrint(spec.value);

  getJobStatus(jobId).subscribe((status) => {
    // display the job progress
    btn.progress = status.progress;

    // job is finished
    if (status.progress === 1) {
      btn.hideSpinner();

      // display urls with errors
      if (status.sourceLoadErrors.length > 0) {
        let errorMessage = 'The following layers encountered errors:<br>';
        status.sourceLoadErrors.forEach((element) => {
          errorMessage = `${errorMessage} - ${element.url}<br>`;
        });
        errors.innerHTML = errorMessage;
      } else {
        errors.innerHTML = '';
      }

      downloadBlob(status.imageBlob, 'inkmap.png');
    }
  });
});

Printing legends

This examples shows how to print legends for vector and raster layers by providing the configuration option legend on a layer with a value of true. Currently this supports WMS, WMTS, GeoJSON and WFS layers. Use the createLegends() function to generate the print spec legends.

The print will generate a separate image containing all the legends at once.

Generate mapGenerate legend
import {
  createLegends,
  downloadBlob,
  getJobStatus,
  queuePrint,
} from '@camptocamp/inkmap';

const root = document.getElementById('example-08');
const mapBtn = /** @type {CustomButton} */ root.querySelector(
  'custom-button.map-btn',
);
const legendBtn = /** @type {CustomButton} */ root.querySelector(
  'custom-button.legend-btn',
);
root.querySelector('custom-button.legend-btn');
const bar = /** @type {CustomProgress} */ root.querySelector('custom-progress');
const spec = /** @type {PrintSpecEditor} */ root.querySelector('print-spec');

// make sure the spec is valid to allow printing
spec.onValidityCheck((valid) => {
  mapBtn.enabled = valid;
  legendBtn.enabled = valid;
});

mapBtn.addEventListener('click', async () => {
  mapBtn.showSpinner();

  // display the job progress
  bar.progress = 0;
  bar.status = 'pending';

  // create a job, get a promise that resolves with the job id
  const jobId = await queuePrint(spec.value);

  getJobStatus(jobId).subscribe((printStatus) => {
    // update the job progress
    bar.progress = printStatus.progress;
    bar.status = printStatus.status;

    // job is finished
    if (printStatus.progress === 1) {
      mapBtn.hideSpinner();
      downloadBlob(printStatus.imageBlob, 'inkmap.png');
    }
  });
});

legendBtn.addEventListener('click', async () => {
  const blob = await createLegends(spec.value);
  downloadBlob(blob, 'legend.svg');
});