Basic support for overviewer

This commit is contained in:
James Lyne 2022-02-21 21:50:31 +00:00
parent 68eccb2b5b
commit b43f1f0fe6
9 changed files with 489 additions and 9 deletions

View File

@ -3,6 +3,7 @@
<module2copyright>
<element module="Dynmap" copyright="Dynmap" />
<element module="Original" copyright="Original" />
<element module="Overviewer." copyright="Overviewer" />
</module2copyright>
</settings>
</component>

View File

@ -1,3 +1,3 @@
<component name="DependencyValidationManager">
<scope name="Original" pattern="!file:src/leaflet/control/ClockControl.ts&amp;&amp;!file:src/leaflet/control/CoordinatesControl.ts&amp;&amp;!file:src/leaflet/control/LinkControl.ts&amp;&amp;!file:src/leaflet/control/LogoControl.ts&amp;&amp;!file:src/leaflet/icon/PlayerIcon.ts&amp;&amp;!file:src/leaflet/icon/GenericIcon.ts&amp;&amp;!file:src/leaflet/tileLayer/DynmapTileLayer.ts&amp;&amp;!file:src/util/areas.ts&amp;&amp;!file:src/util/circles.ts&amp;&amp;!file:src/util/lines.ts&amp;&amp;!file:src/util/markers.ts&amp;&amp;!file[LiveAtlas]:standalone/*&amp;&amp;!file:src/model/LiveAtlasProjection.ts&amp;&amp;!file:src/leaflet/control/LiveAtlasLayerControl.ts&amp;&amp;!file[LiveAtlas]:patches/*&amp;&amp;!file[LiveAtlas]:public/*&amp;&amp;!file[LiveAtlas]:.idea/*&amp;&amp;!file[LiveAtlas]:.idea//*&amp;&amp;!file[LiveAtlas]:patches//*&amp;&amp;!file[LiveAtlas]:public//*&amp;&amp;!file[LiveAtlas]:standalone//*&amp;&amp;!file:FUNDING.yml&amp;&amp;!file:README.md&amp;&amp;!file:tsconfig.json&amp;&amp;!file:.gitignore&amp;&amp;!file:.env&amp;&amp;!file:LICENSE.md&amp;&amp;!file:package-lock.json&amp;&amp;!file:package.json&amp;&amp;!file:vite.config.ts&amp;&amp;!file:index.html&amp;&amp;!file:src/leaflet/control/LoadingControl.ts&amp;&amp;!file:src/scss/style.scss&amp;&amp;!file[LiveAtlas]:src/assets/icons//*" />
</component>
<scope name="Original" pattern="!file:src/leaflet/control/ClockControl.ts&amp;&amp;!file:src/leaflet/control/CoordinatesControl.ts&amp;&amp;!file:src/leaflet/control/LinkControl.ts&amp;&amp;!file:src/leaflet/control/LogoControl.ts&amp;&amp;!file:src/leaflet/icon/PlayerIcon.ts&amp;&amp;!file:src/leaflet/icon/GenericIcon.ts&amp;&amp;!file:src/leaflet/tileLayer/DynmapTileLayer.ts&amp;&amp;!file:src/util/areas.ts&amp;&amp;!file:src/util/circles.ts&amp;&amp;!file:src/util/lines.ts&amp;&amp;!file:src/util/markers.ts&amp;&amp;!file[LiveAtlas]:standalone/*&amp;&amp;!file:src/model/LiveAtlasProjection.ts&amp;&amp;!file:src/leaflet/control/LiveAtlasLayerControl.ts&amp;&amp;!file[LiveAtlas]:patches/*&amp;&amp;!file[LiveAtlas]:public/*&amp;&amp;!file[LiveAtlas]:.idea/*&amp;&amp;!file[LiveAtlas]:.idea//*&amp;&amp;!file[LiveAtlas]:patches//*&amp;&amp;!file[LiveAtlas]:public//*&amp;&amp;!file[LiveAtlas]:standalone//*&amp;&amp;!file:FUNDING.yml&amp;&amp;!file:README.md&amp;&amp;!file:tsconfig.json&amp;&amp;!file:.gitignore&amp;&amp;!file:.env&amp;&amp;!file:LICENSE.md&amp;&amp;!file:package-lock.json&amp;&amp;!file:package.json&amp;&amp;!file:vite.config.ts&amp;&amp;!file:index.html&amp;&amp;!file:src/leaflet/control/LoadingControl.ts&amp;&amp;!file:src/scss/style.scss&amp;&amp;!file[LiveAtlas]:src/assets/icons//*&amp;&amp;!file:src/providers/OverviewerMapProvider.ts&amp;&amp;!file:src/providers/DynmapMapProvider.ts&amp;&amp;!file:src/leaflet/projection/OverviewerProjection.ts&amp;&amp;!file:src/leaflet/tileLayer/OverviewerTileLayer.ts" />
</component>

3
src/index.d.ts vendored
View File

@ -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

View File

@ -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};
}
}

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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'});
}

View File

@ -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<LiveAtlasWorldDefinition> {
const worlds: Map<string, LiveAtlasWorldDefinition> = new Map<string, LiveAtlasWorldDefinition>();
(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<LiveAtlasMapDefinition>(),
});
});
(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<void> {
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
}
}

View File

@ -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 = `<script>window.addEventListener("message", function(e) {
if(!e.data?.key) {
console.warn('Ignoring postmessage without key');
return;
}
try {
e.source.postMessage({
key: e.data.key,
success: true,
result: Function('', "'use strict';" + e.data.code)(),
}, e.origin);
} catch(ex) {
e.source.postMessage({
key: e.data.key,
success: false,
error: ex
}, e.origin);
}
})</script>`;
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<number, (result?: any) => void> = new Map();
const sandboxErrorCallbacks: Map<number, (reason?: any) => 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,
}, '*');
});
}