From 827ea37573b713f6b2f2268c66e5a9a270aefb11 Mon Sep 17 00:00:00 2001
From: Junjie <fallin.jie@qq.com>
Date: 星期五, 06 三月 2026 17:04:07 +0800
Subject: [PATCH] #

---
 src/main/webapp/components/MapCanvas.js |  442 +++++++++++++++++++++++++++++++++++++++++++++++++-----
 1 files changed, 397 insertions(+), 45 deletions(-)

diff --git a/src/main/webapp/components/MapCanvas.js b/src/main/webapp/components/MapCanvas.js
index 3147540..276827b 100644
--- a/src/main/webapp/components/MapCanvas.js
+++ b/src/main/webapp/components/MapCanvas.js
@@ -1,16 +1,32 @@
 Vue.component('map-canvas', {
   template: `
     <div style="width: 100%; height: 100%; position: relative;">
-      <div ref="pixiView"></div>
+      <div ref="pixiView" style="position: absolute; inset: 0;"></div>
+      <div style="position: absolute; top: 12px; left: 14px; z-index: 30; pointer-events: none; max-width: 52%;">
+        <div style="display: flex; flex-direction: column; gap: 6px; align-items: flex-start;">
+          <div v-for="item in cycleCapacity.loopList"
+               :key="'loop-' + item.loopNo"
+               @mouseenter="handleLoopCardEnter(item)"
+               @mouseleave="handleLoopCardLeave(item)"
+               style="padding: 6px 10px; border-radius: 4px; background: rgba(11, 35, 58, 0.72); color: #fff; font-size: 12px; line-height: 1.4; white-space: nowrap; pointer-events: auto;">
+            鍦坽{ item.loopNo }} |
+            绔欑偣: {{ item.stationCount || 0 }} |
+            浠诲姟: {{ item.taskCount || 0 }} |
+            鎵胯浇: {{ formatLoadPercent(item.currentLoad) }}
+          </div>
+        </div>
+      </div>
       <div v-show="shelfTooltip.visible"
            :style="shelfTooltipStyle()">
         {{ shelfTooltip.text }}
       </div>
-      <div style="position: absolute; top: 20px; right: 50px; text-align: right;">
-        <div>FPS:{{mapFps}}</div>
-        <div style="margin-top: 6px; display: flex; gap: 6px; justify-content: flex-end;">
-          <button type="button" @click="rotateMap" style="padding: 2px 8px; font-size: 12px; cursor: pointer;">鏃嬭浆</button>
-          <button type="button" @click="toggleMirror" style="padding: 2px 8px; font-size: 12px; cursor: pointer;">{{ mapMirrorX ? '鍙栨秷闀滃儚' : '闀滃儚' }}</button>
+      <div style="position: absolute; top: 18px; right: 34px; z-index: 30; display: flex; flex-direction: column; align-items: flex-end; gap: 8px;">
+        <div :style="mapToolFpsStyle()">FPS {{ mapFps }}</div>
+        <button type="button" @click="toggleMapToolPanel" :style="mapToolToggleStyle(showMapToolPanel)">{{ showMapToolPanel ? '鏀惰捣鎿嶄綔' : '鍦板浘鎿嶄綔' }}</button>
+        <div v-show="showMapToolPanel" :style="mapToolBarStyle()">
+          <button type="button" @click="toggleStationDirection" :style="mapToolButtonStyle(showStationDirection)">{{ showStationDirection ? '闅愯棌绔欑偣鏂瑰悜' : '鏄剧ず绔欑偣鏂瑰悜' }}</button>
+          <button type="button" @click="rotateMap" :style="mapToolButtonStyle(false)">鏃嬭浆</button>
+          <button type="button" @click="toggleMirror" :style="mapToolButtonStyle(mapMirrorX)">{{ mapMirrorX ? '鍙栨秷闀滃儚' : '闀滃儚' }}</button>
         </div>
       </div>
     </div>
@@ -80,14 +96,28 @@
         item: null
       },
       shelfTooltipMinScale: 0.4,
+      containerResizeObserver: null,
       timer: null,
       adjustLabelTimer: null,
-      isSwitchingFloor: false
+      isSwitchingFloor: false,
+      cycleCapacity: {
+        loopList: [],
+        totalStationCount: 0,
+        taskStationCount: 0,
+        currentLoad: 0
+      },
+      showMapToolPanel: false,
+      showStationDirection: false,
+      hoverLoopNo: null,
+      hoverLoopStationIdSet: new Set(),
+      loopHighlightColor: 0xfff34d,
+      stationDirectionColor: 0xff5a36
     }
   },
     mounted() {
     this.currentLev = this.lev || 1;
     this.createMap();
+    this.startContainerResizeObserve();
     this.loadMapTransformConfig();
     this.loadLocList();
     this.connectWs();
@@ -100,6 +130,7 @@
       this.getCrnInfo();
       this.getDualCrnInfo();
       this.getSiteInfo();
+      this.getCycleCapacityInfo();
       this.getRgvInfo();
     }, 1000);
   },
@@ -108,6 +139,7 @@
 
     if (this.hoverRaf) { cancelAnimationFrame(this.hoverRaf); this.hoverRaf = null; }
     if (this.pixiApp) { this.pixiApp.destroy(true, { children: true }); }
+    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) {} }
@@ -160,6 +192,69 @@
     }
   },
   methods: {
+    mapToolBarStyle() {
+      return {
+        display: 'flex',
+        gap: '8px',
+        alignItems: 'center',
+        padding: '7px',
+        borderRadius: '14px',
+        background: 'rgba(255, 255, 255, 0.72)',
+        border: '1px solid rgba(160, 180, 205, 0.3)',
+        boxShadow: '0 8px 20px rgba(37, 64, 97, 0.08)',
+        backdropFilter: 'blur(4px)'
+      };
+    },
+    mapToolFpsStyle() {
+      return {
+        padding: '4px 10px',
+        borderRadius: '999px',
+        background: 'rgba(255, 255, 255, 0.7)',
+        border: '1px solid rgba(160, 180, 205, 0.28)',
+        color: '#48617c',
+        fontSize: '12px',
+        lineHeight: '18px',
+        letterSpacing: '0.04em',
+        boxShadow: '0 6px 16px rgba(37, 64, 97, 0.06)',
+        userSelect: 'none'
+      };
+    },
+    mapToolToggleStyle(active) {
+      return {
+        appearance: 'none',
+        border: '1px solid ' + (active ? 'rgba(96, 132, 170, 0.36)' : 'rgba(160, 180, 205, 0.3)'),
+        background: active ? 'rgba(235, 243, 251, 0.96)' : 'rgba(255, 255, 255, 0.82)',
+        color: '#46617b',
+        height: '30px',
+        padding: '0 12px',
+        borderRadius: '999px',
+        fontSize: '12px',
+        lineHeight: '30px',
+        cursor: 'pointer',
+        whiteSpace: 'nowrap',
+        boxShadow: '0 6px 16px rgba(37, 64, 97, 0.06)'
+      };
+    },
+    mapToolButtonStyle(active) {
+      return {
+        appearance: 'none',
+        border: '1px solid ' + (active ? 'rgba(255, 136, 93, 0.38)' : 'rgba(160, 180, 205, 0.3)'),
+        background: active ? 'rgba(255, 119, 77, 0.16)' : 'rgba(255, 255, 255, 0.88)',
+        color: active ? '#d85a31' : '#4d647d',
+        height: '30px',
+        padding: '0 12px',
+        borderRadius: '10px',
+        fontSize: '12px',
+        lineHeight: '30px',
+        cursor: 'pointer',
+        transition: 'all 0.2s ease',
+        boxShadow: active ? '0 4px 10px rgba(255, 119, 77, 0.12)' : 'none',
+        whiteSpace: 'nowrap'
+      };
+    },
+    toggleMapToolPanel() {
+      this.showMapToolPanel = !this.showMapToolPanel;
+    },
     createMap() {
       this.pixiApp = new PIXI.Application({ backgroundColor: 0xF5F7F9, antialias: false, powerPreference: 'high-performance', autoDensity: true, resolution: Math.min(window.devicePixelRatio || 1, 2) });
       PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.LINEAR;
@@ -277,11 +372,33 @@
       });
       //*******************FPS*******************
     },
+    startContainerResizeObserve() {
+      if (typeof ResizeObserver === 'undefined' || !this.$el) { return; }
+      this.containerResizeObserver = new ResizeObserver(() => {
+        this.resizeToContainer();
+      });
+      this.containerResizeObserver.observe(this.$el);
+    },
+    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 };
+    },
     resizeToContainer() {
       const w = this.$el.clientWidth || 0;
       const h = this.$el.clientHeight || 0;
       if (w > 0 && h > 0 && this.pixiApp) {
+        const vw = this.pixiApp.renderer && this.pixiApp.renderer.screen ? this.pixiApp.renderer.screen.width : 0;
+        const vh = this.pixiApp.renderer && this.pixiApp.renderer.screen ? this.pixiApp.renderer.screen.height : 0;
+        if (vw === w && vh === h) { return; }
         this.pixiApp.renderer.resize(w, h);
+        if (this.mapContentSize && this.mapContentSize.width > 0 && this.mapContentSize.height > 0) {
+          this.applyMapTransform(true);
+        }
       }
     },
     getMap() {
@@ -289,6 +406,7 @@
     },
     changeFloor(lev) {
       this.currentLev = lev;
+      this.clearLoopStationHighlight();
       this.isSwitchingFloor = true;
       this.hideShelfTooltip();
       this.hoveredShelfCell = null;
@@ -313,6 +431,7 @@
       this.getMap();
     },
     createMapData(map) {
+      this.clearLoopStationHighlight();
       this.hideShelfTooltip();
       this.hoveredShelfCell = null;
       this.mapRowOffsets = [];
@@ -427,6 +546,14 @@
             }
           }
           xCursor += cellWidth;
+        }
+      });
+
+      map.forEach((row, rowIndex) => {
+        for (let colIndex = 0; colIndex < row.length; colIndex++) {
+          const val = row[colIndex];
+          if (!val || val.type !== 'devp' || val.type === 'merge') { continue; }
+          val.stationDirectionList = this.resolveStationDirectionList(map, rowIndex, colIndex, val);
         }
       });
 
@@ -627,21 +754,7 @@
           sta.statusObj = null;
           if (sta.textObj.parent !== sta) { sta.addChild(sta.textObj); sta.textObj.position.set(sta.width / 2, sta.height / 2); }
         }
-        if (status === "site-auto") {
-          this.updateColor(sta, 0x78ff81);
-        } else if (status === "site-auto-run" || status === "site-auto-id" || status === "site-auto-run-id") {
-          this.updateColor(sta, 0xfa51f6);
-        } else if (status === "site-unauto") {
-          this.updateColor(sta, 0xb8b8b8);
-        } else if (status === "machine-pakin") {
-          this.updateColor(sta, 0x30bffc);
-        } else if (status === "machine-pakout") {
-          this.updateColor(sta, 0x97b400);
-        } else if (status === "site-run-block") {
-          this.updateColor(sta, 0xe69138);
-        } else {
-          this.updateColor(sta, 0xb8b8b8);
-        }
+        this.setStationBaseColor(sta, this.getStationStatusColor(status));
       });
     },
     getCrnInfo() {
@@ -659,6 +772,38 @@
     getRgvInfo() {
       if (this.isSwitchingFloor) { return; }
       this.sendWs(JSON.stringify({ url: "/console/latest/data/rgv", data: {} }));
+    },
+    getCycleCapacityInfo() {
+      if (this.isSwitchingFloor) { return; }
+      this.sendWs(JSON.stringify({ url: "/console/latest/data/station/cycle/capacity", data: {} }));
+    },
+    setCycleCapacityInfo(res) {
+      const payload = res && res.code === 200 ? res.data : null;
+      if (res && res.code === 403) { parent.location.href = baseUrl + "/login"; return; }
+      if (!payload) { return; }
+      const loopList = Array.isArray(payload.loopList) ? payload.loopList : [];
+      this.cycleCapacity = {
+        loopList: loopList,
+        totalStationCount: payload.totalStationCount || 0,
+        taskStationCount: payload.taskStationCount || 0,
+        currentLoad: typeof payload.currentLoad === 'number' ? payload.currentLoad : parseFloat(payload.currentLoad || 0)
+      };
+      if (this.hoverLoopNo != null) {
+        const targetLoop = loopList.find(v => v && v.loopNo === this.hoverLoopNo);
+        if (targetLoop) {
+          this.hoverLoopStationIdSet = this.buildStationIdSet(targetLoop.stationIdList);
+          this.applyLoopStationHighlight();
+        } else {
+          this.clearLoopStationHighlight();
+        }
+      }
+    },
+    formatLoadPercent(load) {
+      let value = typeof load === 'number' ? load : parseFloat(load || 0);
+      if (!isFinite(value)) { value = 0; }
+      if (value < 0) { value = 0; }
+      if (value > 1) { value = 1; }
+      return (value * 100).toFixed(1) + "%";
     },
     setCrnInfo(res) {
       let crns = Array.isArray(res) ? res : (res && res.code === 200 ? res.data : null);
@@ -814,6 +959,7 @@
       if (this.wsReconnectTimer) { clearTimeout(this.wsReconnectTimer); this.wsReconnectTimer = null; }
       this.wsReconnectAttempts = 0;
       this.getMap(this.currentLev);
+      this.getCycleCapacityInfo();
     },
     webSocketOnError(e) {
       this.scheduleReconnect();
@@ -828,6 +974,8 @@
         this.setDualCrnInfo(JSON.parse(result.data));
       } else if (result.url === "/console/latest/data/rgv") {
         this.setRgvInfo(JSON.parse(result.data));
+      } else if (result.url === "/console/latest/data/station/cycle/capacity") {
+        this.setCycleCapacityInfo(JSON.parse(result.data));
       } else if (typeof result.url === "string" && result.url.indexOf("/basMap/lev/") === 0) {
         this.setMap(JSON.parse(result.data));
       }
@@ -1136,6 +1284,17 @@
       const brightness = (r * 299 + g * 587 + b * 114) / 1000;
       return brightness > 150 ? '#000000' : '#ffffff';
     },
+    getStationStatusColor(status) {
+      if (status === "site-auto") { return 0x78ff81; }
+      if (status === "site-auto-run") { return 0xfa51f6; }
+      if (status === "site-auto-id") { return 0xc4c400; }
+      if (status === "site-auto-run-id") { return 0x30bffc; }
+      if (status === "site-unauto") { return 0xb8b8b8; }
+      if (status === "machine-pakin") { return 0x30bffc; }
+      if (status === "machine-pakout") { return 0x97b400; }
+      if (status === "site-run-block") { return 0xe69138; }
+      return 0xb8b8b8;
+    },
     getCrnStatusColor(status) {
       if (status === "machine-auto") { return 0x21BA45; }
       if (status === "machine-un-auto") { return 0xBBBBBB; }
@@ -1167,6 +1326,13 @@
         }
         sprite = new PIXI.Sprite(texture);
         sprite._kind = 'devp';
+        const directionOverlay = this.createStationDirectionOverlay(item.width, item.height, item.stationDirectionList);
+        if (directionOverlay) {
+          directionOverlay.visible = this.showStationDirection;
+          sprite.addChild(directionOverlay);
+        }
+        sprite.directionObj = directionOverlay;
+        sprite._stationDirectionList = Array.isArray(item.stationDirectionList) ? item.stationDirectionList.slice() : [];
         let siteId = this.getStationId(value);
         if (siteId === -1) { siteId = item.data; }
         const style = new PIXI.TextStyle({ fontFamily: 'Arial', fontSize: 10, fill: '#000000', stroke: '#ffffff', strokeThickness: 1 });
@@ -1175,7 +1341,11 @@
         text.position.set(sprite.width / 2, sprite.height / 2);
         sprite.addChild(text);
         sprite.textObj = text;
-        if (siteId != null && siteId !== -1) { this.pixiStaMap.set(parseInt(siteId), sprite); }
+        const stationIdInt = parseInt(siteId, 10);
+        if (!isNaN(stationIdInt)) { this.pixiStaMap.set(stationIdInt, sprite); }
+        sprite._stationId = isNaN(stationIdInt) ? null : stationIdInt;
+        sprite._baseColor = 0x00ff7f;
+        sprite._loopHighlighted = false;
         sprite.interactive = true;
         sprite.buttonMode = true;
         sprite.on('pointerdown', () => {
@@ -1377,6 +1547,74 @@
       }
       sprite.tint = color;
     },
+    setStationBaseColor(sprite, color) {
+      if (!sprite) { return; }
+      sprite._baseColor = color;
+      if (this.isStationInHoverLoop(sprite)) {
+        this.applyHighlightColor(sprite);
+      } else {
+        this.updateColor(sprite, color);
+        sprite._loopHighlighted = false;
+      }
+    },
+    applyHighlightColor(sprite) {
+      if (!sprite) { return; }
+      this.updateColor(sprite, this.loopHighlightColor);
+      sprite._loopHighlighted = true;
+    },
+    isStationInHoverLoop(sprite) {
+      if (!sprite || sprite._stationId == null || !this.hoverLoopStationIdSet) { return false; }
+      return this.hoverLoopStationIdSet.has(sprite._stationId);
+    },
+    buildStationIdSet(stationIdList) {
+      const set = new Set();
+      if (!Array.isArray(stationIdList)) { return set; }
+      stationIdList.forEach((id) => {
+        const v = parseInt(id, 10);
+        if (!isNaN(v)) { set.add(v); }
+      });
+      return set;
+    },
+    applyLoopStationHighlight() {
+      if (!this.pixiStaMap) { return; }
+      this.pixiStaMap.forEach((sprite) => {
+        if (!sprite) { return; }
+        if (this.isStationInHoverLoop(sprite)) {
+          this.applyHighlightColor(sprite);
+        } else if (sprite._loopHighlighted) {
+          const baseColor = (typeof sprite._baseColor === 'number') ? sprite._baseColor : 0xb8b8b8;
+          this.updateColor(sprite, baseColor);
+          sprite._loopHighlighted = false;
+        }
+      });
+    },
+    clearLoopStationHighlight() {
+      if (this.pixiStaMap) {
+        this.pixiStaMap.forEach((sprite) => {
+          if (!sprite || !sprite._loopHighlighted) { return; }
+          const baseColor = (typeof sprite._baseColor === 'number') ? sprite._baseColor : 0xb8b8b8;
+          this.updateColor(sprite, baseColor);
+          sprite._loopHighlighted = false;
+        });
+      }
+      this.hoverLoopNo = null;
+      this.hoverLoopStationIdSet = new Set();
+    },
+    handleLoopCardEnter(loopItem) {
+      if (!loopItem) { return; }
+      this.hoverLoopNo = loopItem.loopNo;
+      this.hoverLoopStationIdSet = this.buildStationIdSet(loopItem.stationIdList);
+      this.applyLoopStationHighlight();
+    },
+    handleLoopCardLeave(loopItem) {
+      if (!loopItem) {
+        this.clearLoopStationHighlight();
+        return;
+      }
+      if (this.hoverLoopNo === loopItem.loopNo) {
+        this.clearLoopStationHighlight();
+      }
+    },
     isJson(str) {
       try { JSON.parse(str); return true; } catch (e) { return false; }
     },
@@ -1388,6 +1626,132 @@
     },
     getStationId(obj) {
       if (this.isJson(obj)) { let data = JSON.parse(obj); if (data.stationId == null || data.stationId == undefined) { return -1; } return data.stationId; } else { return -1; }
+    },
+    parseMapValue(obj) {
+      if (obj == null) { return null; }
+      if (typeof obj === 'object') { return obj; }
+      if (!this.isJson(obj)) { return null; }
+      try {
+        return JSON.parse(obj);
+      } catch (e) {
+        return null;
+      }
+    },
+    normalizeDirectionList(direction) {
+      const aliasMap = {
+        top: 'top',
+        up: 'top',
+        north: 'top',
+        bottom: 'bottom',
+        down: 'bottom',
+        south: 'bottom',
+        left: 'left',
+        west: 'left',
+        right: 'right',
+        east: 'right'
+      };
+      let rawList = [];
+      if (Array.isArray(direction)) {
+        rawList = direction;
+      } else if (typeof direction === 'string') {
+        rawList = direction.split(/[,\s|/]+/);
+      }
+      const result = [];
+      const seen = new Set();
+      rawList.forEach((item) => {
+        const key = aliasMap[String(item || '').trim().toLowerCase()];
+        if (!key || seen.has(key)) { return; }
+        seen.add(key);
+        result.push(key);
+      });
+      return result;
+    },
+    resolveStationDirectionList(map, rowIndex, colIndex, item) {
+      const valueObj = this.parseMapValue(item && item.value);
+      const fromValue = this.normalizeDirectionList(valueObj && valueObj.direction);
+      if (fromValue.length > 0) { return fromValue; }
+      const rowSpan = item && item.rowSpan ? item.rowSpan : 1;
+      const colSpan = item && item.colSpan ? item.colSpan : 1;
+      const fallback = [];
+      const candidateList = [
+        { key: 'top', cell: this.resolveMergedCell(map, rowIndex - 1, colIndex) },
+        { key: 'right', cell: this.resolveMergedCell(map, rowIndex, colIndex + colSpan) },
+        { key: 'bottom', cell: this.resolveMergedCell(map, rowIndex + rowSpan, colIndex) },
+        { key: 'left', cell: this.resolveMergedCell(map, rowIndex, colIndex - 1) }
+      ];
+      candidateList.forEach((candidate) => {
+        if (this.isStationDirectionNeighbor(candidate.cell)) {
+          fallback.push(candidate.key);
+        }
+      });
+      return fallback;
+    },
+    isStationDirectionNeighbor(cell) {
+      if (!cell) { return false; }
+      if (cell.type === 'devp') { return true; }
+      return this.isTrackType(cell);
+    },
+    createStationDirectionOverlay(width, height, directionList) {
+      if (!Array.isArray(directionList) || directionList.length === 0) { return null; }
+      const container = new PIXI.Container();
+      const arrowSize = Math.max(4, Math.min(width, height) * 0.22);
+      const margin = Math.max(2, Math.min(width, height) * 0.12);
+      directionList.forEach((direction) => {
+        const arrow = new PIXI.Graphics();
+        this.drawStationDirectionArrow(arrow, width, height, direction, arrowSize, margin);
+        container.addChild(arrow);
+      });
+      return container;
+    },
+    drawStationDirectionArrow(graphics, width, height, direction, size, margin) {
+      if (!graphics) { return; }
+      const halfBase = Math.max(2, size * 0.45);
+      const stemLen = Math.max(3, size * 0.7);
+      const centerX = width / 2;
+      const centerY = height / 2;
+      graphics.beginFill(this.stationDirectionColor, 0.95);
+      if (direction === 'top') {
+        const tipY = margin;
+        const baseY = margin + size;
+        const stemY = Math.min(centerY - 2, baseY + stemLen);
+        if (stemY > baseY) { graphics.moveTo(centerX, stemY); graphics.lineTo(centerX, baseY); }
+        graphics.moveTo(centerX, tipY);
+        graphics.lineTo(centerX - halfBase, baseY);
+        graphics.lineTo(centerX + halfBase, baseY);
+      } else if (direction === 'right') {
+        const tipX = width - margin;
+        const baseX = width - margin - size;
+        const stemX = Math.max(centerX + 2, baseX - stemLen);
+        if (stemX < baseX) { graphics.moveTo(stemX, centerY); graphics.lineTo(baseX, centerY); }
+        graphics.moveTo(tipX, centerY);
+        graphics.lineTo(baseX, centerY - halfBase);
+        graphics.lineTo(baseX, centerY + halfBase);
+      } else if (direction === 'bottom') {
+        const tipY = height - margin;
+        const baseY = height - margin - size;
+        const stemY = Math.max(centerY + 2, baseY - stemLen);
+        if (stemY < baseY) { graphics.moveTo(centerX, stemY); graphics.lineTo(centerX, baseY); }
+        graphics.moveTo(centerX, tipY);
+        graphics.lineTo(centerX - halfBase, baseY);
+        graphics.lineTo(centerX + halfBase, baseY);
+      } else if (direction === 'left') {
+        const tipX = margin;
+        const baseX = margin + size;
+        const stemX = Math.min(centerX - 2, baseX + stemLen);
+        if (stemX > baseX) { graphics.moveTo(stemX, centerY); graphics.lineTo(baseX, centerY); }
+        graphics.moveTo(tipX, centerY);
+        graphics.lineTo(baseX, centerY - halfBase);
+        graphics.lineTo(baseX, centerY + halfBase);
+      }
+      graphics.closePath();
+      graphics.endFill();
+    },
+    applyStationDirectionVisibility() {
+      if (!this.pixiStaMap) { return; }
+      this.pixiStaMap.forEach((sprite) => {
+        if (!sprite || !sprite.directionObj) { return; }
+        sprite.directionObj.visible = this.showStationDirection;
+      });
     },
     getTrackSiteNo(obj) {
       if (this.isJson(obj)) { let data = JSON.parse(obj); if (data.trackSiteNo == null || data.trackSiteNo == undefined) { return -1; } return data.trackSiteNo; } else { return -1; }
@@ -1642,8 +2006,9 @@
     adjustLabelScale() {
       const s = this.pixiApp && this.pixiApp.stage ? Math.abs(this.pixiApp.stage.scale.x || 1) : 1;
       const minPx = 14;
-      const vw = this.pixiApp.view.width;
-      const vh = this.pixiApp.view.height;
+      const viewport = this.getViewportSize();
+      const vw = viewport.width;
+      const vh = viewport.height;
       const margin = 50;
       const mirrorSign = this.mapMirrorX ? -1 : 1;
       const inverseRotation = -((this.mapRotation % 360) * Math.PI / 180);
@@ -1709,6 +2074,10 @@
       this.mapRotation = (this.mapRotation + 90) % 360;
       this.applyMapTransform(true);
       this.saveMapTransformConfig();
+    },
+    toggleStationDirection() {
+      this.showStationDirection = !this.showStationDirection;
+      this.applyStationDirectionVisibility();
     },
     toggleMirror() {
       this.mapMirrorX = !this.mapMirrorX;
@@ -1821,8 +2190,9 @@
       const contentW = size.width || 0;
       const contentH = size.height || 0;
       if (contentW <= 0 || contentH <= 0) { return; }
-      const vw = this.pixiApp.view.width;
-      const vh = this.pixiApp.view.height;
+      const viewport = this.getViewportSize();
+      const vw = viewport.width;
+      const vh = viewport.height;
       let scale = Math.min(vw / contentW, vh / contentH) * 0.95;
       if (!isFinite(scale) || scale <= 0) { scale = 1; }
       const baseW = this.mapContentSize.width || contentW;
@@ -1856,24 +2226,6 @@
     }
   }
 });
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
 
 
 

--
Gitblit v1.9.1