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 {State} from "@/store";
|
||||||
import {DynmapUrlConfig} from "@/dynmap";
|
import {DynmapUrlConfig} from "@/dynmap";
|
||||||
import LiveAtlasMapDefinition from "@/model/LiveAtlasMapDefinition";
|
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 {CoordinatesControlOptions} from "@/leaflet/control/CoordinatesControl";
|
||||||
import {ClockControlOptions} from "@/leaflet/control/ClockControl";
|
import {ClockControlOptions} from "@/leaflet/control/ClockControl";
|
||||||
import {LogoControlOptions} from "@/leaflet/control/LogoControl";
|
import {LogoControlOptions} from "@/leaflet/control/LogoControl";
|
||||||
@ -313,3 +313,20 @@ interface LiveAtlasChat {
|
|||||||
source?: string;
|
source?: string;
|
||||||
timestamp: number;
|
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.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {Coords, DoneCallback, DomUtil} from 'leaflet';
|
import {Coords, DoneCallback} from 'leaflet';
|
||||||
import {useStore} from "@/store";
|
import {useStore} from "@/store";
|
||||||
import {Coordinate} from "@/index";
|
import {Coordinate, LiveAtlasTile} from "@/index";
|
||||||
import {LiveAtlasTileLayerOptions, LiveAtlasTileLayer} from "@/leaflet/tileLayer/LiveAtlasTileLayer";
|
import {LiveAtlasTileLayer, LiveAtlasTileLayerOptions} from "@/leaflet/tileLayer/LiveAtlasTileLayer";
|
||||||
import {computed, watch} from "@vue/runtime-core";
|
import {computed, watch} from "@vue/runtime-core";
|
||||||
import {ComputedRef} from "@vue/reactivity";
|
import {ComputedRef} from "@vue/reactivity";
|
||||||
import {WatchStopHandle} from "vue";
|
import {WatchStopHandle} from "vue";
|
||||||
import {ActionTypes} from "@/store/action-types";
|
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 {
|
export interface TileInfo {
|
||||||
prefix: string;
|
prefix: string;
|
||||||
nightday: string;
|
nightday: string;
|
||||||
@ -56,11 +42,7 @@ const store = useStore();
|
|||||||
|
|
||||||
// noinspection JSUnusedGlobalSymbols
|
// noinspection JSUnusedGlobalSymbols
|
||||||
export class DynmapTileLayer extends LiveAtlasTileLayer {
|
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 _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 _baseUrl: string;
|
||||||
|
|
||||||
private readonly _night: ComputedRef<boolean>;
|
private readonly _night: ComputedRef<boolean>;
|
||||||
@ -69,26 +51,12 @@ export class DynmapTileLayer extends LiveAtlasTileLayer {
|
|||||||
private readonly _updateUnwatch: WatchStopHandle;
|
private readonly _updateUnwatch: WatchStopHandle;
|
||||||
private _updateFrame: number = 0;
|
private _updateFrame: number = 0;
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
declare options: DynmapTileLayerOptions;
|
|
||||||
|
|
||||||
constructor(options: LiveAtlasTileLayerOptions) {
|
constructor(options: LiveAtlasTileLayerOptions) {
|
||||||
super('', options);
|
super('', options);
|
||||||
|
|
||||||
this._mapSettings = options.mapSettings;
|
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();
|
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._pendingUpdates = computed(() => !!store.state.pendingTileUpdates.length);
|
||||||
this._updateUnwatch = watch(this._pendingUpdates, (newValue, oldValue) => {
|
this._updateUnwatch = watch(this._pendingUpdates, (newValue, oldValue) => {
|
||||||
if(newValue && !oldValue && !this._updateFrame) {
|
if(newValue && !oldValue && !this._updateFrame) {
|
||||||
@ -117,75 +85,35 @@ export class DynmapTileLayer extends LiveAtlasTileLayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getTileUrlFromName(name: string, timestamp?: number) {
|
private getTileUrlFromName(name: string, timestamp?: number) {
|
||||||
let url = this._cachedTileUrls.get(name);
|
|
||||||
|
|
||||||
if (!url) {
|
|
||||||
const path = escape(`${this._mapSettings.world.name}/${name}`);
|
const path = escape(`${this._mapSettings.world.name}/${name}`);
|
||||||
url = `${this._baseUrl}${path}`;
|
let url = `${this._baseUrl}${path}`;
|
||||||
|
|
||||||
if(typeof timestamp !== 'undefined') {
|
if(typeof timestamp !== 'undefined') {
|
||||||
url += (url.indexOf('?') === -1 ? `?timestamp=${timestamp}` : `×tamp=${timestamp}`);
|
url += (url.indexOf('?') === -1 ? `?timestamp=${timestamp}` : `×tamp=${timestamp}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._cachedTileUrls.set(name, url);
|
|
||||||
}
|
|
||||||
|
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateNamedTile(name: string, timestamp: number) {
|
private updateNamedTile(name: string, timestamp: number) {
|
||||||
const tile = this._namedTiles.get(name);
|
const tile = this._namedTiles.get(name);
|
||||||
this._cachedTileUrls.delete(name);
|
|
||||||
|
|
||||||
if (tile) {
|
if (tile) {
|
||||||
tile.dataset.src = this.getTileUrlFromName(name, timestamp);
|
tile.dataset.src = this.getTileUrlFromName(name, timestamp);
|
||||||
this._loadQueue.push(tile);
|
this.loadQueue.push(tile);
|
||||||
this._tickLoadQueue();
|
this.tickLoadQueue();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
createTile(coords: Coords, done: DoneCallback) {
|
createTile(coords: Coords, done: DoneCallback) {
|
||||||
//Clone template image instead of creating a new one
|
const tile = super.createTile(coords, done);
|
||||||
const tile = this._tileTemplate.cloneNode(false) as DynmapTileElement;
|
|
||||||
|
|
||||||
tile.tileName = this.getTileName(coords);
|
tile.tileName = this.getTileName(coords);
|
||||||
tile.dataset.src = this.getTileUrl(coords);
|
|
||||||
|
|
||||||
this._namedTiles.set(tile.tileName, tile);
|
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;
|
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
|
// stops loading all tiles in the background layer
|
||||||
_abortLoading() {
|
_abortLoading() {
|
||||||
let tile;
|
let tile;
|
||||||
@ -195,18 +123,12 @@ export class DynmapTileLayer extends LiveAtlasTileLayer {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
tile = this._tiles[i] as DynmapTile;
|
tile = this._tiles[i] as LiveAtlasTile;
|
||||||
|
|
||||||
if (tile.coords.z !== this._tileZoom) {
|
if (tile.coords.z !== this._tileZoom) {
|
||||||
if (tile.loaded && tile.el && tile.el.tileName) {
|
if (tile.loaded && tile.el && tile.el.tileName) {
|
||||||
this._namedTiles.delete(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) {
|
_removeTile(key: string) {
|
||||||
const tile = this._tiles[key] as DynmapTile;
|
const tile = this._tiles[key] as LiveAtlasTile;
|
||||||
|
|
||||||
if (!tile) {
|
if (!tile) {
|
||||||
return;
|
return;
|
||||||
@ -224,15 +146,6 @@ export class DynmapTileLayer extends LiveAtlasTileLayer {
|
|||||||
|
|
||||||
if (tileName) {
|
if (tileName) {
|
||||||
this._namedTiles.delete(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
|
// @ts-ignore
|
||||||
|
@ -14,8 +14,10 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {TileLayer, TileLayerOptions, Util} from 'leaflet';
|
|
||||||
import LiveAtlasMapDefinition from "@/model/LiveAtlasMapDefinition";
|
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 {
|
export interface LiveAtlasTileLayerOptions extends TileLayerOptions {
|
||||||
mapSettings: LiveAtlasMapDefinition;
|
mapSettings: LiveAtlasMapDefinition;
|
||||||
@ -27,10 +29,29 @@ export abstract class LiveAtlasTileLayer extends TileLayer {
|
|||||||
protected _mapSettings: LiveAtlasMapDefinition;
|
protected _mapSettings: LiveAtlasMapDefinition;
|
||||||
declare options: LiveAtlasTileLayerOptions;
|
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) {
|
protected constructor(url: string, options: LiveAtlasTileLayerOptions) {
|
||||||
super(url, options);
|
super(url, options);
|
||||||
|
|
||||||
this._mapSettings = options.mapSettings;
|
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.maxZoom = this._mapSettings.nativeZoomLevels + this._mapSettings.extraZoomLevels;
|
||||||
options.maxNativeZoom = this._mapSettings.nativeZoomLevels;
|
options.maxNativeZoom = this._mapSettings.nativeZoomLevels;
|
||||||
options.zoomReverse = true;
|
options.zoomReverse = true;
|
||||||
@ -43,4 +64,133 @@ export abstract class LiveAtlasTileLayer extends TileLayer {
|
|||||||
throw new TypeError("mapSettings missing");
|
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