Merge branch 'pl3xmap'

# Conflicts:
#	src/api.ts
#	src/components/Map.vue
#	src/components/map/layer/MapLayer.vue
#	src/components/map/layer/MarkerSetLayer.vue
#	src/components/map/vector/Areas.vue
#	src/components/map/vector/Circles.vue
#	src/components/map/vector/Lines.vue
#	src/components/map/vector/Markers.vue
#	src/components/sidebar/WorldListItem.vue
#	src/dynmap.d.ts
#	src/index.d.ts
#	src/leaflet/icon/GenericIcon.ts
#	src/leaflet/layer/LiveAtlasLayerGroup.ts
#	src/leaflet/tileLayer/DynmapTileLayer.ts
#	src/leaflet/vector/LiveAtlasPolygon.ts
#	src/leaflet/vector/LiveAtlasPolyline.ts
#	src/model/LiveAtlasMapDefinition.ts
#	src/model/LiveAtlasProjection.ts
#	src/store/actions.ts
#	src/store/getters.ts
#	src/store/state.ts
#	src/util.ts
#	src/util/areas.ts
#	src/util/circles.ts
#	src/util/lines.ts
#	src/util/markers.ts
This commit is contained in:
James Lyne 2021-07-29 18:15:35 +01:00
commit e07ac55c33
88 changed files with 3218 additions and 2426 deletions

View File

@ -0,0 +1,6 @@
<component name="CopyrightManager">
<copyright>
<option name="notice" value="Copyright &amp;#36;today.year James Lyne&#10; &#10;Some portions of this file were taken from https://github.com/webbukkit/dynmap.&#10;These portions are Copyright 2020 Dynmap Contributors.&#10; &#10;Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);&#10;you may not use this file except in compliance with the License.&#10;You may obtain a copy of the License at&#10; &#10;http://www.apache.org/licenses/LICENSE-2.0&#10; &#10;Unless required by applicable law or agreed to in writing, software&#10;distributed under the License is distributed on an &quot;AS IS&quot; BASIS,&#10;WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.&#10;See the License for the specific language governing permissions and&#10;limitations under the License." />
<option name="myName" value="Dynmap" />
</copyright>
</component>

View File

@ -0,0 +1,6 @@
<component name="CopyrightManager">
<copyright>
<option name="notice" value="Copyright &amp;#36;today.year James Lyne&#10; &#10;Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);&#10;you may not use this file except in compliance with the License.&#10;You may obtain a copy of the License at&#10; &#10;http://www.apache.org/licenses/LICENSE-2.0&#10; &#10;Unless required by applicable law or agreed to in writing, software&#10;distributed under the License is distributed on an &quot;AS IS&quot; BASIS,&#10;WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.&#10;See the License for the specific language governing permissions and&#10;limitations under the License." />
<option name="myName" value="Original" />
</copyright>
</component>

View File

@ -0,0 +1,8 @@
<component name="CopyrightManager">
<settings>
<module2copyright>
<element module="Dynmap" copyright="Dynmap" />
<element module="Original" copyright="Original" />
</module2copyright>
</settings>
</component>

3
.idea/scopes/Dynmap.xml Normal file
View File

@ -0,0 +1,3 @@
<component name="DependencyValidationManager">
<scope name="Dynmap" pattern="file:src/leaflet/tileLayer/DynmapTileLayer.ts||file:src/leaflet/control/ClockControl.ts||file:src/leaflet/control/CoordinatesControl.ts||file:src/leaflet/control/LinkControl.ts||file:src/util/areas.ts||file:src/util/circles.ts||file:src/util/lines.ts||file:src/util/markers.ts||file:src/leaflet/control/LiveAtlasLayerControl.ts||file:src/leaflet/control/LogoControl.ts||file:src/leaflet/icon/PlayerIcon.ts||file:src/leaflet/icon/GenericIcon.ts||file:src/model/LiveAtlasProjection.ts||file:src/scss/style.scss" />
</component>

View File

@ -0,0 +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" />
</component>

View File

@ -1,5 +1,5 @@
<!--
- Copyright 2020 James Lyne
- Copyright 2021 James Lyne
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
@ -43,21 +43,19 @@ export default defineComponent({
setup() {
const store = useStore(),
updateInterval = computed(() => store.state.configuration.updateInterval),
title = computed(() => store.state.configuration.title),
currentUrl = computed(() => store.getters.url),
currentServer = computed(() => store.state.currentServer),
configurationHash = computed(() => store.state.configurationHash),
chatBoxEnabled = computed(() => store.state.components.chatBox),
chatVisible = computed(() => store.state.ui.visibleElements.has('chat')),
updatesEnabled = ref(false),
updateTimeout = ref(0),
configAttempts = ref(0),
loadConfiguration = async () => {
try {
await store.dispatch(ActionTypes.LOAD_CONFIGURATION, undefined);
startUpdates();
await store.dispatch(ActionTypes.START_UPDATES, undefined);
requestAnimationFrame(() => {
hideSplash();
@ -80,36 +78,6 @@ export default defineComponent({
}
},
startUpdates = () => {
updatesEnabled.value = true;
update();
},
update = async () => {
//TODO: Error notification for repeated failures?
try {
await store.dispatch(ActionTypes.GET_UPDATE, undefined);
} finally {
if(updatesEnabled.value) {
if(updateTimeout.value) {
clearTimeout(updateTimeout.value);
}
updateTimeout.value = setTimeout(() => update(), updateInterval.value);
}
}
},
stopUpdates = () => {
updatesEnabled.value = false;
if (updateTimeout.value) {
clearTimeout(updateTimeout.value);
}
updateTimeout.value = 0;
},
handleUrl = () => {
const parsedUrl = parseUrl();
@ -174,7 +142,6 @@ export default defineComponent({
watch(currentUrl, (url) => window.history.replaceState({}, '', url));
watch(currentServer, (newServer?: LiveAtlasServerDefinition) => {
showSplash();
stopUpdates();
if(!newServer) {
return;
@ -182,6 +149,7 @@ export default defineComponent({
//Cleanup
store.commit(MutationTypes.CLEAR_PLAYERS, undefined);
store.commit(MutationTypes.SET_MAX_PLAYERS, 0);
store.commit(MutationTypes.CLEAR_CURRENT_MAP, undefined);
store.commit(MutationTypes.CLEAR_PARSED_URL, undefined);
store.commit(MutationTypes.CLEAR_WORLDS, undefined);
@ -190,17 +158,17 @@ export default defineComponent({
window.history.replaceState({}, '', newServer.id);
loadConfiguration();
}, {deep: true});
watch(configurationHash, (newHash, oldHash) => {
watch(configurationHash, async (newHash, oldHash) => {
if(newHash && oldHash) {
showSplash();
stopUpdates();
store.commit(MutationTypes.CLEAR_PARSED_URL, undefined);
loadConfiguration();
await store.dispatch(ActionTypes.STOP_UPDATES, undefined);
await loadConfiguration();
}
});
onMounted(() => loadConfiguration());
onBeforeUnmount(() => stopUpdates());
onBeforeUnmount(() => store.dispatch(ActionTypes.STOP_UPDATES, undefined));
handleUrl();
onResize();

View File

@ -1,777 +0,0 @@
/*
* Copyright 2020 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.
*/
import {
DynmapArea,
DynmapChat,
DynmapCircle,
DynmapComponentConfig,
DynmapConfigurationResponse,
DynmapLine,
DynmapMarker,
DynmapMarkerSet,
DynmapMarkerSetUpdates,
DynmapPlayer,
DynmapServerConfig,
DynmapTileUpdate,
DynmapUpdate,
DynmapUpdateResponse,
DynmapUpdates
} from "@/dynmap";
import {useStore} from "@/store";
import ChatError from "@/errors/ChatError";
import {LiveAtlasDimension, LiveAtlasServerMessageConfig, LiveAtlasWorldDefinition} from "@/index";
import LiveAtlasMapDefinition from "@/model/LiveAtlasMapDefinition";
const titleColours = /§[0-9a-f]/ig,
netherWorldName = /_?nether(_|$)/i,
endWorldName = /(^|_)end(_|$)/i;
function buildServerConfig(response: any): DynmapServerConfig {
return {
version: response.dynmapversion || '',
grayHiddenPlayers: response.grayplayerswhenhidden || false,
defaultMap: response.defaultmap || undefined,
defaultWorld: response.defaultworld || undefined,
defaultZoom: response.defaultzoom || 0,
followMap: response.followmap || undefined,
followZoom: response.followzoom || 0,
updateInterval: response.updaterate || 3000,
showLayerControl: response.showlayercontrol && response.showlayercontrol !== 'false', //Sent as a string for some reason
title: response.title.replace(titleColours, '') || 'Dynmap',
loginEnabled: response['login-enabled'] || false,
maxPlayers: response.maxcount || 0,
expandUI: response.sidebaropened && response.sidebaropened !== 'false', //Sent as a string for some reason
hash: response.confighash || 0,
};
}
function buildMessagesConfig(response: any): LiveAtlasServerMessageConfig {
return {
chatPlayerJoin: response.joinmessage || '',
chatPlayerQuit: response.quitmessage || '',
chatAnonymousJoin: response['msg-hiddennamejoin'] || '',
chatAnonymousQuit: response['msg-hiddennamequit'] || '',
chatErrorNotAllowed: response['msg-chatnotallowed'] || '',
chatErrorRequiresLogin: response['msg-chatrequireslogin'] || '',
chatErrorCooldown: response.spammessage || '',
worldsHeading: response['msg-maptypes'] || '',
playersHeading: response['msg-players'] || '',
}
}
function buildWorlds(response: any): Array<LiveAtlasWorldDefinition> {
const worlds: Map<string, LiveAtlasWorldDefinition> = new Map<string, LiveAtlasWorldDefinition>();
//Get all the worlds first so we can handle append_to_world properly
(response.worlds || []).forEach((world: any) => {
let worldType: LiveAtlasDimension = 'overworld';
if (netherWorldName.test(world.name) || (world.name == 'DIM-1')) {
worldType = 'nether';
} else if (endWorldName.test(world.name) || (world.name == 'DIM1')) {
worldType = 'end';
}
worlds.set(world.name, {
seaLevel: world.sealevel || 64,
name: world.name,
dimension: worldType,
protected: world.protected || false,
title: world.title || '',
height: world.height || 256,
center: {
x: world.center.x || 0,
y: world.center.y || 0,
z: world.center.z || 0
},
maps: new Map(),
});
});
(response.worlds || []).forEach((world: any) => {
(world.maps || []).forEach((map: any) => {
const worldName = map.append_to_world || world.name,
w = worlds.get(worldName);
if(!w) {
console.warn(`Ignoring map '${map.name}' associated with non-existent world '${worldName}'`);
return;
}
w.maps.set(map.name, new LiveAtlasMapDefinition({
world: world, //Ignore append_to_world here otherwise things break
background: map.background || '#000000',
backgroundDay: map.backgroundday || '#000000',
backgroundNight: map.backgroundnight || '#000000',
icon: map.icon || undefined,
imageFormat: map['image-format'] || 'png',
name: map.name || '(Unnamed map)',
nightAndDay: map.nightandday || false,
prefix: map.prefix || '',
protected: map.protected || false,
title: map.title || '',
mapToWorld: map.maptoworld || undefined,
worldToMap: map.worldtomap || undefined,
nativeZoomLevels: map.mapzoomout || 1,
extraZoomLevels: map.mapzoomin || 0
}));
});
});
return Array.from(worlds.values());
}
function buildComponents(response: any): DynmapComponentConfig {
const components: DynmapComponentConfig = {
markers: {
showLabels: false,
},
chatBox: undefined,
chatBalloons: false,
playerMarkers: undefined,
coordinatesControl: undefined,
linkControl: false,
clockControl: undefined,
logoControls: [],
};
(response.components || []).forEach((component: any) => {
const type = component.type || "unknown";
switch (type) {
case "markers":
components.markers = {
showLabels: component.showlabel || false,
}
break;
case "playermarkers":
components.playerMarkers = {
hideByDefault: component.hidebydefault || false,
layerName: component.label || "Players",
layerPriority: component.layerprio || 0,
showBodies: component.showplayerbody || false,
showSkinFaces: component.showplayerfaces || false,
showHealth: component.showplayerhealth || false,
smallFaces: component.smallplayerfaces || false,
}
break;
case "coord":
components.coordinatesControl = {
showY: !(component.hidey || false),
label: component.label || "Location: ",
showRegion: component['show-mcr'] || false,
showChunk: component['show-chunk'] || false,
}
break;
case "link":
components.linkControl = true;
break;
case "digitalclock":
components.clockControl = {
showDigitalClock: true,
showWeather: false,
showTimeOfDay: false,
}
break;
case "timeofdayclock":
components.clockControl = {
showTimeOfDay: true,
showDigitalClock: component.showdigitalclock || false,
showWeather: component.showweather || false,
}
break;
case "logo":
components.logoControls.push({
text: component.text || '',
url: component.linkurl || undefined,
position: component.position.replace('-', '') || 'topleft',
image: component.logourl || undefined,
});
break;
case "chat":
if (response.allowwebchat) {
components.chatSending = {
loginRequired: response['webchat-requires-login'] || false,
maxLength: response['chatlengthlimit'] || 256,
cooldown: response['webchat-interval'] || 5,
}
}
break;
case "chatbox":
components.chatBox = {
allowUrlName: component.allowurlname || false,
showPlayerFaces: component.showplayerfaces || false,
messageLifetime: component.messagettl || Infinity,
messageHistory: component.scrollback || Infinity,
}
break;
case "chatballoon":
components.chatBalloons = true;
}
});
return components;
}
function buildMarkerSet(id: string, data: any): any {
return {
id,
label: data.label || "Unnamed set",
hidden: data.hide || false,
priority: data.layerprio || 0,
showLabels: data.showlabels || undefined,
minZoom: typeof data.minzoom !== 'undefined' && data.minzoom > -1 ? data.minzoom : undefined,
maxZoom: typeof data.maxzoom !== 'undefined' && data.maxzoom > -1 ? data.maxzoom : undefined,
}
}
function buildMarkers(data: any): Map<string, DynmapMarker> {
const markers = Object.freeze(new Map()) as Map<string, DynmapMarker>;
for (const key in data) {
if (!Object.prototype.hasOwnProperty.call(data, key)) {
continue;
}
markers.set(key, buildMarker(data[key]));
}
return markers;
}
function buildMarker(marker: any): DynmapMarker {
return {
label: marker.label || '',
location: {
x: marker.x || 0,
y: marker.y || 0,
z: marker.z || 0,
},
dimensions: marker.dim ? marker.dim.split('x') : [16, 16],
icon: marker.icon || "default",
isHTML: marker.markup || false,
minZoom: typeof marker.minzoom !== 'undefined' && marker.minzoom > -1 ? marker.minzoom : undefined,
maxZoom: typeof marker.maxzoom !== 'undefined' && marker.maxzoom > -1 ? marker.maxzoom : undefined,
popupContent: marker.desc || undefined,
};
}
function buildAreas(data: any): Map<string, DynmapArea> {
const areas = Object.freeze(new Map()) as Map<string, DynmapArea>;
for (const key in data) {
if (!Object.prototype.hasOwnProperty.call(data, key)) {
continue;
}
areas.set(key, buildArea(data[key]));
}
return areas;
}
function buildArea(area: any): DynmapArea {
return {
style: {
color: area.color || '#ff0000',
opacity: area.opacity || 1,
weight: area.weight || 1,
fillColor: area.fillcolor || '#ff0000',
fillOpacity: area.fillopacity || 0,
},
label: area.label || '',
isHTML: area.markup || false,
x: area.x || [0, 0],
y: [area.ybottom || 0, area.ytop || 0],
z: area.z || [0, 0],
minZoom: typeof area.minzoom !== 'undefined' && area.minzoom > -1 ? area.minzoom : undefined,
maxZoom: typeof area.maxzoom !== 'undefined' && area.maxzoom > -1 ? area.maxzoom : undefined,
popupContent: area.desc || undefined,
};
}
function buildLines(data: any): Map<string, DynmapLine> {
const lines = Object.freeze(new Map()) as Map<string, DynmapLine>;
for (const key in data) {
if (!Object.prototype.hasOwnProperty.call(data, key)) {
continue;
}
lines.set(key, buildLine(data[key]));
}
return lines;
}
function buildLine(line: any): DynmapLine {
return {
x: line.x || [0, 0],
y: line.y || [0, 0],
z: line.z || [0, 0],
style: {
color: line.color || '#ff0000',
opacity: line.opacity || 1,
weight: line.weight || 1,
},
label: line.label || '',
isHTML: line.markup || false,
minZoom: typeof line.minzoom !== 'undefined' && line.minzoom > -1 ? line.minzoom : undefined,
maxZoom: typeof line.maxzoom !== 'undefined' && line.maxzoom > -1 ? line.maxzoom : undefined,
popupContent: line.desc || undefined,
};
}
function buildCircles(data: any): Map<string, DynmapCircle> {
const circles = Object.freeze(new Map()) as Map<string, DynmapCircle>;
for (const key in data) {
if (!Object.prototype.hasOwnProperty.call(data, key)) {
continue;
}
circles.set(key, buildCircle(data[key]));
}
return circles;
}
function buildCircle(circle: any): DynmapCircle {
return {
location: {
x: circle.x || 0,
y: circle.y || 0,
z: circle.z || 0,
},
radius: [circle.xr || 0, circle.zr || 0],
style: {
fillColor: circle.fillcolor || '#ff0000',
fillOpacity: circle.fillopacity || 0,
color: circle.color || '#ff0000',
opacity: circle.opacity || 1,
weight: circle.weight || 1,
},
label: circle.label || '',
isHTML: circle.markup || false,
minZoom: typeof circle.minzoom !== 'undefined' && circle.minzoom > -1 ? circle.minzoom : undefined,
maxZoom: typeof circle.maxzoom !== 'undefined' && circle.maxzoom > -1 ? circle.maxzoom : undefined,
popupContent: circle.desc || undefined,
};
}
function buildUpdates(data: Array<any>): DynmapUpdates {
const updates = {
markerSets: new Map<string, DynmapMarkerSetUpdates>(),
tiles: [] as DynmapTileUpdate[],
chat: [] as DynmapChat[],
},
dropped = {
stale: 0,
noSet: 0,
noId: 0,
unknownType: 0,
unknownCType: 0,
incomplete: 0,
notImplemented: 0,
},
lastUpdate = useStore().state.updateTimestamp;
let accepted = 0;
for (const entry of data) {
switch (entry.type) {
case 'component': {
if (lastUpdate && entry.timestamp < lastUpdate) {
dropped.stale++;
continue;
}
if (!entry.id) {
dropped.noId++;
continue;
}
//Set updates don't have a set field, the id is the set
const set = entry.msg.startsWith("set") ? entry.id : entry.set;
if (!set) {
dropped.noSet++;
continue;
}
if (entry.ctype !== 'markers') {
dropped.unknownCType++;
continue;
}
if (!updates.markerSets.has(set)) {
updates.markerSets.set(set, {
areaUpdates: [],
markerUpdates: [],
lineUpdates: [],
circleUpdates: [],
removed: false,
});
}
const markerSetUpdates = updates.markerSets.get(set),
update: DynmapUpdate = {
id: entry.id,
removed: entry.msg.endsWith('deleted'),
};
if (entry.msg.startsWith("set")) {
markerSetUpdates!.removed = update.removed;
markerSetUpdates!.payload = update.removed ? undefined : buildMarkerSet(set, entry);
} else if (entry.msg.startsWith("marker")) {
update.payload = update.removed ? undefined : buildMarker(entry);
markerSetUpdates!.markerUpdates.push(Object.freeze(update));
} else if (entry.msg.startsWith("area")) {
update.payload = update.removed ? undefined : buildArea(entry);
markerSetUpdates!.areaUpdates.push(Object.freeze(update));
} else if (entry.msg.startsWith("circle")) {
update.payload = update.removed ? undefined : buildCircle(entry);
markerSetUpdates!.circleUpdates.push(Object.freeze(update));
} else if (entry.msg.startsWith("line")) {
update.payload = update.removed ? undefined : buildLine(entry);
markerSetUpdates!.lineUpdates.push(Object.freeze(update));
}
accepted++;
break;
}
case 'chat':
if (!entry.message || !entry.timestamp) {
dropped.incomplete++;
continue;
}
if (entry.timestamp < lastUpdate) {
dropped.stale++;
continue;
}
if (entry.source !== 'player' && entry.source !== 'web') {
dropped.notImplemented++;
continue;
}
updates.chat.push({
type: 'chat',
source: entry.source || undefined,
playerAccount: entry.account || undefined,
playerName: entry.playerName || undefined,
message: entry.message || "",
timestamp: entry.timestamp,
channel: entry.channel || undefined,
});
break;
case 'playerjoin':
if (!entry.account || !entry.timestamp) {
dropped.incomplete++;
continue;
}
if (entry.timestamp < lastUpdate) {
dropped.stale++;
continue;
}
updates.chat.push({
type: 'playerjoin',
playerAccount: entry.account,
playerName: entry.playerName || "",
timestamp: entry.timestamp || undefined,
});
break;
case 'playerquit':
if (!entry.account || !entry.timestamp) {
dropped.incomplete++;
continue;
}
if (entry.timestamp < lastUpdate) {
dropped.stale++;
continue;
}
updates.chat.push({
type: 'playerleave',
playerAccount: entry.account,
playerName: entry.playerName || "",
timestamp: entry.timestamp || undefined,
});
break;
case 'tile':
if (!entry.name || !entry.timestamp) {
dropped.incomplete++;
continue;
}
if (lastUpdate && entry.timestamp < lastUpdate) {
dropped.stale++;
continue;
}
updates.tiles.push({
name: entry.name,
timestamp: entry.timestamp,
});
accepted++;
break;
default:
dropped.unknownType++;
}
}
//Sort chat by newest first
updates.chat = updates.chat.sort((one, two) => {
return two.timestamp - one.timestamp;
});
console.debug(`Updates: ${accepted} accepted. Rejected: `, dropped);
return updates;
}
async function fetchJSON(url: string, signal: AbortSignal) {
let response, json;
try {
response = await fetch(url, {signal});
} catch(e) {
if(e instanceof DOMException && e.name === 'AbortError') {
console.warn(`Request aborted (${url}`);
throw e;
} else {
console.error(e);
}
throw new Error(`Network request failed`);
}
if (!response.ok) {
throw new Error(`Network request failed (${response.statusText || 'Unknown'})`);
}
try {
json = await response.json();
} catch(e) {
if(e instanceof DOMException && e.name === 'AbortError') {
console.warn(`Request aborted (${url}`);
throw e;
} else {
throw new Error('Request returned invalid json');
}
}
return json;
}
let configurationAbort: AbortController | undefined = undefined,
markersAbort: AbortController | undefined = undefined,
updateAbort: AbortController | undefined = undefined;
export default {
async getConfiguration(): Promise<DynmapConfigurationResponse> {
if(configurationAbort) {
configurationAbort.abort();
}
configurationAbort = new AbortController();
const response = await fetchJSON(useStore().getters.serverConfig.dynmap.configuration, configurationAbort.signal);
if (response.error === 'login-required') {
throw new Error("Login required");
} else if (response.error) {
throw new Error(response.error);
}
return {
config: buildServerConfig(response),
messages: buildMessagesConfig(response),
worlds: buildWorlds(response),
components: buildComponents(response),
loggedIn: response.loggedin || false,
}
},
async getUpdate(requestId: number, world: string, timestamp: number): Promise<DynmapUpdateResponse> {
let url = useStore().getters.serverConfig.dynmap.update;
url = url.replace('{world}', world);
url = url.replace('{timestamp}', timestamp.toString());
if(updateAbort) {
updateAbort.abort();
}
updateAbort = new AbortController();
const response = await fetchJSON(url, updateAbort.signal);
const players: Set<DynmapPlayer> = new Set();
(response.players || []).forEach((player: any) => {
const world = player.world && player.world !== '-some-other-bogus-world-' ? player.world : undefined;
players.add({
account: player.account || "",
health: player.health || 0,
armor: player.armor || 0,
name: player.name || "",
sort: player.sort || 0,
hidden: !world,
location: {
//Add 0.5 to position in the middle of a block
x: !isNaN(player.x) ? player.x + 0.5 : 0,
y: !isNaN(player.y) ? player.y : 0,
z: !isNaN(player.z) ? player.z + 0.5 : 0,
world: world,
}
});
});
//Extra fake players for testing
// for(let i = 0; i < 450; i++) {
// players.add({
// account: "VIDEO GAMES " + i,
// health: Math.round(Math.random() * 10),
// armor: Math.round(Math.random() * 10),
// name: "VIDEO GAMES " + i,
// sort: Math.round(Math.random() * 10),
// hidden: false,
// location: {
// x: Math.round(Math.random() * 1000) - 500,
// y: 64,
// z: Math.round(Math.random() * 1000) - 500,
// world: "world",
// }
// });
// }
return {
worldState: {
timeOfDay: response.servertime || 0,
thundering: response.isThundering || false,
raining: response.hasStorm || false,
},
playerCount: response.count || 0,
configHash: response.confighash || 0,
timestamp: response.timestamp || 0,
players,
updates: buildUpdates(response.updates || []),
}
},
async getMarkerSets(world: string): Promise<Map<string, DynmapMarkerSet>> {
const url = `${useStore().getters.serverConfig.dynmap.markers}_markers_/marker_${world}.json`;
if(markersAbort) {
markersAbort.abort();
}
markersAbort = new AbortController();
const response = await fetchJSON(url, markersAbort.signal);
const sets: Map<string, DynmapMarkerSet> = new Map();
response.sets = response.sets || {};
for (const key in response.sets) {
if (!Object.prototype.hasOwnProperty.call(response.sets, key)) {
continue;
}
const set = response.sets[key],
markers = buildMarkers(set.markers || {}),
circles = buildCircles(set.circles || {}),
areas = buildAreas(set.areas || {}),
lines = buildLines(set.lines || {});
sets.set(key, {
...buildMarkerSet(key, set),
markers,
circles,
areas,
lines,
});
}
return sets;
},
sendChatMessage(message: string) {
const store = useStore();
if (!store.state.components.chatSending) {
return Promise.reject(store.state.messages.chatErrorDisabled);
}
return fetch(useStore().getters.serverConfig.dynmap.sendmessage, {
method: 'POST',
body: JSON.stringify({
name: null,
message: message,
})
}).then((response) => {
if (response.status === 403) { //Rate limited
throw new ChatError(store.state.messages.chatErrorCooldown
.replace('%interval%', store.state.components.chatSending!.cooldown.toString()));
}
if (!response.ok) {
throw new Error('Network request failed');
}
return response.json();
}).then(response => {
if (response.error !== 'none') {
throw new ChatError(store.state.messages.chatErrorNotAllowed);
}
}).catch(e => {
if (!(e instanceof ChatError)) {
console.error(store.state.messages.chatErrorUnknown);
console.trace(e);
}
throw e;
});
}
}

View File

@ -1,5 +1,5 @@
<!--
- Copyright 2020 James Lyne
- Copyright 2021 James Lyne
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.

View File

@ -1,5 +1,5 @@
<!--
- Copyright 2020 James Lyne
- Copyright 2021 James Lyne
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
@ -44,12 +44,10 @@ import LinkControl from "@/components/map/control/LinkControl.vue";
import ChatControl from "@/components/map/control/ChatControl.vue";
import LogoControl from "@/components/map/control/LogoControl.vue";
import {MutationTypes} from "@/store/mutation-types";
import {DynmapPlayer} from "@/dynmap";
import {ActionTypes} from "@/store/action-types";
import LiveAtlasLeafletMap from "@/leaflet/LiveAtlasLeafletMap";
import {LoadingControl} from "@/leaflet/control/LoadingControl";
import MapContextMenu from "@/components/map/MapContextMenu.vue";
import {Coordinate} from "@/index";
import {Coordinate, LiveAtlasPlayer} from "@/index";
export default defineComponent({
components: {
@ -125,7 +123,7 @@ export default defineComponent({
followTarget: {
handler(newValue, oldValue) {
if (newValue) {
this.updateFollow(newValue, !oldValue || newValue.account !== oldValue.account);
this.updateFollow(newValue, !oldValue || newValue.name !== oldValue.name);
}
},
deep: true
@ -141,8 +139,14 @@ export default defineComponent({
}
},
currentMap(newValue, oldValue) {
if(this.leaflet && newValue && oldValue) {
const panTarget = this.scheduledPan || oldValue.latLngToLocation(this.leaflet.getCenter(), 64);
if(this.leaflet && newValue) {
let panTarget = this.scheduledPan;
if(!panTarget && oldValue) {
panTarget = oldValue.latLngToLocation(this.leaflet.getCenter(), 64);
} else if(!panTarget) {
panTarget = {x: 0, y: 0, z: 0};
}
if(this.scheduledZoom) {
this.leaflet!.setZoom(this.scheduledZoom, {
@ -165,8 +169,6 @@ export default defineComponent({
if(newValue) {
let location: Coordinate | null = this.scheduledPan;
store.dispatch(ActionTypes.GET_MARKER_SETS, undefined);
// Abort if follow target is present, to avoid panning twice
if(store.state.followTarget && store.state.followTarget.location.world === newValue.name) {
return;
@ -280,7 +282,7 @@ export default defineComponent({
this.leaflet.getContainer().focus();
}
},
updateFollow(player: DynmapPlayer, newFollow: boolean) {
updateFollow(player: LiveAtlasPlayer, newFollow: boolean) {
const store = useStore(),
followMapName = store.state.configuration.followMap,
currentWorld = store.state.currentWorld;
@ -288,17 +290,17 @@ export default defineComponent({
let targetWorld = null;
if(!this.leaflet) {
console.warn(`Cannot follow ${player.account}. Map not yet initialized.`);
console.warn(`Cannot follow ${player.name}. Map not yet initialized.`);
return;
}
if(player.hidden) {
console.warn(`Cannot follow ${player.account}. Player is hidden from the map.`);
console.warn(`Cannot follow ${player.name}. Player is hidden from the map.`);
return;
}
if(!player.location.world) {
console.warn(`Cannot follow ${player.account}. Player isn't in a known world.`);
console.warn(`Cannot follow ${player.name}. Player isn't in a known world.`);
return;
}
@ -309,7 +311,7 @@ export default defineComponent({
}
if (!targetWorld) {
console.warn(`Cannot follow ${player.account}. Player isn't in a known world.`);
console.warn(`Cannot follow ${player.name}. Player isn't in a known world.`);
return;
}
@ -320,7 +322,7 @@ export default defineComponent({
if(map !== store.state.currentMap && (targetWorld !== currentWorld || newFollow)) {
this.scheduledPan = player.location;
if(newFollow) {
if(newFollow && store.state.configuration.followZoom) {
console.log(`Setting zoom for new follow ${store.state.configuration.followZoom}`);
this.scheduledZoom = store.state.configuration.followZoom;
}
@ -330,7 +332,7 @@ export default defineComponent({
} else {
this.leaflet!.panTo(store.state.currentMap?.locationToLatLng(player.location));
if(newFollow) {
if(newFollow && store.state.configuration.followZoom) {
console.log(`Setting zoom for new follow ${store.state.configuration.followZoom}`);
this.leaflet!.setZoom(store.state.configuration.followZoom);
}

View File

@ -1,5 +1,5 @@
<!--
- Copyright 2020 James Lyne
- Copyright 2021 James Lyne
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
@ -17,8 +17,9 @@
<template>
<section class="sidebar" role="none" ref="sidebar">
<header class="sidebar__buttons">
<button v-if="mapCount > 1" :class="{'button--maps': true}" @click="toggleMaps" :title="messageWorlds"
:aria-label="messageWorlds" :aria-expanded="mapsVisible" @keydown="handleMapsKeydown">
<button v-if="mapCount > 1 || serverCount > 1" :class="{'button--maps': true}" @click="toggleMaps"
:title="messageWorlds" :aria-label="messageWorlds" :aria-expanded="mapsVisible"
@keydown="handleMapsKeydown">
<SvgIcon name="maps"></SvgIcon>
</button>
<button v-if="playerMakersEnabled" :class="{'button--players': true}" @click="togglePlayers"

View File

@ -1,5 +1,5 @@
<!--
- Copyright 2020 James Lyne
- Copyright 2021 James Lyne
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.

View File

@ -1,5 +1,5 @@
<!--
- Copyright 2020 James Lyne
- Copyright 2021 James Lyne
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
@ -24,15 +24,15 @@
<script lang="ts">
import {defineComponent, ref, onMounted, computed} from "@vue/runtime-core";
import {DynmapChat} from "@/dynmap";
import {getMinecraftHead} from '@/util';
import {useStore} from "@/store";
import defaultImage from '@/assets/images/player_face.png';
import {LiveAtlasChat} from "@/index";
export default defineComponent({
props: {
message: {
type: Object as () => DynmapChat,
type: Object as () => LiveAtlasChat,
required: true,
}
},

View File

@ -1,3 +1,19 @@
<!--
- Copyright 2021 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.
-->
<template>
<nav role="none" id="map-context-menu" ref="menuElement" :style="style" @keydown="handleKeydown">
<ul class="menu" role="menu">

View File

@ -1,5 +1,5 @@
<!--
- Copyright 2020 James Lyne
- Copyright 2021 James Lyne
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.

View File

@ -1,5 +1,5 @@
<!--
- Copyright 2020 James Lyne
- Copyright 2021 James Lyne
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.

View File

@ -1,5 +1,5 @@
<!--
- Copyright 2020 James Lyne
- Copyright 2021 James Lyne
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.

View File

@ -1,5 +1,5 @@
<!--
- Copyright 2020 James Lyne
- Copyright 2021 James Lyne
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.

View File

@ -1,5 +1,5 @@
<!--
- Copyright 2020 James Lyne
- Copyright 2021 James Lyne
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.

View File

@ -1,5 +1,5 @@
<!--
- Copyright 2020 James Lyne
- Copyright 2021 James Lyne
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
@ -18,10 +18,10 @@
import {defineComponent, onUnmounted, computed, watch} from "@vue/runtime-core";
import {Map} from 'leaflet';
import {useStore} from "@/store";
import {ActionTypes} from "@/store/action-types";
import {getMinecraftTime} from "@/util";
import {DynmapTileLayer} from "@/leaflet/tileLayer/DynmapTileLayer";
import LiveAtlasMapDefinition from "@/model/LiveAtlasMapDefinition";
import {LiveAtlasTileLayer} from "@/leaflet/tileLayer/LiveAtlasTileLayer";
import {Pl3xmapTileLayer} from "@/leaflet/tileLayer/Pl3xmapTileLayer";
export default defineComponent({
props: {
@ -40,58 +40,32 @@ export default defineComponent({
},
setup(props) {
let updateFrame = 0,
stopUpdateWatch: Function;
const store = useStore(),
night = computed(() => getMinecraftTime(store.state.currentWorldState.timeOfDay).night),
active = computed(() => props.map === store.state.currentMap);
let layer: LiveAtlasTileLayer;
if(store.state.currentServer?.type === 'dynmap') {
layer = new DynmapTileLayer({
errorTileUrl: 'images/blank.png',
mapSettings: Object.freeze(JSON.parse(JSON.stringify(props.map))),
night: night.value,
}),
pendingUpdates = computed(() => !!store.state.pendingTileUpdates.length),
active = computed(() => props.map === store.state.currentMap),
enableLayer = () => {
props.leaflet.addLayer(layer);
stopUpdateWatch = watch(pendingUpdates, (newValue, oldValue) => {
if(newValue && !oldValue && !updateFrame) {
handlePendingUpdates();
}
});
} else {
layer = new Pl3xmapTileLayer({
errorTileUrl: 'images/blank.png',
mapSettings: Object.freeze(JSON.parse(JSON.stringify(props.map)))
});
}
const enableLayer = () => {
props.leaflet.addLayer(layer);
},
disableLayer = () => {
layer.remove();
if(stopUpdateWatch) {
stopUpdateWatch();
}
},
handlePendingUpdates = async () => {
const updates = await useStore().dispatch(ActionTypes.POP_TILE_UPDATES, 10);
for(const update of updates) {
layer.updateNamedTile(update.name, update.timestamp);
}
if(pendingUpdates.value) {
// eslint-disable-next-line no-unused-vars
updateFrame = requestAnimationFrame(() => handlePendingUpdates());
} else {
updateFrame = 0;
}
};
watch(active, (newValue) => newValue ? enableLayer() : disableLayer());
watch(night, (newValue) => {
if(props.map.nightAndDay) {
layer.setNight(newValue);
}
});
if(active.value) {
enableLayer();
@ -99,10 +73,6 @@ export default defineComponent({
onUnmounted(() => {
disableLayer();
if(updateFrame) {
cancelAnimationFrame(updateFrame);
}
});
},

View File

@ -1,5 +1,5 @@
<!--
- Copyright 2020 James Lyne
- Copyright 2021 James Lyne
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
@ -24,13 +24,13 @@
<script lang="ts">
import {defineComponent, computed} from "@vue/runtime-core";
import {useStore} from "@/store";
import {DynmapMarkerSet} from "@/dynmap";
import Areas from "@/components/map/vector/Areas.vue";
import Circles from "@/components/map/vector/Circles.vue";
import Lines from "@/components/map/vector/Lines.vue";
import Markers from "@/components/map/vector/Markers.vue";
import LiveAtlasLeafletMap from "@/leaflet/LiveAtlasLeafletMap";
import LiveAtlasLayerGroup from "@/leaflet/layer/LiveAtlasLayerGroup";
import {LiveAtlasMarkerSet} from "@/index";
export default defineComponent({
components: {
@ -47,7 +47,7 @@ export default defineComponent({
},
markerSet: {
type: Object as () => DynmapMarkerSet,
type: Object as () => LiveAtlasMarkerSet,
required: true,
}
},

View File

@ -1,5 +1,5 @@
<!--
- Copyright 2020 James Lyne
- Copyright 2021 James Lyne
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
@ -62,22 +62,24 @@ export default defineComponent({
},
mounted() {
const store = useStore();
if(!this.componentSettings!.hideByDefault) {
this.leaflet.getLayerManager().addLayer(
this.layerGroup,
true,
useStore().state.messages.playersHeading,
store.state.components.playerMarkers!.layerName,
this.componentSettings!.layerPriority);
} else {
this.leaflet.getLayerManager().addHiddenLayer(
this.layerGroup,
useStore().state.messages.playersHeading,
store.state.components.playerMarkers!.layerName,
this.componentSettings!.layerPriority);
}
},
unmounted() {
this.leaflet.removeLayer(this.layerGroup);
this.leaflet.getLayerManager().removeLayer(this.layerGroup);
},
render() {

View File

@ -1,5 +1,5 @@
<!--
- Copyright 2020 James Lyne
- Copyright 2021 James Lyne
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
@ -17,15 +17,15 @@
<script lang="ts">
import {defineComponent, computed, ref, onMounted, onUnmounted} from "@vue/runtime-core";
import {LayerGroup} from 'leaflet';
import {DynmapChat, DynmapPlayer} from "@/dynmap";
import {useStore} from "@/store";
import {PlayerMarker} from "@/leaflet/marker/PlayerMarker";
import {Popup} from "leaflet";
import {LiveAtlasChat, LiveAtlasPlayer} from "@/index";
export default defineComponent({
props: {
player: {
type: Object as () => DynmapPlayer,
type: Object as () => LiveAtlasPlayer,
required: true
},
layerGroup: {
@ -79,7 +79,7 @@ export default defineComponent({
//Chat messages to show in the popup
playerChat = computed(() => {
const messages: DynmapChat[] = [];
const messages: LiveAtlasChat[] = [];
if(!chatBalloonsEnabled.value) {
return messages;
@ -96,7 +96,7 @@ export default defineComponent({
break;
}
if(message.type === 'chat' && message.playerAccount === props.player.account) {
if(message.type === 'chat' && message.playerAccount === props.player.name) {
messages.push(message);
}
}
@ -218,7 +218,7 @@ export default defineComponent({
}
},
},
playerChat(newValue: DynmapChat[]) {
playerChat(newValue: LiveAtlasChat[]) {
if(!this.chatBalloonsEnabled || !this.markerVisible || !newValue.length) {
return;
}

View File

@ -1,5 +1,5 @@
<!--
- Copyright 2020 James Lyne
- Copyright 2021 James Lyne
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
@ -17,18 +17,17 @@
<script lang="ts">
import {defineComponent, computed, onMounted, onUnmounted, watch} from "@vue/runtime-core";
import {useStore} from "@/store";
import {DynmapArea, DynmapMarkerSet} from "@/dynmap";
import {ActionTypes} from "@/store/action-types";
import {createArea, updateArea} from "@/util/areas";
import {getPointConverter} from '@/util';
import LiveAtlasLayerGroup from "@/leaflet/layer/LiveAtlasLayerGroup";
import LiveAtlasPolygon from "@/leaflet/vector/LiveAtlasPolygon";
import LiveAtlasPolyline from "@/leaflet/vector/LiveAtlasPolyline";
import {LiveAtlasArea, LiveAtlasMarkerSet} from "@/index";
export default defineComponent({
props: {
set: {
type: Object as () => DynmapMarkerSet,
type: Object as () => LiveAtlasMarkerSet,
required: true,
},
layerGroup: {
@ -50,9 +49,9 @@ export default defineComponent({
layers = Object.freeze(new Map()) as Map<string, LiveAtlasPolygon | LiveAtlasPolyline>,
createAreas = () => {
const converter = getPointConverter();
const converter = currentMap.value!.locationToLatLng.bind(currentMap.value);
props.set.areas.forEach((area: DynmapArea, id: string) => {
props.set.areas.forEach((area: LiveAtlasArea, id: string) => {
const layer = createArea(area, converter);
layers.set(id, layer);
@ -72,18 +71,17 @@ export default defineComponent({
},
handlePendingUpdates = async () => {
const updates = await useStore().dispatch(ActionTypes.POP_AREA_UPDATES, {
const updates = await store.dispatch(ActionTypes.POP_AREA_UPDATES, {
markerSet: props.set.id,
amount: 10,
});
const converter = getPointConverter();
}),
converter = currentMap.value!.locationToLatLng.bind(currentMap.value);
for(const update of updates) {
if(update.removed) {
deleteArea(update.id);
} else {
const layer = updateArea(layers.get(update.id), update.payload as DynmapArea, converter)
const layer = updateArea(layers.get(update.id), update.payload as LiveAtlasArea, converter);
if(!layers.has(update.id)) {
props.layerGroup.addLayer(layer);
@ -101,10 +99,9 @@ export default defineComponent({
}
};
//FIXME: Prevent unnecessary repositioning when changing worlds
watch(currentMap, (newValue) => {
if(newValue) {
const converter = getPointConverter();
watch(currentMap, (newValue, oldValue) => {
if(newValue && (!oldValue || oldValue.world === newValue.world)) {
const converter = newValue.locationToLatLng.bind(newValue);
for (const [id, area] of props.set.areas) {
updateArea(layers.get(id), area, converter);

View File

@ -1,5 +1,5 @@
<!--
- Copyright 2020 James Lyne
- Copyright 2021 James Lyne
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
@ -17,18 +17,17 @@
<script lang="ts">
import {defineComponent, computed, onMounted, onUnmounted, watch} from "@vue/runtime-core";
import {useStore} from "@/store";
import {DynmapCircle, DynmapMarkerSet} from "@/dynmap";
import {ActionTypes} from "@/store/action-types";
import {createCircle, updateCircle} from "@/util/circles";
import {getPointConverter} from '@/util';
import LiveAtlasPolyline from "@/leaflet/vector/LiveAtlasPolyline";
import LiveAtlasPolygon from "@/leaflet/vector/LiveAtlasPolygon";
import LiveAtlasLayerGroup from "@/leaflet/layer/LiveAtlasLayerGroup";
import {LiveAtlasCircle, LiveAtlasMarkerSet} from "@/index";
export default defineComponent({
props: {
set: {
type: Object as () => DynmapMarkerSet,
type: Object as () => LiveAtlasMarkerSet,
required: true,
},
layerGroup: {
@ -50,9 +49,9 @@ export default defineComponent({
layers = Object.freeze(new Map<string, LiveAtlasPolyline | LiveAtlasPolygon>()),
createCircles = () => {
const converter = getPointConverter();
const converter = currentMap.value!.locationToLatLng.bind(store.state.currentMap);
props.set.circles.forEach((circle: DynmapCircle, id: string) => {
props.set.circles.forEach((circle: LiveAtlasCircle, id: string) => {
const layer = createCircle(circle, converter);
layers.set(id, layer);
@ -75,15 +74,14 @@ export default defineComponent({
const updates = await useStore().dispatch(ActionTypes.POP_CIRCLE_UPDATES, {
markerSet: props.set.id,
amount: 10,
});
const converter = getPointConverter();
}),
converter = currentMap.value!.locationToLatLng.bind(store.state.currentMap);
for(const update of updates) {
if(update.removed) {
deleteCircle(update.id);
} else {
const layer = updateCircle(layers.get(update.id), update.payload as DynmapCircle, converter)
const layer = updateCircle(layers.get(update.id), update.payload as LiveAtlasCircle, converter)
if(!layers.has(update.id)) {
props.layerGroup.addLayer(layer);
@ -101,10 +99,9 @@ export default defineComponent({
}
};
//FIXME: Prevent unnecessary repositioning when changing worlds
watch(currentMap, (newValue) => {
if(newValue) {
const converter = getPointConverter();
watch(currentMap, (newValue, oldValue) => {
if(newValue && (!oldValue || oldValue.world === newValue.world)) {
const converter = currentMap.value!.locationToLatLng.bind(store.state.currentMap);
for (const [id, circle] of props.set.circles) {
updateCircle(layers.get(id), circle, converter);

View File

@ -1,5 +1,5 @@
<!--
- Copyright 2020 James Lyne
- Copyright 2021 James Lyne
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
@ -17,17 +17,16 @@
<script lang="ts">
import {defineComponent, computed, onMounted, onUnmounted, watch} from "@vue/runtime-core";
import {useStore} from "@/store";
import {DynmapLine, DynmapMarkerSet} from "@/dynmap";
import {ActionTypes} from "@/store/action-types";
import {createLine, updateLine} from "@/util/lines";
import {getPointConverter} from '@/util';
import LiveAtlasPolyline from "@/leaflet/vector/LiveAtlasPolyline";
import LiveAtlasLayerGroup from "@/leaflet/layer/LiveAtlasLayerGroup";
import {LiveAtlasLine, LiveAtlasMarkerSet} from "@/index";
export default defineComponent({
props: {
set: {
type: Object as () => DynmapMarkerSet,
type: Object as () => LiveAtlasMarkerSet,
required: true,
},
layerGroup: {
@ -49,9 +48,9 @@ export default defineComponent({
layers = Object.freeze(new Map<string, LiveAtlasPolyline>()),
createLines = () => {
const converter = getPointConverter();
const converter = currentMap.value!.locationToLatLng.bind(store.state.currentMap);
props.set.lines.forEach((line: DynmapLine, id: string) => {
props.set.lines.forEach((line: LiveAtlasLine, id: string) => {
const layer = createLine(line, converter);
layers.set(id, layer);
@ -74,15 +73,14 @@ export default defineComponent({
const updates = await useStore().dispatch(ActionTypes.POP_LINE_UPDATES, {
markerSet: props.set.id,
amount: 10,
});
const converter = getPointConverter();
}),
converter = currentMap.value!.locationToLatLng.bind(store.state.currentMap);
for(const update of updates) {
if(update.removed) {
deleteLine(update.id);
} else {
const layer = updateLine(layers.get(update.id), update.payload as DynmapLine, converter)
const layer = updateLine(layers.get(update.id), update.payload as LiveAtlasLine, converter)
if(!layers.has(update.id)) {
props.layerGroup.addLayer(layer);
@ -100,10 +98,9 @@ export default defineComponent({
}
};
//FIXME: Prevent unnecessary repositioning when changing worlds
watch(currentMap, (newValue) => {
if(newValue) {
const converter = getPointConverter();
watch(currentMap, (newValue, oldValue) => {
if(newValue && (!oldValue || oldValue.world === newValue.world)) {
const converter = currentMap.value!.locationToLatLng.bind(store.state.currentMap);
for (const [id, line] of props.set.lines) {
updateLine(layers.get(id), line, converter);

View File

@ -1,5 +1,5 @@
<!--
- Copyright 2020 James Lyne
- Copyright 2021 James Lyne
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
@ -18,16 +18,15 @@
import {defineComponent, computed, onMounted, onUnmounted, watch} from "@vue/runtime-core";
import {Marker} from 'leaflet';
import {useStore} from "@/store";
import {DynmapMarker, DynmapMarkerSet} from "@/dynmap";
import {ActionTypes} from "@/store/action-types";
import {createMarker, updateMarker} from "@/util/markers";
import LiveAtlasLayerGroup from "@/leaflet/layer/LiveAtlasLayerGroup";
import {getPointConverter} from "@/util";
import {LiveAtlasMarker, LiveAtlasMarkerSet} from "@/index";
export default defineComponent({
props: {
set: {
type: Object as () => DynmapMarkerSet,
type: Object as () => LiveAtlasMarkerSet,
required: true,
},
layerGroup: {
@ -49,9 +48,9 @@ export default defineComponent({
layers = Object.freeze(new Map()) as Map<string, Marker>,
createMarkers = () => {
const converter = getPointConverter();
const converter = currentMap.value!.locationToLatLng.bind(store.state.currentMap);
props.set.markers.forEach((marker: DynmapMarker, id: string) => {
props.set.markers.forEach((marker: LiveAtlasMarker, id: string) => {
const layer = createMarker(marker, converter);
layers.set(id, layer);
@ -74,15 +73,14 @@ export default defineComponent({
const updates = await useStore().dispatch(ActionTypes.POP_MARKER_UPDATES, {
markerSet: props.set.id,
amount: 10,
});
const converter = getPointConverter();
}),
converter = currentMap.value!.locationToLatLng.bind(store.state.currentMap);
for(const update of updates) {
if(update.removed) {
deleteMarker(update.id);
} else {
const layer = updateMarker(layers.get(update.id), update.payload as DynmapMarker, converter);
const layer = updateMarker(layers.get(update.id), update.payload as LiveAtlasMarker, converter);
if(!layers.has(update.id)) {
props.layerGroup.addLayer(layer);
@ -100,11 +98,12 @@ export default defineComponent({
}
};
//FIXME: Prevent unnecessary repositioning when changing worlds
watch(currentMap, (newValue) => {
if(newValue) {
watch(currentMap, (newValue, oldValue) => {
if(newValue && (!oldValue || oldValue.world === newValue.world)) {
const converter = currentMap.value!.locationToLatLng.bind(store.state.currentMap);
for (const [id, marker] of props.set.markers) {
updateMarker(layers.get(id), marker, getPointConverter());
updateMarker(layers.get(id), marker, converter);
}
}
});

View File

@ -1,3 +1,19 @@
<!--
- Copyright 2021 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.
-->
<template>
<section :class="{'sidebar__section': true, 'section--collapsible': true, 'section--collapsed': collapsed}">
<h2 class="section__heading">

View File

@ -1,5 +1,5 @@
<!--
- Copyright 2020 James Lyne
- Copyright 2021 James Lyne
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
@ -21,7 +21,7 @@
<div :class="{'following__target': true, 'following__target--hidden': target.hidden}">
<img width="32" height="32" class="target__icon" :src="image" alt="" />
<span class="target__info">
<span class="target__name" v-html="target.name"></span>
<span class="target__name" v-html="target.displayName"></span>
<span class="target__status" v-show="target.hidden">{{ messageHidden }}</span>
</span>
<button class="target__unfollow" type="button" :title="messageUnfollowTitle"
@ -31,25 +31,25 @@
</template>
<script lang="ts">
import {DynmapPlayer} from "@/dynmap";
import {useStore} from "@/store";
import {MutationTypes} from "@/store/mutation-types";
import {computed, defineComponent, onMounted, ref, watch} from "@vue/runtime-core";
import {getMinecraftHead} from '@/util';
import defaultImage from '@/assets/images/player_face.png';
import {LiveAtlasPlayer} from "@/index";
export default defineComponent({
name: 'FollowTarget',
props: {
target: {
type: Object as () => DynmapPlayer,
type: Object as () => LiveAtlasPlayer,
required: true
}
},
setup(props) {
const store = useStore(),
image = ref(defaultImage),
account = ref(props.target.account),
account = ref(props.target.name),
heading = computed(() => store.state.messages.followingHeading),
messageUnfollow = computed(() => store.state.messages.followingUnfollow),

View File

@ -1,5 +1,5 @@
<!--
- Copyright 2020 James Lyne
- Copyright 2021 James Lyne
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
@ -16,13 +16,13 @@
<template>
<CollapsibleSection name="players" class="players">
<template v-slot:heading>{{ messageHeading }} [{{ players.length }}/{{ maxPlayers }}]</template>
<template v-slot:heading>{{ messageHeading }}</template>
<template v-slot:default>
<div class="section__content">
<input v-if="players && searchEnabled" id="players__search" type="text" name="search"
v-model="searchQuery" :placeholder="messagePlayersSearchPlaceholder" @keydown="onKeydown">
<RadioList v-if="filteredPlayers.length" aria-labelledby="players-heading">
<PlayerListItem v-for="player in filteredPlayers" :key="player.account"
<PlayerListItem v-for="player in filteredPlayers" :key="player.name"
:player="player"></PlayerListItem>
</RadioList>
<div v-else-if="searchQuery" class="section__skeleton">{{ messageSkeletonPlayersSearch }}</div>
@ -49,7 +49,11 @@ export default defineComponent({
setup() {
const store = useStore(),
messageHeading = computed(() => store.state.messages.playersHeading),
messageHeading = computed(() => {
return store.state.messages.playersHeading
.replace('{cur}', players.value.length.toString())
.replace('{max}', maxPlayers.value.toString());
}),
messageSkeletonPlayers = computed(() => store.state.messages.playersSkeleton),
messageSkeletonPlayersSearch = computed(() => store.state.messages.playersSearchSkeleton),
messagePlayersSearchPlaceholder = computed(() => store.state.messages.playersSearchPlaceholder),
@ -62,10 +66,10 @@ export default defineComponent({
const query = searchQuery.value.toLowerCase();
return query ? store.state.sortedPlayers.filter(p => {
return p.account.toLowerCase().indexOf(query) > -1;
return p.name.toLowerCase().indexOf(query) > -1;
}) : store.state.sortedPlayers;
}),
maxPlayers = computed(() => store.state.configuration.maxPlayers),
maxPlayers = computed(() => store.state.maxPlayers || 0),
onKeydown = (e: KeyboardEvent) => {
e.stopImmediatePropagation();

View File

@ -1,5 +1,5 @@
<!--
- Copyright 2020 James Lyne
- Copyright 2021 James Lyne
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
@ -15,36 +15,36 @@
-->
<template>
<input :id="`player-${player.account}`" type="radio" name="player" v-bind:value="player.account" v-model="followTarget"
<input :id="`player-${player.name}`" type="radio" name="player" v-bind:value="player.name" v-model="followTarget"
@click.prevent="onInputClick" />
<label :for="`player-${player.account}`"
<label :for="`player-${player.name}`"
:class="{'player': true, 'player--hidden' : !!player.hidden, 'player--other-world': otherWorld}" :title="title"
@click.prevent="onLabelClick">
<img width="16" height="16" class="player__icon" :src="image" alt="" aria-hidden="true" />
<span class="player__name" v-html="player.name"></span>
<span class="player__name" v-html="player.displayName"></span>
</label>
</template>
<script lang="ts">
import {defineComponent, computed, ref, onMounted} from 'vue';
import {DynmapPlayer} from "@/dynmap";
import {useStore} from "@/store";
import {MutationTypes} from "@/store/mutation-types";
import {getMinecraftHead} from '@/util';
import defaultImage from '@/assets/images/player_face.png';
import {LiveAtlasPlayer} from "@/index";
export default defineComponent({
name: 'PlayerListItem',
props: {
player: {
type: Object as () => DynmapPlayer,
type: Object as () => LiveAtlasPlayer,
required: true
}
},
setup(props) {
const store = useStore(),
otherWorld = computed(() => {
return store.state.configuration.grayHiddenPlayers
return store.state.components.playerMarkers?.grayHiddenPlayers
&& !props.player.hidden
&& (!store.state.currentWorld || store.state.currentWorld.name !== props.player.location.world);
}),
@ -59,7 +59,7 @@ export default defineComponent({
}
}),
followTarget = computed(() => store.state.followTarget ? store.state.followTarget.account : undefined),
followTarget = computed(() => store.state.followTarget ? store.state.followTarget.name : undefined),
pan = () => {
if(!props.player.hidden) {

View File

@ -1,5 +1,5 @@
<!--
- Copyright 2020 James Lyne
- Copyright 2021 James Lyne
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.

View File

@ -1,5 +1,5 @@
<!--
- Copyright 2020 James Lyne
- Copyright 2021 James Lyne
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.

View File

@ -1,5 +1,5 @@
<!--
- Copyright 2020 James Lyne
- Copyright 2021 James Lyne
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.

View File

@ -1,5 +1,5 @@
<!--
- Copyright 2020 James Lyne
- Copyright 2021 James Lyne
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
@ -16,13 +16,13 @@
<template>
<div class="world">
<span class="world__name" aria-hidden="true">{{ world.title }}</span>
<span class="world__name" aria-hidden="true">{{ world.displayName }}</span>
<div class="world__maps menu">
<template v-for="[key, map] in world.maps" :key="`${world.name}_${key}`">
<input :id="`${name}-${world.name}-${key}`" type="radio" :name="name"
v-bind:value="[world.name,map.name]" v-model="currentMap"
:aria-labelledby="`${name}-${world.name}-${key}-label`">
<label :id="`${name}-${world.name}-${key}-label`" class="map" :for="`${name}-${world.name}-${key}`" :title="`${world.title} - ${map.title}`">
<label :id="`${name}-${world.name}-${key}-label`" class="map" :for="`${name}-${world.name}-${key}`" :title="`${world.displayName} - ${map.displayName}`">
<SvgIcon :name="map.getIcon()"></SvgIcon>
</label>
</template>

View File

@ -1,5 +1,5 @@
<!--
- Copyright 2020 James Lyne
- Copyright 2021 James Lyne
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.

181
src/dynmap.d.ts vendored
View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 James Lyne
* Copyright 2021 James Lyne
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -14,17 +14,7 @@
* limitations under the License.
*/
import {PathOptions, PointTuple, PolylineOptions} from "leaflet";
import {CoordinatesControlOptions} from "@/leaflet/control/CoordinatesControl";
import {LogoControlOptions} from "@/leaflet/control/LogoControl";
import {ClockControlOptions} from "@/leaflet/control/ClockControl";
import {
Coordinate,
LiveAtlasLocation,
LiveAtlasServerMessageConfig,
LiveAtlasWorldDefinition,
LiveAtlasWorldState
} from "@/index";
import {LiveAtlasArea, LiveAtlasCircle, LiveAtlasLine, LiveAtlasMarker} from "@/index";
declare global {
// noinspection JSUnusedGlobalSymbols
@ -43,155 +33,6 @@ type DynmapUrlConfig = {
markers: string;
}
interface DynmapServerConfig {
version: string;
defaultMap?: string;
defaultWorld?: string;
defaultZoom: number;
followMap?: string;
followZoom: number;
updateInterval: number;
showLayerControl: boolean;
title: string;
loginEnabled: boolean;
maxPlayers: number;
grayHiddenPlayers: boolean;
expandUI: boolean;
hash: number;
}
interface DynmapComponentConfig {
markers: DynmapMarkersConfig;
playerMarkers?: DynmapPlayerMarkersConfig;
coordinatesControl?: CoordinatesControlOptions;
clockControl ?: ClockControlOptions;
linkControl: boolean;
logoControls: Array<LogoControlOptions>;
chatBox?: DynmapChatBoxConfig;
chatSending?: DynmapChatSendingConfig;
chatBalloons: boolean;
}
interface DynmapMarkersConfig {
showLabels: boolean
}
interface DynmapPlayerMarkersConfig {
hideByDefault: boolean;
layerName: string;
layerPriority: number;
showBodies: boolean;
showSkinFaces: boolean;
showHealth: boolean;
smallFaces: boolean;
}
interface DynmapChatBoxConfig {
allowUrlName: boolean;
showPlayerFaces: boolean;
messageLifetime: number;
messageHistory: number;
}
interface DynmapChatSendingConfig {
loginRequired: boolean;
maxLength: number;
cooldown: number;
}
interface DynmapConfigurationResponse {
config: DynmapServerConfig,
messages: LiveAtlasServerMessageConfig,
worlds: Array<LiveAtlasWorldDefinition>,
components: DynmapComponentConfig,
loggedIn: boolean,
}
interface DynmapUpdateResponse {
worldState: LiveAtlasWorldState;
configHash: number;
playerCount: number;
players: Set<DynmapPlayer>;
updates: DynmapUpdates;
timestamp: number;
}
interface DynmapPlayer {
account: string;
armor: number;
health: number;
name: string;
sort: number;
hidden: boolean;
location: LiveAtlasLocation;
}
interface DynmapMarkerSet {
id: string,
label: string;
hidden: boolean;
priority: number;
minZoom?: number;
maxZoom?: number;
showLabels?: boolean;
markers: Map<string, DynmapMarker>;
areas: Map<string, DynmapArea>;
lines: Map<string, DynmapLine>;
circles: Map<string, DynmapCircle>;
}
interface DynmapMarker {
dimensions: PointTuple;
icon: string;
label: string;
isHTML: boolean;
location: Coordinate;
minZoom?: number;
maxZoom?: number;
popupContent?: string;
}
interface DynmapArea {
style: PolylineOptions;
label: string;
isHTML: boolean;
x: Array<number>;
y: PointTuple;
z: Array<number>;
minZoom?: number;
maxZoom?: number;
popupContent?: string;
}
interface DynmapLine {
x: Array<number>;
y: Array<number>;
z: Array<number>;
style: PolylineOptions;
label: string;
isHTML: boolean;
minZoom?: number;
maxZoom?: number;
popupContent?: string;
}
interface DynmapCircle {
location: Coordinate;
radius: PointTuple;
style: PathOptions;
label: string;
isHTML: boolean;
minZoom?: number;
maxZoom?: number;
popupContent?: string;
}
interface DynmapUpdates {
markerSets: Map<string, DynmapMarkerSetUpdates>,
tiles: Array<DynmapTileUpdate>,
chat: Array<any> //TODO
}
interface DynmapMarkerSetUpdates {
markerUpdates: Array<DynmapMarkerUpdate>
areaUpdates: Array<DynmapAreaUpdate>
@ -215,32 +56,22 @@ interface DynmapUpdate {
}
interface DynmapMarkerUpdate extends DynmapUpdate {
payload?: DynmapMarker
payload?: LiveAtlasMarker
}
interface DynmapAreaUpdate extends DynmapUpdate {
payload?: DynmapArea
payload?: LiveAtlasArea
}
interface DynmapCircleUpdate extends DynmapUpdate {
payload?: DynmapCircle
payload?: LiveAtlasCircle
}
interface DynmapLineUpdate extends DynmapUpdate {
payload?: DynmapLine
payload?: LiveAtlasLine
}
interface DynmapTileUpdate {
name: string
timestamp: number
}
interface DynmapChat {
type: 'chat' | 'playerjoin' | 'playerleave';
playerAccount?: string;
playerName?: string;
channel?: string;
message?: string;
source?: string;
timestamp: number;
}

197
src/index.d.ts vendored
View File

@ -1,6 +1,26 @@
/*
* Copyright 2021 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.
*/
import {State} from "@/store";
import {DynmapPlayer, DynmapUrlConfig} from "@/dynmap";
import {DynmapUrlConfig} from "@/dynmap";
import LiveAtlasMapDefinition from "@/model/LiveAtlasMapDefinition";
import {PathOptions, PointTuple, PolylineOptions} from "leaflet";
import {CoordinatesControlOptions} from "@/leaflet/control/CoordinatesControl";
import {ClockControlOptions} from "@/leaflet/control/ClockControl";
import {LogoControlOptions} from "@/leaflet/control/LogoControl";
declare module "*.png" {
const value: any;
@ -40,13 +60,11 @@ interface LiveAtlasGlobalConfig {
}
interface LiveAtlasServerDefinition {
id: string
label?: string
}
interface LiveAtlasDynmapServerDefinition extends LiveAtlasServerDefinition {
type: 'dynmap',
dynmap: DynmapUrlConfig,
id: string;
label?: string;
type: 'dynmap' | 'pl3xmap';
dynmap?: DynmapUrlConfig;
pl3xmap?: string;
}
// Messages defined directly in LiveAtlas and used for all servers
@ -108,16 +126,27 @@ export type LiveAtlasUIElement = 'layers' | 'chat' | 'players' | 'maps' | 'setti
export type LiveAtlasSidebarSection = 'servers' | 'players' | 'maps';
export type LiveAtlasDimension = 'overworld' | 'nether' | 'end';
interface LiveAtlasSortedPlayers extends Array<DynmapPlayer> {
interface LiveAtlasPlayer {
name: string;
displayName: string;
uuid?: string;
armor: number;
health: number;
sort: number;
hidden: boolean;
location: LiveAtlasLocation;
}
interface LiveAtlasSortedPlayers extends Array<LiveAtlasPlayer> {
dirty?: boolean;
}
interface LiveAtlasWorldDefinition {
seaLevel: number;
name: string;
displayName: string;
dimension: LiveAtlasDimension;
protected: boolean;
title: string;
height: number;
center: Coordinate;
maps: Map<string, LiveAtlasMapDefinition>;
@ -136,3 +165,151 @@ interface LiveAtlasParsedUrl {
zoom?: number;
legacy: boolean;
}
interface LiveAtlasMapProvider {
loadServerConfiguration(): Promise<void>;
populateWorld(world: LiveAtlasWorldDefinition): Promise<void>;
startUpdates(): void;
stopUpdates(): void;
sendChatMessage(message: string): void;
destroy(): void;
getPlayerHeadUrl(entry: HeadQueueEntry): string;
getTilesUrl(): string;
getMarkerIconUrl(icon: string): string;
}
interface LiveAtlasMarkerSet {
id: string,
label: string;
hidden: boolean;
priority: number;
minZoom?: number;
maxZoom?: number;
showLabels?: boolean;
markers: Map<string, LiveAtlasMarker>;
areas: Map<string, LiveAtlasArea>;
lines: Map<string, LiveAtlasLine>;
circles: Map<string, LiveAtlasCircle>;
}
interface LiveAtlasMarker {
dimensions: PointTuple;
icon: string;
label: string;
isLabelHTML: boolean;
location: Coordinate;
minZoom?: number;
maxZoom?: number;
popupContent?: string;
}
interface LiveAtlasPath {
style: PathOptions;
minZoom?: number;
maxZoom?: number;
popupContent?: string;
tooltipContent?: string;
isPopupHTML: boolean;
}
interface LiveAtlasArea extends LiveAtlasPath {
style: PolylineOptions;
outline: boolean;
points: Coordinate[] | Coordinate[][] | Coordinate[][][]
}
interface LiveAtlasLine extends LiveAtlasPath {
points: Coordinate[];
style: PolylineOptions;
}
interface LiveAtlasCircle extends LiveAtlasPath {
location: Coordinate;
radius: PointTuple;
style: PathOptions;
}
interface HeadQueueEntry {
cacheKey: string;
name: string;
uuid?: string;
size: string;
image: HTMLImageElement;
}
interface LiveAtlasServerConfig {
defaultMap?: string;
defaultWorld?: string;
defaultZoom: number;
followMap?: string;
followZoom?: number;
title: string;
expandUI: boolean;
}
interface LiveAtlasComponentConfig {
markers: {
showLabels: boolean;
};
playerMarkers?: LiveAtlasPlayerMarkerConfig;
coordinatesControl?: CoordinatesControlOptions;
clockControl?: ClockControlOptions;
linkControl: boolean;
layerControl: boolean;
logoControls: Array<LogoControlOptions>;
chatBox?: LiveAtlasChatBoxConfig;
chatSending?: LiveAtlasChatSendingConfig;
chatBalloons: boolean;
login: boolean;
}
interface LiveAtlasPartialComponentConfig {
markers?: {
showLabels: boolean;
};
playerMarkers?: LiveAtlasPlayerMarkerConfig;
coordinatesControl?: CoordinatesControlOptions;
clockControl?: ClockControlOptions;
linkControl?: boolean;
layerControl?: boolean;
logoControls?: Array<LogoControlOptions>;
chatBox?: LiveAtlasChatBoxConfig;
chatSending?: LiveAtlasChatSendingConfig;
chatBalloons?: boolean;
login?: boolean;
}
interface LiveAtlasPlayerMarkerConfig {
grayHiddenPlayers: boolean;
hideByDefault: boolean;
layerName: string;
layerPriority: number;
showBodies: boolean;
showSkinFaces: boolean;
showHealth: boolean;
smallFaces: boolean;
}
interface LiveAtlasChatBoxConfig {
allowUrlName: boolean;
showPlayerFaces: boolean;
messageLifetime: number;
messageHistory: number;
}
interface LiveAtlasChatSendingConfig {
loginRequired: boolean;
maxLength: number;
cooldown: number;
}
interface LiveAtlasChat {
type: 'chat' | 'playerjoin' | 'playerleave';
playerAccount?: string;
playerName?: string;
channel?: string;
message?: string;
source?: string;
timestamp: number;
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 James Lyne
* Copyright 2021 James Lyne
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 James Lyne
* Copyright 2021 James Lyne
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 James Lyne
* Copyright 2021 James Lyne
*
* Some portions of this file were taken from https://github.com/webbukkit/dynmap.
* These portions are Copyright 2020 Dynmap Contributors.

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 James Lyne
* Copyright 2021 James Lyne
*
* Some portions of this file were taken from https://github.com/webbukkit/dynmap.
* These portions are Copyright 2020 Dynmap Contributors.

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 James Lyne
* Copyright 2021 James Lyne
*
* Some portions of this file were taken from https://github.com/webbukkit/dynmap.
* These portions are Copyright 2020 Dynmap Contributors.

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 James Lyne
* Copyright 2021 James Lyne
*
* Some portions of this file were taken from https://github.com/webbukkit/dynmap.
* These portions are Copyright 2020 Dynmap Contributors.

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 James Lyne
* Copyright 2021 James Lyne
*
* Some portions of this file were taken from https://github.com/webbukkit/dynmap.
* These portions are Copyright 2020 Dynmap Contributors.

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 James Lyne
* Copyright 2021 James Lyne
*
* Some portions of this file were taken from https://github.com/webbukkit/dynmap.
* These portions are Copyright 2020 Dynmap Contributors.
@ -47,8 +47,14 @@ export class GenericIcon extends DivIcon {
// @ts-ignore
options: GenericIconOptions;
_image?: HTMLImageElement;
_label?: HTMLSpanElement;
private _image?: HTMLImageElement;
private _label?: HTMLSpanElement;
private _container?: HTMLDivElement;
private _labelCreated: boolean = false;
private _onHover: EventListener = () => {
this.createLabel();
};
constructor(options: GenericIconOptions) {
super(Object.assign(GenericIcon.defaultOptions, options));
@ -60,18 +66,45 @@ export class GenericIcon extends DivIcon {
}
const div = markerContainer.cloneNode(false) as HTMLDivElement,
url = `${useStore().getters.serverConfig.dynmap.markers}_markers_/${this.options.icon}.png`,
url = useStore().state.currentMapProvider!.getMarkerIconUrl(this.options.icon),
size = point(this.options.iconSize as PointExpression);
this._image = markerIcon.cloneNode(false) as HTMLImageElement;
this._label = markerLabel.cloneNode(false) as HTMLSpanElement;
const sizeClass = [size.x, size.y].join('x');
this._image.width = size.x;
this._image.height = size.y;
this._image.src = url;
// @ts-ignore
Icon.prototype._setIconStyles.call(this, div, 'icon');
div.appendChild(this._image);
div.classList.add('marker');
if(this.options.className) {
div.classList.add(this.options.className);
}
//Create label lazily on hover
this._image.addEventListener('mouseover', this._onHover);
this._container = div;
return div;
}
createLabel() {
if(!this._container || this._labelCreated) {
return;
}
this._image?.removeEventListener('mouseover', this._onHover);
const size = point(this.options.iconSize as PointExpression),
sizeClass = [size.x, size.y].join('x');
this._label = markerLabel.cloneNode(false) as HTMLSpanElement;
this._label.classList.add(/*'markerName_' + set.id,*/ `marker__label--${sizeClass}`);
if (this.options.isHtml) {
@ -80,23 +113,13 @@ export class GenericIcon extends DivIcon {
this._label.textContent = this.options.label;
}
// @ts-ignore
Icon.prototype._setIconStyles.call(this, div, 'icon');
div.appendChild(this._image);
div.appendChild(this._label);
div.classList.add('marker');
if(this.options.className) {
div.classList.add(this.options.className);
}
return div;
this._container!.appendChild(this._label);
this._labelCreated = true;
}
update(options: GenericIconOptions) {
if(this._image && options.icon !== this.options.icon) {
this._image!.src = `${useStore().getters.serverConfig.dynmap.markers}_markers_/${options.icon}.png`;
this._image!.src = useStore().state.currentMapProvider!.getMarkerIconUrl(this.options.icon);
this.options.icon = options.icon;
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 James Lyne
* Copyright 2021 James Lyne
*
* Some portions of this file were taken from https://github.com/webbukkit/dynmap.
* These portions are Copyright 2020 Dynmap Contributors.
@ -18,9 +18,9 @@
*/
import {MarkerOptions, DivIcon, DomUtil} from 'leaflet';
import {DynmapPlayer} from "@/dynmap";
import {getMinecraftHead} from '@/util';
import playerImage from '@/assets/images/player_face.png';
import {LiveAtlasPlayer} from "@/index";
const noSkinImage: HTMLImageElement = document.createElement('img');
noSkinImage.height = 16;
@ -49,7 +49,7 @@ export interface PlayerIconOptions extends MarkerOptions {
}
export class PlayerIcon extends DivIcon {
private readonly _player: DynmapPlayer;
private readonly _player: LiveAtlasPlayer;
private _container?: HTMLDivElement;
private _playerImage?: HTMLImageElement;
private _playerInfo?: HTMLSpanElement;
@ -65,7 +65,7 @@ export class PlayerIcon extends DivIcon {
// @ts-ignore
options: PlayerIconOptions;
constructor(player: DynmapPlayer, options: PlayerIconOptions) {
constructor(player: LiveAtlasPlayer, options: PlayerIconOptions) {
super(options);
this._player = player;
}
@ -87,7 +87,7 @@ export class PlayerIcon extends DivIcon {
this._playerName = document.createElement('span');
this._playerName.className = 'player__name';
this._playerName.innerHTML = this._currentName = player.name;
this._playerName.innerHTML = this._currentName = player.displayName;
if (this.options.showSkinFace) {
let size;
@ -151,8 +151,8 @@ export class PlayerIcon extends DivIcon {
return;
}
if(this._player!.name !== this._currentName) {
this._playerName!.innerHTML = this._currentName = this._player!.name;
if(this._player!.displayName !== this._currentName) {
this._playerName!.innerHTML = this._currentName = this._player!.displayName;
}
if(this.options.showHealth) {

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 James Lyne
* Copyright 2021 James Lyne
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -25,7 +25,7 @@ export default class LayerManager {
private readonly map: Map;
constructor(map: Map) {
const showControl = computed(() => useStore().state.configuration.showLayerControl);
const showControl = computed(() => useStore().state.components.layerControl);
this.map = map;
this.layerControl = new LiveAtlasLayerControl({}, {},{
position: 'topleft',

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 James Lyne
* Copyright 2021 James Lyne
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -15,6 +15,7 @@
*/
import {Layer, Map as LeafletMap, LayerGroup, LayerOptions, Util, Marker, Path} from "leaflet";
import {GenericMarker} from "@/leaflet/marker/GenericMarker";
export interface LiveAtlasLayerGroupOptions extends LayerOptions {
id: string; //Added to the name of layer group panes
@ -29,11 +30,11 @@ export interface LiveAtlasLayerGroupOptions extends LayerOptions {
export default class LiveAtlasLayerGroup extends LayerGroup {
// @ts-ignore
options: LiveAtlasLayerGroupOptions;
_zoomLimitedLayers: Set<Layer>; //Layers which are zoom limited and should be checked on zoom
private _zoomLimitedLayers: Set<Layer>; //Layers which are zoom limited and should be checked on zoom
_layers: any;
_markerPane?: HTMLElement;
_zoomEndCallback = () => this._updateLayerVisibility();
private _zoomEndCallback = () => this._updateLayerVisibility();
constructor(options: LiveAtlasLayerGroupOptions) {
super([], options);
@ -80,7 +81,7 @@ export default class LiveAtlasLayerGroup extends LayerGroup {
layer.options.pane = `vectors`;
}
const zoomLimited = this._isLayerZoomLimited(layer);
const zoomLimited = LiveAtlasLayerGroup._isLayerZoomLimited(layer);
if (zoomLimited) {
this._zoomLimitedLayers.add(layer);
@ -89,11 +90,11 @@ export default class LiveAtlasLayerGroup extends LayerGroup {
if (this._map) {
//If layer is zoom limited, only add to map if it should be visible
if (zoomLimited) {
if (this._isLayerVisible(layer, this._map.getZoom())) {
this._map.addLayer(layer);
if (LiveAtlasLayerGroup._isLayerVisible(layer, this._map.getZoom())) {
this._addToMap(layer);
}
} else {
this._map.addLayer(layer);
this._addToMap(layer);
}
}
@ -106,7 +107,19 @@ export default class LiveAtlasLayerGroup extends LayerGroup {
}
update(options: LiveAtlasLayerGroupOptions) {
if(this.options.showLabels !== options.showLabels) {
//Create labels if they are now always visible
//TODO: This will be slow when many markers exist. Is it worth doing?
if(options.showLabels) {
this.eachLayer((layer) => {
if(layer instanceof GenericMarker) {
(layer as GenericMarker).createLabel();
}
});
}
this.options.showLabels = options.showLabels;
}
if(this._markerPane) {
this._markerPane.classList.toggle('leaflet-pane--show-labels', options.showLabels);
@ -128,7 +141,7 @@ export default class LiveAtlasLayerGroup extends LayerGroup {
}
}
_updateLayerVisibility(onAdd?: boolean) {
private _updateLayerVisibility(onAdd?: boolean) {
if(!this._map) {
return;
}
@ -142,34 +155,34 @@ export default class LiveAtlasLayerGroup extends LayerGroup {
this.eachLayer((layer) => {
//Per marker zoom limits take precedence, if present
if(this._zoomLimitedLayers.has(layer)) {
this._isLayerVisible(layer, zoom) ? this._map.addLayer(layer) : this._map.removeLayer(layer);
LiveAtlasLayerGroup._isLayerVisible(layer, zoom) ? this._addToMap(layer) : this._removeFromMap(layer);
} else { //Otherwise apply group zoom limit
visible ? this._map.addLayer(layer) : this._map.removeLayer(layer);
visible ? this._addToMap(layer) : this._removeFromMap(layer);
}
}, this);
//Group isn't zoom limited, but some individual markers are
} else if(this._zoomLimitedLayers.size) {
this._zoomLimitedLayers.forEach((layer) => {
this._isLayerVisible(layer, zoom) ? this._map.addLayer(layer) : this._map.removeLayer(layer);
LiveAtlasLayerGroup._isLayerVisible(layer, zoom) ? this._addToMap(layer) : this._removeFromMap(layer);
});
//Nothing is zoom limited, but we've just been added to the map
} else if(onAdd) {
this.eachLayer(this._map.addLayer, this._map);
this.eachLayer((layer: Layer) => this._addToMap(layer), this._map);
}
}
//Returns if this layer group has zoom limits defined
_isZoomLimited() {
private _isZoomLimited() {
return this.options.maxZoom !== undefined || this.options.minZoom !== undefined;
}
//Returns if the given layer has its own zoom limits defined
_isLayerZoomLimited(layer: Layer) {
private static _isLayerZoomLimited(layer: Layer) {
return ((layer as any).options && (layer as any).options.minZoom !== undefined)
&& ((layer as any).options && (layer as any).options.maxZoom !== undefined);
}
_isLayerVisible(layer: Layer, currentZoom: number) {
private static _isLayerVisible(layer: Layer, currentZoom: number) {
let minZoom = -Infinity,
maxZoom = Infinity;
@ -183,4 +196,17 @@ export default class LiveAtlasLayerGroup extends LayerGroup {
return currentZoom >= minZoom && currentZoom <= maxZoom;
}
private _addToMap(layer: Layer) {
this._map.addLayer(layer)
//Create marker label immediately if labels are visible by default
if(layer instanceof GenericMarker && this.options.showLabels) {
(layer as GenericMarker).createLabel();
}
}
private _removeFromMap(layer: Layer) {
this._map.removeLayer(layer)
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 James Lyne
* Copyright 2021 James Lyne
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -15,16 +15,30 @@
*/
import {MarkerOptions, Marker, Util, LatLngExpression, Icon} from 'leaflet';
import {LiveAtlasMarker} from "@/index";
import {GenericIcon} from "@/leaflet/icon/GenericIcon";
export interface GenericMarkerOptions extends MarkerOptions {
icon: GenericIcon;
minZoom?: number;
maxZoom?: number;
}
export class GenericMarker extends Marker {
constructor(latLng: LatLngExpression, options: GenericMarkerOptions) {
super(latLng, options);
Util.setOptions(this, options);
declare options: GenericMarkerOptions;
constructor(latLng: LatLngExpression, options: LiveAtlasMarker) {
super(latLng, {});
this.options.icon = new GenericIcon({
icon: options.icon,
label: options.label,
iconSize: options.dimensions,
isHtml: options.isLabelHTML,
});
this.options.maxZoom = options.maxZoom;
this.options.minZoom = options.maxZoom;
}
// noinspection JSUnusedGlobalSymbols
@ -35,4 +49,8 @@ export class GenericMarker extends Marker {
getIcon(): Icon.Default {
return this.options.icon as Icon.Default;
}
createLabel(): void {
this.options.icon.createLabel();
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 James Lyne
* Copyright 2021 James Lyne
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -15,8 +15,8 @@
*/
import {LatLng, MarkerOptions, Marker, Util} from 'leaflet';
import {DynmapPlayer} from "@/dynmap";
import {PlayerIcon} from "@/leaflet/icon/PlayerIcon";
import {LiveAtlasPlayer} from "@/index";
export interface PlayerMarkerOptions extends MarkerOptions {
smallFace: boolean,
@ -26,9 +26,9 @@ export interface PlayerMarkerOptions extends MarkerOptions {
}
export class PlayerMarker extends Marker {
private _player: DynmapPlayer;
private _player: LiveAtlasPlayer;
constructor(player: DynmapPlayer, options: PlayerMarkerOptions) {
constructor(player: LiveAtlasPlayer, options: PlayerMarkerOptions) {
super(new LatLng(0, 0), options);
this._player = player;
options.draggable = false;

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 James Lyne
* Copyright 2021 James Lyne
*
* Some portions of this file were taken from https://github.com/webbukkit/dynmap.
* These portions are Copyright 2020 Dynmap Contributors.
@ -17,30 +17,14 @@
* limitations under the License.
*/
import {TileLayer, Coords, DoneCallback, TileLayerOptions, DomUtil, Util, LatLng} from 'leaflet';
import {store} from "@/store";
import {Coords, DoneCallback, DomUtil} from 'leaflet';
import {useStore} from "@/store";
import {Coordinate} from "@/index";
import LiveAtlasMapDefinition from "@/model/LiveAtlasMapDefinition";
export interface DynmapTileLayerOptions extends TileLayerOptions {
mapSettings: LiveAtlasMapDefinition;
errorTileUrl: string;
night?: boolean;
}
export interface DynmapTileLayer extends TileLayer {
options: DynmapTileLayerOptions;
_mapSettings: LiveAtlasMapDefinition;
_cachedTileUrls: Map<string, string>;
_namedTiles: Map<string, DynmapTileElement>;
_tileTemplate: DynmapTileElement;
_loadQueue: DynmapTileElement[];
_loadingTiles: Set<DynmapTileElement>;
locationToLatLng(location: Coordinate): LatLng;
latLngToLocation(latLng: LatLng): Coordinate;
}
import {LiveAtlasTileLayerOptions, LiveAtlasTileLayer} from "@/leaflet/tileLayer/LiveAtlasTileLayer";
import {computed, watch} from "@vue/runtime-core";
import {ComputedRef} from "@vue/reactivity";
import {WatchStopHandle} from "vue";
import {ActionTypes} from "@/store/action-types";
export interface DynmapTile {
active?: boolean;
@ -68,43 +52,59 @@ export interface TileInfo {
fmt: string;
}
const store = useStore();
// noinspection JSUnusedGlobalSymbols
export class DynmapTileLayer extends TileLayer {
constructor(options: DynmapTileLayerOptions) {
export class DynmapTileLayer extends LiveAtlasTileLayer {
private readonly _cachedTileUrls: Map<any, any> = Object.seal(new Map());
private readonly _namedTiles: Map<any, any> = Object.seal(new Map());
private readonly _loadQueue: DynmapTileElement[] = [];
private readonly _loadingTiles: Set<DynmapTileElement> = Object.seal(new Set());
private readonly _tileTemplate: DynmapTileElement;
private readonly _baseUrl: string;
private readonly _night: ComputedRef<boolean>;
private readonly _pendingUpdates: ComputedRef<boolean>;
private readonly _nightUnwatch: WatchStopHandle;
private readonly _updateUnwatch: WatchStopHandle;
private _updateFrame: number = 0;
// @ts-ignore
declare options: DynmapTileLayerOptions;
constructor(options: LiveAtlasTileLayerOptions) {
super('', options);
this._mapSettings = options.mapSettings;
options.maxZoom = this._mapSettings.nativeZoomLevels + this._mapSettings.extraZoomLevels;
options.maxNativeZoom = this._mapSettings.nativeZoomLevels;
options.zoomReverse = true;
options.tileSize = 128;
options.minZoom = 0;
Util.setOptions(this, options);
if (options.mapSettings === null) {
throw new TypeError("mapSettings missing");
}
this._cachedTileUrls = Object.seal(new Map());
this._namedTiles = Object.seal(new Map());
this._loadQueue = [];
this._loadingTiles = Object.seal(new Set());
this._tileTemplate = DomUtil.create('img', 'leaflet-tile') as DynmapTileElement;
this._tileTemplate.style.width = this._tileTemplate.style.height = this.options.tileSize + 'px';
this._tileTemplate.alt = '';
this._tileTemplate.tileName = '';
this._tileTemplate.setAttribute('role', 'presentation');
this._baseUrl = store.state.currentMapProvider!.getTilesUrl();
Object.seal(this._tileTemplate);
if(this.options.crossOrigin || this.options.crossOrigin === '') {
this._tileTemplate.crossOrigin = this.options.crossOrigin === true ? '' : this.options.crossOrigin;
}
this._pendingUpdates = computed(() => !!store.state.pendingTileUpdates.length);
this._updateUnwatch = watch(this._pendingUpdates, (newValue, oldValue) => {
if(newValue && !oldValue && !this._updateFrame) {
this.handlePendingUpdates();
}
});
this._night = computed(() => store.getters.night);
this._nightUnwatch = watch(this._night, () => {
if(this._mapSettings.nightAndDay) {
this.redraw();
}
});
}
getTileName(coords: Coordinate) {
private getTileName(coords: Coordinate) {
const info = this.getTileInfo(coords);
// Y is inverted for HD-map.
info.y = -info.y;
@ -116,12 +116,12 @@ export class DynmapTileLayer extends TileLayer {
return this.getTileUrlFromName(this.getTileName(coords));
}
getTileUrlFromName(name: string, timestamp?: number) {
private getTileUrlFromName(name: string, timestamp?: number) {
let url = this._cachedTileUrls.get(name);
if (!url) {
const path = escape(`${this._mapSettings.world.name}/${name}`);
url = `${store.getters.serverConfig.dynmap.tiles}${path}`;
url = `${this._baseUrl}${path}`;
if(typeof timestamp !== 'undefined') {
url += (url.indexOf('?') === -1 ? `?timestamp=${timestamp}` : `&timestamp=${timestamp}`);
@ -133,7 +133,7 @@ export class DynmapTileLayer extends TileLayer {
return url;
}
updateNamedTile(name: string, timestamp: number) {
private updateNamedTile(name: string, timestamp: number) {
const tile = this._namedTiles.get(name);
this._cachedTileUrls.delete(name);
@ -240,14 +240,14 @@ export class DynmapTileLayer extends TileLayer {
}
// Some helper functions.
zoomprefix(amount: number) {
private zoomprefix(amount: number) {
// amount == 0 -> ''
// amount == 1 -> 'z_'
// amount == 2 -> 'zz_'
return 'z'.repeat(amount) + (amount === 0 ? '' : '_');
}
getTileInfo(coords: Coordinate): TileInfo {
private getTileInfo(coords: Coordinate): TileInfo {
// zoom: max zoomed in = this.options.maxZoom, max zoomed out = 0
// izoom: max zoomed in = 0, max zoomed out = this.options.maxZoom
// zoomoutlevel: izoom < mapzoomin -> 0, else -> izoom - mapzoomin (which ranges from 0 till mapzoomout)
@ -259,7 +259,7 @@ export class DynmapTileLayer extends TileLayer {
return {
prefix: this._mapSettings.prefix,
nightday: (this._mapSettings.nightAndDay && !this.options.night) ? '_day' : '',
nightday: (this._mapSettings.nightAndDay && !this._night.value) ? '_day' : '',
scaledx: x >> 5,
scaledy: y >> 5,
zoom: this.zoomprefix(zoomoutlevel),
@ -270,10 +270,34 @@ export class DynmapTileLayer extends TileLayer {
};
}
setNight(night: boolean) {
if(this.options.night !== night) {
this.options.night = night;
this.redraw();
private async handlePendingUpdates() {
const updates = await store.dispatch(ActionTypes.POP_TILE_UPDATES, 10);
for(const update of updates) {
this.updateNamedTile(update.name, update.timestamp);
}
if(this._pendingUpdates.value) {
// eslint-disable-next-line no-unused-vars
this._updateFrame = requestAnimationFrame(() => this.handlePendingUpdates());
} else {
this._updateFrame = 0;
}
}
remove() {
super.remove();
this._nightUnwatch();
if(this._updateFrame) {
cancelAnimationFrame(this._updateFrame);
}
if(this._updateUnwatch) {
this._updateUnwatch();
}
return this;
}
}

View File

@ -0,0 +1,46 @@
/*
* Copyright 2021 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.
*/
import {TileLayer, TileLayerOptions, Util} from 'leaflet';
import LiveAtlasMapDefinition from "@/model/LiveAtlasMapDefinition";
export interface LiveAtlasTileLayerOptions extends TileLayerOptions {
mapSettings: LiveAtlasMapDefinition;
errorTileUrl: string;
}
// noinspection JSUnusedGlobalSymbols
export abstract class LiveAtlasTileLayer extends TileLayer {
protected _mapSettings: LiveAtlasMapDefinition;
declare options: LiveAtlasTileLayerOptions;
protected constructor(url: string, options: LiveAtlasTileLayerOptions) {
super(url, options);
this._mapSettings = options.mapSettings;
options.maxZoom = this._mapSettings.nativeZoomLevels + this._mapSettings.extraZoomLevels;
options.maxNativeZoom = this._mapSettings.nativeZoomLevels;
options.zoomReverse = true;
options.tileSize = 128;
options.minZoom = 0;
Util.setOptions(this, options);
if (options.mapSettings === null) {
throw new TypeError("mapSettings missing");
}
}
}

View File

@ -0,0 +1,34 @@
/*
* Copyright 2021 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.
*/
import {LiveAtlasTileLayer, LiveAtlasTileLayerOptions} from "@/leaflet/tileLayer/LiveAtlasTileLayer";
import {useStore} from "@/store";
import {Util} from "leaflet";
// noinspection JSUnusedGlobalSymbols
export class Pl3xmapTileLayer extends LiveAtlasTileLayer {
constructor(options: LiveAtlasTileLayerOptions) {
const worldName = options.mapSettings.world.name,
baseUrl = useStore().state.currentMapProvider!.getTilesUrl();
super(`${baseUrl}${worldName}/{z}/{x}_{y}.png`, options);
options.tileSize = 512;
options.zoomReverse = false;
Util.setOptions(this, options);
}
}

View File

@ -1,29 +0,0 @@
/*
* Copyright 2020 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.
*/
import {LatLngExpression, Polygon, PolylineOptions, Util} from "leaflet";
export interface LiveAtlasPolygonOptions extends PolylineOptions {
minZoom?: number;
maxZoom?: number;
}
export default class LiveAtlasPolygon extends Polygon {
constructor(latlngs: LatLngExpression[] | LatLngExpression[][] | LatLngExpression[][][], options?: LiveAtlasPolygonOptions) {
super(latlngs, options);
Util.setOptions(this, options);
}
}

View File

@ -1,29 +0,0 @@
/*
* Copyright 2020 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.
*/
import {LatLngExpression, Polyline, PolylineOptions, Util} from "leaflet";
export interface LiveAtlasPolylineOptions extends PolylineOptions {
minZoom?: number;
maxZoom?: number;
}
export default class LiveAtlasPolyline extends Polyline {
constructor(latlngs: LatLngExpression[] | LatLngExpression[][], options?: LiveAtlasPolylineOptions) {
super(latlngs, options);
Util.setOptions(this, options);
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 James Lyne
* Copyright 2021 James Lyne
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@ -1,3 +1,19 @@
/*
* Copyright 2021 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.
*/
import {Coordinate, LiveAtlasWorldDefinition} from "@/index";
import {LatLng} from "leaflet";
import {LiveAtlasProjection} from "@/model/LiveAtlasProjection";
@ -5,8 +21,8 @@ import {LiveAtlasProjection} from "@/model/LiveAtlasProjection";
export interface LiveAtlasMapDefinitionOptions {
world: LiveAtlasWorldDefinition;
name: string;
displayName?: string;
icon?: string;
title?: string;
background?: string;
nightAndDay?: boolean;
backgroundDay?: string;
@ -24,7 +40,7 @@ export default class LiveAtlasMapDefinition {
readonly world: LiveAtlasWorldDefinition;
readonly name: string;
readonly icon?: string;
readonly title: string;
readonly displayName: string;
readonly background: string;
readonly nightAndDay: boolean;
readonly backgroundDay?: string;
@ -35,12 +51,13 @@ export default class LiveAtlasMapDefinition {
private readonly projection?: Readonly<LiveAtlasProjection>;
readonly nativeZoomLevels: number;
readonly extraZoomLevels: number;
readonly scale: number;
constructor(options: LiveAtlasMapDefinitionOptions) {
this.world = options.world; //Ignore append_to_world here otherwise things break
this.name = options.name;
this.icon = options.icon || undefined;
this.title = options.title || '';
this.displayName = options.displayName || '';
this.background = options.background || '#000000';
this.nightAndDay = options.nightAndDay || false;
@ -53,24 +70,25 @@ export default class LiveAtlasMapDefinition {
this.nativeZoomLevels = options.nativeZoomLevels || 1;
this.extraZoomLevels = options.extraZoomLevels || 0;
this.scale = (1 / Math.pow(2, this.nativeZoomLevels));
if(options.mapToWorld || options.worldToMap) {
this.projection = Object.freeze(new LiveAtlasProjection({
this.projection = new LiveAtlasProjection({
mapToWorld: options.mapToWorld || [0, 0, 0, 0, 0, 0, 0, 0, 0],
worldToMap: options.worldToMap || [0, 0, 0, 0, 0, 0, 0, 0, 0],
nativeZoomLevels: this.nativeZoomLevels,
}));
});
}
}
locationToLatLng(location: Coordinate): LatLng {
return this.projection ? this.projection.locationToLatLng(location)
: LiveAtlasMapDefinition.defaultProjection.locationToLatLng(location);
: new LatLng(-location.z * this.scale, location.x * this.scale);
}
latLngToLocation(latLng: LatLng, y: number): Coordinate {
return this.projection ? this.projection.latLngToLocation(latLng, y)
: LiveAtlasMapDefinition.defaultProjection.latLngToLocation(latLng, y);
: {x: latLng.lng / this.scale, y: y, z: -latLng.lat / this.scale};
}
getIcon(): string {
@ -97,14 +115,4 @@ export default class LiveAtlasMapDefinition {
return `block_${worldType}_${mapType}`;
}
static defaultProjection = Object.freeze({
locationToLatLng(location: Coordinate): LatLng {
return new LatLng(location.x, location.z);
},
latLngToLocation(latLng: LatLng, y: number): Coordinate {
return {x: latLng.lat, y: y, z: latLng.lng};
}
})
}

View File

@ -1,59 +0,0 @@
/*
* Copyright 2020 James Lyne
*
* Some portions of this file were taken from https://github.com/webbukkit/dynmap.
* These portions are Copyright 2020 Dynmap 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} from "@/index";
export interface LiveAtlasProjectionOptions {
mapToWorld: [number, number, number, number, number, number, number, number, number],
worldToMap: [number, number, number, number, number, number, number, number, number],
nativeZoomLevels: number
}
export class LiveAtlasProjection {
private readonly options: LiveAtlasProjectionOptions
constructor(options: LiveAtlasProjectionOptions) {
this.options = {
mapToWorld: options.mapToWorld || [0, 0, 0, 0, 0, 0, 0, 0],
worldToMap: options.worldToMap || [0, 0, 0, 0, 0, 0, 0, 0],
nativeZoomLevels: options.nativeZoomLevels || 1
}
}
locationToLatLng(location: Coordinate): LatLng {
const wtp = this.options.worldToMap,
lat = wtp[3] * location.x + wtp[4] * location.y + wtp[5] * location.z,
lng = wtp[0] * location.x + wtp[1] * location.y + wtp[2] * location.z;
return new LatLng(
-((128 - lat) / (1 << this.options.nativeZoomLevels)),
lng / (1 << this.options.nativeZoomLevels));
}
latLngToLocation(latLng: LatLng, y: number): Coordinate {
const ptw = this.options.mapToWorld,
lat = latLng.lng * (1 << this.options.nativeZoomLevels),
lon = 128 + latLng.lat * (1 << this.options.nativeZoomLevels),
x = ptw[0] * lat + ptw[1] * lon + ptw[2] * y,
z = ptw[6] * lat + ptw[7] * lon + ptw[8] * y;
return {x: x, y: y, z: z};
}
}

View File

@ -0,0 +1,821 @@
/*
* Copyright 2021 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.
*/
import {
HeadQueueEntry,
LiveAtlasArea, LiveAtlasChat,
LiveAtlasCircle, LiveAtlasComponentConfig,
LiveAtlasDimension,
LiveAtlasLine,
LiveAtlasMarker,
LiveAtlasMarkerSet,
LiveAtlasPlayer, LiveAtlasServerConfig,
LiveAtlasServerDefinition,
LiveAtlasServerMessageConfig,
LiveAtlasWorldDefinition
} from "@/index";
import {
DynmapMarkerSetUpdates, DynmapTileUpdate, DynmapUpdate
} from "@/dynmap";
import {useStore} from "@/store";
import LiveAtlasMapDefinition from "@/model/LiveAtlasMapDefinition";
import ChatError from "@/errors/ChatError";
import {MutationTypes} from "@/store/mutation-types";
import MapProvider from "@/providers/MapProvider";
import {ActionTypes} from "@/store/action-types";
import {endWorldNameRegex, netherWorldNameRegex, titleColoursRegex} from "@/util";
import {getPoints} from "@/util/areas";
import {getLinePoints} from "@/util/lines";
export default class DynmapMapProvider extends MapProvider {
private configurationAbort?: AbortController = undefined;
private markersAbort?: AbortController = undefined;
private updateAbort?: AbortController = undefined;
private updatesEnabled = false;
private updateTimeout: number = 0;
private updateTimestamp: Date = new Date();
private updateInterval: number = 3000;
constructor(config: LiveAtlasServerDefinition) {
super(config);
}
private static buildServerConfig(response: any): LiveAtlasServerConfig {
return {
defaultMap: response.defaultmap || undefined,
defaultWorld: response.defaultworld || undefined,
defaultZoom: response.defaultzoom || 0,
followMap: response.followmap || undefined,
followZoom: response.followzoom,
title: response.title.replace(titleColoursRegex, '') || 'Dynmap',
expandUI: response.sidebaropened && response.sidebaropened !== 'false', //Sent as a string for some reason
};
}
private static buildMessagesConfig(response: any): LiveAtlasServerMessageConfig {
return {
chatPlayerJoin: response.joinmessage || '',
chatPlayerQuit: response.quitmessage || '',
chatAnonymousJoin: response['msg-hiddennamejoin'] || '',
chatAnonymousQuit: response['msg-hiddennamequit'] || '',
chatErrorNotAllowed: response['msg-chatnotallowed'] || '',
chatErrorRequiresLogin: response['msg-chatrequireslogin'] || '',
chatErrorCooldown: response.spammessage || '',
worldsHeading: response['msg-maptypes'] || '',
playersHeading: response['msg-players'] ? `${response['msg-players']} ({cur}/{max})` : '',
}
}
private buildWorlds(response: any): Array<LiveAtlasWorldDefinition> {
const worlds: Map<string, LiveAtlasWorldDefinition> = new Map<string, LiveAtlasWorldDefinition>();
//Get all the worlds first so we can handle append_to_world properly
(response.worlds || []).forEach((world: any) => {
let worldType: LiveAtlasDimension = 'overworld';
if (netherWorldNameRegex.test(world.name) || (world.name == 'DIM-1')) {
worldType = 'nether';
} else if (endWorldNameRegex.test(world.name) || (world.name == 'DIM1')) {
worldType = 'end';
}
worlds.set(world.name, {
name: world.name,
displayName: world.title || '',
dimension: worldType,
protected: world.protected || false,
height: world.height || 256,
seaLevel: world.sealevel || 64,
center: {
x: world.center.x || 0,
y: world.center.y || 0,
z: world.center.z || 0
},
maps: new Map(),
});
});
(response.worlds || []).forEach((world: any) => {
(world.maps || []).forEach((map: any) => {
const worldName = map.append_to_world || world.name,
w = worlds.get(worldName);
if(!w) {
console.warn(`Ignoring map '${map.name}' associated with non-existent world '${worldName}'`);
return;
}
w.maps.set(map.name, Object.freeze(new LiveAtlasMapDefinition({
world: w, //Ignore append_to_world here otherwise things break
background: map.background || '#000000',
backgroundDay: map.backgroundday || '#000000',
backgroundNight: map.backgroundnight || '#000000',
icon: map.icon || undefined,
imageFormat: map['image-format'] || 'png',
name: map.name || '(Unnamed map)',
nightAndDay: map.nightandday || false,
prefix: map.prefix || '',
protected: map.protected || false,
displayName: map.title || '',
mapToWorld: map.maptoworld || undefined,
worldToMap: map.worldtomap || undefined,
nativeZoomLevels: map.mapzoomout || 1,
extraZoomLevels: map.mapzoomin || 0
})));
});
});
return Array.from(worlds.values());
}
private buildComponents(response: any): LiveAtlasComponentConfig {
const components: LiveAtlasComponentConfig = {
markers: {
showLabels: false,
},
chatBox: undefined,
chatBalloons: false,
playerMarkers: undefined,
coordinatesControl: undefined,
layerControl: response.showlayercontrol && response.showlayercontrol !== 'false', //Sent as a string for some reason
linkControl: false,
clockControl: undefined,
logoControls: [],
login: response['login-enabled'] || false,
};
(response.components || []).forEach((component: any) => {
const type = component.type || "unknown";
switch (type) {
case "markers":
components.markers = {
showLabels: component.showlabel || false,
}
break;
case "playermarkers":
components.playerMarkers = {
grayHiddenPlayers: response.grayplayerswhenhidden || false,
hideByDefault: component.hidebydefault || false,
layerName: component.label || "Players",
layerPriority: component.layerprio || 0,
showBodies: component.showplayerbody || false,
showSkinFaces: component.showplayerfaces || false,
showHealth: component.showplayerhealth || false,
smallFaces: component.smallplayerfaces || false,
}
break;
case "coord":
components.coordinatesControl = {
showY: !(component.hidey || false),
label: component.label || "Location: ",
showRegion: component['show-mcr'] || false,
showChunk: component['show-chunk'] || false,
}
break;
case "link":
components.linkControl = true;
break;
case "digitalclock":
components.clockControl = {
showDigitalClock: true,
showWeather: false,
showTimeOfDay: false,
}
break;
case "timeofdayclock":
components.clockControl = {
showTimeOfDay: true,
showDigitalClock: component.showdigitalclock || false,
showWeather: component.showweather || false,
}
break;
case "logo":
components.logoControls.push({
text: component.text || '',
url: component.linkurl || undefined,
position: component.position.replace('-', '') || 'topleft',
image: component.logourl || undefined,
});
break;
case "chat":
if (response.allowwebchat) {
components.chatSending = {
loginRequired: response['webchat-requires-login'] || false,
maxLength: response['chatlengthlimit'] || 256,
cooldown: response['webchat-interval'] || 5,
}
}
break;
case "chatbox":
components.chatBox = {
allowUrlName: component.allowurlname || false,
showPlayerFaces: component.showplayerfaces || false,
messageLifetime: component.messagettl || Infinity,
messageHistory: component.scrollback || Infinity,
}
break;
case "chatballoon":
components.chatBalloons = true;
}
});
return components;
}
private static buildMarkerSet(id: string, data: any): any {
return {
id,
label: data.label || "Unnamed set",
hidden: data.hide || false,
priority: data.layerprio || 0,
showLabels: data.showlabels || undefined,
minZoom: typeof data.minzoom !== 'undefined' && data.minzoom > -1 ? data.minzoom : undefined,
maxZoom: typeof data.maxzoom !== 'undefined' && data.maxzoom > -1 ? data.maxzoom : undefined,
}
}
private static buildMarkers(data: any): Map<string, LiveAtlasMarker> {
const markers = Object.freeze(new Map()) as Map<string, LiveAtlasMarker>;
for (const key in data) {
if (!Object.prototype.hasOwnProperty.call(data, key)) {
continue;
}
markers.set(key, DynmapMapProvider.buildMarker(data[key]));
}
return markers;
}
private static buildMarker(marker: any): LiveAtlasMarker {
return {
label: marker.label || '',
isLabelHTML: marker.markup || false,
location: {
x: marker.x || 0,
y: marker.y || 0,
z: marker.z || 0,
},
dimensions: marker.dim ? marker.dim.split('x') : [16, 16],
icon: marker.icon || "default",
minZoom: typeof marker.minzoom !== 'undefined' && marker.minzoom > -1 ? marker.minzoom : undefined,
maxZoom: typeof marker.maxzoom !== 'undefined' && marker.maxzoom > -1 ? marker.maxzoom : undefined,
popupContent: marker.desc || undefined,
};
}
private static buildAreas(data: any): Map<string, LiveAtlasArea> {
const areas = Object.freeze(new Map()) as Map<string, LiveAtlasArea>;
for (const key in data) {
if (!Object.prototype.hasOwnProperty.call(data, key)) {
continue;
}
areas.set(key, DynmapMapProvider.buildArea(data[key]));
}
return areas;
}
private static buildArea(area: any): LiveAtlasArea {
const opacity = area.fillopacity || 0,
x = area.x || [0, 0],
y: [number, number] = [area.ybottom || 0, area.ytop || 0],
z = area.z || [0, 0];
return Object.seal({
style: {
color: area.color || '#ff0000',
opacity: area.opacity || 1,
weight: area.weight || 1,
fillColor: area.fillcolor || '#ff0000',
fillOpacity: area.fillopacity || 0,
},
outline: !opacity,
points: getPoints(x, y, z, !opacity),
minZoom: typeof area.minzoom !== 'undefined' && area.minzoom > -1 ? area.minzoom : undefined,
maxZoom: typeof area.maxzoom !== 'undefined' && area.maxzoom > -1 ? area.maxzoom : undefined,
isPopupHTML: area.desc ? true : area.markup || false,
popupContent: area.desc || area.label || undefined,
});
}
private static buildLines(data: any): Map<string, LiveAtlasLine> {
const lines = Object.freeze(new Map()) as Map<string, LiveAtlasLine>;
for (const key in data) {
if (!Object.prototype.hasOwnProperty.call(data, key)) {
continue;
}
lines.set(key, DynmapMapProvider.buildLine(data[key]));
}
return lines;
}
private static buildLine(line: any): LiveAtlasLine {
return Object.seal({
style: {
color: line.color || '#ff0000',
opacity: line.opacity || 1,
weight: line.weight || 1,
},
points: getLinePoints(line.x || [0, 0], line.y || [0, 0], line.z || [0, 0]),
minZoom: typeof line.minzoom !== 'undefined' && line.minzoom > -1 ? line.minzoom : undefined,
maxZoom: typeof line.maxzoom !== 'undefined' && line.maxzoom > -1 ? line.maxzoom : undefined,
isPopupHTML: line.desc ? true : line.markup || false,
popupContent: line.desc || line.label || undefined,
});
}
private static buildCircles(data: any): Map<string, LiveAtlasCircle> {
const circles = Object.freeze(new Map()) as Map<string, LiveAtlasCircle>;
for (const key in data) {
if (!Object.prototype.hasOwnProperty.call(data, key)) {
continue;
}
circles.set(key, DynmapMapProvider.buildCircle(data[key]));
}
return circles;
}
private static buildCircle(circle: any): LiveAtlasCircle {
return Object.seal({
location: {
x: circle.x || 0,
y: circle.y || 0,
z: circle.z || 0,
},
radius: [circle.xr || 0, circle.zr || 0],
style: {
fillColor: circle.fillcolor || '#ff0000',
fillOpacity: circle.fillopacity || 0,
color: circle.color || '#ff0000',
opacity: circle.opacity || 1,
weight: circle.weight || 1,
},
minZoom: typeof circle.minzoom !== 'undefined' && circle.minzoom > -1 ? circle.minzoom : undefined,
maxZoom: typeof circle.maxzoom !== 'undefined' && circle.maxzoom > -1 ? circle.maxzoom : undefined,
isPopupHTML: circle.desc ? true : circle.markup || false,
popupContent: circle.desc || circle.label || undefined,
});
}
private buildUpdates(data: Array<any>) {
const updates = {
markerSets: new Map<string, DynmapMarkerSetUpdates>(),
tiles: [] as DynmapTileUpdate[],
chat: [] as LiveAtlasChat[],
},
dropped = {
stale: 0,
noSet: 0,
noId: 0,
unknownType: 0,
unknownCType: 0,
incomplete: 0,
notImplemented: 0,
},
lastUpdate = this.updateTimestamp;
let accepted = 0;
for (const entry of data) {
switch (entry.type) {
case 'component': {
if (lastUpdate && entry.timestamp < lastUpdate) {
dropped.stale++;
continue;
}
if (!entry.id) {
dropped.noId++;
continue;
}
//Set updates don't have a set field, the id is the set
const set = entry.msg.startsWith("set") ? entry.id : entry.set;
if (!set) {
dropped.noSet++;
continue;
}
if (entry.ctype !== 'markers') {
dropped.unknownCType++;
continue;
}
if (!updates.markerSets.has(set)) {
updates.markerSets.set(set, {
areaUpdates: [],
markerUpdates: [],
lineUpdates: [],
circleUpdates: [],
removed: false,
});
}
const markerSetUpdates = updates.markerSets.get(set),
update: DynmapUpdate = {
id: entry.id,
removed: entry.msg.endsWith('deleted'),
};
if (entry.msg.startsWith("set")) {
markerSetUpdates!.removed = update.removed;
markerSetUpdates!.payload = update.removed ? undefined : DynmapMapProvider.buildMarkerSet(set, entry);
} else if (entry.msg.startsWith("marker")) {
update.payload = update.removed ? undefined : DynmapMapProvider.buildMarker(entry);
markerSetUpdates!.markerUpdates.push(Object.freeze(update));
} else if (entry.msg.startsWith("area")) {
update.payload = update.removed ? undefined : DynmapMapProvider.buildArea(entry);
markerSetUpdates!.areaUpdates.push(Object.freeze(update));
} else if (entry.msg.startsWith("circle")) {
update.payload = update.removed ? undefined : DynmapMapProvider.buildCircle(entry);
markerSetUpdates!.circleUpdates.push(Object.freeze(update));
} else if (entry.msg.startsWith("line")) {
update.payload = update.removed ? undefined : DynmapMapProvider.buildLine(entry);
markerSetUpdates!.lineUpdates.push(Object.freeze(update));
}
accepted++;
break;
}
case 'chat':
if (!entry.message || !entry.timestamp) {
dropped.incomplete++;
continue;
}
if (entry.timestamp < lastUpdate) {
dropped.stale++;
continue;
}
if (entry.source !== 'player' && entry.source !== 'web') {
dropped.notImplemented++;
continue;
}
updates.chat.push({
type: 'chat',
source: entry.source || undefined,
playerAccount: entry.account || undefined,
playerName: entry.playerName || undefined,
message: entry.message || "",
timestamp: entry.timestamp,
channel: entry.channel || undefined,
});
break;
case 'playerjoin':
if (!entry.account || !entry.timestamp) {
dropped.incomplete++;
continue;
}
if (entry.timestamp < lastUpdate) {
dropped.stale++;
continue;
}
updates.chat.push({
type: 'playerjoin',
playerAccount: entry.account,
playerName: entry.playerName || "",
timestamp: entry.timestamp || undefined,
});
break;
case 'playerquit':
if (!entry.account || !entry.timestamp) {
dropped.incomplete++;
continue;
}
if (entry.timestamp < lastUpdate) {
dropped.stale++;
continue;
}
updates.chat.push({
type: 'playerleave',
playerAccount: entry.account,
playerName: entry.playerName || "",
timestamp: entry.timestamp || undefined,
});
break;
case 'tile':
if (!entry.name || !entry.timestamp) {
dropped.incomplete++;
continue;
}
if (lastUpdate && entry.timestamp < lastUpdate) {
dropped.stale++;
continue;
}
updates.tiles.push({
name: entry.name,
timestamp: entry.timestamp,
});
accepted++;
break;
default:
dropped.unknownType++;
}
}
//Sort chat by newest first
updates.chat = updates.chat.sort((one, two) => {
return two.timestamp - one.timestamp;
});
console.debug(`Updates: ${accepted} accepted. Rejected: `, dropped);
return updates;
}
private async getMarkerSets(world: LiveAtlasWorldDefinition): Promise<Map<string, LiveAtlasMarkerSet>> {
const url = `${this.config.dynmap!.markers}_markers_/marker_${world.name}.json`;
if(this.markersAbort) {
this.markersAbort.abort();
}
this.markersAbort = new AbortController();
const response = await DynmapMapProvider.fetchJSON(url, this.markersAbort.signal);
const sets: Map<string, LiveAtlasMarkerSet> = new Map();
response.sets = response.sets || {};
for (const key in response.sets) {
if (!Object.prototype.hasOwnProperty.call(response.sets, key)) {
continue;
}
const set = response.sets[key],
markers = DynmapMapProvider.buildMarkers(set.markers || {}),
circles = DynmapMapProvider.buildCircles(set.circles || {}),
areas = DynmapMapProvider.buildAreas(set.areas || {}),
lines = DynmapMapProvider.buildLines(set.lines || {});
sets.set(key, {
...DynmapMapProvider.buildMarkerSet(key, set),
markers,
circles,
areas,
lines,
});
}
return sets;
}
async loadServerConfiguration(): Promise<void> {
if(this.configurationAbort) {
this.configurationAbort.abort();
}
this.configurationAbort = new AbortController();
const response = await DynmapMapProvider.fetchJSON(this.config.dynmap!.configuration, this.configurationAbort.signal);
if (response.error === 'login-required') {
throw new Error("Login required");
} else if (response.error) {
throw new Error(response.error);
}
const config = DynmapMapProvider.buildServerConfig(response);
this.updateInterval = response.updaterate || 3000;
this.store.commit(MutationTypes.SET_SERVER_CONFIGURATION, config);
this.store.commit(MutationTypes.SET_SERVER_CONFIGURATION_HASH, response.confighash || 0);
this.store.commit(MutationTypes.SET_MAX_PLAYERS, response.maxcount || 0);
this.store.commit(MutationTypes.SET_SERVER_MESSAGES, DynmapMapProvider.buildMessagesConfig(response));
this.store.commit(MutationTypes.SET_WORLDS, this.buildWorlds(response));
this.store.commit(MutationTypes.SET_COMPONENTS, this.buildComponents(response));
this.store.commit(MutationTypes.SET_LOGGED_IN, response.loggedin || false);
}
async populateWorld(world: LiveAtlasWorldDefinition): Promise<void> {
const markerSets = await this.getMarkerSets(world);
useStore().commit(MutationTypes.SET_MARKER_SETS, markerSets);
}
private async getUpdate(): Promise<void> {
let url = this.config.dynmap!.update;
url = url.replace('{world}', this.store.state.currentWorld!.name);
url = url.replace('{timestamp}', this.updateTimestamp.getTime().toString());
if(this.updateAbort) {
this.updateAbort.abort();
}
this.updateAbort = new AbortController();
const response = await DynmapMapProvider.fetchJSON(url, this.updateAbort.signal);
const players: Set<LiveAtlasPlayer> = new Set(),
updates = this.buildUpdates(response.updates || []),
worldState = {
timeOfDay: response.servertime || 0,
thundering: response.isThundering || false,
raining: response.hasStorm || false,
};
(response.players || []).forEach((player: any) => {
const world = player.world && player.world !== '-some-other-bogus-world-' ? player.world : undefined;
players.add({
name: player.account || "",
displayName: player.name || "",
health: player.health || 0,
armor: player.armor || 0,
sort: player.sort || 0,
hidden: !world,
location: {
//Add 0.5 to position in the middle of a block
x: !isNaN(player.x) ? player.x + 0.5 : 0,
y: !isNaN(player.y) ? player.y : 0,
z: !isNaN(player.z) ? player.z + 0.5 : 0,
world: world,
}
});
});
//Extra fake players for testing
// for(let i = 0; i < 450; i++) {
// players.add({
// account: "VIDEO GAMES " + i,
// health: Math.round(Math.random() * 10),
// armor: Math.round(Math.random() * 10),
// name: "VIDEO GAMES " + i,
// sort: Math.round(Math.random() * 10),
// hidden: false,
// location: {
// x: Math.round(Math.random() * 1000) - 500,
// y: 64,
// z: Math.round(Math.random() * 1000) - 500,
// world: "world",
// }
// });
// }
this.updateTimestamp = new Date(response.timestamp || 0);
this.store.commit(MutationTypes.SET_WORLD_STATE, worldState);
this.store.commit(MutationTypes.ADD_MARKER_SET_UPDATES, updates.markerSets);
this.store.commit(MutationTypes.ADD_TILE_UPDATES, updates.tiles);
this.store.commit(MutationTypes.ADD_CHAT, updates.chat);
this.store.commit(MutationTypes.SET_SERVER_CONFIGURATION_HASH, response.confighash || 0);
await this.store.dispatch(ActionTypes.SET_PLAYERS, players);
}
sendChatMessage(message: string) {
const store = useStore();
if (!store.state.components.chatSending) {
return Promise.reject(store.state.messages.chatErrorDisabled);
}
return fetch(this.config.dynmap!.sendmessage, {
method: 'POST',
body: JSON.stringify({
name: null,
message: message,
})
}).then((response) => {
if (response.status === 403) { //Rate limited
throw new ChatError(store.state.messages.chatErrorCooldown
.replace('%interval%', store.state.components.chatSending!.cooldown.toString()));
}
if (!response.ok) {
throw new Error('Network request failed');
}
return response.json();
}).then(response => {
if (response.error !== 'none') {
throw new ChatError(store.state.messages.chatErrorNotAllowed);
}
}).catch(e => {
if (!(e instanceof ChatError)) {
console.error(store.state.messages.chatErrorUnknown);
console.trace(e);
}
throw e;
});
}
startUpdates() {
this.updatesEnabled = true;
this.update();
}
private async update() {
try {
await this.getUpdate();
} finally {
if(this.updatesEnabled) {
if(this.updateTimeout) {
clearTimeout(this.updateTimeout);
}
this.updateTimeout = setTimeout(() => this.update(), this.updateInterval);
}
}
}
stopUpdates() {
this.updatesEnabled = false;
if (this.updateTimeout) {
clearTimeout(this.updateTimeout);
}
this.updateTimeout = 0;
}
getTilesUrl(): string {
return this.config.dynmap!.tiles;
}
getPlayerHeadUrl(head: HeadQueueEntry): string {
const icon = (head.size === 'body') ? `faces/body/${head.name}.png` :`faces/${head.size}x${head.size}/${head.name}.png`
return this.getMarkerIconUrl(icon);
}
getMarkerIconUrl(icon: string): string {
return `${this.config.dynmap!.markers}_markers_/${icon}.png`;
}
destroy() {
super.destroy();
if(this.configurationAbort) {
this.configurationAbort.abort();
}
if(this.updateAbort) {
this.updateAbort.abort();
}
if(this.markersAbort) {
this.markersAbort.abort();
}
}
}

View File

@ -0,0 +1,91 @@
/*
* Copyright 2021 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.
*/
import {
HeadQueueEntry,
LiveAtlasMapProvider,
LiveAtlasServerDefinition,
LiveAtlasWorldDefinition
} from "@/index";
import {useStore} from "@/store";
import {computed, watch} from "@vue/runtime-core";
import {WatchStopHandle} from "vue";
export default abstract class MapProvider implements LiveAtlasMapProvider {
protected readonly store = useStore();
protected readonly config: LiveAtlasServerDefinition;
private readonly currentWorldUnwatch: WatchStopHandle;
protected constructor(config: LiveAtlasServerDefinition) {
this.config = config;
const currentWorld = computed(() => this.store.state.currentWorld);
this.currentWorldUnwatch = watch(currentWorld, (newValue) => {
if (newValue) {
this.populateWorld(newValue);
}
});
}
abstract loadServerConfiguration(): Promise<void>;
abstract populateWorld(world: LiveAtlasWorldDefinition): Promise<void>;
abstract sendChatMessage(message: string): void;
abstract startUpdates(): void;
abstract stopUpdates(): void;
abstract getPlayerHeadUrl(head: HeadQueueEntry): string;
abstract getTilesUrl(): string;
abstract getMarkerIconUrl(icon: string): string;
destroy() {
this.currentWorldUnwatch();
}
protected static async fetchJSON(url: string, signal: AbortSignal) {
let response, json;
try {
response = await fetch(url, {signal});
} catch(e) {
if(e instanceof DOMException && e.name === 'AbortError') {
console.warn(`Request aborted (${url}`);
throw e;
} else {
console.error(e);
}
throw new Error(`Network request failed`);
}
if (!response.ok) {
throw new Error(`Network request failed (${response.statusText || 'Unknown'})`);
}
try {
json = await response.json();
} catch(e) {
if(e instanceof DOMException && e.name === 'AbortError') {
console.warn(`Request aborted (${url}`);
throw e;
} else {
throw new Error('Request returned invalid json');
}
}
return json;
}
}

View File

@ -0,0 +1,520 @@
/*
* Copyright 2021 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.
*/
import {
HeadQueueEntry, LiveAtlasArea, LiveAtlasCircle, LiveAtlasComponentConfig,
LiveAtlasDimension, LiveAtlasLine, LiveAtlasMarker,
LiveAtlasMarkerSet, LiveAtlasPartialComponentConfig,
LiveAtlasPlayer, LiveAtlasServerConfig, LiveAtlasServerDefinition,
LiveAtlasServerMessageConfig,
LiveAtlasWorldDefinition
} from "@/index";
import LiveAtlasMapDefinition from "@/model/LiveAtlasMapDefinition";
import {MutationTypes} from "@/store/mutation-types";
import MapProvider from "@/providers/MapProvider";
import {ActionTypes} from "@/store/action-types";
import {titleColoursRegex} from "@/util";
export default class Pl3xmapMapProvider extends MapProvider {
private configurationAbort?: AbortController = undefined;
private markersAbort?: AbortController = undefined;
private playersAbort?: AbortController = undefined;
private updatesEnabled = false;
private updateTimeout: number = 0;
private updateTimestamp: Date = new Date();
private updateInterval: number = 3000;
private worldSettings: Map<string, {
components: LiveAtlasPartialComponentConfig,
}> = new Map();
constructor(config: LiveAtlasServerDefinition) {
super(config);
}
private static buildServerConfig(response: any): LiveAtlasServerConfig {
return {
title: (response.ui?.title || 'Pl3xmap').replace(titleColoursRegex, ''),
expandUI: response.ui?.sidebar?.pinned === 'pinned',
//Not used by pl3xmap
defaultZoom: 1,
defaultMap: undefined,
defaultWorld: undefined,
followMap: undefined,
followZoom: undefined,
};
}
private static buildMessagesConfig(response: any): LiveAtlasServerMessageConfig {
return {
worldsHeading: response.ui?.sidebar?.world_list_label || '',
playersHeading: response.ui?.sidebar?.player_list_label || '',
//Not used by pl3xmap
chatPlayerJoin: '',
chatPlayerQuit: '',
chatAnonymousJoin: '',
chatAnonymousQuit: '',
chatErrorNotAllowed: '',
chatErrorRequiresLogin: '',
chatErrorCooldown: '',
}
}
private buildWorlds(serverResponse: any, worldResponses: any[]): Array<LiveAtlasWorldDefinition> {
const worlds: Array<LiveAtlasWorldDefinition> = [];
(serverResponse.worlds || []).filter((w: any) => w && !!w.name).forEach((world: any, index: number) => {
const worldResponse = worldResponses[index],
worldConfig: {components: LiveAtlasPartialComponentConfig } = {
components: {},
};
if(worldResponse.player_tracker?.enabled) {
worldConfig.components.playerMarkers = {
grayHiddenPlayers: true,
hideByDefault: !!worldResponse.player_tracker?.default_hidden,
layerName: worldResponse.player_tracker?.label || '',
layerPriority: worldResponse.player_tracker?.priority,
showBodies: false,
showSkinFaces: true,
showHealth: !!worldResponse.player_tracker?.nameplates?.show_health,
smallFaces: true,
}
}
this.worldSettings.set(world.name, worldConfig);
if(!worldResponse) {
console.warn(`World ${world.name} has no matching world config. Ignoring.`);
return;
}
let dimension: LiveAtlasDimension = 'overworld';
if(world.type === 'nether') {
dimension = 'nether';
} else if(world.type === 'the_end') {
dimension = 'nether';
}
const maps: Map<string, LiveAtlasMapDefinition> = new Map();
maps.set('flat', Object.freeze(new LiveAtlasMapDefinition({
world: world,
background: 'transparent',
backgroundDay: 'transparent',
backgroundNight: 'transparent',
icon: undefined,
imageFormat: 'png',
name: 'flat',
displayName: 'Flat',
nativeZoomLevels: worldResponse.zoom.max || 1,
extraZoomLevels: worldResponse.zoom.extra || 0,
})));
worlds.push({
name: world.name || '(Unnamed world)',
displayName: world.display_name || world.name,
dimension,
protected: false,
seaLevel: 0,
height: 256,
center: {x: worldResponse.spawn.x, y: 0, z: worldResponse.spawn.z},
maps,
});
});
return Array.from(worlds.values());
}
private static buildComponents(response: any): LiveAtlasComponentConfig {
const components: LiveAtlasComponentConfig = {
markers: {
showLabels: false,
},
coordinatesControl: undefined,
linkControl: !!response.ui?.link?.enabled,
layerControl: !!response.ui?.coordinates?.enabled,
//Configured per-world
playerMarkers: undefined,
//Not used by pl3xmap
chatBox: undefined,
chatBalloons: false,
clockControl: undefined,
logoControls: [],
login: false,
};
if(response.ui?.coordinates?.enabled) {
//Try to remove {x}/{z} placeholders are we aren't using them
const label = (response.ui?.coordinates?.html || "Location: ").replace(/{x}.*{z}/gi, '').trim(),
labelPlain = new DOMParser().parseFromString(label, 'text/html').body.textContent || "";
components.coordinatesControl = {
showY: false,
label: labelPlain,
showRegion: false,
showChunk: false,
}
}
return components;
}
private async getMarkerSets(world: LiveAtlasWorldDefinition): Promise<Map<string, LiveAtlasMarkerSet>> {
const url = `${this.config.pl3xmap}tiles/${world.name}/markers.json`;
if(this.markersAbort) {
this.markersAbort.abort();
}
this.markersAbort = new AbortController();
const response = await Pl3xmapMapProvider.fetchJSON(url, this.markersAbort.signal);
const sets: Map<string, LiveAtlasMarkerSet> = new Map();
if(!Array.isArray(response)) {
return sets;
}
response.forEach(set => {
if(!set || !set.id) {
console.warn('Ignoring marker set without id');
return;
}
const id = set.id;
const markers: Map<string, LiveAtlasMarker> = new Map(),
circles: Map<string, LiveAtlasCircle> = new Map(),
areas: Map<string, LiveAtlasArea> = new Map(),
lines: Map<string, LiveAtlasLine> = new Map();
(set.markers || []).forEach((marker: any) => {
switch(marker.type) {
case 'icon':
markers.set(`marker-${markers.size}`, Pl3xmapMapProvider.buildMarker(marker));
break;
case 'polyline':
lines.set(`line-${lines.size}`, Pl3xmapMapProvider.buildLine(marker));
break;
case 'rectangle':
areas.set(`area-${areas.size}`, Pl3xmapMapProvider.buildRectangle(marker));
break;
case 'polygon':
areas.set(`area-${areas.size}`, Pl3xmapMapProvider.buildArea(marker));
break;
case 'circle':
case 'ellipse':
circles.set(`circle-${circles.size}`, Pl3xmapMapProvider.buildCircle(marker));
break;
default:
console.warn('Marker type ' + marker.type + ' not supported');
}
});
const e = {
id,
label: set.name || "Unnamed set",
hidden: set.hide || false,
priority: set.order || 0,
showLabels: false,
markers,
circles,
areas,
lines,
};
sets.set(id, e);
});
return sets;
}
private static buildMarker(marker: any): LiveAtlasMarker {
return {
location: {
x: marker.point?.x || 0,
y: 0,
z: marker.point?.z || 0,
},
dimensions: marker.size ? [marker.size.x || 16, marker.size.z || 16] : [16, 16],
icon: marker.icon || "default",
label: (marker.tooltip || '').trim(),
isLabelHTML: true
};
}
private static buildRectangle(area: any): LiveAtlasArea {
return Object.seal({
style: {
stroke: typeof area.stroke !== 'undefined' ? !!area.stroke : true,
color: area.color || '#3388ff',
weight: area.weight || 3,
opacity: typeof area.opacity !== 'undefined' ? area.opacity : 1,
fill: typeof area.stroke !== 'undefined' ? !!area.stroke : true,
fillColor: area.fillColor || area.color || '#3388ff',
fillOpacity: area.fillOpacity || 0.2,
fillRule: area.fillRule,
},
points: [
area.points[0],
{x: area.points[0].x, z: area.points[1].z},
area.points[1],
{x: area.points[1].x, z: area.points[0].z},
],
outline: false,
tooltipContent: area.tooltip,
popupContent: area.popup,
isPopupHTML: true,
});
}
private static buildArea(area: any): LiveAtlasArea {
return Object.seal({
style: {
stroke: typeof area.stroke !== 'undefined' ? !!area.stroke : true,
color: area.color || '#3388ff',
weight: area.weight || 3,
opacity: typeof area.opacity !== 'undefined' ? area.opacity : 1,
fill: typeof area.fill !== 'undefined' ? !!area.fill : true,
fillColor: area.fillColor || area.color || '#3388ff',
fillOpacity: area.fillOpacity || 0.2,
fillRule: area.fillRule,
},
points: area.points,
outline: false,
tooltipContent: area.tooltip,
popupContent: area.popup,
isPopupHTML: true,
});
}
private static buildLine(line: any): LiveAtlasLine {
return Object.seal({
style: {
stroke: typeof line.stroke !== 'undefined' ? !!line.stroke : true,
color: line.color || '#3388ff',
weight: line.weight || 3,
opacity: typeof line.opacity !== 'undefined' ? line.opacity : 1,
},
points: line.points,
tooltipContent: line.tooltip,
popupContent: line.popup,
isPopupHTML: true,
});
}
private static buildCircle(circle: any): LiveAtlasCircle {
return Object.seal({
location: {
x: circle.center?.x || 0,
y: 0,
z: circle.center?.z || 0,
},
radius: [circle.radiusX || circle.radius || 0, circle.radiusZ || circle.radius || 0],
style: {
stroke: typeof circle.stroke !== 'undefined' ? !!circle.stroke : true,
color: circle.color || '#3388ff',
weight: circle.weight || 3,
opacity: typeof circle.opacity !== 'undefined' ? circle.opacity : 1,
fill: typeof circle.stroke !== 'undefined' ? !!circle.stroke : true,
fillColor: circle.fillColor || circle.color || '#3388ff',
fillOpacity: circle.fillOpacity || 0.2,
fillRule: circle.fillRule,
},
tooltipContent: circle.tooltip,
popupContent: circle.popup,
isPopupHTML: true
});
}
async loadServerConfiguration(): Promise<void> {
if(this.configurationAbort) {
this.configurationAbort.abort();
}
this.configurationAbort = new AbortController();
const baseUrl = this.config.pl3xmap,
response = await Pl3xmapMapProvider.fetchJSON(`${baseUrl}tiles/settings.json`, this.configurationAbort.signal);
if (response.error) {
throw new Error(response.error);
}
const config = Pl3xmapMapProvider.buildServerConfig(response),
worldNames: string[] = (response.worlds || []).filter((world: any) => world && !!world.name)
.map((world: any) => world.name);
const worldResponses = await Promise.all(worldNames.map(name =>
Pl3xmapMapProvider.fetchJSON(`${baseUrl}tiles/${name}/settings.json`, this.configurationAbort!.signal)));
this.store.commit(MutationTypes.SET_SERVER_CONFIGURATION, config);
this.store.commit(MutationTypes.SET_SERVER_MESSAGES, Pl3xmapMapProvider.buildMessagesConfig(response));
this.store.commit(MutationTypes.SET_WORLDS, this.buildWorlds(response, worldResponses));
this.store.commit(MutationTypes.SET_COMPONENTS, Pl3xmapMapProvider.buildComponents(response));
//Pl3xmap has no login functionality
this.store.commit(MutationTypes.SET_LOGGED_IN, false);
}
async populateWorld(world: LiveAtlasWorldDefinition) {
const markerSets = await this.getMarkerSets(world),
worldConfig = this.worldSettings.get(world.name);
this.store.commit(MutationTypes.SET_MARKER_SETS, markerSets);
this.store.commit(MutationTypes.SET_COMPONENTS, worldConfig!.components);
}
private async getPlayers(): Promise<Set<LiveAtlasPlayer>> {
const url = `${this.config.pl3xmap}/tiles/players.json`;
if(this.playersAbort) {
this.playersAbort.abort();
}
this.playersAbort = new AbortController();
const response = await Pl3xmapMapProvider.fetchJSON(url, this.playersAbort.signal),
players: Set<LiveAtlasPlayer> = new Set();
(response.players || []).forEach((player: any) => {
console.log(player.uuid);
players.add({
name: (player.name || '').toLowerCase(),
uuid: player.uuid,
displayName: player.name || "",
health: player.health || 0,
armor: player.armor || 0,
sort: 0,
hidden: false,
location: {
//Add 0.5 to position in the middle of a block
x: !isNaN(player.x) ? player.x + 0.5 : 0,
y: 0,
z: !isNaN(player.z) ? player.z + 0.5 : 0,
world: player.world,
}
});
});
// Extra fake players for testing
// for(let i = 0; i < 450; i++) {
// players.add({
// name: "VIDEO GAMES " + i,
// displayName: "VIDEO GAMES " + i,
// health: Math.round(Math.random() * 10),
// armor: Math.round(Math.random() * 10),
// sort: Math.round(Math.random() * 10),
// hidden: false,
// location: {
// x: Math.round(Math.random() * 1000) - 500,
// y: 0,
// z: Math.round(Math.random() * 1000) - 500,
// world: "world",
// }
// });
// }
this.store.commit(MutationTypes.SET_MAX_PLAYERS, response.max || 0);
return players;
}
sendChatMessage(message: string) {
throw new Error('Pl3xmap does not support chat');
}
startUpdates() {
this.updatesEnabled = true;
this.update();
}
private async update() {
try {
const players = await this.getPlayers();
this.updateTimestamp = new Date();
await this.store.dispatch(ActionTypes.SET_PLAYERS, players);
} finally {
if(this.updatesEnabled) {
if(this.updateTimeout) {
clearTimeout(this.updateTimeout);
}
this.updateTimeout = setTimeout(() => this.update(), this.updateInterval);
}
}
}
stopUpdates() {
this.updatesEnabled = false;
if (this.updateTimeout) {
clearTimeout(this.updateTimeout);
}
this.updateTimeout = 0;
}
getTilesUrl(): string {
return `${this.config.pl3xmap}tiles/`;
}
getPlayerHeadUrl(head: HeadQueueEntry): string {
//TODO: Listen to config
return 'https://mc-heads.net/avatar/{uuid}/16'.replace('{uuid}', head.uuid || '');
}
getMarkerIconUrl(icon: string): string {
return `${this.config.pl3xmap}images/icon/registered/${icon}.png`;
}
destroy() {
super.destroy();
if(this.configurationAbort) {
this.configurationAbort.abort();
}
if(this.playersAbort) {
this.playersAbort.abort();
}
if(this.markersAbort) {
this.markersAbort.abort();
}
}
}

View File

@ -1,5 +1,22 @@
/*!
* Copyright 2021 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.
*/
@import "leaflet/controls";
@import "leaflet/popups";
@import "leaflet/tooltips";
@import "leaflet/markers";
.leaflet-container {

View File

@ -1,3 +1,19 @@
/*!
* Copyright 2021 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.
*/
/* Mixin for resetting :focus styles on browsers supporting :focus-visible */
@mixin focus-reset() {
&:focus:not(:focus-visible) {

View File

@ -1,3 +1,19 @@
/*!
* Copyright 2021 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.
*/
.vue-notification-group {
z-index: 130 !important;

View File

@ -1,5 +1,5 @@
/*!
* Copyright 2020 James Lyne
* Copyright 2021 James Lyne
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@ -1,5 +1,5 @@
/*!
* Copyright 2020 James Lyne
* Copyright 2021 James Lyne
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@ -1,3 +1,19 @@
/*!
* Copyright 2021 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.
*/
/*******************
* players on the map
*/

View File

@ -1,5 +1,5 @@
/*!
* Copyright 2020 James Lyne
* Copyright 2021 James Lyne
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@ -0,0 +1,28 @@
/*!
* Copyright 2021 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.
*/
.leaflet-tooltip {
background-color: var(--background-base);
color: var(--text-base);
box-shadow: var(--box-shadow);
border-radius: var(--border-radius);
border: none;
will-change: transform;
&:before {
content: none;
}
}

View File

@ -1,5 +1,5 @@
/*!
* Copyright 2020 James Lyne
* Copyright 2021 James Lyne
*
* Some portions of this file were taken from https://github.com/webbukkit/dynmap.
* These portions are Copyright 2020 Dynmap Contributors.

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 James Lyne
* Copyright 2021 James Lyne
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -16,8 +16,8 @@
export enum ActionTypes {
LOAD_CONFIGURATION = "loadConfiguration",
GET_UPDATE = "getUpdate",
GET_MARKER_SETS = "getMarkerSets",
START_UPDATES = "startUpdates",
STOP_UPDATES = "stopUpdates",
SET_PLAYERS = "setPlayers",
POP_MARKER_UPDATES = "popMarkerUpdates",
POP_AREA_UPDATES = "popAreaUpdates",

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 James Lyne
* Copyright 2021 James Lyne
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -20,15 +20,11 @@ import {State} from "@/store/state";
import {ActionTypes} from "@/store/action-types";
import {Mutations} from "@/store/mutations";
import {
DynmapAreaUpdate, DynmapCircleUpdate,
DynmapConfigurationResponse, DynmapLineUpdate,
DynmapMarkerSet,
DynmapAreaUpdate, DynmapCircleUpdate, DynmapLineUpdate,
DynmapMarkerUpdate,
DynmapPlayer, DynmapTileUpdate,
DynmapUpdateResponse
DynmapTileUpdate,
} from "@/dynmap";
import {getAPI} from "@/util";
import {LiveAtlasWorldDefinition} from "@/index";
import {LiveAtlasMarkerSet, LiveAtlasPlayer, LiveAtlasWorldDefinition} from "@/index";
type AugmentedActionContext = {
commit<K extends keyof Mutations>(
@ -40,17 +36,17 @@ type AugmentedActionContext = {
export interface Actions {
[ActionTypes.LOAD_CONFIGURATION](
{commit}: AugmentedActionContext,
):Promise<DynmapConfigurationResponse>
[ActionTypes.GET_UPDATE](
):Promise<void>
[ActionTypes.START_UPDATES](
{commit}: AugmentedActionContext,
):Promise<DynmapUpdateResponse>
[ActionTypes.GET_MARKER_SETS](
):Promise<void>
[ActionTypes.STOP_UPDATES](
{commit}: AugmentedActionContext,
):Promise<Map<string, DynmapMarkerSet>>
):Promise<void>
[ActionTypes.SET_PLAYERS](
{commit}: AugmentedActionContext,
payload: Set<DynmapPlayer>
):Promise<Map<string, DynmapMarkerSet>>
payload: Set<LiveAtlasPlayer>
):Promise<Map<string, LiveAtlasMarkerSet>>
[ActionTypes.POP_MARKER_UPDATES](
{commit}: AugmentedActionContext,
payload: {markerSet: string, amount: number}
@ -78,21 +74,20 @@ export interface Actions {
}
export const actions: ActionTree<State, State> & Actions = {
async [ActionTypes.LOAD_CONFIGURATION]({commit, state}): Promise<DynmapConfigurationResponse> {
async [ActionTypes.LOAD_CONFIGURATION]({commit, state}): Promise<void> {
//Clear any existing has to avoid triggering a second config load, after this load changes the hash
commit(MutationTypes.CLEAR_SERVER_CONFIGURATION_HASH, undefined);
const config = await getAPI().getConfiguration();
if(!state.currentServer) {
console.warn('No current server');
return;
}
commit(MutationTypes.SET_SERVER_CONFIGURATION, config.config);
commit(MutationTypes.SET_SERVER_MESSAGES, config.messages);
commit(MutationTypes.SET_WORLDS, config.worlds);
commit(MutationTypes.SET_COMPONENTS, config.components);
commit(MutationTypes.SET_LOGGED_IN, config.loggedIn);
await state.currentMapProvider!.loadServerConfiguration();
//Skip default map/ui visibility logic if we already have a map selected (i.e config reload after hash change)
if(state.currentMap) {
return config;
return;
}
//Make UI visible if configured, there's enough space to do so, and this is the first config load
@ -104,8 +99,8 @@ export const actions: ActionTree<State, State> & Actions = {
let worldName, mapName;
// Use config default world if it exists
if(config.config.defaultWorld && state.worlds.has(config.config.defaultWorld)) {
worldName = config.config.defaultWorld;
if(state.configuration.defaultWorld && state.worlds.has(state.configuration.defaultWorld)) {
worldName = state.configuration.defaultWorld;
}
// Prefer world from parsed url if present and it exists
@ -122,8 +117,8 @@ export const actions: ActionTree<State, State> & Actions = {
const world = state.worlds.get(worldName) as LiveAtlasWorldDefinition;
// Use config default map if it exists
if(config.config.defaultMap && world.maps.has(config.config.defaultMap)) {
mapName = config.config.defaultMap;
if(state.configuration.defaultMap && world.maps.has(state.configuration.defaultMap)) {
mapName = state.configuration.defaultMap;
}
// Prefer map from parsed url if present and it exists
@ -142,40 +137,35 @@ export const actions: ActionTree<State, State> & Actions = {
worldName, mapName
});
}
return config;
},
async [ActionTypes.GET_UPDATE]({commit, dispatch, state}) {
async [ActionTypes.START_UPDATES]({state}) {
if(!state.currentWorld) {
return Promise.reject("No current world");
}
const update = await getAPI().getUpdate(state.updateRequestId, state.currentWorld.name, state.updateTimestamp.valueOf());
commit(MutationTypes.SET_WORLD_STATE, update.worldState);
commit(MutationTypes.SET_UPDATE_TIMESTAMP, new Date(update.timestamp));
commit(MutationTypes.INCREMENT_REQUEST_ID, undefined);
commit(MutationTypes.ADD_MARKER_SET_UPDATES, update.updates.markerSets);
commit(MutationTypes.ADD_TILE_UPDATES, update.updates.tiles);
commit(MutationTypes.ADD_CHAT, update.updates.chat);
commit(MutationTypes.SET_SERVER_CONFIGURATION_HASH, update.configHash);
await dispatch(ActionTypes.SET_PLAYERS, update.players);
return update;
state.currentMapProvider!.startUpdates();
},
[ActionTypes.SET_PLAYERS]({commit, state}, players: Set<DynmapPlayer>) {
async [ActionTypes.STOP_UPDATES]({state}) {
if(!state.currentWorld) {
return Promise.reject("No current world");
}
state.currentMapProvider!.stopUpdates();
},
[ActionTypes.SET_PLAYERS]({commit, state}, players: Set<LiveAtlasPlayer>) {
const keep: Set<string> = new Set();
for(const player of players) {
keep.add(player.account);
keep.add(player.name);
}
//Remove any players that aren't in the set
commit(MutationTypes.SYNC_PLAYERS, keep);
const processQueue = (players: Set<DynmapPlayer>, resolve: Function) => {
const processQueue = (players: Set<LiveAtlasPlayer>, resolve: Function) => {
commit(MutationTypes.SET_PLAYERS_ASYNC, players);
if(!players.size) {
@ -191,17 +181,6 @@ export const actions: ActionTree<State, State> & Actions = {
});
},
async [ActionTypes.GET_MARKER_SETS]({commit, state}) {
if(!state.currentWorld) {
throw new Error("No current world");
}
const markerSets = await getAPI().getMarkerSets(state.currentWorld.name)
commit(MutationTypes.SET_MARKER_SETS, markerSets);
return markerSets;
},
async [ActionTypes.POP_MARKER_UPDATES]({commit, state}, {markerSet, amount}: {markerSet: string, amount: number}): Promise<DynmapMarkerUpdate[]> {
if(!state.markerSets.has(markerSet)) {
console.warn(`POP_MARKER_UPDATES: Marker set ${markerSet} doesn't exist`);
@ -263,6 +242,6 @@ export const actions: ActionTree<State, State> & Actions = {
},
async [ActionTypes.SEND_CHAT_MESSAGE]({commit, state}, message: string): Promise<void> {
await getAPI().sendChatMessage(message);
await state.currentMapProvider!.sendChatMessage(message);
},
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 James Lyne
* Copyright 2021 James Lyne
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -17,7 +17,6 @@
import {GetterTree} from "vuex";
import {State} from "@/store/state";
import {getMinecraftTime, getUrlForLocation} from "@/util";
import {LiveAtlasDynmapServerDefinition} from "@/index";
export type Getters = {
playerMarkersEnabled(state: State): boolean;
@ -26,7 +25,6 @@ export type Getters = {
night(state: State): boolean;
mapBackground(state: State, getters: GetterTree<State, State> & Getters): string;
url(state: State, getters: GetterTree<State, State> & Getters): string;
serverConfig(state: State, getters: GetterTree<State, State> & Getters): LiveAtlasDynmapServerDefinition;
}
export const getters: GetterTree<State, State> & Getters = {
@ -74,12 +72,4 @@ export const getters: GetterTree<State, State> & Getters = {
return getUrlForLocation(state.currentMap, {x,y,z}, zoom);
},
serverConfig(state: State): LiveAtlasDynmapServerDefinition {
if(!state.currentServer) {
throw RangeError("No current server");
}
return state.currentServer as LiveAtlasDynmapServerDefinition;
},
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 James Lyne
* Copyright 2021 James Lyne
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 James Lyne
* Copyright 2021 James Lyne
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -28,7 +28,6 @@ export enum MutationTypes {
CLEAR_MARKER_SETS = 'clearMarkerSets',
ADD_WORLD = 'addWorld',
SET_WORLD_STATE = 'setWorldState',
SET_UPDATE_TIMESTAMP = 'setUpdateTimestamp',
ADD_MARKER_SET_UPDATES = 'addMarkerSetUpdates',
ADD_TILE_UPDATES = 'addTileUpdates',
ADD_CHAT = 'addChat',
@ -37,7 +36,7 @@ export enum MutationTypes {
POP_CIRCLE_UPDATES = 'popCircleUpdates',
POP_LINE_UPDATES = 'popLineUpdates',
POP_TILE_UPDATES = 'popTileUpdates',
INCREMENT_REQUEST_ID = 'incrementRequestId',
SET_MAX_PLAYERS = 'setMaxPlayers',
SET_PLAYERS_ASYNC = 'setPlayersAsync',
CLEAR_PLAYERS = 'clearPlayers',
SYNC_PLAYERS = 'syncPlayers',

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 James Lyne
* Copyright 2021 James Lyne
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -18,15 +18,8 @@ import {MutationTree} from "vuex";
import {MutationTypes} from "@/store/mutation-types";
import {State} from "@/store/state";
import {
DynmapArea,
DynmapCircle,
DynmapComponentConfig,
DynmapLine, DynmapMarker,
DynmapMarkerSet,
DynmapMarkerSetUpdates,
DynmapPlayer,
DynmapServerConfig, DynmapTileUpdate,
DynmapChat
DynmapTileUpdate
} from "@/dynmap";
import {
Coordinate,
@ -38,8 +31,18 @@ import {
LiveAtlasParsedUrl,
LiveAtlasGlobalConfig,
LiveAtlasGlobalMessageConfig,
LiveAtlasServerMessageConfig
LiveAtlasServerMessageConfig,
LiveAtlasPlayer,
LiveAtlasCircle,
LiveAtlasLine,
LiveAtlasArea,
LiveAtlasMarker,
LiveAtlasMarkerSet,
LiveAtlasServerDefinition,
LiveAtlasServerConfig, LiveAtlasChat, LiveAtlasPartialComponentConfig, LiveAtlasComponentConfig
} from "@/index";
import DynmapMapProvider from "@/providers/DynmapMapProvider";
import Pl3xmapMapProvider from "@/providers/Pl3xmapMapProvider";
export type CurrentMapPayload = {
worldName: string;
@ -48,21 +51,20 @@ export type CurrentMapPayload = {
export type Mutations<S = State> = {
[MutationTypes.INIT](state: S, config: LiveAtlasGlobalConfig): void
[MutationTypes.SET_SERVER_CONFIGURATION](state: S, config: DynmapServerConfig): void
[MutationTypes.SET_SERVER_CONFIGURATION](state: S, config: LiveAtlasServerConfig): void
[MutationTypes.SET_SERVER_CONFIGURATION_HASH](state: S, hash: number): void
[MutationTypes.CLEAR_SERVER_CONFIGURATION_HASH](state: S): void
[MutationTypes.SET_SERVER_MESSAGES](state: S, messages: LiveAtlasServerMessageConfig): void
[MutationTypes.SET_WORLDS](state: S, worlds: Array<LiveAtlasWorldDefinition>): void
[MutationTypes.CLEAR_WORLDS](state: S): void
[MutationTypes.SET_COMPONENTS](state: S, worlds: DynmapComponentConfig): void
[MutationTypes.SET_MARKER_SETS](state: S, worlds: Map<string, DynmapMarkerSet>): void
[MutationTypes.SET_COMPONENTS](state: S, components: LiveAtlasPartialComponentConfig | LiveAtlasComponentConfig): void
[MutationTypes.SET_MARKER_SETS](state: S, worlds: Map<string, LiveAtlasMarkerSet>): void
[MutationTypes.CLEAR_MARKER_SETS](state: S): void
[MutationTypes.ADD_WORLD](state: S, world: LiveAtlasWorldDefinition): void
[MutationTypes.SET_WORLD_STATE](state: S, worldState: LiveAtlasWorldState): void
[MutationTypes.SET_UPDATE_TIMESTAMP](state: S, time: Date): void
[MutationTypes.ADD_MARKER_SET_UPDATES](state: S, updates: Map<string, DynmapMarkerSetUpdates>): void
[MutationTypes.ADD_TILE_UPDATES](state: S, updates: Array<DynmapTileUpdate>): void
[MutationTypes.ADD_CHAT](state: State, chat: Array<DynmapChat>): void
[MutationTypes.ADD_CHAT](state: State, chat: Array<LiveAtlasChat>): void
[MutationTypes.POP_MARKER_UPDATES](state: S, payload: {markerSet: string, amount: number}): void
[MutationTypes.POP_AREA_UPDATES](state: S, payload: {markerSet: string, amount: number}): void
@ -70,8 +72,8 @@ export type Mutations<S = State> = {
[MutationTypes.POP_LINE_UPDATES](state: S, payload: {markerSet: string, amount: number}): void
[MutationTypes.POP_TILE_UPDATES](state: S, amount: number): void
[MutationTypes.INCREMENT_REQUEST_ID](state: S): void
[MutationTypes.SET_PLAYERS_ASYNC](state: S, players: Set<DynmapPlayer>): Set<DynmapPlayer>
[MutationTypes.SET_MAX_PLAYERS](state: S, maxPlayers: number): void
[MutationTypes.SET_PLAYERS_ASYNC](state: S, players: Set<LiveAtlasPlayer>): Set<LiveAtlasPlayer>
[MutationTypes.SYNC_PLAYERS](state: S, keep: Set<string>): void
[MutationTypes.CLEAR_PLAYERS](state: S): void
[MutationTypes.SET_CURRENT_SERVER](state: S, server: string): void
@ -81,8 +83,8 @@ export type Mutations<S = State> = {
[MutationTypes.SET_PARSED_URL](state: S, payload: LiveAtlasParsedUrl): void
[MutationTypes.CLEAR_PARSED_URL](state: S): void
[MutationTypes.CLEAR_CURRENT_MAP](state: S): void
[MutationTypes.SET_FOLLOW_TARGET](state: S, payload: DynmapPlayer): void
[MutationTypes.SET_PAN_TARGET](state: S, payload: DynmapPlayer): void
[MutationTypes.SET_FOLLOW_TARGET](state: S, payload: LiveAtlasPlayer): void
[MutationTypes.SET_PAN_TARGET](state: S, payload: LiveAtlasPlayer): void
[MutationTypes.CLEAR_FOLLOW_TARGET](state: S, a?: void): void
[MutationTypes.CLEAR_PAN_TARGET](state: S, a?: void): void
@ -158,9 +160,8 @@ export const mutations: MutationTree<State> & Mutations = {
},
// Sets configuration options from the initial config fetch
[MutationTypes.SET_SERVER_CONFIGURATION](state: State, config: DynmapServerConfig) {
[MutationTypes.SET_SERVER_CONFIGURATION](state: State, config: LiveAtlasServerConfig) {
state.configuration = Object.assign(state.configuration, config);
state.configurationHash = config.hash;
},
// Sets configuration hash
@ -222,13 +223,15 @@ export const mutations: MutationTree<State> & Mutations = {
state.currentWorldState.thundering = false;
},
//Sets the state and settings of optional components, from the initial config fetch
[MutationTypes.SET_COMPONENTS](state: State, components: DynmapComponentConfig) {
//Updates the state of optional components (chat, link button, etc)
//Can be called with a LiveAtlasComponentConfig object to replace the whole state
//or a LiveAtlasPartialComponentConfig object for partial updates to the existing state
[MutationTypes.SET_COMPONENTS](state: State, components: LiveAtlasPartialComponentConfig | LiveAtlasComponentConfig) {
state.components = Object.assign(state.components, components);
},
//Sets the existing marker sets from the last marker fetch
[MutationTypes.SET_MARKER_SETS](state: State, markerSets: Map<string, DynmapMarkerSet>) {
[MutationTypes.SET_MARKER_SETS](state: State, markerSets: Map<string, LiveAtlasMarkerSet>) {
state.markerSets.clear();
state.pendingSetUpdates.clear();
@ -257,11 +260,6 @@ export const mutations: MutationTree<State> & Mutations = {
state.currentWorldState = Object.assign(state.currentWorldState, worldState);
},
//Sets the timestamp of the last update fetch
[MutationTypes.SET_UPDATE_TIMESTAMP](state: State, timestamp: Date) {
state.updateTimestamp = timestamp;
},
//Adds markerset related updates from an update fetch to the pending updates list
[MutationTypes.ADD_MARKER_SET_UPDATES](state: State, updates: Map<string, DynmapMarkerSetUpdates>) {
for(const entry of updates) {
@ -277,10 +275,10 @@ export const mutations: MutationTree<State> & Mutations = {
priority: entry[1].payload.priority,
label: entry[1].payload.label,
hidden: entry[1].payload.hidden,
markers: Object.freeze(new Map()) as Map<string, DynmapMarker>,
areas: Object.freeze(new Map()) as Map<string, DynmapArea>,
circles: Object.freeze(new Map()) as Map<string, DynmapCircle>,
lines: Object.freeze(new Map()) as Map<string, DynmapLine>,
markers: Object.freeze(new Map()) as Map<string, LiveAtlasMarker>,
areas: Object.freeze(new Map()) as Map<string, LiveAtlasArea>,
circles: Object.freeze(new Map()) as Map<string, LiveAtlasCircle>,
lines: Object.freeze(new Map()) as Map<string, LiveAtlasLine>,
});
state.pendingSetUpdates.set(entry[0], {
@ -295,7 +293,7 @@ export const mutations: MutationTree<State> & Mutations = {
}
}
const set = state.markerSets.get(entry[0]) as DynmapMarkerSet,
const set = state.markerSets.get(entry[0]) as LiveAtlasMarkerSet,
setUpdates = state.pendingSetUpdates.get(entry[0]) as DynmapMarkerSetUpdates;
//Delete the set if it has been deleted
@ -320,7 +318,7 @@ export const mutations: MutationTree<State> & Mutations = {
if(update.removed) {
set.markers.delete(update.id);
} else {
set.markers.set(update.id, update.payload as DynmapMarker);
set.markers.set(update.id, update.payload as LiveAtlasMarker);
}
}
@ -328,7 +326,7 @@ export const mutations: MutationTree<State> & Mutations = {
if(update.removed) {
set.areas.delete(update.id);
} else {
set.areas.set(update.id, update.payload as DynmapArea);
set.areas.set(update.id, update.payload as LiveAtlasArea);
}
}
@ -336,7 +334,7 @@ export const mutations: MutationTree<State> & Mutations = {
if(update.removed) {
set.circles.delete(update.id);
} else {
set.circles.set(update.id, update.payload as DynmapCircle);
set.circles.set(update.id, update.payload as LiveAtlasCircle);
}
}
@ -344,7 +342,7 @@ export const mutations: MutationTree<State> & Mutations = {
if(update.removed) {
set.lines.delete(update.id);
} else {
set.lines.set(update.id, update.payload as DynmapLine);
set.lines.set(update.id, update.payload as LiveAtlasLine);
}
}
@ -362,7 +360,7 @@ export const mutations: MutationTree<State> & Mutations = {
},
//Adds chat messages from an update fetch to the chat history
[MutationTypes.ADD_CHAT](state: State, chat: Array<DynmapChat>) {
[MutationTypes.ADD_CHAT](state: State, chat: Array<LiveAtlasChat>) {
state.chat.messages.unshift(...chat);
},
@ -411,37 +409,38 @@ export const mutations: MutationTree<State> & Mutations = {
state.pendingTileUpdates.splice(0, amount);
},
//Increments the request id for the next update fetch
[MutationTypes.INCREMENT_REQUEST_ID](state: State) {
state.updateRequestId++;
[MutationTypes.SET_MAX_PLAYERS](state: State, maxPlayers: number) {
state.maxPlayers = maxPlayers;
},
// Set up to 10 players at once
[MutationTypes.SET_PLAYERS_ASYNC](state: State, players: Set<DynmapPlayer>): Set<DynmapPlayer> {
[MutationTypes.SET_PLAYERS_ASYNC](state: State, players: Set<LiveAtlasPlayer>): Set<LiveAtlasPlayer> {
let count = 0;
for(const player of players) {
if(state.players.has(player.account)) {
const existing = state.players.get(player.account);
if(state.players.has(player.name)) {
const existing = state.players.get(player.name);
existing!.health = player.health;
existing!.uuid = player.uuid;
existing!.armor = player.armor;
existing!.location = Object.assign(existing!.location, player.location);
existing!.hidden = player.hidden;
existing!.name = player.name;
existing!.displayName = player.displayName;
existing!.sort = player.sort;
if(existing!.name !== player.name || existing!.sort !== player.sort) {
if(existing!.displayName !== player.displayName || existing!.sort !== player.sort) {
state.sortedPlayers.dirty = true;
}
} else {
state.sortedPlayers.dirty = true;
state.players.set(player.account, {
account: player.account,
state.players.set(player.name, {
name: player.name,
uuid: player.uuid,
health: player.health,
armor: player.armor,
location: player.location,
name: player.name,
displayName: player.displayName,
sort: player.sort,
hidden: player.hidden,
});
@ -461,7 +460,7 @@ export const mutations: MutationTree<State> & Mutations = {
return a.sort - b.sort;
}
return a.account.toLowerCase().localeCompare(b.account.toLowerCase());
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
}) as LiveAtlasSortedPlayers;
}
@ -471,7 +470,7 @@ export const mutations: MutationTree<State> & Mutations = {
//Removes all players not found in the provided keep set
[MutationTypes.SYNC_PLAYERS](state: State, keep: Set<string>) {
for(const [key, player] of state.players) {
if(!keep.has(player.account)) {
if(!keep.has(player.name)) {
state.sortedPlayers.splice(state.sortedPlayers.indexOf(player), 1);
state.players.delete(key);
}
@ -493,6 +492,22 @@ export const mutations: MutationTree<State> & Mutations = {
}
state.currentServer = state.servers.get(serverName);
if(state.currentMapProvider) {
state.currentMapProvider.stopUpdates();
state.currentMapProvider.destroy();
}
switch(state.currentServer!.type) {
case 'pl3xmap':
state.currentMapProvider = Object.seal(
new Pl3xmapMapProvider(state.servers.get(serverName) as LiveAtlasServerDefinition));
break;
case 'dynmap':
state.currentMapProvider = Object.seal(
new DynmapMapProvider(state.servers.get(serverName) as LiveAtlasServerDefinition));
break;
}
},
//Sets the currently active map/world
@ -550,12 +565,12 @@ export const mutations: MutationTree<State> & Mutations = {
},
//Set the follow target, which the map will automatically pan to keep in view
[MutationTypes.SET_FOLLOW_TARGET](state: State, player: DynmapPlayer) {
[MutationTypes.SET_FOLLOW_TARGET](state: State, player: LiveAtlasPlayer) {
state.followTarget = player;
},
//Set the pan target, which the map will immediately pan to once
[MutationTypes.SET_PAN_TARGET](state: State, player: DynmapPlayer) {
[MutationTypes.SET_PAN_TARGET](state: State, player: LiveAtlasPlayer) {
state.panTarget = player;
},

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 James Lyne
* Copyright 2021 James Lyne
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -15,10 +15,8 @@
*/
import {
DynmapComponentConfig, DynmapMarkerSet, DynmapMarkerSetUpdates,
DynmapPlayer,
DynmapServerConfig, DynmapTileUpdate,
DynmapChat
DynmapMarkerSetUpdates,
DynmapTileUpdate
} from "@/dynmap";
import {
Coordinate,
@ -29,37 +27,44 @@ import {
LiveAtlasUIElement,
LiveAtlasWorldDefinition,
LiveAtlasParsedUrl,
LiveAtlasMessageConfig
LiveAtlasMessageConfig,
LiveAtlasMapProvider,
LiveAtlasPlayer,
LiveAtlasMarkerSet,
LiveAtlasComponentConfig,
LiveAtlasServerConfig, LiveAtlasChat
} from "@/index";
import LiveAtlasMapDefinition from "@/model/LiveAtlasMapDefinition";
export type State = {
version: string;
servers: Map<string, LiveAtlasServerDefinition>;
configuration: DynmapServerConfig;
configuration: LiveAtlasServerConfig;
configurationHash: number | undefined;
messages: LiveAtlasMessageConfig;
components: DynmapComponentConfig;
components: LiveAtlasComponentConfig;
loggedIn: boolean;
worlds: Map<string, LiveAtlasWorldDefinition>;
maps: Map<string, LiveAtlasMapDefinition>;
players: Map<string, DynmapPlayer>;
players: Map<string, LiveAtlasPlayer>;
sortedPlayers: LiveAtlasSortedPlayers;
markerSets: Map<string, DynmapMarkerSet>;
maxPlayers: number;
markerSets: Map<string, LiveAtlasMarkerSet>;
chat: {
unread: number;
messages: DynmapChat[];
messages: LiveAtlasChat[];
};
pendingSetUpdates: Map<string, DynmapMarkerSetUpdates>;
pendingTileUpdates: Array<DynmapTileUpdate>;
followTarget?: DynmapPlayer;
panTarget?: DynmapPlayer;
followTarget?: LiveAtlasPlayer;
panTarget?: LiveAtlasPlayer;
currentMapProvider?: Readonly<LiveAtlasMapProvider>;
currentServer?: LiveAtlasServerDefinition;
currentWorldState: LiveAtlasWorldState;
currentWorld?: LiveAtlasWorldDefinition;
@ -67,9 +72,6 @@ export type State = {
currentLocation: Coordinate;
currentZoom: number;
updateRequestId: number;
updateTimestamp: Date;
ui: {
playersAboveMarkers: boolean;
playersSearch: boolean;
@ -91,20 +93,13 @@ export const state: State = {
servers: new Map(),
configuration: {
version: '',
defaultMap: '',
defaultWorld: '',
defaultZoom: 0,
followMap: '',
followZoom: 0,
updateInterval: 3000,
showLayerControl: false,
title: '',
loginEnabled: false,
maxPlayers: 0,
grayHiddenPlayers: false,
expandUI: false,
hash: 0,
},
configurationHash: undefined,
@ -157,6 +152,7 @@ export const state: State = {
maps: new Map(), //Defined maps from configuration.json
players: new Map(), //Online players from world.json
sortedPlayers: [] as LiveAtlasSortedPlayers, //Online players from world.json, sorted by their sort property then alphabetically
maxPlayers: 0,
chat: {
unread: 0,
@ -187,6 +183,9 @@ export const state: State = {
//Optional "link" component. Adds button to copy url for current position
linkControl: false,
//Layers control
layerControl: false,
//Optional "logo" controls.
logoControls: [],
@ -197,12 +196,16 @@ export const state: State = {
chatBox: undefined,
//Chat balloons showing messages above player markers
chatBalloons: false
chatBalloons: false,
//Login/registering (not currently implemented)
login: false,
},
followTarget: undefined,
panTarget: undefined,
currentMapProvider: undefined,
currentServer: undefined,
currentWorld: undefined,
currentMap: undefined,
@ -218,9 +221,6 @@ export const state: State = {
timeOfDay: 0,
},
updateRequestId: 0,
updateTimestamp: new Date(),
ui: {
playersAboveMarkers: true,
playersSearch: true,

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 James Lyne
* Copyright 2021 James Lyne
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -14,17 +14,9 @@
* limitations under the License.
*/
import API from '@/api';
import {DynmapPlayer} from "@/dynmap";
import {useStore} from "@/store";
import LiveAtlasMapDefinition from "@/model/LiveAtlasMapDefinition";
interface HeadQueueEntry {
cacheKey: string;
account: string;
size: string;
image: HTMLImageElement;
}
import {HeadQueueEntry, LiveAtlasPlayer} from "@/index";
const headCache = new Map<string, HTMLImageElement>(),
headUnresolvedCache = new Map<string, Promise<HTMLImageElement>>(),
@ -32,6 +24,10 @@ const headCache = new Map<string, HTMLImageElement>(),
headQueue: HeadQueueEntry[] = [];
export const titleColoursRegex = /§[0-9a-f]/ig;
export const netherWorldNameRegex = /_?nether(_|$)/i;
export const endWorldNameRegex = /(^|_)end(_|$)/i;
export const getMinecraftTime = (serverTime: number) => {
const day = serverTime >= 0 && serverTime < 13700;
@ -49,8 +45,9 @@ export const getMinecraftTime = (serverTime: number) => {
};
}
export const getMinecraftHead = (player: DynmapPlayer | string, size: string): Promise<HTMLImageElement> => {
const account = typeof player === 'string' ? player : player.account,
export const getMinecraftHead = (player: LiveAtlasPlayer | string, size: string): Promise<HTMLImageElement> => {
const account = typeof player === 'string' ? player : player.name,
uuid = typeof player === 'string' ? undefined : player.uuid,
cacheKey = `${account}-${size}`;
if(headCache.has(cacheKey)) {
@ -79,7 +76,8 @@ export const getMinecraftHead = (player: DynmapPlayer | string, size: string): P
};
headQueue.push({
account,
name: account,
uuid,
size,
cacheKey,
image: faceImage,
@ -97,37 +95,14 @@ const tickHeadQueue = () => {
return;
}
const head = headQueue.pop() as HeadQueueEntry,
src = (head.size === 'body') ? `faces/body/${head.account}.png` :`faces/${head.size}x${head.size}/${head.account}.png`;
const head = headQueue.pop() as HeadQueueEntry;
headsLoading.add(head.cacheKey);
head.image.src = concatURL(useStore().getters.serverConfig.dynmap.markers, src);
head.image.src = useStore().state.currentMapProvider!.getPlayerHeadUrl(head);
tickHeadQueue();
}
export const concatURL = (base: string, addition: string) => {
if(base.indexOf('?') >= 0) {
return base + escape(addition);
}
return base + addition;
}
export const getPointConverter = () => {
const map = useStore().state.currentMap;
if(map) {
return (x: number, y: number, z: number) => {
return map.locationToLatLng({x, y, z});
};
} else {
return (x: number, y: number, z: number) => {
return LiveAtlasMapDefinition.defaultProjection.locationToLatLng({x, y, z});
};
}
}
export const parseUrl = () => {
const query = new URLSearchParams(window.location.search),
hash = window.location.hash.replace('#', '');
@ -211,16 +186,6 @@ export const parseMapSearchParams = (query: URLSearchParams) => {
}
}
export const getAPI = () => {
const store = useStore();
if(!store.state.currentServer) {
throw new RangeError("No current server");
}
return API;
}
export const getUrlForLocation = (map: LiveAtlasMapDefinition, location: {
x: number,
y: number,

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 James Lyne
* Copyright 2021 James Lyne
*
* Some portions of this file were taken from https://github.com/webbukkit/dynmap.
* These portions are Copyright 2020 Dynmap Contributors.
@ -18,39 +18,35 @@
*/
import {LatLngExpression} from "leaflet";
import {DynmapArea} from "@/dynmap";
import LiveAtlasPolyline from "@/leaflet/vector/LiveAtlasPolyline";
import LiveAtlasPolygon from "@/leaflet/vector/LiveAtlasPolygon";
import {Coordinate, LiveAtlasArea} from "@/index";
import {arePointsEqual, createPopup, isStyleEqual, tooltipOptions} from "@/util/paths";
export const createArea = (options: DynmapArea, converter: Function): LiveAtlasPolyline | LiveAtlasPolygon => {
export const createArea = (options: LiveAtlasArea, converter: Function): LiveAtlasPolyline | LiveAtlasPolygon => {
const outline = !options.style.fillOpacity || (options.style.fillOpacity <= 0),
points = getPoints(options, converter, outline),
area = outline ? new LiveAtlasPolyline(points, {
...options.style,
minZoom: options.minZoom,
maxZoom: options.maxZoom,
}) : new LiveAtlasPolygon(points, {
...options.style,
minZoom: options.minZoom,
maxZoom: options.maxZoom,
});
points = options.points.map(projectPointsMapCallback, converter) as LatLngExpression[] | LatLngExpression[][],
area = outline ? new LiveAtlasPolyline(points, options) : new LiveAtlasPolygon(points, options);
if (options.label) {
area.bindPopup(() => createPopup(options));
if (options.popupContent) {
area.bindPopup(() => createPopup(options, 'AreaPopup'));
}
if (options.tooltipContent) {
area.bindTooltip(() => options.tooltipContent as string, tooltipOptions);
}
return area;
};
export const updateArea = (area: LiveAtlasPolyline | LiveAtlasPolygon | undefined, options: DynmapArea, converter: Function): LiveAtlasPolyline | LiveAtlasPolygon => {
const outline = !options.style || !options.style.fillOpacity || (options.style.fillOpacity <= 0) as boolean,
points = getPoints(options, converter, outline);
export const updateArea = (area: LiveAtlasPolyline | LiveAtlasPolygon | undefined, options: LiveAtlasArea, converter: Function): LiveAtlasPolyline | LiveAtlasPolygon => {
if (!area) {
return createArea(options, converter);
}
const oldPoints = area.getLatLngs();
const points = options.points.map(projectPointsMapCallback, converter) as LatLngExpression[] | LatLngExpression[][],
oldPoints = area.getLatLngs();
let dirty = false;
//Avoid pointless setStyle() redrawing by checking if styles have actually changed
@ -66,7 +62,7 @@ export const updateArea = (area: LiveAtlasPolyline | LiveAtlasPolygon | undefine
area.closePopup();
area.unbindPopup();
area.bindPopup(() => createPopup(options));
area.bindPopup(() => createPopup(options, 'AreaPopup'));
if(dirty) {
area.redraw();
@ -75,149 +71,129 @@ export const updateArea = (area: LiveAtlasPolyline | LiveAtlasPolygon | undefine
return area;
};
const arePointsEqual = (oldPoints: any, newPoints: any) => {
return JSON.stringify(oldPoints) === JSON.stringify(newPoints);
}
const isStyleEqual = (oldStyle: any, newStyle: any) => {
return oldStyle && newStyle
&& (oldStyle.color === newStyle.color)
&& (oldStyle.weight === newStyle.weight)
&& (oldStyle.opacity === newStyle.opacity)
&& (oldStyle.fillColor === newStyle.fillColor)
&& (oldStyle.fillOpacity === newStyle.fillOpacity)
}
export const createPopup = (options: DynmapArea): HTMLElement => {
const popup = document.createElement('span');
if (options.popupContent) {
popup.classList.add('AreaPopup');
popup.insertAdjacentHTML('afterbegin', options.popupContent);
} else if (options.isHTML) {
popup.classList.add('AreaPopup');
popup.insertAdjacentHTML('afterbegin', options.label);
const projectPointsMapCallback = function(this: Function, point: Coordinate | Coordinate[] | Coordinate[][]): LatLngExpression | LatLngExpression[] {
if(Array.isArray(point)) {
return point.map(projectPointsMapCallback, this) as LatLngExpression[];
} else {
popup.textContent = options.label;
// @ts-ignore
return this(point);
}
return popup;
};
export const getPoints = (options: DynmapArea, converter: Function, outline: boolean): LatLngExpression[] | LatLngExpression[][] => {
if (options.x.length === 2) { /* Only 2 points */
if (options.y[0] === options.y[1]) {
return get2DBoxPoints(options, converter, outline);
export const getPoints = (x: number[], y: [number, number], z: number[], outline: boolean): Coordinate[] | Coordinate[][] => {
if (x.length === 2) { /* Only 2 points */
if (y[0] === y[1]) {
return get2DBoxPoints(x, y, z, outline);
} else {
return get3DBoxPoints(options, converter);
return get3DBoxPoints(x, y, z);
}
} else {
if (options.y[0] === options.y[1]) {
return get2DShapePoints(options, converter, outline);
if (y[0] === y[1]) {
return get2DShapePoints(x, y, z, outline);
} else {
return get3DShapePoints(options, converter);
return get3DShapePoints(x, y, z);
}
}
};
export const get3DBoxPoints = (options: DynmapArea, converter: Function): LatLngExpression[][] => {
const maxX = options.x[0],
minX = options.x[1],
maxY = options.y[0],
minY = options.y[1],
maxZ = options.z[0],
minZ = options.z[1];
export const get3DBoxPoints = (x: number[], y: [number, number], z: number[]): Coordinate[][] => {
const maxX = x[0],
minX = x[1],
maxY = y[0],
minY = y[1],
maxZ = z[0],
minZ = z[1];
return [
[
converter(minX, minY, minZ),
converter(maxX, minY, minZ),
converter(maxX, minY, maxZ),
converter(minX, minY, maxZ)
{x: minX, y: minY, z: minZ},
{x: maxX, y: minY, z: minZ},
{x: maxX, y: minY, z: maxZ},
{x: minX, y: minY, z: maxZ}
], [
converter(minX, maxY, minZ),
converter(maxX, maxY, minZ),
converter(maxX, maxY, maxZ),
converter(minX, maxY, maxZ)
{x: minX, y: maxY, z: minZ},
{x: maxX, y: maxY, z: minZ},
{x: maxX, y: maxY, z: maxZ},
{x: minX, y: maxY, z: maxZ}
], [
converter(minX, minY, minZ),
converter(minX, maxY, minZ),
converter(maxX, maxY, minZ),
converter(maxX, minY, minZ)
{x: minX, y: minY, z: minZ},
{x: minX, y: maxY, z: minZ},
{x: maxX, y: maxY, z: minZ},
{x: maxX, y: minY, z: minZ}
], [
converter(maxX, minY, minZ),
converter(maxX, maxY, minZ),
converter(maxX, maxY, maxZ),
converter(maxX, minY, maxZ)
{x: maxX, y: minY, z: minZ},
{x: maxX, y: maxY, z: minZ},
{x: maxX, y: maxY, z: maxZ},
{x: maxX, y: minY, z: maxZ}
], [
converter(minX, minY, maxZ),
converter(minX, maxY, maxZ),
converter(maxX, maxY, maxZ),
converter(maxX, minY, maxZ)
{x: minX, y: minY, z: maxZ},
{x: minX, y: maxY, z: maxZ},
{x: maxX, y: maxY, z: maxZ},
{x: maxX, y: minY, z: maxZ}
], [
converter(minX, minY, minZ),
converter(minX, maxY, minZ),
converter(minX, maxY, maxZ),
converter(minX, minY, maxZ)
{x: minX, y: minY, z: minZ},
{x: minX, y: maxY, z: minZ},
{x: minX, y: maxY, z: maxZ},
{x: minX, y: minY, z: maxZ}
]
];
};
export const get2DBoxPoints = (options: DynmapArea, converter: Function, outline: boolean): LatLngExpression[] => {
const maxX = options.x[0],
minX = options.x[1],
minY = options.y[1],
maxZ = options.z[0],
minZ = options.z[1];
export const get2DBoxPoints = (x: number[], y: [number, number], z: number[], outline: boolean): Coordinate[] => {
const maxX = x[0],
minX = x[1],
minY = y[1],
maxZ = z[0],
minZ = z[1];
if (outline) {
return [
converter(minX, minY, minZ),
converter(maxX, minY, minZ),
converter(maxX, minY, maxZ),
converter(minX, minY, maxZ),
converter(minX, minY, minZ)
{x: minX, y: minY, z: minZ},
{x: maxX, y: minY, z: minZ},
{x: maxX, y: minY, z: maxZ},
{x: minX, y: minY, z: maxZ},
{x: minX, y: minY, z: minZ}
];
} else {
return [
converter(minX, minY, minZ),
converter(maxX, minY, minZ),
converter(maxX, minY, maxZ),
converter(minX, minY, maxZ)
{x: minX, y: minY, z: minZ},
{x: maxX, y: minY, z: minZ},
{x: maxX, y: minY, z: maxZ},
{x: minX, y: minY, z: maxZ}
];
}
};
export const get3DShapePoints = (options: DynmapArea, converter: Function): LatLngExpression[][] => {
export const get3DShapePoints = (x: number[], y: [number, number], z: number[]): Coordinate[][] => {
const toplist = [],
botlist = [],
polylist = [];
for (let i = 0; i < options.x.length; i++) {
toplist[i] = converter(options.x[i], options.y[0], options.z[i]);
botlist[i] = converter(options.x[i], options.y[1], options.z[i]);
for (let i = 0; i < x.length; i++) {
toplist[i] = {x: x[i], y: y[0], z: z[i]};
botlist[i] = {x: x[i], y: y[1], z: z[i]};
}
for (let i = 0; i < options.x.length; i++) {
const sidelist = [];
sidelist[0] = toplist[i];
sidelist[1] = botlist[i];
sidelist[2] = botlist[(i + 1) % options.x.length];
sidelist[3] = toplist[(i + 1) % options.x.length];
polylist[i] = sidelist;
for (let i = 0; i < x.length; i++) {
polylist[i] = [
toplist[i],
botlist[i],
botlist[(i + 1) % x.length],
toplist[(i + 1) % x.length],
];
}
polylist[options.x.length] = botlist;
polylist[options.x.length + 1] = toplist;
polylist[x.length] = botlist;
polylist[x.length + 1] = toplist;
return polylist;
};
export const get2DShapePoints = (options: DynmapArea, converter: Function, outline: boolean): LatLngExpression[] => {
export const get2DShapePoints = (x: number[], y: [number, number], z: number[], outline: boolean): Coordinate[] => {
const points = [];
for (let i = 0; i < options.x.length; i++) {
points[i] = converter(options.x[i], options.y[1], options.z[i]);
for (let i = 0; i < x.length; i++) {
points[i] = {x: x[i], y: y[1], z: z[i]};
}
if (outline) {
@ -225,4 +201,4 @@ export const get2DShapePoints = (options: DynmapArea, converter: Function, outli
}
return points;
}
};

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 James Lyne
* Copyright 2021 James Lyne
*
* Some portions of this file were taken from https://github.com/webbukkit/dynmap.
* These portions are Copyright 2020 Dynmap Contributors.
@ -17,66 +17,46 @@
* limitations under the License.
*/
import {DynmapCircle} from "@/dynmap";
import {LatLngExpression} from "leaflet";
import LiveAtlasPolyline from "@/leaflet/vector/LiveAtlasPolyline";
import LiveAtlasPolygon from "@/leaflet/vector/LiveAtlasPolygon";
import {LiveAtlasCircle} from "@/index";
import {createPopup, tooltipOptions} from "@/util/paths";
export const createCircle = (options: DynmapCircle, converter: Function): LiveAtlasPolyline | LiveAtlasPolygon => {
export const createCircle = (options: LiveAtlasCircle, converter: Function): LiveAtlasPolyline | LiveAtlasPolygon => {
const outline = !options.style.fillOpacity || (options.style.fillOpacity <= 0),
points = getCirclePoints(options, converter, outline),
circle = outline ? new LiveAtlasPolyline(points, {
...options.style,
minZoom: options.minZoom,
maxZoom: options.maxZoom,
}) : new LiveAtlasPolygon(points, {
...options.style,
minZoom: options.minZoom,
maxZoom: options.maxZoom,
});
circle = outline ? new LiveAtlasPolyline(points, options) : new LiveAtlasPolygon(points, options);
if(options.label) {
circle.bindPopup(() => createPopup(options));
if(options.popupContent) {
circle.bindPopup(() => createPopup(options, 'CirclePopup'));
}
if (options.tooltipContent) {
circle.bindTooltip(() => options.tooltipContent as string, tooltipOptions);
}
return circle;
};
export const updateCircle = (circle: LiveAtlasPolyline | LiveAtlasPolygon | undefined, options: DynmapCircle, converter: Function): LiveAtlasPolyline | LiveAtlasPolygon => {
const outline = (options.style && options.style.fillOpacity && (options.style.fillOpacity <= 0)) as boolean,
points = getCirclePoints(options, converter, outline);
export const updateCircle = (circle: LiveAtlasPolyline | LiveAtlasPolygon | undefined, options: LiveAtlasCircle, converter: Function): LiveAtlasPolyline | LiveAtlasPolygon => {
if (!circle) {
return createCircle(options, converter);
}
const outline = (options.style && options.style.fillOpacity && (options.style.fillOpacity <= 0)) as boolean;
circle.closePopup();
circle.unbindPopup();
circle.bindPopup(() => createPopup(options));
circle.bindPopup(() => createPopup(options, 'CirclePopup'));
circle.setStyle(options.style);
circle.setLatLngs(points);
circle.setLatLngs(getCirclePoints(options, converter, outline));
circle.redraw();
return circle;
}
export const createPopup = (options: DynmapCircle) => {
const popup = document.createElement('span');
if (options.popupContent) {
popup.classList.add('CirclePopup');
popup.insertAdjacentHTML('afterbegin', options.popupContent);
} else if (options.isHTML) {
popup.classList.add('CirclePopup');
popup.insertAdjacentHTML('afterbegin', options.label);
} else {
popup.textContent = options.label;
}
return popup;
}
export const getCirclePoints = (options: DynmapCircle, converter: Function, outline: boolean): LatLngExpression[] => {
export const getCirclePoints = (options: LiveAtlasCircle, converter: Function, outline: boolean): LatLngExpression[] => {
const points = [];
for(let i = 0; i < 360; i++) {
@ -84,7 +64,7 @@ export const getCirclePoints = (options: DynmapCircle, converter: Function, outl
x = options.radius[0] * Math.sin(rad) + options.location.x,
z = options.radius[1] * Math.cos(rad) + options.location.z;
points.push(converter(x, options.location.y, z));
points.push(converter({x, y:options.location.y, z}));
}
if(outline && points.length) {

View File

@ -1,4 +1,20 @@
import {LiveAtlasDynmapServerDefinition, LiveAtlasGlobalConfig, LiveAtlasServerDefinition} from "@/index";
/*
* Copyright 2021 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.
*/
import {LiveAtlasGlobalConfig, LiveAtlasServerDefinition} from "@/index";
import ConfigurationError from "@/errors/ConfigurationError";
import {DynmapUrlConfig} from "@/dynmap";
@ -22,10 +38,10 @@ const validateLiveAtlasConfiguration = (config: any): Map<string, LiveAtlasServe
}
serverConfig.id = server;
serverConfig.type = serverConfig.type || 'dynmap';
switch(serverConfig.type) {
case 'dynmap':
if(typeof serverConfig.pl3xmap !== 'undefined') {
serverConfig.type = 'pl3xmap';
} else if(typeof serverConfig.dynmap !== 'undefined') {
if (!serverConfig.dynmap || serverConfig.dynmap.constructor !== Object) {
throw new ConfigurationError(`Server '${server}': Dynmap configuration object missing. ${check}`);
}
@ -49,13 +65,10 @@ const validateLiveAtlasConfiguration = (config: any): Map<string, LiveAtlasServe
if (!serverConfig.dynmap.sendmessage) {
throw new ConfigurationError(`Server '${server}': Dynmap sendmessage URL missing. ${check}`);
}
break;
case 'pl3xmap':
case 'plexmap':
if (!serverConfig.plexmap || serverConfig.plexmap.constructor !== Object) {
throw new ConfigurationError(`Server '${server}': Pl3xmap configuration object missing. ${check}`);
}
serverConfig.type = 'dynmap';
} else {
throw new ConfigurationError(`Server '${server}': No Dynmap or Pl3xmap configuration defined. ${check}`);
}
result.set(server, serverConfig);
@ -64,7 +77,7 @@ const validateLiveAtlasConfiguration = (config: any): Map<string, LiveAtlasServe
return result;
};
const validateDynmapConfiguration = (config: DynmapUrlConfig): Map<string, LiveAtlasDynmapServerDefinition> => {
const validateDynmapConfiguration = (config: DynmapUrlConfig): Map<string, LiveAtlasServerDefinition> => {
const check = '\nCheck your standalone/config.js file exists and is being loaded correctly.';
if (!config) {
@ -91,7 +104,7 @@ const validateDynmapConfiguration = (config: DynmapUrlConfig): Map<string, LiveA
throw new ConfigurationError(`Dynmap sendmessage URL is missing. ${check}`);
}
const result = new Map<string, LiveAtlasDynmapServerDefinition>();
const result = new Map<string, LiveAtlasServerDefinition>();
result.set('dynmap', {
id: 'dynmap',
label: 'dynmap',

View File

@ -1,3 +1,19 @@
/*
* Copyright 2021 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.
*/
const navigationKeys = new Set<string>([
'ArrowUp',
'ArrowDown',

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 James Lyne
* Copyright 2021 James Lyne
*
* Some portions of this file were taken from https://github.com/webbukkit/dynmap.
* These portions are Copyright 2020 Dynmap Contributors.
@ -17,63 +17,55 @@
* limitations under the License.
*/
import {DynmapLine} from "@/dynmap";
import {LatLngExpression} from "leaflet";
import LiveAtlasPolyline from "@/leaflet/vector/LiveAtlasPolyline";
import {Coordinate, LiveAtlasLine} from "@/index";
import {LatLngExpression} from "leaflet";
import {createPopup, tooltipOptions} from "@/util/paths";
export const createLine = (options: DynmapLine, converter: Function): LiveAtlasPolyline => {
const points = getLinePoints(options, converter),
line = new LiveAtlasPolyline(points, {
...options.style,
minZoom: options.minZoom,
maxZoom: options.maxZoom,
});
export const createLine = (options: LiveAtlasLine, converter: Function): LiveAtlasPolyline => {
const points = options.points.map(projectPointsMapCallback, converter),
line = new LiveAtlasPolyline(points, options);
if(options.label) {
line.bindPopup(() => createPopup(options));
if(options.popupContent) {
line.bindPopup(() => createPopup(options, 'LinePopup'));
}
if (options.tooltipContent) {
line.bindTooltip(() => options.tooltipContent as string, tooltipOptions);
}
return line;
};
export const updateLine = (line: LiveAtlasPolyline | undefined, options: DynmapLine, converter: Function): LiveAtlasPolyline => {
const points = getLinePoints(options, converter);
export const updateLine = (line: LiveAtlasPolyline | undefined, options: LiveAtlasLine, converter: Function): LiveAtlasPolyline => {
if (!line) {
return createLine(options, converter);
}
line.closePopup();
line.unbindPopup();
line.bindPopup(() => createPopup(options));
line.bindPopup(() => createPopup(options, 'LinePopup'));
line.setStyle(options.style);
line.setLatLngs(points);
line.setLatLngs(options.points.map(projectPointsMapCallback, converter));
line.redraw();
return line;
}
export const createPopup = (options: DynmapLine) => {
const popup = document.createElement('span');
if (options.popupContent) {
popup.classList.add('LinePopup');
popup.insertAdjacentHTML('afterbegin', options.popupContent);
} else if (options.isHTML) {
popup.classList.add('LinePopup');
popup.insertAdjacentHTML('afterbegin', options.label);
const projectPointsMapCallback = function(point: Coordinate): LatLngExpression {
if(Array.isArray(point)) {
return projectPointsMapCallback(point);
} else {
popup.textContent = options.label;
// @ts-ignore
return this(point);
}
};
return popup;
}
export const getLinePoints = (options: DynmapLine, converter: Function): LatLngExpression[] => {
export const getLinePoints = (x: number[], y: number[], z: number[]): Coordinate[] => {
const points = [];
for(let i = 0; i < options.x.length; i++) {
points.push(converter(options.x[i], options.y[i], options.z[i]));
for(let i = 0; i < x.length; i++) {
points.push({x: x[i], y: y[i], z: z[i]});
}
return points;

View File

@ -1,5 +1,5 @@
/*
* Copyright 2020 James Lyne
* Copyright 2021 James Lyne
*
* Some portions of this file were taken from https://github.com/webbukkit/dynmap.
* These portions are Copyright 2020 Dynmap Contributors.
@ -18,21 +18,12 @@
*/
import {LeafletMouseEvent, Marker} from "leaflet";
import {DynmapMarker} from "@/dynmap";
import {GenericIcon} from "@/leaflet/icon/GenericIcon";
import {GenericMarker} from "@/leaflet/marker/GenericMarker";
import {LiveAtlasMarker} from "@/index";
export const createMarker = (options: DynmapMarker, converter: Function): Marker => {
const marker = new GenericMarker(converter(options.location.x, options.location.y, options.location.z), {
icon: new GenericIcon({
icon: options.icon,
label: options.label,
iconSize: options.dimensions,
isHtml: options.isHTML,
}),
maxZoom: options.maxZoom,
minZoom: options.minZoom,
});
export const createMarker = (options: LiveAtlasMarker, converter: Function): Marker => {
const marker = new GenericMarker(converter(options.location), options);
marker.on('click', (e: LeafletMouseEvent) => {
e.target._map.panTo(e.target.getLatLng());
@ -45,13 +36,13 @@ export const createMarker = (options: DynmapMarker, converter: Function): Marker
return marker;
};
export const updateMarker = (marker: Marker | undefined, options: DynmapMarker, converter: Function): Marker => {
export const updateMarker = (marker: Marker | undefined, options: LiveAtlasMarker, converter: Function): Marker => {
if (!marker) {
return createMarker(options, converter);
}
const oldLocation = marker.getLatLng(),
newLocation = converter(options.location.x, options.location.y, options.location.z);
newLocation = converter(options.location);
if(!oldLocation.equals(newLocation)) {
marker.setLatLng(newLocation);
@ -65,7 +56,7 @@ export const updateMarker = (marker: Marker | undefined, options: DynmapMarker,
icon: options.icon,
label: options.label,
iconSize: options.dimensions,
isHtml: options.isHTML,
isHtml: options.isLabelHTML,
});
}
}
@ -80,17 +71,12 @@ export const updateMarker = (marker: Marker | undefined, options: DynmapMarker,
return marker;
};
export const createPopup = (options: DynmapMarker) => {
const createPopup = (options: LiveAtlasMarker) => {
const popup = document.createElement('span');
if (options.popupContent) {
popup.classList.add('MarkerPopup');
popup.insertAdjacentHTML('afterbegin', options.popupContent);
} else if (options.isHTML) {
popup.classList.add('MarkerPopup');
popup.insertAdjacentHTML('afterbegin', options.label);
} else {
popup.textContent = options.label;
}
return popup;

52
src/util/paths.ts Normal file
View File

@ -0,0 +1,52 @@
/*
* Copyright 2021 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.
*/
import {Direction, LatLngExpression, PathOptions} from "leaflet";
import {LiveAtlasPath} from "@/index";
export const tooltipOptions = {
direction: 'top' as Direction,
sticky: true,
opacity: 1.0,
interactive: false,
};
export const arePointsEqual = (oldPoints: LatLngExpression | LatLngExpression[] | LatLngExpression[][] | LatLngExpression[][][],
newPoints: LatLngExpression | LatLngExpression[] | LatLngExpression[][] | LatLngExpression[][][]) => {
return JSON.stringify(oldPoints) === JSON.stringify(newPoints);
}
export const isStyleEqual = (oldStyle: PathOptions, newStyle: PathOptions) => {
return oldStyle && newStyle
&& (oldStyle.color === newStyle.color)
&& (oldStyle.weight === newStyle.weight)
&& (oldStyle.opacity === newStyle.opacity)
&& (oldStyle.fillColor === newStyle.fillColor)
&& (oldStyle.fillOpacity === newStyle.fillOpacity)
}
export const createPopup = (options: LiveAtlasPath, className: string): HTMLElement => {
const popup = document.createElement('span');
if(options.isPopupHTML) {
popup.classList.add(className);
popup.insertAdjacentHTML('afterbegin', options.popupContent as string);
} else {
popup.textContent = options.popupContent as string;
}
return popup;
};

View File

@ -1,3 +1,19 @@
/*
* Copyright 2021 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.
*/
const app = document.getElementById('app'),
splash = document.getElementById('splash'),
splashSpinner = document.getElementById('splash__spinner'),