From 33942bbe068b121520578e03f0ca745eaa62b925 Mon Sep 17 00:00:00 2001 From: Arindam Bose Date: Tue, 8 Oct 2019 17:17:31 -0700 Subject: [PATCH 01/14] initial prototype --- src/source/raster_dem_tile_source.js | 3 +- src/source/raster_dem_tile_worker_source.js | 13 +++++- src/util/ajax.js | 44 +++++++++++++++------ src/util/browser.js | 4 ++ src/util/web_worker_transfer.js | 3 +- 5 files changed, 52 insertions(+), 15 deletions(-) diff --git a/src/source/raster_dem_tile_source.js b/src/source/raster_dem_tile_source.js index cfd735330e2..02c8f881b30 100644 --- a/src/source/raster_dem_tile_source.js +++ b/src/source/raster_dem_tile_source.js @@ -54,7 +54,8 @@ class RasterDEMTileSource extends RasterTileSource implements Source { if (this.map._refreshExpiredTiles) tile.setExpiryData(img); delete (img: any).cacheControl; delete (img: any).expires; - const rawImageData = browser.getImageData(img, 1); + const transfer = window.ImageBitmap && img instanceof window.ImageBitmap && browser.supportsOffscreenCanvas(); + const rawImageData = transfer ? img : browser.getImageData(img, 1); const params = { uid: tile.uid, coord: tile.tileID, diff --git a/src/source/raster_dem_tile_worker_source.js b/src/source/raster_dem_tile_worker_source.js index 41e10139b78..63d37c1d9a6 100644 --- a/src/source/raster_dem_tile_worker_source.js +++ b/src/source/raster_dem_tile_worker_source.js @@ -12,14 +12,25 @@ import type { class RasterDEMTileWorkerSource { actor: Actor; loaded: {[string]: DEMData}; + offcreenCanvas: ?OffscreenCanvas; constructor() { this.loaded = {}; + this.offcreenCanvas = new OffscreenCanvas(512, 512); + this.offcreenCanvasContext = this.offcreenCanvas.getContext('2d'); } loadTile(params: WorkerDEMTileParameters, callback: WorkerDEMTileCallback) { const {uid, encoding, rawImageData} = params; - const dem = new DEMData(uid, rawImageData, encoding); + let imagePixels = rawImageData; + if (true) { + this.offcreenCanvas.width = rawImageData.width; + this.offcreenCanvas.height = rawImageData.height; + this.offcreenCanvasContext.drawImage(rawImageData, 0, 0, rawImageData.width, rawImageData.height); + imagePixels = this.offcreenCanvasContext.getImageData(-1, -1, rawImageData.width + 2, rawImageData.height + 2); + } + + const dem = new DEMData(uid, imagePixels, encoding); this.loaded = this.loaded || {}; this.loaded[uid] = dem; diff --git a/src/util/ajax.js b/src/util/ajax.js index 72e6439837f..30a3334e908 100644 --- a/src/util/ajax.js +++ b/src/util/ajax.js @@ -28,6 +28,8 @@ const ResourceType = { }; export {ResourceType}; +const supportsImageBitmap = typeof window.createImageBitmap == 'function'; + if (typeof Object.freeze == 'function') { Object.freeze(ResourceType); } @@ -258,6 +260,30 @@ function sameOrigin(url) { const transparentPngUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVQYV2NgAAIAAAUAAarVyFEAAAAASUVORK5CYII='; +function arrayBufferToImage(data: ArrayBuffer, callback: (err: ?Error, image: ?HTMLImageElement) => void, cacheControl: ?string, expires: ?string) { + const img: HTMLImageElement = new window.Image(); + const URL = window.URL || window.webkitURL; + img.onload = () => { + callback(null, img); + URL.revokeObjectURL(img.src); + }; + img.onerror = () => callback(new Error('Could not load image. Please make sure to use a supported image type such as PNG or JPEG. Note that SVGs are not supported.')); + const blob: Blob = new window.Blob([new Uint8Array(data)], {type: 'image/png'}); + (img: any).cacheControl = cacheControl; + (img: any).expires = expires; + img.src = data.byteLength ? URL.createObjectURL(blob) : transparentPngUrl; +} + +function arrayBufferToImageBitmap(data: ArrayBuffer, callback: (err: ?Error, image: ?ImageBitmap) => void, cacheControl: ?string, expires: ?string) { + const blob: Blob = new window.Blob([new Uint8Array(data)], {type: 'image/png'}); + window.createImageBitmap(blob).then((imgBitmap) => { + callback(null, imgBitmap); + }).catch((e) => { + console.log(e); + callback(new Error('Could not load image. Please make sure to use a supported image type such as PNG or JPEG. Note that SVGs are not supported.')); + }); +} + let imageQueue, numImageRequests; export const resetImageRequestQueue = () => { imageQueue = []; @@ -265,7 +291,7 @@ export const resetImageRequestQueue = () => { }; resetImageRequestQueue(); -export const getImage = function(requestParameters: RequestParameters, callback: Callback): Cancelable { +export const getImage = function(requestParameters: RequestParameters, callback: Callback): Cancelable { // limit concurrent image loads to help with raster sources performance on big screens if (numImageRequests >= config.MAX_PARALLEL_IMAGE_REQUESTS) { const queued = { @@ -303,17 +329,11 @@ export const getImage = function(requestParameters: RequestParameters, callback: if (err) { callback(err); } else if (data) { - const img: HTMLImageElement = new window.Image(); - const URL = window.URL || window.webkitURL; - img.onload = () => { - callback(null, img); - URL.revokeObjectURL(img.src); - }; - img.onerror = () => callback(new Error('Could not load image. Please make sure to use a supported image type such as PNG or JPEG. Note that SVGs are not supported.')); - const blob: Blob = new window.Blob([new Uint8Array(data)], {type: 'image/png'}); - (img: any).cacheControl = cacheControl; - (img: any).expires = expires; - img.src = data.byteLength ? URL.createObjectURL(blob) : transparentPngUrl; + if (supportsImageBitmap) { + arrayBufferToImageBitmap(data, callback, cacheControl, expires); + } else { + arrayBufferToImage(data, callback, cacheControl, expires); + } } }); diff --git a/src/util/browser.js b/src/util/browser.js index 8ae14d1906e..70928b2ec5d 100755 --- a/src/util/browser.js +++ b/src/util/browser.js @@ -56,6 +56,10 @@ const exported = { hardwareConcurrency: window.navigator.hardwareConcurrency || 4, + supportsOffscreenCanvas(): boolean { + return !!window.OffscreenCanvas; + }, + get devicePixelRatio() { return window.devicePixelRatio; }, get prefersReducedMotion(): boolean { if (!window.matchMedia) return false; diff --git a/src/util/web_worker_transfer.js b/src/util/web_worker_transfer.js index 84a41469bfc..795484be137 100644 --- a/src/util/web_worker_transfer.js +++ b/src/util/web_worker_transfer.js @@ -128,7 +128,7 @@ export function serialize(input: mixed, transferables?: Array): Se return input; } - if (input instanceof ArrayBuffer) { + if (input instanceof ArrayBuffer || window.ImageBitmap && input instanceof window.ImageBitmap) { if (transferables) { transferables.push(input); } @@ -219,6 +219,7 @@ export function deserialize(input: Serialized): mixed { input instanceof Date || input instanceof RegExp || input instanceof ArrayBuffer || + input instanceof ImageBitmap || ArrayBuffer.isView(input) || input instanceof ImageData) { return input; From 58756d74d23f8ba9d6d9c76be42f1d8b6fcdca2b Mon Sep 17 00:00:00 2001 From: Arindam Bose Date: Tue, 8 Oct 2019 17:26:39 -0700 Subject: [PATCH 02/14] add instacneof ImageBitmap check back --- src/source/raster_dem_tile_worker_source.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/source/raster_dem_tile_worker_source.js b/src/source/raster_dem_tile_worker_source.js index 63d37c1d9a6..06504b2dbfd 100644 --- a/src/source/raster_dem_tile_worker_source.js +++ b/src/source/raster_dem_tile_worker_source.js @@ -23,7 +23,7 @@ class RasterDEMTileWorkerSource { loadTile(params: WorkerDEMTileParameters, callback: WorkerDEMTileCallback) { const {uid, encoding, rawImageData} = params; let imagePixels = rawImageData; - if (true) { + if (rawImageData instanceof ImageBitmap) { this.offcreenCanvas.width = rawImageData.width; this.offcreenCanvas.height = rawImageData.height; this.offcreenCanvasContext.drawImage(rawImageData, 0, 0, rawImageData.width, rawImageData.height); From 0509d5c72ab8cd6f4826125230431eee428d90bc Mon Sep 17 00:00:00 2001 From: Arindam Bose Date: Wed, 27 Nov 2019 16:43:23 -0800 Subject: [PATCH 03/14] Fix unresolved conflict --- src/util/web_worker_transfer.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/util/web_worker_transfer.js b/src/util/web_worker_transfer.js index 528bad9c32d..496f12ba61e 100644 --- a/src/util/web_worker_transfer.js +++ b/src/util/web_worker_transfer.js @@ -223,12 +223,8 @@ export function deserialize(input: Serialized): mixed { input instanceof String || input instanceof Date || input instanceof RegExp || -<<<<<<< HEAD - input instanceof ArrayBuffer || - input instanceof ImageBitmap || -======= isArrayBuffer(input) || ->>>>>>> 8e8a28025dca26e258ad6a26c064d07596f11527 + input instanceof ImageBitmap || ArrayBuffer.isView(input) || input instanceof ImageData) { return input; From 1fdf49ed07dd73179857e1d94fe8c3dacebac0f2 Mon Sep 17 00:00:00 2001 From: Arindam Bose Date: Wed, 27 Nov 2019 17:47:24 -0800 Subject: [PATCH 04/14] Cleanup offscreencanvas feature detection --- src/source/raster_dem_tile_worker_source.js | 24 +++++++++++++++------ src/util/browser.js | 9 +++++++- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/source/raster_dem_tile_worker_source.js b/src/source/raster_dem_tile_worker_source.js index 06504b2dbfd..91239caee00 100644 --- a/src/source/raster_dem_tile_worker_source.js +++ b/src/source/raster_dem_tile_worker_source.js @@ -13,21 +13,18 @@ class RasterDEMTileWorkerSource { actor: Actor; loaded: {[string]: DEMData}; offcreenCanvas: ?OffscreenCanvas; + offcreenCanvasContext: ?CanvasRenderingContext2D; + constructor() { this.loaded = {}; - this.offcreenCanvas = new OffscreenCanvas(512, 512); - this.offcreenCanvasContext = this.offcreenCanvas.getContext('2d'); } loadTile(params: WorkerDEMTileParameters, callback: WorkerDEMTileCallback) { const {uid, encoding, rawImageData} = params; let imagePixels = rawImageData; if (rawImageData instanceof ImageBitmap) { - this.offcreenCanvas.width = rawImageData.width; - this.offcreenCanvas.height = rawImageData.height; - this.offcreenCanvasContext.drawImage(rawImageData, 0, 0, rawImageData.width, rawImageData.height); - imagePixels = this.offcreenCanvasContext.getImageData(-1, -1, rawImageData.width + 2, rawImageData.height + 2); + imagePixels = this.getImageData(rawImageData); } const dem = new DEMData(uid, imagePixels, encoding); @@ -37,6 +34,21 @@ class RasterDEMTileWorkerSource { callback(null, dem); } + + getImageData(rawImageData: ImageBitmap): ImageData{ + if(!this.offcreenCanvas || !this.offcreenCanvasContext){ + this.offcreenCanvas = new OffscreenCanvas(512, 512); + this.offcreenCanvasContext = this.offcreenCanvas.getContext('2d'); + } + + this.offcreenCanvas.width = rawImageData.width; + this.offcreenCanvas.height = rawImageData.height; + this.offcreenCanvasContext.drawImage(rawImageData, 0, 0, rawImageData.width, rawImageData.height); + return this.offcreenCanvasContext.getImageData(-1, -1, rawImageData.width + 2, rawImageData.height + 2); + } + + + removeTile(params: TileParameters) { const loaded = this.loaded, uid = params.uid; diff --git a/src/util/browser.js b/src/util/browser.js index 70928b2ec5d..41a01bad33b 100755 --- a/src/util/browser.js +++ b/src/util/browser.js @@ -21,6 +21,8 @@ let linkEl; let reducedMotionQuery: MediaQueryList; +let supportsOffscreenCanvas = null; + /** * @private */ @@ -57,7 +59,12 @@ const exported = { hardwareConcurrency: window.navigator.hardwareConcurrency || 4, supportsOffscreenCanvas(): boolean { - return !!window.OffscreenCanvas; + if(supportsOffscreenCanvas == null){ + supportsOffscreenCanvas = window.OffscreenCanvas && + new window.OffscreenCanvas(1,1).getContext('2d') && + typeof window.createImageBitmap === 'function'; + } + return supportsOffscreenCanvas; }, get devicePixelRatio() { return window.devicePixelRatio; }, From 5ad0c74a51edd49df356ffed073c1c0ec9b0b494 Mon Sep 17 00:00:00 2001 From: Arindam Bose Date: Mon, 2 Dec 2019 14:26:17 -0800 Subject: [PATCH 05/14] Cleanup some checks and fix flow and lint errors --- flow-typed/offscreen-canvas.js | 9 +++++++ src/source/image_source.js | 2 +- src/source/raster_dem_tile_source.js | 3 ++- src/source/raster_dem_tile_worker_source.js | 30 +++++++++------------ src/source/worker_source.js | 2 +- src/util/ajax.js | 5 ++-- src/util/browser.js | 11 -------- src/util/offscreen_canvas_supported.js | 14 ++++++++++ src/util/web_worker_transfer.js | 9 +++++-- 9 files changed, 49 insertions(+), 36 deletions(-) create mode 100644 flow-typed/offscreen-canvas.js create mode 100644 src/util/offscreen_canvas_supported.js diff --git a/flow-typed/offscreen-canvas.js b/flow-typed/offscreen-canvas.js new file mode 100644 index 00000000000..7b165f06a52 --- /dev/null +++ b/flow-typed/offscreen-canvas.js @@ -0,0 +1,9 @@ +// @flow strict + +declare class OffscreenCanvas { + width: number; + height: number; + + constructor(width: number, height: number): OffscreenCanvas; + getContext(contextType: '2d' ): CanvasRenderingContext2D; +} \ No newline at end of file diff --git a/src/source/image_source.js b/src/source/image_source.js index 40d1dbc11e9..141b4dbf41c 100644 --- a/src/source/image_source.js +++ b/src/source/image_source.js @@ -78,7 +78,7 @@ class ImageSource extends Evented implements Source { dispatcher: Dispatcher; map: Map; texture: Texture | null; - image: HTMLImageElement; + image: HTMLImageElement | ImageBitmap; tileID: CanonicalTileID; _boundsArray: RasterBoundsArray; boundsBuffer: VertexBuffer; diff --git a/src/source/raster_dem_tile_source.js b/src/source/raster_dem_tile_source.js index c9506e39401..507b2992bdd 100644 --- a/src/source/raster_dem_tile_source.js +++ b/src/source/raster_dem_tile_source.js @@ -4,6 +4,7 @@ import {getImage, ResourceType} from '../util/ajax'; import {extend} from '../util/util'; import {Evented} from '../util/evented'; import browser from '../util/browser'; +import offscreenCanvasSupported from '../util/offscreen_canvas_supported'; import {OverscaledTileID} from './tile_id'; import RasterTileSource from './raster_tile_source'; // ensure DEMData is registered for worker transfer on main thread: @@ -54,7 +55,7 @@ class RasterDEMTileSource extends RasterTileSource implements Source { if (this.map._refreshExpiredTiles) tile.setExpiryData(img); delete (img: any).cacheControl; delete (img: any).expires; - const transfer = window.ImageBitmap && img instanceof window.ImageBitmap && browser.supportsOffscreenCanvas(); + const transfer = window.ImageBitmap && img instanceof window.ImageBitmap && offscreenCanvasSupported(); const rawImageData = transfer ? img : browser.getImageData(img, 1); const params = { uid: tile.uid, diff --git a/src/source/raster_dem_tile_worker_source.js b/src/source/raster_dem_tile_worker_source.js index 91239caee00..2947137d755 100644 --- a/src/source/raster_dem_tile_worker_source.js +++ b/src/source/raster_dem_tile_worker_source.js @@ -1,6 +1,7 @@ // @flow import DEMData from '../data/dem_data'; +import {RGBAImage} from '../util/image'; import type Actor from '../util/actor'; import type { @@ -12,9 +13,8 @@ import type { class RasterDEMTileWorkerSource { actor: Actor; loaded: {[string]: DEMData}; - offcreenCanvas: ?OffscreenCanvas; - offcreenCanvasContext: ?CanvasRenderingContext2D; - + offcreenCanvas: OffscreenCanvas; + offcreenCanvasContext: CanvasRenderingContext2D; constructor() { this.loaded = {}; @@ -22,33 +22,29 @@ class RasterDEMTileWorkerSource { loadTile(params: WorkerDEMTileParameters, callback: WorkerDEMTileCallback) { const {uid, encoding, rawImageData} = params; - let imagePixels = rawImageData; - if (rawImageData instanceof ImageBitmap) { - imagePixels = this.getImageData(rawImageData); - } + const imagePixels = rawImageData instanceof ImageBitmap ? this.getImageData(rawImageData) : rawImageData; const dem = new DEMData(uid, imagePixels, encoding); - this.loaded = this.loaded || {}; this.loaded[uid] = dem; callback(null, dem); } - - getImageData(rawImageData: ImageBitmap): ImageData{ - if(!this.offcreenCanvas || !this.offcreenCanvasContext){ + getImageData(imgBitmap: ImageBitmap): RGBAImage { + // Lazily initialize OffscreenCanvas + if (!this.offcreenCanvas || !this.offcreenCanvasContext) { this.offcreenCanvas = new OffscreenCanvas(512, 512); this.offcreenCanvasContext = this.offcreenCanvas.getContext('2d'); } - this.offcreenCanvas.width = rawImageData.width; - this.offcreenCanvas.height = rawImageData.height; - this.offcreenCanvasContext.drawImage(rawImageData, 0, 0, rawImageData.width, rawImageData.height); - return this.offcreenCanvasContext.getImageData(-1, -1, rawImageData.width + 2, rawImageData.height + 2); + this.offcreenCanvas.width = imgBitmap.width; + this.offcreenCanvas.height = imgBitmap.height; + this.offcreenCanvasContext.drawImage(imgBitmap, 0, 0, imgBitmap.width, imgBitmap.height); + // Insert an additional 1px padding around the image to allow backfilling for neighboring data. + const imgData = this.offcreenCanvasContext.getImageData(-1, -1, imgBitmap.width + 2, imgBitmap.height + 2); + return new RGBAImage({width: imgData.width, height: imgData.height}, imgData.data); } - - removeTile(params: TileParameters) { const loaded = this.loaded, uid = params.uid; diff --git a/src/source/worker_source.js b/src/source/worker_source.js index c0b26deee20..5e5f1097bde 100644 --- a/src/source/worker_source.js +++ b/src/source/worker_source.js @@ -31,7 +31,7 @@ export type WorkerTileParameters = TileParameters & { export type WorkerDEMTileParameters = TileParameters & { coord: { z: number, x: number, y: number, w: number }, - rawImageData: RGBAImage, + rawImageData: RGBAImage | ImageBitmap, encoding: "mapbox" | "terrarium" }; diff --git a/src/util/ajax.js b/src/util/ajax.js index dd10271fc72..39696f16a66 100644 --- a/src/util/ajax.js +++ b/src/util/ajax.js @@ -7,6 +7,7 @@ import config from './config'; import assert from 'assert'; import {cacheGet, cachePut} from './tile_request_cache'; import webpSupported from './webp_supported'; +import offscreenCanvasSupported from './offscreen_canvas_supported'; import type {Callback} from '../types/callback'; import type {Cancelable} from '../types/cancelable'; @@ -29,8 +30,6 @@ const ResourceType = { }; export {ResourceType}; -const supportsImageBitmap = typeof window.createImageBitmap == 'function'; - if (typeof Object.freeze == 'function') { Object.freeze(ResourceType); } @@ -335,7 +334,7 @@ export const getImage = function(requestParameters: RequestParameters, callback: if (err) { callback(err); } else if (data) { - if (supportsImageBitmap) { + if (offscreenCanvasSupported()) { arrayBufferToImageBitmap(data, callback, cacheControl, expires); } else { arrayBufferToImage(data, callback, cacheControl, expires); diff --git a/src/util/browser.js b/src/util/browser.js index 41a01bad33b..8ae14d1906e 100755 --- a/src/util/browser.js +++ b/src/util/browser.js @@ -21,8 +21,6 @@ let linkEl; let reducedMotionQuery: MediaQueryList; -let supportsOffscreenCanvas = null; - /** * @private */ @@ -58,15 +56,6 @@ const exported = { hardwareConcurrency: window.navigator.hardwareConcurrency || 4, - supportsOffscreenCanvas(): boolean { - if(supportsOffscreenCanvas == null){ - supportsOffscreenCanvas = window.OffscreenCanvas && - new window.OffscreenCanvas(1,1).getContext('2d') && - typeof window.createImageBitmap === 'function'; - } - return supportsOffscreenCanvas; - }, - get devicePixelRatio() { return window.devicePixelRatio; }, get prefersReducedMotion(): boolean { if (!window.matchMedia) return false; diff --git a/src/util/offscreen_canvas_supported.js b/src/util/offscreen_canvas_supported.js new file mode 100644 index 00000000000..b4ea46d89a7 --- /dev/null +++ b/src/util/offscreen_canvas_supported.js @@ -0,0 +1,14 @@ +// @flow +import window from './window'; + +let supportsOffscreenCanvas = null; + +export default function offscreenCanvasSupported(): boolean { + if (supportsOffscreenCanvas == null) { + supportsOffscreenCanvas = window.OffscreenCanvas && + new window.OffscreenCanvas(1, 1).getContext('2d') && + typeof window.createImageBitmap === 'function'; + } + + return supportsOffscreenCanvas; +} diff --git a/src/util/web_worker_transfer.js b/src/util/web_worker_transfer.js index 496f12ba61e..a4e2ee7c6d7 100644 --- a/src/util/web_worker_transfer.js +++ b/src/util/web_worker_transfer.js @@ -105,6 +105,11 @@ function isArrayBuffer(val: any): boolean { (val instanceof ArrayBuffer || (val.constructor && val.constructor.name === 'ArrayBuffer')); } +function isImageBitmap(val: any): boolean { + return window.ImageBitmap && + val instanceof window.ImageBitmap; +} + /** * Serialize the given object for transfer to or from a web worker. * @@ -133,7 +138,7 @@ export function serialize(input: mixed, transferables: ?Array): Se return input; } - if (isArrayBuffer(input) || window.ImageBitmap && input instanceof window.ImageBitmap) { + if (isArrayBuffer(input) || isImageBitmap(input)) { if (transferables) { transferables.push(((input: any): ArrayBuffer)); } @@ -224,7 +229,7 @@ export function deserialize(input: Serialized): mixed { input instanceof Date || input instanceof RegExp || isArrayBuffer(input) || - input instanceof ImageBitmap || + isImageBitmap(input) || ArrayBuffer.isView(input) || input instanceof ImageData) { return input; From 5089802aa04e0e185c9bfdad22efba5a46fdcfbe Mon Sep 17 00:00:00 2001 From: Arindam Bose Date: Mon, 2 Dec 2019 14:43:06 -0800 Subject: [PATCH 06/14] One more lint/flow fix --- src/source/raster_dem_tile_source.js | 1 + src/util/ajax.js | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/source/raster_dem_tile_source.js b/src/source/raster_dem_tile_source.js index 507b2992bdd..e4db94b4e51 100644 --- a/src/source/raster_dem_tile_source.js +++ b/src/source/raster_dem_tile_source.js @@ -4,6 +4,7 @@ import {getImage, ResourceType} from '../util/ajax'; import {extend} from '../util/util'; import {Evented} from '../util/evented'; import browser from '../util/browser'; +import window from '../util/window'; import offscreenCanvasSupported from '../util/offscreen_canvas_supported'; import {OverscaledTileID} from './tile_id'; import RasterTileSource from './raster_tile_source'; diff --git a/src/util/ajax.js b/src/util/ajax.js index 39696f16a66..02a9f6961ef 100644 --- a/src/util/ajax.js +++ b/src/util/ajax.js @@ -272,7 +272,7 @@ function arrayBufferToImage(data: ArrayBuffer, callback: (err: ?Error, image: ?H img.src = data.byteLength ? URL.createObjectURL(blob) : transparentPngUrl; } -function arrayBufferToImageBitmap(data: ArrayBuffer, callback: (err: ?Error, image: ?ImageBitmap) => void, cacheControl: ?string, expires: ?string) { +function arrayBufferToImageBitmap(data: ArrayBuffer, callback: (err: ?Error, image: ?ImageBitmap) => void) { const blob: Blob = new window.Blob([new Uint8Array(data)], {type: 'image/png'}); window.createImageBitmap(blob).then((imgBitmap) => { callback(null, imgBitmap); @@ -335,7 +335,7 @@ export const getImage = function(requestParameters: RequestParameters, callback: callback(err); } else if (data) { if (offscreenCanvasSupported()) { - arrayBufferToImageBitmap(data, callback, cacheControl, expires); + arrayBufferToImageBitmap(data, callback); } else { arrayBufferToImage(data, callback, cacheControl, expires); } From 85688767e6c387326b13ce889f2b3e53438adc15 Mon Sep 17 00:00:00 2001 From: Arindam Bose Date: Mon, 2 Dec 2019 15:32:01 -0800 Subject: [PATCH 07/14] Fix flow and tests --- src/source/raster_dem_tile_worker_source.js | 2 ++ src/source/worker_source.js | 2 ++ src/util/web_worker_transfer.js | 6 +++--- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/source/raster_dem_tile_worker_source.js b/src/source/raster_dem_tile_worker_source.js index 2947137d755..bf30d16cf1d 100644 --- a/src/source/raster_dem_tile_worker_source.js +++ b/src/source/raster_dem_tile_worker_source.js @@ -2,6 +2,7 @@ import DEMData from '../data/dem_data'; import {RGBAImage} from '../util/image'; +import window from '../util/window'; import type Actor from '../util/actor'; import type { @@ -9,6 +10,7 @@ import type { WorkerDEMTileCallback, TileParameters } from './worker_source'; +const {ImageBitmap} = window; class RasterDEMTileWorkerSource { actor: Actor; diff --git a/src/source/worker_source.js b/src/source/worker_source.js index 5e5f1097bde..0b2e462d181 100644 --- a/src/source/worker_source.js +++ b/src/source/worker_source.js @@ -11,6 +11,8 @@ import type {CollisionBoxArray} from '../data/array_types'; import type DEMData from '../data/dem_data'; import type {StyleGlyph} from '../style/style_glyph'; import type {StyleImage} from '../style/style_image'; +import window from '../util/window'; +const {ImageBitmap} = window; export type TileParameters = { source: string, diff --git a/src/util/web_worker_transfer.js b/src/util/web_worker_transfer.js index a4e2ee7c6d7..3ef2f74a2dc 100644 --- a/src/util/web_worker_transfer.js +++ b/src/util/web_worker_transfer.js @@ -9,7 +9,7 @@ import CompoundExpression from '../style-spec/expression/compound_expression'; import expressions from '../style-spec/expression/definitions'; import ResolvedImage from '../style-spec/expression/types/resolved_image'; import window from './window'; -const {ImageData} = window; +const {ImageData, ImageBitmap} = window; import type {Transferable} from '../types/transferable'; @@ -106,8 +106,8 @@ function isArrayBuffer(val: any): boolean { } function isImageBitmap(val: any): boolean { - return window.ImageBitmap && - val instanceof window.ImageBitmap; + return ImageBitmap && + val instanceof ImageBitmap; } /** From 4664f65c6be51affec8eb0263bd124da2379593c Mon Sep 17 00:00:00 2001 From: Arindam Bose Date: Mon, 2 Dec 2019 16:34:44 -0800 Subject: [PATCH 08/14] better ImageBitmap check for IE 11 --- src/source/raster_dem_tile_worker_source.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/source/raster_dem_tile_worker_source.js b/src/source/raster_dem_tile_worker_source.js index bf30d16cf1d..3ca8239eeb5 100644 --- a/src/source/raster_dem_tile_worker_source.js +++ b/src/source/raster_dem_tile_worker_source.js @@ -25,7 +25,7 @@ class RasterDEMTileWorkerSource { loadTile(params: WorkerDEMTileParameters, callback: WorkerDEMTileCallback) { const {uid, encoding, rawImageData} = params; - const imagePixels = rawImageData instanceof ImageBitmap ? this.getImageData(rawImageData) : rawImageData; + const imagePixels = (ImageBitmap && rawImageData instanceof ImageBitmap) ? this.getImageData(rawImageData) : rawImageData; const dem = new DEMData(uid, imagePixels, encoding); this.loaded = this.loaded || {}; this.loaded[uid] = dem; From 84119b99efe38a6e644f724648873ee48a28ee0c Mon Sep 17 00:00:00 2001 From: Arindam Bose Date: Thu, 5 Dec 2019 13:55:15 -0800 Subject: [PATCH 09/14] Add HillshadeLoad benchmark --- bench/benchmarks/hillshade_load.js | 54 ++++++++++++++++++++++++++++++ bench/lib/create_map.js | 25 +++++++++----- bench/versions/benchmarks.js | 2 ++ 3 files changed, 73 insertions(+), 8 deletions(-) create mode 100644 bench/benchmarks/hillshade_load.js diff --git a/bench/benchmarks/hillshade_load.js b/bench/benchmarks/hillshade_load.js new file mode 100644 index 00000000000..9f6628f8fe6 --- /dev/null +++ b/bench/benchmarks/hillshade_load.js @@ -0,0 +1,54 @@ +// @flow + +import Benchmark from '../lib/benchmark'; +import createMap from '../lib/create_map'; +import type {StyleSpecification} from '../../src/style-spec/types'; + +export default class HillshadeLoad extends Benchmark { + style: StyleSpecification; + + constructor(style: string) { + super(); + this.style = { + "version": 8, + "name": "Hillshade-only", + "center": [-112.81596278901452, 37.251160384573595], + "zoom": 11.560975632435424, + "bearing": 0, + "pitch": 0, + "sources": { + "mapbox://mapbox.terrain-rgb": { + "url": "mapbox://mapbox.terrain-rgb", + "type": "raster-dem", + "tileSize": 256 + } + }, + "layers": [ + { + "id": "mapbox-terrain-rgb", + "type": "hillshade", + "source": "mapbox://mapbox.terrain-rgb", + "layout": {}, + "paint": {} + } + ] + }; + } + + bench() { + return createMap({ + width: 1024, + height: 1024, + style: this.style, + stubRender: false, + showMap: true + }).then((map) => { + return new Promise((resolve, reject) => { + map.once('idle', () => { + resolve() + map.remove(); + }); + }); + }); + } +} diff --git a/bench/lib/create_map.js b/bench/lib/create_map.js index 318c95daa70..9d2ba01f196 100644 --- a/bench/lib/create_map.js +++ b/bench/lib/create_map.js @@ -4,11 +4,19 @@ import Map from '../../src/ui/map'; export default function (options: any): Promise { return new Promise((resolve, reject) => { + if(options){ + options.stubRender = options.stubRender == null ? true: options.stubRender; + options.showMap = options.showMap == null ? false: options.showMap; + } + const container = document.createElement('div'); container.style.width = `${options.width || 512}px`; container.style.height = `${options.width || 512}px`; container.style.margin = '0 auto'; - container.style.display = 'none'; + + if(!options.showMap){ + container.style.display = 'none'; + } (document.body: any).appendChild(container); const map = new Map(Object.assign({ @@ -18,15 +26,16 @@ export default function (options: any): Promise { map .on('load', () => { - // Stub out `_rerender`; benchmarks need to be the only trigger of `_render` from here on out. - map._rerender = () => {}; + if(options.stubRender){ + // Stub out `_rerender`; benchmarks need to be the only trigger of `_render` from here on out. + map._rerender = () => {}; - // If there's a pending rerender, cancel it. - if (map._frame) { - map._frame.cancel(); - map._frame = null; + // If there's a pending rerender, cancel it. + if (map._frame) { + map._frame.cancel(); + map._frame = null; + } } - resolve(map); }) .on('error', (e) => reject(e.error)) diff --git a/bench/versions/benchmarks.js b/bench/versions/benchmarks.js index 6cab2a05299..2bd19c697ec 100644 --- a/bench/versions/benchmarks.js +++ b/bench/versions/benchmarks.js @@ -11,6 +11,7 @@ import PaintStates from '../benchmarks/paint_states'; import {PropertyLevelRemove, FeatureLevelRemove, SourceLevelRemove} from '../benchmarks/remove_paint_state'; import {LayerBackground, LayerCircle, LayerFill, LayerFillExtrusion, LayerHeatmap, LayerHillshade, LayerLine, LayerRaster, LayerSymbol, LayerSymbolWithIcons} from '../benchmarks/layers'; import Load from '../benchmarks/map_load'; +import HillshadeLoad from '../benchmarks/hillshade_load'; import Validate from '../benchmarks/style_validate'; import StyleLayerCreate from '../benchmarks/style_layer_create'; import QueryPoint from '../benchmarks/query_point'; @@ -69,6 +70,7 @@ register('LayoutDDS', new LayoutDDS()); register('SymbolLayout', new SymbolLayout(style, styleLocations.map(location => location.tileID[0]))); register('FilterCreate', new FilterCreate()); register('FilterEvaluate', new FilterEvaluate()); +register('HillshadeLoad', new HillshadeLoad()); Promise.resolve().then(() => { // Ensure the global worker pool is never drained. Browsers have resource limits From 84b6983bd3f501bd1bf8edfcd293c13555ab494c Mon Sep 17 00:00:00 2001 From: Arindam Bose Date: Thu, 5 Dec 2019 14:27:14 -0800 Subject: [PATCH 10/14] Address CR comments --- bench/benchmarks/hillshade_load.js | 6 ++++-- bench/lib/create_map.js | 10 ++++----- flow-typed/offscreen-canvas.js | 4 ++-- src/source/raster_dem_tile_worker_source.js | 23 ++++++++++++--------- 4 files changed, 24 insertions(+), 19 deletions(-) diff --git a/bench/benchmarks/hillshade_load.js b/bench/benchmarks/hillshade_load.js index 9f6628f8fe6..345da606735 100644 --- a/bench/benchmarks/hillshade_load.js +++ b/bench/benchmarks/hillshade_load.js @@ -7,7 +7,7 @@ import type {StyleSpecification} from '../../src/style-spec/types'; export default class HillshadeLoad extends Benchmark { style: StyleSpecification; - constructor(style: string) { + constructor() { super(); this.style = { "version": 8, @@ -45,9 +45,11 @@ export default class HillshadeLoad extends Benchmark { }).then((map) => { return new Promise((resolve, reject) => { map.once('idle', () => { - resolve() + resolve(); map.remove(); }); + + map.once('error', (e) => reject(e)); }); }); } diff --git a/bench/lib/create_map.js b/bench/lib/create_map.js index 9d2ba01f196..73b633e7aff 100644 --- a/bench/lib/create_map.js +++ b/bench/lib/create_map.js @@ -4,9 +4,9 @@ import Map from '../../src/ui/map'; export default function (options: any): Promise { return new Promise((resolve, reject) => { - if(options){ - options.stubRender = options.stubRender == null ? true: options.stubRender; - options.showMap = options.showMap == null ? false: options.showMap; + if (options) { + options.stubRender = options.stubRender == null ? true : options.stubRender; + options.showMap = options.showMap == null ? false : options.showMap; } const container = document.createElement('div'); @@ -14,7 +14,7 @@ export default function (options: any): Promise { container.style.height = `${options.width || 512}px`; container.style.margin = '0 auto'; - if(!options.showMap){ + if (!options.showMap) { container.style.display = 'none'; } (document.body: any).appendChild(container); @@ -26,7 +26,7 @@ export default function (options: any): Promise { map .on('load', () => { - if(options.stubRender){ + if (options.stubRender) { // Stub out `_rerender`; benchmarks need to be the only trigger of `_render` from here on out. map._rerender = () => {}; diff --git a/flow-typed/offscreen-canvas.js b/flow-typed/offscreen-canvas.js index 7b165f06a52..6c9617dad11 100644 --- a/flow-typed/offscreen-canvas.js +++ b/flow-typed/offscreen-canvas.js @@ -5,5 +5,5 @@ declare class OffscreenCanvas { height: number; constructor(width: number, height: number): OffscreenCanvas; - getContext(contextType: '2d' ): CanvasRenderingContext2D; -} \ No newline at end of file + getContext(contextType: '2d'): CanvasRenderingContext2D; +} diff --git a/src/source/raster_dem_tile_worker_source.js b/src/source/raster_dem_tile_worker_source.js index 3ca8239eeb5..e15593a3cd0 100644 --- a/src/source/raster_dem_tile_worker_source.js +++ b/src/source/raster_dem_tile_worker_source.js @@ -15,8 +15,8 @@ const {ImageBitmap} = window; class RasterDEMTileWorkerSource { actor: Actor; loaded: {[string]: DEMData}; - offcreenCanvas: OffscreenCanvas; - offcreenCanvasContext: CanvasRenderingContext2D; + offscreenCanvas: OffscreenCanvas; + offscreenCanvasContext: CanvasRenderingContext2D; constructor() { this.loaded = {}; @@ -24,7 +24,7 @@ class RasterDEMTileWorkerSource { loadTile(params: WorkerDEMTileParameters, callback: WorkerDEMTileCallback) { const {uid, encoding, rawImageData} = params; - + // Main thread will transfer ImageBitmap if offscreen decode with OffscreenCanvas is supported, else it will transfer an already decoded image. const imagePixels = (ImageBitmap && rawImageData instanceof ImageBitmap) ? this.getImageData(rawImageData) : rawImageData; const dem = new DEMData(uid, imagePixels, encoding); this.loaded = this.loaded || {}; @@ -34,16 +34,19 @@ class RasterDEMTileWorkerSource { getImageData(imgBitmap: ImageBitmap): RGBAImage { // Lazily initialize OffscreenCanvas - if (!this.offcreenCanvas || !this.offcreenCanvasContext) { - this.offcreenCanvas = new OffscreenCanvas(512, 512); - this.offcreenCanvasContext = this.offcreenCanvas.getContext('2d'); + if (!this.offscreenCanvas || !this.offscreenCanvasContext) { + // Dem tiles are typically 256x256 + this.offscreenCanvas = new OffscreenCanvas(256, 256); + this.offscreenCanvasContext = this.offscreenCanvas.getContext('2d'); } - this.offcreenCanvas.width = imgBitmap.width; - this.offcreenCanvas.height = imgBitmap.height; - this.offcreenCanvasContext.drawImage(imgBitmap, 0, 0, imgBitmap.width, imgBitmap.height); + this.offscreenCanvas.width = imgBitmap.width; + this.offscreenCanvas.width= imgBitmap.height; + + this.offscreenCanvasContext.drawImage(imgBitmap, 0, 0, imgBitmap.width, imgBitmap.height); // Insert an additional 1px padding around the image to allow backfilling for neighboring data. - const imgData = this.offcreenCanvasContext.getImageData(-1, -1, imgBitmap.width + 2, imgBitmap.height + 2); + const imgData = this.offscreenCanvasContext.getImageData(-1, -1, imgBitmap.width + 2, imgBitmap.height + 2); + this.offscreenCanvasContext.clearRect(0, 0, this.offscreenCanvas.width, this.offscreenCanvas.height); return new RGBAImage({width: imgData.width, height: imgData.height}, imgData.data); } From 19f4ab7bcc3eadbb94067bdffe83118dbffbd929 Mon Sep 17 00:00:00 2001 From: Arindam Bose Date: Thu, 5 Dec 2019 16:05:37 -0800 Subject: [PATCH 11/14] Address some more CR comments --- src/util/ajax.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/util/ajax.js b/src/util/ajax.js index 02a9f6961ef..31a19bf075c 100644 --- a/src/util/ajax.js +++ b/src/util/ajax.js @@ -260,7 +260,7 @@ const transparentPngUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAA function arrayBufferToImage(data: ArrayBuffer, callback: (err: ?Error, image: ?HTMLImageElement) => void, cacheControl: ?string, expires: ?string) { const img: HTMLImageElement = new window.Image(); - const URL = window.URL || window.webkitURL; + const URL = window.URL; img.onload = () => { callback(null, img); URL.revokeObjectURL(img.src); @@ -277,7 +277,6 @@ function arrayBufferToImageBitmap(data: ArrayBuffer, callback: (err: ?Error, ima window.createImageBitmap(blob).then((imgBitmap) => { callback(null, imgBitmap); }).catch((e) => { - console.log(e); callback(new Error('Could not load image. Please make sure to use a supported image type such as PNG or JPEG. Note that SVGs are not supported.')); }); } From 6d42581230f72af0d17cba100297b02cb934c70d Mon Sep 17 00:00:00 2001 From: Arindam Bose Date: Thu, 5 Dec 2019 16:10:19 -0800 Subject: [PATCH 12/14] Fix lint issue --- src/util/ajax.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/ajax.js b/src/util/ajax.js index 31a19bf075c..3c1d26f4d16 100644 --- a/src/util/ajax.js +++ b/src/util/ajax.js @@ -276,7 +276,7 @@ function arrayBufferToImageBitmap(data: ArrayBuffer, callback: (err: ?Error, ima const blob: Blob = new window.Blob([new Uint8Array(data)], {type: 'image/png'}); window.createImageBitmap(blob).then((imgBitmap) => { callback(null, imgBitmap); - }).catch((e) => { + }).catch(() => { callback(new Error('Could not load image. Please make sure to use a supported image type such as PNG or JPEG. Note that SVGs are not supported.')); }); } From 199c2d89b7cf01413a8f6a889e7f51c86e395716 Mon Sep 17 00:00:00 2001 From: Arindam Bose Date: Fri, 6 Dec 2019 10:35:08 -0800 Subject: [PATCH 13/14] Oh yea, this makes sense! MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Konstantin Käfer --- src/source/raster_dem_tile_worker_source.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/source/raster_dem_tile_worker_source.js b/src/source/raster_dem_tile_worker_source.js index bf1f8967143..ef965491285 100644 --- a/src/source/raster_dem_tile_worker_source.js +++ b/src/source/raster_dem_tile_worker_source.js @@ -36,7 +36,7 @@ class RasterDEMTileWorkerSource { // Lazily initialize OffscreenCanvas if (!this.offscreenCanvas || !this.offscreenCanvasContext) { // Dem tiles are typically 256x256 - this.offscreenCanvas = new OffscreenCanvas(256, 256); + this.offscreenCanvas = new OffscreenCanvas(imgBitmap.width, imgBitmap.height); this.offscreenCanvasContext = this.offscreenCanvas.getContext('2d'); } From d8bc399056050280602d0d8d313188fee865d558 Mon Sep 17 00:00:00 2001 From: Arindam Bose Date: Mon, 9 Dec 2019 11:38:21 -0800 Subject: [PATCH 14/14] Clarify usage by using a maybe type --- src/util/offscreen_canvas_supported.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/offscreen_canvas_supported.js b/src/util/offscreen_canvas_supported.js index b4ea46d89a7..4ec6b188d05 100644 --- a/src/util/offscreen_canvas_supported.js +++ b/src/util/offscreen_canvas_supported.js @@ -1,7 +1,7 @@ // @flow import window from './window'; -let supportsOffscreenCanvas = null; +let supportsOffscreenCanvas: ?boolean; export default function offscreenCanvasSupported(): boolean { if (supportsOffscreenCanvas == null) {