maplibre-gl 2.0.5 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/maplibre-gl.css +1 -1
  2. package/dist/maplibre-gl.d.ts +38 -12
  3. package/dist/maplibre-gl.js +3 -3
  4. package/dist/maplibre-gl.js.map +1 -1
  5. package/package.json +21 -33
  6. package/src/css/maplibre-gl.css +21 -21
  7. package/src/data/bucket/symbol_bucket.test.ts +1 -1
  8. package/src/data/bucket/symbol_bucket.ts +3 -3
  9. package/src/data/program_configuration.ts +1 -1
  10. package/src/render/draw_debug.ts +1 -1
  11. package/src/render/glyph_manager.ts +1 -1
  12. package/src/render/painter.ts +5 -3
  13. package/src/render/program/circle_program.ts +1 -1
  14. package/src/render/program/line_program.ts +3 -3
  15. package/src/render/program/symbol_program.ts +1 -1
  16. package/src/source/geojson_source.test.ts +2 -1
  17. package/src/source/geojson_source.ts +1 -1
  18. package/src/source/raster_dem_tile_source.test.ts +2 -1
  19. package/src/source/raster_dem_tile_source.ts +1 -1
  20. package/src/source/raster_tile_source.test.ts +2 -1
  21. package/src/source/raster_tile_source.ts +1 -1
  22. package/src/source/source_cache.test.ts +1 -1
  23. package/src/source/tile.test.ts +1 -1
  24. package/src/source/tile_id.test.ts +10 -12
  25. package/src/source/tile_id.ts +2 -2
  26. package/src/source/vector_tile_source.test.ts +2 -1
  27. package/src/source/vector_tile_source.ts +2 -2
  28. package/src/style/load_sprite.ts +2 -1
  29. package/src/style/properties.ts +1 -1
  30. package/src/style/style.test.ts +4 -0
  31. package/src/style/style.ts +1 -1
  32. package/src/style/style_layer/custom_style_layer.ts +2 -2
  33. package/src/style/style_layer/fill_extrusion_style_layer.ts +1 -1
  34. package/src/style/style_layer/symbol_style_layer.ts +19 -0
  35. package/src/style/style_layer/symbol_style_layer_properties.ts +6 -0
  36. package/src/style-spec/CHANGELOG.md +6 -0
  37. package/src/style-spec/package.json +1 -1
  38. package/src/style-spec/reference/v8.json +68 -2
  39. package/src/style-spec/types.ts +2 -0
  40. package/src/symbol/collision_index.ts +19 -19
  41. package/src/symbol/grid_index.test.ts +42 -19
  42. package/src/symbol/grid_index.ts +62 -33
  43. package/src/symbol/placement.ts +82 -53
  44. package/src/symbol/symbol_style_layer.test.ts +48 -1
  45. package/src/ui/map.test.ts +61 -0
  46. package/src/ui/map.ts +31 -6
@@ -1,4 +1,5 @@
1
1
  import CollisionIndex from './collision_index';
2
+ import type {FeatureKey} from './collision_index';
2
3
  import EXTENT from '../data/extent';
3
4
  import * as symbolSize from './symbol_size';
4
5
  import * as projection from './projection';
@@ -12,6 +13,7 @@ import type Transform from '../geo/transform';
12
13
  import type StyleLayer from '../style/style_layer';
13
14
  import {PossiblyEvaluated} from '../style/properties';
14
15
  import type {SymbolLayoutProps, SymbolLayoutPropsPossiblyEvaluated} from '../style/style_layer/symbol_style_layer_properties';
16
+ import {getOverlapMode, OverlapMode} from '../style/style_layer/symbol_style_layer';
15
17
 
16
18
  import type Tile from '../source/tile';
17
19
  import SymbolBucket, {CollisionArrays, SingleCollisionBox} from '../data/bucket/symbol_bucket';
@@ -99,7 +101,7 @@ export class RetainedQueryData {
99
101
 
100
102
  type CollisionGroup = {
101
103
  ID: number;
102
- predicate?: any;
104
+ predicate?: (key: FeatureKey) => boolean;
103
105
  };
104
106
 
105
107
  class CollisionGroups {
@@ -181,20 +183,20 @@ export type VariableOffset = {
181
183
  };
182
184
 
183
185
  type TileLayerParameters = {
184
- bucket: SymbolBucket;
185
- layout: PossiblyEvaluated<SymbolLayoutProps, SymbolLayoutPropsPossiblyEvaluated>;
186
- posMatrix: mat4;
187
- textLabelPlaneMatrix: mat4;
188
- labelToScreenMatrix: mat4;
189
- scale: number;
190
- textPixelRatio: number;
191
- holdingForFade: boolean;
192
- collisionBoxArray: CollisionBoxArray;
193
- partiallyEvaluatedTextSize: {
194
- uSize: number;
195
- uSizeT: number;
196
- };
197
- collisionGroup: CollisionGroup;
186
+ bucket: SymbolBucket;
187
+ layout: PossiblyEvaluated<SymbolLayoutProps, SymbolLayoutPropsPossiblyEvaluated>;
188
+ posMatrix: mat4;
189
+ textLabelPlaneMatrix: mat4;
190
+ labelToScreenMatrix: mat4;
191
+ scale: number;
192
+ textPixelRatio: number;
193
+ holdingForFade: boolean;
194
+ collisionBoxArray: CollisionBoxArray;
195
+ partiallyEvaluatedTextSize: {
196
+ uSize: number;
197
+ uSizeT: number;
198
+ };
199
+ collisionGroup: CollisionGroup;
198
200
  };
199
201
 
200
202
  export type BucketPart = {
@@ -344,7 +346,7 @@ export class Placement {
344
346
  textPixelRatio: number,
345
347
  posMatrix: mat4,
346
348
  collisionGroup: CollisionGroup,
347
- textAllowOverlap: boolean,
349
+ textOverlapMode: OverlapMode,
348
350
  symbolInstance: SymbolInstance,
349
351
  bucket: SymbolBucket,
350
352
  orientation: number,
@@ -364,14 +366,14 @@ export class Placement {
364
366
  shiftVariableCollisionBox(
365
367
  textBox, shift.x, shift.y,
366
368
  rotateWithMap, pitchWithMap, this.transform.angle),
367
- textAllowOverlap, textPixelRatio, posMatrix, collisionGroup.predicate);
369
+ textOverlapMode, textPixelRatio, posMatrix, collisionGroup.predicate);
368
370
 
369
371
  if (iconBox) {
370
372
  const placedIconBoxes = this.collisionIndex.placeCollisionBox(
371
373
  shiftVariableCollisionBox(
372
374
  iconBox, shift.x, shift.y,
373
375
  rotateWithMap, pitchWithMap, this.transform.angle),
374
- textAllowOverlap, textPixelRatio, posMatrix, collisionGroup.predicate);
376
+ textOverlapMode, textPixelRatio, posMatrix, collisionGroup.predicate);
375
377
  if (placedIconBoxes.box.length === 0) return;
376
378
  }
377
379
 
@@ -424,8 +426,10 @@ export class Placement {
424
426
 
425
427
  const textOptional = layout.get('text-optional');
426
428
  const iconOptional = layout.get('icon-optional');
427
- const textAllowOverlap = layout.get('text-allow-overlap');
428
- const iconAllowOverlap = layout.get('icon-allow-overlap');
429
+ const textOverlapMode = getOverlapMode(layout, 'text-overlap', 'text-allow-overlap');
430
+ const textAlwaysOverlap = textOverlapMode === 'always';
431
+ const iconOverlapMode = getOverlapMode(layout, 'icon-overlap', 'icon-allow-overlap');
432
+ const iconAlwaysOverlap = iconOverlapMode === 'always';
429
433
  const rotateWithMap = layout.get('text-rotation-alignment') === 'map';
430
434
  const pitchWithMap = layout.get('text-pitch-alignment') === 'map';
431
435
  const hasIconTextFit = layout.get('icon-text-fit') !== 'none';
@@ -445,8 +449,8 @@ export class Placement {
445
449
  // This is the reverse of our normal policy of "fade in on pan", but should look like any other
446
450
  // collision and hopefully not be too noticeable.
447
451
  // See https://github.com/mapbox/mapbox-gl-js/issues/7172
448
- const alwaysShowText = textAllowOverlap && (iconAllowOverlap || !bucket.hasIconData() || iconOptional);
449
- const alwaysShowIcon = iconAllowOverlap && (textAllowOverlap || !bucket.hasTextData() || textOptional);
452
+ const alwaysShowText = textAlwaysOverlap && (iconAlwaysOverlap || !bucket.hasIconData() || iconOptional);
453
+ const alwaysShowIcon = iconAlwaysOverlap && (textAlwaysOverlap || !bucket.hasTextData() || textOptional);
450
454
 
451
455
  if (!bucket.collisionArrays && collisionBoxArray) {
452
456
  bucket.deserializeCollisionBoxes(collisionBoxArray);
@@ -519,8 +523,12 @@ export class Placement {
519
523
 
520
524
  if (!layout.get('text-variable-anchor')) {
521
525
  const placeBox = (collisionTextBox, orientation) => {
522
- const placedFeature = this.collisionIndex.placeCollisionBox(collisionTextBox, textAllowOverlap,
523
- textPixelRatio, posMatrix, collisionGroup.predicate);
526
+ const placedFeature = this.collisionIndex.placeCollisionBox(
527
+ collisionTextBox,
528
+ textOverlapMode,
529
+ textPixelRatio,
530
+ posMatrix,
531
+ collisionGroup.predicate);
524
532
  if (placedFeature && placedFeature.box && placedFeature.box.length) {
525
533
  this.markUsedOrientation(bucket, orientation, symbolInstance);
526
534
  this.placedOrientations[symbolInstance.crossTileID] = orientation;
@@ -562,20 +570,20 @@ export class Placement {
562
570
  const height = collisionTextBox.y2 - collisionTextBox.y1;
563
571
  const textBoxScale = symbolInstance.textBoxScale;
564
572
 
565
- const variableIconBox = hasIconTextFit && !iconAllowOverlap ? collisionIconBox : null;
573
+ const variableIconBox = hasIconTextFit && (iconOverlapMode === 'never') ? collisionIconBox : null;
566
574
 
567
575
  let placedBox: {
568
576
  box: Array<number>;
569
577
  offscreen: boolean;
570
578
  } = {box: [], offscreen: false};
571
- const placementAttempts = textAllowOverlap ? anchors.length * 2 : anchors.length;
579
+ const placementAttempts = (textOverlapMode !== 'never') ? anchors.length * 2 : anchors.length;
572
580
  for (let i = 0; i < placementAttempts; ++i) {
573
581
  const anchor = anchors[i % anchors.length];
574
- const allowOverlap = (i >= anchors.length);
582
+ const overlapMode = (i >= anchors.length) ? textOverlapMode : 'never';
575
583
  const result = this.attemptAnchorPlacement(
576
584
  anchor, collisionTextBox, width, height,
577
585
  textBoxScale, rotateWithMap, pitchWithMap, textPixelRatio, posMatrix,
578
- collisionGroup, allowOverlap, symbolInstance, bucket, orientation, variableIconBox);
586
+ collisionGroup, overlapMode, symbolInstance, bucket, orientation, variableIconBox);
579
587
 
580
588
  if (result) {
581
589
  placedBox = result.placedGlyphBoxes;
@@ -637,26 +645,27 @@ export class Placement {
637
645
  const textPixelPadding = layout.get('text-padding');
638
646
  const circlePixelDiameter = symbolInstance.collisionCircleDiameter;
639
647
 
640
- placedGlyphCircles = this.collisionIndex.placeCollisionCircles(textAllowOverlap,
641
- placedSymbol,
642
- bucket.lineVertexArray,
643
- bucket.glyphOffsetArray,
644
- fontSize,
645
- posMatrix,
646
- textLabelPlaneMatrix,
647
- labelToScreenMatrix,
648
- showCollisionBoxes,
649
- pitchWithMap,
650
- collisionGroup.predicate,
651
- circlePixelDiameter,
652
- textPixelPadding);
648
+ placedGlyphCircles = this.collisionIndex.placeCollisionCircles(
649
+ textOverlapMode,
650
+ placedSymbol,
651
+ bucket.lineVertexArray,
652
+ bucket.glyphOffsetArray,
653
+ fontSize,
654
+ posMatrix,
655
+ textLabelPlaneMatrix,
656
+ labelToScreenMatrix,
657
+ showCollisionBoxes,
658
+ pitchWithMap,
659
+ collisionGroup.predicate,
660
+ circlePixelDiameter,
661
+ textPixelPadding);
653
662
 
654
663
  assert(!placedGlyphCircles.circles.length || (!placedGlyphCircles.collisionDetected || showCollisionBoxes));
655
- // If text-allow-overlap is set, force "placedCircles" to true
664
+ // If text-overlap is set to 'always', force "placedCircles" to true
656
665
  // In theory there should always be at least one circle placed
657
666
  // in this case, but for now quirks in text-anchor
658
667
  // and text-offset may prevent that from being true.
659
- placeText = textAllowOverlap || (placedGlyphCircles.circles.length > 0 && !placedGlyphCircles.collisionDetected);
668
+ placeText = textAlwaysOverlap || (placedGlyphCircles.circles.length > 0 && !placedGlyphCircles.collisionDetected);
660
669
  offscreen = offscreen && placedGlyphCircles.offscreen;
661
670
  }
662
671
 
@@ -673,7 +682,7 @@ export class Placement {
673
682
  rotateWithMap, pitchWithMap, this.transform.angle) :
674
683
  iconBox;
675
684
  return this.collisionIndex.placeCollisionBox(shiftedIconBox,
676
- iconAllowOverlap, textPixelRatio, posMatrix, collisionGroup.predicate);
685
+ iconOverlapMode, textPixelRatio, posMatrix, collisionGroup.predicate);
677
686
  };
678
687
 
679
688
  if (placedVerticalText && placedVerticalText.box && placedVerticalText.box.length && collisionArrays.verticalIconBox) {
@@ -701,22 +710,42 @@ export class Placement {
701
710
 
702
711
  if (placeText && placedGlyphBoxes && placedGlyphBoxes.box) {
703
712
  if (placedVerticalText && placedVerticalText.box && verticalTextFeatureIndex) {
704
- this.collisionIndex.insertCollisionBox(placedGlyphBoxes.box, layout.get('text-ignore-placement'),
705
- bucket.bucketInstanceId, verticalTextFeatureIndex, collisionGroup.ID);
713
+ this.collisionIndex.insertCollisionBox(
714
+ placedGlyphBoxes.box,
715
+ textOverlapMode,
716
+ layout.get('text-ignore-placement'),
717
+ bucket.bucketInstanceId,
718
+ verticalTextFeatureIndex,
719
+ collisionGroup.ID);
706
720
  } else {
707
- this.collisionIndex.insertCollisionBox(placedGlyphBoxes.box, layout.get('text-ignore-placement'),
708
- bucket.bucketInstanceId, textFeatureIndex, collisionGroup.ID);
721
+ this.collisionIndex.insertCollisionBox(
722
+ placedGlyphBoxes.box,
723
+ textOverlapMode,
724
+ layout.get('text-ignore-placement'),
725
+ bucket.bucketInstanceId,
726
+ textFeatureIndex,
727
+ collisionGroup.ID);
709
728
  }
710
729
 
711
730
  }
712
731
  if (placeIcon && placedIconBoxes) {
713
- this.collisionIndex.insertCollisionBox(placedIconBoxes.box, layout.get('icon-ignore-placement'),
714
- bucket.bucketInstanceId, iconFeatureIndex, collisionGroup.ID);
732
+ this.collisionIndex.insertCollisionBox(
733
+ placedIconBoxes.box,
734
+ iconOverlapMode,
735
+ layout.get('icon-ignore-placement'),
736
+ bucket.bucketInstanceId,
737
+ iconFeatureIndex,
738
+ collisionGroup.ID);
715
739
  }
716
740
  if (placedGlyphCircles) {
717
741
  if (placeText) {
718
- this.collisionIndex.insertCollisionCircles(placedGlyphCircles.circles, layout.get('text-ignore-placement'),
719
- bucket.bucketInstanceId, textFeatureIndex, collisionGroup.ID);
742
+ this.collisionIndex.insertCollisionCircles(
743
+ placedGlyphCircles.circles,
744
+ textOverlapMode,
745
+ layout.get('text-ignore-placement'),
746
+ bucket.bucketInstanceId,
747
+ textFeatureIndex,
748
+ collisionGroup.ID);
720
749
  }
721
750
 
722
751
  if (showCollisionBoxes) {
@@ -887,7 +916,7 @@ export class Placement {
887
916
  updateLayerOpacities(styleLayer: StyleLayer, tiles: Array<Tile>) {
888
917
  const seenCrossTileIDs = {};
889
918
  for (const tile of tiles) {
890
- const symbolBucket = (tile.getBucket(styleLayer) as SymbolBucket);
919
+ const symbolBucket = tile.getBucket(styleLayer) as SymbolBucket;
891
920
  if (symbolBucket && tile.latestFeatureIndex && styleLayer.id === symbolBucket.layerIds[0]) {
892
921
  this.updateBucketOpacities(symbolBucket, seenCrossTileIDs, tile.collisionBoxArray);
893
922
  }
@@ -1,4 +1,4 @@
1
- import SymbolStyleLayer from '../style/style_layer/symbol_style_layer';
1
+ import SymbolStyleLayer, {getOverlapMode} from '../style/style_layer/symbol_style_layer';
2
2
  import FormatSectionOverride from '../style/format_section_override';
3
3
  import properties, {SymbolPaintPropsPossiblyEvaluated} from '../style/style_layer/symbol_style_layer_properties';
4
4
  import ZoomHistory from '../style/zoom_history';
@@ -101,3 +101,50 @@ describe('hasPaintOverrides', () => {
101
101
  });
102
102
 
103
103
  });
104
+
105
+ describe('getOverlapMode', () => {
106
+ test('defaults - no props set', () => {
107
+ const props = {};
108
+ const layer = createSymbolLayer(props);
109
+
110
+ expect(getOverlapMode(layer.layout, 'icon-overlap', 'icon-allow-overlap')).toBe('never');
111
+ expect(getOverlapMode(layer.layout, 'text-overlap', 'text-allow-overlap')).toBe('never');
112
+ });
113
+
114
+ test('-allow-overlap set', () => {
115
+ const props = {layout: {'icon-allow-overlap': false, 'text-allow-overlap': true}};
116
+ const layer = createSymbolLayer(props);
117
+
118
+ expect(getOverlapMode(layer.layout, 'icon-overlap', 'icon-allow-overlap')).toBe('never');
119
+ expect(getOverlapMode(layer.layout, 'text-overlap', 'text-allow-overlap')).toBe('always');
120
+ });
121
+
122
+ test('-overlap set', () => {
123
+ let props = {layout: {'icon-overlap': 'never', 'text-overlap': 'always'}};
124
+ let layer = createSymbolLayer(props);
125
+
126
+ expect(getOverlapMode(layer.layout, 'icon-overlap', 'icon-allow-overlap')).toBe('never');
127
+ expect(getOverlapMode(layer.layout, 'text-overlap', 'text-allow-overlap')).toBe('always');
128
+
129
+ props = {layout: {'icon-overlap': 'always', 'text-overlap': 'cooperative'}};
130
+ layer = createSymbolLayer(props);
131
+
132
+ expect(getOverlapMode(layer.layout, 'icon-overlap', 'icon-allow-overlap')).toBe('always');
133
+ expect(getOverlapMode(layer.layout, 'text-overlap', 'text-allow-overlap')).toBe('cooperative');
134
+ });
135
+
136
+ test('-overlap beats -allow-overlap', () => {
137
+ const props = {
138
+ layout: {
139
+ 'icon-overlap': 'never',
140
+ 'icon-allow-overlap': true,
141
+ 'text-overlap': 'cooperative',
142
+ 'text-allow-overlap': false
143
+ }
144
+ };
145
+ const layer = createSymbolLayer(props);
146
+
147
+ expect(getOverlapMode(layer.layout, 'icon-overlap', 'icon-allow-overlap')).toBe('never');
148
+ expect(getOverlapMode(layer.layout, 'text-overlap', 'text-allow-overlap')).toBe('cooperative');
149
+ });
150
+ });
@@ -2048,6 +2048,67 @@ describe('Map', () => {
2048
2048
  });
2049
2049
  });
2050
2050
 
2051
+ describe('setPixelRatio', () => {
2052
+ test('resizes canvas', () => {
2053
+ const container = window.document.createElement('div');
2054
+ Object.defineProperty(container, 'clientWidth', {value: 512});
2055
+ Object.defineProperty(container, 'clientHeight', {value: 512});
2056
+ const map = createMap({container, pixelRatio: 1});
2057
+ expect(map.getCanvas().width).toBe(512);
2058
+ expect(map.getCanvas().height).toBe(512);
2059
+ map.setPixelRatio(2);
2060
+ expect(map.getCanvas().width).toBe(1024);
2061
+ expect(map.getCanvas().height).toBe(1024);
2062
+ });
2063
+
2064
+ test('resizes painter', () => {
2065
+ const container = window.document.createElement('div');
2066
+ Object.defineProperty(container, 'clientWidth', {value: 512});
2067
+ Object.defineProperty(container, 'clientHeight', {value: 512});
2068
+ const map = createMap({container, pixelRatio: 1});
2069
+ expect(map.painter.pixelRatio).toBe(1);
2070
+ expect(map.painter.width).toBe(512);
2071
+ expect(map.painter.height).toBe(512);
2072
+ map.setPixelRatio(2);
2073
+ expect(map.painter.pixelRatio).toBe(2);
2074
+ expect(map.painter.width).toBe(1024);
2075
+ expect(map.painter.height).toBe(1024);
2076
+ });
2077
+ });
2078
+
2079
+ describe('getPixelRatio', () => {
2080
+ test('returns the pixel ratio', () => {
2081
+ const map = createMap({pixelRatio: 1});
2082
+ expect(map.getPixelRatio()).toBe(1);
2083
+ map.setPixelRatio(2);
2084
+ expect(map.getPixelRatio()).toBe(2);
2085
+ });
2086
+ });
2087
+
2088
+ test('pixel ratio defaults to devicePixelRatio', () => {
2089
+ const map = createMap();
2090
+ expect(map.getPixelRatio()).toBe(devicePixelRatio);
2091
+ });
2092
+
2093
+ test('canvas has the expected size', () => {
2094
+ const container = window.document.createElement('div');
2095
+ Object.defineProperty(container, 'clientWidth', {value: 512});
2096
+ Object.defineProperty(container, 'clientHeight', {value: 512});
2097
+ const map = createMap({container, pixelRatio: 2});
2098
+ expect(map.getCanvas().width).toBe(1024);
2099
+ expect(map.getCanvas().height).toBe(1024);
2100
+ });
2101
+
2102
+ test('painter has the expected size and pixel ratio', () => {
2103
+ const container = window.document.createElement('div');
2104
+ Object.defineProperty(container, 'clientWidth', {value: 512});
2105
+ Object.defineProperty(container, 'clientHeight', {value: 512});
2106
+ const map = createMap({container, pixelRatio: 2});
2107
+ expect(map.painter.pixelRatio).toBe(2);
2108
+ expect(map.painter.width).toBe(1024);
2109
+ expect(map.painter.height).toBe(1024);
2110
+ });
2111
+
2051
2112
  });
2052
2113
 
2053
2114
  function createStyle() {
package/src/ui/map.ts CHANGED
@@ -101,6 +101,7 @@ export type MapOptions = {
101
101
  localIdeographFontFamily?: string;
102
102
  style: StyleSpecification | string;
103
103
  pitchWithRotate?: boolean;
104
+ pixelRatio?: number;
104
105
  };
105
106
 
106
107
  // See article here: https://medium.com/terria/typescript-transforming-optional-properties-to-required-properties-that-may-be-undefined-7482cb4e1585
@@ -235,6 +236,7 @@ const defaultOptions = {
235
236
  * @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.
236
237
  * @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.
237
238
  * @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).
239
+ * @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.
238
240
  * @example
239
241
  * var map = new maplibregl.Map({
240
242
  * container: 'map',
@@ -298,6 +300,7 @@ class Map extends Camera {
298
300
  _locale: any;
299
301
  _removed: boolean;
300
302
  _clickTolerance: number;
303
+ _pixelRatio: number;
301
304
 
302
305
  /**
303
306
  * The map's {@link ScrollZoomHandler}, which implements zooming in and out with a scroll wheel or trackpad.
@@ -389,6 +392,7 @@ class Map extends Camera {
389
392
  this._mapId = uniqueId();
390
393
  this._locale = extend({}, defaultLocale, options.locale);
391
394
  this._clickTolerance = options.clickTolerance;
395
+ this._pixelRatio = options.pixelRatio ?? devicePixelRatio;
392
396
 
393
397
  this._requestManager = new RequestManager(options.transformRequest);
394
398
 
@@ -584,9 +588,9 @@ class Map extends Camera {
584
588
  const width = dimensions[0];
585
589
  const height = dimensions[1];
586
590
 
587
- this._resizeCanvas(width, height);
591
+ this._resizeCanvas(width, height, this.getPixelRatio());
588
592
  this.transform.resize(width, height);
589
- this.painter.resize(width, height);
593
+ this.painter.resize(width, height, this.getPixelRatio());
590
594
 
591
595
  const fireMoving = !this._moving;
592
596
  if (fireMoving) {
@@ -602,6 +606,29 @@ class Map extends Camera {
602
606
  return this;
603
607
  }
604
608
 
609
+ /**
610
+ * Returns the map's pixel ratio.
611
+ * @returns {number} The pixel ratio.
612
+ */
613
+ getPixelRatio() {
614
+ return this._pixelRatio;
615
+ }
616
+
617
+ /**
618
+ * Sets the map's pixel ratio. This allows to override `devicePixelRatio`.
619
+ * After this call, the canvas' `width` attribute will be `container.clientWidth * pixelRatio`
620
+ * and its height attribute will be `container.clientHeight * pixelRatio`.
621
+ * @param pixelRatio {number} The pixel ratio.
622
+ */
623
+ setPixelRatio(pixelRatio: number) {
624
+ const [width, height] = this._containerDimensions();
625
+
626
+ this._pixelRatio = pixelRatio;
627
+
628
+ this._resizeCanvas(width, height, pixelRatio);
629
+ this.painter.resize(width, height, pixelRatio);
630
+ }
631
+
605
632
  /**
606
633
  * Returns the map's geographical bounds. When the bearing or pitch is non-zero, the visible region is not
607
634
  * an axis-aligned rectangle, and the result is the smallest bounds that encompasses the visible region.
@@ -2349,7 +2376,7 @@ class Map extends Camera {
2349
2376
  this._canvas.setAttribute('role', 'region');
2350
2377
 
2351
2378
  const dimensions = this._containerDimensions();
2352
- this._resizeCanvas(dimensions[0], dimensions[1]);
2379
+ this._resizeCanvas(dimensions[0], dimensions[1], this.getPixelRatio());
2353
2380
 
2354
2381
  const controlContainer = this._controlContainer = DOM.create('div', 'maplibregl-control-container mapboxgl-control-container', container);
2355
2382
  const positions = this._controlPositions = {};
@@ -2360,9 +2387,7 @@ class Map extends Camera {
2360
2387
  this._container.addEventListener('scroll', this._onMapScroll, false);
2361
2388
  }
2362
2389
 
2363
- _resizeCanvas(width: number, height: number) {
2364
- const pixelRatio = devicePixelRatio || 1;
2365
-
2390
+ _resizeCanvas(width: number, height: number, pixelRatio: number) {
2366
2391
  // Request the required canvas size taking the pixelratio into account.
2367
2392
  this._canvas.width = pixelRatio * width;
2368
2393
  this._canvas.height = pixelRatio * height;