Move and rename player image util methods
This commit is contained in:
parent
f794dcb813
commit
f4481a1d6c
@ -29,12 +29,13 @@ import Sidebar from './components/Sidebar.vue';
|
||||
import ChatBox from './components/ChatBox.vue';
|
||||
import {useStore} from "@/store";
|
||||
import {ActionTypes} from "@/store/action-types";
|
||||
import {clearHeadCache, parseUrl} from '@/util';
|
||||
import {parseUrl} from '@/util';
|
||||
import {hideSplash, showSplash, showSplashError} from '@/util/splash';
|
||||
import {MutationTypes} from "@/store/mutation-types";
|
||||
import {LiveAtlasServerDefinition, LiveAtlasUIElement} from "@/index";
|
||||
import LoginModal from "@/components/login/LoginModal.vue";
|
||||
import {notify} from "@kyvg/vue3-notification";
|
||||
import {clearPlayerImageCache} from "@/util/images";
|
||||
|
||||
export default defineComponent({
|
||||
name: 'App',
|
||||
@ -171,7 +172,7 @@ export default defineComponent({
|
||||
return;
|
||||
}
|
||||
|
||||
clearHeadCache();
|
||||
clearPlayerImageCache();
|
||||
loadingAttempts.value = 0;
|
||||
window.history.replaceState({}, '', newServer.id);
|
||||
loadConfiguration();
|
||||
@ -201,7 +202,7 @@ export default defineComponent({
|
||||
}
|
||||
});
|
||||
watch(playerImageUrl, () => {
|
||||
clearHeadCache();
|
||||
clearPlayerImageCache();
|
||||
});
|
||||
|
||||
handleUrl();
|
||||
|
@ -24,7 +24,7 @@ import {computed, defineComponent, watch} from "@vue/runtime-core";
|
||||
import {LiveAtlasPlayer} from "@/index";
|
||||
import {onMounted, ref} from "vue";
|
||||
import {useStore} from "@/store";
|
||||
import {getMinecraftHead} from "@/util";
|
||||
import {getPlayerImage} from "@/util/images";
|
||||
|
||||
export default defineComponent({
|
||||
name: 'PlayerImage',
|
||||
@ -47,7 +47,7 @@ export default defineComponent({
|
||||
|
||||
if (imagesEnabled.value) {
|
||||
try {
|
||||
const result = await getMinecraftHead(props.player, 'small');
|
||||
const result = await getPlayerImage(props.player, 'small');
|
||||
image.value = result.src;
|
||||
} catch (e) {
|
||||
}
|
||||
|
6
src/index.d.ts
vendored
6
src/index.d.ts
vendored
@ -251,7 +251,7 @@ interface LiveAtlasCircleMarker extends LiveAtlasPathMarker {
|
||||
radius: PointTuple;
|
||||
}
|
||||
|
||||
interface HeadQueueEntry {
|
||||
interface PlayerImageQueueEntry {
|
||||
cacheKey: string;
|
||||
name: string;
|
||||
uuid?: string;
|
||||
@ -277,7 +277,7 @@ interface LiveAtlasComponentConfig {
|
||||
markers?: LiveAtlasPlayerMarkerConfig;
|
||||
showImages: boolean;
|
||||
grayHiddenPlayers: boolean;
|
||||
imageUrl: (entry: HeadQueueEntry) => string;
|
||||
imageUrl: (entry: PlayerImageQueueEntry) => string;
|
||||
};
|
||||
coordinatesControl?: CoordinatesControlOptions;
|
||||
clockControl?: ClockControlOptions;
|
||||
@ -298,7 +298,7 @@ interface LiveAtlasPartialComponentConfig {
|
||||
markers?: LiveAtlasPlayerMarkerConfig;
|
||||
showImages?: boolean;
|
||||
grayHiddenPlayers?: boolean;
|
||||
imageUrl?: (entry: HeadQueueEntry) => string;
|
||||
imageUrl?: (entry: PlayerImageQueueEntry) => string;
|
||||
};
|
||||
coordinatesControl?: CoordinatesControlOptions;
|
||||
clockControl?: ClockControlOptions;
|
||||
|
@ -18,9 +18,9 @@
|
||||
*/
|
||||
|
||||
import {BaseIconOptions, Icon, Layer, LayerOptions, Util} from 'leaflet';
|
||||
import {getImagePixelSize, getMinecraftHead} from '@/util';
|
||||
import defaultImage from '@/assets/images/player_face.png';
|
||||
import {LiveAtlasPlayer, LiveAtlasPlayerImageSize} from "@/index";
|
||||
import {getImagePixelSize, getPlayerImage} from "@/util/images";
|
||||
|
||||
const playerImage: HTMLImageElement = document.createElement('img');
|
||||
playerImage.src = defaultImage;
|
||||
@ -122,7 +122,7 @@ export class PlayerIcon extends Layer implements Icon<PlayerIconOptions> {
|
||||
}
|
||||
|
||||
updateImage() {
|
||||
getMinecraftHead(this._player, this.options.imageSize).then(head => {
|
||||
getPlayerImage(this._player, this.options.imageSize).then(head => {
|
||||
this._playerImage!.src = head.src;
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
@ -30,7 +30,6 @@ import {MutationTypes} from "@/store/mutation-types";
|
||||
import MapProvider from "@/providers/MapProvider";
|
||||
import {
|
||||
getBoundsFromPoints,
|
||||
getDefaultMinecraftHead,
|
||||
getMiddle,
|
||||
guessWorldDimension,
|
||||
runSandboxed,
|
||||
@ -43,6 +42,7 @@ import LiveAtlasMapDefinition from "@/model/LiveAtlasMapDefinition";
|
||||
import {OverviewerProjection} from "@/leaflet/projection/OverviewerProjection";
|
||||
import {LiveAtlasMarkerType} from "@/util/markers";
|
||||
import {useStore} from "@/store";
|
||||
import {getDefaultPlayerImage} from "@/util/images";
|
||||
|
||||
export default class OverviewerMapProvider extends MapProvider {
|
||||
private configurationAbort?: AbortController = undefined;
|
||||
@ -247,7 +247,7 @@ export default class OverviewerMapProvider extends MapProvider {
|
||||
//Not used by Overviewer
|
||||
players: {
|
||||
markers: undefined,
|
||||
imageUrl: getDefaultMinecraftHead,
|
||||
imageUrl: getDefaultPlayerImage,
|
||||
showImages: false,
|
||||
grayHiddenPlayers: false,
|
||||
},
|
||||
|
@ -32,12 +32,13 @@ import LiveAtlasMapDefinition from "@/model/LiveAtlasMapDefinition";
|
||||
import {MutationTypes} from "@/store/mutation-types";
|
||||
import MapProvider from "@/providers/MapProvider";
|
||||
import {ActionTypes} from "@/store/action-types";
|
||||
import {getBoundsFromPoints, getDefaultMinecraftHead, getMiddle, stripHTML, titleColoursRegex} from "@/util";
|
||||
import {getBoundsFromPoints, getMiddle, stripHTML, titleColoursRegex} from "@/util";
|
||||
import {LiveAtlasMarkerType} from "@/util/markers";
|
||||
import {PointTuple} from "leaflet";
|
||||
import ConfigurationError from "@/errors/ConfigurationError";
|
||||
import {Pl3xmapTileLayer} from "@/leaflet/tileLayer/Pl3xmapTileLayer";
|
||||
import {LiveAtlasTileLayer, LiveAtlasTileLayerOptions} from "@/leaflet/tileLayer/LiveAtlasTileLayer";
|
||||
import {getDefaultPlayerImage} from "@/util/images";
|
||||
|
||||
export default class Pl3xmapMapProvider extends MapProvider {
|
||||
private configurationAbort?: AbortController = undefined;
|
||||
@ -120,7 +121,7 @@ export default class Pl3xmapMapProvider extends MapProvider {
|
||||
components: {
|
||||
players: {
|
||||
markers: undefined,
|
||||
imageUrl: getDefaultMinecraftHead,
|
||||
imageUrl: getDefaultPlayerImage,
|
||||
grayHiddenPlayers: true,
|
||||
showImages: true,
|
||||
}
|
||||
@ -214,7 +215,7 @@ export default class Pl3xmapMapProvider extends MapProvider {
|
||||
|
||||
players: {
|
||||
markers: undefined, //Configured per-world
|
||||
imageUrl: getDefaultMinecraftHead,
|
||||
imageUrl: getDefaultPlayerImage,
|
||||
|
||||
//Not configurable
|
||||
showImages: true,
|
||||
|
@ -40,8 +40,9 @@ import {
|
||||
LiveAtlasUIModal,
|
||||
LiveAtlasSidebarSectionState, LiveAtlasMarker, LiveAtlasMapViewTarget
|
||||
} from "@/index";
|
||||
import {getDefaultMinecraftHead, getGlobalMessages} from "@/util";
|
||||
import {getGlobalMessages} from "@/util";
|
||||
import {getServerMapProvider} from "@/util/config";
|
||||
import {getDefaultPlayerImage} from "@/util/images";
|
||||
|
||||
export type CurrentMapPayload = {
|
||||
worldName: string;
|
||||
@ -559,7 +560,7 @@ export const mutations: MutationTree<State> & Mutations = {
|
||||
markers: undefined,
|
||||
showImages: true,
|
||||
grayHiddenPlayers: true,
|
||||
imageUrl: getDefaultMinecraftHead,
|
||||
imageUrl: getDefaultPlayerImage,
|
||||
};
|
||||
state.components.coordinatesControl = undefined;
|
||||
state.components.clockControl = undefined;
|
||||
|
@ -39,7 +39,8 @@ import {
|
||||
LiveAtlasMarker, LiveAtlasMapViewTarget
|
||||
} from "@/index";
|
||||
import LiveAtlasMapDefinition from "@/model/LiveAtlasMapDefinition";
|
||||
import {getDefaultMinecraftHead, getMessages} from "@/util";
|
||||
import {getMessages} from "@/util";
|
||||
import {getDefaultPlayerImage} from "@/util/images";
|
||||
|
||||
export type State = {
|
||||
version: string;
|
||||
@ -162,7 +163,7 @@ export const state: State = {
|
||||
showImages: false,
|
||||
|
||||
// (world-settings.x.player-tracker.heads-url in squaremap)
|
||||
imageUrl: getDefaultMinecraftHead,
|
||||
imageUrl: getDefaultPlayerImage,
|
||||
},
|
||||
|
||||
// Settings for coordinates control
|
||||
|
92
src/util.ts
92
src/util.ts
@ -14,29 +14,21 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import defaultImage from '@/assets/images/player_face.png';
|
||||
import {useStore} from "@/store";
|
||||
import LiveAtlasMapDefinition from "@/model/LiveAtlasMapDefinition";
|
||||
import {
|
||||
Coordinate,
|
||||
HeadQueueEntry,
|
||||
LiveAtlasBounds, LiveAtlasDimension,
|
||||
LiveAtlasBounds,
|
||||
LiveAtlasDimension,
|
||||
LiveAtlasGlobalMessageConfig,
|
||||
LiveAtlasLocation,
|
||||
LiveAtlasMessageConfig,
|
||||
LiveAtlasPlayer,
|
||||
LiveAtlasPlayerImageSize,
|
||||
} from "@/index";
|
||||
import {notify} from "@kyvg/vue3-notification";
|
||||
import {globalMessages, serverMessages} from "../messages";
|
||||
|
||||
const documentRange = document.createRange(),
|
||||
brToSpaceRegex = /<br \/>/g,
|
||||
headCache = new Map<string, HTMLImageElement>(),
|
||||
headUnresolvedCache = new Map<string, Promise<HTMLImageElement>>(),
|
||||
headsLoading = new Set<string>(),
|
||||
|
||||
headQueue: HeadQueueEntry[] = [];
|
||||
brToSpaceRegex = /<br \/>/g;
|
||||
|
||||
export const titleColoursRegex = /§[0-9a-f]/ig;
|
||||
export const netherWorldNameRegex = /[_\s]?nether([\s_]|$)/i;
|
||||
@ -59,84 +51,6 @@ export const getMinecraftTime = (serverTime: number) => {
|
||||
};
|
||||
}
|
||||
|
||||
export const getImagePixelSize = (imageSize: LiveAtlasPlayerImageSize) => {
|
||||
switch(imageSize) {
|
||||
case 'large':
|
||||
case 'body':
|
||||
return 32;
|
||||
|
||||
case 'small':
|
||||
default:
|
||||
return 16;
|
||||
}
|
||||
}
|
||||
|
||||
export const getMinecraftHead = (player: LiveAtlasPlayer | string, size: LiveAtlasPlayerImageSize): Promise<HTMLImageElement> => {
|
||||
const account = typeof player === 'string' ? player : player.name,
|
||||
uuid = typeof player === 'string' ? undefined : player.uuid,
|
||||
cacheKey = `${account}-${size}`;
|
||||
|
||||
if(headCache.has(cacheKey)) {
|
||||
return Promise.resolve(headCache.get(cacheKey) as HTMLImageElement);
|
||||
}
|
||||
|
||||
if(headUnresolvedCache.has(cacheKey)) {
|
||||
return headUnresolvedCache.get(cacheKey) as Promise<HTMLImageElement>;
|
||||
}
|
||||
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
const faceImage = new Image();
|
||||
|
||||
faceImage.onload = function() {
|
||||
headCache.set(cacheKey, faceImage);
|
||||
headsLoading.delete(cacheKey);
|
||||
tickHeadQueue();
|
||||
resolve(faceImage);
|
||||
};
|
||||
|
||||
faceImage.onerror = function(e) {
|
||||
console.warn(`Failed to retrieve face of ${account} with size ${size}!`);
|
||||
headsLoading.delete(cacheKey);
|
||||
tickHeadQueue();
|
||||
reject(e);
|
||||
};
|
||||
|
||||
headQueue.push({
|
||||
name: account,
|
||||
uuid,
|
||||
size,
|
||||
cacheKey,
|
||||
image: faceImage,
|
||||
});
|
||||
}).finally(() => headUnresolvedCache.delete(cacheKey)) as Promise<HTMLImageElement>;
|
||||
|
||||
headUnresolvedCache.set(cacheKey, promise);
|
||||
tickHeadQueue();
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
export const getDefaultMinecraftHead = () => {
|
||||
return defaultImage;
|
||||
}
|
||||
|
||||
const tickHeadQueue = () => {
|
||||
if(headsLoading.size > 8 || !headQueue.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const head = headQueue.pop() as HeadQueueEntry;
|
||||
|
||||
headsLoading.add(head.cacheKey);
|
||||
head.image.src = useStore().state.components.players.imageUrl(head);
|
||||
|
||||
tickHeadQueue();
|
||||
}
|
||||
|
||||
export const clearHeadCache = () => {
|
||||
headCache.clear();
|
||||
}
|
||||
|
||||
export const parseUrl = (url: URL) => {
|
||||
const query = new URLSearchParams(url.search),
|
||||
hash = url.hash.replace('#', '');
|
||||
|
@ -32,8 +32,7 @@ import {
|
||||
} from "@/index";
|
||||
import {getPoints} from "@/util/areas";
|
||||
import {
|
||||
decodeHTMLEntities, getBounds, getImagePixelSize,
|
||||
getMiddle, guessWorldDimension,
|
||||
decodeHTMLEntities, getBounds, getMiddle, guessWorldDimension,
|
||||
stripHTML,
|
||||
titleColoursRegex
|
||||
} from "@/util";
|
||||
@ -53,6 +52,7 @@ import {
|
||||
import {PointTuple} from "leaflet";
|
||||
import {LiveAtlasMarkerType} from "@/util/markers";
|
||||
import {DynmapProjection} from "@/leaflet/projection/DynmapProjection";
|
||||
import {getImagePixelSize} from "@/util/images";
|
||||
|
||||
export function buildServerConfig(response: Options): LiveAtlasServerConfig {
|
||||
let title = 'Dynmap';
|
||||
|
138
src/util/images.ts
Normal file
138
src/util/images.ts
Normal file
@ -0,0 +1,138 @@
|
||||
/*
|
||||
* 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 defaultImage from "@/assets/images/player_face.png";
|
||||
import {PlayerImageQueueEntry, LiveAtlasPlayer, LiveAtlasPlayerImageSize} from "@/index";
|
||||
import {useStore} from "@/store";
|
||||
|
||||
const playerImageCache = new Map<string, HTMLImageElement>(),
|
||||
playerImageUnresolvedCache = new Map<string, Promise<HTMLImageElement>>(),
|
||||
playerImagesLoading = new Set<string>(),
|
||||
|
||||
playerImageQueue: PlayerImageQueueEntry[] = [];
|
||||
|
||||
/**
|
||||
* Returns the corresponding pixel size for the given {@see LiveAtlasPlayerImageSize}
|
||||
* @param {LiveAtlasPlayerImageSize} imageSize The image size to get the pixel size for
|
||||
* @returns The pixel size
|
||||
*/
|
||||
export const getImagePixelSize = (imageSize: LiveAtlasPlayerImageSize) => {
|
||||
switch (imageSize) {
|
||||
case 'large':
|
||||
case 'body':
|
||||
return 32;
|
||||
|
||||
case 'small':
|
||||
default:
|
||||
return 16;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an {@see HTMLImageElement} containing an image representing the given {@see LiveAtlasPlayer}
|
||||
* at the given {@see LiveAtlasPlayerImageSize} and ensures it has loaded successfully
|
||||
*
|
||||
* If an image has previously been loaded for the same player and image size, a cached copy of the image element
|
||||
* will be returned; Otherwise an attempt will be made to load the player image from the URL specified by the current
|
||||
* {@see LiveAtlasMapProvider}
|
||||
*
|
||||
* The number of concurrent image loads is limited and additional loads will be queued. If this method is called
|
||||
* with the same player and image size multiple times, the load will only be queued once and the same element will be
|
||||
* returned for all calls.
|
||||
*
|
||||
* @param {LiveAtlasPlayer} player The player to retrieve the image for
|
||||
* @param {LiveAtlasPlayerImageSize} size The image size to retrieve
|
||||
* @returns {Promise<HTMLImageElement>} A promise which will resolve to a {@see HTMLImageElement} with the loaded player
|
||||
* image as the src. The promise will reject if the image fails to load
|
||||
*/
|
||||
export const getPlayerImage = (player: LiveAtlasPlayer | string, size: LiveAtlasPlayerImageSize): Promise<HTMLImageElement> => {
|
||||
const account = typeof player === 'string' ? player : player.name,
|
||||
uuid = typeof player === 'string' ? undefined : player.uuid,
|
||||
cacheKey = `${account}-${size}`;
|
||||
|
||||
if (playerImageCache.has(cacheKey)) {
|
||||
return Promise.resolve(playerImageCache.get(cacheKey) as HTMLImageElement);
|
||||
}
|
||||
|
||||
if (playerImageUnresolvedCache.has(cacheKey)) {
|
||||
return playerImageUnresolvedCache.get(cacheKey) as Promise<HTMLImageElement>;
|
||||
}
|
||||
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
const faceImage = new Image();
|
||||
|
||||
faceImage.onload = function () {
|
||||
playerImageCache.set(cacheKey, faceImage);
|
||||
playerImagesLoading.delete(cacheKey);
|
||||
tickPlayerImageQueue();
|
||||
resolve(faceImage);
|
||||
};
|
||||
|
||||
faceImage.onerror = function (e) {
|
||||
console.warn(`Failed to retrieve face of ${account} with size ${size}!`);
|
||||
playerImagesLoading.delete(cacheKey);
|
||||
tickPlayerImageQueue();
|
||||
reject(e);
|
||||
};
|
||||
|
||||
playerImageQueue.push({
|
||||
name: account,
|
||||
uuid,
|
||||
size,
|
||||
cacheKey,
|
||||
image: faceImage,
|
||||
});
|
||||
}).finally(() => playerImageUnresolvedCache.delete(cacheKey)) as Promise<HTMLImageElement>;
|
||||
|
||||
playerImageUnresolvedCache.set(cacheKey, promise);
|
||||
tickPlayerImageQueue();
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default "Steve" player image. This image can be used as a placeholder whilst waiting for
|
||||
* {@see getPlayerImage} to complete
|
||||
* @returns The default player image
|
||||
*/
|
||||
export const getDefaultPlayerImage = () => {
|
||||
return defaultImage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ticks the player image load queue, starting additional image loads if any are queued and the concurrent load limit
|
||||
* hasn't been hit
|
||||
*/
|
||||
const tickPlayerImageQueue = () => {
|
||||
if (playerImagesLoading.size > 8 || !playerImageQueue.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const image = playerImageQueue.pop() as PlayerImageQueueEntry;
|
||||
|
||||
playerImagesLoading.add(image.cacheKey);
|
||||
image.image.src = useStore().state.components.players.imageUrl(image);
|
||||
|
||||
tickPlayerImageQueue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the player image cache
|
||||
* Future calls to {@see getPlayerImage} will result in fresh image loads
|
||||
*/
|
||||
export const clearPlayerImageCache = () => {
|
||||
playerImageCache.clear();
|
||||
}
|
Loading…
Reference in New Issue
Block a user