UNPKG

terriajs

Version:

Geospatial data visualization platform.

285 lines 13.7 kB
import i18next from "i18next"; import { action, runInAction } from "mobx"; import retry from "retry"; import formatError from "terriajs-cesium/Source/Core/formatError"; import Rectangle from "terriajs-cesium/Source/Core/Rectangle"; import Resource from "terriajs-cesium/Source/Core/Resource"; import TerriaError from "../Core/TerriaError"; import getUrlForImageryTile from "../Map/ImageryProvider/getUrlForImageryTile"; import CompositeCatalogItem from "../Models/Catalog/CatalogItems/CompositeCatalogItem"; import CommonStrata from "../Models/Definition/CommonStrata"; import DiscretelyTimeVaryingMixin from "./DiscretelyTimeVaryingMixin"; const LEAFLET_EMPTY_IMAGE_URL = "data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs="; /** * A mixin for handling tile errors in raster layers * */ function TileErrorHandlerMixin(Base) { class TileErrorHandlerMixin extends Base { tileFailures = 0; tileRetriesByMap = new Map(); tileRetryOptions = { retries: 8, minTimeout: 200, randomize: true }; /** * A hook that may be implemented by catalog items for custom handling of tile errors. * * @param maybeError A tile request promise that that fails with the tile error * @param tile The tile to be fetched * @returns A modified promise * * The item can then decide to retry the tile request after adding custom parameters * like an authentication token. */ handleTileError; get hasTileErrorHandlerMixin() { return true; } /* * Handling tile errors is really complicated because: * * 1) things go wrong for a variety of weird reasons including server * misconfiguration, servers that are flakey but not totally broken, * etc. * * 2) we want to fail as gracefully as possible, and give flakey servers * every chance chance to shine * * 3) we don't generally have enough information the first time something fails. * * There are several mechanisms in play here: * - Cesium's Resource automatically keeps trying to load any resource * that fails until told to stop, but tells us each time. * - The "retry" library knows how to pace the retries, and when to actually stop. */ onTileLoadError(tileProviderError) { const operation = retry.operation(this.tileRetryOptions); // Cesium's TileProviderError has a native Promise now, but the code // below is adapted from when it had a when.js promise. That's why it's // a bit unusual looking. // // result.reject = stop trying to load this tile // result.resolve = retry loading this tile let result; const promise = new Promise((resolve, reject) => { result = { resolve, reject }; }); const imageryProvider = tileProviderError.provider; const tile = { x: tileProviderError.x, y: tileProviderError.y, level: tileProviderError.level }; // We're only concerned about failures for tiles that actually overlap this item's extent. if (isTileOutsideExtent(tile, runInAction(() => this.cesiumRectangle), imageryProvider)) { tileProviderError.retry = false; return; } /** Helper methods **/ // Give up loading this (definitively, unexpectedly bad) tile and // possibly give up on this layer entirely. const failTile = action((e) => { this.tileFailures += 1; const opts = this.tileErrorHandlingOptions; const thresholdBeforeDisablingItem = opts.thresholdBeforeDisablingItem === undefined ? 5 : opts.thresholdBeforeDisablingItem; if (this.tileFailures > thresholdBeforeDisablingItem && this.show) { if (isThisItemABaseMap()) { this.terria.raiseErrorToUser(new TerriaError({ sender: this, title: i18next.t("models.imageryLayer.accessingBaseMapErrorTitle"), message: i18next.t("models.imageryLayer.accessingBaseMapErrorMessage", { name: this.name }) + "<pre>" + formatError(e) + "</pre>" })); } else { this.terria.raiseErrorToUser(new TerriaError({ sender: this, title: i18next.t("models.imageryLayer.accessingCatalogItemErrorTitle"), message: i18next.t("models.imageryLayer.accessingCatalogItemErrorMessage", { name: this.name }) + "<pre>" + formatError(e) + "</pre>" })); } this.setTrait(CommonStrata.user, "show", false); } operation.stop(); result.reject(); }); const tellMapToSilentlyGiveUp = () => { operation.stop(); result.reject(); }; const retryWithBackoff = (e) => { if (!operation.retry(e)) { failTile(e); } }; const tellMapToRetry = () => { operation.stop(); result.resolve(); }; const getTileKey = (tile) => { const time = DiscretelyTimeVaryingMixin.isMixedInto(this) ? this.currentTime : ""; const key = `L${tile.level}X${tile.x}Y${tile.y}${time}`; return key; }; const isThisItemABaseMap = () => { const baseMap = this.terria.mainViewer.baseMap; return this === baseMap ? true : baseMap instanceof CompositeCatalogItem ? baseMap.memberModels.includes(this) : false; }; /** End helper methods **/ // By setting retry to a promise, we tell cesium/leaflet to // reload the tile if the promise resolves successfully // https://github.com/TerriaJS/cesium/blob/terriajs/Source/Core/TileProviderError.js#L161 tileProviderError.retry = promise; if (tileProviderError.timesRetried === 0) { // There was an intervening success, so restart our count of the tile failures. this.tileFailures = 0; this.tileRetriesByMap.clear(); } operation.attempt(action(async (attemptNumber) => { if (this.show === false) { // If the layer is no longer shown, ignore errors and don't retry. tellMapToSilentlyGiveUp(); return; } const { ignoreUnknownTileErrors, treat403AsError, treat404AsError } = this.tileErrorHandlingOptions; // Browsers don't tell us much about a failed image load, so we do an // XHR to get more error information if needed. const maybeXhr = attemptNumber === 1 ? Promise.reject(tileProviderError.error) : fetchTileImage(tile, imageryProvider); if (this.handleTileError && maybeXhr) { // Give the catalog item a chance to handle this error. this.handleTileError(maybeXhr, tile); } try { await maybeXhr; const key = getTileKey(tile); const retriesByMap = this.tileRetriesByMap .set(key, (this.tileRetriesByMap.get(key) || 0) + 1) .get(key) || 0; if (retriesByMap > 5) { // Be careful: it's conceivable that a request here will always // succeed while a request made by the map will always fail, // e.g. as a result of different request headers. We must not get // stuck repeating the request forever in that scenario. Instead, // we should give up after a few attempts. failTile({ name: i18next.t("models.imageryLayer.tileErrorTitle"), message: i18next.t("models.imageryLayer.tileErrorMessage", { url: getUrlForImageryTile(imageryProvider, tileProviderError.x, tileProviderError.y, tileProviderError.level) }) }); } else { // Either: // - the XHR request for more information surprisingly worked // this time, let's hope the good luck continues when Cesium/Leaflet retries. // - the ImageryCatalogItem looked at the error and said we should try again. tellMapToRetry(); } } catch (error) { // This attempt failed. We'll either retry (for 500s) or give up // depending on the status code. const e = error || {}; if (e.statusCode === undefined) { if (runInAction(() => ignoreUnknownTileErrors)) { tellMapToSilentlyGiveUp(); } else if (e.target !== undefined) { // This is a failed image element, which means we got a 200 response but // could not load it as an image. // If image element is Leaflet's empty pixel ignore this error (See: https://github.com/Leaflet/Leaflet/issues/9311) if (e.target.src === LEAFLET_EMPTY_IMAGE_URL) { tellMapToSilentlyGiveUp(); return; } failTile({ name: i18next.t("models.imageryLayer.tileErrorTitle"), message: i18next.t("models.imageryLayer.tileErrorMessageII", { url: getUrlForImageryTile(imageryProvider, tile.x, tile.y, tile.level) }) }); } else { // Unknown error failTile({ name: i18next.t("models.imageryLayer.unknownTileErrorTitle"), message: i18next.t("models.imageryLayer.unknownTileErrorMessage", { url: getUrlForImageryTile(imageryProvider, tile.x, tile.y, tile.level) }) }); } } else if (e.statusCode >= 400 && e.statusCode < 500) { if (e.statusCode === 403 && treat403AsError === false) { tellMapToSilentlyGiveUp(); } else if (e.statusCode === 404 && treat404AsError === false) { tellMapToSilentlyGiveUp(); } else { failTile(e); } } else if (e.statusCode >= 500 && e.statusCode < 600) { retryWithBackoff(e); } else { failTile(e); } } })); } } /** * Trying fetching image using an XHR request */ function fetchTileImage(tile, imageryProvider) { const tileUrl = getUrlForImageryTile(imageryProvider, tile.x, tile.y, tile.level); if (tileUrl) { return Resource.fetchImage({ url: tileUrl, preferBlob: true }); } return Promise.reject(); } function isTileOutsideExtent(tile, rectangle, imageryProvider) { if (rectangle === undefined) { // If the rectangle is not defined, assume the tile is inside. return false; } const tilingScheme = imageryProvider.tilingScheme; const tileExtent = tilingScheme.tileXYToRectangle(tile.x, tile.y, tile.level); const intersection = Rectangle.intersection(tileExtent, rectangle); return intersection === undefined; } return TileErrorHandlerMixin; } (function (TileErrorHandlerMixin) { function isMixedInto(model) { return model?.hasTileErrorHandlerMixin; } TileErrorHandlerMixin.isMixedInto = isMixedInto; })(TileErrorHandlerMixin || (TileErrorHandlerMixin = {})); export default TileErrorHandlerMixin; //# sourceMappingURL=TileErrorHandlerMixin.js.map