Implement link copying and url handling

This commit is contained in:
James Lyne 2020-12-13 02:50:17 +00:00
parent a0326b00d1
commit 9e28e695ee
13 changed files with 252 additions and 39 deletions

44
package-lock.json generated
View File

@ -1317,6 +1317,12 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"@types/clipboard": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@types/clipboard/-/clipboard-2.0.1.tgz",
"integrity": "sha512-gJJX9Jjdt3bIAePQRRjYWG20dIhAgEqonguyHxXuqALxsoDsDLimihqrSg8fXgVTJ4KZCzkfglKtwsh/8dLfbA==",
"dev": true
},
"@types/connect": { "@types/connect": {
"version": "3.4.33", "version": "3.4.33",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.33.tgz", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.33.tgz",
@ -3929,6 +3935,17 @@
"integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==",
"dev": true "dev": true
}, },
"clipboard": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.6.tgz",
"integrity": "sha512-g5zbiixBRk/wyKakSwCKd7vQXDjFnAMGHoEyBogG/bw9kTD9GvdAvaoRR1ALcEzt3pVKxZR0pViekPMIS0QyGg==",
"dev": true,
"requires": {
"good-listener": "^1.2.2",
"select": "^1.1.2",
"tiny-emitter": "^2.0.0"
}
},
"clipboardy": { "clipboardy": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-2.3.0.tgz", "resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-2.3.0.tgz",
@ -4921,6 +4938,12 @@
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
"dev": true "dev": true
}, },
"delegate": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz",
"integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==",
"dev": true
},
"depd": { "depd": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
@ -6616,6 +6639,15 @@
"slash": "^2.0.0" "slash": "^2.0.0"
} }
}, },
"good-listener": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz",
"integrity": "sha1-1TswzfkxPf+33JoNR3CWqm0UXFA=",
"dev": true,
"requires": {
"delegate": "^3.1.2"
}
},
"graceful-fs": { "graceful-fs": {
"version": "4.2.4", "version": "4.2.4",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz",
@ -10528,6 +10560,12 @@
"ajv-keywords": "^3.5.2" "ajv-keywords": "^3.5.2"
} }
}, },
"select": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz",
"integrity": "sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0=",
"dev": true
},
"select-hose": { "select-hose": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz",
@ -11846,6 +11884,12 @@
"integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=", "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=",
"dev": true "dev": true
}, },
"tiny-emitter": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
"integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==",
"dev": true
},
"tmp": { "tmp": {
"version": "0.0.33", "version": "0.0.33",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",

View File

@ -12,6 +12,7 @@
"vue": "^3.0.0" "vue": "^3.0.0"
}, },
"devDependencies": { "devDependencies": {
"@types/clipboard": "^2.0.1",
"@types/leaflet": "^1.5.19", "@types/leaflet": "^1.5.19",
"@typescript-eslint/eslint-plugin": "^4.1.0", "@typescript-eslint/eslint-plugin": "^4.1.0",
"@typescript-eslint/parser": "^4.1.0", "@typescript-eslint/parser": "^4.1.0",
@ -22,6 +23,7 @@
"@vue/compiler-sfc": "^3.0.0", "@vue/compiler-sfc": "^3.0.0",
"@vue/eslint-config-typescript": "^5.0.2", "@vue/eslint-config-typescript": "^5.0.2",
"babel-eslint": "^10.1.0", "babel-eslint": "^10.1.0",
"clipboard": "^2.0.6",
"eslint": "^7.5.0", "eslint": "^7.5.0",
"eslint-plugin-vue": "^7.0.0-0", "eslint-plugin-vue": "^7.0.0-0",
"leaflet": "^1.7.1", "leaflet": "^1.7.1",

View File

@ -9,6 +9,8 @@ import Map from './components/Map.vue';
import Sidebar from './components/Sidebar.vue'; import Sidebar from './components/Sidebar.vue';
import {useStore} from "./store"; import {useStore} from "./store";
import {ActionTypes} from "@/store/action-types"; import {ActionTypes} from "@/store/action-types";
import Util from '@/util';
import {MutationTypes} from "@/store/mutation-types";
export default defineComponent({ export default defineComponent({
name: 'App', name: 'App',
@ -18,9 +20,11 @@ export default defineComponent({
}, },
setup() { setup() {
const store = useStore(), const initialUrl = window.location.hash.replace('#', ''),
store = useStore(),
updateInterval = computed(() => store.state.configuration.updateInterval), updateInterval = computed(() => store.state.configuration.updateInterval),
title = computed(() => store.state.configuration.title), title = computed(() => store.state.configuration.title),
currentUrl = computed(() => store.getters.url),
updatesEnabled = ref(false), updatesEnabled = ref(false),
updateTimeout = ref(0), updateTimeout = ref(0),
@ -52,12 +56,28 @@ export default defineComponent({
} }
updateTimeout.value = 0; updateTimeout.value = 0;
},
parseUrl = () => {
if(!initialUrl) {
return;
}
try {
const result = Util.parseMapHash(initialUrl);
store.commit(MutationTypes.SET_PARSED_URL, result);
} catch(e) {
console.warn('Ignoring invalid url ' + e);
}
}; };
watch(title, (title) => document.title = title); watch(title, (title) => document.title = title);
watch(currentUrl, (url) => window.history.replaceState({}, '', url));
onMounted(() => loadConfiguration()); onMounted(() => loadConfiguration());
onBeforeUnmount(() => stopUpdates()); onBeforeUnmount(() => stopUpdates());
parseUrl();
}, },
}); });
</script> </script>

View File

@ -12,8 +12,8 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import {defineComponent, computed} from "@vue/runtime-core"; import {computed, defineComponent} from "@vue/runtime-core";
import {LatLng, CRS} from 'leaflet'; import {CRS, LatLng} 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";
@ -23,7 +23,7 @@ import ClockControl from "@/components/map/control/ClockControl.vue";
import LinkControl from "@/components/map/control/LinkControl.vue"; import LinkControl from "@/components/map/control/LinkControl.vue";
import LogoControl from "@/components/map/control/LogoControl.vue"; import LogoControl from "@/components/map/control/LogoControl.vue";
import {MutationTypes} from "@/store/mutation-types"; import {MutationTypes} from "@/store/mutation-types";
import {DynmapPlayer} from "@/dynmap"; import {Coordinate, DynmapPlayer} from "@/dynmap";
import {ActionTypes} from "@/store/action-types"; import {ActionTypes} from "@/store/action-types";
import DynmapMap from "@/leaflet/DynmapMap"; import DynmapMap from "@/leaflet/DynmapMap";
@ -85,9 +85,36 @@ export default defineComponent({
}, },
deep: true deep: true
}, },
currentWorld(newValue) { currentWorld(newValue, oldValue) {
const store = useStore();
if(newValue) { if(newValue) {
useStore().dispatch(ActionTypes.GET_MARKER_SETS, undefined); let location: Coordinate;
let zoom: number;
store.dispatch(ActionTypes.GET_MARKER_SETS, undefined);
if(oldValue || !store.state.parsedUrl.location) {
location = newValue.center;
} else {
location = store.state.parsedUrl.location;
}
if(!oldValue) {
zoom = store.state.parsedUrl.zoom || store.state.configuration.defaultZoom;
}
//Delay the pan a frame, to allow the projection to be updated by the new world
requestAnimationFrame(() => {
this.leaflet!.panTo(this.currentProjection.locationToLatLng(location), {
animate: false,
noMoveStart: true,
});
this.leaflet!.setZoom(zoom, {
animate: false,
});
});
} }
}, },
configuration: { configuration: {
@ -100,7 +127,7 @@ export default defineComponent({
} }
}, },
deep: true, deep: true,
} },
}, },
mounted() { mounted() {
@ -119,16 +146,12 @@ export default defineComponent({
})); }));
this.leaflet.on('moveend', () => { this.leaflet.on('moveend', () => {
const location = this.currentProjection.latLngToLocation(this.leaflet!.getCenter(), 64), useStore().commit(MutationTypes.SET_CURRENT_LOCATION, this.currentProjection.latLngToLocation(this.leaflet!.getCenter(), 64));
locationString = `${Math.round(location.x)},${Math.round(location.y)},${Math.round(location.z)}`, });
url = `#${this.currentWorld!.name};${this.currentMap!.name};${locationString}`;
window.history.replaceState({ this.leaflet.on('zoomend', () => {
location, useStore().commit(MutationTypes.SET_CURRENT_ZOOM, this.leaflet!.getZoom());
world: this.currentWorld!.name, });
map: this.currentMap!.name,
}, '', url);
})
}, },
methods: { methods: {

View File

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import {defineComponent, onMounted, onUnmounted, computed, watch} from "@vue/runtime-core"; import {defineComponent, onUnmounted, computed, watch} from "@vue/runtime-core";
import {DynmapWorldMap} from "@/dynmap"; import {DynmapWorldMap} from "@/dynmap";
import {Map} from 'leaflet'; import {Map} from 'leaflet';
import {useStore} from "@/store"; import {useStore} from "@/store";
@ -36,12 +36,9 @@ export default defineComponent({
active = computed(() => props.map === store.state.currentMap), active = computed(() => props.map === store.state.currentMap),
enableLayer = () => { enableLayer = () => {
console.log('Set current projection');
useStore().commit(MutationTypes.SET_CURRENT_PROJECTION, layer.getProjection()); useStore().commit(MutationTypes.SET_CURRENT_PROJECTION, layer.getProjection());
props.leaflet.addLayer(layer); props.leaflet.addLayer(layer);
props.leaflet.panTo(layer.getProjection().locationToLatLng(props.map.world.center), {
noMoveStart: true,
animate: false,
});
stopUpdateWatch = watch(pendingUpdates, (newValue, oldValue) => { stopUpdateWatch = watch(pendingUpdates, (newValue, oldValue) => {
if(newValue && !oldValue && !updateFrame) { if(newValue && !oldValue && !updateFrame) {
@ -75,13 +72,11 @@ export default defineComponent({
}); });
}; };
watch(active, (newValue) => newValue ? enableLayer() : disableLayer());
onMounted(() => {
if(active.value) { if(active.value) {
enableLayer(); enableLayer();
} }
});
watch(active, (newValue) => newValue ? enableLayer() : disableLayer());
onUnmounted(() => { onUnmounted(() => {
disableLayer(); disableLayer();

7
src/dynmap.d.ts vendored
View File

@ -251,3 +251,10 @@ interface DynmapTileUpdate {
name: string name: string
timestamp: number timestamp: number
} }
interface DynmapParsedUrl {
world?: string;
map?: string;
location?: Coordinate;
zoom?: number;
}

View File

@ -1,6 +1,7 @@
import {Control, ControlOptions, DomUtil, Map} from 'leaflet'; import {Control, ControlOptions, DomUtil, Map} from 'leaflet';
import {useStore} from "@/store"; import {useStore} from "@/store";
import linkIcon from '@/assets/icons/link.svg'; import linkIcon from '@/assets/icons/link.svg';
import ClipboardJS from 'clipboard';
export class LinkControl extends Control { export class LinkControl extends Control {
// @ts-ignore // @ts-ignore
@ -22,9 +23,8 @@ export class LinkControl extends Control {
<use xlink:href="${linkIcon.url}" /> <use xlink:href="${linkIcon.url}" />
</svg>`; </svg>`;
linkButton.addEventListener('click', () => { new ClipboardJS(linkButton, {
const projection = useStore().state.currentProjection; text: () => window.location.href.split("#")[0] + useStore().getters.url,
console.log(projection.latLngToLocation(this._map!.getCenter(), 64));
}); });
return linkButton; return linkButton;

View File

@ -9,8 +9,8 @@ import {
DynmapConfigurationResponse, DynmapLineUpdate, DynmapConfigurationResponse, DynmapLineUpdate,
DynmapMarkerSet, DynmapMarkerSet,
DynmapMarkerUpdate, DynmapMarkerUpdate,
DynmapPlayer, DynmapServerConfig, DynmapTileUpdate, DynmapPlayer, DynmapTileUpdate,
DynmapUpdateResponse DynmapUpdateResponse, DynmapWorld
} from "@/dynmap"; } from "@/dynmap";
type AugmentedActionContext = { type AugmentedActionContext = {
@ -57,17 +57,52 @@ export interface Actions {
} }
export const actions: ActionTree<State, State> & Actions = { export const actions: ActionTree<State, State> & Actions = {
[ActionTypes.LOAD_CONFIGURATION]({commit}): Promise<DynmapConfigurationResponse> { [ActionTypes.LOAD_CONFIGURATION]({commit, state}): Promise<DynmapConfigurationResponse> {
return API.getConfiguration().then(config => { return API.getConfiguration().then(config => {
commit(MutationTypes.SET_CONFIGURATION, config.config); commit(MutationTypes.SET_CONFIGURATION, config.config);
commit(MutationTypes.SET_MESSAGES, config.messages); commit(MutationTypes.SET_MESSAGES, config.messages);
commit(MutationTypes.SET_WORLDS, config.worlds); commit(MutationTypes.SET_WORLDS, config.worlds);
commit(MutationTypes.SET_COMPONENTS, config.components); commit(MutationTypes.SET_COMPONENTS, config.components);
if(config.config.defaultWorld && config.config.defaultMap) { let worldName, mapName;
// Use config default world if it exists
if(config.config.defaultWorld && state.worlds.has(config.config.defaultWorld)) {
worldName = config.config.defaultWorld;
}
// Prefer world from parsed url if present and it exists
if(state.parsedUrl.world && state.worlds.has(state.parsedUrl.world)) {
worldName = state.parsedUrl.world;
}
// Use first world, if any, if neither of the above exist
if(!worldName) {
worldName = state.worlds.size ? state.worlds.entries().next().value.name : undefined;
}
if(worldName) {
const world = state.worlds.get(worldName) as DynmapWorld;
// Use config default map if it exists
if(config.config.defaultMap && world.maps.has(config.config.defaultMap)) {
mapName = config.config.defaultMap;
}
// Prefer map from parsed url if present and it exists
if(state.parsedUrl.map && world.maps.has(state.parsedUrl.map)) {
mapName = state.parsedUrl.map;
}
// Use first map, if any, if neither of the above exist
if(!mapName) {
mapName = world.maps.size ? world.maps.entries().next().value.name : undefined;
}
}
if(worldName && mapName) {
commit(MutationTypes.SET_CURRENT_MAP, { commit(MutationTypes.SET_CURRENT_MAP, {
worldName: config.config.defaultWorld, worldName, mapName
mapName: config.config.defaultMap
}); });
} }

View File

@ -8,6 +8,7 @@ export type Getters = {
clockControlEnabled(state: State): boolean; clockControlEnabled(state: State): boolean;
night(state: State): boolean; night(state: State): boolean;
mapBackground(state: State, getters: GetterTree<State, State> & Getters): string; mapBackground(state: State, getters: GetterTree<State, State> & Getters): string;
url(state: State, getters: GetterTree<State, State> & Getters): string;
} }
export const getters: GetterTree<State, State> & Getters = { export const getters: GetterTree<State, State> & Getters = {
@ -41,5 +42,19 @@ export const getters: GetterTree<State, State> & Getters = {
} }
return state.currentMap.background || 'transparent'; return state.currentMap.background || 'transparent';
},
url(state: State): string {
const x = Math.round(state.currentLocation.x),
y = Math.round(state.currentLocation.y),
z = Math.round(state.currentLocation.z),
locationString = `${x},${y},${z}`,
zoom = state.currentZoom;
if(!state.currentWorld || !state.currentMap) {
return '';
}
return `#${state.currentWorld.name};${state.currentMap.name};${locationString};${zoom}`;
} }
} }

View File

@ -20,6 +20,9 @@ export enum MutationTypes {
SYNC_PLAYERS = 'syncPlayers', SYNC_PLAYERS = 'syncPlayers',
SET_CURRENT_MAP = 'setCurrentMap', SET_CURRENT_MAP = 'setCurrentMap',
SET_CURRENT_PROJECTION = 'setCurrentProjection', SET_CURRENT_PROJECTION = 'setCurrentProjection',
SET_CURRENT_LOCATION = 'setCurrentLocation',
SET_CURRENT_ZOOM = 'setCurrentZoom',
SET_PARSED_URL = 'setParsedUrl',
FOLLOW_PLAYER = 'followPlayer', FOLLOW_PLAYER = 'followPlayer',
CLEAR_FOLLOW = 'clearFollow', CLEAR_FOLLOW = 'clearFollow',
} }

View File

@ -8,7 +8,7 @@ import {
DynmapCircleUpdate, DynmapCircleUpdate,
DynmapComponentConfig, DynmapComponentConfig,
DynmapLine, DynmapLine,
DynmapLineUpdate, DynmapLineUpdate, Coordinate,
DynmapMarker, DynmapMarker,
DynmapMarkerSet, DynmapMarkerSet,
DynmapMarkerSetUpdates, DynmapMarkerSetUpdates,
@ -17,7 +17,7 @@ import {
DynmapPlayer, DynmapPlayer,
DynmapServerConfig, DynmapTileUpdate, DynmapServerConfig, DynmapTileUpdate,
DynmapWorld, DynmapWorld,
DynmapWorldState DynmapWorldState, DynmapParsedUrl
} from "@/dynmap"; } from "@/dynmap";
import {DynmapProjection} from "@/leaflet/projection/DynmapProjection"; import {DynmapProjection} from "@/leaflet/projection/DynmapProjection";
@ -49,6 +49,9 @@ export type Mutations<S = State> = {
[MutationTypes.SYNC_PLAYERS](state: S, keep: Set<string>): void [MutationTypes.SYNC_PLAYERS](state: S, keep: Set<string>): void
[MutationTypes.SET_CURRENT_MAP](state: S, payload: CurrentMapPayload): void [MutationTypes.SET_CURRENT_MAP](state: S, payload: CurrentMapPayload): void
[MutationTypes.SET_CURRENT_PROJECTION](state: S, payload: DynmapProjection): void [MutationTypes.SET_CURRENT_PROJECTION](state: S, payload: DynmapProjection): void
[MutationTypes.SET_CURRENT_LOCATION](state: S, payload: Coordinate): void
[MutationTypes.SET_CURRENT_ZOOM](state: S, payload: number): void
[MutationTypes.SET_PARSED_URL](state: S, payload: DynmapParsedUrl): void
[MutationTypes.FOLLOW_PLAYER](state: S, payload: DynmapPlayer): void [MutationTypes.FOLLOW_PLAYER](state: S, payload: DynmapPlayer): void
[MutationTypes.CLEAR_FOLLOW](state: S, a?: void): void [MutationTypes.CLEAR_FOLLOW](state: S, a?: void): void
} }
@ -288,6 +291,18 @@ export const mutations: MutationTree<State> & Mutations = {
state.currentProjection = projection; state.currentProjection = projection;
}, },
[MutationTypes.SET_CURRENT_LOCATION](state: State, payload: Coordinate) {
state.currentLocation = payload;
},
[MutationTypes.SET_CURRENT_ZOOM](state: State, payload: number) {
state.currentZoom = payload;
},
[MutationTypes.SET_PARSED_URL](state: State, payload: DynmapParsedUrl) {
state.parsedUrl = payload;
},
[MutationTypes.FOLLOW_PLAYER](state: State, player: DynmapPlayer) { [MutationTypes.FOLLOW_PLAYER](state: State, player: DynmapPlayer) {
state.following = player; state.following = player;
}, },

View File

@ -4,7 +4,7 @@ import {
DynmapMessageConfig, DynmapMessageConfig,
DynmapPlayer, DynmapPlayer,
DynmapServerConfig, DynmapTileUpdate, DynmapServerConfig, DynmapTileUpdate,
DynmapWorld, DynmapWorldState DynmapWorld, DynmapWorldState, Coordinate, DynmapParsedUrl
} from "@/dynmap"; } from "@/dynmap";
import {DynmapProjection} from "@/leaflet/projection/DynmapProjection"; import {DynmapProjection} from "@/leaflet/projection/DynmapProjection";
@ -26,10 +26,14 @@ export type State = {
currentWorldState: DynmapWorldState; currentWorldState: DynmapWorldState;
currentWorld?: DynmapWorld; currentWorld?: DynmapWorld;
currentMap?: DynmapWorldMap; currentMap?: DynmapWorldMap;
currentLocation: Coordinate;
currentZoom: number;
currentProjection: DynmapProjection; currentProjection: DynmapProjection;
updateRequestId: number; updateRequestId: number;
updateTimestamp: Date; updateTimestamp: Date;
parsedUrl: DynmapParsedUrl;
} }
export const state: State = { export const state: State = {
@ -100,6 +104,13 @@ export const state: State = {
currentWorld: undefined, currentWorld: undefined,
currentMap: undefined, currentMap: undefined,
currentLocation: {
x: 0,
y: 0,
z: 0,
},
currentZoom: 0,
currentProjection: new DynmapProjection(), //Projection for converting location <-> latlg. Object itself isn't reactive for performance reasons currentProjection: new DynmapProjection(), //Projection for converting location <-> latlg. Object itself isn't reactive for performance reasons
currentWorldState: { currentWorldState: {
raining: false, raining: false,
@ -109,4 +120,11 @@ export const state: State = {
updateRequestId: 0, updateRequestId: 0,
updateTimestamp: new Date(), updateTimestamp: new Date(),
parsedUrl: {
world: undefined,
map: undefined,
location: undefined,
zoom: undefined,
}
}; };

View File

@ -58,5 +58,41 @@ export default {
return (x: number, y: number, z: number) => { return (x: number, y: number, z: number) => {
return projection.locationToLatLng({x, y, z}); return projection.locationToLatLng({x, y, z});
}; };
},
parseMapHash(hash: string) {
const parts = hash.replace('#', '').split(';');
if(parts.length < 3) {
throw new TypeError('Not enough parts');
}
const world = parts[0],
map = parts[1],
location = parts[2].split(',').map(item => parseFloat(item)),
zoom = parts[3] ? parseInt(parts[3]) : undefined;
if(location.length !== 3) {
throw new TypeError('Location should contain exactly 3 numbers');
}
if(location.filter(item => isNaN(item) || !isFinite(item)).length) {
throw new TypeError('Invalid value in location');
}
if(typeof zoom !== 'undefined' && (isNaN(zoom) || zoom < 0 || !isFinite(zoom))) {
throw new TypeError('Invalid value for zoom');
}
return {
world,
map,
location: {
x: location[0],
y: location[1],
z: location[2],
},
zoom,
}
} }
} }