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">
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 MapLayer from "@/components/map/layer/MapLayer.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 {LoadingControl} from "@/leaflet/control/LoadingControl";
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";
export default defineComponent({
@ -88,12 +88,11 @@ export default defineComponent({
mapBackground = computed(() => store.getters.mapBackground),
followTarget = computed(() => store.state.followTarget),
panTarget = computed(() => store.state.panTarget),
viewTarget = computed(() => store.state.viewTarget),
parsedUrl = computed(() => store.state.parsedUrl),
//Location and zoom to pan to upon next projection change
scheduledPan = ref<Coordinate|null>(null),
scheduledZoom = ref<number|null>(null),
scheduledView = ref<LiveAtlasMapViewTarget|null>(null),
mapTitle = computed(() => store.state.messages.mapTitle);
@ -112,15 +111,14 @@ export default defineComponent({
logoControls,
followTarget,
panTarget,
viewTarget,
parsedUrl,
mapBackground,
currentWorld,
currentMap,
scheduledPan,
scheduledZoom,
scheduledView,
mapTitle
}
@ -135,85 +133,82 @@ export default defineComponent({
},
deep: true
},
panTarget(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_PAN_TARGET, undefined);
}
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);
}
this.updateFollow(newValue, false);
}
this.setView(newValue);
}
},
deep: true
},
currentMap(newValue, oldValue) {
if(this.leaflet && newValue) {
let panTarget = this.scheduledPan;
let viewTarget = this.scheduledView;
if(!panTarget && oldValue) {
panTarget = oldValue.latLngToLocation(this.leaflet.getCenter(), 64);
} else if(!panTarget) {
panTarget = {x: 0, y: 0, z: 0};
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};
}
if(this.scheduledZoom) {
this.leaflet!.setZoom(this.scheduledZoom, {
animate: false,
});
}
this.leaflet.panTo(newValue.locationToLatLng(panTarget), {
viewTarget.options = {
animate: false,
noMoveStart: true,
});
noMoveStart: false,
}
this.scheduledZoom = null;
this.scheduledPan = null;
this.setView(viewTarget);
this.scheduledView = null;
}
},
currentWorld(newValue, oldValue) {
const store = useStore();
if(newValue) {
let location: Coordinate | null = this.scheduledPan;
let viewTarget = this.scheduledView || {} as LiveAtlasMapViewTarget;
// Abort if follow target is present, to avoid panning twice
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.
// Also clear it to allow repeated panning.
} else if(store.state.panTarget && store.state.panTarget.location.world === newValue.name) {
store.commit(MutationTypes.CLEAR_PAN_TARGET, undefined);
} else if(store.state.viewTarget && store.state.viewTarget.location.world === newValue.name) {
store.commit(MutationTypes.CLEAR_VIEW_TARGET, undefined);
return;
// Otherwise pan to url location, if present
} else if(store.state.parsedUrl?.location) {
location = store.state.parsedUrl.location;
viewTarget.location = store.state.parsedUrl.location;
if(!oldValue) {
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') {
this.scheduledZoom = newValue.defaultZoom;
viewTarget.zoom = newValue.defaultZoom;
} else {
this.scheduledZoom = store.state.configuration.defaultZoom;
viewTarget.zoom = store.state.configuration.defaultZoom;
}
}
store.commit(MutationTypes.CLEAR_PARSED_URL, undefined);
// Otherwise pan to world center
} else {
location = newValue.center;
viewTarget.location = newValue.center;
}
if(this.scheduledZoom == null) {
if(viewTarget.zoom == null) {
if(typeof newValue.defaultZoom !== 'undefined') {
this.scheduledZoom = newValue.defaultZoom;
viewTarget.zoom = newValue.defaultZoom;
} else {
this.scheduledZoom = store.state.configuration.defaultZoom;
viewTarget.zoom = store.state.configuration.defaultZoom;
}
}
//Set pan location for when the projection changes
this.scheduledPan = location;
this.scheduledView = viewTarget;
}
},
parsedUrl: {
@ -222,36 +217,15 @@ export default defineComponent({
return;
}
//URL points to different map
if(newValue.world !== this.currentWorld!.name || newValue.map !== this.currentMap!.name) {
//Set scheduled pan for after map change
this.scheduledPan = newValue.location;
this.scheduledZoom = newValue.zoom;
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), {
this.setView({
location: {...newValue.location, world: newValue.world},
map: newValue.map,
zoom: newValue.zoom,
options: {
animate: false,
noMoveStart: true,
});
}
}
});
},
deep: true,
}
@ -304,61 +278,73 @@ export default defineComponent({
this.leaflet.getContainer().focus();
}
},
updateFollow(player: LiveAtlasPlayer, newFollow: boolean) {
setView(target: LiveAtlasMapViewTarget) {
const store = useStore(),
followMapName = store.state.configuration.followMap,
currentWorld = store.state.currentWorld;
let targetWorld = null;
currentWorld = store.state.currentWorld,
currentMap = store.state.currentMap?.name,
targetWorld = target.location.world ? store.state.worlds.get(target.location.world) : currentWorld;
if(!this.leaflet) {
console.warn(`Cannot follow ${player.name}. Map not yet initialized.`);
console.warn('Ignoring setView as leaflet not initialised');
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) {
console.warn(`Cannot follow ${player.name}. Player is hidden from the map.`);
return;
}
if(!player.location.world) {
console.warn(`Cannot follow ${player.name}. Player isn't in a known world.`);
return;
if(!currentWorld || currentWorld.name !== player.location.world || newFollow) {
map = store.state.configuration.followMap;
}
if(!currentWorld || currentWorld.name !== player.location.world) {
targetWorld = store.state.worlds.get(player.location.world);
} else {
targetWorld = currentWorld;
}
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);
}
}
this.setView({
location: player.location,
map,
zoom: (newFollow) ? store.state.configuration.followZoom : undefined,
});
}
}
})

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 = () => {
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;
}
interface LiveAtlasMapViewTarget {
location: LiveAtlasLocation | LiveAtlasBounds;
map?: string;
zoom?: number;
options?: FitBoundsOptions;
}
interface LiveAtlasGlobalConfig {
servers: Map<string, LiveAtlasServerDefinition>;
messages: LiveAtlasGlobalMessageConfig;

View File

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

View File

@ -39,7 +39,7 @@ import {
LiveAtlasPartialComponentConfig,
LiveAtlasComponentConfig,
LiveAtlasUIModal,
LiveAtlasSidebarSectionState, LiveAtlasMarker
LiveAtlasSidebarSectionState, LiveAtlasMarker, LiveAtlasMapViewTarget
} from "@/index";
import DynmapMapProvider from "@/providers/DynmapMapProvider";
import Pl3xmapMapProvider from "@/providers/Pl3xmapMapProvider";
@ -79,9 +79,9 @@ export type Mutations<S = State> = {
[MutationTypes.SET_PARSED_URL](state: S, payload: LiveAtlasParsedUrl): void
[MutationTypes.CLEAR_PARSED_URL](state: S): 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_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.TOGGLE_UI_ELEMENT_VISIBILITY](state: S, payload: LiveAtlasUIElement): void
@ -163,7 +163,7 @@ export const mutations: MutationTree<State> & Mutations = {
state.maps.clear();
state.followTarget = undefined;
state.panTarget = undefined;
state.viewTarget = undefined;
state.currentWorldState.timeOfDay = 0;
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
[MutationTypes.SET_PAN_TARGET](state: State, player: LiveAtlasPlayer) {
[MutationTypes.SET_VIEW_TARGET](state: State, target: LiveAtlasMapViewTarget) {
state.followTarget = undefined;
state.panTarget = player;
state.viewTarget = target;
},
//Clear the follow target
@ -459,8 +459,8 @@ export const mutations: MutationTree<State> & Mutations = {
},
//Clear the pan target
[MutationTypes.CLEAR_PAN_TARGET](state: State) {
state.panTarget = undefined;
[MutationTypes.CLEAR_VIEW_TARGET](state: State) {
state.viewTarget = undefined;
},
[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
[MutationTypes.RESET](state: State): void {
state.followTarget = undefined;
state.panTarget = undefined;
state.viewTarget = undefined;
state.players.clear();
state.sortedPlayers.splice(0, state.sortedPlayers.length);

View File

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