Migrate LayerControl to vue
This commit is contained in:
parent
4887acb917
commit
14674b774e
@ -39,6 +39,7 @@ import {MutationTypes} from "@/store/mutation-types";
|
||||
import LiveAtlasLeafletMap from "@/leaflet/LiveAtlasLeafletMap";
|
||||
import {LiveAtlasLocation, LiveAtlasPlayer, LiveAtlasMapViewTarget} from "@/index";
|
||||
import TileLayerOverlay from "@/components/map/layer/TileLayerOverlay.vue";
|
||||
import {ActionTypes} from "@/store/action-types";
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
@ -69,6 +70,7 @@ export default defineComponent({
|
||||
|
||||
//Location and zoom to pan to upon next projection change
|
||||
scheduledView = ref<LiveAtlasMapViewTarget|null>(null),
|
||||
pendingLayerUpdates = computed(() => !!store.state.pendingLayerUpdates.size),
|
||||
|
||||
mapTitle = computed(() => store.state.messages.mapTitle);
|
||||
|
||||
@ -90,6 +92,7 @@ export default defineComponent({
|
||||
currentMap,
|
||||
|
||||
scheduledView,
|
||||
pendingLayerUpdates,
|
||||
|
||||
mapTitle
|
||||
}
|
||||
@ -190,6 +193,21 @@ export default defineComponent({
|
||||
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: {
|
||||
handler(newValue) {
|
||||
if(!newValue || !this.currentMap || !this.leaflet) {
|
||||
|
@ -24,6 +24,7 @@
|
||||
<div class="ui__toolbar toolbar--vertical">
|
||||
<LogoControl v-for="logo in logoControls" :key="JSON.stringify(logo)" :options="logo"></LogoControl>
|
||||
<ZoomControl :leaflet="leaflet"></ZoomControl>
|
||||
<LayerControl></LayerControl>
|
||||
<LoadingControl :leaflet="leaflet" :delay="500"></LoadingControl>
|
||||
</div>
|
||||
</div>
|
||||
@ -61,6 +62,7 @@ import LoginControl from "@/components/map/control/LoginControl.vue";
|
||||
import LiveAtlasLeafletMap from "@/leaflet/LiveAtlasLeafletMap";
|
||||
import LoadingControl from "@/components/map/control/LoadingControl.vue";
|
||||
import ZoomControl from "@/components/map/control/ZoomControl.vue";
|
||||
import LayerControl from "@/components/map/control/LayerControl.vue";
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
@ -71,6 +73,7 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
components: {
|
||||
LayerControl,
|
||||
ZoomControl,
|
||||
LoadingControl,
|
||||
LogoControl,
|
||||
|
@ -31,6 +31,7 @@ import LiveAtlasLeafletMap from "@/leaflet/LiveAtlasLeafletMap";
|
||||
import {onMounted, ref} from "vue";
|
||||
import {Coordinate, CoordinatesControlOptions} from "@/index";
|
||||
import {LeafletMouseEvent} from "leaflet";
|
||||
import {CoordinatesControlOptions} from "@/leaflet/control/CoordinatesControl";
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
@ -42,7 +43,7 @@ export default defineComponent({
|
||||
|
||||
setup(props) {
|
||||
const store = useStore(),
|
||||
componentSettings = computed(() => store.state.components.coordinatesControl),
|
||||
componentSettings = computed(() => store.state.components.coordinatesControl as CoordinatesControlOptions),
|
||||
currentMap = computed(() => store.state.currentMap),
|
||||
|
||||
chunkLabel = computed(() => store.state.messages.locationChunk),
|
||||
|
179
src/components/map/control/LayerControl.vue
Normal file
179
src/components/map/control/LayerControl.vue
Normal 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>
|
@ -21,11 +21,11 @@
|
||||
<script lang="ts">
|
||||
import {defineComponent, computed, onMounted, onUnmounted} from "@vue/runtime-core";
|
||||
import {useStore} from "@/store";
|
||||
import LiveAtlasLeafletMap from "@/leaflet/LiveAtlasLeafletMap";
|
||||
import LiveAtlasLayerGroup from "@/leaflet/layer/LiveAtlasLayerGroup";
|
||||
import {LiveAtlasMarkerSet} from "@/index";
|
||||
import {watch} from "vue";
|
||||
import {markRaw, watch} from "vue";
|
||||
import Markers from "@/components/map/marker/Markers.vue";
|
||||
import {MutationTypes} from "@/store/mutation-types";
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
@ -33,11 +33,6 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
props: {
|
||||
leaflet: {
|
||||
type: Object as () => LiveAtlasLeafletMap,
|
||||
required: true,
|
||||
},
|
||||
|
||||
markerSet: {
|
||||
type: Object as () => LiveAtlasMarkerSet,
|
||||
required: true,
|
||||
@ -65,27 +60,25 @@ export default defineComponent({
|
||||
priority: props.markerSet.priority,
|
||||
});
|
||||
|
||||
if(newValue.hidden) {
|
||||
props.leaflet.getLayerManager()
|
||||
.addHiddenLayer(layerGroup, newValue.label, props.markerSet.priority);
|
||||
} else {
|
||||
props.leaflet.getLayerManager()
|
||||
.addLayer(layerGroup, true, newValue.label, props.markerSet.priority);
|
||||
}
|
||||
// store.commit(MutationTypes.UPDATE_LAYER, {
|
||||
// layer: layerGroup,
|
||||
// options: {enabled: newValue.hidden}
|
||||
// });
|
||||
}
|
||||
}, {deep: true});
|
||||
|
||||
onMounted(() => {
|
||||
if(props.markerSet.hidden) {
|
||||
props.leaflet.getLayerManager()
|
||||
.addHiddenLayer(layerGroup, props.markerSet.label, props.markerSet.priority);
|
||||
} else {
|
||||
props.leaflet.getLayerManager()
|
||||
.addLayer(layerGroup, true, props.markerSet.label, props.markerSet.priority);
|
||||
}
|
||||
store.commit(MutationTypes.ADD_LAYER, {
|
||||
layer: markRaw(layerGroup),
|
||||
name: props.markerSet.label,
|
||||
overlay: true,
|
||||
position: props.markerSet.priority || 0,
|
||||
enabled: !props.markerSet.hidden,
|
||||
showInControl: true
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => props.leaflet.getLayerManager().removeLayer(layerGroup));
|
||||
onUnmounted(() => store.commit(MutationTypes.REMOVE_LAYER, layerGroup));
|
||||
|
||||
return {
|
||||
markerSettings,
|
||||
|
@ -24,6 +24,8 @@ import {defineComponent, computed, watch, onMounted, onUnmounted} from "@vue/run
|
||||
import {useStore} from "@/store";
|
||||
import {LayerGroup} from 'leaflet';
|
||||
import LiveAtlasLeafletMap from "@/leaflet/LiveAtlasLeafletMap";
|
||||
import {MutationTypes} from "@/store/mutation-types";
|
||||
import {markRaw} from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
@ -51,21 +53,17 @@ export default defineComponent({
|
||||
watch(playerCount, (newValue) => playerPane.classList.toggle('no-animations', newValue > 150));
|
||||
|
||||
onMounted(() => {
|
||||
if(!componentSettings.value!.hideByDefault) {
|
||||
props.leaflet.getLayerManager().addLayer(
|
||||
layerGroup,
|
||||
true,
|
||||
store.state.components.players.markers!.layerName,
|
||||
componentSettings.value!.layerPriority);
|
||||
} else {
|
||||
props.leaflet.getLayerManager().addHiddenLayer(
|
||||
layerGroup,
|
||||
store.state.components.players.markers!.layerName,
|
||||
componentSettings.value!.layerPriority);
|
||||
}
|
||||
store.commit(MutationTypes.ADD_LAYER, {
|
||||
layer: markRaw(layerGroup),
|
||||
name: store.state.components.players.markers!.layerName,
|
||||
overlay: true,
|
||||
position: componentSettings.value!.layerPriority,
|
||||
enabled: !componentSettings.value!.hideByDefault,
|
||||
showInControl: true
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => props.leaflet.getLayerManager().removeLayer(layerGroup));
|
||||
onUnmounted(() => store.commit(MutationTypes.REMOVE_LAYER, layerGroup));
|
||||
|
||||
if(playersAboveMarkers.value) {
|
||||
playerPane.style.zIndex = '600';
|
||||
|
@ -19,6 +19,8 @@ import {defineComponent, onUnmounted} from "@vue/runtime-core";
|
||||
import {useStore} from "@/store";
|
||||
import LiveAtlasLeafletMap from "@/leaflet/LiveAtlasLeafletMap";
|
||||
import {LiveAtlasTileLayerOverlay} from "@/index";
|
||||
import {MutationTypes} from "@/store/mutation-types";
|
||||
import {markRaw} from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
@ -37,10 +39,17 @@ export default defineComponent({
|
||||
|
||||
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(() => {
|
||||
props.leaflet.getLayerManager().removeLayer(layer);
|
||||
store.commit(MutationTypes.REMOVE_LAYER, layer)
|
||||
layer.remove();
|
||||
});
|
||||
},
|
||||
|
19
src/index.d.ts
vendored
19
src/index.d.ts
vendored
@ -21,7 +21,7 @@ import {
|
||||
ControlOptions,
|
||||
Coords,
|
||||
DoneCallback, FitBoundsOptions,
|
||||
InternalTiles, LatLng,
|
||||
InternalTiles, LatLng, Layer,
|
||||
PathOptions,
|
||||
PointTuple,
|
||||
PolylineOptions
|
||||
@ -171,6 +171,23 @@ interface LiveAtlasWorldState {
|
||||
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 {
|
||||
world?: string;
|
||||
map?: string;
|
||||
|
@ -15,23 +15,14 @@
|
||||
*/
|
||||
|
||||
import {Map, DomUtil, MapOptions} from 'leaflet';
|
||||
import LayerManager from "@/leaflet/layer/LayerManager";
|
||||
|
||||
export default class LiveAtlasLeafletMap extends Map {
|
||||
declare _controlCorners: any;
|
||||
declare _controlContainer?: HTMLElement;
|
||||
declare _container?: HTMLElement;
|
||||
|
||||
private readonly _layerManager: LayerManager;
|
||||
|
||||
constructor(element: string | HTMLElement, options?: MapOptions) {
|
||||
super(element, options);
|
||||
|
||||
this._layerManager = Object.seal(new LayerManager(this));
|
||||
}
|
||||
|
||||
getLayerManager(): LayerManager {
|
||||
return this._layerManager;
|
||||
}
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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-left, .leaflet-right {
|
||||
|
@ -20,6 +20,7 @@ export enum ActionTypes {
|
||||
START_UPDATES = "startUpdates",
|
||||
STOP_UPDATES = "stopUpdates",
|
||||
SET_PLAYERS = "setPlayers",
|
||||
POP_LAYER_UPDATES = "popLayerUpdates",
|
||||
POP_MARKER_UPDATES = "popMarkerUpdates",
|
||||
POP_TILE_UPDATES = "popTileUpdates",
|
||||
SEND_CHAT_MESSAGE = "sendChatMessage",
|
||||
|
@ -23,6 +23,7 @@ import {DynmapMarkerUpdate, DynmapTileUpdate} from "@/dynmap";
|
||||
import {LiveAtlasGlobalConfig, LiveAtlasMarkerSet, LiveAtlasPlayer, LiveAtlasWorldDefinition} from "@/index";
|
||||
import {nextTick} from "vue";
|
||||
import {startUpdateHandling, stopUpdateHandling} from "@/util/markers";
|
||||
import {Layer} from "leaflet";
|
||||
|
||||
type AugmentedActionContext = {
|
||||
commit<K extends keyof Mutations>(
|
||||
@ -49,6 +50,9 @@ export interface Actions {
|
||||
{commit}: AugmentedActionContext,
|
||||
payload: Set<LiveAtlasPlayer>
|
||||
):Promise<Map<string, LiveAtlasMarkerSet>>
|
||||
[ActionTypes.POP_LAYER_UPDATES](
|
||||
{commit}: AugmentedActionContext,
|
||||
):Promise<[Layer, boolean][]>
|
||||
[ActionTypes.POP_MARKER_UPDATES](
|
||||
{commit}: AugmentedActionContext,
|
||||
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[]> {
|
||||
const updates = state.pendingMarkerUpdates.slice(0, amount);
|
||||
|
||||
|
@ -30,11 +30,15 @@ export enum MutationTypes {
|
||||
ADD_MARKER_UPDATES = 'addMarkerUpdates',
|
||||
ADD_TILE_UPDATES = 'addTileUpdates',
|
||||
ADD_CHAT = 'addChat',
|
||||
POP_LAYER_UPDATES = 'popLayerUpdates',
|
||||
POP_MARKER_UPDATES = 'popMarkerUpdates',
|
||||
POP_TILE_UPDATES = 'popTileUpdates',
|
||||
SET_MAX_PLAYERS = 'setMaxPlayers',
|
||||
SET_PLAYERS_ASYNC = 'setPlayersAsync',
|
||||
SYNC_PLAYERS = 'syncPlayers',
|
||||
ADD_LAYER = 'addLayer',
|
||||
UPDATE_LAYER = 'updateLayer',
|
||||
REMOVE_LAYER = 'removeLayer',
|
||||
SET_LOADED = 'setLoaded',
|
||||
|
||||
SET_CURRENT_SERVER = 'setCurrentServer',
|
||||
|
@ -41,10 +41,12 @@ import {
|
||||
LiveAtlasMarker,
|
||||
LiveAtlasMapViewTarget,
|
||||
LiveAtlasGlobalMessageConfig,
|
||||
LiveAtlasUIConfig, LiveAtlasServerDefinition
|
||||
LiveAtlasUIConfig, LiveAtlasServerDefinition, LiveAtlasLayerDefinition, LiveAtlasPartialLayerDefinition
|
||||
} from "@/index";
|
||||
import {getServerMapProvider} from "@/util/config";
|
||||
import {getDefaultPlayerImage} from "@/util/images";
|
||||
import {Layer} from "leaflet";
|
||||
import {sortLayers} from "@/util/layers";
|
||||
|
||||
export type CurrentMapPayload = {
|
||||
worldName: string;
|
||||
@ -67,12 +69,16 @@ export type Mutations<S = State> = {
|
||||
[MutationTypes.ADD_TILE_UPDATES](state: S, updates: Array<DynmapTileUpdate>): 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_TILE_UPDATES](state: S, amount: number): void
|
||||
|
||||
[MutationTypes.SET_MAX_PLAYERS](state: S, maxPlayers: number): void
|
||||
[MutationTypes.SET_PLAYERS_ASYNC](state: S, players: Set<LiveAtlasPlayer>): Set<LiveAtlasPlayer>
|
||||
[MutationTypes.SYNC_PLAYERS](state: S, keep: Set<string>): void
|
||||
[MutationTypes.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_CURRENT_SERVER](state: S, server: string): void
|
||||
[MutationTypes.SET_CURRENT_MAP](state: S, payload: CurrentMapPayload): void
|
||||
@ -173,6 +179,14 @@ export const mutations: MutationTree<State> & Mutations = {
|
||||
state.worlds.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.viewTarget = undefined;
|
||||
|
||||
@ -299,6 +313,11 @@ export const mutations: MutationTree<State> & Mutations = {
|
||||
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
|
||||
[MutationTypes.POP_MARKER_UPDATES](state: State, amount: number) {
|
||||
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
|
||||
[MutationTypes.SET_LOADED](state: State) {
|
||||
state.firstLoad = false;
|
||||
@ -553,6 +604,15 @@ export const mutations: MutationTree<State> & Mutations = {
|
||||
|
||||
state.worlds.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.currentLocation = {x: 0, y: 0, z: 0};
|
||||
|
||||
|
@ -36,11 +36,12 @@ import {
|
||||
LiveAtlasChat,
|
||||
LiveAtlasUIModal,
|
||||
LiveAtlasSidebarSectionState,
|
||||
LiveAtlasMarker, LiveAtlasMapViewTarget
|
||||
LiveAtlasMarker, LiveAtlasMapViewTarget, LiveAtlasLayerDefinition
|
||||
} from "@/index";
|
||||
import LiveAtlasMapDefinition from "@/model/LiveAtlasMapDefinition";
|
||||
import {getMessages} from "@/util";
|
||||
import {getDefaultPlayerImage} from "@/util/images";
|
||||
import {Layer} from "leaflet";
|
||||
|
||||
export type State = {
|
||||
version: string;
|
||||
@ -58,6 +59,10 @@ export type State = {
|
||||
|
||||
worlds: Map<string, LiveAtlasWorldDefinition>;
|
||||
maps: Map<string, LiveAtlasMapDefinition>;
|
||||
|
||||
layers: Map<Layer, LiveAtlasLayerDefinition>;
|
||||
sortedLayers: LiveAtlasLayerDefinition[];
|
||||
|
||||
players: Map<string, LiveAtlasPlayer>;
|
||||
sortedPlayers: LiveAtlasSortedPlayers;
|
||||
maxPlayers: number;
|
||||
@ -68,6 +73,7 @@ export type State = {
|
||||
messages: LiveAtlasChat[];
|
||||
};
|
||||
|
||||
pendingLayerUpdates: Map<Layer, boolean>; //Pending changes to map layer visibility
|
||||
pendingMarkerUpdates: DynmapMarkerUpdate[];
|
||||
pendingTileUpdates: Array<DynmapTileUpdate>;
|
||||
|
||||
@ -130,6 +136,10 @@ export const state: State = {
|
||||
|
||||
worlds: new Map(), //Defined (loaded) worlds with 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
|
||||
sortedPlayers: [] as LiveAtlasSortedPlayers, //Online players from world.json, sorted by their sort property then alphabetically
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
// Map plugin provided settings for various parts of LiveAtlas
|
||||
|
44
src/util/layers.ts
Normal file
44
src/util/layers.ts
Normal 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}
|
||||
});
|
||||
}
|
Loading…
Reference in New Issue
Block a user