diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml
index 58a1b80..109ed46 100644
--- a/.idea/copyright/profiles_settings.xml
+++ b/.idea/copyright/profiles_settings.xml
@@ -3,6 +3,7 @@
+
\ No newline at end of file
diff --git a/.idea/scopes/Original.xml b/.idea/scopes/Original.xml
index bd38b95..9c2f03c 100644
--- a/.idea/scopes/Original.xml
+++ b/.idea/scopes/Original.xml
@@ -1,3 +1,3 @@
-
-
+
+
\ No newline at end of file
diff --git a/src/index.d.ts b/src/index.d.ts
index 4d3f468..297fde3 100644
--- a/src/index.d.ts
+++ b/src/index.d.ts
@@ -1,5 +1,5 @@
/*
- * Copyright 2021 James Lyne
+ * 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.
@@ -104,6 +104,7 @@ interface LiveAtlasServerDefinition {
dynmap?: DynmapUrlConfig;
pl3xmap?: string;
squaremap?: string;
+ overviewer?: string;
}
// Messages defined directly in LiveAtlas and used for all servers
diff --git a/src/leaflet/projection/OverviewerProjection.ts b/src/leaflet/projection/OverviewerProjection.ts
new file mode 100644
index 0000000..0e6f35b
--- /dev/null
+++ b/src/leaflet/projection/OverviewerProjection.ts
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2022 James Lyne
+ *
+ * Some portions of this file were taken from https://github.com/overviewer/Minecraft-Overviewer.
+ * These portions are Copyright 2022 Minecraft Overviewer 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.
+ */
+
+import {LatLng} from 'leaflet';
+import {Coordinate, LiveAtlasProjection} from "@/index";
+
+export interface OverviewerProjectionOptions {
+ upperRight: number,
+ lowerRight: number,
+ lowerLeft: number,
+ northDirection: number,
+ nativeZoomLevels: number,
+ tileSize: number,
+}
+
+export class OverviewerProjection implements LiveAtlasProjection {
+ private readonly upperRight: number;
+ private readonly lowerRight: number;
+ private readonly lowerLeft: number;
+ private readonly northDirection: number;
+ private readonly nativeZoomLevels: number;
+ private readonly tileSize: number;
+ private readonly perPixel: number;
+
+ constructor(options: OverviewerProjectionOptions) {
+ this.upperRight = options.upperRight;
+ this.lowerRight = options.lowerRight;
+ this.lowerLeft = options.lowerLeft;
+ this.northDirection = options.northDirection;
+ this.nativeZoomLevels = options.nativeZoomLevels || 1;
+ this.tileSize = options.tileSize;
+
+ this.perPixel = 1.0 / (this.tileSize * Math.pow(2, this.nativeZoomLevels));
+ }
+
+ locationToLatLng(location: Coordinate): LatLng {
+ let lng = 0.5 - (1.0 / Math.pow(2, this.nativeZoomLevels + 1));
+ let lat = 0.5;
+
+ if (this.northDirection === this.upperRight) {
+ const temp = location.x;
+ location.x = -location.z + 15;
+ location.z = temp;
+ } else if(this.northDirection === this.lowerRight) {
+ location.x = -location.x + 15;
+ location.z = -location.z + 15;
+ } else if(this.northDirection === this.lowerLeft) {
+ const temp = location.x;
+ location.x = location.z;
+ location.z = -temp + 15;
+ }
+
+ lng += 12 * location.x * this.perPixel;
+ lat -= 6 * location.x * this.perPixel;
+
+ lng += 12 * location.z * this.perPixel;
+ lat += 6 * location.z * this.perPixel;
+
+ lng += 12 * this.perPixel;
+ lat += 12 * (256 - location.y) * this.perPixel;
+
+ return new LatLng(-lat * this.tileSize, lng * this.tileSize);
+ }
+
+ latLngToLocation(latLng: LatLng, y: number): Coordinate {
+ const lat = (-latLng.lat / this.tileSize) - 0.5;
+ const lng = (latLng.lng / this.tileSize) - (0.5 - (1.0 / Math.pow(2, this.nativeZoomLevels + 1)));
+
+ const x = Math.floor((lng - 2 * lat) / (24 * this.perPixel)) + (256 - y),
+ z = Math.floor((lng + 2 * lat) / (24 * this.perPixel)) - (256 - y);
+
+ if (this.northDirection == this.upperRight) {
+ return {x: z, y, z: -x + 15}
+ } else if (this.northDirection == this.lowerRight) {
+ return {x: -x + 15, y, z: -y + 15}
+ } else if (this.northDirection == this.lowerLeft) {
+ return {x: -z + 15, y, z: x}
+ }
+
+ return {x, y, z};
+ }
+}
diff --git a/src/leaflet/tileLayer/OverviewerTileLayer.ts b/src/leaflet/tileLayer/OverviewerTileLayer.ts
new file mode 100644
index 0000000..5b225eb
--- /dev/null
+++ b/src/leaflet/tileLayer/OverviewerTileLayer.ts
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2022 James Lyne
+ *
+ * Some portions of this file were taken from https://github.com/overviewer/Minecraft-Overviewer.
+ * These portions are Copyright 2022 Minecraft Overviewer 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.
+ */
+
+import {LiveAtlasTileLayer, LiveAtlasTileLayerOptions} from "@/leaflet/tileLayer/LiveAtlasTileLayer";
+import {Store, useStore} from "@/store";
+import {Coords, Util} from "leaflet";
+
+// noinspection JSUnusedGlobalSymbols
+export class OverviewerTileLayer extends LiveAtlasTileLayer {
+ private readonly _baseUrl: string;
+ private readonly _store: Store = useStore();
+
+ constructor(options: LiveAtlasTileLayerOptions) {
+ super('', options);
+
+ options.zoomReverse = false;
+
+ Util.setOptions(this, options);
+
+ this._mapSettings = options.mapSettings;
+ this._baseUrl = this._store.state.currentMapProvider!.getTilesUrl();
+ }
+
+ getTileUrl(coords: Coords): string {
+ let url = this._mapSettings.name;
+ const zoom = coords.z,
+ urlBase = this._mapSettings.prefix;
+
+ if(coords.x < 0 || coords.x >= Math.pow(2, zoom) ||
+ coords.y < 0 || coords.y >= Math.pow(2, zoom)) {
+ url += '/blank';
+ } else if(zoom === 0) {
+ url += '/base';
+ } else {
+ for(let z = zoom - 1; z >= 0; --z) {
+ const x = Math.floor(coords.x / Math.pow(2, z)) % 2;
+ const y = Math.floor(coords.y / Math.pow(2, z)) % 2;
+ url += '/' + (x + 2 * y);
+ }
+ }
+ url = url + '.' + this._mapSettings.imageFormat;
+ // if(typeof overviewerConfig.map.cacheTag !== 'undefined') {
+ // url += '?c=' + overviewerConfig.map.cacheTag;
+ // }
+ return(this._baseUrl + urlBase + url);
+ }
+}
diff --git a/src/main.ts b/src/main.ts
index 7974a37..c7eaa37 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,5 +1,5 @@
/*
- * Copyright 2021 James Lyne
+ * 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.
@@ -30,6 +30,7 @@ import DynmapMapProvider from "@/providers/DynmapMapProvider";
import Pl3xmapMapProvider from "@/providers/Pl3xmapMapProvider";
import {showSplashError} from "@/util/splash";
import ConfigurationError from "@/errors/ConfigurationError";
+import OverviewerMapProvider from "@/providers/OverviewerMapProvider";
const splash = document.getElementById('splash'),
svgs = import.meta.globEager('/assets/icons/*.svg');
@@ -55,6 +56,7 @@ store.subscribe((mutation, state) => {
registerMapProvider('dynmap', DynmapMapProvider);
registerMapProvider('pl3xmap', Pl3xmapMapProvider);
registerMapProvider('squaremap', Pl3xmapMapProvider);
+registerMapProvider('overviewer', OverviewerMapProvider);
const config = window.liveAtlasConfig;
diff --git a/src/providers/MapProvider.ts b/src/providers/MapProvider.ts
index 6ac6122..a81a681 100644
--- a/src/providers/MapProvider.ts
+++ b/src/providers/MapProvider.ts
@@ -1,5 +1,5 @@
/*
- * Copyright 2021 James Lyne
+ * 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.
@@ -56,8 +56,8 @@ export default abstract class MapProvider implements LiveAtlasMapProvider {
throw new Error('Provider does not support registration');
}
- protected static async fetchJSON(url: string, options: any) {
- let response, json;
+ protected static async fetch(url: string, options: any) {
+ let response;
try {
response = await fetch(url, options);
@@ -76,6 +76,30 @@ export default abstract class MapProvider implements LiveAtlasMapProvider {
throw new Error(`Network request failed (${response.statusText || 'Unknown'})`);
}
+ return response;
+ }
+
+ protected static async fetchText(url: string, options: any) {
+ const response = await this.fetch(url, options);
+ let text;
+
+ try {
+ text = await response.text();
+ } catch(e) {
+ if(e instanceof DOMException && e.name === 'AbortError') {
+ console.warn(`Request aborted (${url}`);
+ }
+
+ throw e;
+ }
+
+ return text;
+ }
+
+ protected static async fetchJSON(url: string, options: any) {
+ const response = await this.fetch(url, options);
+ let json;
+
try {
json = await response.json();
} catch(e) {
@@ -90,6 +114,10 @@ export default abstract class MapProvider implements LiveAtlasMapProvider {
return json;
}
+ protected static async getText(url: string, signal: AbortSignal) {
+ return MapProvider.fetchText(url, {signal, credentials: 'include'});
+ }
+
protected static async getJSON(url: string, signal: AbortSignal) {
return MapProvider.fetchJSON(url, {signal, credentials: 'include'});
}
diff --git a/src/providers/OverviewerMapProvider.ts b/src/providers/OverviewerMapProvider.ts
new file mode 100644
index 0000000..7de29db
--- /dev/null
+++ b/src/providers/OverviewerMapProvider.ts
@@ -0,0 +1,215 @@
+/*
+ * Copyright 2022 James Lyne
+ *
+ * Some portions of this file were taken from https://github.com/overviewer/Minecraft-Overviewer.
+ * These portions are Copyright 2022 Minecraft Overviewer 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.
+ */
+
+import {
+ LiveAtlasComponentConfig, LiveAtlasDimension,
+ LiveAtlasServerConfig,
+ LiveAtlasServerMessageConfig,
+ LiveAtlasWorldDefinition
+} from "@/index";
+import {MutationTypes} from "@/store/mutation-types";
+import MapProvider from "@/providers/MapProvider";
+import {
+ getDefaultMinecraftHead, runSandboxed,
+} from "@/util";
+import ConfigurationError from "@/errors/ConfigurationError";
+import {LiveAtlasTileLayer, LiveAtlasTileLayerOptions} from "@/leaflet/tileLayer/LiveAtlasTileLayer";
+import {OverviewerTileLayer} from "@/leaflet/tileLayer/OverviewerTileLayer";
+import LiveAtlasMapDefinition from "@/model/LiveAtlasMapDefinition";
+import {OverviewerProjection} from "@/leaflet/projection/OverviewerProjection";
+
+export default class OverviewerMapProvider extends MapProvider {
+ private configurationAbort?: AbortController = undefined;
+
+ constructor(config: string) {
+ super(config);
+
+ if(!this.config) {
+ throw new ConfigurationError("URL missing");
+ }
+
+ if(this.config.slice(-1) !== '/') {
+ this.config = `${config}/`;
+ }
+ }
+
+ private static buildServerConfig(response: any): LiveAtlasServerConfig {
+ return {
+ title: 'Minecraft Overviewer',
+
+ //Not used by overviewer
+ expandUI: false,
+ defaultZoom: 1,
+ defaultMap: undefined,
+ defaultWorld: undefined,
+ followMap: undefined,
+ followZoom: undefined,
+ };
+ }
+
+ private static buildMessagesConfig(response: any): LiveAtlasServerMessageConfig {
+ return {
+ worldsHeading: 'Worlds',
+ playersHeading: 'Players',
+
+ //Not used by pl3xmap
+ chatPlayerJoin: '',
+ chatPlayerQuit: '',
+ chatAnonymousJoin: '',
+ chatAnonymousQuit: '',
+ chatErrorNotAllowed: '',
+ chatErrorRequiresLogin: '',
+ chatErrorCooldown: '',
+ }
+ }
+
+ private buildWorlds(serverResponse: any): Array {
+ const worlds: Map = new Map();
+
+ (serverResponse.worlds || []).forEach((world: string) => {
+ worlds.set(world, {
+ name: world,
+ displayName: world,
+ dimension: 'overworld' as LiveAtlasDimension,
+ seaLevel: 64,
+ center: {x: 0, y: 64, z: 0},
+ defaultZoom: undefined,
+ maps: new Set(),
+ });
+ });
+
+ (serverResponse.tilesets || []).forEach((tileset: any) => {
+ if(!tileset?.world || !worlds.has(tileset.world)) {
+ console.warn(`Ignoring tileset with unknown world ${tileset.world}`);
+ return;
+ }
+
+ const world = worlds.get(tileset.world) as LiveAtlasWorldDefinition,
+ nativeZoomLevels = tileset.zoomLevels,
+ tileSize = serverResponse.CONST.tileSize;
+
+ world.maps.add(new LiveAtlasMapDefinition({
+ world,
+ name: tileset.path,
+ displayName: tileset.name || tileset.path,
+ background: tileset.bgcolor,
+ imageFormat: tileset.imgextension,
+ nativeZoomLevels,
+ extraZoomLevels: 0,
+ tileSize,
+ prefix: tileset.base,
+ projection: new OverviewerProjection({
+ upperRight: serverResponse.CONST.UPPERRIGHT,
+ lowerLeft: serverResponse.CONST.LOWERLEFT,
+ lowerRight: serverResponse.CONST.LOWERRIGHT,
+ northDirection: tileset.north_direction,
+ nativeZoomLevels,
+ tileSize,
+ }),
+ }));
+ });
+
+ return Array.from(worlds.values());
+ }
+
+ private static buildComponents(response: any): LiveAtlasComponentConfig {
+ const components: LiveAtlasComponentConfig = {
+ coordinatesControl: undefined,
+ linkControl: true,
+ layerControl: response?.map?.controls?.overlays,
+
+ //Not configurable
+ markers: {
+ showLabels: false,
+ },
+
+ //Not used by Overviewer
+ players: {
+ markers: undefined,
+ imageUrl: getDefaultMinecraftHead,
+ showImages: false,
+ grayHiddenPlayers: false,
+ },
+ chatBox: undefined,
+ chatBalloons: false,
+ clockControl: undefined,
+ logoControls: [],
+ login: false,
+ };
+
+ if(response?.map?.controls?.coordsBox) {
+ components.coordinatesControl = {
+ showY: false,
+ label: 'Location: ',
+ showRegion: true,
+ showChunk: false,
+ }
+ }
+
+ return components;
+ }
+
+ async loadServerConfiguration(): Promise {
+ if(this.configurationAbort) {
+ this.configurationAbort.abort();
+ }
+
+ this.configurationAbort = new AbortController();
+
+ const baseUrl = this.config,
+ response = await OverviewerMapProvider.getText(`${baseUrl}overviewerConfig.js`, this.configurationAbort.signal);
+
+ try {
+ const result = await runSandboxed(response + ' return overviewerConfig;'),
+ config = OverviewerMapProvider.buildServerConfig(result);
+
+ this.store.commit(MutationTypes.SET_SERVER_CONFIGURATION, config);
+ this.store.commit(MutationTypes.SET_SERVER_MESSAGES, OverviewerMapProvider.buildMessagesConfig(result));
+ this.store.commit(MutationTypes.SET_WORLDS, this.buildWorlds(result));
+ this.store.commit(MutationTypes.SET_COMPONENTS, OverviewerMapProvider.buildComponents(result));
+ } catch(e) {
+ console.error(e);
+ throw e;
+ }
+ }
+
+ async populateWorld(world: LiveAtlasWorldDefinition) {
+ //TODO
+ }
+
+ createTileLayer(options: LiveAtlasTileLayerOptions): LiveAtlasTileLayer {
+ return new OverviewerTileLayer(options);
+ }
+
+ startUpdates() {
+ //TODO
+ }
+
+ stopUpdates() {
+ //TODO
+ }
+
+ getTilesUrl(): string {
+ return this.config;
+ }
+
+ getMarkerIconUrl(icon: string): string {
+ return ''; //TODO
+ }
+}
diff --git a/src/util.ts b/src/util.ts
index de6837c..e1656c9 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -19,8 +19,10 @@ import {useStore} from "@/store";
import LiveAtlasMapDefinition from "@/model/LiveAtlasMapDefinition";
import {
Coordinate,
- HeadQueueEntry, LiveAtlasBounds,
- LiveAtlasGlobalMessageConfig, LiveAtlasLocation,
+ HeadQueueEntry,
+ LiveAtlasBounds,
+ LiveAtlasGlobalMessageConfig,
+ LiveAtlasLocation,
LiveAtlasMessageConfig,
LiveAtlasPlayer,
LiveAtlasPlayerImageSize,
@@ -297,3 +299,73 @@ export const getMiddle = (bounds: LiveAtlasBounds): LiveAtlasLocation => {
z: bounds.min.z + ((bounds.max.z - bounds.min.z) / 2),
};
}
+
+const createIframeSandbox = () => {
+ const frame = document.createElement('iframe');
+ frame.hidden = true;
+ frame.sandbox.add('allow-scripts');
+ frame.srcdoc = ``;
+
+ window.addEventListener('message', e => {
+ if(e.origin !== "null" || e.source !== frame.contentWindow) {
+ console.warn('Ignoring postmessage with invalid source');
+ return;
+ }
+
+ if(!e.data?.key) {
+ console.warn('Ignoring postmessage without key');
+ return;
+ }
+
+ if(!sandboxSuccessCallbacks.has(e.data.key)) {
+ console.warn('Ignoring postmessage with invalid key');
+ return;
+ }
+
+ if(e.data.success) {
+ sandboxSuccessCallbacks.get(e.data.key)!.call(this, e.data.result);
+ } else {
+ sandboxErrorCallbacks.get(e.data.key)!.call(this, e.data.error);
+ }
+ });
+
+ document.body.appendChild(frame);
+ return frame.contentWindow;
+}
+
+const sandboxWindow: Window | null = createIframeSandbox();
+const sandboxSuccessCallbacks: Map void> = new Map();
+const sandboxErrorCallbacks: Map void> = new Map();
+
+export const runSandboxed = async (code: string) => {
+ return new Promise((resolve, reject) => {
+ const key = Math.random();
+
+ sandboxSuccessCallbacks.set(key, resolve);
+ sandboxErrorCallbacks.set(key, reject);
+
+ sandboxWindow!.postMessage({
+ key,
+ code,
+ }, '*');
+ });
+}