2020-12-16 16:54:41 +00:00
|
|
|
/*
|
|
|
|
* Copyright 2020 James Lyne
|
|
|
|
*
|
|
|
|
* Some portions of this file were taken from https://github.com/webbukkit/dynmap.
|
|
|
|
* These portions are Copyright 2020 Dynmap Contributors.
|
|
|
|
*
|
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
* you may not use this file except in compliance with the License.
|
|
|
|
* You may obtain a copy of the License at
|
|
|
|
*
|
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
*
|
|
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
* See the License for the specific language governing permissions and
|
|
|
|
* limitations under the License.
|
|
|
|
*/
|
|
|
|
|
2021-07-25 02:23:29 +00:00
|
|
|
import {Coords, DoneCallback, DomUtil, TileLayerOptions, TileLayer, Util} from 'leaflet';
|
|
|
|
import {useStore} from "@/store";
|
2021-07-23 19:32:15 +00:00
|
|
|
import {Coordinate} from "@/index";
|
|
|
|
import LiveAtlasMapDefinition from "@/model/LiveAtlasMapDefinition";
|
2021-07-25 02:23:29 +00:00
|
|
|
import {computed, watch} from "@vue/runtime-core";
|
|
|
|
import {ComputedRef} from "@vue/reactivity";
|
|
|
|
import {WatchStopHandle} from "vue";
|
|
|
|
import {ActionTypes} from "@/store/action-types";
|
2020-11-24 01:52:31 +00:00
|
|
|
|
|
|
|
export interface DynmapTileLayerOptions extends TileLayerOptions {
|
2021-07-23 19:32:15 +00:00
|
|
|
mapSettings: LiveAtlasMapDefinition;
|
2020-11-24 01:52:31 +00:00
|
|
|
errorTileUrl: string;
|
2021-02-01 00:16:13 +00:00
|
|
|
night?: boolean;
|
2020-11-24 01:52:31 +00:00
|
|
|
}
|
|
|
|
|
2020-12-01 23:20:38 +00:00
|
|
|
export interface DynmapTile {
|
|
|
|
active?: boolean;
|
|
|
|
coords: Coords;
|
|
|
|
current: boolean;
|
|
|
|
el: DynmapTileElement;
|
|
|
|
loaded?: Date;
|
|
|
|
retain?: boolean;
|
|
|
|
complete: boolean;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface DynmapTileElement extends HTMLImageElement {
|
2020-11-24 01:52:31 +00:00
|
|
|
tileName: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface TileInfo {
|
|
|
|
prefix: string;
|
|
|
|
nightday: string;
|
|
|
|
scaledx: number;
|
|
|
|
scaledy: number;
|
|
|
|
zoom: string;
|
|
|
|
zoomprefix: string;
|
|
|
|
x: number;
|
|
|
|
y: number;
|
|
|
|
fmt: string;
|
|
|
|
}
|
|
|
|
|
2021-07-25 02:23:29 +00:00
|
|
|
const store = useStore();
|
|
|
|
|
2021-05-19 23:29:17 +00:00
|
|
|
// noinspection JSUnusedGlobalSymbols
|
2020-12-12 22:04:56 +00:00
|
|
|
export class DynmapTileLayer extends TileLayer {
|
2021-07-25 00:36:47 +00:00
|
|
|
private readonly _mapSettings: LiveAtlasMapDefinition;
|
|
|
|
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;
|
2021-07-25 00:57:59 +00:00
|
|
|
private readonly _baseUrl: string;
|
2021-07-25 02:23:29 +00:00
|
|
|
|
|
|
|
private readonly _night: ComputedRef<boolean>;
|
|
|
|
private readonly _pendingUpdates: ComputedRef<boolean>;
|
|
|
|
private readonly _nightUnwatch: WatchStopHandle;
|
|
|
|
private readonly _updateUnwatch: WatchStopHandle;
|
|
|
|
private _updateFrame: number = 0;
|
|
|
|
|
|
|
|
// @ts-ignore
|
|
|
|
declare options: DynmapTileLayerOptions;
|
2021-07-25 00:36:47 +00:00
|
|
|
|
2020-11-24 01:52:31 +00:00
|
|
|
constructor(options: DynmapTileLayerOptions) {
|
|
|
|
super('', options);
|
2021-05-19 23:29:17 +00:00
|
|
|
|
|
|
|
this._mapSettings = options.mapSettings;
|
|
|
|
options.maxZoom = this._mapSettings.nativeZoomLevels + this._mapSettings.extraZoomLevels;
|
|
|
|
options.maxNativeZoom = this._mapSettings.nativeZoomLevels;
|
|
|
|
options.zoomReverse = true;
|
|
|
|
options.tileSize = 128;
|
|
|
|
options.minZoom = 0;
|
|
|
|
|
2020-12-12 22:04:56 +00:00
|
|
|
Util.setOptions(this, options);
|
2020-11-24 01:52:31 +00:00
|
|
|
|
|
|
|
if (options.mapSettings === null) {
|
|
|
|
throw new TypeError("mapSettings missing");
|
|
|
|
}
|
|
|
|
|
2020-12-12 22:04:56 +00:00
|
|
|
this._tileTemplate = DomUtil.create('img', 'leaflet-tile') as DynmapTileElement;
|
2020-12-01 23:20:38 +00:00
|
|
|
this._tileTemplate.style.width = this._tileTemplate.style.height = this.options.tileSize + 'px';
|
|
|
|
this._tileTemplate.alt = '';
|
|
|
|
this._tileTemplate.tileName = '';
|
|
|
|
this._tileTemplate.setAttribute('role', 'presentation');
|
2021-07-25 00:57:59 +00:00
|
|
|
this._baseUrl = store.state.currentMapProvider!.getTilesUrl();
|
2020-12-01 23:20:38 +00:00
|
|
|
|
|
|
|
Object.seal(this._tileTemplate);
|
|
|
|
|
|
|
|
if(this.options.crossOrigin || this.options.crossOrigin === '') {
|
|
|
|
this._tileTemplate.crossOrigin = this.options.crossOrigin === true ? '' : this.options.crossOrigin;
|
|
|
|
}
|
2021-07-25 02:23:29 +00:00
|
|
|
|
|
|
|
this._pendingUpdates = computed(() => !!store.state.pendingTileUpdates.length);
|
|
|
|
this._updateUnwatch = watch(this._pendingUpdates, (newValue, oldValue) => {
|
|
|
|
if(newValue && !oldValue && !this._updateFrame) {
|
|
|
|
this.handlePendingUpdates();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
this._night = computed(() => store.getters.night);
|
|
|
|
this._nightUnwatch = watch(this._night, () => {
|
|
|
|
if(this._mapSettings.nightAndDay) {
|
|
|
|
this.redraw();
|
|
|
|
}
|
|
|
|
});
|
2020-11-24 01:52:31 +00:00
|
|
|
}
|
|
|
|
|
2021-07-25 02:23:29 +00:00
|
|
|
private getTileName(coords: Coordinate) {
|
2021-05-19 23:29:17 +00:00
|
|
|
const info = this.getTileInfo(coords);
|
|
|
|
// Y is inverted for HD-map.
|
|
|
|
info.y = -info.y;
|
|
|
|
info.scaledy = info.y >> 5;
|
|
|
|
return `${info.prefix}${info.nightday}/${info.scaledx}_${info.scaledy}/${info.zoom}${info.x}_${info.y}.${info.fmt}`;
|
2020-11-24 01:52:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
getTileUrl(coords: Coordinate) {
|
2020-12-11 18:51:23 +00:00
|
|
|
return this.getTileUrlFromName(this.getTileName(coords));
|
|
|
|
}
|
|
|
|
|
2021-07-25 02:23:29 +00:00
|
|
|
private getTileUrlFromName(name: string, timestamp?: number) {
|
2020-12-11 18:51:23 +00:00
|
|
|
let url = this._cachedTileUrls.get(name);
|
2020-11-24 01:52:31 +00:00
|
|
|
|
|
|
|
if (!url) {
|
2020-12-11 18:51:23 +00:00
|
|
|
const path = escape(`${this._mapSettings.world.name}/${name}`);
|
2021-07-25 00:57:59 +00:00
|
|
|
url = `${this._baseUrl}${path}`;
|
2020-12-11 18:51:23 +00:00
|
|
|
|
|
|
|
if(typeof timestamp !== 'undefined') {
|
2020-12-22 18:51:40 +00:00
|
|
|
url += (url.indexOf('?') === -1 ? `?timestamp=${timestamp}` : `×tamp=${timestamp}`);
|
2020-12-11 18:51:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
this._cachedTileUrls.set(name, url);
|
2020-11-24 01:52:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return url;
|
|
|
|
}
|
|
|
|
|
2021-07-25 02:23:29 +00:00
|
|
|
private updateNamedTile(name: string, timestamp: number) {
|
2020-12-01 23:20:38 +00:00
|
|
|
const tile = this._namedTiles.get(name);
|
|
|
|
this._cachedTileUrls.delete(name);
|
2020-11-24 01:52:31 +00:00
|
|
|
|
|
|
|
if (tile) {
|
2020-12-11 18:51:23 +00:00
|
|
|
tile.dataset.src = this.getTileUrlFromName(name, timestamp);
|
|
|
|
this._loadQueue.push(tile);
|
|
|
|
this._tickLoadQueue();
|
2020-11-24 01:52:31 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
createTile(coords: Coords, done: DoneCallback) {
|
2020-12-01 23:20:38 +00:00
|
|
|
//Clone template image instead of creating a new one
|
|
|
|
const tile = this._tileTemplate.cloneNode(false) as DynmapTileElement;
|
|
|
|
|
|
|
|
tile.tileName = this.getTileName(coords);
|
|
|
|
tile.dataset.src = this.getTileUrl(coords);
|
|
|
|
|
|
|
|
this._namedTiles.set(tile.tileName, tile);
|
|
|
|
this._loadQueue.push(tile);
|
2020-11-24 01:52:31 +00:00
|
|
|
|
2020-12-01 23:20:38 +00:00
|
|
|
//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();
|
|
|
|
};
|
2020-11-24 01:52:31 +00:00
|
|
|
|
2020-12-01 23:20:38 +00:00
|
|
|
this._tickLoadQueue();
|
2020-11-24 01:52:31 +00:00
|
|
|
|
|
|
|
return tile;
|
|
|
|
}
|
|
|
|
|
2020-12-01 23:20:38 +00:00
|
|
|
_tickLoadQueue() {
|
2020-12-10 02:22:59 +00:00
|
|
|
if (this._loadingTiles.size > 6) {
|
2020-12-01 23:20:38 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const tile = this._loadQueue.shift();
|
|
|
|
|
|
|
|
if (!tile) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this._loadingTiles.add(tile);
|
|
|
|
tile.src = tile.dataset.src as string;
|
|
|
|
}
|
|
|
|
|
2020-11-24 01:52:31 +00:00
|
|
|
// stops loading all tiles in the background layer
|
|
|
|
_abortLoading() {
|
|
|
|
let tile;
|
2020-12-01 23:20:38 +00:00
|
|
|
|
2020-11-24 01:52:31 +00:00
|
|
|
for (const i in this._tiles) {
|
|
|
|
if (!Object.prototype.hasOwnProperty.call(this._tiles, i)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2020-12-01 23:20:38 +00:00
|
|
|
tile = this._tiles[i] as DynmapTile;
|
2020-11-24 01:52:31 +00:00
|
|
|
|
|
|
|
if (tile.coords.z !== this._tileZoom) {
|
2020-12-01 23:20:38 +00:00
|
|
|
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);
|
2020-11-24 01:52:31 +00:00
|
|
|
}
|
2020-12-01 23:20:38 +00:00
|
|
|
|
|
|
|
this._loadingTiles.delete(tile.el);
|
2020-11-24 01:52:31 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
super._abortLoading.call(this);
|
|
|
|
}
|
|
|
|
|
|
|
|
_removeTile(key: string) {
|
2020-12-01 23:20:38 +00:00
|
|
|
const tile = this._tiles[key] as DynmapTile;
|
2020-11-24 01:52:31 +00:00
|
|
|
|
|
|
|
if (!tile) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-12-01 23:20:38 +00:00
|
|
|
const tileName = tile.el.tileName as string;
|
2020-11-24 01:52:31 +00:00
|
|
|
|
|
|
|
if (tileName) {
|
2020-12-01 23:20:38 +00:00
|
|
|
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;
|
2020-11-24 01:52:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// @ts-ignore
|
|
|
|
super._removeTile.call(this, key);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Some helper functions.
|
2021-07-25 02:23:29 +00:00
|
|
|
private zoomprefix(amount: number) {
|
2021-05-19 23:29:17 +00:00
|
|
|
// amount == 0 -> ''
|
|
|
|
// amount == 1 -> 'z_'
|
|
|
|
// amount == 2 -> 'zz_'
|
|
|
|
return 'z'.repeat(amount) + (amount === 0 ? '' : '_');
|
2020-11-24 01:52:31 +00:00
|
|
|
}
|
|
|
|
|
2021-07-25 02:23:29 +00:00
|
|
|
private getTileInfo(coords: Coordinate): TileInfo {
|
2020-11-24 01:52:31 +00:00
|
|
|
// zoom: max zoomed in = this.options.maxZoom, max zoomed out = 0
|
|
|
|
// izoom: max zoomed in = 0, max zoomed out = this.options.maxZoom
|
|
|
|
// zoomoutlevel: izoom < mapzoomin -> 0, else -> izoom - mapzoomin (which ranges from 0 till mapzoomout)
|
|
|
|
const izoom = this._getZoomForUrl(),
|
|
|
|
zoomoutlevel = Math.max(0, izoom - this._mapSettings.extraZoomLevels),
|
2021-05-27 13:30:56 +00:00
|
|
|
scale = (1 << zoomoutlevel),
|
2020-11-24 01:52:31 +00:00
|
|
|
x = scale * coords.x,
|
|
|
|
y = scale * coords.y;
|
|
|
|
|
|
|
|
return {
|
|
|
|
prefix: this._mapSettings.prefix,
|
2021-07-25 02:23:29 +00:00
|
|
|
nightday: (this._mapSettings.nightAndDay && !this._night.value) ? '_day' : '',
|
2020-11-24 01:52:31 +00:00
|
|
|
scaledx: x >> 5,
|
|
|
|
scaledy: y >> 5,
|
|
|
|
zoom: this.zoomprefix(zoomoutlevel),
|
|
|
|
zoomprefix: (zoomoutlevel == 0) ? "" : (this.zoomprefix(zoomoutlevel) + "_"),
|
|
|
|
x: x,
|
|
|
|
y: y,
|
|
|
|
fmt: this._mapSettings.imageFormat || 'png'
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2021-07-25 02:23:29 +00:00
|
|
|
private async handlePendingUpdates() {
|
|
|
|
const updates = await store.dispatch(ActionTypes.POP_TILE_UPDATES, 10);
|
|
|
|
|
|
|
|
for(const update of updates) {
|
|
|
|
this.updateNamedTile(update.name, update.timestamp);
|
|
|
|
}
|
|
|
|
|
|
|
|
if(this._pendingUpdates.value) {
|
|
|
|
// eslint-disable-next-line no-unused-vars
|
|
|
|
this._updateFrame = requestAnimationFrame(() => this.handlePendingUpdates());
|
|
|
|
} else {
|
|
|
|
this._updateFrame = 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
remove() {
|
|
|
|
super.remove();
|
|
|
|
|
|
|
|
this._nightUnwatch();
|
|
|
|
|
|
|
|
if(this._updateFrame) {
|
|
|
|
cancelAnimationFrame(this._updateFrame);
|
|
|
|
}
|
|
|
|
|
|
|
|
if(this._updateUnwatch) {
|
|
|
|
this._updateUnwatch();
|
2021-02-01 00:16:13 +00:00
|
|
|
}
|
2021-07-25 02:23:29 +00:00
|
|
|
|
|
|
|
return this;
|
2021-02-01 00:16:13 +00:00
|
|
|
}
|
2020-11-24 01:52:31 +00:00
|
|
|
}
|