Refactor Map pan handling

- Add setView method to handle all panning/zooming
- Replaced SET_PAN_TARGET mutation with more generic SET_VIEW_TARGET which accepts locations/bounds/zooms and leaflet options
- Merged all scheduled variables into single scheduledView
This commit is contained in:
James Lyne 2022-01-16 22:15:12 +00:00
parent aaf4ee630d
commit 687a31f0f7
6 changed files with 123 additions and 130 deletions

View File

@ -36,7 +36,7 @@
<script lang="ts"> <script lang="ts">
import {computed, ref, defineComponent} from "@vue/runtime-core"; import {computed, ref, defineComponent} from "@vue/runtime-core";
import {CRS, LatLng} from 'leaflet'; import {CRS, LatLng, LatLngBounds, PanOptions, ZoomPanOptions} from 'leaflet';
import {useStore} from '@/store'; import {useStore} from '@/store';
import MapLayer from "@/components/map/layer/MapLayer.vue"; import MapLayer from "@/components/map/layer/MapLayer.vue";
import PlayersLayer from "@/components/map/layer/PlayersLayer.vue"; import PlayersLayer from "@/components/map/layer/PlayersLayer.vue";
@ -50,7 +50,7 @@ import {MutationTypes} from "@/store/mutation-types";
import LiveAtlasLeafletMap from "@/leaflet/LiveAtlasLeafletMap"; import LiveAtlasLeafletMap from "@/leaflet/LiveAtlasLeafletMap";
import {LoadingControl} from "@/leaflet/control/LoadingControl"; import {LoadingControl} from "@/leaflet/control/LoadingControl";
import MapContextMenu from "@/components/map/MapContextMenu.vue"; import MapContextMenu from "@/components/map/MapContextMenu.vue";
import {Coordinate, LiveAtlasPlayer} from "@/index"; import {LiveAtlasLocation, LiveAtlasPlayer, LiveAtlasMapViewTarget} from "@/index";
import LoginControl from "@/components/map/control/LoginControl.vue"; import LoginControl from "@/components/map/control/LoginControl.vue";
export default defineComponent({ export default defineComponent({
@ -88,12 +88,11 @@ export default defineComponent({
mapBackground = computed(() => store.getters.mapBackground), mapBackground = computed(() => store.getters.mapBackground),
followTarget = computed(() => store.state.followTarget), followTarget = computed(() => store.state.followTarget),
panTarget = computed(() => store.state.panTarget), viewTarget = computed(() => store.state.viewTarget),
parsedUrl = computed(() => store.state.parsedUrl), parsedUrl = computed(() => store.state.parsedUrl),
//Location and zoom to pan to upon next projection change //Location and zoom to pan to upon next projection change
scheduledPan = ref<Coordinate|null>(null), scheduledView = ref<LiveAtlasMapViewTarget|null>(null),
scheduledZoom = ref<number|null>(null),
mapTitle = computed(() => store.state.messages.mapTitle); mapTitle = computed(() => store.state.messages.mapTitle);
@ -112,15 +111,14 @@ export default defineComponent({
logoControls, logoControls,
followTarget, followTarget,
panTarget, viewTarget,
parsedUrl, parsedUrl,
mapBackground, mapBackground,
currentWorld, currentWorld,
currentMap, currentMap,
scheduledPan, scheduledView,
scheduledZoom,
mapTitle mapTitle
} }
@ -135,85 +133,82 @@ export default defineComponent({
}, },
deep: true deep: true
}, },
panTarget(newValue) { viewTarget: {
if(newValue) { handler(newValue) {
if (newValue) {
//Immediately clear if on the correct world, to allow repeated panning //Immediately clear if on the correct world, to allow repeated panning
if(this.currentWorld && newValue.location.world === this.currentWorld.name) { if (this.currentWorld && newValue.location.world === this.currentWorld.name) {
useStore().commit(MutationTypes.CLEAR_PAN_TARGET, undefined); useStore().commit(MutationTypes.CLEAR_VIEW_TARGET, undefined);
} }
this.updateFollow(newValue, false); this.setView(newValue);
} }
}, },
deep: true
},
currentMap(newValue, oldValue) { currentMap(newValue, oldValue) {
if(this.leaflet && newValue) { if(this.leaflet && newValue) {
let panTarget = this.scheduledPan; let viewTarget = this.scheduledView;
if(!panTarget && oldValue) { if(!viewTarget && oldValue) {
panTarget = oldValue.latLngToLocation(this.leaflet.getCenter(), 64); viewTarget = {location: oldValue.latLngToLocation(this.leaflet.getCenter(), 64) as LiveAtlasLocation};
} else if(!panTarget) { } else if(!viewTarget) {
panTarget = {x: 0, y: 0, z: 0}; viewTarget = {location: {x: 0, y: 0, z: 0} as LiveAtlasLocation};
} }
if(this.scheduledZoom) { viewTarget.options = {
this.leaflet!.setZoom(this.scheduledZoom, {
animate: false, animate: false,
}); noMoveStart: false,
} }
this.leaflet.panTo(newValue.locationToLatLng(panTarget), { this.setView(viewTarget);
animate: false, this.scheduledView = null;
noMoveStart: true,
});
this.scheduledZoom = null;
this.scheduledPan = null;
} }
}, },
currentWorld(newValue, oldValue) { currentWorld(newValue, oldValue) {
const store = useStore(); const store = useStore();
if(newValue) { if(newValue) {
let location: Coordinate | null = this.scheduledPan; let viewTarget = this.scheduledView || {} as LiveAtlasMapViewTarget;
// Abort if follow target is present, to avoid panning twice // Abort if follow target is present, to avoid panning twice
if(store.state.followTarget && store.state.followTarget.location.world === newValue.name) { if(store.state.followTarget && store.state.followTarget.location.world === newValue.name) {
return; return;
// Abort if pan target is present, to avoid panning to the wrong place. // Abort if pan target is present, to avoid panning to the wrong place.
// Also clear it to allow repeated panning. // Also clear it to allow repeated panning.
} else if(store.state.panTarget && store.state.panTarget.location.world === newValue.name) { } else if(store.state.viewTarget && store.state.viewTarget.location.world === newValue.name) {
store.commit(MutationTypes.CLEAR_PAN_TARGET, undefined); store.commit(MutationTypes.CLEAR_VIEW_TARGET, undefined);
return; return;
// Otherwise pan to url location, if present // Otherwise pan to url location, if present
} else if(store.state.parsedUrl?.location) { } else if(store.state.parsedUrl?.location) {
location = store.state.parsedUrl.location; viewTarget.location = store.state.parsedUrl.location;
if(!oldValue) { if(!oldValue) {
if(typeof store.state.parsedUrl?.zoom !== 'undefined') { if(typeof store.state.parsedUrl?.zoom !== 'undefined') {
this.scheduledZoom = store.state.parsedUrl?.zoom; viewTarget.zoom = store.state.parsedUrl?.zoom;
} else if(typeof newValue.defaultZoom !== 'undefined') { } else if(typeof newValue.defaultZoom !== 'undefined') {
this.scheduledZoom = newValue.defaultZoom; viewTarget.zoom = newValue.defaultZoom;
} else { } else {
this.scheduledZoom = store.state.configuration.defaultZoom; viewTarget.zoom = store.state.configuration.defaultZoom;
} }
} }
store.commit(MutationTypes.CLEAR_PARSED_URL, undefined); store.commit(MutationTypes.CLEAR_PARSED_URL, undefined);
// Otherwise pan to world center // Otherwise pan to world center
} else { } else {
location = newValue.center; viewTarget.location = newValue.center;
} }
if(this.scheduledZoom == null) { if(viewTarget.zoom == null) {
if(typeof newValue.defaultZoom !== 'undefined') { if(typeof newValue.defaultZoom !== 'undefined') {
this.scheduledZoom = newValue.defaultZoom; viewTarget.zoom = newValue.defaultZoom;
} else { } else {
this.scheduledZoom = store.state.configuration.defaultZoom; viewTarget.zoom = store.state.configuration.defaultZoom;
} }
} }
//Set pan location for when the projection changes //Set pan location for when the projection changes
this.scheduledPan = location; this.scheduledView = viewTarget;
} }
}, },
parsedUrl: { parsedUrl: {
@ -222,36 +217,15 @@ export default defineComponent({
return; return;
} }
//URL points to different map this.setView({
if(newValue.world !== this.currentWorld!.name || newValue.map !== this.currentMap!.name) { location: {...newValue.location, world: newValue.world},
//Set scheduled pan for after map change map: newValue.map,
this.scheduledPan = newValue.location; zoom: newValue.zoom,
this.scheduledZoom = newValue.zoom; options: {
try {
useStore().commit(MutationTypes.SET_CURRENT_MAP, {
worldName: newValue.world,
mapName: newValue.map
});
} catch(e) {
//Clear scheduled pan if change fails
console.warn(`Failed to handle URL change`, e);
this.scheduledPan = null;
this.scheduledZoom = null;
}
} else { //Same map, just pan
this.scheduledPan = null;
this.scheduledZoom = null;
this.leaflet.setZoom(newValue.zoom, {
animate: false,
});
this.leaflet.panTo(this.currentMap.locationToLatLng(newValue.location), {
animate: false, animate: false,
noMoveStart: true, noMoveStart: true,
});
} }
});
}, },
deep: true, deep: true,
} }
@ -304,61 +278,73 @@ export default defineComponent({
this.leaflet.getContainer().focus(); this.leaflet.getContainer().focus();
} }
}, },
updateFollow(player: LiveAtlasPlayer, newFollow: boolean) { setView(target: LiveAtlasMapViewTarget) {
const store = useStore(), const store = useStore(),
followMapName = store.state.configuration.followMap, currentWorld = store.state.currentWorld,
currentWorld = store.state.currentWorld; currentMap = store.state.currentMap?.name,
targetWorld = target.location.world ? store.state.worlds.get(target.location.world) : currentWorld;
let targetWorld = null;
if(!this.leaflet) { if(!this.leaflet) {
console.warn(`Cannot follow ${player.name}. Map not yet initialized.`); console.warn('Ignoring setView as leaflet not initialised');
return; return;
} }
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;
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);
}
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;
if(player.hidden) { if(player.hidden) {
console.warn(`Cannot follow ${player.name}. Player is hidden from the map.`); console.warn(`Cannot follow ${player.name}. Player is hidden from the map.`);
return; return;
} }
if(!player.location.world) { if(!currentWorld || currentWorld.name !== player.location.world || newFollow) {
console.warn(`Cannot follow ${player.name}. Player isn't in a known world.`); map = store.state.configuration.followMap;
return;
} }
if(!currentWorld || currentWorld.name !== player.location.world) { this.setView({
targetWorld = store.state.worlds.get(player.location.world); location: player.location,
} else { map,
targetWorld = currentWorld; zoom: (newFollow) ? store.state.configuration.followZoom : undefined,
} });
if (!targetWorld) {
console.warn(`Cannot follow ${player.name}. Player isn't in a known world.`);
return;
}
let map = followMapName && targetWorld.maps.has(followMapName)
? targetWorld.maps.get(followMapName)
: targetWorld.maps.entries().next().value[1]
if(map !== store.state.currentMap && (targetWorld !== currentWorld || newFollow)) {
this.scheduledPan = player.location;
if(newFollow && store.state.configuration.followZoom) {
console.log(`Setting zoom for new follow ${store.state.configuration.followZoom}`);
this.scheduledZoom = store.state.configuration.followZoom;
}
console.log(`Switching map to match player ${targetWorld.name} ${map.name}`);
store.commit(MutationTypes.SET_CURRENT_MAP, {worldName: targetWorld.name, mapName: map.name});
} else {
this.leaflet!.panTo(store.state.currentMap?.locationToLatLng(player.location));
if(newFollow && store.state.configuration.followZoom) {
console.log(`Setting zoom for new follow ${store.state.configuration.followZoom}`);
this.leaflet!.setZoom(store.state.configuration.followZoom);
}
}
} }
} }
}) })

View File

@ -62,11 +62,11 @@ export default defineComponent({
} }
}), }),
followTarget = computed(() => store.state.followTarget ? store.state.followTarget.name : undefined), followTarget = computed(() => store.state.followTarget?.name),
pan = () => { pan = () => {
if(!props.player.hidden) { if(!props.player.hidden) {
store.commit(MutationTypes.SET_PAN_TARGET, props.player); store.commit(MutationTypes.SET_VIEW_TARGET, {location: props.player.location});
} }
}, },

7
src/index.d.ts vendored
View File

@ -83,6 +83,13 @@ interface LiveAtlasBounds {
world?: string; world?: string;
} }
interface LiveAtlasMapViewTarget {
location: LiveAtlasLocation | LiveAtlasBounds;
map?: string;
zoom?: number;
options?: FitBoundsOptions;
}
interface LiveAtlasGlobalConfig { interface LiveAtlasGlobalConfig {
servers: Map<string, LiveAtlasServerDefinition>; servers: Map<string, LiveAtlasServerDefinition>;
messages: LiveAtlasGlobalMessageConfig; messages: LiveAtlasGlobalMessageConfig;

View File

@ -44,10 +44,10 @@ export enum MutationTypes {
CLEAR_PARSED_URL = 'clearParsedUrl', CLEAR_PARSED_URL = 'clearParsedUrl',
SET_FOLLOW_TARGET = 'setFollowTarget', SET_FOLLOW_TARGET = 'setFollowTarget',
SET_PAN_TARGET = 'setPanTarget', SET_VIEW_TARGET = 'setViewTarget',
CLEAR_FOLLOW_TARGET = 'clearFollow', CLEAR_FOLLOW_TARGET = 'clearFollow',
CLEAR_PAN_TARGET = 'clearPanTarget', CLEAR_VIEW_TARGET = 'clearViewTarget',
SET_SCREEN_SIZE = 'setScreenSize', SET_SCREEN_SIZE = 'setScreenSize',
TOGGLE_UI_ELEMENT_VISIBILITY = 'toggleUIElementVisibility', TOGGLE_UI_ELEMENT_VISIBILITY = 'toggleUIElementVisibility',

View File

@ -39,7 +39,7 @@ import {
LiveAtlasPartialComponentConfig, LiveAtlasPartialComponentConfig,
LiveAtlasComponentConfig, LiveAtlasComponentConfig,
LiveAtlasUIModal, LiveAtlasUIModal,
LiveAtlasSidebarSectionState, LiveAtlasMarker LiveAtlasSidebarSectionState, LiveAtlasMarker, LiveAtlasMapViewTarget
} from "@/index"; } from "@/index";
import DynmapMapProvider from "@/providers/DynmapMapProvider"; import DynmapMapProvider from "@/providers/DynmapMapProvider";
import Pl3xmapMapProvider from "@/providers/Pl3xmapMapProvider"; import Pl3xmapMapProvider from "@/providers/Pl3xmapMapProvider";
@ -79,9 +79,9 @@ export type Mutations<S = State> = {
[MutationTypes.SET_PARSED_URL](state: S, payload: LiveAtlasParsedUrl): void [MutationTypes.SET_PARSED_URL](state: S, payload: LiveAtlasParsedUrl): void
[MutationTypes.CLEAR_PARSED_URL](state: S): void [MutationTypes.CLEAR_PARSED_URL](state: S): void
[MutationTypes.SET_FOLLOW_TARGET](state: S, payload: LiveAtlasPlayer): void [MutationTypes.SET_FOLLOW_TARGET](state: S, payload: LiveAtlasPlayer): void
[MutationTypes.SET_PAN_TARGET](state: S, payload: LiveAtlasPlayer): void [MutationTypes.SET_VIEW_TARGET](state: S, payload: LiveAtlasMapViewTarget): void
[MutationTypes.CLEAR_FOLLOW_TARGET](state: S, a?: void): void [MutationTypes.CLEAR_FOLLOW_TARGET](state: S, a?: void): void
[MutationTypes.CLEAR_PAN_TARGET](state: S, a?: void): void [MutationTypes.CLEAR_VIEW_TARGET](state: S, a?: void): void
[MutationTypes.SET_SCREEN_SIZE](state: S, payload: {width: number, height: number}): void [MutationTypes.SET_SCREEN_SIZE](state: S, payload: {width: number, height: number}): void
[MutationTypes.TOGGLE_UI_ELEMENT_VISIBILITY](state: S, payload: LiveAtlasUIElement): void [MutationTypes.TOGGLE_UI_ELEMENT_VISIBILITY](state: S, payload: LiveAtlasUIElement): void
@ -163,7 +163,7 @@ export const mutations: MutationTree<State> & Mutations = {
state.maps.clear(); state.maps.clear();
state.followTarget = undefined; state.followTarget = undefined;
state.panTarget = undefined; state.viewTarget = undefined;
state.currentWorldState.timeOfDay = 0; state.currentWorldState.timeOfDay = 0;
state.currentWorldState.raining = false; state.currentWorldState.raining = false;
@ -448,9 +448,9 @@ export const mutations: MutationTree<State> & Mutations = {
}, },
//Set the pan target, which the map will immediately pan to once //Set the pan target, which the map will immediately pan to once
[MutationTypes.SET_PAN_TARGET](state: State, player: LiveAtlasPlayer) { [MutationTypes.SET_VIEW_TARGET](state: State, target: LiveAtlasMapViewTarget) {
state.followTarget = undefined; state.followTarget = undefined;
state.panTarget = player; state.viewTarget = target;
}, },
//Clear the follow target //Clear the follow target
@ -459,8 +459,8 @@ export const mutations: MutationTree<State> & Mutations = {
}, },
//Clear the pan target //Clear the pan target
[MutationTypes.CLEAR_PAN_TARGET](state: State) { [MutationTypes.CLEAR_VIEW_TARGET](state: State) {
state.panTarget = undefined; state.viewTarget = undefined;
}, },
[MutationTypes.SET_SCREEN_SIZE](state: State, payload: {width: number, height: number}): void { [MutationTypes.SET_SCREEN_SIZE](state: State, payload: {width: number, height: number}): void {
@ -532,7 +532,7 @@ export const mutations: MutationTree<State> & Mutations = {
//Cleanup for switching servers or reloading the configuration //Cleanup for switching servers or reloading the configuration
[MutationTypes.RESET](state: State): void { [MutationTypes.RESET](state: State): void {
state.followTarget = undefined; state.followTarget = undefined;
state.panTarget = undefined; state.viewTarget = undefined;
state.players.clear(); state.players.clear();
state.sortedPlayers.splice(0, state.sortedPlayers.length); state.sortedPlayers.splice(0, state.sortedPlayers.length);

View File

@ -36,7 +36,7 @@ import {
LiveAtlasChat, LiveAtlasChat,
LiveAtlasUIModal, LiveAtlasUIModal,
LiveAtlasSidebarSectionState, LiveAtlasSidebarSectionState,
LiveAtlasMarker LiveAtlasMarker, LiveAtlasMapViewTarget
} from "@/index"; } from "@/index";
import LiveAtlasMapDefinition from "@/model/LiveAtlasMapDefinition"; import LiveAtlasMapDefinition from "@/model/LiveAtlasMapDefinition";
import {getMessages} from "@/util"; import {getMessages} from "@/util";
@ -70,7 +70,7 @@ export type State = {
pendingTileUpdates: Array<DynmapTileUpdate>; pendingTileUpdates: Array<DynmapTileUpdate>;
followTarget?: LiveAtlasPlayer; followTarget?: LiveAtlasPlayer;
panTarget?: LiveAtlasPlayer; viewTarget?: LiveAtlasMapViewTarget;
currentMapProvider?: Readonly<LiveAtlasMapProvider>; currentMapProvider?: Readonly<LiveAtlasMapProvider>;
currentServer?: LiveAtlasServerDefinition; currentServer?: LiveAtlasServerDefinition;
@ -181,7 +181,7 @@ export const state: State = {
}, },
followTarget: undefined, followTarget: undefined,
panTarget: undefined, viewTarget: undefined,
currentMapProvider: undefined, currentMapProvider: undefined,
currentServer: undefined, currentServer: undefined,