Implement update handling for areas/lines/circles

This commit is contained in:
James Lyne 2020-12-11 15:28:51 +00:00
parent a809b68bb2
commit e77a777a66
15 changed files with 997 additions and 551 deletions

View File

@ -1,11 +1,18 @@
import axios, {AxiosResponse} from 'axios'; import axios, {AxiosResponse} from 'axios';
import { import {
DynmapArea, DynmapCircle, DynmapArea,
DynmapCircle,
DynmapComponentConfig, DynmapComponentConfig,
DynmapConfigurationResponse, DynmapLine, DynmapMap, DynmapMarker, DynmapMarkerSet, DynmapMessageConfig, DynmapConfigurationResponse,
DynmapLine,
DynmapMap,
DynmapMarker,
DynmapMarkerSet,
DynmapMarkerSetUpdates,
DynmapMessageConfig,
DynmapPlayer, DynmapPlayer,
DynmapServerConfig, DynmapServerConfig, DynmapUpdate,
DynmapUpdateResponse, DynmapUpdateResponse, DynmapUpdates,
DynmapWorld DynmapWorld
} from "@/dynmap"; } from "@/dynmap";
@ -166,27 +173,29 @@ function buildMarkers(data: any): Map<string, DynmapMarker> {
continue; continue;
} }
const marker = data[key]; markers.set(key, buildMarker(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,
});
} }
return markers; 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<string, DynmapArea> { function buildAreas(data: any): Map<string, DynmapArea> {
const areas = Object.freeze(new Map()) as Map<string, DynmapArea>; const areas = Object.freeze(new Map()) as Map<string, DynmapArea>;
@ -195,30 +204,32 @@ function buildAreas(data: any): Map<string, DynmapArea> {
continue; continue;
} }
const area = data[key]; areas.set(key, buildArea(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,
});
} }
return areas; 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<string, DynmapLine> { function buildLines(data: any): Map<string, DynmapLine> {
const lines = Object.freeze(new Map()) as Map<string, DynmapLine>; const lines = Object.freeze(new Map()) as Map<string, DynmapLine>;
@ -227,28 +238,30 @@ function buildLines(data: any): Map<string, DynmapLine> {
continue; continue;
} }
const line = data[key]; lines.set(key, buildLine(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,
});
} }
return lines; 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<string, DynmapCircle> { function buildCircles(data: any): Map<string, DynmapCircle> {
const circles = Object.freeze(new Map()) as Map<string, DynmapCircle>; const circles = Object.freeze(new Map()) as Map<string, DynmapCircle>;
@ -257,34 +270,117 @@ function buildCircles(data: any): Map<string, DynmapCircle> {
continue; continue;
} }
const circle = data[key]; circles.set(key, buildCircle(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,
});
} }
return circles; 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<any>): DynmapUpdates {
const updates = {
markerSets: new Map<string, DynmapMarkerSetUpdates>(),
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 { export default {
getConfiguration(): Promise<DynmapConfigurationResponse> { getConfiguration(): Promise<DynmapConfigurationResponse> {
return axios.get(window.config.url.configuration).then((response): DynmapConfigurationResponse => { return axios.get(window.config.url.configuration).then((response): DynmapConfigurationResponse => {
@ -349,6 +445,7 @@ export default {
configHash: data.configHash || 0, configHash: data.configHash || 0,
timestamp: data.timestamp || 0, timestamp: data.timestamp || 0,
players, players,
updates: buildUpdates(data.updates || []),
} }
}); });
}, },
@ -374,6 +471,7 @@ export default {
lines = buildLines(set.lines || {}); lines = buildLines(set.lines || {});
sets.set(key, { sets.set(key, {
id: key,
label: set.label || "Unnamed set", label: set.label || "Unnamed set",
hidden: set.hidden || false, hidden: set.hidden || false,
priority: set.layerprio || 0, priority: set.layerprio || 0,

View File

@ -1,8 +1,8 @@
<template> <template>
<GenericMarker v-for="[id, marker] in markerSet.markers" :options="marker" :key="id" :layer-group="layerGroup"></GenericMarker> <GenericMarker v-for="[id, marker] in markerSet.markers" :options="marker" :key="id" :layer-group="layerGroup"></GenericMarker>
<Areas :areas="markerSet.areas" :layer-group="layerGroup"></Areas> <Areas :layer-group="layerGroup" :set="markerSet"></Areas>
<Circles :circles="markerSet.circles" :layer-group="layerGroup"></Circles> <Circles :layer-group="layerGroup" :set="markerSet"></Circles>
<Lines :lines="markerSet.lines" :layer-group="layerGroup"></Lines> <Lines :layer-group="layerGroup" :set="markerSet"></Lines>
</template> </template>
<script lang="ts"> <script lang="ts">

View File

@ -1,237 +1,106 @@
<script lang="ts"> <script lang="ts">
import {defineComponent, computed} from "@vue/runtime-core"; import {defineComponent, computed, onMounted, onUnmounted, watch} from "@vue/runtime-core";
import L, {LatLngExpression} from 'leaflet'; import {LayerGroup, Polyline, Polygon} from 'leaflet';
import {useStore} from "@/store"; import {useStore} from "@/store";
import {DynmapArea} from "@/dynmap"; import {DynmapArea, DynmapMarkerSet} from "@/dynmap";
import {ActionTypes} from "@/store/action-types";
import {createArea, updateArea} from "@/util/areas";
import Util from '@/util';
export default defineComponent({ export default defineComponent({
props: { props: {
areas: { set: {
type: Object as () => Map<string, DynmapArea>, type: Object as () => DynmapMarkerSet,
required: true required: true,
}, },
layerGroup: { layerGroup: {
type: Object as () => L.LayerGroup, type: Object as () => LayerGroup,
required: true required: true
} }
}, },
setup() { setup(props) {
let updateFrame = 0;
const store = useStore(), const store = useStore(),
currentProjection = computed(() => store.state.currentProjection), currentProjection = computed(() => store.state.currentProjection),
layers = Object.freeze(new Map()) as Map<string, L.Path>; pendingUpdates = computed(() => {
const markerSetUpdates = store.state.pendingSetUpdates.get(props.set.id);
return { return markerSetUpdates && markerSetUpdates.areaUpdates.length;
layers, }),
currentProjection, layers = Object.freeze(new Map()) as Map<string, Polygon | Polyline>,
}
},
watch: { createAreas = () => {
//FIXME: Prevent unnecessary repositioning when changing worlds const converter = Util.getPointConverter();
currentProjection() {
const projection = useStore().state.currentProjection, props.set.areas.forEach((area: DynmapArea, id: string) => {
latLng = (x: number, y: number, z: number) => { const layer = createArea(area, converter);
return projection.locationToLatLng({x, y, z});
layers.set(id, layer);
props.layerGroup.addLayer(layer);
});
},
deleteArea = (id: string) => {
let area = layers.get(id) as Polyline;
if(!area) {
return;
} }
// eslint-disable-next-line no-unused-vars area.remove();
for (const [id, area] of this.areas) { layers.delete(id);
this.updateArea(id, area, latLng); },
handlePendingUpdates = () => {
useStore().dispatch(ActionTypes.POP_AREA_UPDATES, {
markerSet: props.set.id,
amount: 10,
}).then(updates => {
const converter = Util.getPointConverter();
for(const update of updates) {
if(update.removed) {
console.log(`Deleting area ${update.id}`);
deleteArea(update.id);
} else {
console.log(`Updating/creating area ${update.id}`);
layers.set(update.id, updateArea(layers.get(update.id), update.payload as DynmapArea, converter));
}
}
if(pendingUpdates.value) {
console.log('More updates left, scheduling frame');
// eslint-disable-next-line no-unused-vars
updateFrame = requestAnimationFrame(() => handlePendingUpdates());
} else {
updateFrame = 0;
}
});
};
//FIXME: Prevent unnecessary repositioning when changing worlds
watch(currentProjection, () => {
const converter = Util.getPointConverter();
for (const [id, area] of props.set.areas) {
updateArea(layers.get(id), area, converter);
} }
} });
},
mounted() { watch(pendingUpdates, (newValue, oldValue) => {
this.createAreas(); if(newValue && newValue > 0 && oldValue === 0 && !updateFrame) {
}, updateFrame = requestAnimationFrame(() => handlePendingUpdates());
}
unmounted() { });
onMounted(() => createAreas());
onUnmounted(() => updateFrame && cancelAnimationFrame(updateFrame));
}, },
render() { render() {
return null; return null;
},
methods: {
createAreas() {
const projection = useStore().state.currentProjection,
latLng = (x: number, y: number, z: number) => {
return projection.locationToLatLng({x, y, z});
};
this.areas.forEach((area: DynmapArea, id: string) => {
this.createArea(id, area, latLng);
});
},
createArea(id: string, options: DynmapArea, latLng: Function) {
const outline = !options.style.fillOpacity || (options.style.fillOpacity <= 0),
points = this.getPoints(options, latLng, outline),
area: L.Path = outline ? new L.Polyline(points, options.style) : new L.Polygon(points, options.style);
if (options.label) {
area.bindPopup(() => {
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;
});
}
this.layers.set(id, area);
this.layerGroup.addLayer(area);
},
updateArea(id: string, options: DynmapArea, latLng: Function) {
let area = this.layers.get(id) as L.Polyline,
outline = (options.style && options.style.fillOpacity && (options.style.fillOpacity <= 0)) as boolean,
points = this.getPoints(options, latLng, outline);
if (!area) {
return;
}
area.setLatLngs(points);
area.redraw();
},
getPoints(options: DynmapArea, latLng: Function, outline: boolean): LatLngExpression[] | LatLngExpression[][] {
if (options.x.length === 2) { /* Only 2 points */
if (options.y[0] === options.y[1]) {
return this.get2DBoxPoints(options, latLng, outline);
} else {
return this.get3DBoxPoints(options, latLng);
}
} else {
if (options.y[0] === options.y[1]) {
return this.get2DShapePoints(options, latLng, outline);
} else {
return this.get3DShapePoints(options, latLng);
}
}
},
get3DBoxPoints(options: DynmapArea, latLng: 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 [
[
latLng(minX, minY, minZ),
latLng(maxX, minY, minZ),
latLng(maxX, minY, maxZ),
latLng(minX, minY, maxZ)
], [
latLng(minX, maxY, minZ),
latLng(maxX, maxY, minZ),
latLng(maxX, maxY, maxZ),
latLng(minX, maxY, maxZ)
], [
latLng(minX, minY, minZ),
latLng(minX, maxY, minZ),
latLng(maxX, maxY, minZ),
latLng(maxX, minY, minZ)
], [
latLng(maxX, minY, minZ),
latLng(maxX, maxY, minZ),
latLng(maxX, maxY, maxZ),
latLng(maxX, minY, maxZ)
], [
latLng(minX, minY, maxZ),
latLng(minX, maxY, maxZ),
latLng(maxX, maxY, maxZ),
latLng(maxX, minY, maxZ)
], [
latLng(minX, minY, minZ),
latLng(minX, maxY, minZ),
latLng(minX, maxY, maxZ),
latLng(minX, minY, maxZ)
]
];
},
get2DBoxPoints(options: DynmapArea, latLng: 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 [
latLng(minX, minY, minZ),
latLng(maxX, minY, minZ),
latLng(maxX, minY, maxZ),
latLng(minX, minY, maxZ),
latLng(minX, minY, minZ)
];
} else {
return [
latLng(minX, minY, minZ),
latLng(maxX, minY, minZ),
latLng(maxX, minY, maxZ),
latLng(minX, minY, maxZ)
];
}
},
get3DShapePoints(options: DynmapArea, latLng: Function): LatLngExpression[][] {
const toplist = [],
botlist = [],
polylist = [];
for (let i = 0; i < options.x.length; i++) {
toplist[i] = latLng(options.x[i], options.y[0], options.z[i]);
botlist[i] = latLng(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;
},
get2DShapePoints(options: DynmapArea, latLng: Function, outline: boolean): LatLngExpression[] {
const points = [];
for (let i = 0; i < options.x.length; i++) {
points[i] = latLng(options.x[i], options.y[1], options.z[i]);
}
if (outline) {
points.push(points[0]);
}
return points;
}
} }
}) });
</script> </script>
<style scoped>
</style>

View File

@ -1,140 +1,111 @@
<script lang="ts"> <script lang="ts">
import {defineComponent, computed} from "@vue/runtime-core"; import {defineComponent, computed, onMounted, onUnmounted, watch} from "@vue/runtime-core";
import L, {LatLngExpression} from 'leaflet'; import {Polyline, LayerGroup, Polygon} from 'leaflet';
import {useStore} from "@/store"; import {useStore} from "@/store";
import {DynmapCircle} from "@/dynmap"; import {DynmapCircle, DynmapMarkerSet} from "@/dynmap";
import {ActionTypes} from "@/store/action-types";
import {createCircle, updateCircle} from "@/util/circles";
import Util from '@/util';
export default defineComponent({ export default defineComponent({
props: { props: {
circles: { set: {
type: Object as () => Map<string, DynmapCircle>, type: Object as () => DynmapMarkerSet,
required: true required: true,
}, },
layerGroup: { layerGroup: {
type: Object as () => L.LayerGroup, type: Object as () => LayerGroup,
required: true required: true
} }
}, },
setup() { setup(props) {
let updateFrame = 0;
const store = useStore(), const store = useStore(),
currentProjection = computed(() => store.state.currentProjection), currentProjection = computed(() => store.state.currentProjection),
layers = Object.freeze(new Map()) as Map<string, L.Path>; pendingUpdates = computed(() => {
const markerSetUpdates = store.state.pendingSetUpdates.get(props.set.id);
return { return markerSetUpdates && markerSetUpdates.circleUpdates.length;
layers, }),
currentProjection, layers = Object.freeze(new Map<string, Polyline | Polygon>()),
}
},
watch: { createCircles = () => {
//FIXME: Prevent unnecessary repositioning when changing worlds const converter = Util.getPointConverter();
currentProjection() {
const projection = useStore().state.currentProjection, props.set.circles.forEach((circle: DynmapCircle, id: string) => {
latLng = (x: number, y: number, z: number) => { const layer = createCircle(circle, converter);
return projection.locationToLatLng({x, y, z});
layers.set(id, layer);
props.layerGroup.addLayer(layer);
});
},
deleteCircle = (id: string) => {
let circle = layers.get(id) as Polyline;
if (!circle) {
return;
} }
// eslint-disable-next-line no-unused-vars circle.remove();
for (const [id, circle] of this.circles) { layers.delete(id);
this.updateCircle(id, circle, latLng); },
handlePendingUpdates = () => {
useStore().dispatch(ActionTypes.POP_CIRCLE_UPDATES, {
markerSet: props.set.id,
amount: 10,
}).then(updates => {
const converter = Util.getPointConverter();
for(const update of updates) {
if(update.removed) {
console.log(`Deleting circle ${update.id}`);
deleteCircle(update.id);
} else {
console.log(`Updating/creating circle ${update.id}`);
const layer = updateCircle(layers.get(update.id), update.payload as DynmapCircle, converter)
if(!layers.has(update.id)) {
layers.set(update.id, layer);
props.layerGroup.addLayer(layer);
}
}
}
if(pendingUpdates.value) {
console.log('More updates left, scheduling frame');
// eslint-disable-next-line no-unused-vars
updateFrame = requestAnimationFrame(() => handlePendingUpdates());
} else {
updateFrame = 0;
}
});
};
//FIXME: Prevent unnecessary repositioning when changing worlds
watch(currentProjection, () => {
const converter = Util.getPointConverter();
for (const [id, circle] of props.set.circles) {
updateCircle(layers.get(id), circle, converter);
} }
} });
},
mounted() { watch(pendingUpdates, (newValue, oldValue) => {
this.createCircles(); if(newValue && newValue > 0 && oldValue === 0 && !updateFrame) {
}, updateFrame = requestAnimationFrame(() => handlePendingUpdates());
}
unmounted() { });
onMounted(() => createCircles());
onUnmounted(() => updateFrame && cancelAnimationFrame(updateFrame));
}, },
render() { render() {
return null; return null;
},
methods: {
createCircles() {
const projection = useStore().state.currentProjection,
latLng = (x: number, y: number, z: number) => {
return projection.locationToLatLng({x, y, z});
};
this.circles.forEach((circle: DynmapCircle, id: string) => {
this.createCircle(id, circle, latLng);
});
},
createCircle(id: string, options: DynmapCircle, latLng: Function) {
const outline = !options.style.fillOpacity || (options.style.fillOpacity <= 0),
points = this.getPoints(options, latLng, outline);
let circle;
if(outline) {
circle = new L.Polyline(points, options.style);
} else {
circle = new L.Polygon(points, options.style);
}
if(options.label) {
circle.bindPopup(() => {
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;
});
}
this.layers.set(id, circle);
this.layerGroup.addLayer(circle);
},
getPoints(options: DynmapCircle, latLng: 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;
console.log(x,options.location.y,z, latLng(x, options.location.y, z));
points.push(latLng(x, options.location.y, z));
}
if(outline && points.length) {
points.push(points[0]);
}
return points;
},
updateCircle(id: string, options: DynmapCircle, latLng: Function) {
let circle = this.layers.get(id) as L.Polyline,
outline = (options.style && options.style.fillOpacity && (options.style.fillOpacity <= 0)) as boolean,
points = this.getPoints(options, latLng, outline);
if (!circle) {
return;
}
circle.setLatLngs(points);
circle.redraw();
},
} }
}) });
</script> </script>
<style scoped>
</style>

View File

@ -1,122 +1,111 @@
<script lang="ts"> <script lang="ts">
import {defineComponent, computed} from "@vue/runtime-core"; import {defineComponent, computed, onMounted, onUnmounted, watch} from "@vue/runtime-core";
import L, {LatLngExpression} from 'leaflet'; import {Polyline, LayerGroup, Polygon} from 'leaflet';
import {useStore} from "@/store"; import {useStore} from "@/store";
import {DynmapLine} from "@/dynmap"; import {DynmapLine, DynmapMarkerSet} from "@/dynmap";
import {ActionTypes} from "@/store/action-types";
import {createLine, updateLine} from "@/util/lines";
import Util from '@/util';
export default defineComponent({ export default defineComponent({
props: { props: {
lines: { set: {
type: Object as () => Map<string, DynmapLine>, type: Object as () => DynmapMarkerSet,
required: true required: true,
}, },
layerGroup: { layerGroup: {
type: Object as () => L.LayerGroup, type: Object as () => LayerGroup,
required: true required: true
} }
}, },
setup() { setup(props) {
let updateFrame = 0;
const store = useStore(), const store = useStore(),
currentProjection = computed(() => store.state.currentProjection), currentProjection = computed(() => store.state.currentProjection),
layers = Object.freeze(new Map()) as Map<string, L.Path>; pendingUpdates = computed(() => {
const markerSetUpdates = store.state.pendingSetUpdates.get(props.set.id);
return { return markerSetUpdates && markerSetUpdates.lineUpdates.length;
layers, }),
currentProjection, layers = Object.freeze(new Map<string, Polyline | Polygon>()),
}
},
watch: { createLines = () => {
//FIXME: Prevent unnecessary repositioning when changing worlds const converter = Util.getPointConverter();
currentProjection() {
const projection = useStore().state.currentProjection, props.set.lines.forEach((line: DynmapLine, id: string) => {
latLng = (x: number, y: number, z: number) => { const layer = createLine(line, converter);
return projection.locationToLatLng({x, y, z});
layers.set(id, layer);
props.layerGroup.addLayer(layer);
});
},
deleteLine = (id: string) => {
let line = layers.get(id) as Polyline;
if (!line) {
return;
} }
// eslint-disable-next-line no-unused-vars line.remove();
for (const [id, line] of this.lines) { layers.delete(id);
this.updateLine(id, line, latLng); },
handlePendingUpdates = () => {
useStore().dispatch(ActionTypes.POP_LINE_UPDATES, {
markerSet: props.set.id,
amount: 10,
}).then(updates => {
const converter = Util.getPointConverter();
for(const update of updates) {
if(update.removed) {
console.log(`Deleting line ${update.id}`);
deleteLine(update.id);
} else {
console.log(`Updating/creating line ${update.id}`);
const layer = updateLine(layers.get(update.id), update.payload as DynmapLine, converter)
if(!layers.has(update.id)) {
layers.set(update.id, layer);
props.layerGroup.addLayer(layer);
}
}
}
if(pendingUpdates.value) {
console.log('More updates left, scheduling frame');
// eslint-disable-next-line no-unused-vars
updateFrame = requestAnimationFrame(() => handlePendingUpdates());
} else {
updateFrame = 0;
}
});
};
//FIXME: Prevent unnecessary repositioning when changing worlds
watch(currentProjection, () => {
const converter = Util.getPointConverter();
for (const [id, line] of props.set.lines) {
updateLine(layers.get(id), line, converter);
} }
} });
},
mounted() { watch(pendingUpdates, (newValue, oldValue) => {
this.createLines(); if(newValue && newValue > 0 && oldValue === 0 && !updateFrame) {
}, updateFrame = requestAnimationFrame(() => handlePendingUpdates());
}
unmounted() { });
onMounted(() => createLines());
onUnmounted(() => updateFrame && cancelAnimationFrame(updateFrame));
}, },
render() { render() {
return null; return null;
},
methods: {
createLines() {
const projection = useStore().state.currentProjection,
latLng = (x: number, y: number, z: number) => {
return projection.locationToLatLng({x, y, z});
};
this.lines.forEach((line: DynmapLine, id: string) => {
this.createLine(id, line, latLng);
});
},
createLine(id: string, options: DynmapLine, latLng: Function) {
const points = this.getPoints(options, latLng),
line= new L.Polyline(points, options.style);
if(options.label) {
line.bindPopup(() => {
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;
});
}
this.layers.set(id, line);
this.layerGroup.addLayer(line);
},
getPoints(options: DynmapLine, latLng: Function): LatLngExpression[] {
const points = [];
for(let i = 0; i < options.x.length; i++) {
points.push(latLng(options.x[i], options.y[i], options.z[i]));
}
return points;
},
updateLine(id: string, options: DynmapLine, latLng: Function) {
let line = this.layers.get(id) as L.Polyline,
points = this.getPoints(options, latLng);
if (!line) {
return;
}
line.setLatLngs(points);
line.redraw();
},
} }
}) });
</script> </script>
<style scoped>
</style>

38
src/dynmap.d.ts vendored
View File

@ -139,8 +139,8 @@ interface DynmapUpdateResponse {
configHash: number; configHash: number;
playerCount: number; playerCount: number;
players: Set<DynmapPlayer>; players: Set<DynmapPlayer>;
updates: DynmapUpdates;
timestamp: number; timestamp: number;
//TODO: Tiles etc
} }
interface DynmapPlayer { interface DynmapPlayer {
@ -153,6 +153,7 @@ interface DynmapPlayer {
} }
interface DynmapMarkerSet { interface DynmapMarkerSet {
id: string,
label: string; label: string;
hidden: boolean; hidden: boolean;
priority: number; priority: number;
@ -210,3 +211,38 @@ interface DynmapCircle {
maxZoom?: number; maxZoom?: number;
popupContent?: string; popupContent?: string;
} }
interface DynmapUpdates {
markerSets: Map<string, DynmapMarkerSetUpdates>,
tiles: Map<string, number>,
chat: Array<any> //TODO
}
interface DynmapMarkerSetUpdates {
markerUpdates: Array<DynmapMarkerUpdate>
areaUpdates: Array<DynmapAreaUpdate>
circleUpdates: Array<DynmapCircleUpdate>
lineUpdates: Array<DynmapLineUpdate>
}
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
}

View File

@ -3,4 +3,8 @@ export enum ActionTypes {
GET_UPDATE = "getUpdate", GET_UPDATE = "getUpdate",
GET_MARKER_SETS = "getMarkerSets", GET_MARKER_SETS = "getMarkerSets",
SET_PLAYERS = "setPlayers", SET_PLAYERS = "setPlayers",
POP_MARKER_UPDATES = "popMarkerUpdates",
POP_AREA_UPDATES = "popAreaUpdates",
POP_CIRCLE_UPDATES = "popCircleUpdates",
POP_LINE_UPDATES = "popLineUpdates",
} }

View File

@ -4,7 +4,14 @@ import {State} from "@/store/state";
import {ActionTypes} from "@/store/action-types"; import {ActionTypes} from "@/store/action-types";
import API from '@/api'; import API from '@/api';
import {Mutations} from "@/store/mutations"; 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 = { type AugmentedActionContext = {
commit<K extends keyof Mutations>( commit<K extends keyof Mutations>(
@ -27,6 +34,22 @@ export interface Actions {
{commit}: AugmentedActionContext, {commit}: AugmentedActionContext,
payload: Set<DynmapPlayer> payload: Set<DynmapPlayer>
):Promise<Map<string, DynmapMarkerSet>> ):Promise<Map<string, DynmapMarkerSet>>
[ActionTypes.POP_MARKER_UPDATES](
{commit}: AugmentedActionContext,
payload: {markerSet: string, amount: number}
): Promise<DynmapMarkerUpdate[]>
[ActionTypes.POP_AREA_UPDATES](
{commit}: AugmentedActionContext,
payload: {markerSet: string, amount: number}
): Promise<DynmapAreaUpdate[]>
[ActionTypes.POP_CIRCLE_UPDATES](
{commit}: AugmentedActionContext,
payload: {markerSet: string, amount: number}
): Promise<DynmapCircleUpdate[]>
[ActionTypes.POP_LINE_UPDATES](
{commit}: AugmentedActionContext,
payload: {markerSet: string, amount: number}
): Promise<DynmapLineUpdate[]>
} }
export const actions: ActionTree<State, State> & Actions = { export const actions: ActionTree<State, State> & Actions = {
@ -52,10 +75,11 @@ export const actions: ActionTree<State, State> & Actions = {
return Promise.reject("No current world"); 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_WORLD_STATE, update.worldState);
commit(MutationTypes.SET_UPDATE_TIMESTAMP, new Date(update.timestamp)); commit(MutationTypes.SET_UPDATE_TIMESTAMP, new Date(update.timestamp));
commit(MutationTypes.INCREMENT_REQUEST_ID, undefined); commit(MutationTypes.INCREMENT_REQUEST_ID, undefined);
commit(MutationTypes.ADD_MARKER_SET_UPDATES, update.updates.markerSets);
return dispatch(ActionTypes.SET_PLAYERS, update.players).then(() => { return dispatch(ActionTypes.SET_PLAYERS, update.players).then(() => {
return update; return update;
@ -97,5 +121,58 @@ export const actions: ActionTree<State, State> & Actions = {
return markerSets; return markerSets;
}); });
} },
[ActionTypes.POP_MARKER_UPDATES]({commit, state}, {markerSet, amount}: {markerSet: string, amount: number}): Promise<DynmapMarkerUpdate[]> {
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<DynmapAreaUpdate[]> {
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<DynmapCircleUpdate[]> {
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<DynmapLineUpdate[]> {
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);
},
} }

View File

@ -7,6 +7,11 @@ export enum MutationTypes {
ADD_WORLD = 'addWorld', ADD_WORLD = 'addWorld',
SET_WORLD_STATE = 'setWorldState', SET_WORLD_STATE = 'setWorldState',
SET_UPDATE_TIMESTAMP = 'setUpdateTimestamp', 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', INCREMENT_REQUEST_ID = 'incrementRequestId',
SET_PLAYERS = 'setPlayers', SET_PLAYERS = 'setPlayers',
SET_PLAYERS_ASYNC = 'setPlayersAsync', SET_PLAYERS_ASYNC = 'setPlayersAsync',

View File

@ -2,7 +2,17 @@ import {MutationTree} from "vuex";
import {MutationTypes} from "@/store/mutation-types"; import {MutationTypes} from "@/store/mutation-types";
import {State} from "@/store/state"; import {State} from "@/store/state";
import { import {
DynmapComponentConfig, DynmapMarkerSet, DynmapArea,
DynmapAreaUpdate,
DynmapCircle,
DynmapCircleUpdate,
DynmapComponentConfig,
DynmapLine,
DynmapLineUpdate,
DynmapMarker,
DynmapMarkerSet,
DynmapMarkerSetUpdates,
DynmapMarkerUpdate,
DynmapMessageConfig, DynmapMessageConfig,
DynmapPlayer, DynmapPlayer,
DynmapServerConfig, DynmapServerConfig,
@ -25,9 +35,15 @@ export type Mutations<S = State> = {
[MutationTypes.ADD_WORLD](state: S, world: DynmapWorld): void [MutationTypes.ADD_WORLD](state: S, world: DynmapWorld): void
[MutationTypes.SET_WORLD_STATE](state: S, worldState: DynmapWorldState): void [MutationTypes.SET_WORLD_STATE](state: S, worldState: DynmapWorldState): void
[MutationTypes.SET_UPDATE_TIMESTAMP](state: S, time: Date): void [MutationTypes.SET_UPDATE_TIMESTAMP](state: S, time: Date): void
[MutationTypes.ADD_MARKER_SET_UPDATES](state: S, updates: Map<string, DynmapMarkerSetUpdates>): void
[MutationTypes.POP_MARKER_UPDATES](state: S, payload: {markerSet: string, amount: number}): Array<DynmapMarkerUpdate>
[MutationTypes.POP_AREA_UPDATES](state: S, payload: {markerSet: string, amount: number}): Array<DynmapAreaUpdate>
[MutationTypes.POP_CIRCLE_UPDATES](state: S, payload: {markerSet: string, amount: number}): Array<DynmapCircleUpdate>
[MutationTypes.POP_LINE_UPDATES](state: S, payload: {markerSet: string, amount: number}): Array<DynmapLineUpdate>
[MutationTypes.INCREMENT_REQUEST_ID](state: S): void [MutationTypes.INCREMENT_REQUEST_ID](state: S): void
// [MutationTypes.SET_PLAYERS](state: S, players: Array<DynmapPlayer>): void [MutationTypes.SET_PLAYERS_ASYNC](state: S, players: Set<DynmapPlayer>): Set<DynmapPlayer>
[MutationTypes.SET_PLAYERS_ASYNC](state: S, players: Set<DynmapPlayer>): void
[MutationTypes.SYNC_PLAYERS](state: S, keep: Set<string>): void [MutationTypes.SYNC_PLAYERS](state: S, keep: Set<string>): void
[MutationTypes.SET_CURRENT_MAP](state: S, payload: CurrentMapPayload): void [MutationTypes.SET_CURRENT_MAP](state: S, payload: CurrentMapPayload): void
[MutationTypes.SET_CURRENT_PROJECTION](state: S, payload: DynmapProjection): void [MutationTypes.SET_CURRENT_PROJECTION](state: S, payload: DynmapProjection): void
@ -73,6 +89,16 @@ export const mutations: MutationTree<State> & Mutations = {
//Sets the existing marker sets from the last marker fetch //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, DynmapMarkerSet>) {
state.markerSets = markerSets; 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) { [MutationTypes.ADD_WORLD](state: State, world: DynmapWorld) {
@ -89,46 +115,101 @@ export const mutations: MutationTree<State> & Mutations = {
state.updateTimestamp = timestamp; state.updateTimestamp = timestamp;
}, },
//Sets the timestamp of the last update fetch
[MutationTypes.ADD_MARKER_SET_UPDATES](state: State, updates: Map<string, DynmapMarkerSetUpdates>) {
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<DynmapMarkerUpdate> {
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<DynmapAreaUpdate> {
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<DynmapCircleUpdate> {
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<DynmapLineUpdate> {
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 //Increments the request id for the next update fetch
[MutationTypes.INCREMENT_REQUEST_ID](state: State) { [MutationTypes.INCREMENT_REQUEST_ID](state: State) {
state.updateRequestId++; state.updateRequestId++;
}, },
// [MutationTypes.SET_PLAYERS](state: State, players: Array<DynmapPlayer>) { // Set up to 10 players at once
// const existingPlayers: Set<string> = new Set(); [MutationTypes.SET_PLAYERS_ASYNC](state: State, players: Set<DynmapPlayer>): Set<DynmapPlayer> {
//
// 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<DynmapPlayer>) {
let count = 0; let count = 0;
for(const player of players) { for(const player of players) {
@ -154,9 +235,11 @@ export const mutations: MutationTree<State> & Mutations = {
players.delete(player); players.delete(player);
if(++count >= 10) { if(++count >= 10) {
return players; break;
} }
} }
return players;
}, },
//Removes all players not found in the provided keep set //Removes all players not found in the provided keep set
@ -184,6 +267,8 @@ export const mutations: MutationTree<State> & Mutations = {
if(state.currentWorld !== newWorld) { if(state.currentWorld !== newWorld) {
state.currentWorld = state.worlds.get(worldName); state.currentWorld = state.worlds.get(worldName);
state.markerSets.clear(); state.markerSets.clear();
state.pendingSetUpdates.clear();
state.pendingTileUpdates = [];
} }
state.currentMap = state.maps.get(mapName); state.currentMap = state.maps.get(mapName);

View File

@ -1,6 +1,6 @@
import { import {
DynmapComponentConfig, DynmapComponentConfig,
DynmapMap, DynmapMarker, DynmapMarkerSet, DynmapMap, DynmapMarkerSet, DynmapMarkerSetUpdates,
DynmapMessageConfig, DynmapMessageConfig,
DynmapPlayer, DynmapPlayer,
DynmapServerConfig, DynmapServerConfig,
@ -18,9 +18,11 @@ export type State = {
players: Map<string, DynmapPlayer>; players: Map<string, DynmapPlayer>;
markerSets: Map<string, DynmapMarkerSet>; markerSets: Map<string, DynmapMarkerSet>;
pendingSetUpdates: Map<string, DynmapMarkerSetUpdates>;
pendingTileUpdates: Array<string>;
following?: DynmapPlayer; following?: DynmapPlayer;
// currentServer?: string;
currentWorldState: DynmapWorldState; currentWorldState: DynmapWorldState;
currentWorld?: DynmapWorld; currentWorld?: DynmapWorld;
currentMap?: DynmapMap; currentMap?: DynmapMap;
@ -65,7 +67,10 @@ export const state: State = {
worlds: new Map(), //Defined (loaded) worlds with maps from configuration.json worlds: new Map(), //Defined (loaded) worlds with maps from configuration.json
maps: new Map(), //Defined maps from configuration.json maps: new Map(), //Defined maps from configuration.json
players: new Map(), //Online players from world.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. 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 //Dynmap optional components
components: { components: {
@ -73,6 +78,7 @@ export const state: State = {
markers: { markers: {
showLabels: false, showLabels: false,
}, },
// Optional "playermarkers" component. Settings for online player markers. // Optional "playermarkers" component. Settings for online player markers.
// If not present, player markers will be disabled // If not present, player markers will be disabled
playerMarkers: undefined, playerMarkers: undefined,
@ -83,22 +89,23 @@ export const state: State = {
//Optional clock component. Used for both "digitalclock" and "timeofdayclock". Shows world time/weather. //Optional clock component. Used for both "digitalclock" and "timeofdayclock". Shows world time/weather.
clockControl: undefined, 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, linkControl: false,
//Optional "logo" controls.
logoControls: [], logoControls: [],
}, },
following: undefined, following: undefined,
currentWorld: undefined, currentWorld: undefined,
currentMap: undefined,
currentProjection: new DynmapProjection(), //Projection for converting location <-> latlg. Object itself isn't reactive for performance reasons
currentWorldState: { currentWorldState: {
raining: false, raining: false,
thundering: false, thundering: false,
timeOfDay: 0, timeOfDay: 0,
}, },
currentMap: undefined,
currentProjection: new DynmapProjection(), //Projection for converting location <-> latlg. Object itself isn't reactive for performance reasons
updateRequestId: 0, updateRequestId: 0,
updateTimestamp: new Date(), updateTimestamp: new Date(),

View File

@ -1,4 +1,5 @@
import {DynmapPlayer} from "@/dynmap"; import {DynmapPlayer} from "@/dynmap";
import {useStore} from "@/store";
const headCache = new Map<DynmapPlayer, HTMLImageElement>(); const headCache = new Map<DynmapPlayer, HTMLImageElement>();
@ -49,5 +50,13 @@ export default {
} }
return base + addition; return base + addition;
},
getPointConverter() {
const projection = useStore().state.currentProjection;
return (x: number, y: number, z: number) => {
return projection.locationToLatLng({x, y, z});
};
} }
} }

170
src/util/areas.ts Normal file
View File

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

71
src/util/circles.ts Normal file
View File

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

55
src/util/lines.ts Normal file
View File

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