Migrate LayerControl to vue

This commit is contained in:
James Lyne 2022-06-25 21:29:33 +01:00
parent 4887acb917
commit 14674b774e
18 changed files with 392 additions and 404 deletions

View File

@ -39,6 +39,7 @@ import {MutationTypes} from "@/store/mutation-types";
import LiveAtlasLeafletMap from "@/leaflet/LiveAtlasLeafletMap"; import LiveAtlasLeafletMap from "@/leaflet/LiveAtlasLeafletMap";
import {LiveAtlasLocation, LiveAtlasPlayer, LiveAtlasMapViewTarget} from "@/index"; import {LiveAtlasLocation, LiveAtlasPlayer, LiveAtlasMapViewTarget} from "@/index";
import TileLayerOverlay from "@/components/map/layer/TileLayerOverlay.vue"; import TileLayerOverlay from "@/components/map/layer/TileLayerOverlay.vue";
import {ActionTypes} from "@/store/action-types";
export default defineComponent({ export default defineComponent({
components: { components: {
@ -69,6 +70,7 @@ export default defineComponent({
//Location and zoom to pan to upon next projection change //Location and zoom to pan to upon next projection change
scheduledView = ref<LiveAtlasMapViewTarget|null>(null), scheduledView = ref<LiveAtlasMapViewTarget|null>(null),
pendingLayerUpdates = computed(() => !!store.state.pendingLayerUpdates.size),
mapTitle = computed(() => store.state.messages.mapTitle); mapTitle = computed(() => store.state.messages.mapTitle);
@ -90,6 +92,7 @@ export default defineComponent({
currentMap, currentMap,
scheduledView, scheduledView,
pendingLayerUpdates,
mapTitle mapTitle
} }
@ -190,6 +193,21 @@ export default defineComponent({
this.scheduledView = viewTarget; this.scheduledView = viewTarget;
} }
}, },
async pendingLayerUpdates(size) {
const store = useStore();
if(size) {
const updates = await store.dispatch(ActionTypes.POP_LAYER_UPDATES, undefined);
for (const update of updates) {
if(update[1]) {
this.leaflet.addLayer(update[0]);
} else {
this.leaflet.removeLayer(update[0]);
}
}
}
},
parsedUrl: { parsedUrl: {
handler(newValue) { handler(newValue) {
if(!newValue || !this.currentMap || !this.leaflet) { if(!newValue || !this.currentMap || !this.leaflet) {

View File

@ -24,6 +24,7 @@
<div class="ui__toolbar toolbar--vertical"> <div class="ui__toolbar toolbar--vertical">
<LogoControl v-for="logo in logoControls" :key="JSON.stringify(logo)" :options="logo"></LogoControl> <LogoControl v-for="logo in logoControls" :key="JSON.stringify(logo)" :options="logo"></LogoControl>
<ZoomControl :leaflet="leaflet"></ZoomControl> <ZoomControl :leaflet="leaflet"></ZoomControl>
<LayerControl></LayerControl>
<LoadingControl :leaflet="leaflet" :delay="500"></LoadingControl> <LoadingControl :leaflet="leaflet" :delay="500"></LoadingControl>
</div> </div>
</div> </div>
@ -61,6 +62,7 @@ import LoginControl from "@/components/map/control/LoginControl.vue";
import LiveAtlasLeafletMap from "@/leaflet/LiveAtlasLeafletMap"; import LiveAtlasLeafletMap from "@/leaflet/LiveAtlasLeafletMap";
import LoadingControl from "@/components/map/control/LoadingControl.vue"; import LoadingControl from "@/components/map/control/LoadingControl.vue";
import ZoomControl from "@/components/map/control/ZoomControl.vue"; import ZoomControl from "@/components/map/control/ZoomControl.vue";
import LayerControl from "@/components/map/control/LayerControl.vue";
export default defineComponent({ export default defineComponent({
props: { props: {
@ -71,6 +73,7 @@ export default defineComponent({
}, },
components: { components: {
LayerControl,
ZoomControl, ZoomControl,
LoadingControl, LoadingControl,
LogoControl, LogoControl,

View File

@ -31,6 +31,7 @@ import LiveAtlasLeafletMap from "@/leaflet/LiveAtlasLeafletMap";
import {onMounted, ref} from "vue"; import {onMounted, ref} from "vue";
import {Coordinate, CoordinatesControlOptions} from "@/index"; import {Coordinate, CoordinatesControlOptions} from "@/index";
import {LeafletMouseEvent} from "leaflet"; import {LeafletMouseEvent} from "leaflet";
import {CoordinatesControlOptions} from "@/leaflet/control/CoordinatesControl";
export default defineComponent({ export default defineComponent({
props: { props: {
@ -42,7 +43,7 @@ export default defineComponent({
setup(props) { setup(props) {
const store = useStore(), const store = useStore(),
componentSettings = computed(() => store.state.components.coordinatesControl), componentSettings = computed(() => store.state.components.coordinatesControl as CoordinatesControlOptions),
currentMap = computed(() => store.state.currentMap), currentMap = computed(() => store.state.currentMap),
chunkLabel = computed(() => store.state.messages.locationChunk), chunkLabel = computed(() => store.state.messages.locationChunk),

View File

@ -0,0 +1,179 @@
<!--
- Copyright 2022 James Lyne
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
-->
<template>
<div class="layers">
<button ref="button" type="button" class="ui__element ui__button" title="Layers" :aria-expanded="listVisible"
@click.prevent="toggleList"
@keydown.right.prevent.stop="toggleList">
<SvgIcon name="layers"></SvgIcon>
</button>
<section ref="list" :hidden="!listVisible" class="ui__element ui__panel layers__list"
:style="listStyle" @keydown="handleListKeydown">
<div class="layers__base"></div>
<div class="layers__overlays">
<label v-for="layer in overlayLayers" :key="stamp(layer.layer)" class="layer checkbox">
<input type="checkbox" :checked="layer.enabled" @keydown.space.prevent="toggleLayer(layer.layer)"
@input.prevent="toggleLayer(layer.layer)">
<SvgIcon name="checkbox"></SvgIcon>
<span>{{ layer.name }}</span>
</label>
</div>
</section>
</div>
</template>
<script lang="ts">
import SvgIcon from "@/components/SvgIcon.vue";
import {computed, defineComponent, onUnmounted, watch} from "@vue/runtime-core";
import {useStore} from "@/store";
import {MutationTypes} from "@/store/mutation-types";
import '@/assets/icons/layers.svg';
import '@/assets/icons/checkbox.svg';
import {stamp} from "leaflet";
import {toggleLayer} from "@/util/layers";
import {nextTick, onMounted, ref} from "vue";
import {handleKeyboardEvent} from "@/util/events";
export default defineComponent({
components: {SvgIcon},
setup() {
const store = useStore(),
overlayLayers = computed(() => store.state.sortedLayers.filter(layer => layer.overlay)),
baseLayers = computed(() => store.state.sortedLayers.filter(layer => !layer.overlay)),
listVisible = computed(() => store.state.ui.visibleElements.has('layers')),
listStyle = ref({'max-height': 'auto'}),
button = ref<HTMLButtonElement|null>(null),
list = ref<HTMLElement|null>(null);
const toggleList = () => listVisible.value ? closeList() : openList();
const openList = () =>
store.commit(MutationTypes.SET_UI_ELEMENT_VISIBILITY, {element: 'layers', state: true});
const closeList = () =>
store.commit(MutationTypes.SET_UI_ELEMENT_VISIBILITY, {element: 'layers', state: false});
const handleListKeydown = (event: KeyboardEvent) => {
if(event.key === 'ArrowLeft') {
closeList();
event.preventDefault();
return;
}
const elements = Array.from((list.value as HTMLElement).querySelectorAll('input')) as HTMLElement[];
handleKeyboardEvent(event as KeyboardEvent, elements);
}
const handleResize = () => {
const y = button.value!.getBoundingClientRect().y;
//Limit height to remaining vertical space
//Avoid covering bottom bar
listStyle.value['max-height'] = `calc(100vh - ${(y + 10 + 60)}px)`;
};
watch(listVisible, visible => {
if(visible) {
const firstCheckbox = (list.value as HTMLElement).querySelector('.checkbox');
if(firstCheckbox instanceof HTMLElement) {
nextTick(() => firstCheckbox.focus());
}
} else {
nextTick(() => (button.value as HTMLButtonElement).focus());
}
});
onMounted(() => {
window.addEventListener('resize', handleResize);
handleResize();
});
onUnmounted(() => window.addEventListener('resize', handleResize));
return {
overlayLayers,
baseLayers,
listVisible,
listStyle,
button,
list,
toggleList,
openList,
closeList,
handleListKeydown,
toggleLayer,
stamp
}
}
})
</script>
<style lang="scss" scoped>
@import '../../../scss/placeholders';
.layers {
width: auto;
border: none;
color: var(--text-base);
position: relative;
.layers__list[hidden] {
display: none;
}
.layers__list {
@extend %panel;
position: absolute;
top: 0;
left: calc(var(--ui-element-spacing) + var(--ui-button-size));
overflow: auto;
max-width: calc(100vw - 14rem);
box-sizing: border-box;
font-size: 1.5rem;
line-height: 1;
pointer-events: auto;
@media screen and (max-width: 400px) {
max-width: calc(100vw - 13rem);
}
.layers__overlays {
width: 100%;
max-width: 30rem;
}
.layer {
cursor: pointer;
padding: 0.8rem 0 0.7rem;
&:first-child {
margin-top: -0.4rem;
}
&:last-child {
margin-bottom: -0.4rem;
}
}
}
}
</style>

View File

@ -21,11 +21,11 @@
<script lang="ts"> <script lang="ts">
import {defineComponent, computed, onMounted, onUnmounted} from "@vue/runtime-core"; import {defineComponent, computed, onMounted, onUnmounted} from "@vue/runtime-core";
import {useStore} from "@/store"; import {useStore} from "@/store";
import LiveAtlasLeafletMap from "@/leaflet/LiveAtlasLeafletMap";
import LiveAtlasLayerGroup from "@/leaflet/layer/LiveAtlasLayerGroup"; import LiveAtlasLayerGroup from "@/leaflet/layer/LiveAtlasLayerGroup";
import {LiveAtlasMarkerSet} from "@/index"; import {LiveAtlasMarkerSet} from "@/index";
import {watch} from "vue"; import {markRaw, watch} from "vue";
import Markers from "@/components/map/marker/Markers.vue"; import Markers from "@/components/map/marker/Markers.vue";
import {MutationTypes} from "@/store/mutation-types";
export default defineComponent({ export default defineComponent({
components: { components: {
@ -33,11 +33,6 @@ export default defineComponent({
}, },
props: { props: {
leaflet: {
type: Object as () => LiveAtlasLeafletMap,
required: true,
},
markerSet: { markerSet: {
type: Object as () => LiveAtlasMarkerSet, type: Object as () => LiveAtlasMarkerSet,
required: true, required: true,
@ -65,27 +60,25 @@ export default defineComponent({
priority: props.markerSet.priority, priority: props.markerSet.priority,
}); });
if(newValue.hidden) { // store.commit(MutationTypes.UPDATE_LAYER, {
props.leaflet.getLayerManager() // layer: layerGroup,
.addHiddenLayer(layerGroup, newValue.label, props.markerSet.priority); // options: {enabled: newValue.hidden}
} else { // });
props.leaflet.getLayerManager()
.addLayer(layerGroup, true, newValue.label, props.markerSet.priority);
}
} }
}, {deep: true}); }, {deep: true});
onMounted(() => { onMounted(() => {
if(props.markerSet.hidden) { store.commit(MutationTypes.ADD_LAYER, {
props.leaflet.getLayerManager() layer: markRaw(layerGroup),
.addHiddenLayer(layerGroup, props.markerSet.label, props.markerSet.priority); name: props.markerSet.label,
} else { overlay: true,
props.leaflet.getLayerManager() position: props.markerSet.priority || 0,
.addLayer(layerGroup, true, props.markerSet.label, props.markerSet.priority); enabled: !props.markerSet.hidden,
} showInControl: true
});
}); });
onUnmounted(() => props.leaflet.getLayerManager().removeLayer(layerGroup)); onUnmounted(() => store.commit(MutationTypes.REMOVE_LAYER, layerGroup));
return { return {
markerSettings, markerSettings,

View File

@ -24,6 +24,8 @@ import {defineComponent, computed, watch, onMounted, onUnmounted} from "@vue/run
import {useStore} from "@/store"; import {useStore} from "@/store";
import {LayerGroup} from 'leaflet'; import {LayerGroup} from 'leaflet';
import LiveAtlasLeafletMap from "@/leaflet/LiveAtlasLeafletMap"; import LiveAtlasLeafletMap from "@/leaflet/LiveAtlasLeafletMap";
import {MutationTypes} from "@/store/mutation-types";
import {markRaw} from "vue";
export default defineComponent({ export default defineComponent({
components: { components: {
@ -51,21 +53,17 @@ export default defineComponent({
watch(playerCount, (newValue) => playerPane.classList.toggle('no-animations', newValue > 150)); watch(playerCount, (newValue) => playerPane.classList.toggle('no-animations', newValue > 150));
onMounted(() => { onMounted(() => {
if(!componentSettings.value!.hideByDefault) { store.commit(MutationTypes.ADD_LAYER, {
props.leaflet.getLayerManager().addLayer( layer: markRaw(layerGroup),
layerGroup, name: store.state.components.players.markers!.layerName,
true, overlay: true,
store.state.components.players.markers!.layerName, position: componentSettings.value!.layerPriority,
componentSettings.value!.layerPriority); enabled: !componentSettings.value!.hideByDefault,
} else { showInControl: true
props.leaflet.getLayerManager().addHiddenLayer( });
layerGroup,
store.state.components.players.markers!.layerName,
componentSettings.value!.layerPriority);
}
}); });
onUnmounted(() => props.leaflet.getLayerManager().removeLayer(layerGroup)); onUnmounted(() => store.commit(MutationTypes.REMOVE_LAYER, layerGroup));
if(playersAboveMarkers.value) { if(playersAboveMarkers.value) {
playerPane.style.zIndex = '600'; playerPane.style.zIndex = '600';

View File

@ -19,6 +19,8 @@ import {defineComponent, onUnmounted} from "@vue/runtime-core";
import {useStore} from "@/store"; import {useStore} from "@/store";
import LiveAtlasLeafletMap from "@/leaflet/LiveAtlasLeafletMap"; import LiveAtlasLeafletMap from "@/leaflet/LiveAtlasLeafletMap";
import {LiveAtlasTileLayerOverlay} from "@/index"; import {LiveAtlasTileLayerOverlay} from "@/index";
import {MutationTypes} from "@/store/mutation-types";
import {markRaw} from "vue";
export default defineComponent({ export default defineComponent({
props: { props: {
@ -37,10 +39,17 @@ export default defineComponent({
let layer = store.state.currentMapProvider!.createTileLayer(Object.freeze(JSON.parse(JSON.stringify(props.options.tileLayerOptions)))); let layer = store.state.currentMapProvider!.createTileLayer(Object.freeze(JSON.parse(JSON.stringify(props.options.tileLayerOptions))));
props.leaflet.getLayerManager().addHiddenLayer(layer, props.options.label, props.options.priority); store.commit(MutationTypes.ADD_LAYER, {
layer: markRaw(layer),
name: props.options.label,
overlay: true,
position: props.options.priority || 0,
enabled: false,
showInControl: true
});
onUnmounted(() => { onUnmounted(() => {
props.leaflet.getLayerManager().removeLayer(layer); store.commit(MutationTypes.REMOVE_LAYER, layer)
layer.remove(); layer.remove();
}); });
}, },

19
src/index.d.ts vendored
View File

@ -21,7 +21,7 @@ import {
ControlOptions, ControlOptions,
Coords, Coords,
DoneCallback, FitBoundsOptions, DoneCallback, FitBoundsOptions,
InternalTiles, LatLng, InternalTiles, LatLng, Layer,
PathOptions, PathOptions,
PointTuple, PointTuple,
PolylineOptions PolylineOptions
@ -171,6 +171,23 @@ interface LiveAtlasWorldState {
timeOfDay: number; timeOfDay: number;
} }
interface LiveAtlasLayerDefinition {
layer: Layer;
overlay: boolean;
name: string;
position: number;
enabled: boolean;
showInControl: boolean;
}
interface LiveAtlasPartialLayerDefinition {
overlay?: boolean;
name?: string;
position?: number;
enabled?: boolean;
showInControl?: boolean;
}
interface LiveAtlasParsedUrl { interface LiveAtlasParsedUrl {
world?: string; world?: string;
map?: string; map?: string;

View File

@ -15,23 +15,14 @@
*/ */
import {Map, DomUtil, MapOptions} from 'leaflet'; import {Map, DomUtil, MapOptions} from 'leaflet';
import LayerManager from "@/leaflet/layer/LayerManager";
export default class LiveAtlasLeafletMap extends Map { export default class LiveAtlasLeafletMap extends Map {
declare _controlCorners: any; declare _controlCorners: any;
declare _controlContainer?: HTMLElement; declare _controlContainer?: HTMLElement;
declare _container?: HTMLElement; declare _container?: HTMLElement;
private readonly _layerManager: LayerManager;
constructor(element: string | HTMLElement, options?: MapOptions) { constructor(element: string | HTMLElement, options?: MapOptions) {
super(element, options); super(element, options);
this._layerManager = Object.seal(new LayerManager(this));
}
getLayerManager(): LayerManager {
return this._layerManager;
} }
// noinspection JSUnusedGlobalSymbols // noinspection JSUnusedGlobalSymbols

View File

@ -1,235 +0,0 @@
/*
* Copyright 2022 James Lyne
*
* Some portions of this file were taken from https://github.com/webbukkit/dynmap.
* These portions are Copyright 2020 Dynmap Contributors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Control, DomEvent, DomUtil, Layer, LeafletEvent, Map as LeafletMap, Util} from 'leaflet';
import '@/assets/icons/layers.svg';
import '@/assets/icons/checkbox.svg';
import {useStore} from "@/store";
import {MutationTypes} from "@/store/mutation-types";
import {nextTick, watch} from "vue";
import {handleKeyboardEvent} from "@/util/events";
import LayersObject = Control.LayersObject;
import LayersOptions = Control.LayersOptions;
const store = useStore();
/**
* Extension of leaflet's standard {@link Control.Layers}
* Sorts layers by position, adds additional keyboard navigation, adjusts to viewport size and tracks expanded state in vuex
*/
export class LiveAtlasLayerControl extends Control.Layers {
declare _map ?: LeafletMap;
declare _overlaysList?: HTMLElement;
declare _baseLayersList?: HTMLElement;
declare _layerControlInputs?: HTMLElement[];
declare _container?: HTMLElement;
declare _section?: HTMLElement;
declare _separator?: HTMLElement;
private _layersButton?: HTMLElement;
private _layerPositions: Map<Layer, number>;
private visible: boolean = false;
constructor(baseLayers?: LayersObject, overlays?: LayersObject, options?: LayersOptions) {
// noinspection JSUnusedGlobalSymbols
super(baseLayers, overlays, Object.assign(options || {}, {
sortLayers: true,
sortFunction: (layer1: Layer, layer2: Layer, name1: string, name2: string) => {
const priority1 = this._layerPositions.get(layer1) || 0,
priority2 = this._layerPositions.get(layer2) || 0;
if(priority1 !== priority2) {
return priority1 - priority2;
}
return ((name1 < name2) ? -1 : ((name1 > name2) ? 1 : 0));
}
}));
this._layerPositions = new Map<Layer, number>();
}
hasLayer(layer: Layer): boolean {
// @ts-ignore
return !!super._getLayer(Util.stamp(layer));
}
expand() {
this._layersButton!.setAttribute('aria-expanded', 'true');
this._section!.style.display = '';
this.handleResize();
const firstCheckbox = this._container!.querySelector('input');
if(firstCheckbox) {
(firstCheckbox as HTMLElement).focus();
}
// @ts-ignore
super._checkDisabledLayers();
return this;
}
collapse() {
this._layersButton!.setAttribute('aria-expanded', 'false');
this._section!.style.display = 'none';
return this;
}
// noinspection JSUnusedGlobalSymbols
_initLayout() {
const className = 'leaflet-control-layers',
container = this._container = DomUtil.create('div', className),
section = this._section = DomUtil.create('section', className + '-list'),
button = this._layersButton = DomUtil.create('button', className + '-toggle', container);
DomEvent.disableClickPropagation(container);
DomEvent.disableScrollPropagation(container);
//Open layer list on ArrowRight from button
DomEvent.on(button,'keydown', (e: Event) => {
if((e as KeyboardEvent).key === 'ArrowRight') {
store.commit(MutationTypes.SET_UI_ELEMENT_VISIBILITY, {element: 'layers', state: true});
}
});
DomEvent.on(container, 'keydown', (e: Event) => {
//Close layer list on ArrowLeft from within list
if((e as KeyboardEvent).key === 'ArrowLeft') {
e.preventDefault();
store.commit(MutationTypes.SET_UI_ELEMENT_VISIBILITY, {element: 'layers', state: false});
nextTick(() => button.focus());
}
const elements = Array.from(container.querySelectorAll('input')) as HTMLElement[];
handleKeyboardEvent(e as KeyboardEvent, elements);
});
DomEvent.on(button,'click', () => store.commit(MutationTypes.TOGGLE_UI_ELEMENT_VISIBILITY, 'layers'));
section.style.display = 'none';
button.title = store.state.messages.layersTitle;
button.setAttribute('aria-expanded', 'false');
button.innerHTML = `
<svg class="svg-icon" aria-hidden="true">
<use xlink:href="#icon--layers" />
</svg>`;
//Use vuex track expanded state
watch(store.state.ui.visibleElements, (newValue) => {
if(newValue.has('layers') && !this.visible) {
this.expand();
} else if(this.visible && !newValue.has('layers')) {
this.collapse();
}
this.visible = store.state.ui.visibleElements.has('layers');
});
watch(store.state.messages, (newValue) => (button.title = newValue.layersTitle));//
this.visible = store.state.ui.visibleElements.has('layers');
if (this.visible) {
this.expand();
}
this._baseLayersList = DomUtil.create('div', className + '-base', section);
this._separator = DomUtil.create('div', className + '-separator', section);
this._overlaysList = DomUtil.create('div', className + '-overlays', section);
container.appendChild(section);
window.addEventListener('resize', () => this.handleResize());
this.handleResize();
}
handleResize() {
const y = this._layersButton!.getBoundingClientRect().y;
//Limit height to remaining vertical space
// Including 30px element padding, 10px padding from edge of viewport, and 55px padding to avoid covering bottom bar
this._section!.style.maxHeight = `calc(100vh - ${(y + 30 + 10 + 55)}px)`;
}
addOverlayAtPosition(layer: Layer, name: string, position: number): this {
this._layerPositions.set(layer, position);
return super.addOverlay(layer, name);
}
addOverlay(layer: Layer, name: string): this {
this._layerPositions.set(layer, 0);
return super.addOverlay(layer, name);
}
removeLayer(layer: Layer): this {
this._layerPositions.delete(layer);
return super.removeLayer(layer);
}
// noinspection JSUnusedGlobalSymbols
_addItem(obj: any) {
const container = obj.overlay ? this._overlaysList : this._baseLayersList,
item = document.createElement('label'),
label = document.createElement('span'),
checked = this._map!.hasLayer(obj.layer);
let input;
item.className = 'layer checkbox';
if (obj.overlay) {
input = document.createElement('input');
input.type = 'checkbox';
input.className = 'leaflet-control-layers-selector';
input.defaultChecked = checked;
} else {
// @ts-ignore
input = super._createRadioElement('leaflet-base-layers_' + Util.stamp(this), checked);
}
input.layerId = Util.stamp(obj.layer);
this._layerControlInputs!.push(input);
label.textContent = obj.name;
// @ts-ignore
DomEvent.on(input, 'click', (e: LeafletEvent) => super._onInputClick(e), this);
item.appendChild(input);
item.insertAdjacentHTML('beforeend', `
<svg class="svg-icon" aria-hidden="true">
<use xlink:href="#icon--checkbox" />
</svg>`);
item.appendChild(label);
container!.appendChild(item);
// @ts-ignore
super._checkDisabledLayers();
return label;
}
onRemove(map: LeafletMap) {
this._layerControlInputs = [];
(super.onRemove as Function)(map);
}
}

View File

@ -1,79 +0,0 @@
/*
* Copyright 2022 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 {Map, Layer} from 'leaflet';
import {LiveAtlasLayerControl} from "@/leaflet/control/LiveAtlasLayerControl";
import {watch} from "vue";
import {useStore} from "@/store";
import {computed} from "@vue/runtime-core";
export default class LayerManager {
private readonly layerControl: LiveAtlasLayerControl;
private readonly map: Map;
constructor(map: Map) {
const showControl = computed(() => useStore().state.components.layerControl);
this.map = map;
this.layerControl = new LiveAtlasLayerControl({}, {},{
position: 'topleft',
});
if(showControl.value) {
this.map.addControl(this.layerControl);
}
watch(showControl, (show) => {
if(show) {
this.map.addControl(this.layerControl);
} else {
this.map.removeControl(this.layerControl);
}
})
}
addLayer(layer: Layer, showInControl: boolean, name: string, position: number) {
this.map.addLayer(layer);
if(showInControl) {
if(this.layerControl.hasLayer(layer)) {
this.layerControl.removeLayer(layer);
}
if(typeof position !== 'undefined') {
this.layerControl.addOverlayAtPosition(layer, name, position);
} else {
this.layerControl.addOverlay(layer, name);
}
}
}
addHiddenLayer(layer: Layer, name: string, position: number) {
if(this.layerControl.hasLayer(layer)) {
this.layerControl.removeLayer(layer);
}
if(typeof position !== 'undefined') {
this.layerControl.addOverlayAtPosition(layer, name, position);
} else {
this.layerControl.addOverlay(layer, name);
}
}
removeLayer(layer: Layer) {
this.map.removeLayer(layer);
this.layerControl.removeLayer(layer);
}
}

View File

@ -99,45 +99,6 @@
} }
} }
.leaflet-control-layers {
width: auto;
border: none;
color: var(--text-base);
position: relative;
.leaflet-control-layers-list {
@extend %panel;
display: block;
position: absolute;
top: 0;
left: calc(var(--ui-element-spacing) + var(--ui-button-size));
overflow: auto;
max-width: calc(100vw - 14rem);
box-sizing: border-box;
@media screen and (max-width: 400px) {
max-width: calc(100vw - 13rem);
}
.leaflet-control-layers-overlays {
width: 100%;
max-width: 30rem;
}
.layer {
cursor: pointer;
padding: 0.8rem 0 0.7rem;
&:first-child {
margin-top: -0.4rem;
}
&:last-child {
margin-bottom: -0.4rem;
}
}
}
}
.leaflet-top, .leaflet-bottom, .leaflet-top, .leaflet-bottom,
.leaflet-left, .leaflet-right { .leaflet-left, .leaflet-right {

View File

@ -20,6 +20,7 @@ export enum ActionTypes {
START_UPDATES = "startUpdates", START_UPDATES = "startUpdates",
STOP_UPDATES = "stopUpdates", STOP_UPDATES = "stopUpdates",
SET_PLAYERS = "setPlayers", SET_PLAYERS = "setPlayers",
POP_LAYER_UPDATES = "popLayerUpdates",
POP_MARKER_UPDATES = "popMarkerUpdates", POP_MARKER_UPDATES = "popMarkerUpdates",
POP_TILE_UPDATES = "popTileUpdates", POP_TILE_UPDATES = "popTileUpdates",
SEND_CHAT_MESSAGE = "sendChatMessage", SEND_CHAT_MESSAGE = "sendChatMessage",

View File

@ -23,6 +23,7 @@ import {DynmapMarkerUpdate, DynmapTileUpdate} from "@/dynmap";
import {LiveAtlasGlobalConfig, LiveAtlasMarkerSet, LiveAtlasPlayer, LiveAtlasWorldDefinition} from "@/index"; import {LiveAtlasGlobalConfig, LiveAtlasMarkerSet, LiveAtlasPlayer, LiveAtlasWorldDefinition} from "@/index";
import {nextTick} from "vue"; import {nextTick} from "vue";
import {startUpdateHandling, stopUpdateHandling} from "@/util/markers"; import {startUpdateHandling, stopUpdateHandling} from "@/util/markers";
import {Layer} from "leaflet";
type AugmentedActionContext = { type AugmentedActionContext = {
commit<K extends keyof Mutations>( commit<K extends keyof Mutations>(
@ -49,6 +50,9 @@ export interface Actions {
{commit}: AugmentedActionContext, {commit}: AugmentedActionContext,
payload: Set<LiveAtlasPlayer> payload: Set<LiveAtlasPlayer>
):Promise<Map<string, LiveAtlasMarkerSet>> ):Promise<Map<string, LiveAtlasMarkerSet>>
[ActionTypes.POP_LAYER_UPDATES](
{commit}: AugmentedActionContext,
):Promise<[Layer, boolean][]>
[ActionTypes.POP_MARKER_UPDATES]( [ActionTypes.POP_MARKER_UPDATES](
{commit}: AugmentedActionContext, {commit}: AugmentedActionContext,
amount: number amount: number
@ -191,6 +195,14 @@ export const actions: ActionTree<State, State> & Actions = {
}); });
}, },
async [ActionTypes.POP_LAYER_UPDATES]({commit, state}): Promise<[Layer, boolean][]> {
const updates = Array.from(state.pendingLayerUpdates.entries());
commit(MutationTypes.POP_LAYER_UPDATES, undefined);
return updates;
},
async [ActionTypes.POP_MARKER_UPDATES]({commit, state}, amount: number): Promise<DynmapMarkerUpdate[]> { async [ActionTypes.POP_MARKER_UPDATES]({commit, state}, amount: number): Promise<DynmapMarkerUpdate[]> {
const updates = state.pendingMarkerUpdates.slice(0, amount); const updates = state.pendingMarkerUpdates.slice(0, amount);

View File

@ -30,11 +30,15 @@ export enum MutationTypes {
ADD_MARKER_UPDATES = 'addMarkerUpdates', ADD_MARKER_UPDATES = 'addMarkerUpdates',
ADD_TILE_UPDATES = 'addTileUpdates', ADD_TILE_UPDATES = 'addTileUpdates',
ADD_CHAT = 'addChat', ADD_CHAT = 'addChat',
POP_LAYER_UPDATES = 'popLayerUpdates',
POP_MARKER_UPDATES = 'popMarkerUpdates', POP_MARKER_UPDATES = 'popMarkerUpdates',
POP_TILE_UPDATES = 'popTileUpdates', POP_TILE_UPDATES = 'popTileUpdates',
SET_MAX_PLAYERS = 'setMaxPlayers', SET_MAX_PLAYERS = 'setMaxPlayers',
SET_PLAYERS_ASYNC = 'setPlayersAsync', SET_PLAYERS_ASYNC = 'setPlayersAsync',
SYNC_PLAYERS = 'syncPlayers', SYNC_PLAYERS = 'syncPlayers',
ADD_LAYER = 'addLayer',
UPDATE_LAYER = 'updateLayer',
REMOVE_LAYER = 'removeLayer',
SET_LOADED = 'setLoaded', SET_LOADED = 'setLoaded',
SET_CURRENT_SERVER = 'setCurrentServer', SET_CURRENT_SERVER = 'setCurrentServer',

View File

@ -41,10 +41,12 @@ import {
LiveAtlasMarker, LiveAtlasMarker,
LiveAtlasMapViewTarget, LiveAtlasMapViewTarget,
LiveAtlasGlobalMessageConfig, LiveAtlasGlobalMessageConfig,
LiveAtlasUIConfig, LiveAtlasServerDefinition LiveAtlasUIConfig, LiveAtlasServerDefinition, LiveAtlasLayerDefinition, LiveAtlasPartialLayerDefinition
} from "@/index"; } from "@/index";
import {getServerMapProvider} from "@/util/config"; import {getServerMapProvider} from "@/util/config";
import {getDefaultPlayerImage} from "@/util/images"; import {getDefaultPlayerImage} from "@/util/images";
import {Layer} from "leaflet";
import {sortLayers} from "@/util/layers";
export type CurrentMapPayload = { export type CurrentMapPayload = {
worldName: string; worldName: string;
@ -67,12 +69,16 @@ export type Mutations<S = State> = {
[MutationTypes.ADD_TILE_UPDATES](state: S, updates: Array<DynmapTileUpdate>): void [MutationTypes.ADD_TILE_UPDATES](state: S, updates: Array<DynmapTileUpdate>): void
[MutationTypes.ADD_CHAT](state: State, chat: Array<LiveAtlasChat>): void [MutationTypes.ADD_CHAT](state: State, chat: Array<LiveAtlasChat>): void
[MutationTypes.POP_LAYER_UPDATES](state: State): void
[MutationTypes.POP_MARKER_UPDATES](state: S, amount: number): void [MutationTypes.POP_MARKER_UPDATES](state: S, amount: number): void
[MutationTypes.POP_TILE_UPDATES](state: S, amount: number): void [MutationTypes.POP_TILE_UPDATES](state: S, amount: number): void
[MutationTypes.SET_MAX_PLAYERS](state: S, maxPlayers: number): void [MutationTypes.SET_MAX_PLAYERS](state: S, maxPlayers: number): void
[MutationTypes.SET_PLAYERS_ASYNC](state: S, players: Set<LiveAtlasPlayer>): Set<LiveAtlasPlayer> [MutationTypes.SET_PLAYERS_ASYNC](state: S, players: Set<LiveAtlasPlayer>): Set<LiveAtlasPlayer>
[MutationTypes.SYNC_PLAYERS](state: S, keep: Set<string>): void [MutationTypes.SYNC_PLAYERS](state: S, keep: Set<string>): void
[MutationTypes.ADD_LAYER](state: State, layer: LiveAtlasLayerDefinition): void
[MutationTypes.UPDATE_LAYER](state: State, payload: {layer: Layer, options: LiveAtlasPartialLayerDefinition}): void
[MutationTypes.REMOVE_LAYER](state: State, layer: Layer): void
[MutationTypes.SET_LOADED](state: S, a?: void): void [MutationTypes.SET_LOADED](state: S, a?: void): void
[MutationTypes.SET_CURRENT_SERVER](state: S, server: string): void [MutationTypes.SET_CURRENT_SERVER](state: S, server: string): void
[MutationTypes.SET_CURRENT_MAP](state: S, payload: CurrentMapPayload): void [MutationTypes.SET_CURRENT_MAP](state: S, payload: CurrentMapPayload): void
@ -173,6 +179,14 @@ export const mutations: MutationTree<State> & Mutations = {
state.worlds.clear(); state.worlds.clear();
state.maps.clear(); state.maps.clear();
//Mark all layers for removal
for (const layer of state.layers.keys()) {
state.pendingLayerUpdates.set(layer, false);
}
state.layers.clear();
state.sortedLayers.splice(0);
state.followTarget = undefined; state.followTarget = undefined;
state.viewTarget = undefined; state.viewTarget = undefined;
@ -299,6 +313,11 @@ export const mutations: MutationTree<State> & Mutations = {
state.chat.messages.unshift(...chat); state.chat.messages.unshift(...chat);
}, },
//Pops the specified number of marker updates from the pending updates list
[MutationTypes.POP_LAYER_UPDATES](state: State) {
state.pendingLayerUpdates.clear();
},
//Pops the specified number of marker updates from the pending updates list //Pops the specified number of marker updates from the pending updates list
[MutationTypes.POP_MARKER_UPDATES](state: State, amount: number) { [MutationTypes.POP_MARKER_UPDATES](state: State, amount: number) {
state.pendingMarkerUpdates.splice(0, amount); state.pendingMarkerUpdates.splice(0, amount);
@ -379,6 +398,38 @@ export const mutations: MutationTree<State> & Mutations = {
} }
}, },
[MutationTypes.ADD_LAYER](state: State, layer: LiveAtlasLayerDefinition) {
state.layers.set(layer.layer, layer);
state.sortedLayers = sortLayers(state.layers);
state.pendingLayerUpdates.set(layer.layer, layer.enabled);
},
[MutationTypes.UPDATE_LAYER](state: State, {layer, options}) {
if(state.layers.has(layer)) {
const existing = state.layers.get(layer) as LiveAtlasLayerDefinition,
existingEnabled = existing.enabled;
state.layers.set(layer, Object.assign(existing, options));
state.sortedLayers = sortLayers(state.layers);
// Sort layers if position has changed
if((typeof options.enabled === 'boolean' && existingEnabled !== options.enabled)) {
state.pendingLayerUpdates.set(layer, options.enabled);
}
}
},
[MutationTypes.REMOVE_LAYER](state: State, layer: Layer) {
const existing = state.layers.get(layer);
if (existing) {
console.log('removing???');
state.layers.delete(layer);
state.pendingLayerUpdates.set(layer, false); // Remove from map
state.sortedLayers.splice(state.sortedLayers.indexOf(existing, 1));
}
},
//Sets flag indicating LiveAtlas has fully loaded //Sets flag indicating LiveAtlas has fully loaded
[MutationTypes.SET_LOADED](state: State) { [MutationTypes.SET_LOADED](state: State) {
state.firstLoad = false; state.firstLoad = false;
@ -553,6 +604,15 @@ export const mutations: MutationTree<State> & Mutations = {
state.worlds.clear(); state.worlds.clear();
state.maps.clear(); state.maps.clear();
//Mark all layers for removal
for (const layer of state.layers.keys()) {
state.pendingLayerUpdates.set(layer, false);
}
state.layers.clear();
state.sortedLayers.splice(0);
state.currentZoom = 0; state.currentZoom = 0;
state.currentLocation = {x: 0, y: 0, z: 0}; state.currentLocation = {x: 0, y: 0, z: 0};

View File

@ -36,11 +36,12 @@ import {
LiveAtlasChat, LiveAtlasChat,
LiveAtlasUIModal, LiveAtlasUIModal,
LiveAtlasSidebarSectionState, LiveAtlasSidebarSectionState,
LiveAtlasMarker, LiveAtlasMapViewTarget LiveAtlasMarker, LiveAtlasMapViewTarget, LiveAtlasLayerDefinition
} from "@/index"; } from "@/index";
import LiveAtlasMapDefinition from "@/model/LiveAtlasMapDefinition"; import LiveAtlasMapDefinition from "@/model/LiveAtlasMapDefinition";
import {getMessages} from "@/util"; import {getMessages} from "@/util";
import {getDefaultPlayerImage} from "@/util/images"; import {getDefaultPlayerImage} from "@/util/images";
import {Layer} from "leaflet";
export type State = { export type State = {
version: string; version: string;
@ -58,6 +59,10 @@ export type State = {
worlds: Map<string, LiveAtlasWorldDefinition>; worlds: Map<string, LiveAtlasWorldDefinition>;
maps: Map<string, LiveAtlasMapDefinition>; maps: Map<string, LiveAtlasMapDefinition>;
layers: Map<Layer, LiveAtlasLayerDefinition>;
sortedLayers: LiveAtlasLayerDefinition[];
players: Map<string, LiveAtlasPlayer>; players: Map<string, LiveAtlasPlayer>;
sortedPlayers: LiveAtlasSortedPlayers; sortedPlayers: LiveAtlasSortedPlayers;
maxPlayers: number; maxPlayers: number;
@ -68,6 +73,7 @@ export type State = {
messages: LiveAtlasChat[]; messages: LiveAtlasChat[];
}; };
pendingLayerUpdates: Map<Layer, boolean>; //Pending changes to map layer visibility
pendingMarkerUpdates: DynmapMarkerUpdate[]; pendingMarkerUpdates: DynmapMarkerUpdate[];
pendingTileUpdates: Array<DynmapTileUpdate>; pendingTileUpdates: Array<DynmapTileUpdate>;
@ -130,6 +136,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
layers: new Map(), //Leaflet map layers
sortedLayers: [], //Layers sorted by position for layer control
players: new Map(), //Online players from world.json players: new Map(), //Online players from world.json
sortedPlayers: [] as LiveAtlasSortedPlayers, //Online players from world.json, sorted by their sort property then alphabetically sortedPlayers: [] as LiveAtlasSortedPlayers, //Online players from world.json, sorted by their sort property then alphabetically
maxPlayers: 0, maxPlayers: 0,
@ -141,7 +151,8 @@ export const state: State = {
markerSets: new Map(), //Marker sets from world_markers.json, doesn't include the markers themselves for performance reasons markerSets: new Map(), //Marker sets from world_markers.json, doesn't include the markers themselves for performance reasons
pendingMarkerUpdates: [], //Pending updates to markers/areas/etc pendingLayerUpdates: new Map(), //Pending updates to map layer visibility
pendingMarkerUpdates: [], //Pending updates to markers/areas/etc
pendingTileUpdates: [], //Pending updates to map tiles pendingTileUpdates: [], //Pending updates to map tiles
// Map plugin provided settings for various parts of LiveAtlas // Map plugin provided settings for various parts of LiveAtlas

44
src/util/layers.ts Normal file
View File

@ -0,0 +1,44 @@
/*
* Copyright 2022 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 {Layer} from "leaflet";
import {LiveAtlasLayerDefinition} from "@/index";
import {MutationTypes} from "@/store/mutation-types";
import {useStore} from "@/store";
export const sortLayers = (layers: Map<Layer, LiveAtlasLayerDefinition>) => {
return Array.from(layers.values()).sort((entry1, entry2) => {
if (entry1.position != entry2.position) {
return entry1.position - entry2.position;
}
return ((entry1.name < entry2.name) ? -1 : ((entry1.name > entry2.name) ? 1 : 0));
});
}
export const toggleLayer = (layer: Layer) => {
const store = useStore();
if(!store.state.layers.has(layer)) {
return;
}
const enabled = !store.state.layers.get(layer)!.enabled;
store.commit(MutationTypes.UPDATE_LAYER, {
layer: layer,
options: {enabled}
});
}