diff --git a/src/providers/DynmapMapProvider.ts b/src/providers/DynmapMapProvider.ts index 00e2b4e..f776c88 100644 --- a/src/providers/DynmapMapProvider.ts +++ b/src/providers/DynmapMapProvider.ts @@ -16,28 +16,24 @@ import { HeadQueueEntry, - LiveAtlasArea, LiveAtlasChat, - LiveAtlasCircle, LiveAtlasComponentConfig, - LiveAtlasDimension, - LiveAtlasLine, - LiveAtlasMarker, LiveAtlasMarkerSet, - LiveAtlasPlayer, LiveAtlasServerConfig, + LiveAtlasPlayer, LiveAtlasServerDefinition, - LiveAtlasServerMessageConfig, LiveAtlasWorldDefinition } from "@/index"; -import { - DynmapMarkerSetUpdates, DynmapTileUpdate, DynmapUpdate -} from "@/dynmap"; -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"; +import { + buildAreas, + buildCircles, buildComponents, + buildLines, + buildMarkers, + buildMarkerSet, + buildMessagesConfig, + buildServerConfig, buildUpdates, buildWorlds +} from "@/util/dynmap"; export default class DynmapMapProvider extends MapProvider { private configurationAbort?: AbortController = undefined; @@ -53,534 +49,6 @@ export default class DynmapMapProvider extends MapProvider { 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 { - const worlds: Map = new Map(); - - //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 { - const markers = Object.freeze(new Map()) as Map; - - 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 { - const areas = Object.freeze(new Map()) as Map; - - 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 { - const lines = Object.freeze(new Map()) as Map; - - 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 { - const circles = Object.freeze(new Map()) as Map; - - 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) { - const updates = { - markerSets: new Map(), - 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> { const url = `${this.config.dynmap!.markers}_markers_/marker_${world.name}.json`; @@ -601,13 +69,13 @@ export default class DynmapMapProvider extends MapProvider { } 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 || {}); + markers = buildMarkers(set.markers || {}), + circles = buildCircles(set.circles || {}), + areas = buildAreas(set.areas || {}), + lines = buildLines(set.lines || {}); sets.set(key, { - ...DynmapMapProvider.buildMarkerSet(key, set), + ...buildMarkerSet(key, set), markers, circles, areas, @@ -635,16 +103,16 @@ export default class DynmapMapProvider extends MapProvider { throw new Error(response.error); } - const config = DynmapMapProvider.buildServerConfig(response); + const config = 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_SERVER_MESSAGES, buildMessagesConfig(response)); + this.store.commit(MutationTypes.SET_WORLDS, buildWorlds(response)); + this.store.commit(MutationTypes.SET_COMPONENTS, buildComponents(response)); this.store.commit(MutationTypes.SET_LOGGED_IN, response.loggedin || false); } @@ -667,7 +135,7 @@ export default class DynmapMapProvider extends MapProvider { const response = await DynmapMapProvider.getJSON(url, this.updateAbort.signal); const players: Set = new Set(), - updates = this.buildUpdates(response.updates || []), + updates = buildUpdates(response.updates || [], this.updateTimestamp), worldState = { timeOfDay: response.servertime || 0, thundering: response.isThundering || false, diff --git a/src/util/dynmap.ts b/src/util/dynmap.ts new file mode 100644 index 0000000..7853ace --- /dev/null +++ b/src/util/dynmap.ts @@ -0,0 +1,557 @@ +/* + * 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 {DynmapMarkerSetUpdates, DynmapTileUpdate, DynmapUpdate} from "@/dynmap"; +import { + LiveAtlasArea, LiveAtlasChat, + LiveAtlasCircle, + LiveAtlasComponentConfig, LiveAtlasDimension, + LiveAtlasLine, + LiveAtlasMarker, LiveAtlasServerConfig, LiveAtlasServerMessageConfig, + LiveAtlasWorldDefinition +} from "@/index"; +import {getPoints} from "@/util/areas"; +import {endWorldNameRegex, netherWorldNameRegex, titleColoursRegex} from "@/util"; +import {getLinePoints} from "@/util/lines"; +import LiveAtlasMapDefinition from "@/model/LiveAtlasMapDefinition"; + +export function 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 + }; +} + +export 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'] ? `${response['msg-players']} ({cur}/{max})` : '', + } +} + +export function buildWorlds(response: any): Array { + const worlds: Map = new Map(); + + //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 actualWorld = worlds.get(world.name), + assignedWorldName = map.append_to_world || world.name, //handle append_to_world + assignedWorld = worlds.get(assignedWorldName); + + if (!assignedWorld || !actualWorld) { + console.warn(`Ignoring map '${map.name}' associated with non-existent world '${assignedWorldName}'`); + return; + } + + assignedWorld.maps.set(map.name, Object.freeze(new LiveAtlasMapDefinition({ + world: actualWorld, //Ignore append_to_world here for Dynmap URL parity + 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()); +} + +export function 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; +} + +export 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, + } +} + +export function buildMarkers(data: any): Map { + const markers = Object.freeze(new Map()) as Map; + + for (const key in data) { + if (!Object.prototype.hasOwnProperty.call(data, key)) { + continue; + } + + markers.set(key, buildMarker(data[key])); + } + + return markers; +} + +export function buildMarker(marker: any): LiveAtlasMarker { + return Object.seal({ + 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, + }); +} + +export function buildAreas(data: any): Map { + const areas = Object.freeze(new Map()) as Map; + + for (const key in data) { + if (!Object.prototype.hasOwnProperty.call(data, key)) { + continue; + } + + areas.set(key, buildArea(data[key])); + } + + return areas; +} + +export function 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, + }); +} + +export function buildLines(data: any): Map { + const lines = Object.freeze(new Map()) as Map; + + for (const key in data) { + if (!Object.prototype.hasOwnProperty.call(data, key)) { + continue; + } + + lines.set(key, buildLine(data[key])); + } + + return lines; +} + +export function 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, + }); +} + +export function buildCircles(data: any): Map { + const circles = Object.freeze(new Map()) as Map; + + for (const key in data) { + if (!Object.prototype.hasOwnProperty.call(data, key)) { + continue; + } + + circles.set(key, buildCircle(data[key])); + } + + return circles; +} + +export function 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, + }); +} + +export function buildUpdates(data: Array, lastUpdate: Date) { + const updates = { + markerSets: new Map(), + tiles: [] as DynmapTileUpdate[], + chat: [] as LiveAtlasChat[], + }, + dropped = { + stale: 0, + noSet: 0, + noId: 0, + unknownType: 0, + unknownCType: 0, + incomplete: 0, + notImplemented: 0, + }; + + 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; +}