Skip to content

Commit 37d941a

Browse files
authored
Merge pull request #126 from oss-slu/120-frontend-logic-for-image-and-annotation-display-using-mock-data
Implement frontend mock data display for bone images and annotations
2 parents 43b69ce + 89068e2 commit 37d941a

File tree

8 files changed

+473
-62
lines changed

8 files changed

+473
-62
lines changed

boneset-api/server.js

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -89,24 +89,16 @@
8989
//app.listen(PORT, () => {
9090
// console.log(`🚀 Server running on http://127.0.0.1:${PORT}`);
9191
//});
92-
93-
94-
95-
9692
const express = require("express");
9793
const axios = require("axios");
9894
const cors = require("cors");
9995
const path = require("path");
100-
10196
const app = express();
10297
const PORT = process.env.PORT || 8000;
103-
10498
app.use(cors());
105-
10699
const GITHUB_REPO = "https://raw.githubusercontent.com/oss-slu/DigitalBonesBox/data/DataPelvis/";
107100
const BONESET_JSON_URL = `${GITHUB_REPO}boneset/bony_pelvis.json`;
108101
const BONES_DIR_URL = `${GITHUB_REPO}bones/`;
109-
110102
async function fetchJSON(url) {
111103
try {
112104
const response = await axios.get(url);
@@ -116,63 +108,51 @@ async function fetchJSON(url) {
116108
return null;
117109
}
118110
}
119-
120111
app.get("/", (req, res) => {
121112
res.json({ message: "Welcome to the Boneset API (GitHub-Integrated)" });
122113
});
123-
124114
app.get("/combined-data", async (req, res) => {
125115
try {
126116
const bonesetData = await fetchJSON(BONESET_JSON_URL);
127117
if (!bonesetData) return res.status(500).json({ error: "Failed to load boneset data" });
128-
129118
const bonesets = [{ id: bonesetData.id, name: bonesetData.name }];
130119
const bones = [];
131120
const subbones = [];
132-
133121
for (const boneId of bonesetData.bones) {
134122
const boneJsonUrl = `${BONES_DIR_URL}${boneId}.json`;
135123
const boneData = await fetchJSON(boneJsonUrl);
136-
137124
if (boneData) {
138125
bones.push({ id: boneData.id, name: boneData.name, boneset: bonesetData.id });
139126
boneData.subBones.forEach(subBoneId => {
140127
subbones.push({ id: subBoneId, name: subBoneId.replace(/_/g, " "), bone: boneData.id });
141128
});
142129
}
143130
}
144-
145131
res.json({ bonesets, bones, subbones });
146-
147132
} catch (error) {
148133
console.error("Error fetching combined data:", error.message);
149134
res.status(500).json({ error: "Internal Server Error" });
150135
}
151136
});
152-
153137
// --- CORRECTED HTMX ENDPOINT ---
154138
app.get("/api/description/", async (req, res) => { // Path changed here (no :boneId)
155139
const { boneId } = req.query; // Changed from req.params to req.query
156140
if (!boneId) {
157141
return res.send(" "); // Send empty response if no boneId is provided
158142
}
159143
const GITHUB_DESC_URL = `https://raw.githubusercontent.com/oss-slu/DigitalBonesBox/data/DataPelvis/descriptions/${boneId}_description.json`;
160-
161144
try {
162145
const response = await axios.get(GITHUB_DESC_URL);
163146
const descriptionData = response.data;
164-
165147
let html = `<li><strong>${descriptionData.name}</strong></li>`;
166148
descriptionData.description.forEach(point => {
167149
html += `<li>${point}</li>`;
168150
});
169151
res.send(html);
170-
171152
} catch (error) {
172153
res.send("<li>Description not available.</li>");
173154
}
174155
});
175-
176156
app.listen(PORT, () => {
177157
console.log(`🚀 Server running on http://127.0.0.1:${PORT}`);
178-
});
158+
});

templates/js/api.js

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,42 @@
1+
// api.js - Centralized API configuration and data fetching
2+
3+
// Centralized API configuration
4+
const API_CONFIG = {
5+
BASE_URL: "http://127.0.0.1:8000",
6+
ENDPOINTS: {
7+
COMBINED_DATA: "/combined-data",
8+
MOCK_BONE_DATA: "./js/mock-bone-data.json"
9+
}
10+
};
111

212
export async function fetchCombinedData() {
3-
// --- CORRECTED: Use the full URL of the backend server ---
4-
const API_URL = "http://127.0.0.1:8000/combined-data";
13+
const API_URL = `${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.COMBINED_DATA}`;
514

615
try {
716
const response = await fetch(API_URL);
8-
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
17+
if (!response.ok) {
18+
throw new Error(`HTTP error! status: ${response.status}`);
19+
}
920
return await response.json();
1021
} catch (error) {
1122
console.error("Error fetching combined data:", error);
12-
alert("Failed to load data.");
13-
return { bonesets: [], bones: [], subbones: [] };
23+
throw error;
1424
}
15-
}
25+
}
26+
27+
export async function fetchMockBoneData() {
28+
try {
29+
const response = await fetch(API_CONFIG.ENDPOINTS.MOCK_BONE_DATA);
30+
if (!response.ok) {
31+
throw new Error(`HTTP error! status: ${response.status}`);
32+
}
33+
const data = await response.json();
34+
return data;
35+
} catch (error) {
36+
console.error("Error fetching mock bone data:", error);
37+
return null;
38+
}
39+
}
40+
41+
// Export configuration for other modules to use
42+
export { API_CONFIG };

templates/js/dropdowns.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,17 @@ import { loadDescription } from "./description.js";
33

44
export function populateBonesetDropdown(bonesets) {
55
const bonesetSelect = document.getElementById("boneset-select");
6-
bonesetSelect.innerHTML = "<option value=\"\">--Please select a Boneset--</option>";
7-
bonesets.forEach(set => {
6+
bonesetSelect.innerHTML = "<option value=\"\">--Please select a Boneset--</option>";
7+
8+
bonesets.forEach(set => {
89
const option = document.createElement("option");
910
option.value = set.id;
1011
option.textContent = set.name;
1112
bonesetSelect.appendChild(option);
1213
});
1314
}
1415

16+
1517
export function setupDropdownListeners(combinedData) {
1618
const bonesetSelect = document.getElementById("boneset-select");
1719
const boneSelect = document.getElementById("bone-select");

templates/js/main.js

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,55 @@
1-
import { fetchCombinedData } from "./api.js";
1+
import { fetchCombinedData, fetchMockBoneData } from "./api.js";
22
import { populateBonesetDropdown, setupDropdownListeners } from "./dropdowns.js";
33
import { initializeSidebar } from "./sidebar.js";
44
import { setupNavigation, setBoneAndSubbones, disableButtons } from "./navigation.js";
5-
import { loadDescription } from "./description.js"; // ✅ CORRECT function name
5+
import { loadDescription } from "./description.js";
6+
import { displayBoneData, clearViewer } from "./viewer.js";
67

78
let combinedData = { bonesets: [], bones: [], subbones: [] };
9+
let mockBoneData = null;
10+
11+
/**
12+
* Handles bone selection from dropdown
13+
* @param {string} boneId - The ID of the selected bone
14+
*/
15+
function handleBoneSelection(boneId) {
16+
if (!mockBoneData) {
17+
console.log("Mock data not available");
18+
return;
19+
}
20+
21+
const bone = mockBoneData.bones.find(b => b.id === boneId);
22+
if (!bone) {
23+
console.log(`No mock data found for bone: ${boneId}`);
24+
clearViewer();
25+
return;
26+
}
27+
28+
// Use the dedicated viewer module to display the bone
29+
displayBoneData(bone);
30+
}
831

932
document.addEventListener("DOMContentLoaded", async () => {
1033
// 1. Sidebar behavior
1134
initializeSidebar();
1235

13-
// 2. Fetch data and populate dropdowns
36+
// 2. Load mock bone data using centralized API
37+
mockBoneData = await fetchMockBoneData();
38+
39+
// 3. Fetch data and populate dropdowns
1440
combinedData = await fetchCombinedData();
1541
populateBonesetDropdown(combinedData.bonesets);
1642
setupDropdownListeners(combinedData);
1743

18-
// 3. Hook up navigation buttons
44+
// 4. Hook up navigation buttons
1945
const prevButton = document.getElementById("prev-button");
2046
const nextButton = document.getElementById("next-button");
2147
const subboneDropdown = document.getElementById("subbone-select");
2248
const boneDropdown = document.getElementById("bone-select");
2349

2450
setupNavigation(prevButton, nextButton, subboneDropdown, loadDescription);
2551

26-
// 4. Update navigation when bone changes
52+
// 5. Update navigation when bone changes
2753
boneDropdown.addEventListener("change", (event) => {
2854
const selectedBone = event.target.value;
2955

@@ -34,15 +60,25 @@ document.addEventListener("DOMContentLoaded", async () => {
3460
setBoneAndSubbones(selectedBone, relatedSubbones);
3561
populateSubboneDropdown(subboneDropdown, relatedSubbones);
3662
disableButtons(prevButton, nextButton);
63+
64+
// Handle bone selection using dedicated function
65+
if (selectedBone) {
66+
handleBoneSelection(selectedBone);
67+
} else {
68+
clearViewer();
69+
}
3770
});
3871

39-
// 5. Auto-select the first boneset
72+
// 6. Auto-select the first boneset
4073
const boneset = combinedData.bonesets[0];
4174
if (boneset) {
4275
document.getElementById("boneset-select").value = boneset.id;
4376
const event = new Event("change");
4477
document.getElementById("boneset-select").dispatchEvent(event);
4578
}
79+
80+
// 7. Initialize display
81+
clearViewer();
4682
});
4783

4884
function populateSubboneDropdown(dropdown, subbones) {

templates/js/mock-bone-data.json

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
{
2+
"bones": [
3+
{
4+
"id": "ischium",
5+
"name": "Ischium",
6+
"image_url": "https://via.placeholder.com/600x400/4A90E2/FFFFFF?text=Ischium+Bone",
7+
"annotations": [
8+
{
9+
"text": "Ischial Tuberosity - Attachment point for hamstring muscles",
10+
"position": { "x": 300, "y": 150 }
11+
},
12+
{
13+
"text": "Ischial Spine - Forms part of the lesser sciatic notch",
14+
"position": { "x": 250, "y": 100 }
15+
},
16+
{
17+
"text": "Ischial Ramus - Forms part of the obturator foramen",
18+
"position": { "x": 350, "y": 200 }
19+
}
20+
]
21+
},
22+
{
23+
"id": "ilium",
24+
"name": "Ilium",
25+
"image_url": "https://via.placeholder.com/600x400/50C878/FFFFFF?text=Ilium+Bone",
26+
"annotations": []
27+
},
28+
{
29+
"id": "pubis",
30+
"name": "Pubis",
31+
"annotations": [
32+
{
33+
"text": "Pubic Symphysis - Joint where left and right pubic bones meet",
34+
"position": { "x": 300, "y": 60 }
35+
},
36+
{
37+
"text": "Superior Pubic Ramus - Upper branch of the pubis",
38+
"position": { "x": 250, "y": 180 }
39+
}
40+
]
41+
}
42+
]
43+
}

templates/js/viewer.js

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// viewer.js - Dedicated module for managing viewer state and display
2+
3+
/**
4+
* Displays bone image with error handling for broken URLs
5+
* @param {Object} boneData - The bone object from mock data
6+
*/
7+
export function displayBoneImage(boneData) {
8+
const boneImage = document.getElementById("bone-image");
9+
if (!boneImage) {
10+
console.error("Bone image element not found");
11+
return;
12+
}
13+
14+
if (boneData.image_url) {
15+
boneImage.src = boneData.image_url;
16+
boneImage.alt = `${boneData.name} bone image`;
17+
boneImage.style.display = "block";
18+
19+
// Handle image load errors gracefully
20+
boneImage.onerror = () => {
21+
console.warn(`Failed to load image for ${boneData.name}: ${boneData.image_url}`);
22+
boneImage.src = "https://via.placeholder.com/600x400/CCCCCC/666666?text=Image+Load+Failed";
23+
boneImage.alt = `${boneData.name} - Image failed to load`;
24+
};
25+
26+
// Clear any previous error handlers when image loads successfully
27+
boneImage.onload = () => {
28+
boneImage.onerror = null;
29+
};
30+
} else {
31+
// Handle missing image_url
32+
boneImage.src = "https://via.placeholder.com/600x400/CCCCCC/666666?text=No+Image+Available";
33+
boneImage.alt = `${boneData.name} - No image available`;
34+
boneImage.style.display = "block";
35+
console.warn(`No image URL provided for bone: ${boneData.name}`);
36+
}
37+
}
38+
39+
/**
40+
* Displays annotations list for the selected bone
41+
* @param {Array} annotations - Array of annotation objects
42+
*/
43+
export function displayAnnotations(annotations) {
44+
const annotationsOverlay = document.getElementById("annotations-overlay");
45+
if (!annotationsOverlay) {
46+
console.error("Annotations overlay element not found");
47+
return;
48+
}
49+
50+
// Clear previous annotations
51+
annotationsOverlay.innerHTML = "";
52+
53+
if (!annotations || annotations.length === 0) {
54+
annotationsOverlay.innerHTML = "<p>No annotations available for this bone.</p>";
55+
return;
56+
}
57+
58+
// Create annotation list
59+
const annotationsList = document.createElement("ul");
60+
annotationsList.className = "annotations-list";
61+
62+
annotations.forEach((annotation) => {
63+
const listItem = document.createElement("li");
64+
listItem.className = "annotation-item";
65+
listItem.textContent = annotation.text;
66+
annotationsList.appendChild(listItem);
67+
});
68+
69+
annotationsOverlay.appendChild(annotationsList);
70+
}
71+
72+
/**
73+
* Main function to display complete bone data (image + annotations)
74+
* @param {Object} boneData - The complete bone object
75+
*/
76+
export function displayBoneData(boneData) {
77+
if (!boneData) {
78+
console.error("No bone data provided to display");
79+
return;
80+
}
81+
82+
displayBoneImage(boneData);
83+
displayAnnotations(boneData.annotations);
84+
}
85+
86+
/**
87+
* Clears the viewer display
88+
*/
89+
export function clearViewer() {
90+
const boneImage = document.getElementById("bone-image");
91+
const annotationsOverlay = document.getElementById("annotations-overlay");
92+
93+
if (boneImage) {
94+
boneImage.src = "";
95+
boneImage.alt = "";
96+
boneImage.style.display = "none";
97+
boneImage.onerror = null; // Clear error handlers
98+
boneImage.onload = null;
99+
}
100+
101+
if (annotationsOverlay) {
102+
annotationsOverlay.innerHTML = "<p>Select a bone to view image and annotations.</p>";
103+
}
104+
}

0 commit comments

Comments
 (0)