<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Frame Viewer</title> <style> /* Reset some default styles */ * { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: #f5f5f5; color: #333; line-height: 1.6; display: flex; flex-direction: column; min-height: 100vh; /* Ensure body takes at least viewport height */ } /* Header Styling */ header { background-color: #2c3e50; color: #ecf0f1; padding: 15px 20px; text-align: center; font-size: 1.5em; font-weight: bold; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin-bottom: 20px; position: relative; /* For positioning the sort dropdown */ } /* Sorting Dropdown Styling */ #sort-container { position: absolute; top: 15px; right: 20px; display: flex; align-items: center; gap: 10px; } #sort-container label { color: #ecf0f1; font-size: 0.9em; } #sort-select { padding: 5px 10px; border: none; border-radius: 4px; font-size: 0.9em; cursor: pointer; } /* Main Container */ #main-container { display: flex; flex-direction: column; /* Changed to column layout */ align-items: center; padding: 20px; gap: 20px; /* Reduced gap */ max-width: 1200px; margin: 0 auto; flex: 1; /* Allow main container to grow */ } /* Image Section */ #image-section { width: 90%; /* Adjust as needed, but keep it responsive */ max-width: 900px; /* Limit maximum width */ background-color: #fff; padding: 15px; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.1); text-align: center; margin-bottom: 10px; /* Space between image and buttons */ } #image-canvas { width: 100%; height: auto; border-radius: 5px; border: 1px solid #ddd; display: block; /* Ensure image takes full width of its container */ } /* Metadata Section */ #metadata-section { width: 90%; max-width: 900px; background-color: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.1); } #metadata-form h2 { margin-bottom: 15px; font-size: 1.3em; border-bottom: 1px solid #ccc; padding-bottom: 10px; } /* Form Fields */ #metadata-form label { display: block; margin-top: 10px; font-weight: 500; color: #555; } #metadata-form input, #metadata-form textarea, #metadata-form select { width: 100%; padding: 8px 10px; margin-top: 5px; border: 1px solid #ccc; border-radius: 4px; background-color: #fafafa; transition: border-color 0.3s; font-size: 0.95em; } #metadata-form input:focus, #metadata-form textarea:focus, #metadata-form select:focus { border-color: #7f8c8d; outline: none; } /* Button Container */ #button-container { margin-top: 10px; display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; /* Center buttons */ } /* Buttons */ button { padding: 10px 15px; font-size: 0.95em; cursor: pointer; border: none; border-radius: 4px; transition: background-color 0.3s, transform 0.2s; color: #fff; min-width: 100px; /* Ensures buttons have a decent size even on smaller screens */ } button:hover { transform: translateY(-2px); } /* Specific Button Styles */ #prev-btn, #next-btn { background-color: #3498db; flex: 1; } #prev-btn:disabled, #next-btn:disabled { background-color: #95a5a6; cursor: not-allowed; } #save-btn { background-color: #2980b9; flex: 1; } #export-btn { background-color: #27ae60; flex: 1; } #delete-btn { background-color: #e74c3c; flex: 1; } /* Footer (Optional) */ footer { background-color: #2c3e50; color: #ecf0f1; text-align: center; padding: 10px 0; margin-top: 20px; } </style> </head> <body> <header> KeepTrack Frame Viewer <!-- Sorting Dropdown --> <div id="sort-container"> <label for="sort-select">Sort By:</label> <select id="sort-select"> <option value="item_number">Item Number</option> <option value="estimated_worth">Estimated Worth</option> <option value="overall_certainty_flag">Overall Certainty Flag</option> </select> </div> </header> <div id="main-container"> <div id="image-section"> <canvas id="image-canvas"></canvas> <div id="button-container"> <button id="prev-btn" title="Previous Frame">Previous</button> <button id="next-btn" title="Next Frame">Next</button> </div> </div> <div id="metadata-section"> <form id="metadata-form"> <h2>Metadata</h2> <div id="form-fields"></div> </form> <div id="button-container"> <button type="button" id="save-btn">Save Changes</button> <button type="button" id="export-btn">Export Metadata</button> <button type="button" id="delete-btn">Delete Item</button> </div> </div> </div> <footer> © 2024 KeepTrack Frame Viewer App </footer> <script> // Global Variables let frames = []; let currentIndex = 0; const canvas = document.getElementById('image-canvas'); const ctx = canvas.getContext('2d'); const prevBtn = document.getElementById('prev-btn'); const nextBtn = document.getElementById('next-btn'); const formFields = document.getElementById('form-fields'); const saveBtn = document.getElementById('save-btn'); const exportBtn = document.getElementById('export-btn'); const deleteBtn = document.getElementById('delete-btn'); const sortSelect = document.getElementById('sort-select'); // Added // Load JSON Data fetch('frames_metadata_with_boxes.json') .then(response => response.json()) .then(data => { frames = data; if (frames.length > 0) { sortFrames(); // Initial sort based on default selection renderFrame(currentIndex); } else { alert('No frames found in the JSON file.'); } }) .catch(error => { console.error('Error loading JSON:', error); alert('Failed to load frames_metadata_with_boxes.json. Please ensure the file exists and is correctly formatted.'); }); // Event Listeners for Navigation prevBtn.addEventListener('click', () => { if (currentIndex > 0) { currentIndex--; renderFrame(currentIndex); } }); nextBtn.addEventListener('click', () => { if (currentIndex < frames.length - 1) { currentIndex++; renderFrame(currentIndex); } }); // Save Changes Button saveBtn.addEventListener('click', () => { saveFormData(); alert('Changes saved in memory. To export, click "Export Metadata".'); }); // Export Button exportBtn.addEventListener('click', () => { const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(frames, null, 2)); const downloadAnchor = document.createElement('a'); downloadAnchor.setAttribute("href", dataStr); downloadAnchor.setAttribute("download", "modified_frames_metadata_with_boxes.json"); document.body.appendChild(downloadAnchor); downloadAnchor.click(); downloadAnchor.remove(); }); // Delete Item Button deleteBtn.addEventListener('click', () => { if (confirm('Are you sure you want to delete this item?')) { frames.splice(currentIndex, 1); if (frames.length === 0) { alert('All items have been deleted.'); ctx.clearRect(0, 0, canvas.width, canvas.height); formFields.innerHTML = ''; prevBtn.disabled = true; nextBtn.disabled = true; saveBtn.disabled = true; exportBtn.disabled = true; deleteBtn.disabled = true; } else { if (currentIndex >= frames.length) { currentIndex = frames.length - 1; } renderFrame(currentIndex); } } }); // Event Listener for Sorting sortSelect.addEventListener('change', () => { sortFrames(); currentIndex = 0; // Reset to first frame after sorting renderFrame(currentIndex); }); // Function to Render a Frame function renderFrame(index) { const frame = frames[index]; const img = new Image(); img.src = frame.frame_filename; img.onload = () => { // Resize canvas to match image if necessary canvas.width = img.width; canvas.height = img.height; // Draw Image ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.drawImage(img, 0, 0); // Draw Bounding Boxes if (frame.boxes_converted && Array.isArray(frame.boxes_converted)) { frame.boxes_converted.forEach(box => { const [ymin, xmin, ymax, xmax] = box; const width = xmax - xmin; const height = ymax - ymin; ctx.strokeStyle = 'green'; ctx.lineWidth = 4; ctx.strokeRect(xmin, ymin, width, height); }); } }; img.onerror = () => { alert(`Failed to load image: ${frame.frame_filename}`); }; // Populate Metadata Form populateForm(frame); // Update Navigation Buttons updateNavButtons(); } // Function to Update Navigation Buttons function updateNavButtons() { prevBtn.disabled = currentIndex === 0; nextBtn.disabled = currentIndex === frames.length - 1; } // Function to Populate the Form with Metadata function populateForm(frame) { formFields.innerHTML = ''; // Clear previous fields // Define which fields to include in the form const fields = [ { key: 'item_number', label: 'Item Number', type: 'number' }, { key: 'item_name', label: 'Item Name', type: 'text' }, { key: 'item_type', label: 'Item Type', type: 'text' }, { key: 'item_description', label: 'Item Description', type: 'textarea' }, { key: 'item_brand', label: 'Item Brand', type: 'text' }, { key: 'item_condition', label: 'Item Condition', type: 'text' }, { key: 'number_of_items', label: 'Number of Items', type: 'number' }, { key: 'estimated_worth', label: 'Estimated Worth', type: 'number', step: '0.01' }, { key: 'estimated_worth_flag', label: 'Estimated Worth Flag', type: 'number' }, { key: 'mentioned_worth', label: 'Mentioned Worth', type: 'text' }, { key: 'room', label: 'Room', type: 'text' }, { key: 'timestamp', label: 'Timestamp', type: 'text' }, { key: 'overall_certainty_flag', label: 'Overall Certainty Flag', type: 'number' }, { key: 'is_similar_to', label: 'Is Similar To', type: 'text' }, { key: 'frame_filename', label: 'Frame Filename', type: 'text', disabled: true } ]; fields.forEach(field => { const label = document.createElement('label'); label.textContent = field.label; const input = field.type === 'textarea' ? document.createElement('textarea') : document.createElement('input'); if (field.type !== 'textarea') { input.type = field.type; } input.value = frame[field.key] !== 'nan' ? frame[field.key] : ''; input.id = field.key; input.name = field.key; if (field.step) input.step = field.step; if (field.disabled) input.disabled = true; label.appendChild(input); formFields.appendChild(label); }); } // Function to Save Form Data Back to Frames Array function saveFormData() { const frame = frames[currentIndex]; const inputs = formFields.querySelectorAll('input, textarea'); inputs.forEach(input => { if (input.disabled) return; // Skip disabled fields const key = input.name; let value = input.value; // Convert numeric fields const numericFields = [ 'item_number', 'number_of_items', 'estimated_worth', 'estimated_worth_flag', 'overall_certainty_flag' ]; if (numericFields.includes(key)) { value = value === '' ? 'nan' : Number(value); if (isNaN(value)) value = 'nan'; } frame[key] = value; }); } // Function to Sort Frames Based on Selected Field function sortFrames() { const sortBy = sortSelect.value; frames.sort((a, b) => { let aValue = a[sortBy]; let bValue = b[sortBy]; // Handle 'nan' values by treating them as less than any number or string if (aValue === 'nan') return 1; if (bValue === 'nan') return -1; // Convert to numbers if sorting by numerical fields if (sortBy === 'item_number' || sortBy === 'estimated_worth' || sortBy === 'overall_certainty_flag') { aValue = Number(aValue); bValue = Number(bValue); if (isNaN(aValue)) aValue = -Infinity; if (isNaN(bValue)) bValue = -Infinity; } if (aValue < bValue) return -1; if (aValue > bValue) return 1; return 0; }); } </script> </body> </html>