diff --git a/src/source/raster_tile_source.ts b/src/source/raster_tile_source.ts index 7e4193e4d5e..e09f6c3c300 100644 --- a/src/source/raster_tile_source.ts +++ b/src/source/raster_tile_source.ts @@ -175,25 +175,35 @@ 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'; return; } - if (response && response.data) { + if (response) { 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; - 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/source/tile.ts b/src/source/tile.ts index f7445db6df6..9796bca9b95 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; @@ -359,6 +360,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..6811fd44e7b 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. @@ -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); } @@ -181,7 +181,12 @@ 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..65d816f7cad 100644 --- a/src/util/image_request.ts +++ b/src/util/image_request.ts @@ -173,8 +173,12 @@ export namespace ImageRequest { // If HtmlImageElement is used to get image then response type will be HTMLImageElement onSuccess(response as GetResourceResponse); } else if (response.data) { - const img = await arrayBufferToCanvasImageSource(response.data); - onSuccess({data: img, cacheControl: response.cacheControl, expires: response.expires}); + 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;