From 8094f9f4dd2a25d90825bf2e81bfcdd8c9aa29ab Mon Sep 17 00:00:00 2001 From: Chris Lord Date: Wed, 23 Apr 2025 12:38:41 +0100 Subject: [PATCH 1/7] Optimise Bounds construction Bounds creation shows up surprisingly high when scrolling due to its use in TileManager.updateTileDistance. Optimise the Bounds constructor to minimise the amount of unnecessary cloning. Signed-off-by: Chris Lord Change-Id: I5b43e49e565e38d07fa8640d649ee92142523f86 --- browser/src/geometry/Bounds.ts | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/browser/src/geometry/Bounds.ts b/browser/src/geometry/Bounds.ts index ec55b4148aead..ccfc60cc5d565 100644 --- a/browser/src/geometry/Bounds.ts +++ b/browser/src/geometry/Bounds.ts @@ -23,11 +23,21 @@ export class Bounds { if (!a) return; - var points = b ? [a, b] : a; - - for (var i = 0, len = points.length; i < len; i++) { - this.extend(points[i]); - } + // Bounds construction is called very often so it's important to avoid object construction + // when possible. This is the reason for the amount of convolution here (that and ES6's lack + // of multiple constructors...) + if (b) { + this.min = a instanceof Point ? a.clone() : toPoint(a); + const maybeMax = b instanceof Point ? b.clone() : toPoint(b); + if (maybeMax.x >= this.min.x && maybeMax.y >= this.min.y) + this.max = maybeMax; + else { + this.max = this.min.clone(); + this.extend(maybeMax); + } + } else + for (const point of a) + this.extend(point); } public static parse(rectString: string): Bounds { From a2695ffb208f935a2202d507abd81f5bed629130 Mon Sep 17 00:00:00 2001 From: Chris Lord Date: Wed, 23 Apr 2025 15:24:33 +0100 Subject: [PATCH 2/7] If tile updates fail, try to maintain correct tile properties In the case that a keyframe or delta update doesn't result in a tile having an ImageData object, try to still maintain correct tile properties and maintain the empty tiles count. Signed-off-by: Chris Lord Change-Id: Ie940739030febdd1748f87e4dfad10aef4b57022 --- browser/src/app/TilesMiddleware.ts | 42 ++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/browser/src/app/TilesMiddleware.ts b/browser/src/app/TilesMiddleware.ts index 21db83f034ea9..758dc5e7c6cf2 100644 --- a/browser/src/app/TilesMiddleware.ts +++ b/browser/src/app/TilesMiddleware.ts @@ -478,6 +478,32 @@ class TileManager { this.garbageCollect(); } + private static createTileBitmap( + tile: Tile, + delta: any, + deltas: any[], + bitmaps: Promise[], + ) { + if (tile.imgDataCache) { + bitmaps.push( + createImageBitmap(tile.imgDataCache, { + premultiplyAlpha: 'none', + }), + ); + deltas.push(delta); + } else { + // This is an error state, tiles should not have a null ImageData + // after a keyframe or delta update. It's still a good idea to maintain + // correct properties for a chance at recovery and continuing in a + // coherent state. + tile.distanceFromView = Number.MAX_SAFE_INTEGER; + if (delta.isKeyframe) --tile.hasPendingKeyframe; + else --tile.hasPendingDelta; + if (!tile.hasPendingUpdate()) + this.tileReady(tile.coords, this.getVisibleRanges()); + } + } + private static decompressPendingDeltas(message: string) { ++this.pendingTransactions; if (this.worker) { @@ -544,12 +570,7 @@ class TileManager { e.wireMessage, ); - if (tile.imgDataCache) { - bitmaps.push( - createImageBitmap(tile.imgDataCache, { premultiplyAlpha: 'none' }), - ); - pendingDeltas.push(e); - } + this.createTileBitmap(tile, e, pendingDeltas, bitmaps); } Promise.all(bitmaps).then((bitmaps) => { @@ -2136,14 +2157,7 @@ class TileManager { x.wireMessage, ); - if (tile.imgDataCache) { - bitmaps.push( - createImageBitmap(tile.imgDataCache, { - premultiplyAlpha: 'none', - }), - ); - pendingDeltas.push(x); - } + this.createTileBitmap(tile, x, pendingDeltas, bitmaps); } Promise.all(bitmaps).then((bitmaps) => { From ba4eb35347840efa3132d86e65c9b39ffeb9f46f Mon Sep 17 00:00:00 2001 From: Chris Lord Date: Fri, 25 Apr 2025 11:45:46 +0100 Subject: [PATCH 3/7] Handle Worker transactions progressively Handle Worker tile decompression 10ms at a time and allow messages to be processed between tile decompression. This should have no immediate effect, but lays the groundwork for transactions to be interrupted or cancelled. Signed-off-by: Chris Lord Change-Id: I91c04510411a3bb2124ccb08a923615cf7b99199 --- browser/src/layer/tile/CanvasTileWorkerSrc.js | 132 ++++++++++++------ 1 file changed, 87 insertions(+), 45 deletions(-) diff --git a/browser/src/layer/tile/CanvasTileWorkerSrc.js b/browser/src/layer/tile/CanvasTileWorkerSrc.js index c2b1b126e0d5b..279b65fd913e0 100644 --- a/browser/src/layer/tile/CanvasTileWorkerSrc.js +++ b/browser/src/layer/tile/CanvasTileWorkerSrc.js @@ -13,6 +13,86 @@ /* eslint no-unused-vars: ["warn", { "argsIgnorePattern": "^_" }] */ /* global importScripts Uint8Array */ +// Amount of time to spend decompressing deltas before returning to the main loop +const PROCESS_TIME = 10; + +let transactionHandlerId = null; +const transactions = []; + +function transactionCallback(start_time = null) { + if (start_time === null) start_time = performance.now(); + else if (performance.now() - start_time >= PROCESS_TIME) { + transactionHandlerId = setTimeout(() => transactionCallback(), 0); + return; + } + + const transaction = transactions.shift(); + const tileByteSize = + transaction.data.tileSize * transaction.data.tileSize * 4; + + while (transaction.data.deltas.length) { + const tile = transaction.data.deltas.pop(); + + transaction.decompressed.push(tile); + transaction.buffers.push(tile.rawDelta.buffer); + + const deltas = self.fzstd.decompress(tile.rawDelta); + tile.keyframeDeltaSize = 0; + + // Decompress the keyframe buffer + if (tile.isKeyframe) { + const keyframeBuffer = new Uint8Array(tileByteSize); + tile.keyframeDeltaSize = L.CanvasTileUtils.unrle( + deltas, + transaction.data.tileSize, + transaction.data.tileSize, + keyframeBuffer, + ); + tile.keyframeBuffer = new Uint8ClampedArray( + keyframeBuffer.buffer, + keyframeBuffer.byteOffset, + keyframeBuffer.byteLength, + ); + transaction.buffers.push(tile.keyframeBuffer.buffer); + } + + // Now wrap as Uint8ClampedArray as that's what ImageData requires. Don't do + // it earlier to avoid unnecessarily incurring bounds-checking penalties. + tile.deltas = new Uint8ClampedArray( + deltas.buffer, + deltas.byteOffset, + deltas.length, + ); + + transaction.buffers.push(tile.deltas.buffer); + if (performance.now() - start_time >= PROCESS_TIME) break; + } + + if (transaction.data.deltas.length) { + transactions.unshift(transaction); + transactionHandlerId = setTimeout(() => transactionCallback(), 0); + return; + } + + // Transaction is complete, send it back. + postMessage( + { + message: transaction.data.message, + deltas: transaction.decompressed, + tileSize: transaction.data.tileSize, + }, + transaction.buffers, + ); + + if (transactions.length === 0) { + transactionHandlerId = null; + return; + } + + // See if we have time to process further transactions + transactionCallback(start_time); +} + if ('undefined' === typeof window) { self.L = {}; @@ -24,51 +104,13 @@ if ('undefined' === typeof window) { function onMessage(e) { switch (e.data.message) { case 'endTransaction': - var tileByteSize = e.data.tileSize * e.data.tileSize * 4; - var decompressed = []; - var buffers = []; - for (var tile of e.data.deltas) { - var deltas = self.fzstd.decompress(tile.rawDelta); - tile.keyframeDeltaSize = 0; - - // Decompress the keyframe buffer - if (tile.isKeyframe) { - var keyframeBuffer = new Uint8Array(tileByteSize); - tile.keyframeDeltaSize = L.CanvasTileUtils.unrle( - deltas, - e.data.tileSize, - e.data.tileSize, - keyframeBuffer, - ); - tile.keyframeBuffer = new Uint8ClampedArray( - keyframeBuffer.buffer, - keyframeBuffer.byteOffset, - keyframeBuffer.byteLength, - ); - buffers.push(tile.keyframeBuffer.buffer); - } - - // Now wrap as Uint8ClampedArray as that's what ImageData requires. Don't do - // it earlier to avoid unnecessarily incurring bounds-checking penalties. - tile.deltas = new Uint8ClampedArray( - deltas.buffer, - deltas.byteOffset, - deltas.length, - ); - - decompressed.push(tile); - buffers.push(tile.rawDelta.buffer); - buffers.push(tile.deltas.buffer); - } - - postMessage( - { - message: e.data.message, - deltas: decompressed, - tileSize: e.data.tileSize, - }, - buffers, - ); + transactions.push({ + data: e.data, + decompressed: [], + buffers: [], + }); + if (transactionHandlerId !== null) clearTimeout(transactionHandlerId); + transactionCallback(); break; default: From d11671ac181b58679c447130232601d6bb0a0002 Mon Sep 17 00:00:00 2001 From: Chris Lord Date: Fri, 25 Apr 2025 12:55:51 +0100 Subject: [PATCH 4/7] Skip decompressing keyframes that become not-current If a subsequent transaction makes a pending tile dehydration unnecessary, skip it to catch up more quickly. Signed-off-by: Chris Lord Change-Id: I3cd76e414536d2a762291ed857eee0dcdd3307ff --- browser/src/app/TilesMiddleware.ts | 37 ++++++++++++------- browser/src/layer/tile/CanvasTileWorkerSrc.js | 5 +++ 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/browser/src/app/TilesMiddleware.ts b/browser/src/app/TilesMiddleware.ts index 758dc5e7c6cf2..2078b0dedd5e3 100644 --- a/browser/src/app/TilesMiddleware.ts +++ b/browser/src/app/TilesMiddleware.ts @@ -512,6 +512,9 @@ class TileManager { message: message, deltas: this.pendingDeltas, tileSize: this.tileSize, + current: Object.keys(this.tiles).filter( + (k) => this.tiles[k].distanceFromView === 0, + ), }, this.pendingDeltas.map((x: any) => x.rawDelta.buffer), ); @@ -2142,20 +2145,28 @@ class TileManager { } let keyframeImage = null; - if (x.isKeyframe) - keyframeImage = new ImageData( - x.keyframeBuffer, - e.data.tileSize, - e.data.tileSize, + if (x.isKeyframe) { + if (x.keyframeBuffer) + keyframeImage = new ImageData( + x.keyframeBuffer, + e.data.tileSize, + e.data.tileSize, + ); + else { + tile.imgDataCache = null; + tile.rawDeltas = x.rawDelta; + } + } + + if (!x.isKeyframe || keyframeImage) + this.applyDelta( + tile, + x.rawDelta, + x.deltas, + x.keyframeDeltaSize, + keyframeImage, + x.wireMessage, ); - this.applyDelta( - tile, - x.rawDelta, - x.deltas, - x.keyframeDeltaSize, - keyframeImage, - x.wireMessage, - ); this.createTileBitmap(tile, x, pendingDeltas, bitmaps); } diff --git a/browser/src/layer/tile/CanvasTileWorkerSrc.js b/browser/src/layer/tile/CanvasTileWorkerSrc.js index 279b65fd913e0..504b18d74778a 100644 --- a/browser/src/layer/tile/CanvasTileWorkerSrc.js +++ b/browser/src/layer/tile/CanvasTileWorkerSrc.js @@ -18,6 +18,7 @@ const PROCESS_TIME = 10; let transactionHandlerId = null; const transactions = []; +let currentKeys = new Set(); function transactionCallback(start_time = null) { if (start_time === null) start_time = performance.now(); @@ -36,6 +37,9 @@ function transactionCallback(start_time = null) { transaction.decompressed.push(tile); transaction.buffers.push(tile.rawDelta.buffer); + // Skip keyframe tiles that are no longer current + if (tile.isKeyframe && !currentKeys.has(tile.key)) continue; + const deltas = self.fzstd.decompress(tile.rawDelta); tile.keyframeDeltaSize = 0; @@ -104,6 +108,7 @@ if ('undefined' === typeof window) { function onMessage(e) { switch (e.data.message) { case 'endTransaction': + currentKeys = new Set(e.data.current); transactions.push({ data: e.data, decompressed: [], From 22363c4a4ac118abefeb04f66a4dc1319ad399b2 Mon Sep 17 00:00:00 2001 From: Chris Lord Date: Fri, 25 Apr 2025 14:16:27 +0100 Subject: [PATCH 5/7] Make draw pausing around dehydration simpler and more reliable Instead of nesting pause/resume on tile dehydration, just pause once when we go to dehydrate a visible tile and resume when all tiles that don't require fetching have no pending updates. Signed-off-by: Chris Lord Change-Id: I9b3391556884a19678a3885f804577ed8ee8c88a --- browser/src/app/TilesMiddleware.ts | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/browser/src/app/TilesMiddleware.ts b/browser/src/app/TilesMiddleware.ts index 2078b0dedd5e3..f0e9e25630bc7 100644 --- a/browser/src/app/TilesMiddleware.ts +++ b/browser/src/app/TilesMiddleware.ts @@ -242,6 +242,7 @@ class TileManager { // The tile expansion ratio that the visible tile area will be expanded towards when // updating during scrolling private static directionalTileExpansion: number = 2; + private static pausedForDehydration: boolean = false; //private static _debugTime: any = {}; Reserved for future. @@ -475,6 +476,26 @@ class TileManager { } } + if (this.pausedForDehydration) { + // Check if all current tiles are accounted for and resume drawing if so. + let shouldUnpause = true; + for (const key in this.tiles) { + const tile = this.tiles[key]; + if ( + tile.distanceFromView === 0 && + !tile.needsFetch() && + tile.hasPendingUpdate() + ) { + shouldUnpause = false; + break; + } + } + if (shouldUnpause) { + app.sectionContainer.resumeDrawing(); + this.pausedForDehydration = false; + } + } + this.garbageCollect(); } @@ -1489,12 +1510,11 @@ class TileManager { } // If we dehydrated a visible tile, wait for it to be ready before drawing - if (dehydratedVisible) { + if (dehydratedVisible && !this.pausedForDehydration) { app.sectionContainer.pauseDrawing(); - this.endTransaction(() => { - app.sectionContainer.resumeDrawing(); - }); - } else this.endTransaction(null); + this.pausedForDehydration = true; + } + this.endTransaction(null); return queue; } From 4792217af246769f164448fa11f95e9ae3a2dfd6 Mon Sep 17 00:00:00 2001 From: Chris Lord Date: Tue, 29 Apr 2025 15:00:07 +0100 Subject: [PATCH 6/7] Don't expand the current area when we aren't keeping up with dehydration If we can't keep up with tile rehydration (which occurs commonly when dragging the scroll handle to cover a large distance quickly), don't expand the current area. This reduces the amount of dehydration work required and lets us push out more frames. Signed-off-by: Chris Lord Change-Id: I29c223985a63da815ff0a600694c8aaef7afca52 --- browser/src/app/TilesMiddleware.ts | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/browser/src/app/TilesMiddleware.ts b/browser/src/app/TilesMiddleware.ts index f0e9e25630bc7..243b387f063c7 100644 --- a/browser/src/app/TilesMiddleware.ts +++ b/browser/src/app/TilesMiddleware.ts @@ -243,6 +243,7 @@ class TileManager { // updating during scrolling private static directionalTileExpansion: number = 2; private static pausedForDehydration: boolean = false; + private static shrinkCurrentId: any = null; //private static _debugTime: any = {}; Reserved for future. @@ -1477,9 +1478,10 @@ class TileManager { for (var rangeIdx = 0; rangeIdx < tileRanges.length; ++rangeIdx) { // Expand the 'current' area to add a small buffer around the visible area that // helps us avoid visible tile updates. - const tileRange = isCurrent - ? this.expandTileRange(tileRanges[rangeIdx]) - : tileRanges[rangeIdx]; + const tileRange = + isCurrent && !this.shrinkCurrentId + ? this.expandTileRange(tileRanges[rangeIdx]) + : tileRanges[rangeIdx]; for (var j = tileRange.min.y; j <= tileRange.max.y; ++j) { for (var i = tileRange.min.x; i <= tileRange.max.x; ++i) { @@ -2112,8 +2114,7 @@ class TileManager { if (app.map._docLayer.isCalc() && !app.map._docLayer._gotFirstCellCursor) return; - // be sure canvas is initialized already, has correct size and that we aren't - // currently processing a transaction + // be sure canvas is initialized already and has the correct size. const size: any = map.getSize(); if (size.x === 0 || size.y === 0) { setTimeout( @@ -2125,6 +2126,16 @@ class TileManager { return; } + // If an update occurs while we're paused for dehydration, we haven't been able to + // keep up with scrolling. In this case, we should stop expanding the current area + // so that it takes less time to dehydrate it. + if (this.pausedForDehydration) { + if (this.shrinkCurrentId) clearTimeout(this.shrinkCurrentId); + this.shrinkCurrentId = setTimeout(() => { + this.shrinkCurrentId = null; + }, 100); + } + if (app.file.fileBasedView) { this.updateFileBasedView(); return; From c858fb33a405f7d8b34643aa942a7cbc8149edea Mon Sep 17 00:00:00 2001 From: Chris Lord Date: Tue, 29 Apr 2025 16:22:14 +0100 Subject: [PATCH 7/7] Use a Map type for TileManager.tiles Using a Map enables more efficient iteration/storage and gives us more type-checking. Signed-off-by: Chris Lord Change-Id: Ib2ad73f9a2306c1864bb3a361f4089dfbc602512 --- browser/src/app/TilesMiddleware.ts | 92 +++++++++++++++--------------- 1 file changed, 47 insertions(+), 45 deletions(-) diff --git a/browser/src/app/TilesMiddleware.ts b/browser/src/app/TilesMiddleware.ts index 243b387f063c7..02ce4f8e389c4 100644 --- a/browser/src/app/TilesMiddleware.ts +++ b/browser/src/app/TilesMiddleware.ts @@ -233,7 +233,7 @@ class TileManager { private static emptyTilesCount: number = 0; private static debugDeltas: boolean = false; private static debugDeltasDetail: boolean = false; - private static tiles: any = {}; // stores all tiles, keyed by coordinates, and cached, compressed deltas + private static tiles: Map = new Map(); // stores all tiles, keyed by coordinates, and cached, compressed deltas private static tileBitmapList: Tile[] = []; // stores all tiles with bitmaps, sorted by distance from view(s) public static tileSize: number = 256; @@ -265,9 +265,7 @@ class TileManager { var totalSize = 0; var n_bitmaps = 0; var n_current = 0; - var keys = Object.keys(this.tiles); - for (var i = 0; i < keys.length; ++i) { - var tile = this.tiles[keys[i]]; + for (const tile of this.tiles.values()) { if (tile.image) ++n_bitmaps; if (tile.distanceFromView === 0) ++n_current; totalSize += tile.rawDeltas ? tile.rawDeltas.length : 0; @@ -279,7 +277,7 @@ class TileManager { app.map._debug.setOverlayMessage( 'top-tileMem', 'Tiles: ' + - String(keys.length).padStart(4, ' ') + + String(this.tiles.size).padStart(4, ' ') + ', bitmaps: ' + String(n_bitmaps).padStart(3, ' ') + ' current ' + @@ -295,8 +293,10 @@ class TileManager { } private static sortTileKeysByDistance() { - return Object.keys(this.tiles).sort((a: any, b: any) => { - return this.tiles[b].distanceFromView - this.tiles[a].distanceFromView; + return Array.from(this.tiles.keys()).sort((a: any, b: any) => { + return ( + this.tiles.get(b).distanceFromView - this.tiles.get(a).distanceFromView + ); }); } @@ -324,7 +324,7 @@ class TileManager { // FIXME: could maintain this as we go rather than re-accounting it regularly. var totalSize = 0; var tileCount = 0; - for (const tile of Object.values(this.tiles) as Tile[]) { + for (const tile of this.tiles.values()) { // Don't count size of tiles that are visible or that have pending deltas. We don't have // a mechanism to immediately rehydrate tiles, so GC'ing visible tiles would // cause flickering, and the same would happen for tiles with pending deltas. @@ -347,7 +347,7 @@ class TileManager { for (var i = 0; i < keys.length && totalSize > lowDeltaMemory; ++i) { const key = keys[i]; - const tile: Tile = this.tiles[key]; + const tile: Tile = this.tiles.get(key); if ( tile.rawDeltas && tile.distanceFromView !== 0 && @@ -376,7 +376,7 @@ class TileManager { for (var i = 0; i < keys.length - lowTileCount; ++i) { const key = keys[i]; - const tile: Tile = this.tiles[key]; + const tile: Tile = this.tiles.get(key); if (tile.distanceFromView !== 0 && !tile.hasPendingDelta) { this.removeTile(keys[i]); } @@ -450,7 +450,7 @@ class TileManager { const delta = deltas.shift(); const bitmap = bitmaps.shift(); - const tile = this.tiles[delta.key]; + const tile = this.tiles.get(delta.key); if (!tile) continue; this.setBitmapOnTile(tile, bitmap); @@ -480,8 +480,7 @@ class TileManager { if (this.pausedForDehydration) { // Check if all current tiles are accounted for and resume drawing if so. let shouldUnpause = true; - for (const key in this.tiles) { - const tile = this.tiles[key]; + for (const tile of this.tiles.values()) { if ( tile.distanceFromView === 0 && !tile.needsFetch() && @@ -534,8 +533,8 @@ class TileManager { message: message, deltas: this.pendingDeltas, tileSize: this.tileSize, - current: Object.keys(this.tiles).filter( - (k) => this.tiles[k].distanceFromView === 0, + current: Array.from(this.tiles.keys()).filter( + (key) => this.tiles.get(key).distanceFromView === 0, ), }, this.pendingDeltas.map((x: any) => x.rawDelta.buffer), @@ -545,7 +544,7 @@ class TileManager { const bitmaps: Promise[] = []; const pendingDeltas: any[] = []; for (const e of this.pendingDeltas) { - const tile = this.tiles[e.key]; + const tile = this.tiles.get(e.key); if (!tile) { window.app.console.warn( @@ -939,7 +938,7 @@ class TileManager { ) { var key = coords.key(); - var tile: Tile = this.tiles[key]; + const tile: Tile = this.tiles.get(key); if (!tile) return; var emptyTilesCountChanged = false; @@ -962,14 +961,14 @@ class TileManager { } private static createTile(coords: TileCoordData, key: string) { - if (this.tiles[key]) { + if (this.tiles.has(key)) { if (this.debugDeltas) window.app.console.debug('Already created tile ' + key); - return this.tiles[key]; + return this.tiles.get(key); } const tile = new Tile(coords); - this.tiles[key] = tile; + this.tiles.set(key, tile); return tile; } @@ -1192,7 +1191,7 @@ class TileManager { } private static removeTile(key: string) { - var tile = this.tiles[key]; + const tile = this.tiles.get(key); if (!tile) return; if ( @@ -1203,12 +1202,12 @@ class TileManager { this.emptyTilesCount -= 1; this.reclaimTileBitmapMemory(tile); - delete this.tiles[key]; + this.tiles.delete(key); } private static removeAllTiles() { this.tileBitmapList = []; - for (var key in this.tiles) { + for (const key in Array.from(this.tiles.keys())) { this.removeTile(key); } } @@ -1356,7 +1355,7 @@ class TileManager { for (var i = 0; i < partTileQueue.length; ++i) { var coords = partTileQueue[i]; var key = coords.key(); - var tile = this.tiles[key]; + const tile = this.tiles.get(key); // don't send lots of duplicate, fast tilecombines if (tile && tile.requestingTooFast(now)) continue; @@ -1413,7 +1412,7 @@ class TileManager { } private static tileNeedsFetch(key: string) { - const tile: Tile = this.tiles[key]; + const tile: Tile = this.tiles.get(key); return !tile || tile.needsFetch(); } @@ -1466,8 +1465,9 @@ class TileManager { const currentBounds = app.map._docLayer._splitPanesContext ? app.map._docLayer._splitPanesContext.getPxBoundList(pixelBounds) : [pixelBounds]; - for (const key in this.tiles) - this.updateTileDistance(this.tiles[key], zoom, currentBounds); + for (const tile of this.tiles.values()) { + this.updateTileDistance(tile, zoom, currentBounds); + } this.sortTileBitmapList(); } @@ -1496,7 +1496,7 @@ class TileManager { if (!this.isValidTile(coords)) continue; var key = coords.key(); - var tile = this.tiles[key]; + const tile = this.tiles.get(key); if (!tile || tile.needsFetch()) queue.push(coords); else if (isCurrent && this.makeTileCurrent(tile)) { @@ -1560,7 +1560,7 @@ class TileManager { // Ensure tiles exist for requested coordinates for (let i = 0; i < coordsQueue.length; i++) { const key = coordsQueue[i].key(); - let tile: Tile = this.tiles[key]; + let tile: Tile = this.tiles.get(key); if (!tile) { tile = this.createTile(coordsQueue[i], key); @@ -1638,7 +1638,9 @@ class TileManager { } public static refreshTilesInBackground() { - for (const key in this.tiles) this.tiles[key].forceKeyframe(); + for (const tile of this.tiles.values()) { + tile.forceKeyframe(); + } } public static setDebugDeltas(state: boolean) { @@ -1647,7 +1649,7 @@ class TileManager { } public static get(key: string): Tile { - return this.tiles[key]; + return this.tiles.get(key); } private static pixelCoordsToTwipTileBounds(coords: TileCoordData): number[] { @@ -1671,8 +1673,8 @@ class TileManager { let needsNewTiles = false; const calc = app.map._docLayer.isCalc(); - for (const key in this.tiles) { - const coords: TileCoordData = this.tiles[key].coords; + this.tiles.forEach((tile, key) => { + const coords: TileCoordData = tile.coords; const tileRectangle = this.pixelCoordsToTwipTileBounds(coords); if ( @@ -1681,11 +1683,11 @@ class TileManager { (invalidatedRectangle.intersectsRectangle(tileRectangle) || (calc && !this.tileZoomIsCurrent(coords))) // In calc, we invalidate all tiles with different zoom levels. ) { - if (this.tiles[key].distanceFromView === 0) needsNewTiles = true; + if (tile.distanceFromView === 0) needsNewTiles = true; this.invalidateTile(key, wireId); } - } + }); if ( app.map._docLayer._debug.tileInvalidationsOn && @@ -1981,7 +1983,7 @@ class TileManager { var coords = this.tileMsgToCoords(tileMsgObj); var key = coords.key(); - var tile = this.tiles[key]; + let tile = this.tiles.get(key); if (!tile) { tile = this.createTile(coords, key); @@ -2166,7 +2168,7 @@ class TileManager { switch (e.data.message) { case 'endTransaction': for (const x of e.data.deltas) { - const tile = this.tiles[x.key]; + const tile = this.tiles.get(x.key); if (!tile) { window.app.console.warn( @@ -2230,7 +2232,7 @@ class TileManager { for (let i = 0; i < queue.length; i++) { coords = queue[i]; key = coords.key(); - if (!this.tiles[key]) this.createTile(coords, key); + if (!this.tiles.has(key)) this.createTile(coords, key); } this.sendTileCombineRequest(queue); @@ -2286,8 +2288,8 @@ class TileManager { const coordList = Array(); const zoom = app.map.getZoom(); - for (const [, value] of Object.entries(this.tiles)) { - const coords = (value as Tile).coords; + for (const tile of this.tiles.values()) { + const coords = tile.coords; if ( coords.z === zoom && rectangle.intersectsRectangle([ @@ -2397,14 +2399,14 @@ class TileManager { this.sortFileBasedQueue(queue); - for (const key in this.tiles) { + for (const tile of this.tiles.values()) { // Visible tiles' distance property will be set zero below by makeTileCurrent. - this.tiles[key].distanceFromView = Number.MAX_SAFE_INTEGER; + tile.distanceFromView = Number.MAX_SAFE_INTEGER; } this.beginTransaction(); for (i = 0; i < queue.length; i++) { - const tempTile = this.tiles[queue[i].key()]; + const tempTile = this.tiles.get(queue[i].key()); if (tempTile) this.makeTileCurrent(tempTile); } @@ -2420,7 +2422,7 @@ class TileManager { var tileCombineQueue = []; for (var i = 0; i < queue.length; i++) { var key = queue[i].key(); - var tile = this.tiles[key]; + let tile = this.tiles.get(key); if (!tile) tile = this.createTile(queue[i], key); if (tile.needsFetch()) tileCombineQueue.push(queue[i]); } @@ -2434,7 +2436,7 @@ class TileManager { // so we match this to a new incoming tile to unset // the invalid state later. public static invalidateTile(key: any, wireId: number) { - const tile: Tile = this.tiles[key]; + const tile: Tile = this.tiles.get(key); if (!tile) return; tile.invalidateCount++;