Changed: Squaremap configs support LiveAtlas & Squaremap Markers

This commit is contained in:
2024-12-10 22:53:03 +00:00
parent 9e6198b15a
commit b1cb21fddf
162 changed files with 2337 additions and 2 deletions

View File

@@ -0,0 +1,243 @@
/**
* Copyright 2014 JD Fergason
*
* 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.
*/
// https://github.com/jdfergason/Leaflet.Ellipse
L.SVG.include ({
_updateEllipse: function (layer) {
var rx = layer._radiusX,
ry = layer._radiusY,
phi = layer._tiltDeg,
endPoint = layer._endPointParams;
var d = 'M' + endPoint.x0 + ',' + endPoint.y0 +
'A' + rx + ',' + ry + ',' + phi + ',' +
endPoint.largeArc + ',' + endPoint.sweep + ',' +
endPoint.x1 + ',' + endPoint.y1 + ' z';
this._setPath(layer, d);
}
});
L.Canvas.include ({
_updateEllipse: function (layer) {
if (layer._empty()) { return; }
var p = layer._point,
ctx = this._ctx,
r = layer._radiusX,
s = (layer._radiusY || r) / r;
// this breaks "preferCanvas: true"
//this._drawnLayers[layer._leaflet_id] = layer;
ctx.save();
ctx.translate(p.x, p.y);
if (layer._tilt !== 0) {
ctx.rotate( layer._tilt );
}
if (s !== 1) {
ctx.scale(1, s);
}
ctx.beginPath();
ctx.arc(0, 0, r, 0, Math.PI * 2);
ctx.restore();
this._fillStroke(ctx, layer);
},
});
L.Ellipse = L.Path.extend({
options: {
fill: true,
startAngle: 0,
endAngle: 359.9
},
initialize: function (latlng, radii, tilt, options) {
L.setOptions(this, options);
this._latlng = L.latLng(latlng);
if (tilt) {
this._tiltDeg = tilt;
} else {
this._tiltDeg = 0;
}
if (radii) {
this._mRadiusX = radii[0];
this._mRadiusY = radii[1];
}
},
setRadius: function (radii) {
this._mRadiusX = radii[0];
this._mRadiusY = radii[1];
return this.redraw();
},
getRadius: function () {
return new L.point(this._mRadiusX, this._mRadiusY);
},
setTilt: function (tilt) {
this._tiltDeg = tilt;
return this.redraw();
},
getBounds: function () {
// TODO respect tilt (bounds are too big)
var lngRadius = this._getLngRadius(),
latRadius = this._getLatRadius(),
latlng = this._latlng;
return new L.LatLngBounds(
[latlng.lat - latRadius, latlng.lng - lngRadius],
[latlng.lat + latRadius, latlng.lng + lngRadius]);
},
// @method setLatLng(latLng: LatLng): this
// Sets the position of a circle marker to a new location.
setLatLng: function (latlng) {
this._latlng = L.latLng(latlng);
this.redraw();
return this.fire('move', {latlng: this._latlng});
},
// @method getLatLng(): LatLng
// Returns the current geographical position of the circle marker
getLatLng: function () {
return this._latlng;
},
setStyle: L.Path.prototype.setStyle,
_project: function () {
var lngRadius = this._getLngRadius(),
latRadius = this._getLatRadius(),
latlng = this._latlng,
pointLeft = this._map.latLngToLayerPoint([latlng.lat, latlng.lng - lngRadius]),
pointBelow = this._map.latLngToLayerPoint([latlng.lat - latRadius, latlng.lng]);
this._point = this._map.latLngToLayerPoint(latlng);
this._radiusX = Math.max(this._point.x - pointLeft.x, 1) * this._map.options.scale;
this._radiusY = Math.max(pointBelow.y - this._point.y, 1) * this._map.options.scale;
this._tilt = Math.PI * this._tiltDeg / 180;
this._endPointParams = this._centerPointToEndPoint();
this._updateBounds();
},
_updateBounds: function () {
// http://math.stackexchange.com/questions/91132/how-to-get-the-limits-of-rotated-ellipse
var sin = Math.sin(this._tilt);
var cos = Math.cos(this._tilt);
var sinSquare = sin * sin;
var cosSquare = cos * cos;
var aSquare = this._radiusX * this._radiusX;
var bSquare = this._radiusY * this._radiusY;
var halfWidth = Math.sqrt(aSquare*cosSquare+bSquare*sinSquare);
var halfHeight = Math.sqrt(aSquare*sinSquare+bSquare*cosSquare);
var w = this._clickTolerance();
var p = [halfWidth + w, halfHeight + w];
this._pxBounds = new L.Bounds(this._point.subtract(p), this._point.add(p));
},
_update: function () {
if (this._map) {
this._updatePath();
}
},
_updatePath: function () {
this._renderer._updateEllipse(this);
},
_getLatRadius: function () {
var simpleCrs = !!this._map.options.crs.infinite;
if(simpleCrs)
return this._mRadiusY;
else
return (this._mRadiusY / 40075017) * 360;
},
_getLngRadius: function () {
var simpleCrs = !!this._map.options.crs.infinite;
if(simpleCrs)
return this._mRadiusX;
else
return ((this._mRadiusX / 40075017) * 360) / Math.cos((Math.PI / 180) * this._latlng.lat);
},
_centerPointToEndPoint: function () {
// Convert between center point parameterization of an ellipse
// too SVG's end-point and sweep parameters. This is an
// adaptation of the perl code found here:
// http://commons.oreilly.com/wiki/index.php/SVG_Essentials/Paths
var c = this._point,
rx = this._radiusX,
ry = this._radiusY,
theta2 = (this.options.startAngle + this.options.endAngle) * (Math.PI / 180),
theta1 = this.options.startAngle * (Math.PI / 180),
delta = this.options.endAngle,
phi = this._tiltDeg * (Math.PI / 180);
// Determine start and end-point coordinates
var x0 = c.x + Math.cos(phi) * rx * Math.cos(theta1) +
Math.sin(-phi) * ry * Math.sin(theta1);
var y0 = c.y + Math.sin(phi) * rx * Math.cos(theta1) +
Math.cos(phi) * ry * Math.sin(theta1);
var x1 = c.x + Math.cos(phi) * rx * Math.cos(theta2) +
Math.sin(-phi) * ry * Math.sin(theta2);
var y1 = c.y + Math.sin(phi) * rx * Math.cos(theta2) +
Math.cos(phi) * ry * Math.sin(theta2);
var largeArc = (delta > 180) ? 1 : 0;
var sweep = (delta > 0) ? 1 : 0;
return {'x0': x0, 'y0': y0, 'tilt': phi, 'largeArc': largeArc,
'sweep': sweep, 'x1': x1, 'y1': y1};
},
_empty: function () {
return this._radiusX && this._radiusY && !this._renderer._bounds.intersects(this._pxBounds);
},
_containsPoint : function (p) {
// http://stackoverflow.com/questions/7946187/point-and-ellipse-rotated-position-test-algorithm
var sin = Math.sin(this._tilt);
var cos = Math.cos(this._tilt);
var dx = p.x - this._point.x;
var dy = p.y - this._point.y;
var sumA = cos * dx + sin * dy;
var sumB = sin * dx - cos * dy;
if (this.options.fill === false) {
var x = this._radiusX - this.options.weight;
var y = this._radiusY - this.options.weight;
if (sumA * sumA / (x * x) + sumB * sumB / (y * y) <= 1) {
return false;
}
}
return sumA * sumA / (this._radiusX * this._radiusX) + sumB * sumB / (this._radiusY * this._radiusY) <= 1;
}
});
L.ellipse = function (latlng, radii, tilt, options) {
return new L.Ellipse(latlng, radii, tilt, options);
};

View File

@@ -0,0 +1,82 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2015 Benjamin Becquet
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* https://github.com/bbecquet/Leaflet.RotatedMarker
*/
(function() {
// save these original methods before they are overwritten
var proto_initIcon = L.Marker.prototype._initIcon;
var proto_setPos = L.Marker.prototype._setPos;
var oldIE = (L.DomUtil.TRANSFORM === 'msTransform');
L.Marker.addInitHook(function () {
var iconOptions = this.options.icon && this.options.icon.options;
var iconAnchor = iconOptions && this.options.icon.options.iconAnchor;
if (iconAnchor) {
iconAnchor = (iconAnchor[0] + 'px ' + iconAnchor[1] + 'px');
}
this.options.rotationOrigin = this.options.rotationOrigin || iconAnchor || 'center bottom' ;
this.options.rotationAngle = this.options.rotationAngle || 0;
// Ensure marker keeps rotated during dragging
this.on('drag', function(e) { e.target._applyRotation(); });
});
L.Marker.include({
_initIcon: function() {
proto_initIcon.call(this);
},
_setPos: function (pos) {
proto_setPos.call(this, pos);
this._applyRotation();
},
_applyRotation: function () {
if(this.options.rotationAngle) {
this._icon.style[L.DomUtil.TRANSFORM+'Origin'] = this.options.rotationOrigin;
if(oldIE) {
// for IE 9, use the 2D rotation
this._icon.style[L.DomUtil.TRANSFORM] = 'rotate(' + this.options.rotationAngle + 'deg)';
} else {
// for modern browsers, prefer the 3D accelerated version
this._icon.style[L.DomUtil.TRANSFORM] += ' rotateZ(' + this.options.rotationAngle + 'deg)';
}
}
},
setRotationAngle: function(angle) {
this.options.rotationAngle = angle;
this.update();
return this;
},
setRotationOrigin: function(origin) {
this.options.rotationOrigin = origin;
this.update();
return this;
}
});
})();

View File

@@ -0,0 +1,106 @@
import { P } from './Squaremap.js';
import { SquaremapTileLayer } from './SquaremapTileLayer.js';
class LayerControl {
constructor() {
this.layers = new Map();
}
init() {
this.currentLayer = 0;
this.updateInterval = 60;
this.playersLayer = new L.LayerGroup();
this.playersLayer.id = "players_layer";
this.controls = L.control.layers({}, {}, {
position: 'topleft',
sortLayers: true,
sortFunction: (a, b) => {
return a.order - b.order;
}
})
.addTo(P.map);
}
addOverlay(name, layer, hide) {
this.controls.addOverlay(layer, name);
if (this.shouldHide(layer, hide) !== true) {
layer.addTo(P.map);
}
}
removeOverlay(layer) {
this.ignoreLayer = layer;
this.controls.removeLayer(layer);
layer.remove();
this.ignoreLayer = null;
}
shouldHide(layer, def) {
const value = window.localStorage.getItem(`hide_${layer.id}`);
return value == null ? def : value === 'true';
}
hideLayer(layer) {
if (layer != this.ignoreLayer) {
window.localStorage.setItem(`hide_${layer.id}`, 'true');
}
}
showLayer(layer) {
if (layer != this.ignoreLayer) {
window.localStorage.setItem(`hide_${layer.id}`, 'false');
}
}
setupTileLayers(world) {
// setup the map tile layers
// we need 2 layers to swap between for seamless refreshing
if (this.tileLayer1 != null) {
P.map.removeLayer(this.tileLayer1);
}
if (this.tileLayer2 != null) {
P.map.removeLayer(this.tileLayer2);
}
this.tileLayer1 = this.createTileLayer(world);
this.tileLayer2 = this.createTileLayer(world);
// refresh player's control
this.removeOverlay(this.playersLayer);
if (world.player_tracker.show_controls) {
this.addOverlay(world.player_tracker.label,
this.playersLayer,
world.player_tracker.default_hidden);
}
this.playersLayer.order = world.player_tracker.priority;
this.playersLayer.setZIndex(world.player_tracker.z_index);
}
createTileLayer(world) {
return new SquaremapTileLayer(`tiles/${world.name}/{z}/{x}_{y}.png`, {
tileSize: 512,
minNativeZoom: 0,
maxNativeZoom: world.zoom.max,
errorTileUrl: 'images/clear.png'
}).addTo(P.map)
.addEventListener("load", () => {
// when all tiles are loaded, switch to this layer
this.switchTileLayer();
});
}
updateTileLayer() {
// redraw background tile layer
if (this.currentLayer == 1) {
this.tileLayer2.redraw();
} else {
this.tileLayer1.redraw();
}
}
switchTileLayer() {
// swap current tile layer
if (this.currentLayer == 1) {
this.tileLayer1.setZIndex(0);
this.tileLayer2.setZIndex(1);
this.currentLayer = 2;
} else {
this.tileLayer1.setZIndex(1);
this.tileLayer2.setZIndex(0);
this.currentLayer = 1;
}
}
}
export { LayerControl };

View File

@@ -0,0 +1,161 @@
import { Player } from "./util/Player.js";
import { P } from './Squaremap.js';
class PlayerList {
constructor(json) {
this.players = new Map();
this.markers = new Map();
this.following = null;
this.firstTick = true;
this.label = json.player_list_label;
P.map.createPane("nameplate").style.zIndex = 1000;
}
tick() {
if (P.tick_count % P.worldList.curWorld.player_tracker.update_interval == 0) {
P.getJSON("tiles/players.json", (json) => {
this.updatePlayerList(json.players);
const title = `${this.label}`
.replace(/{cur}/g, json.players.length)
.replace(/{max}/g, json.max == null ? "???" : json.max)
if (P.sidebar.players.legend.innerHTML !== title) {
P.sidebar.players.legend.innerHTML = title;
}
});
}
}
showPlayer(uuid) {
const player = this.players.get(uuid);
if (!P.worldList.worlds.has(player.world)) {
return false;
}
P.worldList.showWorld(player.world, () => {
P.map.panTo(P.toLatLng(player.x, player.z));
});
return true;
}
addToList(player) {
const head = document.createElement("img");
head.src = player.getHeadUrl();
const span = document.createElement("span");
span.innerHTML = player.displayName
const link = P.createElement("a", player.uuid, this);
link.onclick = function (e) {
if (this.parent.showPlayer(this.id)) {
this.parent.followPlayerMarker(this.id);
e.stopPropagation();
}
};
link.appendChild(head);
link.appendChild(span);
const fieldset = P.sidebar.players.element;
fieldset.appendChild(link);
Array.from(fieldset.getElementsByTagName("a"))
.sort((a, b) => {
return plain(a.getElementsByTagName("span")[0])
.localeCompare(plain(b.getElementsByTagName("span")[0]));
})
.forEach(link => fieldset.appendChild(link));
}
removeFromList(player) {
const link = document.getElementById(player.uuid);
if (link != null) {
link.remove();
}
this.players.delete(player.uuid);
player.removeMarker();
}
updatePlayerList(players) {
const playersToRemove = Array.from(this.players.keys());
let needsSort = false;
// update players from json
for (let i = 0; i < players.length; i++) {
let player = this.players.get(players[i].uuid);
if (player == null) {
// new player
player = new Player(players[i]);
this.players.set(player.uuid, player);
this.addToList(player);
} else {
const oldDisplayName = player.displayName;
player.update(players[i]);
if (oldDisplayName !== player.displayName) {
needsSort = true;
document.getElementById(player.uuid)
.getElementsByTagName("span")[0]
.innerHTML = player.displayName;
}
}
playersToRemove.remove(players[i].uuid);
}
// remove players not in json
for (let i = 0; i < playersToRemove.length; i++) {
const player = this.players.get(playersToRemove[i]);
this.removeFromList(player);
}
if (needsSort) {
const fieldset = P.sidebar.players.element;
Array.from(fieldset.getElementsByTagName("a"))
.sort((a, b) => {
return plain(a.getElementsByTagName("span")[0])
.localeCompare(plain(b.getElementsByTagName("span")[0]));
})
.forEach(link => fieldset.appendChild(link));
}
// first tick only
if (this.firstTick) {
this.firstTick = false;
// follow uuid from url
const follow = P.getUrlParam("uuid", null);
if (follow != null && this.players.get(follow) != null) {
this.followPlayerMarker(follow);
}
}
// follow highlighted player
if (this.following != null) {
const player = this.players.get(this.following);
if (player != null && P.worldList.curWorld != null) {
if (player.world !== P.worldList.curWorld.name) {
P.worldList.showWorld(player.world, () => {
P.map.panTo(P.toLatLng(player.x, player.z));
});
} else {
P.map.panTo(P.toLatLng(player.x, player.z));
}
}
}
}
clearPlayerMarkers() {
const playersToRemove = Array.from(this.players.keys());
for (let i = 0; i < playersToRemove.length; i++) {
const player = this.players.get(playersToRemove[i]);
player.removeMarker();
}
this.markers.clear();
//P.layerControl.playersLayer.clearLayers();
}
followPlayerMarker(uuid) {
if (this.following != null) {
document.getElementById(this.following).classList.remove("following");
this.following = null;
}
if (uuid != null) {
this.following = uuid;
document.getElementById(this.following).classList.add("following");
}
}
}
function plain(element) {
return element.textContent || element.innerText || "";
}
export { PlayerList };

View File

@@ -0,0 +1,47 @@
import { Pin } from "./util/Pin.js";
import { Fieldset } from "./util/Fieldset.js";
import { P } from './Squaremap.js';
class Sidebar {
constructor(json, show) {
this.sidebar = P.createElement("div", "sidebar", this);
this.showSidebar = show;
if (!show) {
this.sidebar.style.display = "none";
}
this.sidebar.addEventListener("click", (e) => {
P.playerList.followPlayerMarker(null);
});
document.body.appendChild(this.sidebar);
this.pin = new Pin(json.pinned == "pinned");
this.show(this.pin.pinned);
if (json.pinned != "hide") {
this.sidebar.appendChild(this.pin.element);
}
this.worlds = new Fieldset("worlds", json.world_list_label);
this.sidebar.appendChild(this.worlds.element);
this.players = new Fieldset("players", json.player_list_label
.replace(/{cur}/g, 0)
.replace(/{max}/g, 0));
this.sidebar.appendChild(this.players.element);
this.sidebar.onmouseleave = () => {
if (!this.pin.pinned) {
this.show(false);
}
};
this.sidebar.onmouseenter = () => {
if (!this.pin.pinned) {
this.show(true);
}
};
}
show(show) {
this.sidebar.className = show ? "show" : "";
}
}
export { Sidebar };

View File

@@ -0,0 +1,171 @@
import { Sidebar } from "./Sidebar.js";
import { PlayerList } from "./PlayerList.js";
import { WorldList } from "./WorldList.js";
import { UICoordinates } from "./UICoordinates.js";
import { UILink } from "./UILink.js";
import { LayerControl } from "./LayerControl.js";
class SquaremapMap {
constructor() {
this.map = L.map("map", {
crs: L.CRS.Simple,
center: [0, 0],
attributionControl: false,
preferCanvas: true,
noWrap: true
})
.on('overlayadd', (e) => {
this.layerControl.showLayer(e.layer);
})
.on('overlayremove', (e) => {
this.layerControl.hideLayer(e.layer);
})
.on('click', (e) => {
this.playerList.followPlayerMarker(null);
})
.on('dblclick', (e) => {
this.playerList.followPlayerMarker(null);
});
this.tick_count = 1;
this.layerControl = new LayerControl();
this.init();
}
loop() {
if (document.visibilityState === 'visible') {
this.tick();
this.tick_count++;
}
setTimeout(() => this.loop(), 1000);
}
tick() {
// tick player tracker
this.playerList.tick();
// tick world
this.worldList.curWorld.tick();
}
init() {
this.getJSON("tiles/settings.json", (json) => {
this.layerControl.init();
this.title = json.ui.title;
this.sidebar = new Sidebar(json.ui.sidebar, this.getUrlParam("show_sidebar", "true") === "true");
this.playerList = new PlayerList(json.ui.sidebar);
this.worldList = new WorldList(json.worlds);
this.coordinates = new UICoordinates(json.ui.coordinates, this.getUrlParam("show_coordinates", "true") === "true");
this.uiLink = new UILink(json.ui.link, this.getUrlParam("show_link_button", "true") === "true");
this.showControls = this.getUrlParam("show_controls", "true") === "true"
if (!this.showControls) {
let controlLayers = document.getElementsByClassName('leaflet-top leaflet-left');
controlLayers[0].style.display = "none";
}
this.worldList.loadInitialWorld(json, (world) => {
this.loop();
this.centerOn(
this.getUrlParam("x", world.spawn.x),
this.getUrlParam("z", world.spawn.z),
this.getUrlParam("zoom", world.zoom.def));
});
});
}
centerOn(x, z, zoom) {
this.map.setView(this.toLatLng(x, z), zoom);
this.uiLink.update();
return this.map;
}
toLatLng(x, z) {
return L.latLng(this.pixelsToMeters(-z), this.pixelsToMeters(x));
//return this.map.unproject([x, z], this.worldList.curWorld.zoom.max);
}
toPoint(latlng) {
return L.point(this.metersToPixels(latlng.lng), this.metersToPixels(-latlng.lat));
//return this.map.project(latlng, this.worldList.curWorld.zoom.max);
}
pixelsToMeters(num) {
return num * this.scale;
}
metersToPixels(num) {
return num / this.scale;
}
setScale(zoom) {
this.scale = (1 / Math.pow(2, zoom));
// store this on map for ellipse
this.map.options.scale = this.scale;
}
createElement(tag, id, parent) {
const element = document.createElement(tag);
element.id = id;
element.parent = parent;
return element;
}
createTextElement(tag, text) {
const element = document.createElement(tag);
element.appendChild(document.createTextNode(text));
return element;
}
getJSON(url, fn) {
fetch(url, {cache: "no-store"})
.then(async res => {
if (res.ok) {
fn(await res.json());
}
});
}
getUrlParam(query, def) {
const url = window.location.search.substring(1);
const vars = url.split('&');
for (let i = 0; i < vars.length; i++) {
const param = vars[i].split('=');
if (param[0] === query) {
const value = param[1] === undefined ? '' : decodeURIComponent(param[1]);
return value === '' ? def : value;
}
}
return def;
}
getUrlFromView() {
const center = this.toPoint(this.map.getCenter());
const zoom = this.map.getZoom();
const x = Math.floor(center.x);
const z = Math.floor(center.y);
const following = this.playerList.following ? `&uuid=${this.playerList.following}` : '';
let link = `?world=${this.worldList.curWorld.name}&zoom=${zoom}&x=${x}&z=${z}${following}`;
if (!this.showControls) {
link += "&show_controls=false"
}
if (!this.uiLink.showLinkButton) {
link += "&show_link_button=false"
}
if (!this.coordinates.showCoordinates) {
link += "&show_coordinates=false"
}
if (!this.sidebar.showSidebar) {
link += "&show_sidebar=false"
}
return link
}
updateBrowserUrl(url) {
window.history.replaceState(null, "", url);
}
}
export const P = new SquaremapMap();
// https://stackoverflow.com/a/3955096
Array.prototype.remove = function() {
var what, a = arguments, L = a.length, ax;
while (L && this.length) {
what = a[--L];
while ((ax = this.indexOf(what)) !== -1) {
this.splice(ax, 1);
}
}
return this;
};

View File

@@ -0,0 +1,43 @@
export var SquaremapTileLayer = L.TileLayer.extend({
// @method createTile(coords: Object, done?: Function): HTMLElement
// Called only internally, overrides GridLayer's [`createTile()`](#gridlayer-createtile)
// to return an `<img>` HTML element with the appropriate image URL given `coords`. The `done`
// callback is called when the tile has been loaded.
createTile: function (coords, done) {
var tile = document.createElement('img');
L.DomEvent.on(tile, 'load', () => {
//Once image has loaded revoke the object URL as we don't need it anymore
URL.revokeObjectURL(tile.src);
this._tileOnLoad(done, tile)
});
L.DomEvent.on(tile, 'error', L.Util.bind(this._tileOnError, this, done, tile));
if (this.options.crossOrigin || this.options.crossOrigin === '') {
tile.crossOrigin = this.options.crossOrigin === true ? '' : this.options.crossOrigin;
}
tile.alt = '';
tile.setAttribute('role', 'presentation');
//Retrieve image via a fetch instead of just setting the src
//This works around the fact that browsers usually don't make a request for an image that was previously loaded,
//without resorting to changing the URL (which would break caching).
fetch(this.getTileUrl(coords))
.then(res => {
//Call leaflet's error handler if request fails for some reason
if (!res.ok) {
this._tileOnError(done, tile, null);
return;
}
//Get image data and convert into object URL so it can be used as a src
//Leaflet's onload listener will take it from here
res.blob().then(blob => tile.src = URL.createObjectURL(blob));
})
.catch(() => this._tileOnError(done, tile, null));
return tile;
}
});

View File

@@ -0,0 +1,44 @@
import { P } from './Squaremap.js';
class UICoordinates {
constructor(json, show) {
const Coords = L.Control.extend({
_container: null,
options: {
position: 'bottomleft'
},
onAdd: function () {
const coords = L.DomUtil.create('div', 'leaflet-control-layers coordinates');
this._coords = coords;
if (!show) {
this._coords.style.display = "none";
}
return coords;
},
update: function (html, point) {
this.x = point == null ? "---" : Math.floor(point.x);
this.z = point == null ? "---" : Math.floor(point.y);
if (html != null) {
this._coords.innerHTML = html
.replace(/{x}/g, this.x)
.replace(/{z}/g, this.z);
}
}
});
this.showCoordinates = show;
this.html = json.html == null ? "undefined" : json.html;
this.coords = new Coords();
P.map.addControl(this.coords)
.addEventListener('mousemove', (event) => {
if (P.worldList.curWorld != null) {
this.coords.update(this.html, P.toPoint(event.latlng));
}
});
if (!json.enabled) {
this.coords._coords.style.display = "none";
}
this.coords.update(this.html);
}
}
export { UICoordinates };

View File

@@ -0,0 +1,39 @@
import { P } from './Squaremap.js';
class UILink {
constructor(json, show) {
const Link = L.Control.extend({
_container: null,
options: {
position: 'bottomleft'
},
onAdd: function () {
const link = L.DomUtil.create('div', 'leaflet-control-layers link');
this._link = link;
if (!show) {
this._link.style.display = "none";
}
this.update();
return link;
},
update: function() {
const url = P.worldList.curWorld == null ? "" : P.getUrlFromView();
//P.updateBrowserUrl(url); // this spams browser history
this._link.innerHTML = `<a href='${url}'><img src='images/clear.png'/></a>`;
}
});
this.showLinkButton = show;
this.link = new Link();
P.map.addControl(this.link)
.addEventListener('move', () => this.update())
.addEventListener('zoom', () => this.update());
if (!json.enabled) {
this.link._link.style.display = "none";
}
}
update() {
this.link.update();
}
}
export { UILink };

View File

@@ -0,0 +1,97 @@
import { World } from "./util/World.js";
import { P } from './Squaremap.js';
class WorldList {
constructor(json) {
// get worlds from json
const unorderedMap = new Map();
for (let i = 0; i < json.length; i++) {
const world = new World(json[i]);
unorderedMap.set(world.name, world);
}
// sort worlds by order
this.worlds = new Map([...unorderedMap].sort((a, b) => a[1].order - b[1].order));
// set up world list link elements
for (const [name, world] of this.worlds) {
const link = P.createElement("a", name, this);
link.onclick = function () {
const curWorld = this.parent.curWorld;
if (curWorld.name == name) {
P.centerOn(world.spawn.x, world.spawn.z, world.zoom.def)
return;
}
P.playerList.clearPlayerMarkers();
this.parent.loadWorld(name, (world) => {
P.centerOn(world.spawn.x, world.spawn.z, world.zoom.def)
});
};
const img = document.createElement("img");
img.src = this.getIcon(world);
link.appendChild(img);
link.appendChild(P.createTextElement("span", world.display_name));
P.sidebar.worlds.element.appendChild(link);
}
}
getIcon(world) {
if (world.icon != null && world.icon != "") {
return `images/icon/${world.icon}.png`;
}
switch (world.type) {
case "nether":
return "images/icon/red-cube-smol.png";
case "the_end":
return "images/icon/purple-cube-smol.png";
case "normal":
default:
return "images/icon/green-cube-smol.png";
}
}
loadInitialWorld(json, callback) {
let updateUrl = false
let name = P.getUrlParam("world", null)
if (name != null) {
const world = this.worlds.get(name);
if (world == null) {
updateUrl = true
name = null;
}
}
if (name == null) {
name = json.worlds.sort((a, b) => a.order - b.order)[0].name
}
this.loadWorld(name, (a) => {
callback(a)
if (updateUrl) {
P.updateBrowserUrl(`?world=${this.curWorld.name}`);
}
})
}
loadWorld(name, callback) {
// unload current world
if (this.curWorld != null) {
this.curWorld.unload();
}
// load new world
const world = this.worlds.get(name);
this.curWorld = world;
world.load(callback);
}
showWorld(world, callback) {
if (this.curWorld.name == world) {
if (callback != null) {
callback();
}
return;
}
this.loadWorld(world, callback);
P.updateBrowserUrl(P.getUrlFromView());
}
}
export { WorldList };

View File

@@ -0,0 +1,11 @@
import { P } from '../Squaremap.js';
class Fieldset {
constructor(id, title) {
this.element = P.createElement("fieldset", id);
this.legend = P.createTextElement("legend", title);
this.element.appendChild(this.legend);
}
}
export { Fieldset };

View File

@@ -0,0 +1,163 @@
import { P } from '../Squaremap.js';
class Marker {
constructor(opts) {
this.opts = opts;
this.id = this.opts.pop("id");
this.popup = this.opts.pop("popup");
this.popup_sticky = true;
this.tooltip = this.opts.pop("tooltip");
this.tooltip_sticky = true;
}
init() {
if (this.popup != null) {
if (this.popup_sticky) {
this.marker.on('click', (e) => {
L.popup({
direction: this.opts.pop("tooltip_direction", "top")
})
.setLatLng(P.toLatLng(P.coordinates.coords.x, P.coordinates.coords.z))
.setContent(this.popup)
.openOn(P.map);
});
} else {
this.marker.bindPopup(() => this.popup, {
direction: this.opts.pop("tooltip_direction", "top")
});
}
}
if (this.tooltip != null) {
this.marker.bindTooltip(() => this.tooltip, {
direction: this.opts.pop("tooltip_direction", "top"),
sticky: this.tooltip_sticky
});
}
for (const key in this.opts) {
this.marker.options[key] = this.opts[key];
}
}
addTo(layer) {
this.marker.remove();
this.marker.addTo(layer);
}
}
class Options {
constructor(json) {
for (const prop in json) {
this[prop] = json[prop];
}
}
pop(key, def) {
const val = this[key];
delete this[key];
return val == null ? def : val;
}
}
class Rectangle extends Marker {
constructor(opts) {
super(opts);
const points = this.opts.pop("points");
this.marker = L.rectangle([P.toLatLng(points[0].x, points[0].z), P.toLatLng(points[1].x, points[1].z)]);
super.init();
}
}
class PolyLine extends Marker {
constructor(opts) {
super(opts);
const points = this.opts.pop("points");
const outer = [];
for (let i = 0; i < points.length; i++) {
if (Symbol.iterator in Object(points[i])) {
const inner = [];
for (let j = 0; j < points[i].length; j++) {
inner.push(P.toLatLng(points[i][j].x, points[i][j].z));
}
outer.push(inner);
} else {
outer.push(P.toLatLng(points[i].x, points[i].z));
}
}
this.marker = L.polyline(outer);
super.init();
}
}
class Polygon extends Marker {
constructor(opts) {
super(opts);
const points = this.opts.pop("points");
const outer = [];
for (let i = 0; i < points.length; i++) {
if (Symbol.iterator in Object(points[i])) {
const inner = [];
for (let j = 0; j < points[i].length; j++) {
if (Symbol.iterator in Object(points[i][j])) {
const inner2 = [];
for (let k = 0; k < points[i][j].length; k++) {
inner2.push(P.toLatLng(points[i][j][k].x, points[i][j][k].z));
}
inner.push(inner2);
} else {
inner.push(P.toLatLng(points[i][j].x, points[i][j].z));
}
}
outer.push(inner);
} else {
outer.push(P.toLatLng(points[i].x, points[i].z));
}
}
this.marker = L.polygon(outer);
super.init();
}
}
class Circle extends Marker {
constructor(opts) {
super(opts);
const center = this.opts.pop("center");
const radius = this.opts.pop("radius");
this.marker = L.circle(P.toLatLng(center.x, center.z), {
radius: P.pixelsToMeters(radius)
});
super.init();
}
}
class Ellipse extends Marker {
constructor(opts) {
super(opts);
const center = this.opts.pop("center");
const radiusX = this.opts.pop("radiusX");
const radiusZ = this.opts.pop("radiusZ");
const tilt = 0;
this.marker = L.ellipse(P.toLatLng(center.x, center.z), [radiusX, radiusZ], tilt);
super.init();
}
}
class Icon extends Marker {
constructor(opts) {
super(opts);
const point = this.opts.pop("point");
const size = this.opts.pop("size");
const anchor = this.opts.pop("anchor");
const tooltipAnchor = this.opts.pop("tooltip_anchor", L.point(0, -size.z / 2));
this.marker = L.marker(P.toLatLng(point.x, point.z), {
icon: L.icon({
iconUrl: `images/icon/registered/${opts.pop("icon")}.png`,
iconSize: [size.x, size.z],
iconAnchor: [anchor.x, anchor.z],
popupAnchor: [tooltipAnchor.x, tooltipAnchor.z],
tooltipAnchor: [tooltipAnchor.x, tooltipAnchor.z]
})
});
this.popup_sticky = false;
this.tooltip_sticky = false;
super.init();
}
}
export { Marker, Options, Rectangle, PolyLine, Polygon, Circle, Ellipse, Icon };

View File

@@ -0,0 +1,23 @@
import { P } from '../Squaremap.js';
class Pin {
constructor(def) {
this.pinned = def;
this.element = P.createElement("img", "pin", this);
this.element.onclick = () => this.toggle();
this.pin(this.pinned);
}
toggle() {
this.pin(!this.pinned);
}
pin(pin) {
this.pinned = pin;
this.element.className = pin ? "pinned" : "unpinned";
this.element.src = `images/${this.element.className}.png`;
}
}
export { Pin };

View File

@@ -0,0 +1,98 @@
import { P } from '../Squaremap.js';
class Player {
constructor(json) {
this.name = json.name;
this.uuid = json.uuid;
this.world = json.world;
this.displayName = json.display_name !== undefined ? json.display_name : json.name;
this.x = 0;
this.z = 0;
this.armor = 0;
this.health = 20;
this.tooltip = L.tooltip({
permanent: true,
direction: "right",
offset: [10, 0],
pane: "nameplate"
});
this.marker = L.marker(P.toLatLng(json.x, json.z), {
icon: L.icon({
iconUrl: 'images/icon/player.png',
iconSize: [17, 16],
iconAnchor: [8, 9],
tooltipAnchor: [0, 0]
}),
rotationAngle: (180 + json.yaw)
});
if (P.worldList.curWorld.player_tracker.nameplates.enabled) {
this.updateNameplate(json);
this.marker.bindTooltip(this.tooltip);
}
}
getHeadUrl() {
return P.worldList.curWorld.player_tracker.nameplates.heads_url
.replace(/{uuid}/g, this.uuid)
.replace(/{name}/g, this.name);
}
updateNameplate(player) {
let headImg = "";
let armorImg = "";
let healthImg = "";
if (P.worldList.curWorld.player_tracker.nameplates.show_heads) {
headImg = `<img src='${this.getHeadUrl()}' class="head" />`;
}
if (P.worldList.curWorld.player_tracker.nameplates.show_armor && player.armor != null) {
armorImg = `<img src="images/armor/${Math.min(Math.max(player.armor, 0), 20)}.png" class="armor" />`;
}
if (P.worldList.curWorld.player_tracker.nameplates.show_health && player.health != null) {
healthImg = `<img src="images/health/${Math.min(Math.max(player.health, 0), 20)}.png" class="health" />`;
}
this.tooltip.setContent(`<ul><li>${headImg}</li><li>${this.displayName}${healthImg}${armorImg}</li>`);
}
update(player) {
this.x = player.x;
this.z = player.z;
this.world = player.world;
this.armor = player.armor;
this.health = player.health;
this.displayName = player.display_name !== undefined ? player.display_name : player.name;
const link = document.getElementById(player.uuid);
const img = link.getElementsByTagName("img")[0];
const span = link.getElementsByTagName("span")[0];
if (P.worldList.curWorld.name == player.world) {
if (P.worldList.curWorld.player_tracker.enabled) {
this.addMarker();
}
const latlng = P.toLatLng(player.x, player.z);
if (!this.marker.getLatLng().equals(latlng)) {
this.marker.setLatLng(latlng);
}
const angle = 180 + player.yaw;
if (this.marker.options.rotationAngle != angle) {
this.marker.setRotationAngle(angle);
}
img.classList.remove("other-world");
span.classList.remove("other-world");
} else {
this.removeMarker();
img.classList.add("other-world");
span.classList.add("other-world");
}
this.updateNameplate(player);
}
removeMarker() {
this.marker.remove();
P.playerList.markers.delete(this.uuid);
P.map.removeLayer(this.marker);
P.layerControl.playersLayer.removeLayer(this.marker);
}
addMarker() {
if (!P.playerList.markers.has(this.uuid)) {
this.marker.addTo(P.layerControl.playersLayer);
P.playerList.markers.set(this.uuid, this.marker);
}
}
}
export { Player };

View File

@@ -0,0 +1,145 @@
import { Options, Rectangle, PolyLine, Polygon, Circle, Ellipse, Icon } from "./Markers.js";
import { P } from '../Squaremap.js';
class World {
constructor(json) {
this.name = json.name;
this.order = json.order;
this.icon = json.icon;
this.type = json.type;
this.display_name = json.display_name;
this.markerLayers = new Map();
this.player_tracker = {};
this.marker_update_interval = 5;
this.tiles_update_interval = 15;
}
tick() {
// refresh map tile layer
if (P.tick_count % this.tiles_update_interval == 0) {
P.layerControl.updateTileLayer();
}
// load and draw markers
if (P.tick_count % this.marker_update_interval == 0) {
P.getJSON(`tiles/${this.name}/markers.json`, (json) => {
if (this === P.worldList.curWorld) {
this.markers(json);
}
});
}
}
unload() {
P.playerList.clearPlayerMarkers();
const keys = Array.from(this.markerLayers.keys());
for (let i = 0; i < keys.length; i++) {
const layer = this.markerLayers.get(keys[i]);
P.layerControl.controls.removeLayer(layer);
layer.remove();
this.markerLayers.delete(keys[i]);
}
}
load(callback) {
P.getJSON(`tiles/${this.name}/settings.json`, (json) => {
this.player_tracker = json.player_tracker;
this.zoom = json.zoom;
this.spawn = json.spawn;
this.marker_update_interval = json.marker_update_interval;
this.tiles_update_interval = json.tiles_update_interval;
// set the scale for our projection calculations
P.setScale(this.zoom.max);
// set center and zoom
P.centerOn(this.spawn.x, this.spawn.z, this.zoom.def)
.setMinZoom(0) // extra zoom out doesn't work :(
.setMaxZoom(this.zoom.max + this.zoom.extra);
// update page title
document.title = P.title
.replace(/{world}/g, this.display_name);
// setup background
document.getElementById("map").style.background = this.getBackground();
// setup tile layers
P.layerControl.setupTileLayers(this);
// force clear player markers
P.playerList.clearPlayerMarkers();
// tick now, reset counter
P.tick_count = 0;
P.tick();
// force clear player markers
P.playerList.clearPlayerMarkers();
if (callback != null) {
callback(this);
}
});
}
getBackground() {
switch (this.type) {
case "nether":
return "url('images/nether_sky.png')";
case "the_end":
return "url('images/end_sky.png')";
case "normal":
default:
return "url('images/overworld_sky.png')";
}
}
markers(json) {
// check if json is iterable
if (json == null || !(Symbol.iterator in Object(json))) {
return;
}
// iterate layers
for (const entry of json) {
// check if layer exists and needs updating
let layer = this.markerLayers.get(entry.id);
if (layer != null) {
if (layer.timestamp === entry.timestamp) {
continue; // skip
}
// clear existing layer to rebuild
P.layerControl.removeOverlay(layer);
// TODO
// implement marker tracker instead of clearing
// to reduce possible client side lag
}
// setup the layer
layer = new L.LayerGroup();
layer.order = entry.order;
layer.id = entry.id;
layer.timestamp = entry.timestamp;
layer.setZIndex(entry.z_index);
this.markerLayers.set(layer.id, layer);
// setup the layer control
if (entry.control === true) {
P.layerControl.addOverlay(entry.name, layer, entry.hide);
}
// setup the markers
for (const shape in entry.markers) {
let marker;
const opts = new Options(entry.markers[shape]);
switch(opts.pop("type")) {
case "rectangle": marker = new Rectangle(opts); break;
case "polyline": marker = new PolyLine(opts); break;
case "polygon": marker = new Polygon(opts); break;
case "circle": marker = new Circle(opts); break;
case "ellipse": marker = new Ellipse(opts); break;
case "icon": marker = new Icon(opts); break;
}
if (marker != null) {
marker.addTo(layer);
}
}
}
}
}
export { World };