| | <!DOCTYPE html> |
| | <html lang="en"> |
| | <head> |
| | <meta charset="UTF-8"> |
| | <title>Frame Viewer</title> |
| | <style> |
| | |
| | * { |
| | 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; |
| | } |
| | |
| | |
| | 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; |
| | } |
| | |
| | |
| | #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 { |
| | display: flex; |
| | flex-direction: column; |
| | align-items: center; |
| | padding: 20px; |
| | gap: 20px; |
| | max-width: 1200px; |
| | margin: 0 auto; |
| | flex: 1; |
| | } |
| | |
| | |
| | #image-section { |
| | width: 90%; |
| | max-width: 900px; |
| | 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; |
| | } |
| | |
| | #image-canvas { |
| | width: 100%; |
| | height: auto; |
| | border-radius: 5px; |
| | border: 1px solid #ddd; |
| | display: block; |
| | } |
| | |
| | |
| | #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; |
| | } |
| | |
| | |
| | #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 { |
| | margin-top: 10px; |
| | display: flex; |
| | flex-wrap: wrap; |
| | gap: 10px; |
| | justify-content: center; |
| | } |
| | |
| | |
| | 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; |
| | } |
| | |
| | button:hover { |
| | transform: translateY(-2px); |
| | } |
| | |
| | |
| | #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 { |
| | background-color: #2c3e50; |
| | color: #ecf0f1; |
| | text-align: center; |
| | padding: 10px 0; |
| | margin-top: 20px; |
| | } |
| | </style> |
| | </head> |
| | <body> |
| | <header> |
| | KeepTrack Frame Viewer |
| | |
| | <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> |
| | |
| | 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'); |
| | |
| | |
| | fetch('frames_metadata_with_boxes.json') |
| | .then(response => response.json()) |
| | .then(data => { |
| | frames = data; |
| | if (frames.length > 0) { |
| | sortFrames(); |
| | 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.'); |
| | }); |
| | |
| | |
| | prevBtn.addEventListener('click', () => { |
| | if (currentIndex > 0) { |
| | currentIndex--; |
| | renderFrame(currentIndex); |
| | } |
| | }); |
| | |
| | nextBtn.addEventListener('click', () => { |
| | if (currentIndex < frames.length - 1) { |
| | currentIndex++; |
| | renderFrame(currentIndex); |
| | } |
| | }); |
| | |
| | |
| | saveBtn.addEventListener('click', () => { |
| | saveFormData(); |
| | alert('Changes saved in memory. To export, click "Export Metadata".'); |
| | }); |
| | |
| | |
| | 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(); |
| | }); |
| | |
| | |
| | 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); |
| | } |
| | } |
| | }); |
| | |
| | |
| | sortSelect.addEventListener('change', () => { |
| | sortFrames(); |
| | currentIndex = 0; |
| | renderFrame(currentIndex); |
| | }); |
| | |
| | |
| | function renderFrame(index) { |
| | const frame = frames[index]; |
| | const img = new Image(); |
| | img.src = frame.frame_filename; |
| | img.onload = () => { |
| | |
| | canvas.width = img.width; |
| | canvas.height = img.height; |
| | |
| | ctx.clearRect(0, 0, canvas.width, canvas.height); |
| | ctx.drawImage(img, 0, 0); |
| | |
| | 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}`); |
| | }; |
| | |
| | populateForm(frame); |
| | |
| | updateNavButtons(); |
| | } |
| | |
| | |
| | function updateNavButtons() { |
| | prevBtn.disabled = currentIndex === 0; |
| | nextBtn.disabled = currentIndex === frames.length - 1; |
| | } |
| | |
| | |
| | function populateForm(frame) { |
| | formFields.innerHTML = ''; |
| | |
| | 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 saveFormData() { |
| | const frame = frames[currentIndex]; |
| | const inputs = formFields.querySelectorAll('input, textarea'); |
| | inputs.forEach(input => { |
| | if (input.disabled) return; |
| | const key = input.name; |
| | let value = input.value; |
| | |
| | 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 sortFrames() { |
| | const sortBy = sortSelect.value; |
| | frames.sort((a, b) => { |
| | let aValue = a[sortBy]; |
| | let bValue = b[sortBy]; |
| | |
| | |
| | if (aValue === 'nan') return 1; |
| | if (bValue === 'nan') return -1; |
| | |
| | |
| | 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> |
| |
|