MyVision

My Vision v2.0 – Interactive Floor Plan Viewer

https://cdnjs.cloudflare.com/ajax/libs/pannellum/2.5.6/pannellum.js

* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: ‘Segoe UI’, Tahoma, Geneva, Verdana, sans-serif;
}

body {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f5f5;
}

header {
background-color: #333;
color: white;
padding: 10px 20px;
display: flex;
justify-content: space-between;
align-items: center;
}

.app-title {
font-size: 20px;
font-weight: bold;
display: flex;
align-items: center;
}

.app-title i {
margin-right: 10px;
}

.controls {
display: flex;
align-items: center;
}

button {
background-color: #4CAF50;
color: white;
border: none;
padding: 8px 15px;
border-radius: 4px;
cursor: pointer;
margin-left: 10px;
transition: background-color 0.2s;
}

button:hover {
background-color: #45a049;
}

.mode-select {
display: flex;
background-color: #444;
border-radius: 4px;
overflow: hidden;
margin-right: 10px;
}

.mode-btn {
background-color: transparent;
border: none;
color: white;
padding: 8px 15px;
margin: 0;
border-radius: 0;
border-right: 1px solid #555;
}

.mode-btn:last-child {
border-right: none;
}

.mode-btn.active {
background-color: #4CAF50;
}

main {
display: flex;
flex: 1;
overflow: hidden;
}

.sidebar {
width: 300px;
background-color: white;
border-right: 1px solid #ddd;
padding: 15px;
overflow-y: auto;
display: flex;
flex-direction: column;
}

.sidebar-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}

.hotspot-list {
flex: 1;
overflow-y: auto;
}

.hotspot-item {
background-color: #f9f9f9;
border: 1px solid #ddd;
padding: 12px;
margin-bottom: 10px;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
}

.hotspot-item:hover {
background-color: #f0f0f0;
transform: translateY(-2px);
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}

.hotspot-item.selected {
border-color: #4CAF50;
background-color: #f0fff0;
}

.hotspot-name {
font-weight: bold;
margin-bottom: 5px;
display: flex;
align-items: center;
}

.hotspot-name i {
margin-right: 8px;
color: #666;
}

.canvas-area {
flex: 1;
background-color: #e9e9e9;
display: flex;
justify-content: center;
align-items: center;
overflow: auto;
position: relative;
}

.placeholder {
text-align: center;
padding: 20px;
}

.placeholder i {
font-size: 48px;
color: #ccc;
margin-bottom: 10px;
}

.placeholder button {
margin-top: 15px;
}

.floor-plan-container {
position: relative;
display: none;
max-width: 100%;
max-height: 100%;
}

.floor-plan {
max-width: 100%;
max-height: 90vh;
display: block;
border: 1px solid #ddd;
background-color: white;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}

.hotspot-marker {
position: absolute;
width: 24px;
height: 24px;
background-color: rgba(255, 0, 0, 0.8);
border: 2px solid white;
border-radius: 50%;
transform: translate(-50%, -50%);
cursor: pointer;
box-shadow: 0 2px 5px rgba(0,0,0,0.3);
transition: all 0.2s;
display: flex;
justify-content: center;
align-items: center;
color: white;
font-size: 12px;
font-weight: bold;
z-index: 5;
user-select: none;
}

.hotspot-marker:hover {
transform: translate(-50%, -50%) scale(1.2);
z-index: 10;
}

.hotspot-marker.dragging {
cursor: grabbing;
transform: translate(-50%, -50%) scale(1.3);
z-index: 15;
box-shadow: 0 4px 15px rgba(0,0,0,0.5);
}

.hotspot-marker.draggable {
cursor: grab;
}

.hotspot-marker.draggable:hover {
background-color: rgba(255, 100, 100, 0.9);
}

.hotspot-label {
position: absolute;
background-color: rgba(0, 0, 0, 0.7);
color: white;
padding: 3px 8px;
border-radius: 3px;
font-size: 12px;
white-space: nowrap;
pointer-events: none;
transform: translate(15px, -50%);
z-index: 5;
}

.hotspot-form {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
background-color: white;
padding: 20px;
border-top: 1px solid #ddd;
box-shadow: 0 -5px 15px rgba(0,0,0,0.1);
display: none;
z-index: 100;
}

.form-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}

.form-close {
background: none;
border: none;
font-size: 20px;
cursor: pointer;
color: #666;
}

.form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
}

.form-group {
margin-bottom: 15px;
}

.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}

.form-group input,
.form-group select {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}

.form-group.full-width {
grid-column: 1 / -1;
}

.form-buttons {
display: flex;
justify-content: flex-end;
gap: 10px;
grid-column: 1 / -1;
}

.media-viewer {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.9);
display: none;
justify-content: center;
align-items: center;
z-index: 1000;
}

.media-container {
position: relative;
width: 90%;
height: 90%;
}

.media-container img,
.media-container video {
max-width: 100%;
max-height: 90vh;
display: block;
}

.close-btn {
position: absolute;
top: -40px;
right: 0;
background: none;
border: none;
color: white;
font-size: 24px;
cursor: pointer;
}

#panorama-viewer {
width: 100%;
height: 100%;
display: none;
}

#video360-viewer {
width: 100%;
height: 100%;
display: none;
}

.file-format-info {
font-size: 12px;
color: #666;
margin-top: 3px;
}

.media-preview {
grid-column: 1 / -1;
height: 200px;
background-color: #f5f5f5;
border: 1px solid #ddd;
border-radius: 4px;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
}

.media-preview img,
.media-preview video {
max-width: 100%;
max-height: 100%;
}

.media-preview .placeholder-icon {
font-size: 48px;
color: #ccc;
}

.toast {
position: fixed;
top: 20px;
right: 20px;
background-color: rgba(0, 0, 0, 0.7);
color: white;
padding: 10px 20px;
border-radius: 4px;
z-index: 2000;
display: none;
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2);
}

.show-labels-container {
display: flex;
align-items: center;
margin-top: 10px;
padding: 10px;
background-color: #f5f5f5;
border-radius: 4px;
}

.toggle-switch {
position: relative;
display: inline-block;
width: 50px;
height: 24px;
margin-left: 10px;
}

.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}

.toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .4s;
border-radius: 24px;
}

.toggle-slider:before {
position: absolute;
content: “”;
height: 16px;
width: 16px;
left: 4px;
bottom: 4px;
background-color: white;
transition: .4s;
border-radius: 50%;
}

input:checked + .toggle-slider {
background-color: #4CAF50;
}

input:checked + .toggle-slider:before {
transform: translateX(26px);
}

.options-panel {
margin-top: auto;
padding-top: 15px;
border-top: 1px solid #eee;
}

.coordinates-info {
background-color: #f0f8ff;
padding: 8px;
border-radius: 4px;
font-size: 11px;
color: #666;
margin-top: 5px;
}

/* Mobile responsiveness */
@media (max-width: 768px) {
.sidebar {
width: 250px;
}

.form-grid {
grid-template-columns: 1fr;
}
}

My Vision v2.0

Create

Edit

View

New

Replace Plan

Save

Load

No floor plan loaded

Load a floor plan to start

Load Floor Plan

Add Hotspot

×

Name:

Media type:

Image
Video
360° Photo
360° Video

Supported formats: JPEG, PNG

Media file:

Position X (%):

Position Y (%):

Coordinates are saved as percentages for cross-device compatibility

Cancel

Save

×

console.log(‘My Vision v2.0 Loading…’);

// DOM Elements
const createModeBtn = document.getElementById(‘create-mode’);
const editModeBtn = document.getElementById(‘edit-mode’);
const viewModeBtn = document.getElementById(‘view-mode’);
const newBtn = document.getElementById(‘new-btn’);
const saveBtn = document.getElementById(‘save-btn’);
const loadBtn = document.getElementById(‘load-btn’);
const loadFloorPlanBtn = document.getElementById(‘load-floor-plan’);
const replaceFloorPlanBtn = document.getElementById(‘replace-floor-plan’);
const placeholder = document.getElementById(‘placeholder’);
const floorPlanContainer = document.getElementById(‘floor-plan-container’);
const floorPlan = document.getElementById(‘floor-plan’);
const hotspotList = document.getElementById(‘hotspot-list’);
const hotspotCount = document.getElementById(‘hotspot-count’);
const hotspotForm = document.getElementById(‘hotspot-form’);
const formTitle = document.getElementById(‘form-title’);
const formClose = document.getElementById(‘form-close’);
const hotspotNameInput = document.getElementById(‘hotspot-name’);
const hotspotTypeSelect = document.getElementById(‘hotspot-type’);
const formatInfo = document.getElementById(‘format-info’);
const hotspotMediaInput = document.getElementById(‘hotspot-media’);
const hotspotXInput = document.getElementById(‘hotspot-x’);
const hotspotYInput = document.getElementById(‘hotspot-y’);
const mediaPreview = document.getElementById(‘media-preview’);
const saveHotspotBtn = document.getElementById(‘save-hotspot’);
const cancelHotspotBtn = document.getElementById(‘cancel-hotspot’);
const mediaViewer = document.getElementById(‘media-viewer’);
const imageViewer = document.getElementById(‘image-viewer’);
const videoViewer = document.getElementById(‘video-viewer’);
const panoramaViewer = document.getElementById(‘panorama-viewer’);
const video360Viewer = document.getElementById(‘video360-viewer’);
const closeViewerBtn = document.getElementById(‘close-viewer’);
const toast = document.getElementById(‘toast’);
const showLabelsToggle = document.getElementById(‘show-labels’);

// State variables
let currentMode = ‘create’;
let floorplanImage = null;
let hotspots = [];
let currentHotspot = null;
let panoramaInstance = null;
let video360Instance = null;
let projectName = ‘New Project’;
let showLabels = false;
let originalImageWidth = 0;
let originalImageHeight = 0;

// Drag & Drop variables
let isDragging = false;
let dragHotspot = null;
let dragOffset = { x: 0, y: 0 };

console.log(‘Variables initialized’);

// Convert pixel coordinates to percentage
function pixelToPercent(x, y, imgWidth, imgHeight) {
const xPercent = (x / imgWidth) * 100;
const yPercent = (y / imgHeight) * 100;
console.log(`Converting pixels (${x}, ${y}) to percent (${xPercent.toFixed(2)}%, ${yPercent.toFixed(2)}%)`);
return { x: xPercent, y: yPercent };
}

// Convert percentage coordinates to pixel
function percentToPixel(xPercent, yPercent, imgWidth, imgHeight) {
const x = (xPercent / 100) * imgWidth;
const y = (yPercent / 100) * imgHeight;
console.log(`Converting percent (${xPercent}%, ${yPercent}%) to pixels (${x}, ${y})`);
return { x: x, y: y };
}

// Get current image dimensions
function getCurrentImageDimensions() {
const rect = floorPlan.getBoundingClientRect();
return {
width: rect.width,
height: rect.height
};
}

// Update hotspot positions when window resizes
window.addEventListener(‘resize’, function() {
console.log(‘Window resized, updating hotspot positions’);
updateHotspotMarkers();
});

// Function to set the mode
function setMode(mode) {
console.log(‘Setting mode to:’, mode);
currentMode = mode;

// Update UI for mode buttons
createModeBtn.classList.toggle(‘active’, mode === ‘create’);
editModeBtn.classList.toggle(‘active’, mode === ‘edit’);
viewModeBtn.classList.toggle(‘active’, mode === ‘view’);

// Show/hide replace button
if (floorplanImage) {
replaceFloorPlanBtn.style.display = (mode === ‘create’ || mode === ‘edit’) ? ‘inline-block’ : ‘none’;
}

// Handle cursor and interactions
if (floorPlan) {
floorPlan.style.cursor = mode === ‘view’ ? ‘default’ : ‘crosshair’;
}

// Hide form in view mode
if (mode === ‘view’) {
hideHotspotForm();
}

// Update hotspot list to reflect the mode
renderHotspotList();

// Update markers
updateHotspotMarkers();

showToast(`Mode: ${mode === ‘create’ ? ‘Create’ : mode === ‘edit’ ? ‘Edit’ : ‘View’}`);
}

// New project
function newProject() {
console.log(‘Creating new project’);
if (confirm(‘Create a new project? Unsaved data will be lost.’)) {
projectName = ‘New Project’;
floorplanImage = null;
hotspots = [];
currentHotspot = null;
originalImageWidth = 0;
originalImageHeight = 0;

// Reset UI
placeholder.style.display = ‘flex’;
floorPlanContainer.style.display = ‘none’;
floorPlan.src = ”;
renderHotspotList();

showToast(‘New project created’);
}
}

// Update info about media type
function updateMediaTypeInfo() {
const type = hotspotTypeSelect.value;

switch(type) {
case ‘image’:
formatInfo.textContent = ‘Supported formats: JPEG, PNG, GIF, WebP’;
hotspotMediaInput.accept = ‘image/jpeg,image/png,image/gif,image/webp’;
break;
case ‘video’:
formatInfo.textContent = ‘Supported formats: MP4, MOV, M4V, WebM’;
hotspotMediaInput.accept = ”;
break;
case ‘panorama’:
formatInfo.textContent = ‘Supported formats: Equirectangular images (2:1) JPEG, PNG’;
hotspotMediaInput.accept = ‘image/jpeg,image/png’;
break;
case ‘video360’:
formatInfo.textContent = ‘Supported formats: 360° videos MP4, WebM’;
hotspotMediaInput.accept = ”;
break;
}
}

// Preview selected media
function previewSelectedMedia() {
const file = hotspotMediaInput.files[0];
if (!file) {
mediaPreview.innerHTML = ‘

‘;
return;
}

const type = hotspotTypeSelect.value;
const fileExtension = file.name.split(‘.’).pop().toLowerCase();
const isImage = file.type.startsWith(‘image/’);
const isVideo = file.type.startsWith(‘video/’) || [‘mov’, ‘mp4’, ‘webm’, ‘m4v’].includes(fileExtension);

if ((type === ‘image’ || type === ‘panorama’) && !isImage) {
showToast(‘Invalid file type for this hotspot type’, ‘error’);
hotspotMediaInput.value = ”;
return;
}

if ((type === ‘video’ || type === ‘video360’) && !isVideo) {
showToast(‘Invalid file type for this hotspot type’, ‘error’);
hotspotMediaInput.value = ”;
return;
}

const reader = new FileReader();
reader.onload = function(e) {
if (isImage) {
mediaPreview.innerHTML = `Preview`;
} else if (isVideo) {
const blob = new Blob([new Uint8Array(e.target.result)], { type: ‘video/mp4’ });
const blobUrl = URL.createObjectURL(blob);
mediaPreview.innerHTML = ``;
}
};

if (isVideo) {
reader.readAsArrayBuffer(file);
} else {
reader.readAsDataURL(file);
}
}

// Show/hide hotspot labels
function toggleHotspotLabels() {
showLabels = showLabelsToggle.checked;
updateHotspotMarkers();
showToast(`Hotspot names ${showLabels ? ‘visible’ : ‘hidden’}`);
}

// Load floor plan
function loadFloorPlan() {
console.log(‘Loading floor plan’);
const input = document.createElement(‘input’);
input.type = ‘file’;
input.accept = ‘image/jpeg,image/png,image/gif,image/webp’;

input.onchange = function(e) {
const file = e.target.files[0];
if (!file) return;

const reader = new FileReader();
reader.onload = function(e) {
const img = new Image();
img.onload = function() {
// Store original image dimensions
originalImageWidth = img.naturalWidth;
originalImageHeight = img.naturalHeight;

console.log(`Original image dimensions: ${originalImageWidth}x${originalImageHeight}`);

// Save image reference
floorplanImage = {
element: img,
src: e.target.result,
originalWidth: originalImageWidth,
originalHeight: originalImageHeight
};

// Set the image source and show it
floorPlan.src = e.target.result;

// Show the floor plan and hide placeholder
placeholder.style.display = ‘none’;
floorPlanContainer.style.display = ‘block’;

// Show replace button if in create/edit mode
if (currentMode === ‘create’ || currentMode === ‘edit’) {
replaceFloorPlanBtn.style.display = ‘inline-block’;
}

// Set cursor based on the mode
floorPlan.style.cursor = currentMode === ‘view’ ? ‘default’ : ‘crosshair’;

showToast(‘Floor plan loaded successfully’);
};

img.src = e.target.result;
};

reader.readAsDataURL(file);
};

input.click();
}

// Replace floor plan
function replaceFloorPlan() {
console.log(‘Replacing floor plan’);
if (!confirm(‘Replace the current floor plan? This will keep all hotspots but they may need repositioning.’)) {
return;
}

const input = document.createElement(‘input’);
input.type = ‘file’;
input.accept = ‘image/jpeg,image/png,image/gif,image/webp’;

input.onchange = function(e) {
const file = e.target.files[0];
if (!file) return;

const reader = new FileReader();
reader.onload = function(e) {
const img = new Image();
img.onload = function() {
// Store new original image dimensions
const newImageWidth = img.naturalWidth;
const newImageHeight = img.naturalHeight;

console.log(`New image dimensions: ${newImageWidth}x${newImageHeight}`);
console.log(`Previous dimensions: ${originalImageWidth}x${originalImageHeight}`);

// Update image reference
floorplanImage = {
element: img,
src: e.target.result,
originalWidth: newImageWidth,
originalHeight: newImageHeight
};

// Update original dimensions
originalImageWidth = newImageWidth;
originalImageHeight = newImageHeight;

// Set the new image
floorPlan.src = e.target.result;

// Update hotspot markers with new image
updateHotspotMarkers();

showToast(‘Floor plan replaced successfully’);

// Show warning about repositioning if there are hotspots
if (hotspots.length > 0) {
setTimeout(() => {
showToast(‘Check hotspot positions – you may need to reposition them’, ‘info’);
}, 2000);
}
};

img.src = e.target.result;
};

reader.readAsDataURL(file);
};

input.click();
}
function handleFloorPlanClick(e) {
console.log(‘Floor plan clicked, mode:’, currentMode);
if (currentMode === ‘view’) return;

// Get coordinates relative to the image
const rect = e.target.getBoundingClientRect();
const x = e.clientX – rect.left;
const y = e.clientY – rect.top;

// Convert to percentage coordinates
const currentDimensions = getCurrentImageDimensions();
const percentCoords = pixelToPercent(x, y, currentDimensions.width, currentDimensions.height);

// Check if clicked on an existing hotspot
const existingHotspot = findHotspotAt(percentCoords.x, percentCoords.y);
if (existingHotspot) {
currentHotspot = existingHotspot;
if (currentMode === ‘view’) {
showMediaViewer(existingHotspot);
} else {
showHotspotForm(existingHotspot);
}
return;
}

// Create new hotspot (only in create mode)
if (currentMode === ‘create’) {
const newHotspot = {
id: Date.now().toString(),
name: `Hotspot ${hotspots.length + 1}`,
type: ‘image’,
x: percentCoords.x,
y: percentCoords.y,
mediaData: null
};

currentHotspot = newHotspot;
showHotspotForm(newHotspot);
}
}

// Drag & Drop functions
function startDrag(e, hotspot, marker) {
if (currentMode === ‘view’) return;

e.stopPropagation();
e.preventDefault();

isDragging = true;
dragHotspot = hotspot;

// Calculate offset from marker center
const rect = marker.getBoundingClientRect();
const floorRect = floorPlan.getBoundingClientRect();

dragOffset.x = e.clientX – rect.left – rect.width / 2;
dragOffset.y = e.clientY – rect.top – rect.height / 2;

marker.classList.add(‘dragging’);
floorPlan.style.cursor = ‘grabbing’;

console.log(‘Started dragging hotspot:’, hotspot.name);

// Add temporary event listeners
document.addEventListener(‘mousemove’, dragMove);
document.addEventListener(‘mouseup’, endDrag);
}

function dragMove(e) {
if (!isDragging || !dragHotspot) return;

e.preventDefault();

// Get new position relative to floor plan
const floorRect = floorPlan.getBoundingClientRect();
const x = e.clientX – floorRect.left – dragOffset.x;
const y = e.clientY – floorRect.top – dragOffset.y;

// Convert to percentage coordinates
const currentDimensions = getCurrentImageDimensions();
const percentCoords = pixelToPercent(x, y, currentDimensions.width, currentDimensions.height);

// Constrain to image bounds
percentCoords.x = Math.max(0, Math.min(100, percentCoords.x));
percentCoords.y = Math.max(0, Math.min(100, percentCoords.y));

// Update hotspot coordinates
dragHotspot.x = percentCoords.x;
dragHotspot.y = percentCoords.y;

// Update marker position immediately
const marker = document.querySelector(`.hotspot-marker[data-id=”${dragHotspot.id}”]`);
if (marker) {
const newPixelCoords = percentToPixel(percentCoords.x, percentCoords.y, currentDimensions.width, currentDimensions.height);
marker.style.left = `${newPixelCoords.x}px`;
marker.style.top = `${newPixelCoords.y}px`;

// Update label position if visible
const label = document.querySelector(`.hotspot-label[data-id=”${dragHotspot.id}”]`);
if (label) {
label.style.left = `${newPixelCoords.x}px`;
label.style.top = `${newPixelCoords.y}px`;
}
}
}

function endDrag(e) {
if (!isDragging) return;

console.log(‘Ended dragging hotspot:’, dragHotspot ? dragHotspot.name : ‘unknown’);

// Clean up
const marker = document.querySelector(`.hotspot-marker[data-id=”${dragHotspot.id}”]`);
if (marker) {
marker.classList.remove(‘dragging’);
}

floorPlan.style.cursor = currentMode === ‘view’ ? ‘default’ : ‘crosshair’;

// Update hotspot in array
if (dragHotspot) {
const index = hotspots.findIndex(h => h.id === dragHotspot.id);
if (index !== -1) {
hotspots[index] = dragHotspot;
}

// Update the list display
renderHotspotList();

showToast(`Hotspot “${dragHotspot.name}” repositioned`);
}

// Reset drag state
isDragging = false;
dragHotspot = null;
dragOffset = { x: 0, y: 0 };

// Remove temporary event listeners
document.removeEventListener(‘mousemove’, dragMove);
document.removeEventListener(‘mouseup’, endDrag);
}
function findHotspotAt(xPercent, yPercent) {
const threshold = 2; // percentage threshold for clicking

return hotspots.find(hotspot => {
const dx = Math.abs(hotspot.x – xPercent);
const dy = Math.abs(hotspot.y – yPercent);
return dx < threshold && dy h.id === hotspot.id);
formTitle.textContent = isNew ? ‘Add Hotspot’ : ‘Edit Hotspot’;

// Populate the form
hotspotNameInput.value = hotspot.name || ”;
hotspotTypeSelect.value = hotspot.type || ‘image’;
hotspotXInput.value = hotspot.x.toFixed(2);
hotspotYInput.value = hotspot.y.toFixed(2);
hotspotMediaInput.value = ”;

updateMediaTypeInfo();

// Show preview if available
if (hotspot.mediaData) {
if (hotspot.type === ‘image’ || hotspot.type === ‘panorama’) {
mediaPreview.innerHTML = `Preview`;
} else {
mediaPreview.innerHTML = ``;
}
} else {
mediaPreview.innerHTML = ‘

‘;
}

hotspotForm.style.display = ‘block’;
}

// Hide hotspot form
function hideHotspotForm() {
console.log(‘Hiding hotspot form’);
hotspotForm.style.display = ‘none’;
currentHotspot = null;
}

// Save hotspot
function saveHotspot() {
console.log(‘Saving hotspot’);
if (!currentHotspot) return;

const name = hotspotNameInput.value.trim();
if (!name) {
showToast(‘Please enter a name for the hotspot’, ‘error’);
return;
}

// Update basic data
currentHotspot.name = name;
currentHotspot.type = hotspotTypeSelect.value;
currentHotspot.x = parseFloat(hotspotXInput.value);
currentHotspot.y = parseFloat(hotspotYInput.value);

// Handle media file
const mediaFile = hotspotMediaInput.files[0];
if (mediaFile) {
const fileExtension = mediaFile.name.split(‘.’).pop().toLowerCase();
const isImage = mediaFile.type.startsWith(‘image/’);
const isVideo = mediaFile.type.startsWith(‘video/’) || [‘mov’, ‘mp4’, ‘webm’, ‘m4v’].includes(fileExtension);

if ((currentHotspot.type === ‘image’ || currentHotspot.type === ‘panorama’) && !isImage) {
showToast(‘Please select an image file’, ‘error’);
return;
}

if ((currentHotspot.type === ‘video’ || currentHotspot.type === ‘video360’) && !isVideo) {
showToast(‘Please select a video file’, ‘error’);
return;
}

const reader = new FileReader();
reader.onload = function(e) {
currentHotspot.mediaData = e.target.result;
finalizeSaveHotspot();
};
reader.readAsDataURL(mediaFile);
} else {
finalizeSaveHotspot();
}
}

// Finalize hotspot save
function finalizeSaveHotspot() {
console.log(‘Finalizing hotspot save’);
const index = hotspots.findIndex(h => h.id === currentHotspot.id);

if (index === -1) {
hotspots.push(currentHotspot);
} else {
hotspots[index] = currentHotspot;
}

renderHotspotList();
updateHotspotMarkers();
hideHotspotForm();

showToast(‘Hotspot saved successfully’);
}

// Update hotspot list
function renderHotspotList() {
console.log(‘Rendering hotspot list, count:’, hotspots.length);
hotspotCount.textContent = hotspots.length;

if (hotspots.length === 0) {
hotspotList.innerHTML = ‘

No hotspots available

‘;
return;
}

hotspotList.innerHTML = ”;

hotspots.forEach(hotspot => {
const item = document.createElement(‘div’);
item.className = ‘hotspot-item’;
item.dataset.id = hotspot.id;

let typeIcon = ‘fa-image’;
let typeLabel = ‘Image’;

if (hotspot.type === ‘video’) {
typeIcon = ‘fa-video’;
typeLabel = ‘Video’;
}
else if (hotspot.type === ‘panorama’) {
typeIcon = ‘fa-globe’;
typeLabel = ‘360° Photo’;
}
else if (hotspot.type === ‘video360’) {
typeIcon = ‘fa-film’;
typeLabel = ‘360° Video’;
}

let html = `


${hotspot.name}
Type: ${typeLabel}
Position: X=${hotspot.x.toFixed(1)}%, Y=${hotspot.y.toFixed(1)}%

`;

if (currentMode !== ‘view’) {
html += `

Edit

Delete

`;
}

item.innerHTML = html;

item.addEventListener(‘click’, () => {
if (currentMode === ‘view’ && hotspot.mediaData) {
showMediaViewer(hotspot);
} else if (currentMode !== ‘view’) {
currentHotspot = hotspot;
showHotspotForm(hotspot);
}
});

hotspotList.appendChild(item);
});

// Add event listeners for edit/delete buttons
if (currentMode !== ‘view’) {
document.querySelectorAll(‘.edit-btn’).forEach(btn => {
btn.addEventListener(‘click’, (e) => {
e.stopPropagation();
const id = btn.dataset.id;
const hotspot = hotspots.find(h => h.id === id);
if (hotspot) {
currentHotspot = hotspot;
showHotspotForm(hotspot);
}
});
});

document.querySelectorAll(‘.delete-btn’).forEach(btn => {
btn.addEventListener(‘click’, (e) => {
e.stopPropagation();
const id = btn.dataset.id;
if (confirm(‘Are you sure you want to delete this hotspot?’)) {
deleteHotspot(id);
}
});
});
}
}

// Delete hotspot
function deleteHotspot(id) {
console.log(‘Deleting hotspot:’, id);
const index = hotspots.findIndex(h => h.id === id);
if (index !== -1) {
hotspots.splice(index, 1);
renderHotspotList();
updateHotspotMarkers();
showToast(‘Hotspot deleted’);
}
}

// Update hotspot markers on the floor plan
function updateHotspotMarkers() {
console.log(‘Updating hotspot markers’);
// Remove existing markers
document.querySelectorAll(‘.hotspot-marker, .hotspot-label’).forEach(marker => {
marker.remove();
});

if (!floorplanImage || !floorPlan) return;

const currentDimensions = getCurrentImageDimensions();

hotspots.forEach(hotspot => {
// Convert percentage coordinates to current pixel coordinates
const pixelCoords = percentToPixel(hotspot.x, hotspot.y, currentDimensions.width, currentDimensions.height);

// Create marker
const marker = document.createElement(‘div’);
marker.className = ‘hotspot-marker’;
marker.style.left = `${pixelCoords.x}px`;
marker.style.top = `${pixelCoords.y}px`;
marker.dataset.id = hotspot.id;

// Make draggable in create/edit modes
if (currentMode === ‘create’ || currentMode === ‘edit’) {
marker.classList.add(‘draggable’);
marker.addEventListener(‘mousedown’, (e) => startDrag(e, hotspot, marker));
}

let icon = ”;
if (hotspot.type === ‘image’) icon = ‘I’;
if (hotspot.type === ‘video’) icon = ‘V’;
if (hotspot.type === ‘panorama’) icon = ‘360’;
if (hotspot.type === ‘video360’) icon = ‘V360’;

marker.textContent = icon;

marker.addEventListener(‘click’, (e) => {
e.stopPropagation();
if (currentMode === ‘view’) {
if (hotspot.mediaData) {
showMediaViewer(hotspot);
}
} else {
currentHotspot = hotspot;
showHotspotForm(hotspot);
}
});

floorPlanContainer.appendChild(marker);

// Create label if enabled
if (showLabels) {
const label = document.createElement(‘div’);
label.className = ‘hotspot-label’;
label.style.left = `${pixelCoords.x}px`;
label.style.top = `${pixelCoords.y}px`;
label.dataset.id = hotspot.id;
label.textContent = hotspot.name;
floorPlanContainer.appendChild(label);
}
});
}

// Show media viewer
function showMediaViewer(hotspot) {
if (!hotspot.mediaData) return;

// Reset all viewers
imageViewer.style.display = ‘none’;
videoViewer.style.display = ‘none’;
panoramaViewer.style.display = ‘none’;
video360Viewer.style.display = ‘none’;

// Destroy previous instances
if (panoramaInstance) {
panoramaInstance.destroy();
panoramaInstance = null;
}

switch(hotspot.type) {
case ‘image’:
imageViewer.src = hotspot.mediaData;
imageViewer.style.display = ‘block’;
break;

case ‘video’:
videoViewer.src = hotspot.mediaData;
videoViewer.style.display = ‘block’;
break;

case ‘panorama’:
panoramaViewer.style.display = ‘block’;
try {
panoramaInstance = pannellum.viewer(‘panorama-viewer’, {
type: ‘equirectangular’,
panorama: hotspot.mediaData,
autoLoad: true,
compass: true,
title: hotspot.name,
showFullscreenCtrl: true,
showControls: true
});
} catch (error) {
console.error(‘Error loading panorama:’, error);
showToast(‘Error loading panorama’, ‘error’);
imageViewer.src = hotspot.mediaData;
imageViewer.style.display = ‘block’;
panoramaViewer.style.display = ‘none’;
}
break;

case ‘video360’:
video360Viewer.style.display = ‘block’;
try {
video360Viewer.innerHTML = `


`;

const video = document.getElementById(‘video360’);
video.addEventListener(‘canplay’, () => {
try {
panoramaInstance = pannellum.viewer(‘video360-viewer’, {
type: ‘equirectangular’,
panorama: video,
autoLoad: true,
compass: true,
title: hotspot.name,
showFullscreenCtrl: true,
showControls: true,
dynamic: true
});
} catch (err) {
console.error(‘Error creating 360 video viewer:’, err);
videoViewer.src = hotspot.mediaData;
videoViewer.style.display = ‘block’;
video360Viewer.style.display = ‘none’;
}
});

video.onerror = function() {
console.error(‘360 video error’);
videoViewer.src = hotspot.mediaData;
videoViewer.style.display = ‘block’;
video360Viewer.style.display = ‘none’;
};

video.load();
} catch (error) {
console.error(‘Error setting up 360 video:’, error);
videoViewer.src = hotspot.mediaData;
videoViewer.style.display = ‘block’;
video360Viewer.style.display = ‘none’;
}
break;
}

mediaViewer.style.display = ‘flex’;
}

// Close media viewer
function closeMediaViewer() {
mediaViewer.style.display = ‘none’;
videoViewer.pause();

if (panoramaInstance) {
panoramaInstance.destroy();
panoramaInstance = null;
}
}

// Save project
function saveProject() {
console.log(‘Saving project’);
if (!floorplanImage) {
showToast(‘Load a floor plan first!’, ‘error’);
return;
}

const name = prompt(‘Project name:’, projectName);
if (!name) return;

projectName = name;

const projectData = {
name: projectName,
createdAt: new Date().toISOString(),
floorplanData: floorplanImage.src,
hotspots: hotspots,
showLabels: showLabels,
version: ‘2.0’,
originalImageWidth: originalImageWidth,
originalImageHeight: originalImageHeight
};

const jsonData = JSON.stringify(projectData);
const blob = new Blob([jsonData], { type: ‘application/json’ });
const url = URL.createObjectURL(blob);
const a = document.createElement(‘a’);
a.href = url;
a.download = `${projectName.replace(/[^a-z0-9]/gi, ‘_’).toLowerCase()}.json`;
a.click();
URL.revokeObjectURL(url);

showToast(‘Project saved successfully’);
}

// Load project
function loadProject() {
console.log(‘Loading project’);
const input = document.createElement(‘input’);
input.type = ‘file’;
input.accept = ‘.json’;

input.onchange = function(e) {
const file = e.target.files[0];
if (!file) return;

const reader = new FileReader();
reader.onload = function(e) {
try {
const data = JSON.parse(e.target.result);

projectName = data.name || ‘Unnamed Project’;
hotspots = data.hotspots || [];

// Load original image dimensions if available
if (data.originalImageWidth && data.originalImageHeight) {
originalImageWidth = data.originalImageWidth;
originalImageHeight = data.originalImageHeight;
}

// Handle legacy projects (v1.0) that used pixel coordinates
if (!data.version || data.version === ‘1.0’) {
showToast(‘Converting legacy project to v2.0 format…’, ‘info’);
convertLegacyHotspots();
}

if (data.showLabels !== undefined) {
showLabels = data.showLabels;
showLabelsToggle.checked = showLabels;
}

if (data.floorplanData) {
const img = new Image();
img.onload = function() {
// Update original dimensions if not present
if (!originalImageWidth || !originalImageHeight) {
originalImageWidth = img.naturalWidth;
originalImageHeight = img.naturalHeight;
}

floorplanImage = {
element: img,
src: data.floorplanData,
originalWidth: originalImageWidth,
originalHeight: originalImageHeight
};

floorPlan.src = data.floorplanData;

placeholder.style.display = ‘none’;
floorPlanContainer.style.display = ‘block’;

// Show replace button if in create/edit mode
if (currentMode === ‘create’ || currentMode === ‘edit’) {
replaceFloorPlanBtn.style.display = ‘inline-block’;
}

floorPlan.style.cursor = currentMode === ‘view’ ? ‘default’ : ‘crosshair’;

updateHotspotMarkers();
renderHotspotList();
};

img.src = data.floorplanData;
}

showToast(`Project “${projectName}” loaded successfully`);

} catch (error) {
console.error(‘Error loading project:’, error);
showToast(‘Error loading project’, ‘error’);
}
};

reader.readAsText(file);
};

input.click();
}

// Convert legacy pixel coordinates to percentage
function convertLegacyHotspots() {
if (!originalImageWidth || !originalImageHeight) {
showToast(‘Cannot convert legacy project: missing original dimensions’, ‘error’);
return;
}

hotspots.forEach(hotspot => {
// If coordinates are larger than 100, they’re probably pixels
if (hotspot.x > 100 || hotspot.y > 100) {
const percentCoords = pixelToPercent(hotspot.x, hotspot.y, originalImageWidth, originalImageHeight);
hotspot.x = percentCoords.x;
hotspot.y = percentCoords.y;
console.log(`Converted hotspot ${hotspot.name} from pixel to percentage coordinates`);
}
});
}

// Function to show toast messages
function showToast(message, type = ‘info’) {
console.log(‘Toast:’, message, type);
toast.textContent = message;
toast.style.backgroundColor = type === ‘error’ ? ‘rgba(220, 53, 69, 0.9)’ : ‘rgba(0, 0, 0, 0.7)’;
toast.style.display = ‘block’;

setTimeout(() => {
toast.style.display = ‘none’;
}, 3000);
}

// Event listeners
console.log(‘Setting up event listeners…’);

createModeBtn.addEventListener(‘click’, () => setMode(‘create’));
editModeBtn.addEventListener(‘click’, () => setMode(‘edit’));
viewModeBtn.addEventListener(‘click’, () => setMode(‘view’));
newBtn.addEventListener(‘click’, newProject);
saveBtn.addEventListener(‘click’, saveProject);
loadBtn.addEventListener(‘click’, loadProject);
loadFloorPlanBtn.addEventListener(‘click’, loadFloorPlan);
replaceFloorPlanBtn.addEventListener(‘click’, replaceFloorPlan);
saveHotspotBtn.addEventListener(‘click’, saveHotspot);
cancelHotspotBtn.addEventListener(‘click’, hideHotspotForm);
formClose.addEventListener(‘click’, hideHotspotForm);
closeViewerBtn.addEventListener(‘click’, closeMediaViewer);
showLabelsToggle.addEventListener(‘change’, toggleHotspotLabels);
hotspotTypeSelect.addEventListener(‘change’, updateMediaTypeInfo);
hotspotMediaInput.addEventListener(‘change’, previewSelectedMedia);

// Add floor plan click listener
floorPlan.addEventListener(‘click’, handleFloorPlanClick);

// Initialize
updateMediaTypeInfo();
console.log(‘My Vision v2.0 – Ready!’);
showToast(‘My Vision v2.0 loaded successfully’);