Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 32 additions & 4 deletions templates/js/annotationOverlay.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@

const PPT_EMU = { W: 9_144_000, H: 6_858_000 }; // PowerPoint slide size in EMUs

/**
* Ensures the annotation stage (overlay container) exists inside the given container,
* creating it if necessary. The stage holds both the SVG line layer and the label div.
* @param {HTMLElement} container - The parent element to attach the stage to.
* @returns {HTMLElement} The existing or newly created annotation stage element.
*/
function ensureStage(container) {
let stage = container.querySelector(".annotation-stage");
if (!stage) {
Expand All @@ -17,6 +23,11 @@ function ensureStage(container) {
return stage;
}

/**
* Removes the annotation stage and all its contents from the given container.
* @param {HTMLElement} container - The container whose annotation stage should be removed.
* @returns {void}
*/
export function clearAnnotations(container) {
if (!container) return;
const stage = container.querySelector(".annotation-stage");
Expand Down Expand Up @@ -67,8 +78,14 @@ function normalizedPointToPx(pt, box, norm) { // <--- RENAMED to reflect change
}

/**
* Draw labels + lines from a JSON object:
* { annotations: [...], normalized_geometry: { normX, normY, normW, normH } }
* Draws text annotation labels and pointer lines onto the given container.
* Reads normalized geometry and slide dimensions from the JSON to map PowerPoint
* EMU coordinates onto the displayed pixel size of the container.
* @param {HTMLElement} container - The element to draw annotations into.
* @param {Object} annotationsJson - Annotation data from the API, containing:
* `annotations` {Array}, `normalized_geometry` {Object}, `slide_width` {number},
* and `slide_height` {number}.
Comment on lines +82 to +87
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct—this function expects the fields normalized_geometry and either annotations or text_annotations. Although really I think that's a bit messy...why is this function expecting two possible formats, when really only one should exist? The line where it's checking for either annotations or text_annotations should be modified such that it's only looking for one of the two—whichever one actually is expected to exist.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also: The expected properties should be listed in the format documented here: https://jsdoc.app/tags-param#parameters-with-properties
You've already done this correctly with imageDisplay.applyRotation

* @returns {void}
*/
export function drawAnnotations(container, annotationsJson) {
if (!container || !annotationsJson) return;
Expand Down Expand Up @@ -143,7 +160,13 @@ export function drawAnnotations(container, annotationsJson) {
stage.__lastJson = annotationsJson;
}

/** Load JSON from a URL and draw it. Returns a promise. */
/**
* Fetches annotation JSON from a URL, draws it onto the container,
* and attaches a ResizeObserver so annotations redraw when the container resizes.
* @param {HTMLElement} container - The element to draw annotations into.
* @param {string} jsonUrl - URL of the annotation JSON file.
* @returns {Promise<void>}
*/
export async function loadAndDrawAnnotations(container, jsonUrl) {
if (!container || !jsonUrl) return;
const res = await fetch(jsonUrl);
Expand All @@ -155,7 +178,12 @@ export async function loadAndDrawAnnotations(container, jsonUrl) {
attachAutoscale(container); // keep aligned on resize
}

/** Re-draw on container resize using the last JSON used. */
/**
* Attaches a ResizeObserver to the container that redraws annotations whenever
* the container's dimensions change. Does nothing if an observer is already attached.
* @param {HTMLElement} container - The container to watch for size changes.
* @returns {void}
*/
function attachAutoscale(container) {
const stage = ensureStage(container);
if (stage.__resizeObs) return; // already attached
Expand Down
11 changes: 11 additions & 0 deletions templates/js/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ const API_CONFIG = {
}
};

/**
* Fetches the combined boneset/bone/subbone data from the backend API.
* This is the primary data source used to populate all dropdowns on page load.
* @returns {Promise<Object>} Combined data object containing `bonesets`, `bones`, and `subbones` arrays.
* @throws {Error} If the network request fails or the server returns a non-OK response.
*/
export async function fetchCombinedData() {
const API_URL = `${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.COMBINED_DATA}`;

Expand All @@ -25,6 +31,11 @@ export async function fetchCombinedData() {
}
}

/**
* Fetches mock bone data from a local JSON file. Used for development and testing
* without a running backend server.
* @returns {Promise<Object|null>} The mock bone data object, or null if the fetch fails.
*/
export async function fetchMockBoneData() {
try {
const response = await fetch(API_CONFIG.ENDPOINTS.MOCK_BONE_DATA);
Expand Down
2 changes: 2 additions & 0 deletions templates/js/coloredRegionsOverlay.js
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,7 @@ export async function displayColoredRegions(imageElement, boneId, imageIndex = 0
/**
* Clear all colored region overlays from a container
* @param {HTMLElement} container - The container element
* @returns {void}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For what it's worth, adding @returns {void} isn't really necessary, but it's fine that's it's here. No action needs to be taken on this.

*/
export function clearColoredRegions(container) {
if (!container) return;
Expand All @@ -527,6 +528,7 @@ export function clearColoredRegions(container) {

/**
* Clear all colored region overlays in the entire image container
* @returns {void}
*/
export function clearAllColoredRegions() {
const container = document.getElementById("bone-image-container");
Expand Down
8 changes: 8 additions & 0 deletions templates/js/description.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
// js/description.js
const GITHUB_RAW_URL = "https://raw.githubusercontent.com/oss-slu/DigitalBonesBox/data/data/descriptions/";

/**
* Fetches the description JSON for the given bone/subbone ID from GitHub and
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we're transitioning away from fetching our data from GitHub, this should no longer explicitly say that the data is coming from there. In fact, it should not specify where the data is coming from at all, since we're planning on refactoring this to go through other functions anyway.

* renders it as a list of bullet points inside the `#description-Container` element.
* Shows an error message in the container if the fetch fails.
* @param {string} id - The bone or subbone ID (e.g. `"ilium"`, `"iliac_crest"`),
* used to construct the filename `{id}_description.json`.
* @returns {Promise<void>}
*/
export async function loadDescription(id) {
const container = document.getElementById("description-Container");
container.innerHTML = "";
Expand Down
32 changes: 31 additions & 1 deletion templates/js/dropdowns.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,22 @@ document.addEventListener("DOMContentLoaded", () => {
// ---- Map/lookup you can extend later -----------------
let _boneById = {}; // filled in setupDropdownListeners

/**
* Returns the `#bone-image-container` element, which serves as the host for
* displayed bone images and their annotation overlays.
* @returns {HTMLElement|null} The image container element, or null if not found.
*/
function getImageStage() {
return /** @type {HTMLElement|null} */ (document.getElementById("bone-image-container"));
}

/**
* Clears any existing annotation overlay from the image stage.
* Annotation loading is now handled directly in the dropdown change listeners
* via the `opts.annotationsUrl` option passed to `loadBoneImages`.
* @param {string} boneId - The bone ID (unused; retained for future use).
* @returns {Promise<void>}
*/
// Function maybeLoadAnnotations: Logic removed. Annotation URL construction is now in the listeners.
async function maybeLoadAnnotations(boneId) {
const stage = getImageStage();
Expand All @@ -32,7 +44,12 @@ async function maybeLoadAnnotations(boneId) {
// Backend API base (runs on 8000)
const API_BASE = "http://127.0.0.1:8000";

/** Helper: fetch images for a bone/sub-bone and render them */
/** Helper: fetch images for a bone/sub-bone and render them
* @param {string} boneId - The bone or subbone ID to load images for.
* @param {Object} [options={}] - Options forwarded to `displayBoneImages`, e.g.
* `{ annotationsUrl: string, boneId: string, isBonesetSelection: boolean }`.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't include the example of the options here. If the options that displayBoneImages expects are changed, it would be really easy to miss updating this bit of documentation here.

* @returns {Promise<void>}
*/
async function loadBoneImages(boneId, options = {}) {
const stage = getImageStage();
if (!boneId) {
Expand All @@ -57,6 +74,12 @@ async function loadBoneImages(boneId, options = {}) {
}
}

/**
* Populates the boneset `<select>` element with options from the provided array.
* Disables the dropdown if no bonesets are available.
* @param {Array<{id: string, name: string}>} bonesets - Array of boneset objects.
* @returns {void}
*/
export function populateBonesetDropdown(bonesets) {
const bonesetSelect = document.getElementById("boneset-select");
if (!bonesetSelect) {
Expand All @@ -78,6 +101,13 @@ export function populateBonesetDropdown(bonesets) {
bonesetSelect.disabled = false;
}

/**
* Wires up change event listeners on the boneset, bone, and subbone `<select>` elements.
* Each listener loads images, descriptions, and annotations appropriate to the selection.
* @param {Object} combinedData - The full application data set containing:
* `bonesets` {Array}, `bones` {Array}, and `subbones` {Array}.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The expected properties of combinedData should be listed in the format documented here: https://jsdoc.app/tags-param#parameters-with-properties

You've already done this correct with imageDisplay.applyRotation

* @returns {void}
*/
export function setupDropdownListeners(combinedData) {
const bonesetSelect = document.getElementById("boneset-select");
const boneSelect = document.getElementById("bone-select");
Expand Down
8 changes: 8 additions & 0 deletions templates/js/imageCaptions.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
// js/imageCaptions.js
// Image captions for bone images, extracted from PowerPoint slides

/**
* A lookup map of image captions keyed by bone/subbone ID.
* Each entry provides caption strings for up to two images (`image1`, `image2`).
* Captions describe the anatomical view shown in the corresponding image
* (e.g. lateral aspect, medial aspect).
*
* @type {Object.<string, {image1: string|null, image2: string|null}>}
*/
export const imageCaptions = {
// Bony Pelvis (main boneset)
"bony_pelvis": {
Expand Down
66 changes: 61 additions & 5 deletions templates/js/imageDisplay.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,42 @@ import { imageCaptions } from "./imageCaptions.js";
let currentBoneId = null;
let currentIsBonesetSelection = false; // Track if this is a boneset selection

/**
* Returns the `#bone-image-container` DOM element.
* @returns {HTMLElement|null} The image container element, or null if not found.
*/
function getImageContainer() {
return /** @type {HTMLElement|null} */ (
document.getElementById("bone-image-container")
);
}

/** Helper function to get captions for a boneId */
/** Helper function to get captions for a boneId
* @param {string|null} boneId - The bone or subbone ID.
* @returns {{image1: string|null, image2: string|null}} Caption strings for the two images, or nulls if not found.
*/
function getCaptionsForBone(boneId) {
if (!boneId || !imageCaptions[boneId]) {
return { image1: null, image2: null };
}
return imageCaptions[boneId];
}

/** Helper to clear existing caption container */
/** Removes the `#caption-container` element from the DOM if it exists.
* @returns {void}
*/
function clearCaptionContainer() {
const existingCaptions = document.getElementById("caption-container");
if (existingCaptions) {
existingCaptions.remove();
}
}

/** ---- Empty-state / clearing ------------------------------------------- */
/**
* Renders the empty-state placeholder message inside the image container
* and clears all annotations, colored regions, and captions.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clarify that "annotations" refers to text annotations specifically. (In some parts of the repo, we also use "annotation" to refer to the colored region annotations, or other image modifications entirely, so we want to be specific when referring to text annotations.)

* @returns {void}
*/
export function showPlaceholder() {
const c = getImageContainer();
if (!c) return;
Expand All @@ -53,6 +66,10 @@ export function showPlaceholder() {
if (imagesContent) imagesContent.classList.remove("has-images");
}

/**
* Clears all images, annotations, colored regions, and captions from the image container.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above: "annotations" → "text annotations"

* @returns {void}
*/
export function clearImages() {
const c = getImageContainer();
if (c) {
Expand All @@ -72,8 +89,16 @@ export function clearImages() {
if (imagesContent) imagesContent.classList.remove("has-images");
}

/** ---- Public entry: render images array --------------------------------
* Optionally pass { annotationsUrl: 'templates/data/annotations/xyz.json', boneId: 'bone_name' }
/**
* Renders one or more bone images into the image container, applying the appropriate
* layout (single, two-up, or grid) based on the number of images provided.
* Also loads colored region overlays and text annotation overlays if applicable.
* @param {Array<{url?: string, src?: string, alt?: string, filename?: string}>} images - Array of image objects to display.
* @param {Object} [options={}] - Optional display configuration:
* @param {string} [options.annotationsUrl] - API URL for text annotation JSON.
* @param {string} [options.boneId] - Bone ID used for colored region overlays.
* @param {boolean} [options.isBonesetSelection] - True when displaying the full boneset view.
* @returns {void}
*/
export function displayBoneImages(images, options = {}) {
const container = getImageContainer();
Expand Down Expand Up @@ -114,6 +139,13 @@ export function displayBoneImages(images, options = {}) {
}

//** ---- Single image ------------------------------------------------------ */
/**
* Renders a single bone image with its colored region overlay and text annotations.
* @param {{url?: string, src?: string, alt?: string, filename?: string}} image - The image object to display.
* @param {HTMLElement} container - The image container element.
* @param {Object} [options={}] - Options forwarded from `displayBoneImages`.
* @returns {void}
*/
function displaySingleImage(image, container, options = {}) {
// Get captions for this bone
const captions = getCaptionsForBone(currentBoneId);
Expand Down Expand Up @@ -197,6 +229,15 @@ const TWO_IMAGE_ROTATION = {
right: { rot_deg: 0, flipH: false },
};

/**
* Applies a CSS rotation (and optional horizontal flip) to an image element
* based on the PowerPoint rotation template for the given view.
* @param {HTMLImageElement} imgEl - The image element to transform.
* @param {Object} [options={}] - Rotation parameters.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This incorrectly lists the default value for options

* @param {number} [options.rot_deg=0] - Rotation angle in degrees.
* @param {boolean} [options.flipH=false] - Whether to flip the image horizontally.
* @returns {void}
*/
function applyRotation(imgEl, { rot_deg = 0, flipH = false } = {}) {
const parts = [];
if (flipH) parts.push("scaleX(-1)");
Expand All @@ -206,6 +247,14 @@ function applyRotation(imgEl, { rot_deg = 0, flipH = false } = {}) {
imgEl.style.willChange = "transform";
}

/**
* Renders two bone images side by side, each with its own colored region overlay.
* Appends a two-column caption bar beneath the images if captions are available.
* @param {Array<{url?: string, src?: string, alt?: string, filename?: string}>} images - Array of exactly two image objects.
* @param {HTMLElement} container - The image container element.
* @param {Object} [options={}] - Options forwarded from `displayBoneImages`.
* @returns {void}
*/
function displayTwoImages(images, container, options = {}) {
// Get captions for this bone
const captions = getCaptionsForBone(currentBoneId);
Expand Down Expand Up @@ -299,6 +348,13 @@ function displayTwoImages(images, container, options = {}) {
}

/** ---- 3+ images grid ---------------------------------------------------- */
/**
* Renders three or more bone images in a wrapping grid layout.
* Does not load colored regions or annotations (used for supplementary views).
* @param {Array<{url?: string, src?: string, alt?: string, filename?: string}>} images - Array of image objects.
* @param {HTMLElement} container - The image container element.
* @returns {void}
*/
function displayMultipleImages(images, container) {
const wrapper = document.createElement("div");
wrapper.className = "multiple-image-wrapper";
Expand Down
10 changes: 10 additions & 0 deletions templates/js/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,16 @@ document.addEventListener("DOMContentLoaded", async () => {
clearViewer();
});

/**
* Populates the subbone `<select>` element with options for the given subbone IDs.
* Inserts a placeholder option and disables the dropdown if no subbones are provided.
/**
* Populates the subbone `<select>` element with options for the given subbone IDs.
* Inserts a placeholder option and disables the dropdown if no subbones are provided.
Comment on lines +115 to +117
* @param {HTMLSelectElement} dropdown - The subbone select element to populate.
* @param {string[]} subbones - Array of subbone ID strings.
* @returns {void}
*/
function populateSubboneDropdown(dropdown, subbones) {
// Leave a placeholder option so the user must explicitly select a subbone
dropdown.innerHTML = "";
Expand Down
Loading
Loading