Khởi tạo dự án 3dtours
This commit is contained in:
@@ -0,0 +1,291 @@
|
||||
const API_BASE_URL = 'http://localhost:5000/api';
|
||||
|
||||
let map;
|
||||
let tempMarker = null;
|
||||
|
||||
// Initialize when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initMap();
|
||||
checkAuthStatus();
|
||||
loadScenes();
|
||||
});
|
||||
|
||||
/**
|
||||
* Initializes the full-screen Leaflet Map
|
||||
*/
|
||||
function initMap() {
|
||||
// Center of map defaults to Hanoi
|
||||
map = L.map('map').setView([21.0285, 105.8542], 13);
|
||||
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19,
|
||||
attribution: '© OpenStreetMap contributors'
|
||||
}).addTo(map);
|
||||
|
||||
// Event listener for right-click on map to open modal
|
||||
map.on('contextmenu', (e) => {
|
||||
const { lat, lng } = e.latlng;
|
||||
openCreateSceneModal(lat, lng);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if user is logged in (via localStorage JWT) and updates UI
|
||||
*/
|
||||
function checkAuthStatus() {
|
||||
const token = localStorage.getItem('jwt');
|
||||
const username = localStorage.getItem('username');
|
||||
const role = localStorage.getItem('role');
|
||||
|
||||
const authGuest = document.getElementById('auth-guest');
|
||||
const authLoggedIn = document.getElementById('auth-logged-in');
|
||||
|
||||
if (token && username) {
|
||||
authGuest.style.display = 'none';
|
||||
authLoggedIn.style.display = 'block';
|
||||
document.getElementById('logged-username').innerText = username;
|
||||
document.getElementById('logged-role').innerText = role || 'Thành viên';
|
||||
} else {
|
||||
authGuest.style.display = 'block';
|
||||
authLoggedIn.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles user login
|
||||
*/
|
||||
async function handleLogin() {
|
||||
const username = document.getElementById('username-input').value.trim();
|
||||
const password = document.getElementById('password-input').value.trim();
|
||||
|
||||
if (!username || !password) {
|
||||
alert('Please fill in both fields');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (!response.ok) throw new Error(data.message || 'Login failed');
|
||||
|
||||
localStorage.setItem('jwt', data.token);
|
||||
localStorage.setItem('username', data.user.username);
|
||||
localStorage.setItem('role', data.user.role);
|
||||
|
||||
checkAuthStatus();
|
||||
loadScenes(); // Reload scenes to show member/private scenes
|
||||
alert('Logged in successfully!');
|
||||
} catch (error) {
|
||||
alert(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles user registration
|
||||
*/
|
||||
async function handleRegister() {
|
||||
const username = document.getElementById('username-input').value.trim();
|
||||
const password = document.getElementById('password-input').value.trim();
|
||||
|
||||
if (!username || !password) {
|
||||
alert('Please fill in both fields');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/auth/register`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password, role: 'Thành viên' })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (!response.ok) throw new Error(data.message || 'Registration failed');
|
||||
|
||||
alert('Registration successful! You can now log in.');
|
||||
} catch (error) {
|
||||
alert(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles user logout
|
||||
*/
|
||||
function handleLogout() {
|
||||
localStorage.removeItem('jwt');
|
||||
localStorage.removeItem('username');
|
||||
localStorage.removeItem('role');
|
||||
checkAuthStatus();
|
||||
loadScenes(); // Reload scenes to filter out private ones
|
||||
alert('Logged out successfully');
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens Modal for creating a Scene and sets lat/lng inputs
|
||||
*/
|
||||
function openCreateSceneModal(lat, lng) {
|
||||
const token = localStorage.getItem('jwt');
|
||||
if (!token) {
|
||||
alert('Please log in first to create a 3D scene.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Place a temporary marker on the map
|
||||
if (tempMarker) map.removeLayer(tempMarker);
|
||||
tempMarker = L.marker([lat, lng]).addTo(map);
|
||||
|
||||
document.getElementById('modal-lat').value = lat.toFixed(6);
|
||||
document.getElementById('modal-lng').value = lng.toFixed(6);
|
||||
document.getElementById('create-scene-modal').style.display = 'flex';
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the Create Scene Modal and removes temporary marker
|
||||
*/
|
||||
function closeModal() {
|
||||
document.getElementById('create-scene-modal').style.display = 'none';
|
||||
if (tempMarker) {
|
||||
map.removeLayer(tempMarker);
|
||||
tempMarker = null;
|
||||
}
|
||||
document.getElementById('create-scene-form').reset();
|
||||
document.getElementById('shared-with-group').style.display = 'none';
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles visibility of the shared users input based on privacy selection
|
||||
*/
|
||||
function toggleSharedUsers() {
|
||||
const privacy = document.getElementById('modal-privacy').value;
|
||||
const group = document.getElementById('shared-with-group');
|
||||
if (privacy === 'shared') {
|
||||
group.style.display = 'block';
|
||||
} else {
|
||||
group.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Form submission for Scene creation (multipart/form-data)
|
||||
*/
|
||||
async function submitScene(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const form = document.getElementById('create-scene-form');
|
||||
const formData = new FormData(form);
|
||||
|
||||
const token = localStorage.getItem('jwt');
|
||||
if (!token) {
|
||||
alert('Please log in to submit');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/scenes`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: formData
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (!response.ok) throw new Error(data.message || 'Failed to save scene');
|
||||
|
||||
alert('Scene created successfully!');
|
||||
closeModal();
|
||||
loadScenes();
|
||||
} catch (error) {
|
||||
alert(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads and displays visible Scenes on the map
|
||||
*/
|
||||
async function loadScenes() {
|
||||
try {
|
||||
const token = localStorage.getItem('jwt');
|
||||
const headers = {};
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/scenes`, {
|
||||
method: 'GET',
|
||||
headers
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Failed to load scenes');
|
||||
const scenes = await response.json();
|
||||
|
||||
// Clear existing markers (excluding tempMarker)
|
||||
map.eachLayer((layer) => {
|
||||
if (layer instanceof L.Marker && layer !== tempMarker) {
|
||||
map.removeLayer(layer);
|
||||
}
|
||||
});
|
||||
|
||||
// Add Markers for each scene
|
||||
scenes.forEach((scene) => {
|
||||
const marker = L.marker([scene.lat, scene.lng]).addTo(map);
|
||||
|
||||
// Generate Popup content with a View button
|
||||
let popupContent = `
|
||||
<div class="popup-box">
|
||||
<h4>${scene.title}</h4>
|
||||
<p>Owner: <strong>${scene.owner ? scene.owner.username : 'Unknown'}</strong></p>
|
||||
<p>Privacy: <span class="badge">${scene.privacy.toUpperCase()}</span></p>
|
||||
<button class="view-btn" onclick="openScene('${scene._id}', '${scene.privacy}', '${scene.shareToken || ''}')">View 360° Panorama</button>
|
||||
</div>
|
||||
`;
|
||||
marker.bindPopup(popupContent);
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading scenes:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches secure scene details and triggers the Panorama viewer
|
||||
*/
|
||||
async function openScene(sceneId, privacy, shareToken) {
|
||||
try {
|
||||
const token = localStorage.getItem('jwt');
|
||||
const headers = {};
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
let url = `${API_BASE_URL}/scenes/${sceneId}`;
|
||||
if (privacy === 'shared' && shareToken) {
|
||||
url += `?token=${shareToken}`;
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers
|
||||
});
|
||||
|
||||
const scene = await response.json();
|
||||
if (!response.ok) throw new Error(scene.message || 'Failed to fetch scene details');
|
||||
|
||||
// Construct secure image URL passing shareToken if applicable
|
||||
let secureImageUrl = `${API_BASE_URL}/assets/view/${scene.assetId._id}`;
|
||||
if (privacy === 'shared' && scene.shareToken) {
|
||||
secureImageUrl += `?token=${scene.shareToken}`;
|
||||
}
|
||||
|
||||
// Initialize 3D Viewer with secure, referer-protected image stream
|
||||
initPanoramaViewer(secureImageUrl);
|
||||
|
||||
} catch (error) {
|
||||
alert(error.message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
let activeViewer = null;
|
||||
|
||||
/**
|
||||
* Initializes and shows the Pannellum 360° panorama viewer with security overlays.
|
||||
* @param {string} imageUrl - Authorized URL to fetch the secure image stream
|
||||
*/
|
||||
function initPanoramaViewer(imageUrl) {
|
||||
const container = document.getElementById('viewer-container');
|
||||
container.style.display = 'block';
|
||||
|
||||
if (activeViewer) {
|
||||
try {
|
||||
activeViewer.destroy();
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// Initialize Pannellum Equirectangular viewer
|
||||
activeViewer = pannellum.viewer('panorama-viewer', {
|
||||
"type": "equirectangular",
|
||||
"panorama": imageUrl,
|
||||
"autoLoad": true,
|
||||
"showControls": true,
|
||||
"compass": false,
|
||||
"mouseZoom": true,
|
||||
"keyboardZoom": true
|
||||
});
|
||||
|
||||
// Security constraints inside the viewer
|
||||
applyViewerSecurity();
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes and destroys the active panorama viewer.
|
||||
*/
|
||||
function closeViewer() {
|
||||
document.getElementById('viewer-container').style.display = 'none';
|
||||
if (activeViewer) {
|
||||
try {
|
||||
activeViewer.destroy();
|
||||
} catch (e) {}
|
||||
activeViewer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends event listeners to block right-clicks and common image saving shortcuts.
|
||||
*/
|
||||
function applyViewerSecurity() {
|
||||
const viewer = document.getElementById('viewer-container');
|
||||
|
||||
// Block right-clicks inside the 3D Viewer Container
|
||||
viewer.addEventListener('contextmenu', (e) => {
|
||||
e.preventDefault();
|
||||
alert('Security Alert: Direct image downloading or copying is disabled on this asset.');
|
||||
});
|
||||
|
||||
// Block drag and drop
|
||||
viewer.addEventListener('dragstart', (e) => {
|
||||
e.preventDefault();
|
||||
});
|
||||
}
|
||||
|
||||
// Global safety shortcut listeners (F12, Ctrl+S, Ctrl+U, Ctrl+Shift+I)
|
||||
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');
|
||||
const isF12 = e.key === 'F12';
|
||||
const isCtrlShiftI = e.ctrlKey && e.shiftKey && (e.key === 'i' || e.key === 'I');
|
||||
|
||||
if (isCtrlS || isCtrlU || isF12 || isCtrlShiftI) {
|
||||
e.preventDefault();
|
||||
alert('Security Alert: Inspection and saving functions are restricted on this viewer.');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user