let activeViewer = null; let currentHotspots = []; let securityApplied = false; let currentSceneOwnerId = null; // Lưu lại góc nhìn hiện tại vào localStorage trước khi trang bị reload (F5) window.addEventListener('beforeunload', () => { if (activeViewer) { localStorage.setItem('activeScenePitch', activeViewer.getPitch()); localStorage.setItem('activeSceneYaw', activeViewer.getYaw()); } }); /** * Hàm render tùy chỉnh cho hotspot để hiển thị bong bóng callout kèm ảnh thumbnail. * Thay thế icon mặc định của Pannellum bằng cấu trúc HTML mới. */ function renderCustomHotspot(hotSpotDiv, args) { // DỌN DẸP: Xóa sạch các ký tự mặc định (+ - []) của Pannellum hotSpotDiv.innerHTML = ''; hotSpotDiv.classList.add('pnlm-custom-hotspot'); // Tạo container chính cho bong bóng const callout = document.createElement('div'); callout.className = 'hotspot-callout-bubble'; // Tạo khung ảnh thumbnail (Luôn tạo để tránh bong bóng bị rỗng) const imgWrapper = document.createElement('div'); imgWrapper.className = 'hotspot-thumb-frame'; if (args.thumbUrl) { const img = document.createElement('img'); img.src = args.thumbUrl; imgWrapper.appendChild(img); } callout.appendChild(imgWrapper); // Thêm nhãn tiêu đề (tên cảnh đích) const title = document.createElement('div'); title.className = 'hotspot-callout-title'; title.innerText = args.title; callout.appendChild(title); hotSpotDiv.appendChild(callout); // Gắn sự kiện chuột phải trực tiếp vào bong bóng callout callout.addEventListener('contextmenu', (e) => { e.preventDefault(); e.stopPropagation(); // Ngăn chặn sự kiện lan truyền lên viewer // Kiểm tra quyền và mở menu chỉnh sửa hotspot if (typeof window.openHotspotMenu === 'function') { window.openHotspotMenu(args.hotspotData); // Truyền dữ liệu hotspot đầy đủ } }); } /** * Initializes and shows the Pannellum 360° panorama viewer with security overlays. * @param {string} imageUrl - Authorized URL to fetch the secure image stream * @param {Array} hotspots - List of hotspots from the database * @param {string} ownerId - ID of the scene owner * @param {number} initialPitch - Góc nhìn dọc khởi tạo * @param {number} initialYaw - Góc nhìn ngang khởi tạo */ function initPanoramaViewer(imageUrl, hotspots = [], ownerId = null, initialPitch = 0, initialYaw = 0) { currentHotspots = hotspots; currentSceneOwnerId = ownerId; const container = document.getElementById('viewer-container'); container.style.display = 'block'; if (activeViewer) { try { activeViewer.destroy(); } catch (e) {} } // Chuyển đổi dữ liệu hotspots từ DB sang định dạng Pannellum const token = localStorage.getItem('jwt'); const pannellumHotspots = hotspots.map(h => { const target = h.target_scene_id; let thumbUrl = ''; // Kiểm tra target phải là Object đã được populate thành công if (target && typeof target === 'object' && target.assetId) { const assetId = target.assetId._id || target.assetId; thumbUrl = `/api/assets/view/${assetId}`; // Đính kèm token để vượt qua kiểm tra quyền truy cập của middleware if (token) thumbUrl += `?token=${token}`; else if (target.privacy === 'shared' && target.shareToken) thumbUrl += `?token=${target.shareToken}`; } return { pitch: h.coordinates?.pitch || h.pitch, yaw: h.coordinates?.yaw || h.yaw, type: "custom", createTooltipFunc: renderCustomHotspot, createTooltipArgs: { title: h.title || target?.name || target?.title || "Điểm điều hướng", thumbUrl: thumbUrl, hotspotData: h // Truyền toàn bộ dữ liệu hotspot vào args để sử dụng trong renderCustomHotspot }, id: h._id, clickHandlerFunc: () => { if (target) { const targetId = target._id || target; const privacy = target.privacy || ''; const shareToken = target.shareToken || ''; // Chuyển cảnh với đầy đủ thông tin bảo mật openScene(targetId, privacy, shareToken); } } }; }); // Initialize Pannellum Equirectangular viewer activeViewer = pannellum.viewer('panorama-viewer', { "type": "equirectangular", "panorama": imageUrl, "autoLoad": true, "pitch": initialPitch, "yaw": initialYaw, "showControls": true, "compass": false, "mouseZoom": true, "keyboardZoom": true, "crossOrigin": "anonymous", "hotSpots": pannellumHotspots }); // Security constraints inside the viewer applyViewerSecurity(); } /** * Closes and destroys the active panorama viewer. */ function closeViewer() { document.getElementById('viewer-container').style.display = 'none'; // Xóa trạng thái Scene đang hoạt động khi đóng viewer localStorage.removeItem('activeSceneId'); localStorage.removeItem('activeScenePrivacy'); localStorage.removeItem('activeSceneToken'); localStorage.removeItem('activeScenePitch'); localStorage.removeItem('activeSceneYaw'); if (activeViewer) { try { activeViewer.destroy(); } catch (e) {} activeViewer = null; } } /** * Appends event listeners to block right-clicks and common image saving shortcuts. */ function applyViewerSecurity() { if (securityApplied) return; // Chỉ gán sự kiện một lần duy nhất securityApplied = true; // Target the actual viewer element where Pannellum renders const container = document.getElementById('viewer-container'); const panoramaViewer = document.getElementById('panorama-viewer'); const handleContextMenu = (e) => { e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); return false; // Ngăn chặn menu chuột phải mặc định của trình duyệt // Nếu viewer đang hoạt động, lấy tọa độ Pitch/Yaw tại điểm click if (activeViewer) { // Kiểm tra phân quyền trước khi cho phép tương tác chuột phải const userRole = localStorage.getItem('role'); const currentUserId = localStorage.getItem('userId'); // Phân quyền: Admin (Chủ sở hữu) hoặc Người tạo ra Scene này const isAdmin = userRole === 'admin' || userRole === 'Chủ sở hữu'; const isAuthorized = isAdmin || (currentUserId && currentSceneOwnerId && currentUserId.toString() === currentSceneOwnerId.toString()); // Lấy tọa độ cầu (Pitch/Yaw) từ điểm click chuột const coords = activeViewer.mouseEventToCoords(e); if (!coords) return false; const pitch = coords[0]; const yaw = coords[1]; // Nếu không được phép, dừng xử lý và chặn menu mặc định if (!isAuthorized) return false; // Nếu click chuột phải vào vùng trống, mở form tạo hotspot mới if (typeof window.handleHotspotCreation === 'function') { window.handleHotspotCreation(pitch, yaw, null); } console.log(`Coordinates captured: Pitch ${pitch}, Yaw ${yaw}`); } return false; }; // Sử dụng capture phase (true) để bắt sự kiện trước khi nó chạm đến Pannellum // container.addEventListener('contextmenu', handleContextMenu, true); // Bỏ gắn sự kiện này // panoramaViewer.addEventListener('contextmenu', handleContextMenu, true); // Bỏ gắn sự kiện này // Block drag and drop container.addEventListener('dragstart', (e) => { e.preventDefault(); }); } // Global safety shortcut listeners (Ctrl+S, Ctrl+U) - Tạm thời cho phép F12 và Ctrl+Shift+I để debug document.addEventListener('keydown', (e) => { // Only enforce when viewer is active if (document.getElementById('viewer-container').style.display === 'block') { const isCtrlS = e.ctrlKey && (e.key === 's' || e.key === 'S'); const isCtrlU = e.ctrlKey && (e.key === 'u' || e.key === 'U'); if (isCtrlS || isCtrlU) { e.preventDefault(); console.warn('Security Alert: Inspection and saving functions are restricted.'); return false; } } });