From e77a777a66b65a3b83acf8f53fb7e1ed1663b6b6 Mon Sep 17 00:00:00 2001 From: James Lyne Date: Fri, 11 Dec 2020 15:28:51 +0000 Subject: [PATCH] Implement update handling for areas/lines/circles --- src/api.ts | 256 +++++++++++------ src/components/map/layer/MarkerSetLayer.vue | 6 +- src/components/map/vector/Areas.vue | 291 ++++++-------------- src/components/map/vector/Circles.vue | 199 ++++++------- src/components/map/vector/Lines.vue | 181 ++++++------ src/dynmap.d.ts | 38 ++- src/store/action-types.ts | 4 + src/store/actions.ts | 83 +++++- src/store/mutation-types.ts | 5 + src/store/mutations.ts | 163 ++++++++--- src/store/state.ts | 17 +- src/util.ts | 9 + src/util/areas.ts | 170 ++++++++++++ src/util/circles.ts | 71 +++++ src/util/lines.ts | 55 ++++ 15 files changed, 997 insertions(+), 551 deletions(-) create mode 100644 src/util/areas.ts create mode 100644 src/util/circles.ts create mode 100644 src/util/lines.ts diff --git a/src/api.ts b/src/api.ts index a625e37..8db7998 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,11 +1,18 @@ import axios, {AxiosResponse} from 'axios'; import { - DynmapArea, DynmapCircle, + DynmapArea, + DynmapCircle, DynmapComponentConfig, - DynmapConfigurationResponse, DynmapLine, DynmapMap, DynmapMarker, DynmapMarkerSet, DynmapMessageConfig, + DynmapConfigurationResponse, + DynmapLine, + DynmapMap, + DynmapMarker, + DynmapMarkerSet, + DynmapMarkerSetUpdates, + DynmapMessageConfig, DynmapPlayer, - DynmapServerConfig, - DynmapUpdateResponse, + DynmapServerConfig, DynmapUpdate, + DynmapUpdateResponse, DynmapUpdates, DynmapWorld } from "@/dynmap"; @@ -166,27 +173,29 @@ function buildMarkers(data: any): Map { continue; } - const marker = data[key]; - - markers.set(key, { - 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: marker.minzoom || undefined, - maxZoom: marker.maxZoom || undefined, - popupContent: marker.desc || undefined, - }); + 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: marker.minzoom || undefined, + maxZoom: marker.maxZoom || undefined, + popupContent: marker.desc || undefined, + }; +} + function buildAreas(data: any): Map { const areas = Object.freeze(new Map()) as Map; @@ -195,30 +204,32 @@ function buildAreas(data: any): Map { continue; } - const area = data[key]; - - areas.set(key, { - 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: area.minzoom || undefined, - maxZoom: area.maxZoom || undefined, - popupContent: area.desc || undefined, - }); + 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: area.minzoom || undefined, + maxZoom: area.maxZoom || undefined, + popupContent: area.desc || undefined, + }; +} + function buildLines(data: any): Map { const lines = Object.freeze(new Map()) as Map; @@ -227,28 +238,30 @@ function buildLines(data: any): Map { continue; } - const line = data[key]; - - lines.set(key, { - 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: line.minzoom || undefined, - maxZoom: line.maxZoom || undefined, - popupContent: line.desc || undefined, - }); + 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: line.minzoom || undefined, + maxZoom: line.maxZoom || undefined, + popupContent: line.desc || undefined, + }; +} + function buildCircles(data: any): Map { const circles = Object.freeze(new Map()) as Map; @@ -257,34 +270,117 @@ function buildCircles(data: any): Map { continue; } - const circle = data[key]; - - circles.set(key, { - 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: circle.minzoom || undefined, - maxZoom: circle.maxZoom || undefined, - popupContent: circle.desc || undefined, - }); + 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: circle.minzoom || undefined, + maxZoom: circle.maxZoom || undefined, + popupContent: circle.desc || undefined, + }; +} + +function buildUpdates(data: Array): DynmapUpdates { + const updates = { + markerSets: new Map(), + tiles: new Map(), + chat: [], + } + + for(const entry of data) { + switch(entry.type) { + case 'component': { + if(!entry.id) { + console.warn(`Ignoring component update without an ID`); + continue; + } + + if(!entry.set) { + console.warn(`Ignoring component update without a marker set`); + continue; + } + + if(entry.ctype !== 'markers') { + console.warn(`Ignoring component with unknown ctype ${entry.ctype}`); + continue; + } + + if(!updates.markerSets.has(entry.set)) { + updates.markerSets.set(entry.set, { + areaUpdates: [], + markerUpdates: [], + lineUpdates: [], + circleUpdates: [], + }); + } + + const markerSetUpdates = updates.markerSets.get(entry.set), + update: DynmapUpdate = { + id: entry.id, + removed: entry.msg.endsWith('deleted'), + }; + + + 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)); + } + + break; + } + + case 'chat': + //TODO + break; + + case 'tile': + if(!entry.name || !entry.timestamp) { + console.warn(`Ignoring tile update without a name or timestamp`); + break; + } + + updates.tiles.set(entry.name, entry.timestamp); + break; + + default: + console.warn(`Ignoring unknown update type ${entry.type}`); + } + } + + return updates; +} + export default { getConfiguration(): Promise { return axios.get(window.config.url.configuration).then((response): DynmapConfigurationResponse => { @@ -349,6 +445,7 @@ export default { configHash: data.configHash || 0, timestamp: data.timestamp || 0, players, + updates: buildUpdates(data.updates || []), } }); }, @@ -374,6 +471,7 @@ export default { lines = buildLines(set.lines || {}); sets.set(key, { + id: key, label: set.label || "Unnamed set", hidden: set.hidden || false, priority: set.layerprio || 0, diff --git a/src/components/map/layer/MarkerSetLayer.vue b/src/components/map/layer/MarkerSetLayer.vue index 3d3232c..2c0173a 100644 --- a/src/components/map/layer/MarkerSetLayer.vue +++ b/src/components/map/layer/MarkerSetLayer.vue @@ -1,8 +1,8 @@ - - \ No newline at end of file diff --git a/src/components/map/vector/Circles.vue b/src/components/map/vector/Circles.vue index 824cb7b..0cfb198 100644 --- a/src/components/map/vector/Circles.vue +++ b/src/components/map/vector/Circles.vue @@ -1,140 +1,111 @@ - - \ No newline at end of file diff --git a/src/components/map/vector/Lines.vue b/src/components/map/vector/Lines.vue index ab0899c..2d72644 100644 --- a/src/components/map/vector/Lines.vue +++ b/src/components/map/vector/Lines.vue @@ -1,122 +1,111 @@ - - \ No newline at end of file diff --git a/src/dynmap.d.ts b/src/dynmap.d.ts index 23079da..ce663f8 100644 --- a/src/dynmap.d.ts +++ b/src/dynmap.d.ts @@ -139,8 +139,8 @@ interface DynmapUpdateResponse { configHash: number; playerCount: number; players: Set; + updates: DynmapUpdates; timestamp: number; - //TODO: Tiles etc } interface DynmapPlayer { @@ -153,6 +153,7 @@ interface DynmapPlayer { } interface DynmapMarkerSet { + id: string, label: string; hidden: boolean; priority: number; @@ -210,3 +211,38 @@ interface DynmapCircle { maxZoom?: number; popupContent?: string; } + +interface DynmapUpdates { + markerSets: Map, + tiles: Map, + chat: Array //TODO +} + +interface DynmapMarkerSetUpdates { + markerUpdates: Array + areaUpdates: Array + circleUpdates: Array + lineUpdates: Array +} + +interface DynmapUpdate { + id: string, + removed: boolean, + payload?: any, +} + +interface DynmapMarkerUpdate extends DynmapUpdate { + payload?: DynmapMarker +} + +interface DynmapAreaUpdate extends DynmapUpdate { + payload?: DynmapArea +} + +interface DynmapCircleUpdate extends DynmapUpdate { + payload?: DynmapCircle +} + +interface DynmapLineUpdate extends DynmapUpdate { + payload?: DynmapLine +} \ No newline at end of file diff --git a/src/store/action-types.ts b/src/store/action-types.ts index 78b36c4..97b951f 100644 --- a/src/store/action-types.ts +++ b/src/store/action-types.ts @@ -3,4 +3,8 @@ export enum ActionTypes { GET_UPDATE = "getUpdate", GET_MARKER_SETS = "getMarkerSets", SET_PLAYERS = "setPlayers", + POP_MARKER_UPDATES = "popMarkerUpdates", + POP_AREA_UPDATES = "popAreaUpdates", + POP_CIRCLE_UPDATES = "popCircleUpdates", + POP_LINE_UPDATES = "popLineUpdates", } \ No newline at end of file diff --git a/src/store/actions.ts b/src/store/actions.ts index 2866f98..7c7beed 100644 --- a/src/store/actions.ts +++ b/src/store/actions.ts @@ -4,7 +4,14 @@ import {State} from "@/store/state"; import {ActionTypes} from "@/store/action-types"; import API from '@/api'; import {Mutations} from "@/store/mutations"; -import {DynmapConfigurationResponse, DynmapMarkerSet, DynmapPlayer, DynmapUpdateResponse} from "@/dynmap"; +import { + DynmapAreaUpdate, DynmapCircleUpdate, + DynmapConfigurationResponse, DynmapLineUpdate, + DynmapMarkerSet, + DynmapMarkerUpdate, + DynmapPlayer, + DynmapUpdateResponse +} from "@/dynmap"; type AugmentedActionContext = { commit( @@ -27,6 +34,22 @@ export interface Actions { {commit}: AugmentedActionContext, payload: Set ):Promise> + [ActionTypes.POP_MARKER_UPDATES]( + {commit}: AugmentedActionContext, + payload: {markerSet: string, amount: number} + ): Promise + [ActionTypes.POP_AREA_UPDATES]( + {commit}: AugmentedActionContext, + payload: {markerSet: string, amount: number} + ): Promise + [ActionTypes.POP_CIRCLE_UPDATES]( + {commit}: AugmentedActionContext, + payload: {markerSet: string, amount: number} + ): Promise + [ActionTypes.POP_LINE_UPDATES]( + {commit}: AugmentedActionContext, + payload: {markerSet: string, amount: number} + ): Promise } export const actions: ActionTree & Actions = { @@ -52,10 +75,11 @@ export const actions: ActionTree & Actions = { return Promise.reject("No current world"); } - return API.getUpdate(state.updateRequestId, state.currentWorld.name, state.updateTimestamp.getUTCMilliseconds()).then(update => { + return API.getUpdate(state.updateRequestId, state.currentWorld.name, state.updateTimestamp.valueOf()).then(update => { 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); return dispatch(ActionTypes.SET_PLAYERS, update.players).then(() => { return update; @@ -97,5 +121,58 @@ export const actions: ActionTree & Actions = { return markerSets; }); - } + }, + + [ActionTypes.POP_MARKER_UPDATES]({commit, state}, {markerSet, amount}: {markerSet: string, amount: number}): Promise { + if(!state.markerSets.has(markerSet)) { + console.log(`Marker set ${markerSet} doesn't exist`); + return Promise.resolve([]); + } + + const updates = state.pendingSetUpdates.get(markerSet)!.markerUpdates.slice(0, amount); + + commit(MutationTypes.POP_MARKER_UPDATES, {markerSet, amount}); + + return Promise.resolve(updates); + }, + + [ActionTypes.POP_AREA_UPDATES]({commit, state}, {markerSet, amount}: {markerSet: string, amount: number}): Promise { + if(!state.markerSets.has(markerSet)) { + console.log(`Marker set ${markerSet} doesn't exist`); + return Promise.resolve([]); + } + + const updates = state.pendingSetUpdates.get(markerSet)!.areaUpdates.slice(0, amount); + + commit(MutationTypes.POP_AREA_UPDATES, {markerSet, amount}); + + return Promise.resolve(updates); + }, + + [ActionTypes.POP_CIRCLE_UPDATES]({commit, state}, {markerSet, amount}: {markerSet: string, amount: number}): Promise { + if(!state.markerSets.has(markerSet)) { + console.log(`Marker set ${markerSet} doesn't exist`); + return Promise.resolve([]); + } + + const updates = state.pendingSetUpdates.get(markerSet)!.circleUpdates.slice(0, amount); + + commit(MutationTypes.POP_CIRCLE_UPDATES, {markerSet, amount}); + + return Promise.resolve(updates); + }, + + [ActionTypes.POP_LINE_UPDATES]({commit, state}, {markerSet, amount}: {markerSet: string, amount: number}): Promise { + if(!state.markerSets.has(markerSet)) { + console.log(`Marker set ${markerSet} doesn't exist`); + return Promise.resolve([]); + } + + const updates = state.pendingSetUpdates.get(markerSet)!.lineUpdates.slice(0, amount); + + commit(MutationTypes.POP_LINE_UPDATES, {markerSet, amount}); + + return Promise.resolve(updates); + }, + } \ No newline at end of file diff --git a/src/store/mutation-types.ts b/src/store/mutation-types.ts index 4d98dcd..12f361f 100644 --- a/src/store/mutation-types.ts +++ b/src/store/mutation-types.ts @@ -7,6 +7,11 @@ export enum MutationTypes { ADD_WORLD = 'addWorld', SET_WORLD_STATE = 'setWorldState', SET_UPDATE_TIMESTAMP = 'setUpdateTimestamp', + ADD_MARKER_SET_UPDATES = 'addMarkerSetUpdates', + POP_MARKER_UPDATES = 'popMarkerUpdates', + POP_AREA_UPDATES = 'popAreaUpdates', + POP_CIRCLE_UPDATES = 'popCircleUpdates', + POP_LINE_UPDATES = 'popLineUpdates', INCREMENT_REQUEST_ID = 'incrementRequestId', SET_PLAYERS = 'setPlayers', SET_PLAYERS_ASYNC = 'setPlayersAsync', diff --git a/src/store/mutations.ts b/src/store/mutations.ts index 7d3a1fd..09a8a93 100644 --- a/src/store/mutations.ts +++ b/src/store/mutations.ts @@ -2,7 +2,17 @@ import {MutationTree} from "vuex"; import {MutationTypes} from "@/store/mutation-types"; import {State} from "@/store/state"; import { - DynmapComponentConfig, DynmapMarkerSet, + DynmapArea, + DynmapAreaUpdate, + DynmapCircle, + DynmapCircleUpdate, + DynmapComponentConfig, + DynmapLine, + DynmapLineUpdate, + DynmapMarker, + DynmapMarkerSet, + DynmapMarkerSetUpdates, + DynmapMarkerUpdate, DynmapMessageConfig, DynmapPlayer, DynmapServerConfig, @@ -25,9 +35,15 @@ export type Mutations = { [MutationTypes.ADD_WORLD](state: S, world: DynmapWorld): void [MutationTypes.SET_WORLD_STATE](state: S, worldState: DynmapWorldState): void [MutationTypes.SET_UPDATE_TIMESTAMP](state: S, time: Date): void + [MutationTypes.ADD_MARKER_SET_UPDATES](state: S, updates: Map): void + + [MutationTypes.POP_MARKER_UPDATES](state: S, payload: {markerSet: string, amount: number}): Array + [MutationTypes.POP_AREA_UPDATES](state: S, payload: {markerSet: string, amount: number}): Array + [MutationTypes.POP_CIRCLE_UPDATES](state: S, payload: {markerSet: string, amount: number}): Array + [MutationTypes.POP_LINE_UPDATES](state: S, payload: {markerSet: string, amount: number}): Array + [MutationTypes.INCREMENT_REQUEST_ID](state: S): void - // [MutationTypes.SET_PLAYERS](state: S, players: Array): void - [MutationTypes.SET_PLAYERS_ASYNC](state: S, players: Set): void + [MutationTypes.SET_PLAYERS_ASYNC](state: S, players: Set): Set [MutationTypes.SYNC_PLAYERS](state: S, keep: Set): void [MutationTypes.SET_CURRENT_MAP](state: S, payload: CurrentMapPayload): void [MutationTypes.SET_CURRENT_PROJECTION](state: S, payload: DynmapProjection): void @@ -73,6 +89,16 @@ export const mutations: MutationTree & Mutations = { //Sets the existing marker sets from the last marker fetch [MutationTypes.SET_MARKER_SETS](state: State, markerSets: Map) { state.markerSets = markerSets; + state.pendingSetUpdates.clear(); + + for(const entry of markerSets) { + state.pendingSetUpdates.set(entry[0], { + markerUpdates: [], + areaUpdates: [], + circleUpdates: [], + lineUpdates: [], + }); + } }, [MutationTypes.ADD_WORLD](state: State, world: DynmapWorld) { @@ -89,46 +115,101 @@ export const mutations: MutationTree & Mutations = { state.updateTimestamp = timestamp; }, + //Sets the timestamp of the last update fetch + [MutationTypes.ADD_MARKER_SET_UPDATES](state: State, updates: Map) { + for(const entry of updates) { + if(!state.markerSets.has(entry[0])) { + console.log(`Marker set ${entry[0]} doesn't exist`); + continue; + } + + const set = state.markerSets.get(entry[0]) as DynmapMarkerSet, + setUpdates = state.pendingSetUpdates.get(entry[0]) as DynmapMarkerSetUpdates; + + //Update non-reactive lists + for(const update of entry[1].markerUpdates) { + if(update.removed) { + set.markers.delete(update.id); + } else { + set.markers.set(update.id, update.payload as DynmapMarker); + } + } + + for(const update of entry[1].areaUpdates) { + if(update.removed) { + set.areas.delete(update.id); + } else { + set.areas.set(update.id, update.payload as DynmapArea); + } + } + + for(const update of entry[1].circleUpdates) { + if(update.removed) { + set.circles.delete(update.id); + } else { + set.circles.set(update.id, update.payload as DynmapCircle); + } + } + + for(const update of entry[1].lineUpdates) { + if(update.removed) { + set.lines.delete(update.id); + } else { + set.lines.set(update.id, update.payload as DynmapLine); + } + } + + //Add to reactive pending updates lists + setUpdates.markerUpdates = setUpdates.markerUpdates.concat(entry[1].markerUpdates); + setUpdates.areaUpdates = setUpdates.areaUpdates.concat(entry[1].areaUpdates); + setUpdates.circleUpdates = setUpdates.circleUpdates.concat(entry[1].circleUpdates); + setUpdates.lineUpdates = setUpdates.lineUpdates.concat(entry[1].lineUpdates); + } + }, + + [MutationTypes.POP_MARKER_UPDATES](state: State, {markerSet, amount}): Array { + if(!state.markerSets.has(markerSet)) { + console.log(`Marker set ${markerSet} doesn't exist`); + return []; + } + + return state.pendingSetUpdates.get(markerSet)!.markerUpdates.splice(0, amount); + }, + + [MutationTypes.POP_AREA_UPDATES](state: State, {markerSet, amount}): Array { + if(!state.markerSets.has(markerSet)) { + console.log(`Marker set ${markerSet} doesn't exist`); + return []; + } + + return state.pendingSetUpdates.get(markerSet)!.areaUpdates.splice(0, amount); + }, + + [MutationTypes.POP_CIRCLE_UPDATES](state: State, {markerSet, amount}): Array { + if(!state.markerSets.has(markerSet)) { + console.log(`Marker set ${markerSet} doesn't exist`); + return []; + } + + return state.pendingSetUpdates.get(markerSet)!.circleUpdates.splice(0, amount); + }, + + [MutationTypes.POP_LINE_UPDATES](state: State, {markerSet, amount}): Array { + if(!state.markerSets.has(markerSet)) { + console.log(`Marker set ${markerSet} doesn't exist`); + return []; + } + + return state.pendingSetUpdates.get(markerSet)!.lineUpdates.splice(0, amount); + }, + //Increments the request id for the next update fetch [MutationTypes.INCREMENT_REQUEST_ID](state: State) { state.updateRequestId++; }, - // [MutationTypes.SET_PLAYERS](state: State, players: Array) { - // const existingPlayers: Set = new Set(); - // - // players.forEach(player => { - // existingPlayers.add(player.account); - // - // if (state.players.has(player.account)) { - // const existing = state.players.get(player.account); - // - // existing!.health = player.health; - // existing!.armor = player.armor; - // existing!.location = Object.assign(existing!.location, player.location); - // existing!.name = player.name; - // existing!.sort = player.sort; - // } else { - // state.players.set(player.account, { - // account: player.account, - // health: player.health, - // armor: player.armor, - // location: player.location, - // name: player.name, - // sort: player.sort, - // }); - // } - // }); - // - // for (const key of state.players.keys()) { - // if (!existingPlayers.has(key)) { - // state.players.delete(key); - // } - // } - // }, - - //Set up to 10 players at once, returning the rest for future setting - [MutationTypes.SET_PLAYERS_ASYNC](state: State, players: Set) { + // Set up to 10 players at once + [MutationTypes.SET_PLAYERS_ASYNC](state: State, players: Set): Set { let count = 0; for(const player of players) { @@ -154,9 +235,11 @@ export const mutations: MutationTree & Mutations = { players.delete(player); if(++count >= 10) { - return players; + break; } } + + return players; }, //Removes all players not found in the provided keep set @@ -184,6 +267,8 @@ export const mutations: MutationTree & Mutations = { if(state.currentWorld !== newWorld) { state.currentWorld = state.worlds.get(worldName); state.markerSets.clear(); + state.pendingSetUpdates.clear(); + state.pendingTileUpdates = []; } state.currentMap = state.maps.get(mapName); diff --git a/src/store/state.ts b/src/store/state.ts index 6aac7cb..7df637f 100644 --- a/src/store/state.ts +++ b/src/store/state.ts @@ -1,6 +1,6 @@ import { DynmapComponentConfig, - DynmapMap, DynmapMarker, DynmapMarkerSet, + DynmapMap, DynmapMarkerSet, DynmapMarkerSetUpdates, DynmapMessageConfig, DynmapPlayer, DynmapServerConfig, @@ -18,9 +18,11 @@ export type State = { players: Map; markerSets: Map; + pendingSetUpdates: Map; + pendingTileUpdates: Array; + following?: DynmapPlayer; - // currentServer?: string; currentWorldState: DynmapWorldState; currentWorld?: DynmapWorld; currentMap?: DynmapMap; @@ -65,7 +67,10 @@ export const state: State = { worlds: new Map(), //Defined (loaded) worlds with maps from configuration.json maps: new Map(), //Defined maps from configuration.json players: new Map(), //Online players from world.json + markerSets: new Map(), //Markers from world_markers.json. Contents of each set isn't reactive for performance reasons. + pendingSetUpdates: new Map(), //Pending updates to markers/areas/etc for each marker set + pendingTileUpdates: [], //Pending updates to map tiles //Dynmap optional components components: { @@ -73,6 +78,7 @@ export const state: State = { markers: { showLabels: false, }, + // Optional "playermarkers" component. Settings for online player markers. // If not present, player markers will be disabled playerMarkers: undefined, @@ -83,22 +89,23 @@ export const state: State = { //Optional clock component. Used for both "digitalclock" and "timeofdayclock". Shows world time/weather. clockControl: undefined, - //Optional "link" component. Adds button to get url for current position + //Optional "link" component. Adds button to copy url for current position linkControl: false, + //Optional "logo" controls. logoControls: [], }, following: undefined, currentWorld: undefined, + currentMap: undefined, + currentProjection: new DynmapProjection(), //Projection for converting location <-> latlg. Object itself isn't reactive for performance reasons currentWorldState: { raining: false, thundering: false, timeOfDay: 0, }, - currentMap: undefined, - currentProjection: new DynmapProjection(), //Projection for converting location <-> latlg. Object itself isn't reactive for performance reasons updateRequestId: 0, updateTimestamp: new Date(), diff --git a/src/util.ts b/src/util.ts index a9f8f1c..ad451c2 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,4 +1,5 @@ import {DynmapPlayer} from "@/dynmap"; +import {useStore} from "@/store"; const headCache = new Map(); @@ -49,5 +50,13 @@ export default { } return base + addition; + }, + + getPointConverter() { + const projection = useStore().state.currentProjection; + + return (x: number, y: number, z: number) => { + return projection.locationToLatLng({x, y, z}); + }; } } \ No newline at end of file diff --git a/src/util/areas.ts b/src/util/areas.ts new file mode 100644 index 0000000..bebcf11 --- /dev/null +++ b/src/util/areas.ts @@ -0,0 +1,170 @@ +import {LatLngExpression, Polygon, Polyline} from "leaflet"; +import {DynmapArea} from "@/dynmap"; + +export const createArea = (options: DynmapArea, converter: Function): Polyline | Polygon => { + const outline = !options.style.fillOpacity || (options.style.fillOpacity <= 0), + points = getPoints(options, converter, outline), + area = outline ? new Polyline(points, options.style) : new Polygon(points, options.style); + + if (options.label) { + area.bindPopup(() => createPopup(options)); + } + + return area; +}; + +export const updateArea = (area: Polyline | Polygon | undefined, options: DynmapArea, converter: Function): Polyline | Polygon => { + const outline = (options.style && options.style.fillOpacity && (options.style.fillOpacity <= 0)) as boolean, + points = getPoints(options, converter, outline); + + if (!area) { + return createArea(options, converter); + } + + area.unbindPopup(); + area.bindPopup(() => createPopup(options)); + area.setStyle(options.style); + area.setLatLngs(points); + area.redraw(); + + return area; +}; + +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); + } else { + popup.textContent = options.label; + } + + 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); + } else { + return get3DBoxPoints(options, converter); + } + } else { + if (options.y[0] === options.y[1]) { + return get2DShapePoints(options, converter, outline); + } else { + return get3DShapePoints(options, converter); + } + } +}; + +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]; + + return [ + [ + converter(minX, minY, minZ), + converter(maxX, minY, minZ), + converter(maxX, minY, maxZ), + converter(minX, minY, maxZ) + ], [ + converter(minX, maxY, minZ), + converter(maxX, maxY, minZ), + converter(maxX, maxY, maxZ), + converter(minX, maxY, maxZ) + ], [ + converter(minX, minY, minZ), + converter(minX, maxY, minZ), + converter(maxX, maxY, minZ), + converter(maxX, minY, minZ) + ], [ + converter(maxX, minY, minZ), + converter(maxX, maxY, minZ), + converter(maxX, maxY, maxZ), + converter(maxX, minY, maxZ) + ], [ + converter(minX, minY, maxZ), + converter(minX, maxY, maxZ), + converter(maxX, maxY, maxZ), + converter(maxX, minY, maxZ) + ], [ + converter(minX, minY, minZ), + converter(minX, maxY, minZ), + converter(minX, maxY, maxZ), + converter(minX, minY, 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]; + + if (outline) { + return [ + converter(minX, minY, minZ), + converter(maxX, minY, minZ), + converter(maxX, minY, maxZ), + converter(minX, minY, maxZ), + converter(minX, minY, minZ) + ]; + } else { + return [ + converter(minX, minY, minZ), + converter(maxX, minY, minZ), + converter(maxX, minY, maxZ), + converter(minX, minY, maxZ) + ]; + } +}; + +export const get3DShapePoints = (options: DynmapArea, converter: Function): LatLngExpression[][] => { + 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 < 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; + } + + polylist[options.x.length] = botlist; + polylist[options.x.length + 1] = toplist; + + return polylist; +}; + +export const get2DShapePoints = (options: DynmapArea, converter: Function, outline: boolean): LatLngExpression[] => { + const points = []; + + for (let i = 0; i < options.x.length; i++) { + points[i] = converter(options.x[i], options.y[1], options.z[i]); + } + + if (outline) { + points.push(points[0]); + } + + return points; +} \ No newline at end of file diff --git a/src/util/circles.ts b/src/util/circles.ts new file mode 100644 index 0000000..959318c --- /dev/null +++ b/src/util/circles.ts @@ -0,0 +1,71 @@ +import {DynmapCircle} from "@/dynmap"; +import {Polyline, Polygon, LatLngExpression} from "leaflet"; + +export const createCircle = (options: DynmapCircle, converter: Function): Polyline | Polygon => { + const outline = !options.style.fillOpacity || (options.style.fillOpacity <= 0), + points = getCirclePoints(options, converter, outline); + let circle; + + if(outline) { + circle = new Polyline(points, options.style); + } else { + circle = new Polygon(points, options.style); + } + + if(options.label) { + circle.bindPopup(() => createPopup(options)); + } + + return circle; +}; + +export const updateCircle = (circle: Polyline | Polygon | undefined, options: DynmapCircle, converter: Function): Polyline | Polygon => { + const outline = (options.style && options.style.fillOpacity && (options.style.fillOpacity <= 0)) as boolean, + points = getCirclePoints(options, converter, outline); + + if (!circle) { + return createCircle(options, converter); + } + + circle.unbindPopup(); + circle.bindPopup(() => createPopup(options)); + circle.setStyle(options.style); + circle.setLatLngs(points); + 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[] => { + const points = []; + + for(let i = 0; i < 360; i++) { + const rad = i * Math.PI / 180.0, + 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)); + } + + if(outline && points.length) { + points.push(points[0]); + } + + return points; +}; diff --git a/src/util/lines.ts b/src/util/lines.ts new file mode 100644 index 0000000..6f05cc8 --- /dev/null +++ b/src/util/lines.ts @@ -0,0 +1,55 @@ +import {DynmapLine} from "@/dynmap"; +import {Polyline, Polygon, LatLngExpression} from "leaflet"; + +export const createLine = (options: DynmapLine, converter: Function): Polyline | Polygon => { + const points = getLinePoints(options, converter), + line = new Polyline(points, options.style); + + if(options.label) { + line.bindPopup(() => createPopup(options)); + } + + return line; +}; + +export const updateLine = (line: Polyline | Polygon | undefined, options: DynmapLine, converter: Function): Polyline | Polygon => { + const points = getLinePoints(options, converter); + + if (!line) { + return createLine(options, converter); + } + + line.unbindPopup(); + line.bindPopup(() => createPopup(options)); + line.setStyle(options.style); + line.setLatLngs(points); + 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); + } else { + popup.textContent = options.label; + } + + return popup; +} + +export const getLinePoints = (options: DynmapLine, converter: Function): LatLngExpression[] => { + const points = []; + + for(let i = 0; i < options.x.length; i++) { + points.push(converter(options.x[i], options.y[i], options.z[i])); + } + + return points; +};