maplibre-gl 3.4.0 → 3.4.1
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.
- package/dist/maplibre-gl-csp-worker.js +1 -1
- package/dist/maplibre-gl-csp-worker.js.map +1 -1
- package/dist/maplibre-gl-csp.js +1 -1
- package/dist/maplibre-gl-csp.js.map +1 -1
- package/dist/maplibre-gl-dev.js +238 -58
- package/dist/maplibre-gl-dev.js.map +1 -1
- package/dist/maplibre-gl.d.ts +4 -3
- package/dist/maplibre-gl.js +4 -4
- package/dist/maplibre-gl.js.map +1 -1
- package/package.json +9 -9
- package/src/render/glyph_manager.test.ts +10 -9
- package/src/render/glyph_manager.ts +17 -10
- package/src/source/raster_dem_tile_source.ts +21 -9
- package/src/source/raster_dem_tile_worker_source.ts +7 -24
- package/src/style/style.ts +3 -0
- package/src/style/style_glyph.ts +4 -3
- package/src/symbol/quads.ts +4 -2
- package/src/ui/map.test.ts +17 -0
- package/src/util/offscreen_canvas_distorted.test.ts +13 -0
- package/src/util/offscreen_canvas_distorted.ts +39 -0
- package/src/util/util.test.ts +171 -1
- package/src/util/util.ts +150 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "maplibre-gl",
|
|
3
3
|
"description": "BSD licensed community fork of mapbox-gl, a WebGL interactive maps library",
|
|
4
|
-
"version": "3.4.
|
|
4
|
+
"version": "3.4.1",
|
|
5
5
|
"main": "dist/maplibre-gl.js",
|
|
6
6
|
"style": "dist/maplibre-gl.css",
|
|
7
7
|
"license": "BSD-3-Clause",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"@mapbox/mvt-fixtures": "^3.10.0",
|
|
45
45
|
"@rollup/plugin-commonjs": "^25.0.5",
|
|
46
46
|
"@rollup/plugin-json": "^6.0.1",
|
|
47
|
-
"@rollup/plugin-node-resolve": "^15.2.
|
|
47
|
+
"@rollup/plugin-node-resolve": "^15.2.3",
|
|
48
48
|
"@rollup/plugin-replace": "^5.0.3",
|
|
49
49
|
"@rollup/plugin-strip": "^3.0.3",
|
|
50
50
|
"@rollup/plugin-terser": "^0.4.4",
|
|
@@ -62,12 +62,12 @@
|
|
|
62
62
|
"@types/minimist": "^1.2.3",
|
|
63
63
|
"@types/murmurhash-js": "^1.0.4",
|
|
64
64
|
"@types/nise": "^1.4.2",
|
|
65
|
-
"@types/node": "^20.8.
|
|
65
|
+
"@types/node": "^20.8.3",
|
|
66
66
|
"@types/offscreencanvas": "^2019.7.1",
|
|
67
67
|
"@types/pixelmatch": "^5.2.4",
|
|
68
68
|
"@types/pngjs": "^6.0.2",
|
|
69
69
|
"@types/react": "^18.2.25",
|
|
70
|
-
"@types/react-dom": "^18.2.
|
|
70
|
+
"@types/react-dom": "^18.2.11",
|
|
71
71
|
"@types/request": "^2.48.9",
|
|
72
72
|
"@types/shuffle-seed": "^1.1.0",
|
|
73
73
|
"@types/window-or-global": "^1.0.4",
|
|
@@ -79,10 +79,10 @@
|
|
|
79
79
|
"cssnano": "^6.0.1",
|
|
80
80
|
"d3": "^7.8.5",
|
|
81
81
|
"d3-queue": "^3.0.7",
|
|
82
|
-
"devtools-protocol": "^0.0.
|
|
82
|
+
"devtools-protocol": "^0.0.1206220",
|
|
83
83
|
"diff": "^5.1.0",
|
|
84
84
|
"dts-bundle-generator": "^8.0.1",
|
|
85
|
-
"eslint": "^8.
|
|
85
|
+
"eslint": "^8.51.0",
|
|
86
86
|
"eslint-config-mourner": "^3.0.0",
|
|
87
87
|
"eslint-plugin-html": "^7.1.0",
|
|
88
88
|
"eslint-plugin-import": "^2.28.1",
|
|
@@ -111,10 +111,10 @@
|
|
|
111
111
|
"postcss-cli": "^10.1.0",
|
|
112
112
|
"postcss-inline-svg": "^6.0.0",
|
|
113
113
|
"pretty-bytes": "^6.1.1",
|
|
114
|
-
"puppeteer": "^21.3.
|
|
114
|
+
"puppeteer": "^21.3.8",
|
|
115
115
|
"react": "^18.2.0",
|
|
116
116
|
"react-dom": "^18.2.0",
|
|
117
|
-
"rollup": "^
|
|
117
|
+
"rollup": "^4.0.2",
|
|
118
118
|
"rollup-plugin-sourcemaps": "^0.6.3",
|
|
119
119
|
"rw": "^1.3.3",
|
|
120
120
|
"semver": "^7.5.4",
|
|
@@ -126,7 +126,7 @@
|
|
|
126
126
|
"ts-jest": "^29.1.1",
|
|
127
127
|
"ts-node": "^10.9.1",
|
|
128
128
|
"tslib": "^2.6.2",
|
|
129
|
-
"typedoc": "^0.25.
|
|
129
|
+
"typedoc": "^0.25.2",
|
|
130
130
|
"typedoc-plugin-markdown": "^3.16.0",
|
|
131
131
|
"typedoc-plugin-missing-exports": "^2.1.0",
|
|
132
132
|
"typescript": "^5.2.2"
|
|
@@ -95,13 +95,13 @@ describe('GlyphManager', () => {
|
|
|
95
95
|
manager.getGlyphs({'Arial Unicode MS': [0x3005]}, (err, glyphs) => {
|
|
96
96
|
expect(err).toBeFalsy();
|
|
97
97
|
expect(glyphs['Arial Unicode MS'][0x3005]).not.toBeNull();
|
|
98
|
-
//Request char from Katakana range (te)
|
|
98
|
+
//Request char from Katakana range (te テ)
|
|
99
99
|
manager.getGlyphs({'Arial Unicode MS': [0x30C6]}, (err, glyphs) => {
|
|
100
100
|
expect(err).toBeFalsy();
|
|
101
101
|
const glyph = glyphs['Arial Unicode MS'][0x30c6];
|
|
102
102
|
//Ensure that te is locally generated.
|
|
103
|
-
expect(glyph.bitmap.height).toBe(
|
|
104
|
-
expect(glyph.bitmap.width).toBe(
|
|
103
|
+
expect(glyph.bitmap.height).toBe(12);
|
|
104
|
+
expect(glyph.bitmap.width).toBe(12);
|
|
105
105
|
done();
|
|
106
106
|
});
|
|
107
107
|
});
|
|
@@ -110,9 +110,10 @@ describe('GlyphManager', () => {
|
|
|
110
110
|
test('GlyphManager generates CJK PBF locally', done => {
|
|
111
111
|
const manager = createGlyphManager('sans-serif');
|
|
112
112
|
|
|
113
|
+
// character 平
|
|
113
114
|
manager.getGlyphs({'Arial Unicode MS': [0x5e73]}, (err, glyphs) => {
|
|
114
115
|
expect(err).toBeFalsy();
|
|
115
|
-
expect(glyphs['Arial Unicode MS'][0x5e73].metrics.advance).toBe(
|
|
116
|
+
expect(glyphs['Arial Unicode MS'][0x5e73].metrics.advance).toBe(0.5);
|
|
116
117
|
done();
|
|
117
118
|
});
|
|
118
119
|
});
|
|
@@ -120,10 +121,10 @@ describe('GlyphManager', () => {
|
|
|
120
121
|
test('GlyphManager generates Katakana PBF locally', done => {
|
|
121
122
|
const manager = createGlyphManager('sans-serif');
|
|
122
123
|
|
|
123
|
-
// Katakana letter te
|
|
124
|
+
// Katakana letter te テ
|
|
124
125
|
manager.getGlyphs({'Arial Unicode MS': [0x30c6]}, (err, glyphs) => {
|
|
125
126
|
expect(err).toBeFalsy();
|
|
126
|
-
expect(glyphs['Arial Unicode MS'][0x30c6].metrics.advance).toBe(
|
|
127
|
+
expect(glyphs['Arial Unicode MS'][0x30c6].metrics.advance).toBe(0.5);
|
|
127
128
|
done();
|
|
128
129
|
});
|
|
129
130
|
});
|
|
@@ -131,10 +132,10 @@ describe('GlyphManager', () => {
|
|
|
131
132
|
test('GlyphManager generates Hiragana PBF locally', done => {
|
|
132
133
|
const manager = createGlyphManager('sans-serif');
|
|
133
134
|
|
|
134
|
-
//Hiragana letter te
|
|
135
|
+
//Hiragana letter te て
|
|
135
136
|
manager.getGlyphs({'Arial Unicode MS': [0x3066]}, (err, glyphs) => {
|
|
136
137
|
expect(err).toBeFalsy();
|
|
137
|
-
expect(glyphs['Arial Unicode MS'][0x3066].metrics.advance).toBe(
|
|
138
|
+
expect(glyphs['Arial Unicode MS'][0x3066].metrics.advance).toBe(0.5);
|
|
138
139
|
done();
|
|
139
140
|
});
|
|
140
141
|
});
|
|
@@ -143,7 +144,7 @@ describe('GlyphManager', () => {
|
|
|
143
144
|
|
|
144
145
|
const manager = createGlyphManager('sans-serif');
|
|
145
146
|
const drawSpy = GlyphManager.TinySDF.prototype.draw = jest.fn().mockImplementation(() => {
|
|
146
|
-
return {data: new Uint8ClampedArray(
|
|
147
|
+
return {data: new Uint8ClampedArray(60 * 60)} as any;
|
|
147
148
|
});
|
|
148
149
|
|
|
149
150
|
// Katakana letter te
|
|
@@ -180,6 +180,10 @@ export class GlyphManager {
|
|
|
180
180
|
return;
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
// Client-generated glyphs are rendered at 2x texture scale,
|
|
184
|
+
// because CJK glyphs are more detailed than others.
|
|
185
|
+
const textureScale = 2;
|
|
186
|
+
|
|
183
187
|
let tinySDF = entry.tinySDF;
|
|
184
188
|
if (!tinySDF) {
|
|
185
189
|
let fontWeight = '400';
|
|
@@ -191,9 +195,9 @@ export class GlyphManager {
|
|
|
191
195
|
fontWeight = '200';
|
|
192
196
|
}
|
|
193
197
|
tinySDF = entry.tinySDF = new GlyphManager.TinySDF({
|
|
194
|
-
fontSize: 24,
|
|
195
|
-
buffer: 3,
|
|
196
|
-
radius: 8,
|
|
198
|
+
fontSize: 24 * textureScale,
|
|
199
|
+
buffer: 3 * textureScale,
|
|
200
|
+
radius: 8 * textureScale,
|
|
197
201
|
cutoff: 0.25,
|
|
198
202
|
fontFamily,
|
|
199
203
|
fontWeight
|
|
@@ -215,17 +219,20 @@ export class GlyphManager {
|
|
|
215
219
|
* To approximately align TinySDF glyphs with server-provided glyphs, we use this baseline adjustment
|
|
216
220
|
* factor calibrated to be in between DIN Pro and Arial Unicode (but closer to Arial Unicode)
|
|
217
221
|
*/
|
|
218
|
-
const topAdjustment = 27;
|
|
222
|
+
const topAdjustment = 27.5;
|
|
223
|
+
|
|
224
|
+
const leftAdjustment = 0.5;
|
|
219
225
|
|
|
220
226
|
return {
|
|
221
227
|
id,
|
|
222
|
-
bitmap: new AlphaImage({width: char.width || 30, height: char.height || 30}, char.data),
|
|
228
|
+
bitmap: new AlphaImage({width: char.width || 30 * textureScale, height: char.height || 30 * textureScale}, char.data),
|
|
223
229
|
metrics: {
|
|
224
|
-
width: char.glyphWidth || 24,
|
|
225
|
-
height: char.glyphHeight || 24,
|
|
226
|
-
left: char.glyphLeft || 0,
|
|
227
|
-
top: char.glyphTop - topAdjustment || -8,
|
|
228
|
-
advance: char.glyphAdvance || 24
|
|
230
|
+
width: char.glyphWidth / textureScale || 24,
|
|
231
|
+
height: char.glyphHeight / textureScale || 24,
|
|
232
|
+
left: (char.glyphLeft / textureScale + leftAdjustment) || 0,
|
|
233
|
+
top: char.glyphTop / textureScale - topAdjustment || -8,
|
|
234
|
+
advance: char.glyphAdvance / textureScale || 24,
|
|
235
|
+
isDoubleResolution: true
|
|
229
236
|
}
|
|
230
237
|
};
|
|
231
238
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {ImageRequest} from '../util/image_request';
|
|
2
2
|
import {ResourceType} from '../util/request_manager';
|
|
3
|
-
import {extend, isImageBitmap} from '../util/util';
|
|
3
|
+
import {extend, isImageBitmap, readImageUsingVideoFrame} from '../util/util';
|
|
4
4
|
import {Evented} from '../util/evented';
|
|
5
5
|
import {browser} from '../util/browser';
|
|
6
6
|
import {offscreenCanvasSupported} from '../util/offscreen_canvas_supported';
|
|
@@ -16,6 +16,8 @@ import type {Tile} from './tile';
|
|
|
16
16
|
import type {Callback} from '../types/callback';
|
|
17
17
|
import type {RasterDEMSourceSpecification} from '@maplibre/maplibre-gl-style-spec';
|
|
18
18
|
import type {ExpiryData} from '../util/ajax';
|
|
19
|
+
import {isOffscreenCanvasDistorted} from '../util/offscreen_canvas_distorted';
|
|
20
|
+
import {RGBAImage} from '../util/image';
|
|
19
21
|
|
|
20
22
|
/**
|
|
21
23
|
* A source containing raster DEM tiles (See the [Style Specification](https://maplibre.org/maplibre-style-spec/) for detailed documentation of options.)
|
|
@@ -54,10 +56,9 @@ export class RasterDEMTileSource extends RasterTileSource implements Source {
|
|
|
54
56
|
|
|
55
57
|
loadTile(tile: Tile, callback: Callback<void>) {
|
|
56
58
|
const url = tile.tileID.canonical.url(this.tiles, this.map.getPixelRatio(), this.scheme);
|
|
57
|
-
|
|
58
|
-
|
|
59
|
+
const request = this.map._requestManager.transformRequest(url, ResourceType.Tile);
|
|
59
60
|
tile.neighboringTiles = this._getNeighboringTiles(tile.tileID);
|
|
60
|
-
|
|
61
|
+
tile.request = ImageRequest.getImage(request, async (err: Error, img: (HTMLImageElement | ImageBitmap), expiry: ExpiryData) => {
|
|
61
62
|
delete tile.request;
|
|
62
63
|
if (tile.aborted) {
|
|
63
64
|
tile.state = 'unloaded';
|
|
@@ -66,11 +67,9 @@ export class RasterDEMTileSource extends RasterTileSource implements Source {
|
|
|
66
67
|
tile.state = 'errored';
|
|
67
68
|
callback(err);
|
|
68
69
|
} else if (img) {
|
|
69
|
-
if (this.map._refreshExpiredTiles) tile.setExpiryData(
|
|
70
|
-
delete img.cacheControl;
|
|
71
|
-
delete img.expires;
|
|
70
|
+
if (this.map._refreshExpiredTiles) tile.setExpiryData(expiry);
|
|
72
71
|
const transfer = isImageBitmap(img) && offscreenCanvasSupported();
|
|
73
|
-
const rawImageData = transfer ? img :
|
|
72
|
+
const rawImageData = transfer ? img : await readImageNow(img);
|
|
74
73
|
const params = {
|
|
75
74
|
uid: tile.uid,
|
|
76
75
|
coord: tile.tileID,
|
|
@@ -85,9 +84,22 @@ export class RasterDEMTileSource extends RasterTileSource implements Source {
|
|
|
85
84
|
|
|
86
85
|
if (!tile.actor || tile.state === 'expired') {
|
|
87
86
|
tile.actor = this.dispatcher.getActor();
|
|
88
|
-
tile.actor.send('loadDEMTile', params, done
|
|
87
|
+
tile.actor.send('loadDEMTile', params, done);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}, this.map._refreshExpiredTiles);
|
|
91
|
+
|
|
92
|
+
async function readImageNow(img: ImageBitmap | HTMLImageElement): Promise<RGBAImage | ImageData> {
|
|
93
|
+
if (typeof VideoFrame !== 'undefined' && isOffscreenCanvasDistorted()) {
|
|
94
|
+
const width = img.width + 2;
|
|
95
|
+
const height = img.height + 2;
|
|
96
|
+
try {
|
|
97
|
+
return new RGBAImage({width, height}, await readImageUsingVideoFrame(img, -1, -1, width, height));
|
|
98
|
+
} catch (e) {
|
|
99
|
+
// fall-back to browser canvas decoding
|
|
89
100
|
}
|
|
90
101
|
}
|
|
102
|
+
return browser.getImageData(img, 1);
|
|
91
103
|
}
|
|
92
104
|
|
|
93
105
|
function done(err, data) {
|
|
@@ -6,46 +6,29 @@ import type {
|
|
|
6
6
|
WorkerDEMTileCallback,
|
|
7
7
|
TileParameters
|
|
8
8
|
} from './worker_source';
|
|
9
|
-
import {isImageBitmap} from '../util/util';
|
|
9
|
+
import {getImageData, isImageBitmap} from '../util/util';
|
|
10
10
|
|
|
11
11
|
export class RasterDEMTileWorkerSource {
|
|
12
12
|
actor: Actor;
|
|
13
13
|
loaded: {[_: string]: DEMData};
|
|
14
|
-
offscreenCanvas: OffscreenCanvas;
|
|
15
|
-
offscreenCanvasContext: OffscreenCanvasRenderingContext2D;
|
|
16
14
|
|
|
17
15
|
constructor() {
|
|
18
16
|
this.loaded = {};
|
|
19
17
|
}
|
|
20
18
|
|
|
21
|
-
loadTile(params: WorkerDEMTileParameters, callback: WorkerDEMTileCallback) {
|
|
19
|
+
async loadTile(params: WorkerDEMTileParameters, callback: WorkerDEMTileCallback) {
|
|
22
20
|
const {uid, encoding, rawImageData, redFactor, greenFactor, blueFactor, baseShift} = params;
|
|
23
|
-
|
|
24
|
-
const
|
|
21
|
+
const width = rawImageData.width + 2;
|
|
22
|
+
const height = rawImageData.height + 2;
|
|
23
|
+
const imagePixels: RGBAImage = isImageBitmap(rawImageData) ?
|
|
24
|
+
new RGBAImage({width, height}, await getImageData(rawImageData, -1, -1, width, height)) :
|
|
25
|
+
rawImageData;
|
|
25
26
|
const dem = new DEMData(uid, imagePixels, encoding, redFactor, greenFactor, blueFactor, baseShift);
|
|
26
27
|
this.loaded = this.loaded || {};
|
|
27
28
|
this.loaded[uid] = dem;
|
|
28
29
|
callback(null, dem);
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
getImageData(imgBitmap: ImageBitmap): RGBAImage {
|
|
32
|
-
// Lazily initialize OffscreenCanvas
|
|
33
|
-
if (!this.offscreenCanvas || !this.offscreenCanvasContext) {
|
|
34
|
-
// Dem tiles are typically 256x256
|
|
35
|
-
this.offscreenCanvas = new OffscreenCanvas(imgBitmap.width, imgBitmap.height);
|
|
36
|
-
this.offscreenCanvasContext = this.offscreenCanvas.getContext('2d', {willReadFrequently: true});
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
this.offscreenCanvas.width = imgBitmap.width;
|
|
40
|
-
this.offscreenCanvas.height = imgBitmap.height;
|
|
41
|
-
|
|
42
|
-
this.offscreenCanvasContext.drawImage(imgBitmap, 0, 0, imgBitmap.width, imgBitmap.height);
|
|
43
|
-
// Insert an additional 1px padding around the image to allow backfilling for neighboring data.
|
|
44
|
-
const imgData = this.offscreenCanvasContext.getImageData(-1, -1, imgBitmap.width + 2, imgBitmap.height + 2);
|
|
45
|
-
this.offscreenCanvasContext.clearRect(0, 0, this.offscreenCanvas.width, this.offscreenCanvas.height);
|
|
46
|
-
return new RGBAImage({width: imgData.width, height: imgData.height}, imgData.data);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
32
|
removeTile(params: TileParameters) {
|
|
50
33
|
const loaded = this.loaded,
|
|
51
34
|
uid = params.uid;
|
package/src/style/style.ts
CHANGED
package/src/style/style_glyph.ts
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import type {AlphaImage} from '../util/image';
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* The glyph's metrices
|
|
5
|
-
*/
|
|
6
3
|
export type GlyphMetrics = {
|
|
7
4
|
width: number;
|
|
8
5
|
height: number;
|
|
9
6
|
left: number;
|
|
10
7
|
top: number;
|
|
11
8
|
advance: number;
|
|
9
|
+
/**
|
|
10
|
+
* isDoubleResolution = true for 48px textures
|
|
11
|
+
*/
|
|
12
|
+
isDoubleResolution?: boolean;
|
|
12
13
|
};
|
|
13
14
|
|
|
14
15
|
/**
|
package/src/symbol/quads.ts
CHANGED
|
@@ -274,10 +274,12 @@ export function getGlyphQuads(
|
|
|
274
274
|
builtInOffset = [0, 0];
|
|
275
275
|
}
|
|
276
276
|
|
|
277
|
+
const textureScale = positionedGlyph.metrics.isDoubleResolution ? 2 : 1;
|
|
278
|
+
|
|
277
279
|
const x1 = (positionedGlyph.metrics.left - rectBuffer) * positionedGlyph.scale - halfAdvance + builtInOffset[0];
|
|
278
280
|
const y1 = (-positionedGlyph.metrics.top - rectBuffer) * positionedGlyph.scale + builtInOffset[1];
|
|
279
|
-
const x2 = x1 + textureRect.w * positionedGlyph.scale / pixelRatio;
|
|
280
|
-
const y2 = y1 + textureRect.h * positionedGlyph.scale / pixelRatio;
|
|
281
|
+
const x2 = x1 + textureRect.w / textureScale * positionedGlyph.scale / pixelRatio;
|
|
282
|
+
const y2 = y1 + textureRect.h / textureScale * positionedGlyph.scale / pixelRatio;
|
|
281
283
|
|
|
282
284
|
const tl = new Point(x1, y1);
|
|
283
285
|
const tr = new Point(x2, y1);
|
package/src/ui/map.test.ts
CHANGED
|
@@ -288,6 +288,23 @@ describe('Map', () => {
|
|
|
288
288
|
|
|
289
289
|
});
|
|
290
290
|
|
|
291
|
+
test('setStyle back to the first style should work', done => {
|
|
292
|
+
const redStyle = {version: 8 as const, sources: {}, layers: [
|
|
293
|
+
{id: 'background', type: 'background' as const, paint: {'background-color': 'red'}},
|
|
294
|
+
]};
|
|
295
|
+
const blueStyle = {version: 8 as const, sources: {}, layers: [
|
|
296
|
+
{id: 'background', type: 'background' as const, paint: {'background-color': 'blue'}},
|
|
297
|
+
]};
|
|
298
|
+
const map = createMap({style: redStyle});
|
|
299
|
+
map.setStyle(blueStyle);
|
|
300
|
+
map.once('style.load', () => {
|
|
301
|
+
map.setStyle(redStyle);
|
|
302
|
+
const serializedStyle = map.style.serialize();
|
|
303
|
+
expect(serializedStyle.layers[0].paint['background-color']).toBe('red');
|
|
304
|
+
done();
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
|
|
291
308
|
test('style transform overrides unmodified map transform', done => {
|
|
292
309
|
const map = new Map({container: window.document.createElement('div')} as any as MapOptions);
|
|
293
310
|
map.transform.lngRange = [-120, 140];
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import {isOffscreenCanvasDistorted} from './offscreen_canvas_distorted';
|
|
2
|
+
import {Canvas} from 'canvas';
|
|
3
|
+
import {offscreenCanvasSupported} from './offscreen_canvas_supported';
|
|
4
|
+
|
|
5
|
+
test('normal operation does not mangle canvas', () => {
|
|
6
|
+
const OffscreenCanvas = (window as any).OffscreenCanvas = jest.fn((width:number, height: number) => {
|
|
7
|
+
return new Canvas(width, height);
|
|
8
|
+
});
|
|
9
|
+
expect(offscreenCanvasSupported()).toBeTruthy();
|
|
10
|
+
OffscreenCanvas.mockClear();
|
|
11
|
+
expect(isOffscreenCanvasDistorted()).toBeFalsy();
|
|
12
|
+
expect(OffscreenCanvas).toHaveBeenCalledTimes(1);
|
|
13
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import {offscreenCanvasSupported} from './offscreen_canvas_supported';
|
|
2
|
+
|
|
3
|
+
let offscreenCanvasDistorted: boolean;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Some browsers don't return the exact pixels from a canvas to prevent user fingerprinting (see #3185).
|
|
7
|
+
* This function writes pixels to an OffscreenCanvas and reads them back using getImageData, returning false
|
|
8
|
+
* if they don't match.
|
|
9
|
+
*
|
|
10
|
+
* @returns true if the browser supports OffscreenCanvas but it distorts getImageData results, false otherwise.
|
|
11
|
+
*/
|
|
12
|
+
export function isOffscreenCanvasDistorted(): boolean {
|
|
13
|
+
if (offscreenCanvasDistorted == null) {
|
|
14
|
+
offscreenCanvasDistorted = false;
|
|
15
|
+
if (offscreenCanvasSupported()) {
|
|
16
|
+
const size = 5;
|
|
17
|
+
const canvas = new OffscreenCanvas(size, size);
|
|
18
|
+
const context = canvas.getContext('2d', {willReadFrequently: true});
|
|
19
|
+
if (context) {
|
|
20
|
+
// fill each pixel with an RGB value that should make the byte at index i equal to i (except alpha channel):
|
|
21
|
+
// [0, 1, 2, 255, 4, 5, 6, 255, 8, 9, 10, 255, ...]
|
|
22
|
+
for (let i = 0; i < size * size; i++) {
|
|
23
|
+
const base = i * 4;
|
|
24
|
+
context.fillStyle = `rgb(${base},${base + 1},${base + 2})`;
|
|
25
|
+
context.fillRect(i % size, Math.floor(i / size), 1, 1);
|
|
26
|
+
}
|
|
27
|
+
const data = context.getImageData(0, 0, size, size).data;
|
|
28
|
+
for (let i = 0; i < size * size * 4; i++) {
|
|
29
|
+
if (i % 4 !== 3 && data[i] !== i) {
|
|
30
|
+
offscreenCanvasDistorted = true;
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return offscreenCanvasDistorted || false;
|
|
39
|
+
}
|
package/src/util/util.test.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import Point from '@mapbox/point-geometry';
|
|
2
|
-
import {arraysIntersect, asyncAll, bezier, clamp, clone, deepEqual, easeCubicInOut, extend, filterObject, findLineIntersection, isClosedPolygon, isCounterClockwise, isPowerOfTwo, keysDifference, mapObject, nextPowerOfTwo, parseCacheControl, pick, uniqueId, wrap} from './util';
|
|
2
|
+
import {arraysIntersect, asyncAll, bezier, clamp, clone, deepEqual, easeCubicInOut, extend, filterObject, findLineIntersection, isClosedPolygon, isCounterClockwise, isPowerOfTwo, keysDifference, mapObject, nextPowerOfTwo, parseCacheControl, pick, readImageDataUsingOffscreenCanvas, readImageUsingVideoFrame, uniqueId, wrap} from './util';
|
|
3
|
+
import {Canvas} from 'canvas';
|
|
3
4
|
|
|
4
5
|
describe('util', () => {
|
|
5
6
|
expect(easeCubicInOut(0)).toBe(0);
|
|
@@ -346,3 +347,172 @@ describe('util findLineIntersection', () => {
|
|
|
346
347
|
expect(intersection).toBeNull();
|
|
347
348
|
});
|
|
348
349
|
});
|
|
350
|
+
|
|
351
|
+
describe('util readImageUsingVideoFrame', () => {
|
|
352
|
+
let format = 'RGBA';
|
|
353
|
+
const frame = {
|
|
354
|
+
get format() {
|
|
355
|
+
return format;
|
|
356
|
+
},
|
|
357
|
+
copyTo: jest.fn(buf => {
|
|
358
|
+
buf.set(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]).subarray(0, buf.length));
|
|
359
|
+
return Promise.resolve();
|
|
360
|
+
}),
|
|
361
|
+
close: jest.fn(),
|
|
362
|
+
};
|
|
363
|
+
(window as any).VideoFrame = jest.fn(() => frame);
|
|
364
|
+
const canvas = document.createElement('canvas');
|
|
365
|
+
canvas.width = canvas.height = 2;
|
|
366
|
+
|
|
367
|
+
beforeEach(() => {
|
|
368
|
+
format = 'RGBA';
|
|
369
|
+
frame.copyTo.mockClear();
|
|
370
|
+
frame.close.mockReset();
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
test('copy RGB', async () => {
|
|
374
|
+
format = 'RGBA';
|
|
375
|
+
const result = await readImageUsingVideoFrame(canvas, 0, 0, 2, 2);
|
|
376
|
+
expect(result).toHaveLength(4 * 4);
|
|
377
|
+
expect(frame.copyTo).toHaveBeenCalledWith(expect.anything(), {
|
|
378
|
+
layout: [{offset: 0, stride: 8}],
|
|
379
|
+
rect: {x: 0, y: 0, width: 2, height: 2}
|
|
380
|
+
});
|
|
381
|
+
expect(result).toEqual(new Uint8ClampedArray([
|
|
382
|
+
1, 2, 3, 4, 5, 6, 7, 8,
|
|
383
|
+
9, 10, 11, 12, 13, 14, 15, 16
|
|
384
|
+
]));
|
|
385
|
+
expect(frame.close).toHaveBeenCalledTimes(1);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
test('flip BRG', async () => {
|
|
389
|
+
format = 'BGRX';
|
|
390
|
+
const result = await readImageUsingVideoFrame(canvas, 0, 0, 2, 2);
|
|
391
|
+
expect(result).toEqual(new Uint8ClampedArray([
|
|
392
|
+
3, 2, 1, 4, 7, 6, 5, 8,
|
|
393
|
+
11, 10, 9, 12, 15, 14, 13, 16
|
|
394
|
+
]));
|
|
395
|
+
expect(frame.close).toHaveBeenCalledTimes(1);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
test('ignore bad format', async () => {
|
|
399
|
+
format = 'OTHER';
|
|
400
|
+
await expect(readImageUsingVideoFrame(canvas, 0, 0, 2, 2)).rejects.toThrow();
|
|
401
|
+
expect(frame.close).toHaveBeenCalledTimes(1);
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
describe('layout/rect', () => {
|
|
405
|
+
beforeEach(() => {
|
|
406
|
+
(window as any).VideoFrame = jest.fn(() => frame);
|
|
407
|
+
canvas.width = canvas.height = 3;
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
test('full rectangle', async () => {
|
|
411
|
+
await readImageUsingVideoFrame(canvas, 0, 0, 3, 3);
|
|
412
|
+
expect(frame.copyTo).toHaveBeenCalledWith(expect.anything(), {
|
|
413
|
+
layout: [{offset: 0, stride: 12}],
|
|
414
|
+
rect: {x: 0, y: 0, width: 3, height: 3}
|
|
415
|
+
});
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
test('top left', async () => {
|
|
419
|
+
await readImageUsingVideoFrame(canvas, 0, 0, 2, 2);
|
|
420
|
+
expect(frame.copyTo).toHaveBeenCalledWith(expect.anything(), {
|
|
421
|
+
layout: [{offset: 0, stride: 8}],
|
|
422
|
+
rect: {x: 0, y: 0, width: 2, height: 2}
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
test('top right', async () => {
|
|
427
|
+
await readImageUsingVideoFrame(canvas, 1, 0, 2, 2);
|
|
428
|
+
expect(frame.copyTo).toHaveBeenCalledWith(expect.anything(), {
|
|
429
|
+
layout: [{offset: 0, stride: 8}],
|
|
430
|
+
rect: {x: 1, y: 0, width: 2, height: 2}
|
|
431
|
+
});
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
test('bottom left', async () => {
|
|
435
|
+
await readImageUsingVideoFrame(canvas, 0, 1, 2, 2);
|
|
436
|
+
expect(frame.copyTo).toHaveBeenCalledWith(expect.anything(), {
|
|
437
|
+
layout: [{offset: 0, stride: 8}],
|
|
438
|
+
rect: {x: 0, y: 1, width: 2, height: 2}
|
|
439
|
+
});
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
test('bottom right', async () => {
|
|
443
|
+
await readImageUsingVideoFrame(canvas, 1, 1, 2, 2);
|
|
444
|
+
expect(frame.copyTo).toHaveBeenCalledWith(expect.anything(), {
|
|
445
|
+
layout: [{offset: 0, stride: 8}],
|
|
446
|
+
rect: {x: 1, y: 1, width: 2, height: 2}
|
|
447
|
+
});
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
test('middle', async () => {
|
|
451
|
+
await readImageUsingVideoFrame(canvas, 1, 1, 1, 1);
|
|
452
|
+
expect(frame.copyTo).toHaveBeenCalledWith(expect.anything(), {
|
|
453
|
+
layout: [{offset: 0, stride: 4}],
|
|
454
|
+
rect: {x: 1, y: 1, width: 1, height: 1}
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
test('extend past on all sides', async () => {
|
|
459
|
+
await readImageUsingVideoFrame(canvas, -1, -1, 5, 5);
|
|
460
|
+
expect(frame.copyTo).toHaveBeenCalledWith(expect.anything(), {
|
|
461
|
+
layout: [{offset: 4 * 5 + 4, stride: 4 * 5}],
|
|
462
|
+
rect: {x: 0, y: 0, width: 3, height: 3}
|
|
463
|
+
});
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
test('overhang top left', async () => {
|
|
467
|
+
await readImageUsingVideoFrame(canvas, -1, -1, 2, 2);
|
|
468
|
+
expect(frame.copyTo).toHaveBeenCalledWith(expect.anything(), {
|
|
469
|
+
layout: [{offset: 4 * 2 + 4, stride: 4 * 2}],
|
|
470
|
+
rect: {x: 0, y: 0, width: 1, height: 1}
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
test('overhang top right', async () => {
|
|
475
|
+
await readImageUsingVideoFrame(canvas, 2, -1, 2, 2);
|
|
476
|
+
expect(frame.copyTo).toHaveBeenCalledWith(expect.anything(), {
|
|
477
|
+
layout: [{offset: 4 * 2, stride: 4 * 2}],
|
|
478
|
+
rect: {x: 2, y: 0, width: 1, height: 1}
|
|
479
|
+
});
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
test('overhang bottom left', async () => {
|
|
483
|
+
await readImageUsingVideoFrame(canvas, -1, 2, 2, 2);
|
|
484
|
+
expect(frame.copyTo).toHaveBeenCalledWith(expect.anything(), {
|
|
485
|
+
layout: [{offset: 4, stride: 4 * 2}],
|
|
486
|
+
rect: {x: 0, y: 2, width: 1, height: 1}
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
test('overhang bottom right', async () => {
|
|
491
|
+
await readImageUsingVideoFrame(canvas, 2, 2, 2, 2);
|
|
492
|
+
expect(frame.copyTo).toHaveBeenCalledWith(expect.anything(), {
|
|
493
|
+
layout: [{offset: 0, stride: 4 * 2}],
|
|
494
|
+
rect: {x: 2, y: 2, width: 1, height: 1}
|
|
495
|
+
});
|
|
496
|
+
});
|
|
497
|
+
});
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
describe('util readImageDataUsingOffscreenCanvas', () => {
|
|
501
|
+
test('reads pixels from image', async () => {
|
|
502
|
+
(window as any).OffscreenCanvas = Canvas;
|
|
503
|
+
const image = new Canvas(2, 2);
|
|
504
|
+
const context = image.getContext('2d');
|
|
505
|
+
context.fillStyle = 'rgb(10,0,0)';
|
|
506
|
+
context.fillRect(0, 0, 1, 1);
|
|
507
|
+
context.fillStyle = 'rgb(0,20,0)';
|
|
508
|
+
context.fillRect(1, 0, 1, 1);
|
|
509
|
+
context.fillStyle = 'rgb(0,0,30)';
|
|
510
|
+
context.fillRect(0, 1, 1, 1);
|
|
511
|
+
context.fillStyle = 'rgb(40,40,40)';
|
|
512
|
+
context.fillRect(1, 1, 1, 1);
|
|
513
|
+
expect([...await readImageDataUsingOffscreenCanvas(image as any, 0, 0, 2, 2)]).toEqual([
|
|
514
|
+
10, 0, 0, 255, 0, 20, 0, 255,
|
|
515
|
+
0, 0, 30, 255, 40, 40, 40, 255,
|
|
516
|
+
]);
|
|
517
|
+
});
|
|
518
|
+
});
|