From 6bdc6cd683b90b193ea2f41e772609f99fbfb6d7 Mon Sep 17 00:00:00 2001 From: joshua-dean Date: Mon, 7 Oct 2024 13:23:21 -0500 Subject: [PATCH 1/9] Add `ErrorLevel` enum --- src/error_handling.ts | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 src/error_handling.ts diff --git a/src/error_handling.ts b/src/error_handling.ts new file mode 100644 index 00000000..10acdb14 --- /dev/null +++ b/src/error_handling.ts @@ -0,0 +1,9 @@ +/** + * Error handling utilities for ULabel. + */ + +export enum ErrorLevel { + INFO = 0, + STANDARD = 1, + FATAL = 2, +} From bc9c4fd7608dbdd2abff1d59d693c95135b37a96 Mon Sep 17 00:00:00 2001 From: joshua-dean Date: Mon, 7 Oct 2024 13:39:26 -0500 Subject: [PATCH 2/9] Port existing logging hooey to new call --- src/error_handling.ts | 9 ----- src/error_logging.ts | 40 ++++++++++++++++++++ src/index.js | 88 +++++++++++++++++++++---------------------- 3 files changed, 83 insertions(+), 54 deletions(-) delete mode 100644 src/error_handling.ts create mode 100644 src/error_logging.ts diff --git a/src/error_handling.ts b/src/error_handling.ts deleted file mode 100644 index 10acdb14..00000000 --- a/src/error_handling.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Error handling utilities for ULabel. - */ - -export enum ErrorLevel { - INFO = 0, - STANDARD = 1, - FATAL = 2, -} diff --git a/src/error_logging.ts b/src/error_logging.ts new file mode 100644 index 00000000..9d4ab786 --- /dev/null +++ b/src/error_logging.ts @@ -0,0 +1,40 @@ +/** + * Error handling utilities for ULabel. + */ + +export enum LogLevel { + VERBOSE = 0, + INFO = 1, + WARNING = 2, + ERROR = 3, +} + +/** + * Log a message to the console at a level. + * This was ported from code that didn't use the console log levels, + * and is kept for compatibility. + * + * @param message Message to log + * @param log_level Level to log at + */ +export function log_message( + message: string, + log_level: LogLevel = LogLevel.INFO, +) { + switch (log_level) { + case LogLevel.VERBOSE: + console.debug(message); + break; + case LogLevel.INFO: + console.log(message); + break; + case LogLevel.WARNING: + console.warn(message); + alert("[WARNING] " + message); + break; + case LogLevel.ERROR: + console.error(message); + alert("[ERROR] " + message); + throw new Error(message); + } +} diff --git a/src/index.js b/src/index.js index 4ce2c7e5..eb98e0b7 100644 --- a/src/index.js +++ b/src/index.js @@ -37,6 +37,7 @@ import { import { create_ulabel_listeners, remove_ulabel_listeners } from "../build/listeners"; import { NightModeCookie } from "../build/cookies"; +import { log_message, LogLevel } from "../build/error_logging"; import $ from "jquery"; const jQuery = $; @@ -62,20 +63,6 @@ jQuery.fn.outer_html = function () { }; export class ULabel { - // ================= Internal constants ================= - - static get elvl_info() { - return 0; - } - - static get elvl_standard() { - return 1; - } - - static get elvl_fatal() { - return 2; - } - static version() { return ULABEL_VERSION; } @@ -453,7 +440,10 @@ export class ULabel { }; } if (first_non_ro === null) { - ul.raise_error("You must have at least one subtask without 'read_only' set to true.", ULabel.elvl_fatal); + log_message( + "You must have at least one subtask without 'read_only' set to true.", + LogLevel.ERROR, + ); } } @@ -502,7 +492,10 @@ export class ULabel { } else if ("spacing" in raw_img_dat && "frames" in raw_img_dat) { return raw_img_dat; } else { - ul.raise_error(`Image data object not understood. Must be of form "http://url.to/img" OR ["img1", "img2", ...] OR {spacing: {x: , y: , z: , units: }, frames: ["img1", "img2", ...]}. Provided: ${JSON.stringify(raw_img_dat)}`, ULabel.elvl_fatal); + log_message( + `Image data object not understood. Must be of form "http://url.to/img" OR ["img1", "img2", ...] OR {spacing: {x: , y: , z: , units: }, frames: ["img1", "img2", ...]}. Provided: ${JSON.stringify(raw_img_dat)}`, + LogLevel.ERROR, + ); return null; } } @@ -787,7 +780,10 @@ export class ULabel { callback(); }).catch((err) => { console.log(err); - this.raise_error("Unable to load images: " + JSON.stringify(err), ULabel.elvl_fatal); + log_message( + "Failed to load images: " + JSON.stringify(err), + LogLevel.ERROR, + ); }); // Final code to be called after the object is initialized @@ -902,7 +898,10 @@ export class ULabel { return; } else { - this.raise_error(`Initial crop must contain properties "width", "height", "left", and "top". Ignoring.`, ULabel.elvl_info); + log_message( + `Initial crop must contain properties "width", "height", "left", and "top". Ignoring.`, + LogLevel.INFO, + ); } } this.show_whole_image(); @@ -1268,7 +1267,10 @@ export class ULabel { ]; default: // TODO broader refactor of error handling and detecting/preventing corruption - this.raise_error("Annotation mode is not understood", ULabel.elvl_info); + log_message( + "Annotation mode is not understood", + LogLevel.INFO, + ); return null; } } @@ -1475,9 +1477,9 @@ export class ULabel { tbar_pts = spatial_payload; return [tbar_pts[tbi][0], tbar_pts[tbj][1]]; default: - this.raise_error( + log_message( "Unable to apply access string to annotation of type " + spatial_type, - ULabel.elvl_standard, + LogLevel.WARNING, ); } } @@ -1558,9 +1560,9 @@ export class ULabel { } break; default: - this.raise_error( + log_message( "Unable to apply access string to annotation of type " + spatial_type, - ULabel.elvl_standard, + LogLevel.WARNING, ); } } @@ -2069,7 +2071,12 @@ export class ULabel { this.draw_global_annotation(annotation_object, subtask); break; default: - this.raise_error("Warning: Annotation " + annotation_object["id"] + " not understood", ULabel.elvl_info); + // TODO (joshua-dean): why would this log at info level, + // and then write "warning" in the message? + log_message( + "Warning: Annotation " + annotation_object["id"] + " not understood", + LogLevel.INFO, + ); break; } } @@ -4204,7 +4211,10 @@ export class ULabel { this.rebuild_containing_box(actid); break; default: - this.raise_error(`Annotation mode is not understood: ${spatial_type}`, ULabel.elvl_info); + log_message( + `Annotation mode is not understood: ${spatial_type}`, + LogLevel.INFO, + ); break; } this.redraw_annotation(actid); @@ -4640,10 +4650,16 @@ export class ULabel { break; case "contour": // TODO contour editing - this.raise_error("Annotation mode is not currently editable", ULabel.elvl_info); + log_message( + "Annotation mode is not currently editable", + LogLevel.INFO, + ); break; default: - this.raise_error("Annotation mode is not understood", ULabel.elvl_info); + log_message( + "Annotation mode is not understood", + LogLevel.INFO, + ); break; } } @@ -5456,24 +5472,6 @@ export class ULabel { } } - // ================= Error handlers ================= - - // Notify the user of information at a given level - raise_error(message, level = ULabel.elvl_standard) { - switch (level) { - // TODO less crude here - case ULabel.elvl_info: - console.log("[info] " + message); - break; - case ULabel.elvl_standard: - alert("[error] " + message); - break; - case ULabel.elvl_fatal: - alert("[fatal] " + message); - throw new Error(message); - } - } - // ================= Mouse event interpreters ================= // Get the mouse position on the screen From 7812ae252c01bdc08c8ad9b38f4944031624ce20 Mon Sep 17 00:00:00 2001 From: joshua-dean Date: Mon, 7 Oct 2024 14:08:00 -0500 Subject: [PATCH 3/9] Pull static canvas functions --- index.d.ts | 8 +++++- src/canvas_utils.ts | 70 +++++++++++++++++++++++++++++++++++++++++++++ src/index.js | 53 ++-------------------------------- 3 files changed, 80 insertions(+), 51 deletions(-) create mode 100644 src/canvas_utils.ts diff --git a/index.d.ts b/index.d.ts index 156778e4..02035e63 100644 --- a/index.d.ts +++ b/index.d.ts @@ -169,7 +169,7 @@ export type ImageData = { export type ULabelSubtasks = { [key: string]: ULabelSubtask }; export class ULabel { - subtasks: ULabelSubtask[]; + subtasks: ULabelSubtasks; state: { // Viewer state zoom_val: number; @@ -356,6 +356,12 @@ export class ULabel { thumbnail?: boolean, nonspatial?: boolean, ): void; + + // Canvases + public get_init_canvas_context_id( + annotation_id: string, + subtask?: string, // SUBTASK KEY + ): string; } declare global { diff --git a/src/canvas_utils.ts b/src/canvas_utils.ts new file mode 100644 index 00000000..d2ed6f05 --- /dev/null +++ b/src/canvas_utils.ts @@ -0,0 +1,70 @@ +/** + * Annotation canvas utilities. + * TODO (joshua-dean): Pull the rest of the canvas functions into here + */ + +import type { ULabel, ULabelSubtasks } from ".."; +import { NONSPATIAL_MODES } from "./annotation"; +import { Configuration, DEFAULT_N_ANNOS_PER_CANVAS, TARGET_MAX_N_CANVASES_PER_SUBTASK } from "./configuration"; + +/** + * If the user doesn't provide a number of annotations per canvas, set it dynamically. + * This should help with performance. + * + * @param config ULabel configuration + * @param subtasks ULabel subtasks + */ +function dynamically_set_n_annos_per_canvas( + config: Configuration, + subtasks: ULabelSubtasks, +) { + // If they didn't provide a value, we'll still be using the default + if (config.n_annos_per_canvas === DEFAULT_N_ANNOS_PER_CANVAS) { + // Count max annotations per subtask + const max_annos = Math.max( + ...Object.values(subtasks).map(subtask => subtask.annotations.ordering.length), + ); + + // Performance starts to deteriorate when we require many canvases to be drawn on + // To be safe, check if max_annos / DEFAULT_N_ANNOS_PER_CANVAS is greater than TARGET_MAX_N_CANVASES_PER_SUBTASK + if (max_annos / DEFAULT_N_ANNOS_PER_CANVAS > TARGET_MAX_N_CANVASES_PER_SUBTASK) { + // If so, raise the default + config.n_annos_per_canvas = Math.ceil(max_annos / TARGET_MAX_N_CANVASES_PER_SUBTASK); + } + } +} + +/** + * Initialize annotation canvases and assign annotations to them + * + * @param ulabel ULabel instance + * @param subtask_key Subtask key. If null, this will dynamically initialize for all subtasks. + */ +export function initialize_annotation_canvases( + ulabel: ULabel, + subtask_key: string = null, +) { + if (subtask_key === null) { + dynamically_set_n_annos_per_canvas( + ulabel.config, + ulabel.subtasks, + ); + for (const subtask_key in ulabel.subtasks) { + initialize_annotation_canvases(ulabel, subtask_key); + } + return; + } + + // TODO (joshua-dean): shouldn't this just be a separate function? + // Create the canvas for each annotation + const subtask = ulabel.subtasks[subtask_key]; + for (const annotation_id in subtask.annotations.access) { + const annotation = subtask.annotations.access[annotation_id]; + if (!NONSPATIAL_MODES.includes(annotation.spatial_type)) { + annotation["canvas_id"] = ulabel.get_init_canvas_context_id( + annotation_id, + subtask_key, + ); + } + } +} diff --git a/src/index.js b/src/index.js index eb98e0b7..75fefd4f 100644 --- a/src/index.js +++ b/src/index.js @@ -14,8 +14,6 @@ import { GeometricUtils } from "../build/geometric_utils"; import { AllowedToolboxItem, Configuration, - DEFAULT_N_ANNOS_PER_CANVAS, - TARGET_MAX_N_CANVASES_PER_SUBTASK, } from "../build/configuration"; import { get_gradient } from "../build/drawing_utilities"; import { @@ -38,6 +36,7 @@ import { import { create_ulabel_listeners, remove_ulabel_listeners } from "../build/listeners"; import { NightModeCookie } from "../build/cookies"; import { log_message, LogLevel } from "../build/error_logging"; +import { initialize_annotation_canvases } from "../build/canvas_utils"; import $ from "jquery"; const jQuery = $; @@ -447,25 +446,6 @@ export class ULabel { } } - static initialize_annotation_canvases(ul, subtask_key = null) { - if (subtask_key === null) { - ul.dynamically_set_n_annos_per_canvas(); - for (const subtask_key in ul.subtasks) { - ULabel.initialize_annotation_canvases(ul, subtask_key); - } - return; - } - - // Create the canvas for each annotation - const subtask = ul.subtasks[subtask_key]; - for (const annotation_id in subtask.annotations.access) { - let annotation = subtask.annotations.access[annotation_id]; - if (!NONSPATIAL_MODES.includes(annotation.spatial_type)) { - annotation["canvas_id"] = ul.get_init_canvas_context_id(annotation_id, subtask_key); - } - } - } - static expand_image_data(ul, raw_img_dat) { if (typeof raw_img_dat === "string") { return { @@ -739,7 +719,7 @@ export class ULabel { } // Create the annotation canvases for the resume_from annotations - ULabel.initialize_annotation_canvases(that); + initialize_annotation_canvases(that); // Add the ID dialogs' HTML to the document build_id_dialogs(that); @@ -1288,33 +1268,6 @@ export class ULabel { } } - /** - * If no user-provided n_annos_per_canvas is provided, - * Check if we should dynamically set it based on the number of annotations - * in the subtasks, to help with performance. - * - */ - dynamically_set_n_annos_per_canvas() { - // Check if we should increase n_annos_per_canvas - // First, check if the value is still the default - if (this.config.n_annos_per_canvas === DEFAULT_N_ANNOS_PER_CANVAS) { - // See if we should dynamically raise the default by checking max number of annotations in a subtask - let max_annos = 0; - for (const subtask_key in this.subtasks) { - const subtask = this.subtasks[subtask_key]; - if (subtask.annotations.ordering.length > max_annos) { - max_annos = subtask.annotations.ordering.length; - } - } - // Performance starts to deteriorate when we require many canvases to be drawn on - // To be safe, check if max_annos / DEFAULT_N_ANNOS_PER_CANVAS is greater than TARGET_MAX_N_CANVASES_PER_SUBTASK - if (max_annos / DEFAULT_N_ANNOS_PER_CANVAS > TARGET_MAX_N_CANVASES_PER_SUBTASK) { - // If so, raise the default - this.config.n_annos_per_canvas = Math.ceil(max_annos / TARGET_MAX_N_CANVASES_PER_SUBTASK); - } - } - } - /** * Find the next available annotation context and return its ID. * If all annotation contexts are in use, create a new canvas and return it's id. @@ -6280,7 +6233,7 @@ export class ULabel { } // Set new annotations and initialize canvases ULabel.process_resume_from(this, subtask, { resume_from: new_annotations }); - ULabel.initialize_annotation_canvases(this, subtask); + initialize_annotation_canvases(this, subtask); // Redraw all annotations to render them this.redraw_all_annotations(subtask); // Calculate distances for all annotations if FilterDistance is present From 064304c50954056d5b6c941e0c2ed4285f5b8ee0 Mon Sep 17 00:00:00 2001 From: joshua-dean Date: Mon, 7 Oct 2024 14:22:01 -0500 Subject: [PATCH 4/9] Change `after_init` to be instance function --- src/index.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/index.js b/src/index.js index 75fefd4f..59d0678f 100644 --- a/src/index.js +++ b/src/index.js @@ -515,16 +515,6 @@ export class ULabel { }; } - /** - * Code to be called after ULabel has finished initializing. - */ - static after_init(ulabel) { - // Perform the after_init method for each toolbox item - for (const toolbox_item of ulabel.toolbox.items) { - toolbox_item.after_init(); - } - } - // ================= Construction/Initialization ================= constructor(kwargs) { @@ -767,11 +757,21 @@ export class ULabel { }); // Final code to be called after the object is initialized - ULabel.after_init(this); + this.after_init(); console.log(`Time taken to construct and initialize: ${Date.now() - this.begining_time}`); } + /** + * Code to be called after ULabel has finished initializing. + */ + after_init() { + // Perform the after_init method for each toolbox item + for (const toolbox_item of this.toolbox.items) { + toolbox_item.after_init(); + } + } + version() { return ULabel.version(); } From 40320ece884f9d797bfabe9ef609180c47cc6228 Mon Sep 17 00:00:00 2001 From: joshua-dean Date: Mon, 7 Oct 2024 14:31:41 -0500 Subject: [PATCH 5/9] Remove `load_image_promise` in favor of `decode` --- src/index.js | 23 +++-------------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/src/index.js b/src/index.js index 59d0678f..3b074ade 100644 --- a/src/index.js +++ b/src/index.js @@ -480,18 +480,6 @@ export class ULabel { } } - static load_image_promise(img_el) { - return new Promise((resolve, reject) => { - try { - img_el.onload = () => { - resolve(img_el); - }; - } catch (err) { - reject(err); - } - }); - } - static handle_deprecated_arguments() { // Warn users that this method is deprecated console.warn(` @@ -665,14 +653,9 @@ export class ULabel { $("#" + this.config["container_id"]).addClass("ulabel-night"); } - var images = [document.getElementById(`${this.config["image_id_pfx"]}__0`)]; - let mappable_images = []; - for (let i = 0; i < images.length; i++) { - mappable_images.push(images[i]); - break; - } - let image_promises = mappable_images.map(ULabel.load_image_promise); - Promise.all(image_promises).then((loaded_imgs) => { + let first_img = document.getElementById(`${this.config["image_id_pfx"]}__0`); + first_img.decode().then(() => { + const loaded_imgs = [first_img]; // Store image dimensions that.config["image_height"] = loaded_imgs[0].naturalHeight; that.config["image_width"] = loaded_imgs[0].naturalWidth; From eeaad53cb257959f422231dc8707a6eb191947c5 Mon Sep 17 00:00:00 2001 From: joshua-dean Date: Mon, 7 Oct 2024 14:34:06 -0500 Subject: [PATCH 6/9] Remove unecessary array cast --- src/index.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/index.js b/src/index.js index 3b074ade..55ebf28d 100644 --- a/src/index.js +++ b/src/index.js @@ -655,10 +655,9 @@ export class ULabel { let first_img = document.getElementById(`${this.config["image_id_pfx"]}__0`); first_img.decode().then(() => { - const loaded_imgs = [first_img]; // Store image dimensions - that.config["image_height"] = loaded_imgs[0].naturalHeight; - that.config["image_width"] = loaded_imgs[0].naturalWidth; + that.config["image_height"] = first_img.naturalHeight; + that.config["image_width"] = first_img.naturalWidth; // Add canvasses for each subtask and get their rendering contexts for (const st in that.subtasks) { From 03bbdeaa7e536611b1fb2c76d53b9b436857eb85 Mon Sep 17 00:00:00 2001 From: joshua-dean Date: Mon, 7 Oct 2024 14:39:31 -0500 Subject: [PATCH 7/9] `first_img` -> `first_bg_img` --- src/index.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/index.js b/src/index.js index 55ebf28d..185fcff6 100644 --- a/src/index.js +++ b/src/index.js @@ -653,11 +653,11 @@ export class ULabel { $("#" + this.config["container_id"]).addClass("ulabel-night"); } - let first_img = document.getElementById(`${this.config["image_id_pfx"]}__0`); - first_img.decode().then(() => { + let first_bg_img = document.getElementById(`${this.config["image_id_pfx"]}__0`); + first_bg_img.decode().then(() => { // Store image dimensions - that.config["image_height"] = first_img.naturalHeight; - that.config["image_width"] = first_img.naturalWidth; + that.config["image_height"] = first_bg_img.naturalHeight; + that.config["image_width"] = first_bg_img.naturalWidth; // Add canvasses for each subtask and get their rendering contexts for (const st in that.subtasks) { From 879a644fe27ca8080f2a736e9a03b6a6ef0e202e Mon Sep 17 00:00:00 2001 From: joshua-dean Date: Mon, 7 Oct 2024 14:53:18 -0500 Subject: [PATCH 8/9] Fix other case of `raise_error` --- index.d.ts | 1 - src/html_builder.ts | 6 +++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/index.d.ts b/index.d.ts index 02035e63..0071f682 100644 --- a/index.d.ts +++ b/index.d.ts @@ -244,7 +244,6 @@ export class ULabel { public show_annotation_mode( target_jq?: JQuery, // TODO (joshua-dean): validate this type ); - public raise_error(message: string, level?: number); public rezoom(): void; public update_frame(delta?: number, new_frame?: number): void; public handle_id_dialog_hover( diff --git a/src/html_builder.ts b/src/html_builder.ts index f791703b..372819a4 100644 --- a/src/html_builder.ts +++ b/src/html_builder.ts @@ -16,6 +16,7 @@ import { GLOBAL_SVG, get_init_style, } from "../src/blobs"; +import { log_message, LogLevel } from "./error_logging"; /** * Creates a style document, populates it with the styles in get_init_style, and appends it to the page. @@ -224,7 +225,10 @@ export function prep_window_html(ulabel: ULabel, toolbox_item_order: unknown[] = // If initial_crop exists but doesn't have the appropriate properties, // then raise an error and return false - ulabel.raise_error("initial_crop missing necessary properties. Ignoring."); + log_message( + "initial_crop missing necessary properties. Ignoring.", + LogLevel.INFO, + ); return false; }; From 8939827f1bf31d1312b7f3eb9b08aaa4e4538d29 Mon Sep 17 00:00:00 2001 From: Joshua Dean Date: Thu, 10 Oct 2024 07:38:11 -0500 Subject: [PATCH 9/9] Clarify log message for invalid spatial type Co-authored-by: Trevor Burgoyne <82477095+TrevorBurgoyne@users.noreply.github.com> --- src/index.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/index.js b/src/index.js index 185fcff6..91ab7106 100644 --- a/src/index.js +++ b/src/index.js @@ -2006,10 +2006,8 @@ export class ULabel { this.draw_global_annotation(annotation_object, subtask); break; default: - // TODO (joshua-dean): why would this log at info level, - // and then write "warning" in the message? log_message( - "Warning: Annotation " + annotation_object["id"] + " not understood", + "Annotation mode " + annotation_object["spatial_type"] + " not understood", LogLevel.INFO, ); break;