diff --git a/CHANGELOG.md b/CHANGELOG.md
index 13063f1..44d26da 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+### Added
+
+- Add snapshot control to download and print map. #202
+
## [v2.2.2] - 2023-09-02
### Fixed
diff --git a/examples/simple-html-consumer/static/index.html b/examples/simple-html-consumer/static/index.html
index 5a1514d..db5c766 100644
--- a/examples/simple-html-consumer/static/index.html
+++ b/examples/simple-html-consumer/static/index.html
@@ -36,6 +36,7 @@
myMap.addBehavior("sidePanel");
myMap.addBehavior("layerSwitcherInSidePanel");
myMap.addBehavior("snappingGrid");
+ myMap.addBehavior("snapshot");
// Display popup with coordinates when not clicking a feature.
myMap.addPopup(function (event) {
diff --git a/package-lock.json b/package-lock.json
index 441edbd..008e1c0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,7 +15,8 @@
"ol-grid": "^1.1.7",
"ol-layerswitcher": "^3.7.0",
"ol-popup": "^4.0.0",
- "ol-side-panel": "^1.0.6"
+ "ol-side-panel": "^1.0.6",
+ "print-js": "^1.6.0"
},
"devDependencies": {
"copy-webpack-plugin": "^8.1.1",
@@ -6647,6 +6648,12 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/print-js": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/print-js/-/print-js-1.6.0.tgz",
+ "integrity": "sha512-BfnOIzSKbqGRtO4o0rnj/K3681BSd2QUrsIZy/+WdCIugjIswjmx3lDEZpXB2ruGf9d4b3YNINri81+J0FsBWg==",
+ "license": "MIT"
+ },
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
@@ -14479,6 +14486,11 @@
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
"dev": true
},
+ "print-js": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/print-js/-/print-js-1.6.0.tgz",
+ "integrity": "sha512-BfnOIzSKbqGRtO4o0rnj/K3681BSd2QUrsIZy/+WdCIugjIswjmx3lDEZpXB2ruGf9d4b3YNINri81+J0FsBWg=="
+ },
"process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
diff --git a/package.json b/package.json
index 597c5c6..9d365bd 100644
--- a/package.json
+++ b/package.json
@@ -37,6 +37,7 @@
"ol-grid": "^1.1.7",
"ol-layerswitcher": "^3.7.0",
"ol-popup": "^4.0.0",
- "ol-side-panel": "^1.0.6"
+ "ol-side-panel": "^1.0.6",
+ "print-js": "^1.6.0"
}
}
diff --git a/src/behavior/index.js b/src/behavior/index.js
index f4dd22f..0d82059 100644
--- a/src/behavior/index.js
+++ b/src/behavior/index.js
@@ -17,4 +17,5 @@ export default {
rememberLayer: lazyLoadedBehavior('rememberLayer'),
sidePanel: lazyLoadedBehavior('sidePanel'),
snappingGrid: lazyLoadedBehavior('snappingGrid'),
+ snapshot: lazyLoadedBehavior('snapshot'),
};
diff --git a/src/behavior/snapshot.js b/src/behavior/snapshot.js
new file mode 100644
index 0000000..519714f
--- /dev/null
+++ b/src/behavior/snapshot.js
@@ -0,0 +1,10 @@
+import Snapshot from '../control/Snapshot/Snapshot';
+
+export default {
+ attach(instance) {
+
+ // Create the Snapshot control and add it to the map.
+ const control = new Snapshot();
+ instance.map.addControl(control);
+ },
+};
diff --git a/src/control/Snapshot/Snapshot.css b/src/control/Snapshot/Snapshot.css
new file mode 100644
index 0000000..92e7f0f
--- /dev/null
+++ b/src/control/Snapshot/Snapshot.css
@@ -0,0 +1,18 @@
+.ol-snapshot.ol-control {
+ top: 0.5em;
+ right: 6.5em;
+}
+
+.ol-snapshot.ol-control .download,
+.ol-snapshot.ol-control .print {
+ display: none;
+}
+
+.ol-snapshot.ol-control.active .download,
+.ol-snapshot.ol-control.active .print {
+ display: block;
+}
+
+.ol-snapshot.ol-control button {
+ display: inline-block;
+}
diff --git a/src/control/Snapshot/Snapshot.js b/src/control/Snapshot/Snapshot.js
new file mode 100644
index 0000000..ea648c7
--- /dev/null
+++ b/src/control/Snapshot/Snapshot.js
@@ -0,0 +1,140 @@
+import Control from 'ol/control/Control';
+import { CLASS_CONTROL, CLASS_UNSELECTABLE } from 'ol/css';
+import EventType from 'ol/events/EventType';
+import MapEventType from 'ol/MapEventType';
+import './Snapshot.css';
+import printJS from 'print-js';
+
+/**
+ * @classdesc
+ * OpenLayers Snapshot Control.
+ *
+ * @api
+ */
+class Snapshot extends Control {
+
+ /**
+ * @param {Options=} opts Snapshot options.
+ */
+ constructor(opts) {
+ const options = opts || {};
+
+ // Call the parent control constructor.
+ super({
+ element: document.createElement('div'),
+ target: options.target,
+ });
+
+ // Create the snapshot button element.
+ const className = options.className || 'ol-snapshot';
+ const button = document.createElement('button');
+ button.innerHTML = options.label || '';
+ button.title = options.tooltip || 'Snapshot';
+ button.className = className;
+ button.type = 'button';
+
+ // Register a click event on the button.
+ button.addEventListener(EventType.CLICK, this.captureImage.bind(this), false);
+
+ // Add the button and CSS classes to the control element.
+ const { element } = this;
+ element.className = `${className} ${CLASS_UNSELECTABLE} ${CLASS_CONTROL}`;
+ element.appendChild(button);
+
+ // Create a download button with link.
+ const link = document.createElement('a');
+ link.setAttribute('download', document.title);
+ link.innerHTML = '';
+ this.link = link;
+ const download = document.createElement('button');
+ download.title = 'Download snapshot';
+ download.className = 'download';
+ download.appendChild(link);
+ element.appendChild(download);
+
+ // Create a print button.
+ const print = document.createElement('button');
+ print.innerHTML = '';
+ print.title = 'Print snapshot';
+ print.className = 'print';
+ print.addEventListener('click', this.printSnapshot.bind(this));
+ element.appendChild(print);
+ }
+
+ /**
+ * Callback to deactivate the snapshot control.
+ * @private
+ */
+ deactivate() {
+ this.element.classList.remove('active');
+ }
+
+ /**
+ * Callback for the snapshot button click event.
+ * @param {MouseEvent} event The event to handle
+ * @private
+ */
+ captureImage(event) {
+ event.preventDefault();
+
+ // Create a new canvas element to combine multiple map canvas data to.
+ const outputCanvas = document.createElement('canvas');
+ const [width, height] = this.getMap().getSize();
+ outputCanvas.width = width;
+ outputCanvas.height = height;
+ const outputContext = outputCanvas.getContext('2d');
+
+ // Draw each canvas from this map into the new canvas.
+ // Logic for transforming and drawing canvases derived from ol export-pdf example.
+ // https://github.com/openlayers/openlayers/blob/6f2ca3b9635f273f6fbddab834bd9126c7d48964/examples/export-pdf.js#L61-L85
+ Array.from(this.getMap().getTargetElement().querySelectorAll('.ol-layer canvas'))
+ .filter(canvas => canvas.width > 0)
+ .forEach((canvas) => {
+ const { opacity } = canvas.parentNode.style;
+ outputContext.globalAlpha = opacity === '' ? 1 : Number(opacity);
+
+ // Get the transform parameters from the style's transform matrix.
+ // This is necessary so that vectors align with raster layers.
+ const { transform } = canvas.style;
+ const matrix = transform
+ .match(/^matrix\(([^(]*)\)$/)[1]
+ .split(',')
+ .map(Number);
+
+ // Apply the transform to the export map context.
+ CanvasRenderingContext2D.prototype.setTransform.apply(
+ outputContext,
+ matrix,
+ );
+ outputContext.drawImage(canvas, 0, 0);
+ });
+
+ // Build a jpeg data url and update link.
+ const url = outputCanvas.toDataURL('image/jpeg');
+ this.link.href = url;
+
+ // Remove the output canvas.
+ outputCanvas.remove();
+
+ // Enable the snapshot actions.
+ this.element.classList.add('active');
+
+ // Subscribe to events to deactivate snapshot actions.
+ this.getMap().on(EventType.CLICK, this.deactivate.bind(this));
+ this.getMap().on(EventType.CHANGE, this.deactivate.bind(this));
+ this.getMap().on(MapEventType.MOVESTART, this.deactivate.bind(this));
+ }
+
+ /**
+ * Callback for the snapshot button click event.
+ * @private
+ */
+ printSnapshot() {
+ if (this.link.href.length) {
+ printJS(this.link.href, 'image');
+ }
+ }
+
+}
+
+export default Snapshot;