mrdbourke's picture
Upload 73 files
ba0645f verified
<!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>