From 8e12709c6446551d99fd2791a623456d0b2b9585 Mon Sep 17 00:00:00 2001 From: Jean-Francois Leblanc-Richard Date: Thu, 13 Jan 2022 15:55:29 -0500 Subject: [PATCH] Allow setting a custom pixel ratio (#769) --- CHANGELOG.md | 1 + bench/lib/tile_parser.ts | 6 ++- src/render/draw_debug.ts | 2 +- src/render/painter.ts | 8 +-- src/render/program/circle_program.ts | 2 +- src/render/program/line_program.ts | 6 +-- src/render/program/symbol_program.ts | 2 +- src/source/geojson_source.test.ts | 3 +- src/source/geojson_source.ts | 2 +- src/source/raster_dem_tile_source.test.ts | 3 +- src/source/raster_dem_tile_source.ts | 2 +- src/source/raster_tile_source.test.ts | 3 +- src/source/raster_tile_source.ts | 2 +- src/source/tile_id.test.ts | 22 ++++---- src/source/tile_id.ts | 4 +- src/source/vector_tile_source.test.ts | 3 +- src/source/vector_tile_source.ts | 4 +- src/style/load_sprite.ts | 3 +- src/style/style.test.ts | 4 ++ src/style/style.ts | 2 +- src/ui/map.test.ts | 61 +++++++++++++++++++++++ src/ui/map.ts | 37 +++++++++++--- 22 files changed, 141 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29698319c9..40d3b6b3ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ - Improve attribution controls accessibility. See [#359](https://github.com/maplibre/maplibre-gl-js/issues/359) - Allow maxPitch value up to 85, use values greater than 60 at your own risk (#574) - `getImage` uses createImageBitmap when supported (#650) +- Allow setting a custom pixel ratio by adding a `MapOptions#pixelRatio` property and a `Map#setPixelRatio` method (#769) - *...Add new stuff here...* ### 🐞 Bug fixes diff --git a/bench/lib/tile_parser.ts b/bench/lib/tile_parser.ts index 064ad669c5..b93f44c9bb 100644 --- a/bench/lib/tile_parser.ts +++ b/bench/lib/tile_parser.ts @@ -22,6 +22,10 @@ class StubMap extends Evented { super(); this._requestManager = new RequestManager(); } + + getPixelRatio() { + return devicePixelRatio; + } } const mapStub = new StubMap() as any as Map; @@ -104,7 +108,7 @@ export default class TileParser { } fetchTile(tileID: OverscaledTileID) { - return fetch(tileID.canonical.url(this.tileJSON.tiles)) + return fetch(tileID.canonical.url(this.tileJSON.tiles, devicePixelRatio)) .then(response => response.arrayBuffer()) .then(buffer => ({tileID, buffer})); } diff --git a/src/render/draw_debug.ts b/src/render/draw_debug.ts index 1a76fcacda..23250c50fd 100644 --- a/src/render/draw_debug.ts +++ b/src/render/draw_debug.ts @@ -55,7 +55,7 @@ function drawDebugSSRect(painter: Painter, x: number, y: number, width: number, const gl = context.gl; gl.enable(gl.SCISSOR_TEST); - gl.scissor(x * devicePixelRatio, y * devicePixelRatio, width * devicePixelRatio, height * devicePixelRatio); + gl.scissor(x * painter.pixelRatio, y * painter.pixelRatio, width * painter.pixelRatio, height * painter.pixelRatio); context.clear({color}); gl.disable(gl.SCISSOR_TEST); } diff --git a/src/render/painter.ts b/src/render/painter.ts index 990a8cf9ee..aa1f3d0583 100644 --- a/src/render/painter.ts +++ b/src/render/painter.ts @@ -92,6 +92,7 @@ class Painter { emptyProgramConfiguration: ProgramConfiguration; width: number; height: number; + pixelRatio: number; tileExtentBuffer: VertexBuffer; tileExtentSegments: SegmentVector; debugBuffer: VertexBuffer; @@ -146,9 +147,10 @@ class Painter { * Update the GL viewport, projection matrix, and transforms to compensate * for a new width and height value. */ - resize(width: number, height: number) { - this.width = width * devicePixelRatio; - this.height = height * devicePixelRatio; + resize(width: number, height: number, pixelRatio: number) { + this.width = width * pixelRatio; + this.height = height * pixelRatio; + this.pixelRatio = pixelRatio; this.context.viewport.set([0, 0, this.width, this.height]); if (this.style) { diff --git a/src/render/program/circle_program.ts b/src/render/program/circle_program.ts index 8d208fa3c4..73cd8d38f0 100644 --- a/src/render/program/circle_program.ts +++ b/src/render/program/circle_program.ts @@ -53,7 +53,7 @@ const circleUniformValues = ( layer.paint.get('circle-translate'), layer.paint.get('circle-translate-anchor')), 'u_pitch_with_map': +(pitchWithMap), - 'u_device_pixel_ratio': devicePixelRatio, + 'u_device_pixel_ratio': painter.pixelRatio, 'u_extrude_scale': extrudeScale }; }; diff --git a/src/render/program/line_program.ts b/src/render/program/line_program.ts index 3cecd7f49d..2d528361fd 100644 --- a/src/render/program/line_program.ts +++ b/src/render/program/line_program.ts @@ -99,7 +99,7 @@ const lineUniformValues = (painter: Painter, tile: Tile, layer: LineStyleLayer): return { 'u_matrix': calculateMatrix(painter, tile, layer), 'u_ratio': 1 / pixelsToTileUnits(tile, 1, transform.zoom), - 'u_device_pixel_ratio': devicePixelRatio, + 'u_device_pixel_ratio': painter.pixelRatio, 'u_units_to_pixels': [ 1 / transform.pixelsToGLUnits[0], 1 / transform.pixelsToGLUnits[1] @@ -127,7 +127,7 @@ const linePatternUniformValues = ( 'u_texsize': tile.imageAtlasTexture.size, // camera zoom ratio 'u_ratio': 1 / pixelsToTileUnits(tile, 1, transform.zoom), - 'u_device_pixel_ratio': devicePixelRatio, + 'u_device_pixel_ratio': painter.pixelRatio, 'u_image': 0, 'u_scale': [tileZoomRatio, crossfade.fromScale, crossfade.toScale], 'u_fade': crossfade.t, @@ -160,7 +160,7 @@ const lineSDFUniformValues = ( return extend(lineUniformValues(painter, tile, layer), { 'u_patternscale_a': [tileRatio / widthA, -posA.height / 2], 'u_patternscale_b': [tileRatio / widthB, -posB.height / 2], - 'u_sdfgamma': lineAtlas.width / (Math.min(widthA, widthB) * 256 * devicePixelRatio) / 2, + 'u_sdfgamma': lineAtlas.width / (Math.min(widthA, widthB) * 256 * painter.pixelRatio) / 2, 'u_image': 0, 'u_tex_y_a': posA.y, 'u_tex_y_b': posB.y, diff --git a/src/render/program/symbol_program.ts b/src/render/program/symbol_program.ts index d8318c9aa3..f4b424a105 100644 --- a/src/render/program/symbol_program.ts +++ b/src/render/program/symbol_program.ts @@ -195,7 +195,7 @@ const symbolSDFUniformValues = ( rotateInShader, pitchWithMap, painter, matrix, labelPlaneMatrix, glCoordMatrix, isText, texSize), { 'u_gamma_scale': (pitchWithMap ? Math.cos(transform._pitch) * transform.cameraToCenterDistance : 1), - 'u_device_pixel_ratio': devicePixelRatio, + 'u_device_pixel_ratio': painter.pixelRatio, 'u_is_halo': +isHalo }); }; diff --git a/src/source/geojson_source.test.ts b/src/source/geojson_source.test.ts index 9cbd4c84cd..f85d7ed72c 100644 --- a/src/source/geojson_source.test.ts +++ b/src/source/geojson_source.test.ts @@ -291,7 +291,8 @@ describe('GeoJSONSource#update', () => { const source = new GeoJSONSource('id', {data: {}} as GeoJSONSourceOptions, mockDispatcher, undefined); source.map = { - transform: {} as Transform + transform: {} as Transform, + getPixelRatio() { return 1; } } as any; source.on('data', (e) => { diff --git a/src/source/geojson_source.ts b/src/source/geojson_source.ts index 4f4347c7d5..08ce047e2f 100644 --- a/src/source/geojson_source.ts +++ b/src/source/geojson_source.ts @@ -301,7 +301,7 @@ class GeoJSONSource extends Evented implements Source { maxZoom: this.maxzoom, tileSize: this.tileSize, source: this.id, - pixelRatio: devicePixelRatio, + pixelRatio: this.map.getPixelRatio(), showCollisionBoxes: this.map.showCollisionBoxes, promoteId: this.promoteId }; diff --git a/src/source/raster_dem_tile_source.test.ts b/src/source/raster_dem_tile_source.test.ts index e07a402a4a..e051d34d44 100644 --- a/src/source/raster_dem_tile_source.test.ts +++ b/src/source/raster_dem_tile_source.test.ts @@ -10,7 +10,8 @@ function createSource(options, transformCallback?) { source.onAdd({ transform: {angle: 0, pitch: 0, showCollisionBoxes: false}, _getMapId: () => 1, - _requestManager: new RequestManager(transformCallback) + _requestManager: new RequestManager(transformCallback), + getPixelRatio() { return 1; } } as any); source.on('error', (e) => { diff --git a/src/source/raster_dem_tile_source.ts b/src/source/raster_dem_tile_source.ts index 676f4f0ede..546f54a49f 100644 --- a/src/source/raster_dem_tile_source.ts +++ b/src/source/raster_dem_tile_source.ts @@ -37,7 +37,7 @@ class RasterDEMTileSource extends RasterTileSource implements Source { } loadTile(tile: Tile, callback: Callback) { - const url = tile.tileID.canonical.url(this.tiles, this.scheme); + const url = tile.tileID.canonical.url(this.tiles, this.map.getPixelRatio(), this.scheme); tile.request = getImage(this.map._requestManager.transformRequest(url, ResourceType.Tile), imageLoaded.bind(this)); tile.neighboringTiles = this._getNeighboringTiles(tile.tileID); diff --git a/src/source/raster_tile_source.test.ts b/src/source/raster_tile_source.test.ts index 103e084cbd..7c1ef6b7ed 100644 --- a/src/source/raster_tile_source.test.ts +++ b/src/source/raster_tile_source.test.ts @@ -10,7 +10,8 @@ function createSource(options, transformCallback?) { source.onAdd({ transform: {angle: 0, pitch: 0, showCollisionBoxes: false}, _getMapId: () => 1, - _requestManager: new RequestManager(transformCallback) + _requestManager: new RequestManager(transformCallback), + getPixelRatio() { return 1; } } as any); source.on('error', (e) => { diff --git a/src/source/raster_tile_source.ts b/src/source/raster_tile_source.ts index 560157c519..67eb6addf2 100644 --- a/src/source/raster_tile_source.ts +++ b/src/source/raster_tile_source.ts @@ -104,7 +104,7 @@ class RasterTileSource extends Evented implements Source { } loadTile(tile: Tile, callback: Callback) { - const url = tile.tileID.canonical.url(this.tiles, this.scheme); + const url = tile.tileID.canonical.url(this.tiles, this.map.getPixelRatio(), this.scheme); tile.request = getImage(this.map._requestManager.transformRequest(url, ResourceType.Tile), (err, img) => { delete tile.request; diff --git a/src/source/tile_id.test.ts b/src/source/tile_id.test.ts index dc8ee3db27..476169e314 100644 --- a/src/source/tile_id.test.ts +++ b/src/source/tile_id.test.ts @@ -33,35 +33,33 @@ describe('CanonicalTileID', () => { }); test('.url replaces {z}/{x}/{y}', () => { - expect(new CanonicalTileID(2, 1, 0).url(['{z}/{x}/{y}.json'])).toBe('2/1/0.json'); + expect(new CanonicalTileID(2, 1, 0).url(['{z}/{x}/{y}.json'], 1)).toBe('2/1/0.json'); }); test('.url replaces {quadkey}', () => { - expect(new CanonicalTileID(1, 0, 0).url(['quadkey={quadkey}'])).toBe('quadkey=0'); - expect(new CanonicalTileID(2, 0, 0).url(['quadkey={quadkey}'])).toBe('quadkey=00'); - expect(new CanonicalTileID(2, 1, 1).url(['quadkey={quadkey}'])).toBe('quadkey=03'); - expect(new CanonicalTileID(17, 22914, 52870).url(['quadkey={quadkey}'])).toBe('quadkey=02301322130000230'); + expect(new CanonicalTileID(1, 0, 0).url(['quadkey={quadkey}'], 1)).toBe('quadkey=0'); + expect(new CanonicalTileID(2, 0, 0).url(['quadkey={quadkey}'], 1)).toBe('quadkey=00'); + expect(new CanonicalTileID(2, 1, 1).url(['quadkey={quadkey}'], 1)).toBe('quadkey=03'); + expect(new CanonicalTileID(17, 22914, 52870).url(['quadkey={quadkey}'], 1)).toBe('quadkey=02301322130000230'); // Test case confirmed by quadkeytools package // https://bitbucket.org/steele/quadkeytools/rollup/build/tsc/src/master/test/quadkey.js?fileviewer=file-view-default#quadkey.js-57 - expect(new CanonicalTileID(6, 29, 3).url(['quadkey={quadkey}'])).toBe('quadkey=011123'); + expect(new CanonicalTileID(6, 29, 3).url(['quadkey={quadkey}'], 1)).toBe('quadkey=011123'); }); test('.url replaces {bbox-epsg-3857}', () => { - expect(new CanonicalTileID(1, 0, 0).url(['bbox={bbox-epsg-3857}'])).toBe('bbox=-20037508.342789244,0,0,20037508.342789244'); + expect(new CanonicalTileID(1, 0, 0).url(['bbox={bbox-epsg-3857}'], 1)).toBe('bbox=-20037508.342789244,0,0,20037508.342789244'); }); test('.url replaces {ratio}', () => { - devicePixelRatio = 2; - expect(new CanonicalTileID(1, 0, 0).url(['r={ratio}'])).toBe('r=@2x'); - devicePixelRatio = 1; - expect(new CanonicalTileID(1, 0, 0).url(['r={ratio}'])).toBe('r='); + expect(new CanonicalTileID(1, 0, 0).url(['r={ratio}'], 2)).toBe('r=@2x'); + expect(new CanonicalTileID(1, 0, 0).url(['r={ratio}'], 1)).toBe('r='); }); //Tests that multiple values of the same placeholder are replaced. test('.url replaces {z}/{x}/{y}/{z}/{x}/{y}', () => { - expect(new CanonicalTileID(2, 1, 0).url(['{z}/{x}/{y}/{z}/{x}/{y}.json'])).toBe('2/1/0/2/1/0.json'); + expect(new CanonicalTileID(2, 1, 0).url(['{z}/{x}/{y}/{z}/{x}/{y}.json'], 1)).toBe('2/1/0/2/1/0.json'); }); }); diff --git a/src/source/tile_id.ts b/src/source/tile_id.ts index 2b73e3dc52..2275ee8c76 100644 --- a/src/source/tile_id.ts +++ b/src/source/tile_id.ts @@ -28,7 +28,7 @@ export class CanonicalTileID { } // given a list of urls, choose a url template and return a tile URL - url(urls: Array, scheme?: string | null) { + url(urls: Array, pixelRatio: number, scheme?: string | null) { const bbox = getTileBBox(this.x, this.y, this.z); const quadkey = getQuadkey(this.z, this.x, this.y); @@ -37,7 +37,7 @@ export class CanonicalTileID { .replace(/{z}/g, String(this.z)) .replace(/{x}/g, String(this.x)) .replace(/{y}/g, String(scheme === 'tms' ? (Math.pow(2, this.z) - this.y - 1) : this.y)) - .replace(/{ratio}/g, devicePixelRatio > 1 ? '@2x' : '') + .replace(/{ratio}/g, pixelRatio > 1 ? '@2x' : '') .replace(/{quadkey}/g, quadkey) .replace(/{bbox-epsg-3857}/g, bbox); } diff --git a/src/source/vector_tile_source.test.ts b/src/source/vector_tile_source.test.ts index bff34bea5c..b8099ec03f 100644 --- a/src/source/vector_tile_source.test.ts +++ b/src/source/vector_tile_source.test.ts @@ -15,7 +15,8 @@ function createSource(options, transformCallback?) { transform: {showCollisionBoxes: false}, _getMapId: () => 1, _requestManager: new RequestManager(transformCallback), - style: {sourceCaches: {id: {clearTiles: () => {}}}} + style: {sourceCaches: {id: {clearTiles: () => {}}}}, + getPixelRatio() { return 1; } } as any as Map); source.on('error', (e) => { diff --git a/src/source/vector_tile_source.ts b/src/source/vector_tile_source.ts index a0378e3958..8bb2c9d0da 100644 --- a/src/source/vector_tile_source.ts +++ b/src/source/vector_tile_source.ts @@ -178,7 +178,7 @@ class VectorTileSource extends Evented implements Source { } loadTile(tile: Tile, callback: Callback) { - const url = tile.tileID.canonical.url(this.tiles, this.scheme); + const url = tile.tileID.canonical.url(this.tiles, this.map.getPixelRatio(), this.scheme); const params = { request: this.map._requestManager.transformRequest(url, ResourceType.Tile), uid: tile.uid, @@ -187,7 +187,7 @@ class VectorTileSource extends Evented implements Source { tileSize: this.tileSize * tile.tileID.overscaleFactor(), type: this.type, source: this.id, - pixelRatio: devicePixelRatio, + pixelRatio: this.map.getPixelRatio(), showCollisionBoxes: this.map.showCollisionBoxes, promoteId: this.promoteId }; diff --git a/src/style/load_sprite.ts b/src/style/load_sprite.ts index 3cbc6c127d..ddc19a9fc2 100644 --- a/src/style/load_sprite.ts +++ b/src/style/load_sprite.ts @@ -11,10 +11,11 @@ import type {Cancelable} from '../types/cancelable'; export default function( baseURL: string, requestManager: RequestManager, + pixelRatio: number, callback: Callback<{[_: string]: StyleImage}> ): Cancelable { let json: any, image, error; - const format = devicePixelRatio > 1 ? '@2x' : ''; + const format = pixelRatio > 1 ? '@2x' : ''; let jsonRequest = getJSON(requestManager.transformRequest(requestManager.normalizeSpriteURL(baseURL, format, '.json'), ResourceType.SpriteJSON), (err?: Error | null, data?: any | null) => { jsonRequest = null; diff --git a/src/style/style.test.ts b/src/style/style.test.ts index fc1807e412..469c74b992 100644 --- a/src/style/style.test.ts +++ b/src/style/style.test.ts @@ -60,6 +60,10 @@ class StubMap extends Evented { _getMapId() { return 1; } + + getPixelRatio() { + return 1; + } } const getStubMap = () => new StubMap() as any; diff --git a/src/style/style.ts b/src/style/style.ts index abf4d6a4de..c5059b1f7f 100644 --- a/src/style/style.ts +++ b/src/style/style.ts @@ -276,7 +276,7 @@ class Style extends Evented { } _loadSprite(url: string) { - this._spriteRequest = loadSprite(url, this.map._requestManager, (err, images) => { + this._spriteRequest = loadSprite(url, this.map._requestManager, this.map.getPixelRatio(), (err, images) => { this._spriteRequest = null; if (err) { this.fire(new ErrorEvent(err)); diff --git a/src/ui/map.test.ts b/src/ui/map.test.ts index 84db1db339..89e6d22695 100755 --- a/src/ui/map.test.ts +++ b/src/ui/map.test.ts @@ -2048,6 +2048,67 @@ describe('Map', () => { }); }); + describe('setPixelRatio', () => { + test('resizes canvas', () => { + const container = window.document.createElement('div'); + Object.defineProperty(container, 'clientWidth', {value: 512}); + Object.defineProperty(container, 'clientHeight', {value: 512}); + const map = createMap({container, pixelRatio: 1}); + expect(map.getCanvas().width).toBe(512); + expect(map.getCanvas().height).toBe(512); + map.setPixelRatio(2); + expect(map.getCanvas().width).toBe(1024); + expect(map.getCanvas().height).toBe(1024); + }); + + test('resizes painter', () => { + const container = window.document.createElement('div'); + Object.defineProperty(container, 'clientWidth', {value: 512}); + Object.defineProperty(container, 'clientHeight', {value: 512}); + const map = createMap({container, pixelRatio: 1}); + expect(map.painter.pixelRatio).toBe(1); + expect(map.painter.width).toBe(512); + expect(map.painter.height).toBe(512); + map.setPixelRatio(2); + expect(map.painter.pixelRatio).toBe(2); + expect(map.painter.width).toBe(1024); + expect(map.painter.height).toBe(1024); + }); + }); + + describe('getPixelRatio', () => { + test('returns the pixel ratio', () => { + const map = createMap({pixelRatio: 1}); + expect(map.getPixelRatio()).toBe(1); + map.setPixelRatio(2); + expect(map.getPixelRatio()).toBe(2); + }); + }); + + test('pixel ratio defaults to devicePixelRatio', () => { + const map = createMap(); + expect(map.getPixelRatio()).toBe(devicePixelRatio); + }); + + test('canvas has the expected size', () => { + const container = window.document.createElement('div'); + Object.defineProperty(container, 'clientWidth', {value: 512}); + Object.defineProperty(container, 'clientHeight', {value: 512}); + const map = createMap({container, pixelRatio: 2}); + expect(map.getCanvas().width).toBe(1024); + expect(map.getCanvas().height).toBe(1024); + }); + + test('painter has the expected size and pixel ratio', () => { + const container = window.document.createElement('div'); + Object.defineProperty(container, 'clientWidth', {value: 512}); + Object.defineProperty(container, 'clientHeight', {value: 512}); + const map = createMap({container, pixelRatio: 2}); + expect(map.painter.pixelRatio).toBe(2); + expect(map.painter.width).toBe(1024); + expect(map.painter.height).toBe(1024); + }); + }); function createStyle() { diff --git a/src/ui/map.ts b/src/ui/map.ts index 701335445f..9b13883153 100755 --- a/src/ui/map.ts +++ b/src/ui/map.ts @@ -101,6 +101,7 @@ export type MapOptions = { localIdeographFontFamily?: string; style: StyleSpecification | string; pitchWithRotate?: boolean; + pixelRatio?: number; }; // See article here: https://medium.com/terria/typescript-transforming-optional-properties-to-required-properties-that-may-be-undefined-7482cb4e1585 @@ -252,6 +253,7 @@ const defaultOptions = { * @param {number} [options.fadeDuration=300] Controls the duration of the fade-in/fade-out animation for label collisions, in milliseconds. This setting affects all symbol layers. This setting does not affect the duration of runtime styling transitions or raster tile cross-fading. * @param {boolean} [options.crossSourceCollisions=true] If `true`, symbols from multiple sources can collide with each other during collision detection. If `false`, collision detection is run separately for the symbols in each source. * @param {Object} [options.locale=null] A patch to apply to the default localization table for UI strings, e.g. control tooltips. The `locale` object maps namespaced UI string IDs to translated strings in the target language; see `src/ui/default_locale.js` for an example with all supported string IDs. The object may specify all UI strings (thereby adding support for a new translation) or only a subset of strings (thereby patching the default translation table). + * @param {number} [options.pixelRatio] The pixel ratio. The canvas' `width` attribute will be `container.clientWidth * pixelRatio` and its `height` attribute will be `container.clientHeight * pixelRatio`. Defaults to `devicePixelRatio` if not specified. * @example * var map = new maplibregl.Map({ * container: 'map', @@ -315,6 +317,7 @@ class Map extends Camera { _locale: any; _removed: boolean; _clickTolerance: number; + _pixelRatio: number; /** * The map's {@link ScrollZoomHandler}, which implements zooming in and out with a scroll wheel or trackpad. @@ -406,6 +409,7 @@ class Map extends Camera { this._mapId = uniqueId(); this._locale = extend({}, defaultLocale, options.locale); this._clickTolerance = options.clickTolerance; + this._pixelRatio = options.pixelRatio ?? devicePixelRatio; this._requestManager = new RequestManager(options.transformRequest); @@ -601,9 +605,9 @@ class Map extends Camera { const width = dimensions[0]; const height = dimensions[1]; - this._resizeCanvas(width, height); + this._resizeCanvas(width, height, this.getPixelRatio()); this.transform.resize(width, height); - this.painter.resize(width, height); + this.painter.resize(width, height, this.getPixelRatio()); const fireMoving = !this._moving; if (fireMoving) { @@ -619,6 +623,29 @@ class Map extends Camera { return this; } + /** + * Returns the map's pixel ratio. + * @returns {number} The pixel ratio. + */ + getPixelRatio() { + return this._pixelRatio; + } + + /** + * Sets the map's pixel ratio. This allows to override `devicePixelRatio`. + * After this call, the canvas' `width` attribute will be `container.clientWidth * pixelRatio` + * and its height attribute will be `container.clientHeight * pixelRatio`. + * @param pixelRatio {number} The pixel ratio. + */ + setPixelRatio(pixelRatio: number) { + const [width, height] = this._containerDimensions(); + + this._pixelRatio = pixelRatio; + + this._resizeCanvas(width, height, pixelRatio); + this.painter.resize(width, height, pixelRatio); + } + /** * Returns the map's geographical bounds. When the bearing or pitch is non-zero, the visible region is not * an axis-aligned rectangle, and the result is the smallest bounds that encompasses the visible region. @@ -2368,7 +2395,7 @@ class Map extends Camera { this._canvas.setAttribute('role', 'region'); const dimensions = this._containerDimensions(); - this._resizeCanvas(dimensions[0], dimensions[1]); + this._resizeCanvas(dimensions[0], dimensions[1], this.getPixelRatio()); const controlContainer = this._controlContainer = DOM.create('div', 'maplibregl-control-container mapboxgl-control-container', container); const positions = this._controlPositions = {}; @@ -2379,9 +2406,7 @@ class Map extends Camera { this._container.addEventListener('scroll', this._onMapScroll, false); } - _resizeCanvas(width: number, height: number) { - const pixelRatio = devicePixelRatio || 1; - + _resizeCanvas(width: number, height: number, pixelRatio: number) { // Request the required canvas size taking the pixelratio into account. this._canvas.width = pixelRatio * width; this._canvas.height = pixelRatio * height;