Move and rename player image util methods

This commit is contained in:
James Lyne 2022-02-26 14:41:23 +00:00
parent f794dcb813
commit f4481a1d6c
11 changed files with 166 additions and 110 deletions

View File

@ -29,12 +29,13 @@ import Sidebar from './components/Sidebar.vue';
import ChatBox from './components/ChatBox.vue'; import ChatBox from './components/ChatBox.vue';
import {useStore} from "@/store"; import {useStore} from "@/store";
import {ActionTypes} from "@/store/action-types"; import {ActionTypes} from "@/store/action-types";
import {clearHeadCache, parseUrl} from '@/util'; import {parseUrl} from '@/util';
import {hideSplash, showSplash, showSplashError} from '@/util/splash'; import {hideSplash, showSplash, showSplashError} from '@/util/splash';
import {MutationTypes} from "@/store/mutation-types"; import {MutationTypes} from "@/store/mutation-types";
import {LiveAtlasServerDefinition, LiveAtlasUIElement} from "@/index"; import {LiveAtlasServerDefinition, LiveAtlasUIElement} from "@/index";
import LoginModal from "@/components/login/LoginModal.vue"; import LoginModal from "@/components/login/LoginModal.vue";
import {notify} from "@kyvg/vue3-notification"; import {notify} from "@kyvg/vue3-notification";
import {clearPlayerImageCache} from "@/util/images";
export default defineComponent({ export default defineComponent({
name: 'App', name: 'App',
@ -171,7 +172,7 @@ export default defineComponent({
return; return;
} }
clearHeadCache(); clearPlayerImageCache();
loadingAttempts.value = 0; loadingAttempts.value = 0;
window.history.replaceState({}, '', newServer.id); window.history.replaceState({}, '', newServer.id);
loadConfiguration(); loadConfiguration();
@ -201,7 +202,7 @@ export default defineComponent({
} }
}); });
watch(playerImageUrl, () => { watch(playerImageUrl, () => {
clearHeadCache(); clearPlayerImageCache();
}); });
handleUrl(); handleUrl();

View File

@ -24,7 +24,7 @@ import {computed, defineComponent, watch} from "@vue/runtime-core";
import {LiveAtlasPlayer} from "@/index"; import {LiveAtlasPlayer} from "@/index";
import {onMounted, ref} from "vue"; import {onMounted, ref} from "vue";
import {useStore} from "@/store"; import {useStore} from "@/store";
import {getMinecraftHead} from "@/util"; import {getPlayerImage} from "@/util/images";
export default defineComponent({ export default defineComponent({
name: 'PlayerImage', name: 'PlayerImage',
@ -47,7 +47,7 @@ export default defineComponent({
if (imagesEnabled.value) { if (imagesEnabled.value) {
try { try {
const result = await getMinecraftHead(props.player, 'small'); const result = await getPlayerImage(props.player, 'small');
image.value = result.src; image.value = result.src;
} catch (e) { } catch (e) {
} }

6
src/index.d.ts vendored
View File

@ -251,7 +251,7 @@ interface LiveAtlasCircleMarker extends LiveAtlasPathMarker {
radius: PointTuple; radius: PointTuple;
} }
interface HeadQueueEntry { interface PlayerImageQueueEntry {
cacheKey: string; cacheKey: string;
name: string; name: string;
uuid?: string; uuid?: string;
@ -277,7 +277,7 @@ interface LiveAtlasComponentConfig {
markers?: LiveAtlasPlayerMarkerConfig; markers?: LiveAtlasPlayerMarkerConfig;
showImages: boolean; showImages: boolean;
grayHiddenPlayers: boolean; grayHiddenPlayers: boolean;
imageUrl: (entry: HeadQueueEntry) => string; imageUrl: (entry: PlayerImageQueueEntry) => string;
}; };
coordinatesControl?: CoordinatesControlOptions; coordinatesControl?: CoordinatesControlOptions;
clockControl?: ClockControlOptions; clockControl?: ClockControlOptions;
@ -298,7 +298,7 @@ interface LiveAtlasPartialComponentConfig {
markers?: LiveAtlasPlayerMarkerConfig; markers?: LiveAtlasPlayerMarkerConfig;
showImages?: boolean; showImages?: boolean;
grayHiddenPlayers?: boolean; grayHiddenPlayers?: boolean;
imageUrl?: (entry: HeadQueueEntry) => string; imageUrl?: (entry: PlayerImageQueueEntry) => string;
}; };
coordinatesControl?: CoordinatesControlOptions; coordinatesControl?: CoordinatesControlOptions;
clockControl?: ClockControlOptions; clockControl?: ClockControlOptions;

View File

@ -18,9 +18,9 @@
*/ */
import {BaseIconOptions, Icon, Layer, LayerOptions, Util} from 'leaflet'; import {BaseIconOptions, Icon, Layer, LayerOptions, Util} from 'leaflet';
import {getImagePixelSize, getMinecraftHead} from '@/util';
import defaultImage from '@/assets/images/player_face.png'; import defaultImage from '@/assets/images/player_face.png';
import {LiveAtlasPlayer, LiveAtlasPlayerImageSize} from "@/index"; import {LiveAtlasPlayer, LiveAtlasPlayerImageSize} from "@/index";
import {getImagePixelSize, getPlayerImage} from "@/util/images";
const playerImage: HTMLImageElement = document.createElement('img'); const playerImage: HTMLImageElement = document.createElement('img');
playerImage.src = defaultImage; playerImage.src = defaultImage;
@ -122,7 +122,7 @@ export class PlayerIcon extends Layer implements Icon<PlayerIconOptions> {
} }
updateImage() { updateImage() {
getMinecraftHead(this._player, this.options.imageSize).then(head => { getPlayerImage(this._player, this.options.imageSize).then(head => {
this._playerImage!.src = head.src; this._playerImage!.src = head.src;
}).catch(() => {}); }).catch(() => {});
} }

View File

@ -30,7 +30,6 @@ import {MutationTypes} from "@/store/mutation-types";
import MapProvider from "@/providers/MapProvider"; import MapProvider from "@/providers/MapProvider";
import { import {
getBoundsFromPoints, getBoundsFromPoints,
getDefaultMinecraftHead,
getMiddle, getMiddle,
guessWorldDimension, guessWorldDimension,
runSandboxed, runSandboxed,
@ -43,6 +42,7 @@ import LiveAtlasMapDefinition from "@/model/LiveAtlasMapDefinition";
import {OverviewerProjection} from "@/leaflet/projection/OverviewerProjection"; import {OverviewerProjection} from "@/leaflet/projection/OverviewerProjection";
import {LiveAtlasMarkerType} from "@/util/markers"; import {LiveAtlasMarkerType} from "@/util/markers";
import {useStore} from "@/store"; import {useStore} from "@/store";
import {getDefaultPlayerImage} from "@/util/images";
export default class OverviewerMapProvider extends MapProvider { export default class OverviewerMapProvider extends MapProvider {
private configurationAbort?: AbortController = undefined; private configurationAbort?: AbortController = undefined;
@ -247,7 +247,7 @@ export default class OverviewerMapProvider extends MapProvider {
//Not used by Overviewer //Not used by Overviewer
players: { players: {
markers: undefined, markers: undefined,
imageUrl: getDefaultMinecraftHead, imageUrl: getDefaultPlayerImage,
showImages: false, showImages: false,
grayHiddenPlayers: false, grayHiddenPlayers: false,
}, },

View File

@ -32,12 +32,13 @@ import LiveAtlasMapDefinition from "@/model/LiveAtlasMapDefinition";
import {MutationTypes} from "@/store/mutation-types"; import {MutationTypes} from "@/store/mutation-types";
import MapProvider from "@/providers/MapProvider"; import MapProvider from "@/providers/MapProvider";
import {ActionTypes} from "@/store/action-types"; 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 {LiveAtlasMarkerType} from "@/util/markers";
import {PointTuple} from "leaflet"; import {PointTuple} from "leaflet";
import ConfigurationError from "@/errors/ConfigurationError"; import ConfigurationError from "@/errors/ConfigurationError";
import {Pl3xmapTileLayer} from "@/leaflet/tileLayer/Pl3xmapTileLayer"; import {Pl3xmapTileLayer} from "@/leaflet/tileLayer/Pl3xmapTileLayer";
import {LiveAtlasTileLayer, LiveAtlasTileLayerOptions} from "@/leaflet/tileLayer/LiveAtlasTileLayer"; import {LiveAtlasTileLayer, LiveAtlasTileLayerOptions} from "@/leaflet/tileLayer/LiveAtlasTileLayer";
import {getDefaultPlayerImage} from "@/util/images";
export default class Pl3xmapMapProvider extends MapProvider { export default class Pl3xmapMapProvider extends MapProvider {
private configurationAbort?: AbortController = undefined; private configurationAbort?: AbortController = undefined;
@ -120,7 +121,7 @@ export default class Pl3xmapMapProvider extends MapProvider {
components: { components: {
players: { players: {
markers: undefined, markers: undefined,
imageUrl: getDefaultMinecraftHead, imageUrl: getDefaultPlayerImage,
grayHiddenPlayers: true, grayHiddenPlayers: true,
showImages: true, showImages: true,
} }
@ -214,7 +215,7 @@ export default class Pl3xmapMapProvider extends MapProvider {
players: { players: {
markers: undefined, //Configured per-world markers: undefined, //Configured per-world
imageUrl: getDefaultMinecraftHead, imageUrl: getDefaultPlayerImage,
//Not configurable //Not configurable
showImages: true, showImages: true,

View File

@ -40,8 +40,9 @@ import {
LiveAtlasUIModal, LiveAtlasUIModal,
LiveAtlasSidebarSectionState, LiveAtlasMarker, LiveAtlasMapViewTarget LiveAtlasSidebarSectionState, LiveAtlasMarker, LiveAtlasMapViewTarget
} from "@/index"; } from "@/index";
import {getDefaultMinecraftHead, getGlobalMessages} from "@/util"; import {getGlobalMessages} from "@/util";
import {getServerMapProvider} from "@/util/config"; import {getServerMapProvider} from "@/util/config";
import {getDefaultPlayerImage} from "@/util/images";
export type CurrentMapPayload = { export type CurrentMapPayload = {
worldName: string; worldName: string;
@ -559,7 +560,7 @@ export const mutations: MutationTree<State> & Mutations = {
markers: undefined, markers: undefined,
showImages: true, showImages: true,
grayHiddenPlayers: true, grayHiddenPlayers: true,
imageUrl: getDefaultMinecraftHead, imageUrl: getDefaultPlayerImage,
}; };
state.components.coordinatesControl = undefined; state.components.coordinatesControl = undefined;
state.components.clockControl = undefined; state.components.clockControl = undefined;

View File

@ -39,7 +39,8 @@ import {
LiveAtlasMarker, LiveAtlasMapViewTarget LiveAtlasMarker, LiveAtlasMapViewTarget
} from "@/index"; } from "@/index";
import LiveAtlasMapDefinition from "@/model/LiveAtlasMapDefinition"; import LiveAtlasMapDefinition from "@/model/LiveAtlasMapDefinition";
import {getDefaultMinecraftHead, getMessages} from "@/util"; import {getMessages} from "@/util";
import {getDefaultPlayerImage} from "@/util/images";
export type State = { export type State = {
version: string; version: string;
@ -162,7 +163,7 @@ export const state: State = {
showImages: false, showImages: false,
// (world-settings.x.player-tracker.heads-url in squaremap) // (world-settings.x.player-tracker.heads-url in squaremap)
imageUrl: getDefaultMinecraftHead, imageUrl: getDefaultPlayerImage,
}, },
// Settings for coordinates control // Settings for coordinates control

View File

@ -14,29 +14,21 @@
* limitations under the License. * limitations under the License.
*/ */
import defaultImage from '@/assets/images/player_face.png';
import {useStore} from "@/store"; import {useStore} from "@/store";
import LiveAtlasMapDefinition from "@/model/LiveAtlasMapDefinition"; import LiveAtlasMapDefinition from "@/model/LiveAtlasMapDefinition";
import { import {
Coordinate, Coordinate,
HeadQueueEntry, LiveAtlasBounds,
LiveAtlasBounds, LiveAtlasDimension, LiveAtlasDimension,
LiveAtlasGlobalMessageConfig, LiveAtlasGlobalMessageConfig,
LiveAtlasLocation, LiveAtlasLocation,
LiveAtlasMessageConfig, LiveAtlasMessageConfig,
LiveAtlasPlayer,
LiveAtlasPlayerImageSize,
} from "@/index"; } from "@/index";
import {notify} from "@kyvg/vue3-notification"; import {notify} from "@kyvg/vue3-notification";
import {globalMessages, serverMessages} from "../messages"; import {globalMessages, serverMessages} from "../messages";
const documentRange = document.createRange(), const documentRange = document.createRange(),
brToSpaceRegex = /<br \/>/g, brToSpaceRegex = /<br \/>/g;
headCache = new Map<string, HTMLImageElement>(),
headUnresolvedCache = new Map<string, Promise<HTMLImageElement>>(),
headsLoading = new Set<string>(),
headQueue: HeadQueueEntry[] = [];
export const titleColoursRegex = /§[0-9a-f]/ig; export const titleColoursRegex = /§[0-9a-f]/ig;
export const netherWorldNameRegex = /[_\s]?nether([\s_]|$)/i; 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) => { export const parseUrl = (url: URL) => {
const query = new URLSearchParams(url.search), const query = new URLSearchParams(url.search),
hash = url.hash.replace('#', ''); hash = url.hash.replace('#', '');

View File

@ -32,8 +32,7 @@ import {
} from "@/index"; } from "@/index";
import {getPoints} from "@/util/areas"; import {getPoints} from "@/util/areas";
import { import {
decodeHTMLEntities, getBounds, getImagePixelSize, decodeHTMLEntities, getBounds, getMiddle, guessWorldDimension,
getMiddle, guessWorldDimension,
stripHTML, stripHTML,
titleColoursRegex titleColoursRegex
} from "@/util"; } from "@/util";
@ -53,6 +52,7 @@ import {
import {PointTuple} from "leaflet"; import {PointTuple} from "leaflet";
import {LiveAtlasMarkerType} from "@/util/markers"; import {LiveAtlasMarkerType} from "@/util/markers";
import {DynmapProjection} from "@/leaflet/projection/DynmapProjection"; import {DynmapProjection} from "@/leaflet/projection/DynmapProjection";
import {getImagePixelSize} from "@/util/images";
export function buildServerConfig(response: Options): LiveAtlasServerConfig { export function buildServerConfig(response: Options): LiveAtlasServerConfig {
let title = 'Dynmap'; let title = 'Dynmap';

138
src/util/images.ts Normal file
View 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();
}