LiveAtlas/src/components/Map.vue

386 lines
12 KiB
Vue
Raw Normal View History

2020-12-16 16:54:41 +00:00
<!--
2021-07-25 14:12:40 +00:00
- Copyright 2021 James Lyne
2020-12-16 16:54:41 +00:00
-
2021-07-25 14:12:40 +00:00
- 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
2020-12-16 16:54:41 +00:00
-
2021-07-25 14:12:40 +00:00
- http://www.apache.org/licenses/LICENSE-2.0
2020-12-16 16:54:41 +00:00
-
2021-07-25 14:12:40 +00:00
- 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.
2020-12-16 16:54:41 +00:00
-->
2020-12-01 23:20:38 +00:00
<template>
2021-05-26 20:13:54 +00:00
<div class="map" :style="{backgroundColor: mapBackground }" v-bind="$attrs" :aria-label="mapTitle">
2021-05-28 18:50:08 +00:00
<template v-if="leaflet">
<MapLayer v-for="[name, map] in maps" :key="name" :map="map" :name="name" :leaflet="leaflet"></MapLayer>
<PlayersLayer v-if="playerMarkersEnabled" :leaflet="leaflet"></PlayersLayer>
<MarkerSetLayer v-for="[name, markerSet] in markerSets" :key="name" :markerSet="markerSet" :leaflet="leaflet"></MarkerSetLayer>
<LogoControl v-for="logo in logoControls" :key="JSON.stringify(logo)" :options="logo" :leaflet="leaflet"></LogoControl>
<CoordinatesControl v-if="coordinatesControlEnabled" :leaflet="leaflet"></CoordinatesControl>
<LinkControl v-if="linkControlEnabled" :leaflet="leaflet"></LinkControl>
<ClockControl v-if="clockControlEnabled" :leaflet="leaflet"></ClockControl>
2021-08-30 21:27:41 +00:00
<LoginControl v-if="loginEnabled" :leaflet="leaflet"></LoginControl>
2021-05-28 18:50:08 +00:00
<ChatControl v-if="chatBoxEnabled" :leaflet="leaflet"></ChatControl>
</template>
2020-12-01 23:20:38 +00:00
</div>
2021-08-30 21:27:41 +00:00
<MapContextMenu :leaflet="leaflet" v-if="leaflet"></MapContextMenu>
2020-12-01 23:20:38 +00:00
</template>
<script lang="ts">
import {computed, ref, defineComponent} from "@vue/runtime-core";
import {CRS, LatLng, LatLngBounds, PanOptions, ZoomPanOptions} from 'leaflet';
2020-12-01 23:20:38 +00:00
import {useStore} from '@/store';
import MapLayer from "@/components/map/layer/MapLayer.vue";
import PlayersLayer from "@/components/map/layer/PlayersLayer.vue";
import MarkerSetLayer from "@/components/map/layer/MarkerSetLayer.vue";
import CoordinatesControl from "@/components/map/control/CoordinatesControl.vue";
import ClockControl from "@/components/map/control/ClockControl.vue";
import LinkControl from "@/components/map/control/LinkControl.vue";
2020-12-17 14:50:12 +00:00
import ChatControl from "@/components/map/control/ChatControl.vue";
2020-12-01 23:20:38 +00:00
import LogoControl from "@/components/map/control/LogoControl.vue";
import {MutationTypes} from "@/store/mutation-types";
import LiveAtlasLeafletMap from "@/leaflet/LiveAtlasLeafletMap";
2020-12-15 01:51:19 +00:00
import {LoadingControl} from "@/leaflet/control/LoadingControl";
2021-05-24 15:42:32 +00:00
import MapContextMenu from "@/components/map/MapContextMenu.vue";
import {LiveAtlasLocation, LiveAtlasPlayer, LiveAtlasMapViewTarget} from "@/index";
2021-08-30 21:27:41 +00:00
import LoginControl from "@/components/map/control/LoginControl.vue";
2020-12-01 23:20:38 +00:00
export default defineComponent({
components: {
2021-05-24 15:42:32 +00:00
MapContextMenu,
2020-12-01 23:20:38 +00:00
MapLayer,
PlayersLayer,
MarkerSetLayer,
CoordinatesControl,
ClockControl,
LinkControl,
2020-12-17 14:50:12 +00:00
ChatControl,
2021-08-30 21:27:41 +00:00
LogoControl,
LoginControl
2020-12-01 23:20:38 +00:00
},
setup() {
const store = useStore(),
2021-05-15 19:25:03 +00:00
leaflet = undefined as any,
2020-12-01 23:20:38 +00:00
maps = computed(() => store.state.maps),
markerSets = computed(() => store.state.markerSets),
configuration = computed(() => store.state.configuration),
playerMarkersEnabled = computed(() => store.getters.playerMarkersEnabled),
coordinatesControlEnabled = computed(() => store.getters.coordinatesControlEnabled),
clockControlEnabled = computed(() => store.getters.clockControlEnabled),
linkControlEnabled = computed(() => store.state.components.linkControl),
2021-01-06 16:02:36 +00:00
chatBoxEnabled = computed(() => store.state.components.chatBox),
2021-08-30 21:27:41 +00:00
loginEnabled = computed(() => store.state.components.login),
2020-12-01 23:20:38 +00:00
logoControls = computed(() => store.state.components.logoControls),
currentWorld = computed(() => store.state.currentWorld),
currentMap = computed(() => store.state.currentMap),
2020-12-17 00:13:28 +00:00
mapBackground = computed(() => store.getters.mapBackground),
followTarget = computed(() => store.state.followTarget),
viewTarget = computed(() => store.state.viewTarget),
parsedUrl = computed(() => store.state.parsedUrl),
//Location and zoom to pan to upon next projection change
scheduledView = ref<LiveAtlasMapViewTarget|null>(null),
2021-05-26 20:13:54 +00:00
mapTitle = computed(() => store.state.messages.mapTitle);
2020-12-01 23:20:38 +00:00
return {
leaflet,
maps,
markerSets,
configuration,
2021-05-24 15:42:32 +00:00
2020-12-01 23:20:38 +00:00
playerMarkersEnabled,
coordinatesControlEnabled,
clockControlEnabled,
linkControlEnabled,
2021-01-06 16:02:36 +00:00
chatBoxEnabled,
2021-08-30 21:27:41 +00:00
loginEnabled,
2021-05-24 15:42:32 +00:00
2020-12-01 23:20:38 +00:00
logoControls,
2020-12-17 00:13:28 +00:00
followTarget,
viewTarget,
parsedUrl,
2020-12-01 23:20:38 +00:00
mapBackground,
2021-05-24 15:42:32 +00:00
2020-12-01 23:20:38 +00:00
currentWorld,
currentMap,
2021-05-24 15:42:32 +00:00
scheduledView,
2021-05-26 20:13:54 +00:00
mapTitle
2020-12-01 23:20:38 +00:00
}
},
watch: {
2020-12-17 00:13:28 +00:00
followTarget: {
2020-12-01 23:20:38 +00:00
handler(newValue, oldValue) {
if (newValue) {
2021-07-24 03:09:33 +00:00
this.updateFollow(newValue, !oldValue || newValue.name !== oldValue.name);
2020-12-01 23:20:38 +00:00
}
},
deep: true
},
viewTarget: {
handler(newValue) {
if (newValue) {
//Immediately clear if on the correct world, to allow repeated panning
if (this.currentWorld && newValue.location.world === this.currentWorld.name) {
useStore().commit(MutationTypes.CLEAR_VIEW_TARGET, undefined);
}
2020-12-18 14:53:06 +00:00
this.setView(newValue);
}
},
deep: true
2020-12-17 00:13:28 +00:00
},
currentMap(newValue, oldValue) {
if(this.leaflet && newValue) {
let viewTarget = this.scheduledView;
if(!viewTarget && oldValue) {
viewTarget = {location: oldValue.latLngToLocation(this.leaflet.getCenter(), 64) as LiveAtlasLocation};
} else if(!viewTarget) {
viewTarget = {location: {x: 0, y: 0, z: 0} as LiveAtlasLocation};
}
viewTarget.options = {
animate: false,
noMoveStart: false,
}
this.setView(viewTarget);
this.scheduledView = null;
}
},
currentWorld(newValue, oldValue) {
const store = useStore();
2020-12-01 23:20:38 +00:00
if(newValue) {
store.state.currentMapProvider!.populateWorld(newValue);
let viewTarget = this.scheduledView || {} as LiveAtlasMapViewTarget;
// Abort if follow target is present, to avoid panning twice
2020-12-18 14:53:06 +00:00
if(store.state.followTarget && store.state.followTarget.location.world === newValue.name) {
return;
// Abort if pan target is present, to avoid panning to the wrong place.
2020-12-18 14:53:06 +00:00
// Also clear it to allow repeated panning.
} else if(store.state.viewTarget && store.state.viewTarget.location.world === newValue.name) {
store.commit(MutationTypes.CLEAR_VIEW_TARGET, undefined);
2020-12-18 14:53:06 +00:00
return;
// Otherwise pan to url location, if present
} else if(store.state.parsedUrl?.location) {
viewTarget.location = store.state.parsedUrl.location;
if(!oldValue) {
if(typeof store.state.parsedUrl?.zoom !== 'undefined') {
viewTarget.zoom = store.state.parsedUrl?.zoom;
} else if(typeof newValue.defaultZoom !== 'undefined') {
viewTarget.zoom = newValue.defaultZoom;
} else {
viewTarget.zoom = store.state.configuration.defaultZoom;
}
}
store.commit(MutationTypes.CLEAR_PARSED_URL, undefined);
// Otherwise pan to world center
2020-12-18 14:53:06 +00:00
} else {
viewTarget.location = newValue.center;
}
if(viewTarget.zoom == null) {
if(typeof newValue.defaultZoom !== 'undefined') {
viewTarget.zoom = newValue.defaultZoom;
} else {
viewTarget.zoom = store.state.configuration.defaultZoom;
}
}
//Set pan location for when the projection changes
this.scheduledView = viewTarget;
2020-12-01 23:20:38 +00:00
}
},
parsedUrl: {
handler(newValue) {
if(!newValue || !this.currentMap || !this.leaflet) {
return;
}
this.setView({
location: {...newValue.location, world: newValue.world},
map: newValue.map,
zoom: newValue.zoom,
options: {
animate: false,
noMoveStart: true,
}
});
},
deep: true,
}
2020-12-01 23:20:38 +00:00
},
mounted() {
this.leaflet = new LiveAtlasLeafletMap(this.$el.nextElementSibling, Object.freeze({
2020-12-01 23:20:38 +00:00
zoom: this.configuration.defaultZoom,
2020-12-12 22:04:56 +00:00
center: new LatLng(0, 0),
2020-12-01 23:20:38 +00:00
fadeAnimation: false,
zoomAnimation: true,
zoomControl: true,
preferCanvas: true,
attributionControl: false,
2020-12-12 22:04:56 +00:00
crs: CRS.Simple,
2020-12-01 23:20:38 +00:00
worldCopyJump: false,
2020-12-10 02:22:29 +00:00
// markerZoomAnimation: false,
})) as LiveAtlasLeafletMap;
2020-12-01 23:20:38 +00:00
2021-05-26 22:55:50 +00:00
window.addEventListener('keydown', this.handleKeydown);
this.leaflet.createPane('vectors');
2020-12-15 01:51:19 +00:00
this.leaflet.addControl(new LoadingControl({
position: 'topleft',
delayIndicator: 500,
}));
2020-12-01 23:20:38 +00:00
this.leaflet.on('moveend', () => {
if(this.currentMap) {
useStore().commit(MutationTypes.SET_CURRENT_LOCATION, this.currentMap
.latLngToLocation(this.leaflet!.getCenter(), 64));
}
});
this.leaflet.on('zoomend', () => {
useStore().commit(MutationTypes.SET_CURRENT_ZOOM, this.leaflet!.getZoom());
});
2020-12-01 23:20:38 +00:00
},
2021-05-26 22:55:50 +00:00
unmounted() {
window.removeEventListener('keydown', this.handleKeydown);
2021-05-28 18:50:08 +00:00
this.leaflet.remove();
2021-05-26 22:55:50 +00:00
},
2020-12-01 23:20:38 +00:00
methods: {
2021-05-26 22:55:50 +00:00
handleKeydown(e: KeyboardEvent) {
if(e.key === 'Escape') {
e.preventDefault();
this.leaflet.getContainer().focus();
}
},
setView(target: LiveAtlasMapViewTarget) {
2020-12-01 23:20:38 +00:00
const store = useStore(),
currentWorld = store.state.currentWorld,
currentMap = store.state.currentMap?.name,
targetWorld = target.location.world ? store.state.worlds.get(target.location.world) : currentWorld;
2020-12-01 23:20:38 +00:00
if(!this.leaflet) {
console.warn('Ignoring setView as leaflet not initialised');
return;
2020-12-01 23:20:38 +00:00
}
if(!targetWorld) {
console.warn(`Ignoring setView with unknown world ${target.location.world}`);
return;
}
if(targetWorld && (targetWorld !== currentWorld) || (target.map && currentMap !== target.map)) {
const mapName = target.map && targetWorld!.maps.has(target.map) ?
targetWorld!.maps.get(target.map)!.name :
targetWorld!.maps.entries().next().value[1].name;
this.scheduledView = target;
2020-12-01 23:20:38 +00:00
try {
store.commit(MutationTypes.SET_CURRENT_MAP, {worldName: targetWorld!.name, mapName});
} catch(e) {
//Clear scheduled move if change fails
console.warn(`Failed to handle map setView`, e);
this.scheduledView = null;
}
} else {
console.debug('Moving to', JSON.stringify(target));
if(typeof target.zoom !== 'undefined') {
this.leaflet!.setZoom(target.zoom, target.options as ZoomPanOptions);
}
2020-12-01 23:20:38 +00:00
if('min' in target.location) { // Bounds
this.leaflet!.fitBounds(new LatLngBounds(
store.state.currentMap?.locationToLatLng(target.location.min) as LatLng,
store.state.currentMap?.locationToLatLng(target.location.max) as LatLng,
), target.options);
} else { // Location
const location = store.state.currentMap?.locationToLatLng(target.location) as LatLng;
this.leaflet!.panTo(location, target.options as PanOptions);
}
}
},
updateFollow(player: LiveAtlasPlayer, newFollow: boolean) {
const store = useStore(),
currentWorld = store.state.currentWorld;
let map = undefined;
2020-12-01 23:20:38 +00:00
if(player.hidden) {
console.warn(`Cannot follow ${player.name}. Player is hidden from the map.`);
return;
}
2020-12-01 23:20:38 +00:00
if(!currentWorld || currentWorld.name !== player.location.world || newFollow) {
map = store.state.configuration.followMap;
}
this.setView({
location: player.location,
map,
zoom: (newFollow) ? store.state.configuration.followZoom : undefined,
});
2020-12-01 23:20:38 +00:00
}
}
})
</script>
<style lang="scss" scoped>
@import '../scss/_mixins.scss';
2020-12-01 23:20:38 +00:00
.map {
width: 100%;
height: 100%;
2021-05-15 22:24:29 +00:00
background: transparent;
2020-12-01 23:20:38 +00:00
z-index: 0;
2021-05-24 15:42:32 +00:00
cursor: default;
box-sizing: border-box;
position: relative;
&:focus:before {
content: '';
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
border: 0.2rem solid var(--outline-focus);
display: block;
z-index: 2000;
pointer-events: none;
}
@include focus-reset {
&:before {
content: none;
}
}
2020-12-01 23:20:38 +00:00
}
2021-07-19 15:40:30 +00:00
</style>