Use fetch for tile loads, move loading logic to LiveAtlasTileLayer

This commit is contained in:
James Lyne 2021-08-14 00:08:01 +01:00
parent 280e036276
commit f316c0dd50
3 changed files with 181 additions and 101 deletions

19
src/index.d.ts vendored
View File

@ -17,7 +17,7 @@
import {State} from "@/store";
import {DynmapUrlConfig} from "@/dynmap";
import LiveAtlasMapDefinition from "@/model/LiveAtlasMapDefinition";
import {PathOptions, PointTuple, PolylineOptions} from "leaflet";
import {Coords, DoneCallback, PathOptions, PointTuple, PolylineOptions} from "leaflet";
import {CoordinatesControlOptions} from "@/leaflet/control/CoordinatesControl";
import {ClockControlOptions} from "@/leaflet/control/ClockControl";
import {LogoControlOptions} from "@/leaflet/control/LogoControl";
@ -313,3 +313,20 @@ interface LiveAtlasChat {
source?: string;
timestamp: number;
}
export interface LiveAtlasTile {
active?: boolean;
coords: Coords;
current: boolean;
el: LiveAtlasTileElement;
loaded?: Date;
retain?: boolean;
complete: boolean;
}
export interface LiveAtlasTileElement extends HTMLImageElement {
tileName?: string;
url: string;
callback: DoneCallback;
abortController: AbortController;
}

View File

@ -17,29 +17,15 @@
* limitations under the License.
*/
import {Coords, DoneCallback, DomUtil} from 'leaflet';
import {Coords, DoneCallback} from 'leaflet';
import {useStore} from "@/store";
import {Coordinate} from "@/index";
import {LiveAtlasTileLayerOptions, LiveAtlasTileLayer} from "@/leaflet/tileLayer/LiveAtlasTileLayer";
import {Coordinate, LiveAtlasTile} from "@/index";
import {LiveAtlasTileLayer, LiveAtlasTileLayerOptions} from "@/leaflet/tileLayer/LiveAtlasTileLayer";
import {computed, watch} from "@vue/runtime-core";
import {ComputedRef} from "@vue/reactivity";
import {WatchStopHandle} from "vue";
import {ActionTypes} from "@/store/action-types";
export interface DynmapTile {
active?: boolean;
coords: Coords;
current: boolean;
el: DynmapTileElement;
loaded?: Date;
retain?: boolean;
complete: boolean;
}
export interface DynmapTileElement extends HTMLImageElement {
tileName: string;
}
export interface TileInfo {
prefix: string;
nightday: string;
@ -56,11 +42,7 @@ const store = useStore();
// noinspection JSUnusedGlobalSymbols
export class DynmapTileLayer extends LiveAtlasTileLayer {
private readonly _cachedTileUrls: Map<any, any> = Object.seal(new Map());
private readonly _namedTiles: Map<any, any> = Object.seal(new Map());
private readonly _loadQueue: DynmapTileElement[] = [];
private readonly _loadingTiles: Set<DynmapTileElement> = Object.seal(new Set());
private readonly _tileTemplate: DynmapTileElement;
private readonly _baseUrl: string;
private readonly _night: ComputedRef<boolean>;
@ -69,26 +51,12 @@ export class DynmapTileLayer extends LiveAtlasTileLayer {
private readonly _updateUnwatch: WatchStopHandle;
private _updateFrame: number = 0;
// @ts-ignore
declare options: DynmapTileLayerOptions;
constructor(options: LiveAtlasTileLayerOptions) {
super('', options);
this._mapSettings = options.mapSettings;
this._tileTemplate = DomUtil.create('img', 'leaflet-tile') as DynmapTileElement;
this._tileTemplate.style.width = this._tileTemplate.style.height = this.options.tileSize + 'px';
this._tileTemplate.alt = '';
this._tileTemplate.tileName = '';
this._tileTemplate.setAttribute('role', 'presentation');
this._baseUrl = store.state.currentMapProvider!.getTilesUrl();
Object.seal(this._tileTemplate);
if(this.options.crossOrigin || this.options.crossOrigin === '') {
this._tileTemplate.crossOrigin = this.options.crossOrigin === true ? '' : this.options.crossOrigin;
}
this._pendingUpdates = computed(() => !!store.state.pendingTileUpdates.length);
this._updateUnwatch = watch(this._pendingUpdates, (newValue, oldValue) => {
if(newValue && !oldValue && !this._updateFrame) {
@ -117,17 +85,11 @@ export class DynmapTileLayer extends LiveAtlasTileLayer {
}
private getTileUrlFromName(name: string, timestamp?: number) {
let url = this._cachedTileUrls.get(name);
const path = escape(`${this._mapSettings.world.name}/${name}`);
let url = `${this._baseUrl}${path}`;
if (!url) {
const path = escape(`${this._mapSettings.world.name}/${name}`);
url = `${this._baseUrl}${path}`;
if(typeof timestamp !== 'undefined') {
url += (url.indexOf('?') === -1 ? `?timestamp=${timestamp}` : `&timestamp=${timestamp}`);
}
this._cachedTileUrls.set(name, url);
if(typeof timestamp !== 'undefined') {
url += (url.indexOf('?') === -1 ? `?timestamp=${timestamp}` : `&timestamp=${timestamp}`);
}
return url;
@ -135,57 +97,23 @@ export class DynmapTileLayer extends LiveAtlasTileLayer {
private updateNamedTile(name: string, timestamp: number) {
const tile = this._namedTiles.get(name);
this._cachedTileUrls.delete(name);
if (tile) {
tile.dataset.src = this.getTileUrlFromName(name, timestamp);
this._loadQueue.push(tile);
this._tickLoadQueue();
this.loadQueue.push(tile);
this.tickLoadQueue();
}
}
createTile(coords: Coords, done: DoneCallback) {
//Clone template image instead of creating a new one
const tile = this._tileTemplate.cloneNode(false) as DynmapTileElement;
const tile = super.createTile(coords, done);
tile.tileName = this.getTileName(coords);
tile.dataset.src = this.getTileUrl(coords);
this._namedTiles.set(tile.tileName, tile);
this._loadQueue.push(tile);
//Use addEventListener here
tile.onload = () => {
this._tileOnLoad(done, tile);
this._loadingTiles.delete(tile);
this._tickLoadQueue();
};
tile.onerror = () => {
this._tileOnError(done, tile, {name: 'Error', message: 'Error'});
this._loadingTiles.delete(tile);
this._tickLoadQueue();
};
this._tickLoadQueue();
return tile;
}
_tickLoadQueue() {
if (this._loadingTiles.size > 6) {
return;
}
const tile = this._loadQueue.shift();
if (!tile) {
return;
}
this._loadingTiles.add(tile);
tile.src = tile.dataset.src as string;
}
// stops loading all tiles in the background layer
_abortLoading() {
let tile;
@ -195,18 +123,12 @@ export class DynmapTileLayer extends LiveAtlasTileLayer {
continue;
}
tile = this._tiles[i] as DynmapTile;
tile = this._tiles[i] as LiveAtlasTile;
if (tile.coords.z !== this._tileZoom) {
if (tile.loaded && tile.el && tile.el.tileName) {
this._namedTiles.delete(tile.el.tileName);
}
if(this._loadQueue.includes(tile.el)) {
this._loadQueue.splice(this._loadQueue.indexOf(tile.el), 1);
}
this._loadingTiles.delete(tile.el);
}
}
@ -214,7 +136,7 @@ export class DynmapTileLayer extends LiveAtlasTileLayer {
}
_removeTile(key: string) {
const tile = this._tiles[key] as DynmapTile;
const tile = this._tiles[key] as LiveAtlasTile;
if (!tile) {
return;
@ -224,15 +146,6 @@ export class DynmapTileLayer extends LiveAtlasTileLayer {
if (tileName) {
this._namedTiles.delete(tileName);
this._cachedTileUrls.delete(tileName);
this._loadingTiles.delete(tile.el);
if(this._loadQueue.includes(tile.el)) {
this._loadQueue.splice(this._loadQueue.indexOf(tile.el), 1);
}
tile.el.onerror = null;
tile.el.onload = null;
}
// @ts-ignore

View File

@ -14,8 +14,10 @@
* limitations under the License.
*/
import {TileLayer, TileLayerOptions, Util} from 'leaflet';
import LiveAtlasMapDefinition from "@/model/LiveAtlasMapDefinition";
import {Coords, DomUtil, DoneCallback, TileLayer, TileLayerOptions, Util} from "leaflet";
import {LiveAtlasTile, LiveAtlasTileElement} from "@/index";
import falseFn = Util.falseFn;
export interface LiveAtlasTileLayerOptions extends TileLayerOptions {
mapSettings: LiveAtlasMapDefinition;
@ -27,10 +29,29 @@ export abstract class LiveAtlasTileLayer extends TileLayer {
protected _mapSettings: LiveAtlasMapDefinition;
declare options: LiveAtlasTileLayerOptions;
private readonly tileTemplate: LiveAtlasTileElement;
protected readonly loadQueue: LiveAtlasTileElement[] = [];
private readonly loadingTiles: Set<LiveAtlasTileElement> = Object.seal(new Set());
private static genericLoadError = new Error('Tile failed to load');
protected constructor(url: string, options: LiveAtlasTileLayerOptions) {
super(url, options);
this._mapSettings = options.mapSettings;
this.tileTemplate = DomUtil.create('img', 'leaflet-tile') as LiveAtlasTileElement;
this.tileTemplate.style.width = this.tileTemplate.style.height = this.options.tileSize + 'px';
this.tileTemplate.alt = '';
this.tileTemplate.tileName = '';
this.tileTemplate.callback = falseFn;
this.tileTemplate.setAttribute('role', 'presentation');
if(this.options.crossOrigin || this.options.crossOrigin === '') {
this.tileTemplate.crossOrigin = this.options.crossOrigin === true ? '' : this.options.crossOrigin;
}
Object.seal(this.tileTemplate);
options.maxZoom = this._mapSettings.nativeZoomLevels + this._mapSettings.extraZoomLevels;
options.maxNativeZoom = this._mapSettings.nativeZoomLevels;
options.zoomReverse = true;
@ -43,4 +64,133 @@ export abstract class LiveAtlasTileLayer extends TileLayer {
throw new TypeError("mapSettings missing");
}
}
// @method createTile(coords: Object, done?: Function): HTMLElement
// Called only internally, overrides GridLayer's [`createTile()`](#gridlayer-createtile)
// to return an `<img>` HTML element with the appropriate image URL given `coords`. The `done`
// callback is called when the tile has been loaded.
createTile(coords: Coords, done: DoneCallback) {
const tile = this.tileTemplate.cloneNode(false) as LiveAtlasTileElement;
this.loadQueue.push(tile);
tile.onload = () => {
URL.revokeObjectURL(tile.src); //Revoke the object URL as we don't need it anymore
this._tileOnLoad(done, tile);
this.loadingTiles.delete(tile);
this.tickLoadQueue();
};
tile.onerror = () => {
this._tileOnError(done, tile, LiveAtlasTileLayer.genericLoadError);
this.loadingTiles.delete(tile);
this.tickLoadQueue();
};
tile.url = this.getTileUrl(coords);
tile.callback = done;
this.tickLoadQueue();
return tile;
}
private async fetchTile(tile: LiveAtlasTileElement) {
if(tile.abortController && !tile.abortController.signal.aborted) {
tile.abortController.abort();
}
tile.abortController = new AbortController();
try {
//Retrieve image via a fetch instead of just setting the src
//This works around the fact that browsers usually don't make a request for an image that was previously loaded,
//without resorting to changing the URL (which would break caching).
const response = await fetch(tile.url, {signal: tile.abortController.signal});
//Call leaflet's error handler if request fails for some reason
if (!response.ok) {
this._tileOnError(tile.callback, tile, new Error('Response was not ok'));
return;
}
//Get image data and convert into object URL so it can be used as a src
//The tile onload listener will take it from here
const blob = await response.blob();
tile.src = URL.createObjectURL(blob);
} catch(e) {
if (e instanceof DOMException && e.name === 'AbortError') {
return;
}
console.error(e);
this._tileOnError(tile.callback, tile, e);
}
}
protected tickLoadQueue() {
if (this.loadingTiles.size > 6) {
return;
}
const tile = this.loadQueue.shift();
if (!tile) {
return;
}
this.loadingTiles.add(tile);
this.fetchTile(tile);
}
_abortLoading() {
let tile;
for (const i in this._tiles) {
if (!Object.prototype.hasOwnProperty.call(this._tiles, i)) {
continue;
}
tile = this._tiles[i] as LiveAtlasTile;
if (tile.coords.z !== this._tileZoom) {
if (!tile.loaded && tile.el && tile.el.abortController) {
tile.el.abortController.abort();
}
if(this.loadQueue.includes(tile.el)) {
this.loadQueue.splice(this.loadQueue.indexOf(tile.el), 1);
}
this.loadingTiles.delete(tile.el);
}
}
super._abortLoading.call(this);
}
_removeTile(key: string) {
const tile = this._tiles[key] as LiveAtlasTile;
if (!tile) {
return;
}
this.loadingTiles.delete(tile.el);
if(this.loadQueue.includes(tile.el)) {
this.loadQueue.splice(this.loadQueue.indexOf(tile.el), 1);
}
tile.el.onerror = null;
tile.el.onload = null;
if(!tile.loaded && tile.el.abortController) {
tile.el.abortController.abort();
}
// @ts-ignore
super._removeTile(key);
}
}