diff --git a/examples/disable-image-smoothing.css b/examples/disable-image-smoothing.css new file mode 100644 index 0000000000..db58102d58 --- /dev/null +++ b/examples/disable-image-smoothing.css @@ -0,0 +1,15 @@ +@media (min-width: 800px) { + .wrapper { + display: flex; + } + .half { + padding: 0 10px; + width: 50%; + float: left; + } +} +#opacity { + display: inline-block; + width: 150px; + vertical-align: text-bottom; +} diff --git a/examples/disable-image-smoothing.html b/examples/disable-image-smoothing.html new file mode 100644 index 0000000000..918aa558b4 --- /dev/null +++ b/examples/disable-image-smoothing.html @@ -0,0 +1,45 @@ +--- +layout: example.html +title: Disable Image Smoothing +shortdesc: Example of disabling image smoothing +docs: > + Example of disabling image smoothing when using raster DEM (digital elevation model) data. + The imageSmoothing: false setting is used to disable canvas image smoothing during + reprojection and rendering. Elevation data is + calculated from the pixel value returned by forEachLayerAtPixel. For comparison a second map + with smoothing enabled returns inaccuate elevations which are very noticeable close to 3107 meters + due to how elevation is calculated from the pixel value. +tags: "disable image smoothing, xyz, maptiler, reprojection" +cloak: + - key: get_your_own_D6rA4zTHduk6KOKTXzGB + value: Get your own API key at https://www.maptiler.com/cloud/ +--- +
+
+

Smoothing Disabled

+
+
+ +
+
+ +
+
+
+

Uncorrected Comparison

+
+
+ +
+
+
diff --git a/examples/disable-image-smoothing.js b/examples/disable-image-smoothing.js new file mode 100644 index 0000000000..5c231e8e49 --- /dev/null +++ b/examples/disable-image-smoothing.js @@ -0,0 +1,110 @@ +import Map from '../src/ol/Map.js'; +import View from '../src/ol/View.js'; +import TileLayer from '../src/ol/layer/Tile.js'; +import XYZ from '../src/ol/source/XYZ.js'; + +const key = 'get_your_own_D6rA4zTHduk6KOKTXzGB'; +const attributions = '© MapTiler ' + + '© OpenStreetMap contributors'; + +const disabledLayer = new TileLayer({ + // specify className so forEachLayerAtPixel can distinguish layers + className: 'ol-layer-dem', + source: new XYZ({ + attributions: attributions, + url: 'https://api.maptiler.com/tiles/terrain-rgb/{z}/{x}/{y}.png?key=' + key, + maxZoom: 10, + crossOrigin: '', + imageSmoothing: false + }) +}); + +const imagery = new TileLayer({ + className: 'ol-layer-imagery', + source: new XYZ({ + attributions: attributions, + url: 'https://api.maptiler.com/tiles/satellite/{z}/{x}/{y}.jpg?key=' + key, + maxZoom: 20, + crossOrigin: '' + }) +}); + +const enabledLayer = new TileLayer({ + source: new XYZ({ + attributions: attributions, + url: 'https://api.maptiler.com/tiles/terrain-rgb/{z}/{x}/{y}.png?key=' + key, + maxZoom: 10, + crossOrigin: '' + }) +}); + +imagery.on('prerender', function(evt) { + // use opaque background to conceal DEM while fully opaque imagery renders + if (imagery.getOpacity() === 1) { + evt.context.fillStyle = 'white'; + evt.context.fillRect(0, 0, evt.context.canvas.width, evt.context.canvas.height); + } +}); + +const control = document.getElementById('opacity'); +const output = document.getElementById('output'); +control.addEventListener('input', function() { + output.innerText = control.value; + imagery.setOpacity(control.value / 100); +}); +output.innerText = control.value; +imagery.setOpacity(control.value / 100); + +const view = new View({ + center: [6.893, 45.8295], + zoom: 16, + projection: 'EPSG:4326' +}); + +const map1 = new Map({ + target: 'map1', + layers: [disabledLayer, imagery], + view: view +}); + +const map2 = new Map({ + target: 'map2', + layers: [enabledLayer], + view: view +}); + +const info1 = document.getElementById('info1'); +const info2 = document.getElementById('info2'); + +const showElevations = function(evt) { + if (evt.dragging) { + return; + } + map1.forEachLayerAtPixel( + evt.pixel, + function(layer, pixel) { + const height = -10000 + (pixel[0] * 256 * 256 + pixel[1] * 256 + pixel[2]) * 0.1; + info1.innerText = height.toFixed(1); + }, + { + layerFilter: function(layer) { + return layer === disabledLayer; + } + } + ); + map2.forEachLayerAtPixel( + evt.pixel, + function(layer, pixel) { + const height = -10000 + (pixel[0] * 256 * 256 + pixel[1] * 256 + pixel[2]) * 0.1; + info2.innerText = height.toFixed(1); + }, + { + layerFilter: function(layer) { + return layer === enabledLayer; + } + } + ); +}; + +map1.on('pointermove', showElevations); +map2.on('pointermove', showElevations); diff --git a/rendering/cases/reproj-tile-disable-smoothing/expected.png b/rendering/cases/reproj-tile-disable-smoothing/expected.png new file mode 100644 index 0000000000..6c51884467 Binary files /dev/null and b/rendering/cases/reproj-tile-disable-smoothing/expected.png differ diff --git a/rendering/cases/reproj-tile-disable-smoothing/main.js b/rendering/cases/reproj-tile-disable-smoothing/main.js new file mode 100644 index 0000000000..fb194dfe0c --- /dev/null +++ b/rendering/cases/reproj-tile-disable-smoothing/main.js @@ -0,0 +1,35 @@ +import Map from '../../../src/ol/Map.js'; +import View from '../../../src/ol/View.js'; +import XYZ from '../../../src/ol/source/XYZ.js'; +import TileLayer from '../../../src/ol/layer/Tile.js'; +import {toLonLat} from '../../../src/ol/proj.js'; +import {createXYZ} from '../../../src/ol/tilegrid.js'; + +const tileGrid = createXYZ(); +const extent = tileGrid.getTileCoordExtent([5, 5, 12]); +const center = [(extent[0] + extent[2]) / 2, (extent[1] + extent[3]) / 2]; + +const source = new XYZ({ + transition: 0, + minZoom: 5, + maxZoom: 5, + imageSmoothing: false, + url: '/data/tiles/osm/{z}/{x}/{y}.png' +}); + +const layer = new TileLayer({ + source: source +}); + +new Map({ + pixelRatio: 1, + target: 'map', + layers: [layer], + view: new View({ + projection: 'EPSG:4326', + center: toLonLat(center), + zoom: 10 + }) +}); + +render(); diff --git a/rendering/cases/tile-disable-smoothing/expected.png b/rendering/cases/tile-disable-smoothing/expected.png new file mode 100644 index 0000000000..46f555f607 Binary files /dev/null and b/rendering/cases/tile-disable-smoothing/expected.png differ diff --git a/rendering/cases/tile-disable-smoothing/main.js b/rendering/cases/tile-disable-smoothing/main.js new file mode 100644 index 0000000000..805344e9b2 --- /dev/null +++ b/rendering/cases/tile-disable-smoothing/main.js @@ -0,0 +1,33 @@ +import Map from '../../../src/ol/Map.js'; +import View from '../../../src/ol/View.js'; +import XYZ from '../../../src/ol/source/XYZ.js'; +import TileLayer from '../../../src/ol/layer/Tile.js'; +import {createXYZ} from '../../../src/ol/tilegrid.js'; + +const tileGrid = createXYZ(); +const extent = tileGrid.getTileCoordExtent([5, 5, 12]); +const center = [(extent[0] + extent[2]) / 2, (extent[1] + extent[3]) / 2]; + +const source = new XYZ({ + transition: 0, + minZoom: 5, + maxZoom: 5, + imageSmoothing: false, + url: '/data/tiles/osm/{z}/{x}/{y}.png' +}); + +const layer = new TileLayer({ + source: source +}); + +new Map({ + pixelRatio: 1, + target: 'map', + layers: [layer], + view: new View({ + center: center, + zoom: 10 + }) +}); + +render(); diff --git a/src/ol/renderer/canvas/TileLayer.js b/src/ol/renderer/canvas/TileLayer.js index 51a4d8a372..afdcdd9143 100644 --- a/src/ol/renderer/canvas/TileLayer.js +++ b/src/ol/renderer/canvas/TileLayer.js @@ -10,6 +10,7 @@ import CanvasLayerRenderer from './Layer.js'; import {apply as applyTransform, compose as composeTransform, makeInverse} from '../../transform.js'; import {numberSafeCompareFunction} from '../../array.js'; import {createTransformString} from '../../render/canvas.js'; +import {assign} from '../../obj.js'; /** * @classdesc @@ -271,6 +272,7 @@ class CanvasTileLayerRenderer extends CanvasLayerRenderer { this.clipUnrotated(context, frameState, layerExtent); } + assign(context, tileSource.getContextOptions()); this.preRender(context, frameState); this.renderedTiles.length = 0; diff --git a/src/ol/reproj.js b/src/ol/reproj.js index 7593c6bc3b..94a7e813db 100644 --- a/src/ol/reproj.js +++ b/src/ol/reproj.js @@ -5,6 +5,7 @@ import {createCanvasContext2D} from './dom.js'; import {containsCoordinate, createEmpty, extend, getHeight, getTopLeft, getWidth} from './extent.js'; import {solveLinearSystem} from './math.js'; import {getPointResolution, transform} from './proj.js'; +import {assign} from './obj.js'; /** @@ -88,14 +89,16 @@ function enlargeClipPoint(centroidX, centroidY, x, y) { * Array of sources. * @param {number} gutter Gutter of the sources. * @param {boolean=} opt_renderEdges Render reprojection edges. + * @param {object=} opt_contextOptions Properties to set on the canvas context. * @return {HTMLCanvasElement} Canvas with reprojected data. */ export function render(width, height, pixelRatio, sourceResolution, sourceExtent, targetResolution, targetExtent, - triangulation, sources, gutter, opt_renderEdges) { + triangulation, sources, gutter, opt_renderEdges, opt_contextOptions) { const context = createCanvasContext2D(Math.round(pixelRatio * width), Math.round(pixelRatio * height)); + assign(context, opt_contextOptions); if (sources.length === 0) { return context.canvas; @@ -113,6 +116,7 @@ export function render(width, height, pixelRatio, const stitchContext = createCanvasContext2D( Math.round(pixelRatio * canvasWidthInUnits / sourceResolution), Math.round(pixelRatio * canvasHeightInUnits / sourceResolution)); + assign(stitchContext, opt_contextOptions); const stitchScale = pixelRatio / sourceResolution; diff --git a/src/ol/reproj/Tile.js b/src/ol/reproj/Tile.js index 7defc9e48b..aa58d8779a 100644 --- a/src/ol/reproj/Tile.js +++ b/src/ol/reproj/Tile.js @@ -38,7 +38,8 @@ class ReprojTile extends Tile { * Function returning source tiles (z, x, y, pixelRatio). * @param {number=} opt_errorThreshold Acceptable reprojection error (in px). * @param {boolean=} opt_renderEdges Render reprojection edges. - */ + * @param {object=} opt_contextOptions Properties to set on the canvas context. + */ constructor( sourceProj, sourceTileGrid, @@ -50,7 +51,8 @@ class ReprojTile extends Tile { gutter, getTileFunction, opt_errorThreshold, - opt_renderEdges + opt_renderEdges, + opt_contextOptions ) { super(tileCoord, TileState.IDLE); @@ -60,6 +62,12 @@ class ReprojTile extends Tile { */ this.renderEdges_ = opt_renderEdges !== undefined ? opt_renderEdges : false; + /** + * @private + * @type {object} + */ + this.contextOptions_ = opt_contextOptions; + /** * @private * @type {number} @@ -241,7 +249,7 @@ class ReprojTile extends Tile { this.canvas_ = renderReprojected(width, height, this.pixelRatio_, sourceResolution, this.sourceTileGrid_.getExtent(), targetResolution, targetExtent, this.triangulation_, sources, - this.gutter_, this.renderEdges_); + this.gutter_, this.renderEdges_, this.contextOptions_); this.state = TileState.LOADED; } diff --git a/src/ol/source/BingMaps.js b/src/ol/source/BingMaps.js index b394cbd313..ecc88d4e16 100644 --- a/src/ol/source/BingMaps.js +++ b/src/ol/source/BingMaps.js @@ -55,6 +55,7 @@ const TOS_ATTRIBUTION = '} [params] ArcGIS Rest parameters. This field is optional. Service defaults will be * used for any fields not specified. `FORMAT` is `PNG32` by default. `F` is `IMAGE` by * default. `TRANSPARENT` is `true` by default. `BBOX`, `SIZE`, `BBOXSR`, @@ -72,6 +73,7 @@ class TileArcGISRest extends TileImage { attributions: options.attributions, cacheSize: options.cacheSize, crossOrigin: options.crossOrigin, + imageSmoothing: options.imageSmoothing, projection: options.projection, reprojectionErrorThreshold: options.reprojectionErrorThreshold, tileGrid: options.tileGrid, diff --git a/src/ol/source/TileImage.js b/src/ol/source/TileImage.js index 7e32dd149b..c96aa9638a 100644 --- a/src/ol/source/TileImage.js +++ b/src/ol/source/TileImage.js @@ -10,6 +10,7 @@ import EventType from '../events/EventType.js'; import {equivalent, get as getProjection} from '../proj.js'; import ReprojTile from '../reproj/Tile.js'; import UrlTile from './UrlTile.js'; +import {IMAGE_SMOOTHING_DISABLED} from './common.js'; import {getKey, getKeyZXY} from '../tilecoord.js'; import {getForProjection as getTileGridForProjection} from '../tilegrid.js'; @@ -21,6 +22,7 @@ import {getForProjection as getTileGridForProjection} from '../tilegrid.js'; * @property {null|string} [crossOrigin] The `crossOrigin` attribute for loaded images. Note that * you must provide a `crossOrigin` value if you want to access pixel data with the Canvas renderer. * See https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image for more detail. + * @property {boolean} [imageSmoothing=true] Enable image smoothing. * @property {boolean} [opaque=true] Whether the layer is opaque. * @property {import("../proj.js").ProjectionLike} [projection] Projection. Default is the view projection. * @property {number} [reprojectionErrorThreshold=0.5] Maximum allowed reprojection error (in pixels). @@ -123,6 +125,13 @@ class TileImage extends UrlTile { */ this.reprojectionErrorThreshold_ = options.reprojectionErrorThreshold; + /** + * @private + * @type {object|undefined} + */ + this.contextOptions_ = options.imageSmoothing === false ? + IMAGE_SMOOTHING_DISABLED : undefined; + /** * @private * @type {boolean} @@ -166,6 +175,13 @@ class TileImage extends UrlTile { } } + /** + * @inheritDoc + */ + getContextOptions() { + return this.contextOptions_; + } + /** * @inheritDoc */ @@ -296,7 +312,7 @@ class TileImage extends UrlTile { function(z, x, y, pixelRatio) { return this.getTileInternal(z, x, y, pixelRatio, sourceProjection); }.bind(this), this.reprojectionErrorThreshold_, - this.renderReprojectionEdges_); + this.renderReprojectionEdges_, this.contextOptions_); newTile.key = key; if (tile) { diff --git a/src/ol/source/TileJSON.js b/src/ol/source/TileJSON.js index 90887a2312..9667c76fda 100644 --- a/src/ol/source/TileJSON.js +++ b/src/ol/source/TileJSON.js @@ -43,6 +43,7 @@ import {createXYZ, extentFromProjection} from '../tilegrid.js'; * @property {null|string} [crossOrigin] The `crossOrigin` attribute for loaded images. Note that * you must provide a `crossOrigin` value if you want to access pixel data with the Canvas renderer. * See https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image for more detail. + * @property {boolean} [imageSmoothing=true] Enable image smoothing. * @property {boolean} [jsonp=false] Use JSONP with callback to load the TileJSON. * Useful when the server does not support CORS.. * @property {number} [reprojectionErrorThreshold=0.5] Maximum allowed reprojection error (in pixels). @@ -78,6 +79,7 @@ class TileJSON extends TileImage { attributions: options.attributions, cacheSize: options.cacheSize, crossOrigin: options.crossOrigin, + imageSmoothing: options.imageSmoothing, projection: getProjection('EPSG:3857'), reprojectionErrorThreshold: options.reprojectionErrorThreshold, state: SourceState.LOADING, diff --git a/src/ol/source/TileWMS.js b/src/ol/source/TileWMS.js index 9ee929b63c..d716e78e44 100644 --- a/src/ol/source/TileWMS.js +++ b/src/ol/source/TileWMS.js @@ -24,6 +24,7 @@ import {appendParams} from '../uri.js'; * @property {null|string} [crossOrigin] The `crossOrigin` attribute for loaded images. Note that * you must provide a `crossOrigin` value if you want to access pixel data with the Canvas renderer. * See https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image for more detail. + * @property {boolean} [imageSmoothing=true] Enable image smoothing. * @property {Object} params WMS request parameters. * At least a `LAYERS` param is required. `STYLES` is * `''` by default. `VERSION` is `1.3.0` by default. `WIDTH`, `HEIGHT`, `BBOX` @@ -91,6 +92,7 @@ class TileWMS extends TileImage { attributions: options.attributions, cacheSize: options.cacheSize, crossOrigin: options.crossOrigin, + imageSmoothing: options.imageSmoothing, opaque: !transparent, projection: options.projection, reprojectionErrorThreshold: options.reprojectionErrorThreshold, diff --git a/src/ol/source/WMTS.js b/src/ol/source/WMTS.js index 77663ad2a3..5a6cdb2493 100644 --- a/src/ol/source/WMTS.js +++ b/src/ol/source/WMTS.js @@ -18,6 +18,7 @@ import {appendParams} from '../uri.js'; * @property {null|string} [crossOrigin] The `crossOrigin` attribute for loaded images. Note that * you must provide a `crossOrigin` value if you want to access pixel data with the Canvas renderer. * See https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image for more detail. + * @property {boolean} [imageSmoothing=true] Enable image smoothing. * @property {import("../tilegrid/WMTS.js").default} tileGrid Tile grid. * @property {import("../proj.js").ProjectionLike} [projection] Projection. Default is the view projection. * @property {number} [reprojectionErrorThreshold=0.5] Maximum allowed reprojection error (in pixels). @@ -84,6 +85,7 @@ class WMTS extends TileImage { attributions: options.attributions, cacheSize: options.cacheSize, crossOrigin: options.crossOrigin, + imageSmoothing: options.imageSmoothing, projection: options.projection, reprojectionErrorThreshold: options.reprojectionErrorThreshold, tileClass: options.tileClass, diff --git a/src/ol/source/XYZ.js b/src/ol/source/XYZ.js index 85b4ae595e..df62402006 100644 --- a/src/ol/source/XYZ.js +++ b/src/ol/source/XYZ.js @@ -13,6 +13,7 @@ import {createXYZ, extentFromProjection} from '../tilegrid.js'; * @property {null|string} [crossOrigin] The `crossOrigin` attribute for loaded images. Note that * you must provide a `crossOrigin` value if you want to access pixel data with the Canvas renderer. * See https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image for more detail. + * @property {boolean} [imageSmoothing=true] Enable image smoothing. * @property {boolean} [opaque=true] Whether the layer is opaque. * @property {import("../proj.js").ProjectionLike} [projection='EPSG:3857'] Projection. * @property {number} [reprojectionErrorThreshold=0.5] Maximum allowed reprojection error (in pixels). @@ -90,6 +91,7 @@ class XYZ extends TileImage { attributions: options.attributions, cacheSize: options.cacheSize, crossOrigin: options.crossOrigin, + imageSmoothing: options.imageSmoothing, opaque: options.opaque, projection: projection, reprojectionErrorThreshold: options.reprojectionErrorThreshold, diff --git a/src/ol/source/Zoomify.js b/src/ol/source/Zoomify.js index 0c0e36533a..543dc954fe 100644 --- a/src/ol/source/Zoomify.js +++ b/src/ol/source/Zoomify.js @@ -85,6 +85,7 @@ export class CustomTile extends ImageTile { * @property {null|string} [crossOrigin] The `crossOrigin` attribute for loaded images. Note that * you must provide a `crossOrigin` value you want to access pixel data with the Canvas renderer. * See https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image for more detail. + * @property {boolean} [imageSmoothing=true] Enable image smoothing. * @property {import("../proj.js").ProjectionLike} [projection] Projection. * @property {number} [tilePixelRatio] The pixel ratio used by the tile service. For example, if the tile service advertizes 256px by 256px tiles but actually sends 512px by 512px images (for retina/hidpi devices) then `tilePixelRatio` should be set to `2` * @property {number} [reprojectionErrorThreshold=0.5] Maximum allowed reprojection error (in pixels). @@ -246,6 +247,7 @@ class Zoomify extends TileImage { attributions: options.attributions, cacheSize: options.cacheSize, crossOrigin: options.crossOrigin, + imageSmoothing: options.imageSmoothing, projection: options.projection, tilePixelRatio: tilePixelRatio, reprojectionErrorThreshold: options.reprojectionErrorThreshold, diff --git a/src/ol/source/common.js b/src/ol/source/common.js index ebf0d96808..e78ca95101 100644 --- a/src/ol/source/common.js +++ b/src/ol/source/common.js @@ -7,3 +7,12 @@ * @type {string} */ export const DEFAULT_WMS_VERSION = '1.3.0'; + +/** + * Context options to disable image smoothing. + * @type {Object} + */ +export const IMAGE_SMOOTHING_DISABLED = { + imageSmoothingEnabled: false, + msImageSmoothingEnabled: false +};