1

I've got an Openlayers map with a custom tileUrlFunction loading tiles from a custom tile server. Sometimes it can happen that the tileUrlFunction returns an adress that, deliberately, does not contain a tile. There can be two use cases for this:

  1. Empty tiles. That would be comparable to a completely blue tile in OpenStreetMap, which shows nothing but the sea.
  2. Default tiles. The tile would look exactly like another tile from some kind of default layer. For example, the map might have two raster tile layers, a default satellite view and a satellite view with certain structures highlighted. If a place has no highlighted structures, the tile from the highlighted view would look exactly like the one from the default view.

To reduce the number of files, my custom tile server does not contain such tiles, i. e. the paths simply do not exist. Whenever I try to load a tile from such an empty path, there is (of course) a tile error. I would like to use a fallback tile from another path. Depending on the adress of the tile that had failed to load, the new tile might use different paths.

Here's my solution so far:

var rasterLayer = new TileLayer({
        source: new XYZ({
          tileGrid: ... ,
          tileUrlFunction: ...,
        }),
        useInterimTilesOnError: false      
      });

// Error handling
rasterLayer_tiles.getSource().on("tileloaderror", errorFunction);
   
function errorFunction(e) {
      console.log("tile:", e.tile);                    // erroneous tile      
      console.log("src:", e.tile.src_);                // path that did not exist
      console.log("tileCoord:", e.tile.tileCoord);     // tile coordinates

      var tileCoord = e.tile.tileCoord;
      var replacement_Tile;
      if( ... ) {             // retrieve the path of fallback tile, potentially using information from e.tile
         // ...
         replacement_Tile = "errorTiles/blank.jpg";
       } else {
         // ...
         replacement_Tile = "defaultTiles/" + tileCoord[0] + "/" + tileCoord[1] + "/" + tileCoord[2] + ".jpg";
       }

      e.tile.src_ = replacement_Tile;
      e.tile.load();
}

This can be used to load the correct tiles; however there are several problems with that method:

  • The initial tile load error will still propagate to the console.
  • Apparently, there's no way to distinguish between errors where the path just didn't contain a tile and actual network errors. Possibly, this could also lead to an endless loop.
  • The solution relies on the private source attribute to be exposed (see also here).

Is there a more elegant solution? I am looking for some kind of customizable errorTileUrlFunction here.

1

1 Answer 1

4

An xhr based tileLoadFunction will give you access to the error code. You can then set the src to the objectUrl created from a successfully xhr (object urls should be revoked after use to release the memory) or an appropriate error tile, for example

tileLoadFunction: function(tile, src) {
  var xhr = new XMLHttpRequest();
  xhr.responseType = 'blob';
  xhr.addEventListener('loadend', function (evt) {
    var data = this.response;
    if (data !== undefined) {
      var objectUrl = URL.createObjectURL(data);
      tile.getImage().onload  = function(){
        URL.revokeObjectURL(objectUrl);
      }
      tile.getImage().src = objectUrl;
    } else if (xhr.status == 404) {
      tile.getImage().src = 'missing.png';
    } else {
      tile.getImage().src = 'error.png';
    }
  });
  xhr.addEventListener('error', function () {
    if (xhr.status == 404) {
      tile.getImage().src = 'missing.png';
    } else {
      tile.getImage().src = 'error.png';
    }
  });
  xhr.open('GET', src);
  xhr.send();
});

Try a different type of tile before resorting to a "missing" tile you would need a second attempt xhr

tileLoadFunction: function(tile, src) {
  function secondAttempt(tile, src) {
    var xhr = new XMLHttpRequest();
    xhr.responseType = 'blob';
    xhr.addEventListener('loadend', function (evt) {
      var data = this.response;
      if (data !== undefined) {
        var objectUrl = URL.createObjectURL(data);
        tile.getImage().onload  = function(){
          URL.revokeObjectURL(objectUrl);
        }
        tile.getImage().src = objectUrl;
      } else if (xhr.status == 404) {
        tile.getImage().src = 'missing.png';
      } else {
        tile.getImage().src = 'error.png';
      }
    });
    xhr.addEventListener('error', function () {
      if (xhr.status == 404) {
        tile.getImage().src = 'missing.png';
      } else {
        tile.getImage().src = 'error.png';
      }
    });
    xhr.open('GET', src);
    xhr.send();
  }
  var xhr = new XMLHttpRequest();
  xhr.responseType = 'blob';
  xhr.addEventListener('loadend', function (evt) {
    var data = this.response;
    if (data !== undefined) {
      var objectUrl = URL.createObjectURL(data);
      tile.getImage().onload  = function(){
        URL.revokeObjectURL(objectUrl);
      }
      tile.getImage().src = objectUrl;
    } else {
      secondAttempt(tile, src.replace('fancy', 'normal'));
    }
  });
  xhr.addEventListener('error', function () {
    secondAttempt(tile, src.replace('fancy', 'normal'));
  });
  xhr.open('GET', src);
  xhr.send();
});
6
  • Thank you for this answer! Unfortunately, using the code in the tileLoadFunction of the source, the error handling doesn't work for me. Normal tiles (including those missing) continue to be loaded. I've checked the tileLoadFunction is called correctly but for some reason, the error case is never reached. Whenever I check xhr.status, it is always 0, even if the 404 error propagates to the console. This problem occurs both when testing locally and testing on the server. Jun 14, 2021 at 13:11
  • 1
    I've updated the answer as the 404 status needs to be tested in the success callback, see stackoverflow.com/questions/30426277/catch-a-404-error-for-xhr
    – Mike
    Jun 14, 2021 at 14:17
  • A bit of a late follow-up question but I revisited this and came across a problem with it. For my use case, I have to do the two checks for missing and error tiles in sequence. For example, starting with fancyTiles/2/2/1.png. If the fancy tile does not exist, I move programmatically to normalTiles/2/2/1.png (this works). But if the normal tile is empty, I need to use missing.png instead. My problem here is that the XHR request is fired only once and the missing tile is never loaded. Could you give me a hint on how to adapt your code for this use case? @Mike Jan 11, 2022 at 9:27
  • 1
    You would need a follow-up xhr, similar to updated answer
    – Mike
    Jan 11, 2022 at 10:21
  • Thank you very much! Jan 11, 2022 at 12:09

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.