Skip to content

Commit

Permalink
attempting to fix zoom flickering #147. Added abortController to Tile…
Browse files Browse the repository at this point in the history
…dGrid in order to cancel ongoing requests when zoom level changes
  • Loading branch information
joewdavies committed Jan 8, 2025
1 parent a55515c commit 31d4568
Show file tree
Hide file tree
Showing 4 changed files with 188 additions and 99 deletions.
145 changes: 95 additions & 50 deletions dist/gridviz.js

Large diffs are not rendered by default.

69 changes: 41 additions & 28 deletions src/core/GeoCanvas.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,12 @@ export class GeoCanvas {
this.extGeo = { xMin: NaN, xMax: NaN, yMin: NaN, yMax: NaN }
this.updateExtentGeo()

//rely on d3 zoom for pan/zoom
//rely on d3 for zoom
if (!opts.disableZoom) {
let tP = zoomIdentity
let debounceTimeout = null // Add a debounce timeout variable
const z = d3zoom()
//to make the zooming a bit faster
// to make the zooming a bit faster
.wheelDelta((e) => -e.deltaY * (e.deltaMode === 1 ? 0.07 : e.deltaMode ? 1 : 0.004))
.on('zoom', (e) => {
const t = e.transform
Expand All @@ -101,31 +102,8 @@ export class GeoCanvas {
const dy = tP.y - t.y
this.pan(dx * this.view.z, -dy * this.view.z)
} else {
const se = e.sourceEvent

if (se instanceof WheelEvent) {
// Zoom at mouse position, adjusted by container offset
this.zoom(f, this.pixToGeoX(offsetX), this.pixToGeoY(offsetY))
} else if (se instanceof TouchEvent) {
if (!se.targetTouches.length) return

// Compute average position of the touches
let tx = 0,
ty = 0
for (let tt of se.targetTouches) {
tx += tt.clientX
ty += tt.clientY
}
tx /= se.targetTouches.length
ty /= se.targetTouches.length

// Adjust for container's offset
tx -= containerRect.left
ty -= containerRect.top

// Zoom at the average touch position
this.zoom(f, this.pixToGeoX(tx), this.pixToGeoY(ty))
}
// Throttling the zoom
handleZoom(e.sourceEvent, containerRect, f, offsetX, offsetY)
}
tP = t

Expand All @@ -144,13 +122,43 @@ export class GeoCanvas {
if (this.onZoomStartFun) this.onZoomStartFun(e)
})
.on('end', (e) => {
// end of zoom event
// end of pan/zoom event
//console.log('zoom redraw')
this.redraw()
this.canvasSave = { c: null, dx: 0, dy: 0, f: 1 }

if (this.onZoomEndFun) this.onZoomEndFun(e)
})
z(select(this.canvas))

const handleZoom = (se, containerRect, f, offsetX, offsetY) => {
// cancel ongoing data requests
this.cancelCurrentRequests()

if (se instanceof WheelEvent) {
// Zoom at mouse position, adjusted by container offset
this.zoom(f, this.pixToGeoX(offsetX), this.pixToGeoY(offsetY))
} else if (se instanceof TouchEvent) {
if (!se.targetTouches.length) return

// Compute average position of the touches
let tx = 0,
ty = 0
for (let tt of se.targetTouches) {
tx += tt.clientX
ty += tt.clientY
}
tx /= se.targetTouches.length
ty /= se.targetTouches.length

// Adjust for container's offset
tx -= containerRect.left
ty -= containerRect.top

// Zoom at the average touch position
this.zoom(f, this.pixToGeoX(tx), this.pixToGeoY(ty))
}
}
}

//center extent
Expand Down Expand Up @@ -225,6 +233,11 @@ export class GeoCanvas {
throw new Error('Method redraw not implemented.')
}

/** When the zoom level changes, ensures that any ongoing requests are aborted before new ones are initiated. */
cancelCurrentRequests() {
throw new Error('Method cancelCurrentRequests not implemented.')
}

/**
* Clear. To be used before a redraw for example.
* @param {string} color
Expand Down
13 changes: 13 additions & 0 deletions src/core/Map.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,19 @@ export class Map {
this.geoCanvas.redraw = () => {
this.redraw()
}
this.geoCanvas.cancelCurrentRequests = () => {
// when the zoom level changes, avoid drawing outdated tiles, and ensure that requests are properly aborted when necessary
for (const layer of this.layers) {
//multires
if (layer.dataset?.datasets) {
for (const dataset of layer.dataset?.datasets) {
if (dataset?.cancelCurrentRequests) dataset.cancelCurrentRequests()
}
}
//single res
if (layer.dataset?.cancelCurrentRequests) layer.dataset?.cancelCurrentRequests()
}
}

// legend div
this.legend = opts.legendContainer
Expand Down
60 changes: 39 additions & 21 deletions src/dataset/TiledGrid.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,7 @@ export class TiledGrid extends Dataset {
this.infoLoadingStatus = 'failed'
}
})()
} else if (this.infoLoadingStatus === 'loaded' || this.infoLoadingStatus === 'failed')
this.map.redraw()
} else if (this.infoLoadingStatus === 'loaded' || this.infoLoadingStatus === 'failed') this.map.redraw()
return this
}

Expand Down Expand Up @@ -106,6 +105,14 @@ export class TiledGrid extends Dataset {
async getData(extGeo) {
if (!this.info) return this

// Capture the current zoom level
const currentZoom = this.map.geoCanvas.view.z
this.currentZoomLevel = currentZoom

// Create an AbortController for the current data request
this.abortController = new AbortController()
const signal = this.abortController.signal

// Get the tiling envelope and check bounds
const tb = this.getTilingEnvelope(extGeo)
if (!tb) return this
Expand All @@ -117,72 +124,83 @@ export class TiledGrid extends Dataset {
const yMin = Math.max(tb.yMin, gbYMin)
const yMax = Math.min(tb.yMax, gbYMax)

// Count the total number of tiles
const totalTiles = (xMax - xMin + 1) * (yMax - yMin + 1)

let processedTiles = 0 // Counter for loaded tiles
let processedTiles = 0
const tilePromises = []

// Iterate over tiles within bounds
for (let xT = xMin; xT <= xMax; xT++) {
for (let yT = yMin; yT <= yMax; yT++) {
if (!this.cache[xT]) this.cache[xT] = {}

// Skip already loaded or loading tiles
if (this.cache[xT][yT]) {
// Skip already loaded tiles or retry failed ones
if (this.cache[xT][yT] && this.cache[xT][yT] !== 'failed') {
++processedTiles
continue
}

// Mark tile as loading
this.cache[xT][yT] = 'loading'

// Prepare the tile request and store it in tilePromises
tilePromises.push(
this.loadTile(xT, yT)
this.loadTile(xT, yT, currentZoom, signal)
.then((tile) => {
// Before we increment processedTiles, check if it's the last tile
const isLastTile = ++processedTiles === totalTiles

// Store the tile in the cache
this.cache[xT][yT] = tile

// Pass the isLastTile argument to checkAndRedraw
// Check if this is the last tile
const isLastTile = ++processedTiles === totalTiles
this.checkAndRedraw(tile, isLastTile)
})
.catch(() => {
// Mark as failed but allow processing to continue
this.cache[xT][yT] = 'failed'
++processedTiles
})
)
}
}

// Handle all tile promises, including failures
await Promise.allSettled(tilePromises)
return this
}

async loadTile(xT, yT) {
/**
* Load a tile.
*
* @param {number} xT
* @param {number} yT
* @param {number} requestZoom
* @param {AbortSignal} signal
* @returns {Promise<any>}
*/
async loadTile(xT, yT, requestZoom, signal) {
try {
const data = await csv(`${this.url}${xT}/${yT}.csv`)
const data = await csv(`${this.url}${xT}/${yT}.csv`, { signal })

// Preprocess or filter the data if needed
const cells = this.preprocess ? data.filter((cell) => this.preprocess(cell) !== false) : data

if (!this.info) throw new Error('Tile info unknown')

return getGridTile(cells, xT, yT, this.info)
} catch (error) {
console.warn(`Failed to load tile ${xT}, ${yT}:`, error)
if (error.name === 'AbortError') {
console.warn(`Tile request for ${xT}, ${yT} was aborted.`)
}
throw error
}
}

/**
* Cancel ongoing data requests when zoom level changes.
*/
cancelCurrentRequests() {
if (this.abortController) {
this.abortController.abort()
}
}

checkAndRedraw(tile, isLastTile) {
// Check if any visible layer depends on this dataset
//check if redraw is really needed, that is if:
// check if redraw is really needed, that is if:
// 1. the dataset belongs to a layer which is visible at the current zoom level
let needsRedraw = false
//go through the layers
Expand Down

0 comments on commit 31d4568

Please sign in to comment.