From b44bdc47f08daf092a4f60960872596ecf60aaa3 Mon Sep 17 00:00:00 2001 From: Nathan Date: Fri, 14 Mar 2025 08:50:38 -0700 Subject: [PATCH 1/5] track tile modification time, and add If-Modified-Since header to reload requests --- src/source/raster_tile_source.ts | 12 ++++++++++-- src/source/tile.ts | 5 +++++ src/util/ajax.ts | 6 ++++-- src/util/image_request.ts | 2 +- 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/source/raster_tile_source.ts b/src/source/raster_tile_source.ts index 7e4193e4d5e..2f96aa0092d 100644 --- a/src/source/raster_tile_source.ts +++ b/src/source/raster_tile_source.ts @@ -175,7 +175,15 @@ export class RasterTileSource extends Evented implements Source { const url = tile.tileID.canonical.url(this.tiles, this.map.getPixelRatio(), this.scheme); tile.abortController = new AbortController(); try { - const response = await ImageRequest.getImage(this.map._requestManager.transformRequest(url, ResourceType.Tile), tile.abortController, this.map._refreshExpiredTiles); + const requestParameters = this.map._requestManager.transformRequest(url, ResourceType.Tile); + if (tile.modificationTime) + { + if (!requestParameters.headers) { + requestParameters.headers = {}; + } + requestParameters.headers["If-Modified-Since"] = new Date(tile.modificationTime).toUTCString(); + } + const response = await ImageRequest.getImage(requestParameters, tile.abortController, this.map._refreshExpiredTiles); delete tile.abortController; if (tile.aborted) { tile.state = 'unloaded'; @@ -183,7 +191,7 @@ export class RasterTileSource extends Evented implements Source { } if (response && response.data) { if (this.map._refreshExpiredTiles && response.cacheControl && response.expires) { - tile.setExpiryData({cacheControl: response.cacheControl, expires: response.expires}); + tile.setExpiryData({cacheControl: response.cacheControl, expires: response.expires, lastModified: response.lastModified}); } const context = this.map.painter.context; const gl = context.gl; diff --git a/src/source/tile.ts b/src/source/tile.ts index ba183dc5cf6..5b8344418dc 100644 --- a/src/source/tile.ts +++ b/src/source/tile.ts @@ -63,6 +63,7 @@ export class Tile { glyphAtlasImage: AlphaImage; glyphAtlasTexture: Texture; expirationTime: any; + modificationTime: any; expiredRequestCount: number; state: TileState; timeAdded: number = 0; @@ -357,6 +358,10 @@ export class Tile { this.expirationTime = new Date(data.expires).getTime(); } + if (data.lastModified) { + this.modificationTime = new Date(data.lastModified).getTime(); + } + if (this.expirationTime) { const now = Date.now(); let isExpired = false; diff --git a/src/util/ajax.ts b/src/util/ajax.ts index 7b9671f8d8c..8c086e9c39e 100644 --- a/src/util/ajax.ts +++ b/src/util/ajax.ts @@ -11,7 +11,7 @@ export const GLOBAL_DISPATCHER_ID = 'global-dispatcher'; /** * A type used to store the tile's expiration date and cache control definition */ -export type ExpiryData = {cacheControl?: string | null; expires?: Date | string | null}; +export type ExpiryData = {cacheControl?: string | null; expires?: Date | string | null; lastModified?: Date | string | null}; /** * A `RequestParameters` object to be returned from Map.options.transformRequest callbacks. @@ -181,7 +181,9 @@ async function makeFetchRequest(requestParameters: RequestParameters, abortContr if (abortController.signal.aborted) { throw createAbortError(); } - return {data: result, cacheControl: response.headers.get('Cache-Control'), expires: response.headers.get('Expires')}; + return {data: result, cacheControl: response.headers.get('Cache-Control'), expires: response.headers.get('Expires'), + lastModified: response.headers.get('Last-Modified') + }; } function makeXMLHttpRequest(requestParameters: RequestParameters, abortController: AbortController): Promise> { diff --git a/src/util/image_request.ts b/src/util/image_request.ts index 434e8b29f05..767e58de65b 100644 --- a/src/util/image_request.ts +++ b/src/util/image_request.ts @@ -174,7 +174,7 @@ export namespace ImageRequest { onSuccess(response as GetResourceResponse); } else if (response.data) { const img = await arrayBufferToCanvasImageSource(response.data); - onSuccess({data: img, cacheControl: response.cacheControl, expires: response.expires}); + onSuccess({data: img, cacheControl: response.cacheControl, expires: response.expires, lastModified: response.lastModified}); } } catch (err) { delete itemInQueue.abortController; From d3dabc2fae0099c993cef101229ad9c7fdc63c4a Mon Sep 17 00:00:00 2001 From: Nathan Date: Fri, 14 Mar 2025 09:43:49 -0700 Subject: [PATCH 2/5] don't error on 304 --- src/util/ajax.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/ajax.ts b/src/util/ajax.ts index 8c086e9c39e..d18fd9408b0 100644 --- a/src/util/ajax.ts +++ b/src/util/ajax.ts @@ -165,7 +165,7 @@ async function makeFetchRequest(requestParameters: RequestParameters, abortContr throw new AJAXError(0, e.message, requestParameters.url, new Blob()); } - if (!response.ok) { + if (!response.ok && response.status != 304) { const body = await response.blob(); throw new AJAXError(response.status, response.statusText, requestParameters.url, body); } From 4b865d8ada89194543cd184524204d3dd3eb56bc Mon Sep 17 00:00:00 2001 From: Nathan Date: Fri, 14 Mar 2025 11:33:45 -0700 Subject: [PATCH 3/5] no success if image not received --- src/util/image_request.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/image_request.ts b/src/util/image_request.ts index 767e58de65b..698ce9085b7 100644 --- a/src/util/image_request.ts +++ b/src/util/image_request.ts @@ -172,7 +172,7 @@ export namespace ImageRequest { // User using addProtocol can directly return HTMLImageElement/ImageBitmap type // If HtmlImageElement is used to get image then response type will be HTMLImageElement onSuccess(response as GetResourceResponse); - } else if (response.data) { + } else if (response.data && response.data.byteLength > 0) { const img = await arrayBufferToCanvasImageSource(response.data); onSuccess({data: img, cacheControl: response.cacheControl, expires: response.expires, lastModified: response.lastModified}); } From d76b56c70594bc9d336117bfb6a680f6be7d2a45 Mon Sep 17 00:00:00 2001 From: Nathan Date: Fri, 14 Mar 2025 15:04:52 -0700 Subject: [PATCH 4/5] act like success if a tile is not reloaded because it is unmodified. --- src/source/raster_tile_source.ts | 22 ++++++++++++---------- src/util/image_request.ts | 10 +++++++--- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/source/raster_tile_source.ts b/src/source/raster_tile_source.ts index 2f96aa0092d..c04124399df 100644 --- a/src/source/raster_tile_source.ts +++ b/src/source/raster_tile_source.ts @@ -189,19 +189,21 @@ export class RasterTileSource extends Evented implements Source { tile.state = 'unloaded'; return; } - if (response && response.data) { + if (response) { if (this.map._refreshExpiredTiles && response.cacheControl && response.expires) { tile.setExpiryData({cacheControl: response.cacheControl, expires: response.expires, lastModified: response.lastModified}); } - const context = this.map.painter.context; - const gl = context.gl; - const img = response.data; - tile.texture = this.map.painter.getTileTexture(img.width); - if (tile.texture) { - tile.texture.update(img, {useMipmap: true}); - } else { - tile.texture = new Texture(context, img, gl.RGBA, {useMipmap: true}); - tile.texture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE, gl.LINEAR_MIPMAP_NEAREST); + if (response.data) { + const context = this.map.painter.context; + const gl = context.gl; + const img = response.data; + tile.texture = this.map.painter.getTileTexture(img.width); + if (tile.texture) { + tile.texture.update(img, {useMipmap: true}); + } else { + tile.texture = new Texture(context, img, gl.RGBA, {useMipmap: true}); + tile.texture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE, gl.LINEAR_MIPMAP_NEAREST); + } } tile.state = 'loaded'; } diff --git a/src/util/image_request.ts b/src/util/image_request.ts index 698ce9085b7..65d816f7cad 100644 --- a/src/util/image_request.ts +++ b/src/util/image_request.ts @@ -172,9 +172,13 @@ export namespace ImageRequest { // User using addProtocol can directly return HTMLImageElement/ImageBitmap type // If HtmlImageElement is used to get image then response type will be HTMLImageElement onSuccess(response as GetResourceResponse); - } else if (response.data && response.data.byteLength > 0) { - const img = await arrayBufferToCanvasImageSource(response.data); - onSuccess({data: img, cacheControl: response.cacheControl, expires: response.expires, lastModified: response.lastModified}); + } else if (response.data) { + if (response.data.byteLength > 0) { + const img = await arrayBufferToCanvasImageSource(response.data); + onSuccess({data: img, cacheControl: response.cacheControl, expires: response.expires, lastModified: response.lastModified}); + } else { + onSuccess({data: null, cacheControl: response.cacheControl, expires: response.expires, lastModified: response.lastModified}); + } } } catch (err) { delete itemInQueue.abortController; From 448d1bcf4f3608cc0b051a52e2f3b1c2bf3d934e Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 23 Apr 2025 16:00:00 -0700 Subject: [PATCH 5/5] formatting --- src/source/raster_tile_source.ts | 2 +- src/util/ajax.ts | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/source/raster_tile_source.ts b/src/source/raster_tile_source.ts index c04124399df..e09f6c3c300 100644 --- a/src/source/raster_tile_source.ts +++ b/src/source/raster_tile_source.ts @@ -181,7 +181,7 @@ export class RasterTileSource extends Evented implements Source { if (!requestParameters.headers) { requestParameters.headers = {}; } - requestParameters.headers["If-Modified-Since"] = new Date(tile.modificationTime).toUTCString(); + requestParameters.headers['If-Modified-Since'] = new Date(tile.modificationTime).toUTCString(); } const response = await ImageRequest.getImage(requestParameters, tile.abortController, this.map._refreshExpiredTiles); delete tile.abortController; diff --git a/src/util/ajax.ts b/src/util/ajax.ts index d18fd9408b0..6811fd44e7b 100644 --- a/src/util/ajax.ts +++ b/src/util/ajax.ts @@ -181,7 +181,10 @@ async function makeFetchRequest(requestParameters: RequestParameters, abortContr if (abortController.signal.aborted) { throw createAbortError(); } - return {data: result, cacheControl: response.headers.get('Cache-Control'), expires: response.headers.get('Expires'), + return { + data: result, + cacheControl: response.headers.get('Cache-Control'), + expires: response.headers.get('Expires'), lastModified: response.headers.get('Last-Modified') }; }