terriajs
Version:
Geospatial data visualization platform.
285 lines • 13.7 kB
JavaScript
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