Preparation for overlay TileLayers
This commit is contained in:
parent
06ac12ba29
commit
80bb800e04
@ -17,7 +17,9 @@
|
||||
<template>
|
||||
<div class="map" :style="{backgroundColor: mapBackground }" v-bind="$attrs" :aria-label="mapTitle">
|
||||
<template v-if="leaflet">
|
||||
<MapLayer v-for="[name, map] in maps" :key="name" :map="map" :name="name" :leaflet="leaflet"></MapLayer>
|
||||
<TileLayer v-for="[name, map] in maps" :key="name" :options="map" :leaflet="leaflet"></TileLayer>
|
||||
|
||||
<TileLayerOverlay v-for="[name, overlay] in overlays" :key="name" :options="overlay" :leaflet="leaflet"></TileLayerOverlay>
|
||||
<PlayersLayer v-if="playerMarkersEnabled" :leaflet="leaflet"></PlayersLayer>
|
||||
<MarkerSetLayer v-for="[name, markerSet] in markerSets" :key="name" :markerSet="markerSet" :leaflet="leaflet"></MarkerSetLayer>
|
||||
|
||||
@ -38,7 +40,7 @@
|
||||
import {computed, ref, defineComponent} from "@vue/runtime-core";
|
||||
import {CRS, LatLng, LatLngBounds, PanOptions, ZoomPanOptions} from 'leaflet';
|
||||
import {useStore} from '@/store';
|
||||
import MapLayer from "@/components/map/layer/MapLayer.vue";
|
||||
import TileLayer from "@/components/map/layer/TileLayer.vue";
|
||||
import PlayersLayer from "@/components/map/layer/PlayersLayer.vue";
|
||||
import MarkerSetLayer from "@/components/map/layer/MarkerSetLayer.vue";
|
||||
import CoordinatesControl from "@/components/map/control/CoordinatesControl.vue";
|
||||
@ -52,11 +54,13 @@ import {LoadingControl} from "@/leaflet/control/LoadingControl";
|
||||
import MapContextMenu from "@/components/map/MapContextMenu.vue";
|
||||
import {LiveAtlasLocation, LiveAtlasPlayer, LiveAtlasMapViewTarget} from "@/index";
|
||||
import LoginControl from "@/components/map/control/LoginControl.vue";
|
||||
import TileLayerOverlay from "@/components/map/layer/TileLayerOverlay.vue";
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
TileLayerOverlay,
|
||||
MapContextMenu,
|
||||
MapLayer,
|
||||
TileLayer,
|
||||
PlayersLayer,
|
||||
MarkerSetLayer,
|
||||
CoordinatesControl,
|
||||
@ -72,6 +76,7 @@ export default defineComponent({
|
||||
leaflet = undefined as any,
|
||||
|
||||
maps = computed(() => store.state.maps),
|
||||
overlays = computed(() => store.state.currentMap?.overlays),
|
||||
markerSets = computed(() => store.state.markerSets),
|
||||
configuration = computed(() => store.state.configuration),
|
||||
|
||||
@ -100,6 +105,7 @@ export default defineComponent({
|
||||
return {
|
||||
leaflet,
|
||||
maps,
|
||||
overlays,
|
||||
markerSets,
|
||||
configuration,
|
||||
|
||||
|
@ -15,38 +15,29 @@
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent, onUnmounted, computed, watch} from "@vue/runtime-core";
|
||||
import {Map} from 'leaflet';
|
||||
import {computed, defineComponent, onUnmounted, watch} from "@vue/runtime-core";
|
||||
import {useStore} from "@/store";
|
||||
import {LiveAtlasTileLayerOptions} from "@/leaflet/tileLayer/LiveAtlasTileLayer";
|
||||
import LiveAtlasLeafletMap from "@/leaflet/LiveAtlasLeafletMap";
|
||||
import LiveAtlasMapDefinition from "@/model/LiveAtlasMapDefinition";
|
||||
import {LiveAtlasTileLayer} from "@/leaflet/tileLayer/LiveAtlasTileLayer";
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
map: {
|
||||
type: Object as () => LiveAtlasMapDefinition,
|
||||
options: {
|
||||
type: Object as () => LiveAtlasTileLayerOptions,
|
||||
required: true
|
||||
},
|
||||
leaflet: {
|
||||
type: Object as () => Map,
|
||||
type: Object as () => LiveAtlasLeafletMap,
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
|
||||
setup(props) {
|
||||
const store = useStore(),
|
||||
active = computed(() => props.map === store.state.currentMap);
|
||||
active = computed(() => props.options instanceof LiveAtlasMapDefinition && props.options === store.state.currentMap);
|
||||
|
||||
let layer: LiveAtlasTileLayer;
|
||||
|
||||
layer = store.state.currentMapProvider!.createTileLayer({
|
||||
errorTileUrl: 'images/blank.png',
|
||||
mapSettings: Object.freeze(JSON.parse(JSON.stringify(props.map))),
|
||||
});
|
||||
let layer = store.state.currentMapProvider!.createTileLayer(Object.freeze(JSON.parse(JSON.stringify(props.options))));
|
||||
|
||||
const enableLayer = () => props.leaflet.addLayer(layer),
|
||||
disableLayer = () => layer.remove();
|
52
src/components/map/layer/TileLayerOverlay.vue
Normal file
52
src/components/map/layer/TileLayerOverlay.vue
Normal file
@ -0,0 +1,52 @@
|
||||
<!--
|
||||
- Copyright 2022 James Lyne
|
||||
-
|
||||
- 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.
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import {defineComponent, onUnmounted} from "@vue/runtime-core";
|
||||
import {useStore} from "@/store";
|
||||
import LiveAtlasLeafletMap from "@/leaflet/LiveAtlasLeafletMap";
|
||||
import {LiveAtlasTileLayerOverlay} from "@/index";
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
options: {
|
||||
type: Object as () => LiveAtlasTileLayerOverlay,
|
||||
required: true
|
||||
},
|
||||
leaflet: {
|
||||
type: Object as () => LiveAtlasLeafletMap,
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
|
||||
setup(props) {
|
||||
const store = useStore();
|
||||
|
||||
let layer = store.state.currentMapProvider!.createTileLayer(Object.freeze(JSON.parse(JSON.stringify(props.options.tileLayerOptions))));
|
||||
|
||||
props.leaflet.getLayerManager().addHiddenLayer(layer, props.options.label, props.options.priority);
|
||||
|
||||
onUnmounted(() => {
|
||||
props.leaflet.getLayerManager().removeLayer(layer);
|
||||
layer.remove();
|
||||
});
|
||||
},
|
||||
|
||||
render() {
|
||||
return null;
|
||||
},
|
||||
});
|
||||
</script>
|
9
src/index.d.ts
vendored
9
src/index.d.ts
vendored
@ -193,16 +193,23 @@ interface LiveAtlasMapProvider {
|
||||
register(formData: FormData): void;
|
||||
}
|
||||
|
||||
interface LiveAtlasMarkerSet {
|
||||
interface LiveAtlasOverlay {
|
||||
id: string,
|
||||
label: string;
|
||||
hidden: boolean;
|
||||
priority: number;
|
||||
minZoom?: number;
|
||||
maxZoom?: number;
|
||||
}
|
||||
|
||||
interface LiveAtlasMarkerSet extends LiveAtlasOverlay {
|
||||
showLabels?: boolean;
|
||||
}
|
||||
|
||||
interface LiveAtlasTileLayerOverlay extends LiveAtlasOverlay {
|
||||
tileLayerOptions: LiveAtlasTileLayerOptions;
|
||||
}
|
||||
|
||||
interface LiveAtlasMarker {
|
||||
id: string;
|
||||
type: LiveAtlasMarkerType;
|
||||
|
@ -31,7 +31,6 @@ import {TileInformation} from "dynmap";
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
export class DynmapTileLayer extends LiveAtlasTileLayer {
|
||||
private readonly _namedTiles: Map<any, any>;
|
||||
private readonly _baseUrl: string;
|
||||
private readonly _store: Store = useStore();
|
||||
|
||||
private readonly _night: ComputedRef<boolean>;
|
||||
@ -41,12 +40,9 @@ export class DynmapTileLayer extends LiveAtlasTileLayer {
|
||||
private _updateFrame: number = 0;
|
||||
|
||||
constructor(options: LiveAtlasTileLayerOptions) {
|
||||
super('', options);
|
||||
super(options);
|
||||
|
||||
this._mapSettings = options.mapSettings;
|
||||
this._baseUrl = options.mapSettings.baseUrl;
|
||||
this._namedTiles = Object.seal(new Map());
|
||||
|
||||
this._pendingUpdates = computed(() => !!this._store.state.pendingTileUpdates.length);
|
||||
this._night = computed(() => this._store.getters.night);
|
||||
}
|
||||
@ -62,7 +58,7 @@ export class DynmapTileLayer extends LiveAtlasTileLayer {
|
||||
});
|
||||
|
||||
this._nightUnwatch = watch(this._night, () => {
|
||||
if(this._mapSettings.nightAndDay) {
|
||||
if(this.options.nightAndDay) {
|
||||
this.redraw();
|
||||
}
|
||||
});
|
||||
@ -83,8 +79,7 @@ export class DynmapTileLayer extends LiveAtlasTileLayer {
|
||||
}
|
||||
|
||||
private getTileUrlFromName(name: string, timestamp?: number) {
|
||||
const path = escape(`${this._mapSettings.world.name}/${name}`);
|
||||
let url = `${this._baseUrl}${path}`;
|
||||
let url = this.options.baseUrl + name;
|
||||
|
||||
if(typeof timestamp !== 'undefined') {
|
||||
url += (url.indexOf('?') === -1 ? `?timestamp=${timestamp}` : `×tamp=${timestamp}`);
|
||||
@ -164,21 +159,21 @@ export class DynmapTileLayer extends LiveAtlasTileLayer {
|
||||
// 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),
|
||||
zoomoutlevel = Math.max(0, izoom - (this.options.extraZoomLevels || 0)),
|
||||
scale = (1 << zoomoutlevel),
|
||||
x = scale * coords.x,
|
||||
y = scale * coords.y;
|
||||
|
||||
return {
|
||||
prefix: this._mapSettings.prefix,
|
||||
nightday: (this._mapSettings.nightAndDay && !this._night.value) ? '_day' : '',
|
||||
prefix: this.options.prefix || '',
|
||||
nightday: (this.options.nightAndDay && !this._night.value) ? '_day' : '',
|
||||
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'
|
||||
fmt: this.options.imageFormat || 'png'
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -14,33 +14,66 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import LiveAtlasMapDefinition from "@/model/LiveAtlasMapDefinition";
|
||||
import {Map as LeafletMap, Coords, DomUtil, DoneCallback, TileLayer, TileLayerOptions, Util} from "leaflet";
|
||||
import {LiveAtlasInternalTiles, LiveAtlasTileElement} from "@/index";
|
||||
import falseFn = Util.falseFn;
|
||||
import {ImageFormat} from "dynmap";
|
||||
|
||||
export interface LiveAtlasTileLayerOptions extends TileLayerOptions {
|
||||
mapSettings: LiveAtlasMapDefinition;
|
||||
errorTileUrl: string;
|
||||
export interface LiveAtlasTileLayerOptions {
|
||||
baseUrl: string;
|
||||
tileSize: number;
|
||||
imageFormat: ImageFormat;
|
||||
prefix?: string;
|
||||
nightAndDay?: boolean;
|
||||
nativeZoomLevels: number;
|
||||
extraZoomLevels?: number;
|
||||
minZoom?: number;
|
||||
maxZoom?: number;
|
||||
tileUpdateInterval?: number;
|
||||
}
|
||||
|
||||
export interface LiveAtlasTileLayerInternalOptions extends TileLayerOptions {
|
||||
baseUrl: string;
|
||||
imageFormat: ImageFormat;
|
||||
prefix: string;
|
||||
nightAndDay: boolean;
|
||||
extraZoomLevels: number;
|
||||
tileUpdateInterval?: number;
|
||||
}
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
export abstract class LiveAtlasTileLayer extends TileLayer {
|
||||
declare options: LiveAtlasTileLayerOptions;
|
||||
declare options: LiveAtlasTileLayerInternalOptions;
|
||||
declare _tiles: LiveAtlasInternalTiles;
|
||||
declare _url: string;
|
||||
|
||||
protected _mapSettings: LiveAtlasMapDefinition;
|
||||
private readonly tileTemplate: LiveAtlasTileElement;
|
||||
protected readonly tileTemplate: LiveAtlasTileElement;
|
||||
protected readonly loadQueue: LiveAtlasTileElement[] = [];
|
||||
private readonly loadingTiles: Set<LiveAtlasTileElement> = Object.seal(new Set());
|
||||
private refreshTimeout?: ReturnType<typeof setTimeout>;
|
||||
protected readonly loadingTiles: Set<LiveAtlasTileElement> = Object.seal(new Set());
|
||||
protected refreshTimeout?: ReturnType<typeof setTimeout>;
|
||||
|
||||
private static genericLoadError = new Error('Tile failed to load');
|
||||
protected static genericLoadError = new Error('Tile failed to load');
|
||||
|
||||
protected constructor(url: string, options: LiveAtlasTileLayerOptions) {
|
||||
super(url, options);
|
||||
protected constructor(options: LiveAtlasTileLayerOptions) {
|
||||
super('', {
|
||||
errorTileUrl: 'images/blank.png',
|
||||
zoomReverse: true,
|
||||
tileSize: options.tileSize,
|
||||
maxNativeZoom: options.nativeZoomLevels,
|
||||
minZoom: options.minZoom,
|
||||
maxZoom: options.maxZoom || options.nativeZoomLevels + (options.extraZoomLevels || 0),
|
||||
});
|
||||
|
||||
Util.setOptions(this, {
|
||||
imageFormat: options.imageFormat,
|
||||
baseUrl: options.baseUrl,
|
||||
tileUpdateInterval: options.tileUpdateInterval,
|
||||
nightAndDay: !!options.nightAndDay,
|
||||
prefix: options.prefix || '',
|
||||
extraZoomLevels: options.extraZoomLevels || 0,
|
||||
nativeZoomLevels: options.nativeZoomLevels,
|
||||
});
|
||||
|
||||
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 = '';
|
||||
@ -53,14 +86,6 @@ export abstract class LiveAtlasTileLayer extends TileLayer {
|
||||
}
|
||||
|
||||
Object.seal(this.tileTemplate);
|
||||
|
||||
options.maxZoom = this._mapSettings.nativeZoomLevels + this._mapSettings.extraZoomLevels;
|
||||
options.maxNativeZoom = this._mapSettings.nativeZoomLevels;
|
||||
options.zoomReverse = true;
|
||||
options.tileSize = this._mapSettings.tileSize;
|
||||
options.minZoom = 0;
|
||||
|
||||
Util.setOptions(this, options);
|
||||
}
|
||||
|
||||
// @method createTile(coords: Object, done?: Function): HTMLElement
|
||||
@ -209,8 +234,8 @@ export abstract class LiveAtlasTileLayer extends TileLayer {
|
||||
}
|
||||
|
||||
onAdd(map: LeafletMap): this {
|
||||
if(this._mapSettings.tileUpdateInterval) {
|
||||
this.refreshTimeout = setTimeout(() => this.handlePeriodicRefresh(), this._mapSettings.tileUpdateInterval);
|
||||
if(this.options.tileUpdateInterval) {
|
||||
this.refreshTimeout = setTimeout(() => this.handlePeriodicRefresh(), this.options.tileUpdateInterval);
|
||||
}
|
||||
|
||||
return super.onAdd(map);
|
||||
@ -229,6 +254,6 @@ export abstract class LiveAtlasTileLayer extends TileLayer {
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
this.refreshTimeout = setTimeout(() => this.handlePeriodicRefresh(), this._mapSettings.tileUpdateInterval);
|
||||
this.refreshTimeout = setTimeout(() => this.handlePeriodicRefresh(), this.options.tileUpdateInterval);
|
||||
}
|
||||
}
|
||||
|
@ -22,23 +22,15 @@ import {Coords, Util} from "leaflet";
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
export class OverviewerTileLayer extends LiveAtlasTileLayer {
|
||||
private readonly _baseUrl: string;
|
||||
|
||||
constructor(options: LiveAtlasTileLayerOptions) {
|
||||
super('', options);
|
||||
super(options);
|
||||
|
||||
options.zoomReverse = false;
|
||||
|
||||
Util.setOptions(this, options);
|
||||
|
||||
this._mapSettings = options.mapSettings;
|
||||
this._baseUrl = options.mapSettings.baseUrl;
|
||||
Util.setOptions(this, {zoomReverse: false});
|
||||
}
|
||||
|
||||
getTileUrl(coords: Coords): string {
|
||||
let url = this._mapSettings.name;
|
||||
const zoom = coords.z,
|
||||
urlBase = this._mapSettings.prefix;
|
||||
let url = this.options.prefix;
|
||||
const zoom = coords.z;
|
||||
|
||||
if(coords.x < 0 || coords.x >= Math.pow(2, zoom) ||
|
||||
coords.y < 0 || coords.y >= Math.pow(2, zoom)) {
|
||||
@ -52,10 +44,10 @@ export class OverviewerTileLayer extends LiveAtlasTileLayer {
|
||||
url += '/' + (x + 2 * y);
|
||||
}
|
||||
}
|
||||
url = url + '.' + this._mapSettings.imageFormat;
|
||||
url += `.${this.options.imageFormat}`;
|
||||
// if(typeof overviewerConfig.map.cacheTag !== 'undefined') {
|
||||
// url += '?c=' + overviewerConfig.map.cacheTag;
|
||||
// }
|
||||
return(this._baseUrl + urlBase + url);
|
||||
return this.options.baseUrl + url;
|
||||
}
|
||||
}
|
||||
|
@ -19,11 +19,9 @@ import {Util} from "leaflet";
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
export class Pl3xmapTileLayer extends LiveAtlasTileLayer {
|
||||
constructor(options: LiveAtlasTileLayerOptions) {
|
||||
super(`${options.mapSettings.baseUrl}${options.mapSettings.world.name}/{z}/{x}_{y}.png`, options);
|
||||
|
||||
options.zoomReverse = false;
|
||||
|
||||
Util.setOptions(this, options);
|
||||
constructor(map: LiveAtlasTileLayerOptions) {
|
||||
super(map);
|
||||
this._url = `${map.baseUrl}{z}/{x}_{y}.png`;
|
||||
Util.setOptions(this, {zoomReverse: false});
|
||||
}
|
||||
}
|
||||
|
@ -14,11 +14,12 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {Coordinate, LiveAtlasProjection, LiveAtlasWorldDefinition} from "@/index";
|
||||
import {Coordinate, LiveAtlasProjection, LiveAtlasTileLayerOverlay, LiveAtlasWorldDefinition} from "@/index";
|
||||
import {LatLng} from "leaflet";
|
||||
import {ImageFormat} from "dynmap";
|
||||
import {LiveAtlasTileLayerOptions} from "@/leaflet/tileLayer/LiveAtlasTileLayer";
|
||||
|
||||
export interface LiveAtlasMapDefinitionOptions {
|
||||
export interface LiveAtlasMapDefinitionOptions extends LiveAtlasTileLayerOptions {
|
||||
world: LiveAtlasWorldDefinition;
|
||||
appendedWorld?: LiveAtlasWorldDefinition; // append_to_world
|
||||
|
||||
@ -45,9 +46,10 @@ export interface LiveAtlasMapDefinitionOptions {
|
||||
|
||||
tileUpdateInterval?: number;
|
||||
center?: Coordinate;
|
||||
overlays?: Map<string, LiveAtlasTileLayerOverlay>;
|
||||
}
|
||||
|
||||
export default class LiveAtlasMapDefinition {
|
||||
export default class LiveAtlasMapDefinition implements LiveAtlasTileLayerOptions {
|
||||
readonly world: LiveAtlasWorldDefinition;
|
||||
readonly appendedWorld?: LiveAtlasWorldDefinition;
|
||||
|
||||
@ -74,6 +76,7 @@ export default class LiveAtlasMapDefinition {
|
||||
|
||||
readonly tileUpdateInterval?: number;
|
||||
readonly center?: Coordinate;
|
||||
readonly overlays: Map<string, LiveAtlasTileLayerOverlay>;
|
||||
|
||||
readonly scale: number;
|
||||
|
||||
@ -105,6 +108,8 @@ export default class LiveAtlasMapDefinition {
|
||||
this.tileUpdateInterval = options.tileUpdateInterval || undefined;
|
||||
this.center = options.center || undefined;
|
||||
|
||||
this.overlays = options.overlays || new Map();
|
||||
|
||||
this.scale = (1 / Math.pow(2, this.nativeZoomLevels));
|
||||
}
|
||||
|
||||
|
@ -120,7 +120,7 @@ export default class OverviewerMapProvider extends MapProvider {
|
||||
name: tileset.path,
|
||||
displayName: tileset.name || tileset.path,
|
||||
|
||||
baseUrl: this.config,
|
||||
baseUrl: `${this.config}${tileset.base}/${tileset.path}`,
|
||||
tileSize,
|
||||
projection: new OverviewerProjection({
|
||||
upperRight: serverResponse.CONST.UPPERRIGHT,
|
||||
|
@ -187,7 +187,7 @@ export default class Pl3xmapMapProvider extends MapProvider {
|
||||
displayName: 'Flat',
|
||||
icon: world.icon ? `${this.config}images/icon/${world.icon}.png` : undefined,
|
||||
|
||||
baseUrl: `${this.config}tiles/`,
|
||||
baseUrl: `${this.config}tiles/${w.name}/`,
|
||||
imageFormat: 'png',
|
||||
tileSize: 512,
|
||||
|
||||
|
@ -143,7 +143,7 @@ export function buildWorlds(response: Configuration, config: DynmapUrlConfig): A
|
||||
displayName: map.title,
|
||||
icon: (map.icon || undefined) as string | undefined,
|
||||
|
||||
baseUrl: config.tiles,
|
||||
baseUrl: `${config.tiles}${actualWorld.name}/`,
|
||||
imageFormat: map['image-format'] || 'png',
|
||||
tileSize,
|
||||
projection: new DynmapProjection({
|
||||
|
Loading…
Reference in New Issue
Block a user