<!DOCTYPE html>
|
<html lang="en">
|
<head>
|
<meta charset="UTF-8">
|
<title>库位地图</title>
|
<link rel="stylesheet" href="../../static/vue/element/element.css">
|
<script type="text/javascript" src="../../static/js/jquery/jquery-3.3.1.min.js"></script>
|
<script type="text/javascript" src="../../static/js/common.js"></script>
|
<script type="text/javascript" src="../../static/vue/js/vue.min.js"></script>
|
<script type="text/javascript" src="../../static/vue/element/element.js"></script>
|
<script src="../../static/js/gsap.min.js"></script>
|
<script src="../../static/js/pixi-legacy.min.js"></script>
|
<style>
|
html, body, #app {
|
width: 100%;
|
height: 100%;
|
margin: 0;
|
overflow: hidden;
|
}
|
|
body {
|
background: linear-gradient(180deg, #eef4f8 0%, #e7edf4 100%);
|
font-family: "Helvetica Neue", Arial, sans-serif;
|
}
|
|
* {
|
box-sizing: border-box;
|
}
|
|
.locmap-shell {
|
position: relative;
|
width: 100%;
|
height: 100%;
|
overflow: hidden;
|
}
|
|
.locmap-canvas {
|
position: absolute;
|
inset: 0;
|
}
|
|
.locmap-title-card {
|
position: absolute;
|
top: 18px;
|
left: 18px;
|
z-index: 20;
|
padding: 14px 16px;
|
min-width: 220px;
|
border-radius: 18px;
|
border: 1px solid rgba(255, 255, 255, 0.42);
|
background: rgba(248, 251, 253, 0.92);
|
box-shadow: 0 10px 24px rgba(88, 110, 136, 0.08);
|
backdrop-filter: blur(6px);
|
pointer-events: none;
|
}
|
|
.locmap-title {
|
color: #243447;
|
font-size: 18px;
|
font-weight: 700;
|
line-height: 1.2;
|
}
|
|
.locmap-title-desc {
|
margin-top: 6px;
|
color: #6b7b8d;
|
font-size: 12px;
|
line-height: 1.5;
|
}
|
|
.locmap-tools {
|
position: absolute;
|
top: 18px;
|
right: 28px;
|
z-index: 30;
|
display: flex;
|
flex-direction: column;
|
align-items: flex-end;
|
gap: 8px;
|
}
|
|
.locmap-fps {
|
padding: 4px 10px;
|
border-radius: 999px;
|
background: rgba(255, 255, 255, 0.7);
|
border: 1px solid rgba(160, 180, 205, 0.28);
|
color: #48617c;
|
font-size: 12px;
|
line-height: 18px;
|
letter-spacing: 0.04em;
|
box-shadow: 0 6px 16px rgba(37, 64, 97, 0.06);
|
user-select: none;
|
}
|
|
.locmap-tool-toggle,
|
.locmap-tool-btn,
|
.locmap-floor-btn,
|
.locmap-detail-close {
|
appearance: none;
|
cursor: pointer;
|
transition: all .18s ease;
|
outline: none;
|
}
|
|
.locmap-tool-toggle {
|
height: 30px;
|
padding: 0 12px;
|
border-radius: 999px;
|
border: 1px solid rgba(160, 180, 205, 0.3);
|
background: rgba(255, 255, 255, 0.82);
|
color: #46617b;
|
font-size: 12px;
|
line-height: 30px;
|
box-shadow: 0 6px 16px rgba(37, 64, 97, 0.06);
|
}
|
|
.locmap-tool-toggle.is-active {
|
border-color: rgba(96, 132, 170, 0.36);
|
background: rgba(235, 243, 251, 0.96);
|
}
|
|
.locmap-tool-panel {
|
display: flex;
|
flex-direction: column;
|
gap: 8px;
|
min-width: 168px;
|
padding: 8px;
|
border-radius: 14px;
|
background: rgba(255, 255, 255, 0.72);
|
border: 1px solid rgba(160, 180, 205, 0.3);
|
box-shadow: 0 8px 20px rgba(37, 64, 97, 0.08);
|
backdrop-filter: blur(4px);
|
}
|
|
.locmap-tool-row {
|
display: flex;
|
flex-wrap: wrap;
|
gap: 8px;
|
justify-content: flex-end;
|
}
|
|
.locmap-tool-btn,
|
.locmap-floor-btn {
|
min-width: 64px;
|
height: 30px;
|
padding: 0 12px;
|
border-radius: 10px;
|
border: 1px solid rgba(160, 180, 205, 0.3);
|
background: rgba(255, 255, 255, 0.88);
|
color: #4d647d;
|
font-size: 12px;
|
line-height: 30px;
|
white-space: nowrap;
|
}
|
|
.locmap-tool-btn:hover,
|
.locmap-floor-btn:hover,
|
.locmap-detail-close:hover,
|
.locmap-tool-toggle:hover {
|
transform: translateY(-1px);
|
}
|
|
.locmap-tool-btn.is-active,
|
.locmap-floor-btn.is-active {
|
border-color: rgba(255, 136, 93, 0.38);
|
background: rgba(255, 119, 77, 0.16);
|
color: #d85a31;
|
}
|
|
.locmap-tool-section {
|
display: flex;
|
flex-direction: column;
|
gap: 4px;
|
padding-top: 6px;
|
border-top: 1px solid rgba(160, 180, 205, 0.22);
|
}
|
|
.locmap-tool-label {
|
color: #6a7f95;
|
font-size: 10px;
|
line-height: 14px;
|
text-align: right;
|
}
|
|
.locmap-detail-panel {
|
position: absolute;
|
top: 92px;
|
right: 18px;
|
bottom: 18px;
|
z-index: 25;
|
width: min(max(320px, 25vw), calc(100vw - 92px));
|
border-radius: 20px;
|
border: 1px solid rgba(255, 255, 255, 0.42);
|
background: rgba(248, 251, 253, 0.94);
|
box-shadow: 0 10px 24px rgba(88, 110, 136, 0.08);
|
overflow: hidden;
|
display: flex;
|
flex-direction: column;
|
backdrop-filter: blur(6px);
|
}
|
|
.locmap-detail-header {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
gap: 12px;
|
padding: 16px 16px 12px;
|
border-bottom: 1px solid rgba(226, 232, 240, 0.72);
|
background: rgba(255, 255, 255, 0.24);
|
}
|
|
.locmap-detail-title-wrap {
|
min-width: 0;
|
}
|
|
.locmap-detail-type {
|
display: inline-flex;
|
align-items: center;
|
height: 22px;
|
padding: 0 10px;
|
border-radius: 999px;
|
background: rgba(103, 149, 193, 0.12);
|
color: #4b6782;
|
font-size: 11px;
|
font-weight: 700;
|
}
|
|
.locmap-detail-title {
|
margin-top: 8px;
|
color: #243447;
|
font-size: 18px;
|
font-weight: 700;
|
line-height: 1.2;
|
}
|
|
.locmap-detail-subtitle {
|
margin-top: 6px;
|
color: #6b7b8d;
|
font-size: 12px;
|
line-height: 1.4;
|
}
|
|
.locmap-detail-close {
|
flex-shrink: 0;
|
width: 32px;
|
height: 32px;
|
border-radius: 10px;
|
border: 1px solid rgba(160, 180, 205, 0.28);
|
background: rgba(255, 255, 255, 0.84);
|
color: #4d647d;
|
font-size: 18px;
|
line-height: 30px;
|
text-align: center;
|
}
|
|
.locmap-detail-body {
|
flex: 1;
|
min-height: 0;
|
padding: 16px;
|
overflow: auto;
|
}
|
|
.locmap-kv-grid {
|
display: grid;
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
gap: 12px;
|
}
|
|
.locmap-kv-card {
|
padding: 14px;
|
border-radius: 16px;
|
border: 1px solid rgba(224, 232, 239, 0.92);
|
background: rgba(255, 255, 255, 0.62);
|
box-shadow: 0 8px 18px rgba(148, 163, 184, 0.06);
|
}
|
|
.locmap-kv-label {
|
color: #7d8fa2;
|
font-size: 12px;
|
line-height: 1.4;
|
}
|
|
.locmap-kv-value {
|
margin-top: 8px;
|
color: #334155;
|
font-size: 18px;
|
font-weight: 600;
|
line-height: 1.35;
|
word-break: break-all;
|
}
|
|
.locmap-empty {
|
position: absolute;
|
right: 18px;
|
bottom: 18px;
|
z-index: 20;
|
width: min(max(260px, 20vw), calc(100vw - 92px));
|
padding: 18px;
|
border-radius: 18px;
|
border: 1px solid rgba(255, 255, 255, 0.4);
|
background: rgba(248, 251, 253, 0.92);
|
box-shadow: 0 10px 24px rgba(88, 110, 136, 0.08);
|
color: #6b7b8d;
|
font-size: 12px;
|
line-height: 1.7;
|
backdrop-filter: blur(6px);
|
}
|
|
@media (max-width: 960px) {
|
.locmap-title-card {
|
right: 18px;
|
min-width: 0;
|
}
|
|
.locmap-detail-panel {
|
top: auto;
|
height: min(54vh, 460px);
|
width: calc(100vw - 36px);
|
}
|
|
.locmap-empty {
|
width: calc(100vw - 36px);
|
}
|
|
.locmap-kv-grid {
|
grid-template-columns: 1fr;
|
}
|
}
|
</style>
|
</head>
|
<body>
|
<div id="app">
|
<loc-map-canvas></loc-map-canvas>
|
</div>
|
|
<script>
|
Vue.component('loc-map-canvas', {
|
template: `
|
<div class="locmap-shell" ref="shell">
|
<div ref="pixiView" class="locmap-canvas"></div>
|
|
<div class="locmap-title-card">
|
<div class="locmap-title">库位地图</div>
|
<div class="locmap-title-desc">点击库位后在右侧查看详情。</div>
|
</div>
|
|
<div class="locmap-tools">
|
<div class="locmap-fps">FPS {{ mapFps }}</div>
|
<button type="button"
|
class="locmap-tool-toggle"
|
:class="{ 'is-active': showMapToolPanel }"
|
@click="toggleMapToolPanel">{{ showMapToolPanel ? '收起操作' : '地图操作' }}</button>
|
<div v-show="showMapToolPanel" class="locmap-tool-panel">
|
<div class="locmap-tool-row">
|
<button type="button" class="locmap-tool-btn" @click="fitStageToContent">重置视图</button>
|
<button type="button" class="locmap-tool-btn" @click="rotateMap">旋转</button>
|
<button type="button"
|
class="locmap-tool-btn"
|
:class="{ 'is-active': mapMirrorX }"
|
@click="toggleMirror">{{ mapMirrorX ? '取消镜像' : '镜像' }}</button>
|
</div>
|
<div v-if="floorList && floorList.length > 0" class="locmap-tool-section">
|
<div class="locmap-tool-label">楼层</div>
|
<div class="locmap-tool-row">
|
<button v-for="lev in floorList"
|
:key="'loc-floor-' + lev"
|
type="button"
|
class="locmap-floor-btn"
|
:class="{ 'is-active': currentLev === lev }"
|
@click="changeFloor(lev)">{{ lev }}F</button>
|
</div>
|
</div>
|
</div>
|
</div>
|
|
<div v-if="detailPanelOpen" class="locmap-detail-panel" ref="detailPanel">
|
<div class="locmap-detail-header">
|
<div class="locmap-detail-title-wrap">
|
<div class="locmap-detail-type">{{ detailTypeLabel }}</div>
|
<div class="locmap-detail-title">{{ detailTitle }}</div>
|
<div class="locmap-detail-subtitle">{{ detailSubtitle }}</div>
|
</div>
|
<button type="button" class="locmap-detail-close" @click="closeDetailPanel">×</button>
|
</div>
|
<div class="locmap-detail-body">
|
<div class="locmap-kv-grid">
|
<div v-for="item in detailFields" :key="item.label" class="locmap-kv-card">
|
<div class="locmap-kv-label">{{ item.label }}</div>
|
<div class="locmap-kv-value">{{ formatDetailValue(item.value) }}</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
|
<div v-else class="locmap-empty">
|
点击库位可查看库位状态,点击站点可查看站点作业详情。地图操作在右上角工具面板中。
|
</div>
|
</div>
|
`,
|
data() {
|
return {
|
cellWidth: 25,
|
cellHeight: 25,
|
pixiApp: null,
|
pixiStageList: [],
|
pixiStaMap: new Map(),
|
pixiTrackMap: new Map(),
|
pixiLabelList: [],
|
mapRoot: null,
|
shelvesContainer: null,
|
objectsContainer: null,
|
objectsOverlayContainer: null,
|
tracksGraphics: null,
|
mapContentSize: { width: 0, height: 0 },
|
textureMap: {},
|
locChunkList: [],
|
locChunkSize: 1024,
|
locCullPadding: 160,
|
locCullRaf: null,
|
ws: null,
|
wsReconnectTimer: null,
|
wsReconnectAttempts: 0,
|
wsReconnectBaseDelay: 1000,
|
wsReconnectMaxDelay: 15000,
|
containerResizeObserver: null,
|
adjustLabelTimer: null,
|
map: [],
|
floorList: [],
|
currentLev: 1,
|
reloadMap: true,
|
mapFps: 0,
|
showMapToolPanel: false,
|
detailPanelOpen: false,
|
detailType: '',
|
detailPayload: null,
|
highlightedSprite: null,
|
highlightedLocCell: null,
|
highlightedLocGraphic: null,
|
mapRotation: 0,
|
mapMirrorX: false,
|
mapConfigCodes: {
|
rotate: 'map_canvas_rotation',
|
mirror: 'map_canvas_mirror_x'
|
}
|
};
|
},
|
computed: {
|
detailTypeLabel() {
|
return this.detailType === 'site' ? '站点信息' : '库位详情';
|
},
|
detailTitle() {
|
if (this.detailType === 'site' && this.detailPayload) {
|
return '站点 ' + this.formatDetailValue(this.detailPayload.siteId);
|
}
|
if (this.detailType === 'loc' && this.detailPayload) {
|
return this.formatDetailValue(this.detailPayload.locNo);
|
}
|
return '详情';
|
},
|
detailSubtitle() {
|
if (this.detailType === 'site') {
|
return '站点作业状态、来源目标和作业参数';
|
}
|
return '库位当前状态和所在排列层信息';
|
},
|
detailFields() {
|
if (!this.detailPayload) { return []; }
|
if (this.detailType === 'site') {
|
return [
|
{ label: '站点', value: this.detailPayload.siteId },
|
{ label: '工作号', value: this.detailPayload.workNo },
|
{ label: '工作状态', value: this.detailPayload.wrkSts },
|
{ label: '工作类型', value: this.detailPayload.ioType },
|
{ label: '源站', value: this.detailPayload.sourceStaNo },
|
{ label: '目标站', value: this.detailPayload.staNo },
|
{ label: '源库位', value: this.detailPayload.sourceLocNo },
|
{ label: '目标库位', value: this.detailPayload.locNo },
|
{ label: '自动', value: this.detailPayload.autoing },
|
{ label: '有物', value: this.detailPayload.loading },
|
{ label: '能入', value: this.detailPayload.canining },
|
{ label: '能出', value: this.detailPayload.canouting }
|
];
|
}
|
return [
|
{ label: '库位号', value: this.detailPayload.locNo },
|
{ label: '库位状态', value: this.detailPayload.locSts },
|
{ label: '排', value: this.detailPayload.row },
|
{ label: '列', value: this.detailPayload.bay },
|
{ label: '层', value: this.detailPayload.lev }
|
];
|
}
|
},
|
mounted() {
|
this.createMap();
|
this.startContainerResizeObserve();
|
this.loadMapTransformConfig();
|
this.initLev();
|
this.connectWs();
|
setTimeout(() => {
|
this.getMap(this.currentLev);
|
}, 300);
|
},
|
beforeDestroy() {
|
if (this.adjustLabelTimer) {
|
clearTimeout(this.adjustLabelTimer);
|
this.adjustLabelTimer = null;
|
}
|
if (this.locCullRaf) {
|
cancelAnimationFrame(this.locCullRaf);
|
this.locCullRaf = null;
|
}
|
if (this.containerResizeObserver) {
|
this.containerResizeObserver.disconnect();
|
this.containerResizeObserver = null;
|
}
|
window.removeEventListener('resize', this.resizeToContainer);
|
if (this.wsReconnectTimer) {
|
clearTimeout(this.wsReconnectTimer);
|
this.wsReconnectTimer = null;
|
}
|
if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)) {
|
try { this.ws.close(); } catch (e) {}
|
}
|
if (window.gsap && this.pixiApp && this.pixiApp.stage) {
|
window.gsap.killTweensOf(this.pixiApp.stage.position);
|
}
|
this.clearLocChunks();
|
if (this.pixiApp) {
|
this.pixiApp.destroy(true, { children: true });
|
this.pixiApp = null;
|
}
|
},
|
methods: {
|
toggleMapToolPanel() {
|
this.showMapToolPanel = !this.showMapToolPanel;
|
},
|
formatDetailValue(value) {
|
return value == null || value === '' ? '-' : value;
|
},
|
initLev() {
|
$.ajax({
|
url: baseUrl + "/console/map/lev/list",
|
headers: { token: localStorage.getItem('token') },
|
method: 'get',
|
success: (res) => {
|
if (res.code === 200) {
|
this.floorList = Array.isArray(res.data) ? res.data : [];
|
if (this.floorList.length > 0 && this.floorList.indexOf(this.currentLev) === -1) {
|
this.currentLev = this.floorList[0];
|
}
|
} else if (res.code === 403) {
|
parent.location.href = baseUrl + "/login";
|
} else {
|
this.showMessage('error', res.msg || '楼层信息加载失败');
|
}
|
}
|
});
|
},
|
getMap(lev) {
|
$.ajax({
|
url: baseUrl + "/console/map/" + lev + "/auth",
|
headers: { token: localStorage.getItem('token') },
|
method: 'get',
|
success: (res) => {
|
if (res.code === 200) {
|
this.reloadMap = true;
|
this.createMapData(res.data || []);
|
} else if (res.code === 403) {
|
parent.location.href = baseUrl + "/login";
|
} else {
|
this.showMessage('error', res.msg || '地图加载失败');
|
}
|
}
|
});
|
},
|
changeFloor(lev) {
|
if (this.currentLev === lev) { return; }
|
this.currentLev = lev;
|
this.reloadMap = true;
|
this.closeDetailPanel();
|
this.getMap(lev);
|
},
|
connectWs() {
|
if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)) {
|
return;
|
}
|
this.ws = new WebSocket("ws://" + window.location.host + baseUrl + "/console/websocket");
|
this.ws.onopen = this.webSocketOnOpen;
|
this.ws.onerror = this.webSocketOnError;
|
this.ws.onmessage = this.webSocketOnMessage;
|
this.ws.onclose = this.webSocketClose;
|
},
|
scheduleReconnectWs() {
|
if (this.wsReconnectTimer) { return; }
|
const delay = Math.min(this.wsReconnectMaxDelay, this.wsReconnectBaseDelay * Math.pow(2, this.wsReconnectAttempts));
|
this.wsReconnectAttempts += 1;
|
this.wsReconnectTimer = setTimeout(() => {
|
this.wsReconnectTimer = null;
|
this.connectWs();
|
}, delay);
|
},
|
webSocketOnOpen() {
|
this.wsReconnectAttempts = 0;
|
},
|
webSocketOnError() {},
|
webSocketOnMessage(event) {
|
let result = null;
|
try {
|
result = JSON.parse(event.data);
|
} catch (e) {
|
return;
|
}
|
if (!result || !result.url) { return; }
|
if (result.url === "/console/map/auth" || result.url === "/console/locMap/auth") {
|
let data = [];
|
try {
|
data = JSON.parse(result.data || '[]');
|
} catch (e) {
|
data = [];
|
}
|
this.setMap(data);
|
}
|
},
|
webSocketClose() {
|
this.scheduleReconnectWs();
|
},
|
setMap(data) {
|
if (!Array.isArray(data) || this.currentLev == null) { return; }
|
this.reloadMap = true;
|
this.createMapData(data);
|
},
|
showMessage(type, message) {
|
if (this.$message) {
|
this.$message({ type: type, message: message });
|
}
|
},
|
createMap() {
|
this.pixiApp = new PIXI.Application({
|
backgroundColor: 0xEEF4F8,
|
resizeTo: this.$refs.shell,
|
antialias: true,
|
autoDensity: true
|
});
|
this.$refs.pixiView.appendChild(this.pixiApp.view);
|
this.createBaseTextures();
|
|
this.mapRoot = new PIXI.Container();
|
this.pixiApp.stage.addChild(this.mapRoot);
|
|
this.shelvesContainer = new PIXI.Container();
|
this.mapRoot.addChild(this.shelvesContainer);
|
|
this.objectsContainer = new PIXI.Container();
|
this.mapRoot.addChild(this.objectsContainer);
|
|
this.tracksGraphics = new PIXI.Graphics();
|
this.mapRoot.addChild(this.tracksGraphics);
|
|
this.objectsOverlayContainer = new PIXI.Container();
|
this.mapRoot.addChild(this.objectsOverlayContainer);
|
|
this.initStageInteractions();
|
this.initFpsTicker();
|
},
|
createBaseTextures() {
|
this.textureMap = {
|
locEmpty: this.pixiApp.renderer.generateTexture(this.buildCellGraphic(0x5faeff)),
|
locFull: this.pixiApp.renderer.generateTexture(this.buildCellGraphic(0xf05d5d)),
|
site: this.pixiApp.renderer.generateTexture(this.buildCellGraphic(0xf6ca4b)),
|
charge: this.pixiApp.renderer.generateTexture(this.buildCellGraphic(0xffa66b)),
|
elevator: this.pixiApp.renderer.generateTexture(this.buildCellGraphic(0x7dd9ff)),
|
lock: this.pixiApp.renderer.generateTexture(this.buildCellGraphic(0xf83333))
|
};
|
},
|
buildCellGraphic(fillColor) {
|
const graphics = new PIXI.Graphics();
|
graphics.beginFill(fillColor);
|
graphics.lineStyle(1, 0xffffff, 1);
|
graphics.drawRect(0, 0, this.cellWidth, this.cellHeight);
|
graphics.endFill();
|
return graphics;
|
},
|
initStageInteractions() {
|
let stageOriginalPos = null;
|
let mouseDownPoint = null;
|
let touchBlank = false;
|
let pointerDownMoved = false;
|
const interaction = this.pixiApp.renderer.plugins.interaction;
|
|
interaction.on('pointerdown', (event) => {
|
const globalPos = event.data.global;
|
stageOriginalPos = [this.pixiApp.stage.position.x, this.pixiApp.stage.position.y];
|
mouseDownPoint = [globalPos.x, globalPos.y];
|
pointerDownMoved = false;
|
touchBlank = !event.target;
|
});
|
|
interaction.on('pointermove', (event) => {
|
const globalPos = event.data.global;
|
if (mouseDownPoint) {
|
const dragDx = globalPos.x - mouseDownPoint[0];
|
const dragDy = globalPos.y - mouseDownPoint[1];
|
if (Math.abs(dragDx) > 4 || Math.abs(dragDy) > 4) {
|
pointerDownMoved = true;
|
}
|
}
|
if (!touchBlank || !stageOriginalPos || !mouseDownPoint) { return; }
|
const dx = globalPos.x - mouseDownPoint[0];
|
const dy = globalPos.y - mouseDownPoint[1];
|
this.pixiApp.stage.position.set(stageOriginalPos[0] + dx, stageOriginalPos[1] + dy);
|
this.scheduleAdjustLabels();
|
this.scheduleLocChunkCulling();
|
});
|
|
interaction.on('pointerup', (event) => {
|
if (touchBlank && !pointerDownMoved && event && event.data && event.data.global) {
|
this.handleBlankPointerTap(event.data.global);
|
}
|
touchBlank = false;
|
mouseDownPoint = null;
|
stageOriginalPos = null;
|
});
|
|
interaction.on('pointerupoutside', () => {
|
touchBlank = false;
|
mouseDownPoint = null;
|
stageOriginalPos = null;
|
});
|
|
this.pixiApp.view.addEventListener('wheel', (event) => {
|
event.stopPropagation();
|
event.preventDefault();
|
const rect = this.pixiApp.view.getBoundingClientRect();
|
const sx = event.clientX - rect.left;
|
const sy = event.clientY - rect.top;
|
const oldZoomX = this.pixiApp.stage.scale.x || 1;
|
const oldZoomY = this.pixiApp.stage.scale.y || 1;
|
const oldZoomAbs = Math.abs(oldZoomX) || 1;
|
let newZoomAbs = oldZoomAbs * Math.pow(0.999, event.deltaY);
|
newZoomAbs = Math.max(0.08, Math.min(newZoomAbs, 8));
|
const mirrorX = this.mapMirrorX ? -1 : 1;
|
const newZoomX = mirrorX * newZoomAbs;
|
const newZoomY = newZoomAbs;
|
const worldX = (sx - this.pixiApp.stage.position.x) / oldZoomX;
|
const worldY = (sy - this.pixiApp.stage.position.y) / oldZoomY;
|
const newPosX = sx - worldX * newZoomX;
|
const newPosY = sy - worldY * newZoomY;
|
this.pixiApp.stage.setTransform(newPosX, newPosY, newZoomX, newZoomY, 0, 0, 0, 0, 0);
|
this.scheduleAdjustLabels();
|
this.scheduleLocChunkCulling();
|
}, { passive: false });
|
},
|
initFpsTicker() {
|
let lastTs = 0;
|
let deltaSum = 0;
|
let frameCount = 0;
|
const updateInterval = 200;
|
this.pixiApp.ticker.add(() => {
|
const now = Date.now();
|
if (!lastTs) {
|
lastTs = now;
|
return;
|
}
|
deltaSum += now - lastTs;
|
frameCount += 1;
|
lastTs = now;
|
if (deltaSum >= updateInterval) {
|
this.mapFps = deltaSum > 0 ? Math.round(frameCount * 1000 / deltaSum) : 0;
|
deltaSum = 0;
|
frameCount = 0;
|
}
|
});
|
},
|
createMapData(map) {
|
if (!this.reloadMap) {
|
this.map = map;
|
return;
|
}
|
this.reloadMap = false;
|
this.map = map;
|
this.pixiStageList = [];
|
this.pixiStaMap = new Map();
|
this.pixiLabelList = [];
|
this.restoreHighlightedSprite();
|
this.clearLocHighlight();
|
this.clearLocChunks();
|
this.objectsContainer.removeChildren();
|
this.objectsOverlayContainer.removeChildren();
|
if (this.tracksGraphics) { this.tracksGraphics.clear(); }
|
|
let rows = Array.isArray(map) ? map.length : 0;
|
let maxCols = 0;
|
map.forEach((row, rowIndex) => {
|
if (!Array.isArray(row)) { return; }
|
this.pixiStageList[rowIndex] = [];
|
maxCols = Math.max(maxCols, row.length);
|
row.forEach((cell, colIndex) => {
|
if (cell.value < 0 && cell.value !== -999) { return; }
|
if (parseInt(cell.value, 10) === 0) {
|
this.pixiStageList[rowIndex][colIndex] = null;
|
return;
|
}
|
if (this.isTrackCell(cell)) {
|
cell.trackMask = this.resolveTrackMask(map, rowIndex, colIndex);
|
}
|
const sprite = this.createSprite(cell, rowIndex, colIndex);
|
if (cell.value === -999) {
|
this.objectsOverlayContainer.addChild(sprite);
|
} else {
|
this.objectsContainer.addChild(sprite);
|
}
|
this.pixiStageList[rowIndex][colIndex] = sprite;
|
});
|
});
|
|
this.mapContentSize = {
|
width: maxCols * this.cellWidth,
|
height: rows * this.cellHeight
|
};
|
this.buildLocChunks(map, this.mapContentSize.width, this.mapContentSize.height);
|
this.drawTracks(map);
|
this.applyMapTransform(true);
|
},
|
createSprite(cell, rowIndex, colIndex) {
|
const sprite = this.isTrackCell(cell)
|
? this.createTrackSprite(this.cellWidth, this.cellHeight, cell.trackMask)
|
: new PIXI.Sprite(this.getTextureForCell(cell));
|
sprite.position.set(colIndex * this.cellWidth, rowIndex * this.cellHeight);
|
sprite.baseTint = 0xFFFFFF;
|
sprite.cellData = cell;
|
|
if (cell.value === 4 || cell.value === 67) {
|
this.attachCellLabel(sprite, cell.data);
|
if (cell.value === 4 && cell.data != null) {
|
this.pixiStaMap.set(parseInt(cell.data, 10), sprite);
|
}
|
}
|
|
if (!this.isTrackCell(cell)) {
|
sprite.interactive = true;
|
sprite.buttonMode = true;
|
sprite.on('pointerdown', () => {
|
if (cell.value === 4) {
|
this.openSiteDetail(cell, sprite);
|
} else {
|
this.openLocDetail(rowIndex, colIndex, sprite);
|
}
|
});
|
}
|
return sprite;
|
},
|
createTrackSprite(width, height, mask) {
|
const trackMask = mask != null ? mask : 10;
|
const key = width + '-' + height + '-' + trackMask;
|
let texture = this.pixiTrackMap.get(key);
|
if (!texture) {
|
texture = this.createTrackTexture(width, height, trackMask);
|
this.pixiTrackMap.set(key, texture);
|
}
|
return new PIXI.Sprite(texture);
|
},
|
createTrackTexture(width, height, mask) {
|
const TRACK_N = 1;
|
const TRACK_E = 2;
|
const TRACK_S = 4;
|
const TRACK_W = 8;
|
const trackMask = mask != null ? mask : (TRACK_E | TRACK_W);
|
const g = new PIXI.Graphics();
|
const size = Math.max(1, Math.min(width, height));
|
const rail = Math.max(2, Math.round(size * 0.12));
|
const gap = Math.max(4, Math.round(size * 0.38));
|
const midX = Math.round(width / 2);
|
const midY = Math.round(height / 2);
|
|
const hasN = (trackMask & TRACK_N) !== 0;
|
const hasE = (trackMask & TRACK_E) !== 0;
|
const hasS = (trackMask & TRACK_S) !== 0;
|
const hasW = (trackMask & TRACK_W) !== 0;
|
|
const hasH = hasW || hasE;
|
const hasV = hasN || hasS;
|
const isCorner = hasH && hasV && !(hasW && hasE) && !(hasN && hasS);
|
const railColor = 0x555555;
|
|
if (hasH && !isCorner) {
|
const hStart = hasW ? 0 : midX;
|
const hEnd = hasE ? width : midX;
|
const hWidth = Math.max(1, hEnd - hStart);
|
g.beginFill(railColor);
|
g.drawRect(hStart, midY - Math.round(rail / 2), hWidth, rail);
|
g.endFill();
|
}
|
if (hasV && !isCorner) {
|
const vStart = hasN ? 0 : midY;
|
const vEnd = hasS ? height : midY;
|
const vHeight = Math.max(1, vEnd - vStart);
|
g.beginFill(railColor);
|
g.drawRect(midX - Math.round(rail / 2), vStart, rail, vHeight);
|
g.endFill();
|
}
|
if (isCorner) {
|
const cornerEast = hasE;
|
const cornerSouth = hasS;
|
const centerX = cornerEast ? (width - 1) : 0;
|
const centerY = cornerSouth ? (height - 1) : 0;
|
const angleStart = (cornerEast && cornerSouth) ? Math.PI : (cornerEast ? Math.PI / 2 : (cornerSouth ? -Math.PI / 2 : 0));
|
const angleEnd = (cornerEast && cornerSouth) ? Math.PI * 1.5 : (cornerEast ? Math.PI : (cornerSouth ? 0 : Math.PI / 2));
|
const radius = Math.min(Math.abs(centerX - midX), Math.abs(centerY - midY));
|
g.lineStyle(rail, railColor, 1);
|
g.arc(centerX, centerY, radius, angleStart, angleEnd);
|
g.lineStyle(0, 0, 0);
|
}
|
|
const rt = PIXI.RenderTexture.create({ width: width, height: height });
|
this.pixiApp.renderer.render(g, rt);
|
return rt;
|
},
|
attachCellLabel(sprite, textValue) {
|
const text = new PIXI.Text(String(textValue == null ? '' : textValue), {
|
fontFamily: 'Arial',
|
fontSize: 10,
|
fill: '#1f2937',
|
align: 'center'
|
});
|
text.anchor.set(0.5);
|
text.position.set(sprite.width / 2, sprite.height / 2);
|
sprite.addChild(text);
|
sprite.textObj = text;
|
this.pixiLabelList.push(sprite);
|
},
|
getTextureForCell(cell) {
|
const value = parseInt(cell.value, 10);
|
if (value === 4) { return this.textureMap.site; }
|
if (value === 5) { return this.textureMap.charge; }
|
if (value === 67) { return this.textureMap.elevator; }
|
if (value === -999) { return this.textureMap.lock; }
|
return this.textureMap.locEmpty;
|
},
|
isTrackCell(cell) {
|
if (!cell) { return false; }
|
const type = cell.type ? String(cell.type).toLowerCase() : '';
|
if (type === 'track' || type === 'crn' || type === 'dualcrn' || type === 'rgv') { return true; }
|
if (cell.trackSiteNo != null) { return true; }
|
const value = parseInt(cell.value, 10);
|
return value === 3 || value === 9;
|
},
|
resolveTrackMask(map, rowIndex, colIndex) {
|
const TRACK_N = 1;
|
const TRACK_E = 2;
|
const TRACK_S = 4;
|
const TRACK_W = 8;
|
const row = Array.isArray(map) ? map[rowIndex] : null;
|
const cell = row && row[colIndex] ? row[colIndex] : null;
|
if (!this.isTrackCell(cell)) {
|
return TRACK_E | TRACK_W;
|
}
|
let mask = 0;
|
const north = rowIndex > 0 && Array.isArray(map[rowIndex - 1]) ? map[rowIndex - 1][colIndex] : null;
|
const east = row && colIndex + 1 < row.length ? row[colIndex + 1] : null;
|
const south = rowIndex + 1 < map.length && Array.isArray(map[rowIndex + 1]) ? map[rowIndex + 1][colIndex] : null;
|
const west = row && colIndex > 0 ? row[colIndex - 1] : null;
|
if (north && this.isTrackCell(north)) { mask |= TRACK_N; }
|
if (east && this.isTrackCell(east)) { mask |= TRACK_E; }
|
if (south && this.isTrackCell(south)) { mask |= TRACK_S; }
|
if (west && this.isTrackCell(west)) { mask |= TRACK_W; }
|
return mask || (TRACK_E | TRACK_W);
|
},
|
drawTracks(map) {
|
if (this.tracksGraphics) { this.tracksGraphics.clear(); }
|
},
|
buildLocChunks(map, contentW, contentH) {
|
this.clearLocChunks();
|
if (!this.pixiApp || !this.pixiApp.renderer || !this.shelvesContainer || !Array.isArray(map)) { return; }
|
const chunkSize = Math.max(256, parseInt(this.locChunkSize, 10) || 1024);
|
const chunkMap = new Map();
|
for (let rowIndex = 0; rowIndex < map.length; rowIndex += 1) {
|
const row = map[rowIndex];
|
if (!Array.isArray(row)) { continue; }
|
for (let colIndex = 0; colIndex < row.length; colIndex += 1) {
|
const cell = row[colIndex];
|
if (!cell || parseInt(cell.value, 10) !== 0) { continue; }
|
const posX = colIndex * this.cellWidth;
|
const posY = rowIndex * this.cellHeight;
|
const chunkX = Math.floor(posX / chunkSize);
|
const chunkY = Math.floor(posY / chunkSize);
|
const key = chunkX + ',' + chunkY;
|
let list = chunkMap.get(key);
|
if (!list) {
|
list = [];
|
chunkMap.set(key, list);
|
}
|
list.push({
|
x: posX,
|
y: posY,
|
width: this.cellWidth,
|
height: this.cellHeight,
|
color: cell.locSts === 'F' ? 0xf05d5d : 0x5faeff
|
});
|
}
|
}
|
|
const chunkList = [];
|
chunkMap.forEach((cells, key) => {
|
const keyParts = key.split(',');
|
const chunkX = parseInt(keyParts[0], 10) || 0;
|
const chunkY = parseInt(keyParts[1], 10) || 0;
|
const chunkLeft = chunkX * chunkSize;
|
const chunkTop = chunkY * chunkSize;
|
const chunkWidth = Math.max(1, Math.min(chunkSize, contentW - chunkLeft));
|
const chunkHeight = Math.max(1, Math.min(chunkSize, contentH - chunkTop));
|
const graphics = new PIXI.Graphics();
|
for (let i = 0; i < cells.length; i += 1) {
|
const cell = cells[i];
|
graphics.beginFill(cell.color);
|
graphics.lineStyle(1, 0xffffff, 1);
|
graphics.drawRect(cell.x - chunkLeft, cell.y - chunkTop, cell.width, cell.height);
|
graphics.endFill();
|
}
|
const texture = this.pixiApp.renderer.generateTexture(
|
graphics,
|
PIXI.SCALE_MODES.LINEAR,
|
1,
|
new PIXI.Rectangle(0, 0, chunkWidth, chunkHeight)
|
);
|
graphics.destroy(true);
|
const sprite = new PIXI.Sprite(texture);
|
sprite.position.set(chunkLeft, chunkTop);
|
sprite._chunkBounds = {
|
x: chunkLeft,
|
y: chunkTop,
|
width: chunkWidth,
|
height: chunkHeight
|
};
|
this.shelvesContainer.addChild(sprite);
|
chunkList.push(sprite);
|
});
|
this.locChunkList = chunkList;
|
this.updateVisibleLocChunks();
|
},
|
clearLocChunks() {
|
if (this.locCullRaf) {
|
cancelAnimationFrame(this.locCullRaf);
|
this.locCullRaf = null;
|
}
|
this.locChunkList = [];
|
if (!this.shelvesContainer) { return; }
|
const children = this.shelvesContainer.removeChildren();
|
children.forEach((child) => {
|
if (child && typeof child.destroy === 'function') {
|
child.destroy({ children: true, texture: true, baseTexture: true });
|
}
|
});
|
},
|
getViewportLocalBounds(padding) {
|
if (!this.mapRoot || !this.pixiApp) { return null; }
|
const viewport = this.getViewportSize();
|
const pad = Math.max(0, Number(padding) || 0);
|
const points = [
|
new PIXI.Point(-pad, -pad),
|
new PIXI.Point(viewport.width + pad, -pad),
|
new PIXI.Point(-pad, viewport.height + pad),
|
new PIXI.Point(viewport.width + pad, viewport.height + pad)
|
];
|
let minX = Infinity;
|
let minY = Infinity;
|
let maxX = -Infinity;
|
let maxY = -Infinity;
|
points.forEach((point) => {
|
const local = this.mapRoot.toLocal(point);
|
if (local.x < minX) { minX = local.x; }
|
if (local.y < minY) { minY = local.y; }
|
if (local.x > maxX) { maxX = local.x; }
|
if (local.y > maxY) { maxY = local.y; }
|
});
|
if (!isFinite(minX) || !isFinite(minY) || !isFinite(maxX) || !isFinite(maxY)) { return null; }
|
return { minX: minX, minY: minY, maxX: maxX, maxY: maxY };
|
},
|
updateVisibleLocChunks() {
|
if (!this.locChunkList || this.locChunkList.length === 0) { return; }
|
const localBounds = this.getViewportLocalBounds(this.locCullPadding);
|
if (!localBounds) { return; }
|
for (let i = 0; i < this.locChunkList.length; i += 1) {
|
const sprite = this.locChunkList[i];
|
const bounds = sprite && sprite._chunkBounds;
|
if (!bounds) { continue; }
|
const visible = bounds.x < localBounds.maxX &&
|
bounds.x + bounds.width > localBounds.minX &&
|
bounds.y < localBounds.maxY &&
|
bounds.y + bounds.height > localBounds.minY;
|
if (sprite.visible !== visible) {
|
sprite.visible = visible;
|
}
|
}
|
},
|
scheduleLocChunkCulling() {
|
if (this.locCullRaf) { return; }
|
this.locCullRaf = requestAnimationFrame(() => {
|
this.locCullRaf = null;
|
this.updateVisibleLocChunks();
|
});
|
},
|
handleBlankPointerTap(globalPos) {
|
if (!globalPos || !this.mapRoot || !Array.isArray(this.map) || this.map.length === 0) { return; }
|
const local = this.mapRoot.toLocal(new PIXI.Point(globalPos.x, globalPos.y));
|
const rowIndex = Math.floor(local.y / this.cellHeight);
|
const colIndex = Math.floor(local.x / this.cellWidth);
|
if (rowIndex < 0 || colIndex < 0 || rowIndex >= this.map.length) {
|
this.closeDetailPanel();
|
return;
|
}
|
const row = this.map[rowIndex];
|
if (!Array.isArray(row) || colIndex >= row.length) {
|
this.closeDetailPanel();
|
return;
|
}
|
const cell = row[colIndex];
|
if (!cell || parseInt(cell.value, 10) !== 0) {
|
this.closeDetailPanel();
|
return;
|
}
|
this.openLocDetail(rowIndex, colIndex);
|
},
|
parseLocNoMeta(locNo) {
|
if (locNo == null || locNo === '') { return { row: null, bay: null, lev: null }; }
|
const parts = String(locNo).split('-');
|
return {
|
row: parts.length > 0 ? parts[0] : null,
|
bay: parts.length > 1 ? parts[1] : null,
|
lev: parts.length > 2 ? parts[2] : null
|
};
|
},
|
openLocDetail(rowIndex, colIndex, sprite) {
|
const cell = this.map[rowIndex] && this.map[rowIndex][colIndex] ? this.map[rowIndex][colIndex] : null;
|
if (!cell) { return; }
|
const locMeta = this.parseLocNoMeta(cell.locNo);
|
if (sprite) {
|
this.highlightSprite(sprite, 0x915eff);
|
} else {
|
this.restoreHighlightedSprite();
|
this.highlightLocCell(rowIndex, colIndex);
|
}
|
this.detailType = 'loc';
|
this.detailPayload = {
|
locNo: cell.locNo,
|
locSts: cell.locSts,
|
row: cell.row != null ? cell.row : locMeta.row,
|
bay: cell.bay != null ? cell.bay : locMeta.bay,
|
lev: cell.lev != null ? cell.lev : (locMeta.lev != null ? locMeta.lev : this.currentLev)
|
};
|
this.detailPanelOpen = true;
|
},
|
openSiteDetail(cell, sprite) {
|
this.clearLocHighlight();
|
this.highlightSprite(sprite, 0xff7e47);
|
$.ajax({
|
url: baseUrl + "/console/site/detail",
|
headers: { token: localStorage.getItem('token') },
|
data: { siteId: cell.data },
|
method: 'post',
|
success: (res) => {
|
if (res.code === 200) {
|
this.detailType = 'site';
|
this.detailPayload = res.data || {};
|
this.detailPanelOpen = true;
|
} else if (res.code === 403) {
|
parent.location.href = baseUrl + "/login";
|
} else {
|
this.showMessage('error', res.msg || '站点详情加载失败');
|
}
|
}
|
});
|
},
|
highlightSprite(sprite, tint) {
|
this.restoreHighlightedSprite();
|
if (!sprite) { return; }
|
sprite.tint = tint;
|
this.highlightedSprite = sprite;
|
},
|
restoreHighlightedSprite() {
|
if (this.highlightedSprite) {
|
this.highlightedSprite.tint = this.highlightedSprite.baseTint || 0xFFFFFF;
|
this.highlightedSprite = null;
|
}
|
},
|
highlightLocCell(rowIndex, colIndex) {
|
this.clearLocHighlight();
|
this.highlightedLocCell = { rowIndex: rowIndex, colIndex: colIndex };
|
const graphic = new PIXI.Graphics();
|
graphic.lineStyle(2, 0x915eff, 0.95);
|
graphic.beginFill(0x915eff, 0.14);
|
graphic.drawRect(colIndex * this.cellWidth, rowIndex * this.cellHeight, this.cellWidth, this.cellHeight);
|
graphic.endFill();
|
this.objectsOverlayContainer.addChild(graphic);
|
this.highlightedLocGraphic = graphic;
|
},
|
clearLocHighlight() {
|
this.highlightedLocCell = null;
|
if (this.highlightedLocGraphic) {
|
if (this.highlightedLocGraphic.parent) {
|
this.highlightedLocGraphic.parent.removeChild(this.highlightedLocGraphic);
|
}
|
this.highlightedLocGraphic.destroy(true);
|
this.highlightedLocGraphic = null;
|
}
|
},
|
closeDetailPanel() {
|
this.detailPanelOpen = false;
|
this.detailType = '';
|
this.detailPayload = null;
|
this.restoreHighlightedSprite();
|
this.clearLocHighlight();
|
},
|
parseRotation(value) {
|
const num = parseInt(value, 10);
|
if (!isFinite(num)) { return 0; }
|
const rotation = ((num % 360) + 360) % 360;
|
return rotation === 90 || rotation === 180 || rotation === 270 ? rotation : 0;
|
},
|
parseMirror(value) {
|
if (value === true || value === false) { return value; }
|
if (value == null) { return false; }
|
const str = String(value).toLowerCase();
|
return str === '1' || str === 'true' || str === 'y';
|
},
|
buildMissingMapConfigList(byCode) {
|
const list = [];
|
if (!byCode[this.mapConfigCodes.rotate]) {
|
list.push({
|
name: '地图旋转',
|
code: this.mapConfigCodes.rotate,
|
value: String(this.mapRotation || 0),
|
type: 1,
|
status: 1,
|
selectType: 'map'
|
});
|
}
|
if (!byCode[this.mapConfigCodes.mirror]) {
|
list.push({
|
name: '地图镜像',
|
code: this.mapConfigCodes.mirror,
|
value: this.mapMirrorX ? '1' : '0',
|
type: 1,
|
status: 1,
|
selectType: 'map'
|
});
|
}
|
return list;
|
},
|
createMapConfigs(list) {
|
if (!Array.isArray(list) || list.length === 0) { return; }
|
list.forEach((cfg) => {
|
$.ajax({
|
url: baseUrl + "/config/add/auth",
|
headers: { token: localStorage.getItem('token') },
|
method: 'POST',
|
data: cfg
|
});
|
});
|
},
|
loadMapTransformConfig() {
|
$.ajax({
|
url: baseUrl + "/config/listAll/auth",
|
headers: { token: localStorage.getItem('token') },
|
dataType: 'json',
|
method: 'GET',
|
success: (res) => {
|
if (!res || res.code !== 200 || !Array.isArray(res.data)) {
|
if (res && res.code === 403) { parent.location.href = baseUrl + "/login"; }
|
return;
|
}
|
const byCode = {};
|
res.data.forEach((item) => {
|
if (item && item.code) {
|
byCode[item.code] = item;
|
}
|
});
|
if (byCode[this.mapConfigCodes.rotate] && byCode[this.mapConfigCodes.rotate].value != null) {
|
this.mapRotation = this.parseRotation(byCode[this.mapConfigCodes.rotate].value);
|
}
|
if (byCode[this.mapConfigCodes.mirror] && byCode[this.mapConfigCodes.mirror].value != null) {
|
this.mapMirrorX = this.parseMirror(byCode[this.mapConfigCodes.mirror].value);
|
}
|
this.createMapConfigs(this.buildMissingMapConfigList(byCode));
|
if (this.mapContentSize.width > 0 && this.mapContentSize.height > 0) {
|
this.applyMapTransform(true);
|
}
|
}
|
});
|
},
|
saveMapTransformConfig() {
|
$.ajax({
|
url: baseUrl + "/config/updateBatch",
|
headers: { token: localStorage.getItem('token') },
|
data: JSON.stringify([
|
{ code: this.mapConfigCodes.rotate, value: String(this.mapRotation || 0) },
|
{ code: this.mapConfigCodes.mirror, value: this.mapMirrorX ? '1' : '0' }
|
]),
|
dataType: 'json',
|
contentType: 'application/json;charset=UTF-8',
|
method: 'POST'
|
});
|
},
|
rotateMap() {
|
this.mapRotation = (this.mapRotation + 90) % 360;
|
this.applyMapTransform(true);
|
this.saveMapTransformConfig();
|
},
|
toggleMirror() {
|
this.mapMirrorX = !this.mapMirrorX;
|
this.applyMapTransform(true);
|
this.saveMapTransformConfig();
|
},
|
getViewportSize() {
|
if (!this.pixiApp || !this.pixiApp.renderer) { return { width: 0, height: 0 }; }
|
const screen = this.pixiApp.renderer.screen;
|
if (screen && screen.width > 0 && screen.height > 0) {
|
return { width: screen.width, height: screen.height };
|
}
|
const rect = this.pixiApp.view ? this.pixiApp.view.getBoundingClientRect() : null;
|
return { width: rect ? rect.width : 0, height: rect ? rect.height : 0 };
|
},
|
getViewportPadding() {
|
return { top: 24, right: 24, bottom: 24, left: 24 };
|
},
|
getTransformedContentSize() {
|
const width = this.mapContentSize.width || 0;
|
const height = this.mapContentSize.height || 0;
|
const rotation = ((this.mapRotation % 360) + 360) % 360;
|
const swap = rotation === 90 || rotation === 270;
|
return { width: swap ? height : width, height: swap ? width : height };
|
},
|
fitStageToContent() {
|
if (!this.pixiApp || !this.mapContentSize.width || !this.mapContentSize.height) { return; }
|
const size = this.getTransformedContentSize();
|
const viewport = this.getViewportSize();
|
const padding = this.getViewportPadding();
|
const availableW = Math.max(1, viewport.width - padding.left - padding.right);
|
const availableH = Math.max(1, viewport.height - padding.top - padding.bottom);
|
let scale = Math.min(availableW / size.width, availableH / size.height) * 0.95;
|
if (!isFinite(scale) || scale <= 0) { scale = 1; }
|
const mirrorX = this.mapMirrorX ? -1 : 1;
|
const scaleX = scale * mirrorX;
|
const scaleY = scale;
|
const centerX = padding.left + availableW / 2;
|
const centerY = padding.top + availableH / 2;
|
const posX = centerX - (this.mapContentSize.width / 2) * scaleX;
|
const posY = centerY - (this.mapContentSize.height / 2) * scaleY;
|
this.pixiApp.stage.setTransform(posX, posY, scaleX, scaleY, 0, 0, 0, 0, 0);
|
this.scheduleAdjustLabels();
|
this.scheduleLocChunkCulling();
|
},
|
applyMapTransform(fitToView) {
|
if (!this.mapRoot || !this.mapContentSize.width || !this.mapContentSize.height) { return; }
|
const contentW = this.mapContentSize.width;
|
const contentH = this.mapContentSize.height;
|
this.mapRoot.pivot.set(contentW / 2, contentH / 2);
|
this.mapRoot.position.set(contentW / 2, contentH / 2);
|
this.mapRoot.rotation = (this.mapRotation % 360) * Math.PI / 180;
|
this.mapRoot.scale.set(1, 1);
|
if (fitToView) {
|
this.fitStageToContent();
|
} else {
|
this.scheduleAdjustLabels();
|
this.scheduleLocChunkCulling();
|
}
|
},
|
scheduleAdjustLabels() {
|
if (this.adjustLabelTimer) {
|
clearTimeout(this.adjustLabelTimer);
|
}
|
this.adjustLabelTimer = setTimeout(() => {
|
this.adjustLabelScale();
|
this.adjustLabelTimer = null;
|
}, 20);
|
},
|
adjustLabelScale() {
|
if (!this.pixiApp || !this.pixiLabelList.length) { return; }
|
const scaleX = this.pixiApp.stage.scale.x || 1;
|
const scaleY = this.pixiApp.stage.scale.y || 1;
|
const scale = Math.max(Math.abs(scaleX), Math.abs(scaleY), 0.0001);
|
const mirrorSign = scaleX < 0 ? -1 : 1;
|
const inverseRotation = -this.mapRoot.rotation;
|
const visible = scale >= 0.25;
|
this.pixiLabelList.forEach((sprite) => {
|
if (!sprite || !sprite.textObj) { return; }
|
const textObj = sprite.textObj;
|
let textScale = 1 / scale;
|
textScale = Math.max(0.9, Math.min(textScale, 3));
|
textObj.scale.set(textScale * mirrorSign, textScale);
|
textObj.rotation = inverseRotation;
|
textObj.visible = visible;
|
textObj.position.set(sprite.width / 2, sprite.height / 2);
|
});
|
},
|
startContainerResizeObserve() {
|
this.resizeToContainer = () => {
|
if (!this.pixiApp || !this.mapContentSize.width || !this.mapContentSize.height) { return; }
|
this.fitStageToContent();
|
};
|
if (window.ResizeObserver) {
|
this.containerResizeObserver = new ResizeObserver(() => {
|
this.resizeToContainer();
|
});
|
this.containerResizeObserver.observe(this.$refs.shell);
|
} else {
|
window.addEventListener('resize', this.resizeToContainer);
|
}
|
}
|
}
|
});
|
|
new Vue({
|
el: '#app'
|
});
|
</script>
|
</body>
|
</html>
|