Files
3dtours/frontend/js/viewer360.js
T

234 lines
8.9 KiB
JavaScript

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) => {
// Nếu click trúng vào hotspot hiện có thì không xử lý tại đây
// để listener của hotspot (trong renderCustomHotspot) được chạy.
if (e.target.closest('.pnlm-custom-hotspot')) {
return;
}
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
// 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);
panoramaViewer.addEventListener('contextmenu', handleContextMenu, true);
// 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;
}
}
});