diff --git a/examples/.eslintrc b/examples/.eslintrc
index 769357d35a..c404b2e6e6 100644
--- a/examples/.eslintrc
+++ b/examples/.eslintrc
@@ -5,6 +5,7 @@
"common": false,
"createMapboxStreetsV6Style": false,
"d3": false,
+ "domtoimage": false,
"geojsonvt": false,
"GyroNorm": false,
"jsPDF": false,
diff --git a/examples/print-to-scale.css b/examples/print-to-scale.css
new file mode 100644
index 0000000000..7911e178ab
--- /dev/null
+++ b/examples/print-to-scale.css
@@ -0,0 +1,6 @@
+.wrapper {
+ max-width: 566px;
+ width: 100%;
+ height: 400px;
+ overflow: hidden;
+}
diff --git a/examples/print-to-scale.html b/examples/print-to-scale.html
new file mode 100644
index 0000000000..54d88db427
--- /dev/null
+++ b/examples/print-to-scale.html
@@ -0,0 +1,47 @@
+---
+layout: example.html
+title: Print to scale example
+shortdesc: Example of printing a map to a specified scale.
+docs: >
+ Example of printing a map to a specified scale.
+ The print is exported as a PDF using the jsPDF library.
+ Unlike the Export PDF example the on screen map is only used to set the center and rotation.
+ The extent printed depends on the scale and page size. To print the scale bar and attributions the example uses the
+ dom-to-image-more library. Due to browser
+ limitations and restrictions Internet Explorer and Safari are not supported .
+tags: "print, printing, scale, scaleline, export, pdf"
+resources:
+ - https://unpkg.com/dom-to-image-more@2.8.0/dist/dom-to-image-more.min.js
+ - https://cdnjs.cloudflare.com/ajax/libs/jspdf/1.5.3/jspdf.min.js
+---
+
+
+Export PDF
diff --git a/examples/print-to-scale.js b/examples/print-to-scale.js
new file mode 100644
index 0000000000..ec8ef2ae23
--- /dev/null
+++ b/examples/print-to-scale.js
@@ -0,0 +1,128 @@
+import Map from '../src/ol/Map.js';
+import View from '../src/ol/View.js';
+import {defaults as defaultControls, ScaleLine} from '../src/ol/control.js';
+import WMTSCapabilities from '../src/ol/format/WMTSCapabilities.js';
+import TileLayer from '../src/ol/layer/Tile.js';
+import {get as getProjection, getPointResolution} from '../src/ol/proj.js';
+import {register} from '../src/ol/proj/proj4.js';
+import WMTS, {optionsFromCapabilities} from '../src/ol/source/WMTS.js';
+import proj4 from 'proj4';
+
+
+proj4.defs('EPSG:27700', '+proj=tmerc +lat_0=49 +lon_0=-2 +k=0.9996012717 ' +
+ '+x_0=400000 +y_0=-100000 +ellps=airy ' +
+ '+towgs84=446.448,-125.157,542.06,0.15,0.247,0.842,-20.489 ' +
+ '+units=m +no_defs');
+
+register(proj4);
+
+const proj27700 = getProjection('EPSG:27700');
+proj27700.setExtent([0, 0, 700000, 1300000]);
+
+const raster = new TileLayer();
+
+const url = 'https://tiles.arcgis.com/tiles/qHLhLQrcvEnxjtPr/arcgis/rest/services/OS_Open_Raster/MapServer/WMTS';
+fetch(url)
+ .then(function(response) {
+ return response.text();
+ })
+ .then(function(text) {
+ const result = new WMTSCapabilities().read(text);
+ const options = optionsFromCapabilities(result, {
+ layer: 'OS_Open_Raster'
+ });
+ options.attributions = 'Contains OS data © Crown Copyright and database right ' + new Date().getFullYear();
+ options.crossOrigin = '';
+ options.projection = proj27700;
+ options.wrapX = false;
+ raster.setSource(new WMTS(options));
+ });
+
+
+const map = new Map({
+ layers: [raster],
+ controls: defaultControls({
+ attributionOptions: {collapsible: false}
+ }),
+ target: 'map',
+ view: new View({
+ center: [373500, 436500],
+ projection: proj27700,
+ zoom: 7
+ })
+});
+
+const scaleLine = new ScaleLine({bar: true, text: true, minWidth: 125});
+map.addControl(scaleLine);
+
+
+const dims = {
+ a0: [1189, 841],
+ a1: [841, 594],
+ a2: [594, 420],
+ a3: [420, 297],
+ a4: [297, 210],
+ a5: [210, 148]
+};
+
+
+// export options for html-to-image.
+// See: https://github.com/bubkoo/html-to-image#options
+const exportOptions = {
+ filter: function(element) {
+ const className = element.className || '';
+ return (
+ className.indexOf('ol-control') === -1 ||
+ className.indexOf('ol-scale') > -1 ||
+ (className.indexOf('ol-attribution') > -1 &&
+ className.indexOf('ol-uncollapsible'))
+ );
+ }
+};
+
+const exportButton = document.getElementById('export-pdf');
+
+exportButton.addEventListener('click', function() {
+
+ exportButton.disabled = true;
+ document.body.style.cursor = 'progress';
+
+ const format = document.getElementById('format').value;
+ const resolution = document.getElementById('resolution').value;
+ const scale = document.getElementById('scale').value;
+ const dim = dims[format];
+ const width = Math.round(dim[0] * resolution / 25.4);
+ const height = Math.round(dim[1] * resolution / 25.4);
+ const viewResolution = map.getView().getResolution();
+ const scaleResolution = scale / getPointResolution(
+ map.getView().getProjection(),
+ resolution / 25.4,
+ map.getView().getCenter()
+ );
+
+ map.once('rendercomplete', function() {
+ exportOptions.width = width;
+ exportOptions.height = height;
+ domtoimage.toJpeg(map.getViewport(), exportOptions).then(function(dataUrl) {
+ const pdf = new jsPDF('landscape', undefined, format);
+ pdf.addImage(dataUrl, 'JPEG', 0, 0, dim[0], dim[1]);
+ pdf.save('map.pdf');
+ // Reset original map size
+ scaleLine.setDpi();
+ map.getTargetElement().style.width = '';
+ map.getTargetElement().style.height = '';
+ map.updateSize();
+ map.getView().setResolution(viewResolution);
+ exportButton.disabled = false;
+ document.body.style.cursor = 'auto';
+ });
+ });
+
+ // Set print size
+ scaleLine.setDpi(resolution);
+ map.getTargetElement().style.width = width + 'px';
+ map.getTargetElement().style.height = height + 'px';
+ map.updateSize();
+ map.getView().setResolution(scaleResolution);
+
+}, false);
diff --git a/src/ol/control/ScaleLine.js b/src/ol/control/ScaleLine.js
index e0d7733ea3..b910182ab1 100644
--- a/src/ol/control/ScaleLine.js
+++ b/src/ol/control/ScaleLine.js
@@ -34,11 +34,18 @@ export const Units = {
*/
const LEADING_DIGITS = [1, 2, 5];
+/**
+ * @const
+ * @type {number}
+ */
+const DEFAULT_DPI = 25.4 / 0.28;
+
/**
* @typedef {Object} Options
* @property {string} [className='ol-scale-line'] CSS Class name.
- * @property {number} [minWidth=64] Minimum width in pixels.
+ * @property {number} [minWidth=64] Minimum width in pixels at the OGC default dpi. The width will be
+ * adjusted to match the dpi used.
* @property {function(import("../MapEvent.js").default)} [render] Function called when the control
* should be re-rendered. This is called in a `requestAnimationFrame` callback.
* @property {HTMLElement|string} [target] Specify a target if you want the control
@@ -49,6 +56,8 @@ const LEADING_DIGITS = [1, 2, 5];
* for best results. Only applies when `bar` is `true`.
* @property {boolean} [text=false] Render the text scale above of the scalebar. Only applies
* when `bar` is `true`.
+ * @property {number|undefined} [dpi=undefined] dpi of output device such as printer. Only applies
+ * when `bar` is `true`. If undefined the OGC default screen pixel size of 0.28mm will be assumed.
*/
@@ -146,6 +155,12 @@ class ScaleLine extends Control {
*/
this.scaleBarText_ = options.text || false;
+ /**
+ * @private
+ * @type {number|undefined}
+ */
+ this.dpi_ = options.dpi || undefined;
+
}
/**
@@ -176,6 +191,15 @@ class ScaleLine extends Control {
this.set(UNITS_PROP, units);
}
+ /**
+ * Specify the dpi of output device such as printer.
+ * @param {number|undefined} dpi The dpi of output device.
+ * @api
+ */
+ setDpi(dpi) {
+ this.dpi_ = dpi;
+ }
+
/**
* @private
*/
@@ -199,7 +223,9 @@ class ScaleLine extends Control {
let pointResolution =
getPointResolution(projection, viewState.resolution, center, pointResolutionUnits);
- let nominalCount = this.minWidth_ * pointResolution;
+ const minWidth = this.minWidth_ * (this.dpi_ || DEFAULT_DPI) / DEFAULT_DPI;
+
+ let nominalCount = minWidth * pointResolution;
let suffix = '';
if (units == Units.DEGREES) {
const metersPerDegree = METERS_PER_UNIT[ProjUnits.DEGREES];
@@ -256,7 +282,7 @@ class ScaleLine extends Control {
}
let i = 3 * Math.floor(
- Math.log(this.minWidth_ * pointResolution) / Math.log(10));
+ Math.log(minWidth * pointResolution) / Math.log(10));
let count, width, decimalCount;
while (true) {
decimalCount = Math.floor(i / 3);
@@ -267,7 +293,7 @@ class ScaleLine extends Control {
this.element.style.display = 'none';
this.renderedVisible_ = false;
return;
- } else if (width >= this.minWidth_) {
+ } else if (width >= minWidth) {
break;
}
++i;
@@ -406,8 +432,12 @@ class ScaleLine extends Control {
* @return {number} The appropriate scale.
*/
getScaleForResolution() {
- const resolution = this.getMap().getView().getResolution();
- const dpi = 25.4 / 0.28;
+ const resolution = getPointResolution(
+ this.viewState_.projection,
+ this.viewState_.resolution,
+ this.viewState_.center
+ );
+ const dpi = this.dpi_ || DEFAULT_DPI;
const mpu = this.viewState_.projection.getMetersPerUnit();
const inchesPerMeter = 39.37;
return parseFloat(resolution.toString()) * mpu * inchesPerMeter * dpi;