Use fetch for tile loads, move loading logic to LiveAtlasTileLayer
This commit is contained in:
parent
280e036276
commit
f316c0dd50
19
src/index.d.ts
vendored
19
src/index.d.ts
vendored
@ -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;
|
||||
}
|
||||
|
@ -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,75 +85,35 @@ export class DynmapTileLayer extends LiveAtlasTileLayer {
|
||||
}
|
||||
|
||||
private getTileUrlFromName(name: string, timestamp?: number) {
|
||||
let url = this._cachedTileUrls.get(name);
|
||||
|
||||
if (!url) {
|
||||
const path = escape(`${this._mapSettings.world.name}/${name}`);
|
||||
url = `${this._baseUrl}${path}`;
|
||||
let url = `${this._baseUrl}${path}`;
|
||||
|
||||
if(typeof timestamp !== 'undefined') {
|
||||
url += (url.indexOf('?') === -1 ? `?timestamp=${timestamp}` : `×tamp=${timestamp}`);
|
||||
}
|
||||
|
||||
this._cachedTileUrls.set(name, url);
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user