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..f88d892 --- /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;