diff --git a/.github/tasks.md b/.github/tasks.md index c54445be..d51ecef5 100644 --- a/.github/tasks.md +++ b/.github/tasks.md @@ -1,2 +1,2 @@ ## Tasks -- + diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8bfe207f..9b2db972 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -59,4 +59,40 @@ jobs: with: name: playwright-report path: playwright-report/ - retention-days: 30 \ No newline at end of file + retention-days: 30 + + type-tests: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm install + + - name: Build project + run: npm run build + + - name: Pack tarball + run: npm pack + + - name: Install tarball in temp consumer project + run: | + mkdir /tmp/type-test-consumer + cp tests/types/index.test-d.ts /tmp/type-test-consumer/ + cp tests/types/tsconfig.json /tmp/type-test-consumer/ + cd /tmp/type-test-consumer + npm init -y + npm install $GITHUB_WORKSPACE/ulabel-*.tgz + npm install --save-dev typescript @types/jquery + + - name: Verify type declarations compile + run: | + cd /tmp/type-test-consumer + npx tsc --noEmit \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index ad7b66fc..5064361f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented here. ## [unreleased] +## [0.23.4] - May 5th, 2026 +- Fix type declarations for npm consumers +- Add `"files"` field to `package.json` to explicitly control published package contents. +- Add CI type-test job that validates type declarations against a packed tarball. + ## [0.23.3] - Mar 18th, 2026 - Add `get_keypoint_slider_value()` public API method to get the current keypoint slider value (0-1). - Add `get_distance_filter_value()` public API method to get the current distance filter slider values. diff --git a/index.d.ts b/index.d.ts index b359ca2f..767bb9ed 100644 --- a/index.d.ts +++ b/index.d.ts @@ -4,6 +4,8 @@ import { FilterDistanceOverlay } from "./src/overlays"; import { ULabelSubtask } from "./src/subtask"; import { Toolbox, AnnotationResizeItem } from "./src/toolbox"; +export { ULabelAnnotation, AllowedToolboxItem, Configuration, FilterDistanceOverlay, ULabelSubtask, Toolbox, AnnotationResizeItem }; + export type DistanceFromPolyline = { distance: number; polyline_id?: string; @@ -15,7 +17,7 @@ export type DistanceFromPolyline = { */ export type DistanceFromPolylineClasses = { closest_row: DistanceFromPolyline; - [key: number]: DistanceFromPolyline; + [key: string]: DistanceFromPolyline; }; export type AbstractPoint = { @@ -63,7 +65,7 @@ export type ClassDefinition = { name: string; id: number; color: string; - keybind?: string; + keybind: string | null; }; export type SliderInfo = { @@ -133,7 +135,7 @@ export type ULabelAnnotations = { [key: string]: ULabelAnnotation[] }; export type ULabelSubmitData = { annotations: ULabelAnnotations; - task_meta: object; + task_meta: object | null; }; export type ULabelSubmitHandler = (submitData: ULabelSubmitData) => void; @@ -239,6 +241,23 @@ export type ULabelActionCandidate = { export type ULabelSubtasks = { [key: string]: ULabelSubtask }; +export type ULabelConstructorArgs = { + container_id: string; + image_data: string | string[]; + username: string; + submit_buttons: ULabelSubmitButton[]; + subtasks: ULabelSubtasks; + task_meta?: object; + annotation_meta?: object; + px_per_px?: number; + initial_crop?: InitialCrop; + initial_line_size?: number; + instructions_url?: string; + toolbox_order?: AllowedToolboxItem[]; + /** @deprecated Use top-level properties instead. */ + config_data?: object; +}; + export class ULabel { subtasks: ULabelSubtasks; state: { @@ -256,6 +275,9 @@ export class ULabel { anno_scaling_mode: AnnoScalingMode; // Keybind editing state is_editing_keybind: boolean; + // Original keybind storage + original_config_keybinds?: { [config_key: string]: string }; + original_class_keybinds?: { [class_id: number]: string | null }; // Render state // TODO (joshua-dean): this is never assigned, is it used? demo_canvas_context: CanvasRenderingContext2D; @@ -272,9 +294,13 @@ export class ULabel { begining_time: number; is_init: boolean; resize_observers: ResizeObserver[]; + /** * @link https://github.com/SenteraLLC/ulabel/blob/main/api_spec.md#ulabel-constructor */ + constructor(kwargs: ULabelConstructorArgs); + + /** @deprecated Pass a single kwargs object instead of positional arguments. */ constructor( container_id: string, image_data: string | string[], @@ -294,6 +320,7 @@ export class ULabel { /** * @link https://github.com/SenteraLLC/ulabel/blob/main/api_spec.md#display-utility-functions */ + public version(): string; public init(callback: () => void): void; public after_init(): void; public show_initial_crop(): void; @@ -309,22 +336,22 @@ export class ULabel { public switch_to_next_subtask(): void; // Annotations - public get_annotations(subtask: ULabelSubtask): ULabelAnnotation[]; - public set_annotations(annotations: ULabelAnnotation[], subtask: ULabelSubtask); - public set_saved(saved: boolean); + public get_annotations(subtask: string): ULabelAnnotation[]; + public set_annotations(annotations: ULabelAnnotation[], subtask: string): void; + public set_saved(saved: boolean): void; public draw_annotation_from_id(id: string, offset?: Offset, subtask?: string): void; public redraw_annotation(annotation_id: string, subtask?: string, offset?: Offset): void; public redraw_all_annotations( - subtask?: string, // TODO (joshua-dean): THIS IS SUBTASK KEY, NAME PROPERLY - offset?: number, + subtask?: string, + offset?: number | null, spatial_only?: boolean, - ); - public redraw_multiple_spatial_annotations(annotation_ids: string[], subtask?: string, offset?: Offset); + ): void; + public redraw_multiple_spatial_annotations(annotation_ids: string[], subtask?: string, offset?: Offset): void; public clear_nonspatial_annotation(annotation_id: string): void; public show_annotation_mode( - target_jq?: JQuery, // TODO (joshua-dean): validate this type - ); - public update_frame(delta?: number, new_frame?: number): void; + target_jq?: JQuery | null, // TODO (joshua-dean): validate this type + ): void; + public update_frame(delta?: number | null, new_frame?: number | null): void; public rebuild_containing_box(actid: string, ignore_final?: boolean, subtask?: string): void; public update_filter_distance_during_polyline_move( annotation_id: string, @@ -341,7 +368,7 @@ export class ULabel { public get_keypoint_slider_value(): number | null; public get_distance_filter_value(): DistanceFromPolylineClasses | null; public fly_to_next_annotation(increment: number, max_zoom?: number): boolean; - public fly_to_annotation_id(annotation_id: string, subtask_key?: string, max_zoom?: number): boolean; + public fly_to_annotation_id(annotation_id: string, subtask_key?: string | null, max_zoom?: number): boolean; public fly_to_annotation(annotation: ULabelAnnotation, subtask_key?: string, max_zoom?: number): boolean; // Brush @@ -361,9 +388,10 @@ export class ULabel { public remove_listeners(): void; // Static functions + static version(): string; static get_time(): string; - static get_allowed_toolbox_item_enum(): AllowedToolboxItem; - static get_resize_toolbox_item(): AnnotationResizeItem; + static get_allowed_toolbox_item_enum(): typeof AllowedToolboxItem; + static get_resize_toolbox_item(): typeof AnnotationResizeItem; static process_classes(ulabel_obj: ULabel, arg1: string, subtask_obj: ULabelSubtask): void; static build_id_dialogs(ulabel_obj: ULabel): void; @@ -375,8 +403,8 @@ export class ULabel { // Annotation lifecycle // TODO (joshua-dean): type for redo_payload - public begin_annotation(mouse_event: JQuery.TriggeredEvent, annotation_id?: string, redo_payload?: object): void; - public continue_annotation(mouse_event: JQuery.TriggeredEvent, is_click?: boolean, annotation_id?: string, redo_payload?: object): void; + public begin_annotation(mouse_event: JQuery.TriggeredEvent | null | undefined, annotation_id?: string | null, redo_payload?: object | null): void; + public continue_annotation(mouse_event: JQuery.TriggeredEvent | null | undefined, is_click?: boolean, annotation_id?: string | null, redo_payload?: object | null): void; public delete_annotation( annotation_id: string, redoing?: boolean, @@ -468,14 +496,14 @@ export class ULabel { // Edit suggestions public suggest_edits( - mouse_event?: JQuery.TriggeredEvent, - nonspatial_id?: string, + mouse_event?: JQuery.TriggeredEvent | null, + nonspatial_id?: string | null, force_refresh?: boolean, ): void; public show_global_edit_suggestion( annid: string, - offset?: Offset, - nonspatial_id?: string, + offset?: Offset | null, + nonspatial_id?: string | null, ): void; public hide_global_edit_suggestion(): void; public hide_edit_suggestion(): void; @@ -485,7 +513,7 @@ export class ULabel { annid: string, access_str: string, as_though_pre_splice: boolean, - ); + ): unknown; // Drawing public rezoom( @@ -537,3 +565,5 @@ declare global { replaceLowerConcat(before: string, after: string, concat_string?: string): string; } } + +export default ULabel; diff --git a/package.json b/package.json index 03697073..c9e700e3 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,15 @@ { "name": "ulabel", "description": "An image annotation tool.", - "version": "0.23.3", + "version": "0.23.4", "main": "dist/ulabel.min.js", "module": "dist/ulabel.min.js", "types": "index.d.ts", + "files": [ + "dist/", + "src/", + "index.d.ts" + ], "exports": { ".": { "types": "./index.d.ts", @@ -34,7 +39,7 @@ "build-dev-and-demo": "npm run build-dev && npm run demo", "build-and-test": "npm run build && npm run test:both", "prepare": "husky", - "lint": "eslint . --no-fix" + "lint": "tsc --noEmit && eslint . --no-fix" }, "lint-staged": { "**/*.{js,mjs,cjs,ts}": "eslint --fix" diff --git a/src/actions.ts b/src/actions.ts index 1eb2a665..82fe2c2d 100644 --- a/src/actions.ts +++ b/src/actions.ts @@ -28,7 +28,7 @@ import { log_message, LogLevel } from "./error_logging"; export function record_action(ulabel: ULabel, raw_action: ULabelActionRaw, is_redo: boolean = false, add_to_action_stream: boolean = true) { ulabel.set_saved(false); const current_subtask = ulabel.get_current_subtask(); - const annotation = current_subtask.annotations.access[raw_action.annotation_id]; + const annotation = current_subtask.annotations.access[raw_action.annotation_id!]; // After a new action, you can no longer redo old actions if (add_to_action_stream && !is_redo) { @@ -45,7 +45,7 @@ export function record_action(ulabel: ULabel, raw_action: ULabelActionRaw, is_re redo_payload: JSON.stringify(raw_action.redo_payload), prev_timestamp: annotation?.last_edited_at || null, prev_user: annotation?.last_edited_by || "unknown", - }; + } as ULabelAction; // Add to stream if (add_to_action_stream) { @@ -72,7 +72,7 @@ export function record_action(ulabel: ULabel, raw_action: ULabelActionRaw, is_re export function record_finish(ulabel: ULabel, active_id: string) { // Set up constants for convenience const current_subtask = ulabel.get_current_subtask(); - const action = current_subtask.actions.stream.pop(); + const action = current_subtask.actions.stream.pop()!; // Parse and complete the redo payload const redo_payload = JSON.parse(action.redo_payload); @@ -145,7 +145,7 @@ export function record_finish_move( ) { // Set up constants for convenience const current_subtask = ulabel.get_current_subtask(); - const action = current_subtask.actions.stream.pop(); + const action = current_subtask.actions.stream.pop()!; // Parse and complete the redo/undo payloads const redo_payload = JSON.parse(action.redo_payload); @@ -169,7 +169,7 @@ export function record_finish_move( // undo/redo the move record_action(ulabel, { act_type: "finish_move", - annotation_id: action.annotation_id, + annotation_id: action.annotation_id!, frame: ulabel.state.current_frame, undo_payload: {}, redo_payload: {}, @@ -293,11 +293,11 @@ function trigger_action_listeners( // For actions without a specific "redo" listener, call the "action" listener instead (is_redo && !("redo" in action_map[action.act_type]) && "action" in action_map[action.act_type]) ) { - action_map[action.act_type].action(ulabel, action); + action_map[action.act_type].action!(ulabel, action); } else if (is_undo && "undo" in action_map[action.act_type]) { - action_map[action.act_type].undo(ulabel, action, is_undo); + action_map[action.act_type].undo!(ulabel, action, is_undo); } else if (is_redo && "redo" in action_map[action.act_type]) { - action_map[action.act_type].redo(ulabel, action); + action_map[action.act_type].redo!(ulabel, action); } } } @@ -316,7 +316,7 @@ function on_start_annotation_spatial_modification( is_undo: boolean = false, ) { // Draw new annotation - ulabel.draw_annotation_from_id(action.annotation_id); + ulabel.draw_annotation_from_id(action.annotation_id!); } /** @@ -335,16 +335,16 @@ function on_in_progress_annotation_spatial_modification( const subtask_key = ulabel.get_current_subtask_key(); const current_subtask = ulabel.subtasks[subtask_key]; const offset: Offset = current_subtask.state.move_candidate?.offset || { - id: action.annotation_id, + id: action.annotation_id!, diffX: 0, diffY: 0, diffZ: 0, }; // Update the toolbox filter distance - ulabel.update_filter_distance_during_polyline_move(action.annotation_id, true, false, offset); + ulabel.update_filter_distance_during_polyline_move(action.annotation_id!, true, false, offset); // Update the annotation rendering - ulabel.rebuild_containing_box(action.annotation_id, false, subtask_key); - ulabel.redraw_annotation(action.annotation_id, subtask_key, offset); + ulabel.rebuild_containing_box(action.annotation_id!, false, subtask_key); + ulabel.redraw_annotation(action.annotation_id!, subtask_key, offset); // Update dialogs ulabel.suggest_edits(); } @@ -364,15 +364,15 @@ function on_finish_annotation_spatial_modification( is_undo: boolean = false, ) { // Update annotation rendering - ulabel.rebuild_containing_box(action.annotation_id); - ulabel.redraw_annotation(action.annotation_id); + ulabel.rebuild_containing_box(action.annotation_id!); + ulabel.redraw_annotation(action.annotation_id!); // Update dialogs ulabel.suggest_edits(null, null, true); // Update the toolbox - ulabel.update_filter_distance(action.annotation_id); + ulabel.update_filter_distance(action.annotation_id!); ulabel.toolbox.redraw_update_items(ulabel); // Ensure there are no lingering enders - ulabel.destroy_polygon_ender(action.annotation_id); + ulabel.destroy_polygon_ender(action.annotation_id!); } /** @@ -392,22 +392,22 @@ function on_annotation_deletion( // Check if it still exists, because if so we need to redraw const current_subtask = ulabel.get_current_subtask(); const annotations = current_subtask.annotations.access; - if (action.annotation_id in annotations) { - const spatial_type = annotations[action.annotation_id]?.spatial_type; - if (NONSPATIAL_MODES.includes(spatial_type)) { + if (action.annotation_id! in annotations) { + const spatial_type = annotations[action.annotation_id!]?.spatial_type; + if (NONSPATIAL_MODES.includes(spatial_type!)) { // Render the change - ulabel.clear_nonspatial_annotation(action.annotation_id); + ulabel.clear_nonspatial_annotation(action.annotation_id!); } else { - ulabel.redraw_annotation(action.annotation_id); + ulabel.redraw_annotation(action.annotation_id!); // Force filter points if necessary - if (annotations[action.annotation_id].spatial_type === "polyline") { - ulabel.update_filter_distance(action.annotation_id, false, true); + if (annotations[action.annotation_id!].spatial_type === "polyline") { + ulabel.update_filter_distance(action.annotation_id!, false, true); } } } // Ensure there are no lingering enders - ulabel.destroy_polygon_ender(action.annotation_id); + ulabel.destroy_polygon_ender(action.annotation_id!); // Update dialogs ulabel.suggest_edits(null, null, true); // Update the toolbox @@ -427,7 +427,7 @@ function on_annotation_id_change( is_undo: boolean = false, ) { // Update the annotation rendering - ulabel.redraw_annotation(action.annotation_id); + ulabel.redraw_annotation(action.annotation_id!); ulabel.recolor_active_polygon_ender(); ulabel.recolor_brush_circle(); @@ -442,7 +442,7 @@ function on_annotation_id_change( // If the filter_distance_toolbox_item exists, // Check if the FilterDistance ToolboxItem is in this ULabel instance if (ulabel.config.toolbox_order.includes(AllowedToolboxItem.FilterDistance)) { - const spatial_type = ulabel.get_current_subtask().annotations.access[action.annotation_id].spatial_type; + const spatial_type = ulabel.get_current_subtask().annotations.access[action.annotation_id!].spatial_type; if (spatial_type === "polyline") { // Get the toolbox item const filter_distance_toolbox_item = ulabel.toolbox.items.find((item) => item.get_toolbox_item_type() === "FilterDistance") as FilterPointDistanceFromRow | undefined; @@ -473,7 +473,7 @@ function on_annotation_revert( is_undo: boolean = false, ) { // Redraw the annotation - ulabel.redraw_annotation(action.annotation_id); + ulabel.redraw_annotation(action.annotation_id!); } // ================= Undo / Redo ================= @@ -497,7 +497,7 @@ export function undo(ulabel: ULabel, is_internal_undo: boolean = false) { ulabel.hide_id_dialog(); } - let undo_candidate = action_stream.pop(); + let undo_candidate = action_stream.pop()!; // Finish action if it is marked as unfinished if (JSON.parse(undo_candidate.redo_payload).finished === false) { @@ -505,7 +505,7 @@ export function undo(ulabel: ULabel, is_internal_undo: boolean = false) { // TODO: better way of doing this? action_stream.push(undo_candidate); finish_action(ulabel, undo_candidate); - undo_candidate = action_stream.pop(); + undo_candidate = action_stream.pop()!; } // Set internal undo status @@ -531,7 +531,7 @@ export function redo(ulabel: ULabel) { if (undone_stack.length === 0) return; // Redo the action - const redo_candidate = undone_stack.pop(); + const redo_candidate = undone_stack.pop()!; redo_action(ulabel, redo_candidate); } @@ -542,13 +542,14 @@ export function redo(ulabel: ULabel) { * @param action Action to undo */ function undo_action(ulabel: ULabel, action: ULabelAction) { - ulabel.update_frame(null, action.frame); + ulabel.update_frame(undefined, action.frame); const undo_payload = JSON.parse(action.undo_payload); const annotations = ulabel.get_current_subtask().annotations.access; + const annotation_id = action.annotation_id!; // For some actions like delete_annotations_in_polygon, the annotation may no longer exist - if (action.annotation_id in annotations) { - const annotation = annotations[action.annotation_id]; + if (annotation_id in annotations) { + const annotation = annotations[annotation_id]; // Revert the annotation's last edited info annotation.last_edited_at = action.prev_timestamp; @@ -557,43 +558,43 @@ function undo_action(ulabel: ULabel, action: ULabelAction) { switch (action.act_type) { case "begin_annotation": - ulabel.begin_annotation__undo(action.annotation_id); + ulabel.begin_annotation__undo(annotation_id); break; case "continue_annotation": - ulabel.continue_annotation__undo(action.annotation_id); + ulabel.continue_annotation__undo(annotation_id); break; case "finish_annotation": - ulabel.finish_annotation__undo(action.annotation_id); + ulabel.finish_annotation__undo(annotation_id); break; case "begin_edit": - ulabel.begin_edit__undo(action.annotation_id, undo_payload); + ulabel.begin_edit__undo(annotation_id, undo_payload); break; case "begin_move": - ulabel.begin_move__undo(action.annotation_id, undo_payload); + ulabel.begin_move__undo(annotation_id, undo_payload); break; case "delete_annotation": - ulabel.delete_annotation__undo(action.annotation_id); + ulabel.delete_annotation__undo(annotation_id); break; case "delete_vertex": - ulabel.delete_vertex__undo(action.annotation_id, undo_payload); + ulabel.delete_vertex__undo(annotation_id, undo_payload); break; case "cancel_annotation": - ulabel.cancel_annotation__undo(action.annotation_id, undo_payload); + ulabel.cancel_annotation__undo(annotation_id, undo_payload); break; case "assign_annotation_id": - ulabel.assign_annotation_id__undo(action.annotation_id, undo_payload); + ulabel.assign_annotation_id__undo(annotation_id, undo_payload); break; case "create_annotation": - ulabel.create_annotation__undo(action.annotation_id); + ulabel.create_annotation__undo(annotation_id); break; case "create_nonspatial_annotation": - ulabel.create_nonspatial_annotation__undo(action.annotation_id); + ulabel.create_nonspatial_annotation__undo(annotation_id); break; case "start_complex_polygon": - ulabel.start_complex_polygon__undo(action.annotation_id); + ulabel.start_complex_polygon__undo(annotation_id); break; case "merge_polygon_complex_layer": - ulabel.merge_polygon_complex_layer__undo(action.annotation_id, undo_payload); + ulabel.merge_polygon_complex_layer__undo(annotation_id, undo_payload); // If the undo was triggered by the user, they // expect ctrl+z to undo the previous action as well if (!action.is_internal_undo) { @@ -601,7 +602,7 @@ function undo_action(ulabel: ULabel, action: ULabelAction) { } break; case "simplify_polygon_complex_layer": - ulabel.simplify_polygon_complex_layer__undo(action.annotation_id, undo_payload); + ulabel.simplify_polygon_complex_layer__undo(annotation_id, undo_payload); // If the undo was triggered by the user, they // expect ctrl+z to undo the previous action as well if (!action.is_internal_undo) { @@ -612,10 +613,10 @@ function undo_action(ulabel: ULabel, action: ULabelAction) { ulabel.delete_annotations_in_polygon__undo(undo_payload); break; case "begin_brush": - ulabel.begin_brush__undo(action.annotation_id, undo_payload); + ulabel.begin_brush__undo(annotation_id, undo_payload); break; case "finish_modify_annotation": - ulabel.finish_modify_annotation__undo(action.annotation_id, undo_payload); + ulabel.finish_modify_annotation__undo(annotation_id, undo_payload); break; default: log_message(`Action type not recognized for undo: ${action.act_type}`, LogLevel.WARNING); @@ -630,58 +631,59 @@ function undo_action(ulabel: ULabel, action: ULabelAction) { * @param action Action to redo */ export function redo_action(ulabel: ULabel, action: ULabelAction) { - ulabel.update_frame(null, action.frame); + ulabel.update_frame(undefined, action.frame); const redo_payload = JSON.parse(action.redo_payload); + const annotation_id = action.annotation_id!; switch (action.act_type) { case "begin_annotation": - ulabel.begin_annotation(null, action.annotation_id, redo_payload); + ulabel.begin_annotation(undefined, annotation_id, redo_payload); break; case "continue_annotation": - ulabel.continue_annotation(null, null, action.annotation_id, redo_payload); + ulabel.continue_annotation(undefined, undefined, annotation_id, redo_payload); break; case "finish_annotation": - ulabel.finish_annotation__redo(action.annotation_id); + ulabel.finish_annotation__redo(annotation_id); break; case "begin_edit": - ulabel.begin_edit__redo(action.annotation_id, redo_payload); + ulabel.begin_edit__redo(annotation_id, redo_payload); break; case "begin_move": - ulabel.begin_move__redo(action.annotation_id, redo_payload); + ulabel.begin_move__redo(annotation_id, redo_payload); break; case "delete_annotation": - ulabel.delete_annotation__redo(action.annotation_id); + ulabel.delete_annotation__redo(annotation_id); break; case "delete_vertex": - ulabel.delete_vertex__redo(action.annotation_id, redo_payload); + ulabel.delete_vertex__redo(annotation_id, redo_payload); break; case "cancel_annotation": - ulabel.cancel_annotation(action.annotation_id); + ulabel.cancel_annotation(annotation_id); break; case "assign_annotation_id": - ulabel.assign_annotation_id(action.annotation_id, redo_payload); + ulabel.assign_annotation_id(annotation_id, redo_payload); break; case "create_annotation": - ulabel.create_annotation__redo(action.annotation_id, redo_payload); + ulabel.create_annotation__redo(annotation_id, redo_payload); break; case "create_nonspatial_annotation": - ulabel.create_nonspatial_annotation(action.annotation_id, redo_payload); + ulabel.create_nonspatial_annotation(annotation_id, redo_payload); break; case "start_complex_polygon": - ulabel.start_complex_polygon(action.annotation_id); + ulabel.start_complex_polygon(annotation_id); break; case "merge_polygon_complex_layer": - ulabel.merge_polygon_complex_layer(action.annotation_id, redo_payload.layer_idx, false, true); + ulabel.merge_polygon_complex_layer(annotation_id, redo_payload.layer_idx, false, true); break; case "simplify_polygon_complex_layer": - ulabel.simplify_polygon_complex_layer(action.annotation_id, redo_payload.active_idx, true); + ulabel.simplify_polygon_complex_layer(annotation_id, redo_payload.active_idx, true); // Since this is an internal operation, user expects redo of the next action ulabel.redo(); break; case "delete_annotations_in_polygon": - ulabel.delete_annotations_in_polygon(action.annotation_id, redo_payload); + ulabel.delete_annotations_in_polygon(annotation_id, redo_payload); break; case "finish_modify_annotation": - ulabel.finish_modify_annotation__redo(action.annotation_id, redo_payload); + ulabel.finish_modify_annotation__redo(annotation_id, redo_payload); break; default: log_message(`Action type not recognized for redo: ${action.act_type}`, LogLevel.WARNING); diff --git a/src/ambient.d.ts b/src/ambient.d.ts new file mode 100644 index 00000000..ac57395e --- /dev/null +++ b/src/ambient.d.ts @@ -0,0 +1,4 @@ +// Ambient module declarations for dependencies that don't ship their own types +// or are only available at build time (not to npm consumers). +declare module "@turf/turf"; +declare module "polygon-clipping"; diff --git a/src/annotation.ts b/src/annotation.ts index b88fa3bf..dd41137f 100644 --- a/src/annotation.ts +++ b/src/annotation.ts @@ -19,7 +19,7 @@ export type PolygonSpatialData = { spatial_payload: [number[]][]; spatial_payload_holes: boolean[]; spatial_payload_child_indices: number[][]; - containing_box: ULabelContainingBox; + containing_box: ULabelContainingBox | null; }; export class ULabelAnnotation { @@ -60,7 +60,7 @@ export class ULabelAnnotation { let remaining_confidence = 1.0; // Filter out any classification payloads items that use the DELETE_CLASS_ID - this.classification_payloads = this.classification_payloads.filter((payload) => { + this.classification_payloads = this.classification_payloads!.filter((payload) => { return payload.class_id !== DELETE_CLASS_ID; }); @@ -158,7 +158,7 @@ export class ULabelAnnotation { try { this.spatial_payload[i] = GeometricUtils.turf_simplify_complex_polygon([layer])[0]; } catch (error) { - log_message(`Error simplifying polygon layer ${i} of id ${this.id}. Removing layer. Error: ${error.message}`, LogLevel.WARNING, true); + log_message(`Error simplifying polygon layer ${i} of id ${this.id}. Removing layer. Error: ${(error as Error).message}`, LogLevel.WARNING, true); indices_to_remove.push(i); } } @@ -213,10 +213,10 @@ export class ULabelAnnotation { */ public is_delete_annotation(): boolean { // Check if the annotation is a delete annotation - return this.classification_payloads[0]["class_id"] === DELETE_CLASS_ID; + return this.classification_payloads![0]["class_id"] === DELETE_CLASS_ID; } - public static from_json(json_block: object): ULabelAnnotation { + public static from_json(json_block: object): ULabelAnnotation | null { const ret = new ULabelAnnotation(); Object.assign(ret, json_block); // Convert deprecated spatial payloads if necessary diff --git a/src/annotation_operators.ts b/src/annotation_operators.ts index f272400c..ab7e7750 100644 --- a/src/annotation_operators.ts +++ b/src/annotation_operators.ts @@ -22,9 +22,9 @@ import { ULabelSubtask } from "./subtask"; */ export function get_annotation_confidence(annotation: ULabelAnnotation) { let current_confidence = -1; - for (const type_of_id in annotation.classification_payloads) { - if (annotation.classification_payloads[type_of_id].confidence > current_confidence) { - current_confidence = annotation.classification_payloads[type_of_id].confidence; + for (const type_of_id in annotation.classification_payloads!) { + if (annotation.classification_payloads![type_of_id].confidence > current_confidence) { + current_confidence = annotation.classification_payloads![type_of_id].confidence; } } return current_confidence; @@ -38,10 +38,10 @@ export function get_annotation_confidence(annotation: ULabelAnnotation) { */ export function get_annotation_class_id(annotation: ULabelAnnotation): string { // Keep track of the most likely class id and its confidence - let id: number, confidence: number; + let id: number = 0, confidence: number; // Go through each item in the classification payload - annotation.classification_payloads.forEach((current_payload) => { + annotation.classification_payloads!.forEach((current_payload) => { // The confidence will be undefined the first time through, so set the id and confidence for a baseline // Otherwise replace the id if the conidence is higher @@ -118,7 +118,8 @@ export function filter_high(annotations: ULabelAnnotation[], property: string, f // Make sure the annotation is not a human deprecated one if (!annotation.deprecated_by["human"]) { // Run the annotation through the filter with the passed in property - const should_deprecate: boolean = value_is_higher_than_filter(annotation[property], filter); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const should_deprecate: boolean = value_is_higher_than_filter((annotation as any)[property], filter); // Mark the point deprecated mark_deprecated(annotation, should_deprecate, deprecated_by_key); @@ -192,13 +193,13 @@ function calculate_distance_from_point_to_line( * @param offset Offset of a particular annotation in the set. Used when an annotation is being moved by the user * @returns The distance from a point to a polyline */ -function get_distance_from_point_to_line(point_annotation: ULabelAnnotation, line_annotation: ULabelAnnotation, offset: Offset = null) { +function get_distance_from_point_to_line(point_annotation: ULabelAnnotation, line_annotation: ULabelAnnotation, offset: Offset | null = null) { // Create constants for the point's x and y value const point_x: number = point_annotation.spatial_payload[0][0]; const point_y: number = point_annotation.spatial_payload[0][1]; // Initialize the distance from the point to the polyline - let distance: number; + let distance: number = Infinity; // Loop through each segment of the polyline for (let idx = 0; idx < line_annotation.spatial_payload.length - 1; idx++) { @@ -230,7 +231,7 @@ function get_distance_from_point_to_line(point_annotation: ULabelAnnotation, lin ); // Check if the distance to this segment is undefined or less than the distance to another segment - if (distance === undefined || distance_to_segment < distance) { + if (distance_to_segment < distance) { distance = distance_to_segment; } } @@ -248,7 +249,7 @@ function get_distance_from_point_to_line(point_annotation: ULabelAnnotation, lin export function assign_closest_line_to_each_point( point_annotations: ULabelAnnotation[], line_annotations: ULabelAnnotation[], - offset: Offset = null, + offset: Offset | null = null, ) { // Loop through every point and assign it a distance from line point_annotations.forEach((current_point) => { @@ -268,7 +269,7 @@ export function assign_closest_line_to_each_point( export function assign_closest_line_to_single_point( point_annotation: ULabelAnnotation, line_annotations: ULabelAnnotation[], - offset: Offset = null, + offset: Offset | null = null, ) { // Create a new distance_from object for the point annotation const distance_from: DistanceFromPolylineClasses = { closest_row: { distance: Infinity } }; @@ -314,7 +315,7 @@ export function update_distance_from_line_to_each_point( line_annotation: ULabelAnnotation, point_annotations: ULabelAnnotation[], all_line_annotations: ULabelAnnotation[], - offset: Offset = null, + offset: Offset | null = null, ) { // Get the class id of the line annotation const line_class_id = get_annotation_class_id(line_annotation); @@ -322,7 +323,7 @@ export function update_distance_from_line_to_each_point( // Loop through each point and update the distance from the line to the point point_annotations.forEach((current_point) => { // Check if the line was the closest line to the point for any class - if (is_closest_line_to_point(line_annotation.id, current_point)) { + if (is_closest_line_to_point(line_annotation.id!, current_point)) { // Recalculate the distance from the point to all lines, since this may no longer be its closest line assign_closest_line_to_single_point(current_point, all_line_annotations, offset); } else { @@ -331,10 +332,10 @@ export function update_distance_from_line_to_each_point( // Check if the line is the closest line of its class to the point if ( - current_point.distance_from[line_class_id] === undefined || - distance < current_point.distance_from[line_class_id].distance + current_point.distance_from![line_class_id] === undefined || + distance < current_point.distance_from![line_class_id].distance ) { - current_point.distance_from[line_class_id] = { + current_point.distance_from![line_class_id] = { distance: distance, polyline_id: line_annotation.id, }; @@ -342,10 +343,10 @@ export function update_distance_from_line_to_each_point( // Check if the line is the closest line to the point if ( - current_point.distance_from.closest_row === undefined || - distance < current_point.distance_from.closest_row.distance + current_point.distance_from!.closest_row === undefined || + distance < current_point.distance_from!.closest_row.distance ) { - current_point.distance_from.closest_row = { + current_point.distance_from!.closest_row = { distance: distance, polyline_id: line_annotation.id, }; @@ -425,7 +426,7 @@ export function get_point_and_line_annotations(ulabel: ULabel): [ULabelAnnotatio * @param offset Offset of a particular annotation. Used when filter is called while an annotation is being moved * @param override Used to filter annotations without calling the dom */ -export function filter_points_distance_from_line(ulabel: ULabel, recalculate_distances: boolean = false, offset: Offset = null, override: FilterDistanceOverride = null) { +export function filter_points_distance_from_line(ulabel: ULabel, recalculate_distances: boolean = false, offset: Offset | null = null, override: FilterDistanceOverride | null = null) { // Get a set of all point and polyline annotations const annotations: [ULabelAnnotation[], ULabelAnnotation[]] = get_point_and_line_annotations(ulabel); const point_annotations: ULabelAnnotation[] = annotations[0]; @@ -435,7 +436,7 @@ export function filter_points_distance_from_line(ulabel: ULabel, recalculate_dis let multi_class_mode: boolean = false; let show_overlay: boolean; let should_redraw: boolean; - let distances: DistanceFromPolylineClasses = { closest_row: undefined }; + let distances: DistanceFromPolylineClasses = { closest_row: { distance: 0 } }; // If the override is null grab the necessary info from the dom if (override === null) { @@ -443,8 +444,8 @@ export function filter_points_distance_from_line(ulabel: ULabel, recalculate_dis let return_early: boolean = false; // Try to grab the elements from the dom - const multi_checkbox: HTMLInputElement = document.querySelector("#filter-slider-distance-multi-checkbox"); - const show_overlay_checkbox: HTMLInputElement = document.querySelector("#filter-slider-distance-toggle-overlay-checkbox"); + const multi_checkbox: HTMLInputElement | null = document.querySelector("#filter-slider-distance-multi-checkbox"); + const show_overlay_checkbox: HTMLInputElement | null = document.querySelector("#filter-slider-distance-toggle-overlay-checkbox"); const sliders: NodeListOf = document.querySelectorAll(".filter-row-distance-slider"); // Check to make sure each element exists before trying to use @@ -463,12 +464,12 @@ export function filter_points_distance_from_line(ulabel: ULabel, recalculate_dis if (multi_checkbox) { multi_class_mode = multi_checkbox.checked; } - show_overlay = show_overlay_checkbox.checked; + show_overlay = show_overlay_checkbox!.checked; // Loop through each slider and populate distances for (let idx = 0; idx < sliders.length; idx++) { // Use a regex to get the string after the final - character in the slider id (Which is the class id or the string "closest_row") - const slider_class_name = /[^-]*$/.exec(sliders[idx].id)[0]; + const slider_class_name = /[^-]*$/.exec(sliders[idx].id)![0]; // Use the class id as a key to store the slider's value distances[slider_class_name] = { distance: sliders[idx].valueAsNumber, @@ -507,13 +508,13 @@ export function filter_points_distance_from_line(ulabel: ULabel, recalculate_dis // If the annotation is smaller than the filter value for any id, it passes if ( - annotation.distance_from[id] !== undefined && - annotation.distance_from[id].distance <= distances[id].distance + annotation.distance_from![id] !== undefined && + annotation.distance_from![id].distance <= distances[id].distance ) { if (annotation.deprecated) { // Undeprecate the annotation mark_deprecated(annotation, false, "distance_from_row"); - annotations_ids_to_redraw_by_subtask[annotation.subtask_key].push(annotation.id); + annotations_ids_to_redraw_by_subtask[annotation.subtask_key!].push(annotation.id!); } break check_distances; } @@ -521,21 +522,21 @@ export function filter_points_distance_from_line(ulabel: ULabel, recalculate_dis // Only here if break not called if (!annotation.deprecated) { mark_deprecated(annotation, true, "distance_from_row"); - annotations_ids_to_redraw_by_subtask[annotation.subtask_key].push(annotation.id); + annotations_ids_to_redraw_by_subtask[annotation.subtask_key!].push(annotation.id!); } } }); } else { // Single-class mode point_annotations.forEach((annotation) => { - const should_deprecate = annotation.distance_from.closest_row.distance > distances.closest_row.distance; + const should_deprecate = annotation.distance_from!.closest_row.distance > distances.closest_row.distance; // Only change deprecated status and redraw if it needs to be changed if (should_deprecate && !annotation.deprecated) { mark_deprecated(annotation, true, "distance_from_row"); - annotations_ids_to_redraw_by_subtask[annotation.subtask_key].push(annotation.id); + annotations_ids_to_redraw_by_subtask[annotation.subtask_key!].push(annotation.id!); } else if (!should_deprecate && annotation.deprecated) { mark_deprecated(annotation, false, "distance_from_row"); - annotations_ids_to_redraw_by_subtask[annotation.subtask_key].push(annotation.id); + annotations_ids_to_redraw_by_subtask[annotation.subtask_key!].push(annotation.id!); } }); } diff --git a/src/blobs.d.ts b/src/blobs.d.ts new file mode 100644 index 00000000..457f2a73 --- /dev/null +++ b/src/blobs.d.ts @@ -0,0 +1,17 @@ +export const BBOX_SVG: string; +export const DELETE_BBOX_SVG: string; +export const BBOX3_SVG: string; +export const POINT_SVG: string; +export const POLYGON_SVG: string; +export const DELETE_POLYGON_SVG: string; +export const CONTOUR_SVG: string; +export const TBAR_SVG: string; +export const POLYLINE_SVG: string; +export const WHOLE_IMAGE_SVG: string; +export const GLOBAL_SVG: string; +export const DEMO_ANNOTATION: object; +export function get_init_style(ulabel_id: string): string; +export const COLORS: string[]; +export const BUTTON_LOADER_HTML: string; +export const FRONT_Z_INDEX: number; +export const BACK_Z_INDEX: number; diff --git a/src/canvas_utils.ts b/src/canvas_utils.ts index f6e56819..0830024c 100644 --- a/src/canvas_utils.ts +++ b/src/canvas_utils.ts @@ -42,7 +42,7 @@ function dynamically_set_n_annos_per_canvas( */ export function initialize_annotation_canvases( ulabel: ULabel, - subtask_key: string = null, + subtask_key: string | null = null, ) { if (subtask_key === null) { dynamically_set_n_annos_per_canvas( @@ -60,10 +60,10 @@ export function initialize_annotation_canvases( 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)) { + if (!NONSPATIAL_MODES.includes(annotation.spatial_type!)) { annotation["canvas_id"] = ulabel.get_init_canvas_context_id( annotation_id, - subtask_key, + subtask_key!, ); } } diff --git a/src/configuration.ts b/src/configuration.ts index d475f914..9f4b00df 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -72,6 +72,9 @@ export const DEFAULT_IMAGE_FILTERS_CONFIG: ImageFiltersConfig = { }; export class Configuration { + // Index signature to allow string indexing for dynamic property access + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [key: string]: any; /** * Dynamically get all keybind configuration property names. * Scans the Configuration class for properties ending in "_keybind" or known keybind properties. @@ -99,7 +102,7 @@ export class Configuration { public container_id: string = "container"; public px_per_px: number = 1; public anno_scaling_mode: AnnoScalingMode = "fixed"; - public initial_crop: InitialCrop = null; + public initial_crop: InitialCrop | null = null; public annbox_id: string = "annbox"; public imwrap_id: string = "imwrap"; public canvas_fid_pfx: string = "front-canvas"; @@ -111,8 +114,8 @@ export class Configuration { public toolbox_id: string = "toolbox"; // Dimensions of various components of the tool - public image_width: number = null; - public image_height: number = null; + public image_width: number | null = null; + public image_height: number | null = null; public demo_width: number = 120; public demo_height: number = 40; public polygon_ender_size: number = 15; @@ -120,7 +123,7 @@ export class Configuration { public brush_size: number = 60; // Configuration for the annotation task itself - public image_data: ImageData = null; + public image_data: ImageData | null = null; public allow_soft_id: boolean = false; public default_annotation_color: string = "#fa9d2a"; public username: string = "ULabelUser"; @@ -132,14 +135,14 @@ export class Configuration { public inner_prop: number = 0.3; // Behavior on special interactions - public instructions_url: string = null; + public instructions_url: string | null = null; public submit_buttons: ULabelSubmitButton[] = []; // Passthrough public task_meta: object = {}; public annotation_meta: object = {}; - public subtasks: object = null; + public subtasks: object | null = null; /** * Map from AllowedToolboxItem enum to the class that implements it. @@ -147,7 +150,8 @@ export class Configuration { * [abstract construct signature](https://www.typescriptlang.org/docs/handbook/2/classes.html#abstract-construct-signatures) * to handle the different constructors for each toolbox item. */ - public toolbox_map = new Map ToolboxItem>([ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public toolbox_map = new Map ToolboxItem>([ [AllowedToolboxItem.ModeSelect, ModeSelectionToolboxItem], [AllowedToolboxItem.ZoomPan, ZoomPanToolboxItem], [AllowedToolboxItem.AnnotationResize, AnnotationResizeItem], @@ -200,7 +204,7 @@ export class Configuration { public delete_vertex_keybind: string = "x"; - public keypoint_slider_default_value: number; + public keypoint_slider_default_value: number = 0; public filter_annotations_on_load: boolean = true; diff --git a/src/drawing_utilities.ts b/src/drawing_utilities.ts index 4cfa4ba8..1c68fd63 100644 --- a/src/drawing_utilities.ts +++ b/src/drawing_utilities.ts @@ -115,6 +115,6 @@ export function get_gradient( * @returns {string} Color hex */ export function color_to_hex(color: string): string { - if (color.toLowerCase() in VALID_HTML_COLORS) return VALID_HTML_COLORS[color.toLowerCase()]; + if (color.toLowerCase() in VALID_HTML_COLORS) return (VALID_HTML_COLORS as Record)[color.toLowerCase()]; return color; } diff --git a/src/geometric_utils.ts b/src/geometric_utils.ts index a16d48ba..b9373846 100644 --- a/src/geometric_utils.ts +++ b/src/geometric_utils.ts @@ -1,3 +1,5 @@ +// eslint-disable-next-line @typescript-eslint/triple-slash-reference -- required for consumer type resolution +/// import * as turf from "@turf/turf"; import * as polygonClipping from "polygon-clipping"; @@ -7,9 +9,9 @@ export type LineSegment2D = [Point2D, Point2D]; export type ULabelSpatialPayload2D = Point2D[]; export type ULabelSpatialPayload3D = Point3D[]; export type PointAccessObject = { - access: string | number; // Access string or number that acts as the index of the point in the original spatial payload - distance: number; - point: Point2D; + access: string | number | null; // Access string or number that acts as the index of the point in the original spatial payload + distance: number | null; + point: Point2D | null; }; export type LineEquation = { a: number; @@ -76,7 +78,7 @@ export class GeometricUtils { // Given two points, return the line that goes through them in the form of // ax + by + c = 0 - public static get_line_equation_through_points(p1: Point2D, p2: Point2D): LineEquation { + public static get_line_equation_through_points(p1: Point2D, p2: Point2D): LineEquation | null { const a: number = (p2[1] - p1[1]); const b: number = (p1[0] - p2[0]); @@ -97,10 +99,10 @@ export class GeometricUtils { public static get_nearest_point_on_segment( ref_x: number, ref_y: number, - eq: LineEquation, + eq: LineEquation | null, kp1: Point2D, kp2: Point2D, - ): { dst: number; prop: number } { + ): { dst: number; prop: number } | null { // Check to make sure eq exists if (eq === null) return null; @@ -144,12 +146,12 @@ export class GeometricUtils { // Check if two line segments are on the same line public static line_segments_are_on_same_line(line1: LineSegment2D, line2: LineSegment2D): boolean { - const eq1: LineEquation = GeometricUtils.get_line_equation_through_points(line1[0], line1[1]); - const eq2: LineEquation = GeometricUtils.get_line_equation_through_points(line2[0], line2[1]); + const eq1: LineEquation | null = GeometricUtils.get_line_equation_through_points(line1[0], line1[1]); + const eq2: LineEquation | null = GeometricUtils.get_line_equation_through_points(line2[0], line2[1]); return ( - (eq1["a"] === eq2["a"]) && - (eq1["b"] === eq2["b"]) && - (eq1["c"] === eq2["c"]) + (eq1!["a"] === eq2!["a"]) && + (eq1!["b"] === eq2!["b"]) && + (eq1!["c"] === eq2!["c"]) ); } @@ -187,7 +189,7 @@ export class GeometricUtils { // Discard the parts of the line that are inside the polygon const remaining_splits = turf.featureCollection( - split.features.filter((feature) => { + split.features.filter((feature: { geometry: { coordinates: Point2D[] } }) => { // If the point at the middle of the lineString is inside the polygon, discard it const middle_pt_idx = Math.floor(feature.geometry.coordinates.length / 2); return !GeometricUtils.point_is_within_simple_polygon( @@ -198,12 +200,14 @@ export class GeometricUtils { ); // Sort the remaining splits by length - remaining_splits.features.sort((a, b) => { + remaining_splits.features.sort((a: unknown, b: unknown) => { return turf.length(b) - turf.length(a); }); - // Return the longest remaining split - // TODO: split into multiple polylines? + // Return the longest remaining split, or empty if all parts were inside the polygon + if (remaining_splits.features.length === 0) { + return []; + } return remaining_splits.features[0].geometry.coordinates; } @@ -212,9 +216,9 @@ export class GeometricUtils { public static merge_polygons_at_intersection( poly1: ULabelSpatialPayload2D, poly2: ULabelSpatialPayload2D, - ): ULabelSpatialPayload2D[] { + ): ULabelSpatialPayload2D[] | null { // Find the intersection, if it exists - const intersection: ULabelSpatialPayload2D = GeometricUtils.get_polygon_intersection_single(poly1, poly2); + const intersection: ULabelSpatialPayload2D | null = GeometricUtils.get_polygon_intersection_single(poly1, poly2); // If there's no intersection, return null if (intersection === null) { return null; @@ -244,7 +248,8 @@ export class GeometricUtils { ).geometry.coordinates; // When the two polygons have no intersection, turf.union returns a quad nested list instead of a triple nested list // So we can just return complex_poly1 - if (ret[0][0][0][0] === undefined) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if ((ret as any)[0][0][0][0] === undefined) { return GeometricUtils.turf_simplify_complex_polygon(ret); } else { return complex_poly1; @@ -255,7 +260,7 @@ export class GeometricUtils { public static subtract_polygons( complex_poly1: ULabelSpatialPayload2D[], complex_poly2: ULabelSpatialPayload2D[], - ): ULabelSpatialPayload2D[] { + ): ULabelSpatialPayload2D[] | null { let ret: ULabelSpatialPayload2D[]; complex_poly1 = GeometricUtils.ensure_valid_turf_complex_polygon(complex_poly1); complex_poly2 = GeometricUtils.ensure_valid_turf_complex_polygon(complex_poly2); @@ -272,7 +277,8 @@ export class GeometricUtils { // When turf.difference creates a fill, it adds it as a new polygon, ie [complex_poly, fill] instead of just complex_poly // so we need to append the fill to the complex_poly and return that when turf returns a quad nested list instead of a triple nested list - if (temp_coords[0][0][0][0] === undefined) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if ((temp_coords as any)[0][0][0][0] === undefined) { ret = temp_coords; } else { // TODO (joshua-dean): See if this casting can be better @@ -327,8 +333,8 @@ export class GeometricUtils { for (let kpi: number = 0; kpi < poly_pts.length - 1; kpi++) { const kp1: Point2D = poly_pts[kpi]; const kp2: Point2D = poly_pts[kpi + 1]; - const eq: { a: number; b: number; c: number } = GeometricUtils.get_line_equation_through_points(kp1, kp2); - const nr: { dst: number; prop: number } = GeometricUtils.get_nearest_point_on_segment(ref_x, ref_y, eq, kp1, kp2); + const eq: { a: number; b: number; c: number } | null = GeometricUtils.get_line_equation_through_points(kp1, kp2); + const nr: { dst: number; prop: number } | null = GeometricUtils.get_nearest_point_on_segment(ref_x, ref_y, eq, kp1, kp2); if ((nr != null) && (nr["dst"] < dstmax) && (ret["distance"] === null || nr["dst"] < ret["distance"])) { ret["access"] = "" + (kpi + nr["prop"]); ret["distance"] = nr["dst"]; @@ -343,7 +349,7 @@ export class GeometricUtils { public static get_polygon_intersection_single( poly1: ULabelSpatialPayload2D, poly2: ULabelSpatialPayload2D, - ): ULabelSpatialPayload2D { + ): ULabelSpatialPayload2D | null { // Convert to turf polygons try { const poly1_turf = turf.polygon([poly1]); @@ -378,7 +384,7 @@ export class GeometricUtils { let ret: boolean = false; if (poly.length > 2) { try { - ret = poly[0][0] === poly.at(-1)[0] && poly[0][1] === poly.at(-1)[1]; + ret = poly[0][0] === poly[poly.length - 1][0] && poly[0][1] === poly[poly.length - 1][1]; } catch { /* empty */ } } return ret; @@ -437,8 +443,8 @@ export class GeometricUtils { // Scale a polygon about a center point, or the centroid if no center is provided public static scale_polygon( poly: ULabelSpatialPayload2D, - scale, - center: Point2D = null, + scale: number, + center: Point2D | null = null, ): ULabelSpatialPayload2D { const ret: ULabelSpatialPayload2D = []; if (center === null) { @@ -523,7 +529,8 @@ export class GeometricUtils { } // Check if a point is within a ulabel complex polygon - public static point_is_within_polygon_annotation(point: Point2D, annotation_object: object): boolean { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public static point_is_within_polygon_annotation(point: Point2D, annotation_object: Record): boolean { // Check if a point is within any of the filled regions (non-holes) for (let i = 0; i < annotation_object["spatial_payload"].length; i++) { if ( diff --git a/src/html_builder.ts b/src/html_builder.ts index 8f223935..660a3595 100644 --- a/src/html_builder.ts +++ b/src/html_builder.ts @@ -42,7 +42,7 @@ export function add_style_to_document(ulabel: ULabel) { * @param subtasks ULabel's Subtasks object * @returns html for a mode button */ -function get_md_button(md_key, md_name, svg_blob, cur_md, subtasks): string { +function get_md_button(md_key: string, md_name: string, svg_blob: string, cur_md: string, subtasks: Record): string { let sel: string = ""; let href: string = ` href="#"`; if (cur_md == md_key) { @@ -72,14 +72,14 @@ function get_images_html(ulabel: ULabel): string { let images_html: string = ""; let display: string; - for (let i = 0; i < ulabel.config["image_data"].frames.length; i++) { + for (let i = 0; i < ulabel.config["image_data"]!.frames.length; i++) { if (i != 0) { display = "none"; } else { display = "block"; } images_html += ` - + `; } return images_html; @@ -138,7 +138,7 @@ function get_frame_annotation_dialogs(ulabel: ULabel): string { return frame_annotation_dialog; } -export function prep_window_html(ulabel: ULabel, toolbox_item_order: unknown[] = null): boolean | void { +export function prep_window_html(ulabel: ULabel, toolbox_item_order: unknown[] | null = null): boolean | void { // Bring image and annotation scaffolding in // TODO multi-image with spacing etc. @@ -149,7 +149,7 @@ export function prep_window_html(ulabel: ULabel, toolbox_item_order: unknown[] = // const toolbox = configuration.create_toolbox(); const toolbox: Toolbox = new Toolbox( [], - Toolbox.create_toolbox(ulabel, toolbox_item_order), + Toolbox.create_toolbox(ulabel, toolbox_item_order!), ); const tool_html: string = toolbox.setup_toolbox_html( @@ -162,7 +162,7 @@ export function prep_window_html(ulabel: ULabel, toolbox_item_order: unknown[] = // Set the container's html to the toolbox html we just created $("#" + ulabel.config["container_id"]).html(tool_html); const container = document.getElementById(ulabel.config["container_id"]); - ULabelLoader.add_loader_div(container); + ULabelLoader.add_loader_div(container!); // Build toolbox for the current subtask only const current_subtask: string = Object.keys(ulabel.subtasks)[0]; @@ -191,7 +191,7 @@ export function prep_window_html(ulabel: ULabel, toolbox_item_order: unknown[] = ulabel.show_annotation_mode(null); // Make sure that entire toolbox is shown - if ($("#" + ulabel.config["toolbox_id"] + " .toolbox_inner_cls").height() > $("#" + ulabel.config["container_id"]).height()) { + if ($("#" + ulabel.config["toolbox_id"] + " .toolbox_inner_cls").height()! > $("#" + ulabel.config["container_id"]).height()!) { $("#" + ulabel.config["toolbox_id"]).css("overflow-y", "scroll"); } @@ -214,16 +214,16 @@ export function prep_window_html(ulabel: ULabel, toolbox_item_order: unknown[] = }; // Check if initial_crop exists and has the appropriate properties - const initial_crop_is_valid = function (initial_crop) { + const initial_crop_is_valid = function (initial_crop: unknown) { // If initial_crop doesn't exist, return false if (initial_crop == null) return false; // If initial_crop has the appropriate properties, return true if ( - "width" in initial_crop && - "height" in initial_crop && - "left" in initial_crop && - "top" in initial_crop + "width" in (initial_crop as object) && + "height" in (initial_crop as object) && + "left" in (initial_crop as object) && + "top" in (initial_crop as object) ) { return true; } @@ -243,11 +243,11 @@ export function prep_window_html(ulabel: ULabel, toolbox_item_order: unknown[] = if (initial_crop_is_valid(ulabel.config.initial_crop)) { // Grab the initial crop button and rename it const initial_crop_button = document.getElementById("recenter-button"); - initial_crop_button.innerHTML = "Initial Crop"; + initial_crop_button!.innerHTML = "Initial Crop"; } else { // Grab the whole image button and set its display to none - const whole_image_button = document.getElementById("recenter-whole-image-button"); - whole_image_button.style.display = "none"; + const whole_image_button = document.getElementById("recenter-whole-image-button") as HTMLElement; + whole_image_button!.style.display = "none"; } } } @@ -401,13 +401,13 @@ export function build_id_dialogs(ulabel: ULabel) { let toolbox_html: string = `
`; const class_ids: number[] = JSON.parse(JSON.stringify(ulabel.subtasks[st]["class_ids"])); // Add the reserved DELETE_CLASS_ID if it is present in the class_defs - if (ulabel.subtasks[st]["class_defs"].at(-1)["id"] === DELETE_CLASS_ID) { + if (ulabel.subtasks[st]["class_defs"][ulabel.subtasks[st]["class_defs"].length - 1]["id"] === DELETE_CLASS_ID) { class_ids.push(DELETE_CLASS_ID); } for (let i = 0; i < class_ids.length; i++) { const this_id: string = class_ids[i].toString(); - const this_color: string = ulabel.color_info[this_id]; + const this_color: string = ulabel.color_info[this_id as unknown as number]; // let this_color: string = ulabel.subtasks[st]["class_defs"][i]["color"]; const this_name: string = ulabel.subtasks[st]["class_defs"][i]["name"]; @@ -572,7 +572,7 @@ export class SliderHandler { slider_event: (slider_val: number | string) => void; class?: string; label_units?: string = ""; - main_label: string; + main_label!: string; min: string = "0"; max: string = "100"; step: string = "1"; @@ -674,8 +674,8 @@ export class SliderHandler { } private updateLabel() { - const slider: HTMLInputElement = document.querySelector(`#${this.id}`); - const label: HTMLLabelElement = document.querySelector(`#${this.id}-value-label`); + const slider: HTMLInputElement = document.querySelector(`#${this.id}`)!; + const label: HTMLLabelElement = document.querySelector(`#${this.id}-value-label`)!; // Set the label as a concatenation of the value and the units label.innerText = slider.value + this.label_units; @@ -687,7 +687,7 @@ export class SliderHandler { */ private incrementSlider() { // Get the slider element - const slider: HTMLInputElement = document.querySelector(`#${this.id}`); + const slider: HTMLInputElement = document.querySelector(`#${this.id}`)!; // Add the step value const new_value = slider.valueAsNumber + this.step_as_number; @@ -708,7 +708,7 @@ export class SliderHandler { */ private decrementSlider() { // Get the slider element - const slider: HTMLInputElement = document.querySelector(`#${this.id}`); + const slider: HTMLInputElement = document.querySelector(`#${this.id}`)!; // Add the step value const new_value = slider.valueAsNumber - this.step_as_number; diff --git a/src/initializer.ts b/src/initializer.ts index a72b8fd2..0422ff2b 100644 --- a/src/initializer.ts +++ b/src/initializer.ts @@ -37,13 +37,13 @@ function make_image_canvases( + height=${ulabel.config["image_height"]! * ulabel.config["px_per_px"]} + width=${ulabel.config["image_width"]! * ulabel.config["px_per_px"]}>
@@ -55,8 +55,8 @@ function make_image_canvases( // Get canvas contexts const canvas_bid = document.getElementById(ulabel.subtasks[st]["canvas_bid"]); const canvas_fid = document.getElementById(ulabel.subtasks[st]["canvas_fid"]); - ulabel.subtasks[st]["state"]["back_context"] = canvas_bid.getContext("2d"); - ulabel.subtasks[st]["state"]["front_context"] = canvas_fid.getContext("2d"); + ulabel.subtasks[st]["state"]["back_context"] = canvas_bid.getContext("2d")!; + ulabel.subtasks[st]["state"]["front_context"] = canvas_fid.getContext("2d")!; } } @@ -77,12 +77,12 @@ function store_original_keybinds(ulabel: ULabel) { ulabel.state["original_config_keybinds"] = original_config_keybinds; // Store original class keybinds in the ULabel state for later reference - const original_class_keybinds: { [class_id: number]: string } = {}; + const original_class_keybinds: { [class_id: number]: string | null } = {}; for (const subtask_key in ulabel.subtasks) { const subtask = ulabel.subtasks[subtask_key]; if (subtask.class_defs) { for (const class_def of subtask.class_defs) { - original_class_keybinds[class_def.id] = class_def?.keybind; + original_class_keybinds[class_def.id] = class_def.keybind; } } } @@ -175,7 +175,7 @@ export async function ulabel_init( const image_width = ulabel.config["image_width"]; for (const subtask of Object.values(ulabel.subtasks) as ULabelSubtask[]) { for (const anno of Object.values(subtask.annotations.access) as ULabelAnnotation[]) { - anno.clamp_annotation_to_image_bounds(image_width, image_height); + anno.clamp_annotation_to_image_bounds(image_width!, image_height!); } } } diff --git a/src/listeners.ts b/src/listeners.ts index bdc42af2..8a6d32cf 100644 --- a/src/listeners.ts +++ b/src/listeners.ts @@ -85,8 +85,8 @@ function handle_keypress_event( // Create the coordinates for the bbox's spatial payload let bbox_top_left: [number, number] = [0, 0]; let bbox_bottom_right: [number, number] = [ - ulabel.config.image_width, - ulabel.config.image_height, + ulabel.config.image_width!, + ulabel.config.image_height!, ]; // If an initial crop exists, use that instead @@ -149,7 +149,7 @@ function handle_keypress_event( if (!DELETE_MODES.includes(current_subtask.state.spatial_type)) { for (let i = 0; i < current_subtask.class_defs.length; i++) { const class_def = current_subtask.class_defs[i]; - if (class_def.keybind !== null && event_matches_keybind(keypress_event, class_def.keybind)) { + if (class_def.keybind !== null && event_matches_keybind(keypress_event, class_def.keybind!)) { const st_key = ulabel.get_current_subtask_key(); const class_button = $(`#tb-id-app--${st_key} a.tbid-opt`).eq(i); if (class_button.hasClass("sel")) { @@ -197,10 +197,10 @@ function handle_soft_id_toolbox_button_click( const current_id_button = $(pfx + " a.tbid-opt.sel"); current_id_button.attr("href", "#"); current_id_button.removeClass("sel"); - const old_id = parseInt(current_id_button.attr("id").split("_").at(-1)); + const old_id = parseInt(current_id_button.attr("id")!.split("_").at(-1)!); tgt_jq.addClass("sel"); tgt_jq.removeAttr("href"); - const idarr = tgt_jq.attr("id").split("_"); + const idarr = tgt_jq.attr("id")!.split("_"); const rawid = parseInt(idarr[idarr.length - 1]); ulabel.set_id_dialog_payload_nopin( current_subtask["class_ids"].indexOf(rawid), @@ -361,6 +361,8 @@ function handle_keydown_event( AnnotationResizeItem.update_annotation_size(ulabel, ulabel.get_current_subtask_key(), INCREMENT_ANNOTATION_SIZE, true); return false; } + + return false; } /** @@ -422,7 +424,8 @@ export function create_ulabel_listeners( // ================= Uncategorized ================= - $(document).on( + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- jQuery overloads don't support namespaced event strings + ($(document) as any).on( "keypress" + ULABEL_NAMESPACE, (keypress_event: JQuery.KeyPressEvent) => { handle_keypress_event(keypress_event, ulabel); @@ -435,7 +438,7 @@ export function create_ulabel_listeners( // Detection ctrl+scroll document.getElementById( ulabel.config["annbox_id"], - ).addEventListener( + )!.addEventListener( "wheel", (wheel_event) => ulabel.handle_wheel(wheel_event), ); @@ -447,7 +450,7 @@ export function create_ulabel_listeners( // Observe the changes on the imwrap_id element dialog_resize_observer.observe( - document.getElementById(ulabel.config["imwrap_id"]), + document.getElementById(ulabel.config["imwrap_id"])!, ); // Store a reference @@ -460,14 +463,15 @@ export function create_ulabel_listeners( // Observe the changes on the ulabel container tb_overflow_resize_observer.observe( - document.getElementById(ulabel.config["container_id"]), + document.getElementById(ulabel.config["container_id"])!, ); // Store a reference ulabel.resize_observers.push(tb_overflow_resize_observer); // create_soft_id_toolbox_button_listener(ulabel); - $(document).on( + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- jQuery overloads don't support namespaced event strings + ($(document) as any).on( "click" + ULABEL_NAMESPACE, `#${ulabel.config["toolbox_id"]} a.tbid-opt`, (click_event: JQuery.ClickEvent) => { @@ -479,7 +483,7 @@ export function create_ulabel_listeners( "click" + ULABEL_NAMESPACE, "a.tb-st-switch[href]", (click_event) => { - const switch_to = $(click_event.target).attr("id").split("--")[1]; + const switch_to = $(click_event.target).attr("id")!.split("--")[1]; // Ignore if in the middle of annotation if (ulabel.get_current_subtask()["state"]["is_in_progress"]) return; @@ -489,7 +493,8 @@ export function create_ulabel_listeners( ); // Keybind to switch active subtask - $(document).on( + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- jQuery overloads don't support namespaced event strings + ($(document) as any).on( "keypress" + ULABEL_NAMESPACE, (keypress_event: JQuery.KeyPressEvent) => { // Ignore if in the middle of annotation @@ -557,8 +562,8 @@ export function create_ulabel_listeners( (click_event) => { // Show idd ulabel.show_id_dialog( - click_event.pageX, - click_event.pageY, + click_event.pageX!, + click_event.pageY!, click_event.target.id.substring("reclf__".length), false, true, @@ -573,7 +578,7 @@ export function create_ulabel_listeners( // Show thumbnail for idd ulabel.suggest_edits( null, - $(mouse_event.currentTarget).attr("id").substring("row__".length), + $(mouse_event.currentTarget).attr("id")!.substring("row__".length), ); }, ); @@ -593,7 +598,8 @@ export function create_ulabel_listeners( }, ); - $(document).on( + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- jQuery overloads don't support namespaced event strings + ($(document) as any).on( "keypress" + ULABEL_NAMESPACE, (keypress_event: JQuery.KeyPressEvent) => { // Check the key pressed against the delete annotation keybind in the config @@ -649,7 +655,7 @@ export function create_ulabel_listeners( "#" + ulabel.config["annbox_id"] + " .delete_suggestion", () => { const crst = ulabel.get_current_subtask(); - ulabel.delete_annotation(crst["state"]["move_candidate"]["annid"]); + ulabel.delete_annotation(crst["state"]["move_candidate"]!["annid"]); }, ); @@ -695,7 +701,8 @@ export function create_ulabel_listeners( ); // Keyboard only events - $(document).on( + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- jQuery overloads don't support namespaced event strings + ($(document) as any).on( "keydown" + ULABEL_NAMESPACE, (keydown_event: JQuery.KeyDownEvent) => { handle_keydown_event(keydown_event, ulabel); diff --git a/src/overlays.ts b/src/overlays.ts index 4a2a81bf..75ebc283 100644 --- a/src/overlays.ts +++ b/src/overlays.ts @@ -1,20 +1,21 @@ import type { AbstractPoint, DistanceFromPolylineClasses, Offset } from ".."; import { ULabelAnnotation } from "./annotation"; import { get_annotation_class_id } from "./annotation_operators"; +import { log_message, LogLevel } from "./error_logging"; import { ULabelSpatialPayload2D } from "./geometric_utils"; /** * Basic class to hold generic methods useful for creating overlays. */ class ULabelOverlay { - canvas: HTMLCanvasElement; - context: CanvasRenderingContext2D; + canvas!: HTMLCanvasElement; + context!: CanvasRenderingContext2D; protected px_per_px: number; // Resolution compared to the constructor(canvas_width: number, canvas_height: number, px_per_px: number) { this.create_canvas(canvas_width, canvas_height); - this.context = this.canvas.getContext("2d"); + this.context = this.canvas.getContext("2d")!; this.px_per_px = px_per_px; @@ -46,7 +47,7 @@ class ULabelOverlay { head.appendChild(style); } - public create_canvas(canvas_width, canvas_height): void { + public create_canvas(canvas_width: number, canvas_height: number): void { // Create the canvas element this.canvas = document.createElement("canvas"); @@ -126,9 +127,9 @@ class ULabelOverlay { export class FilterDistanceOverlay extends ULabelOverlay { private polyline_annotations: ULabelAnnotation[]; // Set of polyline annotations the overlay will be drawn based on - private distances: DistanceFromPolylineClasses = { closest_row: undefined }; // The current distance from a line annotation - private multi_class_mode: boolean; - private display_overlay: boolean; // Whether or not the overlay should currently be displayed + private distances: DistanceFromPolylineClasses = { closest_row: { distance: 0 } }; // The current distance from a line annotation + private multi_class_mode: boolean = false; + private display_overlay: boolean = false; // Whether or not the overlay should currently be displayed constructor(canvas_width: number, canvas_height: number, polyline_annotations: ULabelAnnotation[], px_per_px: number) { super(canvas_width, canvas_height, px_per_px); @@ -148,7 +149,7 @@ export class FilterDistanceOverlay extends ULabelOverlay { * @param endpoint_2 * @returns A normal vector */ - private calculate_normal_vector(endpoint_1: AbstractPoint, endpoint_2: AbstractPoint): AbstractPoint { + private calculate_normal_vector(endpoint_1: AbstractPoint, endpoint_2: AbstractPoint): AbstractPoint | null { // Calculate the x and y of the normal vector let normal_x: number = endpoint_1.y - endpoint_2.y; let normal_y: number = endpoint_2.x - endpoint_1.x; @@ -160,7 +161,7 @@ export class FilterDistanceOverlay extends ULabelOverlay { if (scalar === 0) { // This will happen when point 1 and point 2 are the same point // In which case the concept of a normal vector doesn't really apply - console.error("claculateNormalVector divide by 0 error"); + log_message("calculateNormalVector divide by 0 error", LogLevel.WARNING); return null; } @@ -235,7 +236,7 @@ export class FilterDistanceOverlay extends ULabelOverlay { * * @param offset Used when an annotation is currently being moved. Offset to be added to the annotation with the matching id to the id inside of the Offset object */ - public draw_overlay(offset: Offset = null): void { + public draw_overlay(offset: Offset | null = null): void { // Clear the canvas in order to have a clean slate to re-draw from this.clear_canvas(); @@ -261,7 +262,7 @@ export class FilterDistanceOverlay extends ULabelOverlay { const annotation_class_id: string = get_annotation_class_id(annotation); // Use the class id if in multi-class mode, otherwise use the single class distance - const distance: number = this.multi_class_mode ? this.distances[annotation_class_id].distance : this.distances.closest_row.distance; + const distance: number = this.multi_class_mode ? this.distances[annotation_class_id as unknown as number].distance : this.distances.closest_row.distance; // length - 1 because the final endpoint doesn't have another endpoint to form a pair with for (let idx = 0; idx < spatial_payload.length - 1; idx++) { @@ -284,7 +285,7 @@ export class FilterDistanceOverlay extends ULabelOverlay { } // Get a vector that's perpendicular to endpoint_1 and endpoint_2 and has a magnitude of 1 - const normal_vector: AbstractPoint = this.calculate_normal_vector(endpoint_1, endpoint_2); + const normal_vector: AbstractPoint | null = this.calculate_normal_vector(endpoint_1, endpoint_2); /* In the case the endpoint_1 === endpoint_2 the normal vector will be null In which case draw a circle around one endpoint and skip to the next annotation. */ diff --git a/src/subtask.ts b/src/subtask.ts index 9c7336a4..01c3eca3 100644 --- a/src/subtask.ts +++ b/src/subtask.ts @@ -13,14 +13,16 @@ export type ULabelDialogPosition = { export class ULabelSubtask { public actions: { stream: ULabelAction[]; undone_stack: ULabelAction[] }; public class_ids: number[] = []; - public class_defs: ClassDefinition[]; - public annotations: { + public class_defs!: ClassDefinition[]; + public annotations!: { access: { [key: string]: ULabelAnnotation }; ordering: string[]; }; - public single_class_mode: boolean; - public state: { + public canvas_bid!: string; + public canvas_fid!: string; + public single_class_mode!: boolean; + public state!: { active_id: string; annotation_mode: string; back_context: CanvasRenderingContext2D; @@ -75,7 +77,8 @@ export class ULabelSubtask { }; } - public static from_json(subtask_key: string, subtask_json: object): ULabelSubtask { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public static from_json(subtask_key: string, subtask_json: Record): ULabelSubtask { const ret = new ULabelSubtask( subtask_json["display_name"], subtask_json["classes"], diff --git a/src/toolbox.ts b/src/toolbox.ts index 7551188c..b3b17685 100644 --- a/src/toolbox.ts +++ b/src/toolbox.ts @@ -45,12 +45,12 @@ const toolboxDividerDiv = "
"; /** Chains the replaceAll method and the toLowerCase method. * Optionally concatenates a string at the end of the method. */ -String.prototype.replaceLowerConcat = function (before: string, after: string, concat_string: string = null) { +String.prototype.replaceLowerConcat = function (before: string, after: string, concat_string: string | null = null) { if (typeof (concat_string) === "string") { - return this.replaceAll(before, after).toLowerCase().concat(concat_string); + return this.split(before).join(after).toLowerCase().concat(concat_string); } - return this.replaceAll(before, after).toLowerCase(); + return this.split(before).join(after).toLowerCase(); }; /** @@ -81,7 +81,7 @@ export class Toolbox { const toolbox_instance_list = []; // Go through the items in toolbox_item_order and add their instance to the toolbox instance list for (let i = 0; i < toolbox_item_order.length; i++) { - let args: object, toolbox_key: number; + let args: object | null = null, toolbox_key: number; // If the value of toolbox_item_order[i] is a number then that means the it is one of the // enumerated toolbox items, so set it to the key, otherwise the element must be an array @@ -90,11 +90,11 @@ export class Toolbox { if (typeof (toolbox_item_order[i]) === "number") { toolbox_key = toolbox_item_order[i]; } else { - toolbox_key = toolbox_item_order[i][0]; - args = toolbox_item_order[i][1]; + toolbox_key = (toolbox_item_order[i] as [number, object])[0]; + args = (toolbox_item_order[i] as [number, object])[1]; } - const toolbox_item_class = ulabel.config.toolbox_map.get(toolbox_key); + const toolbox_item_class = ulabel.config.toolbox_map.get(toolbox_key)!; if (args == null) { toolbox_instance_list.push(new toolbox_item_class(ulabel)); @@ -408,7 +408,7 @@ export class ModeSelectionToolboxItem extends ToolboxItem { if (target_jq.hasClass("sel") || current_subtask["state"]["is_in_progress"]) return; // Get the new mode and set it to ulabel's current mode - const new_mode = target_jq.attr("id").split("--")[1]; + const new_mode = target_jq.attr("id")!.split("--")[1]; current_subtask["state"]["annotation_mode"] = new_mode; // Show the BrushToolboxItem when polygon mode is selected @@ -461,7 +461,7 @@ export class ModeSelectionToolboxItem extends ToolboxItem { // Grab the currently selected mode button const selected_mode_button = Array.from(document.getElementsByClassName("md-btn sel"))[0]; // There's only ever going to be one element in this array, so grab the first one - let new_button_index: number; + let new_button_index: number = 0; // Loop through all of the mode select buttons that are currently displayed // to find which one is the currently selected button. Once its found add 1 @@ -579,7 +579,7 @@ export class ModeSelectionToolboxItem extends ToolboxItem { * Toolbox item for resizing all annotations */ export class BrushToolboxItem extends ToolboxItem { - public html: string; + public html!: string; private ulabel: ULabel; /** * CSS class indicating the brush button is active @@ -648,7 +648,7 @@ export class BrushToolboxItem extends ToolboxItem { const button = $(event.currentTarget); // Use the button id to get what size to resize the annotations to - const button_id: string = button.attr("id"); + const button_id: string = button.attr("id") || ""; switch (button_id) { case "brush-mode": @@ -711,7 +711,7 @@ export class BrushToolboxItem extends ToolboxItem { * Toolbox item for zooming and panning. */ export class ZoomPanToolboxItem extends ToolboxItem { - public frame_range: string; + public frame_range!: string; constructor( public ulabel: ULabel, ) { @@ -887,7 +887,7 @@ export class ZoomPanToolboxItem extends ToolboxItem { } private add_event_listeners() { - const frames_exist = this.ulabel.config["image_data"].frames.length > 1; + const frames_exist = this.ulabel.config["image_data"]!.frames.length > 1; $(document).on("click.ulabel", ".ulabel-zoom-button", (event) => { if ($(event.currentTarget).hasClass("ulabel-zoom-out")) { @@ -905,13 +905,13 @@ export class ZoomPanToolboxItem extends ToolboxItem { $(document).on("click.ulabel", ".ulabel-pan", (event) => { const annbox = $("#" + this.ulabel.config.annbox_id); if ($(event.currentTarget).hasClass("ulabel-pan-up")) { - annbox.scrollTop(annbox.scrollTop() - 20); + annbox.scrollTop(annbox.scrollTop()! - 20); } else if ($(event.currentTarget).hasClass("ulabel-pan-down")) { - annbox.scrollTop(annbox.scrollTop() + 20); + annbox.scrollTop(annbox.scrollTop()! + 20); } else if ($(event.currentTarget).hasClass("ulabel-pan-left")) { - annbox.scrollLeft(annbox.scrollLeft() - 20); + annbox.scrollLeft(annbox.scrollLeft()! - 20); } else if ($(event.currentTarget).hasClass("ulabel-pan-right")) { - annbox.scrollLeft(annbox.scrollLeft() + 20); + annbox.scrollLeft(annbox.scrollLeft()! + 20); } }); @@ -934,19 +934,19 @@ export class ZoomPanToolboxItem extends ToolboxItem { const annbox = $("#" + this.ulabel.config.annbox_id); switch (event.key) { case "ArrowLeft": - annbox.scrollLeft(annbox.scrollLeft() - 20); + annbox.scrollLeft(annbox.scrollLeft()! - 20); event.preventDefault(); break; case "ArrowRight": - annbox.scrollLeft(annbox.scrollLeft() + 20); + annbox.scrollLeft(annbox.scrollLeft()! + 20); event.preventDefault(); break; case "ArrowUp": - annbox.scrollTop(annbox.scrollTop() - 20); + annbox.scrollTop(annbox.scrollTop()! - 20); event.preventDefault(); break; case "ArrowDown": - annbox.scrollTop(annbox.scrollTop() + 20); + annbox.scrollTop(annbox.scrollTop()! + 20); event.preventDefault(); break; default: @@ -964,16 +964,16 @@ export class ZoomPanToolboxItem extends ToolboxItem { $(document).on("keypress.ulabel", (e) => { if (e.key == this.ulabel.config.reset_zoom_keybind) { - document.getElementById("recenter-button").click(); + document.getElementById("recenter-button")!.click(); } if (e.key == this.ulabel.config.show_full_image_keybind) { - document.getElementById("recenter-whole-image-button").click(); + document.getElementById("recenter-whole-image-button")!.click(); } }); } - private set_frame_range(ulabel) { - if (ulabel.config["image_data"]["frames"].length == 1) { + private set_frame_range(ulabel: ULabel) { + if (ulabel.config["image_data"]!["frames"].length == 1) { this.frame_range = ``; return; } @@ -983,7 +983,7 @@ export class ZoomPanToolboxItem extends ToolboxItem {
Frame   - +
@@ -1036,7 +1036,7 @@ export class ZoomPanToolboxItem extends ToolboxItem { * Toolbox item for selection Annotation ID. */ export class AnnotationIDToolboxItem extends ToolboxItem { - instructions: string; + instructions!: string; constructor( public ulabel: ULabel, ) { @@ -1111,12 +1111,12 @@ export class AnnotationIDToolboxItem extends ToolboxItem { } export class ClassCounterToolboxItem extends ToolboxItem { - public html: string; + public html!: string; public inner_HTML: string; // TODO (joshua-dean): Find the correct way to handle this - // eslint-disable-next-line @typescript-eslint/no-unused-vars - constructor(...args) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any + constructor(...args: any[]) { super(); this.inner_HTML = `

Annotation Count

`; this.add_styles(); @@ -1158,7 +1158,7 @@ export class ClassCounterToolboxItem extends ToolboxItem { } const class_ids = subtask.class_ids; let i: number, j: number; - const class_counts = {}; + const class_counts: Record = {}; for (i = 0; i < class_ids.length; i++) { class_counts[class_ids[i]] = 0; } @@ -1168,8 +1168,8 @@ export class ClassCounterToolboxItem extends ToolboxItem { for (i = 0; i < annotation_ids.length; i++) { current_annotation = annotations[annotation_ids[i]]; if (current_annotation.deprecated === false) { - for (j = 0; j < current_annotation.classification_payloads.length; j++) { - current_payload = current_annotation.classification_payloads[j]; + for (j = 0; j < current_annotation.classification_payloads!.length; j++) { + current_payload = current_annotation.classification_payloads![j]; if (current_payload.confidence > 0.0) { class_counts[current_payload.class_id] += 1; break; @@ -1217,7 +1217,7 @@ export class ClassCounterToolboxItem extends ToolboxItem { */ export class AnnotationResizeItem extends ToolboxItem { public cached_size: number = 1.5; - public html: string; + public html!: string; private ulabel: ULabel; constructor(ulabel: ULabel) { @@ -1317,7 +1317,7 @@ export class AnnotationResizeItem extends ToolboxItem { const button = $(event.currentTarget); // Use the button id to get what size to resize the annotations to - const button_value = button.attr("id").slice(18); + const button_value = button.attr("id")!.slice(18); let annotation_size: number; let increment: boolean = false; @@ -1339,7 +1339,7 @@ export class AnnotationResizeItem extends ToolboxItem { case ValidResizeValues.VANISH: // Toggle the vanished flag for the current subtask and return AnnotationResizeItem.toggle_subtask_vanished(this.ulabel, current_subtask_key); - break; + return; default: log_message(`Invalid Resize Value: ${button_value}`, LogLevel.ERROR, true); return; @@ -1548,7 +1548,7 @@ export class RecolorActiveItem extends ToolboxItem { private update_color(class_id: number | string, color: string, need_to_save: boolean = true): void { // Update the color_info for annotations appropriately - this.ulabel.color_info[class_id] = color; + (this.ulabel.color_info as Record)[class_id] = color; // Update the color in the AnnotationId button for this class const button_color_square = document.querySelector(`#toolbox_sel_${class_id} > div`); @@ -1670,7 +1670,11 @@ export class RecolorActiveItem extends ToolboxItem { const color: string = event.target.id.slice(13); // Get the currently selected class id - const active_class_id: number = get_active_class_id(this.ulabel); + const active_class_id = get_active_class_id(this.ulabel); + if (active_class_id === undefined) { + log_message("Cannot change color: no active class id found", LogLevel.WARNING); + return; + } // Overwrite the color info with the new color this.update_color(active_class_id, color); @@ -1686,7 +1690,11 @@ export class RecolorActiveItem extends ToolboxItem { const color: string = event.currentTarget.value; // Get the currently selected class id - const active_class_id: number = get_active_class_id(this.ulabel); + const active_class_id = get_active_class_id(this.ulabel); + if (active_class_id === undefined) { + log_message("Cannot change color: no active class id found", LogLevel.WARNING); + return; + } // Update the color for this class this.update_color(active_class_id, color); @@ -1780,7 +1788,7 @@ export class RecolorActiveItem extends ToolboxItem { } export class KeypointSliderItem extends ToolboxItem { - public html: string; + public html!: string; public inner_HTML: string; public name: string; public slider_bar_id: string; @@ -1882,7 +1890,7 @@ export class KeypointSliderItem extends ToolboxItem { * @param redraw whether or not to redraw the annotations after filtering * @returns Annotations that were modified, organized by subtask key */ - private filter_annotations(ulabel: ULabel, filter_value: number = null, redraw: boolean = false): void { + private filter_annotations(ulabel: ULabel, filter_value: number | null = null, redraw: boolean = false): void { if (filter_value === null) { // Use stored filter value if none is passed in filter_value = Math.round(this.filter_value * 100); @@ -1912,7 +1920,7 @@ export class KeypointSliderItem extends ToolboxItem { ) { // Mark this annotation as either deprecated or undeprecated by the confidence filter this.mark_deprecated(annotation, should_deprecate, "confidence_filter"); - annotations_ids_to_redraw_by_subtask[annotation.subtask_key].push(annotation.id); + annotations_ids_to_redraw_by_subtask[annotation.subtask_key!].push(annotation.id!); } } @@ -1933,9 +1941,9 @@ export class KeypointSliderItem extends ToolboxItem { class: "keypoint-slider", default_value: Math.round(this.filter_value * 100).toString(), label_units: "%", - slider_event: (slider_value: number) => { + slider_event: (slider_value: number | string) => { // Filter the annotations, then redraw them - this.filter_annotations(this.ulabel, slider_value, true); + this.filter_annotations(this.ulabel, Number(slider_value), true); }, }); @@ -1968,28 +1976,28 @@ export class KeypointSliderItem extends ToolboxItem { } export class FilterPointDistanceFromRow extends ToolboxItem { - name: string; // Component name shown to users - component_name: string; // Internal component name - default_values: DistanceFromPolylineClasses; // Values sliders are set to on page load - filter_min: number; // Minimum value slider may be set to - filter_max: number; // Maximum value slider may be set to - step_value: number; // Value slider increments by - filter_on_load: boolean; // Whether or not to filter annotations on page load - multi_class_mode: boolean; // Whether or not the component is currently in multi-class mode - disable_multi_class_mode: boolean; // Whether or not to disable the checkbox to enable multi-class mode - show_options: boolean; // Whether or not the options dialog will be visable + name!: string; // Component name shown to users + component_name!: string; // Internal component name + default_values!: DistanceFromPolylineClasses; // Values sliders are set to on page load + filter_min!: number; // Minimum value slider may be set to + filter_max!: number; // Maximum value slider may be set to + step_value!: number; // Value slider increments by + filter_on_load!: boolean; // Whether or not to filter annotations on page load + multi_class_mode!: boolean; // Whether or not the component is currently in multi-class mode + disable_multi_class_mode!: boolean; // Whether or not to disable the checkbox to enable multi-class mode + show_options!: boolean; // Whether or not the options dialog will be visable collapse_options: boolean; // Whether or not the options is in a collapsed state - show_overlay: boolean; // Whether or not the overlay will be shown - toggle_overlay_keybind: string; - filter_during_polyline_move: boolean; // Whether or not to filter annotations during a pending mode/edit of a polyline - overlay: FilterDistanceOverlay; + show_overlay!: boolean; // Whether or not the overlay will be shown + toggle_overlay_keybind!: string; + filter_during_polyline_move!: boolean; // Whether or not to filter annotations during a pending mode/edit of a polyline + overlay!: FilterDistanceOverlay; ulabel: ULabel; // The ULabel object. Must be passed in config: FilterDistanceConfig; // This object's config object // TODO (joshua-dean): Resolve kwargs usage and narrow any // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any - constructor(ulabel: ULabel, kwargs: { [name: string]: any } = null) { + constructor(ulabel: ULabel, kwargs: { [name: string]: any } | null = null) { super(); this.ulabel = ulabel; @@ -2000,13 +2008,15 @@ export class FilterPointDistanceFromRow extends ToolboxItem { // For each key missing from the config, set the default value for (const key in DEFAULT_FILTER_DISTANCE_CONFIG) { if (!Object.prototype.hasOwnProperty.call(this.config, key)) { - this.config[key] = DEFAULT_FILTER_DISTANCE_CONFIG[key]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (this.config as any)[key] = (DEFAULT_FILTER_DISTANCE_CONFIG as any)[key]; } } // Set the component's properties to be the same as the config's properties for (const property in this.config) { - this[property] = this.config[property]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (this as any)[property] = (this.config as any)[property]; } // Force disable multi-class mode if the config doesn't allow it @@ -2161,7 +2171,7 @@ export class FilterPointDistanceFromRow extends ToolboxItem { if (event.key !== this.toggle_overlay_keybind) return; // Grab the show overlay checkbox and click it - const show_overlay_checkbox: HTMLInputElement = document.querySelector("#filter-slider-distance-toggle-overlay-checkbox"); + const show_overlay_checkbox: HTMLInputElement = document.querySelector("#filter-slider-distance-toggle-overlay-checkbox")!; show_overlay_checkbox.click(); }); } @@ -2193,7 +2203,7 @@ export class FilterPointDistanceFromRow extends ToolboxItem { const line_annotations: ULabelAnnotation[] = get_point_and_line_annotations(this.ulabel)[1]; // Initialize an object to hold the distances points are allowed to be from each class as well as any line - const filter_values: DistanceFromPolylineClasses = { closest_row: undefined }; + const filter_values: DistanceFromPolylineClasses = { closest_row: { distance: 0 } }; // Grab all filter-distance-sliders on the page const sliders: NodeListOf = document.querySelectorAll(".filter-row-distance-slider"); @@ -2201,7 +2211,7 @@ export class FilterPointDistanceFromRow extends ToolboxItem { // Loop through each slider and populate filter_values for (let idx = 0; idx < sliders.length; idx++) { // Use a regex to get the string after the final - character in the slider id (Which is the class id or the string "closest_row") - const slider_class_name = /[^-]*$/.exec(sliders[idx].id)[0]; + const slider_class_name = /[^-]*$/.exec(sliders[idx].id)![0]; // Use the class id as a key to store the slider's value filter_values[slider_class_name] = { @@ -2211,8 +2221,8 @@ export class FilterPointDistanceFromRow extends ToolboxItem { // Create and assign an overlay class instance to ulabel to be able to access it this.overlay = new FilterDistanceOverlay( - this.ulabel.config["image_width"] * this.ulabel.config["px_per_px"], - this.ulabel.config["image_height"] * this.ulabel.config["px_per_px"], + this.ulabel.config["image_width"]! * this.ulabel.config["px_per_px"], + this.ulabel.config["image_height"]! * this.ulabel.config["px_per_px"], line_annotations, this.ulabel.config["px_per_px"], ); @@ -2389,7 +2399,7 @@ export class FilterPointDistanceFromRow extends ToolboxItem { if (multi_container !== null) { const sliders = multi_container.querySelectorAll(".filter-row-distance-slider"); for (let idx = 0; idx < sliders.length; idx++) { - const slider_class_name = /[^-]*$/.exec(sliders[idx].id)[0]; + const slider_class_name = /[^-]*$/.exec(sliders[idx].id)![0]; filter_values[slider_class_name] = { distance: sliders[idx].valueAsNumber, }; diff --git a/src/toolbox_items/annotation_list.ts b/src/toolbox_items/annotation_list.ts index 4c1ba4bc..50ab7f7e 100644 --- a/src/toolbox_items/annotation_list.ts +++ b/src/toolbox_items/annotation_list.ts @@ -305,11 +305,11 @@ export class AnnotationListToolboxItem extends ToolboxItem { const annotation = current_subtask.annotations.access[annotation_id]; if (annotation && !annotation.deprecated) { current_subtask.state.edit_candidate = { - annid: annotation.id, - spatial_type: annotation.spatial_type, - access: null, - distance: null, - point: null, + annid: annotation.id!, + spatial_type: annotation.spatial_type!, + access: 0, + distance: 0, + point: [0, 0], }; } } @@ -415,7 +415,7 @@ export class AnnotationListToolboxItem extends ToolboxItem { const class_def = subtask.class_defs.find((def) => def.id === class_id); const class_name = class_def ? class_def.name : "Unknown"; const color = this.ulabel.color_info[class_id] || "#cccccc"; - const svg = this.get_spatial_type_svg(annotation.spatial_type, color); + const svg = this.get_spatial_type_svg(annotation.spatial_type!, color); html += `
@@ -470,7 +470,7 @@ export class AnnotationListToolboxItem extends ToolboxItem { for (let i = 0; i < group_annotations.length; i++) { const annotation = group_annotations[i]; const overall_idx = annotations.indexOf(annotation); - const svg = this.get_spatial_type_svg(annotation.spatial_type, color); + const svg = this.get_spatial_type_svg(annotation.spatial_type!, color); html += `
diff --git a/src/toolbox_items/image_filters.ts b/src/toolbox_items/image_filters.ts index 6c159bdd..f93f3b22 100644 --- a/src/toolbox_items/image_filters.ts +++ b/src/toolbox_items/image_filters.ts @@ -24,11 +24,11 @@ const DEFAULT_FILTER_VALUES: ImageFilterValues = { */ export class ImageFiltersToolboxItem extends ToolboxItem { private filter_values: ImageFilterValues; - private brightness_slider: SliderHandler; - private contrast_slider: SliderHandler; - private hue_rotate_slider: SliderHandler; - private invert_slider: SliderHandler; - private saturate_slider: SliderHandler; + private brightness_slider!: SliderHandler; + private contrast_slider!: SliderHandler; + private hue_rotate_slider!: SliderHandler; + private invert_slider!: SliderHandler; + private saturate_slider!: SliderHandler; private ulabel: ULabel; private is_collapsed: boolean = false; diff --git a/src/toolbox_items/keybinds.ts b/src/toolbox_items/keybinds.ts index d4489e54..bc8a26ba 100644 --- a/src/toolbox_items/keybinds.ts +++ b/src/toolbox_items/keybinds.ts @@ -452,7 +452,7 @@ export class KeybindsToolboxItem extends ToolboxItem { if (class_def.id === DELETE_CLASS_ID) continue; keybinds.push({ - key: class_def.keybind, + key: class_def.keybind!, label: class_def.name, description: `Select class: ${class_def.name}`, configurable: true, @@ -602,7 +602,7 @@ export class KeybindsToolboxItem extends ToolboxItem { /** * Get original class keybinds (before customization) */ - private get_original_class_keybinds(): { [class_id: number]: string } { + private get_original_class_keybinds(): { [class_id: number]: string | null } { // Get from ULabel state (stored during initialization) return this.ulabel.state["original_class_keybinds"] || {}; } @@ -725,7 +725,7 @@ export class KeybindsToolboxItem extends ToolboxItem { `; for (const keybind of configurable) { const has_collision = this.has_collision(keybind.key, all_keybinds); - const is_customized = this.is_keybind_customized(keybind.config_key); + const is_customized = this.is_keybind_customized(keybind.config_key!); const collision_class = has_collision ? " collision" : ""; const customized_class = is_customized ? " customized" : ""; const display_key = keybind.key !== null && keybind.key !== undefined ? keybind.key : "none"; @@ -758,7 +758,7 @@ export class KeybindsToolboxItem extends ToolboxItem { `; for (const keybind of class_keybinds) { const has_collision = this.has_collision(keybind.key, all_keybinds); - const is_customized = this.is_class_keybind_customized(keybind.class_id); + const is_customized = this.is_class_keybind_customized(keybind.class_id!); const collision_class = has_collision ? " collision" : ""; const customized_class = is_customized ? " customized" : ""; const display_key = keybind.key != null ? keybind.key : "none"; @@ -872,7 +872,7 @@ export class KeybindsToolboxItem extends ToolboxItem { /** * Build a keybind chord string from a keyboard event */ - private build_chord_string(keyEvent: JQuery.KeyDownEvent): string { + private build_chord_string(keyEvent: JQuery.KeyDownEvent): string | null { const modifiers: string[] = []; const key = keyEvent.key; @@ -1129,7 +1129,8 @@ export class KeybindsToolboxItem extends ToolboxItem { }; // Attach the keydown handler - $(document).on("keydown.keybind-edit", keyHandler); + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- jQuery overloads don't support namespaced event strings + ($(document) as any).on("keydown.keybind-edit", keyHandler); }); // Click outside to cancel editing @@ -1146,7 +1147,7 @@ export class KeybindsToolboxItem extends ToolboxItem { const current_subtask = this.ulabel.get_current_subtask(); const class_def = current_subtask.class_defs.find((cd) => cd.id === class_id); if (class_def) { - editing_key.text(class_def.keybind); + editing_key.text(class_def.keybind!); } } else { editing_key.text(this.ulabel.config[config_key]); diff --git a/src/toolbox_items/submit_buttons.ts b/src/toolbox_items/submit_buttons.ts index 414cca37..b328b48e 100644 --- a/src/toolbox_items/submit_buttons.ts +++ b/src/toolbox_items/submit_buttons.ts @@ -59,7 +59,7 @@ export class SubmitButtons extends ToolboxItem { button.appendChild(animation); // Create the submit payload - const submit_payload = { + const submit_payload: { task_meta: object | null; annotations: Record } = { task_meta: ulabel.config["task_meta"], annotations: {}, }; @@ -69,7 +69,7 @@ export class SubmitButtons extends ToolboxItem { submit_payload["annotations"][stkey] = []; // Add all of the annotations in that subtask - let annotation: ULabelAnnotation; + let annotation: ULabelAnnotation | null = null; let temp_annotation: ULabelAnnotation | object; for (let i = 0; i < ulabel.subtasks[stkey]["annotations"]["ordering"].length; i++) { temp_annotation = ulabel.subtasks[stkey]["annotations"]["access"][ulabel.subtasks[stkey]["annotations"]["ordering"][i]]; @@ -89,12 +89,12 @@ export class SubmitButtons extends ToolboxItem { } // Skip any delete modes - if (DELETE_MODES.includes(annotation.spatial_type)) { + if (DELETE_MODES.includes(annotation.spatial_type!)) { continue; } // Skip spatial annotations that have an empty spatial payload - if (NONSPATIAL_MODES.includes(annotation.spatial_type) || + if (NONSPATIAL_MODES.includes(annotation.spatial_type!) || annotation.spatial_payload.length === 0) { continue; } @@ -102,8 +102,8 @@ export class SubmitButtons extends ToolboxItem { // Ensure annotation is within the image if required if (!ulabel.config.allow_annotations_outside_image) { annotation.clamp_annotation_to_image_bounds( - ulabel.config["image_width"], - ulabel.config["image_height"], + ulabel.config["image_width"]!, + ulabel.config["image_height"]!, ); } diff --git a/src/utilities.ts b/src/utilities.ts index 5eb34ea4..7388f8ca 100644 --- a/src/utilities.ts +++ b/src/utilities.ts @@ -6,6 +6,7 @@ import { ULabel } from "../src/index"; import { ULabelSubtask } from "./subtask"; import { DELETE_CLASS_ID, DELETE_MODES } from "./annotation"; +import { log_message, LogLevel } from "./error_logging"; /** * Checks if something is an object, not an array, and not null @@ -43,7 +44,7 @@ export function time_function( return replacement_method; } -export function get_active_class_id(ulabel: ULabel): number { +export function get_active_class_id(ulabel: ULabel): number | undefined { // Grab the current subtask from the ulabel object const current_subtask_key: string = ulabel.state.current_subtask; const current_subtask: ULabelSubtask = ulabel.subtasks[current_subtask_key]; @@ -61,12 +62,14 @@ export function get_active_class_id(ulabel: ULabel): number { // If the payload is an object then return its id if its confidence is > 0 if (payload.confidence > 0) { - console.log(`payload: ${payload}`); return payload.class_id; } } - console.error(`get_active_class_id was unable to determine an active class id. - current_subtask: ${JSON.stringify(current_subtask)}`); + log_message( + `get_active_class_id was unable to determine an active class id. current_subtask: ${JSON.stringify(current_subtask)}`, + LogLevel.WARNING, + ); + return undefined; } /** diff --git a/src/version.d.ts b/src/version.d.ts new file mode 100644 index 00000000..741529ac --- /dev/null +++ b/src/version.d.ts @@ -0,0 +1 @@ +export const ULABEL_VERSION: string; diff --git a/src/version.js b/src/version.js index ce72e17b..b27ead70 100644 --- a/src/version.js +++ b/src/version.js @@ -1 +1 @@ -export const ULABEL_VERSION = "0.23.3"; +export const ULABEL_VERSION = "0.23.4"; diff --git a/tests/annotation.test.js b/tests/annotation.test.js index 12599014..3938c472 100644 --- a/tests/annotation.test.js +++ b/tests/annotation.test.js @@ -161,4 +161,36 @@ describe("Annotation Processing", () => { expect(id1.length).toBeGreaterThan(0); }); }); + + describe("Annotation Classification", () => { + test("classification_payloads should determine class_id correctly", () => { + const resume_config = { + ...mock_config, + subtasks: { + test_task: { + ...mock_config.subtasks.test_task, + resume_from: [ + { + spatial_type: "point", + spatial_payload: [[10, 10]], + classification_payloads: [ + { class_id: 1, confidence: 0.9 }, + ], + }, + ], + }, + }, + }; + + const ulabel = new ULabel(resume_config); + const annotation = ulabel.subtasks.test_task.annotations.access[ + ulabel.subtasks.test_task.annotations.ordering[0] + ]; + + // classification_payloads should be preserved exactly + expect(annotation.classification_payloads).toEqual([ + { class_id: 1, confidence: 0.9 }, + ]); + }); + }); }); diff --git a/tests/e2e/api-behavior.spec.js b/tests/e2e/api-behavior.spec.js new file mode 100644 index 00000000..1a65397d --- /dev/null +++ b/tests/e2e/api-behavior.spec.js @@ -0,0 +1,263 @@ +/** + * E2E tests for ULabel API behavior contracts. + * + * Verifies that null-dependent control flow in public methods like + * suggest_edits, fly_to_annotation_id, and show_global_edit_suggestion + * behaves correctly. + */ +import { test, expect } from "./fixtures"; +import { draw_bbox, draw_point, draw_polyline } from "../testing-utils/drawing_utils"; +import { wait_for_ulabel_init } from "../testing-utils/init_utils"; + +test.describe("ULabel API Behavior", () => { + test("suggest_edits with no arguments should not throw", async ({ page }) => { + await wait_for_ulabel_init(page); + + // Create an annotation so there's something to suggest edits for + await draw_bbox(page, [100, 100], [200, 200]); + + // suggest_edits() with no arguments should use null defaults internally + // and fall through to last_move. Should not throw. + const result = await page.evaluate(() => { + try { + window.ulabel.suggest_edits(); + return { success: true }; + } catch (e) { + return { success: false, error: e.message }; + } + }); + + expect(result.success).toBe(true); + }); + + test("suggest_edits with null nonspatial_id should not create bad candidate", async ({ page }) => { + await wait_for_ulabel_init(page); + + await draw_bbox(page, [100, 100], [200, 200]); + + // Passing null for nonspatial_id should NOT create a best_candidate + // (the check is: if (nonspatial_id !== null)) + const result = await page.evaluate(() => { + try { + window.ulabel.suggest_edits(null, null, true); + return { success: true }; + } catch (e) { + return { success: false, error: e.message }; + } + }); + + expect(result.success).toBe(true); + }); + + test("fly_to_annotation_id with null subtask_key should not switch subtasks", async ({ page }) => { + await wait_for_ulabel_init(page); + + // Create an annotation to fly to + await draw_point(page, [150, 150]); + + const result = await page.evaluate(() => { + const annotations = window.ulabel.get_current_subtask().annotations; + const annotation_id = annotations.ordering[0]; + const initial_subtask = window.ulabel.state.current_subtask; + + // null subtask_key should NOT trigger set_subtask + window.ulabel.fly_to_annotation_id(annotation_id, null); + + return { + subtask_unchanged: window.ulabel.state.current_subtask === initial_subtask, + }; + }); + + expect(result.subtask_unchanged).toBe(true); + }); + + test("show_global_edit_suggestion with null nonspatial_id uses spatial path", async ({ page }) => { + await wait_for_ulabel_init(page); + + // Create a spatial annotation + await draw_bbox(page, [100, 100], [200, 200]); + + // With null nonspatial_id, should use the spatial path (containing_box) + // not the nonspatial path (reclf__ DOM element) + const result = await page.evaluate(() => { + const annotations = window.ulabel.get_current_subtask().annotations; + const annotation_id = annotations.ordering[0]; + + try { + window.ulabel.show_global_edit_suggestion(annotation_id, null, null); + return { success: true }; + } catch (e) { + return { success: false, error: e.message }; + } + }); + + expect(result.success).toBe(true); + }); + + test("submit payload preserves task_meta value from config", async ({ page }) => { + await wait_for_ulabel_init(page); + + // Check that task_meta in config is preserved as-is + const task_meta = await page.evaluate(() => { + return window.ulabel.config.task_meta; + }); + + // task_meta should be whatever the demo page configured (likely {} or an object) + // The key assertion: it should NOT be unexpectedly converted + expect(task_meta).not.toBeUndefined(); + }); + + test("class keybinds should be null when not configured", async ({ page }) => { + await wait_for_ulabel_init(page); + + const keybind_values = await page.evaluate(() => { + const subtask = window.ulabel.get_current_subtask(); + return subtask.class_defs.map((cd) => ({ + id: cd.id, + keybind: cd.keybind, + keybind_is_null: cd.keybind === null, + keybind_is_undefined: cd.keybind === undefined, + })); + }); + + // Classes without configured keybinds should have null (not undefined or "") + for (const entry of keybind_values) { + if (entry.keybind === null) { + expect(entry.keybind_is_null).toBe(true); + expect(entry.keybind_is_undefined).toBe(false); + } + } + }); + + test("annotation class_id derived from classification_payloads should be a valid number string", async ({ page }) => { + await wait_for_ulabel_init(page); + + // Create a point annotation + await draw_point(page, [150, 150]); + + const result = await page.evaluate(() => { + const subtask = window.ulabel.get_current_subtask(); + const annotation_id = subtask.annotations.ordering[0]; + const annotation = subtask.annotations.access[annotation_id]; + + return { + has_classification: annotation.classification_payloads != null, + payload_length: annotation.classification_payloads?.length, + first_class_id: annotation.classification_payloads?.[0]?.class_id, + class_id_type: typeof annotation.classification_payloads?.[0]?.class_id, + }; + }); + + // classification_payloads must exist and have at least one entry + expect(result.has_classification).toBe(true); + expect(result.payload_length).toBeGreaterThan(0); + // class_id must be a number (not undefined or "0" from default) + expect(result.class_id_type).toBe("number"); + expect(result.first_class_id).toBeGreaterThan(0); + }); + + test("distance_from property should have numeric distance values", async ({ page }) => { + await wait_for_ulabel_init(page); + + // Create a polyline (acts as the "row") + await draw_polyline(page, [[100, 200], [300, 200], [500, 200]]); + + // Create a point annotation near the polyline + await draw_point(page, [200, 250]); + + // Force the distance filter to recalculate all point-to-line distances + await page.evaluate(() => { + window.ulabel.update_filter_distance(null, false, true); + }); + + const result = await page.evaluate(() => { + const subtask = window.ulabel.get_current_subtask(); + const annotations = subtask.annotations; + + // Find point annotations that should have distance_from assigned + for (const id of annotations.ordering) { + const ann = annotations.access[id]; + if (ann.spatial_type === "point" && ann.distance_from) { + if (!ann.distance_from.closest_row) { + return { valid: false, error: "closest_row missing from distance_from" }; + } + if (typeof ann.distance_from.closest_row.distance !== "number") { + return { valid: false, error: "distance is not a number, got: " + typeof ann.distance_from.closest_row.distance }; + } + if (Number.isNaN(ann.distance_from.closest_row.distance)) { + return { valid: false, error: "distance is NaN" }; + } + // The point is 50px below the line, so distance should be positive + return { valid: true, distance: ann.distance_from.closest_row.distance }; + } + } + return { valid: false, error: "no point annotation with distance_from found" }; + }); + + expect(result.valid).toBe(true); + // The point at y=250 is ~50px from the line at y=200 + expect(result.distance).toBeGreaterThan(0); + }); + + test("show_annotation_mode with null should use default selector", async ({ page }) => { + await wait_for_ulabel_init(page); + + // show_annotation_mode(null) should select the current mode button via jQuery + // and update the .current_mode label without throwing + const result = await page.evaluate(() => { + try { + window.ulabel.show_annotation_mode(null); + const mode_label = document.querySelector(".current_mode"); + return { + success: true, + has_label: mode_label !== null, + label_text: mode_label ? mode_label.innerHTML : null, + }; + } catch (e) { + return { success: false, error: e.message }; + } + }); + + expect(result.success).toBe(true); + expect(result.has_label).toBe(true); + // Label should contain the name of the current mode + expect(result.label_text).toBeTruthy(); + }); + + test("redraw_all_annotations with null offset should not throw", async ({ page }) => { + await wait_for_ulabel_init(page); + + await draw_bbox(page, [100, 100], [200, 200]); + + const result = await page.evaluate(() => { + try { + // First arg is subtask key, second is offset (null = no offset) + const subtask_key = Object.keys(window.ulabel.subtasks)[0]; + window.ulabel.redraw_all_annotations(subtask_key, null, false); + return { success: true }; + } catch (e) { + return { success: false, error: e.message }; + } + }); + + expect(result.success).toBe(true); + }); + + test("redraw_all_annotations with null subtask should redraw all subtasks", async ({ page }) => { + await wait_for_ulabel_init(page); + + await draw_bbox(page, [100, 100], [200, 200]); + + const result = await page.evaluate(() => { + try { + // null subtask means "redraw all subtasks" + window.ulabel.redraw_all_annotations(null, null, false); + return { success: true }; + } catch (e) { + return { success: false, error: e.message }; + } + }); + + expect(result.success).toBe(true); + }); +}); diff --git a/tests/geometric_utils.test.js b/tests/geometric_utils.test.js new file mode 100644 index 00000000..c12eaf18 --- /dev/null +++ b/tests/geometric_utils.test.js @@ -0,0 +1,47 @@ +// Tests for geometric utility functions +const { GeometricUtils } = require("../build/geometric_utils"); + +describe("GeometricUtils", () => { + describe("subtract_simple_polygon_from_polyline", () => { + test("should return empty array when polyline is entirely inside polygon", () => { + // A polyline completely inside a large polygon + const polyline = [[2, 2], [3, 3], [4, 4]]; + const polygon = [[0, 0], [10, 0], [10, 10], [0, 10], [0, 0]]; + + const result = GeometricUtils.subtract_simple_polygon_from_polyline(polyline, polygon); + expect(result).toEqual([]); + }); + + test("should return the polyline unchanged when entirely outside polygon", () => { + // A polyline completely outside the polygon + const polyline = [[20, 20], [30, 30], [40, 40]]; + const polygon = [[0, 0], [10, 0], [10, 10], [0, 10], [0, 0]]; + + const result = GeometricUtils.subtract_simple_polygon_from_polyline(polyline, polygon); + expect(result).toEqual(polyline); + }); + + test("should return partial polyline when it crosses through the polygon", () => { + // A polyline that crosses through the polygon + const polyline = [[-5, 5], [5, 5], [15, 5]]; + const polygon = [[0, 0], [10, 0], [10, 10], [0, 10], [0, 0]]; + + const result = GeometricUtils.subtract_simple_polygon_from_polyline(polyline, polygon); + // Should return a portion of the line (the longest part outside the polygon) + expect(result.length).toBeGreaterThan(0); + }); + + test("should not throw when all split segments are inside the polygon", () => { + // A polyline that enters and exits but the remaining parts after split + // are all inside — edge case that previously caused a crash + const polyline = [[1, 5], [5, 5], [9, 5]]; + const polygon = [[0, 0], [10, 0], [10, 10], [0, 10], [0, 0]]; + + // Should not throw, should return empty array + expect(() => { + const result = GeometricUtils.subtract_simple_polygon_from_polyline(polyline, polygon); + expect(result).toEqual([]); + }).not.toThrow(); + }); + }); +}); diff --git a/tests/types/index.test-d.ts b/tests/types/index.test-d.ts new file mode 100644 index 00000000..048e8127 --- /dev/null +++ b/tests/types/index.test-d.ts @@ -0,0 +1,89 @@ +/** + * Type-level tests for the ulabel package. + * This file is compiled (but not executed) against the packed npm tarball + * to verify that type declarations are correct and accessible to consumers. + */ + +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import ULabel, { + AllowedToolboxItem, + ULabelAnnotation, + ULabelSubtask, + Configuration, + Toolbox, + ULabelConstructorArgs, + ULabelSubmitButton, + ULabelSubtasks, +} from "ulabel"; + +// === Static methods return correct types === + +// get_allowed_toolbox_item_enum returns the enum object, not a single value +const toolbox_enum = ULabel.get_allowed_toolbox_item_enum(); +const mode_select: AllowedToolboxItem = toolbox_enum.ModeSelect; +const zoom_pan: AllowedToolboxItem = toolbox_enum.ZoomPan; +const brush: AllowedToolboxItem = toolbox_enum.Brush; +const keybinds: AllowedToolboxItem = toolbox_enum.Keybinds; + +// get_resize_toolbox_item returns the class (constructor), not an instance +const ResizeItem = ULabel.get_resize_toolbox_item(); +const instance = new ResizeItem({} as any); + +// version returns a string +const ver: string = ULabel.version(); + +// === Constructor accepts kwargs object === + +const submit_button: ULabelSubmitButton = { + name: "Submit", + hook: (data) => {}, +}; + +const kwargs: ULabelConstructorArgs = { + container_id: "container", + image_data: "image.png", + username: "TestUser", + submit_buttons: [submit_button], + subtasks: {} as ULabelSubtasks, +}; + +// This should compile without error +declare const ulabel: ULabel; + +// === Instance methods have correct parameter types === + +// get_annotations takes a string (subtask key), not a ULabelSubtask object +const annotations: ULabelAnnotation[] = ulabel.get_annotations("my_subtask"); + +// set_annotations takes a string (subtask key), not a ULabelSubtask object +ulabel.set_annotations([], "my_subtask"); + +// instance version() +const instance_ver: string = ulabel.version(); + +// === Exported types are accessible === + +const annotation: ULabelAnnotation = {} as ULabelAnnotation; +const subtask: ULabelSubtask = {} as ULabelSubtask; +const config: Configuration = {} as Configuration; +const toolbox: Toolbox = {} as Toolbox; + +// === AllowedToolboxItem enum members are accessible === + +const all_items: AllowedToolboxItem[] = [ + AllowedToolboxItem.ModeSelect, + AllowedToolboxItem.ZoomPan, + AllowedToolboxItem.AnnotationResize, + AllowedToolboxItem.AnnotationID, + AllowedToolboxItem.RecolorActive, + AllowedToolboxItem.ClassCounter, + AllowedToolboxItem.KeypointSlider, + AllowedToolboxItem.SubmitButtons, + AllowedToolboxItem.FilterDistance, + AllowedToolboxItem.Brush, + AllowedToolboxItem.ImageFilters, + AllowedToolboxItem.AnnotationList, + AllowedToolboxItem.Keybinds, +]; diff --git a/tests/types/tsconfig.json b/tests/types/tsconfig.json new file mode 100644 index 00000000..91aa1874 --- /dev/null +++ b/tests/types/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "es2020", + "module": "commonjs", + "noEmit": true, + "esModuleInterop": true, + "skipLibCheck": true, + "types": ["jquery"] + }, + "files": ["index.test-d.ts"] +} diff --git a/tests/ulabel.test.js b/tests/ulabel.test.js index bd290c18..54b3e140 100644 --- a/tests/ulabel.test.js +++ b/tests/ulabel.test.js @@ -142,4 +142,94 @@ describe("ULabel Core Functionality", () => { expect(typeof new_id).toBe("number"); }); }); + + describe("Class Keybind Storage", () => { + test("class_def.keybind should be null when not provided", () => { + const ulabel = new ULabel(mock_config); + const class_defs = ulabel.subtasks.test_task.class_defs; + + // Class without keybind should have null, not undefined or "" + expect(class_defs[0].keybind).toBe(null); + }); + + test("class_def.keybind should preserve provided value", () => { + const config = { + ...mock_config, + subtasks: { + test_task: { + display_name: "Test Task", + classes: [ + { name: "Class1", id: 1, color: "red", keybind: "1" }, + { name: "Class2", id: 2, color: "blue", keybind: "2" }, + ], + allowed_modes: ["bbox", "polygon", "point"], + }, + }, + }; + const ulabel = new ULabel(config); + const class_defs = ulabel.subtasks.test_task.class_defs; + + expect(class_defs[0].keybind).toBe("1"); + expect(class_defs[1].keybind).toBe("2"); + }); + }); + + describe("Configuration Defaults", () => { + test("task_meta should default to empty object when not configured", () => { + const ulabel = new ULabel(mock_config); + expect(ulabel.config.task_meta).toEqual({}); + }); + + test("task_meta should preserve configured value", () => { + const config_with_meta = { + ...mock_config, + task_meta: { project: "test" }, + }; + const ulabel = new ULabel(config_with_meta); + expect(ulabel.config.task_meta).toEqual({ project: "test" }); + }); + + test("task_meta null should be preserved when explicitly set", () => { + const config_with_null_meta = { + ...mock_config, + task_meta: null, + }; + const ulabel = new ULabel(config_with_null_meta); + expect(ulabel.config.task_meta).toBeNull(); + }); + }); + + describe("String.prototype.replaceLowerConcat", () => { + // This method was changed from replaceAll to split/join + // Ensure behavior is identical for the patterns used in the codebase + beforeAll(() => { + // Loading ULabel attaches replaceLowerConcat to String.prototype + require("./testing-utils/build_loader"); + }); + + test("should replace spaces with dashes and lowercase", () => { + const result = "Keypoint Slider".replaceLowerConcat(" ", "-"); + expect(result).toBe("keypoint-slider"); + }); + + test("should replace spaces with underscores and concat suffix", () => { + const result = "Keypoint Slider".replaceLowerConcat(" ", "_", "_default_value"); + expect(result).toBe("keypoint_slider_default_value"); + }); + + test("should handle strings with no match for before", () => { + const result = "noSpaces".replaceLowerConcat(" ", "-"); + expect(result).toBe("nospaces"); + }); + + test("should handle multiple spaces", () => { + const result = "Filter Point Distance".replaceLowerConcat(" ", "-"); + expect(result).toBe("filter-point-distance"); + }); + + test("should return lowercase without concat when concat_string is null", () => { + const result = "Test String".replaceLowerConcat(" ", "-", null); + expect(result).toBe("test-string"); + }); + }); }); diff --git a/tsconfig.json b/tsconfig.json index 49984078..26cc96a8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "strict": true, "target": "es5", "module": "commonjs", "sourceMap": false, @@ -9,4 +10,7 @@ ], "outDir": "./build", }, + "exclude": [ + "tests/types", + ], } \ No newline at end of file