Preparation for overlay TileLayers

This commit is contained in:
James Lyne 2022-02-25 18:05:36 +00:00
parent 06ac12ba29
commit 80bb800e04
12 changed files with 154 additions and 83 deletions

View File

@ -17,7 +17,9 @@
<template> <template>
<div class="map" :style="{backgroundColor: mapBackground }" v-bind="$attrs" :aria-label="mapTitle"> <div class="map" :style="{backgroundColor: mapBackground }" v-bind="$attrs" :aria-label="mapTitle">
<template v-if="leaflet"> <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> <PlayersLayer v-if="playerMarkersEnabled" :leaflet="leaflet"></PlayersLayer>
<MarkerSetLayer v-for="[name, markerSet] in markerSets" :key="name" :markerSet="markerSet" :leaflet="leaflet"></MarkerSetLayer> <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 {computed, ref, defineComponent} from "@vue/runtime-core";
import {CRS, LatLng, LatLngBounds, PanOptions, ZoomPanOptions} from 'leaflet'; import {CRS, LatLng, LatLngBounds, PanOptions, ZoomPanOptions} from 'leaflet';
import {useStore} from '@/store'; 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 PlayersLayer from "@/components/map/layer/PlayersLayer.vue";
import MarkerSetLayer from "@/components/map/layer/MarkerSetLayer.vue"; import MarkerSetLayer from "@/components/map/layer/MarkerSetLayer.vue";
import CoordinatesControl from "@/components/map/control/CoordinatesControl.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 MapContextMenu from "@/components/map/MapContextMenu.vue";
import {LiveAtlasLocation, LiveAtlasPlayer, LiveAtlasMapViewTarget} from "@/index"; import {LiveAtlasLocation, LiveAtlasPlayer, LiveAtlasMapViewTarget} from "@/index";
import LoginControl from "@/components/map/control/LoginControl.vue"; import LoginControl from "@/components/map/control/LoginControl.vue";
import TileLayerOverlay from "@/components/map/layer/TileLayerOverlay.vue";
export default defineComponent({ export default defineComponent({
components: { components: {
TileLayerOverlay,
MapContextMenu, MapContextMenu,
MapLayer, TileLayer,
PlayersLayer, PlayersLayer,
MarkerSetLayer, MarkerSetLayer,
CoordinatesControl, CoordinatesControl,
@ -72,6 +76,7 @@ export default defineComponent({
leaflet = undefined as any, leaflet = undefined as any,
maps = computed(() => store.state.maps), maps = computed(() => store.state.maps),
overlays = computed(() => store.state.currentMap?.overlays),
markerSets = computed(() => store.state.markerSets), markerSets = computed(() => store.state.markerSets),
configuration = computed(() => store.state.configuration), configuration = computed(() => store.state.configuration),
@ -100,6 +105,7 @@ export default defineComponent({
return { return {
leaflet, leaflet,
maps, maps,
overlays,
markerSets, markerSets,
configuration, configuration,

View File

@ -15,38 +15,29 @@
--> -->
<script lang="ts"> <script lang="ts">
import {defineComponent, onUnmounted, computed, watch} from "@vue/runtime-core"; import {computed, defineComponent, onUnmounted, watch} from "@vue/runtime-core";
import {Map} from 'leaflet';
import {useStore} from "@/store"; import {useStore} from "@/store";
import {LiveAtlasTileLayerOptions} from "@/leaflet/tileLayer/LiveAtlasTileLayer";
import LiveAtlasLeafletMap from "@/leaflet/LiveAtlasLeafletMap";
import LiveAtlasMapDefinition from "@/model/LiveAtlasMapDefinition"; import LiveAtlasMapDefinition from "@/model/LiveAtlasMapDefinition";
import {LiveAtlasTileLayer} from "@/leaflet/tileLayer/LiveAtlasTileLayer";
export default defineComponent({ export default defineComponent({
props: { props: {
name: { options: {
type: String, type: Object as () => LiveAtlasTileLayerOptions,
required: true
},
map: {
type: Object as () => LiveAtlasMapDefinition,
required: true required: true
}, },
leaflet: { leaflet: {
type: Object as () => Map, type: Object as () => LiveAtlasLeafletMap,
required: true, required: true,
} }
}, },
setup(props) { setup(props) {
const store = useStore(), const store = useStore(),
active = computed(() => props.map === store.state.currentMap); active = computed(() => props.options instanceof LiveAtlasMapDefinition && props.options === store.state.currentMap);
let layer: LiveAtlasTileLayer; let layer = store.state.currentMapProvider!.createTileLayer(Object.freeze(JSON.parse(JSON.stringify(props.options))));
layer = store.state.currentMapProvider!.createTileLayer({
errorTileUrl: 'images/blank.png',
mapSettings: Object.freeze(JSON.parse(JSON.stringify(props.map))),
});
const enableLayer = () => props.leaflet.addLayer(layer), const enableLayer = () => props.leaflet.addLayer(layer),
disableLayer = () => layer.remove(); disableLayer = () => layer.remove();

View 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
View File

@ -193,16 +193,23 @@ interface LiveAtlasMapProvider {
register(formData: FormData): void; register(formData: FormData): void;
} }
interface LiveAtlasMarkerSet { interface LiveAtlasOverlay {
id: string, id: string,
label: string; label: string;
hidden: boolean; hidden: boolean;
priority: number; priority: number;
minZoom?: number; minZoom?: number;
maxZoom?: number; maxZoom?: number;
}
interface LiveAtlasMarkerSet extends LiveAtlasOverlay {
showLabels?: boolean; showLabels?: boolean;
} }
interface LiveAtlasTileLayerOverlay extends LiveAtlasOverlay {
tileLayerOptions: LiveAtlasTileLayerOptions;
}
interface LiveAtlasMarker { interface LiveAtlasMarker {
id: string; id: string;
type: LiveAtlasMarkerType; type: LiveAtlasMarkerType;

View File

@ -31,7 +31,6 @@ import {TileInformation} from "dynmap";
// noinspection JSUnusedGlobalSymbols // noinspection JSUnusedGlobalSymbols
export class DynmapTileLayer extends LiveAtlasTileLayer { export class DynmapTileLayer extends LiveAtlasTileLayer {
private readonly _namedTiles: Map<any, any>; private readonly _namedTiles: Map<any, any>;
private readonly _baseUrl: string;
private readonly _store: Store = useStore(); private readonly _store: Store = useStore();
private readonly _night: ComputedRef<boolean>; private readonly _night: ComputedRef<boolean>;
@ -41,12 +40,9 @@ export class DynmapTileLayer extends LiveAtlasTileLayer {
private _updateFrame: number = 0; private _updateFrame: number = 0;
constructor(options: LiveAtlasTileLayerOptions) { constructor(options: LiveAtlasTileLayerOptions) {
super('', options); super(options);
this._mapSettings = options.mapSettings;
this._baseUrl = options.mapSettings.baseUrl;
this._namedTiles = Object.seal(new Map()); this._namedTiles = Object.seal(new Map());
this._pendingUpdates = computed(() => !!this._store.state.pendingTileUpdates.length); this._pendingUpdates = computed(() => !!this._store.state.pendingTileUpdates.length);
this._night = computed(() => this._store.getters.night); this._night = computed(() => this._store.getters.night);
} }
@ -62,7 +58,7 @@ export class DynmapTileLayer extends LiveAtlasTileLayer {
}); });
this._nightUnwatch = watch(this._night, () => { this._nightUnwatch = watch(this._night, () => {
if(this._mapSettings.nightAndDay) { if(this.options.nightAndDay) {
this.redraw(); this.redraw();
} }
}); });
@ -83,8 +79,7 @@ export class DynmapTileLayer extends LiveAtlasTileLayer {
} }
private getTileUrlFromName(name: string, timestamp?: number) { private getTileUrlFromName(name: string, timestamp?: number) {
const path = escape(`${this._mapSettings.world.name}/${name}`); let url = this.options.baseUrl + name;
let url = `${this._baseUrl}${path}`;
if(typeof timestamp !== 'undefined') { if(typeof timestamp !== 'undefined') {
url += (url.indexOf('?') === -1 ? `?timestamp=${timestamp}` : `&timestamp=${timestamp}`); url += (url.indexOf('?') === -1 ? `?timestamp=${timestamp}` : `&timestamp=${timestamp}`);
@ -164,21 +159,21 @@ export class DynmapTileLayer extends LiveAtlasTileLayer {
// izoom: max zoomed in = 0, max zoomed out = this.options.maxZoom // izoom: max zoomed in = 0, max zoomed out = this.options.maxZoom
// zoomoutlevel: izoom < mapzoomin -> 0, else -> izoom - mapzoomin (which ranges from 0 till mapzoomout) // zoomoutlevel: izoom < mapzoomin -> 0, else -> izoom - mapzoomin (which ranges from 0 till mapzoomout)
const izoom = this._getZoomForUrl(), const izoom = this._getZoomForUrl(),
zoomoutlevel = Math.max(0, izoom - this._mapSettings.extraZoomLevels), zoomoutlevel = Math.max(0, izoom - (this.options.extraZoomLevels || 0)),
scale = (1 << zoomoutlevel), scale = (1 << zoomoutlevel),
x = scale * coords.x, x = scale * coords.x,
y = scale * coords.y; y = scale * coords.y;
return { return {
prefix: this._mapSettings.prefix, prefix: this.options.prefix || '',
nightday: (this._mapSettings.nightAndDay && !this._night.value) ? '_day' : '', nightday: (this.options.nightAndDay && !this._night.value) ? '_day' : '',
scaledx: x >> 5, scaledx: x >> 5,
scaledy: y >> 5, scaledy: y >> 5,
zoom: this.zoomprefix(zoomoutlevel), zoom: this.zoomprefix(zoomoutlevel),
zoomprefix: (zoomoutlevel == 0) ? "" : (this.zoomprefix(zoomoutlevel) + "_"), zoomprefix: (zoomoutlevel == 0) ? "" : (this.zoomprefix(zoomoutlevel) + "_"),
x: x, x: x,
y: y, y: y,
fmt: this._mapSettings.imageFormat || 'png' fmt: this.options.imageFormat || 'png'
}; };
} }

View File

@ -14,33 +14,66 @@
* limitations under the License. * limitations under the License.
*/ */
import LiveAtlasMapDefinition from "@/model/LiveAtlasMapDefinition";
import {Map as LeafletMap, Coords, DomUtil, DoneCallback, TileLayer, TileLayerOptions, Util} from "leaflet"; import {Map as LeafletMap, Coords, DomUtil, DoneCallback, TileLayer, TileLayerOptions, Util} from "leaflet";
import {LiveAtlasInternalTiles, LiveAtlasTileElement} from "@/index"; import {LiveAtlasInternalTiles, LiveAtlasTileElement} from "@/index";
import falseFn = Util.falseFn; import falseFn = Util.falseFn;
import {ImageFormat} from "dynmap";
export interface LiveAtlasTileLayerOptions extends TileLayerOptions { export interface LiveAtlasTileLayerOptions {
mapSettings: LiveAtlasMapDefinition; baseUrl: string;
errorTileUrl: 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 // noinspection JSUnusedGlobalSymbols
export abstract class LiveAtlasTileLayer extends TileLayer { export abstract class LiveAtlasTileLayer extends TileLayer {
declare options: LiveAtlasTileLayerOptions; declare options: LiveAtlasTileLayerInternalOptions;
declare _tiles: LiveAtlasInternalTiles; declare _tiles: LiveAtlasInternalTiles;
declare _url: string;
protected _mapSettings: LiveAtlasMapDefinition; protected readonly tileTemplate: LiveAtlasTileElement;
private readonly tileTemplate: LiveAtlasTileElement;
protected readonly loadQueue: LiveAtlasTileElement[] = []; protected readonly loadQueue: LiveAtlasTileElement[] = [];
private readonly loadingTiles: Set<LiveAtlasTileElement> = Object.seal(new Set()); protected readonly loadingTiles: Set<LiveAtlasTileElement> = Object.seal(new Set());
private refreshTimeout?: ReturnType<typeof setTimeout>; 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) { protected constructor(options: LiveAtlasTileLayerOptions) {
super(url, options); 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 = DomUtil.create('img', 'leaflet-tile') as LiveAtlasTileElement;
this.tileTemplate.style.width = this.tileTemplate.style.height = this.options.tileSize + 'px'; this.tileTemplate.style.width = this.tileTemplate.style.height = this.options.tileSize + 'px';
this.tileTemplate.alt = ''; this.tileTemplate.alt = '';
@ -53,14 +86,6 @@ export abstract class LiveAtlasTileLayer extends TileLayer {
} }
Object.seal(this.tileTemplate); 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 // @method createTile(coords: Object, done?: Function): HTMLElement
@ -209,8 +234,8 @@ export abstract class LiveAtlasTileLayer extends TileLayer {
} }
onAdd(map: LeafletMap): this { onAdd(map: LeafletMap): this {
if(this._mapSettings.tileUpdateInterval) { if(this.options.tileUpdateInterval) {
this.refreshTimeout = setTimeout(() => this.handlePeriodicRefresh(), this._mapSettings.tileUpdateInterval); this.refreshTimeout = setTimeout(() => this.handlePeriodicRefresh(), this.options.tileUpdateInterval);
} }
return super.onAdd(map); return super.onAdd(map);
@ -229,6 +254,6 @@ export abstract class LiveAtlasTileLayer extends TileLayer {
this.refresh(); this.refresh();
} }
this.refreshTimeout = setTimeout(() => this.handlePeriodicRefresh(), this._mapSettings.tileUpdateInterval); this.refreshTimeout = setTimeout(() => this.handlePeriodicRefresh(), this.options.tileUpdateInterval);
} }
} }

View File

@ -22,23 +22,15 @@ import {Coords, Util} from "leaflet";
// noinspection JSUnusedGlobalSymbols // noinspection JSUnusedGlobalSymbols
export class OverviewerTileLayer extends LiveAtlasTileLayer { export class OverviewerTileLayer extends LiveAtlasTileLayer {
private readonly _baseUrl: string;
constructor(options: LiveAtlasTileLayerOptions) { constructor(options: LiveAtlasTileLayerOptions) {
super('', options); super(options);
options.zoomReverse = false; Util.setOptions(this, {zoomReverse: false});
Util.setOptions(this, options);
this._mapSettings = options.mapSettings;
this._baseUrl = options.mapSettings.baseUrl;
} }
getTileUrl(coords: Coords): string { getTileUrl(coords: Coords): string {
let url = this._mapSettings.name; let url = this.options.prefix;
const zoom = coords.z, const zoom = coords.z;
urlBase = this._mapSettings.prefix;
if(coords.x < 0 || coords.x >= Math.pow(2, zoom) || if(coords.x < 0 || coords.x >= Math.pow(2, zoom) ||
coords.y < 0 || coords.y >= 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 += '/' + (x + 2 * y);
} }
} }
url = url + '.' + this._mapSettings.imageFormat; url += `.${this.options.imageFormat}`;
// if(typeof overviewerConfig.map.cacheTag !== 'undefined') { // if(typeof overviewerConfig.map.cacheTag !== 'undefined') {
// url += '?c=' + overviewerConfig.map.cacheTag; // url += '?c=' + overviewerConfig.map.cacheTag;
// } // }
return(this._baseUrl + urlBase + url); return this.options.baseUrl + url;
} }
} }

View File

@ -19,11 +19,9 @@ import {Util} from "leaflet";
// noinspection JSUnusedGlobalSymbols // noinspection JSUnusedGlobalSymbols
export class Pl3xmapTileLayer extends LiveAtlasTileLayer { export class Pl3xmapTileLayer extends LiveAtlasTileLayer {
constructor(options: LiveAtlasTileLayerOptions) { constructor(map: LiveAtlasTileLayerOptions) {
super(`${options.mapSettings.baseUrl}${options.mapSettings.world.name}/{z}/{x}_{y}.png`, options); super(map);
this._url = `${map.baseUrl}{z}/{x}_{y}.png`;
options.zoomReverse = false; Util.setOptions(this, {zoomReverse: false});
Util.setOptions(this, options);
} }
} }

View File

@ -14,11 +14,12 @@
* limitations under the License. * limitations under the License.
*/ */
import {Coordinate, LiveAtlasProjection, LiveAtlasWorldDefinition} from "@/index"; import {Coordinate, LiveAtlasProjection, LiveAtlasTileLayerOverlay, LiveAtlasWorldDefinition} from "@/index";
import {LatLng} from "leaflet"; import {LatLng} from "leaflet";
import {ImageFormat} from "dynmap"; import {ImageFormat} from "dynmap";
import {LiveAtlasTileLayerOptions} from "@/leaflet/tileLayer/LiveAtlasTileLayer";
export interface LiveAtlasMapDefinitionOptions { export interface LiveAtlasMapDefinitionOptions extends LiveAtlasTileLayerOptions {
world: LiveAtlasWorldDefinition; world: LiveAtlasWorldDefinition;
appendedWorld?: LiveAtlasWorldDefinition; // append_to_world appendedWorld?: LiveAtlasWorldDefinition; // append_to_world
@ -45,9 +46,10 @@ export interface LiveAtlasMapDefinitionOptions {
tileUpdateInterval?: number; tileUpdateInterval?: number;
center?: Coordinate; center?: Coordinate;
overlays?: Map<string, LiveAtlasTileLayerOverlay>;
} }
export default class LiveAtlasMapDefinition { export default class LiveAtlasMapDefinition implements LiveAtlasTileLayerOptions {
readonly world: LiveAtlasWorldDefinition; readonly world: LiveAtlasWorldDefinition;
readonly appendedWorld?: LiveAtlasWorldDefinition; readonly appendedWorld?: LiveAtlasWorldDefinition;
@ -74,6 +76,7 @@ export default class LiveAtlasMapDefinition {
readonly tileUpdateInterval?: number; readonly tileUpdateInterval?: number;
readonly center?: Coordinate; readonly center?: Coordinate;
readonly overlays: Map<string, LiveAtlasTileLayerOverlay>;
readonly scale: number; readonly scale: number;
@ -105,6 +108,8 @@ export default class LiveAtlasMapDefinition {
this.tileUpdateInterval = options.tileUpdateInterval || undefined; this.tileUpdateInterval = options.tileUpdateInterval || undefined;
this.center = options.center || undefined; this.center = options.center || undefined;
this.overlays = options.overlays || new Map();
this.scale = (1 / Math.pow(2, this.nativeZoomLevels)); this.scale = (1 / Math.pow(2, this.nativeZoomLevels));
} }

View File

@ -120,7 +120,7 @@ export default class OverviewerMapProvider extends MapProvider {
name: tileset.path, name: tileset.path,
displayName: tileset.name || tileset.path, displayName: tileset.name || tileset.path,
baseUrl: this.config, baseUrl: `${this.config}${tileset.base}/${tileset.path}`,
tileSize, tileSize,
projection: new OverviewerProjection({ projection: new OverviewerProjection({
upperRight: serverResponse.CONST.UPPERRIGHT, upperRight: serverResponse.CONST.UPPERRIGHT,

View File

@ -187,7 +187,7 @@ export default class Pl3xmapMapProvider extends MapProvider {
displayName: 'Flat', displayName: 'Flat',
icon: world.icon ? `${this.config}images/icon/${world.icon}.png` : undefined, icon: world.icon ? `${this.config}images/icon/${world.icon}.png` : undefined,
baseUrl: `${this.config}tiles/`, baseUrl: `${this.config}tiles/${w.name}/`,
imageFormat: 'png', imageFormat: 'png',
tileSize: 512, tileSize: 512,

View File

@ -143,7 +143,7 @@ export function buildWorlds(response: Configuration, config: DynmapUrlConfig): A
displayName: map.title, displayName: map.title,
icon: (map.icon || undefined) as string | undefined, icon: (map.icon || undefined) as string | undefined,
baseUrl: config.tiles, baseUrl: `${config.tiles}${actualWorld.name}/`,
imageFormat: map['image-format'] || 'png', imageFormat: map['image-format'] || 'png',
tileSize, tileSize,
projection: new DynmapProjection({ projection: new DynmapProjection({