Reset data bị lỗi, thêm hotspot
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const hotspotSchema = new mongoose.Schema({
|
||||
parent_scene_id: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'Scene',
|
||||
required: true
|
||||
},
|
||||
target_scene_id: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'Scene'
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
trim: true
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
trim: true
|
||||
},
|
||||
coordinates: {
|
||||
yaw: { type: Number, required: true },
|
||||
pitch: { type: Number, required: true }
|
||||
},
|
||||
is_auto_return: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
}, {
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
module.exports = mongoose.model('Hotspot', hotspotSchema);
|
||||
+8
-31
@@ -1,25 +1,24 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const sceneSchema = new mongoose.Schema({
|
||||
title: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true
|
||||
},
|
||||
scene_url: {
|
||||
type: String
|
||||
},
|
||||
assetId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'Asset',
|
||||
required: true
|
||||
},
|
||||
lat: {
|
||||
type: Number,
|
||||
required: true
|
||||
gps: {
|
||||
lat: { type: Number, required: true },
|
||||
lng: { type: Number, required: true }
|
||||
},
|
||||
lng: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
owner: {
|
||||
createdBy: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true
|
||||
@@ -38,28 +37,6 @@ const sceneSchema = new mongoose.Schema({
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User'
|
||||
}],
|
||||
hotspots: [{
|
||||
pitch: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
yaw: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
trim: true
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
trim: true
|
||||
},
|
||||
targetSceneId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'Scene'
|
||||
}
|
||||
}]
|
||||
}, {
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
+143
-88
@@ -7,6 +7,7 @@ const crypto = require('crypto');
|
||||
const User = require('../models/User');
|
||||
const Asset = require('../models/Asset');
|
||||
const Scene = require('../models/Scene');
|
||||
const Hotspot = require('../models/Hotspot'); // Giả định bạn đã tạo model mới
|
||||
|
||||
const { protect, optionalAuth } = require('../middlewares/authMiddleware');
|
||||
const { verifyReferer, setNoCacheHeaders } = require('../middlewares/securityMiddleware');
|
||||
@@ -77,8 +78,8 @@ router.post('/scenes', protect, uploadSinglePanorama, async (req, res) => {
|
||||
return res.status(400).json({ message: 'Please upload a panorama image' });
|
||||
}
|
||||
|
||||
const latitude = parseFloat(lat);
|
||||
const longitude = parseFloat(lng);
|
||||
const latitude = Number(lat);
|
||||
const longitude = Number(lng);
|
||||
|
||||
if (isNaN(latitude) || isNaN(longitude)) {
|
||||
// Cleanup uploaded file on validation error
|
||||
@@ -134,11 +135,14 @@ router.post('/scenes', protect, uploadSinglePanorama, async (req, res) => {
|
||||
|
||||
// 7. Save Scene to DB
|
||||
const scene = new Scene({
|
||||
title,
|
||||
name: title,
|
||||
assetId: asset._id,
|
||||
lat: latitude,
|
||||
lng: longitude,
|
||||
owner: req.user._id,
|
||||
scene_url: processedFilePath, // Lưu đường dẫn ảnh trực tiếp
|
||||
gps: {
|
||||
lat: latitude,
|
||||
lng: longitude
|
||||
},
|
||||
createdBy: req.user._id,
|
||||
privacy: privacy || 'private',
|
||||
shareToken,
|
||||
sharedWith: parsedSharedWith
|
||||
@@ -176,7 +180,7 @@ router.get('/scenes', optionalAuth, async (req, res) => {
|
||||
{ privacy: 'public' },
|
||||
{ privacy: 'member' },
|
||||
{ privacy: 'shared' }, // shareToken will be required to fetch panorama, but coordinates show on map
|
||||
{ owner: req.user._id },
|
||||
{ createdBy: req.user._id },
|
||||
{ sharedWith: req.user._id }
|
||||
]
|
||||
};
|
||||
@@ -191,8 +195,8 @@ router.get('/scenes', optionalAuth, async (req, res) => {
|
||||
}
|
||||
|
||||
const scenes = await Scene.find(query)
|
||||
.populate('owner', 'username')
|
||||
.populate('assetId', 'coordinates createdAt');
|
||||
.populate('createdBy', 'username')
|
||||
.lean();
|
||||
|
||||
console.log(`[Data Load] Đã tìm thấy ${scenes.length} scenes. Gửi phản hồi về Frontend...`);
|
||||
res.json(scenes);
|
||||
@@ -210,7 +214,7 @@ router.get('/scenes', optionalAuth, async (req, res) => {
|
||||
router.get('/scenes/:id', optionalAuth, async (req, res) => {
|
||||
try {
|
||||
const scene = await Scene.findById(req.params.id)
|
||||
.populate('owner', 'username')
|
||||
.populate('createdBy', 'username')
|
||||
.populate('assetId');
|
||||
|
||||
if (!scene) {
|
||||
@@ -220,7 +224,7 @@ router.get('/scenes/:id', optionalAuth, async (req, res) => {
|
||||
const hasAccess =
|
||||
scene.privacy === 'public' ||
|
||||
(scene.privacy === 'member' && req.user) ||
|
||||
(req.user && scene.owner._id.toString() === req.user._id.toString()) ||
|
||||
(req.user && scene.createdBy._id.toString() === req.user._id.toString()) ||
|
||||
(req.user && scene.sharedWith.includes(req.user._id)) ||
|
||||
(scene.privacy === 'shared' && req.query.token === scene.shareToken);
|
||||
|
||||
@@ -235,83 +239,130 @@ router.get('/scenes/:id', optionalAuth, async (req, res) => {
|
||||
});
|
||||
|
||||
/**
|
||||
* @route POST /api/scenes/:id/hotspots
|
||||
* @desc Add a new hotspot to a scene
|
||||
* @access Private (Owner only)
|
||||
* @route GET /api/hotspots/:scene_id
|
||||
* @desc Lấy toàn bộ danh sách hotspot của scene hiện tại
|
||||
*/
|
||||
router.post('/scenes/:id/hotspots', protect, async (req, res) => {
|
||||
router.get('/hotspots/:scene_id', async (req, res) => {
|
||||
try {
|
||||
const { hotspotId, pitch, yaw, text, description, targetSceneId } = req.body;
|
||||
const scene = await Scene.findById(req.params.id);
|
||||
const hotspots = await Hotspot.find({ parent_scene_id: req.params.scene_id });
|
||||
res.json(hotspots);
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
if (!scene) {
|
||||
return res.status(404).json({ message: 'Scene not found' });
|
||||
/**
|
||||
* @route POST /api/hotspots/create
|
||||
* @desc Tạo mới Hotspot + Tự động tạo liên kết ngược
|
||||
*/
|
||||
router.post('/hotspots/create', protect, async (req, res) => {
|
||||
try {
|
||||
const { parent_scene_id, target_scene_id, title, description, coordinates } = req.body;
|
||||
|
||||
const parentScene = await Scene.findById(parent_scene_id);
|
||||
// Phân quyền: Admin hoặc Người tạo Scene
|
||||
const isAuthorized = req.user.role === 'Chủ sở hữu' || (parentScene && parentScene.createdBy.toString() === req.user._id.toString());
|
||||
if (!parentScene || !isAuthorized) {
|
||||
return res.status(403).json({ message: 'Không có quyền tạo hotspot cho scene này' });
|
||||
}
|
||||
|
||||
// Chỉ chủ sở hữu mới có quyền chỉnh sửa hotspots
|
||||
if (scene.owner.toString() !== req.user._id.toString()) {
|
||||
return res.status(403).json({ message: 'Access denied: Only the owner can add hotspots' });
|
||||
}
|
||||
|
||||
if (hotspotId) {
|
||||
// CẬP NHẬT HOTSPOT HIỆN CÓ
|
||||
const hs = scene.hotspots.id(hotspotId);
|
||||
if (!hs) return res.status(404).json({ message: 'Hotspot not found' });
|
||||
|
||||
hs.pitch = parseFloat(pitch) ?? hs.pitch;
|
||||
hs.yaw = parseFloat(yaw) ?? hs.yaw;
|
||||
hs.text = text ?? hs.text;
|
||||
hs.description = description ?? hs.description;
|
||||
hs.targetSceneId = targetSceneId ?? hs.targetSceneId;
|
||||
} else {
|
||||
// THÊM MỚI HOTSPOT
|
||||
const newHotspot = {
|
||||
pitch: parseFloat(pitch),
|
||||
yaw: parseFloat(yaw),
|
||||
text: text || '',
|
||||
description: description || '',
|
||||
targetSceneId: targetSceneId || undefined
|
||||
};
|
||||
|
||||
if (isNaN(newHotspot.pitch) || isNaN(newHotspot.yaw)) {
|
||||
return res.status(400).json({ message: 'Invalid coordinates' });
|
||||
}
|
||||
scene.hotspots.push(newHotspot);
|
||||
}
|
||||
|
||||
await scene.save();
|
||||
|
||||
// LOGIC "MẸ - CON": TỰ ĐỘNG TẠO ĐIỂM QUAY LẠI
|
||||
if (targetSceneId && targetSceneId !== 'null' && targetSceneId !== '' && typeof targetSceneId === 'string') {
|
||||
try {
|
||||
const targetScene = await Scene.findById(targetSceneId);
|
||||
if (targetScene) {
|
||||
const hasReverse = targetScene.hotspots.some(h =>
|
||||
h.targetSceneId && h.targetSceneId.toString() === scene._id.toString()
|
||||
);
|
||||
|
||||
if (!hasReverse) {
|
||||
const originYaw = parseFloat(yaw) || 0;
|
||||
const reverseYaw = originYaw > 0 ? originYaw - 180 : originYaw + 180;
|
||||
|
||||
targetScene.hotspots.push({
|
||||
pitch: 0,
|
||||
yaw: reverseYaw,
|
||||
text: `Quay lại: ${scene.title}`,
|
||||
targetSceneId: scene._id
|
||||
});
|
||||
await targetScene.save();
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Lỗi tạo hotspot ngược:", err.message);
|
||||
}
|
||||
}
|
||||
|
||||
res.status(201).json({
|
||||
message: 'Hotspot added successfully',
|
||||
hotspots: scene.hotspots
|
||||
const hotspot = new Hotspot({
|
||||
parent_scene_id,
|
||||
target_scene_id,
|
||||
title,
|
||||
description,
|
||||
coordinates: {
|
||||
yaw: Number(coordinates.yaw),
|
||||
pitch: Number(coordinates.pitch)
|
||||
},
|
||||
is_auto_return: false
|
||||
});
|
||||
await hotspot.save();
|
||||
|
||||
if (target_scene_id) {
|
||||
const targetScene = await Scene.findById(target_scene_id);
|
||||
if (targetScene) {
|
||||
const reverseYaw = coordinates.yaw > 0 ? coordinates.yaw - 180 : coordinates.yaw + 180;
|
||||
|
||||
// Fallback đa tầng cho tiêu đề quay lại
|
||||
const backLabel = title || parentScene.name || parentScene.title || 'cảnh trước';
|
||||
|
||||
const reverseHotspot = new Hotspot({
|
||||
parent_scene_id: target_scene_id,
|
||||
target_scene_id: parent_scene_id,
|
||||
title: `Quay lại ${backLabel}`,
|
||||
coordinates: { yaw: reverseYaw, pitch: 0 },
|
||||
is_auto_return: true
|
||||
});
|
||||
await reverseHotspot.save();
|
||||
}
|
||||
}
|
||||
|
||||
res.status(201).json(hotspot);
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @route PUT /api/hotspots/update/:id
|
||||
* @desc Cập nhật hotspot
|
||||
*/
|
||||
router.put('/hotspots/update/:id', protect, async (req, res) => {
|
||||
try {
|
||||
const { title, description, coordinates } = req.body;
|
||||
const hotspot = await Hotspot.findById(req.params.id);
|
||||
if (!hotspot) return res.status(404).json({ message: 'Hotspot không tồn tại' });
|
||||
|
||||
const parentScene = await Scene.findById(hotspot.parent_scene_id);
|
||||
// Phân quyền Admin hoặc Owner
|
||||
const isAuthorized = req.user.role === 'Chủ sở hữu' || (parentScene && parentScene.createdBy.toString() === req.user._id.toString());
|
||||
if (!isAuthorized) {
|
||||
return res.status(403).json({ message: 'Không có quyền cập nhật' });
|
||||
}
|
||||
|
||||
if (title) hotspot.title = title;
|
||||
if (description) hotspot.description = description;
|
||||
if (coordinates) {
|
||||
hotspot.coordinates = {
|
||||
yaw: Number(coordinates.yaw),
|
||||
pitch: Number(coordinates.pitch)
|
||||
};
|
||||
}
|
||||
|
||||
await hotspot.save();
|
||||
res.json(hotspot);
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @route DELETE /api/hotspots/delete/:id
|
||||
* @desc Xóa hotspot + Xóa luôn hotspot ngược tương ứng
|
||||
*/
|
||||
router.delete('/hotspots/delete/:id', protect, async (req, res) => {
|
||||
try {
|
||||
const hotspot = await Hotspot.findById(req.params.id);
|
||||
if (!hotspot) return res.status(404).json({ message: 'Hotspot không tồn tại' });
|
||||
|
||||
const parentScene = await Scene.findById(hotspot.parent_scene_id);
|
||||
// Phân quyền Admin hoặc Owner
|
||||
const isAuthorized = req.user.role === 'Chủ sở hữu' || (parentScene && parentScene.createdBy.toString() === req.user._id.toString());
|
||||
if (!isAuthorized) {
|
||||
return res.status(403).json({ message: 'Không có quyền xóa' });
|
||||
}
|
||||
|
||||
if (hotspot.target_scene_id) {
|
||||
await Hotspot.deleteOne({
|
||||
parent_scene_id: hotspot.target_scene_id,
|
||||
target_scene_id: hotspot.parent_scene_id,
|
||||
is_auto_return: true
|
||||
});
|
||||
}
|
||||
|
||||
await Hotspot.findByIdAndDelete(req.params.id);
|
||||
res.json({ message: 'Hotspot deleted successfully' });
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: error.message });
|
||||
}
|
||||
@@ -340,7 +391,7 @@ router.get('/assets/view/:assetId', verifyReferer, optionalAuth, async (req, res
|
||||
const hasAccess =
|
||||
scene.privacy === 'public' ||
|
||||
(scene.privacy === 'member' && req.user) ||
|
||||
(req.user && scene.owner.toString() === req.user._id.toString()) ||
|
||||
(req.user && scene.createdBy.toString() === req.user._id.toString()) ||
|
||||
(req.user && scene.sharedWith.includes(req.user._id)) ||
|
||||
(scene.privacy === 'shared' && req.query.token === scene.shareToken);
|
||||
|
||||
@@ -381,15 +432,17 @@ router.put('/scenes/:id', protect, uploadSinglePanorama, async (req, res) => {
|
||||
const { title, privacy, sharedWithUsers, lat, lng } = req.body;
|
||||
const scene = await Scene.findById(req.params.id);
|
||||
|
||||
if (!scene || scene.owner.toString() !== req.user._id.toString()) {
|
||||
// Phân quyền Admin hoặc Owner
|
||||
const isAuthorized = req.user.role === 'Chủ sở hữu' || (scene && scene.createdBy.toString() === req.user._id.toString());
|
||||
if (!scene || !isAuthorized) {
|
||||
return res.status(403).json({ message: 'Not authorized' });
|
||||
}
|
||||
|
||||
// Update basic info
|
||||
scene.title = title || scene.title;
|
||||
scene.name = title || scene.name;
|
||||
scene.privacy = privacy || scene.privacy;
|
||||
scene.lat = lat ? parseFloat(lat) : scene.lat;
|
||||
scene.lng = lng ? parseFloat(lng) : scene.lng;
|
||||
if (lat) scene.gps.lat = Number(lat);
|
||||
if (lng) scene.gps.lng = Number(lng);
|
||||
|
||||
if (privacy === 'shared' && !scene.shareToken) {
|
||||
scene.shareToken = crypto.randomBytes(24).toString('hex');
|
||||
@@ -428,7 +481,9 @@ router.put('/scenes/:id', protect, uploadSinglePanorama, async (req, res) => {
|
||||
router.delete('/scenes/:id', protect, async (req, res) => {
|
||||
try {
|
||||
const scene = await Scene.findById(req.params.id);
|
||||
if (!scene || scene.owner.toString() !== req.user._id.toString()) {
|
||||
// Phân quyền Admin hoặc Owner
|
||||
const isAuthorized = req.user.role === 'Chủ sở hữu' || (scene && scene.createdBy.toString() === req.user._id.toString());
|
||||
if (!scene || !isAuthorized) {
|
||||
return res.status(403).json({ message: 'Not authorized' });
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.2 MiB |
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 6.2 MiB After Width: | Height: | Size: 6.2 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.1 MiB |
+11
-1
@@ -74,7 +74,7 @@ html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 2000;
|
||||
z-index: 4000;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
@@ -170,6 +170,16 @@ html, body {
|
||||
#close-viewer-btn:hover {
|
||||
background: white;
|
||||
}
|
||||
/* Modal Overlay */
|
||||
.modal-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0; left: 0; width: 100%; height: 100%;
|
||||
background: rgba(0,0,0,0.7);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
z-index: 4000;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
|
||||
+51
-16
@@ -104,6 +104,22 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hotspot Action Choice Modal -->
|
||||
<div id="hotspot-action-modal" class="modal">
|
||||
<div class="modal-content action-modal-content">
|
||||
<span class="close-btn" onclick="closeHotspotActionModal()">×</span>
|
||||
<h2 id="hs-action-title">Tùy chọn Hotspot</h2>
|
||||
<div class="action-buttons">
|
||||
<button id="btn-hs-edit" class="edit-btn-large">
|
||||
<span class="icon">✏️</span> Chỉnh sửa Hotspot
|
||||
</button>
|
||||
<button id="btn-hs-delete" class="delete-btn-large">
|
||||
<span class="icon">🗑️</span> Xóa Hotspot
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 3D Panorama Viewer Container -->
|
||||
<div id="viewer-container" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 2000; background: #000;">
|
||||
<div id="panorama-viewer"></div>
|
||||
@@ -134,15 +150,21 @@
|
||||
</div>
|
||||
|
||||
<div class="form-group divider">
|
||||
<label>Liên kết tới ảnh 3D khác:</label>
|
||||
<div class="tab-container">
|
||||
<button type="button" class="tab-btn active" onclick="switchHSTab('select')">Chọn ảnh có sẵn</button>
|
||||
<button type="button" class="tab-btn" onclick="switchHSTab('upload')">Tải ảnh mới lên</button>
|
||||
<label>Kiểu liên kết:</label>
|
||||
<div class="radio-group">
|
||||
<label class="radio-item">
|
||||
<input type="radio" name="hsLinkType" value="existing" checked onclick="toggleHSLinkType('existing')">
|
||||
Chọn từ ảnh có sẵn
|
||||
</label>
|
||||
<label class="radio-item">
|
||||
<input type="radio" name="hsLinkType" value="upload" onclick="toggleHSLinkType('upload')">
|
||||
Upload ảnh mới
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab 1: Chọn ảnh có sẵn -->
|
||||
<div id="hs-tab-select" class="tab-content">
|
||||
<!-- Lựa chọn B: Chọn ảnh có sẵn -->
|
||||
<div id="hs-section-existing" class="tab-content">
|
||||
<label for="hs-target-id">Danh sách Scene của bạn:</label>
|
||||
<select id="hs-target-id" name="targetSceneId">
|
||||
<option value="">-- Chọn một cảnh để liên kết --</option>
|
||||
@@ -150,22 +172,35 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Tab 2: Tải ảnh mới -->
|
||||
<div id="hs-tab-upload" class="tab-content" style="display: none;">
|
||||
<!-- Lựa chọn A: Tải ảnh mới -->
|
||||
<div id="hs-section-upload" class="tab-content" style="display: none;">
|
||||
<label for="hs-panorama-file">Chọn ảnh Panorama 360°:</label>
|
||||
<input type="file" id="hs-panorama-file" name="panorama-file" accept="image/*">
|
||||
|
||||
<div class="gps-inheritance">
|
||||
<label>Xử lý GPS cho ảnh mới:</label>
|
||||
<select id="hs-gps-mode" onchange="toggleManualGPS()">
|
||||
<option value="auto">Đọc từ EXIF ảnh (Mặc định)</option>
|
||||
<option value="inherit">Lấy GPS của cảnh hiện tại</option>
|
||||
<option value="manual">Nhập thủ công</option>
|
||||
</select>
|
||||
|
||||
<div class="radio-group" style="flex-direction: column; gap: 5px;">
|
||||
<label class="radio-item">
|
||||
<input type="radio" name="hsGPSMode" value="map" checked onclick="toggleHSGPSMode('map')">
|
||||
Map Selection (Chọn trên bản đồ con)
|
||||
</label>
|
||||
<label class="radio-item">
|
||||
<input type="radio" name="hsGPSMode" value="manual" onclick="toggleHSGPSMode('manual')">
|
||||
Manual Input (Nhập thủ công)
|
||||
</label>
|
||||
<label class="radio-item">
|
||||
<input type="radio" name="hsGPSMode" value="inherit" onclick="toggleHSGPSMode('inherit')">
|
||||
Inherit Parent (Kế thừa từ cảnh hiện tại)
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div id="hs-map-selector" style="margin-top: 10px;">
|
||||
<div id="hs-mini-map"></div>
|
||||
</div>
|
||||
|
||||
<div id="hs-manual-gps" style="display: none; margin-top: 10px;">
|
||||
<input type="number" step="any" id="hs-lat" placeholder="Latitude">
|
||||
<input type="number" step="any" id="hs-lng" placeholder="Longitude">
|
||||
<input type="number" step="any" id="hs-lat" name="hs-lat" placeholder="Latitude">
|
||||
<input type="number" step="any" id="hs-lng" name="hs-lng" placeholder="Longitude">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+241
-91
@@ -5,6 +5,8 @@ let tempMarker = null;
|
||||
let markerClusterGroup;
|
||||
let currentSceneId = null;
|
||||
let previousSceneId = null;
|
||||
let miniMap = null;
|
||||
let miniMapMarker = null;
|
||||
|
||||
// Initialize when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
@@ -66,11 +68,17 @@ function initMap() {
|
||||
iconCreateFunction: function(cluster) {
|
||||
const childMarkers = cluster.getAllChildMarkers();
|
||||
try {
|
||||
// Thay vì tạo object mới phức tạp, lấy HTML của marker đầu tiên
|
||||
const firstIcon = childMarkers[0].options.icon;
|
||||
if (firstIcon) return firstIcon;
|
||||
if (childMarkers.length > 0 && childMarkers[0].options.icon) {
|
||||
const childIcon = childMarkers[0].options.icon;
|
||||
// Trả về một DivIcon MỚI dựa trên cấu hình của con, tránh dùng chung instance gây crash render
|
||||
return L.divIcon({
|
||||
html: childIcon.options.html,
|
||||
className: childIcon.options.className,
|
||||
iconSize: childIcon.options.iconSize,
|
||||
iconAnchor: childIcon.options.iconAnchor
|
||||
});
|
||||
}
|
||||
} catch (e) {}
|
||||
// Fallback an toàn nếu có lỗi
|
||||
return L.divIcon({ className: 'cluster-fallback', html: '<div style="background:#007bff;width:10px;height:10px;border-radius:50%;"></div>' });
|
||||
}
|
||||
});
|
||||
@@ -99,6 +107,11 @@ function initMap() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Chỉ Admin (Chủ sở hữu) mới có quyền tạo Scene mới trực tiếp trên map qua contextmenu
|
||||
const userRole = localStorage.getItem('role');
|
||||
const isAdmin = userRole === 'Chủ sở hữu' || userRole === 'admin';
|
||||
if (!isAdmin) return;
|
||||
|
||||
const { lat, lng } = e.latlng;
|
||||
openCreateSceneModal(lat, lng);
|
||||
});
|
||||
@@ -357,8 +370,9 @@ async function loadScenes() {
|
||||
|
||||
// Chỉ lặp qua danh sách Scene mẹ, lọc bỏ các hotspots trùng tọa độ
|
||||
scenes.forEach((scene) => {
|
||||
const latNum = parseFloat(scene.lat);
|
||||
const lngNum = parseFloat(scene.lng);
|
||||
// Ép kiểu tọa độ về Number để tránh lỗi render bản đồ
|
||||
const latNum = Number(scene.gps?.lat || scene.lat);
|
||||
const lngNum = Number(scene.gps?.lng || scene.lng);
|
||||
|
||||
if (isNaN(latNum) || isNaN(lngNum)) return;
|
||||
|
||||
@@ -368,12 +382,10 @@ async function loadScenes() {
|
||||
seenCoordinates.add(coordKey);
|
||||
|
||||
// Kiểm tra an toàn dữ liệu từ MongoDB trước khi truy cập
|
||||
if (!scene.assetId || !scene.assetId._id) {
|
||||
console.warn(`Scene "${scene.title}" thiếu dữ liệu ảnh (AssetId), bỏ qua.`);
|
||||
return;
|
||||
}
|
||||
const assetId = scene.assetId?._id || scene.assetId;
|
||||
const sceneName = scene.name || scene.title;
|
||||
|
||||
let thumbUrl = `${API_BASE_URL}/assets/view/${scene.assetId._id}`;
|
||||
let thumbUrl = `${API_BASE_URL}/assets/view/${assetId}`;
|
||||
if (token) thumbUrl += `?token=${token}`;
|
||||
else if (scene.privacy === 'shared' && scene.shareToken) thumbUrl += `?token=${scene.shareToken}`;
|
||||
|
||||
@@ -382,7 +394,7 @@ async function loadScenes() {
|
||||
html: `
|
||||
<div class="scene-callout">
|
||||
<div class="scene-img-wrapper">
|
||||
<img src="${thumbUrl}" alt="${scene.title}">
|
||||
<img src="${thumbUrl}" alt="${sceneName}">
|
||||
</div>
|
||||
</div>`,
|
||||
iconSize: [64, 64],
|
||||
@@ -391,16 +403,16 @@ async function loadScenes() {
|
||||
|
||||
const marker = L.marker([latNum, lngNum], {
|
||||
icon: calloutIcon,
|
||||
title: scene.title // Tooltip khi di chuột qua
|
||||
title: sceneName
|
||||
});
|
||||
|
||||
// Tạo nội dung thông tin khi Hover (Tooltip)
|
||||
const createdDate = scene.assetId?.createdAt ? new Date(scene.assetId.createdAt).toLocaleDateString('vi-VN') : 'N/A';
|
||||
const tooltipContent = `
|
||||
<div class="scene-hover-info">
|
||||
<strong>${scene.title}</strong><br>
|
||||
<strong>${sceneName}</strong><br>
|
||||
${scene.description ? `<small>${scene.description}</small><br>` : ''}
|
||||
<span>Người tạo: ${scene.owner ? scene.owner.username : 'Ẩn danh'}</span><br>
|
||||
<span>Người tạo: ${scene.createdBy ? scene.createdBy.username : 'Ẩn danh'}</span><br>
|
||||
<span>Ngày tạo: ${createdDate}</span>
|
||||
</div>
|
||||
`;
|
||||
@@ -423,9 +435,13 @@ async function loadScenes() {
|
||||
}
|
||||
|
||||
const currentUserId = localStorage.getItem('userId');
|
||||
const ownerId = scene.owner?._id || scene.owner;
|
||||
const userRole = localStorage.getItem('role');
|
||||
// Hỗ trợ cả schema cũ (owner) và mới (createdBy)
|
||||
const ownerId = scene.createdBy?._id || scene.createdBy || scene.owner?._id || scene.owner;
|
||||
|
||||
if (currentUserId && ownerId && ownerId.toString() === currentUserId.toString()) {
|
||||
// Phân quyền: Admin hoặc Chủ sở hữu Scene
|
||||
const isAdmin = userRole === 'Chủ sở hữu' || userRole === 'admin';
|
||||
if (isAdmin || (currentUserId && ownerId && ownerId.toString() === currentUserId.toString())) {
|
||||
handleEditDeleteScene(scene);
|
||||
} else {
|
||||
alert("Bạn không có quyền chỉnh sửa scene này.");
|
||||
@@ -482,9 +498,11 @@ function closeActionModal() {
|
||||
*/
|
||||
function openEditSceneModal(scene) {
|
||||
document.getElementById('modal-scene-id').value = scene._id;
|
||||
document.getElementById('modal-lat').value = scene.lat;
|
||||
document.getElementById('modal-lng').value = scene.lng;
|
||||
document.getElementById('modal-title').value = scene.title;
|
||||
// Cập nhật để hỗ trợ cả cấu trúc cũ và mới (gps.lat/name)
|
||||
document.getElementById('modal-lat').value = scene.gps?.lat || scene.lat;
|
||||
document.getElementById('modal-lng').value = scene.gps?.lng || scene.lng;
|
||||
const sceneName = scene.name || scene.title;
|
||||
document.getElementById('modal-title').value = sceneName;
|
||||
document.getElementById('modal-privacy').value = scene.privacy;
|
||||
document.getElementById('modal-panorama').required = false; // Photo update is optional
|
||||
|
||||
@@ -537,22 +555,28 @@ async function openScene(sceneId, privacy, shareToken, force = false) {
|
||||
localStorage.setItem('activeScenePrivacy', privacy || '');
|
||||
localStorage.setItem('activeSceneToken', shareToken || '');
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers
|
||||
});
|
||||
// Nạp đồng thời Scene và danh sách Hotspots từ Collection riêng
|
||||
const [sceneRes, hotspotsRes] = await Promise.all([
|
||||
fetch(url, { method: 'GET', headers }),
|
||||
fetch(`${API_BASE_URL}/hotspots/${sceneId}`, { method: 'GET', headers })
|
||||
]);
|
||||
|
||||
const scene = await response.json();
|
||||
if (!response.ok) throw new Error(scene.message || 'Failed to fetch scene details');
|
||||
const scene = await sceneRes.json();
|
||||
const hotspots = await hotspotsRes.json();
|
||||
|
||||
if (!sceneRes.ok) throw new Error(scene.message || 'Failed to fetch scene details');
|
||||
|
||||
// Lấy ID người tạo (createdBy) để phân quyền chuột phải trong viewer
|
||||
const sceneOwnerId = scene.createdBy?._id || scene.createdBy || scene.owner?._id || scene.owner;
|
||||
|
||||
// Tự động focus bản đồ vào vị trí của Scene
|
||||
if (map) {
|
||||
map.flyTo([scene.lat, scene.lng], 16);
|
||||
map.flyTo([scene.gps?.lat || scene.lat, scene.gps?.lng || scene.lng], 16);
|
||||
}
|
||||
|
||||
// Cập nhật tọa độ vào các input ẩn để hỗ trợ GPS inheritance cho hotspot khi tải ảnh mới
|
||||
document.getElementById('modal-lat').value = scene.lat;
|
||||
document.getElementById('modal-lng').value = scene.lng;
|
||||
document.getElementById('modal-lat').value = scene.gps?.lat || scene.lat;
|
||||
document.getElementById('modal-lng').value = scene.gps?.lng || scene.lng;
|
||||
|
||||
// Cập nhật lịch sử di chuyển để hỗ trợ tạo hotspot ngược tự động
|
||||
if (currentSceneId && currentSceneId !== sceneId) {
|
||||
@@ -560,8 +584,11 @@ async function openScene(sceneId, privacy, shareToken, force = false) {
|
||||
}
|
||||
currentSceneId = sceneId;
|
||||
|
||||
// Construct secure image URL passing shareToken if applicable
|
||||
let secureImageUrl = `${API_BASE_URL}/assets/view/${scene.assetId._id}`;
|
||||
// Kiểm tra an toàn assetId (hỗ trợ cả dạng Object và String ID)
|
||||
const assetId = scene.assetId?._id || scene.assetId;
|
||||
if (!assetId) throw new Error("Dữ liệu hình ảnh của cảnh này bị lỗi hoặc chưa xử lý xong.");
|
||||
|
||||
let secureImageUrl = `${API_BASE_URL}/assets/view/${assetId}`;
|
||||
|
||||
// Ưu tiên JWT token nếu đang đăng nhập, nếu không thì dùng shareToken
|
||||
if (token) {
|
||||
@@ -571,7 +598,7 @@ async function openScene(sceneId, privacy, shareToken, force = false) {
|
||||
}
|
||||
|
||||
// Initialize 3D Viewer with secure, referer-protected image stream
|
||||
initPanoramaViewer(secureImageUrl, scene.hotspots || []);
|
||||
initPanoramaViewer(secureImageUrl, hotspots || [], sceneOwnerId);
|
||||
|
||||
} catch (error) {
|
||||
localStorage.removeItem('activeSceneId');
|
||||
@@ -609,14 +636,22 @@ window.handleHotspotCreation = async function(pitch, yaw, existingHotspot = null
|
||||
const modal = document.getElementById('hotspot-modal');
|
||||
const form = document.getElementById('hotspot-form');
|
||||
|
||||
// Hiển thị Modal TRƯỚC để các logic UI (như Mini Map) tính toán được kích thước
|
||||
modal.style.display = 'flex';
|
||||
|
||||
// Reset form và gán tọa độ
|
||||
form.reset();
|
||||
switchHSTab('select'); // Luôn mặc định về tab chọn ảnh có sẵn khi mở modal
|
||||
document.getElementById('hs-pitch').value = pitch;
|
||||
document.getElementById('hs-yaw').value = yaw;
|
||||
document.getElementById('hs-id').value = existingHotspot ? existingHotspot._id : '';
|
||||
document.getElementById('hotspot-modal-title').innerText = existingHotspot ? 'Cập nhật điểm điều hướng' : 'Thêm điểm điều hướng mới';
|
||||
|
||||
// Reset UI states
|
||||
document.querySelector('input[name="hsLinkType"][value="existing"]').checked = true;
|
||||
window.toggleHSLinkType('existing');
|
||||
document.querySelector('input[name="hsGPSMode"][value="map"]').checked = true;
|
||||
window.toggleHSGPSMode('map');
|
||||
|
||||
// Lấy danh sách Scene có sẵn để đổ vào dropdown
|
||||
try {
|
||||
const res = await fetch(`${API_BASE_URL}/scenes`, { headers: { 'Authorization': `Bearer ${token}` } });
|
||||
@@ -625,52 +660,68 @@ window.handleHotspotCreation = async function(pitch, yaw, existingHotspot = null
|
||||
select.innerHTML = '<option value="">-- Chọn một cảnh để liên kết --</option>';
|
||||
scenes.forEach(s => {
|
||||
if (s._id !== currentSceneId) { // Không liên kết tới chính nó
|
||||
select.innerHTML += `<option value="${s._id}">${s.title}</option>`;
|
||||
select.innerHTML += `<option value="${s._id}">${s.name || s.title}</option>`;
|
||||
}
|
||||
});
|
||||
|
||||
// QUAN TRỌNG: Chỉ điền dữ liệu hotspot cũ SAU KHI dropdown đã được nạp đầy đủ options
|
||||
if (existingHotspot) {
|
||||
document.getElementById('hs-title').value = existingHotspot.text || '';
|
||||
document.getElementById('hs-title').value = existingHotspot.title || '';
|
||||
document.getElementById('hs-desc').value = existingHotspot.description || '';
|
||||
if (existingHotspot.targetSceneId) {
|
||||
select.value = existingHotspot.targetSceneId;
|
||||
if (existingHotspot.target_scene_id) {
|
||||
select.value = existingHotspot.target_scene_id;
|
||||
}
|
||||
}
|
||||
} catch (e) { console.error("Lỗi nạp danh sách scene:", e); }
|
||||
|
||||
modal.style.display = 'flex';
|
||||
|
||||
// Xử lý sự kiện submit form
|
||||
form.onsubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(form);
|
||||
|
||||
// Logic: Nếu chọn upload file mới, tạo Scene trước
|
||||
let finalTargetId = formData.get('targetSceneId');
|
||||
const file = document.getElementById('hs-panorama-file').files[0];
|
||||
|
||||
if (file) {
|
||||
const linkType = formData.get('hsLinkType');
|
||||
|
||||
if (linkType === 'upload') {
|
||||
const file = document.getElementById('hs-panorama-file').files[0];
|
||||
if (!file) {
|
||||
alert('Vui lòng chọn ảnh panorama.');
|
||||
return;
|
||||
}
|
||||
|
||||
const gpsMode = formData.get('hsGPSMode');
|
||||
let lat = 0, lng = 0;
|
||||
|
||||
if (gpsMode === 'inherit') {
|
||||
// Ép kiểu về Number khi lấy từ input
|
||||
lat = Number(document.getElementById('modal-lat').value);
|
||||
lng = Number(document.getElementById('modal-lng').value);
|
||||
} else {
|
||||
lat = Number(document.getElementById('hs-lat').value);
|
||||
lng = Number(document.getElementById('hs-lng').value);
|
||||
if (!lat || !lng) {
|
||||
alert('Vui lòng chọn vị trí GPS.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const sceneData = new FormData();
|
||||
sceneData.append('panorama', file);
|
||||
sceneData.append('title', formData.get('title'));
|
||||
const gpsMode = document.getElementById('hs-gps-mode').value;
|
||||
if (gpsMode === 'manual') {
|
||||
sceneData.append('lat', document.getElementById('hs-lat').value);
|
||||
sceneData.append('lng', document.getElementById('hs-lng').value);
|
||||
} else if (gpsMode === 'inherit') {
|
||||
sceneData.append('lat', document.getElementById('modal-lat').value);
|
||||
sceneData.append('lng', document.getElementById('modal-lng').value);
|
||||
}
|
||||
sceneData.append('lat', lat); // FormData sẽ convert sang string, Backend cần ép kiểu lại
|
||||
sceneData.append('lng', lng);
|
||||
sceneData.append('privacy', 'public');
|
||||
|
||||
uploadWithProgress(`${API_BASE_URL}/scenes`, 'POST', sceneData, token, 'hs', async (sceneRes) => {
|
||||
await saveHotspotToDB(pitch, yaw, formData.get('title'), formData.get('description'), sceneRes.scene._id, existingHotspot?._id);
|
||||
closeHotspotModal();
|
||||
});
|
||||
return; // Dừng luồng cũ vì uploadWithProgress đã tiếp quản
|
||||
return;
|
||||
}
|
||||
|
||||
// Lưu Hotspot
|
||||
const finalTargetId = formData.get('targetSceneId');
|
||||
if (!finalTargetId) {
|
||||
alert('Vui lòng chọn cảnh để liên kết.');
|
||||
return;
|
||||
}
|
||||
await saveHotspotToDB(pitch, yaw, formData.get('title'), formData.get('description'), finalTargetId, existingHotspot?._id);
|
||||
modal.style.display = 'none';
|
||||
};
|
||||
@@ -684,25 +735,64 @@ function closeHotspotModal() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Chuyển đổi giữa tab Chọn ảnh có sẵn và Tải ảnh mới
|
||||
* Khởi tạo hoặc cập nhật Mini Map trong Hotspot Modal
|
||||
*/
|
||||
function initHSMiniMap() {
|
||||
const pLat = parseFloat(document.getElementById('modal-lat').value) || 21.0285;
|
||||
const pLng = parseFloat(document.getElementById('modal-lng').value) || 105.8542;
|
||||
|
||||
if (miniMap) {
|
||||
miniMap.setView([pLat, pLng], 15);
|
||||
updateHSMiniMapMarker(pLat, pLng);
|
||||
// Fix lỗi vỡ tiles của Leaflet trong Modal
|
||||
setTimeout(() => miniMap.invalidateSize(), 200);
|
||||
return;
|
||||
}
|
||||
|
||||
miniMap = L.map('hs-mini-map').setView([pLat, pLng], 15);
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(miniMap);
|
||||
|
||||
miniMap.on('click', (e) => {
|
||||
const { lat, lng } = e.latlng;
|
||||
updateHSMiniMapMarker(lat, lng);
|
||||
});
|
||||
|
||||
updateHSMiniMapMarker(pLat, pLng);
|
||||
setTimeout(() => miniMap.invalidateSize(), 200);
|
||||
}
|
||||
|
||||
function updateHSMiniMapMarker(lat, lng) {
|
||||
if (miniMapMarker) miniMap.removeLayer(miniMapMarker);
|
||||
miniMapMarker = L.marker([lat, lng]).addTo(miniMap);
|
||||
document.getElementById('hs-lat').value = lat.toFixed(6);
|
||||
document.getElementById('hs-lng').value = lng.toFixed(6);
|
||||
}
|
||||
|
||||
window.toggleHSLinkType = function(type) {
|
||||
document.getElementById('hs-section-existing').style.display = type === 'existing' ? 'block' : 'none';
|
||||
document.getElementById('hs-section-upload').style.display = type === 'upload' ? 'block' : 'none';
|
||||
|
||||
if (type === 'upload') {
|
||||
const gpsMode = document.querySelector('input[name="hsGPSMode"]:checked')?.value || 'map';
|
||||
window.toggleHSGPSMode(gpsMode);
|
||||
}
|
||||
};
|
||||
|
||||
window.toggleHSGPSMode = function(mode) {
|
||||
document.getElementById('hs-map-selector').style.display = mode === 'map' ? 'block' : 'none';
|
||||
document.getElementById('hs-manual-gps').style.display = mode === 'manual' ? 'block' : 'none';
|
||||
|
||||
if (mode === 'map') initHSMiniMap();
|
||||
};
|
||||
|
||||
/**
|
||||
* Chuyển đổi tab cũ (giữ lại để tương thích nếu cần)
|
||||
*/
|
||||
function switchHSTab(tabName) {
|
||||
const selectTab = document.getElementById('hs-tab-select');
|
||||
const uploadTab = document.getElementById('hs-tab-upload');
|
||||
const btns = document.querySelectorAll('.tab-btn');
|
||||
|
||||
btns.forEach(btn => btn.classList.remove('active'));
|
||||
|
||||
if (tabName === 'select') {
|
||||
selectTab.style.display = 'block';
|
||||
uploadTab.style.display = 'none';
|
||||
btns[0].classList.add('active');
|
||||
document.getElementById('hs-panorama-file').value = ''; // Reset file input
|
||||
window.toggleHSLinkType('existing');
|
||||
} else {
|
||||
selectTab.style.display = 'none';
|
||||
uploadTab.style.display = 'block';
|
||||
btns[1].classList.add('active');
|
||||
document.getElementById('hs-target-id').value = ''; // Reset select
|
||||
window.toggleHSLinkType('upload');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -718,40 +808,43 @@ function toggleManualGPS() {
|
||||
/**
|
||||
* Gửi dữ liệu lưu Hotspot lên Backend
|
||||
*/
|
||||
async function saveHotspotToDB(pitch, yaw, text, description, targetSceneId, hotspotId) {
|
||||
async function saveHotspotToDB(pitch, yaw, title, description, targetSceneId, hotspotId) {
|
||||
const token = localStorage.getItem('jwt');
|
||||
// Gọi đúng API create hoặc update tùy vào trạng thái
|
||||
const url = hotspotId ? `${API_BASE_URL}/hotspots/update/${hotspotId}` : `${API_BASE_URL}/hotspots/create`;
|
||||
const method = hotspotId ? 'PUT' : 'POST';
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/scenes/${currentSceneId}/hotspots`, {
|
||||
method: 'POST',
|
||||
const body = {
|
||||
title,
|
||||
description,
|
||||
target_scene_id: targetSceneId,
|
||||
coordinates: {
|
||||
pitch: Number(pitch),
|
||||
yaw: Number(yaw)
|
||||
}
|
||||
};
|
||||
|
||||
// Nếu tạo mới, cần gửi kèm ID của scene hiện tại làm parent
|
||||
if (!hotspotId) {
|
||||
body.parent_scene_id = currentSceneId;
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
hotspotId,
|
||||
pitch,
|
||||
yaw,
|
||||
text,
|
||||
description,
|
||||
targetSceneId
|
||||
})
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (!response.ok) throw new Error(data.message || 'Lỗi khi lưu hotspot');
|
||||
|
||||
alert('Lưu điểm điều hướng thành công!');
|
||||
|
||||
// Refresh lại scene hiện tại để cập nhật viewer
|
||||
// Chúng ta cần lấy lại thông tin scene để có assetId mới nếu có
|
||||
const res = await fetch(`${API_BASE_URL}/scenes/${currentSceneId}`, {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
const updatedScene = await res.json();
|
||||
|
||||
let secureImageUrl = `${API_BASE_URL}/assets/view/${updatedScene.assetId._id}?token=${token}`;
|
||||
// Buộc nạp lại để cập nhật danh sách hotspot mới
|
||||
openScene(currentSceneId, updatedScene.privacy, updatedScene.shareToken || '', true);
|
||||
openScene(currentSceneId, localStorage.getItem('activeScenePrivacy'), localStorage.getItem('activeSceneToken'), true);
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@@ -785,3 +878,60 @@ window.systemReset = async function() {
|
||||
alert("Lỗi reset: " + e.message);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Mở Menu tùy chọn cho Hotspot (Sửa/Xóa)
|
||||
*/
|
||||
window.openHotspotMenu = function(hotspot) {
|
||||
const modal = document.getElementById('hotspot-action-modal');
|
||||
const editBtn = document.getElementById('btn-hs-edit');
|
||||
const deleteBtn = document.getElementById('btn-hs-delete');
|
||||
|
||||
modal.style.display = 'flex';
|
||||
|
||||
// Hành động Sửa: Mở form biên tập với dữ liệu cũ
|
||||
editBtn.onclick = () => {
|
||||
closeHotspotActionModal();
|
||||
window.handleHotspotCreation(hotspot.pitch, hotspot.yaw, hotspot);
|
||||
};
|
||||
|
||||
// Hành động Xóa: Xác nhận và gọi API xóa
|
||||
deleteBtn.onclick = async () => {
|
||||
if (confirm('Bạn có chắc chắn muốn xóa điểm điều hướng này?')) {
|
||||
closeHotspotActionModal();
|
||||
await deleteHotspot(hotspot._id);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Đóng Modal tùy chọn Hotspot
|
||||
*/
|
||||
function closeHotspotActionModal() {
|
||||
document.getElementById('hotspot-action-modal').style.display = 'none';
|
||||
}
|
||||
|
||||
/**
|
||||
* Xóa Hotspot thông qua API
|
||||
*/
|
||||
async function deleteHotspot(hotspotId) {
|
||||
const token = localStorage.getItem('jwt');
|
||||
try {
|
||||
// Gọi đúng API delete hotspot độc lập
|
||||
const response = await fetch(`${API_BASE_URL}/hotspots/delete/${hotspotId}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const err = await response.json();
|
||||
throw new Error(err.message || 'Lỗi xóa hotspot');
|
||||
}
|
||||
|
||||
alert('Đã xóa điểm điều hướng.');
|
||||
// Refresh lại scene hiện tại để cập nhật viewer
|
||||
openScene(currentSceneId, null, null, true);
|
||||
} catch (e) {
|
||||
alert(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
+34
-11
@@ -1,14 +1,17 @@
|
||||
let activeViewer = null;
|
||||
let currentHotspots = [];
|
||||
let securityApplied = false;
|
||||
let currentSceneOwnerId = null;
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
function initPanoramaViewer(imageUrl, hotspots = []) {
|
||||
function initPanoramaViewer(imageUrl, hotspots = [], ownerId = null) {
|
||||
currentHotspots = hotspots;
|
||||
currentSceneOwnerId = ownerId;
|
||||
const container = document.getElementById('viewer-container');
|
||||
container.style.display = 'block';
|
||||
|
||||
@@ -20,15 +23,15 @@ function initPanoramaViewer(imageUrl, hotspots = []) {
|
||||
|
||||
// Chuyển đổi dữ liệu hotspots từ DB sang định dạng Pannellum
|
||||
const pannellumHotspots = hotspots.map(h => ({
|
||||
pitch: h.pitch,
|
||||
yaw: h.yaw,
|
||||
pitch: h.coordinates?.pitch || h.pitch,
|
||||
yaw: h.coordinates?.yaw || h.yaw,
|
||||
type: "info",
|
||||
text: h.text || "Điểm điều hướng",
|
||||
text: h.title || "Điểm điều hướng",
|
||||
id: h._id,
|
||||
clickHandlerFunc: () => {
|
||||
if (h.targetSceneId) {
|
||||
if (h.target_scene_id || h.targetSceneId) {
|
||||
// Gọi hàm openScene từ main_map.js
|
||||
openScene(h.targetSceneId);
|
||||
openScene(h.target_scene_id || h.targetSceneId);
|
||||
}
|
||||
}
|
||||
}));
|
||||
@@ -87,23 +90,43 @@ function applyViewerSecurity() {
|
||||
|
||||
// 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 === 'Chủ sở hữu' || userRole === 'admin';
|
||||
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;
|
||||
if (!coords) return false;
|
||||
|
||||
const pitch = coords[0];
|
||||
const yaw = coords[1];
|
||||
|
||||
// Kiểm tra xem có hotspot nào gần điểm click không (ngưỡng 2 độ)
|
||||
const existing = currentHotspots.find(h =>
|
||||
Math.abs(h.pitch - pitch) < 2 && Math.abs(h.yaw - yaw) < 2
|
||||
Math.abs((h.coordinates?.pitch || h.pitch) - pitch) < 2 &&
|
||||
Math.abs((h.coordinates?.yaw || h.yaw) - yaw) < 2
|
||||
);
|
||||
|
||||
if (typeof window.handleHotspotCreation === 'function') {
|
||||
window.handleHotspotCreation(pitch, yaw, existing);
|
||||
// Nếu không được phép, dừng xử lý và chặn menu mặc định
|
||||
if (!isAuthorized) return false;
|
||||
|
||||
if (existing) {
|
||||
// ĐÃ CÓ Hotspot -> Hiện Menu: [Sửa Hotspot] / [Xóa Hotspot]
|
||||
if (typeof window.openHotspotMenu === 'function') {
|
||||
window.openHotspotMenu(existing);
|
||||
}
|
||||
} else {
|
||||
console.log(`Coordinates captured: Pitch ${pitch}, Yaw ${yaw}`);
|
||||
// CHƯA CÓ Hotspot -> Hiện Form: [Tạo mới Hotspot]
|
||||
if (typeof window.handleHotspotCreation === 'function') {
|
||||
window.handleHotspotCreation(pitch, yaw, null);
|
||||
}
|
||||
}
|
||||
console.log(`Coordinates captured: Pitch ${pitch}, Yaw ${yaw}`);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user