From b0eb8093ff0eab9a909074a819a3a6207a6348ea Mon Sep 17 00:00:00 2001 From: Csanad Tabajdi <cs.tabajdi@cytocast.com> Date: Fri, 15 Nov 2024 22:10:02 +0100 Subject: [PATCH] init --- .gitignore | 131 + .gitlab-ci.yml | 31 + Gruntfile.js | 51 + LICENSE | 21 + Makefile | 6 + README.md | 1238 ++++++- package-lock.json | 2163 ++++++++++++ package.json | 19 + src/app.html | 1212 +++++++ src/css/main.css | 247 ++ src/js/buffer.js | 509 +++ src/js/draw.js | 3722 +++++++++++++++++++++ src/js/file_manager.js | 331 ++ src/js/file_metadata.js | 116 + src/js/global_variables.js | 272 ++ src/js/img_manipulation.js | 106 + src/js/message.js | 65 + src/js/modal.js | 44 + src/js/project.js | 608 ++++ src/js/science_plugins.js | 441 +++ src/js/servercom.js | 479 +++ src/js/settings.js | 616 ++++ src/js/sidebar.js | 2114 ++++++++++++ src/js/ui_handler.js | 304 ++ src/js/undo_redo.js | 362 ++ src/js/via.js | 6430 ++++++++++++++++++++++++++++++++++++ src/js/zoom.js | 134 + src/style/main.css | 247 ++ 28 files changed, 21962 insertions(+), 57 deletions(-) create mode 100644 .gitignore create mode 100644 .gitlab-ci.yml create mode 100644 Gruntfile.js create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/app.html create mode 100644 src/css/main.css create mode 100644 src/js/buffer.js create mode 100644 src/js/draw.js create mode 100644 src/js/file_manager.js create mode 100644 src/js/file_metadata.js create mode 100644 src/js/global_variables.js create mode 100644 src/js/img_manipulation.js create mode 100644 src/js/message.js create mode 100644 src/js/modal.js create mode 100644 src/js/project.js create mode 100644 src/js/science_plugins.js create mode 100644 src/js/servercom.js create mode 100644 src/js/settings.js create mode 100644 src/js/sidebar.js create mode 100644 src/js/ui_handler.js create mode 100644 src/js/undo_redo.js create mode 100644 src/js/via.js create mode 100644 src/js/zoom.js create mode 100644 src/style/main.css diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..364cc39 --- /dev/null +++ b/.gitignore @@ -0,0 +1,131 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..a8ed05e --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,31 @@ +stages: + - build + - test + +variables: + NODE_ENV: production + +default: + image: node:22 + +before_script: + - npm install + +build: + stage: build + script: + - make init + - make build + artifacts: + paths: + - dist/ + expire_in: 1 day + +verify_build: + stage: test + script: + - test -f dist/app.html || (echo "dist/app.htm does not exist!" && exit 1) + - test -f dist/assets/js/main.js || (echo "dist/assets/js/main.js does not exist!" && exit 1) + - test -f dist/assets/js/main.min.js || (echo "dist/assets/js/main.min.js does not exist!" && exit 1) + - test -f dist/assets/css/main.min.css || (echo "dist/assets/css/main.min.css does not exist!" && exit 1) + - echo "All required files exist after the build process." diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 0000000..ee02630 --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,51 @@ +module.exports = function (grunt) { + grunt.initConfig({ + pkg: grunt.file.readJSON('package.json'), + concat: { + options: { + separator: ';', + }, + dist: { + src: ['src/js/global_variables.js', 'src/js/message.js', 'src/js/settings.js', 'src/js/project.js', 'src/js/sidebar.js', 'src/js/draw.js', 'src/js/modal.js', 'src/js/file_manager.js', 'src/js/science_plugins.js', 'src/js/img_manipulation.js', 'src/js/zoom.js', 'src/js/file_metadata.js', 'src/js/buffer.js', 'src/js/via.js', 'src/js/servercom.js', 'src/js/undo_redo.js', 'src/js/ui_handler.js'], + dest: 'dist/assets/js/main.js', + }, + }, + uglify: { + dist: { + files: { + 'dist/assets/js/main.min.js': ['dist/assets/js/main.js'], + }, + }, + }, + cssmin: { + target: { + files: [{ + expand: true, + cwd: 'src/css', + src: ['*.css', '!*.min.css'], + dest: 'dist/assets/css', + ext: '.min.css' + }] + } + }, + htmlmin: { + dist: { + options: { + removeComments: true, + collapseWhitespace: true + }, + files: { + 'dist/app.html': 'src/app.html', + } + }, + } + }); + + grunt.loadNpmTasks('grunt-contrib-concat'); + grunt.loadNpmTasks('grunt-contrib-uglify'); + grunt.loadNpmTasks('grunt-contrib-cssmin'); + grunt.loadNpmTasks('grunt-contrib-htmlmin'); + + + grunt.registerTask('default', ['concat', 'uglify', 'cssmin', 'htmlmin']); +}; diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..79f17a4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Csana111 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..4e68358 --- /dev/null +++ b/Makefile @@ -0,0 +1,6 @@ + +init: + npm install + +build: + npm run build \ No newline at end of file diff --git a/README.md b/README.md index 92733a6..011163f 100644 --- a/README.md +++ b/README.md @@ -1,93 +1,1217 @@ -# InfarctSize-AI-frontend +Code Documentation for InfarctSize-AI frontend V1.0 +============================================== +Author: [Csanád Tabajdi](mailto:tabajdi.csanad@gmail.com), Version: November 2024 +> This code documentation is based on VIA-2.0.x version. While there are some +> major differences between VIA-2.0.x and InfarctSize-frontend codebase, this code +> documentation is still useful to understand the basic architecture and working +> of the InfarctSize-frontend. -## Getting started +InfarctSize-frontend is a web-based tool that can be used to annotate images. It is built on top of the VIA image annotation tool. InfarctSize-frontend is designed to be used by domain experts who are not familiar with programming. -To make it easy for you to get started with GitLab, here's a list of recommended next steps. +There are 14 main components in InfarctSize-frontend: +- [Sidebar](#sidebar) +- [Drawer](#drawer) +- [Settings](#settings) +- [Image Manipulation](#image-manipulation) +- [Message](#message) +- [Modal](#modal) +- [Science plugins](#science-plugins) +- [Auto Annotate](#auto-annotate) +- [Undo Redo](#undo-redo) +- [Image Buffer](#image-buffer) +- [File Manager](#file-manager) +- [File Metadata](#file-metadata) +- [Project](#project) +- [UI handler](#ui-handler) -Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)! -## Add your files +Sidebar +------- -- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files -- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command: +## Overview Sidebar Class +The `sidebar.js` file is a crucial part of the InfarctSize-AI frontend application. It contains the implementation of the `Sidebar` class, which manages all functionalities related to the sidebar within the application. The sidebar includes components for project management, image file lists, and annotation editing, providing users with an interface to interact with images and their associated metadata. + +## Key Features + +### Image File List Management + +- **Display and Interaction**: Shows a list of loaded image files (`img_fn_list`) and allows users to interact with them. +- **Filtering**: + - **Regular Expression Filtering**: Users can filter the image list using custom regular expressions (`img_fn_list_regex`). + - **Preset Filters**: Offers predefined filters to quickly sort images based on criteria like files without regions, files missing annotations, etc. +- **Context Menu**: Implements a context menu for file management actions such as deleting images from the project. +- **Scrolling and Highlighting**: Automatically scrolls to and highlights the currently selected image in the list. + +### Annotation Editor Panel + +- **Display Modes**: + - **Single Region Mode**: Shows metadata for a single selected region, typically displayed near the region on the image. + - **All Regions Mode**: Displays metadata for all regions in a separate panel, allowing bulk editing. +- **Metadata Management**: + - **Region and File Metadata**: Handles both region-level and file-level metadata, supporting various attribute types (text, checkbox, radio, dropdown, image). + - **Dynamic Content**: Updates the annotation editor content based on user interactions and changes in the image or region selection. +- **Context Menu**: Provides quick actions on regions through a context menu, such as deleting regions or editing specific attributes. +- **Attribute Editing**: Allows users to edit attributes directly within the annotation editor, with support for various input types. + +### Event Handling + +- **User Interaction**: Listens for and handles events such as clicks, key presses, and context menu actions within the sidebar and annotation editor. +- **Metadata Updates**: Responds to changes in metadata fields and updates the application's data structures accordingly. +- **Focus Management**: Manages focus events to highlight selected regions or files when interacting with the annotation editor. + +### Mode Toggling + +- **Annotation Editor Modes**: Allows toggling between editing region metadata and file metadata. +- **On-Image Editor Toggle**: Enables or disables the on-image annotation editor, switching between displaying annotations on the image or in a separate panel. + +## Usage + +The `Sidebar` class is instantiated once and manages the sidebar's state and behavior throughout the application's lifecycle. + +### Initialization + +```javascript +const sidebar = new Sidebar(); ``` -cd existing_repo -git remote add origin https://dev.itk.ppke.hu/infarctsize-ai/infarctsize-ai-frontend.git -git branch -M main -git push -uf origin main + +Upon initialization, the `Sidebar` class: + +- Selects and stores references to key DOM elements used in the sidebar. +- Sets up event listeners for user interactions. +- Initializes the image file list and annotation editor panel. + +### Key Methods + +- **Image List Management**: + - `init_img_fn_list()`: Initializes the image filename list with the current images. + - `update_img_fn_list()`: Updates the image list, applying any filters or reflecting changes in the loaded images. + - `img_fn_list_onregex()`: Filters the image list based on the regular expression entered by the user. + - `img_fn_list_onpresetfilter_select()`: Applies a preset filter selected by the user to the image list. +- **Context Menu Handling**: + - `file_manager_contextmenu(event)`: Displays a context menu for file management actions when the user right-clicks on an image in the list. + - `annotation_editor_contextmenu(event)`: Shows a context menu with quick annotation options when the user right-clicks on the display area. +- **Annotation Editor Control**: + - `annotation_editor_show()`: Displays the annotation editor panel based on the current mode and selection. + - `annotation_editor_hide()`: Hides the annotation editor panel. + - `annotation_editor_update_content()`: Updates the content of the annotation editor to reflect the current metadata. + - `annotation_editor_toggle_on_image_editor()`: Toggles the visibility of the on-image annotation editor. +- **Metadata Handling**: + - `annotation_editor_on_metadata_update(element)`: Handles updates to metadata fields when the user changes values in the annotation editor. + - `annotation_editor_update_file_metadata()`: Updates file-level metadata based on user input. + - `annotation_editor_update_region_metadata()`: Updates region-level metadata based on user input. + - `set_region_annotations_to_default_value(region_id)`: Resets region annotations to their default values. + - `set_file_annotations_to_default_value(image_id)`: Resets file annotations to their default values. +- **Mode Toggling**: + - `toggleAEMode(disable = false)`: Toggles between region metadata and file metadata editing modes in the annotation editor. + +## Event Listeners + +The `Sidebar` class sets up various event listeners to handle user interactions: + +- **Image File List**: + - Click events to select and navigate to images. + - Context menu events for file management. +- **Annotation Editor**: + - Focus and change events on metadata fields to update annotations. + - Context menu events for quick annotation actions. +- **Display Area**: + - Context menu events to toggle sorting order or display quick annotator options. + +## Notes + +- **Dependency on jQuery**: The class uses jQuery extensively for DOM manipulation and event handling. Ensure that jQuery is included in the project. +- **Interaction with Other Components**: The `Sidebar` class interacts with other parts of the application, such as the drawing module (`drawing`), project management (`project`), and settings (`settings`). +- **Customization**: Attribute types and options are defined in the `project.attributes` object, allowing for customization of metadata fields. +- **Error Handling**: The class includes error handling for situations such as invalid metadata updates or missing attributes. + + +Drawer +------ +## Drawer Class Overview + +The `Drawer` class is a core component responsible for handling drawing operations on the canvas. It manages user interactions, shape drawing, region selection, and various canvas-related functionalities within the application. + +### Key Features: + +- **Initialization**: Sets up the drawing context (`this.ctx`), initializes mouse handlers for user interactions, and defines default settings for drawing parameters like colors, line widths, and tolerances. + +- **Shape Drawing**: Supports drawing and manipulating multiple shapes, including rectangles, circles, ellipses, polygons, polylines, and points. + +- **Mouse Event Handling**: Handles events such as `pointerdown`, `pointerup`, `pointermove`, and `pointerover` to facilitate interactive drawing and editing of regions on the canvas. + +- **Region Selection and Manipulation**: + - **Selection**: Allows users to select regions for editing. + - **Movement**: Enables moving selected regions across the canvas. + - **Resizing**: Provides functionality to resize regions by dragging their edges or corners. + - **Trimming**: Implements a trimming tool to modify polygon regions. + +- **Collision Detection**: Contains methods to detect cursor position relative to regions (inside, on edge, or on corner), enhancing precision in user interactions. + +- **Rendering**: Renders all regions on the canvas, including their boundaries, control points, and IDs if enabled. + +- **Utility Functions**: + - **Browser Detection**: Determines the user's browser to handle browser-specific features. + - **Canvas Scaling**: Adjusts the canvas scale for zooming functionalities. + - **Math Helpers**: Provides mathematical functions for calculations related to shapes and regions. + +### Usage Example: + +To utilize the `Drawer` class, instantiate it and integrate it with the canvas element: + +```javascript +// Assuming a canvas element with the ID 'region_canvas' +const drawing = new Drawer(); + +// Set the current shape to draw (e.g., rectangle) +drawing.setCurrentShape(VIA_REGION_SHAPE.RECT); ``` -## Integrate with your tools +### Event Handling: -- [ ] [Set up project integrations](https://dev.itk.ppke.hu/infarctsize-ai/infarctsize-ai-frontend/-/settings/integrations) +The class listens to various pointer events to manage drawing and editing: -## Collaborate with your team +- **`canvasMouseDownHandler`**: Initiates drawing or region manipulation based on the cursor position and current tool. +- **`canvasMouseUpHandler`**: Finalizes drawing or manipulation actions. +- **`canvasMouseMoveHandler`**: Updates the canvas in real-time as the cursor moves. +- **`canvasMouseOverHandler`**: Changes the cursor style when hovering over interactive elements. -- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/) -- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html) -- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically) -- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/) -- [ ] [Set auto-merge](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html) +### Customization: -## Test and Deploy +Developers can customize the drawing behavior and appearance: -Use the built-in continuous integration in GitLab. +- **Set Shape**: `setCurrentShape(shape)` to change the current drawing tool. +- **Set Colors and Styles**: Modify properties like `theme_control_point_color` and `region_boundary_line_color`. +- **Adjust Tolerances**: Fine-tune interaction sensitivity with properties like `region_edge_tol`. -- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html) -- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing (SAST)](https://docs.gitlab.com/ee/user/application_security/sast/) -- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html) -- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/) -- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html) -*** +Settings +-------- +## Settings Class Overview -# Editing this README +The `Settings` class is responsible for managing the application's settings, including user preferences and configurations. It handles the initialization, loading, saving, and updating of settings, ensuring that user preferences are preserved and applied throughout the application. -When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thanks to [makeareadme.com](https://www.makeareadme.com/) for this template. +### Key Features: -## Suggestions for a good README +- **Initialization**: When a new instance of `Settings` is created, it initializes default settings or loads existing settings from local storage. It sets up various configuration parameters related to the project, regions, attributes, scoring, and other functionalities. -Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information. +- **Settings Panel Management**: Provides methods to toggle the visibility of the settings panel, allowing users to view and modify application settings through the user interface. -## Name -Choose a self-explaining name for your project. +- **Persistence**: Implements `load` and `save` methods to retrieve and store settings in the browser's local storage. This ensures that user preferences are maintained across sessions. -## Description -Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors. +- **Project Settings**: Manages project-specific settings such as the project name, default path, search path, buffer size, and auto-save options. -## Badges -On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge. +- **Region Settings**: Handles settings related to region display, including highlighting regions, scrolling behavior, region labels, colors, and font styles. -## Visuals -Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method. +- **Attribute Settings**: Manages the display of additional attributes like scores, score colors, pixel areas, and file attributes in the annotation editor. -## Installation -Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection. +- **Scoring Settings**: Configures scoring thresholds and colors to visually represent higher and lower scores in the application. -## Usage -Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README. +- **Quick Buttons Configuration**: Allows users to customize the visibility and functionality of quick access buttons in the application's toolbar. + +- **Event Handlers**: Initializes event handlers for UI elements, ensuring that changes made in the settings panel are reflected in the application's behavior. + +### Usage Example: + +To use the `Settings` class: + +```javascript +// Create an instance of the Settings class +const settings = new Settings(); + +// Toggle the settings panel visibility +settings.toggleSettings(); + +// Enable region highlighting +settings.enableHighlightRegion(); + +// Update the project name +settings.projectName = 'New_Project_Name'; +settings.save(); // Save changes to local storage +``` + +Image Manipulation +---------------- +## ImgManipulation Class + +The `ImgManipulation` class provides functionalities for manipulating images within the application. It allows users to adjust various visual properties of an image, enhancing the viewing experience and facilitating better analysis. + +### Features + +- **Brightness Adjustment**: Modify the lightness or darkness of the image. +- **Contrast Adjustment**: Alter the difference between the light and dark areas. +- **Hue Rotation**: Change the overall color tone of the image. +- **Saturation Adjustment**: Increase or decrease the intensity of the colors. +- **Scaling**: Resize the image by scaling it up or down. +- **Reset Functionality**: Reset all adjustments back to their default values. + +### How It Works + +- **Sliders Integration**: The class integrates with slider controls (`brightnessRange`, `contrastRange`, `hueRange`, `saturationRange`) to allow real-time adjustments. +- **Event Listeners**: It sets up event listeners on these sliders to update the image properties whenever the slider values change. +- **CSS Filters**: Uses CSS filter properties to apply the visual adjustments to the image. +- **Image Hooking**: The `hook()` method associates the class with a specific image element in the DOM. + +### Usage Example + +```javascript +// Create an instance of ImgManipulation +const imageManipulation = new ImgManipulation(); + +// Hook the image you want to manipulate +imageManipulation.hook(); + +// Show the image manipulation interface +await imageManipulation.show(); + +// Adjust brightness to 120% +imageManipulation.brightness = 120; +imageManipulation.changeImgSettings(); + +// Reset all adjustments to default values +imageManipulation.resetFilters(); +``` + +### Methods Overview + +- **`addEventListeners()`**: Sets up event listeners for the sliders to handle user input. +- **`hook(img)`**: Associates the manipulation functionalities with a specific image element. +- **`setSliderValues()`**: Initializes the sliders with default or current values. +- **`show()`**: Displays the image manipulation interface. +- **`changeImgSettings()`**: Applies the current slider values to the image using CSS filters. +- **`resetFilters()`**: Resets all image adjustments to their default settings. +- **`resize(w, h)`**: Changes the size of the image to the specified width and height. +- **`scaleImage(scale)`**: Scales the image by a given factor. + +### Notes + +- The class relies on jQuery for DOM manipulation and event handling. +- Commented-out methods like `rotate()`, `flip()`, and `mirror()` indicate potential additional functionalities that can be implemented in the future. + +Message +------- +## Message Class Overview + +The `Message` class manages the display of Bootstrap toast messages within the application. It provides a straightforward interface to show various types of messages—such as errors, warnings, information, and success messages—to the user in a consistent and visually appealing manner. + +### Key Features: + +- **Static Implementation**: Utilizes static properties and methods, allowing you to call message functions without creating an instance of the class. + +- **Bootstrap Toast Integration**: Leverages Bootstrap's toast component to display messages, ensuring responsiveness and a modern UI experience. + +- **Multiple Message Types**: Supports different message categories including: + - **Error** + - **Warning** + - **Info** + - **Success** + - **Custom** + +- **Automatic Styling**: Automatically applies appropriate styling based on the message type, using Bootstrap badge classes for visual cues. + +- **Convenience Methods**: Provides helper methods for commonly used message types: + - `showError(message)` + - `showInfo(message)` + +### Usage Example: + +To display a message to the user, you can use the static methods provided by the `Message` class: + +```javascript +// Display an error message +Message.showError("An unexpected error occurred."); + +// Display an informational message +Message.showInfo("Data loaded successfully."); + +// Display a custom message +Message.show({ + address: "Custom", + body: "This is a custom message.", +}); +``` + +### Methods: + +- **`show(message)`**: Displays a toast message. + - **Parameters**: + - `message`: An object containing: + - `address`: The type or title of the message (e.g., "Error", "Info"). + - `body`: The content of the message. + - **Usage**: + ```javascript + Message.show({ + address: "Success", + body: "Your changes have been saved.", + }); + ``` + +- **`showError(message)`**: Convenience method to display an error message. + - **Parameters**: + - `message`: A string containing the error message. + - **Usage**: + ```javascript + Message.showError("Failed to load the data."); + ``` + +- **`showInfo(message)`**: Convenience method to display an informational message. + - **Parameters**: + - `message`: A string containing the informational message. + - **Usage**: + ```javascript + Message.showInfo("New updates are available."); + ``` + +- **`test()`**: Displays a test message to verify that the messaging system works. + - **Usage**: + ```javascript + Message.test(); + ``` + +### Integration Details: + +- **HTML Elements**: Ensure your HTML includes the following elements with the specified IDs: + - `#liveToast`: The container for the toast message. + - `#toastAddress`: The element where the message title or type will be displayed. + - `#toastBody`: The element where the message content will be displayed. + +- **Bootstrap Dependency**: The class depends on Bootstrap's CSS classes for styling the toast messages and badges. + +### Example HTML Structure: + +```html +<div id="liveToast" class="toast" role="alert" aria-live="assertive" aria-atomic="true"> + <div class="toast-header"> + <strong id="toastAddress" class="me-auto"></strong> + <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button> + </div> + <div id="toastBody" class="toast-body"></div> +</div> +``` + +### Notes: + +- The `_via_is_message_visible` flag is used internally to control the visibility of messages based on user preferences or application state. + +- The class uses jQuery selectors (`$`) to access and manipulate DOM elements. Ensure that jQuery is included in your project. + +Science plugins +--------------------- +## science_plugins.js Overview + +The `science_plugins.js` file provides key functionalities for grouping regions, managing scores, and exporting detailed region sizes in the Infarct Size AI application. It includes a web worker for efficient region grouping and a main `Science_plugins` class that orchestrates these operations. + +### Key Features: + +- **Region Grouping**: Utilizes a web worker (`infarctSizeAiWorker`) to group regions based on spatial relationships, improving performance by offloading computation from the main thread. + +- **Centroid Calculation**: Calculates centroids of polygonal regions to determine if one region is contained within another, essential for hierarchical grouping. + +- **Polygon Operations**: + - **Point-in-Polygon Test**: Determines if a point (e.g., a centroid) lies inside a given polygon region. + - **Area Calculation**: Efficiently computes the area of polygonal regions, necessary for quantitative analysis. + +- **Score Management**: + - **Get and Set Scores**: Provides methods to retrieve and assign scores to regions. + - **Score Updating**: Updates region scores based on groupings, ensuring consistency and handling cases where scores might be unavailable. + +- **Group Identifier Management**: Allows changing group identifiers and selecting all regions within a group for streamlined operations and analysis. + +- **Data Export**: + - **Export Areas to CSV**: Compiles region size and score data across images and exports it in CSV format for reporting and further analysis. + +### Usage Example: + +```javascript +// Instantiate the Science_plugins class +const plugin = new Science_plugins(); + +// Update region groupings based on "Slice" regions +plugin.updateSliceRegion(_via_image_id); + +// Update scores for regions based on their groups +plugin.updateScoresBasedOnGroup(); + +// Export area and score data to a CSV file +plugin.ExportArea(); +``` + +Auto Annotate +------------- +## Annotate Class Overview + +The `Annotate` class in `servercom.js` is responsible for handling server communication related to image annotation within the application. It facilitates the process of sending images to a server for automatic annotation, receiving the annotated data, and integrating the annotations into the application's image metadata. + +### Key Features: + +- **Server Communication**: Manages the upload of images to a server endpoint for annotation. It handles both single-image and batch-image uploads. + +- **Annotation Integration**: Processes the server's response by adding the received annotations, including region attributes and scores, to the corresponding images in the application. + +- **Progress Tracking**: Implements a progress bar and modal dialogs to provide visual feedback during the annotation process, enhancing user experience. + +- **User Interaction**: + - **Re-Annotation Prompts**: Detects if an image has been previously annotated and prompts the user to confirm if they wish to re-annotate, preventing unintentional overwrites. + - **Modal Dialogs**: Displays informative messages and options to the user through modal dialogs. + +- **Batch Processing**: Supports annotating multiple images simultaneously, optimizing workflow efficiency. + +- **Error Handling**: Includes mechanisms to handle network errors and server-side issues, providing appropriate messages to the user. + +### Usage Example: + +To utilize the `Annotate` class for annotating images: + +```javascript +// Instantiate the Annotate class +const AutoAnnotator = new Annotate(); + +// Run annotation on the current image or selected images +AutoAnnotator.run_annotation(); +``` + +When `run_annotation()` is called, the class: + +1. Determines the current display context (single image or image grid). +2. If in image grid view, it initiates batch annotation for all selected images. +3. If in single image view, it checks if the image is already annotated and prompts the user accordingly. +4. Sends the image(s) to the server for annotation. +5. Updates the progress bar to reflect the annotation process status. +6. Receives the annotations from the server and integrates them into the application. + +Modal +----- +## Modal Class Overview + +The `Modal` class in `modal.js` provides a simplified interface for creating and controlling Bootstrap modal dialogs within your application. It encapsulates common modal functionalities, allowing you to display messages, forms, or any custom content in a modal window with ease. + +### Key Features: + +- **Initialization**: The class selects and stores references to various parts of the modal dialog using jQuery selectors: + - The modal container (`#staticBackdropModal`) + - Modal title (`.modal-title`) + - Modal body (`.modal-body`) + - Modal footer (`.modal-footer`) + - Close button (`.btn-close`) + +- **Event Handling**: Automatically binds a click event to the close button to hide the modal when clicked. + +- **Show Method**: The `show` method displays the modal with specified title, body content, and footer. It also allows you to optionally disable the close button. + +- **Update Method**: The `update` method lets you change the modal's content (title, body, footer) without showing or hiding the modal. + +- **Hide and Clear Methods**: Provides `hide` and `clear` methods to hide the modal and clear its content, respectively. + +### Usage Example: + +Here's how you can use the `Modal` class in your application: + +```javascript +// Instantiate the Modal class +const modal = new Modal(); + +// Show the modal with title, body, and footer +modal.show( + 'Modal Title', + '<p>This is the body of the modal.</p>', + '<button type="button" class="btn btn-primary">Save changes</button>', + false // Set to true to hide the close button +); + +// Update the modal content without showing or hiding +modal.update( + 'Updated Title', + '<p>This is the updated body content.</p>', + '<button type="button" class="btn btn-secondary">Close</button>' +); + +// Hide the modal +modal.hide(); + +// Clear the modal content +modal.clear(); +``` + +### Integration Details: + +- **HTML Structure**: Ensure your HTML includes a modal structure compatible with Bootstrap and the selectors used in the class. For example: + + ```html + <div id="staticBackdropModal" class="modal" tabindex="-1" aria-hidden="true"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title"></h5> + <button type="button" class="btn-close" aria-label="Close"></button> + </div> + <div class="modal-body"></div> + <div class="modal-footer"></div> + </div> + </div> + </div> + ``` + +- **Bootstrap Dependency**: The `Modal` class relies on Bootstrap's modal component. Ensure you have included Bootstrap's CSS and JS files in your project. + +- **jQuery Dependency**: The class uses jQuery for DOM manipulation. Make sure jQuery is included in your project. + +### Notes: + +- **Close Button Visibility**: When calling the `show` method, you can control the visibility of the close button by setting the `dis_close` parameter: + - `dis_close = false` (default): Close button is visible. + - `dis_close = true`: Close button is hidden. + +- **Modal Reusability**: The `update` method allows you to change the modal's content dynamically, making it reusable for different messages or forms without needing to recreate or re-initialize the modal. + +- **Event Listeners**: Since the close button's click event is bound in the constructor, it will always hide the modal when clicked, unless you modify the class to change this behavior. + +Undo Redo +--------- + +## Undo/Redo Functionality Overview + +The `undo_redo.js` file implements the undo and redo functionality within the application, allowing users to revert and reapply changes made to image annotations or metadata. This feature enhances user experience by providing flexibility and control over editing actions. + +### Key Features: + +- **Command Pattern Implementation**: Utilizes the Command design pattern to encapsulate actions (like incrementing or decrementing a counter), making it easier to manage undo and redo operations. + +- **Web Worker Integration**: Offloads the undo/redo logic to a separate web worker thread (`undoRedoWorker`) to prevent blocking the main UI thread, ensuring smooth and responsive user interactions. + +- **State Management with Mementos**: Maintains a history of annotation states (region metadata) using a memento pattern. Each state represents a snapshot of the annotations at a given time. + +- **Proxy for Change Detection**: Employs JavaScript Proxy objects to monitor changes in the region metadata. This allows automatic tracking of changes and updating the undo/redo history accordingly. + +- **Undo/Redo Controls**: Provides functions to undo or redo actions, updating the application state and the user interface to reflect these changes. + +### How It Works: + +1. **Command Manager**: Creates commands (`INCREMENT`, `DECREMENT`) to manage the counter representing the position in the history stack. The command manager handles executing and undoing these commands. + +2. **Worker Thread**: + - Listens for messages from the main thread containing commands (`undo`, `redo`, `add`, `reset`) and the current region metadata. + - Manages a history stack (`regionMementos`) to store the sequence of annotation states. + - Updates the state based on the command received and posts messages back to the main thread with the updated state and controls status (whether undo/redo is enabled). + +3. **Main Thread Communication**: + - Sends commands to the worker thread when the user performs actions that modify annotations. + - Receives updates from the worker thread to enable or disable undo/redo buttons and to update the displayed annotations. + +4. **Change Detection**: + - Uses proxies (`validator`, `validator2`) to detect changes in the region metadata. + - When changes are detected, and undo/redo is enabled, it adds the new state to the history stack. + +### Integration Details: + +- **Event Listeners**: The main thread listens for messages from the worker to update the UI accordingly. The worker thread listens for messages from the main thread to perform the necessary operations. + +- **Undo/Redo Buttons**: The availability of undo and redo actions is reflected by enabling or disabling the respective buttons in the UI (`#nav_undo`, `#nav_redo`). + +- **Reset Functionality**: Includes the ability to reset the history stack, clearing all stored states. + +### Usage: + +- **Undo Action**: Reverts the most recent change by moving back one state in the history stack. + +- **Redo Action**: Reapplies a previously undone change by moving forward one state in the history stack. + +- **Adding a State**: When annotations are modified, a new state is added to the history stack, capturing the current annotations. + +- **Resetting History**: Clears the undo/redo history, disabling both actions until new changes are made. + +Image Buffer +------------ + +## ImageBuffer Class Overview + +The `ImageBuffer` class in `buffer.js` manages the loading and display of images within the application. It is primarily responsible for buffering images to enhance performance and user experience. This implementation is based on the VGG VIA annotator's image buffer, encapsulated into a class structure and enhanced with loading indicators. -## Support -Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc. +### Key Responsibilities: -## Roadmap -If you have ideas for releases in the future, it is a good idea to list them in the README. +- **Image Loading and Display**: Handles the loading of images into the buffer and displays them on the image panel when requested. -## Contributing -State if you are open to contributions and what your requirements are for accepting them. +- **Buffer Management**: Maintains a buffer of recently accessed images to optimize performance and reduce loading times during navigation. -For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self. +- **Preloading**: Implements image preloading to anticipate user actions and improve the responsiveness of the application. -You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser. +- **Loading Indicators**: Provides visual feedback during image loading processes to inform the user of ongoing operations. -## Authors and acknowledgment -Show your appreciation to those who have contributed to the project. +### How It Works: -## License -For open source projects, say how it is licensed. +- **Image Buffering**: The class keeps track of images currently in the buffer (`bufferIdList`) and their corresponding timestamps (`bufferTimestamp`). This allows efficient retrieval and management of images based on usage patterns. -## Project status -If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers. +- **Image Display**: + - When an image is requested (`showImage` method), the class checks if it's already in the buffer. + - If it is, the image is displayed directly from the buffer. + - If not, the image is loaded from its source and added to the buffer. + +- **Buffer Optimization**: + - The buffer size is managed to ensure it doesn't exceed a predefined limit (configured via settings). + - Least recently used images are removed from the buffer when necessary to make room for new images. + +- **Preloading Images**: + - Anticipates the next images the user might view and preloads them in the background. + - Improves navigation speed between images by having them ready in the buffer. + +- **Loading Feedback**: + - Uses a loading indicator (`this.loading`) to provide visual feedback during image loading operations. + - Prevents multiple simultaneous loading processes to avoid conflicts. + +### Usage: + +- **Initialization**: An instance of `ImageBuffer` is created and used throughout the application to manage image loading and display. + + ```javascript + const buffer = new ImageBuffer(); + ``` + +- **Displaying an Image**: To display an image at a specific index: + + ```javascript + buffer.showImage(imageIndex); + ``` + +- **Preloading**: The class automatically handles preloading of images based on the current image index. + +### Notes: + +- **Integration with Other Components**: The `ImageBuffer` class interacts with other components like the drawing module and the sidebar to ensure seamless updates to the user interface when images change. + +- **Legacy Code Encapsulation**: While the core functionality mirrors that of the VGG VIA annotator, the encapsulation into a class and the addition of loading indicators enhance modularity and user experience. + +- **Settings Dependency**: Buffer size and other configurations are managed via the application's settings, allowing for customization based on user preferences or system capabilities. + + +File Manager +------------ +## FileManager Class Overview + +The `FileManager` class in `file_manager.js` is responsible for managing file operations within the application, such as downloading region data and importing annotations from files. It facilitates data export in multiple formats and handles user interactions related to file management. + +### Key Features: + +- **Download Region Data**: Allows users to download all annotation region data in various formats: + - **CSV**: For compatibility with spreadsheet applications. + - **JSON**: For structured data exchange and storage. + - **COCO**: Exports data in the COCO (Common Objects in Context) format, widely used in computer vision tasks. + +- **Import Annotations**: Supports importing annotation data from external files in CSV or JSON formats, integrating them into the application's dataset. + +- **User Interaction via Modals**: Utilizes modal dialogs to interact with the user, providing options for download formats and confirming actions. + +- **Data Packaging and Formatting**: + - Handles the conversion of internal annotation data to the selected export format. + - Ensures proper formatting and escaping of data for CSV exports. + - Converts project data into COCO format, including images, annotations, and categories. + +- **File Saving**: Provides methods to save data blobs to the local filesystem, triggering downloads of the exported files. + +### Usage Example: + +- **Downloading All Region Data**: + + ```javascript + // Instantiate the FileManager class + const fileManager = new FileManager(); + + // Open a modal to choose download options + fileManager.downloadAllRegionDataModal(); + ``` + + This will display a modal dialog where users can select the desired export format(s) and initiate the download. + +- **Importing Annotations from a File**: + + ```javascript + // Use the FileManager to import annotations + fileManager.importAnnotationsFromFile(); + ``` + + This function will prompt the user to select a file containing annotations, which will then be read and integrated into the application's data. + +### Integration Details: + +- **Modal Dependency**: The class relies on the `Modal` class to display dialogs for user interactions. It creates instances of `Modal` to show messages and gather user input. + +- **Data Handling**: + - **Exporting**: Collects annotation data from the application's metadata and formats it according to the selected export type. + - **Importing**: Reads and parses annotation data from files, updating the application's metadata accordingly. + +- **File Operations**: + - **Saving Files**: Utilizes the browser's `Blob` and `URL` APIs to create downloadable files. + - **Reading Files**: Uses the `FileReader` API to read files selected by the user during the import process. + +- **Error Handling**: Provides feedback to the user in case of errors during file reading or writing operations, ensuring that the user is informed of any issues. + + +File Metadata +------------- + +## FileMetadata Class Overview + +The `FileMetadata` class efficiently manages metadata for files within the application. It encapsulates all relevant information about a file, including its name, size, associated regions, attributes, and grouping details. + +### Key Features: + +- **File Information**: Stores the `filename` and `size` (in bytes) of the file. + +- **Regions Management**: + - **Regions List**: Maintains an array of regions (`regions`) associated with the file. + - **Add Region**: Method `addRegion(region)` to add a new region to the file. + - **Clear Regions**: Method `clearRegions()` to remove all regions. + +- **File Attributes**: + - **Default Attributes**: Includes default attributes like `ID` and `Treatment`. + - **Set Attributes**: Method `setFileAttributes(file_attributes)` to update file attributes. + +- **Locked Regions**: + - **Lock Management**: Uses a `Set` (`lockedRegions`) to keep track of locked regions. + - **Add Locked Region**: Method `addLockedRegion(region)` to lock a region. + - **Check Lock Status**: Method `isRegionLocked(region)` to check if a region is locked. + - **Clear Locks**: Methods `clearLockedRegions()` and `clearRegionLock(region)` to unlock regions. + +- **Grouped Regions**: + - **Grouping Data**: Manages grouped regions with `groupedRegions`, containing `groupBy`, `groups`, and `groupIDs`. + - **Add Grouped Region**: Method `addGroupedRegion(region, group)` to assign a region to a group. + - **Retrieve Grouped Region**: Method `getGroupedRegion(region)` to get the group of a region. + - **Clear Groupings**: Methods `clearGroupedRegions()`, `clearGroups()`, and `clearGroupedRegion(region)` to manage groupings. + - **Groupability Check**: Method `isGroupable()` to determine if the file has groupable regions. + - **Grouped Key Check**: Method `isGroupedKey(region)` to check if a region is a key in grouped regions. + +- **Auto-Annotated Flag**: + - Indicates if the file has been auto-annotated (`autoAnnotated`). + +- **Data Loading**: + - **From JSON**: Method `loadFromJSON(data)` to populate the instance with data from a JSON object. + +### Example Usage: + +```javascript +// Creating a new FileMetadata instance +const fileMetadata = new FileMetadata('image.jpg', 102400); + +// Setting file attributes +fileMetadata.setFileAttributes({ + ID: { type: "text", description: "File ID", default_value: "12345" }, + Treatment: { type: "text", description: "Treatment info", default_value: "A" }, +}); + +// Adding a region +fileMetadata.addRegion(newRegion); + +// Locking a region +fileMetadata.addLockedRegion(regionId); + +// Checking if a region is locked +if (fileMetadata.isRegionLocked(regionId)) { + // Handle locked region +} + +// Grouping regions +fileMetadata.addGroupedRegion(regionId, groupId); + +// Loading metadata from JSON +fileMetadata.loadFromJSON(jsonData); +``` + +Project +------- + +## Project Class Overview + +The `Project` class is central to managing project data within the application. It encapsulates all the necessary functionalities to create, save, load, and manipulate projects, which include images, annotations, and associated metadata. + +### Key Features: + +- **Project Initialization**: + - **Default Project Name**: Automatically generates a default project name based on the current date and time if none is provided. + - **Attributes Setup**: Initializes default region and file attributes, such as types and options for annotations. + +- **Project Management**: + - **Set and Get Project Name**: Methods to set (`setName(name)`) and retrieve (`getProjectName()`) the project's name. + - **Save Project**: + - **With Confirmation**: Displays a modal to confirm project saving and specify the project and file names. + - **Serialization**: Converts the project data into JSON format, including handling of complex data structures like `Map` and `Set` through a custom replacer function. + - **Open Project**: + - **File Selection**: Allows users to select a project file to open. + - **Parsing and Validity Checks**: Parses the selected JSON file and checks for validity before loading. + - **Image Loading**: Handles the loading of images associated with the project, prompting the user if images are missing. + +- **Image Handling**: + - **Add Images**: + - **Local Files**: Supports adding images from local files through file input dialogs. + - **URLs**: Allows adding images via absolute paths or URLs. + - **Remove Images**: + - **With Confirmation**: Provides a confirmation dialog before removing an image from the project. + - **Buffer Management**: Updates the image buffer and sidebar accordingly after removal. + - **Image Metadata**: + - **Initialization**: Creates new `FileMetadata` instances for each image. + - **Validation**: Ensures image metadata is valid when loading projects. + +- **Attributes Management**: + - **Import Attributes**: Supports importing region and file attributes from external JSON files. + - **Parsing Attributes**: Extracts and updates attributes from loaded project metadata. + +- **Utility Functions**: + - **Data Clearing**: `clearViaData()` function to reset the application's data when loading a new project. + - **Serialization Helper**: Custom `replacer` function to handle serialization of `Map` and `Set` objects during JSON conversion. + +### Example Usage: + +```javascript +// Create a new Project instance +const project = new Project(); + +// Set the project name +project.setName('MyAnnotationProject'); + +// Save the project with user confirmation +project.saveWithConfirm(); + +// Open an existing project +project.openSelectProjectFile(); + +// Add a local image file to the project +// (Assuming an event from a file input element) +project.addLocalFile(event); + +// Add an image using a URL +project.addUrlFileWithInput(); + +// Remove an image from the project with confirmation +project.fileRemoveWithConfirm(); +``` + +### Notes: + +- **Integration with Other Components**: The `Project` class interacts with various other classes and modules such as `Settings`, `Modal`, `FileManager`, and `ImageBuffer` to provide a cohesive project management experience. +- **User Interaction**: Utilizes modals extensively to confirm actions and gather input from the user, ensuring that critical operations like saving or deleting data are intentional. +- **Data Validation**: Implements checks to ensure that project files and image metadata are valid before loading, preventing potential errors or data corruption. +- **Support for Different Image Sources**: Accommodates images from local files, absolute paths, and URLs, providing flexibility in how users can add images to their projects. + +UI Handler +---------- +## UI Handler Overview + +The `ui_handler.js` file contains event handlers for various user interface (UI) elements within the application. It leverages jQuery to bind functions to UI components such as buttons, menu items, and icons, ensuring that user interactions trigger the appropriate actions and navigate through the application seamlessly. + +### Key Features: + +- **Navigation Handlers**: Manages events for primary navigation elements like the home button and brand logo to display the home panel. + + ```javascript + $("#nav_brand").click(function () { show_home_panel(); return false; }); + $("#nav_home").click(function () { show_home_panel(); return false; }); + ``` + +- **Project Management**: + + - **Loading and Saving Projects**: Binds actions to load and save project options. + + ```javascript + $("#nav_project_load").click(function () { project.openSelectProjectFile(); return false; }); + $("#nav_project_save").click(function () { project.saveWithConfirm(); return false; }); + ``` + + - **Project Settings**: Toggles the project settings panel. + + ```javascript + $("#nav_project_setting").click(function () { settings.toggleSettings(); return false; }); + ``` + + - **Adding Files**: Handles adding local files or files via URLs to the project. + + ```javascript + $("#nav_project_addLocFiles").click(function () { sel_local_images(); return false; }); + $("#nav_project_addFilUrl").click(function () { project.addUrlFileWithInput(); return false; }); + $("#nav_project_addFilAbs").click(function () { project.addAbsPathFileWithInput(); return false; }); + ``` + +- **Annotation Actions**: + + - **Exporting and Importing Annotations**: Triggers the download modal and import functionality for annotations. + + ```javascript + $("#nav_annotation_export").click(function () { fileManager.downloadAllRegionDataModal(); return false; }); + $("#nav_annotation_import").click(function () { fileManager.importAnnotationsFromFile(); return false; }); + ``` + + - **Preview and Auto-Annotation**: Handles actions to preview annotations and run automatic annotation processes. + + ```javascript + $("#nav_annotation_prevAnn").click(function () { show_annotation_data(); return false; }); + $("#nav_annotation_autoAnn").click(function () { AutoAnnotator.run_annotation(); return false; }); + ``` + + - **Download Annotated Image**: Allows users to download the current image with annotations. + + ```javascript + $("#nav_annotation_downImage").click(function () { download_as_image(); return false; }); + ``` + +- **View Controls**: + + - **Toggle Views**: Enables toggling between different views such as image grid, sidebar visibility, and status messages. + + ```javascript + $("#nav_view_imgGrid").click(function () { image_grid_toggle(); return false; }); + $("#nav_view_sidebar").click(function () { leftsidebar_toggle(); return false; }); + $("#nav_view_status").click(function () { toggle_message_visibility(); return false; }); + ``` + + - **Region Display Options**: Controls the visibility of region boundaries and labels. + + ```javascript + $("#nav_view_regionBound").click(function () { toggle_region_boundary_visibility(); return false; }); + $("#nav_view_regionLabel").click(function () { toggle_region_id_visibility(); return false; }); + ``` + +- **Toolbar Icons**: + + - **Project Operations**: Quick access icons for opening and saving projects, and accessing settings. + + ```javascript + $("#nav_openProject").click(function () { project.openSelectProjectFile(); return false; }); + $("#nav_saveProject").click(function () { project.saveWithConfirm(); return false; }); + $("#nav_settings").click(function () { settings.toggleSettings(); return false; }); + ``` + + - **Image Navigation**: Icons for navigating between images and toggling views. + + ```javascript + $("#nav_imgGrid").click(function () { image_grid_toggle(); return false; }); + $("#nav_sidePanel").click(function () { leftsidebar_toggle(); return false; }); + ``` + + - **Annotation Tools**: Icons for running auto-annotation and calculating areas. + + ```javascript + $("#nav_autoAnn").click(function () { AutoAnnotator.run_annotation(); return false; }); + $("#nav_calcAreas").click(function () { plugin.ExportArea(); return false; }); + ``` + + - **Undo/Redo and Zoom Controls**: + + ```javascript + $("#nav_undo").click(function () { undoredo_worker.postMessage({ commands: "undo" }); return false; }); + $("#nav_redo").click(function () { undoredo_worker.postMessage({ commands: "redo" }); return false; }); + $("#nav_zoomIn").click(function () { zoom.zoomIn(); return false; }); + $("#nav_zoomOut").click(function () { zoom.zoomOut(); return false; }); + ``` + + - **Region Selection and Manipulation**: + + ```javascript + $("#nav_selAllReg").click(function () { sel_all_regions(); return false; }); + $("#nav_copySelReg").click(function () { copy_sel_regions(); return false; }); + $("#nav_pasteReg").click(function () { paste_sel_regions_in_current_image(); return false; }); + $("#nav_delSelReg").click(function () { del_sel_regions(); return false; }); + ``` + +- **Drawing Tools**: + + - **Shape Selection**: Enables users to select different shapes for annotation. + + ```javascript + $("#shape_drag").click(function () { select_region_shape("drag"); return false; }); + $("#shape_polygon").click(function () { select_region_shape("polygon"); return false; }); + // Additional shapes like circle, ellipse, pen, rect, etc. + ``` + + - **Editing Tools**: Provides options to edit, remove, and trim annotations. + + ```javascript + $("#shape_edit").click(function () { select_region_shape("edit"); return false; }); + $("#shape_remove").click(function () { select_region_shape("remove"); return false; }); + $("#shape_trim").click(function () { select_region_shape("trim"); return false; }); + ``` + +- **Sidebar Controls**: + + - **Project Name Update**: Updates the project name when changed in the sidebar. + + ```javascript + $("#project_name").change(function () { project.onNameUpdate(this); return false; }); + ``` + + - **File Management**: Buttons for adding files and removing the current file. + + ```javascript + $("#sidebar_addFiles").click(function () { sel_local_images(); return false; }); + $("#sidebar_addUrl").click(function () { project.addUrlFileWithInput(); return false; }); + $("#sidebar_remove").click(function () { project.fileRemoveWithConfirm(); return false; }); + ``` + + - **Annotation Editor Modes**: Toggles between different modes in the annotation editor. + + ```javascript + $("#sidebar_ToggleAEModes").click(function () { sidebar.toggleAEMode(); return false; }); + ``` + + - **Slice Update**: Updates slices for the current image. + + ```javascript + $("#sidebar_UpdateSlices").click(function () { plugin.updateSliceRegion(); return false; }); + ``` + +- **Image Manipulation**: + + - **Reset Filters**: Resets image filters to default settings. + + ```javascript + $("#image_reset").click(function () { image.resetFilters(); return false; }); + ``` + + - **Return to Sidebar**: Navigates back to the sidebar from the image editor. + + ```javascript + $("#sidebar_reset").click(function () { + image.backToSidebar(); + select_region_shape("edit"); + return false; + }); + ``` + + - **On-Image Annotation Editor**: Toggles the visibility of the on-image annotation editor. + + ```javascript + $("#sidebar_onImageEditor").click(function () { + sidebar.annotation_editor_toggle_on_image_editor(); + return false; + }); + ``` + +- **Startup Screen Actions**: + + - **Initial Options**: Event handlers for actions available on the startup screen. + + ```javascript + $("#start_selectImages").click(function () { sel_local_images(); return false; }); + $("#start_addUrlImages").click(function () { project.addUrlFileWithInput(); return false; }); + $("#start_settings").click(function () { settings.toggleSettings(); return false; }); + $("#start_saveProject").click(function () { project.saveWithConfirm(); return false; }); + $("#start_loadProject").click(function () { project.openSelectProjectFile(); return false; }); + $("#start_gettingStarted").click(function () { + set_display_area_content(VIA_DISPLAY_AREA_CONTENT_NAME.PAGE_GETTING_STARTED); + return false; + }); + ``` + +- **404 Error Page Actions**: + + - **Error Recovery Options**: Provides options to navigate when a page or resource is not found. + + ```javascript + $("#404_settings").click(function () { settings.toggleSettings(); return false; }); + $("#404_selectImages").click(function () { sel_local_images(); return false; }); + $("#404_selectFolder").click(function () { project.loadAllImages(); return false; }); + $("#404_gettingStarted").click(function () { + set_display_area_content(VIA_DISPLAY_AREA_CONTENT_NAME.PAGE_GETTING_STARTED); + return false; + }); + ``` + +- **Help and Support**: + + - **Getting Started Guide**: Navigates to the getting started page. + + ```javascript + $("#nav_help_getStart").click(function () { + set_display_area_content(VIA_DISPLAY_AREA_CONTENT_NAME.PAGE_GETTING_STARTED); + return false; + }); + ``` + +- **Image Grid Toolbar**: + + - **Group By Selection**: Handles changes in the group-by selection dropdown in the image grid toolbar. + + ```javascript + $("#image_grid_toolbar_group_by_select").change(function () { + image_grid_toolbar_onchange_group_by_select(this); + return false; + }); + ``` + +### Usage: + +By defining these event handlers, the application ensures that when users interact with the UI elements, the corresponding functions are executed. This setup provides a responsive and intuitive user experience, allowing users to: + +- Navigate through different parts of the application. +- Manage projects and files effectively. +- Perform annotations and manipulate images. +- Access help and settings easily. + + +# Summary of InfarctSize-AI Frontend V1.0 Code Documentation + +InfarctSize-frontend is a web-based image annotation tool built upon the VIA (VGG Image Annotator) platform. Designed for domain experts without programming expertise, it facilitates efficient image annotation, particularly in medical imaging. Despite significant differences from the original VIA-2.0.x codebase, the underlying architecture and functionalities remain comparable. + +## Main Components + +1. **Sidebar**: Implemented in `sidebar.js`, the sidebar is located on the left side of the application. It includes project management features—like loaded image files, selectors, and search modules—and annotation tools. It utilizes jQuery for DOM manipulation to provide an interactive user interface. + +2. **Drawer**: The `Drawer` class handles all drawing operations on the canvas. It manages user interactions for drawing shapes (rectangles, circles, polygons, etc.), selecting and manipulating regions, and rendering annotations. Key features include mouse event handling, region selection and manipulation, collision detection, and utility functions for mathematical calculations related to shapes. + +3. **Settings**: The `Settings` class manages user preferences and configurations. It ensures that settings are initialized, loaded, saved, and consistently applied throughout the application. Users can adjust project settings, region display options, scoring thresholds, and quick access buttons. + +4. **Image Manipulation**: The `ImgManipulation` class allows users to adjust visual properties of images, such as brightness, contrast, hue, saturation, and scaling. It enhances image analysis by providing real-time adjustments using sliders and CSS filters, improving the visibility of image details. + +5. **Message**: The `Message` class provides a simple interface for displaying Bootstrap toast messages. It supports various message types—including errors, warnings, info, and success messages—to communicate with users effectively. + +6. **Modal**: Encapsulated in `modal.js`, the `Modal` class simplifies the creation and control of Bootstrap modal dialogs. It enables the display of messages, forms, or custom content within modal windows, enhancing user interaction for tasks like confirmations and data input. + +7. **Science Plugins**: Found in `science_plugins.js`, these plugins handle advanced functionalities like region grouping based on spatial relationships, score management, and exporting region sizes. They utilize web workers to perform computations efficiently without blocking the main UI thread. + +8. **Auto Annotate**: The `Annotate` class manages server communication for automatic image annotation. It sends images to a server, receives annotations, and integrates them into the application. This feature streamlines the annotation process by automating repetitive tasks. + +9. **Undo Redo**: Implemented in `undo_redo.js`, this functionality allows users to revert and reapply changes made to annotations. It uses the Command design pattern and web workers to maintain a history stack of annotation states, providing flexibility and control over editing actions. + +10. **Image Buffer**: The `ImageBuffer` class in `buffer.js` manages the loading and display of images. It optimizes performance by buffering images, preloading anticipated images, and providing loading indicators, resulting in a smoother navigation experience. + +11. **File Manager**: The `FileManager` class in `file_manager.js` handles file operations such as downloading region data and importing annotations. It supports multiple formats (CSV, JSON, COCO) and interacts with users through modal dialogs for tasks like choosing download formats. + +12. **File Metadata**: The `FileMetadata` class manages metadata associated with each file, including filenames, sizes, regions, attributes, and grouping details. It offers methods to manipulate regions, handle locked regions, and manage grouped regions efficiently. + +13. **Project**: Central to the application, the `Project` class manages all project-related data, including images, annotations, and metadata. It provides functionalities to create, save, load, and manipulate projects, ensuring that data is organized and easily accessible. + +14. **UI Handler**: The `ui_handler.js` file contains event handlers for various UI elements. It binds user interactions—such as clicks on buttons, menu items, and icons—to corresponding functions, ensuring a responsive and intuitive user experience. + +Also, to provide the documentation for the rest of the code `via.js` visit the [Visual +Geometry Group (VGG) Image Annotator (VIA)](https://www.robots.ox.ac.uk/~vgg/software/via/) official page. + +Source Code License +------------------- + +VIA is an open source project actively maintained by the [Visual +Geometry Group (VGG)](http://www.robots.ox.ac.uk/~vgg/). Its source code +is a distributed under the [BSD-2 clause +license](https://en.wikipedia.org/wiki/BSD_licenses#2-clause_license_.28.22Simplified_BSD_License.22_or_.22FreeBSD_License.22.29). + +``` +Copyright (c) 2016-2017, Abhishek Dutta. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. +Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..c0166d6 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2163 @@ +{ + "name": "infarctsize-ai", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "infarctsize-ai", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "grunt-contrib-concat": "^2.1.0", + "grunt-contrib-uglify": "^5.2.2" + }, + "devDependencies": { + "grunt-contrib-cssmin": "^5.0.0", + "grunt-contrib-htmlmin": "^3.1.0", + "grunt-minify-html": "^3.0.0" + } + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC", + "peer": true + }, + "node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argh": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/argh/-/argh-0.1.4.tgz", + "integrity": "sha512-sQN85FUGbEUBLyQiSJp4v8yAHTST2ao1WVXb/L8jkVqQTsypZuJQD0gMVeOLoSZBz21p22izF6HsBQP16QKQtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "peer": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/array-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", + "integrity": "sha512-zHjL5SZa68hkKHBFBK6DJCTtr9sfTCPCaph/L7tMSLcTFgy+zX7E+6q5UArbtOtMBCtxdICpfTCspRse+ywyXA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/array-slice": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", + "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT", + "peer": true + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT", + "peer": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "license": "MIT", + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "peer": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/camel-case": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz", + "integrity": "sha512-+MbKztAYHXPr1jNTSKQF52VpcFjwY5RkR7fxksV8Doo4KAYc5Fl4UJRgthBbTmEx8C54DqahhbLJkDwjI3PI/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "no-case": "^2.2.0", + "upper-case": "^1.1.1" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/clean-css": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", + "integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "source-map": "~0.6.0" + }, + "engines": { + "node": ">= 10.0" + } + }, + "node_modules/clean-css/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/cli-color": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/cli-color/-/cli-color-1.4.0.tgz", + "integrity": "sha512-xu6RvQqqrWEo6MPR1eixqGPywhYBHRs653F9jfXB2Hx4jdM/3WxiNE1vppRmxtMIfl16SFYTpYlrnqH/HsK/2w==", + "dev": true, + "license": "ISC", + "dependencies": { + "ansi-regex": "^2.1.1", + "d": "1", + "es5-ext": "^0.10.46", + "es6-iterator": "^2.0.3", + "memoizee": "^0.4.14", + "timers-ext": "^0.1.5" + } + }, + "node_modules/color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/color/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/colornames": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/colornames/-/colornames-1.1.1.tgz", + "integrity": "sha512-/pyV40IrsdulWv+wFPmERh9k/mjsPZ64yUMDmWrtj/k1nmgrzzIENWKdaVKyBbvFdQWqkcaRxr+polCo3VMe7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/colors": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", + "integrity": "sha512-ENwblkFQpqqia6b++zLD/KUWafYlVY/UNnAp7oz7LY7E924wmpye416wBOmvv/HMWzl8gL1kJlfvId/1Dg176w==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "color": "^3.1.3", + "text-hex": "1.0.x" + } + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT", + "peer": true + }, + "node_modules/d": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", + "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", + "dev": true, + "license": "ISC", + "dependencies": { + "es5-ext": "^0.10.64", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/dateformat": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", + "license": "MIT", + "peer": true, + "engines": { + "node": "*" + } + }, + "node_modules/detect-file": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", + "integrity": "sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/diagnostics": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/diagnostics/-/diagnostics-1.1.1.tgz", + "integrity": "sha512-8wn1PmdunLJ9Tqbx+Fx/ZEuHfJf4NKSN2ZBj7SJC/OWRWha843+WsTjqMe1B5E3p28jqBlp+mJ2fPVxPyNgYKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "colorspace": "1.1.x", + "enabled": "1.0.x", + "kuler": "1.0.x" + } + }, + "node_modules/dom-serializer": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", + "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "entities": "^2.0.0" + } + }, + "node_modules/dom-serializer/node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/dom-serializer/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", + "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "1" + } + }, + "node_modules/domutils": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", + "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "license": "MIT" + }, + "node_modules/each-async": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/each-async/-/each-async-1.1.1.tgz", + "integrity": "sha512-0hJGub96skwr+sUojv7zQ0kc9i4jn3SwLiLk8Jr7KDz7aaaMzkN5UX3a/9ZhzC0OfZVyXHhlHcjC0KVOiKZ+HQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^1.0.0", + "set-immediate-shim": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/emits": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emits/-/emits-3.0.0.tgz", + "integrity": "sha512-WJSCMaN/qjIkzWy5Ayu0MDENFltcu4zTPPnWqdFPOVBtsENVTN+A3d76G61yuiVALsMK+76MejdPrwmccv/wag==", + "dev": true, + "license": "MIT" + }, + "node_modules/enabled": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-1.0.2.tgz", + "integrity": "sha512-nnzgVSpB35qKrUN8358SjO1bYAmxoThECTWw9s3J0x5G8A9hokKHVDFzBjVpCoSryo6MhN8woVyascN5jheaNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-variable": "0.0.x" + } + }, + "node_modules/entities": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", + "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/env-variable": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/env-variable/-/env-variable-0.0.6.tgz", + "integrity": "sha512-bHz59NlBbtS0NhftmR8+ExBEekE7br0e01jw+kk0NDro7TtZzBYZ5ScGPs3OmwnpyfHTHOtr1Y6uedCdrIldtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/es5-ext": { + "version": "0.10.64", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", + "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", + "dev": true, + "hasInstallScript": true, + "license": "ISC", + "dependencies": { + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "esniff": "^2.0.1", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", + "dev": true, + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/es6-symbol": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz", + "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "d": "^1.0.2", + "ext": "^1.7.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/es6-weak-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", + "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", + "dev": true, + "license": "ISC", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.46", + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/esniff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", + "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", + "dev": true, + "license": "ISC", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "event-emitter": "^0.3.5", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "peer": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "node_modules/eventemitter2": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz", + "integrity": "sha512-K7J4xq5xAD5jHsGM5ReWXRTFa3JRGofHiMcVgQ8PRwgWxzjHpMWCIzsmyf60+mh8KLsqYPcjUMa0AC4hd6lPyQ==", + "license": "MIT", + "peer": true + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "peer": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expand-tilde": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", + "integrity": "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==", + "license": "MIT", + "peer": true, + "dependencies": { + "homedir-polyfill": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ext": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", + "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", + "dev": true, + "license": "ISC", + "dependencies": { + "type": "^2.7.2" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT", + "peer": true + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "peer": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/findup-sync": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-5.0.0.tgz", + "integrity": "sha512-MzwXju70AuyflbgeOhzvQWAvvQdo1XL0A9bVvlXsYcFEBM87WR4OakL4OfZq+QRmr+duJubio+UtNQCPsVESzQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "detect-file": "^1.0.0", + "is-glob": "^4.0.3", + "micromatch": "^4.0.4", + "resolve-dir": "^1.0.1" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/fined": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fined/-/fined-1.2.0.tgz", + "integrity": "sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng==", + "license": "MIT", + "peer": true, + "dependencies": { + "expand-tilde": "^2.0.2", + "is-plain-object": "^2.0.3", + "object.defaults": "^1.1.0", + "object.pick": "^1.2.0", + "parse-filepath": "^1.0.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/flagged-respawn": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz", + "integrity": "sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/for-own": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", + "integrity": "sha512-0OABksIGrxKK8K4kynWkQ7y1zounQxP+CWnyclVwj81KW3vlLlGUx57DKGcP/LH216GzqnstnPocF16Nxs0Ycg==", + "license": "MIT", + "peer": true, + "dependencies": { + "for-in": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC", + "peer": true + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/getobject": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/getobject/-/getobject-1.0.2.tgz", + "integrity": "sha512-2zblDBaFcb3rB4rF77XVnuINOE2h2k/OnqXAiy0IrTxUfV1iFp3la33oAQVY9pCpWU268WFYVt2t71hlMuLsOg==", + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/glob": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "peer": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/global-modules": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", + "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", + "license": "MIT", + "peer": true, + "dependencies": { + "global-prefix": "^1.0.1", + "is-windows": "^1.0.1", + "resolve-dir": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/global-prefix": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", + "integrity": "sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==", + "license": "MIT", + "peer": true, + "dependencies": { + "expand-tilde": "^2.0.2", + "homedir-polyfill": "^1.0.1", + "ini": "^1.3.4", + "is-windows": "^1.0.1", + "which": "^1.2.14" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/global-prefix/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "license": "ISC", + "peer": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/grunt": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/grunt/-/grunt-1.6.1.tgz", + "integrity": "sha512-/ABUy3gYWu5iBmrUSRBP97JLpQUm0GgVveDCp6t3yRNIoltIYw7rEj3g5y1o2PGPR2vfTRGa7WC/LZHLTXnEzA==", + "license": "MIT", + "peer": true, + "dependencies": { + "dateformat": "~4.6.2", + "eventemitter2": "~0.4.13", + "exit": "~0.1.2", + "findup-sync": "~5.0.0", + "glob": "~7.1.6", + "grunt-cli": "~1.4.3", + "grunt-known-options": "~2.0.0", + "grunt-legacy-log": "~3.0.0", + "grunt-legacy-util": "~2.0.1", + "iconv-lite": "~0.6.3", + "js-yaml": "~3.14.0", + "minimatch": "~3.0.4", + "nopt": "~3.0.6" + }, + "bin": { + "grunt": "bin/grunt" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/grunt-cli": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/grunt-cli/-/grunt-cli-1.4.3.tgz", + "integrity": "sha512-9Dtx/AhVeB4LYzsViCjUQkd0Kw0McN2gYpdmGYKtE2a5Yt7v1Q+HYZVWhqXc/kGnxlMtqKDxSwotiGeFmkrCoQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "grunt-known-options": "~2.0.0", + "interpret": "~1.1.0", + "liftup": "~3.0.1", + "nopt": "~4.0.1", + "v8flags": "~3.2.0" + }, + "bin": { + "grunt": "bin/grunt" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/grunt-cli/node_modules/nopt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", + "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", + "license": "ISC", + "peer": true, + "dependencies": { + "abbrev": "1", + "osenv": "^0.1.4" + }, + "bin": { + "nopt": "bin/nopt.js" + } + }, + "node_modules/grunt-contrib-concat": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/grunt-contrib-concat/-/grunt-contrib-concat-2.1.0.tgz", + "integrity": "sha512-Vnl95JIOxfhEN7bnYIlCgQz41kkbi7tsZ/9a4usZmxNxi1S2YAIOy8ysFmO8u4MN26Apal1O106BwARdaNxXQw==", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "source-map": "^0.5.3" + }, + "engines": { + "node": ">=0.12.0" + }, + "peerDependencies": { + "grunt": ">=1.4.1" + } + }, + "node_modules/grunt-contrib-cssmin": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/grunt-contrib-cssmin/-/grunt-contrib-cssmin-5.0.0.tgz", + "integrity": "sha512-SNp4H4+85mm2xaHYi83FBHuOXylpi5vcwgtNoYCZBbkgeXQXoeTAKa59VODRb0woTDBvxouP91Ff5PzCkikg6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "clean-css": "^5.3.2", + "maxmin": "^3.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/grunt-contrib-htmlmin": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/grunt-contrib-htmlmin/-/grunt-contrib-htmlmin-3.1.0.tgz", + "integrity": "sha512-Khaa+0MUuqqNroDIe9tsjZkioZnW2Y+iTGbonBkLWaG7+SkSFExfb4jLt7M6rxKV3RSqlS7NtVvu4SVIPkmKXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^2.4.2", + "html-minifier": "^4.0.0", + "pretty-bytes": "^5.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/grunt-contrib-htmlmin/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/grunt-contrib-htmlmin/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/grunt-contrib-htmlmin/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/grunt-contrib-htmlmin/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/grunt-contrib-htmlmin/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/grunt-contrib-htmlmin/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/grunt-contrib-uglify": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/grunt-contrib-uglify/-/grunt-contrib-uglify-5.2.2.tgz", + "integrity": "sha512-ITxiWxrjjP+RZu/aJ5GLvdele+sxlznh+6fK9Qckio5ma8f7Iv8woZjRkGfafvpuygxNefOJNc+hfjjBayRn2Q==", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "maxmin": "^3.0.0", + "uglify-js": "^3.16.1", + "uri-path": "^1.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/grunt-known-options": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/grunt-known-options/-/grunt-known-options-2.0.0.tgz", + "integrity": "sha512-GD7cTz0I4SAede1/+pAbmJRG44zFLPipVtdL9o3vqx9IEyb7b4/Y3s7r6ofI3CchR5GvYJ+8buCSioDv5dQLiA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/grunt-legacy-log": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-3.0.0.tgz", + "integrity": "sha512-GHZQzZmhyq0u3hr7aHW4qUH0xDzwp2YXldLPZTCjlOeGscAOWWPftZG3XioW8MasGp+OBRIu39LFx14SLjXRcA==", + "license": "MIT", + "peer": true, + "dependencies": { + "colors": "~1.1.2", + "grunt-legacy-log-utils": "~2.1.0", + "hooker": "~0.2.3", + "lodash": "~4.17.19" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/grunt-legacy-log-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-2.1.0.tgz", + "integrity": "sha512-lwquaPXJtKQk0rUM1IQAop5noEpwFqOXasVoedLeNzaibf/OPWjKYvvdqnEHNmU+0T0CaReAXIbGo747ZD+Aaw==", + "license": "MIT", + "peer": true, + "dependencies": { + "chalk": "~4.1.0", + "lodash": "~4.17.19" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/grunt-legacy-util": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-2.0.1.tgz", + "integrity": "sha512-2bQiD4fzXqX8rhNdXkAywCadeqiPiay0oQny77wA2F3WF4grPJXCvAcyoWUJV+po/b15glGkxuSiQCK299UC2w==", + "license": "MIT", + "peer": true, + "dependencies": { + "async": "~3.2.0", + "exit": "~0.1.2", + "getobject": "~1.0.0", + "hooker": "~0.2.3", + "lodash": "~4.17.21", + "underscore.string": "~3.3.5", + "which": "~2.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/grunt-minify-html": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/grunt-minify-html/-/grunt-minify-html-3.0.0.tgz", + "integrity": "sha512-//Paw/bXj9H+aZltKAC6fGXRbhtr5QKla/ZPOQjMiqZnI1sIrI82+3Q6fIEHhjeW4rJxmZ/Bwc1yJF1L+PySog==", + "dev": true, + "license": "MIT", + "dependencies": { + "each-async": "^1.0.0", + "make-dir": "^1.0.0", + "minimize": "^2.1.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "grunt": ">=0.4.0" + } + }, + "node_modules/gzip-size": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-5.1.1.tgz", + "integrity": "sha512-FNHi6mmoHvs1mxZAds4PpdCS6QG8B4C1krxJsMutgxl5t3+GlRTzzI3NEkifXx2pVsOvJdOGSmIgDhQ55FwdPA==", + "license": "MIT", + "dependencies": { + "duplexer": "^0.1.1", + "pify": "^4.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/homedir-polyfill": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", + "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", + "license": "MIT", + "peer": true, + "dependencies": { + "parse-passwd": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/hooker": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz", + "integrity": "sha512-t+UerCsQviSymAInD01Pw+Dn/usmz1sRO+3Zk1+lx8eg+WKpD2ulcwWqHHL0+aseRBr+3+vIhiG1K1JTwaIcTA==", + "peer": true, + "engines": { + "node": "*" + } + }, + "node_modules/html-minifier": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-4.0.0.tgz", + "integrity": "sha512-aoGxanpFPLg7MkIl/DDFYtb0iWz7jMFGqFhvEDZga6/4QTjneiD8I/NXL1x5aaoCp7FSIT6h/OhykDdPsbtMig==", + "dev": true, + "license": "MIT", + "dependencies": { + "camel-case": "^3.0.0", + "clean-css": "^4.2.1", + "commander": "^2.19.0", + "he": "^1.2.0", + "param-case": "^2.1.1", + "relateurl": "^0.2.7", + "uglify-js": "^3.5.1" + }, + "bin": { + "html-minifier": "cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/html-minifier/node_modules/clean-css": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.4.tgz", + "integrity": "sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A==", + "dev": true, + "license": "MIT", + "dependencies": { + "source-map": "~0.6.0" + }, + "engines": { + "node": ">= 4.0" + } + }, + "node_modules/html-minifier/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/htmlparser2": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", + "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^1.3.1", + "domhandler": "^2.3.0", + "domutils": "^1.5.1", + "entities": "^1.1.1", + "inherits": "^2.0.1", + "readable-stream": "^3.1.1" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "peer": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "peer": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC", + "peer": true + }, + "node_modules/interpret": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz", + "integrity": "sha512-CLM8SNMDu7C5psFCn6Wg/tgpj/bKAg7hc2gWqcuR9OD5Ft9PhBpIu8PLicPeis+xDd6YX2ncI8MCA64I9tftIA==", + "license": "MIT", + "peer": true + }, + "node_modules/is-absolute": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", + "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", + "license": "MIT", + "peer": true, + "dependencies": { + "is-relative": "^1.0.0", + "is-windows": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "peer": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "license": "MIT", + "peer": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-relative": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", + "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", + "license": "MIT", + "peer": true, + "dependencies": { + "is-unc-path": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-unc-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", + "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "unc-path-regex": "^0.1.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC", + "peer": true + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "license": "MIT", + "peer": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/kuler": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-1.0.1.tgz", + "integrity": "sha512-J9nVUucG1p/skKul6DU3PUZrhs0LPulNaeUOox0IyXDi8S4CztTHs1gQphhuZmzXG7VOQSf6NJfKuzteQLv9gQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "colornames": "^1.1.1" + } + }, + "node_modules/liftup": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/liftup/-/liftup-3.0.1.tgz", + "integrity": "sha512-yRHaiQDizWSzoXk3APcA71eOI/UuhEkNN9DiW2Tt44mhYzX4joFoCZlxsSOF7RyeLlfqzFLQI1ngFq3ggMPhOw==", + "license": "MIT", + "peer": true, + "dependencies": { + "extend": "^3.0.2", + "findup-sync": "^4.0.0", + "fined": "^1.2.0", + "flagged-respawn": "^1.0.1", + "is-plain-object": "^2.0.4", + "object.map": "^1.0.1", + "rechoir": "^0.7.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/liftup/node_modules/findup-sync": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-4.0.0.tgz", + "integrity": "sha512-6jvvn/12IC4quLBL1KNokxC7wWTvYncaVUYSoxWw7YykPLuRrnv4qdHcSOywOI5RpkOVGeQRtWM8/q+G6W6qfQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "detect-file": "^1.0.0", + "is-glob": "^4.0.0", + "micromatch": "^4.0.2", + "resolve-dir": "^1.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lower-case": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz", + "integrity": "sha512-2Fgx1Ycm599x+WGpIYwJOvsjmXFzTSc34IwDWALRA/8AopUKAVPwfJ+h5+f85BCp0PWmmJcWzEpxOpoXycMpdA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", + "integrity": "sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es5-ext": "~0.10.2" + } + }, + "node_modules/make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/make-dir/node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/make-iterator": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz", + "integrity": "sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==", + "license": "MIT", + "peer": true, + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/maxmin": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/maxmin/-/maxmin-3.0.0.tgz", + "integrity": "sha512-wcahMInmGtg/7c6a75fr21Ch/Ks1Tb+Jtoan5Ft4bAI0ZvJqyOw8kkM7e7p8hDSzY805vmxwHT50KcjGwKyJ0g==", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "figures": "^3.2.0", + "gzip-size": "^5.1.1", + "pretty-bytes": "^5.3.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/memoizee": { + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.17.tgz", + "integrity": "sha512-DGqD7Hjpi/1or4F/aYAspXKNm5Yili0QDAFAY4QYvpqpgiY6+1jOfqpmByzjxbWd/T9mChbCArXAbDAsTm5oXA==", + "dev": true, + "license": "ISC", + "dependencies": { + "d": "^1.0.2", + "es5-ext": "^0.10.64", + "es6-weak-map": "^2.0.3", + "event-emitter": "^0.3.5", + "is-promise": "^2.2.2", + "lru-queue": "^0.1.0", + "next-tick": "^1.1.0", + "timers-ext": "^0.1.7" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "peer": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz", + "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==", + "license": "ISC", + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimize": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/minimize/-/minimize-2.2.0.tgz", + "integrity": "sha512-IxR2XMbw9pXCxApkdD9BTcH2U4XlXhbeySUrv71rmMS9XDA8BVXEsIuFu24LtwCfBgfbL7Fuh8/ZzkO5DaTLlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "argh": "^0.1.4", + "async": "^2.1.5", + "cli-color": "^1.2.0", + "diagnostics": "^1.1.0", + "emits": "^3.0.0", + "htmlparser2": "^3.9.2", + "uuid": "^3.0.0" + }, + "bin": { + "minimize": "bin/minimize" + } + }, + "node_modules/minimize/node_modules/async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.14" + } + }, + "node_modules/next-tick": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/no-case": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz", + "integrity": "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "lower-case": "^1.1.1" + } + }, + "node_modules/nopt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha512-4GUt3kSEYmk4ITxzB/b9vaIDfUVWN/Ml1Fwl11IlnIG2iaJ9O6WXZ9SrYM9NLI8OCBieN2Y8SWC2oJV0RQ7qYg==", + "license": "ISC", + "peer": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + } + }, + "node_modules/object.defaults": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", + "integrity": "sha512-c/K0mw/F11k4dEUBMW8naXUuBuhxRCfG7W+yFy8EcijU/rSmazOUd1XAEEe6bC0OuXY4HUKjTJv7xbxIMqdxrA==", + "license": "MIT", + "peer": true, + "dependencies": { + "array-each": "^1.0.1", + "array-slice": "^1.0.0", + "for-own": "^1.0.0", + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz", + "integrity": "sha512-3+mAJu2PLfnSVGHwIWubpOFLscJANBKuB/6A4CxBstc4aqwQY0FWcsppuy4jU5GSB95yES5JHSI+33AWuS4k6w==", + "license": "MIT", + "peer": true, + "dependencies": { + "for-own": "^1.0.0", + "make-iterator": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "peer": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", + "integrity": "sha512-GZ+g4jayMqzCRMgB2sol7GiCLjKfS1PINkjmx8spcKce1LiVqcbQreXwqs2YAFXC6R03VIG28ZS31t8M866v6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "peer": true, + "dependencies": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "node_modules/param-case": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-2.1.1.tgz", + "integrity": "sha512-eQE845L6ot89sk2N8liD8HAuH4ca6Vvr7VWAWwt7+kvvG5aBcPmmphQ68JsEG2qa9n1TykS2DLeMt363AAH8/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "no-case": "^2.2.0" + } + }, + "node_modules/parse-filepath": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", + "integrity": "sha512-FwdRXKCohSVeXqwtYonZTXtbGJKrn+HNyWDYVcp5yuJlesTwNH4rsmRZ+GrKAPJ5bLpRxESMeS+Rl0VCHRvB2Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "is-absolute": "^1.0.0", + "map-cache": "^0.2.0", + "path-root": "^0.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT", + "peer": true + }, + "node_modules/path-root": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", + "integrity": "sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg==", + "license": "MIT", + "peer": true, + "dependencies": { + "path-root-regex": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-root-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", + "integrity": "sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/rechoir": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.1.tgz", + "integrity": "sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg==", + "license": "MIT", + "peer": true, + "dependencies": { + "resolve": "^1.9.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "license": "MIT", + "peer": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-dir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", + "integrity": "sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==", + "license": "MIT", + "peer": true, + "dependencies": { + "expand-tilde": "^2.0.0", + "global-modules": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT", + "peer": true + }, + "node_modules/set-immediate-shim": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", + "integrity": "sha512-Li5AOqrZWCVA2n5kryzEmqai6bKSIvpz5oUJHPVj6+dsbD3X1ixtsY5tEnsaNpH3pFAHmG8eIHUrtEtohrg+UQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "dev": true, + "license": "MIT" + }, + "node_modules/timers-ext": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.8.tgz", + "integrity": "sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww==", + "dev": true, + "license": "ISC", + "dependencies": { + "es5-ext": "^0.10.64", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/type": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", + "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "license": "BSD-2-Clause", + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/unc-path-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", + "integrity": "sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/underscore.string": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-3.3.6.tgz", + "integrity": "sha512-VoC83HWXmCrF6rgkyxS9GHv8W9Q5nhMKho+OadDJGzL2oDYbYEppBaCMH6pFlwLeqj2QS+hhkw2kpXkSdD1JxQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "sprintf-js": "^1.1.1", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/underscore.string/node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/upper-case": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz", + "integrity": "sha512-WRbjgmYzgXkCV7zNVpy5YgrHgbBv126rMALQQMrmzOVC4GM2waQ9x7xtm8VU+1yF2kWyPzI9zbZ48n4vSxwfSA==", + "dev": true, + "license": "MIT" + }, + "node_modules/uri-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/uri-path/-/uri-path-1.0.0.tgz", + "integrity": "sha512-8pMuAn4KacYdGMkFaoQARicp4HSw24/DHOVKWqVRJ8LhhAwPPFpdGvdL9184JVmUwe7vz7Z9n6IqI6t5n2ELdg==", + "license": "WTFPL OR MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/v8flags": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz", + "integrity": "sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg==", + "license": "MIT", + "peer": true, + "dependencies": { + "homedir-polyfill": "^1.0.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "peer": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC", + "peer": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..f6634fc --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name": "infarctsize-ai", + "version": "1.0.0", + "description": "https://dev.itk.ppke.hu/infarctsize-ai/infarctsize-ai-frontend", + "main": "Gruntfile.js", + "scripts": { + "build": "grunt", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "MIT", + "dependencies": { + "grunt-contrib-concat": "^2.1.0", + "grunt-contrib-uglify": "^5.2.2", + "grunt-contrib-cssmin": "^5.0.0", + "grunt-contrib-htmlmin": "^3.1.0", + "grunt-minify-html": "^3.0.0" + } +} diff --git a/src/app.html b/src/app.html new file mode 100644 index 0000000..72b0614 --- /dev/null +++ b/src/app.html @@ -0,0 +1,1212 @@ +<!DOCTYPE html> +<html lang="en"> + +<head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + + <title>InfarctSize</title> + <link href="assets/css/main.min.css" rel="stylesheet" /> + <link rel="shortcut icon" href="assets/favicon.ico"> +</head> + +<body onload="_via_init()" onresize="drawing.updateUiComponents()"> + <!-- used by invoke_with_user_inputs() to gather user inputs --> + <input id="fileInput" name="files" style="display: none" type="file" /> + <input type="file" id="projectHelperFileInput" name="fileList" webkitdirectory style="display: none" /> + <div id="user_input_panel"></div> + + <!-- navigation bar --> + <nav aria-label="the main toolbar" id="ui_top_panel" + class="navbar navbar-pharma py-0 navbar-expand-lg navbar-dark mb-1"> + <div class="container-fluid"> + <a class="navbar-brand" href="https://www.pharmahungary.com/" target="_blank"> + <img src="https://www.pharmahungary.com/wp-content/themes/pharma/assets/img/logo.svg" alt="" width="140" + height="40" class="d-inline-block align-text-top" /> + </a> + <button aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation" + class="navbar-toggler" data-bs-target="#navbarSupportedContent" data-bs-toggle="collapse" type="button"> + <span class="navbar-toggler-icon"></span> + </button> + <div class="collapse navbar-collapse" id="navbarSupportedContent"> + <ul class="navbar-nav me-auto mb-2 mb-lg-0"> + <li class="nav-item"> + <a aria-current="page" class="nav-link" href="#" id="nav_home">Home</a> + </li> + <li class="nav-item dropdown"> + <a aria-expanded="false" class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#" + id="projectDropdown" role="button"> + Files + </a> + <ul aria-labelledby="navbarDropdown" class="dropdown-menu"> + <li> + <a class="dropdown-item" href="#" id="nav_project_load">Load project</a> + </li> + <li> + <a class="dropdown-item" href="#" id="nav_project_save">Save project</a> + </li> + <li> + <a class="dropdown-item" href="#" id="nav_annotation_downImage">Save image with annotations</a> + </li> + <li> + <hr class="dropdown-divider" /> + </li> + <li> + <a class="dropdown-item" href="#" id="nav_project_addLocFiles">Add local files</a> + </li> + <li> + <a class="dropdown-item" href="#" id="nav_project_addFilUrl">Add files from URL</a> + </li> + <li> + <a class="dropdown-item" href="#" id="nav_project_addFilAbs">Load image folder</a> + </li> + <li> + <hr class="dropdown-divider" /> + </li> + <li> + <a class="dropdown-item" href="#" id="nav_project_setting">Settings</a> + </li> + </ul> + </li> + <li class="nav-item dropdown"> + <a aria-expanded="false" class="nav-link" href="#" id="nav_annotation_autoAnn" role="button"> + Auto annotation + <i class="bi bi-stars"></i> + </a> + </li> + <li class="nav-item dropdown"> + <a aria-expanded="false" class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#" + id="viewrDropdown" role="button"> + View + </a> + <ul aria-labelledby="navbarDropdown" class="dropdown-menu"> + <li> + <a class="dropdown-item" href="#" id="nav_view_imgGrid">Image grid view</a> + </li> + <li> + <a class="dropdown-item" href="#" id="nav_view_sidebar">Show/hide sidebar</a> + </li> + <li> + <a class="dropdown-item" href="#" id="nav_view_status">Show/hide message</a> + </li> + <li> + <hr class="dropdown-divider" /> + </li> + <li> + <a class="dropdown-item" href="#" id="nav_view_regionBound">Show/hider region boundaries (b)</a> + </li> + <li> + <a class="dropdown-item" href="#" id="nav_view_regionLabel">Show/hide region labels (l)</a> + </li> + </ul> + </li> + <li class="nav-item dropdown"> + <a aria-expanded="false" class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#" + id="helpDropdown" role="button"> + Help + </a> + <ul aria-labelledby="navbarDropdown" class="dropdown-menu"> + <li> + <a class="dropdown-item" href="#" id="nav_help_getStart">Getting started</a> + </li> + <li> + <a class="dropdown-item" href="#" data-bs-target="#Report" data-bs-toggle="offcanvas" + id="nav_help_reportIssue">Report issue</a> + </li> + <li> + <hr class="dropdown-divider" /> + </li> + <li> + <a class="dropdown-item" data-bs-target="#About" data-bs-toggle="offcanvas" href="#" + id="nav_help_about">About</a> + </li> + </ul> + </li> + <li class="nav-item d-none d-lg-block" style="margin-left: 1em"> + <a class="nav-link" data-bs-placement="bottom" data-bs-toggle="tooltip" href="#" id="nav_openProject" + title="Open project"> + <i class="bi bi-archive"></i> + </a> + </li> + <li class="nav-item d-none d-lg-block"> + <a class="nav-link" data-bs-placement="bottom" data-bs-toggle="tooltip" href="#" id="nav_saveProject" + title="Save project"> + <i class="bi bi-save"></i> + </a> + </li> + <li class="nav-item d-none d-lg-block"> + <a class="nav-link" data-bs-placement="bottom" data-bs-toggle="tooltip" href="#" id="nav_settings" + title="Settings"> + <i class="bi bi-gear"></i> + </a> + </li> + <li class="nav-item d-none d-lg-block" style="margin-left: 1em"> + <a class="nav-link" data-bs-placement="bottom" data-bs-toggle="tooltip" href="#" id="nav_imgGrid" + title="Image grid view"> + <i class="bi bi-grid"></i> + </a> + </li> + <li class="nav-item d-none d-lg-block"> + <a class="nav-link" data-bs-placement="bottom" data-bs-toggle="tooltip" href="#" id="nav_sidePanel" + title="Toggle side panel"> + <i class="bi bi-layout-sidebar"></i> + </a> + </li> + <li class="nav-item d-none d-lg-block" style="margin-left: 1em"> + <a class="nav-link" data-bs-placement="bottom" data-bs-toggle="tooltip" href="#" id="nav_autoAnn" + title="Auto annotation"> + <i class="bi bi-stars"></i> + </a> + </li> + <li class="nav-item d-none d-lg-block"> + <a class="nav-link" data-bs-placement="bottom" data-bs-toggle="tooltip" href="#" id="nav_calcAreas" + title="Export results"> + <i class="bi bi-file-earmark-arrow-down"></i> + </a> + </li> + <li class="nav-item d-none d-lg-block" style="margin-left: 1em"> + <a class="nav-link" data-bs-placement="bottom" data-bs-toggle="tooltip" href="#" id="nav_prev" + title="Previous"> + <i class="bi bi-caret-left"></i> + </a> + </li> + <li class="nav-item d-none d-lg-block"> + <a class="nav-link" data-bs-placement="bottom" data-bs-toggle="tooltip" href="#" id="nav_next" title="Next"> + <i class="bi bi-caret-right"></i> + </a> + </li> + <li class="nav-item d-none d-lg-block" style="margin-left: 1em"> + <a class="nav-link disabled" data-bs-placement="bottom" data-bs-toggle="tooltip" href="#" id="nav_undo" + title="Undo"> + <i class="bi bi-arrow-counterclockwise"></i> + </a> + </li> + <li class="nav-item d-none d-lg-block"> + <a class="nav-link disabled" data-bs-placement="bottom" data-bs-toggle="tooltip" href="#" id="nav_redo" + title="Redo"> + <i class="bi bi-arrow-clockwise"></i> + </a> + </li> + <li class="nav-item d-none d-lg-block" style="margin-left: 1em"> + <a class="nav-link" data-bs-placement="bottom" data-bs-toggle="tooltip" href="#" id="nav_zoomIn" + title="Zoom in"> + <i class="bi bi-zoom-in"></i> + </a> + </li> + <li class="nav-item d-none d-lg-block"> + <a class="nav-link" data-bs-placement="bottom" data-bs-toggle="tooltip" href="#" id="nav_zoomOut" + title="Zoom out"> + <i class="bi bi-zoom-out"></i> + </a> + </li> + <li class="nav-item d-none d-lg-block" style="margin-left: 1em"> + <a class="nav-link" data-bs-placement="bottom" data-bs-toggle="tooltip" href="#" id="nav_selAllReg" + title="Select all regions"> + <i class="bi bi-check2-all"></i> + </a> + </li> + <li class="nav-item d-none d-lg-block"> + <a class="nav-link" data-bs-placement="bottom" data-bs-toggle="tooltip" href="#" id="nav_copySelReg" + title="Copy selected regions"> + <i class="bi bi-clipboard-check"></i> + </a> + </li> + <li class="nav-item d-none d-lg-block"> + <a class="nav-link" data-bs-placement="bottom" data-bs-toggle="tooltip" href="#" id="nav_pasteReg" + title="Paste regions"> + <i class="bi bi-clipboard"></i> + </a> + </li> + <li class="nav-item d-none d-lg-block"> + <a class="nav-link" data-bs-placement="bottom" data-bs-toggle="tooltip" href="#" id="nav_delSelReg" + title="Delete selected regions"> + <i class="bi bi-trash3"></i> + </a> + </li> + <li class="nav-item"> + <a aria-current="page" class="nav-link" href="/profile" id="nav_profile"> + Profile + <i class="bi bi-person"></i> + </a> + </li> + <li class="nav-item"> + <a class="nav-link" href="/logout" id="nav_logout"> + Logout + <i class="bi bi-box-arrow-right"></i> + </a> + </li> + </ul> + </div> + </div> + </nav> + <!-- end of navigation bar--> + + <!-- start of main content --> + + <!-- image buffer --> + <div id="image_buffer" style="display: none"></div> + + <!-- end of image buffer --> + + <!-- middle panel --> + <div class="container-fluid"> + <div class="row mb-2"> + <!-- sidebar --> + <div class="col-lg-3 g-0" id="sidebar_container"> + <div class="accordion" id="leftsidebar"> + <div class="accordion-item"> + <h2 class="accordion-header" id="panelsStayOpen-headingOne"> + <button class="accordion-button" type="button" data-bs-toggle="collapse" + data-bs-target="#panelsStayOpen-collapseOne" aria-expanded="true" + aria-controls="panelsStayOpen-collapseOne"> + Project + </button> + </h2> + <div id="panelsStayOpen-collapseOne" class="accordion-collapse collapse show" + aria-labelledby="panelsStayOpen-headingOne"> + <div class="accordion-body"> + <div class="form-floating mb-3"> + <input class="form-control" id="project_name" placeholder="Name of project" type="text" /> + <label for="project_name">Project name</label> + </div> + + <div class="input-group-sm mb-3"> + <select aria-label="Fn list search options" class="form-control" id="filelist_preset_filters_list"> + <option value="all">All files</option> + <option value="files_without_region"> + Show files without regions + </option> + <option value="files_error_loading"> + Files that could not be loaded + </option> + </select> + + <input aria-label="Fn list search text box" class="form-control" id="img_fn_list_regex" + placeholder="Search files" type="text" /> + </div> + + <div aria-hidden="true" class="card mb-3 position-relative overflow-auto" style="height: 150px"> + <div class="card-body position-relative overflow-auto h-25" id="img_fn_list"> + <h5 class="card-title placeholder-glow"> + <span class="placeholder col-6"></span> + </h5> + <p class="card-text placeholder-glow"> + <span class="placeholder col-7"></span> + <span class="placeholder col-4"></span> + <span class="placeholder col-4"></span> + <span class="placeholder col-6"></span> + <span class="placeholder col-8"></span> + </p> + </div> + </div> + + <div aria-label="Sidebar fn list buttons" class="btn-group w-100" role="group"> + <button class="btn btn-primary" id="sidebar_addFiles" type="button"> + Add files + </button> + <button class="btn btn-primary" id="sidebar_addUrl" type="button"> + Add URL + </button> + </div> + </div> + </div> + </div> + <div class="accordion-item"> + <h2 class="accordion-header" id="panelsStayOpen-headingTwo"> + <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" + data-bs-target="#panelsStayOpen-collapseTwo" aria-expanded="false" + aria-controls="panelsStayOpen-collapseTwo"> + Annotations + </button> + </h2> + <div id="panelsStayOpen-collapseTwo" class="accordion-collapse collapse" + aria-labelledby="panelsStayOpen-headingTwo"> + <div class="accordion-body"> + <div id="annotation_editor_container" aria-hidden="true" class="card mb-3 overflow-auto" + style="height: 250px"> + <div class="card-body" id="annotation_editor_panel"> + <h5 class="card-title placeholder-glow"> + <span class="placeholder col-6"></span> + </h5> + <p class="card-text placeholder-glow"> + <span class="placeholder col-7"></span> + <span class="placeholder col-4"></span> + <span class="placeholder col-4"></span> + <span class="placeholder col-6"></span> + <span class="placeholder col-8"></span> + </p> + </div> + </div> + <div aria-label="Sidebar ae list buttons" class="btn-group w-100" role="group"> + <button class="btn btn-outline-primary" id="sidebar_ToggleAEModes" type="button" + data-bs-toggle="button"> + Regions + </button> + <!-- <button class="btn btn-primary" id="sidebar_onImageEditor" type="button">On image editor</button> --> + <button class="btn btn-primary" id="sidebar_UpdateSlices" type="button" data-bs-toggle="tooltip" + data-bs-original-title="Update the groups based on the slices"> + Group slices + </button> + </div> + </div> + </div> + </div> + </div> + </div> + + <div class="col-lg-3 g-0 d-none" id="image_man_container"> + <div class="d-flex flex-column flex-shrink-0 p-3 rounded-3 shadow"> + <a class="d-flex align-items-center mb-3 mb-md-0 me-md-auto link-dark text-decoration-none" href="/"> + <span class="fs-4">Quick image settings</span> + </a> + <hr /> + <div class="container"> + <a> + <label for="brightnessRange" class="form-label">Brightness</label> + <input type="range" class="form-range" min="0" max="200.0" step="2" id="brightnessRange" /> + </a> + <a> + <label for="contrastRange" class="form-label">Contrast</label> + <input type="range" class="form-range" min="0" max="200" step="2" id="contrastRange" /> + </a> + </div> + <hr /> + <a class="d-flex align-items-center mb-3 mb-md-0 me-md-auto link-dark text-decoration-none" href="/"> + <span class="fs-4">Color settings</span> + </a> + <hr /> + <div class="container"> + <a> + <label for="saturationRange" class="form-label">Saturation</label> + <input type="range" class="form-range" min="0" max="200" step="2" id="saturationRange" /> + </a> + <a> + <label for="hueRange" class="form-label">Hue</label> + <input type="range" class="form-range" min="0" max="360" step="2" id="hueRange" /> + </a> + </div> + <hr /> + <div class="container"></div> + <div aria-label="Sidebar ae list buttons" class="btn-group w-100" role="group"> + <button class="btn btn-primary" id="sidebar_reset" type="button"> + Back + </button> + <button class="btn btn-primary" id="image_reset" type="button"> + Reset + </button> + </div> + </div> + </div> + <!-- end of sidebar --> + + <!-- main panel --> + <div class="col-lg"> + <div class="d-flex align-items-center position-absolute d-none" style="z-index: 150; right: 10px" + id="annotation_spinner"> + <div class="container px-1"> + <div class="spinner-grow spinner-grow-sm text-info" role="status"> + <span class="visually-hidden">Loading...</span> + </div> + </div> + <span class="badge rounded-pill text-bg-info">Annotating... </span> + </div> + + <div id="selection_panel" class="container-fluid shadow p-0 bg-body"> + <div class="list-group position-absolute" style="z-index: 40"> + <button aria-current="true" class="list-group-item list-group-item-action active" data-bs-placement="right" + data-bs-toggle="tooltip" id="shape_edit" title="Editor mode" type="button"> + <svg style="width: 24px; height: 24px" viewBox="0 0 24 24"> + <path + d="M10.07,14.27C10.57,14.03 11.16,14.25 11.4,14.75L13.7,19.74L15.5,18.89L13.19,13.91C12.95,13.41 13.17,12.81 13.67,12.58L13.95,12.5L16.25,12.05L8,5.12V15.9L9.82,14.43L10.07,14.27M13.64,21.97C13.14,22.21 12.54,22 12.31,21.5L10.13,16.76L7.62,18.78C7.45,18.92 7.24,19 7,19A1,1 0 0,1 6,18V3A1,1 0 0,1 7,2C7.24,2 7.47,2.09 7.64,2.23L7.65,2.22L19.14,11.86C19.57,12.22 19.62,12.85 19.27,13.27C19.12,13.45 18.91,13.57 18.7,13.61L15.54,14.23L17.74,18.96C18,19.46 17.76,20.05 17.26,20.28L13.64,21.97Z" + fill="currentColor" /> + </svg> + </button> + <button class="list-group-item list-group-item-action" data-bs-placement="right" data-bs-toggle="tooltip" + id="shape_drag" title="Pan tool" type="button"> + <svg style="width: 24px; height: 24px" viewBox="0 0 24 24"> + <path fill="currentColor" + d="M3 16C3 20.42 6.58 24 11 24C14.43 24 17.5 21.91 18.77 18.73L21.33 12.3C21.58 11.66 21.56 10.92 21.18 10.35C20.69 9.61 19.82 9.29 19 9.5L18.22 9.73C17.76 9.85 17.34 10.08 17 10.39V4.5C17 3.12 15.88 2 14.5 2C14.31 2 14.13 2 13.96 2.06C13.75 .89 12.73 0 11.5 0C10.44 0 9.54 .66 9.17 1.59C8.96 1.53 8.73 1.5 8.5 1.5C7.12 1.5 6 2.62 6 4V4.55C5.84 4.5 5.67 4.5 5.5 4.5C4.12 4.5 3 5.62 3 7V16M5 7C5 6.72 5.22 6.5 5.5 6.5S6 6.72 6 7V12H8V4C8 3.72 8.22 3.5 8.5 3.5S9 3.72 9 4V12H11V2.5C11 2.22 11.22 2 11.5 2S12 2.22 12 2.5V12H14V4.5C14 4.22 14.22 4 14.5 4S15 4.22 15 4.5V15H17L18 12.5C18.15 12.05 18.5 11.71 19 11.59L19.5 11.45L16.91 18C15.95 20.41 13.61 22 11 22C7.69 22 5 19.31 5 16V7Z" /> + </svg> + </button> + <!-- + <button class="list-group-item list-group-item-action" data-bs-placement="right" + data-bs-toggle="tooltip" + id="shape_rect" title="Draw rectangle" type="button"> + <svg style="width:24px;height:24px" viewBox="0 0 24 24"> + <path d="M2,4H8V6H16V4H22V10H20V14H22V20H16V18H8V20H2V14H4V10H2V4M16,10V8H8V10H6V14H8V16H16V14H18V10H16M4,6V8H6V6H4M18,6V8H20V6H18M4,16V18H6V16H4M18,16V18H20V16H18Z" + fill="currentColor"/> + </svg> + </button> + <button class="list-group-item list-group-item-action" data-bs-placement="right" + data-bs-toggle="tooltip" + id="shape_circle" title="Draw circle" type="button"> + <svg style="width:24px;height:24px" viewBox="0 0 24 24"> + <path d="M22,9H19.97C18.7,5.41 15.31,3 11.5,3A9,9 0 0,0 2.5,12C2.5,17 6.53,21 11.5,21C15.31,21 18.7,18.6 20,15H22M20,11V13H18V11M17.82,15C16.66,17.44 14.2,19 11.5,19C7.64,19 4.5,15.87 4.5,12C4.5,8.14 7.64,5 11.5,5C14.2,5 16.66,6.57 17.81,9H16V15" + fill="currentColor"/> + </svg> + </button> + <button class="list-group-item list-group-item-action" data-bs-placement="right" + data-bs-toggle="tooltip" + id="shape_ellipse" title="Draw ellipse" type="button"> + <svg style="width:24px;height:24px" viewBox="0 0 24 24"> + <path d="M23,9V15H20.35C19.38,17.12 17.43,18.78 15,19.54V22H9V19.54C5.5,18.45 3,15.5 3,12C3,7.58 7.03,4 12,4C15.78,4 19,6.07 20.35,9H23M17,15V9H18.06C16.85,7.21 14.59,6 12,6C8.13,6 5,8.69 5,12C5,14.39 6.64,16.46 9,17.42V16H15V17.42C16.29,16.9 17.35,16.05 18.06,15H17M19,13H21V11H19V13M11,20H13V18H11V20Z" + fill="currentColor"/> + </svg> + </button> + --> + <button class="list-group-item list-group-item-action" data-bs-placement="right" data-bs-toggle="tooltip" + id="shape_polygon" title="Draw polygon" type="button"> + <svg style="width: 24px; height: 24px" viewBox="0 0 24 24"> + <path + d="M2,2V8H4.28L5.57,16H4V22H10V20.06L15,20.05V22H21V16H19.17L20,9H22V3H16V6.53L14.8,8H9.59L8,5.82V2M4,4H6V6H4M18,5H20V7H18M6.31,8H7.11L9,10.59V14H15V10.91L16.57,9H18L17.16,16H15V18.06H10V16H7.6M11,10H13V12H11M6,18H8V20H6M17,18H19V20H17" + fill="currentColor" /> + </svg> + </button> + <!-- + <button class="list-group-item list-group-item-action" data-bs-placement="right" + data-bs-toggle="tooltip" + id="shape_pen" title="Draw with pencil" type="button"> + <svg style="width:24px;height:24px" viewBox="0 0 24 24"> + <path d="M14.06,9L15,9.94L5.92,19H5V18.08L14.06,9M17.66,3C17.41,3 17.15,3.1 16.96,3.29L15.13,5.12L18.88,8.87L20.71,7.04C21.1,6.65 21.1,6 20.71,5.63L18.37,3.29C18.17,3.09 17.92,3 17.66,3M14.06,6.19L3,17.25V21H6.75L17.81,9.94L14.06,6.19Z" + fill="currentColor"/> + </svg> + </button> + --> + <button class="list-group-item list-group-item-action" data-bs-placement="right" data-bs-toggle="tooltip" + id="shape_remove" + title="Reduce points tool (for poly and pencil elements). Also you can select a region and press R to apply for the selected mask." + type="button"> + <svg style="width: 24px; height: 24px" viewBox="0 0 24 24"> + <path + d="M20 20V17H22V20C22 21.11 21.1 22 20 22H17V20H20M2 20V17H4V20H7V22H4C2.9 22 2 21.1 2 20M10 20H14V22H10V20M14.59 8L12 10.59L9.41 8L8 9.41L10.59 12L8 14.59L9.41 16L12 13.41L14.59 16L16 14.59L13.41 12L16 9.41L14.59 8M20 10H22V14H20V10M2 10H4V14H2V10M2 4C2 2.89 2.9 2 4 2H7V4H4V7H2V4M22 4V7H20V4H17V2H20C21.1 2 22 2.9 22 4M10 2H14V4H10V2Z" + fill="currentColor" /> + </svg> + </button> + <button class="list-group-item list-group-item-action" data-bs-placement="right" data-bs-toggle="tooltip" + id="shape_trim" title="Trim (for poly and pencil elements)" type="button"> + <svg style="width: 24px; height: 24px" viewBox="0 0 24 24"> + <path + d="M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M7,13H17V11H7" + fill="currentColor" /> + </svg> + </button> + <button class="list-group-item list-group-item-action" data-bs-placement="right" data-bs-toggle="tooltip" + id="image_settings" title="Image settings" type="button"> + <svg style="width: 24px; height: 24px" viewBox="0 0 24 24"> + <path fill="currentColor" + d="M3,17V19H9V17H3M3,5V7H13V5H3M13,21V19H21V17H13V15H11V21H13M7,9V11H3V13H7V15H9V9H7M21,13V11H11V13H21M15,9H17V7H21V5H17V3H15V9Z" /> + </svg> + </button> + </div> + </div> + + <div class="container-fluid" id="display_area"> + <div class="container-fluid overflow-hidden rounded display_area_content d-none" + style="height: 90vh; background-color: #fff;" id="image_panel_container"> + <div class="container container-fluid" id="image_panel"> + <!-- buffer images using <img> element will be added here --> + <canvas class="position-absolute" height="1" id="region_canvas" tabindex="1" width="1" + style="z-index: 3; transform-origin: top left"> + </canvas> + </div> + </div> + <!--image grid panel--> + <div id="image_grid_panel" class="display_area_content display_none"> + <div class="card mb-2"> + <div class="card-header" style="padding: 0 0.5rem"> + <div class="row g-2 align-items-center"> + <div class="col-auto"> + <label for="image_grid_toolbar_group_by_select" class="col-form-label">Group by </label> + </div> + <div class="col-auto"> + <select aria-label="Grid panel selector 1" class="form-select form-select-sm" + id="image_grid_toolbar_group_by_select"></select> + </div> + </div> + </div> + <div class="card-body d-none" id="image_grid_group_panel_card" style="padding: 0.2rem 0.5rem"> + <div id="image_grid_group_panel"></div> + </div> + </div> + + <div class="container mb-1"> + <div class="row align-items-center" id="image_grid_toolbar"> + <div class="input-group input-group-sm"> + <span class="input-group-text">Selected</span> + <span id="image_grid_group_by_sel_img_count" class="input-group-text">0</span> + <span class="input-group-text">of</span> + <span id="image_grid_group_by_img_count" class="input-group-text">0</span> + <span class="input-group-text">images in current group, show</span> + <button class="btn btn-secondary dropdown-toggle" type="button" id="image_grid_show_image_policy" + data-bs-toggle="dropdown" aria-expanded="false"> + Select option + </button> + <ul class="dropdown-menu" id="image_grid_show_image_policy_dropdown" + aria-labelledby="image_grid_show_image_policy"> + <li> + <a class="dropdown-item" href="#" data-value="all">all images (paginated)</a> + </li> + <li> + <a class="dropdown-item" href="#" data-value="first_mid_last">only first, middle and last + image</a> + </li> + <li> + <a class="dropdown-item" href="#" data-value="even_indexed">even indexed images (i.e. + 0,2,4,...)</a> + </li> + <li> + <a class="dropdown-item" href="#" data-value="odd_indexed">odd indexed images (i.e. 1,3,5,...)</a> + </li> + <li> + <a class="dropdown-item" href="#" data-value="gap5">images 1, 5, 10, 15,...</a> + </li> + <li> + <a class="dropdown-item" href="#" data-value="gap25">images 1, 25, 50, 75, ...</a> + </li> + <li> + <a class="dropdown-item" href="#" data-value="gap50">images 1, 50, 100, 150, ...</a> + </li> + </ul> + </div> + </div> + <div class="row align-items-center"> + <div class="input-group input-group-sm" id="image_grid_nav"> + <!-- Navigation content here --> + </div> + </div> + </div> + + <div id="image_grid_content"> + <div id="image_grid_content_img"></div> + <svg xmlns:xlink="http://www.w3.org/2000/svg" id="image_grid_content_rshape"></svg> + </div> + + <div id="image_grid_info"></div> + </div> + <!-- end of image grid panel --> + + <!-- settings panel --> + <div id="settings_panel" class="display_area_content container-fluid d-none"> + <div class="row"> + <div class="col"> + <div class="card"> + <div class="card-body"> + <h5 class="card-title">Settings</h5> + <div class="row mb-3 p-3"> + <div class="card"> + <div class="card-body"> + <h5 class="card-title">Top panel</h5> + <div class="col-auto"> + <label for="settings_quickButtons" class="form-label">Quick buttons <small> (enable or + disable quick buttons on the top bar)</small></label> + <div class="container my-4" id="settings_quickButtons"> + <div class="row row-cols-1 row-cols-sm-2 row-cols-md-4 g-3"> + <div class="col"> + <input type="checkbox" class="btn-check" id="btn-openProject" autocomplete="off"> + <label class="btn btn-outline-primary" for="btn-openProject">Open project</label> + </div> + <div class="col"> + <input type="checkbox" class="btn-check" id="btn-saveProject" autocomplete="off"> + <label class="btn btn-outline-primary" for="btn-saveProject">Save project</label> + </div> + <div class="col"> + <input type="checkbox" class="btn-check" id="btn-settings" autocomplete="off"> + <label class="btn btn-outline-primary" for="btn-settings">Settings</label> + </div> + <div class="col"> + <input type="checkbox" class="btn-check" id="btn-imgGrid" autocomplete="off"> + <label class="btn btn-outline-primary" for="btn-imgGrid">Image grid view</label> + </div> + <div class="col"> + <input type="checkbox" class="btn-check" id="btn-sidePanel" autocomplete="off"> + <label class="btn btn-outline-primary" for="btn-sidePanel">Toggle side panel</label> + </div> + <div class="col"> + <input type="checkbox" class="btn-check" id="btn-autoAnn" autocomplete="off"> + <label class="btn btn-outline-primary" for="btn-autoAnn">Auto annotation</label> + </div> + <div class="col"> + <input type="checkbox" class="btn-check" id="btn-calcAreas" autocomplete="off"> + <label class="btn btn-outline-primary" for="btn-calcAreas">Export results</label> + </div> + <div class="col"> + <input type="checkbox" class="btn-check" id="btn-prev" autocomplete="off"> + <label class="btn btn-outline-primary" for="btn-prev">Previous</label> + </div> + <div class="col"> + <input type="checkbox" class="btn-check" id="btn-next" autocomplete="off"> + <label class="btn btn-outline-primary" for="btn-next">Next</label> + </div> + <div class="col"> + <input type="checkbox" class="btn-check" id="btn-undo" autocomplete="off"> + <label class="btn btn-outline-secondary" for="btn-undo">Undo</label> + </div> + <div class="col"> + <input type="checkbox" class="btn-check" id="btn-redo" autocomplete="off"> + <label class="btn btn-outline-secondary" for="btn-redo">Redo</label> + </div> + <div class="col"> + <input type="checkbox" class="btn-check" id="btn-zoomIn" autocomplete="off"> + <label class="btn btn-outline-primary" for="btn-zoomIn">Zoom in</label> + </div> + <div class="col"> + <input type="checkbox" class="btn-check" id="btn-zoomOut" autocomplete="off"> + <label class="btn btn-outline-primary" for="btn-zoomOut">Zoom out</label> + </div> + <div class="col"> + <input type="checkbox" class="btn-check" id="btn-selAllReg" autocomplete="off"> + <label class="btn btn-outline-primary" for="btn-selAllReg">Select all + regions</label> + </div> + <div class="col"> + <input type="checkbox" class="btn-check" id="btn-copySelReg" autocomplete="off"> + <label class="btn btn-outline-primary" for="btn-copySelReg">Copy selected + regions</label> + </div> + <div class="col"> + <input type="checkbox" class="btn-check" id="btn-pasteReg" autocomplete="off"> + <label class="btn btn-outline-primary" for="btn-pasteReg">Paste regions</label> + </div> + <div class="col"> + <input type="checkbox" class="btn-check" id="btn-delSelReg" autocomplete="off"> + <label class="btn btn-outline-primary" for="btn-delSelReg">Delete selected + regions</label> + </div> + </div> + </div> + </div> + </div> + </div> + </div> + <div class="row mb-3 p-3"> + <div class="card"> + <div class="card-body"> + <h5 class="card-title">Optimization</h5> + <div class="col-auto"> + <div class="form-check form-switch"> + <input class="form-check-input" type="checkbox" role="switch" id="settings_autoSave" + onchange="settings.projectAutoSave()" disabled /> + <label class="form-check-label" for="settings_autoSave">Project auto save + </label> + </div> + </div> + <div class="col-auto"> + <label for="settings_preloadBuffer" class="form-label">Preload Buffer Size <small> (how many + images to preload in memory; more images will consume more memory)</small></label> + <input type="range" class="form-range" min="2" max="50" id="settings_preloadBuffer" /> + </div> + <div class="col-auto"> + <label for="settings_serverReduceRatio" class="form-label">Vertex reduction ratio <small> + (reduce the number of vertices in the region shape by this ratio on AI processed + regions)</small></label> + <input type="range" class="form-range" min="0" max="5" id="settings_serverReduceRatio" /> + </div> + </div> + </div> + </div> + <div class="row mb-3 p-3"> + <div class="card"> + <div class="card-body"> + <h5 class="card-title">Annotation panel</h5> + <div class="col-auto"> + <div class="form-check form-switch"> + <input class="form-check-input" type="checkbox" role="switch" + id="settings_highlightRegion" onchange="settings.enableHighlightRegion()" /> + <label class="form-check-label" for="settings_highlightRegion">Highlight the selected + region</label> + </div> + </div> + <div class="col-auto"> + <div class="form-check form-switch"> + <input class="form-check-input" type="checkbox" role="switch" id="settings_scrollToRow" + onchange="settings.enableScrollingCheckbox()" /> + <label class="form-check-label" for="settings_scrollToRow">Scroll to annotation list row, + when select a + region</label> + </div> + </div> + <div class="col-auto"> + <div class="form-check form-switch"> + <input class="form-check-input" type="checkbox" role="switch" id="settings_showScore" + onchange="settings.showScore()" /> + <label class="form-check-label" for="settings_showScore">Show score (AI) in annotation + list <small>(if the region is AI processed)</small> + </label> + </div> + </div> + <div class="col-auto"> + <div class="form-check form-switch"> + <input class="form-check-input" type="checkbox" role="switch" id="settings_showArea" + onchange="settings.showPixelArea()" /> + <label class="form-check-label" for="settings_showArea">Show pixel area in annotation list + for each region + </label> + </div> + </div> + </div> + </div> + + </div> + <div class="row mb-3 p-3"> + <div class="card"> + <div class="card-body"> + <h5 class="card-title">Scoring (AI)</h5> + <div class="col-auto"> + <label for="settings_scoring" class="col-form-label">Score threshold</label> + <input type="text" class="form-control" id="settings_scoring" placeholder="E.g. 0.1" /> + </div> + <div class="col-auto mt-2"> + <div class="form-check form-switch"> + <input class="form-check-input" type="checkbox" role="switch" id="settings_showScoreColor" + onchange="settings.showScoreColor()" /> + <label class="form-check-label" for="settings_showScoreColor">Show score color in + annotation list <small>(if the region is AI processed)</small> + </label> + </div> + </div> + <div class="row mb-3 py-3"> + <label for="settings_scoreHighColor" class="col-sm-2 col-form-label">Higher than threshold + (row color)</label> + <div class="col-auto"> + <input type="color" class="form-control form-control-color" id="settings_scoreHighColor" + value="#20c997" title="Choose your color" /> + </div> + <label for="settings_scoreLowColor" class="col-sm-2 col-form-label">Lower than threshold + (row color)</label> + <div class="col-auto"> + <input type="color" class="form-control form-control-color" id="settings_scoreLowColor" + value="#c56771" title="Choose your color" /> + </div> + </div> + </div> + </div> + + </div> + <div class="row mb-3 p-3"> + <div class="card"> + <div class="card-body"> + <h5 class="card-title">Annotation editor</h5> + </div> + <ul class="nav nav-pills mb-3"> + <li class="nav-item"> + <a class="nav-link active" aria-current="page" href="#" + onclick="show_region_attributes_update_panel()" id="button_show_region_attributes" + title="Show region attribute editor">Region attributes</a> + </li> + <li class="nav-item"> + <a class="nav-link" href="#" onclick="show_file_attributes_update_panel()" + id="button_show_file_attributes" title="Show file attribute editor">File attributes</a> + </li> + </ul> + <div class="row"> + <div class="col-auto" id="attributes_update_panel"> + <div class="row"> + <div class="col"> + <div class="input-group mb-3"> + <input type="text" class="form-control" placeholder="attribute name" + id="user_input_attribute_id" aria-label="Attribute editor things"> + <button class="btn btn-outline-secondary" type="button" + onclick="add_new_attribute_from_user_input()" id="button_add_new_attribute"> + <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" + class="bi bi-plus-circle" viewBox="0 0 16 16"> + <path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16" /> + <path + d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4" /> + </svg> + </button> + <button class="btn btn-outline-secondary" type="button" + onclick="delete_existing_attribute_with_confirm()" id="button_del_attribute"> + <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" + class="bi bi-dash-circle" viewBox="0 0 16 16"> + <path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16" /> + <path d="M4 8a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7A.5.5 0 0 1 4 8" /> + </svg> + </button> + </div> + </div> + </div> + <div class="row"> + <div class="col mb-3"> + <select class="form-select" aria-label="List of existing attributes" + title="List of existing attributes" onchange="update_current_attribute_id(this)" + id="attributes_name_list"></select> + </div> + </div> + <div class="row"> + <div id="attribute_properties" class="col"></div> + </div> + <div class="row"> + <div id="attribute_options" class="col"></div> + </div> + </div> + <div class="col-auto"> + <label for="attributes_example" class="form-label">Preview</label> + <div class="card" id="attributes_example"> + + </div> + </div> + </div> + </div> + </div> + </div> + </div> + </div> + </div> + </div> + <!-- End of settings panel --> + + <!-- start panel --> + <div class="display_area_content container-fluid" id="page_start_info"> + <div class="card"> + <div class="card-body"> + <h5 class="card-title">Welcome</h5> + <h6 class="card-subtitle mb-2 text-muted">InfarctSize-AI</h6> + <p class="card-text"> + To start using InfarctSize, select images and draw regions.<br> + Use settings to fine-tune the application.<br> + Save your project to continue later. You can also load a saved project to continue annotation.<br> + Also remember to save your project before closing this application so that you can load it later to + continue annotation.<br> + For help, see the help page. + </p> + <a href="#" class="card-link" id="start_selectImages">Select images</a> + <a href="#" class="card-link" id="start_addUrlImages">Add images from folder</a> + <a href="#" class="card-link" id="start_settings">Settings</a> + <a href="#" class="card-link" id="start_loadProject">Load project</a> + <a href="#" class="card-link" id="start_gettingStarted">Help</a> + </div> + </div> + </div> + <!-- end of start panel --> + + <!-- 404 page --> + <div class="display_area_content container-fluid" id="page_404"> + <div class="card"> + <div class="card-body"> + <h5 class="card-title">404 - File not found</h5> + <h6 class="card-subtitle mb-2 text-muted" id="page_404_filename"> + + </h6> + <p class="card-text"> + We recommend that you to select the folder which contains the project images. <small>(Select + folder)</small> + Another approach is to select manually and add this file. <small>(Select images)</small> + </p> + <a href="#" class="card-link" id="404_selectImages">Select images</a> + <a href="#" class="card-link" id="404_selectFolder">Select folder</a> + <a href="#" class="card-link" id="404_gettingStarted">Help</a> + <a href="#" class="card-link" id="404_settings">Settings</a> + </div> + </div> + </div> + <!-- end of 404 page --> + + <!-- help panel --> + <div class="display_area_content container overflow-y-scroll" style="height: 30em" id="page_getting_started"> + <div class="row"> + <div class="col-4"> + <nav aria-label="Getting started panel sections" id="navbar-gettingstarted" + class="h-100 flex-column align-items-stretch pe-4 border-end"> + <nav aria-label="Load section" class="nav nav-pills flex-column"> + <a class="nav-link" href="#item-1">Load images</a> + <nav aria-label="Load section in section" class="nav nav-pills flex-column"> + <a class="nav-link ms-3 my-1" href="#item-1-1">Method 1</a> + <a class="nav-link ms-3 my-1" href="#item-1-2">Method 2</a> + <a class="nav-link ms-3 my-1" href="#item-1-3">Method 3</a> + </nav> + <a class="nav-link" href="#item-2">Draw regions</a> + <nav aria-label="Drawing section" class="nav nav-pills flex-column"> + <a class="nav-link ms-3 my-1" href="#item-2-1">Rectangle, Circle and Ellipse</a> + <a class="nav-link ms-3 my-1" href="#item-2-2">Point</a> + <a class="nav-link ms-3 my-1" href="#item-2-3">Polygon</a> + </nav> + <a class="nav-link" href="#item-3">Create Annotations</a> + <a class="nav-link" href="#item-4">Export Annotations</a> + <a class="nav-link" href="#item-5">Save Project</a> + </nav> + </nav> + </div> + + <div class="col-8"> + <div data-bs-spy="scroll" data-bs-target="#navbar-gettingstarted" data-bs-smooth-scroll="true" + tabindex="0"> + <div id="item-1"> + <h4>Load Images</h4> + <p> + The first step is to load all the images that you wish + to annotate. There are multiple ways to add images to a + VIA project. Choose the method that suits your use case. + </p> + </div> + <div id="item-1-1"> + <h5> + Selecting local files using browser's file selector + </h5> + <p>Click Project -> Add local files.</p> + <p>Select desired images and click Open</p> + </div> + <div id="item-1-2"> + <h5>Adding files from URL or absolute path</h5> + <p>Click Project -> Add files from URL</p> + <p>Enter URL and click OK</p> + </div> + <div id="item-1-3"> + <h5> + Adding files from list of url or absolute path stored in + text file + </h5> + <p> + Create a text file containing URL and absolute path (one + per line) + </p> + <p>Click Project -> Add url or path from text file</p> + </div> + <div id="item-2"> + <h4>Draw Regions</h4> + <p> + Select a region shape (rectangle, circle, ellipse, + polygon, point) from the shapes panel and draw regions + as follows + </p> + </div> + <div id="item-2-1"> + <h5>Rectangle, Circle and Ellipse</h5> + <p> + Press left mouse button, drag mouse cursor and release + mouse button. + </p> + <p> + To define a point inside an existing region, click + inside the region to select it (if not already + selected), now press left mouse button, drag and release + to draw region inside existing region. + </p> + <p> + To select, click inside the region. If the click point + contains multiple regions, then clicking multiple times + at that location shuffles selection through those + regions. + </p> + </div> + <div id="item-2-2"> + <h5>Point</h5> + <p>Click to define points.</p> + <p> + To draw a region inside existing region, click inside + the region to select it (if not already selected), now + click again to define the point. + </p> + <p>To select, click on (or near) the existing point.</p> + </div> + <div id="item-2-3"> + <h5>Polygon and Polyline</h5> + <p>Click to define vertices.</p> + <p> + Press [Enter] to finish drawing the region or press + [Esc] to cancel. + </p> + <p> + If the first vertex needs to be defined inside an + existing region, click inside the region to select it + (if not already selected), now click again to define the + vertex. + </p> + <p> + To select, click inside the region. If the click point + contains multiple regions, then clicking multiple times + at that location shuffles selection through those + regions. + </p> + </div> + <div id="item-3"> + <h4>Create Annotations</h4> + <p> + For a more detailed description of this step, see + Creating Annotations : VIA User Guide. Click the + Settings icon to show attributes editor panel and add + the desired file or region attributes (e.g. name). Use + Ctrl + space to toggle the list of annotations. + </p> + </div> + <div id="item-4"> + <h5>Export Annotations</h5> + <p> + To export the annotations in json or csv format, click + Annotation -> Export annotations in top menubar. + </p> + </div> + <div id="item-5"> + <h5>Save Project</h5> + <p> + To save the project, click Project -> Save in top + menubar. + </p> + </div> + </div> + </div> + </div> + </div> + <!-- end of help panel --> + + <!-- Loading screen --> + <div class="display_area_content container-fluid" id="page_load_ongoing"> + <div style="text-align: center"> + <a href="https://www.robots.ox.ac.uk/~vgg/software/via/"> + <svg height="160" style="background-color: #212121" viewbox="0 0 400 160"> + <use xlink:href="#via_logo"></use> + </svg> + </a> + <div style="margin-top: 4rem">Loading ...</div> + </div> + </div> + <!-- end of Loading screen --> + + <div class="offcanvas offcanvas-start" id="About" tabindex="-1"> + <div class="offcanvas-header"> + <h5 class="offcanvas-title" id="offcanvasAbout"> + About InfarctSize-AI + </h5> + <button aria-label="Close" class="btn-close" data-bs-dismiss="offcanvas" type="button"></button> + </div> + <div class="offcanvas-body"> + <div></div> + + <p> + <button class="btn btn-primary" type="button" data-bs-toggle="collapse" + data-bs-target="#collapseExample" aria-expanded="false" aria-controls="collapseExample"> + License + </button> + </p> + <div class="collapse" id="collapseExample"> + <div class="card card-body"></div> + </div> + </div> + </div> + <!-- end of page_about --> + <!-- Report issue --> + <div class="offcanvas offcanvas-start" id="Report" tabindex="-1"> + <div class="offcanvas-header"> + <h5 class="offcanvas-title" id="offcanvasReport"> + Report issue + </h5> + <button aria-label="Close" class="btn-close" data-bs-dismiss="offcanvas" type="button"></button> + </div> + <div class="offcanvas-body"> + <div> + <h4>Do you have a problem?</h4> + <p> + If you encounter any problems or have feedback about our + product or service, please let us know by sending an email + to + <a href="mailto:infarctsize@pharmahungary.com">infarctsize@pharmahungary.com</a>. Our team will review + your report and take appropriate + action to resolve the issue as soon as possible. + </p> + + <h4>Contact</h4> + <p> + <em>Pharmahungary Group - Infarctsize</em><br /> + <strong>Email:</strong> + <a href="mailto:infarctsize@pharmahungary.com">infarctsize@pharmahungary.com</a><br /> + </p> + + <h5>Versions</h5> + <ul> + <li>InfarctSize-AI 1.0.0 (beta)</li> + </ul> + </div> + </div> + </div> + <!-- end of page_report --> + </div> + </div> + <!-- end of main panel --> + </div> + </div> + <!-- end of middle panel--> + + <!-- loading panel --> + <div class="modal fade" id="staticBackdropProgress" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" + aria-labelledby="staticBackdropLabel" aria-hidden="true"> + <div class="modal-dialog modal-dialog-centered"> + <div class="modal-content"> + <div class="modal-header"> + <h1 class="modal-title fs-5" id="staticBackdropLabel"> + Please wait... + </h1> + </div> + <div class="modal-body"> + <div class="progress"> + <div id="annotationProgress" class="progress-bar" role="progressbar" + aria-label="Progressbar for annotations" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div> + </div> + </div> + </div> + </div> + </div> + <!-- end of loading panel --> + <!-- helper modal --> + <div class="modal fade" id="staticBackdropModal" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" + aria-labelledby="modalTitle" aria-hidden="true"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <h1 class="modal-title fs-5" id="modalTitle"></h1> + <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> + </div> + <div class="modal-body" id="modalBody"></div> + <div class="modal-footer" id="modalFooter"></div> + </div> + </div> + </div> + <!-- end of helper modal --> + + <!-- message panel --> + <div aria-atomic="true" aria-live="polite" class="toast" id="liveToast" role="alert" + style="position: absolute; bottom: 10px; right: 10px"> + <div class="toast-header"> + <span class="badge text-bg-primary me-auto" id="toastAddress">Hello</span> + <small id="toastInfo">now</small> + <button aria-label="Close" class="btn-close" data-bs-dismiss="toast" type="button"></button> + </div> + <div class="toast-body" id="toastBody">Hello!</div> + </div> + <!-- end of message panel --> + + <!-- end of main panel --> + + <div id="loading" class="d-flex justify-content-center align-items-center" style=" + height: 100vh; + width: 100vw; + position: fixed; + top: 0; + left: 0; + background-color: rgba(0, 0, 0, 0.5); + z-index: 9999; + "> + <div class="spinner-border text-primary" role="status"> + <span class="visually-hidden">Loading...</span> + </div> + </div> + + <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" + integrity="sha256-CDOy6cOibCWEdsRiZuaHf8dSGGJRYuBGC+mjoJimHGw=" crossorigin="anonymous"></script> + <script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js" + integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script> + <script> + $(window).on("load", function () { + $("#loading").fadeOut(500).addClass("d-none"); + }); + </script> + + <script src="https://cdn.jsdelivr.net/npm/@panzoom/panzoom@4.5.1/dist/panzoom.min.js" + integrity="sha256-9PI4Hcqdhk6UDuIArOUvzRL9S/uJRmDpkA2o4gIigDs=" crossorigin="anonymous"></script> + + <script src="assets/js/main.js"></script> + + <div style="display:none" class="d-none" id="identifier"> + InfarctSize-AI + </div> +</body> + +</html> \ No newline at end of file diff --git a/src/css/main.css b/src/css/main.css new file mode 100644 index 0000000..63edc5c --- /dev/null +++ b/src/css/main.css @@ -0,0 +1,247 @@ +@import url("https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"); +@import url("https://fonts.googleapis.com/css2?family=Open+Sans:wght@200;300;400;500;600;700&display=swap"); +@import url("https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"); + +/* image grid view */ +#image_grid_panel #image_grid_content { position:relative; overflow:hidden; margin:0; padding:0; outline:none; } +#image_grid_panel #image_grid_content #image_grid_content_img img { margin:0.3em; padding:0; border: 2px solid #3db4c4;} +#image_grid_panel #image_grid_content #image_grid_content_img .not_sel { opacity:0.6; border: none;} +#image_grid_panel #image_grid_content #image_grid_content_img { position:absolute; top:0; left:0; width:100%; height:100%; } +#image_grid_panel #image_grid_content #image_grid_content_rshape { position:absolute; top:0; left:0; width:100%; height:100%; pointer-events:none; } +#image_grid_panel #image_grid_content img { float:left; margin:0; } + +::-webkit-scrollbar { + width: 10px; + height: 10px; +} + +::-webkit-scrollbar-track { + background-color: #e4e4e4; + border-radius: 50px; +} + +::-webkit-scrollbar-thumb { + background-color: gray; + border-radius: 50px; +} + + +.btn-primary { + color: #fff; + background-color: #3db4c4; + border-color: #3db4c4; +} + +.btn-primary:hover { + color: #fff; + background-color: #1c4073; + border-color: #272b2d; +} + +.nav-tabs .nav-link { + color: #1c4073; +} + +.form-range::-webkit-slider-thumb { + background-color: #3db4c4; + border: none; +} + +.form-check .form-switch:checked { + background-color: #3db4c4; +} + +.form-check .form-switch { + color: #3db4c4; +} + +.landing-text-card { + color: #fff; +} + +h1, h2 { + color: #3db4c4; +} + +.text-primary { + color: #3db4c4 !important; +} + +.bg-primary { + background-color: #3db4c4 !important; +} + +.bg-secondary { + background-color: #bdf283 !important; +} + +.underline { + text-decoration: underline; + text-decoration-color: #bdf283; + border-radius: 10px; +} + +.navbar-pharma { + background-color: #3db4c4; +} + +.navbar-pharma .nav-link { + color: #fff; +} + +body { + background-color: #f8f9fa !important; +} + +.navbar { + background-color: #f8f9fa !important; +} + +.navbar .nav-link { + color: #1c4073; +} + +.landing-text { + color: #1c4073; + font-weight: 600; +} + +.navbar .nav-link:hover { + color: #1c4073; +} + +.navbar .navbar-brand { + color: #fff; +} + +.navbar .navbar-brand:hover { + color: #fff; +} + +.navbar .nav-link.disabled { + color: #8a8484; +} + +.navbar .button { + color: #fff; + background-color: #3db4c4; + border-color: #3db4c4; +} + +.btn-outline-primary { + color: #3db4c4; + border-color: #3db4c4; +} + +.btn-outline-primary.active { + color: #fff; + background-color: #3db4c4; + border-color: #3db4c4; +} + +.btn-outline-primary:hover { + color: #fff; + background-color: #1c4073; + border-color: #3db4c4; +} + +.card-link { + color: #1c4073; +} + +.accordion-button:not(.collapsed) { + color: #fff; + background-color: #3db4c4; + border-color: #3db4c4; +} + +.accordion-button:not(.collapsed)::after { + color: #fff; + background-color: #3db4c4; + border-color: #3db4c4; +} + +.accordion-button:not(.collapsed):hover { + color: #fff; + background-color: #1c4073; + border-color: #3db4c4; +} + +.accordion-button:not(.collapsed):hover::after { + color: #fff; + background-color: #1c4073; + border-color: #3db4c4; +} + +.accordion-button:not(.collapsed):focus { + box-shadow: 0 0 0 0.25rem rgba(61, 180, 196, 0.5); +} + +.accordion-button:not(.collapsed):focus::after { + box-shadow: 0 0 0 0.25rem rgba(61, 180, 196, 0.5); +} + +.accordion-button:not(.collapsed):active { + color: #fff; + background-color: #1c4073; + border-color: #3db4c4; +} + + +.list-group-item.active { + color: #fff; + background-color: #3db4c4; + border-color: #3db4c4; +} + +.list-group-item.active:hover { + color: #fff; + background-color: #1c4073; + border-color: #3db4c4; +} + +.list-group-item.active:focus { + z-index: 2; + color: #fff; + background-color: #1c4073; + border-color: #3db4c4; +} + +.list-group-item.active.focus { + z-index: 2; + color: #fff; + background-color: #1c4073; + border-color: #3db4c4; +} + + +.btn-check:checked + .btn-outline-primary { + color: #fff; + background-color: #3db4c4; + border-color: #3db4c4; +} + +/* Hover state */ +.btn-outline-primary:hover { + color: #fff; + background-color: #1c4073; /* A darker shade of the primary color */ + border-color: #3db4c4; +} + +/* Active state (when the button is pressed) */ +.btn-outline-primary:active { + color: #fff; + background-color: #1c4073; /* A darker shade of the primary color */ + border-color: #3db4c4; +} + +/* For disabled buttons, to slightly change appearance on hover (optional) */ +.btn-outline-secondary:disabled:hover { + color: #3db4c4; + background-color: #fff; + border-color: #3db4c4; +} + +.table { + --bs-table-bg: transparent; /* Set to your desired default table background color */ +} \ No newline at end of file diff --git a/src/js/buffer.js b/src/js/buffer.js new file mode 100644 index 0000000..3d1658f --- /dev/null +++ b/src/js/buffer.js @@ -0,0 +1,509 @@ +// buffer.js +// Description: This file contains the code for the ImageBuffer class. +// ImageBuffer class is responsible for loading images into the buffer and displaying them on the image panel. It also handles the preloading of images to improve the user experience. (mainly legacy code from VIA) +class ImageBuffer { + constructor() { + this.loading = $("#loading"); + this.imgLoading = false; // + this.imgLoaded = false; + this.bufferIdList = []; //_via_buffer_img_index_list + this.bufferTimestamp = []; //_via_buffer_img_shown_timestamp + this.preLoadedId = -1; //_via_buffer_preload_img_index + this.preLoadPromiseList = []; + } + + stopLoading() { + this.imgLoading = false; + } + + showImage(idx) { + this.loading.removeClass("d-none"); + if (this.imgLoading) { + return; + } + + let imgID = _via_image_id_list[idx]; + + if (!_via_img_metadata.hasOwnProperty(imgID)) { + this.loading.addClass("d-none"); + Message.showError("The requested image does not exist!"); + return; + } + + if (_via_img_fileref[imgID] === undefined || !(_via_img_fileref[imgID] instanceof File)) { + if (_via_img_src[imgID] === undefined || _via_img_src[imgID] === "") { + if (is_url(_via_img_metadata[imgID].filename)) { + _via_img_src[imgID] = _via_img_metadata[imgID].filename; + this.showImage(idx); + return; + } else { + this.searchPathSearch(idx); + } + } + } + + if (this.bufferIdList.includes(idx)) { + this.imgLoaded = false; + this.showImageFromBuffer(idx).then( + function (_) { + Promise.all(this.preLoadPromiseList).then(function (_) { + this.preLoadPromiseList = []; + let preloadPromise = this.startPreload(idx, 0); + this.preLoadPromiseList.push(preloadPromise); + }.bind(this)); + undoredo_worker.postMessage({ + commands: "reset", + }); + }.bind(this), + function (errImgIndex) { + console.log("showImageFromBuffer() failed for file: " + _via_image_filename_list[errImgIndex]); + this.loading.addClass("d-none"); + this.imgLoading = false; + }.bind(this) + ); + } else { + this.imgLoading = true; + this.addImageToBuffer(idx).then( + function (_) { + this.imgLoading = false; + this.showImage(idx); + }.bind(this), + function (_) { + this.imgLoading = false; + this.loading.addClass("d-none"); + Message.showError("The requested image does not exist!"); + show_page_404(idx); + }.bind(this) + ); + + } + + } + + searchPathSearch(idx) { + let search_path_list = _via_file_get_search_path_list(); + if (search_path_list.length === 0) { + search_path_list.push(""); // search using just the filename + } + + _via_file_resolve(idx, search_path_list).then( + function (_) { + this.showImage(idx); + }.bind(this), + function (_) { + this.loading.addClass("d-none"); + Message.showError("The requested image does not exist!"); + show_page_404(idx); + }.bind(this) + ); + } + + showImageFromBuffer(idx) { + return new Promise(function (ok_callback, err_callback) { + this.hideCurrentImage(); + + let domID = "bim" + idx; + _via_current_image = document.getElementById(domID); + if (!_via_current_image) { + err_callback(idx); + return; + } + //_via_current_image.classList.add('d-none'); // now show the new image + _via_image_index = idx; + _via_image_id = _via_image_id_list[_via_image_index]; + _via_current_image_filename = _via_img_metadata[_via_image_id].filename; + this.imgLoaded = true; + + image.hook(); + + this.postImageLoad(idx); + + ok_callback(idx); + }.bind(this)); + } + + postImageLoad(idx) { + _via_img_metadata[_via_image_id].clearGroupedRegions(); + + let arr_idx = this.bufferIdList.indexOf(idx); + this.bufferTimestamp[arr_idx] = Date.now(); + + drawing.drawingSetVariables(); + + _via_current_image_width = _via_current_image.naturalWidth; + _via_current_image_height = _via_current_image.naturalHeight; + + if (_via_current_image_width === 0 || _via_current_image_height === 0) { + // for error image icon + _via_current_image_width = 640; + _via_current_image_height = 480; + } + + let de = document.documentElement; + let imgPanelWidth = de.clientWidth - leftsidebar.clientWidth - 20; + if (imgPanelWidth < 50) { + imgPanelWidth = de.clientWidth; + } + if (leftsidebar.style.display === "none") { + imgPanelWidth = de.clientWidth; + } + let imgPanelHeight = de.clientHeight - 2 * ui_top_panel.offsetHeight; + + _via_canvas_width = _via_current_image_width; + _via_canvas_height = _via_current_image_height; + + if (_via_canvas_width > imgPanelWidth) { + // resize image to match the panel width + let scale_width = imgPanelWidth / _via_current_image.naturalWidth; + _via_canvas_width = imgPanelWidth; + _via_canvas_height = _via_current_image.naturalHeight * scale_width; + } + if (_via_canvas_height > imgPanelHeight) { + // resize further image if its height is larger than the image panel + let scale_height = imgPanelHeight / _via_canvas_height; + _via_canvas_height = imgPanelHeight; + _via_canvas_width = _via_canvas_width * scale_height; + } + _via_canvas_width = Math.round(_via_canvas_width); + _via_canvas_height = Math.round(_via_canvas_height); + + _via_canvas_scale = _via_current_image.naturalWidth / _via_canvas_width + zoom.setStartScale(1 / _via_canvas_scale); + _via_canvas_scale_without_zoom = _via_canvas_scale; + _via_canvas_scale = 1; + + drawing.setAllCanvasSize(_via_current_image_width, _via_current_image_height); + + // reset all regions to "not selected" state + toggle_all_regions_selection(false); + + // ensure that all the canvas are visible + set_display_area_content(VIA_DISPLAY_AREA_CONTENT_NAME.IMAGE); + + // update img_fn_list + sidebar.img_fn_list_ith_entry_selected(_via_image_index, true); + sidebar.img_fn_list_scroll_to_current_file(); + sidebar.annotation_editor_update_content(); + + _via_reg_canvas.focus(); + zoom.resetZoom(); + _via_load_canvas_regions(); // image to canvas space transform + drawing.redrawRegCanvas(); + image.resize(); + zoom.fixCanvasBlur() + image.show().then(_ => this.loading.addClass("d-none")); + + if (_via_img_metadata[_via_image_id].autoAnnotated === undefined) { + if (_via_img_metadata[_via_image_id].autoAnnotated) { + plugin.updateSliceRegion() + } + } + } + + addImageToBuffer(idx) { + return new Promise(function (ok_callback, err_callback) { + if (this.bufferIdList.includes(idx)) { + ok_callback(idx); + return; + } + + let imgID = _via_image_id_list[idx]; + if (!_via_img_metadata.hasOwnProperty(imgID)) { + err_callback(idx); + return; + } + + if (_via_img_fileref[imgID] instanceof File) { + this.loadFromFileRef(idx, imgID).then( + function (okImgIndex) { + ok_callback(okImgIndex); + }, + function (errImgIndex) { + err_callback(errImgIndex); + this.loading.addClass("d-none"); + } + ); + return; + } + + if (_via_img_src[imgID] === undefined || _via_img_src[imgID] === "") { + err_callback(idx); + this.loading.addClass("d-none"); + return; + } else { + this.loadFromSrc(idx, imgID).then( + function (okImgIndex) { + ok_callback(okImgIndex); + }, + function (errImgIndex) { + err_callback(errImgIndex); + this.loading.addClass("d-none"); + } + ); + } + }.bind(this), false); + } + + loadFromFileRef(idx, imgID) { + return new Promise(function (ok_callback, err_callback) { + let tmpUrl = URL.createObjectURL(_via_img_fileref[imgID]); + let imgElement = new Image(); + imgElement.id = "bim" + idx; + imgElement.src = tmpUrl; + imgElement.alt = "Image loaded from base64 data of a local file selected by user."; + imgElement.classList.add("d-none"); + image_panel.insertBefore(imgElement, _via_reg_canvas); + imgElement.addEventListener("error", function () { + URL.revokeObjectURL(tmpUrl); + project.fileLoadOnFail(idx); + err_callback(idx); + this.loading.addClass("d-none"); + }); + imgElement.addEventListener("load", function () { + URL.revokeObjectURL(tmpUrl); + img_stat_set(idx, [imgElement.naturalWidth, imgElement.naturalHeight]); + image_panel.insertBefore(imgElement, _via_reg_canvas); + project.fileLoadOnSuccess(idx); + sidebar.img_fn_list_ith_entry_remove_css_class(idx, "text-muted"); + let arr_idx = this.bufferIdList.length; + this.bufferIdList.push(idx); + this.bufferTimestamp[arr_idx] = Date.now(); + ok_callback(idx); + }.bind(this)); + }.bind(this), false); + } + + loadFromSrc(idx, imgID) { + return new Promise(function (ok_callback, err_callback) { + let imageElement = new Image(); + imageElement.id = "bim" + idx; + _via_img_src[imgID] = _via_img_src[imgID].replace("#", "%23"); + imageElement.src = _via_img_src[imgID]; + if (_via_img_src[imgID].startsWith("data:image")) { + imageElement.alt = "Source: image data in base64 format"; + } else { + imageElement.alt = "Source: " + _via_img_src[imgID]; + } + image_panel.insertBefore(imageElement, _via_reg_canvas); + imageElement.addEventListener("abort", function () { + project.fileLoadOnFail(idx); + err_callback(idx); + }); + imageElement.addEventListener("error", function () { + project.fileLoadOnFail(idx); + err_callback(idx); + this.loading.addClass("d-none"); + }); + imageElement.addEventListener("load", function () { + img_stat_set(idx, [imageElement.naturalWidth, imageElement.naturalHeight]); + image_panel.insertBefore(imageElement, _via_reg_canvas); + project.fileLoadOnSuccess(idx); + sidebar.img_fn_list_ith_entry_remove_css_class(idx, "text-muted"); + let arr_idx = this.bufferIdList.length; + this.bufferIdList.push(idx); + this.bufferTimestamp[arr_idx] = Date.now(); + ok_callback(idx); + }.bind(this), false); + }.bind(this), false); + } + + hideCurrentImage() { + sidebar.img_fn_list_ith_entry_selected(_via_image_index, false); + drawing.clearCanvas(); // clear old region shapes + if (_via_current_image) { + _via_current_image.classList.add("d-none"); + } + } + + startPreload(idx, _) { + return new Promise(function (ok_callback, _) { + this.preLoadedId = idx; + this.preloadImage(this.preLoadedId, 0).then( + function (ok_img_index_list) { + ok_callback(ok_img_index_list); + } + ); + }.bind(this)); + } + + preloadImage(idx, preloadIdx) { + return new Promise(function (ok_callback, _) { + let preloadImgIdx = this.getPreloadImgIndex( + idx, + preloadIdx + ); + + if (this.preLoadedId !== _via_image_index) { + ok_callback([]); + return; + } + + // ensure that there is sufficient buffer space left for preloading image + if (this.bufferIdList.length > settings.bufferSize) { + while (this.bufferIdList.length > settings.bufferSize) { //@todo setting + console.log("Buffer full. Removing least useful image ..."); + console.log("Buffer size: ", this.bufferIdList.length); + console.log("Buffer: ", settings.bufferSize); + this.removeLeastUsefulImage(); + if (_via_image_index !== this.preLoadedId) { + // current image has changed therefore, we need to cancel this preload operation + ok_callback([]); + return; + } + } + } + + this.addImageToBuffer(preloadImgIdx).then( + function (ok_img_index) { + if (_via_image_index !== this.preLoadedId) { + ok_callback([ok_img_index]); + return; + } + let nextPreloadIdx = preloadIdx + 1; + if (nextPreloadIdx !== VIA_IMG_PRELOAD_COUNT) { + this.preloadImage(idx, nextPreloadIdx).then( + function (ok_img_index_list) { + ok_img_index_list.push(ok_img_index); + ok_callback(ok_img_index_list); + } + ); + } else { + ok_callback([ok_img_index]); + } + }.bind(this), + function (_) { + // continue with preload of other images in sequence + let nextPreloadIdx = preloadIdx + 1; + if (nextPreloadIdx !== VIA_IMG_PRELOAD_COUNT) { + this.preloadImage(preloadImgIdx, nextPreloadIdx).then( + function (ok_img_index_list) { + ok_callback(ok_img_index_list); + } + ); + } else { + ok_callback([]); + } + }.bind(this) + ); + }.bind(this)); + } + + getPreloadImgIndex(idx, preloadIdx) { + let preloadImgIdx = idx + VIA_IMG_PRELOAD_INDICES[preloadIdx]; + if ((preloadImgIdx < 0) || (preloadImgIdx >= _via_img_count)) { + if (preloadImgIdx < 0) { + preloadImgIdx = _via_img_count + preloadImgIdx; + } else { + preloadImgIdx = preloadImgIdx - _via_img_count; + } + } + return preloadImgIdx; + } + + removeLeastUsefulImage() { + let notInPreload = this.getNotInPreloadList(); + let oldestBufferIdx = this.getOldestInList(notInPreload); + + if (this.bufferIdList[oldestBufferIdx] !== _via_image_index) { + this.removeImageFromBuffer(oldestBufferIdx); + } else { + let furthestBufferIdx = + this.getFurthestFromCurrentImg(); + this.removeImageFromBuffer(furthestBufferIdx); + } + } + + removeImageFromBuffer(_idx) { + let idx = this.bufferIdList[_idx]; + let domID = "bim" + idx; + let e = document.getElementById(domID); + if (e) { + this.bufferIdList.splice(_idx, 1); + this.bufferTimestamp.splice(_idx, 1); + e.parentNode.removeChild(e); + sidebar.img_fn_list_ith_entry_add_css_class(idx, "text-muted"); + } + } + + emptyBuffer() { + let i, n; + n = this.bufferIdList.length; + for (i = 0; i < n; ++i) { + let idx = this.bufferIdList[i]; + let domID = "bim" + idx; + let e = document.getElementById(domID); + if (e) { + e.parentNode.removeChild(e); + sidebar.img_fn_list_ith_entry_add_css_class(idx, "text-muted"); + } + } + this.bufferIdList = []; + this.bufferTimestamp = []; + } + + getOldestInList(notInPreload) { + let i; + let n = notInPreload.length; + let oldestBufferIdx = -1; + let oldestBufferTimestamp = Date.now(); + + for (i = 0; i < n; ++i) { + let _idx = notInPreload[i]; + if (this.bufferTimestamp[_idx] < oldestBufferTimestamp) { + oldestBufferTimestamp = this.bufferTimestamp[i]; + oldestBufferIdx = i; + } + } + return oldestBufferIdx; + } + + getFurthestFromCurrentImg() { + let i, dist1, dist2, dist; + let n = this.bufferIdList.length; + let furthestIdx = 0; + dist1 = Math.abs(this.bufferIdList[0] - _via_image_index); + dist2 = _via_img_count - dist1; // assuming the list is circular + let furthestDist = Math.min(dist1, dist2); + + for (i = 1; i < n; ++i) { + dist1 = Math.abs(this.bufferIdList[i] - _via_image_index); + dist2 = _via_img_count - dist1; // assuming the list is circular + dist = Math.min(dist1, dist2); + // image has been seen by user at least once + if (dist > furthestDist) { + furthestDist = dist; + furthestIdx = i; + } + } + return furthestIdx; + + } + + getNotInPreloadList() { + let preloadList = this.getPreloadList() + let notInPreload = []; + for (let i = 0; i < this.bufferIdList.length; i++) { + if (!preloadList.includes(this.bufferIdList[i])) { + notInPreload.push(i); + } + } + return notInPreload; + } + + getPreloadList() { + let preloadList = [_via_image_index]; + for (let i = 0; i < VIA_IMG_PRELOAD_COUNT; ++i) { + let preloadIdx = _via_image_index + VIA_IMG_PRELOAD_INDICES[i]; + if (preloadIdx < 0) { + preloadIdx = _via_img_count + preloadIdx; + } + if (preloadIdx >= _via_img_count) { + preloadIdx = preloadIdx - _via_img_count; + } + preloadList.push(preloadIdx); + } + return preloadList; + } + +} +const buffer = new ImageBuffer(); \ No newline at end of file diff --git a/src/js/draw.js b/src/js/draw.js new file mode 100644 index 0000000..4344326 --- /dev/null +++ b/src/js/draw.js @@ -0,0 +1,3722 @@ +// draw.js +class Drawer { + constructor() { + //History -> _via_reg_canvas = document.getElementById("region_canvas"); + this.ctx = _via_reg_canvas.getContext("2d"); + + //Init mouse handlers + this.initMouseHandlers(); + + //Mouse click position + this.click0 = { x: 0, y: 0 }; + this.click1 = { x: 0, y: 0 }; + this.current_point = { x: 0, y: 0 }; + this.region_click = { x: 0, y: 0 }; + + //Colors for regions + this.theme_control_point_color = "#ff0000"; + this.selected_region_boundary_line_color = "#ffc300"; + this.selected_region_fill_color = "#808080"; + this.region_boundary_line_color = "#000000"; + this.region_fill_color = "#ffc300"; + this.region_color_list = [ + "#e69f00", + "#56b4e9", + "#009e73", + "#d55e00", + "#cc79a7", + "#f0e442", + "#ffffff", + ]; + this.canvas_regions_group_color = {}; + + //Region edge tolerance + this.region_edge = [-1, -1]; + this.polygon_vertex_match_tol = 10; + this.ellipse_edge_tol = 0.2; + this.theta_tol = Math.PI; + this.region_edge_tol = 5; + this.mouse_click_tol = 2; + + //Current shape (default: editor mode) + this.current_shape = VIA_REGION_SHAPE.EDIT; + + //Trimming + this.trim_points = {}; + this.trim_region_id = -1; + this.trim_line = {}; + this.trim_choose_points = []; + this.trim_choose_points_id = []; + this.trim_phase_id = 0; + + //User interaction flags + this.is_user_moving_region = false; + this.is_user_drawing_region = false; + this.is_user_triming_region = false; + this.is_region_selected = false; + this.is_user_drawing_polygon = false; + this.is_user_resizing_region = false; + this.is_window_resized = false; + + //Current region id-s + this.user_sel_region_id = -1; + this.current_polygon_region_id = -1; + + //Etc drawing parameters + this.polygon_resize_vertex_offset = 100; + this.theme_region_boundary_width = this.setBoundarySize(); + this.region_shapes_points_radius = 3; + this.theme_sel_region_opacity = 0.5; + this.region_point_radius = 3; + this.region_point_radius_default = 3; + this.current_browser = this.getBrowser(); + } + + setFontSize(size) { + let fontSize = parseInt( + window.getComputedStyle(document.body).getPropertyValue("font-size") + ); + fontSize += size; + this.ctx.font = "bold " + fontSize + "px Arial"; + } + + setBoundarySize(size = 3) { + this.theme_region_boundary_width = size; + } + + getBrowser() { + let ua = navigator.userAgent, + tem, + M = + ua.match( + /(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i + ) || []; + if (/trident/i.test(M[1])) { + tem = /\brv[ :]+(\d+)/g.exec(ua) || []; + return "IE " + (tem[1] || ""); + } + if (M[1] === "Chrome") { + tem = ua.match(/\bOPR\/(\d+)/); + if (tem != null) { + return "Opera " + tem[1]; + } + } + M = M[2] ? [M[1], M[2]] : [navigator.appName, navigator.appVersion, "-?"]; + if ((tem = ua.match(/version\/(\d+)/i)) != null) { + M.splice(1, 1, tem[1]); + } + + return M[0]; + } + + //-------------------------------------------- + // Setters and getters + //-------------------------------------------- + + setCurrentShape(shape) { + this.current_shape = shape; + } + + currentShape() { + return this.current_shape; + } + + setIsUserDrawingPolygon(state) { + this.is_user_drawing_polygon = state; + } + + isUserDrawingPolygon() { + return this.is_user_drawing_polygon; + } + + setCurrentPolygonRegionId(state) { + this.current_polygon_region_id = state; + } + + currentPolygonRegionId() { + return this.current_polygon_region_id; + } + + setIsRegionSelected(state) { + this.is_region_selected = state; + } + + isRegionSelected() { + return this.is_region_selected; + } + + setIsUserResizingRegion(state) { + this.is_user_resizing_region = state; + } + + isUserResizingRegion() { + return this.is_user_resizing_region; + } + + userSelRegionId() { + return this.user_sel_region_id; + } + + setUserSelRegionId(id) { + this.user_sel_region_id = id; + } + + isUserDrawingRegion() { + return this.is_user_drawing_region; + } + + setIsUserDrawingRegion(state) { + this.is_user_drawing_region = state; + } + + isUserMovingRegion() { + return this.is_user_moving_region; + } + + setIsUserMovingRegion(state) { + this.is_user_moving_region = state; + } + + regionEdgeTol() { + return this.region_edge_tol; + } + + //-------------------------------------------- + // Canvas CTX helpers + //-------------------------------------------- + + lineStyle( + width = this.theme_region_boundary_width / 2, + color = this.region_fill_color + ) { + this.ctx.strokeStyle = color; + this.ctx.lineWidth = width; + } + + beginFill( + color = this.selected_region_fill_color, + alpha = this.theme_sel_region_opacity + ) { + this.ctx.fillStyle = color; + this.ctx.globalAlpha = alpha; + } + + //-------------------------------------------- + // Auto colors for regions (for each group) + //-------------------------------------------- + + regionsGroupColorInit() { + this.canvas_regions_group_color = {}; + let aid = _via_settings.ui.image.region_color; + if (aid !== "__via_default_region_color__") { + let avalue; + for (const element of _via_img_metadata[_via_image_id].regions) { + if (element.region_attributes.hasOwnProperty(aid)) { + avalue = element.region_attributes[aid]; + if (!this.canvas_regions_group_color.hasOwnProperty(avalue)) { + this.canvas_regions_group_color[avalue] = + this.region_color_list[ + Object.keys(this.canvas_regions_group_color).length + ]; + } + } + } + let color_index = 0; + for (avalue in this.canvas_regions_group_color) { + this.canvas_regions_group_color[avalue] = + this.region_color_list[color_index % this.region_color_list.length]; + color_index = color_index + 1; + } + } + } + + //-------------------------------------------- + // Image click handlers + //-------------------------------------------- + + /** + * Handle mouse click events on the canvas + */ + initMouseHandlers() { + //canvas click, also for pan + _via_reg_canvas.addEventListener( + "pointerdown", + this.canvasMouseDownHandler.bind(this), + false + ); + _via_reg_canvas.addEventListener( + "pointerup", + this.canvasMouseUpHandler.bind(this), + false + ); + _via_reg_canvas.addEventListener( + "pointermove", + this.canvasMouseMoveHandler.bind(this), + false + ); + _via_reg_canvas.addEventListener( + "pointerover", + this.canvasMouseOverHandler.bind(this), + false + ); + _via_reg_canvas.addEventListener( + "contextmenu", + this.canvasContextMenuHandler.bind(this), + false + ); + } + + /** + * Handle mouse down event + * Detect if user is drawing a new region or moving an existing region or trimming + * @param e event + */ + canvasMouseDownHandler(e) { + e.stopPropagation(); + zoom.handleMoseDown(e); + if (this.current_shape === VIA_REGION_SHAPE.DRAG || is_alt_pressed || e.button === 2) { + return; + } + if (this.current_shape === VIA_REGION_SHAPE.TRIM) { + this.is_user_triming_region = true; + } else { + this.is_user_triming_region = false; + this.destroyTrim(); + } + this.click0.x = e.offsetX; + this.click0.y = e.offsetY; + + if (this.trim_phase_id === 1) { + this.trim_line["x0"] = e.offsetX; + this.trim_line["y0"] = e.offsetY; + return; + } + if (this.trim_phase_id === 4) { + return; + } + + this.region_edge = this.isOnRegionCorner(this.click0); + let region_id = this.isInsideRegion(this.click0); + if (this.is_region_selected) { + // check if user clicked on the region boundary + if (this.region_edge[1] > 0) { + if (!this.is_user_resizing_region) { + if (this.region_edge[0] !== this.user_sel_region_id) { + this.user_sel_region_id = this.region_edge[0]; + } // resize region + this.is_user_resizing_region = true; + } + } else { + let yes = this.isInsideThisRegion(this.click0, this.user_sel_region_id); + + if (yes) { + if (!this.is_user_moving_region) { + this.is_user_moving_region = true; + this.region_click.x = this.click0.x; + this.region_click.y = this.click0.y; + } + } + + if (region_id === -1) { + // mousedown on outside any region + this.is_user_drawing_region = true; //modSote + // unselect all regions + this.is_region_selected = false; + this.user_sel_region_id = -1; + toggle_all_regions_selection(false); + } + } + } else { + if (region_id === -1) { + // mousedown outside a region + if ( + this.current_shape !== VIA_REGION_SHAPE.POLYGON && + this.current_shape !== VIA_REGION_SHAPE.POLYLINE && + this.current_shape !== VIA_REGION_SHAPE.POINT + ) { + // this is a bounding box drawing event + this.is_user_drawing_region = true; + } + } else { + // mousedown inside a region + // this could lead to (1) region selection or (2) region drawing + this.is_user_drawing_region = true; + } + } + } + + /** + * Handle mouse up event + * It fires up the region drawing event and region selection event + * It also calculates the region attributes + * @param e event + */ + canvasMouseUpHandler(e) { + e.stopPropagation(); + zoom.handleMoseUp(e); + if (this.current_shape === VIA_REGION_SHAPE.DRAG || is_alt_pressed || e.button === 2) { + return; + } + this.click1.x = e.offsetX; + this.click1.y = e.offsetY; + + if (this.trim_phase_id === 1) { + this.trim_line["x1"] = e.offsetX; + this.trim_line["y1"] = e.offsetY; + this.trim_phase_id = 2; + this.calculateTrimPoints(); + return; + } + + if (this.trim_phase_id === 4) { + if (this.isInsideTrimPolygon(this.click1, this.trim_points["polygon1"])) { + this.trimFromMetadata(this.trim_points["polygon1"]); + } + if (this.isInsideTrimPolygon(this.click1, this.trim_points["polygon2"])) { + this.trimFromMetadata(this.trim_points["polygon2"]); + } + } + + let click_dx = Math.abs(this.click1.x - this.click0.x); + let click_dy = Math.abs(this.click1.y - this.click0.y); + + if (this.is_user_moving_region) { + this.is_user_moving_region = false; + _via_reg_canvas.style.cursor = "default"; + + let move_x = Math.round(this.click1.x - this.region_click.x); + let move_y = Math.round(this.click1.y - this.region_click.y); + + if ( + Math.abs(move_x) > this.mouse_click_tol || + Math.abs(move_y) > this.mouse_click_tol + ) { + // move all selected regions + this.moveSelectedRegions(move_x, move_y); + } else { + // indicates a user click on an already selected region + // this could indicate the user's intention to select another + // nested region within this region + // OR + // draw a nested region (i.e. region inside a region) + // traverse the canvas regions in alternating ascending + // and descending order to solve the issue of nested regions + + let nested_region_id = this.isInsideRegion(this.click0, true); + + if ( + nested_region_id >= 0 && + nested_region_id !== this.user_sel_region_id + ) { + this.user_sel_region_id = nested_region_id; + this.is_region_selected = true; + this.is_user_moving_region = false; // de-select all other regions if the user has not pressed Shift + if (this.is_user_triming_region) { + this.trimRegionSection(nested_region_id); + } + + if (!e.shiftKey) { + toggle_all_regions_selection(false); + } + + set_region_select_state(nested_region_id, true); + sidebar.annotation_editor_show(); + } else { + // user clicking inside an already selected region + // indicates that the user intends to draw a nested region + toggle_all_regions_selection(false); + this.is_region_selected = false; + + switch (this.current_shape) { + case VIA_REGION_SHAPE.POLYLINE: // handled by case for POLYGON + + case VIA_REGION_SHAPE.POLYGON: + // user has clicked on the first point in a new polygon + // see also event 'mouseup' for this.is_user_drawing_polygon=true + this.is_user_drawing_polygon = true; + let canvas_polygon_region = new File_Region(); + canvas_polygon_region.shape_attributes["name"] = + this.current_shape; + canvas_polygon_region.shape_attributes["all_points_x"] = [ + Math.round(this.click0.x), + ]; + canvas_polygon_region.shape_attributes["all_points_y"] = [ + Math.round(this.click0.y), + ]; + + let new_length = _via_canvas_regions.push(canvas_polygon_region); + + this.current_polygon_region_id = new_length - 1; + break; + + case VIA_REGION_SHAPE.POINT: + // user has marked a landmark point + let point_region = new File_Region(); + point_region.shape_attributes["name"] = VIA_REGION_SHAPE.POINT; + point_region.shape_attributes["cx"] = Math.round( + this.click0.x * _via_canvas_scale + ); + point_region.shape_attributes["cy"] = Math.round( + this.click0.y * _via_canvas_scale + ); + + _via_img_metadata[_via_image_id].regions.push(point_region); + + let canvas_point_region = new File_Region(); + canvas_point_region.shape_attributes["name"] = + VIA_REGION_SHAPE.POINT; + canvas_point_region.shape_attributes["cx"] = Math.round( + this.click0.x + ); + canvas_point_region.shape_attributes["cy"] = Math.round( + this.click0.y + ); + + _via_canvas_regions.push(canvas_point_region); + + break; + } + + sidebar.annotation_editor_update_content(); + } + } + + this.redrawRegCanvas(); + + _via_reg_canvas.focus(); + + return; + } // indicates that user has finished resizing a region + + if (this.is_user_resizing_region) { + // _via_click(x0,y0) to _via_click(x1,y1) + this.is_user_resizing_region = false; + _via_reg_canvas.style.cursor = "default"; // update the region + + let region_id = this.region_edge[0]; + let image_attr = + _via_img_metadata[_via_image_id].regions[region_id].shape_attributes; + let canvas_attr = _via_canvas_regions[region_id].shape_attributes; + if ( + _via_img_metadata[_via_image_id].regions[region_id].hasOwnProperty( + "score" + ) + ) { + _via_img_metadata[_via_image_id].regions[region_id].score = "n/a"; + plugin.updateScoresBasedOnGroup() + } + + if (!this.is_user_triming_region) { + switch (canvas_attr["name"]) { + case VIA_REGION_SHAPE.RECT: { + let d = [canvas_attr["x"], canvas_attr["y"], 0, 0]; + d[2] = d[0] + canvas_attr["width"]; + d[3] = d[1] + canvas_attr["height"]; + + let mx = this.current_point.x; + let my = this.current_point.y; + + let preserve_aspect_ratio = false; // constrain (mx,my) to lie on a line connecting a diagonal of rectangle + + if (_via_is_ctrl_pressed) { + preserve_aspect_ratio = true; + } + + this.rectUpdateCorner( + this.region_edge[1], + d, + mx, + my, + preserve_aspect_ratio + ); + this.rectStandardizeCoordinates(d); + let w = Math.abs(d[2] - d[0]); + let h = Math.abs(d[3] - d[1]); + image_attr["x"] = Math.round(d[0] * _via_canvas_scale); + image_attr["y"] = Math.round(d[1] * _via_canvas_scale); + image_attr["width"] = Math.round(w * _via_canvas_scale); + image_attr["height"] = Math.round(h * _via_canvas_scale); + canvas_attr["x"] = Math.round(image_attr["x"] / _via_canvas_scale); + canvas_attr["y"] = Math.round(image_attr["y"] / _via_canvas_scale); + canvas_attr["width"] = Math.round( + image_attr["width"] / _via_canvas_scale + ); + canvas_attr["height"] = Math.round( + image_attr["height"] / _via_canvas_scale + ); + break; + } + case VIA_REGION_SHAPE.CIRCLE: { + let dx = Math.abs(canvas_attr["cx"] - this.current_point.x); + let dy = Math.abs(canvas_attr["cy"] - this.current_point.y); + let new_r = Math.sqrt(dx * dx + dy * dy); + image_attr["r"] = fixfloat(new_r * _via_canvas_scale); + canvas_attr["r"] = Math.round(image_attr["r"] / _via_canvas_scale); + break; + } + case VIA_REGION_SHAPE.ELLIPSE: { + let new_rx = canvas_attr["rx"]; + let new_ry = canvas_attr["ry"]; + let new_theta = canvas_attr["theta"]; + let dx = Math.abs(canvas_attr["cx"] - this.current_point.x); + let dy = Math.abs(canvas_attr["cy"] - this.current_point.y); + + switch (this.region_edge[1]) { + case 5: + new_ry = Math.sqrt(dx * dx + dy * dy); + new_theta = Math.atan2( + -(this.current_point.x - canvas_attr["cx"]), + this.current_point.y - canvas_attr["cy"] + ); + break; + + case 6: + new_rx = Math.sqrt(dx * dx + dy * dy); + new_theta = Math.atan2( + this.current_point.y - canvas_attr["cy"], + this.current_point.x - canvas_attr["cx"] + ); + break; + + default: + new_rx = dx; + new_ry = dy; + new_theta = 0; + break; + } + + image_attr["rx"] = fixfloat(new_rx * _via_canvas_scale); + image_attr["ry"] = fixfloat(new_ry * _via_canvas_scale); + image_attr["theta"] = fixfloat(new_theta); + canvas_attr["rx"] = Math.round( + image_attr["rx"] / _via_canvas_scale + ); + canvas_attr["ry"] = Math.round( + image_attr["ry"] / _via_canvas_scale + ); + canvas_attr["theta"] = fixfloat(new_theta); + break; + } + case VIA_REGION_SHAPE.POLYLINE: // handled by polygon + + case VIA_REGION_SHAPE.POLYGON: { + let moved_vertex_id = + this.region_edge[1] - this.polygon_resize_vertex_offset; + + if (e.ctrlKey || e.metaKey) { + // if on vertex, delete it + // if on edge, add a new vertex + let r = + _via_canvas_regions[this.user_sel_region_id].shape_attributes; + let shape = r.name; + + let is_on_vertex = this.isOnPolygonVertex( + r["all_points_x"], + r["all_points_y"], + this.current_point + ); + + if (is_on_vertex === this.region_edge[1]) { + // click on vertex, hence delete vertex + if (this.polygonDelVertex(region_id, moved_vertex_id)) { + show_message( + "Deleted vertex " + moved_vertex_id + " from region" + ); + } + } else { + let is_on_edge = this.is_on_polygon_edge( + r["all_points_x"], + r["all_points_y"], + this.current_point.x, + this.current_point.y + ); + + // click on edge, hence add new vertex + let vertex_index = + is_on_edge - this.polygon_resize_vertex_offset; + + let canvas_x0 = Math.round(this.click1.x); + let canvas_y0 = Math.round(this.click1.y); + let img_x0 = Math.round(canvas_x0 * _via_canvas_scale); + let img_y0 = Math.round(canvas_y0 * _via_canvas_scale); + canvas_x0 = Math.round(img_x0 / _via_canvas_scale); + canvas_y0 = Math.round(img_y0 / _via_canvas_scale); + + _via_canvas_regions[region_id].shape_attributes[ + "all_points_x" + ].splice(vertex_index + 1, 0, canvas_x0); + + _via_canvas_regions[region_id].shape_attributes[ + "all_points_y" + ].splice(vertex_index + 1, 0, canvas_y0); + + _via_img_metadata[_via_image_id].regions[ + region_id + ].shape_attributes["all_points_x"].splice( + vertex_index + 1, + 0, + img_x0 + ); + + _via_img_metadata[_via_image_id].regions[ + region_id + ].shape_attributes["all_points_y"].splice( + vertex_index + 1, + 0, + img_y0 + ); + + show_message("Added 1 new vertex to " + shape + " region"); + + } + } else { + // update coordinate of vertex + let imx = Math.round(this.current_point.x * _via_canvas_scale); + let imy = Math.round(this.current_point.y * _via_canvas_scale); + image_attr["all_points_x"][moved_vertex_id] = imx; + image_attr["all_points_y"][moved_vertex_id] = imy; + canvas_attr["all_points_x"][moved_vertex_id] = Math.round( + imx / _via_canvas_scale + ); + canvas_attr["all_points_y"][moved_vertex_id] = Math.round( + imy / _via_canvas_scale + ); + } + + break; + } + } // end of switch() + } + + this.redrawRegCanvas(); + + _via_reg_canvas.focus(); + + return; + } // denotes a single click (= mouse down + mouse up) + + if (click_dx < this.mouse_click_tol || click_dy < this.mouse_click_tol) { + // if user is already drawing polygon, then each click adds a new point + if (this.is_user_drawing_polygon) { + let canvas_x0 = Math.round(this.click1.x); + let canvas_y0 = Math.round(this.click1.y); + + let n = + _via_canvas_regions[this.current_polygon_region_id].shape_attributes[ + "all_points_x" + ].length; + + let last_x0 = + _via_canvas_regions[this.current_polygon_region_id].shape_attributes[ + "all_points_x" + ][n - 1]; + let last_y0 = + _via_canvas_regions[this.current_polygon_region_id].shape_attributes[ + "all_points_y" + ][n - 1]; // discard if the click was on the last vertex + + if (canvas_x0 !== last_x0 || canvas_y0 !== last_y0) { + // user clicked on a new polygon point + _via_canvas_regions[this.current_polygon_region_id].shape_attributes[ + "all_points_x" + ].push(canvas_x0); + + _via_canvas_regions[this.current_polygon_region_id].shape_attributes[ + "all_points_y" + ].push(canvas_y0); + } + } else { + let region_id = this.isInsideRegion(this.click0); + + if (region_id >= 0) { + // first click selects region + this.user_sel_region_id = region_id; + this.is_region_selected = true; + this.is_user_moving_region = false; + this.is_user_drawing_region = false; // de-select all other regions if the user has not pressed Shift + + if (!e.shiftKey) { + sidebar.annotation_editor_highlight_row(); + toggle_all_regions_selection(false); + } + + set_region_select_state(region_id, true); // show annotation editor only when a single region is selected + + if (!e.shiftKey) { + sidebar.annotation_editor_show(); + } else { + sidebar.annotation_editor_hide(); + } // show the region info + + if (_via_is_region_info_visible) { + let canvas_attr = _via_canvas_regions[region_id].shape_attributes; + + switch (canvas_attr["name"]) { + case VIA_REGION_SHAPE.RECT: { + break; + } + case VIA_REGION_SHAPE.CIRCLE: { + let rf = document.getElementById("region_info"); + let attr = + _via_canvas_regions[this.user_sel_region_id].shape_attributes; + rf.innerHTML += "," + " Radius:" + attr["r"]; + break; + } + case VIA_REGION_SHAPE.ELLIPSE: { + let rf = document.getElementById("region_info"); + let attr = + _via_canvas_regions[this.user_sel_region_id].shape_attributes; + rf.innerHTML += + "," + + " X-radius:" + + attr["rx"] + + "," + + " Y-radius:" + + attr["ry"]; + break; + } + case VIA_REGION_SHAPE.POLYLINE: + case VIA_REGION_SHAPE.POLYGON: { + break; + } + } + } + + if (this.is_user_triming_region) { + this.trimRegionSection(region_id); + } + + show_message( + "Region selected. If you intended to draw a region, click again inside the selected region to start drawing a region." + ); + } else { + if (this.is_user_drawing_region) { + // clear all region selection + this.is_user_drawing_region = false; + this.is_region_selected = false; + toggle_all_regions_selection(false); + sidebar.annotation_editor_hide(); + } else { + switch (this.current_shape) { + case VIA_REGION_SHAPE.POLYLINE: // handled by case for POLYGON + + case VIA_REGION_SHAPE.POLYGON: { + // user has clicked on the first point in a new polygon + // see also event 'mouseup' for this._via_is_user_moving_region=true + this.is_user_drawing_polygon = true; + let canvas_polygon_region = new File_Region(); + canvas_polygon_region.shape_attributes["name"] = + this.current_shape; + canvas_polygon_region.shape_attributes["all_points_x"] = [ + Math.round(this.click0.x), + ]; + canvas_polygon_region.shape_attributes["all_points_y"] = [ + Math.round(this.click0.y), + ]; + + let new_length = _via_canvas_regions.push( + canvas_polygon_region + ); + + this.current_polygon_region_id = new_length - 1; + break; + } + case VIA_REGION_SHAPE.POINT: { + // user has marked a landmark point + let point_region = new File_Region(); + point_region.shape_attributes["name"] = VIA_REGION_SHAPE.POINT; + point_region.shape_attributes["cx"] = Math.round( + this.click0.x * _via_canvas_scale + ); + point_region.shape_attributes["cy"] = Math.round( + this.click0.y * _via_canvas_scale + ); + + let region_count = + _via_img_metadata[_via_image_id].regions.push(point_region); + let new_region_id = region_count - 1; + sidebar.set_region_annotations_to_default_value(new_region_id); + + let canvas_point_region = new File_Region(); + canvas_point_region.shape_attributes["name"] = + VIA_REGION_SHAPE.POINT; + canvas_point_region.shape_attributes["cx"] = Math.round( + this.click0.x + ); + canvas_point_region.shape_attributes["cy"] = Math.round( + this.click0.y + ); + + _via_canvas_regions.push(canvas_point_region); + + sidebar.annotation_editor_update_content(); + break; + } + } + } + } + } + + this.redrawRegCanvas(); + + _via_reg_canvas.focus(); + + return; + } // indicates that user has finished drawing a new region + + if (this.is_user_drawing_region) { + this.is_user_drawing_region = false; + let region_x0 = this.click0.x; + let region_y0 = this.click0.y; + let original_img_region = new File_Region(); + let canvas_img_region = new File_Region(); + let region_dx = Math.abs(this.click1.x - region_x0); + let region_dy = Math.abs(this.click1.y - region_y0); + let new_region_added = false; + + if (region_dx > VIA_REGION_MIN_DIM && region_dy > VIA_REGION_MIN_DIM) { + // avoid regions with 0 dim + switch (this.current_shape) { + case VIA_REGION_SHAPE.RECT: { + // ensure that (x0,y0) is top-left and (x1,y1) is bottom-right + if (this.click0.x < this.click1.x) { + region_x0 = this.click0.x; + } else { + region_x0 = this.click1.x; + } + + if (this.click0.y < this.click1.y) { + region_y0 = this.click0.y; + } else { + region_y0 = this.click1.y; + } + + let x = Math.round(region_x0 * _via_canvas_scale); + let y = Math.round(region_y0 * _via_canvas_scale); + let width = Math.round(region_dx * _via_canvas_scale); + let height = Math.round(region_dy * _via_canvas_scale); + original_img_region.shape_attributes["name"] = "rect"; + original_img_region.shape_attributes["x"] = x; + original_img_region.shape_attributes["y"] = y; + original_img_region.shape_attributes["width"] = width; + original_img_region.shape_attributes["height"] = height; + canvas_img_region.shape_attributes["name"] = "rect"; + canvas_img_region.shape_attributes["x"] = Math.round( + x / _via_canvas_scale + ); + canvas_img_region.shape_attributes["y"] = Math.round( + y / _via_canvas_scale + ); + canvas_img_region.shape_attributes["width"] = Math.round( + width / _via_canvas_scale + ); + canvas_img_region.shape_attributes["height"] = Math.round( + height / _via_canvas_scale + ); + new_region_added = true; + break; + } + case VIA_REGION_SHAPE.CIRCLE: { + let cx = Math.round(region_x0 * _via_canvas_scale); + let cy = Math.round(region_y0 * _via_canvas_scale); + let r = Math.round( + Math.sqrt(region_dx * region_dx + region_dy * region_dy) * + _via_canvas_scale + ); + original_img_region.shape_attributes["name"] = "circle"; + original_img_region.shape_attributes["cx"] = cx; + original_img_region.shape_attributes["cy"] = cy; + original_img_region.shape_attributes["r"] = r; + canvas_img_region.shape_attributes["name"] = "circle"; + canvas_img_region.shape_attributes["cx"] = Math.round( + cx / _via_canvas_scale + ); + canvas_img_region.shape_attributes["cy"] = Math.round( + cy / _via_canvas_scale + ); + canvas_img_region.shape_attributes["r"] = Math.round( + r / _via_canvas_scale + ); + new_region_added = true; + break; + } + case VIA_REGION_SHAPE.ELLIPSE: { + let cx = Math.round(region_x0 * _via_canvas_scale); + let cy = Math.round(region_y0 * _via_canvas_scale); + let rx = Math.round(region_dx * _via_canvas_scale); + let ry = Math.round(region_dy * _via_canvas_scale); + let theta = 0; + original_img_region.shape_attributes["name"] = "ellipse"; + original_img_region.shape_attributes["cx"] = cx; + original_img_region.shape_attributes["cy"] = cy; + original_img_region.shape_attributes["rx"] = rx; + original_img_region.shape_attributes["ry"] = ry; + original_img_region.shape_attributes["theta"] = theta; + canvas_img_region.shape_attributes["name"] = "ellipse"; + canvas_img_region.shape_attributes["cx"] = Math.round( + cx / _via_canvas_scale + ); + canvas_img_region.shape_attributes["cy"] = Math.round( + cy / _via_canvas_scale + ); + canvas_img_region.shape_attributes["rx"] = Math.round( + rx / _via_canvas_scale + ); + canvas_img_region.shape_attributes["ry"] = Math.round( + ry / _via_canvas_scale + ); + canvas_img_region.shape_attributes["theta"] = theta; + new_region_added = true; + break; + } + case VIA_REGION_SHAPE.REMOVE: { + // ensure that (x0,y0) is top-left and (x1,y1) is bottom-right + if (this.click0.x < this.click1.x) { + region_x0 = this.click0.x; + } else { + region_x0 = this.click1.x; + } + + if (this.click0.y < this.click1.y) { + region_y0 = this.click0.y; + } else { + region_y0 = this.click1.y; + } + + let xx = Math.round(region_x0 * _via_canvas_scale); + let yy = Math.round(region_y0 * _via_canvas_scale); + let widthh = Math.round(region_dx * _via_canvas_scale); + let heightt = Math.round(region_dy * _via_canvas_scale); + let canvx = Math.round(xx / _via_canvas_scale); + let canvy = Math.round(yy / _via_canvas_scale); + let canvw = Math.round(widthh / _via_canvas_scale); + let canvh = Math.round(heightt / _via_canvas_scale); + this.drawSelector(canvx, canvy, canvw, canvh); //handled by via_plugin + + break; + } + case VIA_REGION_SHAPE.POINT: // handled by case VIA_REGION_SHAPE.POLYGON + + case VIA_REGION_SHAPE.POLYLINE: // handled by case VIA_REGION_SHAPE.POLYGON + + case VIA_REGION_SHAPE.POLYGON: { + // handled by this.is_user_drawing_polygon + break; + } + } // end of switch + + if (new_region_added) { + let n1 = + _via_img_metadata[_via_image_id].regions.push(original_img_region); + + let n2 = _via_canvas_regions.push(canvas_img_region); + + if (n1 !== n2) { + console.log( + "_via_img_metadata.regions[" + + n1 + + "] and _via_canvas_regions[" + + n2 + + "] count mismatch" + ); + Message.show({ + address: "Error", + body: "Region count mismatch", + color: "#cc1100", + }); + } + + let new_region_id = n1 - 1; + sidebar.set_region_annotations_to_default_value(new_region_id); + select_only_region(new_region_id); + + if ( + _via_annotation_editor_mode === + VIA_ANNOTATION_EDITOR_MODE.ALL_REGIONS && + _via_metadata_being_updated === "region" + ) { + sidebar.annotation_editor_add_row(new_region_id); + sidebar.annotation_editor_scroll_to_row(new_region_id); + sidebar.annotation_editor_clear_row_highlight(); + + sidebar.annotation_editor_highlight_row(new_region_id); + } + sidebar.annotation_editor_show(); + } + + this.redrawRegCanvas(); + + _via_reg_canvas.focus(); + } else { + show_message("Prevented accidental addition of a very small region."); + } + } + } + + /** + * Handle mouse over event on the region canvas + * Redraws the region canvas with all the regions + * if the region canvas is visible and the image is loaded + */ + canvasMouseOverHandler() { + // change the mouse cursor icon + if (this.is_user_drawing_polygon) { + _via_reg_canvas.style.cursor = "crosshair"; + } else { + _via_reg_canvas.style.cursor = "default"; + } + this.redrawRegCanvas(); //handled by via_plugin; + _via_reg_canvas.focus(); + } + + /** + * Handle mouse out event on the region canvas + * It draws the regions state when mouse downed, before mouseup event + * @param e event + */ + canvasMouseMoveHandler(e) { + if (!buffer.imgLoaded) { + return; + } + + zoom.handleMoseMove(e); + + if (this.current_shape === VIA_REGION_SHAPE.DRAG || is_alt_pressed) { + return; + } + + this.current_point.x = e.offsetX; + this.current_point.y = e.offsetY; + + if (this.trim_phase_id === 1 && this.trim_line.hasOwnProperty("x0")) { + this.redrawRegCanvas(); + this.drawLineRegion( + this.trim_line["x0"], + this.trim_line["y0"], + this.current_point.x, + this.current_point.y, + false + ); + return; + } + + let rf = document.getElementById("region_info"); + + if (rf != null && _via_is_region_info_visible) { + let img_x = Math.round(this.current_point.x * _via_canvas_scale); + let img_y = Math.round(this.current_point.y * _via_canvas_scale); + + rf.innerHTML = "X:" + img_x + "," + " Y:" + img_y; + } + + if (this.is_region_selected) { + // display the region's info if a region is selected + if ( + rf != null && + _via_is_region_info_visible && + this.user_sel_region_id !== -1 + ) { + let canvas_attr = + _via_canvas_regions[this.user_sel_region_id].shape_attributes; + + switch (canvas_attr["name"]) { + case VIA_REGION_SHAPE.RECT: { + break; + } + case VIA_REGION_SHAPE.CIRCLE: { + let rf = document.getElementById("region_info"); + let attr = + _via_canvas_regions[this.user_sel_region_id].shape_attributes; + rf.innerHTML += "," + " Radius:" + attr["r"]; + break; + } + case VIA_REGION_SHAPE.ELLIPSE: { + let rf = document.getElementById("region_info"); + let attr = + _via_canvas_regions[this.user_sel_region_id].shape_attributes; + rf.innerHTML += + "," + " X-radius:" + attr["rx"] + "," + " Y-radius:" + attr["ry"]; + break; + } + case VIA_REGION_SHAPE.POLYLINE: + case VIA_REGION_SHAPE.POLYGON: { + break; + } + } + } + + if (!this.is_user_resizing_region) { + // check if user moved mouse cursor to region boundary + // which indicates an intention to resize the region + this.region_edge = this.isOnRegionCorner(this.current_point); + + if (this.region_edge[0] === this.user_sel_region_id) { + switch (this.region_edge[1]) { + // rect + case 1: // Fall-through // top-left corner of rect + + case 3: { + // bottom-right corner of rect + _via_reg_canvas.style.cursor = "nwse-resize"; + break; + } + case 2: // Fall-through // top-right corner of rect + + case 4: { + // bottom-left corner of rect + _via_reg_canvas.style.cursor = "nesw-resize"; + break; + } + case 5: // Fall-through // top-middle point of rect + + case 7: { + // bottom-middle point of rect + _via_reg_canvas.style.cursor = "ns-resize"; + break; + } + case 6: // Fall-through // top-middle point of rect + + case 8: { + // bottom-middle point of rect + _via_reg_canvas.style.cursor = "ew-resize"; + break; + // circle and ellipse + } + case 5: { + _via_reg_canvas.style.cursor = "n-resize"; + break; + } + case 6: { + _via_reg_canvas.style.cursor = "e-resize"; + break; + } + default: { + _via_reg_canvas.style.cursor = "default"; + break; + } + } + + if (this.region_edge[1] >= this.polygon_resize_vertex_offset) { + // indicates mouse over polygon vertex + _via_reg_canvas.style.cursor = "crosshair"; + show_message( + "To move vertex, simply drag the vertex. To add vertex, press [Ctrl] key and click on the edge. To delete vertex, press [Ctrl] (or [Command]) key and click on vertex." + ); + } + } else { + let yes = this.isInsideThisRegion( + this.current_point, + this.user_sel_region_id + ); + + if (yes) { + _via_reg_canvas.style.cursor = "move"; + } else { + _via_reg_canvas.style.cursor = "default"; + } + } + } else { + sidebar.annotation_editor_hide(); // resizing + } + } + + if (this.is_user_drawing_region) { + // draw region as the user drags the mouse cursor + if (_via_canvas_regions.length) { + this.redrawRegCanvas(); // clear old intermediate rectangle + } else { + // first region being drawn, just clear the full region canvas + this.clearCanvas(); + } + + let region_x0 = this.click0.x; + let region_y0 = this.click0.y; + let dx = Math.round(Math.abs(this.current_point.x - this.click0.x)); + let dy = Math.round(Math.abs(this.current_point.y - this.click0.y)); + + this.lineStyle(); + + switch (this.current_shape) { + case VIA_REGION_SHAPE.RECT: { + if (this.click0.x < this.current_point.x) { + if (this.click0.y < this.current_point.y) { + region_x0 = this.click0.x; + region_y0 = this.click0.y; + } else { + region_x0 = this.click0.x; + region_y0 = this.current_point.y; + } + } else { + if (this.click0.y < this.current_point.y) { + region_x0 = this.current_point.x; + region_y0 = this.click0.y; + } else { + region_x0 = this.current_point.x; + region_y0 = this.current_point.y; + } + } + + this.drawRectRegion(region_x0, region_y0, dx, dy, false); // display the current region info + + if (rf != null && _via_is_region_info_visible) { + rf.innerHTML += "," + " W:" + dx + "," + " H:" + dy; + } + + break; + } + case VIA_REGION_SHAPE.CIRCLE: { + let circle_radius = Math.round(Math.sqrt(dx * dx + dy * dy)); + + this.drawCircleRegion(region_x0, region_y0, circle_radius, false); // display the current region info + + if (rf != null && _via_is_region_info_visible) { + rf.innerHTML += "," + " Radius:" + circle_radius; + } + + break; + } + case VIA_REGION_SHAPE.ELLIPSE: { + this.drawEllipseRegion(region_x0, region_y0, dx, dy, 0, false); // display the current region info + + if (rf != null && _via_is_region_info_visible) { + rf.innerHTML += + "," + + " X-radius:" + + fixfloat(dx) + + "," + + " Y-radius:" + + fixfloat(dy); + } + + break; + } + case VIA_REGION_SHAPE.REMOVE: { + if (this.click0.x < this.current_point.x) { + if (this.click0.y < this.current_point.y) { + region_x0 = this.click0.x; + region_y0 = this.click0.y; + } else { + region_x0 = this.click0.x; + region_y0 = this.current_point.y; + } + } else { + if (this.click0.y < this.current_point.y) { + region_x0 = this.current_point.x; + region_y0 = this.click0.y; + } else { + region_x0 = this.current_point.x; + region_y0 = this.current_point.y; + } + } + + this.lineStyle("", "#dc143c"); + + this.ctx.setLineDash([5, 3]); + this.drawRectRegion(region_x0, region_y0, dx, dy, false); // display the current region info + this.ctx.setLineDash([]); + + if (rf != null && _via_is_region_info_visible) { + rf.innerHTML += "," + " W:" + dx + "," + " H:" + dy; + } + + break; + } + case VIA_REGION_SHAPE.POLYLINE: // handled by polygon + + case VIA_REGION_SHAPE.POLYGON: { + // this is handled by the if ( this.is_user_drawing_polygon ) { ... } + // see below + break; + } + } + + _via_reg_canvas.focus(); + } + + if (this.is_user_resizing_region) { + // user has clicked mouse on bounding box edge and is now moving it + // draw region as the user drags the mouse coursor + if (_via_canvas_regions.length) { + this.redrawRegCanvas(); // clear old intermediate rectangle + } else { + // first region being drawn, just clear the full region canvas + this.clearCanvas(); + } + + let region_id = this.region_edge[0]; + //if(_via_canvas_regions[region_id] === undefined || !_via_canvas_regions[region_id].hasOwnProperty("shape_attributes")) { + let attr = _via_canvas_regions[region_id].shape_attributes; + if (!this.is_user_triming_region) { + switch (attr["name"]) { + case VIA_REGION_SHAPE.RECT: { + // original rectangle + let d = [attr["x"], attr["y"], 0, 0]; + d[2] = d[0] + attr["width"]; + d[3] = d[1] + attr["height"]; + + let mx = this.current_point.x; + let my = this.current_point.y; + let preserve_aspect_ratio = false; // constrain (mx,my) to lie on a line connecting a diagonal of rectangle + + if (_via_is_ctrl_pressed) { + preserve_aspect_ratio = true; + } + + this.rectUpdateCorner( + this.region_edge[1], + d, + mx, + my, + preserve_aspect_ratio + ); + this.rectStandardizeCoordinates(d); + let w = Math.abs(d[2] - d[0]); + let h = Math.abs(d[3] - d[1]); + + this.drawRectRegion(d[0], d[1], w, h, true); + + if (rf != null && _via_is_region_info_visible) { + rf.innerHTML += "," + " W:" + w + "," + " H:" + h; + } + + break; + } + case VIA_REGION_SHAPE.CIRCLE: { + let dx = Math.abs(attr["cx"] - this.current_point.x); + let dy = Math.abs(attr["cy"] - this.current_point.y); + let new_r = Math.sqrt(dx * dx + dy * dy); + + this.drawCircleRegion(attr["cx"], attr["cy"], new_r, true); + + if (rf != null && _via_is_region_info_visible) { + let curr_texts = rf.innerHTML.split(","); + rf.innerHTML = ""; + rf.innerHTML += + curr_texts[0] + + "," + + curr_texts[1] + + "," + + " Radius:" + + Math.round(new_r); + } + + break; + } + case VIA_REGION_SHAPE.ELLIPSE: { + let new_rx = attr["rx"]; + let new_ry = attr["ry"]; + let new_theta = attr["theta"]; + let dx = Math.abs(attr["cx"] - this.current_point.x); + let dy = Math.abs(attr["cy"] - this.current_point.y); + + switch (this.region_edge[1]) { + case 5: + new_ry = Math.sqrt(dx * dx + dy * dy); + new_theta = Math.atan2( + -(this.current_point.x - attr["cx"]), + this.current_point.y - attr["cy"] + ); + break; + + case 6: + new_rx = Math.sqrt(dx * dx + dy * dy); + + new_theta = Math.atan2( + this.current_point.y - attr["cy"], + this.current_point.x - attr["cx"] + ); + + break; + + default: + new_rx = dx; + new_ry = dy; + new_theta = 0; + break; + } + + this.drawEllipseRegion( + attr["cx"], + attr["cy"], + new_rx, + new_ry, + new_theta, + true + ); + + if (rf != null && _via_is_region_info_visible) { + let curr_texts = rf.innerHTML.split(","); + rf.innerHTML = ""; + rf.innerHTML = + curr_texts[0] + + "," + + curr_texts[1] + + "," + + " X-radius:" + + fixfloat(new_rx) + + "," + + " Y-radius:" + + fixfloat(new_ry); + } + + break; + } + case VIA_REGION_SHAPE.POLYLINE: // handled by polygon + + case VIA_REGION_SHAPE.POLYGON: { + let moved_all_points_x = attr["all_points_x"].slice(0); + let moved_all_points_y = attr["all_points_y"].slice(0); + let moved_vertex_id = + this.region_edge[1] - this.polygon_resize_vertex_offset; + moved_all_points_x[moved_vertex_id] = this.current_point.x; + moved_all_points_y[moved_vertex_id] = this.current_point.y; + + this.drawPolygonRegion( + moved_all_points_x, + moved_all_points_y, + true, + attr["name"] + ); + + if (rf != null && _via_is_region_info_visible) { + rf.innerHTML += "," + " Vertices:" + attr["all_points_x"].length; + } + + break; + } + } + } + + _via_reg_canvas.focus(); + //} + } + + if (this.is_user_moving_region) { + // draw region as the user drags the mouse coursor + if (_via_canvas_regions.length) { + this.redrawRegCanvas(); // clear old intermediate rectangle + } else { + // first region being drawn, just clear the full region canvas + this.clearCanvas(); + } + + let move_x = this.current_point.x - this.region_click.x; + let move_y = this.current_point.y - this.region_click.y; + let attr = _via_canvas_regions[this.user_sel_region_id].shape_attributes; + + switch (attr["name"]) { + case VIA_REGION_SHAPE.RECT: { + this.drawRectRegion( + attr["x"] + move_x, + attr["y"] + move_y, + attr["width"], + attr["height"], + true + ); // display the current region info + + if (rf != null && _via_is_region_info_visible) { + rf.innerHTML += + "," + " W:" + attr["width"] + "," + " H:" + attr["height"]; + } + + break; + } + case VIA_REGION_SHAPE.CIRCLE: { + this.drawCircleRegion( + attr["cx"] + move_x, + attr["cy"] + move_y, + attr["r"], + true + ); + + break; + } + case VIA_REGION_SHAPE.ELLIPSE: { + if (typeof attr["theta"] === "undefined") { + attr["theta"] = 0; + } + + this.drawEllipseRegion( + attr["cx"] + move_x, + attr["cy"] + move_y, + attr["rx"], + attr["ry"], + attr["theta"], + true + ); + + break; + } + case VIA_REGION_SHAPE.POLYLINE: // handled by polygon + + case VIA_REGION_SHAPE.POLYGON: { + let moved_all_points_x = attr["all_points_x"].slice(0); + let moved_all_points_y = attr["all_points_y"].slice(0); + + for (let i = 0; i < moved_all_points_x.length; ++i) { + moved_all_points_x[i] += move_x; + moved_all_points_y[i] += move_y; + } + + this.drawPolygonRegion( + moved_all_points_x, + moved_all_points_y, + true, + attr["name"] + ); + + if (rf != null && _via_is_region_info_visible) { + rf.innerHTML += "," + " Vertices:" + attr["all_points_x"].length; + } + + break; + } + case VIA_REGION_SHAPE.POINT: { + this.drawPointRegion(attr["cx"] + move_x, attr["cy"] + move_y, true); + + break; + } + } + + _via_reg_canvas.focus(); + + sidebar.annotation_editor_hide(); // moving + + return; + } + + if (this.is_user_drawing_polygon) { + if (_via_canvas_regions.length) { + this.redrawRegCanvas(); // clear old intermediate rectangle + } else { + // first region being drawn, just clear the full region canvas + this.clearCanvas(); + } + + // if(_via_canvas_regions[this.current_polygon_region_id] === undefined || _via_canvas_regions[this.current_polygon_region_id].hasOwnProperty("shape_attributes")) { + let attr = + _via_canvas_regions[this.current_polygon_region_id].shape_attributes; + let all_points_x = attr["all_points_x"]; + let all_points_y = attr["all_points_y"]; + let npts = all_points_x.length; + + if (npts > 0) { + let line_x = [all_points_x.slice(npts - 1), this.current_point.x]; + let line_y = [all_points_y.slice(npts - 1), this.current_point.y]; + + this.drawPolygonRegion(line_x, line_y, false, attr["name"]); + } + + if (rf != null && _via_is_region_info_visible) { + rf.innerHTML += "," + " Vertices:" + npts; + } + //} + } + } + + canvasContextMenuHandler(e) { + e.preventDefault(); + if (this.is_user_drawing_polygon) { + _via_polyshape_finish_drawing(); + } + } + + //-------------------------------------------- + // Move region + //-------------------------------------------- + + /** + * Move all selected regions by (dx,dy) + * @param move_x {number} - x offset + * @param move_y {number} - y offset + */ + moveSelectedRegions(move_x, move_y) { + let i, n; + n = _via_canvas_regions.length; + + for (i = 0; i < n; ++i) { + if (_via_region_selected_flag.has(i)) { + this.moveRegion(i, move_x, move_y); + } + } + } + + /** + * Move region by (dx,dy) + * @param x {number} - x offset + * @param y {number} - y offset + * @param canvas_attr {object} - shape attributes of the region + * @returns {boolean} - true if region is moved, false otherwise + */ + validateMoveRegion(x, y, canvas_attr) { + switch (canvas_attr["name"]) { + case VIA_REGION_SHAPE.RECT: + // left and top boundary check + if (x < 0 || y < 0) { + show_message("Region moved beyond image boundary. Resetting."); + return false; + } // right and bottom boundary check + + if ( + y + canvas_attr["height"] > _via_current_image_height || + x + canvas_attr["width"] > _via_current_image_width + ) { + show_message("Region moved beyond image boundary. Resetting."); + return false; + } + + // same validation for all + + case VIA_REGION_SHAPE.CIRCLE: + case VIA_REGION_SHAPE.ELLIPSE: + case VIA_REGION_SHAPE.POINT: + case VIA_REGION_SHAPE.POLYLINE: + case VIA_REGION_SHAPE.POLYGON: + if ( + x < 0 || + y < 0 || + x > _via_current_image_width || + y > _via_current_image_height + ) { + show_message("Region moved beyond image boundary. Resetting."); + return false; + } + } + return true; + } + + /** + * Move one region by (dx,dy) + * @param region_id {number} - id of the region to be moved + * @param move_x {number} - x offset + * @param move_y {number} - y offset + */ + moveRegion(region_id, move_x, move_y) { + let image_attr = + _via_img_metadata[_via_image_id].regions[region_id].shape_attributes; + let canvas_attr = _via_canvas_regions[region_id].shape_attributes; + + switch (canvas_attr["name"]) { + case VIA_REGION_SHAPE.RECT: { + let xnew = image_attr["x"] + Math.round(move_x * _via_canvas_scale); + let ynew = image_attr["y"] + Math.round(move_y * _via_canvas_scale); + + let is_valid = this.validateMoveRegion(xnew, ynew, image_attr); + + if (!is_valid) { + break; + } + + image_attr["x"] = xnew; + image_attr["y"] = ynew; + canvas_attr["x"] = Math.round(image_attr["x"] / _via_canvas_scale); + canvas_attr["y"] = Math.round(image_attr["y"] / _via_canvas_scale); + break; + } + case VIA_REGION_SHAPE.CIRCLE: // Fall-through + + case VIA_REGION_SHAPE.ELLIPSE: // Fall-through + + case VIA_REGION_SHAPE.POINT: { + let cxnew = image_attr["cx"] + Math.round(move_x * _via_canvas_scale); + let cynew = image_attr["cy"] + Math.round(move_y * _via_canvas_scale); + + let is_valid = this.validateMoveRegion(cxnew, cynew, image_attr); + + if (!is_valid) { + break; + } + + image_attr["cx"] = cxnew; + image_attr["cy"] = cynew; + canvas_attr["cx"] = Math.round(image_attr["cx"] / _via_canvas_scale); + canvas_attr["cy"] = Math.round(image_attr["cy"] / _via_canvas_scale); + break; + } + case VIA_REGION_SHAPE.POLYLINE: // handled by polygon + + case VIA_REGION_SHAPE.POLYGON: { + let img_px = image_attr["all_points_x"]; + let img_py = image_attr["all_points_y"]; + let canvas_px = canvas_attr["all_points_x"]; + let canvas_py = canvas_attr["all_points_y"]; // clone for reverting if valiation fails + + let img_px_old = Object.assign({}, img_px); + let img_py_old = Object.assign({}, img_py); // validate move + + for (let i = 0; i < img_px.length; ++i) { + let pxnew = img_px[i] + Math.round(move_x * _via_canvas_scale); + let pynew = img_py[i] + Math.round(move_y * _via_canvas_scale); + + if (!this.validateMoveRegion(pxnew, pynew, image_attr)) { + img_px = img_px_old; + img_py = img_py_old; + break; + } + } // move points + + for (let i = 0; i < img_px.length; ++i) { + img_px[i] = img_px[i] + Math.round(move_x * _via_canvas_scale); + img_py[i] = img_py[i] + Math.round(move_y * _via_canvas_scale); + } + + for (let i = 0; i < canvas_px.length; ++i) { + canvas_px[i] = Math.round(img_px[i] / _via_canvas_scale); + canvas_py[i] = Math.round(img_py[i] / _via_canvas_scale); + } + + break; + } + } + } + + //-------------------------------------------- + // Canvas update routines + //-------------------------------------------- + + /** + * Update canvas, by clearing it and redrawing all regions + */ + redrawRegCanvas() { + if (buffer.imgLoaded) { + this.clearCanvas(); + if (_via_canvas_regions.length > 0) { + if (_via_is_region_boundary_visible) { + this.drawAllRegions(); + } + + if (_via_is_region_id_visible) { + this.drawAllRegionId(); + } + } + } + } // draw all regions + + /** + * Clear all regions from canvas + */ + clearCanvas() { + this.ctx.clearRect(0, 0, _via_reg_canvas.width, _via_reg_canvas.height); + } // clear all regions from canvas + + /** + * Draw all regions + * It iterates over all regions and calls draw...Region() + */ + drawAllRegions() { + let aid = _via_settings.ui.image.region_color; + let attr, is_selected, avalue; + + if (this.is_user_triming_region && this.trim_points !== null) { + this.lineStyle(this.theme_region_boundary_width + 1, "#ff5733"); + + this.ctx.setLineDash([5, 3]); + this.updateTrimPoints(); + this.drawPolygonRegion( + this.trim_points["x"], + this.trim_points["y"], + false + ); + this.ctx.setLineDash([]); + } + + for (let i = 0; i < _via_canvas_regions.length; ++i) { + attr = _via_canvas_regions[i].shape_attributes; + is_selected = _via_region_selected_flag.has(i); // region stroke style may depend on attribute value + + this.lineStyle(); + + if ( + !this.is_user_drawing_polygon && + aid !== "__via_default_region_color__" + ) { + if ( + _via_img_metadata[_via_image_id].regions[i].region_attributes !== + undefined + ) { + avalue = + _via_img_metadata[_via_image_id].regions[i].region_attributes[aid]; + + if (this.canvas_regions_group_color.hasOwnProperty(avalue)) { + this.lineStyle( + this.theme_region_boundary_width / 2, + this.canvas_regions_group_color[avalue] + ); + } + } + } + + switch (attr["name"]) { + case VIA_REGION_SHAPE.RECT: + this.drawRectRegion( + attr["x"], + attr["y"], + attr["width"], + attr["height"], + is_selected + ); + + break; + + case VIA_REGION_SHAPE.CIRCLE: + this.drawCircleRegion(attr["cx"], attr["cy"], attr["r"], is_selected); + + break; + + case VIA_REGION_SHAPE.ELLIPSE: + if (typeof attr["theta"] === "undefined") { + attr["theta"] = 0; + } + + this.drawEllipseRegion( + attr["cx"], + attr["cy"], + attr["rx"], + attr["ry"], + attr["theta"], + is_selected + ); + + break; + + case VIA_REGION_SHAPE.POLYLINE: // handled by polygon + + case VIA_REGION_SHAPE.POLYGON: + this.drawPolygonRegion( + attr["all_points_x"], + attr["all_points_y"], + is_selected, + attr["name"] + ); + + break; + + case VIA_REGION_SHAPE.POINT: + this.drawPointRegion(attr["cx"], attr["cy"], is_selected); + + break; + } + } + } // draw all region + + //-------------------------------------------- + // Region draw functions + //-------------------------------------------- + + /** + * Draw circle point + * This is a helper function for client (e.g. it creates the circles on the rectangle corners) + * @param cx - x coordinate of circle center + * @param cy - y coordinate of circle center + */ + drawControlPoint(cx, cy) { + this.ctx.beginPath(); + this.ctx.arc( + cx, + cy, + this.region_shapes_points_radius, + 0, + 2 * Math.PI, + false + ); + this.ctx.closePath(); + this.ctx.fillStyle = this.theme_control_point_color; + this.ctx.fill(); + this.ctx.globalAlpha = 1.0; + } // draw control point + + /** + * Draw a rectangle region + * @param x - x coordinate of top-left corner + * @param y - y coordinate of top-left corner + * @param w - width of rectangle + * @param h - height of rectangle + * @param is_selected - true if this region is selected + */ + drawRectRegion(x, y, w, h, is_selected) { + if (is_selected) { + this.lineStyle( + this.theme_region_boundary_width / 2, + this.selected_region_boundary_line_color + ); + this.beginFill(); + + this.ctx.beginPath(); + if (settings.is_highlight_region) { + this.ctx.fillRect(x, y, w, h); + } + this.ctx.globalAlpha = 1.0; + this.ctx.strokeRect(x, y, w, h); + this.ctx.closePath(); + + this.drawControlPoint(x, y); + this.drawControlPoint(x + w, y); + this.drawControlPoint(x, y + h); + this.drawControlPoint(x + w, y + h); + this.drawControlPoint(x + w / 2, y); + this.drawControlPoint(x + w / 2, y + h); + this.drawControlPoint(x, y + h / 2); + this.drawControlPoint(x + w, y + h / 2); + } else { + this.ctx.beginPath(); + this.ctx.strokeRect(x, y, w, h); + this.ctx.closePath(); + } + } // draw rect region + + /** + * Draw a circle region + * @param cx - x coordinate of circle center + * @param cy - y coordinate of circle center + * @param r - radius of circle + * @param is_selected - true if this region is selected + */ + drawCircleRegion(cx, cy, r, is_selected) { + if (is_selected) { + this.ctx.beginPath(); + this.ctx.arc(cx, cy, r, 0, 2 * Math.PI, false); + this.ctx.closePath(); + + this.lineStyle( + this.theme_region_boundary_width / 2, + this.selected_region_boundary_line_color + ); + this.beginFill(); + this.ctx.stroke(); + if (settings.is_highlight_region) { + this.ctx.fill(); + } + this.drawControlPoint(cx + r, cy); + } else { + this.ctx.beginPath(); + this.ctx.arc(cx, cy, r, 0, 2 * Math.PI, false); + this.ctx.closePath(); + this.ctx.stroke(); + } + } // draw circle region + + /** + * Draw an ellipse region + * @param cx - x coordinate of ellipse center + * @param cy - y coordinate of ellipse center + * @param rx - x radius of ellipse + * @param ry - y radius of ellipse + * @param rr - rotation angle of ellipse + * @param is_selected - true if this region is selected + */ + drawEllipseRegion(cx, cy, rx, ry, rr, is_selected) { + if (is_selected) { + this.lineStyle( + this.theme_region_boundary_width / 2, + this.selected_region_boundary_line_color + ); + this.beginFill(); + + this.ctx.beginPath(); + this.ctx.ellipse(cx, cy, rx, ry, rr, 0, 2 * Math.PI, false); + this.ctx.closePath(); + + this.ctx.stroke(); + if (settings.is_highlight_region) { + this.ctx.fill(); + } + this.drawControlPoint(cx + rx * Math.cos(rr), cy + rx * Math.sin(rr)); + this.drawControlPoint(cx - rx * Math.cos(rr), cy - rx * Math.sin(rr)); + this.drawControlPoint(cx + ry * Math.sin(rr), cy - ry * Math.cos(rr)); + this.drawControlPoint(cx - ry * Math.sin(rr), cy + ry * Math.cos(rr)); + } else { + this.ctx.beginPath(); + this.ctx.ellipse(cx, cy, rx, ry, rr, 0, 2 * Math.PI, false); + this.ctx.closePath(); + + this.ctx.stroke(); + } + } // draw ellipse region + + /** + * Draw a polygon region + * @param x - array of x coordinates of polygon vertices + * @param y - array of y coordinates of polygon vertices + * @param is_selected - true if this region is selected + */ + drawPolygonRegion(x, y, is_selected) { + if (x === undefined || y === undefined) { + return; + } + + if (is_selected) { + this.lineStyle( + this.theme_region_boundary_width / 2, + this.selected_region_boundary_line_color + ); + this.beginFill(); + this.ctx.beginPath(); + this.ctx.moveTo(x[0], y[0]); + for (let i = 1; i < x.length; i++) { + this.ctx.lineTo(x[i], y[i]); + } + if (this.is_user_drawing_polygon) { + this.ctx.lineTo(x[0], y[0]); + } + this.ctx.closePath(); + this.ctx.stroke(); + if (settings.is_highlight_region) { + this.ctx.fill(); + } + for (let i = 0; i < x.length; i++) { + this.drawControlPoint(x[i], y[i]); + } + } else { + this.ctx.beginPath(); + this.ctx.moveTo(x[0], y[0]); + for (let i = 1; i < x.length; i++) { + this.ctx.lineTo(x[i], y[i]); + } + if (this.is_user_drawing_polygon) { + this.ctx.lineTo(x[0], y[0]); + } + this.ctx.closePath(); + this.ctx.stroke(); + } + } // draw polygon region + + /** + * Draw a point region + * @param x - x coordinate of point + * @param y - y coordinate of point + * @param is_selected - true if this region is selected + */ + drawPointRegion(x, y, is_selected) { + if (is_selected) { + this.ctx.beginPath(); + this.ctx.arc(x, y, this.region_point_radius, 0, 2 * Math.PI, false); + this.ctx.closePath(); + + this.lineStyle( + this.theme_region_boundary_width / 2, + this.selected_region_boundary_line_color + ); + this.beginFill(); + this.ctx.stroke(); + if (settings.is_highlight_region) { + this.ctx.fill(); + } + this.drawControlPoint(x, y); + } else { + this.ctx.beginPath(); + this.ctx.arc(x, y, this.region_point_radius, 0, 2 * Math.PI, false); + this.ctx.closePath(); + + this.ctx.stroke(); + } + } // draw point region + + /** + * Draw line region + */ + + drawLineRegion(x1, y1, x2, y2, is_selected) { + if (is_selected) { + this.lineStyle( + this.theme_region_boundary_width / 2, + this.selected_region_boundary_line_color + ); + this.beginFill(); + this.ctx.beginPath(); + this.ctx.moveTo(x1, y1); + this.ctx.lineTo(x2, y2); + this.ctx.closePath(); + this.ctx.stroke(); + if (settings.is_highlight_region) { + this.ctx.fill(); + } + this.drawControlPoint(x1, y1); + this.drawControlPoint(x2, y2); + } else { + this.ctx.beginPath(); + this.ctx.moveTo(x1, y1); + this.ctx.lineTo(x2, y2); + this.ctx.closePath(); + this.ctx.stroke(); + } + } // draw line region + + //-------------------------------------------- + // Region ID, label drawing + //-------------------------------------------- + + /** + * Draw a little rounded rectangle at the calculated position + * witch fill color and line color are the same as the region boundary + * And fill with a region id number + * It iterates over all regions and draw the region ID + */ + drawAllRegionId() { + let aid = _via_settings.ui.image.region_color; + for (let i = 0; i < _via_img_metadata[_via_image_id].regions.length; ++i) { + let canvas_reg = _via_canvas_regions[i]; + let bbox = this.getRegionBoundingBox(canvas_reg); + let x = bbox[0]; + let y = bbox[1]; + let w = Math.abs(bbox[2] - bbox[0]); + let char_width = Math.floor(this.ctx.measureText("M").width); + let char_height = 1.8 * char_width; + let annotation_str = (i + 1).toString(); + let rattr = + _via_img_metadata[_via_image_id].regions[i].region_attributes[ + _via_settings.ui.image.region_label + ]; + if (_via_settings.ui.image.region_label !== "__via_region_id__") { + if (typeof rattr !== "undefined") { + switch (typeof rattr) { + case "string": + annotation_str = rattr; + break; + case "object": + annotation_str = Object.keys(rattr).join(","); + break; + default: + annotation_str = rattr; + break; + } + } else { + annotation_str = "undefined"; + } + } + let bgnd_rect_width; + let strw = this.ctx.measureText(annotation_str).width; + if (strw > w) { + if (_via_settings.ui.image.region_label === "__via_region_id__") { + // region-id is always visible in full + bgnd_rect_width = strw + char_width; + } else { + // if text overflows, crop it + let str_max = Math.floor((w * annotation_str.length) / strw); + if (str_max > 1) { + annotation_str = annotation_str.substring(0, str_max - 1) + "."; + bgnd_rect_width = w; + } else { + annotation_str = annotation_str.substring(0, 1) + "."; + bgnd_rect_width = 2 * char_width; + } + } + } else { + bgnd_rect_width = strw + char_width; + } + if ( + canvas_reg.shape_attributes["name"] === VIA_REGION_SHAPE.POLYGON || + canvas_reg.shape_attributes["name"] === VIA_REGION_SHAPE.POLYLINE + ) { + // put label near the first vertex + //MODSote + if ( + canvas_reg.shape_attributes["id_x"] === undefined || + !canvas_reg.shape_attributes["all_points_x"].includes( + canvas_reg.shape_attributes["id_x"] + ) + ) { + let random = Math.floor( + Math.random() * canvas_reg.shape_attributes["all_points_x"].length + ); + x = canvas_reg.shape_attributes["all_points_x"][random]; + y = canvas_reg.shape_attributes["all_points_y"][random]; + _via_canvas_regions[i].shape_attributes["id_x"] = x; + _via_canvas_regions[i].shape_attributes["id_y"] = y; + } else { + x = canvas_reg.shape_attributes["id_x"]; + y = canvas_reg.shape_attributes["id_y"]; + } + } else { + // center the label + x = x - (bgnd_rect_width / 2 - w / 2); + } + // ensure that the text is within the image boundaries + if (y < char_height) { + y = char_height; + } + let avalue = + _via_img_metadata[_via_image_id].regions[i].region_attributes[aid]; + + if (this.canvas_regions_group_color.hasOwnProperty(avalue)) { + this.beginFill(this.canvas_regions_group_color[avalue], 0.8); + } else { + this.beginFill("#0d6efd", 0.8); + } + this.ctx.beginPath(); + + if (this.current_browser === "Firefox") { + this.ctx.rect( + Math.floor(x), + Math.floor(y - 1.1 * char_height), + Math.floor(bgnd_rect_width), + Math.floor(char_height) + ); + } else { + this.ctx.roundRect( + Math.floor(x), + Math.floor(y - 1.1 * char_height), + Math.floor(bgnd_rect_width), + Math.floor(char_height), + 5 + ); + } + this.ctx.fill(); + this.ctx.closePath(); + this.ctx.globalAlpha = 1.0; + this.ctx.fillStyle = "white"; + this.ctx.fillText( + annotation_str, + Math.floor(x + 0.5 * char_width), + Math.floor(y - 0.35 * char_height) + ); + } + } // draw all region labels + + /** + * Calculates region ID position + * Helper function for drawAllRegionId() + * @param region - region to calculate bounding box + * @returns {any[]} - bounding box of the region + */ + getRegionBoundingBox(region) { + let d = region.shape_attributes; + let bbox = new Array(4); + + switch (d["name"]) { + case "rect": { + bbox[0] = d["x"]; + bbox[1] = d["y"]; + bbox[2] = d["x"] + d["width"]; + bbox[3] = d["y"] + d["height"]; + break; + } + case "circle": { + bbox[0] = d["cx"] - d["r"]; + bbox[1] = d["cy"] - d["r"]; + bbox[2] = d["cx"] + d["r"]; + bbox[3] = d["cy"] + d["r"]; + break; + } + case "ellipse": { + let radians = d["theta"]; + let radians90 = radians + Math.PI / 2; + let ux = d["rx"] * Math.cos(radians); + let uy = d["rx"] * Math.sin(radians); + let vx = d["ry"] * Math.cos(radians90); + let vy = d["ry"] * Math.sin(radians90); + let width = Math.sqrt(ux * ux + vx * vx) * 2; + let height = Math.sqrt(uy * uy + vy * vy) * 2; + bbox[0] = d["cx"] - width / 2; + bbox[1] = d["cy"] - height / 2; + bbox[2] = d["cx"] + width / 2; + bbox[3] = d["cy"] + height / 2; + break; + } + case "polyline": // handled by polygon + + case "polygon": { + let all_points_x = d["all_points_x"]; + let all_points_y = d["all_points_y"]; + let minx = Number.MAX_SAFE_INTEGER; + let miny = Number.MAX_SAFE_INTEGER; + let maxx = 0; + let maxy = 0; + + for (let i = 0; i < all_points_x.length; ++i) { + if (all_points_x[i] < minx) { + minx = all_points_x[i]; + } + + if (all_points_x[i] > maxx) { + maxx = all_points_x[i]; + } + + if (all_points_y[i] < miny) { + miny = all_points_y[i]; + } + + if (all_points_y[i] > maxy) { + maxy = all_points_y[i]; + } + } + + bbox[0] = minx; + bbox[1] = miny; + bbox[2] = maxx; + bbox[3] = maxy; + break; + } + case "point": { + bbox[0] = d["cx"] - this.region_point_radius; + bbox[1] = d["cy"] - this.region_point_radius; + bbox[2] = d["cx"] + this.region_point_radius; + bbox[3] = d["cy"] + this.region_point_radius; + break; + } + } + + return bbox; + } + + //-------------------------------------------- + // Region collision routines + //-------------------------------------------- + + /** + * Checks if a point is inside a region + * @param p - point to check + * @param descending_order - if true, then regions are checked in descending order of their area + * @returns {number} - region id if point is inside a region, else -1 + */ + isInsideRegion(p, descending_order = false, ignore_lock = false) { + let N = _via_canvas_regions.length; + + if (N === 0) { + return -1; + } + + let start, end, del; // traverse the canvas regions in alternating ascending + // and descending order to solve the issue of nested regions + + if (descending_order) { + start = N - 1; + end = -1; + del = -1; + } else { + start = 0; + end = N; + del = 1; + } + let i = start; + + while (i !== end) { + if (this.isInsideThisRegion(p, i, ignore_lock)) { + return i; + } + + i = i + del; + } + return -1; + } // is that point inside any region on the canvas? Mouse click detection + + //check all region if the point is inside and return all region id + isInsideAllRegion(p, descending_order = false, ignore_lock = false) { + let N = _via_canvas_regions.length; + + if (N === 0) { + return -1; + } + + let start, end, del; // traverse the canvas regions in alternating ascending + // and descending order to solve the issue of nested regions + + if (descending_order) { + start = N - 1; + end = -1; + del = -1; + } else { + start = 0; + end = N; + del = 1; + } + let i = start; + let result = []; + while (i !== end) { + if (this.isInsideThisRegion(p, i, ignore_lock)) { + result.push(i); + } + i = i + del; + } + return result; + } + + /** + * Checks if a point is inside a region + * Helper function for isInsideRegion() + * @param p - point to check + * @param region_id - region to check + * @returns {boolean} - true if point is inside the region, else false + */ + isInsideThisRegion(p, region_id, ignore_lock = false) { + let attr = _via_canvas_regions[region_id].shape_attributes; + let result = false; + if (_via_img_metadata[_via_image_id].isRegionLocked(region_id) && !ignore_lock) { + return false; + } + + switch (attr["name"]) { + case VIA_REGION_SHAPE.RECT: + result = this.isInsideRect( + attr["x"], + attr["y"], + attr["width"], + attr["height"], + p.x, + p.y + ); + break; + + case VIA_REGION_SHAPE.CIRCLE: + result = this.isInsideCircle( + attr["cx"], + attr["cy"], + attr["r"], + p.x, + p.y + ); + break; + + case VIA_REGION_SHAPE.ELLIPSE: + result = this.isInsideEllipse( + attr["cx"], + attr["cy"], + attr["rx"], + attr["ry"], + attr["theta"], + p.x, + p.y + ); + break; + + case VIA_REGION_SHAPE.POLYLINE: // handled by POLYGON + + case VIA_REGION_SHAPE.POLYGON: + result = this.isInsidePolygon( + attr["all_points_x"], + attr["all_points_y"], + p.x, + p.y + ); + break; + + case VIA_REGION_SHAPE.POINT: + result = this.isInsidePoint(attr["cx"], attr["cy"], p.x, p.y); + break; + } + + return result; + } // is that point inside a shape? Helper function for isInsideRegion() + + /** + * Checks if a point is inside a circle region + * @param cx - x-coordinate of circle center + * @param cy - y-coordinate of circle center + * @param r - radius of circle + * @param px - x-coordinate of point + * @param py - y-coordinate of point + * @returns {boolean} - true if point is inside the circle, else false + */ + isInsideCircle(cx, cy, r, px, py) { + let dx = px - cx; + let dy = py - cy; + return dx * dx + dy * dy < r * r; + } // is that point inside a circle? Helper function for isInsideThisRegion() + + /** + * Checks if a point is inside a rectangle region + * @param x - x-coordinate of top-left corner of rectangle + * @param y - y-coordinate of top-left corner of rectangle + * @param w - width of rectangle + * @param h - height of rectangle + * @param px - x-coordinate of point + * @param py - y-coordinate of point + * @returns {boolean} - true if point is inside the rectangle, else false + */ + isInsideRect(x, y, w, h, px, py) { + return px > x && px < x + w && py > y && py < y + h; + } // is that point inside a rectangle? Helper function for isInsideThisRegion() + + /** + * Checks if a point is inside an ellipse region + * @param cx - x-coordinate of ellipse center + * @param cy - y-coordinate of ellipse center + * @param rx - x-radius of ellipse + * @param ry - y-radius of ellipse + * @param rr - rotation angle of ellipse + * @param px - x-coordinate of point + * @param py - y-coordinate of point + * @returns {boolean} - true if point is inside the ellipse, else false + */ + isInsideEllipse(cx, cy, rx, ry, rr, px, py) { + // Inverse rotation of pixel coordinates + let dx = Math.cos(-rr) * (cx - px) - Math.sin(-rr) * (cy - py); + let dy = Math.sin(-rr) * (cx - px) + Math.cos(-rr) * (cy - py); + return (dx * dx) / (rx * rx) + (dy * dy) / (ry * ry) < 1; + } // is that point inside an ellipse? Helper function for isInsideThisRegion() + // returns 0 when (px,py) is outside the polygon + // source: http://geomalgorithms.com/a03-_inclusion.html + + /** + * Checks if a point is inside a polygon region + * @param all_points_x - x-coordinates of polygon vertices + * @param all_points_y - y-coordinates of polygon vertices + * @param px - x-coordinate of point + * @param py - y-coordinate of point + * @returns {number} - 0 if point is outside the polygon, 1 if point is inside the polygon + */ + isInsidePolygon(all_points_x, all_points_y, px, py) { + if (all_points_x.length === 0 || all_points_y.length === 0) { + return 0; + } + + let wn = 0; // the winding number counter + + let n = all_points_x.length; + let i; // loop through all edges of the polygon + + for (i = 0; i < n - 1; ++i) { + // edge from V[i] to V[i+1] + let is_left_value = this.isLeft( + all_points_x[i], + all_points_y[i], + all_points_x[i + 1], + all_points_y[i + 1], + px, + py + ); + + if (all_points_y[i] <= py) { + if (all_points_y[i + 1] > py && is_left_value > 0) { + ++wn; + } + } else { + if (all_points_y[i + 1] <= py && is_left_value < 0) { + --wn; + } + } + } // also take into account the loop closing edge that connects last point with first point + + let is_left_value = this.isLeft( + all_points_x[n - 1], + all_points_y[n - 1], + all_points_x[0], + all_points_y[0], + px, + py + ); + + if (all_points_y[n - 1] <= py) { + if (all_points_y[0] > py && is_left_value > 0) { + ++wn; + } + } else { + if (all_points_y[0] <= py && is_left_value < 0) { + --wn; + } + } + + if (wn === 0) { + return 0; + } else { + return 1; + } + } // is that point inside a polygon? Helper function for isInsideThisRegion() + + /** + * Checks if a point is inside a point region + * @param cx - x-coordinate of point + * @param cy - y-coordinate of point + * @param px - x-coordinate of point + * @param py - y-coordinate of point + * @returns {boolean} - true if point is inside the point, else false + */ + isInsidePoint(cx, cy, px, py) { + let dx = px - cx; + let dy = py - cy; + let r2 = this.polygon_vertex_match_tol * this.polygon_vertex_match_tol; + return dx * dx + dy * dy < r2; + } // is that point inside a point? Helper function for isInsideThisRegion() + // >0 if (x2,y2) lies on the left side of line joining (x0,y0) and (x1,y1) + // =0 if (x2,y2) lies on the line joining (x0,y0) and (x1,y1) + // >0 if (x2,y2) lies on the right side of line joining (x0,y0) and (x1,y1) + // source: http://geomalgorithms.com/a03-_inclusion.html + + /** + * Checks if a point is left of a line + * @param x0 - x-coordinate of first point of line + * @param y0 - y-coordinate of first point of line + * @param x1 - x-coordinate of second point of line + * @param y1 - y-coordinate of second point of line + * @param x2 - x-coordinate of point + * @param y2 - y-coordinate of point + * @returns {number} - >0 if point is left of line, =0 if point is on line, <0 if point is right of line + */ + isLeft(x0, y0, x1, y1, x2, y2) { + return (x1 - x0) * (y2 - y0) - (x2 - x0) * (y1 - y0); + } //helper function for isInsidePolygon() + + /** + * Checks if a point is on a region boundary + * @param p - point + * @returns {number[]} - array of region indices that the point is on the boundary of + */ + isOnRegionCorner(p) { + let _region_edge = [-1, -1]; // region_id, corner_id [top-left=1,top-right=2,bottom-right=3,bottom-left=4] + + /*if(this.is_region_selected){ + let attr = _via_canvas_regions[this.user_sel_region_id].shape_attributes; + let result = false; + switch (attr["name"]) { + case VIA_REGION_SHAPE.RECT: + result = this.isOnRectEdge( + attr["x"], + attr["y"], + attr["width"], + attr["height"], + p.x, + p.y + ); + break; + + case VIA_REGION_SHAPE.CIRCLE: + result = this.isOnCircleEdge(attr["cx"], attr["cy"], attr["r"], p.x, p.y); + break; + + case VIA_REGION_SHAPE.ELLIPSE: + result = this.isOnEllipseEdge( + attr["cx"], + attr["cy"], + attr["rx"], + attr["ry"], + attr["theta"], + p.x, + p.y + ); + break; + + case VIA_REGION_SHAPE.POLYLINE: // handled by polygon + + case VIA_REGION_SHAPE.POLYGON: + result = this.isOnPolygonVertex( + attr["all_points_x"], + attr["all_points_y"], + p + ); + + if (result === 0) { + result = this.isOnPolygonEdge( + attr["all_points_x"], + attr["all_points_y"], + p.x, + p.y + ); + } + + break; + + case VIA_REGION_SHAPE.POINT: + // since there are no edges of a point + result = 0; + break; + } + + + if (result > 0) { + _region_edge[1] = result; + return _region_edge; + } + }*/ + + for (let i = 0; i < _via_canvas_regions.length; ++i) { + let attr = _via_canvas_regions[i].shape_attributes; + let result = false; + _region_edge[0] = i; + if (_via_img_metadata[_via_image_id].isRegionLocked(i)) { + continue; + } + + switch (attr["name"]) { + case VIA_REGION_SHAPE.RECT: + result = this.isOnRectEdge( + attr["x"], + attr["y"], + attr["width"], + attr["height"], + p.x, + p.y + ); + break; + + case VIA_REGION_SHAPE.CIRCLE: + result = this.isOnCircleEdge( + attr["cx"], + attr["cy"], + attr["r"], + p.x, + p.y + ); + break; + + case VIA_REGION_SHAPE.ELLIPSE: + result = this.isOnEllipseEdge( + attr["cx"], + attr["cy"], + attr["rx"], + attr["ry"], + attr["theta"], + p.x, + p.y + ); + break; + + case VIA_REGION_SHAPE.POLYLINE: // handled by polygon + + case VIA_REGION_SHAPE.POLYGON: + result = this.isOnPolygonVertex( + attr["all_points_x"], + attr["all_points_y"], + p + ); + + if (result === 0) { + result = this.isOnPolygonEdge( + attr["all_points_x"], + attr["all_points_y"], + p.x, + p.y + ); + } + + break; + + case VIA_REGION_SHAPE.POINT: + // since there are no edges of a point + result = 0; + break; + } + + if (result > 0) { + _region_edge[1] = result; + return _region_edge; + } + } + + _region_edge[0] = -1; + return _region_edge; + } // is the mouse pointer on a region corner? + + /** + * Checks if a point is on a rectangle boundary + * @param x - x-coordinate of rectangle + * @param y - y-coordinate of rectangle + * @param w - width of rectangle + * @param h - height of rectangle + * @param px - x-coordinate of point + * @param py - y-coordinate of point + * @returns {number} - 0 if point is not on boundary, else corner id + */ + isOnRectEdge(x, y, w, h, px, py) { + let dx0 = Math.abs(x - px); + let dy0 = Math.abs(y - py); + let dx1 = Math.abs(x + w - px); + let dy1 = Math.abs(y + h - py); //[top-left=1,top-right=2,bottom-right=3,bottom-left=4] + + if (dx0 < this.region_edge_tol && dy0 < this.region_edge_tol) { + return 1; + } + + if (dx1 < this.region_edge_tol && dy0 < this.region_edge_tol) { + return 2; + } + + if (dx1 < this.region_edge_tol && dy1 < this.region_edge_tol) { + return 3; + } + + if (dx0 < this.region_edge_tol && dy1 < this.region_edge_tol) { + return 4; + } + + let mx0 = Math.abs(x + w / 2 - px); + let my0 = Math.abs(y + h / 2 - py); //[top-middle=5,right-middle=6,bottom-middle=7,left-middle=8] + + if (mx0 < this.region_edge_tol && dy0 < this.region_edge_tol) { + return 5; + } + + if (dx1 < this.region_edge_tol && my0 < this.region_edge_tol) { + return 6; + } + + if (mx0 < this.region_edge_tol && dy1 < this.region_edge_tol) { + return 7; + } + + if (dx0 < this.region_edge_tol && my0 < this.region_edge_tol) { + return 8; + } + return 0; + } // is the mouse pointer on a rectangle edge? Helper function for isOnRegionCorner() + + /** + * Checks if a point is on a circle boundary + * @param cx - x-coordinate of circle center + * @param cy - y-coordinate of circle center + * @param r - radius of circle + * @param px - x-coordinate of point + * @param py - y-coordinate of point + * @returns {number} - 0 if point is not on boundary, else corner id + */ + isOnCircleEdge(cx, cy, r, px, py) { + let dx = cx - px; + let dy = cy - py; + + if (Math.abs(Math.sqrt(dx * dx + dy * dy) - r) < this.region_edge_tol) { + let theta = Math.atan2(py - cy, px - cx); + + if ( + Math.abs(theta - Math.PI / 2) < this.theta_tol || + Math.abs(theta + Math.PI / 2) < this.theta_tol + ) { + return 5; + } + + if ( + Math.abs(theta) < this.theta_tol || + Math.abs(Math.abs(theta) - Math.PI) < this.theta_tol + ) { + return 6; + } + + if (theta > 0 && theta < Math.PI / 2) { + return 1; + } + + if (theta > Math.PI / 2 && theta < Math.PI) { + return 4; + } + + if (theta < 0 && theta > -(Math.PI / 2)) { + return 2; + } + + if (theta < -(Math.PI / 2) && theta > -Math.PI) { + return 3; + } + } else { + return 0; + } + } // is the mouse pointer on a circle edge? Helper function for isOnRegionCorner() + + /** + * Checks if a point is on an ellipse boundary + * @param cx - x-coordinate of ellipse center + * @param cy - y-coordinate of ellipse center + * @param rx - x-radius of ellipse + * @param ry - y-radius of ellipse + * @param rr - rotation of ellipse + * @param px - x-coordinate of point + * @param py - y-coordinate of point + * @returns {number} - 0 if point is not on boundary, else corner id + */ + isOnEllipseEdge(cx, cy, rx, ry, rr, px, py) { + // Inverse rotation of pixel coordinates + px = px - cx; + py = py - cy; + let px_ = Math.cos(-rr) * px - Math.sin(-rr) * py; + let py_ = Math.sin(-rr) * px + Math.cos(-rr) * py; + px = px_ + cx; + py = py_ + cy; + let dx = (cx - px) / rx; + let dy = (cy - py) / ry; + + if (Math.abs(Math.sqrt(dx * dx + dy * dy) - 1) < this.ellipse_edge_tol) { + let theta = Math.atan2(py - cy, px - cx); + + if ( + Math.abs(theta - Math.PI / 2) < this.theta_tol || + Math.abs(theta + Math.PI / 2) < this.theta_tol + ) { + return 5; + } + + if ( + Math.abs(theta) < this.theta_tol || + Math.abs(Math.abs(theta) - Math.PI) < this.theta_tol + ) { + return 6; + } + } else { + return 0; + } + } // is the mouse pointer on an ellipse edge? Helper function for isOnRegionCorner() + + /** + * Checks if a point is on a polygon vertex + * @param all_points_x - x-coordinates of polygon vertices + * @param all_points_y - y-coordinates of polygon vertices + * @param p - point to check + * @returns {number} - 0 if point is not on a vertex, else corner id + */ + isOnPolygonVertex(all_points_x, all_points_y, p) { + let i, n; + n = all_points_x.length; + + for (i = 0; i < n; ++i) { + if ( + Math.abs(all_points_x[i] - p.x) < this.polygon_vertex_match_tol && + Math.abs(all_points_y[i] - p.y) < this.polygon_vertex_match_tol + ) { + return this.polygon_resize_vertex_offset + i; + } + } + + return 0; + } // is the mouse pointer on a polygon vertex? Helper function for isOnRegionCorner() + + /** + * Checks if a point is on a polygon edge + * @param all_points_x - x-coordinates of polygon vertices + * @param all_points_y - y-coordinates of polygon vertices + * @param px - x-coordinate of point + * @param py - y-coordinate of point + * @returns {number} - 0 if point is not on the edge, else corner id + */ + isOnPolygonEdge(all_points_x, all_points_y, px, py) { + let i, n, di, d; + n = all_points_x.length; + d = []; + + for (i = 0; i < n - 1; ++i) { + di = this.distToLine( + px, + py, + all_points_x[i], + all_points_y[i], + all_points_x[i + 1], + all_points_y[i + 1] + ); + d.push(di); + } // closing edge + + di = this.distToLine( + px, + py, + all_points_x[n - 1], + all_points_y[n - 1], + all_points_x[0], + all_points_y[0] + ); + d.push(di); + let smallest_value = d[0]; + let smallest_index = 0; + n = d.length; + + for (i = 1; i < n; ++i) { + if (d[i] < smallest_value) { + smallest_value = d[i]; + smallest_index = i; + } + } + let sm_index = smallest_index + 1; + if (sm_index >= n) { + sm_index = 0; + } + let smx = all_points_x[sm_index]; + let smy = all_points_y[sm_index]; + let smm = all_points_x[smallest_index]; + let smn = all_points_y[smallest_index]; + + //measure the distance from the px py to sx sy and smx smy + let d1 = Math.sqrt((px - smx) * (px - smx) + (py - smy) * (py - smy)); + let d2 = Math.sqrt((px - smm) * (px - smm) + (py - smn) * (py - smn)); + + if (d1 < d2) { + smallest_index = sm_index; + } // if the distance from the point to the edge is less than the distance from the point to the vertex, then the point is on the edge + + if (smallest_value < this.polygon_vertex_match_tol) { + return this.polygon_resize_vertex_offset + smallest_index; + } else { + return 0; + } + } // is the mouse pointer on a polygon edge? Helper function for isOnRegionCorner() + + is_on_polygon_edge(all_points_x, all_points_y, px, py) { + var i, n, di, d; + n = all_points_x.length; + d = []; + for (i = 0; i < n - 1; ++i) { + di = this.distToLine(px, py, all_points_x[i], all_points_y[i], all_points_x[i + 1], all_points_y[i + 1]); + d.push(di); + } + // closing edge + di = this.distToLine(px, py, all_points_x[n - 1], all_points_y[n - 1], all_points_x[0], all_points_y[0]); + d.push(di); + + var smallest_value = d[0]; + var smallest_index = 0; + n = d.length; + for (i = 1; i < n; ++i) { + if (d[i] < smallest_value) { + smallest_value = d[i]; + smallest_index = i; + } + } + if (smallest_value < this.polygon_vertex_match_tol) { + return (this.polygon_resize_vertex_offset + smallest_index); + } else { + return 0; + } + } + + + /** + * Checks if a point is inside a generated rectangle + * Helper function for distToLine() + * @param x - x-coordinate of point + * @param y - y-coordinate of point + * @param x1 - x-coordinate of rectangle top left corner + * @param y1 - y-coordinate of rectangle top left corner + * @param x2 - x-coordinate of rectangle bottom right corner + * @param y2 - y-coordinate of rectangle bottom right corner + * @returns {boolean} - true if point is inside rectangle, else false + */ + isPointInsideBoundingBox(x, y, x1, y1, x2, y2) { + // ensure that (x1,y1) is top left and (x2,y2) is bottom right corner of rectangle + let rect = {}; + + if (x1 < x2) { + rect.x1 = x1; + rect.x2 = x2; + } else { + rect.x1 = x2; + rect.x2 = x1; + } + + if (y1 < y2) { + rect.y1 = y1; + rect.y2 = y2; + } else { + rect.y1 = y2; + rect.y2 = y1; + } + + return !!(x >= rect.x1 && x <= rect.x2 && y >= rect.y1 && y <= rect.y2); // return true if point is inside rectangle modSote + } // is the mouse pointer inside a bounding box? Helper function for isOnRegionCorner() + + /** + * Helper of isOnPolygonEdge() + * @param x - x-coordinate of point + * @param y - y-coordinate of point + * @param x1 - x-coordinate of line start + * @param y1 - y-coordinate of line start + * @param x2 - x-coordinate of line end + * @param y2 - y-coordinate of line end + * @returns {number} - distance from point to line + */ + distToLine(x, y, x1, y1, x2, y2) { + if (this.isPointInsideBoundingBox(x, y, x1, y1, x2, y2)) { + let dy = y2 - y1; + let dx = x2 - x1; + let nr = Math.abs(dy * x - dx * y + x2 * y1 - y2 * x1); + let dr = Math.sqrt(dx * dx + dy * dy); + let dist = nr / dr; + return Math.round(dist); + } else { + return Number.MAX_SAFE_INTEGER; + } + } // distance from point to line segment + + /** + * ensure that (d[0],d[1]) is top-left corner while (d[2],d[3]) is bottom-right corner + * @param d - region data + */ + rectStandardizeCoordinates(d) { + // d[x0,y0,x1,y1] + // ensures that (d[0],d[1]) is top-left corner while + // (d[2],d[3]) is bottom-right corner + if (d[0] > d[2]) { + // swap + let t = d[0]; + d[0] = d[2]; + d[2] = t; + } + + if (d[1] > d[3]) { + // swap + let t = d[1]; + d[1] = d[3]; + d[3] = t; + } + } // ensure that (d[0],d[1]) is top-left corner while (d[2],d[3]) is bottom-right corner + + /** + * Efficiently move a corner of a rectangle + * @param corner_id - corner id + * @param d - region data + * @param x - x-coordinate of mouse pointer + * @param y - y-coordinate of mouse pointer + * @param preserve_aspect_ratio - preserve aspect ratio of rectangle + */ + rectUpdateCorner(corner_id, d, x, y, preserve_aspect_ratio) { + // pre-condition : d[x0,y0,x1,y1] is standardized + // post-condition : corner is moved ( d may not stay standardized ) + if (preserve_aspect_ratio) { + switch (corner_id) { + case 1: // Fall-through // top-left + + case 3: { + // bottom-right + let dx = d[2] - d[0]; + let dy = d[3] - d[1]; + let norm = Math.sqrt(dx * dx + dy * dy); + let nx = dx / norm; // x component of unit vector along the diagonal of rect + + let ny = dy / norm; // y component + + let proj = (x - d[0]) * nx + (y - d[1]) * ny; + let proj_x = nx * proj; + let proj_y = ny * proj; // constrain (mx,my) to lie on a line connecting (x0,y0) and (x1,y1) + + x = Math.round(d[0] + proj_x); + y = Math.round(d[1] + proj_y); + break; + } + case 2: // Fall-through // top-right + + case 4: { + // bottom-left + let dx = d[2] - d[0]; + let dy = d[1] - d[3]; + let norm = Math.sqrt(dx * dx + dy * dy); + let nx = dx / norm; // x component of unit vector along the diagonal of rect + + let ny = dy / norm; // y component + + let proj = (x - d[0]) * nx + (y - d[3]) * ny; + let proj_x = nx * proj; + let proj_y = ny * proj; // constrain (mx,my) to lie on a line connecting (x0,y0) and (x1,y1) + + x = Math.round(d[0] + proj_x); + y = Math.round(d[3] + proj_y); + break; + } + } + } + + switch (corner_id) { + case 1: + // top-left + d[0] = x; + d[1] = y; + break; + + case 3: + // bottom-right + d[2] = x; + d[3] = y; + break; + + case 2: + // top-right + d[2] = x; + d[1] = y; + break; + + case 4: + // bottom-left + d[0] = x; + d[3] = y; + break; + + case 5: + // top-middle + d[1] = y; + break; + + case 6: + // right-middle + d[2] = x; + break; + + case 7: + // bottom-middle + d[3] = y; + break; + + case 8: + // left-middle + d[0] = x; + break; + } + } // move a corner of a rectangle + + /** + * Set the canvas size + * @param w - width of the canvas + * @param h - height of the canvas + */ + setAllCanvasSize(w, h) { + _via_reg_canvas.height = h; + _via_reg_canvas.width = w; + } + + + /** + * Set the canvas scale + */ + setCanvasScale(scale) { + this.ctx.scale(scale, scale); + } + + //-------------------------------------------- + // Tools + //-------------------------------------------- + + /** + * Trim the selected Polygon region + * @param region_id - id of the region to be trimmed + */ + trimRegionSection(region_id) { + let attr = _via_canvas_regions[region_id].shape_attributes; + if (attr.name === "polygon") { + this.trim_region_id = region_id; + this.trim_phase_id = 1; + this.trim_points = { x: attr["all_points_x"], y: attr["all_points_y"] }; + } + } + + updateTrimPoints() { + if (this.trim_phase_id === -1) { + return; + } + if (_via_canvas_regions.hasOwnProperty(this.trim_region_id)) { + let attr = _via_canvas_regions[this.trim_region_id].shape_attributes; + this.trim_points = { x: attr["all_points_x"], y: attr["all_points_y"] }; + } + } + + calculateTrimPoints() { + if (this.trim_phase_id !== 2) { + return; + } + + let intersection = []; + // Calculate the intersection points + for (let i = 0; i < this.trim_points.x.length; i++) { + let x1 = this.trim_points.x[i]; + let y1 = this.trim_points.y[i]; + let x2 = this.trim_points.x[(i + 1) % this.trim_points.x.length]; + let y2 = this.trim_points.y[(i + 1) % this.trim_points.x.length]; + let x3 = this.trim_line["x0"]; + let y3 = this.trim_line["y0"]; + let x4 = this.trim_line["x1"]; + let y4 = this.trim_line["y1"]; + intersection.push( + this.lineLineIntersection(x1, y1, x2, y2, x3, y3, x4, y4) + ); + } + + for (let i = 0; i < intersection.length; i++) { + if (intersection[i][0] !== -1 && intersection[i][1] !== -1) { + this.trim_choose_points.push(intersection[i][0]); + this.trim_choose_points.push(intersection[i][1]); + } + } + + if (this.trim_choose_points.length < 4) { + this.destroyTrim(); + return; + } + if (this.trim_choose_points.length >= 4) { + this.trim_phase_id = 3; + this.drawTrimemdRegions(); + } else { + this.destroyTrim(); + } + } + + lineLineIntersection(x1, y1, x2, y2, x3, y3, x4, y4) { + const denominator = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4); + if (denominator === 0) { + return [-1, -1]; + } + + const numeratorX = (x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4); + const numeratorY = (x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4); + + let x = Math.round(numeratorX / denominator); + let y = Math.round(numeratorY / denominator); + + if (x >= Math.min(x1, x2) && x <= Math.max(x1, x2) && + x >= Math.min(x3, x4) && x <= Math.max(x3, x4) && + y >= Math.min(y1, y2) && y <= Math.max(y1, y2) && + y >= Math.min(y3, y4) && y <= Math.max(y3, y4)) { + return [x, y]; + } + + return [-1, -1]; + } + + drawTrimemdRegions() { + if (this.trim_phase_id < 3) { + return; + } + + //separate two polygons by the line + let polygon1 = { x: [], y: [] }; + let polygon2 = { x: [], y: [] }; + let is_first_polygon = true; + for (let i = 0; i < this.trim_points.x.length; i++) { + let x1 = this.trim_points.x[i]; + let y1 = this.trim_points.y[i]; + let x2 = this.trim_points.x[(i + 1) % this.trim_points.x.length]; + let y2 = this.trim_points.y[(i + 1) % this.trim_points.x.length]; + let x3 = this.trim_line["x0"]; + let y3 = this.trim_line["y0"]; + let x4 = this.trim_line["x1"]; + let y4 = this.trim_line["y1"]; + let intersection = this.lineLineIntersection( + x1, + y1, + x2, + y2, + x3, + y3, + x4, + y4 + ); + if (intersection[0] !== -1 && intersection[1] !== -1) { + if (is_first_polygon) { + polygon1.x.push(x1); + polygon1.y.push(y1); + polygon1.x.push(intersection[0]); + polygon1.y.push(intersection[1]); + polygon2.x.push(intersection[0]); + polygon2.y.push(intersection[1]); + is_first_polygon = false; + } else { + polygon2.x.push(x1); + polygon2.y.push(y1); + polygon2.x.push(intersection[0]); + polygon2.y.push(intersection[1]); + polygon1.x.push(intersection[0]); + polygon1.y.push(intersection[1]); + is_first_polygon = true; + } + } else { + if (is_first_polygon) { + polygon1.x.push(x1); + polygon1.y.push(y1); + } else { + polygon2.x.push(x1); + polygon2.y.push(y1); + } + } + } + this.trim_points["polygon1"] = polygon1; + this.trim_points["polygon2"] = polygon2; + + //draw polygons + this.ctx.beginPath(); + this.ctx.moveTo(polygon1.x[0], polygon1.y[0]); + for (let i = 1; i < polygon1.x.length; i++) { + this.ctx.lineTo(polygon1.x[i], polygon1.y[i]); + } + this.ctx.closePath(); + this.ctx.fillStyle = "rgba(255, 0, 0, 0.5)"; + this.ctx.fill(); + this.ctx.beginPath(); + this.ctx.moveTo(polygon2.x[0], polygon2.y[0]); + for (let i = 1; i < polygon2.x.length; i++) { + this.ctx.lineTo(polygon2.x[i], polygon2.y[i]); + } + this.ctx.closePath(); + this.ctx.fillStyle = "rgba(0, 255, 0, 0.5)"; + this.ctx.fill(); + this.trim_phase_id = 4; + } + + isInsideTrimPolygon(p, polygon) { + if (polygon === undefined) { + return false; + } + let inside = false; + for (let i = 0, j = polygon.x.length - 1; i < polygon.x.length; j = i++) { + let xi = polygon.x[i], + yi = polygon.y[i]; + let xj = polygon.x[j], + yj = polygon.y[j]; + let intersect = + yi > p.y !== yj > p.y && + p.x < ((xj - xi) * (p.y - yi)) / (yj - yi) + xi; + if (intersect) inside = !inside; + } + return inside; + } + + trimFromMetadata(polygon) { + if (this.trim_phase_id < 4) { + return; + } + let metaAttr = + _via_img_metadata[_via_image_id].regions[this.trim_region_id] + .shape_attributes; + + metaAttr = this.thisPointIsThePolygonPoint(polygon, metaAttr); + + _via_img_metadata[_via_image_id].regions[ + this.trim_region_id + ].shape_attributes = metaAttr; + drawing.setIsRegionSelected(false); + drawing.setUserSelRegionId(-1); + drawing.clearCanvas(); + _via_load_canvas_regions(); + if (_via_canvas_regions.length === 0) { + drawing.clearCanvas(); + } else { + drawing.redrawRegCanvas(); + } + this.destroyTrim(); + } + + thisPointIsThePolygonPoint(polygon, metaAttr) { + let newPoints = []; + let newPointsY = []; + let pointsX = metaAttr["all_points_x"]; + let pointsY = metaAttr["all_points_y"]; + let point = { + x: Math.round(pointsX[0] / _via_canvas_scale), + y: Math.round(pointsY[0] / _via_canvas_scale), + }; + for (let i = 0; i < polygon.x.length; i++) { + for (let j = 0; j < pointsX.length; j++) { + point = { + x: Math.round(pointsX[j] / _via_canvas_scale), + y: Math.round(pointsY[j] / _via_canvas_scale), + }; + if (polygon.x[i] === point.x && polygon.y[i] === point.y) { + newPoints.push(point.x * _via_canvas_scale); + newPointsY.push(point.y * _via_canvas_scale); + break; + } + } + for (let j = 0; j < this.trim_choose_points.length; j += 2) { + if ( + polygon.x[i] === this.trim_choose_points[j] && + polygon.y[i] === this.trim_choose_points[j + 1] + ) { + newPoints.push(polygon.x[i] * _via_canvas_scale); + newPointsY.push(polygon.y[i] * _via_canvas_scale); + break; + } + } + } + metaAttr["all_points_x"] = newPoints; + metaAttr["all_points_y"] = newPointsY; + return metaAttr; + } + + /** + * Destroy the trimming helper variables + */ + destroyTrim() { + this.trim_points = {}; + this.trim_choose_points = []; + this.trim_choose_points_id = []; + this.trim_phase_id = 0; + this.trim_region_id = -1; + this.trim_line = {}; + } + + /** + * The function to check if the rectangle is on the vertex of the polygon + * @param x - x coordinate of rectangle + * @param y - y coordinate of the rectangle + * @param w - width of the rectangle + * @param h - height of the rectangle + * @param px - x coordinate of the point to be checked + * @param py - y coordinate of the point to be checked + * @returns {boolean} - true if the rect is on the vertex of the polygon + */ + is_inside_points(x, y, w, h, px, py) { + return px > x && px < x + w && py > y && py < y + h; + } + + /** + * Delete a polygon vertex + * @param region_id {number} - id of the region to which the vertex belongs (polygon) + * @param vertex_id {number} - id of the vertex to be deleted + * @returns {boolean} - true if vertex is deleted, false otherwise + */ + polygonDelVertex(region_id, vertex_id) { + let rs = _via_canvas_regions[region_id].shape_attributes; + let npts = rs["all_points_x"].length; + let shape = rs["name"]; + + if ( + shape !== VIA_REGION_SHAPE.POLYGON && + shape !== VIA_REGION_SHAPE.POLYLINE + ) { + show_message("Vertices can only be deleted from polygon/polyline."); + return false; + } + + if (npts <= 3 && shape === VIA_REGION_SHAPE.POLYGON) { + show_message( + "Failed to delete vertex because a polygon must have at least 3 vertices." + ); + return false; + } + + if (npts <= 2 && shape === VIA_REGION_SHAPE.POLYLINE) { + show_message( + "Failed to delete vertex because a polyline must have at least 2 vertices." + ); + return false; + } // delete vertex from canvas + + _via_canvas_regions[region_id].shape_attributes["all_points_x"].splice( + vertex_id, + 1 + ); + + _via_canvas_regions[region_id].shape_attributes["all_points_y"].splice( + vertex_id, + 1 + ); // delete vertex from image metadata + + _via_img_metadata[_via_image_id].regions[region_id].shape_attributes[ + "all_points_x" + ].splice(vertex_id, 1); + + _via_img_metadata[_via_image_id].regions[region_id].shape_attributes[ + "all_points_y" + ].splice(vertex_id, 1); + + return true; + } + + drawSelector(x, y, w, h) { + let N = _via_canvas_regions.length; + if (N === 0) { + return; + } + for (let i = 0; i < N; ++i) { + if (!_via_img_metadata[_via_image_id].isRegionLocked(i)) { + let rs = _via_canvas_regions[i].shape_attributes; + if (rs.name === "polygon") { + for (let j = 0; j < rs.all_points_x.length; ++j) { + if ( + this.is_inside_points( + x, + y, + w, + h, + rs.all_points_x[j], + rs.all_points_y[j] + ) + ) { + this.polygonDelVertex(i, j); + } + } + } + } + } + } + + //-------------------------------------------- + // Etc. functions + //-------------------------------------------- + + lockRegionHandler(element) { + let region_id = parseInt(element.id.slice(5)); + let context = element.id.slice(element.id.length - 8); + //determine id contain _context or not + if (context === "_context") { + region_id = parseInt(element.id.slice(5, element.id.length - 8)); + } + if (!_via_img_metadata[_via_image_id].lockedRegions.has(region_id)) { + _via_img_metadata[_via_image_id].lockedRegions.delete(region_id); + } + if (context === "_context") { + if ($( + "#lock_" + region_id + "_context" + ).is(":checked")) { + _via_img_metadata[_via_image_id].addLockedRegion(region_id); + } else { + _via_img_metadata[_via_image_id].clearRegionLock(region_id); + } + } else { + if ($("#lock_" + region_id).is( + ":checked" + )) { + _via_img_metadata[_via_image_id].addLockedRegion(region_id); + } + else { + _via_img_metadata[_via_image_id].clearRegionLock(region_id); + } + } + + if (_via_img_metadata[_via_image_id].isRegionLocked(region_id)) { + $("#lock_" + region_id).prop("checked", true); + if ($("#lock_" + region_id + "_context")) { + $("#lock_" + region_id + "_context").prop("checked", true); + } + return; + } + $("#lock_" + region_id).prop("checked", false); + if ($("#lock_" + region_id + "_context")) { + $("#lock_" + region_id + "_context").prop("checked", false); + } + } + + updateCheckedLockHtml() { + if (!_via_img_metadata.hasOwnProperty(_via_image_id)) { + return; + } + for (let i = 0; i < _via_img_metadata[_via_image_id].regions.length; i++) { + if (_via_img_metadata[_via_image_id].isRegionLocked(i)) { + $("#lock_" + i).prop("checked", true); + if ($("#lock_" + i + "_context")) { + $("#lock_" + i + "_context").prop("checked", true); + } + } + } + } + + updateUiComponents() { + if (!buffer.imgLoaded) { + return; + } + + show_message("Updating user interface components."); + + switch (_via_display_area_content_name) { + case VIA_DISPLAY_AREA_CONTENT_NAME.IMAGE_GRID: + image_grid_set_content_panel_height_fixed(); + image_grid_set_content_to_current_group(); + break; + + case VIA_DISPLAY_AREA_CONTENT_NAME.IMAGE: + if (!this.is_window_resized && buffer.imgLoaded) { + this.is_window_resized = true; + + buffer.showImage(_via_image_index); + } + + break; + } + } // update UI components based on current state + + drawingSetVariables() { + this.click0.x = 0; + this.click0.y = 0; + this.click1.x = 0; + this.click1.y = 0; + this.is_user_drawing_region = false; + this.is_window_resized = false; + this.is_user_resizing_region = false; + this.is_user_moving_region = false; + this.is_user_drawing_polygon = false; + this.is_region_selected = false; + this.user_sel_region_id = -1; + } // reset all drawing variables + + toString() { + // for debugging + return "Canvas controller -> " + _via_reg_canvas; + } +} +const drawing = new Drawer(); \ No newline at end of file diff --git a/src/js/file_manager.js b/src/js/file_manager.js new file mode 100644 index 0000000..32b563d --- /dev/null +++ b/src/js/file_manager.js @@ -0,0 +1,331 @@ +// file_manager.js +// Description: This file contains the code for the file manager class. +// The file manager class is responsible for managing the file operations like downloading the region data, importing annotations from a file, etc. +class FileManager { + constructor() { + this.modal = new Modal(); + } + + downloadAllRegionDataModal() { + let header = "Download all region data"; + let body = + '<div class="form-check form-switch">' + + '<input class="form-check-input" type="checkbox" id="download_all_region_data_csv">' + '' + + '<label class="form-check-label" for="download_all_region_data_csv">Download as CSV</label>' + + '</div>' + + '<div class="form-check form-switch">' + + '<input class="form-check-input" type="checkbox" id="download_all_region_data_json">' + + '<label class="form-check-label" for="download_all_region_data_json">Download as JSON</label>' + + '</div>' + + '<div class="form-check form-switch">' + + '<input class="form-check-input" type="checkbox" id="download_all_region_data_coco">' + + '<label class="form-check-label" for="download_all_region_data_coco">Download as COCO</label>' + + '</div>'; + let footer = + '<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>' + + '<button type="button" class="btn btn-primary" onclick="fileManager.downloadAllRegionDataModalDownload()">Download</button>'; + this.modal = new Modal(); + this.modal.show(header, body, footer); + } + + downloadAllRegionDataModalDownload() { + let download_all_region_data_csv = document.getElementById("download_all_region_data_csv").checked; + let download_all_region_data_json = document.getElementById("download_all_region_data_json").checked; + let download_all_region_data_coco = document.getElementById("download_all_region_data_coco").checked; + if (download_all_region_data_csv) { + this.downloadAllRegionData("csv"); + } + if (download_all_region_data_json) { + this.downloadAllRegionData("json"); + } + if (download_all_region_data_coco) { + this.downloadAllRegionData("coco"); + } + this.modal.hide(); + delete this.modal; + } + + downloadAllRegionData(type, file_extension = type) { + this.packMetadata(type).then( + data => { + let blob_attr = { type: `text/${file_extension};charset=utf-8` }; + let all_region_data_blob = new Blob(data, blob_attr); + + let filename = settings.projectName || "InfarctSizeExport"; + if (file_extension !== "csv" || file_extension !== "json") { + file_extension = "json"; + filename += `_${type}.${file_extension}`; + } + this.saveDataToLocalFile(all_region_data_blob, filename); + }, + err => { + show_message(`Failed to download data: [${err}]`); + } + ); + } + + packMetadata(type) { + let return_type = type; + let csvheader = "filename,file_size,file_attributes,region_count,region_id,region_shape_attributes,region_attributes"; + let csvdata = [csvheader]; + + return new Promise((ok_callback, err_callback) => { + let returnData = return_type === "csv" ? csvDataToReturn() : + return_type === "coco" ? cocoDataToReturn() : [JSON.stringify(_via_img_metadata)]; + + ok_callback(returnData); + }); + + function csvDataToReturn() { + for (let image_id in _via_img_metadata) { + let fattr = fileManager.escapeForCsv(map_to_json(_via_img_metadata[image_id].file_attributes)); + let prefix = `\n${_via_img_metadata[image_id].filename},${_via_img_metadata[image_id].size},"${fattr}"`; + let r = _via_img_metadata[image_id].regions; + + if (r.length !== 0) { + csvdata.push(...r.map((region, i) => { + let sattr = '"' + fileManager.escapeForCsv(map_to_json(region.shape_attributes)) + '"'; + let rattr = '"' + fileManager.escapeForCsv(map_to_json(region.region_attributes)) + '"'; + return `${prefix},${r.length},${i},${sattr},${rattr}`; + })); + } else { + csvdata.push(`${prefix},0,0,"{}","{}"`); + } + } + return csvdata; + } + + function cocoDataToReturn() { + return img_stat_set_all().then( + () => [fileManager.exportProjectToCocoFormat()], + err => { + throw err; + } + ); + } + } + + saveDataToLocalFile(data, filename) { + let a = document.createElement("a"); + let url = URL.createObjectURL(data); + a.href = url; + a.download = filename; + + // simulate a mouse click event + let event = new MouseEvent("click", { + view: window, + bubbles: true, + cancelable: true, + }); + a.dispatchEvent(event); + + // revoke the object URL to free up memory + URL.revokeObjectURL(url); + } + + escapeForCsv(s) { + return s.replace(/["]/g, '""'); + } + + incrementSkippedAnnotationCount() { + this.skipped_annotation_count++; + show_message( + "Skipped " + + this.skipped_annotation_count + + " annotations. COCO format only supports the following attribute types: " + + JSON.stringify(VIA_COCO_EXPORT_ATTRIBUTE_TYPE) + + " and region shapes: " + + JSON.stringify(VIA_COCO_EXPORT_RSHAPE) + ); + } + + shouldAssignUniqueId() { + for (let img_id in _via_img_metadata) { + if (Number.isNaN(parseInt(img_id))) { + return true; + } + } + + for (let attr_name in project.attributes) { + if (!VIA_COCO_EXPORT_ATTRIBUTE_TYPE.includes(project.attributes[attr_name]["type"])) { + continue; + } + + for (let attr_option_id in project.attributes[attr_name]["options"]) { + if (this.attribute_option_id_list.includes(attr_option_id) || Number.isNaN(parseInt(attr_option_id))) { + return true; + } else { + this.attribute_option_id_list.push(attr_option_id); + } + } + } + + return false; + } + + exportProjectToCocoFormat() { + let coco = { + info: {}, + images: [], + annotations: [], + licenses: [], + categories: [], + }; + coco["info"] = { + year: new Date().getFullYear(), + version: "1.0", + description: + "Annotations exported to COCO format using InfarctSize", + contributor: "", + url: "https://infarctsize.com/", + date_created: new Date().toString(), + }; + coco["licenses"] = [{ id: 0, name: "Unknown License", url: "" }]; // indicates that license is unknown + + this.skipped_annotation_count = 0; + this.attribute_option_id_list = []; + let assign_unique_id = this.shouldAssignUniqueId(); + + // add categories + let attr_option_id_to_category_id = {}; + let unique_category_id = 1; + for (let attr_name in project.attributes.region) { + if (VIA_COCO_EXPORT_ATTRIBUTE_TYPE.includes(project.attributes.region[attr_name]["type"])) { + for (let attr_option_id in project.attributes.region[attr_name]["options"]) { + let category_id = assign_unique_id ? unique_category_id++ : parseInt(attr_option_id); + coco["categories"].push({ + supercategory: attr_name, + id: category_id, + name: project.attributes.region[attr_name]["options"][attr_option_id], + }); + attr_option_id_to_category_id[attr_option_id] = category_id; + } + } + } + + // add files and all their associated annotations + let annotation_id = 1; + let unique_img_id = 1; + for (let img_index in _via_image_id_list) { + let img_id = _via_image_id_list[img_index]; + let file_src = _via_settings["core"]["default_filepath"] + _via_img_metadata[img_id].filename; + if (_via_img_fileref[img_id] instanceof File) { + file_src = _via_img_fileref[img_id].filename; + } + + let coco_img_id = assign_unique_id ? unique_img_id++ : parseInt(img_id); + + coco["images"].push({ + id: coco_img_id, + width: _via_img_stat[img_index][0], + height: _via_img_stat[img_index][1], + file_name: _via_img_metadata[img_id].filename, + license: 0, + flickr_url: file_src, + coco_url: file_src, + date_captured: "", + }); + + // add all annotations associated with this file + for (let rindex in _via_img_metadata[img_id].regions) { + let region = _via_img_metadata[img_id].regions[rindex]; + if (!VIA_COCO_EXPORT_RSHAPE.includes(region.shape_attributes["name"])) { + this.incrementSkippedAnnotationCount(); + continue; + } + + let coco_annotation = via_region_shape_to_coco_annotation(region.shape_attributes); + coco_annotation["id"] = annotation_id; + coco_annotation["image_id"] = coco_img_id; + + for (let region_attribute_id in region["region_attributes"]) { + let region_attribute_value = region["region_attributes"][region_attribute_id]; + if (attr_option_id_to_category_id.hasOwnProperty(region_attribute_value)) { + coco_annotation["category_id"] = attr_option_id_to_category_id[region_attribute_value]; + coco["annotations"].push(coco_annotation); + annotation_id++; + } else { + this.incrementSkippedAnnotationCount(); + } + } + } + } + + return [JSON.stringify(coco)]; + } + + importAnnotationsFromFile() { + let input = document.createElement("input"); + input.type = "file"; + input.accept = ".csv,.json,.coco"; + //add event listener + input.addEventListener("change", this.importAnnotationsFromFileOnchange); + input.click(); + } + + importAnnotationsFromFileOnchange(e) { + let files = e.target.files; + + for (let i = 0; i < files.length; i++) { + let file = files[i]; + let type = file.type; + if (type === "application/json" || type === "text/json") { + this.importAnnotationsFromJson(file); + } else if (type === "text/csv") { + FileManager.loadTextFile(file, import_annotations_from_csv); + } + } + } + + + importAnnotationsFromJson(file) { + let reader = new FileReader(); + let file_extension = file.name.split(".").pop(); + reader.readAsText(file); + + reader.onload = function (e) { + let data = e.target.result; + let parsed = JSON.parse(data); + if (parsed.hasOwnProperty("images") && parsed.hasOwnProperty("annotations")) { + FileManager.loadTextFile(file, import_coco_annotations_from_json); + } else { + FileManager.loadTextFile(file, import_annotations_from_json); + } + } + + } + + static loadTextFile(text_file, callback_function) { + if (text_file) { + let text_reader = new FileReader(); + text_reader.addEventListener( + "progress", + function (e) { + Message.showInfo("Loading data from file : " + text_file.name + " ... "); + }, + false + ); + + text_reader.addEventListener( + "error", + function () { + Message.showError( + "Error loading data text file : " + text_file.name + " !" + ); + callback_function(""); + }, + false + ); + + text_reader.addEventListener( + "load", + function () { + callback_function(text_reader.result); + }, + false + ); + text_reader.readAsText(text_file, "utf-8"); + } + } +} +const fileManager = new FileManager(); \ No newline at end of file diff --git a/src/js/file_metadata.js b/src/js/file_metadata.js new file mode 100644 index 0000000..97bba23 --- /dev/null +++ b/src/js/file_metadata.js @@ -0,0 +1,116 @@ +// file_metadata.js +// Description: FileMetadata class to store metadata of a file +// FileMetadata class is used to store metadata of a file. It stores the filename, size, regions, file attributes, locked regions, grouped regions, and autoAnnotated flag. It also provides methods to set and get the filename, size, regions, file attributes, locked regions, grouped regions, and autoAnnotated flag. It also provides methods to add a region, add a locked region, check if a region is locked, clear locked regions, clear region lock, clear regions, load from JSON, add a grouped region, get a grouped region, clear grouped regions, clear groups, clear grouped region, check if it is groupable, and check if it is grouped key. +class FileMetadata { + constructor(filename, size) { + this.filename = filename; + this.size = size; // file size in bytes + this.regions = []; // array of File_Region() + this.file_attributes = { + ID: { type: "text", description: "", default_value: "" }, + Treatment: { type: "text", description: "", default_value: "" }, + }; + this.fileAttributes = new Map( + [ + ["ID", { type: "text", description: "", default_value: "" }], + ["Treatment", { type: "text", description: "", default_value: "" }], + ] + ) + this.lockedRegions = new Set(); + this.groupedRegions = { + groupBy: new Array(), + groups: new Map(), + groupIDs: new Map() + }; + this.autoAnnotated = false; + } + + setFilename(filename) { + this.filename = filename; + } + + setFileAttributes(file_attributes) { + this.file_attributes = file_attributes; + } + + addRegion(region) { + this.regions.push(region); + } + + addLockedRegion(region) { + this.lockedRegions.add(region); + } + + isRegionLocked(region) { + return this.lockedRegions.has(region); + } + + clearLockedRegions() { + this.lockedRegions.clear(); + } + + clearRegionLock(region) { + this.lockedRegions.delete(region); + } + + clearRegions() { + this.regions = []; + } + + loadFromJSON(data) { + if (data.regions) { + this.regions = data.regions; + } + if (data.file_attributes) { + this.file_attributes = data.file_attributes; + } + if (data.lockedRegions.dataType === "Set") { + this.lockedRegions = new Set(data.lockedRegions.value); + } else { + this.lockedRegions = new Set(); + } + if (data.groupedRegions.groupBy) { + this.groupedRegions.groupBy = new Set(data.groupedRegions.groupBy.value); + } + if (data.groupedRegions.groups) { + this.groupedRegions.groups = new Map(data.groupedRegions.groups.value); + } + if (data.groupedRegions.groupIDs) { + this.groupedRegions.groupIDs = new Map(data.groupedRegions.groupIDs.value); + } + if (data.autoAnnotated) { + this.autoAnnotated = data.autoAnnotated; + } + } + + addGroupedRegion(region, group) { + this.groupedRegions.groups.set(region, group); + } + + getGroupedRegion(region) { + return this.groupedRegions.groups.get(region); + } + + clearGroupedRegions() { + this.groupedRegions.groups = new Map(); + this.groupedRegions.groupBy = []; + } + + clearGroups() { + this.groupedRegions.groups = new Map(); + } + + clearGroupedRegion(region) { + this.groupedRegions.groups.delete(region); + } + + isGroupable() { + return this.groupedRegions.groupBy.length > 0; + } + + isGroupedKey(region) { + return this.groupedRegions.groups.has(region); + } + + +} \ No newline at end of file diff --git a/src/js/global_variables.js b/src/js/global_variables.js new file mode 100644 index 0000000..840f24e --- /dev/null +++ b/src/js/global_variables.js @@ -0,0 +1,272 @@ +// global_variables.js +// Description: This file contains all the global variables used in the application. + +const SAA_VERSION = "1.1.0 (pre-release)"; +const SAA_NAME = "InfarctSize"; +const SAA_SHORT_NAME = "ISAI"; +const VIA_REGION_SHAPE = { + EDIT: "edit", + RECT: "rect", + CIRCLE: "circle", + ELLIPSE: "ellipse", + POLYGON: "polygon", + POINT: "point", + POLYLINE: "polyline", + PEN: "pen", + REMOVE: "remove", + TRIM: "trim", + DRAG: "drag", +}; + +const VIA_ATTRIBUTE_TYPE = { + TEXT: "text", + CHECKBOX: "checkbox", + RADIO: "radio", + IMAGE: "image", + DROPDOWN: "dropdown", +}; + +const VIA_DISPLAY_AREA_CONTENT_NAME = { + IMAGE: "image_panel_container", + IMAGE_GRID: "image_grid_panel", + SETTINGS: "settings_panel", + PAGE_404: "page_404", + PAGE_GETTING_STARTED: "page_getting_started", + PAGE_ABOUT: "page_about", + PAGE_START_INFO: "page_start_info", + PAGE_LICENSE: "page_license", +}; + +const VIA_ANNOTATION_EDITOR_MODE = { + SINGLE_REGION: "single_region", + ALL_REGIONS: "all_regions", + HYBRID: "hybrid", +}; + +const VIA_ANNOTATION_EDITOR_PLACEMENT = { + NEAR_REGION: "NEAR_REGION", + IMAGE_BOTTOM: "IMAGE_BOTTOM", + DISABLE: "DISABLE", +}; + +let changes = []; + +let VIA_REGION_MIN_DIM = 3; + +let VIA_CANVAS_DEFAULT_ZOOM_LEVEL_INDEX = 3; +let VIA_CANVAS_ZOOM_LEVELS = [ + 0.25, 0.5, 0.75, 1.0, 1.5, 2.0, 2.5, 3.0, 4, 5, 6, 7, 8, 9, 10, +]; + +let VIA_THEME_MESSAGE_TIMEOUT_MS = 6000; + +let VIA_CSV_SEP = ","; +let VIA_CSV_KEYVAL_SEP = ":"; + +// data structure to store loaded images metadata + +let _via_img_metadata = {}; +let _via_img_src = {}; // image content {abs. path, url, base64 data, etc} +let _via_img_fileref = {}; // reference to local images selected by using browser file selector +let _via_img_count = 0; // count of the loaded images +let _via_canvas_regions = []; // image regions spec. in canvas space +let _via_canvas_scale = 1.0; // current scale of canvas image + +let dpi = window.devicePixelRatio; + +let _via_image_id = ""; // id={filename+length} of current image +let _via_image_index = -1; // index + +let _via_current_image_filename; +let _via_current_image; +let _via_current_image_width; +let _via_current_image_height; + +// a record of image statistics (e.g. width, height) +let _via_img_stat = {}; + +// image canvas +let _via_display_area = document.getElementById("display_area"); +let _via_reg_canvas = document.getElementById("region_canvas"); +let _via_canvas_width, _via_canvas_height; + +// canvas zoom +let _via_canvas_zoom_level_index = VIA_CANVAS_DEFAULT_ZOOM_LEVEL_INDEX; // 1.0 +let _via_canvas_scale_without_zoom = 1.0; + +// state of the application +//let _via_is_user_drawing_region = false; +let _via_is_all_region_selected = false; +let _via_is_canvas_zoomed = false; +let _via_is_loading_current_image = false; +let _via_is_region_id_visible = true; +let _via_is_region_boundary_visible = true; +let _via_is_region_info_visible = false; +let _via_is_ctrl_pressed = false; +let _via_is_debug_mode = false; +let _via_is_message_visible = true; + +// region copy/paste +let _via_region_selected_flag = new Set(); // region select flag for current image +let _via_copied_image_regions = []; +let _via_paste_to_multiple_images_input; + +// message +let _via_message_clear_timer; + +// attributes +let _via_attribute_being_updated = "region"; // {region, file} + + +let _via_attributes = { + region: { + Type: { + default_options: { Slice: true }, + description: "", + options: { + Slice: "Slice", + Hole: "Hole", + Risk: "Risk", + Infarct: "Infarct", + }, + type: "dropdown", + }, + }, + file: { + ID: { type: "text", description: "", default_value: "" }, + }, +}; //MODSote + +let _via_current_attribute_id = ""; + +// region group color +//let _via_canvas_regions_group_color = {}; // color of each region + +// invoke a method after receiving user input +let _via_user_input_ok_handler = null; +let _via_user_input_cancel_handler = null; +let _via_user_input_data = {}; + +// annotation editor +let _via_metadata_being_updated = "region"; // {region, file} +let _via_annotation_editor_mode = VIA_ANNOTATION_EDITOR_MODE.SINGLE_REGION; + +// all the image_id and image_filename of images added by the user is +// stored in _via_image_id_list and _via_image_filename_list +// +// Image filename list (img_fn_list) contains a filtered list of images +// currently accessible by the user. The img_fn_list is visible in the +// left side toolbar. image_grid, next/prev, etc operations depend on +// the contents of _via_img_fn_list_img_index_list. +let _via_image_id_list = []; // array of all image id (in order they were added by user) +let _via_image_filename_list = []; // array of all image filename +let _via_image_load_error = []; // {true, false} + +let _via_reload_img_fn_list_table = true; +let _via_img_fn_list_img_index_list = []; // image index list of images show in img_fn_list +let _via_img_fn_list_html = []; // html representation of image filename list + +// image grid +let image_grid_panel = document.getElementById("image_grid_panel"); +let _via_display_area_content_name = ""; // describes what is currently shown in display area +let _via_display_area_content_name_prev = ""; +let _via_image_grid_load_ongoing = false; +let _via_image_grid_page_first_index = 0; // array index in _via_img_fn_list_img_index_list[] +let _via_image_grid_page_last_index = -1; +let _via_image_grid_selected_img_index_list = []; +let _via_image_grid_page_img_index_list = []; // list of all image index in current page of image grid +let _via_image_grid_visible_img_index_list = []; // list of images currently visible in grid +let _via_image_grid_mousedown_img_index = -1; +let _via_image_grid_mouseup_img_index = -1; +let _via_image_grid_img_index_list = []; // list of all image index in the image grid +let _via_image_grid_group = {}; // {'value':[image_index_list]} +let _via_image_grid_group_var = []; // {type, name, value} +let _via_image_grid_stack_prev_page = []; // stack of first img index of every page navigated so far + +// image buffer +let VIA_IMG_PRELOAD_INDICES = [1, -1, 2, 3, -2, 4]; // for any image, preload previous 2 and next 4 images +let VIA_IMG_PRELOAD_COUNT = 4; + +// via settings +let _via_settings = {}; +_via_settings.ui = {}; +_via_settings.ui.annotation_editor_fontsize = 0.8; // in rem +_via_settings.ui.leftsidebar_width = 18; // in rem +_via_settings.ui.image_grid = {}; +_via_settings.ui.image_grid.img_height = 80; // in pixel +_via_settings.ui.image_grid.rshape_fill = "none"; +_via_settings.ui.image_grid.rshape_fill_opacity = 0.3; +_via_settings.ui.image_grid.rshape_stroke = "yellow"; +_via_settings.ui.image_grid.rshape_stroke_width = 2; +_via_settings.ui.image_grid.show_region_shape = true; +_via_settings.ui.image_grid.show_image_policy = "all"; +_via_settings.ui.image = {}; +_via_settings.ui.image.region_label = "__via_region_id__"; // default: region_id +//_via_settings.ui.image.region_color = '__via_default_region_color__'; // default color: yellow //MODSote +_via_settings.ui.image.region_color = "Type"; //MODSote +_via_settings.ui.image.region_label_font = "20px Sans"; +_via_settings.ui.image.on_image_annotation_editor_placement = + VIA_ANNOTATION_EDITOR_PLACEMENT.NEAR_REGION; + +_via_settings.core = {}; +_via_settings.core.buffer_size = 4 * VIA_IMG_PRELOAD_COUNT + 2; +_via_settings.core.filepath = {}; +_via_settings.core.default_filepath = ""; + +// UI html elements +const fileInput = document.getElementById("fileInput"); +const projectFileInput = document.getElementById("projectHelperFileInput"); +const ui_top_panel = document.getElementById("ui_top_panel"); +const image_panel = document.getElementById("image_panel"); +const img_fn_list_panel = document.getElementById("img_fn_list_panel"); +const img_fn_list = document.getElementById("img_fn_list"); +const attributes_panel = document.getElementById("attributes_panel"); +const leftsidebar = document.getElementById("leftsidebar"); + +let VIA_ANNOTATION_EDITOR_FONTSIZE_CHANGE = 0.1; // in rem +let VIA_IMAGE_GRID_IMG_HEIGHT_CHANGE = 20; // in percent +let VIA_LEFTSIDEBAR_WIDTH_CHANGE = 1; // in rem +let VIA_POLYGON_SEGMENT_SUBTENDED_ANGLE = 5; // in degree (used to approximate shapes using polygon) +let VIA_FLOAT_PRECISION = 3; // number of decimal places to include in float values + +// COCO Export +let VIA_COCO_EXPORT_RSHAPE = ["rect", "circle", "ellipse", "polygon", "point"]; +let VIA_COCO_EXPORT_ATTRIBUTE_TYPE = [ + VIA_ATTRIBUTE_TYPE.DROPDOWN, + VIA_ATTRIBUTE_TYPE.RADIO, +]; + +//Tooltips (bootsrap) +const tooltipTriggerList = document.querySelectorAll( + '[data-bs-toggle="tooltip"]' +); +const tooltipList = [...tooltipTriggerList].map( + (tooltipTriggerEl) => new bootstrap.Tooltip(tooltipTriggerEl) +); + +let annotation_editor = document.getElementById("annotation_editor_panel"); +let locked_regions = []; + +const SegemntaitonServerIP = "192.168.1.129:5000/"; + +let is_alt_pressed = false; + +let current_shape = "rect"; + +const img_buffer = $("#image_buffer"); + +const Shapes = { + EDIT: "edit", + RECTANGLE: "rect", + CIRCLE: "circle", + ELLIPSE: "ellipse", + POLYGON: "polygon", + POINT: "point", + PEN: "pen", + REMOVE: "remove", + TRIM: "trim", + DRAG: "drag", +}; + +let img_locked_regions = {}; +let Scores = {}; diff --git a/src/js/img_manipulation.js b/src/js/img_manipulation.js new file mode 100644 index 0000000..bd3722e --- /dev/null +++ b/src/js/img_manipulation.js @@ -0,0 +1,106 @@ +// img_manipulation.js +// Description: This file contains the code for image manipulation, like change brightness, contrast, hue, saturation, resize, scale, rotate, flip, mirror. +// ImgManipulation class is used to manipulate the image. +class ImgManipulation { + constructor() { + this.image = null + + this.contrast = 100; + this.brightness = 100; + this.hue = 0; + this.saturation = 100; + + this.brightnessSlider = $("#brightnessRange"); + this.contrastSlider = $("#contrastRange"); + this.hueSlider = $("#hueRange"); + this.saturationSlider = $("#saturationRange"); + + this.setSliderValues(); + this.addEventListeners(); + } + + addEventListeners() { + this.brightnessSlider.on("input", () => { + this.brightness = this.brightnessSlider.val(); + this.changeImgSettings(); + }); + this.contrastSlider.on("input", () => { + this.contrast = this.contrastSlider.val(); + this.changeImgSettings(); + }); + this.hueSlider.on("input", () => { + this.hue = this.hueSlider.val(); + this.changeImgSettings(); + }); + this.saturationSlider.on("input", () => { + this.saturation = this.saturationSlider.val(); + this.changeImgSettings(); + }); + } + + hook(img = _via_current_image) { + this.image = document.getElementById(img.id); + this.image.style.transformOrigin = "top left"; + } + + setSliderValues() { + this.brightnessSlider.val(this.brightness); + this.contrastSlider.val(this.contrast); + this.hueSlider.val(this.hue); + this.saturationSlider.val(this.saturation); + } + + backToSidebar() { + $("#image_man_container").addClass("d-none"); + $("#sidebar_container").removeClass("d-none"); + } + + async show() { + this.image.classList.add("position-absolute"); + this.image.classList.remove("d-none"); + } + + changeImgSettings() { + this.image.style.filter = `brightness(${this.brightnessSlider.val()}%) contrast(${this.contrastSlider.val()}%) hue-rotate(${this.hueSlider.val()}deg) saturate(${this.saturationSlider.val()}%)`; + } + + //reset all filters + resetFilters() { + this.brightness = 100; + this.contrast = 100; + this.hue = 0; + this.saturation = 100; + this.setSliderValues(); + this.changeImgSettings(); + } + + // resize image + resize(w = _via_current_image_width, h = _via_current_image_height) { + this.image.width = w; + this.image.height = h; + } + + scaleImage(scale) { + this.image.style.transform = `scale(${scale})`; + } + + // // rotate image + // rotate(angle) { + // this.ctx.rotate(angle); + // this.image.style.transform = `rotate(${angle}deg)`; + // } + // + // // flip image + // flip() { + // this.ctx.scale(-1, 1); + // this.image.style.transform = "scaleX(-1)"; + // } + // + // // mirror image + // mirror() { + // this.ctx.scale(1, -1); + // this.image.style.transform = "scaleY(-1)"; + // } + +} +const image = new ImgManipulation(); \ No newline at end of file diff --git a/src/js/message.js b/src/js/message.js new file mode 100644 index 0000000..7f47ede --- /dev/null +++ b/src/js/message.js @@ -0,0 +1,65 @@ +// message.js +// Description: This file contains the message class that controls the bootstrap toast messages +// Message class controlling the bootstrap toast messages +class Message { + static toast = $("#liveToast"); + static address = $("#toastAddress"); + static body = $("#toastBody"); + + static show(message) { + if (message.address === "" || !_via_is_message_visible && message.address !== "Error") { + return; + } + switch (message.address) { + case "Error": + Message.address.text("Error"); + Message.address.attr("class", "badge text-bg-danger me-auto"); + break; + case "Warning": + Message.address.text("Warning"); + Message.address.attr("class", "badge text-bg-warning me-auto"); + break; + case "Info": + Message.address.text("Info"); + Message.address.attr("class", "badge text-bg-info me-auto"); + break; + case "Success": + Message.address.text("Success"); + Message.address.attr("class", "badge text-bg-success me-auto"); + break; + default: + Message.address.text(message.address); + Message.address.attr("class", "badge text-bg-primary me-auto"); + } + Message.body.text(message.body); + Message.toast.toast("show"); + } + + static test() { + Message.show({ + address: "test", + body: "test", + }); + } + + static showError(message) { + if (message === "") { + return; + } + Message.show({ + address: "Error", + body: message, + }); + } + + static showInfo(message) { + if (message === "" || !_via_is_message_visible) { + return; + } + Message.show({ + address: "Info", + body: message, + }); + } +} +const message = new Message(); \ No newline at end of file diff --git a/src/js/modal.js b/src/js/modal.js new file mode 100644 index 0000000..96d3a74 --- /dev/null +++ b/src/js/modal.js @@ -0,0 +1,44 @@ +// modal.js +// Description: This file contains the code for the modal class. +// Modal class is used to create a modal and show it on the screen. Interface to control the bootstrap modal. +class Modal { + constructor() { + this.modal = $("#staticBackdropModal"); + this.modalBody = $("#staticBackdropModal .modal-body"); + this.modalTitle = $("#staticBackdropModal .modal-title"); + this.modalFooter = $("#staticBackdropModal .modal-footer"); + this.modalClose = $("#staticBackdropModal .btn-close"); + this.modalClose.click(() => { + this.modal.modal("hide"); + }); + } + + show(title, body, footer, dis_close = false) { + this.modalTitle.html(title); + this.modalBody.html(body); + this.modalFooter.html(footer); + this.modal.modal("show"); + if (dis_close) { + this.modalClose.hide(); + } else { + this.modalClose.show(); + } + } + + update(title, body, footer) { + this.modalTitle.html(title); + this.modalBody.html(body); + this.modalFooter.html(footer); + } + + hide() { + this.modal.modal("hide"); + } + + clear() { + this.modalTitle.html(""); + this.modalBody.html(""); + this.modalFooter.html(""); + } +} +const modal = new Modal(); \ No newline at end of file diff --git a/src/js/project.js b/src/js/project.js new file mode 100644 index 0000000..24b07df --- /dev/null +++ b/src/js/project.js @@ -0,0 +1,608 @@ +// project.js +// Description: This file contains the Project class which is responsible for managing the project data. +// Project class data includes the project name, attributes, images, metadata, and files. +class Project { + constructor() { + this.name = settings.projectGetDefaultProjectName(); + this.attributes = this.initAttributes(); + this.images = {}; + this.metadata = {}; + this.files = {}; + this.idlist = []; + } + + initAttributes() { + return { + region: { + Type: { + default_options: { Slice: true }, + description: "", + options: { + Slice: "Slice", + Hole: "Hole", + Risk: "Risk", + Infarct: "Infarct", + }, + type: "dropdown", + }, + }, + file: { + ID: { type: "text", description: "", default_value: "" }, + } + }; + } + + + setName(name) { + this.name = name; + } + + initDefaultProject() { + if (!settings.isThereAProject()) { + this.project = {}; + } + this.setName(settings.projectGetDefaultProjectName()); + } + + onNameUpdate(p) { + this.setName(p.value); + } + + getProjectName() { + return this.name; + } + + saveWithConfirm() { + let config = { title: "Save Project" }; + let name = this.getProjectName(); + let projectName = "<div class=\"mb-3\">\n" + + " <label for=\"projectNameModal\" class=\"form-label\">Project name</label>\n" + + " <input type=\"email\" class=\"form-control\" id=\"projectNameModal\" placeholder=\"" + name + "\">\n" + + "</div>\n" + let fileName = "<div class=\"mb-3\">\n" + + " <label for=\"fileNameModal\" class=\"form-label\">Project file name</label>\n" + + " <input type=\"email\" class=\"form-control\" id=\"fileNameModal\" placeholder=\"" + name + "\">\n" + + "</div>\n" + + let footer = "<button type='button' class='btn btn-primary' onclick='project.saveConfirmed(document.getElementById(\"projectNameModal\").value, document.getElementById(\"fileNameModal\").value)'>Save</button>"; + modal.show("Save Project", projectName + fileName, footer, false); + + //invoke_with_user_inputs(this.saveConfirmed.bind(this), input, config); + } + + static replacer(key, value) { + if (value instanceof Map) { + return { + dataType: 'Map', + value: Array.from(value.entries()), // or with spread: value: [...value] + }; + } else if (value instanceof Set) { + return { + dataType: 'Set', + value: Array.from(value), // or with spread: value: [...value] + }; + } else { + return value; + } + } + + saveConfirmed(pName, fName) { + if (pName === "") { + pName = this.getProjectName(); + } + if (fName === "") { + fName = this.getProjectName(); + } + let obj_urls = {}; + let project = { + name: pName, + settings: settings.getSettings(), + img_metadata: _via_img_metadata, + attributes: this.attributes, + image_ids: _via_image_id_list, + img_src: _via_img_src, + } + let data = JSON.stringify(project, this.replacer); + let blob = new Blob([data], { type: "application/json" }); + fileManager.saveDataToLocalFile(blob, fName + ".json"); + modal.hide(); + modal.clear(); + + } + + openSelectProjectFile() { + modal.show("Open Project", "Select a project file to open", "", false); + if (fileInput) { + fileInput.accept = ".json"; + fileInput.onchange = this.projectOpen; + fileInput.removeAttribute("multiple"); + fileInput.click(); + } + } + + projectOpen(e) { + let selected_file = e.target.files[0]; + FileManager.loadTextFile(selected_file, project.parseJsonFile.bind(project)); + } + + parseJsonFile(data) { + let p = JSON.parse(data); + if (this.checkProjectFileValidity(p)) { + settings.importSettings(p.settings); + clearViaData(); + + for (let img_id in p.img_metadata) { + if (this.checkMetadataValidity(p.img_metadata[img_id])) { + _via_img_metadata[img_id] = new FileMetadata(p.img_metadata[img_id].filename, p.img_metadata[img_id].size); + _via_img_metadata[img_id].loadFromJSON(p.img_metadata[img_id]); + _via_image_id_list.push(img_id); + _via_image_filename_list.push(p.img_metadata[img_id].filename); + _via_img_count += 1; + sidebar.set_file_annotations_to_default_value(img_id); + } else { + Message.showError("Invalid metadata for image " + img_id); + } + } + if (p.image_ids) { + _via_image_id_list = p.image_ids; + } + + if (p.attributes) { + this.attributes = p.attributes; + this.parseAttributesFromMetadata(); + let fattr_id_list = Object.keys(this.attributes.file); + let rattr_id_list = Object.keys(this.attributes.region); + if (rattr_id_list.length) { + _via_attribute_being_updated = "region"; + _via_current_attribute_id = rattr_id_list[0]; + } else { + if (fattr_id_list.length) { + _via_attribute_being_updated = "file"; + _via_current_attribute_id = fattr_id_list[0]; + } + } + } + + if (p.img_src) { + _via_img_src = p.img_src; + } + + if (settings.defaultPath !== "") { + _via_file_resolve_all_to_default_filepath(); + modal.hide(); + modal.clear(); + } else { + // show modal that inform user that project is loaded but images are not loaded and create footer button to load all images (loadAllImages) + modal.update( + "Project Loaded", + "Project loaded successfully, but images are not loaded. Do you want to load all project images?", + "<div class='btn-group' data-toggle='buttons'>" + + "<button type='button' class='btn btn-primary' onclick='sel_local_images();'>Select images</button>" + + "<button type='button' class='btn btn-primary' onclick='project.loadAllImages()'>Select image folder</button>" + + "</div>" + ); + } + + Message.show("Project loaded successfully"); + + if (_via_image_id_list.length > 0) { + _via_image_index = 0; + buffer.showImage(_via_image_index); + sidebar.update_img_fn_list(); + _via_reload_img_fn_list_table = true; + } + + } else { + modal.hide(); + modal.clear(); + Message.showError("Invalid project file"); + } + + } + + loadAllImages() { + if (projectFileInput) { + projectFileInput.accept = "image/*"; + projectFileInput.onchange = this.solveImageLoading; + projectFileInput.click(); + } + } + + solveImageLoading(e) { + let files = e.target.files; + for (const element of files) { + let filetype = element.type.substring(0, 5); + if (filetype === "image") { + if (_via_image_filename_list.indexOf(element.name) === -1) { + continue; + } + let img_index = _via_image_filename_list.indexOf(element.name); + let img_id = _via_image_id_list[img_index]; + _via_img_fileref[img_id] = element; + _via_img_metadata[img_id]["size"] = element.size; + } + } + if (_via_image_id_list.length > 0) { + _via_image_index = 0; + buffer.showImage(_via_image_index); + sidebar.update_img_fn_list(); + _via_reload_img_fn_list_table = true; + } + modal.hide(); + modal.clear(); + projectFileInput.value = ""; + + } + + checkProjectFileValidity(project) { + return !!(project.settings && project.img_metadata && project.attributes && project.image_ids); + } + + checkMetadataValidity(metadata) { + return !!(metadata.filename && metadata.size && metadata.file_attributes && metadata.regions); + } + + parseAttributesFromMetadata() { + if (!this.attributes.hasOwnProperty('file')) { + this.attributes.file = {}; + } + if (!this.attributes.hasOwnProperty('region')) { + this.attributes.region = {}; + } + + for (let img_id in _via_img_metadata) { + for (let fa in _via_img_metadata[img_id].file_attributes) { + if (!this.attributes.file.hasOwnProperty(fa)) { + this.attributes.file[fa] = {}; + this.attributes.file[fa]['type'] = 'text'; + } + } + + for (let ri = 0; ri < _via_img_metadata[img_id].regions.length; ++ri) { + for (let ra in _via_img_metadata[img_id].regions[ri].region_attributes) { + if (!this.attributes.region.hasOwnProperty(ra)) { + this.attributes.region[ra] = {}; + this.attributes.region[ra]["type"] = "text"; + } + } + } + + } + } + + fileRemoveWithConfirm() { + let img_id = _via_image_id_list[_via_image_index]; + let filename = _via_img_metadata[img_id].filename; + let region_count = _via_img_metadata[img_id].regions.length; + + let config = { title: "Remove File" }; + let input = { + img_index: { + type: "text", + name: "File Id", + value: _via_image_index + 1, + disabled: true, + size: 8, + }, + filename: { + type: "text", + name: "Filename", + value: filename, + disabled: true, + size: 30, + }, + region_count: { + type: "text", + name: "Number of regions", + disabled: true, + value: region_count, + size: 8, + }, + }; + invoke_with_user_inputs(this.fileRemoveConfirmed, input, config); + } + + fileRemoveConfirmed(input) { + let img_idx = input.img_index - 1; + this.removeFile(img_idx); + + if (img_idx === _via_img_count) { + if (_via_img_count === 0) { + buffer.imgLoaded = false; + show_home_panel(); + } else { + buffer.showImage(img_idx - 1); + } + } else { + buffer.showImage(img_idx); + } + + _via_reload_img_fn_list_table = true; + sidebar.update_img_fn_list(); + Message.show("Removed file: " + input.filename); + + user_input_default_cancel_handler(); + } + + removeFile(img_idx) { + if (img_idx < 0 || img_idx >= _via_img_count) { + console.log("project_remove_file(): invalid img_index " + img_idx); + return; + } + let img_id = _via_image_id_list[img_idx]; + + // remove img_index from all array + // this invalidates all image_index > img_index + _via_image_id_list.splice(img_idx, 1); + _via_image_filename_list.splice(img_idx, 1); + + let img_fn_list_index = _via_img_fn_list_img_index_list.indexOf(img_idx); + if (img_fn_list_index !== -1) { + _via_img_fn_list_img_index_list.splice(img_fn_list_index, 1); + } + + // @TODO: it is wasteful to clear all the buffer instead of removing a single image + buffer.emptyBuffer(); + + sidebar.img_fn_list_add_css_class("text-muted"); + + drawing.clearCanvas(); + delete _via_img_metadata[img_id]; + delete _via_img_src[img_id]; + delete _via_img_fileref[img_id]; + + _via_img_count -= 1; + } + + addFile(filename, size, fil_id) { + let img_id = fil_id; + if (typeof fil_id === "undefined") { + if (typeof size === "undefined") { + size = -1; + } + img_id = _via_get_image_id(filename, size); + } + + if (!_via_img_metadata.hasOwnProperty(img_id)) { + _via_img_metadata[img_id] = new FileMetadata(filename, size); + _via_image_id_list.push(img_id); + _via_image_filename_list.push(filename); + _via_img_count += 1; + } + return img_id; + } + + addLocalFile(event) { + let user_selected_images = event.target.files; + + let new_img_index_list = []; + let discarded_file_count = 0; + for (const element of user_selected_images) { + let filetype = element.type.substring(0, 5); + if (filetype === "image") { + let img_index = _via_image_filename_list.indexOf(element.name); + if (img_index === -1) { + // a new file was added to project + let new_img_id = project.addFile(element.name, element.size); + _via_img_fileref[new_img_id] = element; + sidebar.set_file_annotations_to_default_value(new_img_id); + new_img_index_list.push(_via_image_id_list.indexOf(new_img_id)); + } else { + let img_id = _via_image_id_list[img_index]; + _via_img_fileref[img_id] = element; + _via_img_metadata[img_id]["size"] = element.size; + } + } else { + discarded_file_count += 1; + } + } + + if (_via_img_metadata) { + let status_msg = "Loaded " + new_img_index_list.length + " images."; + if (discarded_file_count) { + status_msg += + " ( Discarded " + discarded_file_count + " non-image files! )"; + } + show_message(status_msg); + + if (new_img_index_list.length) { + buffer.showImage(new_img_index_list[0]); + } else { + // show original image + buffer.showImage(_via_image_index); + } + sidebar.update_img_fn_list(); + } else { + show_message("Please upload some image files!"); + } + } + + addAbsPathFileWithInput() { + let config = { title: "Add File" }; + let input = { + abs_path: { + type: "text", + name: "Absolute Path", + value: "", + disabled: false, + size: 30, + }, + }; + invoke_with_user_inputs(this.addAbsPathFileConfirmed, input, config); + } + + addAbsPathFileConfirmed(input) { + let abs_path = input.abs_path; + if (abs_path !== "") { + let filename = abs_path.split("/").pop(); + let img_id = this.addUrlFile(abs_path); + let img_index = _via_image_id_list.indexOf(img_id); + buffer.showImage(img_index); + sidebar.update_img_fn_list(); + user_input_default_cancel_handler(); + Message.showInfo("Added file: " + filename); + } else { + if (input.absolute_path_list.value !== "") { + let absolute_path_list_str = input.absolute_path_list.value; + import_files_url_from_csv(absolute_path_list_str).then(() => + console.log("csv") + ); + } else { + Message.showError("Please enter a valid path"); + } + } + } + + addUrlFileWithInput() { + let config = { title: "Add File using URL" }; + let input = { + url: { + type: "text", + name: "add one URL", + placeholder: + "https://example.com/image.jpg", + disabled: false, + size: 50, + }, + url_list: { + type: "textarea", + name: "or, add multiple URL (one url per line)", + placeholder: + "https://example.com/image1.jpg\nhttps://example.com/image2.jpg\nhttps://example.com/image3.png", + disabled: false, + rows: 5, + cols: 80, + }, + }; + + invoke_with_user_inputs(this.addUrlFileConfirmed, input, config); + } + + addUrlFileConfirmed(input) { + let url = ""; + if (input && input.url && input.url.value) { + url = input.url.value.trim() + let img_id = this.addUrlFile(url); + let img_index = _via_image_id_list.indexOf(img_id); + show_message("Added file at url [" + url + "]"); + sidebar.update_img_fn_list(); + buffer.showImage(img_index); + user_input_default_cancel_handler(); + } else { + if (input.url_list.value !== "") { + let url_list_str = input.url_list.value; + import_files_url_from_csv(url_list_str).then(() => console.log("csv")); + } + } + } + + addUrlFile(url) { + if (url !== "") { + let size = -1; // convention: files added using url have size = -1 + let img_id = _via_get_image_id(url, size); + + if (!_via_img_metadata.hasOwnProperty(img_id)) { + img_id = project.addFile(url); + _via_img_src[img_id] = _via_img_metadata[img_id].filename; + sidebar.set_file_annotations_to_default_value(img_id); + return img_id; + } + } + } + + fileLoadOnFail(img_idx) { + let img_id = _via_image_id_list[img_idx]; + _via_img_src[img_id] = ""; + _via_image_load_error[img_idx] = true; + sidebar.img_fn_list_ith_entry_error(img_idx, true); + } + + fileLoadOnSuccess(img_idx) { + _via_image_load_error[img_idx] = false; + sidebar.img_fn_list_ith_entry_error(img_idx, false); + } + + importAttributesFromFile(event) { + let selected_files = event.target.files; + for (let i = 0; i < selected_files.length; ++i) { + let file = selected_files[i]; + FileManager.loadTextFile(file, this.importAttributesFromJson); + } + } + + importAttributesFromJson(data) { + try { + let d = JSON.parse(data); + let attr; + let fattr_count = 0; + let rattr_count = 0; + // process file attributes + for (attr in d["file"]) { + this.attributes.file[attr] = JSON.parse( + JSON.stringify(d["file"][attr]) + ); + fattr_count += 1; + } + + // process region attributes + for (attr in d["region"]) { + this.attributes.region[attr] = JSON.parse( + JSON.stringify(d["region"][attr]) + ); + rattr_count += 1; + } + + if (fattr_count > 0 || rattr_count > 0) { + let fattr_id_list = Object.keys(this.attributes.file); + let rattr_id_list = Object.keys(this.attributes.region); + if (rattr_id_list.length) { + _via_attribute_being_updated = "region"; + _via_current_attribute_id = rattr_id_list[0]; + } else { + if (fattr_id_list.length) { + _via_attribute_being_updated = "file"; + _via_current_attribute_id = fattr_id_list[0]; + } + } + attribute_update_panel_set_active_button(); + update_attributes_update_panel(); + sidebar.annotation_editor_update_content(); + } + Message.showInfo( + "Imported " + + fattr_count + + " file attributes and " + + rattr_count + + " region attributes" + ); + } catch (error) { + Message.showError("Failed to import attributes: [" + error + "]"); + } + } +} + +function clearViaData() { + // clear existing data (if any) + _via_image_id_list = []; + _via_image_filename_list = []; + _via_img_count = 0; + _via_img_metadata = {}; + _via_img_fileref = {}; + _via_img_src = {}; + project.attributes = { region: {}, file: {} }; + buffer.emptyBuffer(); +} + +function import_img_metadata(img_metadata) { + var img_id; + for (img_id in img_metadata) { + if (!_via_img_metadata.hasOwnProperty(img_id)) { + _via_img_metadata[img_id] = img_metadata[img_id]; + _via_image_id_list.push(img_id); + _via_image_filename_list.push(img_metadata[img_id].filename); + _via_img_count += 1; + } + } +} + +const project = new Project(); \ No newline at end of file diff --git a/src/js/science_plugins.js b/src/js/science_plugins.js new file mode 100644 index 0000000..02fb4dd --- /dev/null +++ b/src/js/science_plugins.js @@ -0,0 +1,441 @@ +// science_plugins.js +// Description: This file contains the group and score functionality for the InfarctSize. Also contains the export functionality for detailed region sizes. +// Worker function for grouping regions +function infarctSizeAiWorker() { + onmessage = function (e) { + let slice_regions_id = JSON.parse(e.data.slice_regions_id); + let regions = JSON.parse(e.data.regions); + let update = e.data.update; + let grouped_regions = {}; + + // Pre-calculate centroids for all regions + let centroids = regions.map(region => centroidOfPolygon({ + x: region.shape_attributes["all_points_x"], + y: region.shape_attributes["all_points_y"] + })); + + for (let i = 0; i < slice_regions_id.length; ++i) { + for (let j = 0; j < slice_regions_id.length; ++j) { + if (i !== j) { + let polygon1 = regions[slice_regions_id[i]].shape_attributes; + let centroid = centroids[slice_regions_id[j]]; + if ( + isInsidePolygon( + polygon1["all_points_x"], + polygon1["all_points_y"], + centroid.x, + centroid.y + ) + ) { + slice_regions_id.splice(j, 1); + j--; + if (j < i) { + i--; + } + } + } + } + } + + for (const element of slice_regions_id) { + let polygon = regions[element].shape_attributes; + let tmp = []; + + for (let j = 0; j < regions.length; j++) { + if (j !== element && regions[j].shape_attributes.name === "polygon") { + let centroid = centroids[j]; + if ( + isInsidePolygon( + polygon["all_points_x"], + polygon["all_points_y"], + centroid.x, + centroid.y + ) + ) { + tmp.push(j); + } + } + } + grouped_regions[element] = tmp; + } + postMessage({ + slice_regions_id: JSON.stringify(slice_regions_id), + grouped_regions: JSON.stringify(grouped_regions), + update: update, + }); + }; + + /** + * Calculates the centroid of a polygon + * @param vertices - x,y -coordinates of polygon vertices + * @returns {{x: number, y: number}} - x,y -coordinates of centroid + */ + function centroidOfPolygon(vertices) { + let length = vertices.x.length; + + let sumX = vertices.x.reduce((a, b) => a + b, 0); + let sumY = vertices.y.reduce((a, b) => a + b, 0); + + return { x: Math.floor(sumX / length), y: Math.floor(sumY / length) }; + } + + /** + * Checks if a point is inside a polygon region + * @param all_points_x - x-coordinates of polygon vertices + * @param all_points_y - y-coordinates of polygon vertices + * @param px - x,y -coordinates of point + * @param py - x,y -coordinates of point + * @returns {boolean} - 0 if point is outside the polygon, 1 if point is inside the polygon + */ + function isInsidePolygon(all_points_x, all_points_y, px, py) { + if (all_points_x === undefined || all_points_y === undefined) { + return false; + } + if (all_points_x.length === 0 || all_points_y.length === 0) { + return false; + } + + let wn = 0; // the winding number counter + + + let n = all_points_x.length; + let i; + + for (i = 0; i < n; ++i) { + let next = (i + 1) % n; // Use the next point, or the first point if at the end of the array + + let is_left_value = isLeft( + all_points_x[i], + all_points_y[i], + all_points_x[next], + all_points_y[next], + px, + py + ); + + if (all_points_y[i] <= py) { + if (all_points_y[next] > py && is_left_value > 0) { + ++wn; + } + } else { + if (all_points_y[next] <= py && is_left_value < 0) { + --wn; + } + } + } + + return wn !== 0; + } + + /** + * Checks if a point is left of a line + * @param x0 - x-coordinate of first point of line + * @param y0 - y-coordinate of first point of line + * @param x1 - x-coordinate of second point of line + * @param y1 - y-coordinate of second point of line + * @param x2 - x-coordinate of point + * @param y2 - y-coordinate of point + * @returns {number} - >0 if point is left of line, =0 if point is on line, <0 if point is right of line + */ + function isLeft(x0, y0, x1, y1, x2, y2) { + return (x1 - x0) * (y2 - y0) - (x2 - x0) * (y1 - y0); + } //helper function for isInsidePolygon() +} + +/** +* @fileOverview This file contains the Infarct Size AI plugin. +* name: "InfarctSizeAI", +* description: "Plugins for SAA Infarct Size AI", +* version: "0.1.0", +* @constructor +*/ +function Science_plugins() { + this.grouped_regions = {}; + this.slice_regions_id = []; + this.GroupingWorker = new Worker( + URL.createObjectURL( + new Blob(["(" + infarctSizeAiWorker.toString() + ")()"], { + type: "text/javascript", + }) + ) + ); + this.GroupingWorker.onmessage = this.groupingWorkerOnMessage.bind(this); +} + +Science_plugins.prototype.updateSliceRegion = function (img_id = _via_image_id, update = true) { + let groupBy = []; + for (let i = 0; i < _via_img_metadata[img_id].regions.length; ++i) { + if ( + _via_img_metadata[img_id].regions[i].region_attributes.Type === + "Slice" + ) { + groupBy.push(i); + } + } + if (groupBy.length === 0) { + console.log("No slice regions found"); + return false; + } + if (groupBy === _via_img_metadata[img_id].groupedRegions.groupBy) { + console.log("No change in slice regions") + return false; + } + _via_img_metadata[img_id].clearGroups(); + this.GroupingWorker.postMessage({ + slice_regions_id: JSON.stringify(groupBy), + regions: JSON.stringify(_via_img_metadata[img_id].regions), + grouped_regions: JSON.stringify(_via_img_metadata[_via_image_id].groupedRegions.groups, Project.replacer), + update: update, + }); +}; + +// Grouping regions on other thread +Science_plugins.prototype.groupingWorkerOnMessage = function (e) { + let groups = JSON.parse(e.data.grouped_regions); + for (const element in groups) { + _via_img_metadata[_via_image_id].addGroupedRegion(parseInt(element), groups[element]) + } + _via_img_metadata[_via_image_id].groupedRegions.groupBy = JSON.parse(e.data.slice_regions_id); + if (e.data.update) { + sidebar.annotation_editor_update_content(); + } +}; + +// Grouping modify name to group +Science_plugins.prototype.changeGroupIdentifier = function (dom) { + // get value from input by onclick event + let groupId = parseInt(dom.id.split("_")[1]); + let newIdentifier = dom.value + _via_img_metadata[_via_image_id].groupedRegions.groupIDs.set(groupId, newIdentifier); +} + +// Get and select all regions in group and highlight the row in the annotation editor +Science_plugins.prototype.selectAllRegionsInGroup = function (dom) { + let groupId = parseInt(dom.id.split("_")[2]); + toggle_all_regions_selection(false); + sidebar.annotation_editor_clear_row_highlight(); + _via_region_selected_flag.add(groupId); + let regions = _via_img_metadata[_via_image_id].getGroupedRegion(groupId); + for (const region in regions) { + _via_region_selected_flag.add(regions[region]); + } + sidebar.annotation_editor_highlight_row(groupId); + drawing.setIsRegionSelected(true); + drawing.setUserSelRegionId(groupId); + drawing.redrawRegCanvas(); + +} + +// Get the score of a region +Science_plugins.prototype.getScores = function (region_id) { + if (_via_img_metadata[_via_image_id].regions[region_id].hasOwnProperty("score")) { + return _via_img_metadata[_via_image_id].regions[region_id].score; + } + return "n/a"; +} + +// Set the score of a region +Science_plugins.prototype.setScores = function (region_id, score) { + _via_img_metadata[_via_image_id].regions[region_id].score = score; + sidebar.annotation_editor_update_content(); +} + +// Update the scores of the regions based on the group +Science_plugins.prototype.updateScoresBasedOnGroup = function () { + // update the scores based on the group and if there is a n/a score in the group from the same type then set the score to n/a + for (const element in _via_img_metadata[_via_image_id].groupedRegions.groups) { + let isModified = { + Infarct: false, + Risk: false, + }; + for (let i = 0; i < _via_img_metadata[_via_image_id].groupedRegions.groups[element].length; i++) { + let region = _via_img_metadata[_via_image_id]. + regions[_via_img_metadata[_via_image_id].groupedRegions.groups[element][i]]; + if (region.hasOwnProperty("score") && region.score === "n/a") { + switch (region.region_attributes.Type) { + case "Infarct": + isModified.Infarct = true; + break; + case "Risk": + isModified.Risk = true; + break; + } + } + } + if (isModified.Infarct || isModified.Risk) { + for (let i = 0; i < _via_img_metadata[_via_image_id].groupedRegions.groups[element].length; i++) { + let region = _via_img_metadata[_via_image_id]. + regions[_via_img_metadata[_via_image_id].groupedRegions.groups[element][i]]; + if (isModified.Infarct && region.region_attributes.Type === "Infarct") { + region.score = "n/a"; + } + if (isModified.Risk && region.region_attributes.Type === "Risk") { + region.score = "n/a"; + } + } + } + } +} + +// Efficiently calculate the area of a polygon +Science_plugins.prototype.calcPolygonArea = function (xCoordinates, yCoordinates) { + let total = 0; + const n = xCoordinates.length; + + for (let i = 0; i < n; i++) { + const nextIndex = (i + 1) % n; + const addTerm = xCoordinates[i] * yCoordinates[nextIndex]; + const subTerm = yCoordinates[i] * xCoordinates[nextIndex]; + total += addTerm - subTerm; + } + + return 0.5 * Math.abs(total); +} + +// Export the region sizes to a csv file +Science_plugins.prototype.ExportArea = async function () { + let rows = [ + ["Filename", "File_ID", "Treatment_ID", "Slice_ID", "Slice_area", "Infract_area", "Risk_area", "Slice_score", "Infract_score", "Risk_score"] + ]; + let done = 0; + for (let img_index in _via_image_id_list) { + let img_id = _via_image_id_list[img_index]; + if (_via_img_metadata[img_id].regions.length === 0) { + continue; + } + if (!_via_img_metadata[img_id].groupedRegions.groups.size > 0) { + if (!this.updateSliceRegion(img_id, false)) { + console.log("Slice regions updated but not grouped yet. Skipping image: " + img_id); + continue; + } + } + await new Promise((resolve) => { + let interval = setInterval(() => { + if (_via_img_metadata[img_id].groupedRegions.groups.size > 0) { + clearInterval(interval); + resolve(); + } + }, 100); + }).then(() => { + let slices = []; + if (_via_img_metadata[img_id].groupedRegions.groups === 0) { + return; + } + for (let i = 0; i < _via_img_metadata[img_id].groupedRegions.groupBy.length; i++) { + let element = _via_img_metadata[img_id].groupedRegions.groupBy[i]; + let slice = { + ID: i + 1, + Area: 0, + InfarctArea: 0, + RiskArea: 0, + SliceScore: "n/a", + InfarctScore: "n/a", + RiskScore: "n/a", + }; + let scores = { + score_exists: false, + Slice: 0, + Slice_no: 0, + Infarct: 0, + Infarct_no: 0, + Risk: 0, + Risk_no: 0, + }; + if (_via_img_metadata[img_id].groupedRegions.groupIDs.has(element)) { + slice.ID = _via_img_metadata[img_id].groupedRegions.groupIDs.get(element); + } + slice.Area = this.calcPolygonArea(_via_img_metadata[img_id].regions[element].shape_attributes.all_points_x, _via_img_metadata[img_id].regions[element].shape_attributes.all_points_y); + if (_via_img_metadata[img_id].regions[element].hasOwnProperty("score")) { + scores.score_exists = true; + scores.Slice += _via_img_metadata[img_id].regions[element].score; + scores.Slice_no++; + } + for (let i = 0; i < _via_img_metadata[img_id].getGroupedRegion(element).length; i++) { + let region = _via_img_metadata[img_id].regions[_via_img_metadata[img_id].getGroupedRegion(element)[i]]; + let Area = this.calcPolygonArea(region.shape_attributes.all_points_x, region.shape_attributes.all_points_y); + if (region.hasOwnProperty("score")) { + scores.score_exists = true; + switch (region.region_attributes.Type) { + case "Slice": + if (region.score === "n/a") { + break; + } + scores.Slice += region.score; + scores.Slice_no++; + break; + case "Risk": + if (region.score === "n/a") { + break; + } + scores.Risk += region.score; + scores.Risk_no++; + break; + case "Infarct": + if (region.score === "n/a") { + break; + } + scores.Infarct += region.score; + scores.Infarct_no++; + break; + } + } + switch (region.region_attributes.Type) { + case "Slice": + slice.Area -= Area; + break; + case "Risk": + slice.RiskArea += Area; + break; + case "Infarct": + slice.InfarctArea += Area; + break; + } + } + if (scores.score_exists) { + if (scores.Slice_no > 0 && scores.Slice > 0) { + slice.SliceScore = scores.Slice / scores.Slice_no; + } else { + slice.SliceScore = "n/a"; + } + if (scores.Infarct_no > 0 && scores.Infarct > 0) { + slice.InfarctScore = scores.Infarct / scores.Infarct_no; + } else { + slice.InfarctScore = "n/a"; + } + if (scores.Risk_no > 0 && scores.Risk > 0) { + slice.RiskScore = scores.Risk / scores.Risk_no; + } else { + slice.RiskScore = "n/a"; + } + } + slices.push(slice); + } + let img_file = _via_image_filename_list[img_index]; + let file_id = _via_img_metadata[img_id].file_attributes.ID || ""; + slices.forEach((slice, i) => { + rows.push([ + img_file, + file_id, + slice.ID, + slice.Area, + slice.InfarctArea, + slice.RiskArea, + slice.SliceScore, + slice.InfarctScore, + slice.RiskScore + ]); + }); + done++; + }); + + } + let csvContent = "data:text/csv;charset=utf-8," + rows.map(e => e.join(",")).join("\n"); + let encodedUri = encodeURI(csvContent); + let link = document.createElement("a"); + link.setAttribute("href", encodedUri); + link.setAttribute("download", "RegionSizes.csv"); + document.body.appendChild(link); + link.click(); +}; + +const plugin = new Science_plugins(); \ No newline at end of file diff --git a/src/js/servercom.js b/src/js/servercom.js new file mode 100644 index 0000000..a7ba422 --- /dev/null +++ b/src/js/servercom.js @@ -0,0 +1,479 @@ +// servercom.js +// Description: This file contains the functions for the server communication. +// Annotations are sent to the server and the server sends the annotations back. +// The annotations are then added to the image metadata. +// Annotate function and prototype for the annotation process +function Annotate() { + this.canvas = document.createElement("canvas"); + this.ctx = this.canvas.getContext("2d"); + this.modal = $("#staticBackdropProgress"); + this.progress = $("#annotationProgress"); + this.progressNum = 0; + this.reAnnotateCommand = ""; +} + +const AutoAnnotator = new Annotate(); + +Annotate.prototype.run_annotation = function () { + switch (_via_display_area_content_name) { + case VIA_DISPLAY_AREA_CONTENT_NAME.IMAGE_GRID: + this.modal.modal("show"); + this.annotate_all_imgs(); + break; + + case VIA_DISPLAY_AREA_CONTENT_NAME.IMAGE: + this.modal.modal("show"); + this.annotate_single_img().then(() => { + this.setProgressBar(100); + if (this.reAnnotateCommand === "cancel") { + this.reAnnotateCommand = ""; + return; + } + $("#annotation_spinner").removeClass("d-none"); + Message.show({ + address: "Success", + body: "The image uploaded successfully.", + }); + }); + break; + + default: + Message.show({ + address: "Error", + body: "Please load an image first.", + }); + } +}; + +Annotate.prototype.annotate_all_imgs = function () { + try { + this.progressNum = 10; + this.setProgressBar(this.progressNum); + this.sendImageAll().then(() => { + this.setProgressBar(100); + $("#annotation_spinner").removeClass("d-none"); + }); + } catch (e) { + this.progressNum = 0; + setTimeout(function () { + $("#staticBackdropProgress").modal("hide"); + }, 1000); + this.progress.css("width", 0 + "%"); + console.log(e); + Message.show({ + address: "Error", + body: "Please check your network connection or load the image again.", + }); + } +}; + +Annotate.prototype.annotate_single_img = async function () { + try { + this.progressNum = 10; + this.setProgressBar(this.progressNum); + let image_index = _via_image_index; + let img_id = _via_image_id_list[image_index]; + const imageElement = $("#bim" + image_index)[0]; + const imageBlob = await this.getImageBlobFromElement(imageElement); + await this.isAnnotated(img_id).then((res) => { + }); + switch (this.reAnnotateCommand) { + case "reAnnotate": + _via_img_metadata[img_id].autoAnnotated = false; + break; + case "cancel": + this.setProgressBar(100); + Message.show({ + address: "Info", + body: "The image was not annotated.", + }); + return; + default: + break; + } + await this.sendImage( + imageBlob, + _via_img_metadata[_via_image_id_list[image_index]].filename + ); + } catch (e) { + console.log(e); + this.progressNum = 0; + setTimeout(function () { + $("#staticBackdropProgress").modal("hide"); + }, 1000); + this.progress.css("width", 0 + "%"); + + Message.show({ + address: "Error", + body: "Please check your network connection or load the image again.", + }); + } +}; + +Annotate.prototype.setProgressBar = function (num) { + this.progress.animate( + { + width: num + "%", + }, + 100 + ); + if (num === 100) { + this.progress.css("width", 100 + "%"); + setTimeout( + function () { + this.modal.modal("hide"); + this.progress.css("width", 0 + "%"); + }.bind(this), + 1000 + ); + } +}; + +Annotate.prototype.setModal = function (title, body, footer) { + $("#staticBackdropModal h1").html(title); + $("#modalBody").html(body); + $("#modalFooter").html(footer); + setTimeout(function () { + $("#staticBackdropModal").modal("show"); + }, 1000); +}; + +Annotate.prototype.clearModal = function () { + $("#staticBackdropModal").modal("hide"); + $("#staticBackdropModal h1").html(""); + $("#modalBody").html(""); + $("#modalFooter").html(""); +}; + +Annotate.prototype.awaitUserInput = function (isAll = false) { + return new Promise((resolve) => { + $("#modalFooter").on("click", "#reAnnotate", function () { + resolve("reAnnotate"); + }); + $("#modalFooter").on("click", "#cancel", function () { + resolve("cancel"); + }); + if (isAll) { + $("#modalFooter").on("click", "#reAnnotateAll", function () { + resolve("reAnnotateAll"); + }); + } + }); +}; + +Annotate.prototype.addScores = function (img_id, res, attr, slice_id) { + if ( + res.hasOwnProperty("slice_scores") && + res.hasOwnProperty("infarct_scores") && + res.hasOwnProperty("risk_scores") + ) { + if (attr !== undefined) { + switch (attr["region_attributes"]["Type"]) { + case "Slice": + attr.score = res["slice_scores"][slice_id]; + break; + case "Infarct": + attr.score = res["infarct_scores"][slice_id]; + break; + case "Risk": + attr.score = res["risk_scores"][slice_id]; + break; + } + } + } +}; + +Annotate.prototype.getImageBlobFromElement = async function (imageElement) { + return new Promise((resolve, reject) => { + const canvas = document.createElement("canvas"); + const context = canvas.getContext("2d"); + canvas.width = imageElement.width; + canvas.height = imageElement.height; + context.drawImage( + imageElement, + 0, + 0, + imageElement.width, + imageElement.height + ); + canvas.toBlob((blob) => { + resolve(blob); + }, "image/png"); + }); +}; + +Annotate.prototype.isAnnotated = async function (img_id) { + if (_via_img_metadata[img_id].autoAnnotated) { + this.setModal( + "Notice", + '<p class ="fw-bold" >This image is already annotated. Do you want to re-annotate them?</p>' + + '<p class ="fw-light">If you click "Re-Annotate", the old annotations will be deleted.</p>' + + '<p class ="fw-light">If you click "Cancel", the old annotations will be kept.</p>', + '<button type="button" class="btn btn-primary" id="reAnnotate">Re-Annotate</button>' + + '<button type="button" class="btn btn-secondary" id="cancel">Cancel</button>' + ); + this.reAnnotateCommand = await this.awaitUserInput(); + this.clearModal(); + } +}; + +Annotate.prototype.isAnnotatedAll = async function (img_id) { + if ( + _via_img_metadata[img_id].autoAnnotated && + this.reAnnotateCommand !== "reAnnotateAll" + ) { + $("#staticBackdropModal h1").html("Notice"); + this.setModal( + "Notice", + '<p class ="fw-bold" >This image is already annotated. Do you want to re-annotate them?</p>' + + '<p class ="fw-light">If you click "Re-Annotate", the old annotations will be deleted.</p>' + + '<p class ="fw-light">If you click "Cancel", the old annotations will be kept.</p>', + '<button type="button" class="btn btn-primary" id="reAnnotate">Re-Annotate</button>' + + '<button type="button" class="btn btn-primary" id="reAnnotateAll">Re-Annotate all</button>' + + '<button type="button" class="btn btn-secondary" id="cancel">Cancel</button>' + ); + this.reAnnotateCommand = await this.awaitUserInput(true); + this.clearModal(); + } +}; + +Annotate.prototype.addAnnotations = async function (img_id, res) { + let enableSlice = true; + let slice_id = -1; + if (this.reAnnotateCommand === "reAnnotate" || this.reAnnotateCommand === "reAnnotateAll") { + _via_img_metadata[img_id].regions = []; + _via_img_metadata[img_id].autoAnnotated = false; + _via_img_metadata[img_id].lockedRegions.clear(); + } + for (let m = 0; m < res["masks"].length; m++) { + let region_i = { shape_attributes: {}, region_attributes: {} }; + let Type = "Risk"; + if (res["class_ids"][m] === 0) Type = "Slice"; + else if (res["class_ids"][m] === 1) Type = "Infarct"; + region_i["region_attributes"]["Type"] = Type; + if (Type === "Slice" && enableSlice) { + slice_id++; + enableSlice = false; + } + if (Type !== "Slice" && !enableSlice) { + enableSlice = true; + } + this.addScores(img_id, res, region_i, slice_id); + let Xpoints = []; + let Ypoints = []; + let iter = 1; + if (res["masks"][m].length > 1) { + for (let i = 0; i < res["masks"][m].length; i = i + 2) { + if (res["masks"][m].length > 15) { + if (iter % (settings.reduction + 1) === 0) { + Xpoints.push(res["masks"][m][i]); + Ypoints.push(res["masks"][m][i + 1]); + } + } else { + Xpoints.push(res["masks"][m][i]); + Ypoints.push(res["masks"][m][i + 1]); + } + iter++; + } + region_i["shape_attributes"] = { + name: "polygon", + all_points_x: Xpoints, + all_points_y: Ypoints, + }; + if (Xpoints.length > 2) { + _via_img_metadata[img_id].regions.push(region_i); + _via_img_metadata[img_id].addLockedRegion( + _via_img_metadata[img_id].regions.length - 1 + ); + } + } + } + _via_img_metadata[img_id].autoAnnotated = true; +}; + +Annotate.prototype.sendImage = async function (img_blob, img_filename) { + try { + const formData = new FormData(); + formData.append("image", img_blob, img_filename); + $.ajax({ + type: "POST", + url: settings.serverAddress + "/upload", + data: formData, + contentType: false, + processData: false, + xhr: function () { + const xhr = new window.XMLHttpRequest(); + xhr.upload.addEventListener("progress", function (event) { + if (event.lengthComputable) { + const progress = (event.loaded / event.total) * 100; + AutoAnnotator.setProgressBar(progress); + } + }); + return xhr; + }, + success: function (response) { + $("#annotation_spinner").addClass("d-none"); + if (response === undefined || response === null) { + Message.show({ + address: "Error", + body: "Error sending images!", + }); + return; + } + if (response === "login error") { + Message.show({ + address: "Error", + body: "Please login again.", + }); + setTimeout(function () { + window.location.href = "/login"; + }, 2000); + return; + } + zoom.resetZoom(); + for (let file in response) { + let img_id = + _via_image_id_list[_via_image_filename_list.indexOf(file)]; + AutoAnnotator.addAnnotations(img_id, response[file]); + } + AutoAnnotator.setAllItemSuccess(); + Message.show({ + address: "Success", + body: "the image annotated successfully.", + }); + }, + error: function (error) { + console.error(error); + AutoAnnotator.setProgressBar(0); + $("#annotation_spinner").addClass("d-none"); + setTimeout(function () { + $("#staticBackdropProgress").modal("hide"); + }, 1000); + Message.show({ + address: "Error", + body: "Error sending images!", + }); + }, + }); + } catch (e) { + console.log(e); + return false; + } +}; + +Annotate.prototype.sendImageAll = async function () { + try { + const formData = new FormData(); + let reannotate = false; + for (const element of _via_image_grid_selected_img_index_list) { + let image_index = element; + let img_id = _via_image_id_list[image_index]; + await this.isAnnotatedAll(img_id); + if (this.reAnnotateCommand === "cancel") { + continue; + } + if (this.reAnnotateCommand === "reAnnotateAll" || this.reAnnotateCommand === "reAnnotate") { + reannotate = true; + } + let imageElement = $("#bim" + image_index)[0]; + if (imageElement === undefined) { + await buffer.addImageToBuffer(image_index); + imageElement = $("#bim" + image_index)[0]; + } + let img_blob = await this.getImageBlobFromElement(imageElement); + let img_filename = _via_img_metadata[img_id].filename; + formData.append("image", img_blob, img_filename); + } + if (reannotate) { + this.reAnnotateCommand = "reAnnotate"; + } + if (this.reAnnotateCommand === "cancel") { + this.progressNum = 100; + this.setProgressBar(this.progressNum); + return; + } + $.ajax({ + type: "POST", + url: settings.serverAddress + "/upload", + data: formData, + contentType: false, + processData: false, + xhr: function () { + const xhr = new window.XMLHttpRequest(); + xhr.upload.addEventListener("progress", function (event) { + if (event.lengthComputable) { + const progress = (event.loaded / event.total) * 100; + AutoAnnotator.setProgressBar(progress); + } + }); + return xhr; + }, + success: function (response) { + $("#annotation_spinner").addClass("d-none"); + if (response === undefined || response === null) { + Message.show({ + address: "Error", + body: "Error sending images!", + }); + return; + } + if (response === "login error") { + Message.show({ + address: "Error", + body: "Please login again.", + }); + setTimeout(function () { + window.location.href = "/login"; + }, 2000); + return; + } + zoom.resetZoom(); + for (let file in response) { + let img_id = _via_image_id_list[_via_image_filename_list.indexOf(file)]; + if (reannotate) { + _via_img_metadata[img_id].regions = []; + } + AutoAnnotator.addAnnotations(img_id, response[file]); + } + AutoAnnotator.setAllItemSuccess(); + }.bind(this), + error: function (error) { + console.error(error); + $("#annotation_spinner").addClass("d-none"); + AutoAnnotator.setProgressBar(0); + setTimeout(function () { + $("#staticBackdropProgress").modal("hide"); + }, 1000); + Message.show({ + address: "Error", + body: "Error sending images!", + }); + }, + }); + } catch (e) { + console.log(e); + return false; + } +}; + +Annotate.prototype.setAllItemSuccess = function () { + _via_is_all_region_selected = false; + drawing.setIsRegionSelected(false); + drawing.setUserSelRegionId(-1); + zoom.resetZoom(); + _via_load_canvas_regions(); + if (_via_canvas_regions.length === 0) { + drawing.clearCanvas(); + } else { + drawing.redrawRegCanvas(); + } + zoom.fixCanvasBlur(); + _via_reg_canvas.focus(); + setTimeout(function () { + plugin.updateSliceRegion(); + }, 1000); + + this.progressNum = 100; + this.setProgressBar(this.progressNum); + this.reAnnotateCommand = ""; +}; \ No newline at end of file diff --git a/src/js/settings.js b/src/js/settings.js new file mode 100644 index 0000000..cf11b73 --- /dev/null +++ b/src/js/settings.js @@ -0,0 +1,616 @@ +// settings.js +// Description: This file contains the Settings class which is used to manage the settings of the application. +// Settings class contains all the settings for the application +class Settings { + constructor() { + // settings panel + this.panel = $("#settings_panel"); + + // project panel + this.projectName = this.projectGetDefaultProjectName(); + this.defaultPath = ""; + this.searchPath = ""; + this.bufferSize = 20; + this.projectSave = false; + + // region panel + // this.regionLabel = $("#settings_regionLabel") + // .find("option:selected") + // .text(); + // this.regionColorStyle = $("#settings_regionColorStyle") + // .find("option:selected") + // .text(); + // this.regionColor = $("#settings_regionColor").val(); + // this.fontStyle = $("#settings_fontStyle").find("option:selected").val(); + this.is_highlight_region = $("#settings_highlightRegion").is(":checked"); + this.is_scrolling = $("#settings_scrollToRow").is(":checked"); + + // attribute panel + this.attributeShowScore = false; + this.attributeShowScoreColor = false; + this.attributeShowPixelArea = false; + this.attributeShowFile = false; + + // score panel + this.serverAddress = ""; + // $("#settings_serverAddress").val(this.serverAddress); + $("#settings_serverReduceRatio").val(this.reduction); + this.scoreThreshold = "0.85"; + this.scoreHigherColor = $("#settings_scoreHighColor").val(); + this.scoreLowerColor = $("#settings_scoreLowColor").val(); + + // other panel + this.reduction = 0; + this.quickbuttons = {}; + + + this.image_grid_content = "all"; + + // project autosave + this.autoSave = false; + this.project = {}; + + // load settings + this.load(); + this.initQuickButtons(); + this.initProject(); + this.initRegion(); + this.initScore(); + this.initOther(); + } + + // + // Settings UI + // + toggleSettings() { + if (this.panel.hasClass("d-none")) { + // show settings panel + clear_display_area(); + this.panel.removeClass("d-none"); + $(`#selection_panel`).hide(); + } else { + // hide settings panel + clear_display_area(); + show_single_image_view(); + this.panel.addClass("d-none"); + } + } + + // + // Settings Local Storage Management + // + + load() { + let settings_saved = JSON.parse(localStorage.getItem("saa_settings")); + if (settings_saved) { + this.defaultPath = settings_saved.defaultPath; + this.searchPath = settings_saved.searchPath; + this.bufferSize = settings_saved.bufferSize; + this.projectSave = settings_saved.projectSave; + + this.is_highlight_region = settings_saved.is_highlight_region; + this.is_scrolling = settings_saved.is_scrolling; + + this.attributeShowScore = settings_saved.attributeShowScore; + this.attributeShowScoreColor = settings_saved.attributeShowScoreColor; + this.attributeShowPixelArea = settings_saved.attributeShowPixelArea; + + this.scoreThreshold = settings_saved.scoreThreshold; + this.scoreHigherColor = settings_saved.scoreHigherColor; + this.scoreLowerColor = settings_saved.scoreLowerColor; + + this.reduction = settings_saved.reduction; + this.quickbuttons = settings_saved.quickbuttons; + + this.image_grid_content = settings_saved.image_grid_content; + this.autoSave = settings_saved.autoSave; + } else { + // no settings found + console.log("No settings found"); + this.save(false); // if no settings in local storage, save default settings + } + } + + save(msg = true) { + try { + let settings_save = { + defaultPath: this.defaultPath, + searchPath: this.searchPath, + bufferSize: this.bufferSize, + projectSave: this.projectSave, + + is_highlight_region: this.is_highlight_region, + is_scrolling: this.is_scrolling, + + attributeShowScore: this.attributeShowScore, + attributeShowScoreColor: this.attributeShowScoreColor, + attributeShowPixelArea: this.attributeShowPixelArea, + attributeShowFile: this.attributeShowFile, + + scoreThreshold: this.scoreThreshold, + scoreHigherColor: this.scoreHigherColor, + scoreLowerColor: this.scoreLowerColor, + + reduction: this.reduction, + quickbuttons: this.quickbuttons, + image_grid_content: this.image_grid_content, + autoSave: this.autoSave, + + }; + localStorage.setItem("saa_settings", JSON.stringify(settings_save)); + } catch (e) { + console.log(e); + Message.show({ + address: "Error", + body: "There was an error saving settings. Please try again.", + color: "#cc1100", + }); + } + if (msg) { + Message.show({ + address: "Save", + body: "Settings saved successfully", + color: "#4ADEDE", + }); + } + } + + // + // Settings Functions - Project + // + initProject() { + $("#settings_projectName") + .val(this.projectName) + .on("input", () => { + this.projectName = $("#settings_projectName").val(); + $("#project_name").val(this.projectName); + this.save(); + }); + $("#project_name") + .val(this.projectName) + .on("input", () => { + this.projectName = $("#project_name").val(); + $("#settings_projectName").val(this.projectName); + }); + $("#settings_defaultPath") + .val(this.defaultPath) + .on("input", () => { + this.defaultPath = $("#settings_defaultPath").val() + "/"; + this.save(); + console.log("Default path changed to " + this.defaultPath); + }); + $("#settings_searchPath") + .val(this.searchPath) + .on("input", () => { + this.searchPath = $("#settings_searchPath").val(); + this.save(); + }); + } + + // + // Settings Functions - Region + // + initRegion() { + $("#settings_regionLabel").on("change", () => { + this.regionLabel = $("#settings_regionLabel") + .find("option:selected") + .text(); + this.save(); + }); + $("#settings_regionColorStyle").on("change", () => { + this.regionColorStyle = $("#settings_regionColorStyle") + .find("option:selected") + .text(); + this.save(); + }); + $("#settings_regionColor") + .val(this.regionColor) + .on("input", () => { + this.regionColor = $("#settings_regionColor").val(); + this.save(); + }); + $("#settings_fontStyle") + .val(this.fontStyle) + .on("input", () => { + this.fontStyle = $("#settings_fontStyle").val(); + this.save(); + }); + this.toggleHighlightRegionCheckbox(); + this.toggleScrollingCheckbox(); + } + + enableHighlightRegion() { + this.is_highlight_region = !this.is_highlight_region; + this.save(); + this.toggleHighlightRegionCheckbox(); + } + + toggleHighlightRegionCheckbox() { + if (this.is_highlight_region) { + $("#settings_highlightRegion").attr("checked", true); + } else { + $("#settings_highlightRegion").attr("checked", false); + } + } + + enableScrollingCheckbox() { + this.is_scrolling = !this.is_scrolling; + this.save(); + this.toggleScrollingCheckbox(); + } + + toggleScrollingCheckbox() { + console.log(this.is_scrolling); + if (this.is_scrolling) { + $("#settings_scrollToRow").attr("checked", true); + } else { + $("#settings_scrollToRow").attr("checked", false); + } + } + + toggleScoreCheckbox() { + if (this.attributeShowScore) { + $("#settings_showScore").attr("checked", true); + } else { + $("#settings_showScore").attr("checked", false); + } + } + + toggleScoreColorCheckbox() { + if (this.attributeShowScoreColor) { + $("#settings_showScoreColor").attr("checked", true); + } else { + $("#settings_showScoreColor").attr("checked", false); + } + } + + togglePixelAreaCheckbox() { + if (this.attributeShowPixelArea) { + $("#settings_showArea").attr("checked", true); + } else { + $("#settings_showArea").attr("checked", false); + } + } + + toggleFileAttributesCheckbox() { + if (this.attributeShowFile) { + $("#settings_showFileAttributes").attr("checked", true); + } else { + $("#settings_showFileAttributes").attr("checked", false); + } + } + + initScore() { + $("#settings_scoring") + .val(this.scoreThreshold) + .on("input", () => { + this.scoreThreshold = $("#settings_scoring").val(); + this.save(); + }); + $("#settings_scoreHighColor") + .val(this.scoreHigherColor) + .on("input", () => { + this.scoreHigherColor = $("#settings_scoreHighColor").val(); + console.log(this.scoreHigherColor); + this.save(); + }); + $("#settings_scoreLowColor") + .val(this.scoreLowerColor) + .on("input", () => { + this.scoreLowerColor = $("#settings_scoreLowColor").val(); + this.save(); + }); + this.toggleScoreCheckbox(); + this.toggleScoreColorCheckbox(); + this.togglePixelAreaCheckbox(); + } + + projectAutoSave() { + this.projectSave = !this.projectSave; + // this.save(); + } + + showScore() { + this.attributeShowScore = !this.attributeShowScore; + this.save(); + } + + showScoreColor() { + this.attributeShowScoreColor = !this.attributeShowScoreColor; + this.save(); + } + + showPixelArea() { + this.attributeShowPixelArea = !this.attributeShowPixelArea; + this.save(); + } + + showFileAttributes() { + this.attributeShowFile = !this.attributeShowFile; + this.save(); + } + + initQuickButtons() { + if (this.quickbuttons === undefined || this.quickbuttons === null || Object.keys(this.quickbuttons).length === 0) { + this.quickbuttons = { + openProject: true, + saveProject: true, + settings: true, + imgGrid: true, + sidePanel: false, + autoAnn: true, + calcAreas: true, + prev: false, + next: false, + undo: true, + redo: true, + zoomIn: false, + zoomOut: false, + selAllReg: false, + copySelReg: false, + pasteReg: false, + delSelReg: false, + }; + this.save(false); + } + } + + toggleQuickButtonsCheckbox() { + if (this.quickbuttons) { + for (let key in this.quickbuttons) { + if (this.quickbuttons[key]) { + $(`#btn-${key}`).attr("checked", true); + } else { + $(`#btn-${key}`).attr("checked", false); + } + } + } + } + + toggleQuickButtons() { + if (this.quickbuttons) { + for (let key in this.quickbuttons) { + if (this.quickbuttons[key]) { + $(`#nav_${key}`).show(); + } else { + $(`#nav_${key}`).hide(); + } + } + } + } + + initOther() { + this.toggleQuickButtonsCheckbox(); + this.toggleQuickButtons(); + $("#settings_serverReduceRatio").val(this.reduction).on("input", () => { + this.reduction = $("#settings_serverReduceRatio").val(); + this.reduction = parseInt(this.reduction); + this.save(); + }); + $("#settings_preloadBuffer").val(this.bufferSize).on("input", () => { + this.bufferSize = $("#settings_preloadBuffer").val(); + this.bufferSize = parseInt(this.bufferSize); + this.save(); + }); + $("#btn-openProject") + .val(this.quickbuttons.openProject) + .on("change", () => { + this.quickbuttons.openProject = $("#btn-openProject").is(":checked"); + this.toggleQuickButtons(); + this.save(); + }); + $("#btn-saveProject") + .val(this.quickbuttons.saveProject) + .on("change", () => { + this.quickbuttons.saveProject = $("#btn-saveProject").is(":checked"); + this.toggleQuickButtons(); + this.save(); + }); + $("#btn-settings") + .val(this.quickbuttons.settings) + .on("change", () => { + this.quickbuttons.settings = $("#btn-settings").is(":checked"); + this.toggleQuickButtons(); + this.save(); + }); + $("#btn-imgGrid") + .val(this.quickbuttons.imgGrid) + .on("change", () => { + this.quickbuttons.imgGrid = $("#btn-imgGrid").is(":checked"); + this.toggleQuickButtons(); + this.save(); + }); + $("#btn-sidePanel") + .val(this.quickbuttons.sidePanel) + .on("change", () => { + this.quickbuttons.sidePanel = $("#btn-sidePanel").is(":checked"); + this.toggleQuickButtons(); + this.save(); + }); + $("#btn-autoAnn") + .val(this.quickbuttons.autoAnn) + .on("change", () => { + this.quickbuttons.autoAnn = $("#btn-autoAnn").is(":checked"); + this.toggleQuickButtons(); + this.save(); + }); + $("#btn-calcAreas") + .val(this.quickbuttons.calcAreas) + .on("change", () => { + this.quickbuttons.calcAreas = $("#btn-calcAreas").is(":checked"); + this.toggleQuickButtons(); + this.save(); + }); + $("#btn-prev") + .val(this.quickbuttons.prev) + .on("change", () => { + this.quickbuttons.prev = $("#btn-prev").is(":checked"); + this.toggleQuickButtons(); + this.save(); + }); + $("#btn-next") + .val(this.quickbuttons.next) + .on("change", () => { + this.quickbuttons.next = $("#btn-next").is(":checked"); + this.toggleQuickButtons(); + this.save(); + }); + $("#btn-undo") + .val(this.quickbuttons.undo) + .on("change", () => { + this.quickbuttons.undo = $("#btn-undo").is(":checked"); + this.toggleQuickButtons(); + this.save(); + }); + $("#btn-redo") + .val(this.quickbuttons.redo) + .on("change", () => { + this.quickbuttons.redo = $("#btn-redo").is(":checked"); + this.toggleQuickButtons(); + this.save(); + }); + $("#btn-settings") + .val(this.quickbuttons.settings) + .on("change", () => { + this.quickbuttons.settings = $("#btn-settings").is(":checked"); + this.toggleQuickButtons(); + this.save(); + }); + + $("#btn-zoomIn") + .val(this.quickbuttons.zoomIn) + .on("change", () => { + this.quickbuttons.zoomIn = $("#btn-zoomIn").is(":checked"); + this.toggleQuickButtons(); + this.save(); + }); + $("#btn-zoomOut") + .val(this.quickbuttons.zoomOut) + .on("change", () => { + this.quickbuttons.zoomOut = $("#btn-zoomOut").is(":checked"); + this.toggleQuickButtons(); + this.save(); + }); + $("#btn-selAllReg") + .val(this.quickbuttons.selAllReg) + .on("change", () => { + this.quickbuttons.selAllReg = $("#btn-selAllReg").is(":checked"); + this.toggleQuickButtons(); + this.save(); + }); + $("#btn-copySelReg") + .val(this.quickbuttons.copySelReg) + .on("change", () => { + this.quickbuttons.copySelReg = $("#btn-copySelReg").is(":checked"); + this.toggleQuickButtons(); + this.save(); + }); + $("#btn-pasteReg") + .val(this.quickbuttons.pasteReg) + .on("change", () => { + this.quickbuttons.pasteReg = $("#btn-pasteReg").is(":checked"); + this.toggleQuickButtons(); + this.save(); + }); + $("#btn-delSelReg") + .val(this.quickbuttons.delSelReg) + .on("change", () => { + this.quickbuttons.delSelReg = $("#btn-delSelReg").is(":checked"); + this.toggleQuickButtons(); + this.save(); + }); + } + + projectGetDefaultProjectName() { + const now = new Date(); + let MONTH_SHORT_NAME = [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + ]; + let ts = + now.getDate() + + MONTH_SHORT_NAME[now.getMonth()] + + now.getFullYear() + + "_" + + now.getHours() + + ":" + + now.getMinutes(); + + return "ISAI_" + ts; + } + + autoSaveProject() { + if (this.autoSave) { + this.save(false); + } + } + + updateProjectData(project = {}) { + this.project = project; + } + + getProjectData() { + return this.project; + } + + isThereAProject() { + return Object.keys(this.project).length > 0; + } + + getSettings() { + return { + defaultPath: this.defaultPath, + searchPath: this.searchPath, + bufferSize: this.bufferSize, + projectSave: this.projectSave, + projectName: this.projectName, + is_highlight_region: this.is_highlight_region, + is_scrolling: this.is_scrolling, + attributeShowScore: this.attributeShowScore, + attributeShowScoreColor: this.attributeShowScoreColor, + attributeShowPixelArea: this.attributeShowPixelArea, + scoreThreshold: this.scoreThreshold, + scoreHigherColor: this.scoreHigherColor, + scoreLowerColor: this.scoreLowerColor, + reduction: this.reduction, + quickbuttons: this.quickbuttons, + image_grid_content: this.image_grid_content, + autoSave: this.autoSave, + + }; + } + + importSettings(settings) { + this.defaultPath = settings.defaultPath; + this.searchPath = settings.searchPath; + this.bufferSize = settings.bufferSize; + this.projectSave = settings.projectSave; + this.projectName = settings.projectName; + this.is_highlight_region = settings.is_highlight_region; + this.is_scrolling = settings.is_scrolling; + this.attributeShowScore = settings.attributeShowScore; + this.attributeShowScoreColor = settings.attributeShowScoreColor; + this.attributeShowPixelArea = settings.attributeShowPixelArea; + this.scoreThreshold = settings.scoreThreshold; + this.scoreHigherColor = settings.scoreHigherColor; + this.scoreLowerColor = settings.scoreLowerColor; + this.reduction = settings.reduction; + this.quickbuttons = settings.quickbuttons; + this.image_grid_content = settings.image_grid_content; + this.autoSave = settings.autoSave; + + this.save(false); + } + +} +const settings = new Settings(); \ No newline at end of file diff --git a/src/js/sidebar.js b/src/js/sidebar.js new file mode 100644 index 0000000..1a68933 --- /dev/null +++ b/src/js/sidebar.js @@ -0,0 +1,2114 @@ +// sidebar.js +// Description: This file contains all the functions related to the sidebar. +// Sidebar class to controll the sidebar +class Sidebar { + constructor() { + this.img_fn_list = $("#img_fn_list"); + this.img_fn_list_regex = $("#img_fn_list_regex"); + this.img_fn_list_regex.val(""); + this.img_fn_list_regex.keyup( + function () { + this.img_fn_list_onregex(); + }.bind(this) + ); + this.img_fn_list_preset_filters = $("#filelist_preset_filters_list"); + this.img_fn_list_preset_filters.change( + function () { + this.img_fn_list_onpresetfilter_select(); + }.bind(this) + ); + this.img_fn_list_html = []; + this.init_img_fn_list(); + + this.aep = $("#annotation_editor_panel"); + this.ae = $("#annotation_editor"); + this.aec = $("#annotation_editor_content"); + this.display_area = $("#display_area"); + this.descending_order = true; + this.addEventlisteners(); + } + + addEventlisteners() { + //for context menu + this.img_fn_list.on( + "contextmenu", + "li", + function (e) { + e.preventDefault(); + this.file_manager_contextmenu(e); + }.bind(this) + ); + //for display area + this.display_area.on( + "contextmenu", + "div", + function (e) { + e.preventDefault(); + this.descending_order = !this.descending_order; + this.annotation_editor_contextmenu(e); + }.bind(this) + ); + } + + //-------------------------------------------- + // Image Filename List HTML Generation + //-------------------------------------------- + + file_manager_contextmenu(e) { + //calculate positions for context menu + let x = e.pageX; + let y = e.pageY; + let w = this.aep.width(); + let h = this.aep.height(); + let dw = this.display_area.width(); + let dh = this.display_area.height(); + let dx = this.display_area.offset().left; + let dy = this.display_area.offset().top; + + //check if context menu is out of display area + if (x + w > dx + dw) { + x = dx + dw - w; + } + if (y + h > dy + dh) { + y = dy + dh - h; + } + + //create context menu + let contextmenu = $( + '<ul class="list-group" id="contextmenu_delete_root" style="position: absolute; z-index: 100"></ul>' + ); + if (contextmenu.length > 0) { + $("#contextmenu_delete_root").remove(); + } + contextmenu.css("left", x); + contextmenu.css("top", y); + contextmenu.append( + '<li class="list-group-item" id="contextmenu_delete">Delete</li>' + ); + + //add event listeners + contextmenu.find("li").hover(function () { + $(this).addClass("active"); + }); + contextmenu.find("li").mouseleave(function () { + $(this).removeClass("active"); + }); + + contextmenu.find("#contextmenu_delete").click( + function () { + //caalculate image id from img_fn_list + let image_title = $(e.target).attr("title"); + let image_id = _via_image_filename_list.indexOf(image_title); + project.removeFile(image_id); + if (image_id === _via_img_count) { + if (_via_img_count === 0) { + buffer.imgLoaded = false; + show_home_panel(); + } else { + buffer.showImage(image_id - 1); + } + } else { + buffer.showImage(image_id); + } + _via_reload_img_fn_list_table = true; + sidebar.update_img_fn_list(); + show_message("Removed file [" + image_title + "] from project"); + user_input_default_cancel_handler(); + contextmenu.remove(); + this.update_img_fn_list(); + }.bind(this) + ); + + //delete context menu if clicked outside + $(document).click(function (e) { + if (e.target.id !== "contextmenu_delete") { + contextmenu.remove(); + } + }); + + //append context menu to display area + this.display_area.append(contextmenu); + } + + annotation_editor_contextmenu(e) { + if ($("#image_panel_container").hasClass("d-none")) { + return; + } + + //calculate positions for context menu + let x = e.pageX; + let y = e.pageY; + + //create context menu + //let contextmenu = $('<ul id="annotation_editor_contextmenu" class="list-group" style="position: absolute; z-index: 100"></ul>'); + let contextmenu = $("<table>", { + style: "position: absolute; z-index: 100", + class: "table table-sm table-borderless", + id: "annotation_editor_contextmenu", + }); + //if there is already a context menu, remove it + if (contextmenu.length) { + $("#annotation_editor_contextmenu").remove(); + } + + //set width of context menu + contextmenu.css("width", "auto"); + //set position of context menu + contextmenu.css("left", x); + contextmenu.css("top", y); + contextmenu.addClass("position-absolute card p-1 m"); + contextmenu.css({ + display: "inline-block", + width: "auto", + padding: "8px!important", + }); + + let thead = $("<thead>"); + //contextmenu.append(thead); + //thead.append('<tr><div class="text-center">Quick annotator</div></tr>'); + let tbody = $("<tbody>"); + contextmenu.append(tbody); + //tbody.append('<tr><td class="text-center" id="contextmenu_delete">Delete</td></tr>'); + + if ( + Object.keys(project.attributes.region).length && + project.attributes.region.constructor === Object + ) { + // to enable automatic hiding of this content + // add annotation editor to image_panel + let ids = drawing.isInsideAllRegion( + { x: e.offsetX, y: e.offsetY }, + this.descending_order, + true + ); + if (ids.length === 0 || ids[0] === undefined || ids[0] === -1) { + return; + } + for (let i = 0; i < ids.length; i++) { + let id = ids[i]; + if (id === undefined) { + continue; + } + let row = this.annotation_editor_get_metadata_row_html(id, true); + tbody.append(row); + } + + this.display_area.append(contextmenu); + this.annotation_editor_update_content(); + //append context menu to display area + } + + //delete context menu if clicked outside + $(document).click(function (e) { + // detect de all objects in the context menu + let contextmenu = $("#annotation_editor_contextmenu"); + let contextmenu_children = contextmenu.find("*"); + let is_contextmenu = false; + for (let i = 0; i < contextmenu_children.length; i++) { + if (e.target === contextmenu_children[i]) { + is_contextmenu = true; + } + } + if (!is_contextmenu) { + contextmenu.remove(); + } + }); + + //todo: checkbox + drawing.updateCheckedLockHtml(); + } + + init_img_fn_list() { + this.img_fn_list_html = []; + this.img_fn_list_html.push('<ul class="list-group">'); + for (let i = 0; i < _via_image_filename_list.length; ++i) { + this.img_fn_list_html.push(this.img_fn_list_ith_entry_html(i)); + } + this.img_fn_list_html.push("</ul>"); + this.img_fn_list.innerHTML = this.img_fn_list_html.join(""); + this.img_fn_list_scroll_to_current_file(); + } + + img_fn_list_onregex() { + let regex = this.img_fn_list_regex.val(); + let p = this.img_fn_list_preset_filters; + if (p.selectedIndex === 6) { + this.filter_selected_region_attribute(regex); + return; + } + + this.img_fn_list_generate_html(regex); + img_fn_list.innerHTML = this.img_fn_list_html.join(""); + this.img_fn_list_scroll_to_current_file(); + + // select 'regex' in the predefined filter list + if (regex === "") { + p.selectedIndex = 0; + } + } + + update_img_fn_list() { + let regex = this.img_fn_list_regex.val(); + let p = this.img_fn_list_preset_filters; + if (regex === "" || regex === null) { + if (p.selectedIndex === 0) { + // show all files + this.img_fn_list_html = []; + _via_img_fn_list_img_index_list = []; + this.img_fn_list_html.push('<ul class="list-group">'); + for (let i = 0; i < _via_image_filename_list.length; ++i) { + this.img_fn_list_html.push(this.img_fn_list_ith_entry_html(i)); + _via_img_fn_list_img_index_list.push(i); + } + this.img_fn_list_html.push("</ul>"); + this.img_fn_list.innerHTML = this.img_fn_list_html.join(""); + this.img_fn_list_scroll_to_current_file(); + } else { + // filter according to preset filters + this.img_fn_list_onpresetfilter_select(); + } + } else { + if (p.selectedIndex === 6) { + // Filter By Region Attribute + this.filter_selected_region_attribute(regex); + } else { + // RegExp + this.img_fn_list_generate_html(regex); + img_fn_list.innerHTML = _via_img_fn_list_html.join(""); + this.img_fn_list_scroll_to_current_file(); + } + } + } + + img_fn_list_onpresetfilter_select() { + let p = this.img_fn_list_preset_filters; + let filter = p.find(":selected").val(); + switch (filter) { + case "all": + document.getElementById("img_fn_list_regex").value = ""; + this.img_fn_list_generate_html(); + img_fn_list.innerHTML = this.img_fn_list_html.join(""); + this.img_fn_list_scroll_to_current_file(); + break; + case "regex": + this.img_fn_list_onregex(); + this.img_fn_list_regex.focus(); + break; + case "files_region_attribute": + this.img_fn_list_onregex(); + this.img_fn_list_regex.focus(); + break; + default: + this.img_fn_list_html = []; + _via_img_fn_list_img_index_list = []; + this.img_fn_list_html.push('<ul class="list-group">'); + let i; + for (i = 0; i < _via_image_filename_list.length; ++i) { + let img_id = _via_image_id_list[i]; + let add_to_list = false; + switch (filter) { + case "files_without_region": + if (_via_img_metadata[img_id].regions.length === 0) { + add_to_list = true; + } + break; + case "files_missing_region_annotations": + if (this.is_region_annotation_missing(img_id)) { + add_to_list = true; + } + break; + case "files_missing_file_annotations": + if (this.is_file_annotation_missing(img_id)) { + add_to_list = true; + } + break; + case "files_error_loading": + if (_via_image_load_error[i] === true) { + add_to_list = true; + } + } + if (add_to_list) { + this.img_fn_list_html.push(this.img_fn_list_ith_entry_html(i)); + _via_img_fn_list_img_index_list.push(i); + } + } + this.img_fn_list_html.push("</ul>"); + img_fn_list.innerHTML = this.img_fn_list_html.join(""); + this.img_fn_list_scroll_to_current_file(); + break; + } + } + + is_region_annotation_missing(img_id) { + let region_attribute; + let i; + for (i = 0; i < _via_img_metadata[img_id].regions.length; ++i) { + for (region_attribute in project.attributes.region) { + if ( + _via_img_metadata[img_id].regions[i].region_attributes.hasOwnProperty( + region_attribute + ) + ) { + if ( + _via_img_metadata[img_id].regions[i].region_attributes[ + region_attribute + ] === "" + ) { + return true; + } + } else { + return true; + } + } + } + return false; + } + + is_file_annotation_missing(img_id) { + let file_attribute; + for (file_attribute in project.attributes.file) { + if ( + _via_img_metadata[img_id].file_attributes.hasOwnProperty(file_attribute) + ) { + if (_via_img_metadata[img_id].file_attributes[file_attribute] === "") { + return true; + } + } else { + return true; + } + } + return false; + } + + img_fn_list_ith_entry_selected(img_index, is_selected) { + if (is_selected) { + this.img_fn_list_ith_entry_add_css_class(img_index, "active"); + } else { + this.img_fn_list_ith_entry_remove_css_class(img_index, "active"); + } + } + + img_fn_list_ith_entry_error(img_index, is_error) { + if (is_error) { + this.img_fn_list_ith_entry_add_css_class(img_index, "error"); + } else { + this.img_fn_list_ith_entry_remove_css_class(img_index, "error"); + } + } + + img_fn_list_ith_entry_add_css_class(img_index, classname) { + let li = document.getElementById("fl" + img_index); + if (li && !li.classList.contains(classname)) { + li.classList.add(classname); + } + } + + img_fn_list_ith_entry_remove_css_class(img_index, classname) { + let li = document.getElementById("fl" + img_index); + if (li && li.classList.contains(classname)) { + li.classList.remove(classname); + } + } + + img_fn_list_clear_all_style() { + $("#img_fn_list li").removeClass("active"); + } + + img_fn_list_add_css_class(classname) { + $("#img_fn_list li").removeClass(); + $("#img_fn_list li").addClass(classname); + } + + img_fn_list_ith_entry_html(i) { + let htmlI = ""; + let filename = _via_image_filename_list[i]; + if (is_url(filename)) { + filename = + filename.substring(0, 4) + "..." + get_filename_from_url(filename); + } + let isActive = false; + htmlI += '<li id="fl' + i + '"'; + if ( + _via_display_area_content_name === + VIA_DISPLAY_AREA_CONTENT_NAME.IMAGE_GRID + ) { + if (_via_image_grid_page_img_index_list.includes(i)) { + // highlight images being shown in image grid + isActive = true; + } + } else { + if (i === _via_image_index) { + // highlight the current entry + isActive = true; + } + } + if (isActive) { + htmlI += ' class="list-group-item active"'; + } else { + htmlI += ' class="list-group-item text-muted"'; + } + htmlI += + ' style="height:40px; overflow: hidden; white-space: nowrap; cursor: default;" onclick="jump_to_image(' + + i + + ')" title="' + + _via_image_filename_list[i] + + '">[' + + (i + 1) + + "] " + + decodeURIComponent(filename) + + "</li>"; + return htmlI; + } + + img_fn_list_generate_html(regex) { + this.img_fn_list_html = []; + _via_img_fn_list_img_index_list = []; + this.img_fn_list_html.push('<ul class="list-group">'); + for (let i = 0; i < _via_image_filename_list.length; ++i) { + let filename = _via_image_filename_list[i]; + if (filename.match(regex) !== null) { + this.img_fn_list_html.push(this.img_fn_list_ith_entry_html(i)); + _via_img_fn_list_img_index_list.push(i); + } + } + this.img_fn_list_html.push("</ul>"); + } + + img_fn_list_scroll_to_current_file() { + this.img_fn_list_scroll_to_file(_via_image_index); + } + + img_fn_list_scroll_to_file(file_index) { + if (_via_img_fn_list_img_index_list.includes(file_index)) { + let sel_file = document.getElementById("fl" + file_index); + let panel_height = img_fn_list.clientHeight - 20; + let window_top = img_fn_list.scrollTop; + let window_bottom = img_fn_list.scrollTop + panel_height; + if (sel_file.offsetTop > window_top) { + if (sel_file.offsetTop > window_bottom) { + img_fn_list.scrollTop = sel_file.offsetTop; + } + } else { + img_fn_list.scrollTop = sel_file.offsetTop - panel_height; + } + } + } + + filter_selected_region_attribute(property_entry) { + const propArray = property_entry.split("="); + if (propArray.length !== 2 || propArray[0] === "" || propArray[1] === "") + return; + let selected_attribute = propArray[0]; + let selected_value = propArray[1]; + if (selected_value === '""') selected_value = ""; + + _via_img_fn_list_html = []; + _via_img_fn_list_img_index_list = []; + _via_img_fn_list_html.push("<ul>"); + + if (project.attributes.region.hasOwnProperty(selected_attribute)) { + let i; + + for (i = 0; i < _via_image_filename_list.length; ++i) { + let img_id = _via_image_id_list[i]; + let idx; + + for (idx = 0; idx < _via_img_metadata[img_id].regions.length; ++idx) { + if ( + _via_img_metadata[img_id].regions[ + idx + ].region_attributes.hasOwnProperty(selected_attribute) + ) { + if ( + JSON.stringify( + _via_img_metadata[img_id].regions[idx].region_attributes[ + selected_attribute + ] + ) === JSON.stringify(selected_value) + ) { + _via_img_fn_list_html.push(this.img_fn_list_ith_entry_html(i)); + _via_img_fn_list_img_index_list.push(i); + break; + } + } + } + } + } + _via_img_fn_list_html.push("</ul>"); + img_fn_list.innerHTML = _via_img_fn_list_html.join(""); + this.img_fn_list_scroll_to_current_file(); + } + + //-------------------------------------------- + // Annotation Editor Panel + //-------------------------------------------- + + /** + * Show the annotation editor panel + * Create the annotation editor panel if it does not exist + * and also an on image annotation editor panel + */ + annotation_editor_show() { + // remove existing annotation editor (if any) + this.annotation_editor_remove(); + + // create new container of annotation editor + this.aep.empty(); + + let ae_tmp = $("<table>", { + id: "annotation_editor", + class: "table table-sm", + }); + + if ( + _via_annotation_editor_mode === VIA_ANNOTATION_EDITOR_MODE.SINGLE_REGION + ) { + if ( + _via_settings.ui.image.on_image_annotation_editor_placement === + VIA_ANNOTATION_EDITOR_PLACEMENT.DISABLE + ) { + return; + } + + // only display on-image annotation editor if + // - region attribute are defined + // - region is selected + if ( + drawing.isRegionSelected() && + Object.keys(project.attributes.region).length && + project.attributes.region.constructor === Object + ) { + ae_tmp.addClass("position-absolute card p-1 m"); + ae_tmp.css({ display: "inline-block", width: "auto" }); + + // to enable automatic hiding of this content + // add annotation editor to image_panel + if ( + _via_settings.ui.image.on_image_annotation_editor_placement === + VIA_ANNOTATION_EDITOR_PLACEMENT.NEAR_REGION + ) { + let html_position = this.annotation_editor_get_placement( + drawing.userSelRegionId() + ); + ae_tmp.css({ top: html_position.top, left: html_position.left }); + } + + this.display_area.append(ae_tmp); + //this.ae = $('#annotation_editor'); + this.annotation_editor_update_content(); + } + } else { + // show annotation editor in a separate panel at the bottom + this.aep.append(ae_tmp); + this.ae = $("#annotation_editor"); + + this.annotation_editor_update_content(); + + if (drawing.isRegionSelected()) { + // highlight entry for region_id in annotation editor panel + this.annotation_editor_scroll_to_row(drawing.userSelRegionId()); + this.annotation_editor_highlight_row(drawing.userSelRegionId()); + } + } + } + + annotation_editor_contextmenu_handler(e) { + e.stopPropagation(); + e.preventDefault(); + + let region_id = parseInt(e.target.dataset.regionId); + let region = _via_img_metadata[_via_image_id].regions[region_id]; + let region_attributes = region.region_attributes; + + let menu = new Menu(); + menu.append( + new MenuItem({ + label: "Delete", + click: () => { + drawing.deleteRegion(region_id); + this.annotation_editor_remove(); + }, + }) + ); + + if (region_attributes.hasOwnProperty("type")) { + let type = region_attributes["type"]; + if (type === "text") { + menu.append( + new MenuItem({ + label: "Edit Text", + click: () => { + drawing.editRegionText(region_id); + this.annotation_editor_remove(); + }, + }) + ); + } + } + + menu.popup(remote.getCurrentWindow()); + } + + /** + * Remove the annotation editor panel + */ + annotation_editor_hide() { + if ( + _via_annotation_editor_mode === VIA_ANNOTATION_EDITOR_MODE.SINGLE_REGION + ) { + // remove existing annotation editor (if any) + this.annotation_editor_remove(); + } else { + this.annotation_editor_clear_row_highlight(); + } + } + + /** + * Toggle the annotation editor panel + * Create the annotation editor panel if it does not exist + */ + annotation_editor_toggle_on_image_editor() { + if ( + _via_settings.ui.image.on_image_annotation_editor_placement === + VIA_ANNOTATION_EDITOR_PLACEMENT.DISABLE + ) { + _via_annotation_editor_mode = VIA_ANNOTATION_EDITOR_MODE.SINGLE_REGION; + _via_settings.ui.image.on_image_annotation_editor_placement = + VIA_ANNOTATION_EDITOR_PLACEMENT.NEAR_REGION; + this.annotation_editor_show(); + show_message("Enabled on image annotation editor"); + } else { + _via_settings.ui.image.on_image_annotation_editor_placement = + VIA_ANNOTATION_EDITOR_PLACEMENT.DISABLE; + _via_annotation_editor_mode = VIA_ANNOTATION_EDITOR_MODE.ALL_REGIONS; + this.annotation_editor_hide(); + show_message("Disabled on image annotation editor"); + } + } + + /** + * Update the annotation editor panel + */ + annotation_editor_update_content() { + try { + if (this.ae !== undefined) { + this.ae.empty(); + this.annotation_editor_update_header_html(); + this.annotation_editor_update_metadata_html(); + drawing.updateCheckedLockHtml(); + } + } catch (err) { + console.log(err); + Message.show({ + address: "Warning", + body: "Cannot update annotation editor content", + color: "#e8f800", + }); + } + } + + /** + * Calculate the position of on image annotation editor panel + * @param region_id + * @returns {{}} position of on image annotation editor panel top and left properties + */ + annotation_editor_get_placement(region_id) { + let html_position = {}; + let r = _via_canvas_regions[region_id]["shape_attributes"]; + let shape = r["name"]; + switch (shape) { + case "rect": + html_position.top = r["y"] + r["height"]; + html_position.left = r["x"] + r["width"]; + break; + case "circle": + html_position.top = r["cy"] + r["r"]; + html_position.left = r["cx"]; + break; + case "ellipse": + html_position.top = r["cy"] + r["ry"] * Math.cos(r["theta"]); + html_position.left = r["cx"] - r["ry"] * Math.sin(r["theta"]); + break; + case "polygon": + case "polyline": + let most_left = Object.keys(r["all_points_x"]).reduce(function (a, b) { + return r["all_points_x"][a] > r["all_points_x"][b] ? a : b; + }); + html_position.top = Math.max(r["all_points_y"][most_left]); + html_position.left = Math.max(r["all_points_x"][most_left]); + break; + case "point": + html_position.top = r["cy"]; + html_position.left = r["cx"]; + break; + } + html_position.top = html_position.top + image_panel.offsetTop; + // drawing.regionEdgeTol() + panzoom.getPan().y +'px'; + html_position.left = html_position.left + image_panel.offsetLeft; + //drawing.regionEdgeTol() + panzoom.getPan().x +'px'; + return html_position; + } + + /** + * Remove the annotation editor panel + */ + annotation_editor_remove() { + if (this.ae !== undefined) { + this.ae.remove(); + } + } + + /** + * @returns {boolean} true if annotation editor panel is visible + */ + is_annotation_editor_visible() { + return this.aep !== undefined; + } + + /** + * Toggle the annotation editor panel + */ + annotation_editor_toggle_all_regions_editor() { + + _via_annotation_editor_mode = VIA_ANNOTATION_EDITOR_MODE.ALL_REGIONS; + this.aep.css( + "font-size", + _via_settings.ui.annotation_editor_fontsize + "rem" + ); + + this.annotation_editor_show(); + } + + /** + * Set the active row in annotation editor panel + */ + annotation_editor_set_active_button() { + let attribute_type; + for (attribute_type in project.attributes) { + let bid = "button_edit_" + attribute_type + "_metadata"; + $("#" + bid).removeClass("active"); + } + let bid = "button_edit_" + _via_metadata_being_updated + "_metadata"; + $("#" + bid).addClass("active"); + } + + edit_region_metadata_in_annotation_editor() { + _via_metadata_being_updated = "region"; + this.annotation_editor_set_active_button(); + this.annotation_editor_update_content(); + } + + edit_file_metadata_in_annotation_editor() { + _via_metadata_being_updated = "file"; + this.annotation_editor_set_active_button(); + this.annotation_editor_update_content(); + } + + annotation_editor_update_header_html(rt = false) { + let head = $("<thead>", { id: "annotation_editor_header" }); + let tr = $("<tr>"); + + if (_via_metadata_being_updated === "region") { + let rid_col = $("<th>", { scope: "col", html: "#" }); + tr.append(rid_col); + //problem + } + + if (_via_metadata_being_updated === "file") { + let rid_col = $("<th>", { scope: "col" }); + if ( + _via_display_area_content_name === + VIA_DISPLAY_AREA_CONTENT_NAME.IMAGE_GRID + ) { + //sidebar.toggleAEMode(true, "regions"); + rid_col.html("group"); + } else { + rid_col.html("file"); + } + tr.append(rid_col); + } + + let attr_id; + for (attr_id in project.attributes[_via_metadata_being_updated]) { + $("<th>", { scope: "col", html: attr_id }).appendTo(tr); + } + if (_via_metadata_being_updated === "region") { + $("<th>", { + scope: "col", + html: "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" class=\"bi bi-lock-fill\" viewBox=\"0 0 16 16\">\n" + + " <path d=\"M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2m3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2\"/>\n" + + "</svg>", + }).appendTo(tr); + if (settings.attributeShowScore) { + $("<th>", { + scope: "col", + html: "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" class=\"bi bi-calculator-fill\" viewBox=\"0 0 16 16\">\n" + + " <path d=\"M2 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2zm2 .5v2a.5.5 0 0 0 .5.5h7a.5.5 0 0 0 .5-.5v-2a.5.5 0 0 0-.5-.5h-7a.5.5 0 0 0-.5.5m0 4v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5M4.5 9a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5zM4 12.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5M7.5 6a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5zM7 9.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5m.5 2.5a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5zM10 6.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5m.5 2.5a.5.5 0 0 0-.5.5v4a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-4a.5.5 0 0 0-.5-.5z\"/>\n" + + "</svg>", + }).appendTo(tr); + } + if (settings.attributeShowPixelArea) { + $("<th>", { + scope: "col", + html: "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" class=\"bi bi-arrows-angle-expand\" viewBox=\"0 0 16 16\">\n" + + " <path fill-rule=\"evenodd\" d=\"M5.828 10.172a.5.5 0 0 0-.707 0l-4.096 4.096V11.5a.5.5 0 0 0-1 0v3.975a.5.5 0 0 0 .5.5H4.5a.5.5 0 0 0 0-1H1.732l4.096-4.096a.5.5 0 0 0 0-.707m4.344-4.344a.5.5 0 0 0 .707 0l4.096-4.096V4.5a.5.5 0 1 0 1 0V.525a.5.5 0 0 0-.5-.5H11.5a.5.5 0 0 0 0 1h2.768l-4.096 4.096a.5.5 0 0 0 0 .707\"/>\n" + + "</svg>", + }).appendTo(tr); + } + + /*$('<th>', {scope: 'col', + html:'<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 40 40"><path d="M8.458 34.167q-1.083 0-1.854-.771-.771-.771-.771-1.854V8.458q0-1.083.771-1.854.771-.771 1.854-.771h23.084q1.083 0 1.854.771.771.771.771 1.854v23.084q0 1.083-.771 1.854-.771.771-1.854.771Zm6.417-12.334 6.792 6.792 10.416-10.417v-9.75q0-.208-.166-.375-.167-.166-.375-.166H8.458q-.208 0-.375.166-.166.167-.166.375V28.75Zm5.5-2.208v-9.5h1.792v9.5Zm4.708 0-2.916-4.75 2.916-4.75h2.167l-2.917 4.625 2.917 4.875Zm-13.291 0v-5.708H16v-2h-4.208v-1.792h6v5.542h-4.209v2.166h4.209v1.792Zm3.083 5.083L7.833 31.75q0 .083.146.208.146.125.354.125h23.209q.208 0 .375-.166.166-.167.166-.375V21.125L21.667 31.542Zm-6.958 6.834v.541V7.917v.541Z"/></svg>' + }).appendTo(tr);*/ + } + head.append(tr); + if (rt) { + return head; + } + + if (this.ae.length === 0) { + this.ae.append(head); + return; + } else { + if (this.ae.first().attr("id") === "annotation_editor_header") { + this.ae.first().replaceWith(head); + //problem + } else { + // header node is absent + this.ae.prepend(head); + } + return; + } + } + + annotation_editor_update_metadata_html() { + if (!_via_img_count) { + return; + } + + if (!this.ae.has("tbody").length) { + this.ae.append('<tbody id="annotation_editor_content"></tbody>'); + this.aec = $("#annotation_editor_content"); + } + + switch (_via_metadata_being_updated) { + case "region": + let rindex; + if (_via_display_area_content_name === VIA_DISPLAY_AREA_CONTENT_NAME.IMAGE_GRID) { + this.aec.append(this.annotation_editor_get_metadata_row_html(0)); + } else { + if (_via_display_area_content_name === VIA_DISPLAY_AREA_CONTENT_NAME.IMAGE) { + if (_via_annotation_editor_mode === VIA_ANNOTATION_EDITOR_MODE.SINGLE_REGION) { + this.aec.append( + this.annotation_editor_get_metadata_row_html( + drawing.userSelRegionId() + ) + ); + } else { + if (_via_img_metadata[_via_image_id].isGroupable()) { + let tmp = []; + let colspan = 4; + let done_ids = []; + for (rindex = 0; rindex < _via_img_metadata[_via_image_id].groupedRegions.groupBy.length; ++rindex) { + if (!_via_img_metadata[_via_image_id].isGroupedKey(_via_img_metadata[_via_image_id].groupedRegions.groupBy[rindex])) { + continue; + } + $("<tr>", { + id: "ae_row_" + _via_img_metadata[_via_image_id].groupedRegions.groupBy[rindex], + onclick: "plugin.selectAllRegionsInGroup(this)", + style: "height: 40px;", + class: "align-bottom", + }).appendTo(this.aec); + $("<th>", { + scope: "row", + id: "ae_" + _via_img_metadata[_via_image_id].groupedRegions.groupBy[rindex] + "_rid", + html: "O", + }).appendTo("#ae_row_" + _via_img_metadata[_via_image_id].groupedRegions.groupBy[rindex]); + if (_via_img_metadata[_via_image_id].groupedRegions.groupIDs.has(_via_img_metadata[_via_image_id].groupedRegions.groupBy[rindex])) { + $("<td> ", { + id: "ae_row_" + _via_img_metadata[_via_image_id].groupedRegions.groupBy[rindex] + "_group", + colspan: colspan, + html: "<input class='form-control form-control-sm' onchange='plugin.changeGroupIdentifier(this)' type='text' placeholder='" + _via_img_metadata[_via_image_id].groupedRegions.groupIDs.get(_via_img_metadata[_via_image_id].groupedRegions.groupBy[rindex]) + "' aria-label='group identifier' id='group_" + _via_img_metadata[_via_image_id].groupedRegions.groupBy[rindex] + "'>" + }).appendTo("#ae_row_" + _via_img_metadata[_via_image_id].groupedRegions.groupBy[rindex]); + } else { + $("<td> ", { + id: "ae_row_" + _via_img_metadata[_via_image_id].groupedRegions.groupBy[rindex] + "_group", + colspan: colspan, + html: "<input class='form-control form-control-sm' onchange='plugin.changeGroupIdentifier(this)' type='text' placeholder='" + "Group: " + (rindex + 1) + "' aria-label='group identifier' id='group_" + _via_img_metadata[_via_image_id].groupedRegions.groupBy[rindex] + "'>" + }).appendTo("#ae_row_" + _via_img_metadata[_via_image_id].groupedRegions.groupBy[rindex]); + } + + this.aec.append( + this.annotation_editor_get_metadata_row_html(_via_img_metadata[_via_image_id].groupedRegions.groupBy[rindex]) + ); + done_ids.push(_via_img_metadata[_via_image_id].groupedRegions.groupBy[rindex]); + tmp = _via_img_metadata[_via_image_id].getGroupedRegion(_via_img_metadata[_via_image_id].groupedRegions.groupBy[rindex]); + for (const element of tmp) { + this.aec.append( + this.annotation_editor_get_metadata_row_html(element) + ); + done_ids.push(element); + } + } + $("<tr>", { + id: "ae_no_group", + style: "height: 40px;", + class: "align-bottom", + }).appendTo(this.aec); + $("<th>", { + scope: "row", + id: "ae_no_group_rid", + html: "X", + }).appendTo("#ae_no_group"); + $("<td> ", { + id: "ae_row_no_group", + colspan: colspan, + html: "Other", + }).appendTo("#ae_no_group"); + for ( + rindex = 0; + rindex < _via_img_metadata[_via_image_id].regions.length; + ++rindex + ) { + if (!done_ids.includes(rindex)) { + this.aec.append( + this.annotation_editor_get_metadata_row_html(rindex) + ); + } + } + } else { + for ( + rindex = 0; + rindex < _via_img_metadata[_via_image_id].regions.length; + ++rindex + ) { + this.aec.append( + this.annotation_editor_get_metadata_row_html(rindex) + ); + } + } + } + } + } + break; + + case "file": + this.aec.append(this.annotation_editor_get_metadata_row_html(0)); + break; + } + } + + annotation_editor_update_row(row_id) { + if (!this.ae.has("tbody").length) { + this.ae.append('<tbody id="annotation_editor_content"></tbody>'); + this.aec = $("#annotation_editor_content"); + } + let new_row = this.annotation_editor_get_metadata_row_html(row_id); + let id = new_row.attr("id"); + + this.aec + .find(id) + .replaceWith(this.annotation_editor_get_metadata_row_html(row_id)); + } + + annotation_editor_add_row(row_id) { + if (this.is_annotation_editor_visible()) { + if (!this.ae.has("tbody").length) { + this.ae.append('<tbody id="annotation_editor_content"></tbody>'); + this.aec = $("#annotation_editor_content"); + } + let new_row = this.annotation_editor_get_metadata_row_html(row_id); + let penultimate_row_id = parseInt(row_id) - 1; + if (penultimate_row_id >= 0) { + let penultimate_row_html_id = + "ae_" + _via_metadata_being_updated + "_" + penultimate_row_id; + $("#" + penultimate_row_html_id).after(new_row); + } else { + this.aec.append(new_row); + } + } + } + + annotation_editor_get_metadata_row_html(row_id, is_context_menu = false, is_example = false) { + let id = "ae_" + _via_metadata_being_updated + "_" + row_id; + if (is_context_menu) { + id = "ae_cm_" + _via_metadata_being_updated + "_" + row_id; + } + if (is_example) { + id = "ae_example_" + _via_metadata_being_updated + "_" + row_id; + } + let row = $("<tr>", { id: id, class: "align-middle" }); + if ( + _via_metadata_being_updated === "region" && + _via_display_area_content_name === VIA_DISPLAY_AREA_CONTENT_NAME.IMAGE && settings.attributeShowScoreColor + ) { + if (_via_img_metadata[_via_image_id].regions[row_id] !== undefined) { + if ( + _via_img_metadata[_via_image_id].regions[row_id].hasOwnProperty( + "score" + ) + ) { + let score = _via_img_metadata[_via_image_id].regions[row_id].score; + if (score > settings.scoreThreshold) { + row.css("background-color", settings.scoreHigherColor); + } else if (score < 0) { + row.removeAttr("style"); + } else { + row.css("background-color", settings.scoreLowerColor); + } + } + } + } + + if (_via_metadata_being_updated === "region") { + let rid = $("<th>", { scope: "row" }); + + switch (_via_display_area_content_name) { + case VIA_DISPLAY_AREA_CONTENT_NAME.IMAGE_GRID: + rid.html( + "Grouped regions in " + + _via_image_grid_selected_img_index_list.length + + " files" + ); + break; + case VIA_DISPLAY_AREA_CONTENT_NAME.IMAGE: + rid.html(row_id + 1); + rid.attr("id", "0__" + row_id); + rid.attr("onclick", "sidebar.annotation_editor_on_metadata_focus(this)"); + //problem + break; + default: + rid.html(row_id + 1); + rid.attr("id", "0__" + row_id); + break; + } + row.append(rid); + } + + if (_via_metadata_being_updated === "file") { + let rid = $("<th>", { scope: "row" }); + switch (_via_display_area_content_name) { + case VIA_DISPLAY_AREA_CONTENT_NAME.IMAGE_GRID: + rid.html( + "Group of " + + _via_image_grid_selected_img_index_list.length + + " files" + ); + break; + case VIA_DISPLAY_AREA_CONTENT_NAME.IMAGE: + rid.html(_via_image_filename_list[_via_image_index]); + break; + + default: + rid.html("file_example.png"); + break; + } + + row.append(rid); + } + + let attr_id; + for (attr_id in project.attributes[_via_metadata_being_updated]) { + let col = $("<td>"); + + let attr_type = + project.attributes[_via_metadata_being_updated][attr_id].type; + let attr_desc = ""; + if (project.attributes[_via_metadata_being_updated][attr_id].hasOwnProperty("desc")) { + attr_desc = project.attributes[_via_metadata_being_updated][attr_id].desc; + } + + let attr_html_id = attr_id + "__" + row_id; + + let attr_value = ""; + let attr_placeholder = ""; + if ( + _via_display_area_content_name === VIA_DISPLAY_AREA_CONTENT_NAME.IMAGE + ) { + switch (_via_metadata_being_updated) { + case "region": + if ( + _via_img_metadata[_via_image_id].regions[ + row_id + ].region_attributes.hasOwnProperty(attr_id) + ) { + attr_value = + _via_img_metadata[_via_image_id].regions[row_id] + .region_attributes[attr_id]; + } else { + attr_placeholder = "not defined yet!"; + } + break; + case "file": + if ( + _via_img_metadata[_via_image_id].file_attributes.hasOwnProperty( + attr_id + ) + ) { + attr_value = + _via_img_metadata[_via_image_id].file_attributes[attr_id]; + } else { + attr_placeholder = "not defined yet!"; + } + break; + } + } + + if ( + _via_display_area_content_name === + VIA_DISPLAY_AREA_CONTENT_NAME.IMAGE_GRID + ) { + let attr_metadata_stat; + switch (_via_metadata_being_updated) { + case "region": + attr_metadata_stat = this._via_get_region_metadata_stat( + _via_image_grid_selected_img_index_list, + attr_id + ); + break; + case "file": + attr_metadata_stat = this._via_get_file_metadata_stat( + _via_image_grid_selected_img_index_list, + attr_id + ); + break; + } + + switch (attr_type) { + case "text": + if (attr_metadata_stat.hasOwnProperty(attr_id)) { + let attr_value_set = Object.keys(attr_metadata_stat[attr_id]); + if (attr_value_set.includes("undefined")) { + attr_value = ""; + attr_placeholder = + "includes " + + attr_metadata_stat[attr_id]["undefined"] + + " undefined values"; + } else { + switch (attr_value_set.length) { + case 0: + attr_value = ""; + attr_placeholder = "not applicable"; + break; + case 1: + attr_value = attr_value_set[0]; + attr_placeholder = ""; + break; + default: + attr_value = ""; + attr_placeholder = + attr_value_set.length + + " different values: " + + JSON.stringify(attr_value_set).replace(/"/g, "'"); + } + } + } else { + attr_value = ""; + attr_placeholder = "not defined yet!"; + } + break; + + case "radio": // fallback + case "dropdown": // fallback + case "image": // fallback + if (attr_metadata_stat.hasOwnProperty(attr_id)) { + let attr_value_set = Object.keys(attr_metadata_stat[attr_id]); + if (attr_value_set.length === 1) { + attr_value = attr_value_set[0]; + } else { + attr_value = ""; + } + } else { + attr_value = ""; + } + break; + + case "checkbox": + attr_value = {}; + if (attr_metadata_stat.hasOwnProperty(attr_id)) { + let attr_value_set = Object.keys(attr_metadata_stat[attr_id]); + let same_count = true; + let i, n; + let attr_value_curr, attr_value_next; + n = attr_value_set.length; + for (i = 0; i < n - 1; ++i) { + attr_value_curr = attr_value_set[i]; + attr_value_next = attr_value_set[i + 1]; + + if ( + attr_metadata_stat[attr_id][attr_value_curr] !== + attr_metadata_stat[attr_id][attr_value_next] + ) { + same_count = false; + break; + } + } + if (same_count) { + let attr_value_i; + for (attr_value_i in attr_metadata_stat[attr_id]) { + attr_value[attr_value_i] = true; + } + } + } + break; + } + } + + switch (attr_type) { + case "text": + col.attr("id", attr_html_id); + col.html( + "<textarea " + + 'title="' + + attr_desc + + '" ' + + 'placeholder="' + + attr_placeholder + + '" ' + + 'onchange="' + + "sidebar.annotation_editor_on_metadata_update(this); return false;" + + '" ' + + 'onfocus="' + + "sidebar.annotation_editor_on_metadata_focus(this); return false;" + + '">' + + attr_value + + "</textarea>" + ); + if (!is_example) { + col.attr("id", attr_html_id); + } + + break; + case "checkbox": { + let options = + project.attributes[_via_metadata_being_updated][attr_id].options; + let option_id; + for (option_id in options) { + let option_html_id = attr_html_id + "__" + option_id; + + let option = $("<input>", { + value: option_id, + class: "form-check-input", + type: "checkbox", + }); + if (!is_example) { + option.attr("id", option_html_id); + option.attr( + "onfocus", + "sidebar.annotation_editor_on_metadata_focus(this); return false;" + ); + option.attr( + "onchange", + "sidebar.annotation_editor_on_metadata_update(this); return false;" + ); + } + + let option_desc = + project.attributes[_via_metadata_being_updated][attr_id].options[ + option_id + ]; + if (option_desc === "" || typeof option_desc === "undefined") { + // option description is optional, use option_id when description is not present + option_desc = option_id; + } + + // set the value of options based on the user annotations + if (typeof attr_value !== "undefined") { + if (attr_value.hasOwnProperty(option_id)) { + option.prop("checked", attr_value[option_id]); + } + } + + let label = $("<label>"); + label.attr("for", option_html_id); + label.html(option_desc); + + let container = $("<div>", { class: "row" }); + let col_ = $("<div>", { class: "col" }); + col_.append(option); + col_.append(label); + container.append(col_); + col.append(container); + } + break; + } + case "radio": { + let option_id; + for (option_id in project.attributes[_via_metadata_being_updated][ + attr_id + ].options) { + let option_html_id = attr_html_id + "__" + option_id; + let option = $("<input>", { + type: "radio", + name: attr_html_id, + class: "form-check-input", + value: option_id, + }); + if (!is_example) { + option.attr("id", option_html_id); + option.attr( + "onchange", + "sidebar.annotation_editor_on_metadata_update(this); return false;" + ); + option.attr( + "oncfocus", + "sidebar.annotation_editor_on_metadata_focus(this); return false;" + ); + } + + let option_desc = + project.attributes[_via_metadata_being_updated][attr_id].options[ + option_id + ]; + if (option_desc === "" || typeof option_desc === "undefined") { + // option description is optional, use option_id when description is not present + option_desc = option_id; + } + + if (attr_value === option_id) { + option.prop("checked", true) + } + + let label = $("<label>", { + for: option_html_id, + html: option_desc, + }); + + let container = $("<div>", { class: "row" }); + let col_ = $("<div>", { class: "col" }); + col_.append(option); + col_.append(label); + container.append(col_); + col.append(container); + } + break; + } + case "image": { + let option_id; + let option_count = 0; + for (option_id in project.attributes[_via_metadata_being_updated][ + attr_id + ].options) { + option_count = option_count + 1; + } + let img_options = $("<div>", { class: "image_options" }); + //problem bootstrap + col.append(img_options); + + for (option_id in project.attributes[_via_metadata_being_updated][ + attr_id + ].options) { + let option_html_id = attr_html_id + "__" + option_id; + let option = $("<input>", { + type: "radio", + name: attr_html_id, + class: "form-check-input", + value: option_id, + }); + if (!is_example) { + option.attr("id", option_html_id); + option.attr( + "onchange", + "sidebar.annotation_editor_on_metadata_update(this); return false;" + ); + option.attr( + "onfocus", + "sidebar.annotation_editor_on_metadata_focus(this); return false;" + ); + } + + let option_desc = + project.attributes[_via_metadata_being_updated][attr_id].options[ + option_id + ]; + if (option_desc === "" || typeof option_desc === "undefined") { + // option description is optional, use option_id when description is not present + option_desc = option_id; + } + + if (attr_value === option_id) { + option.prop("checked", true); + } + + let label = $("<label>", { for: option_html_id }); + label.html( + '<img src="' + option_desc + '" alt=""><p>' + option_id + "</p>" + ); + + let container = $("<div>", { class: "row" }); + let col_ = $("<div>", { class: "col" }); + col_.append(option); + col_.append(label); + container.append(col_); + img_options.append(container); + } + break; + } + case "dropdown": { + let sel = $("<select>"); + if (!is_example) { + sel.attr("id", attr_html_id); + sel.attr( + "onfocus", + "sidebar.annotation_editor_on_metadata_focus(this); return false;" + ); + sel.attr( + "onchange", + "sidebar.annotation_editor_on_metadata_update(this); return false;" + ); + } + sel.addClass("form-select form-select-sm"); + //problem + let option_id; + let option_selected = false; + for (option_id in project.attributes[_via_metadata_being_updated][ + attr_id + ].options) { + let option = $("<option>", { value: option_id }); + + let option_desc = + project.attributes[_via_metadata_being_updated][attr_id].options[ + option_id + ]; + if (option_desc === "" || typeof option_desc === "undefined") { + // option description is optional, use option_id when description is not present + option_desc = option_id; + } + + if (option_id === attr_value) { + option.attr("selected", "selected"); + option_selected = true; + } + option.html(option_desc); + sel.append(option); + } + + if (!option_selected) { + sel.selectedIndex = -1; //MODSote + } + col.append(sel); + break; + } + } + row.append(col); + } + try { + if (is_example) { + let lock = $("<td>").html($('<input>', { type: 'checkbox', class: 'form-check-input' })); + row.append(lock); + if (settings.attributeShowScore) { + let score = $("<td>").html("n/a"); + row.append(score); + } + if (settings.attributeShowPixelArea) { + let actAreaHtml = $("<td>").html("100"); + row.append(actAreaHtml); + } + return row; + } + if ( + _via_metadata_being_updated === "region" && + _via_img_metadata[_via_image_id].regions[row_id] !== undefined + ) { + let lock = $("<td>"); + + let id_lock = "lock_" + row_id; + if (is_context_menu) { + id_lock = "lock_" + row_id + "_context"; + } + let checkbox = $("<input>", { + type: "checkbox", + class: "form-check-input", + }); + checkbox.attr("id", id_lock); + checkbox.attr("onchange", "drawing.lockRegionHandler(this); return false;"); + lock.append(checkbox); + row.append(lock); + let act_region = _via_img_metadata[_via_image_id].regions[row_id]; + if (settings.attributeShowScore) { + let score = $("<td>").html("n/a"); + if (act_region.hasOwnProperty("score") && !isNaN(act_region.score)) { + score.html(Math.round(act_region.score * 100) / 100); + } + row.append(score); + } + if (settings.attributeShowPixelArea) { + let actArea; + if (act_region.shape_attributes.name === "polygon") { + actArea = plugin.calcPolygonArea( + act_region.shape_attributes.all_points_x, + act_region.shape_attributes.all_points_y + ); + } else { + actArea = + '<svg width="15" height="15" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M8.46457 14.1213C8.07404 14.5118 8.07404 15.145 8.46457 15.5355C8.85509 15.926 9.48825 15.926 9.87878 15.5355L15.5356 9.87862C15.9262 9.4881 15.9262 8.85493 15.5356 8.46441C15.1451 8.07388 14.5119 8.07388 14.1214 8.46441L8.46457 14.1213Z" fill="currentColor" /><path fill-rule="evenodd" clip-rule="evenodd" d="M6.34315 17.6569C9.46734 20.781 14.5327 20.781 17.6569 17.6569C20.781 14.5327 20.781 9.46734 17.6569 6.34315C14.5327 3.21895 9.46734 3.21895 6.34315 6.34315C3.21895 9.46734 3.21895 14.5327 6.34315 17.6569ZM16.2426 16.2426C13.8995 18.5858 10.1005 18.5858 7.75736 16.2426C5.41421 13.8995 5.41421 10.1005 7.75736 7.75736C10.1005 5.41421 13.8995 5.41421 16.2426 7.75736C18.5858 10.1005 18.5858 13.8995 16.2426 16.2426Z" fill="currentColor" /></svg>'; + } + let actAreaHtml = $("<td>").html(actArea); + row.append(actAreaHtml); + } + } + drawing.updateCheckedLockHtml(); + } catch (e) { + console.log("Ok, no problem, just no lock for this region :D"); + } + return row; + } + + annotation_editor_scroll_to_row(row_id) { + if (this.is_annotation_editor_visible() && settings.is_scrolling) { + if (_via_metadata_being_updated === "file") { + row_id = 0; + } + let row_html_id = "ae_" + _via_metadata_being_updated + "_" + row_id; + let row = document.getElementById(row_html_id); + row.scrollIntoView(false); + //problem + } + } + + annotation_editor_highlight_row(row_id) { + if (this.is_annotation_editor_visible()) { + let row_html_id = "ae_" + _via_metadata_being_updated + "_" + row_id; + $("#" + row_html_id).addClass("table-active"); + } + } + + annotation_editor_clear_row_highlight() { + if (this.is_annotation_editor_visible()) { + let ae = $("#annotation_editor_content"); + $(".table-active", ae).removeClass("table-active"); + } + } + + annotation_editor_extract_html_id_components(html_id) { + // html_id : attribute_name__row-id__option_id + let parts = html_id.split("__"); + let parsed_id = {}; + switch (parts.length) { + case 3: + // html_id : attribute-id__row-id__option_id + parsed_id.attr_id = parts[0]; + parsed_id.row_id = parts[1]; + parsed_id.option_id = parts[2]; + break; + case 2: + // html_id : attribute-id__row-id + parsed_id.attr_id = parts[0]; + parsed_id.row_id = parts[1]; + break; + default: + } + return parsed_id; + } + + _via_get_file_metadata_stat(img_index_list, attr_id) { + let stat = {}; + stat[attr_id] = {}; + let i, n, img_id, img_index, value; + n = img_index_list.length; + for (i = 0; i < n; ++i) { + img_index = img_index_list[i]; + img_id = _via_image_id_list[img_index]; + if (_via_img_metadata[img_id].file_attributes.hasOwnProperty(attr_id)) { + value = _via_img_metadata[img_id].file_attributes[attr_id]; + if (typeof value === "object") { + // checkbox has multiple values and hence is object + let key; + for (key in value) { + if (stat[attr_id].hasOwnProperty(key)) { + stat[attr_id][key] += 1; + } else { + stat[attr_id][key] = 1; + } + } + } else { + if (stat[attr_id].hasOwnProperty(value)) { + stat[attr_id][value] += 1; + } else { + stat[attr_id][value] = 1; + } + } + } + } + return stat; + } + + _via_get_region_metadata_stat(img_index_list, attr_id) { + let stat = {}; + stat[attr_id] = {}; + let i, n, img_id, img_index, value; + let j, m; + n = img_index_list.length; + for (i = 0; i < n; ++i) { + img_index = img_index_list[i]; + img_id = _via_image_id_list[img_index]; + m = _via_img_metadata[img_id].regions.length; + for (j = 0; j < m; ++j) { + if ( + !image_grid_is_region_in_current_group( + _via_img_metadata[img_id].regions[j].region_attributes + ) + ) { + // skip region not in current group + continue; + } + + value = _via_img_metadata[img_id].regions[j].region_attributes[attr_id]; + if (typeof value === "object") { + // checkbox has multiple values and hence is object + let key; + for (key in value) { + if (stat[attr_id].hasOwnProperty(key)) { + stat[attr_id][key] += 1; + } else { + stat[attr_id][key] = 1; + } + } + } else { + if (stat[attr_id].hasOwnProperty(value)) { + stat[attr_id][value] += 1; + } else { + stat[attr_id][value] = 1; + } + } + } + } + return stat; + } + + annotation_editor_on_metadata_focus(p) { + if ( + _via_annotation_editor_mode === VIA_ANNOTATION_EDITOR_MODE.ALL_REGIONS + ) { + let pid = this.annotation_editor_extract_html_id_components(p.id); + let region_id = pid.row_id; + // clear existing highlights (if any) + toggle_all_regions_selection(false); + this.annotation_editor_clear_row_highlight(); + // set new selection highlights + set_region_select_state(region_id, true); + this.annotation_editor_highlight_row(region_id); + drawing.setIsRegionSelected(true); + drawing.setUserSelRegionId(region_id); + drawing.redrawRegCanvas(); + } + } + + annotation_editor_on_metadata_update(p) { + let pid = this.annotation_editor_extract_html_id_components(p.id); + let img_index_list = [_via_image_index]; + let region_id = pid.row_id; + if ( + _via_display_area_content_name === + VIA_DISPLAY_AREA_CONTENT_NAME.IMAGE_GRID + ) { + img_index_list = _via_image_grid_selected_img_index_list.slice(0); + region_id = -1; // this flag denotes that we want to update all regions + } + + if (_via_metadata_being_updated === "file") { + let update_count = this.annotation_editor_update_file_metadata( + img_index_list, + pid.attr_id, + p.value, + p.checked + ); + this.annotation_editor_on_metadata_update_done( + "file", + pid.attr_id, + update_count + ); + return; + } + + if (_via_metadata_being_updated === "region") { + let update_count = this.annotation_editor_update_region_metadata( + img_index_list, + region_id, + pid.attr_id, + p.value, + p.checked + ); + this.annotation_editor_on_metadata_update_done( + "region", + pid.attr_id, + update_count + ); + } + } + + annotation_editor_on_metadata_update_done(type, attr_id, update_count) { + show_message( + "Updated " + type + " attributes of " + update_count + " " + type + "s" + ); + // check if the updated attribute is one of the group variables + let i, n; //problem + n = _via_image_grid_group_var.length; + let clear_all_group = false; + for (i = 0; i < n; ++i) { + if ( + _via_image_grid_group_var[i].type === type && + _via_image_grid_group_var[i].name === attr_id + ) { + clear_all_group = true; + break; + } + } + drawing.regionsGroupColorInit(); + drawing.redrawRegCanvas(); + + // @todo: it is wasteful to cancel the full set of groups. + // we should only cancel the groups that are affected by this update. + if ( + _via_display_area_content_name === + VIA_DISPLAY_AREA_CONTENT_NAME.IMAGE_GRID + ) { + if (clear_all_group) { + image_grid_show_all_project_images(); + } + } + } + + annotation_editor_update_file_metadata( + img_index_list, + attr_id, + new_value, + new_checked + ) { + try { + let i, n, img_id, img_index; + n = img_index_list.length; + let update_count = 0; + for (i = 0; i < n; ++i) { + img_index = img_index_list[i]; + img_id = _via_image_id_list[img_index]; + + switch (project.attributes.file[attr_id].type) { + case "text": // fallback + case "radio": // fallback + case "dropdown": // fallback + case "image": + _via_img_metadata[img_id].file_attributes[attr_id] = new_value; + update_count += 1; + break; + + case "checkbox": + let option_id = new_value; + if ( + _via_img_metadata[img_id].file_attributes.hasOwnProperty(attr_id) + ) { + if ( + typeof _via_img_metadata[img_id].file_attributes[attr_id] !== + "object" + ) { + let old_value = + _via_img_metadata[img_id].file_attributes[attr_id]; + _via_img_metadata[img_id].file_attributes[attr_id] = {}; + if ( + Object.keys( + project.attributes.file[attr_id]["options"] + ).includes(old_value) + ) { + // transform existing value as checkbox option + _via_img_metadata[img_id].file_attributes[attr_id] = {}; + _via_img_metadata[img_id].file_attributes[attr_id][ + old_value + ] = true; + } + } + } else { + _via_img_metadata[img_id].file_attributes[attr_id] = {}; + } + if (new_checked) { + _via_img_metadata[img_id].file_attributes[attr_id][ + option_id + ] = true; + } else { + // false option values are not stored + delete _via_img_metadata[img_id].file_attributes[attr_id][ + option_id + ]; + } + update_count += 1; + break; + } + } + return update_count; + } catch (err) { + console.log(err); + Message.show({ + address: "Error", + body: "Problem with update file metadata", + color: "#cc1100", + }); + } + } + + annotation_editor_update_region_metadata( + img_index_list, + region_id, + attr_id, + new_value, + new_checked + ) { + try { + let i, n, img_id, img_index; + n = img_index_list.length; + let update_count = 0; + let j, m; + + if (region_id === -1) { + // update all regions on a file (for image grid view) + for (i = 0; i < n; ++i) { + img_index = img_index_list[i]; + img_id = _via_image_id_list[img_index]; + + m = _via_img_metadata[img_id].regions.length; + for (j = 0; j < m; ++j) { + if ( + !image_grid_is_region_in_current_group( + _via_img_metadata[img_id].regions[j].region_attributes + ) + ) { + continue; + } + + switch (project.attributes.region[attr_id].type) { + case "text": // fallback + case "dropdown": // fallback + case "radio": // fallback + case "image": + _via_img_metadata[img_id].regions[j].region_attributes[ + attr_id + ] = new_value; + update_count += 1; + break; + case "checkbox": + let option_id = new_value; + if ( + _via_img_metadata[img_id].regions[ + j + ].region_attributes.hasOwnProperty(attr_id) + ) { + if ( + typeof _via_img_metadata[img_id].regions[j] + .region_attributes[attr_id] !== "object" + ) { + let old_value = + _via_img_metadata[img_id].regions[j].region_attributes[ + attr_id + ]; + _via_img_metadata[img_id].regions[j].region_attributes[ + attr_id + ] = {}; + if ( + Object.keys( + project.attributes.region[attr_id]["options"] + ).includes(old_value) + ) { + // transform existing value as checkbox option + _via_img_metadata[img_id].regions[j].region_attributes[ + attr_id + ][old_value] = true; + } + } + } else { + _via_img_metadata[img_id].regions[j].region_attributes[ + attr_id + ] = {}; + } + + if (new_checked) { + _via_img_metadata[img_id].regions[j].region_attributes[ + attr_id + ][option_id] = true; + } else { + // false option values are not stored + delete _via_img_metadata[img_id].regions[j].region_attributes[ + attr_id + ][option_id]; + } + update_count += 1; + break; + } + } + } + } else { + // update a single region in a file (for single image view) + // update all regions on a file (for image grid view) + for (i = 0; i < n; ++i) { + img_index = img_index_list[i]; + img_id = _via_image_id_list[img_index]; + + switch (project.attributes.region[attr_id].type) { + case "text": // fallback + case "dropdown": // fallback + case "radio": // fallback + case "image": + _via_img_metadata[img_id].regions[region_id].region_attributes[ + attr_id + ] = new_value; + update_count += 1; + break; + case "checkbox": + let option_id = new_value; + + if ( + _via_img_metadata[img_id].regions[ + region_id + ].region_attributes.hasOwnProperty(attr_id) + ) { + if ( + typeof _via_img_metadata[img_id].regions[region_id] + .region_attributes[attr_id] !== "object" + ) { + let old_value = + _via_img_metadata[img_id].regions[region_id] + .region_attributes[attr_id]; + _via_img_metadata[img_id].regions[ + region_id + ].region_attributes[attr_id] = {}; + if ( + Object.keys( + project.attributes.region[attr_id]["options"] + ).includes(old_value) + ) { + // transform existing value as checkbox option + _via_img_metadata[img_id].regions[ + region_id + ].region_attributes[attr_id][old_value] = true; + } + } + } else { + _via_img_metadata[img_id].regions[region_id].region_attributes[ + attr_id + ] = {}; + } + + if (new_checked) { + _via_img_metadata[img_id].regions[region_id].region_attributes[ + attr_id + ][option_id] = true; + } else { + // false option values are not stored + delete _via_img_metadata[img_id].regions[region_id] + .region_attributes[attr_id][option_id]; + } + update_count += 1; + break; + } + } + } + return update_count; + } catch (err) { + console.log(err); + Message.show({ + address: "Error", + body: "Problem with update region metadata", + color: "#cc1100", + }); + } + } + + set_region_annotations_to_default_value(rid) { + let attr_id; + for (attr_id in project.attributes.region) { + let attr_type = project.attributes.region[attr_id].type; + switch (attr_type) { + case "text": + let default_value = project.attributes.region[attr_id].default_value; + if (default_value !== undefined) { + _via_img_metadata[_via_image_id].regions[rid].region_attributes[ + attr_id + ] = default_value; + } + break; + case "image": // fallback + case "dropdown": // fallback + case "radio": { + _via_img_metadata[_via_image_id].regions[rid].region_attributes[ + attr_id + ] = ""; + let default_options = + project.attributes.region[attr_id].default_options; + if (typeof default_options !== "undefined") { + _via_img_metadata[_via_image_id].regions[rid].region_attributes[ + attr_id + ] = Object.keys(default_options)[0]; + } + break; + } + case "checkbox": { + _via_img_metadata[_via_image_id].regions[rid].region_attributes[ + attr_id + ] = {}; + let default_options = + project.attributes.region[attr_id].default_options; + if (default_options !== undefined) { + let option_id; + for (option_id in default_options) { + let default_value = default_options[option_id]; + if (default_value !== undefined) { + _via_img_metadata[_via_image_id].regions[rid].region_attributes[ + attr_id + ][option_id] = default_value; + } + } + } + break; + } + } + } + } + + set_file_annotations_to_default_value(image_id) { + let attr_id; + for (attr_id in project.attributes.file) { + let attr_type = project.attributes.file[attr_id].type; + switch (attr_type) { + case "text": { + _via_img_metadata[image_id].file_attributes[attr_id] = + project.attributes.file[attr_id].default_value; + break; + } + case "image": // fallback + case "dropdown": // fallback + case "radio": { + _via_img_metadata[image_id].file_attributes[attr_id] = ""; + let default_options = + project.attributes.file[attr_id].default_options; + _via_img_metadata[image_id].file_attributes[attr_id] = + Object.keys(default_options)[0]; + break; + } + case "checkbox": { + _via_img_metadata[image_id].file_attributes[attr_id] = {}; + let default_options = + project.attributes.file[attr_id].default_options; + let option_id; + for (option_id in default_options) { + _via_img_metadata[image_id].file_attributes[attr_id][option_id] = + default_options[option_id]; + } + break; + } + } + } + } + + toggleAEMode(disable = false) { + if (_via_metadata_being_updated === "region") { + this.edit_file_metadata_in_annotation_editor(); + $("#sidebar_ToggleAEModes").prop("disabled", disable); + $("#sidebar_ToggleAEModes").html("Files"); + } else { + this.edit_region_metadata_in_annotation_editor(); + if (disable) { + $("#sidebar_ToggleAEModes").prop("disabled", true); + } else { + $("#sidebar_ToggleAEModes").prop("disabled", false); + } + $("#sidebar_ToggleAEModes").html("Regions"); + } + } +} +const sidebar = new Sidebar(); \ No newline at end of file diff --git a/src/js/ui_handler.js b/src/js/ui_handler.js new file mode 100644 index 0000000..bf606c3 --- /dev/null +++ b/src/js/ui_handler.js @@ -0,0 +1,304 @@ +// ui_handler.js +// Description: UI elements event handlers +$("#nav_brand").click(function () { + show_home_panel(); + return false; +}); +$("#nav_home").click(function () { + show_home_panel(); + return false; +}); + +//project dropdown elements +$("#nav_project_load").click(function () { + project.openSelectProjectFile(); + return false; +}); +$("#nav_project_save").click(function () { + project.saveWithConfirm(); + return false; +}); +$("#nav_project_setting").click(function () { + settings.toggleSettings(); + return false; +}); +$("#nav_project_addLocFiles").click(function () { + sel_local_images(); + return false; +}); +$("#nav_project_addFilUrl").click(function () { + project.addUrlFileWithInput(); + return false; +}); +$("#nav_project_addFilAbs").click(function () { + project.addAbsPathFileWithInput(); + return false; +}); +//$('#nav_project_addFilText').click(function(){ sel_local_data_file('files_url'); return false; }); +//$('#nav_project_removeFile').click(function(){ project_file_remove_with_confirm(); return false; }); +//$('#nav_project_importAttributes').click(function(){ sel_local_data_file('attributes'); return false; }); +//$('#nav_project_exportAttributes').click(function(){ project_save_attributes(); return false; }); + +//annotation dropdown elements +$("#nav_annotation_export").click(function () { + fileManager.downloadAllRegionDataModal(); + return false; +}); +$("#nav_annotation_import").click(function () { + fileManager.importAnnotationsFromFile(); + return false; +}); +$("#nav_annotation_prevAnn").click(function () { + show_annotation_data(); + return false; +}); +$("#nav_annotation_downImage").click(function () { + download_as_image(); + return false; +}); +$("#nav_annotation_autoAnn").click(function () { + AutoAnnotator.run_annotation(); + return false; +}); + +//view dropdown elements +$("#nav_view_imgGrid").click(function () { + image_grid_toggle(); + return false; +}); +$("#nav_view_sidebar").click(function () { + leftsidebar_toggle(); + return false; +}); +$("#nav_view_status").click(function () { + toggle_message_visibility(); + return false; +}); +$("#nav_view_regionBound").click(function () { + toggle_region_boundary_visibility(); + return false; +}); +$("#nav_view_regionLabel").click(function () { + toggle_region_id_visibility(); + return false; +}); + +//icons +$("#nav_openProject").click(function () { + project.openSelectProjectFile(); + return false; +}); +$("#nav_saveProject").click(function () { + project.saveWithConfirm(); + return false; +}); +$("#nav_settings").click(function () { + settings.toggleSettings(); + return false; +}); +$("#nav_imgGrid").click(function () { + image_grid_toggle(); + return false; +}); +$("#nav_sidePanel").click(function () { + leftsidebar_toggle(); + return false; +}); +$("#nav_autoAnn").click(function () { + AutoAnnotator.run_annotation(); + return false; +}); +$("#nav_calcAreas").click(function () { + plugin.ExportArea(); + return false; +}); +$("#nav_prev").click(function () { + move_to_prev_image(); + return false; +}); +$("#nav_next").click(function () { + move_to_next_image(); + return false; +}); +$("#nav_undo").click(function () { + undoredo_worker.postMessage({ commands: "undo" }); + return false; +}); +$("#nav_redo").click(function () { + undoredo_worker.postMessage({ commands: "redo" }); + return false; +}); +$("#nav_zoomIn").click(function () { + zoom.zoomIn(); + return false; +}); +$("#nav_zoomOut").click(function () { + zoom.zoomOut(); + return false; +}); +$("#nav_selAllReg").click(function () { + sel_all_regions(); + return false; +}); +$("#nav_copySelReg").click(function () { + copy_sel_regions(); + return false; +}); +$("#nav_pasteReg").click(function () { + paste_sel_regions_in_current_image(); + return false; +}); +$("#nav_delSelReg").click(function () { + del_sel_regions(); + return false; +}); + +// +//Drawing elements +// +$("#shape_drag").click(function () { + select_region_shape("drag"); + return false; +}); +$("#shape_circle").click(function () { + select_region_shape("circle"); + return false; +}); +$("#shape_ellipse").click(function () { + select_region_shape("ellipse"); + return false; +}); +$("#shape_pen").click(function () { + select_region_shape("pen"); + return false; +}); +$("#shape_polygon").click(function () { + select_region_shape("polygon"); + return false; +}); +$("#shape_edit").click(function () { + select_region_shape("edit"); + return false; +}); +$("#shape_remove").click(function () { + select_region_shape("remove"); + return false; +}); +$("#shape_rect").click(function () { + select_region_shape("rect"); + return false; +}); +$("#shape_trim").click(function () { + select_region_shape("trim"); + return false; +}); +$("#image_settings").click(function () { + select_region_shape("img_set"); + return false; +}); + +// +//Sidebar +// +$("#project_name").change(function () { + project.onNameUpdate(this); + return false; +}); +$("#sidebar_addFiles").click(function () { + sel_local_images(); + return false; +}); +$("#sidebar_addUrl").click(function () { + project.addUrlFileWithInput(); + return false; +}); +$("#sidebar_remove").click(function () { + project.fileRemoveWithConfirm(); + return false; +}); +$("#sidebar_ToggleAEModes").click(function () { + sidebar.toggleAEMode(); + return false; +}); +$("#sidebar_UpdateSlices").click(function () { + plugin.updateSliceRegion(); + return false; +}); + +// +//Image manipulation +// +$("#sidebar_reset").click(function () { + image.backToSidebar(); + select_region_shape("edit"); + return false; +}); +$("#image_reset").click(function () { + image.resetFilters(); + return false; +}); +$("#sidebar_onImageEditor").click(function () { + sidebar.annotation_editor_toggle_on_image_editor(); + return false; +}); + + +$("#start_selectImages").click(function () { + sel_local_images(); + return false; +}); + +$("#start_addUrlImages").click(function () { + project.addUrlFileWithInput(); + return false; +}); + +$("#start_settings").click(function () { + settings.toggleSettings(); + return false; +}); + +$("#start_saveProject").click(function () { + project.saveWithConfirm(); + return false; +}); + +$("#start_loadProject").click(function () { + project.openSelectProjectFile(); + return false; +}); + +$("#start_gettingStarted").click(function () { + set_display_area_content(VIA_DISPLAY_AREA_CONTENT_NAME.PAGE_GETTING_STARTED); + return false; +}); + +$("#404_settings").click(function () { + settings.toggleSettings(); + return false; +}); + +$("#404_selectImages").click(function () { + sel_local_images(); + return false; +}); + +$("#404_selectFolder").click(function () { + project.loadAllImages(); + return false; +}); + +$("#404_gettingStarted").click(function () { + set_display_area_content(VIA_DISPLAY_AREA_CONTENT_NAME.PAGE_GETTING_STARTED); + return false; +}); + +$("#nav_help_getStart").click(function () { + set_display_area_content(VIA_DISPLAY_AREA_CONTENT_NAME.PAGE_GETTING_STARTED); + return false; +}); + +$("#image_grid_toolbar_group_by_select").change(function () { + image_grid_toolbar_onchange_group_by_select(this); + return false; +}); \ No newline at end of file diff --git a/src/js/undo_redo.js b/src/js/undo_redo.js new file mode 100644 index 0000000..115f6cb --- /dev/null +++ b/src/js/undo_redo.js @@ -0,0 +1,362 @@ +// undo_redo.js +// Description: This file contains the implementation of the undo/redo feature. +// The undo/redo feature is implemented using the Command pattern. +// The undo/redo feature is implemented in a separate worker thread. +function undoRedoWorker() { + const INCREMENT = "INCREMENT"; + const DECREMENT = "DECREMENT"; + + let region_metadata = {}; + + /** + * Named counter, to make it reusable for other undo/redo implementations + * @param name - name of the counter + * @returns {{name, count: number}} - counter number + */ + const createNamedCounter = (name) => { + return { + name, + count: 0, + }; + }; + + /** + * It defines the incrementation of the named counter + * @param counter - the named counter + * @returns {{undo(): void, execute(): void}} - the incrementation command + */ + const createIncrementCommand = (counter) => { + const previousCount = counter.count; + + return { + execute() { + counter.count += 1; + }, + undo() { + counter.count = previousCount; + }, + }; + }; + + /** + * It defines the decrement of the named counter + * @param counter - the named counter + * @returns {{undo(): void, execute(): void}} - the decrement command + */ + const createDecrementCommand = (counter) => { + const previousCount = counter.count; + + return { + execute() { + counter.count -= 1; + }, + undo() { + counter.count = previousCount; + }, + }; + }; + + const commands = { + [INCREMENT]: createIncrementCommand, + [DECREMENT]: createDecrementCommand, + }; + + /** + * It defines the undo/redo commands + * @param target - the target of the command + * @returns {{undo(): void, redo(): void, doCommand(*): void}} - the undo/redo command + */ + const createCommandManager = (target) => { + let history = [null]; + let position = 0; + + return { + doCommand(commandType) { + if (position < history.length - 1) { + history = history.slice(0, position + 1); + } + + if (commands[commandType]) { + const concreteCommand = commands[commandType](target); + history.push(concreteCommand); + position += 1; + + concreteCommand.execute(); + } + }, + + undo() { + if (position > 0) { + history[position].undo(); + position -= 1; + } + updateState(); + }, + + redo() { + if (position < history.length - 1) { + position += 1; + history[position].execute(); + } + updateState(); + }, + }; + }; + + const counter = createNamedCounter("via_metadata"); + const commandManager = createCommandManager(counter); + + let regionMementos = []; + + /** + * It adds the region's metadata to the regionMementos array + */ + function addState() { + if (counter.count !== regionMementos.length) { + if (counter.count > regionMementos.length) { + regionMementos.splice(0, regionMementos.length); + counter.count = regionMementos.length; + } else { + regionMementos.splice(counter.count, regionMementos.length); + counter.count = regionMementos.length; + } + } + + let memento = JSON.stringify(region_metadata); + for (const element of regionMementos) { + if (element === memento) { + return; + } + } + regionMementos.push(memento); + if (regionMementos.length > 1) { + self.postMessage({ undo_enabled: true, redo_enabled: false }); + } + commandManager.doCommand(INCREMENT); + } + + /** + * It updates the state of the valid data, when the undo/redo is called + */ + function updateState() { + if (counter.count < regionMementos.length) { + region_metadata = JSON.parse(regionMementos[counter.count]); + if (counter.count === 0 && regionMementos.length > 1) { + self.postMessage({ + undo_enabled: false, + redo_enabled: true, + region_metadata: region_metadata, + is_update: true, + }); + return; + } + if ( + counter.count === regionMementos.length - 1 && + regionMementos.length > 1 + ) { + self.postMessage({ + undo_enabled: true, + redo_enabled: false, + region_metadata: region_metadata, + is_update: true, + }); + return; + } + if (counter.count === 0 && regionMementos.length === 1) { + self.postMessage({ + undo_enabled: false, + redo_enabled: false, + region_metadata: region_metadata, + is_update: true, + }); + return; + } + self.postMessage({ + undo_enabled: true, + redo_enabled: true, + region_metadata: region_metadata, + is_update: true, + }); + } else { + commandManager.undo(); + } + } + + function resetState() { + regionMementos = []; + counter.count = 0; + self.postMessage({ undo_enabled: false, redo_enabled: false }); + } + + /** + * It handles the undo/redo commands from the main thread + * @param e - the event + */ + self.onmessage = function (e) { + switch (e.data.commands) { + case "undo": + commandManager.undo(); + region_metadata = e.data.region_metadata; + break; + case "redo": + commandManager.redo(); + region_metadata = e.data.region_metadata; + break; + case "add": + region_metadata = e.data.region_metadata; + addState(); + break; + case "reset": + resetState(); + break; + } + }; +} + +const undoredo_worker = new Worker( + URL.createObjectURL( + new Blob(["(" + undoRedoWorker.toString() + ")()"], { + type: "text/javascript", + }) + ) +); + +// +// Data structure [2] to store metadata about file and regions +// + +/** +* Element of the region data structure +* The two element object is used to store the region's and its children's metadata +* Two proxies are used to handle the changes of the region's metadata +* @constructor - creates a new region element +*/ +let File_Region = function (shape_attributes = {}, region_attributes = {}) { + this.shape_attributes = new Proxy(shape_attributes, validator); // region shape attributes + this.region_attributes = new Proxy(region_attributes, validator); // region attributes +}; + +/** +* It detects the data structures inside the object +* It needs to make new proxies for these data structures +* @param obj - the object to be validated +* @returns {string} - the type of the object +*/ +function trueTypeOf(obj) { + return Object.prototype.toString.call(obj).slice(8, -1).toLowerCase(); +} + +/** +* It validates the changes of the region's metadata +* Set function is called when a new property is added to the object, or change an existing property +* @type {{set: (function(*, *, *): boolean), get: ((function(*, *): (boolean|*))|*), deleteProperty: (function(*, *): boolean)}} +*/ +let validator = { + get: function (obj, prop) { + // If the property is "_isProxy", item is already being intercepted by this proxy handler + // return true + if (prop === "_isProxy") { + return true; + } + + // If the property is an array or object and not already a proxy, make it one + if ( + ["object", "array"].includes(trueTypeOf(obj[prop])) && + !obj[prop]._isProxy + ) { + obj[prop] = new Proxy(obj[prop], validator); + } + + return obj[prop]; + }, + set: function (obj, prop, value) { + obj[prop] = value; + if (undo_redo_enabled.enabled) { + enableUndoRedo(); + setTimeout(function () { + enableUndoRedo(); + return true; + }, 250); + } + return true; + }, + + deleteProperty: function (obj, prop) { + delete obj[prop]; + return true; + }, +}; + +function enableUndoRedo() { + undo_redo_enabled.enabled = !undo_redo_enabled.enabled; +} + +/** +* It is a helper to filter the changes of the region's metadata +* @type {{set: validator2.set, get: (function(*, *): *)}} +*/ +const validator2 = { + get: function (obj, prop) { + return obj[prop]; + }, + set: function (obj, prop, value) { + obj[prop] = value; + if (undo_redo_enabled.enabled) { + if (now_update === false) { + undoredo_worker.postMessage({ + commands: "add", + region_metadata: JSON.stringify( + _via_img_metadata[_via_image_id].regions + ), + }); + } else { + now_update = false; + } + } + return true; + }, +}; + +const undo_redo_enabled = new Proxy({ enabled: true }, validator2); +let now_update = false; + +/** +* This is an event listener for the undo redo worker thread +* It receives the changes of the region's metadata in the worker thread +*/ +undoredo_worker.addEventListener( + "message", + function handleMessageFromWorker(msg) { + if (msg.data.undo_enabled) { + $("#nav_undo").removeClass("disabled"); + } else { + $("#nav_undo").addClass("disabled"); + } + if (msg.data.redo_enabled) { + $("#nav_redo").removeClass("disabled"); + } else { + $("#nav_redo").addClass("disabled"); + } + + if (!msg.data.hasOwnProperty("region_metadata")) { + return; + } + + undo_redo_enabled.enabled = false; + now_update = true; + let tmp_metadata = JSON.parse(msg.data.region_metadata); + _via_img_metadata[_via_image_id].regions = []; + for (const element of tmp_metadata) { + _via_img_metadata[_via_image_id].regions.push( + new File_Region(element.shape_attributes, element.region_attributes) + ); + } + _via_is_all_region_selected = false; + _via_load_canvas_regions(); + drawing.redrawRegCanvas(); + drawing.setIsRegionSelected(false); + drawing.setUserSelRegionId(-1); + _via_reg_canvas.focus(); + undo_redo_enabled.enabled = true; + } +); \ No newline at end of file diff --git a/src/js/via.js b/src/js/via.js new file mode 100644 index 0000000..874254d --- /dev/null +++ b/src/js/via.js @@ -0,0 +1,6430 @@ +/* + VGG Image Annotator (via) + www.robots.ox.ac.uk/~vgg/software/via/ + + Copyright (c) 2016-2019, Abhishek Dutta, Visual Geometry Group, Oxford University and VIA Contributors. + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. +*/ + +/* + Links: + - https://gitlab.com/vgg/via/blob/master/Contributors.md : list of developers who have contributed code to the VIA project. + - https://gitlab.com/vgg/via/blob/master/CodeDoc.md : source code documentation + - https://gitlab.com/vgg/via/blob/master/CONTRIBUTING.md : guide for contributors + + This source code can be grouped into the following categories: + - Data structure for annotations + - Initialization routine + - Handlers for top navigation bar + - Local file uploaders + - Data Importer + - Data Exporter + - Maintainers of user interface + - Image click handlers + - Canvas update routines + - Region collision routines + - Shortcut key handlers + - Persistence of annotation data in browser cache (i.e. localStorage) + - Handlers for attributes input panel (spreadsheet like user input panel) +*/ + +// +// Data structure [1] to store metadata about file and regions +// + +function file_metadata(filename, size) { + this.filename = filename; + this.size = size; // file size in bytes + this.regions = []; // array of File_Region() + this.file_attributes = { + ID: { type: "text", description: "", default_value: "" }, + Treatment: { type: "text", description: "", default_value: "" }, + }; + this.fileAttributes = new Map( + [ + ["ID", { type: "text", description: "", default_value: "" }], + ["Treatment", { type: "text", description: "", default_value: "" }], + ] + ); + this.lockedRegions = new Set(); + this.groupedRegions = new Map(); + this.autoAnnotated = false; +} + +// +// Initialization routine +// +function _via_init() { + console.log(SAA_NAME); + show_message( + SAA_NAME + " (" + SAA_SHORT_NAME + ") version " + SAA_VERSION + ". Ready !" + ); + + if (_via_is_debug_mode) { + document.getElementById("ui_top_panel").innerHTML += + "<span>DEBUG MODE</span>"; + } + + // initialize default project + project.initDefaultProject() + + // initialize region canvas 2D context + //_via_init_reg_canvas_context(); + + // initialize user input handlers (for both window and via_reg_canvas) + // handles drawing of regions by user over the image + _via_init_keyboard_handlers(); + + // initialize image grid + image_grid_init(); + + //show_annotator_editor(); + show_single_image_view(); + update_attributes_update_panel(); + attribute_update_panel_set_active_button(); + + sidebar.annotation_editor_set_active_button(); + sidebar.annotation_editor_toggle_all_regions_editor(); + + // run attached sub-modules (if any) + // e.g. demo modules + if (typeof _via_load_submodules === "function") { + console.log("Loading VIA submodule"); + setTimeout(async function () { + await _via_load_submodules(); + }, 100); + } +} + +function _via_init_keyboard_handlers() { + window.addEventListener("keydown", _via_window_keydown_handler, false); + _via_reg_canvas.addEventListener( + "keydown", + _via_reg_canvas_keydown_handler, + false + ); + _via_reg_canvas.addEventListener( + "keyup", + _via_reg_canvas_keyup_handler, + false + ); +} + +// +// Download image with annotations +// + +function download_as_image() { + if ( + _via_display_area_content_name !== VIA_DISPLAY_AREA_CONTENT_NAME["IMAGE"] + ) { + show_message( + "This functionality is only available in single image view mode" + ); + } else { + var c = document.createElement("canvas"); + + // ensures that downloaded image is scaled at current zoom level + c.width = _via_reg_canvas.width; + c.height = _via_reg_canvas.height; + + var ct = c.getContext("2d"); + // draw current image + ct.drawImage( + _via_current_image, + 0, + 0, + _via_reg_canvas.width, + _via_reg_canvas.height + ); + // draw current regions + ct.drawImage(_via_reg_canvas, 0, 0); + + var cur_img_mime = "image/jpeg"; + if (_via_current_image.src.startsWith("data:")) { + var c1 = _via_current_image.src.indexOf(":", 0); + var c2 = _via_current_image.src.indexOf(";", c1); + cur_img_mime = _via_current_image.src.substring(c1 + 1, c2); + } + + // extract image data from canvas + var saved_img = c.toDataURL(cur_img_mime); + saved_img.replace(cur_img_mime, "image/octet-stream"); + + // simulate user click to trigger download of image + var a = document.createElement("a"); + a.href = saved_img; + a.target = "_blank"; + a.download = _via_current_image_filename; + + // simulate a mouse click event + var event = new MouseEvent("click", { + view: window, + bubbles: true, + cancelable: true, + }); + + a.dispatchEvent(event); + } +} + +async function download_grid_selected_images() { + //add annotations to image as well + for (const element of _via_image_grid_selected_img_index_list) { + let image_index = element; + let img_id = _via_image_id_list[image_index]; + let img_filename = _via_img_metadata[img_id].filename; + let imageElement = $("#bim" + image_index)[0]; + if (imageElement === undefined) { + await buffer.addImageToBuffer(image_index); + imageElement = $("#bim" + image_index)[0]; + } + let canvas = document.createElement("canvas"); + canvas.width = imageElement.naturalWidth; + canvas.height = imageElement.naturalHeight; + let ctx = canvas.getContext("2d"); + ctx.drawImage(imageElement, 0, 0); + let i; + let n = _via_img_metadata[img_id].regions.length; + for (i = 0; i < n; ++i) { + if ( + !image_grid_is_region_in_current_group( + _via_img_metadata[img_id].regions[i].region_attributes + ) + ) { + // skip drawing this region which is not in current group + continue; + } + + let r = _via_img_metadata[img_id].regions[i].shape_attributes; + let dimg; // region coordinates in original image space + switch (r.name) { + case VIA_REGION_SHAPE.RECT: + dimg = [r["x"], r["y"], r["x"] + r["width"], r["y"] + r["height"]]; + break; + case VIA_REGION_SHAPE.CIRCLE: + dimg = [r["cx"], r["cy"], r["cx"] + r["r"], r["cy"] + r["r"]]; + break; + case VIA_REGION_SHAPE.ELLIPSE: + dimg = [r["cx"], r["cy"], r["cx"] + r["rx"], r["cy"] + r["ry"]]; + break; + case VIA_REGION_SHAPE.POLYLINE: // handled by POLYGON + case VIA_REGION_SHAPE.POLYGON: + let j; + dimg = []; + for (j = 0; j < r["all_points_x"].length; ++j) { + dimg.push(r["all_points_x"][j]); + dimg.push(r["all_points_y"][j]); + } + break; + case VIA_REGION_SHAPE.POINT: + dimg = [r["cx"], r["cy"]]; + break; + } + let scale_factor = imageElement.height / imageElement.naturalHeight; // new_height / original height + let offset_x = 0; + let offset_y = 0; + let r2 = new _via_region( + r.name, + i, + dimg, + scale_factor, + offset_x, + offset_y + ); + let r2_svg = r2.get_svg_element(); + r2_svg.setAttribute("class", "region"); + r2_svg.setAttribute("fill", "none"); + r2_svg.setAttribute("stroke", "red"); + r2_svg.setAttribute("stroke-width", "2"); + + let svgXml = new XMLSerializer().serializeToString(r2_svg); + let svgImage = new Image(); + svgImage.src = "data:image/svg+xml;base64," + btoa(svgXml); + svgImage.onload = function () { + ctx.drawImage(svgImage, 0, 0); + }; + } + let saved_img = canvas.toDataURL("image/jpeg"); + saved_img.replace("image/jpeg", "image/octet-stream"); + let a = document.createElement("a"); + a.href = saved_img; + a.target = "_blank"; + a.download = img_filename; + let event = new MouseEvent("click", { + view: window, + bubbles: true, + cancelable: true, + }); + a.dispatchEvent(event); + } +} + +// +// Display area content +// +function clear_display_area() { + let panels = document.getElementsByClassName("display_area_content"); + let i; + for (i = 0; i < panels.length; ++i) { + panels[i].classList.add("d-none"); //modSote + } + $(`#selection_panel`).hide(); +} + +function is_content_name_valid(content_name) { + var e; + for (e in VIA_DISPLAY_AREA_CONTENT_NAME) { + if (VIA_DISPLAY_AREA_CONTENT_NAME[e] === content_name) { + return true; + } + } + return false; +} + +function show_home_panel() { + show_single_image_view(); +} + +function set_display_area_content(content_name) { + if (is_content_name_valid(content_name)) { + _via_display_area_content_name_prev = _via_display_area_content_name; + clear_display_area(); + var p = document.getElementById(content_name); + p.classList.remove("d-none"); + _via_display_area_content_name = content_name; + if (content_name === VIA_DISPLAY_AREA_CONTENT_NAME.IMAGE) { + $(`#selection_panel`).show(); + } + } +} + +function show_single_image_view() { + if (buffer.imgLoaded) { + sidebar.img_fn_list_clear_all_style(); + buffer.showImage(_via_image_index); + set_display_area_content(VIA_DISPLAY_AREA_CONTENT_NAME.IMAGE); + $("#selection_panel").show(); + sidebar.annotation_editor_update_content(); + } else { + set_display_area_content(VIA_DISPLAY_AREA_CONTENT_NAME.PAGE_START_INFO); + $("#selection_panel").hide(); + } +} + +function show_image_grid_view() { + if (buffer.imgLoaded) { + sidebar.img_fn_list_clear_all_style(); + set_display_area_content(VIA_DISPLAY_AREA_CONTENT_NAME.IMAGE_GRID); + $(`#selection_panel`).hide(); + image_grid_toolbar_update_group_by_select(); + + if (_via_image_grid_group_var.length === 0) { + image_grid_show_all_project_images(); + } + sidebar.annotation_editor_update_content(); + + sidebar.edit_file_metadata_in_annotation_editor(); + } else { + set_display_area_content(VIA_DISPLAY_AREA_CONTENT_NAME.PAGE_START_INFO); + $(`#selection_panel`).hide(); + } +} + +// +// Handlers for top navigation bar +// +function sel_local_images() { + // source: https://developer.mozilla.org/en-US/docs/Using_files_from_web_applications + if (fileInput) { + fileInput.setAttribute("multiple", "multiple"); + fileInput.accept = ".jpg,.jpeg,.png,.bmp"; + fileInput.onchange = project.addLocalFile; + fileInput.click(); + } +} + +// invoked by menu-item buttons in HTML UI +function download_all_region_data(type, file_extension) { + if (typeof file_extension === "undefined") { + file_extension = type; + } + // Javascript strings (DOMString) is automatically converted to utf-8 + // see: https://developer.mozilla.org/en-US/docs/Web/API/Blob/Blob + pack_via_metadata(type).then( + function (data) { + var blob_attr = { type: "text/" + file_extension + ";charset=utf-8" }; + var all_region_data_blob = new Blob(data, blob_attr); + + var filename = "via_export"; + if ( + typeof _via_settings !== "undefined" && + _via_settings.hasOwnProperty("project") && + _via_settings["project"]["name"] !== "" + ) { + filename = _via_settings["project"]["name"]; + } + if (file_extension !== "csv" || file_extension !== "json") { + filename += "_" + type + "." + file_extension; + } + save_data_to_local_file(all_region_data_blob, filename); + }.bind(this), + function (err) { + show_message("Failed to download data: [" + err + "]"); + }.bind(this) + ); +} + +function sel_local_data_file(type) { + if (fileInput) { + switch (type) { + case "annotations": + fileInput.accept = ".csv,.json"; + fileInput.onchange = import_annotations_from_file; + break; + + case "annotations_coco": + fileInput.accept = ".json"; + fileInput.onchange = load_coco_annotations_json_file; + break; + + case "files_url": + fileInput.accept = ""; + fileInput.onchange = import_files_url_from_file; + break; + + case "attributes": + fileInput.accept = "json"; + fileInput.onchange = project.importAttributesFromFile; + break; + + default: + console.log("sel_local_data_file() : unknown type " + type); + return; + } + fileInput.removeAttribute("multiple"); + fileInput.click(); + } +} + +// +// Data Importer +// +function import_files_url_from_file(event) { + var selected_files = event.target.files; + var i, file; + for (i = 0; i < selected_files.length; ++i) { + file = selected_files[i]; + FileManager.loadTextFile(file, import_files_url_from_csv); + } +} + +function import_annotations_from_file(event) { + var selected_files = event.target.files; + var i, file; + for (i = 0; i < selected_files.length; ++i) { + file = selected_files[i]; + switch (file.type) { + case "": // Fall-through // Windows 10: Firefox and Chrome do not report filetype + show_message( + "File type for " + + file.name + + " cannot be determined! Assuming text/plain." + ); + case "text/plain": // Fall-through + case "application/vnd.ms-excel": // Fall-through // @todo: filetype of VIA csv annotations in Windows 10 , fix this (reported by @Eli Walker) + case "text/csv": + FileManager.loadTextFile(file, import_annotations_from_csv); + break; + + case "text/json": // Fall-through + case "application/json": + FileManager.loadTextFile(file, import_annotations_from_json); + break; + + default: + show_message( + "Annotations cannot be imported from file of type " + file.type + ); + break; + } + } +} + +function load_coco_annotations_json_file(event) { + FileManager.loadTextFile(event.target.files[0], import_coco_annotations_from_json); +} + +function import_annotations_from_csv(data) { + return new Promise(function (ok_callback, err_callback) { + if (data === '' || typeof (data) === 'undefined') { + err_callback(); + } + + var region_import_count = 0; + var malformed_csv_lines_count = 0; + var file_added_count = 0; + + var line_split_regex = new RegExp('\n|\r|\r\n', 'g'); + var csvdata = data.split(line_split_regex); + + var parsed_header = parse_csv_header_line(csvdata[0]); + if (!parsed_header.is_header) { + show_message('Header line missing in the CSV file'); + err_callback(); + return; + } + + var n = csvdata.length; + var i; + var first_img_id = ''; + for (i = 1; i < n; ++i) { + // ignore blank lines + if (csvdata[i].charAt(0) === '\n' || csvdata[i].charAt(0) === '') { + continue; + } + + var d = parse_csv_line(csvdata[i]); + + // check if csv line was malformed + if (d.length !== parsed_header.csv_column_count) { + malformed_csv_lines_count += 1; + continue; + } + + var filename = d[parsed_header.filename_index]; + var size = d[parsed_header.size_index]; + var img_id = _via_get_image_id(filename, size); + + // check if file is already present in this project + if (!_via_img_metadata.hasOwnProperty(img_id)) { + img_id = project.addFile(filename, size); + if (_via_settings.core.default_filepath === '') { + _via_img_src[img_id] = filename; + } else { + _via_file_resolve_file_to_default_filepath(img_id); + } + file_added_count += 1; + + if (first_img_id === '') { + first_img_id = img_id; + } + } + + // copy file attributes + if (d[parsed_header.file_attr_index] !== '"{}"') { + var fattr = d[parsed_header.file_attr_index]; + fattr = remove_prefix_suffix_quotes(fattr); + fattr = unescape_from_csv(fattr); + + var m = json_str_to_map(fattr); + for (var key in m) { + _via_img_metadata[img_id].file_attributes[key] = m[key]; + + // add this file attribute to _via_attributes + if (!project.attributes.file.hasOwnProperty(key)) { + project.attributes.file[key] = { 'type': 'text' }; + } + } + } + + var region_i = new File_Region(); + // copy regions shape attributes + if (d[parsed_header.region_shape_attr_index] !== '"{}"') { + var sattr = d[parsed_header.region_shape_attr_index]; + sattr = remove_prefix_suffix_quotes(sattr); + sattr = unescape_from_csv(sattr); + + var m = json_str_to_map(sattr); + for (var key in m) { + region_i.shape_attributes[key] = m[key]; + } + } + + // copy region attributes + if (d[parsed_header.region_attr_index] !== '"{}"') { + var rattr = d[parsed_header.region_attr_index]; + rattr = remove_prefix_suffix_quotes(rattr); + rattr = unescape_from_csv(rattr); + + var m = json_str_to_map(rattr); + for (var key in m) { + region_i.region_attributes[key] = m[key]; + + // add this region attribute to _via_attributes + if (!project.attributes.region.hasOwnProperty(key)) { + project.attributes.region[key] = { 'type': 'text' }; + } + } + } + + // add regions only if they are present + if (Object.keys(region_i.shape_attributes).length > 0 || + Object.keys(region_i.region_attributes).length > 0) { + _via_img_metadata[img_id].regions.push(region_i); + region_import_count += 1; + } + } + show_message('Import Summary : [' + file_added_count + '] new files, ' + + '[' + region_import_count + '] regions, ' + + '[' + malformed_csv_lines_count + '] malformed csv lines.'); + + if (file_added_count) { + sidebar.update_img_fn_list(); + } + + if (buffer.imgLoaded) { + if (region_import_count) { + update_attributes_update_panel(); + sidebar.annotation_editor_update_content(); + _via_load_canvas_regions(); // image to canvas space transform + drawing.redrawRegCanvas(); + _via_reg_canvas.focus(); + } + } else { + if (file_added_count) { + var first_img_index = _via_image_id_list.indexOf(first_img_id); + buffer.showImage(first_img_index); + } + } + ok_callback([file_added_count, region_import_count, malformed_csv_lines_count]); + }); +} + +function parse_csv_header_line(line) { + var header_via_10x = '#filename,file_size,file_attributes,region_count,region_id,region_shape_attributes,region_attributes'; // VIA versions 1.0.x + var header_via_11x = 'filename,file_size,file_attributes,region_count,region_id,region_shape_attributes,region_attributes'; // VIA version 1.1.x + + if (line === header_via_10x || line === header_via_11x) { + return { + 'is_header': true, + 'filename_index': 0, + 'size_index': 1, + 'file_attr_index': 2, + 'region_shape_attr_index': 5, + 'region_attr_index': 6, + 'csv_column_count': 7 + } + } else { + return { 'is_header': false }; + } +} + + +// see http://cocodataset.org/#format-data +function import_coco_annotations_from_json(data_str) { + return new Promise(function (ok_callback, err_callback) { + if (data_str === "" || typeof data_str === "undefined") { + show_message("Empty file"); + return; + } + var coco = JSON.parse(data_str); + if ( + !coco.hasOwnProperty("info") || + !coco.hasOwnProperty("categories") || + !coco.hasOwnProperty("annotations") || + !coco.hasOwnProperty("images") + ) { + show_message("File does not contain valid annotations in COCO format."); + return; + } + + // create _via_attributes from coco['categories'] + var category_id_to_attribute_name = {}; + for (var i in coco["categories"]) { + var sc = coco["categories"][i]["supercategory"]; + var cid = coco["categories"][i]["id"]; + var cname = coco["categories"][i]["name"]; + if (!project.attributes.region.hasOwnProperty(sc)) { + project.attributes.region[sc] = { + type: VIA_ATTRIBUTE_TYPE.RADIO, + description: + 'coco["categories"][' + + i + + "]=" + + JSON.stringify(coco["categories"][i]), + options: {}, + default_options: {}, + }; + } + project.attributes.region[sc]["options"][cid] = cname; + category_id_to_attribute_name[cid] = sc; + } + // if more than 5 options, convert the attribute type to DROPDOWN + for (var attr_name in project.attributes.region) { + if ( + Object.keys(project.attributes.region[attr_name]["options"]).length > 5 + ) { + project.attributes.region[attr_name]["type"] = + VIA_ATTRIBUTE_TYPE.DROPDOWN; + } + } + + // create an map of image_id and their annotations + var image_id_to_annotation_index = {}; + for (var annotation_index in coco["annotations"]) { + var coco_image_id = coco.annotations[annotation_index]["image_id"]; + if (!image_id_to_annotation_index.hasOwnProperty(coco_image_id)) { + image_id_to_annotation_index[coco_image_id] = []; + } + image_id_to_annotation_index[coco_image_id].push(annotation_index); + } + + // add all files and annotations + _via_img_metadata = {}; + _via_image_id_list = []; + _via_image_filename_list = []; + _via_img_count = 0; + var imported_file_count = 0; + var imported_region_count = 0; + for (var coco_img_index in coco["images"]) { + var coco_img_id = coco["images"][coco_img_index]["id"]; + var filename; + if ( + coco.images[coco_img_index].hasOwnProperty("coco_url") && + coco.images[coco_img_index]["coco_url"] !== "" + ) { + filename = coco.images[coco_img_index]["coco_url"]; + } else { + filename = coco.images[coco_img_index]["file_name"]; + } + _via_img_metadata[coco_img_id] = { + filename: filename, + size: -1, + regions: [], + file_attributes: { + width: coco.images[coco_img_index]["width"], + height: coco.images[coco_img_index]["height"], + }, + }; + _via_image_id_list.push(coco_img_id); + _via_image_filename_list.push(filename); + _via_img_count = _via_img_count + 1; + + // add all annotations associated with this file + if (image_id_to_annotation_index.hasOwnProperty(coco_img_id)) { + for (var i in image_id_to_annotation_index[coco_img_id]) { + var annotation_i = + coco["annotations"][image_id_to_annotation_index[coco_img_id][i]]; + var bbox_from_polygon = polygon_to_bbox( + annotation_i["segmentation"][0] + ); + + // ensure rectangles get imported as rectangle (and not as polygon) + var is_rectangle = true; + for (var j = 0; j < annotation_i["bbox"].length; ++j) { + if (annotation_i["bbox"][j] !== bbox_from_polygon[j]) { + is_rectangle = false; + break; + } + } + + var region_i = { shape_attributes: {}, region_attributes: {} }; + var attribute_name = + category_id_to_attribute_name[annotation_i["category_id"]]; + var attribute_value = annotation_i["category_id"].toString(); + region_i["region_attributes"][attribute_name] = attribute_value; + + if (annotation_i["segmentation"][0].length === 8 && is_rectangle) { + region_i["shape_attributes"] = { + name: "rect", + x: annotation_i["bbox"][0], + y: annotation_i["bbox"][1], + width: annotation_i["bbox"][2], + height: annotation_i["bbox"][3], + }; + } else { + region_i["shape_attributes"] = { + name: "polygon", + all_points_x: [], + all_points_y: [], + }; + for ( + var j = 0; + j < annotation_i["segmentation"][0].length; + j = j + 2 + ) { + region_i["shape_attributes"]["all_points_x"].push( + annotation_i["segmentation"][0][j] + ); + region_i["shape_attributes"]["all_points_y"].push( + annotation_i["segmentation"][0][j + 1] + ); + } + } + _via_img_metadata[coco_img_id]["regions"].push(region_i); + imported_region_count = imported_region_count + 1; + } + } + } + show_message( + "Import Summary : [" + + _via_img_count + + "] new files, " + + "[" + + imported_region_count + + "] regions." + ); + + if (_via_img_count) { + sidebar.update_img_fn_list(); + } + + if (buffer.imgLoaded) { + if (imported_region_count) { + update_attributes_update_panel(); + sidebar.annotation_editor_update_content(); + _via_load_canvas_regions(); // image to canvas space transform + drawing.redrawRegCanvas(); + _via_reg_canvas.focus(); + } + } else { + if (_via_img_count) { + buffer.showImage(0); + } + } + ok_callback([_via_img_count, imported_region_count, 0]); + }); +} + +function import_annotations_from_json(data_str) { + return new Promise(function (ok_callback, err_callback) { + if (data_str === "" || typeof data_str === "undefined") { + return; + } + + var d = JSON.parse(data_str); + var region_import_count = 0; + var file_added_count = 0; + var malformed_entries_count = 0; + for (var img_id in d) { + if (!_via_img_metadata.hasOwnProperty(img_id)) { + project.addFile(d[img_id].filename, d[img_id].size, img_id); + if (settings.defaultPath === "") { + _via_img_src[img_id] = d[img_id].filename; + } else { + _via_file_resolve_file_to_default_filepath(img_id); + } + file_added_count += 1; + } + + // copy file attributes + for (var key in d[img_id].file_attributes) { + if (key === "") { + continue; + } + + _via_img_metadata[img_id].file_attributes[key] = + d[img_id].file_attributes[key]; + + // add this file attribute to _via_attributes + if (!project.attributes.file.hasOwnProperty(key)) { + project.attributes.file[key] = { type: "text" }; + } + } + + // copy regions + var regions = d[img_id].regions; + for (var i in regions) { + var region_i = new File_Region(); + for (var sid in regions[i].shape_attributes) { + region_i.shape_attributes[sid] = regions[i].shape_attributes[sid]; + } + for (var rid in regions[i].region_attributes) { + if (rid === "") { + continue; + } + + region_i.region_attributes[rid] = regions[i].region_attributes[rid]; + + // add this region attribute to _via_attributes + if (!project.attributes.region.hasOwnProperty(rid)) { + project.attributes.region[rid] = { type: "text" }; + } + } + + // add regions only if they are present + if ( + Object.keys(region_i.shape_attributes).length > 0 || + Object.keys(region_i.region_attributes).length > 0 + ) { + _via_img_metadata[img_id].regions.push(region_i); + region_import_count += 1; + } + } + } + show_message( + "Import Summary : [" + + file_added_count + + "] new files, " + + "[" + + region_import_count + + "] regions, " + + "[" + + malformed_entries_count + + "] malformed entries." + ); + + if (file_added_count) { + sidebar.update_img_fn_list(); + } + + if (buffer.imgLoaded) { + if (region_import_count) { + update_attributes_update_panel(); + sidebar.annotation_editor_update_content(); + _via_load_canvas_regions(); // image to canvas space transform + drawing.redrawRegCanvas(); + _via_reg_canvas.focus(); + } + } else { + if (file_added_count) { + buffer.showImage(0); + } + } + + ok_callback([ + file_added_count, + region_import_count, + malformed_entries_count, + ]); + }); +} + +// assumes that csv line follows the RFC 4180 standard +// see: https://en.wikipedia.org/wiki/Comma-separated_values +function parse_csv_line(s, field_separator) { + if (typeof (s) === 'undefined' || s.length === 0) { + return []; + } + + if (typeof (field_separator) === 'undefined') { + field_separator = ','; + } + var double_quote_seen = false; + var start = 0; + var d = []; + + var i = 0; + while (i < s.length) { + if (s.charAt(i) === field_separator) { + if (double_quote_seen) { + // field separator inside double quote is ignored + i = i + 1; + } else { + //var part = s.substr(start, i - start); + d.push(s.substr(start, i - start)); + start = i + 1; + i = i + 1; + } + } else { + if (s.charAt(i) === '"') { + if (double_quote_seen) { + if (s.charAt(i + 1) === '"') { + // ignore escaped double quotes + i = i + 2; + } else { + // closing of double quote + double_quote_seen = false; + i = i + 1; + } + } else { + double_quote_seen = true; + start = i; + i = i + 1; + } + } else { + i = i + 1; + } + } + + } + // extract the last field (csv rows have no trailing comma) + d.push(s.substr(start)); + return d; +} + +// s = '{"name":"rect","x":188,"y":90,"width":243,"height":233}' +function json_str_to_map(s) { + if (typeof s === "undefined" || s.length === 0) { + return {}; + } + return JSON.parse(s); +} + +// ensure the exported json string conforms to RFC 4180 +// see: https://en.wikipedia.org/wiki/Comma-separated_values +function map_to_json(m) { + var s = []; + for (var key in m) { + var v = m[key]; + var si = JSON.stringify(key); + si += VIA_CSV_KEYVAL_SEP; + si += JSON.stringify(v); + s.push(si); + } + return "{" + s.join(VIA_CSV_SEP) + "}"; +} + +function escape_for_csv(s) { + return s.replace(/["]/g, '""'); +} + +function unescape_from_csv(s) { + return s.replace(/""/g, '"'); +} + +function remove_prefix_suffix_quotes(s) { + if (s.charAt(0) === '"' && s.charAt(s.length - 1) === '"') { + return s.substring(1, s.length - 1); + } else { + return s; + } +} + +function clone_image_region(r0) { + var r1 = new File_Region(); + + // copy shape attributes + for (var key in r0.shape_attributes) { + r1.shape_attributes[key] = clone_value(r0.shape_attributes[key]); + } + + // copy region attributes + for (var key in r0.region_attributes) { + r1.region_attributes[key] = clone_value(r0.region_attributes[key]); + } + return r1; +} + +function clone_value(value) { + if (typeof value === "object") { + if (Array.isArray(value)) { + return value.slice(0); + } else { + var copy = {}; + for (var p in value) { + if (value.hasOwnProperty(p)) { + copy[p] = clone_value(value[p]); + } + } + return copy; + } + } + return value; +} + +function _via_get_image_id(filename, size) { + if (typeof size === "undefined") { + return filename; + } else { + return filename + size; + } +} + +function load_text_file(text_file, callback_function) { + if (text_file) { + var text_reader = new FileReader(); + text_reader.addEventListener( + "progress", + function (e) { + show_message("Loading data from file : " + text_file.name + " ... "); + }, + false + ); + + text_reader.addEventListener( + "error", + function () { + show_message( + "Error loading data text file : " + text_file.name + " !" + ); + callback_function(""); + }, + false + ); + + text_reader.addEventListener( + "load", + function () { + callback_function(text_reader.result); + }, + false + ); + text_reader.readAsText(text_file, "utf-8"); + } +} + +function import_files_url_from_csv(data) { + return new Promise(function (ok_callback, err_callback) { + if (data === "" || typeof data === "undefined") { + err_callback(); + } + + var malformed_url_count = 0; + var url_added_count = 0; + + var line_split_regex = new RegExp("\n|\r|\r\n", "g"); + var csvdata = data.split(line_split_regex); + + var percent_completed = 0; + var n = csvdata.length; + var i; + var img_id; + var first_img_id = ""; + for (i = 0; i < n; ++i) { + // ignore blank lines + if (csvdata[i].charAt(0) === "\n" || csvdata[i].charAt(0) === "") { + malformed_url_count += 1; + } else { + img_id = project.addUrlFile(csvdata[i]); + if (first_img_id === "") { + first_img_id = img_id; + } + url_added_count += 1; + } + } + show_message("Added " + url_added_count + " files to project"); + if (url_added_count) { + var first_img_index = _via_image_id_list.indexOf(first_img_id); + buffer.showImage(first_img_index); + sidebar.update_img_fn_list(); + } + }); +} + +// +// Data Exporter +// +function pack_via_metadata(return_type) { + return new Promise( + function (ok_callback, err_callback) { + if (return_type === "csv") { + var csvdata = []; + var csvheader = + "filename,file_size,file_attributes,region_count,region_id,region_shape_attributes,region_attributes"; + csvdata.push(csvheader); + + for (var image_id in _via_img_metadata) { + var fattr = map_to_json(_via_img_metadata[image_id].file_attributes); + fattr = escape_for_csv(fattr); + + var prefix = "\n" + _via_img_metadata[image_id].filename; + prefix += "," + _via_img_metadata[image_id].size; + prefix += ',"' + fattr + '"'; + + var r = _via_img_metadata[image_id].regions; + + if (r.length !== 0) { + for (var i = 0; i < r.length; ++i) { + var csvline = []; + csvline.push(prefix); + csvline.push(r.length); + csvline.push(i); + + var sattr = map_to_json(r[i].shape_attributes); + sattr = '"' + escape_for_csv(sattr) + '"'; + csvline.push(sattr); + + var rattr = map_to_json(r[i].region_attributes); + rattr = '"' + escape_for_csv(rattr) + '"'; + csvline.push(rattr); + csvdata.push(csvline.join(VIA_CSV_SEP)); + } + } else { + // @todo: reconsider this practice of adding an empty entry + csvdata.push(prefix + ',0,0,"{}","{}"'); + } + } + ok_callback(csvdata); + } + + // see http://cocodataset.org/#format-data + if (return_type === "coco") { + img_stat_set_all().then( + function (ok) { + var coco = export_project_to_coco_format(); + ok_callback([coco]); + }.bind(this), + function (err) { + err_callback(err); + }.bind(this) + ); + } else { + // default format is JSON + ok_callback([JSON.stringify(_via_img_metadata)]); + } + }.bind(this) + ); +} + +function export_project_to_coco_format() { + var coco = { + info: {}, + images: [], + annotations: [], + licenses: [], + categories: [], + }; + coco["info"] = { + year: new Date().getFullYear(), + version: "1.0", + description: + "VIA project exported to COCO format using VGG Image Annotator (http://www.robots.ox.ac.uk/~vgg/software/via/)", + contributor: "", + url: "http://www.robots.ox.ac.uk/~vgg/software/via/", + date_created: new Date().toString(), + }; + coco["licenses"] = [{ id: 0, name: "Unknown License", url: "" }]; // indicates that license is unknown + + var skipped_annotation_count = 0; + // We want to ensure that a COCO project imported in VIA and then exported again back to + // COCO format using VIA retains the image_id and category_id present in the original COCO project. + // A VIA project that has been created by importing annotations from a COCO project contains + // unique image_id of type integer and contains all unique option id. If we detect this, we reuse + // the existing image_id and category_id, otherwise we assign a new unique id sequentially. + // Currently, it is not possible to preserve the annotation_id + var assign_unique_id = false; + for (var img_id in _via_img_metadata) { + if (Number.isNaN(parseInt(img_id))) { + assign_unique_id = true; // since COCO only supports image_id of type integer, we cannot reuse the VIA's image-id + break; + } + } + if (assign_unique_id) { + // check if all the options have unique id + var attribute_option_id_list = []; + for (var attr_name in project.attributes) { + if ( + !VIA_COCO_EXPORT_ATTRIBUTE_TYPE.includes( + project.attributes[attr_name]["type"] + ) + ) { + continue; // skip this attribute as it will not be included in COCO export + } + + for (var attr_option_id in project.attributes[attr_name]["options"]) { + if ( + attribute_option_id_list.includes(attr_option_id) || + Number.isNaN(parseInt(attr_option_id)) + ) { + assign_unique_id = true; + break; + } else { + attribute_option_id_list.push(assign_unique_id); + } + } + } + } + + // add categories + var attr_option_id_list = []; //MODSote + var attr_option_id_to_category_id = {}; + var unique_category_id = 1; + for (var attr_name in project.attributes.region) { + if ( + VIA_COCO_EXPORT_ATTRIBUTE_TYPE.includes( + project.region[attr_name]["type"] + ) + ) { + for (var attr_option_id in project.project.attributes.region[attr_name][ + "options" + ]) { + var category_id; + if (assign_unique_id) { + category_id = unique_category_id; + unique_category_id = unique_category_id + 1; + } else { + category_id = parseInt(attr_option_id); + } + coco["categories"].push({ + supercategory: attr_name, + id: category_id, + name: project.attributes.region[attr_name]["options"][attr_option_id], + }); + attr_option_id_to_category_id[attr_option_id] = category_id; + } + } + } + + // add files and all their associated annotations + var annotation_id = 1; + var unique_img_id = 1; + for (var img_index in _via_image_id_list) { + var img_id = _via_image_id_list[img_index]; + var file_src = + _via_settings["core"]["default_filepath"] + + _via_img_metadata[img_id].filename; + if (_via_img_fileref[img_id] instanceof File) { + file_src = _via_img_fileref[img_id].filename; + } + + var coco_img_id; + if (assign_unique_id) { + coco_img_id = unique_img_id; + unique_img_id = unique_img_id + 1; + } else { + coco_img_id = parseInt(img_id); + } + + coco["images"].push({ + id: coco_img_id, + width: _via_img_stat[img_index][0], + height: _via_img_stat[img_index][1], + file_name: _via_img_metadata[img_id].filename, + license: 0, + flickr_url: file_src, + coco_url: file_src, + date_captured: "", + }); + + // add all annotations associated with this file + for (var rindex in _via_img_metadata[img_id].regions) { + var region = _via_img_metadata[img_id].regions[rindex]; + if (!VIA_COCO_EXPORT_RSHAPE.includes(region.shape_attributes["name"])) { + skipped_annotation_count = skipped_annotation_count + 1; + continue; // skip this region as COCO does not allow it + } + + var coco_annotation = via_region_shape_to_coco_annotation( + region.shape_attributes + ); + coco_annotation["id"] = annotation_id; + coco_annotation["image_id"] = coco_img_id; + + var region_aid_list = Object.keys(region["region_attributes"]); + for (var region_attribute_id in region["region_attributes"]) { + var region_attribute_value = + region["region_attributes"][region_attribute_id]; + if ( + attr_option_id_to_category_id.hasOwnProperty(region_attribute_value) + ) { + coco_annotation["category_id"] = + attr_option_id_to_category_id[region_attribute_value]; + coco["annotations"].push(coco_annotation); + annotation_id = annotation_id + 1; + } else { + skipped_annotation_count = skipped_annotation_count + 1; + // skip attribute value not supported by COCO format + } + } + } + } + + show_message( + "Skipped " + + skipped_annotation_count + + " annotations. COCO format only supports the following attribute types: " + + JSON.stringify(VIA_COCO_EXPORT_ATTRIBUTE_TYPE) + + " and region shapes: " + + JSON.stringify(VIA_COCO_EXPORT_RSHAPE) + ); + return [JSON.stringify(coco)]; +} + +function via_region_shape_to_coco_annotation(shape_attributes) { + var annotation = { segmentation: [[]], area: [], bbox: [], iscrowd: 0 }; + + switch (shape_attributes["name"]) { + case "rect": + var x0 = shape_attributes["x"]; + var y0 = shape_attributes["y"]; + var w = parseInt(shape_attributes["width"]); + var h = parseInt(shape_attributes["height"]); + var x1 = x0 + w; + var y1 = y0 + h; + annotation["segmentation"][0] = [x0, y0, x1, y0, x1, y1, x0, y1]; + annotation["area"] = w * h; + + annotation["bbox"] = [x0, y0, w, h]; + break; + + case "point": + var cx = shape_attributes["cx"]; + var cy = shape_attributes["cy"]; + // 2 is for visibility - currently set to always inside segmentation. + // see Keypoint Detection: http://cocodataset.org/#format-data + annotation["keypoints"] = [cx, cy, 2]; + annotation["num_keypoints"] = 1; + break; + + case "circle": + var a, b; + a = shape_attributes["r"]; + b = shape_attributes["r"]; + var theta_to_radian = Math.PI / 180; + + for ( + var theta = 0; + theta < 360; + theta = theta + VIA_POLYGON_SEGMENT_SUBTENDED_ANGLE + ) { + var theta_radian = theta * theta_to_radian; + var x = shape_attributes["cx"] + a * Math.cos(theta_radian); + var y = shape_attributes["cy"] + b * Math.sin(theta_radian); + annotation["segmentation"][0].push(fixfloat(x), fixfloat(y)); + } + annotation["bbox"] = polygon_to_bbox(annotation["segmentation"][0]); + annotation["area"] = annotation["bbox"][2] * annotation["bbox"][3]; + break; + + case "ellipse": + var a, b; + a = shape_attributes["rx"]; + b = shape_attributes["ry"]; + var rotation = 0; + // older version of VIA2 did not support rotated ellipse and hence 'theta' attribute may not be available + if (shape_attributes.hasOwnProperty("theta")) { + rotation = shape_attributes["theta"]; + } + + var theta_to_radian = Math.PI / 180; + + for ( + var theta = 0; + theta < 360; + theta = theta + VIA_POLYGON_SEGMENT_SUBTENDED_ANGLE + ) { + var theta_radian = theta * theta_to_radian; + var x = + shape_attributes["cx"] + + a * Math.cos(theta_radian) * Math.cos(rotation) - + b * Math.sin(theta_radian) * Math.sin(rotation); + var y = + shape_attributes["cy"] + + a * Math.cos(theta_radian) * Math.sin(rotation) + + b * Math.sin(theta_radian) * Math.cos(rotation); + annotation["segmentation"][0].push(fixfloat(x), fixfloat(y)); + } + annotation["bbox"] = polygon_to_bbox(annotation["segmentation"][0]); + annotation["area"] = annotation["bbox"][2] * annotation["bbox"][3]; + break; + + case "polygon": + annotation["segmentation"][0] = []; + var x0 = +Infinity; + var y0 = +Infinity; + var x1 = -Infinity; + var y1 = -Infinity; + for (var i in shape_attributes["all_points_x"]) { + annotation["segmentation"][0].push(shape_attributes["all_points_x"][i]); + annotation["segmentation"][0].push(shape_attributes["all_points_y"][i]); + if (shape_attributes["all_points_x"][i] < x0) { + x0 = shape_attributes["all_points_x"][i]; + } + if (shape_attributes["all_points_y"][i] < y0) { + y0 = shape_attributes["all_points_y"][i]; + } + if (shape_attributes["all_points_x"][i] > x1) { + x1 = shape_attributes["all_points_x"][i]; + } + if (shape_attributes["all_points_y"][i] > y1) { + y1 = shape_attributes["all_points_y"][i]; + } + } + var w = x1 - x0; + var h = y1 - y0; + annotation["bbox"] = [x0, y0, w, h]; + annotation["area"] = w * h; // approximate area + } + return annotation; +} + +function save_data_to_local_file(data, filename) { + var a = document.createElement("a"); + a.href = URL.createObjectURL(data); + a.download = filename; + + // simulate a mouse click event + var event = new MouseEvent("click", { + view: window, + bubbles: true, + cancelable: true, + }); + a.dispatchEvent(event); + + // @todo: replace a.dispatchEvent() with a.click() + // a.click() based trigger is supported in Chrome 70 and Safari 11/12 but **not** in Firefox 63 + //a.click(); +} + +// +// Maintainers of user interface +// + +function init_message_panel() { + var p = document.getElementById("message_panel"); + p.addEventListener( + "mousedown", + function () { + this.style.display = "none"; + }, + false + ); + p.addEventListener( + "mouseover", + function () { + clearTimeout(_via_message_clear_timer); // stop any previous timeouts + }, + false + ); +} + +function toggle_message_visibility() { + if (_via_is_message_visible) { + show_message("Disabled status messages"); + _via_is_message_visible = false; + } else { + _via_is_message_visible = true; + show_message("Status messages are now visible"); + } +} + +function show_message(msg, t) { + if (!_via_is_message_visible) { + return; + } + Message.show({ + address: "Info", + body: msg, + color: "#4ADEDE", + }); +} + +// transform regions in image space to canvas space +function _via_load_canvas_regions() { + drawing.regionsGroupColorInit(); + + // load all existing annotations into _via_canvas_regions + var regions = _via_img_metadata[_via_image_id].regions; + _via_canvas_regions = []; + for (var i = 0; i < regions.length; ++i) { + var region_i = new File_Region(); + for (var key in regions[i].shape_attributes) { + region_i.shape_attributes[key] = regions[i].shape_attributes[key]; + } + _via_canvas_regions.push(region_i); + + switch (_via_canvas_regions[i].shape_attributes["name"]) { + case VIA_REGION_SHAPE.RECT: + var x = regions[i].shape_attributes["x"] / _via_canvas_scale; + var y = regions[i].shape_attributes["y"] / _via_canvas_scale; + var width = regions[i].shape_attributes["width"] / _via_canvas_scale; + var height = regions[i].shape_attributes["height"] / _via_canvas_scale; + + _via_canvas_regions[i].shape_attributes["x"] = Math.round(x); + _via_canvas_regions[i].shape_attributes["y"] = Math.round(y); + _via_canvas_regions[i].shape_attributes["width"] = Math.round(width); + _via_canvas_regions[i].shape_attributes["height"] = Math.round(height); + break; + + case VIA_REGION_SHAPE.CIRCLE: + var cx = regions[i].shape_attributes["cx"] / _via_canvas_scale; + var cy = regions[i].shape_attributes["cy"] / _via_canvas_scale; + var r = regions[i].shape_attributes["r"] / _via_canvas_scale; + _via_canvas_regions[i].shape_attributes["cx"] = Math.round(cx); + _via_canvas_regions[i].shape_attributes["cy"] = Math.round(cy); + _via_canvas_regions[i].shape_attributes["r"] = Math.round(r); + break; + + case VIA_REGION_SHAPE.ELLIPSE: + var cx = regions[i].shape_attributes["cx"] / _via_canvas_scale; + var cy = regions[i].shape_attributes["cy"] / _via_canvas_scale; + var rx = regions[i].shape_attributes["rx"] / _via_canvas_scale; + var ry = regions[i].shape_attributes["ry"] / _via_canvas_scale; + // rotation in radians + var theta = regions[i].shape_attributes["theta"]; + _via_canvas_regions[i].shape_attributes["cx"] = Math.round(cx); + _via_canvas_regions[i].shape_attributes["cy"] = Math.round(cy); + _via_canvas_regions[i].shape_attributes["rx"] = Math.round(rx); + _via_canvas_regions[i].shape_attributes["ry"] = Math.round(ry); + _via_canvas_regions[i].shape_attributes["theta"] = theta; + break; + + case VIA_REGION_SHAPE.POLYLINE: // handled by polygon + case VIA_REGION_SHAPE.POLYGON: + var all_points_x = regions[i].shape_attributes["all_points_x"].slice(0); + var all_points_y = regions[i].shape_attributes["all_points_y"].slice(0); + for (var j = 0; j < all_points_x.length; ++j) { + all_points_x[j] = Math.round(all_points_x[j] / _via_canvas_scale); + all_points_y[j] = Math.round(all_points_y[j] / _via_canvas_scale); + } + _via_canvas_regions[i].shape_attributes["all_points_x"] = all_points_x; + _via_canvas_regions[i].shape_attributes["all_points_y"] = all_points_y; + break; + + case VIA_REGION_SHAPE.POINT: + var cx = regions[i].shape_attributes["cx"] / _via_canvas_scale; + var cy = regions[i].shape_attributes["cy"] / _via_canvas_scale; + + _via_canvas_regions[i].shape_attributes["cx"] = Math.round(cx); + _via_canvas_regions[i].shape_attributes["cy"] = Math.round(cy); + break; + } + } +} + +// updates currently selected region shape +function select_region_shape(sel_shape_name) { + if (drawing.currentShape() === sel_shape_name) { + return; + } + + if (drawing.is_user_drawing_polygon) { + return; + } + + $("#selection_panel button").removeClass("active"); + + if (sel_shape_name === "img_set") { + $('#selection_panel button[id="image_settings"]').addClass("active"); + $("#image_man_container").removeClass("d-none"); + $("#sidebar_container").addClass("d-none"); + } + + if (sel_shape_name === "drag") { + zoom.enablePan(); + _via_reg_canvas.cursor = "move"; + } else { + zoom.disablePan(); + _via_reg_canvas.cursor = "crosshair"; + } + + $('#selection_panel button[id="shape_' + sel_shape_name + '"]').addClass( + "active" + ); + + drawing.setCurrentShape(sel_shape_name); + + switch ( + drawing.currentShape() //classm + ) { + case VIA_REGION_SHAPE.RECT: // Fall-through + case VIA_REGION_SHAPE.CIRCLE: // Fall-through + case VIA_REGION_SHAPE.ELLIPSE: + Message.show({ + address: "Info", + body: + "Press single click and drag mouse to draw " + + drawing.currentShape() + + " region", //classm + color: "#1d3557", + }); + break; + + case VIA_REGION_SHAPE.POLYLINE: + case VIA_REGION_SHAPE.POLYGON: + drawing.setIsUserDrawingPolygon(false); + drawing.setCurrentPolygonRegionId(-1); + Message.show({ + address: "Info", + body: + "[Single Click] to define polygon/polyline vertices, " + + "[Backspace] to delete last vertex, [Enter] to finish, [Esc] to cancel drawing.", + }); + break; + + case VIA_REGION_SHAPE.POINT: + Message.show({ + address: "Info", + body: "Press single click to define points (or landmarks)", + }); + + break; + case VIA_REGION_SHAPE.REMOVE: + Message.show({ + address: "Info", + body: "Draw a rectangle with this tool to remove some points from the regions (poly and pen regions) within the rectangle", + }); + + break; + + case VIA_REGION_SHAPE.TRIM: + Message.show({ + address: "Info", + body: "Select a polygon to trim", + }); + break; + + case VIA_REGION_SHAPE.DRAG: + Message.show({ + address: "Info", + body: "Now you can pan the image", + }); + break; + + default: + Message.show({ + address: "Info", + body: "Editor mode, drawing is disabled", + }); + break; + } +} + +function set_all_canvas_size(w, h) { + image_panel.style.height = h + "px"; + image_panel.style.width = w + "px"; +} + +function set_all_canvas_scale(s) { + //_via_reg_ctx.scale(s, s); +} + +function show_all_canvas() { + image_panel.style.display = "inline-block"; +} + +function hide_all_canvas() { + image_panel.style.display = "none"; +} + +function jump_to_image(image_index) { + if (_via_img_count <= 0) { + return; + } + + undoredo_worker.postMessage({ + commands: "reset", + }); + undoredo_worker.postMessage({ + commands: "add", + }); + switch (_via_display_area_content_name) { + case VIA_DISPLAY_AREA_CONTENT_NAME.IMAGE_GRID: + if (image_index >= 0 && image_index < _via_img_count) { + // @todo: jump to image grid page view with the given first image index + show_single_image_view(); + buffer.showImage(image_index); + } + break; + default: + if (image_index >= 0 && image_index < _via_img_count) { + buffer.showImage(image_index); + } + break; + } +} + +function count_missing_region_attr(img_id) { + var miss_region_attr_count = 0; + var attr_count = Object.keys(_via_region_attributes).length; + for (var i = 0; i < _via_img_metadata[img_id].regions.length; ++i) { + var set_attr_count = Object.keys( + _via_img_metadata[img_id].regions[i].region_attributes + ).length; + miss_region_attr_count += attr_count - set_attr_count; + } + return miss_region_attr_count; +} + +function count_missing_file_attr(img_id) { + return ( + Object.keys(_via_file_attributes).length - + Object.keys(_via_img_metadata[img_id].file_attributes).length + ); +} + +function toggle_all_regions_selection(is_selected) { + var n = _via_img_metadata[_via_image_id].regions.length; + var i; + _via_region_selected_flag.clear(); + if (is_selected) { + for (i = 0; i < n; ++i) { + _via_region_selected_flag.add(i); + } + } + _via_is_all_region_selected = is_selected; + sidebar.annotation_editor_hide(); + if (_via_annotation_editor_mode === VIA_ANNOTATION_EDITOR_MODE.ALL_REGIONS) { + sidebar.annotation_editor_clear_row_highlight(); + } +} + +function select_only_region(region_id) { + toggle_all_regions_selection(false); + set_region_select_state(region_id, true); + drawing.setIsRegionSelected(true); //classm + _via_is_all_region_selected = false; + drawing.setUserSelRegionId(region_id); //classm +} + +function set_region_select_state(region_id, is_selected) { + if (is_selected) { + _via_region_selected_flag.add(parseInt(region_id)); + } else { + _via_region_selected_flag.delete(parseInt(region_id)); + } +} + +function show_annotation_data() { + pack_via_metadata("csv").then( + function (data) { + var hstr = "<pre>" + data.join("") + "</pre>"; + var window_features = + "toolbar=no,menubar=no,location=no,resizable=yes,scrollbars=yes,status=no"; + window_features += ",width=800,height=600"; + var annotation_data_window = window.open( + "", + "Annotations (preview) ", + window_features + ); + annotation_data_window.document.body.innerHTML = hstr; + }.bind(this), + function (err) { + show_message("Failed to collect annotation data!"); + }.bind(this) + ); +} + +// +// Shortcut key handlers +// +function _via_window_keydown_handler(e) { + if (e.target === document.body) { + // process the keyboard event + _via_handle_global_keydown_event(e); + } +} + +// global keys are active irrespective of element focus +// arrow keys, n, p, s, o, space, d, Home, End, PageUp, PageDown +function _via_handle_global_keydown_event(e) { + // zoom + if (buffer.imgLoaded) { + if (e.key === "+") { + zoom.zoomIn(); + return; + } + + if (e.key === "=") { + zoom.resetZoom(); + return; + } + + if (e.key === "-") { + zoom.zoomOut(); + return; + } + } + + if (e.key === "Delete") { + if ( + drawing.isRegionSelected() || //classm + _via_is_all_region_selected + ) { + del_sel_regions(); + } + e.preventDefault(); + return; + } + + if (e.key === "ArrowRight" || e.key === "n") { + move_to_next_image(); + e.preventDefault(); + return; + } + if (e.key === "ArrowLeft" || e.key === "p") { + move_to_prev_image(); + e.preventDefault(); + return; + } + + if (e.key === "ArrowUp") { + region_visualisation_update("region_label", "__via_region_id__", 1); + e.preventDefault(); + return; + } + + if (e.key === "ArrowDown") { + region_visualisation_update( + "region_color", + "__via_default_region_color__", + -1 + ); + e.preventDefault(); + return; + } + if (e.key === "s") { + //MODSote + AutoAnnotator.run_annotation(); + e.preventDefault(); + return; + } + if (e.key === "Home") { + show_first_image(); + e.preventDefault(); + return; + } + if (e.key === "End") { + show_last_image(); + e.preventDefault(); + return; + } + if (e.key === "PageDown") { + jump_to_next_image_block(); + e.preventDefault(); + return; + } + if (e.key === "PageUp") { + jump_to_prev_image_block(); + e.preventDefault(); + return; + } + + if (e.key === "a") { + if ( + _via_display_area_content_name === + VIA_DISPLAY_AREA_CONTENT_NAME.IMAGE_GRID + ) { + // select all in image grid + image_grid_group_toggle_select_all(); + } + } + + if (e.key === "Escape") { + e.preventDefault(); + if (_via_is_loading_current_image) { + buffer.stopLoading(); + } + + if (drawing.isUserResizingRegion()) { + //classm + // cancel region resizing action + drawing.setIsUserResizingRegion(false); //classm + } + + if (drawing.isRegionSelected()) { + //classm + // clear all region selections + drawing.setIsRegionSelected(false); //classm + drawing.setUserSelRegionId(-1); //classm + toggle_all_regions_selection(false); + } + + if (drawing.isUserDrawingPolygon()) { + //classm + drawing.setIsUserDrawingPolygon(false); //classm + _via_canvas_regions.splice(drawing.currentPolygonRegionId(), 1); //classm + } + + if (drawing.isUserDrawingRegion()) { + //classm + drawing.setIsUserDrawingRegion(false); //classm + } + + if (drawing.isUserResizingRegion()) { + //classm + drawing.setIsUserResizingRegion(false); //classm + } + + if (drawing.isUserMovingRegion()) { + //classm + drawing.setIsUserMovingRegion(false); //classm + } + + drawing.redrawRegCanvas(); + return; + } + + if (e.key === " ") { + // Space key + if (e.ctrlKey) { + sidebar.annotation_editor_toggle_on_image_editor(); + } else { + leftsidebar_toggle(); + _via_reg_canvas.focus(); + } + e.preventDefault(); + return; + } + + if (e.key === "F1") { + // F1 for help + set_display_area_content( + VIA_DISPLAY_AREA_CONTENT_NAME.PAGE_GETTING_STARTED + ); + $(`#selection_panel`).hide(); + e.preventDefault(); + return; + } + if (e.key === "F2") { + // F2 for about + set_display_area_content(VIA_DISPLAY_AREA_CONTENT_NAME.PAGE_ABOUT); + $(`#selection_panel`).hide(); + e.preventDefault(); + } +} + +function _via_reg_canvas_keyup_handler(e) { + if (e.key === "Control") { + _via_is_ctrl_pressed = false; + } + if (e.key === "Alt") { + select_region_shape(current_shape); + is_alt_pressed = false; + zoom.disablePan(); + } +} + +function _via_reg_canvas_keydown_handler(e) { + if (e.key === "Control") { + _via_is_ctrl_pressed = true; + } + + if (e.key === "Alt") { + if (!is_alt_pressed) { + current_shape = drawing.currentShape(); + select_region_shape("edit"); + zoom.enablePan(); + } + is_alt_pressed = true; + } + + if (buffer.imgLoaded) { + if (e.key === "Enter") { + if ( + drawing.currentShape() === VIA_REGION_SHAPE.POLYLINE || //classm + drawing.currentShape() === VIA_REGION_SHAPE.POLYGON + ) { + //classm + _via_polyshape_finish_drawing(); + } + } + if (e.key === "Backspace") { + if ( + drawing.currentShape() === VIA_REGION_SHAPE.POLYLINE || //classm + drawing.currentShape() === VIA_REGION_SHAPE.POLYGON + ) { + //classm + _via_polyshape_delete_last_vertex(); + } + } + + if (e.key === "a") { + sel_all_regions(); + e.preventDefault(); + return; + } + + if (e.key === "c") { + if ( + drawing.isRegionSelected() || //classm + _via_is_all_region_selected + ) { + copy_sel_regions(); + } + e.preventDefault(); + return; + } + + if (e.key === "v") { + paste_sel_regions_in_current_image(); + e.preventDefault(); + return; + } + + if (e.key === "b") { + toggle_region_boundary_visibility(); + e.preventDefault(); + return; + } + + if (e.key === "l") { + toggle_region_id_visibility(); + e.preventDefault(); + return; + } + + if (e.key === "r") { + if (drawing.isRegionSelected() || _via_is_all_region_selected) { + reducePointsSelRegion(); + } + e.preventDefault(); + return; + } + + if (drawing.isRegionSelected()) { + //classm + if ( + e.key === "ArrowRight" || + e.key === "ArrowLeft" || + e.key === "ArrowDown" || + e.key === "ArrowUp" + ) { + var del = 1; + if (e.shiftKey) { + del = 10; + } + var move_x = 0; + var move_y = 0; + switch (e.key) { + case "ArrowLeft": + move_x = -del; + break; + case "ArrowUp": + move_y = -del; + break; + case "ArrowRight": + move_x = del; + break; + case "ArrowDown": + move_y = del; + break; + } + drawing.moveSelectedRegions(move_x, move_y); + drawing.redrawRegCanvas(); + e.preventDefault(); + return; + } + } + if (e.key === "d" && _via_is_ctrl_pressed) { + del_sel_regions(); + } + if (e.key === "z" && _via_is_ctrl_pressed) { + undoredo_worker.postMessage({ + commands: "undo", + }); + } + if (e.key === "v" && _via_is_ctrl_pressed) { + undoredo_worker.postMessage({ + commands: "redo", + }); + } + } + _via_handle_global_keydown_event(e); +} + +function _via_polyshape_finish_drawing() { + if (drawing.isUserDrawingPolygon()) { + var new_region_id = drawing.currentPolygonRegionId(); //classm + var new_region_shape = drawing.currentShape(); //classm + + var npts = + _via_canvas_regions[new_region_id].shape_attributes["all_points_x"] + .length; + if (npts <= 2 && new_region_shape === VIA_REGION_SHAPE.POLYGON) { + show_message( + "For a polygon, you must define at least 3 points. " + + "Press [Esc] to cancel drawing operation.!" + ); + return; + } + if (npts <= 1 && new_region_shape === VIA_REGION_SHAPE.POLYLINE) { + show_message( + "A polyline must have at least 2 points. " + + "Press [Esc] to cancel drawing operation.!" + ); + return; + } + + var img_id = _via_image_id; + drawing.setCurrentPolygonRegionId(-1); //classm + drawing.setIsUserDrawingPolygon(false); //classm + drawing.setIsUserDrawingRegion(false); //classm + + _via_img_metadata[img_id].regions[new_region_id] = {}; // create placeholder + _via_polyshape_add_new_polyshape(img_id, new_region_shape, new_region_id); + select_only_region(new_region_id); // select new region + sidebar.set_region_annotations_to_default_value(new_region_id); + sidebar.annotation_editor_add_row(new_region_id); + sidebar.annotation_editor_scroll_to_row(new_region_id); + + drawing.redrawRegCanvas(); + _via_reg_canvas.focus(); + } +} + +function _via_polyshape_delete_last_vertex() { + if (drawing.isUserDrawingPolygon()) { + //classm + var npts = + _via_canvas_regions[drawing.currentPolygonRegionId()].shape_attributes[ + "all_points_x" + ].length; //classm + if (npts > 0) { + _via_canvas_regions[drawing.currentPolygonRegionId()].shape_attributes[ + "all_points_x" + ].splice(npts - 1, 1); //classm + _via_canvas_regions[drawing.currentPolygonRegionId()].shape_attributes[ + "all_points_y" + ].splice(npts - 1, 1); //classm + + drawing.redrawRegCanvas(); + _via_reg_canvas.focus(); + } + } +} + +function _via_polyshape_add_new_polyshape(img_id, region_shape, region_id) { + // add all polygon points stored in _via_canvas_regions[] + var all_points_x = + _via_canvas_regions[region_id].shape_attributes["all_points_x"].slice(0); + var all_points_y = + _via_canvas_regions[region_id].shape_attributes["all_points_y"].slice(0); + + var canvas_all_points_x = []; + var canvas_all_points_y = []; + var n = all_points_x.length; + var i; + for (i = 0; i < n; ++i) { + all_points_x[i] = Math.round(all_points_x[i] * _via_canvas_scale); + all_points_y[i] = Math.round(all_points_y[i] * _via_canvas_scale); + + canvas_all_points_x[i] = Math.round(all_points_x[i] / _via_canvas_scale); + canvas_all_points_y[i] = Math.round(all_points_y[i] / _via_canvas_scale); + } + + var polygon_region = new File_Region(); + polygon_region.shape_attributes["name"] = region_shape; + polygon_region.shape_attributes["all_points_x"] = all_points_x; + polygon_region.shape_attributes["all_points_y"] = all_points_y; + _via_img_metadata[img_id].regions[region_id] = polygon_region; + + // update canvas + if (img_id === _via_image_id) { + _via_canvas_regions[region_id].shape_attributes["name"] = region_shape; + _via_canvas_regions[region_id].shape_attributes["all_points_x"] = + canvas_all_points_x; + _via_canvas_regions[region_id].shape_attributes["all_points_y"] = + canvas_all_points_y; + } +} + +function del_sel_regions() { + if ( + _via_display_area_content_name === VIA_DISPLAY_AREA_CONTENT_NAME.IMAGE_GRID + ) { + return; + } + + if (!buffer.imgLoaded) { + show_message("First load some images!"); + return; + } + + var del_region_count = 0; + if (_via_is_all_region_selected) { + del_region_count = _via_canvas_regions.length; + _via_canvas_regions.splice(0); + _via_img_metadata[_via_image_id].clearRegions(); + _via_img_metadata[_via_image_id].clearLockedRegions(); + } else { + var sorted_sel_reg_id = []; + for (var i = 0; i < _via_canvas_regions.length; ++i) { + if (_via_region_selected_flag.has(i)) { + sorted_sel_reg_id.push(i); + _via_region_selected_flag.delete(i); + } + } + sorted_sel_reg_id.sort(function (a, b) { + return b - a; + }); + for (const element of sorted_sel_reg_id) { + _via_canvas_regions.splice(element, 1); + _via_img_metadata[_via_image_id].regions.splice(element, 1); + if (_via_img_metadata[_via_image_id].isRegionLocked(element)) { + _via_img_metadata[_via_image_id].clearRegionLock(element); + } + del_region_count += 1; + } + + if (sorted_sel_reg_id.length) { + _via_reg_canvas.style.cursor = "default"; + } + } + + _via_is_all_region_selected = false; + drawing.setIsRegionSelected(false); //classm + drawing.setUserSelRegionId(-1); //classm + drawing.updateCheckedLockHtml(); + + if (_via_canvas_regions.length === 0) { + // all regions were deleted, hence clear region canvas + drawing.clearCanvas(); + } else { + drawing.redrawRegCanvas(); + } + _via_reg_canvas.focus(); + sidebar.annotation_editor_show(); + + show_message("Deleted " + del_region_count + " selected regions"); +} + +function reducePointsSelRegion() { + if ( + _via_display_area_content_name === VIA_DISPLAY_AREA_CONTENT_NAME.IMAGE_GRID + ) { + return; + } + + if (!buffer.imgLoaded) { + show_message("First load some images!"); + return; + } + + let del_region_count = 0; + if (_via_is_all_region_selected) { + let N = _via_canvas_regions.length; + if (N === 0) { + return; + } + + for (let i = 0; i < N; ++i) { + if (!_via_img_metadata[_via_image_id].isRegionLocked(i)) { + let rs = _via_canvas_regions[i].shape_attributes; + if (rs.name === "polygon") { + for (let j = 0; j < rs.all_points_x.length; ++j) { + drawing.polygonDelVertex(i, j); + } + } + } + } + } else { + let sorted_sel_reg_id = []; + for (let i = 0; i < _via_canvas_regions.length; ++i) { + if (_via_region_selected_flag.has(i)) { + sorted_sel_reg_id.push(i); + _via_region_selected_flag.delete(i); + } + } + sorted_sel_reg_id.sort(function (a, b) { + return b - a; + }); + for (const element of sorted_sel_reg_id) { + let rs = _via_canvas_regions[element].shape_attributes; + if (rs.name === "polygon") { + for (let j = 0; j < rs.all_points_x.length; ++j) { + if (!_via_img_metadata[_via_image_id].isRegionLocked(element)) { + drawing.polygonDelVertex(element, j); + } + } + } + del_region_count += 1; + } + + if (sorted_sel_reg_id.length) { + _via_reg_canvas.style.cursor = "default"; + } + } + + _via_is_all_region_selected = false; + drawing.setIsRegionSelected(false); //classm + drawing.setUserSelRegionId(-1); //classm + + if (_via_canvas_regions.length === 0) { + // all regions were deleted, hence clear region canvas + drawing.clearCanvas(); + } else { + drawing.redrawRegCanvas(); + } + _via_reg_canvas.focus(); + sidebar.annotation_editor_show(); + + show_message("Reduced points for" + del_region_count + " selected regions"); +} + +function sel_all_regions() { + if ( + _via_display_area_content_name === VIA_DISPLAY_AREA_CONTENT_NAME.IMAGE_GRID + ) { + image_grid_group_toggle_select_all(); + return; + } + + if (!buffer.imgLoaded) { + show_message("First load some images!"); + return; + } + + toggle_all_regions_selection(true); + _via_is_all_region_selected = true; + drawing.redrawRegCanvas(); +} + +function copy_sel_regions() { + if ( + _via_display_area_content_name === VIA_DISPLAY_AREA_CONTENT_NAME.IMAGE_GRID + ) { + return; + } + + if (!buffer.imgLoaded) { + show_message("First load some images!"); + return; + } + + if ( + drawing.isRegionSelected() || //classm + _via_is_all_region_selected + ) { + _via_copied_image_regions.splice(0); + for (var i = 0; i < _via_img_metadata[_via_image_id].regions.length; ++i) { + var img_region = _via_img_metadata[_via_image_id].regions[i]; + var canvas_region = _via_canvas_regions[i]; + if (_via_region_selected_flag.has(i)) { + _via_copied_image_regions.push(clone_image_region(img_region)); + } + } + show_message( + "Copied " + + _via_copied_image_regions.length + + " selected regions. Press Ctrl + v to paste" + ); + } else { + show_message("Select a region first!"); + } +} + +function paste_sel_regions_in_current_image() { + if ( + _via_display_area_content_name === VIA_DISPLAY_AREA_CONTENT_NAME.IMAGE_GRID + ) { + return; + } + + if (!buffer.imgLoaded) { + show_message("First load some images!"); + return; + } + + if (_via_copied_image_regions.length) { + var pasted_reg_count = 0; + for (var i = 0; i < _via_copied_image_regions.length; ++i) { + // ensure copied the regions are within this image's boundaries + var bbox = drawing.getRegionBoundingBox(_via_copied_image_regions[i]); + if ( + bbox[2] < _via_current_image_width && + bbox[3] < _via_current_image_height + ) { + var r = clone_image_region(_via_copied_image_regions[i]); + _via_img_metadata[_via_image_id].regions.push(r); + + pasted_reg_count += 1; + } + } + _via_load_canvas_regions(); + var discarded_reg_count = + _via_copied_image_regions.length - pasted_reg_count; + show_message( + "Pasted " + + pasted_reg_count + + " regions. " + + "Discarded " + + discarded_reg_count + + " regions exceeding image boundary." + ); + drawing.redrawRegCanvas(); + _via_reg_canvas.focus(); + } else { + show_message( + "To paste a region, you first need to select a region and copy it!" + ); + } +} + +function paste_to_multiple_images_with_confirm() { + if ( + _via_display_area_content_name === VIA_DISPLAY_AREA_CONTENT_NAME.IMAGE_GRID + ) { + return; + } + + if (_via_copied_image_regions.length === 0) { + show_message("First copy some regions!"); + return; + } + + var config = { title: "Paste Regions to Multiple Images" }; + var input = { + region_count: { + type: "text", + name: "Number of copied regions", + value: _via_copied_image_regions.length, + disabled: true, + }, + prev_next_count: { + type: "text", + name: 'Copy to (count format)<br><span style="font-size:0.8rem">For example: to paste copied regions to the <i>previous 2 images</i> and <i>next 3 images</i>, type <strong>2,3</strong> in the textbox and to paste only in <i>next 5 images</i>, type <strong>0,5</strong></span>', + placeholder: "2,3", + disabled: false, + size: 30, + }, + img_index_list: { + type: "text", + name: 'Copy to (image index list)<br><span style="font-size:0.8rem">For example: <strong>2-5,7,9</strong> pastes the copied regions to the images with the following id <i>2,3,4,5,7,9</i> and <strong>3,8,141</strong> pastes to the images with id <i>3,8 and 141</i></span>', + placeholder: "2-5,7,9", + disabled: false, + size: 30, + }, + regex: { + type: "text", + name: 'Copy to filenames matching a regular expression<br><span style="font-size:0.8rem">For example: <strong>_large</strong> pastes the copied regions to all images whose filename contain the keyword <i>_large</i></span>', + placeholder: "regular expression", + disabled: false, + size: 30, + }, + include_region_attributes: { + type: "checkbox", + name: "Paste also the region annotations", + checked: true, + }, + }; + + invoke_with_user_inputs(paste_to_multiple_images_confirmed, input, config); +} + +function paste_to_multiple_images_confirmed(input) { + // keep a copy of user inputs for the undo operation + _via_paste_to_multiple_images_input = input; + var intersect = generate_img_index_list(input); + var i; + var total_pasted_region_count = 0; + for (i = 0; i < intersect.length; i++) { + total_pasted_region_count += paste_regions(intersect[i]); + } + + show_message( + "Pasted [" + + total_pasted_region_count + + "] regions " + + "in " + + intersect.length + + " images" + ); + + if (intersect.includes(_via_image_index)) { + _via_load_canvas_regions(); + drawing.redrawRegCanvas(); + _via_reg_canvas.focus(); + } + user_input_default_cancel_handler(); +} + +function paste_regions(img_index) { + var pasted_reg_count = 0; + if (_via_copied_image_regions.length) { + var img_id = _via_image_id_list[img_index]; + var i; + for (i = 0; i < _via_copied_image_regions.length; ++i) { + var r = clone_image_region(_via_copied_image_regions[i]); + _via_img_metadata[img_id].regions.push(r); + + pasted_reg_count += 1; + } + } + return pasted_reg_count; +} + +function del_sel_regions_with_confirm() { + if ( + _via_display_area_content_name === VIA_DISPLAY_AREA_CONTENT_NAME.IMAGE_GRID + ) { + return; + } + + if (_via_copied_image_regions.length === 0) { + show_message("First copy some regions!"); + return; + } + + var prev_next_count, img_index_list, regex; + if (_via_paste_to_multiple_images_input) { + prev_next_count = _via_paste_to_multiple_images_input.prev_next_count.value; + img_index_list = _via_paste_to_multiple_images_input.img_index_list.value; + regex = _via_paste_to_multiple_images_input.regex.value; + } + + var config = { title: "Undo Regions Pasted to Multiple Images" }; + var input = { + region_count: { + type: "text", + name: "Number of regions selected", + value: _via_copied_image_regions.length, + disabled: true, + }, + prev_next_count: { + type: "text", + name: 'Delete from (count format)<br><span style="font-size:0.8rem">For example: to delete copied regions from the <i>previous 2 images</i> and <i>next 3 images</i>, type <strong>2,3</strong> in the textbox and to delete regions only in <i>next 5 images</i>, type <strong>0,5</strong></span>', + placeholder: "2,3", + disabled: false, + size: 30, + value: prev_next_count, + }, + img_index_list: { + type: "text", + name: 'Delete from (image index list)<br><span style="font-size:0.8rem">For example: <strong>2-5,7,9</strong> deletes the copied regions to the images with the following id <i>2,3,4,5,7,9</i> and <strong>3,8,141</strong> deletes regions from the images with id <i>3,8 and 141</i></span>', + placeholder: "2-5,7,9", + disabled: false, + size: 30, + value: img_index_list, + }, + regex: { + type: "text", + name: 'Delete from filenames matching a regular expression<br><span style="font-size:0.8rem">For example: <strong>_large</strong> deletes the copied regions from all images whose filename contain the keyword <i>_large</i></span>', + placeholder: "regular expression", + disabled: false, + size: 30, + value: regex, + }, + }; + + invoke_with_user_inputs(del_sel_regions_confirmed, input, config); +} + +function del_sel_regions_confirmed(input) { + user_input_default_cancel_handler(); + var intersect = generate_img_index_list(input); + var i; + var total_deleted_region_count = 0; + for (i = 0; i < intersect.length; i++) { + total_deleted_region_count += delete_regions(intersect[i]); + } + + show_message( + "Deleted [" + + total_deleted_region_count + + "] regions " + + "in " + + intersect.length + + " images" + ); + + if (intersect.includes(_via_image_index)) { + _via_load_canvas_regions(); + drawing.redrawRegCanvas(); + _via_reg_canvas.focus(); + } +} + +function delete_regions(img_index) { + var del_region_count = 0; + if (_via_copied_image_regions.length) { + var img_id = _via_image_id_list[img_index]; + var i; + for (i = 0; i < _via_copied_image_regions.length; ++i) { + var copied_region_shape_str = JSON.stringify( + _via_copied_image_regions[i].shape_attributes + ); + var j; + // start from last region in order to delete the last pasted region + for (j = _via_img_metadata[img_id].regions.length - 1; j >= 0; --j) { + if ( + JSON.stringify( + _via_img_metadata[img_id].regions[j].shape_attributes + ) === copied_region_shape_str + ) { + _via_img_metadata[img_id].regions.splice(j, 1); + del_region_count += 1; + break; // delete only one matching region + } + } + } + } + return del_region_count; +} + +function show_first_image() { + if ( + _via_display_area_content_name === VIA_DISPLAY_AREA_CONTENT_NAME.IMAGE_GRID + ) { + if (_via_image_grid_group_var.length) { + image_grid_group_prev({ value: 0 }); // simulate button click + } else { + show_message( + 'First, create groups by selecting items from "Group by" dropdown list' + ); + } + return; + } + + if (_via_img_count > 0) { + buffer.showImage(_via_img_fn_list_img_index_list[0]); + } +} + +function show_last_image() { + if ( + _via_display_area_content_name === VIA_DISPLAY_AREA_CONTENT_NAME.IMAGE_GRID + ) { + if (_via_image_grid_group_var.length) { + image_grid_group_prev({ value: _via_image_grid_group_var.length - 1 }); // simulate button click + } else { + show_message( + 'First, create groups by selecting items from "Group by" dropdown list' + ); + } + return; + } + + if (_via_img_count > 0) { + var last_img_index = _via_img_fn_list_img_index_list.length - 1; + buffer.showImage(_via_img_fn_list_img_index_list[last_img_index]); + } +} + +function jump_image_block_get_count() { + var n = _via_img_fn_list_img_index_list.length; + if (n < 20) { + return 2; + } + if (n < 100) { + return 10; + } + if (n < 1000) { + return 25; + } + if (n < 5000) { + return 50; + } + if (n < 10000) { + return 100; + } + if (n < 50000) { + return 500; + } + + return Math.round(n / 50); +} + +function jump_to_next_image_block() { + if ( + _via_display_area_content_name === VIA_DISPLAY_AREA_CONTENT_NAME.IMAGE_GRID + ) { + return; + } + + var jump_count = jump_image_block_get_count(); + if (jump_count > 1) { + var current_img_index = _via_image_index; + if (_via_img_fn_list_img_index_list.includes(current_img_index)) { + var list_index = + _via_img_fn_list_img_index_list.indexOf(current_img_index); + var next_list_index = list_index + jump_count; + if (next_list_index + 1 > _via_img_fn_list_img_index_list.length) { + next_list_index = 0; + } + var next_img_index = _via_img_fn_list_img_index_list[next_list_index]; + buffer.showImage(next_img_index); + } + } else { + move_to_next_image(); + } +} + +function jump_to_prev_image_block() { + if ( + _via_display_area_content_name === VIA_DISPLAY_AREA_CONTENT_NAME.IMAGE_GRID + ) { + return; + } + + var jump_count = jump_image_block_get_count(); + if (jump_count > 1) { + var current_img_index = _via_image_index; + if (_via_img_fn_list_img_index_list.includes(current_img_index)) { + var list_index = + _via_img_fn_list_img_index_list.indexOf(current_img_index); + var prev_list_index = list_index - jump_count; + if (prev_list_index < 0) { + prev_list_index = _via_img_fn_list_img_index_list.length - 1; + } + var prev_img_index = _via_img_fn_list_img_index_list[prev_list_index]; + buffer.showImage(prev_img_index); + } + } else { + move_to_prev_image(); + } +} + +function move_to_prev_image() { + if ( + _via_display_area_content_name === VIA_DISPLAY_AREA_CONTENT_NAME.IMAGE_GRID + ) { + if (_via_image_grid_group_var.length) { + var last_group_index = _via_image_grid_group_var.length - 1; + image_grid_group_prev({ value: last_group_index }); // simulate button click + } else { + show_message( + 'First, create groups by selecting items from "Group by" dropdown list' + ); + } + return; + } + + if (_via_img_count > 0) { + var current_img_index = _via_image_index; + if (_via_img_fn_list_img_index_list.includes(current_img_index)) { + var list_index = + _via_img_fn_list_img_index_list.indexOf(current_img_index); + var next_list_index = list_index - 1; + if (next_list_index === -1) { + next_list_index = _via_img_fn_list_img_index_list.length - 1; + } + var next_img_index = _via_img_fn_list_img_index_list[next_list_index]; + buffer.showImage(next_img_index); + } else { + if (_via_img_fn_list_img_index_list.length === 0) { + show_message("Filtered file list does not any files!"); + } else { + buffer.showImage(_via_img_fn_list_img_index_list[0]); + } + } + + if (typeof _via_hook_prev_image === "function") { + _via_hook_prev_image(current_img_index); + } + } +} + +function move_to_next_image() { + if ( + _via_display_area_content_name === VIA_DISPLAY_AREA_CONTENT_NAME.IMAGE_GRID + ) { + if (_via_image_grid_group_var.length) { + var last_group_index = _via_image_grid_group_var.length - 1; + image_grid_group_next({ value: last_group_index }); // simulate button click + } else { + show_message( + 'First, create groups by selecting items from "Group by" dropdown list' + ); + } + return; + } + + if (_via_img_count > 0) { + var current_img_index = _via_image_index; + if (_via_img_fn_list_img_index_list.includes(current_img_index)) { + var list_index = + _via_img_fn_list_img_index_list.indexOf(current_img_index); + var next_list_index = list_index + 1; + if (next_list_index === _via_img_fn_list_img_index_list.length) { + next_list_index = 0; + } + var next_img_index = _via_img_fn_list_img_index_list[next_list_index]; + buffer.showImage(next_img_index); + } else { + if (_via_img_fn_list_img_index_list.length === 0) { + show_message("Filtered file list does not contain any files!"); + } else { + buffer.showImage(_via_img_fn_list_img_index_list[0]); + } + } + + if (typeof _via_hook_next_image === "function") { + _via_hook_next_image(current_img_index); + } + } +} + +function toggle_region_boundary_visibility() { + if (_via_display_area_content_name === VIA_DISPLAY_AREA_CONTENT_NAME.IMAGE) { + _via_is_region_boundary_visible = !_via_is_region_boundary_visible; + drawing.redrawRegCanvas(); + _via_reg_canvas.focus(); + } + + if ( + _via_display_area_content_name === VIA_DISPLAY_AREA_CONTENT_NAME.IMAGE_GRID + ) { + if (_via_settings.ui.image_grid.show_region_shape) { + _via_settings.ui.image_grid.show_region_shape = false; + document.getElementById("image_grid_content_rshape").innerHTML = ""; + } else { + _via_settings.ui.image_grid.show_region_shape = true; + image_grid_page_show_all_regions(); + } + } +} + +function toggle_region_id_visibility() { + _via_is_region_id_visible = !_via_is_region_id_visible; + drawing.redrawRegCanvas(); + _via_reg_canvas.focus(); +} + +function toggle_region_info_visibility() { + var elem = document.getElementById("region_info"); + // toggle between displaying and not displaying + if (elem.classList.contains("d-none")) { + elem.classList.remove("d-none"); + _via_is_region_info_visible = true; + } else { + elem.classList.add("d-none"); //modSote + _via_is_region_info_visible = false; + } +} + +function region_visualisation_update(type, default_id, next_offset) { + var attr_list = [default_id]; + attr_list = attr_list.concat(Object.keys(project.attributes.region)); + var n = attr_list.length; + var current_index = attr_list.indexOf(_via_settings.ui.image[type]); + var new_index; + if (current_index !== -1) { + new_index = current_index + next_offset; + + if (new_index < 0) { + new_index = n + new_index; + } + if (new_index >= n) { + new_index = new_index - n; + } + switch (type) { + case "region_label": + _via_settings.ui.image.region_label = attr_list[new_index]; + drawing.redrawRegCanvas(); + break; + case "region_color": + _via_settings.ui.image.region_color = attr_list[new_index]; + drawing.regionsGroupColorInit(); + drawing.redrawRegCanvas(); + } + + var type_str = type.replace("_", " "); + if (_via_settings.ui.image[type].startsWith("__via")) { + show_message(type_str + " cleared"); + } else { + show_message( + type_str + + " set to region attribute [" + + _via_settings.ui.image[type] + + "]" + ); + } + } +} + +// +// left sidebar toolbox maintainer +// + +function leftsidebar_toggle() { + $("#sidebar_container").toggle("fast"); + drawing.updateUiComponents(); +} + +// +// region and file attributes update panel +// +function attribute_update_panel_set_active_button() { + var attribute_type; + for (attribute_type in project.attributes) { + var bid = "button_show_" + attribute_type + "_attributes"; + document.getElementById(bid).classList.remove("active"); + } + var bid = "button_show_" + _via_attribute_being_updated + "_attributes"; + document.getElementById(bid).classList.add("active"); +} + +function show_region_attributes_update_panel() { + _via_attribute_being_updated = "region"; + var rattr_list = Object.keys(project.attributes.region); + if (rattr_list.length) { + _via_current_attribute_id = rattr_list[0]; + } else { + _via_current_attribute_id = ""; + } + update_attributes_update_panel(); + attribute_update_panel_set_active_button(); +} + +function show_file_attributes_update_panel() { + _via_attribute_being_updated = "file"; + var fattr_list = Object.keys(project.attributes.file); + if (fattr_list.length) { + _via_current_attribute_id = fattr_list[0]; + } else { + _via_current_attribute_id = ""; + } + update_attributes_update_panel(); + attribute_update_panel_set_active_button(); +} + +function update_attributes_name_list() { + var p = document.getElementById("attributes_name_list"); + p.innerHTML = ""; + + var attr; + for (attr in project.attributes[_via_attribute_being_updated]) { + var option = document.createElement("option"); + option.setAttribute("value", attr); + option.innerHTML = attr; + if (attr === _via_current_attribute_id) { + option.setAttribute("selected", "selected"); + } + p.appendChild(option); + } +} + +function update_attributes_update_panel() { + update_attributes_name_list(); + show_attribute_properties(); + show_attribute_options(); + show_attribute_example(); +} + +function update_attribute_properties_panel() { + if ( + !document + .getElementById("settings_panel") + .classList.contains("d-none") + ) { + show_attribute_properties(); + show_attribute_options(); + show_attribute_example(); + } +} + +function show_attribute_example() { + let p = $("#attributes_example"); + p.addClass("d-none"); + p.html(""); + let e = $("<table>", { + class: "table table-sm table-borderless", + id: "annotation_editor_example_table", + }); + let tbody = $("<tbody>"); + tbody.append(sidebar.annotation_editor_get_metadata_row_html(100, false, true)); + e.append(sidebar.annotation_editor_update_header_html(true)); + e.append(tbody); + p.append(e); + + p.removeClass("d-none"); +} + +function show_attribute_properties() { + let attr_list = document.getElementById("attributes_name_list"); + let prop = document.getElementById("attribute_properties"); + prop.classList.add("d-none"); + prop.innerHTML = ""; + + if (attr_list.options.length === 0) { + return; + } + + if ( + typeof _via_current_attribute_id === "undefined" || + _via_current_attribute_id === "" + ) { + _via_current_attribute_id = attr_list.options[0].value; + } + + let attr_id = _via_current_attribute_id; + let attr_type = _via_attribute_being_updated; + let attr_input_type = project.attributes[attr_type][attr_id].type; + let attr_desc = project.attributes[attr_type][attr_id].description; + + attribute_property_add_input_property( + "Name of attribute (appears in exported annotations)", + "Name", + attr_id, + "attribute_name", + prop + ); + attribute_property_add_input_property( + "Description of attribute (shown to user during annotation session)", + "Desc.", + attr_desc, + "attribute_description", + prop + ); + + if (attr_input_type === "text") { + var attr_default_value = project.attributes[attr_type][attr_id].default_value; + attribute_property_add_input_property( + "Default value of this attribute", + "Def.", + attr_default_value, + "attribute_default_value", + prop + ); + } + + // add dropdown for type of attribute + let p = document.createElement("div"); + p.setAttribute("class", "input-group mb-1"); + let c0 = document.createElement("span"); + c0.setAttribute("class", "input-group-text"); + c0.setAttribute("title", "Attribute type (e.g. text, checkbox, radio, etc)"); + c0.innerHTML = "Type"; + let c1b = document.createElement("select"); + c1b.setAttribute("class", "form-select") + c1b.setAttribute("aria-label", "Attribute type"); + c1b.setAttribute("onchange", "attribute_property_on_update(this)"); + c1b.setAttribute("id", "attribute_type"); + let type_id; + for (type_id in VIA_ATTRIBUTE_TYPE) { + let type = VIA_ATTRIBUTE_TYPE[type_id]; + let option = document.createElement("option"); + option.setAttribute("value", type); + option.innerHTML = type; + if (attr_input_type == type) { + option.setAttribute("selected", "selected"); + } + c1b.appendChild(option); + } + p.appendChild(c0); + p.appendChild(c1b); + document.getElementById("attribute_properties").appendChild(p); + prop.classList.remove("d-none"); +} + +function show_attribute_options() { + let attr_list = document.getElementById("attributes_name_list"); + let opt = document.getElementById("attribute_options") + opt.classList.add("d-none"); + opt.innerHTML = ""; + if (attr_list.options.length === 0) { + return; + } + + let attr_id = attr_list.value; + let attr_type = project.attributes[_via_attribute_being_updated][attr_id].type; + + // populate additional options based on attribute type + switch (attr_type) { + case VIA_ATTRIBUTE_TYPE.TEXT: + // text does not have any additional properties + break; + case VIA_ATTRIBUTE_TYPE.IMAGE: + var p = document.createElement("div"); + p.setAttribute("class", "row mt-2"); + var c0 = document.createElement("span"); + c0.setAttribute("class", "col"); + c0.setAttribute( + "title", + "When selected, this is the value that appears in exported annotations" + ); + c0.innerHTML = "id"; + var c1 = document.createElement("span"); + c1.setAttribute("class", "col"); + c1.setAttribute( + "title", + "URL or base64 (see https://www.base64-image.de/) encoded image data that corresponds to the image shown as an option to the annotator" + ); + c1.innerHTML = "image url or b64"; + var c2 = document.createElement("span"); + c2.setAttribute("class", "col"); + c2.setAttribute("title", "The default value of this attribute"); + c2.innerHTML = "def."; + p.appendChild(c0); + p.appendChild(c1); + p.appendChild(c2); + document.getElementById("attribute_options").appendChild(p); + + var options = + project.attributes[_via_attribute_being_updated][attr_id].options; + var option_id; + for (option_id in options) { + var option_desc = options[option_id]; + + var option_default = + project.attributes[_via_attribute_being_updated][attr_id] + .default_options[option_id]; + attribute_property_add_option( + attr_id, + option_id, + option_desc, + option_default, + attr_type + ); + } + attribute_property_add_new_entry_option(attr_id, attr_type); + break; + case VIA_ATTRIBUTE_TYPE.CHECKBOX: // handled by next case + case VIA_ATTRIBUTE_TYPE.DROPDOWN: // handled by next case + case VIA_ATTRIBUTE_TYPE.RADIO: + var p = document.createElement("div"); + p.setAttribute("class", "row mt-2"); + var c0 = document.createElement("span"); + c0.setAttribute("class", "col"); + c0.setAttribute( + "title", + "When selected, this is the value that appears in exported annotations" + ); + c0.innerHTML = "id"; + var c1 = document.createElement("span"); + c1.setAttribute("class", "col"); + c1.setAttribute( + "title", + "This is the text shown as an option to the annotator" + ); + c1.innerHTML = "description"; + var c2 = document.createElement("span"); + c2.setAttribute("title", "The default value of this attribute"); + c2.setAttribute("class", "col"); + c2.innerHTML = "def."; + p.appendChild(c0); + p.appendChild(c1); + p.appendChild(c2); + document.getElementById("attribute_options").appendChild(p); + + var options = + project.attributes[_via_attribute_being_updated][attr_id].options; + var option_id; + for (option_id in options) { + var option_desc = options[option_id]; + + var option_default = + project.attributes[_via_attribute_being_updated][attr_id] + .default_options[option_id]; + attribute_property_add_option( + attr_id, + option_id, + option_desc, + option_default, + attr_type + ); + } + attribute_property_add_new_entry_option(attr_id, attr_type); + break; + default: + console.log("Attribute type " + attr_type + " is unavailable"); + } + opt.classList.remove("d-none"); +} + +function attribute_property_add_input_property(title, name, value, id, prop) { + let p = document.createElement("div"); + p.setAttribute("class", "input-group mb-1"); + let c0 = document.createElement("span"); + c0.setAttribute("title", title); + c0.setAttribute("class", "input-group-text") + c0.innerHTML = name; + let c1b = document.createElement("input"); + c1b.setAttribute("onchange", "attribute_property_on_update(this)"); + c1b.setAttribute("type", "text"); + c1b.setAttribute("class", "form-control"); + if (typeof value !== "undefined") { + c1b.setAttribute("value", value); + } + c1b.setAttribute("id", id); + p.appendChild(c0); + p.appendChild(c1b); + + prop.appendChild(p); +} + +function attribute_property_add_option( + attr_id, + option_id, + option_desc, + option_default, + attribute_type +) { + let p = document.createElement("div"); + p.setAttribute("class", "row mb-1"); + var c0 = document.createElement("div"); + c0.setAttribute("class", "col") + var c0b = document.createElement("input"); + c0b.setAttribute("type", "text"); + c0b.setAttribute("value", option_id); + c0b.setAttribute("title", option_id); + c0b.setAttribute("onchange", "attribute_property_on_option_update(this)"); + c0b.setAttribute("id", "_via_attribute_option_id_" + option_id); + c0b.setAttribute("class", "col form-control"); + + var c1 = document.createElement("div"); + c1.setAttribute("class", "col") + var c1b = document.createElement("input"); + c1b.setAttribute("type", "text"); + c1b.setAttribute("class", "form-control") + + if (attribute_type === VIA_ATTRIBUTE_TYPE.IMAGE) { + var option_desc_info = option_desc.length + " bytes of base64 image data"; + c1b.setAttribute("value", option_desc_info); + c1b.setAttribute( + "title", + "To update, copy and paste base64 image data in this text box" + ); + } else { + c1b.setAttribute("value", option_desc); + c1b.setAttribute("title", option_desc); + } + c1b.setAttribute("onchange", "attribute_property_on_option_update(this)"); + c1b.setAttribute("id", "_via_attribute_option_description_" + option_id); + + var c2 = document.createElement("div"); + c2.setAttribute("class", "col"); + var c2b = document.createElement("input"); + c2b.setAttribute("type", attribute_type); + c2b.setAttribute("class", "form-check-input"); + if (typeof option_default !== "undefined") { + c2b.checked = option_default; + } + if ( + attribute_type === "radio" || + attribute_type === "image" || + attribute_type === "dropdown" + ) { + // ensured that user can activate only one radio button + c2b.setAttribute("type", "radio"); + c2b.setAttribute("name", attr_id); + } + + c2b.setAttribute("onchange", "attribute_property_on_option_update(this)"); + c2b.setAttribute("id", "_via_attribute_option_default_" + option_id); + + c0.appendChild(c0b); + c1.appendChild(c1b); + c2.appendChild(c2b); + p.appendChild(c0); + p.appendChild(c1); + p.appendChild(c2); + + document.getElementById("attribute_options").appendChild(p); +} + +function attribute_property_add_new_entry_option(attr_id, attribute_type) { + var p = document.createElement("div"); + p.setAttribute("class", "row") + let c0 = document.createElement("div"); + c0.setAttribute("class", "col mt-2 mb-3"); + let c0b = document.createElement("input"); + c0b.setAttribute("type", "text"); + c0b.setAttribute("onchange", "attribute_property_on_option_add(this)"); + c0b.setAttribute("id", "_via_attribute_new_option_id"); + c0b.setAttribute("placeholder", "Add new option id"); + c0b.setAttribute("class", "form-control") + c0.appendChild(c0b); + p.appendChild(c0); + document.getElementById("attribute_options").appendChild(p); +} + +function attribute_property_on_update(p) { + var attr_id = get_current_attribute_id(); + var attr_type = _via_attribute_being_updated; + var new_attr_type = p.value; + + switch (p.id) { + case "attribute_name": + if (new_attr_type !== attr_id) { + Object.defineProperty( + project.attributes[attr_type], + new_attr_type, + Object.getOwnPropertyDescriptor(project.attributes[attr_type], attr_id) + ); + + delete project.attributes[attr_type][attr_id]; + update_attributes_update_panel(); + sidebar.annotation_editor_update_content(); + } + break; + case "attribute_description": + project.attributes[attr_type][attr_id].description = new_attr_type; + update_attributes_update_panel(); + sidebar.annotation_editor_update_content(); + break; + case "attribute_default_value": + project.attributes[attr_type][attr_id].default_value = new_attr_type; + update_attributes_update_panel(); + sidebar.annotation_editor_update_content(); + break; + case "attribute_type": + var old_attr_type = project.attributes[attr_type][attr_id].type; + project.attributes[attr_type][attr_id].type = new_attr_type; + if (new_attr_type === VIA_ATTRIBUTE_TYPE.TEXT) { + project.attributes[attr_type][attr_id].default_value = ""; + delete project.attributes[attr_type][attr_id].options; + delete project.attributes[attr_type][attr_id].default_options; + } else { + // add options entry (if missing) + if (!project.attributes[attr_type][attr_id].hasOwnProperty("options")) { + project.attributes[attr_type][attr_id].options = {}; + project.attributes[attr_type][attr_id].default_options = {}; + } + if ( + project.attributes[attr_type][attr_id].hasOwnProperty("default_value") + ) { + delete project.attributes[attr_type][attr_id].default_value; + } + + // 1. gather all the attribute values in existing metadata + var existing_attr_values = attribute_get_unique_values( + attr_type, + attr_id + ); + + // 2. for checkbox, radio, dropdown: create options based on existing options and existing values + for (var option_id in project.attributes[attr_type][attr_id]["options"]) { + if (!existing_attr_values.includes(option_id)) { + project.attributes[attr_type][attr_id]["options"][option_id] = + option_id; + } + } + + // update existing metadata to reflect changes in attribute type + // ensure that attribute has only one value + for (var img_id in _via_img_metadata) { + for (var rindex in _via_img_metadata[img_id]["regions"]) { + if ( + _via_img_metadata[img_id]["regions"][rindex][ + "region_attributes" + ].hasOwnProperty(attr_id) + ) { + if ( + old_attr_type === VIA_ATTRIBUTE_TYPE.CHECKBOX && + (new_attr_type === VIA_ATTRIBUTE_TYPE.RADIO || + new_attr_type === VIA_ATTRIBUTE_TYPE.DROPDOWN) + ) { + // add only if checkbox has only single option selected + var sel_option_count = 0; + var sel_option_id; + for (var option_id in _via_img_metadata[img_id]["regions"][ + rindex + ]["region_attributes"][attr_id]) { + if ( + _via_img_metadata[img_id]["regions"][rindex][ + "region_attributes" + ][attr_id][option_id] + ) { + sel_option_count = sel_option_count + 1; + sel_option_id = option_id; + } + } + if (sel_option_count === 1) { + _via_img_metadata[img_id]["regions"][rindex][ + "region_attributes" + ][attr_id] = sel_option_id; + } else { + // delete as multiple options cannot be represented as radio or dropdown + delete _via_img_metadata[img_id]["regions"][rindex][ + "region_attributes" + ][attr_id]; + } + } + if ( + (old_attr_type === VIA_ATTRIBUTE_TYPE.RADIO || + old_attr_type === VIA_ATTRIBUTE_TYPE.DROPDOWN) && + new_attr_type === VIA_ATTRIBUTE_TYPE.CHECKBOX + ) { + var old_option_id = + _via_img_metadata[img_id]["regions"][rindex][ + "region_attributes" + ][attr_id]; + _via_img_metadata[img_id]["regions"][rindex][ + "region_attributes" + ][attr_id] = {}; + _via_img_metadata[img_id]["regions"][rindex][ + "region_attributes" + ][attr_id][old_option_id] = true; + } + } + } + } + } + show_attribute_properties(); + show_attribute_options(); + show_attribute_example(); + sidebar.annotation_editor_update_content(); + break; + } +} + +function attribute_get_unique_values(attr_type, attr_id) { + var values = []; + switch (attr_type) { + case "file": + var img_id, attr_val; + for (img_id in _via_img_metadata) { + if (_via_img_metadata[img_id].file_attributes.hasOwnProperty(attr_id)) { + attr_val = _via_img_metadata[img_id].file_attributes[attr_id]; + if (!values.includes(attr_val)) { + values.push(attr_val); + } + } + } + break; + case "region": + var img_id, attr_val, i; + for (img_id in _via_img_metadata) { + for (i = 0; i < _via_img_metadata[img_id].regions.length; ++i) { + if ( + _via_img_metadata[img_id].regions[ + i + ].region_attributes.hasOwnProperty(attr_id) + ) { + attr_val = + _via_img_metadata[img_id].regions[i].region_attributes[attr_id]; + if (typeof attr_val === "object") { + for (var option_id in _via_img_metadata[img_id].regions[i] + .region_attributes[attr_id]) { + if (!values.includes(option_id)) { + values.push(option_id); + } + } + } else { + if (!values.includes(attr_val)) { + values.push(attr_val); + } + } + } + } + } + break; + default: + break; + } + return values; +} + +function attribute_property_on_option_update(p) { + var attr_id = get_current_attribute_id(); + if (p.id.startsWith("_via_attribute_option_id_")) { + var old_key = p.id.substring("_via_attribute_option_id_".length); + var new_key = p.value; + if (old_key !== new_key) { + var option_id_test = attribute_property_option_id_is_valid( + attr_id, + new_key + ); + if (option_id_test.is_valid) { + update_attribute_option_id_with_confirm( + _via_attribute_being_updated, + attr_id, + old_key, + new_key + ); + } else { + p.value = old_key; // restore old value + show_message(option_id_test.message); + show_attribute_properties(); + } + return; + } + } + + if (p.id.startsWith("_via_attribute_option_description_")) { + var key = p.id.substring("_via_attribute_option_description_".length); + var old_value = + project.attributes[_via_attribute_being_updated][attr_id].options[key]; + var new_value = p.value; + if (new_value !== old_value) { + project.attributes[_via_attribute_being_updated][attr_id].options[key] = + new_value; + show_attribute_properties(); + sidebar.annotation_editor_update_content(); + } + } + + if (p.id.startsWith("_via_attribute_option_default_")) { + var new_default_option_id = p.id.substring( + "_via_attribute_option_default_".length + ); + var old_default_option_id_list = Object.keys( + project.attributes[_via_attribute_being_updated][attr_id].default_options + ); + + if (old_default_option_id_list.length === 0) { + // default set for the first time + project.attributes[_via_attribute_being_updated][attr_id].default_options[ + new_default_option_id + ] = p.checked; + } else { + switch (project.attributes[_via_attribute_being_updated][attr_id].type) { + case "image": // fallback + case "dropdown": // fallback + case "radio": // fallback + // to ensure that only one radio button is selected at a time + project.attributes[_via_attribute_being_updated][ + attr_id + ].default_options = {}; + project.attributes[_via_attribute_being_updated][ + attr_id + ].default_options[new_default_option_id] = p.checked; + break; + case "checkbox": + project.attributes[_via_attribute_being_updated][ + attr_id + ].default_options[new_default_option_id] = p.checked; + break; + } + } + // default option updated + attribute_property_on_option_default_update( + _via_attribute_being_updated, + attr_id, + new_default_option_id + ).then(function () { + show_attribute_properties(); + sidebar.annotation_editor_update_content(); + }); + } +} + +function attribute_property_on_option_default_update( + attribute_being_updated, + attr_id, + new_default_option_id +) { + return new Promise(function (ok_callback, err_callback) { + // set all metadata to new_value if: + // - metadata[attr_id] is missing + // - metadata[attr_id] is set to option_old_value + var img_id, attr_value, n, i; + var attr_type = project.attributes[attribute_being_updated][attr_id].type; + switch (attribute_being_updated) { + case "file": + for (img_id in _via_img_metadata) { + if ( + !_via_img_metadata[img_id].file_attributes.hasOwnProperty(attr_id) + ) { + _via_img_metadata[img_id].file_attributes[attr_id] = + new_default_option_id; + } + } + break; + case "region": + for (img_id in _via_img_metadata) { + n = _via_img_metadata[img_id].regions.length; + for (i = 0; i < n; ++i) { + if ( + !_via_img_metadata[img_id].regions[ + i + ].region_attributes.hasOwnProperty(attr_id) + ) { + _via_img_metadata[img_id].regions[i].region_attributes[attr_id] = + new_default_option_id; + } + } + } + break; + } + ok_callback(); + }); +} + +function attribute_property_on_option_add(p) { + if (p.value === "" || p.value === null) { + return; + } + + if (p.id === "_via_attribute_new_option_id") { + var attr_id = get_current_attribute_id(); + var option_id = p.value; + var option_id_test = attribute_property_option_id_is_valid( + attr_id, + option_id + ); + if (option_id_test.is_valid) { + project.attributes[_via_attribute_being_updated][attr_id].options[ + option_id + ] = ""; + show_attribute_options(); + sidebar.annotation_editor_update_content(); + } else { + show_message(option_id_test.message); + attribute_property_reset_new_entry_inputs(); + } + } +} + +function attribute_property_reset_new_entry_inputs() { + var container = document.getElementById("attribute_options"); + var p = container.lastChild; + if (p.childNodes[0]) { + p.childNodes[0].value = ""; + } + if (p.childNodes[1]) { + p.childNodes[1].value = ""; + } +} + +function attribute_property_show_new_entry_inputs(attr_id, attribute_type) { + var n0 = document.createElement("div"); + n0.classList.add("property"); + var n1a = document.createElement("span"); + var n1b = document.createElement("input"); + n1b.setAttribute("onchange", "attribute_property_on_option_add(this)"); + n1b.setAttribute("placeholder", "Add new id"); + n1b.setAttribute("value", ""); + n1b.setAttribute("id", "_via_attribute_new_option_id"); + n1a.appendChild(n1b); + + var n2a = document.createElement("span"); + var n2b = document.createElement("input"); + n2b.setAttribute("onchange", "attribute_property_on_option_add(this)"); + n2b.setAttribute("placeholder", "Optional description"); + n2b.setAttribute("value", ""); + n2b.setAttribute("id", "_via_attribute_new_option_description"); + n2a.appendChild(n2b); + + var n3a = document.createElement("span"); + var n3b = document.createElement("input"); + n3b.setAttribute("type", attribute_type); + if (attribute_type === "radio") { + n3b.setAttribute("name", attr_id); + } + n3b.setAttribute("onchange", "attribute_property_on_option_add(this)"); + n3b.setAttribute("id", "_via_attribute_new_option_default"); + n3a.appendChild(n3b); + + n0.appendChild(n1a); + n0.appendChild(n2a); + n0.appendChild(n3a); + + var container = document.getElementById("attribute_options"); + container.appendChild(n0); +} + +function attribute_property_option_id_is_valid(attr_id, new_option_id) { + var option_id; + for (option_id in project.attributes[_via_attribute_being_updated][attr_id] + .options) { + if (option_id === new_option_id) { + return { + is_valid: false, + message: "Option id [" + attr_id + "] already exists", + }; + } + } + + if (new_option_id.includes("__")) { + // reserved separator for attribute-id, row-id, option-id + return { + is_valid: false, + message: "Option id cannot contain two consecutive underscores", + }; + } + + return { is_valid: true }; +} + +function attribute_property_id_exists(name) { + var attr_name; + for (attr_name in project.attributes[_via_attribute_being_updated]) { + if (attr_name === name) { + return true; + } + } + return false; +} + +function delete_existing_attribute_with_confirm() { + var attr_id = document.getElementById("user_input_attribute_id").value; + if (attr_id === "") { + show_message("Enter the name of attribute that you wish to delete"); + return; + } + if (attribute_property_id_exists(attr_id)) { + var config = { + title: + "Delete " + + _via_attribute_being_updated + + " attribute [" + + attr_id + + "]", + warning: + "Warning: Deleting an attribute will lead to the attribute being deleted in all the annotations. Please click OK only if you are sure.", + }; + var input = { + attr_type: { + type: "text", + name: "Attribute Type", + value: _via_attribute_being_updated, + disabled: true, + }, + attr_id: { + type: "text", + name: "Attribute Id", + value: attr_id, + disabled: true, + }, + }; + invoke_with_user_inputs(delete_existing_attribute_confirmed, input, config); + } else { + show_message("Attribute [" + attr_id + "] does not exist!"); + } +} + +function delete_existing_attribute_confirmed(input) { + var attr_type = input.attr_type.value; + var attr_id = input.attr_id.value; + delete_existing_attribute(attr_type, attr_id); + document.getElementById("user_input_attribute_id").value = ""; + show_message("Deleted " + attr_type + " attribute [" + attr_id + "]"); + user_input_default_cancel_handler(); +} + +function delete_existing_attribute(attribute_type, attr_id) { + if (project.attributes[attribute_type].hasOwnProperty(attr_id)) { + var attr_id_list = Object.keys(project.attributes[attribute_type]); + if (attr_id_list.length === 1) { + _via_current_attribute_id = ""; + } else { + var current_index = attr_id_list.indexOf(attr_id); + var next_index = current_index + 1; + if (next_index === attr_id_list.length) { + next_index = current_index - 1; + } + _via_current_attribute_id = attr_id_list[next_index]; + } + delete project.attributes[attribute_type][attr_id]; + delete_region_attribute_in_all_metadata(attr_id); + update_attributes_update_panel(); + sidebar.annotation_editor_update_content(); + } +} + +function add_new_attribute_from_user_input() { + var attr_id = document.getElementById("user_input_attribute_id").value; + if (attr_id === "") { + show_message("Enter the name of attribute that you wish to delete"); + return; + } + + if (attribute_property_id_exists(attr_id)) { + show_message( + "The " + + _via_attribute_being_updated + + " attribute [" + + attr_id + + "] already exists." + ); + } else { + _via_current_attribute_id = attr_id; + add_new_attribute(attr_id); + update_attributes_update_panel(); + sidebar.annotation_editor_update_content(); + show_message( + "Added " + _via_attribute_being_updated + " attribute [" + attr_id + "]." + ); + } +} + +function add_new_attribute(attribute_id) { + project.attributes[_via_attribute_being_updated][attribute_id] = {}; + project.attributes[_via_attribute_being_updated][attribute_id].type = "text"; + project.attributes[_via_attribute_being_updated][attribute_id].description = ""; + project.attributes[_via_attribute_being_updated][attribute_id].default_value = + ""; +} + +function update_current_attribute_id(p) { + _via_current_attribute_id = p.options[p.selectedIndex].value; + update_attribute_properties_panel(); +} + +function get_current_attribute_id() { + return document.getElementById("attributes_name_list").value; +} + +function update_attribute_option_id_with_confirm( + attr_type, + attr_id, + option_id, + new_option_id +) { + var is_delete = false; + var config; + if (new_option_id === "" || typeof new_option_id === "undefined") { + // an empty new_option_id indicates deletion of option_id + config = { title: "Delete an option for " + attr_type + " attribute" }; + is_delete = true; + } else { + config = { title: "Rename an option for " + attr_type + " attribute" }; + } + + var input = { + attr_type: { + type: "text", + name: "Attribute Type", + value: attr_type, + disabled: true, + }, + attr_id: { + type: "text", + name: "Attribute Id", + value: attr_id, + disabled: true, + }, + }; + + if (is_delete) { + input["option_id"] = { + type: "text", + name: "Attribute Option", + value: option_id, + disabled: true, + }; + } else { + (input["option_id"] = { + type: "text", + name: "Attribute Option (old)", + value: option_id, + disabled: true, + }), + (input["new_option_id"] = { + type: "text", + name: "Attribute Option (new)", + value: new_option_id, + disabled: true, + }); + } + + invoke_with_user_inputs( + update_attribute_option_id_confirmed, + input, + config, + update_attribute_option_id_cancel + ); +} + +function update_attribute_option_id_cancel(input) { + update_attribute_properties_panel(); +} + +function update_attribute_option_id_confirmed(input) { + var attr_type = input.attr_type.value; + var attr_id = input.attr_id.value; + var option_id = input.option_id.value; + var is_delete; + var new_option_id; + if ( + typeof input.new_option_id === "undefined" || + input.new_option_id === "" + ) { + is_delete = true; + new_option_id = ""; + } else { + is_delete = false; + new_option_id = input.new_option_id.value; + } + + update_attribute_option( + is_delete, + attr_type, + attr_id, + option_id, + new_option_id + ); + + if (is_delete) { + show_message( + "Deleted option [" + + option_id + + "] for " + + attr_type + + " attribute [" + + attr_id + + "]." + ); + } else { + show_message( + "Renamed option [" + + option_id + + "] to [" + + new_option_id + + "] for " + + attr_type + + " attribute [" + + attr_id + + "]." + ); + } + update_attribute_properties_panel(); + sidebar.annotation_editor_update_content(); + user_input_default_cancel_handler(); +} + +function update_attribute_option( + is_delete, + attr_type, + attr_id, + option_id, + new_option_id +) { + switch (attr_type) { + case "region": + update_region_attribute_option_in_all_metadata( + is_delete, + attr_id, + option_id, + new_option_id + ); + if (!is_delete) { + Object.defineProperty( + project.attributes[attr_type][attr_id].options, + new_option_id, + Object.getOwnPropertyDescriptor( + project.attributes[_via_attribute_being_updated][attr_id].options, + option_id + ) + ); + } + delete project.attributes.region[attr_id].options[option_id]; + + break; + case "file": + update_file_attribute_option_in_all_metadata(attr_id, option_id); + if (!is_delete) { + Object.defineProperty( + project.attributes[attr_type][attr_id].options, + new_option_id, + Object.getOwnPropertyDescriptor( + project.attributes[_via_attribute_being_updated][attr_id].options, + option_id + ) + ); + } + + delete project.attributes.file[attr_id].options[option_id]; + break; + } +} + +function update_file_attribute_option_in_all_metadata( + is_delete, + attr_id, + option_id, + new_option_id +) { + var image_id; + for (image_id in _via_img_metadata) { + if (_via_img_metadata[image_id].file_attributes.hasOwnProperty(attr_id)) { + if ( + _via_img_metadata[image_id].file_attributes[attr_id].hasOwnProperty( + option_id + ) + ) { + Object.defineProperty( + _via_img_metadata[image_id].file_attributes[attr_id], + new_option_id, + Object.getOwnPropertyDescriptor( + _via_img_metadata[image_id].file_attributes[attr_id], + option_id + ) + ); + delete _via_img_metadata[image_id].file_attributes[attr_id][option_id]; + } + } + } +} + +function update_region_attribute_option_in_all_metadata( + is_delete, + attr_id, + option_id, + new_option_id +) { + var image_id; + for (image_id in _via_img_metadata) { + for (var i = 0; i < _via_img_metadata[image_id].regions.length; ++i) { + if ( + _via_img_metadata[image_id].regions[i].region_attributes.hasOwnProperty( + attr_id + ) + ) { + if ( + _via_img_metadata[image_id].regions[i].region_attributes[ + attr_id + ].hasOwnProperty(option_id) + ) { + Object.defineProperty( + _via_img_metadata[image_id].regions[i].region_attributes[attr_id], + new_option_id, + Object.getOwnPropertyDescriptor( + _via_img_metadata[image_id].regions[i].region_attributes[attr_id], + option_id + ) + ); + delete _via_img_metadata[image_id].regions[i].region_attributes[ + attr_id + ][option_id]; + } + } + } + } +} + +function delete_region_attribute_in_all_metadata(attr_id) { + var image_id; + for (image_id in _via_img_metadata) { + for (var i = 0; i < _via_img_metadata[image_id].regions.length; ++i) { + if ( + _via_img_metadata[image_id].regions[i].region_attributes.hasOwnProperty( + attr_id + ) + ) { + delete _via_img_metadata[image_id].regions[i].region_attributes[ + attr_id + ]; + } + } + } +} + +function delete_file_attribute_option_from_all_metadata(attr_id, option_id) { + var image_id; + for (image_id in _via_img_metadata) { + if (_via_img_metadata.hasOwnProperty(image_id)) { + delete_file_attribute_option_from_metadata(image_id, attr_id, option_id); + } + } +} + +function delete_file_attribute_option_from_metadata( + image_id, + attr_id, + option_id +) { + var i; + if (_via_img_metadata[image_id].file_attributes.hasOwnProperty(attr_id)) { + if ( + _via_img_metadata[image_id].file_attributes[attr_id].hasOwnProperty( + option_id + ) + ) { + delete _via_img_metadata[image_id].file_attributes[attr_id][option_id]; + } + } +} + +function delete_file_attribute_from_all_metadata(image_id, attr_id) { + var image_id; + for (image_id in _via_img_metadata) { + if (_via_img_metadata.hasOwnProperty(image_id)) { + if (_via_img_metadata[image_id].file_attributes.hasOwnProperty(attr_id)) { + delete _via_img_metadata[image_id].file_attributes[attr_id]; + } + } + } +} + +// +// invoke a method after receiving inputs from user +// +function invoke_with_user_inputs(ok_handler, input, config, cancel_handler) { + setup_user_input_panel(ok_handler, input, config, cancel_handler); + show_user_input_panel(); +} + +function setup_user_input_panel( + ok_handler, + input, + config, + cancel_handler, + fileinput = false +) { + // create html page with OK and CANCEL button + // when OK is clicked + // - setup input with all the user entered values + // - invoke handler with input + // when CANCEL is clicked + // - invoke user_input_cancel() + _via_user_input_ok_handler = ok_handler; + _via_user_input_cancel_handler = cancel_handler; + _via_user_input_data = input; + + //var p = document.getElementById('user_input_panel'); + var c = document.createElement("div"); + c.setAttribute("class", "content"); + var html = []; + + html.push('<div class="container">'); + var key; + for (key in _via_user_input_data) { + html.push('<div class="row">'); + html.push('<div class="col">' + _via_user_input_data[key].name + "</div>"); + var disabled_html = ""; + if (_via_user_input_data[key].disabled) { + disabled_html = 'disabled="disabled"'; + } + var value_html = ""; + if (_via_user_input_data[key].value) { + value_html = 'value="' + _via_user_input_data[key].value + '"'; + } + switch (_via_user_input_data[key].type) { + case "checkbox": + if (_via_user_input_data[key].checked) { + value_html = 'checked="checked"'; + } else { + value_html = ""; + } + html.push( + '<div class="col">' + + '<input class="form-check-input" ' + + value_html + + " " + + disabled_html + + " " + + 'type="checkbox" id="' + + key + + '"></div>' + ); + break; + case "text": + var size = "50"; + if (_via_user_input_data[key].size) { + size = _via_user_input_data[key].size; + } + var placeholder = ""; + if (_via_user_input_data[key].placeholder) { + placeholder = _via_user_input_data[key].placeholder; + } + html.push( + '<div class="col">' + + '<input class="form-control" ' + + value_html + + " " + + disabled_html + + " " + + 'size="' + + size + + '" ' + + 'placeholder="' + + placeholder + + '" ' + + 'type="text" id="' + + key + + '"></div>' + ); + + break; + case "textarea": + var rows = "5"; + var cols = "50"; + if (_via_user_input_data[key].rows) { + rows = _via_user_input_data[key].rows; + } + if (_via_user_input_data[key].cols) { + cols = _via_user_input_data[key].cols; + } + var placeholder = ""; + if (_via_user_input_data[key].placeholder) { + placeholder = _via_user_input_data[key].placeholder; + } + html.push( + '<div class="col">' + + '<textarea class="form-control" ' + + disabled_html + + " " + + 'rows="' + + rows + + '" ' + + 'cols="' + + cols + + '" ' + + 'placeholder="' + + placeholder + + '" ' + + 'id="' + + key + + '">' + + value_html + + "</textarea></div>" + ); + + break; + } + html.push("</div>"); // end of row + } + html.push("</div>"); // end of user_input div + // optional warning before confirmation + if (config.hasOwnProperty("warning")) { + html.push('<div class="alert alert-warning">' + config.warning + "</div>"); + } + //html.push("<button type=\"button\" class=\"btn btn-primary\" onclick=\"user_input_parse_and_invoke_handler()\" id=\"user_input_ok_button\">Ok</button>" + "<button type=\"button\" class=\"btn btn-secondary\" onclick=\"user_input_cancel_handler()\" id=\"user_input_cancel_button\">Cancel</button>"); + + c.innerHTML = html.join(""); + //p.innerHTML = ''; + + // html.push("<button type=\"button\" class=\"btn btn-primary\" onclick=\"user_input_parse_and_invoke_handler()\" id=\"user_input_ok_button\">Ok</button>" + "<button type=\"button\" class=\"btn btn-secondary\" onclick=\"user_input_cancel_handler()\" id=\"user_input_cancel_button\">Cancel</button>"); + + let body = html.join(""); + + let footer = + '<button type="button" class="btn btn-primary" onclick="user_input_parse_and_invoke_handler()" id="user_input_ok_button">Ok</button>' + + '<button type="button" class="btn btn-secondary" onclick="user_input_cancel_handler()" id="user_input_cancel_button">Cancel</button>'; + + modal.show(config.title, body, footer); + //p.appendChild(c); +} + +function user_input_default_cancel_handler() { + hide_user_input_panel(); + _via_user_input_data = {}; + _via_user_input_ok_handler = null; + _via_user_input_cancel_handler = null; +} + +function user_input_cancel_handler() { + if (_via_user_input_cancel_handler) { + _via_user_input_cancel_handler(); + } + user_input_default_cancel_handler(); +} + +function user_input_parse_and_invoke_handler() { + var elist = document.getElementsByClassName("_via_user_input_variable"); + var i; + for (i = 0; i < elist.length; ++i) { + var eid = elist[i].id; + if (_via_user_input_data.hasOwnProperty(eid)) { + switch (_via_user_input_data[eid].type) { + case "checkbox": + _via_user_input_data[eid].value = elist[i].checked; + break; + default: + _via_user_input_data[eid].value = elist[i].value; + break; + } + } + } + if (typeof _via_user_input_data.confirm !== "undefined") { + if (_via_user_input_data.confirm.value) { + _via_user_input_ok_handler(_via_user_input_data); + } else { + if (_via_user_input_cancel_handler) { + _via_user_input_cancel_handler(); + } + } + } else { + _via_user_input_ok_handler(_via_user_input_data); + } + user_input_default_cancel_handler(); +} + +function show_user_input_panel() { + document.getElementById("user_input_panel").style.display = "block"; +} + +function hide_user_input_panel() { + $("#staticBackdropModal").modal("hide"); + document.getElementById("user_input_panel").style.display = "none"; +} + +// +// image grid +// +function image_grid_init() { + var p = document.getElementById("image_grid_content"); + p.focus(); + p.addEventListener("mousedown", image_grid_mousedown_handler, false); + p.addEventListener("mouseup", image_grid_mouseup_handler, false); + p.addEventListener("dblclick", image_grid_dblclick_handler, false); + + image_grid_set_content_panel_height_fixed(); + + //add event listeeners for dropdown options + + let policy = settings.image_grid_content; + image_grid_set_policy_btn_text(policy); + $("#image_grid_show_image_policy_dropdown").on("click", "a", function () { + let policy = $(this).data("value"); + image_grid_set_policy_btn_text(policy); + image_grid_onchange_show_image_policy(policy); + }); +} + +function image_grid_set_policy_btn_text(policy) { + switch (policy) { + case "all": + $("#image_grid_show_image_policy").text("all"); + break; + case "first_mid_last": + $("#image_grid_show_image_policy").text("first, mid, last"); + break; + case "even_indexed": + $("#image_grid_show_image_policy").text("even"); + break; + case "odd_indexed": + $("#image_grid_show_image_policy").text("odd"); + break; + case "gap5": + $("#image_grid_show_image_policy").text("gap 5"); + break; + case "gap25": + $("#image_grid_show_image_policy").text("gap 25"); + break; + case "gap50": + $("#image_grid_show_image_policy").text("gap 50"); + break; + default: + $("#image_grid_show_image_policy").text("all"); + } +} + +function image_grid_update() { + if ( + _via_display_area_content_name === VIA_DISPLAY_AREA_CONTENT_NAME.IMAGE_GRID + ) { + image_grid_set_content(_via_image_grid_img_index_list); + } +} + +function image_grid_toggle() { + var p = document.getElementById("toolbar_image_grid_toggle"); + if ( + _via_display_area_content_name === VIA_DISPLAY_AREA_CONTENT_NAME.IMAGE_GRID + ) { + image_grid_clear_all_groups(); + show_single_image_view(); + } else { + show_image_grid_view(); + } +} + +function image_grid_show_all_project_images() { + var all_img_index_list = []; + var i, n; + //n = _via_image_id_list.length; + n = _via_img_fn_list_img_index_list.length; + for (i = 0; i < n; ++i) { + all_img_index_list.push(_via_img_fn_list_img_index_list[i]); + } + + image_grid_clear_all_groups(); + + var p = document.getElementById("image_grid_toolbar_group_by_select"); + p.selectedIndex = 0; + + image_grid_set_content(all_img_index_list); +} + +function image_grid_clear_all_groups() { + var i, n; + n = _via_image_grid_group_var.length; + for (i = 0; i < n; ++i) { + image_grid_remove_html_group_panel(_via_image_grid_group_var[i]); + image_grid_group_by_select_set_disabled( + _via_image_grid_group_var[i].type, + _via_image_grid_group_var[i].name, + false + ); + } + _via_image_grid_group = {}; + _via_image_grid_group_var = []; +} + +function image_grid_set_content(img_index_list) { + if (img_index_list.length === 0) { + return; + } + if (_via_image_grid_load_ongoing) { + return; + } + + _via_image_grid_img_index_list = img_index_list.slice(0); + _via_image_grid_selected_img_index_list = img_index_list.slice(0); + + document.getElementById("image_grid_group_by_img_count").innerHTML = + _via_image_grid_img_index_list.length.toString(); + + _via_image_grid_page_first_index = 0; + _via_image_grid_page_last_index = null; + _via_image_grid_stack_prev_page = []; + _via_image_grid_page_img_index_list = []; + + image_grid_clear_content(); + image_grid_set_content_panel_height_fixed(); + _via_image_grid_load_ongoing = true; + + var n = _via_image_grid_img_index_list.length; + switch (settings.image_grid_content) { + case "all": + _via_image_grid_page_img_index_list = + _via_image_grid_img_index_list.slice(0); + break; + case "first_mid_last": + if (n < 3) { + var i; + for (i = 0; i < n; ++i) { + _via_image_grid_page_img_index_list.push( + _via_image_grid_img_index_list[i] + ); + } + } else { + _via_image_grid_page_img_index_list.push( + _via_image_grid_img_index_list[0] + ); + _via_image_grid_page_img_index_list.push( + _via_image_grid_img_index_list[Math.floor(n / 2)] + ); + _via_image_grid_page_img_index_list.push( + _via_image_grid_img_index_list[n - 1] + ); + } + break; + case "even_indexed": + var i; + for (i = 0; i < n; ++i) { + if (i % 2 !== 0) { + // since the user views (i+1) based indexing + _via_image_grid_page_img_index_list.push( + _via_image_grid_img_index_list[i] + ); + } + } + break; + case "odd_indexed": + var i; + for (i = 0; i < n; ++i) { + if (i % 2 === 0) { + // since the user views (i+1) based indexing + _via_image_grid_page_img_index_list.push( + _via_image_grid_img_index_list[i] + ); + } + } + break; + case "gap5": // fallback + case "gap25": // fallback + case "gap50": // fallback + var del = parseInt( + _via_settings.ui.image_grid.show_image_policy.substring("gap".length) + ); + var i; + for (i = 0; i < n; i = i + del) { + _via_image_grid_page_img_index_list.push( + _via_image_grid_img_index_list[i] + ); + } + break; + default: + _via_image_grid_page_img_index_list = + _via_image_grid_img_index_list.slice(0); + } + + _via_image_grid_visible_img_index_list = []; + + image_grid_update_sel_count_html(); + sidebar.annotation_editor_update_content(); + + image_grid_content_append_img(_via_image_grid_page_first_index); + + show_message( + "[Click] toggles selection, " + + "[Shift + Click] selects everything a image, " + + "[Click] or [Ctrl + Click] removes selection of all subsequent or preceeding images." + ); +} + +function image_grid_clear_content() { + var img_container = document.getElementById("image_grid_content_img"); + var img_rshape = document.getElementById("image_grid_content_rshape"); + img_container.innerHTML = ""; + img_rshape.innerHTML = ""; + _via_image_grid_visible_img_index_list = []; +} + +function image_grid_set_content_panel_height_fixed() { + var pc = document.getElementById("image_grid_content"); + var de = document.documentElement; + pc.style.height = de.clientHeight - 3 * ui_top_panel.offsetHeight + "px"; +} + +// We do not know how many images will fit in the display area. +// Therefore, we add images one-by-one until overflow of parent +// container is detected. +function image_grid_content_append_img(img_grid_index) { + let img_index = _via_image_grid_page_img_index_list[img_grid_index]; + let html_img_id = image_grid_get_html_img_id(img_index); + let img_id = _via_image_id_list[img_index]; + let e = document.createElement("img"); + if (_via_img_fileref[img_id] instanceof File) { + var img_reader = new FileReader(); + img_reader.addEventListener( + "error", + function () { + //@todo + }, + false + ); + img_reader.addEventListener( + "load", + function () { + e.src = img_reader.result.toString(); //MODSote + }, + false + ); + img_reader.readAsDataURL(_via_img_fileref[img_id]); + } else { + e.src = _via_img_src[img_id]; + } + e.setAttribute("id", html_img_id); + //e.setAttribute('height', _via_settings.ui.image_grid.img_height + 'px'); + e.style.height = _via_settings.ui.image_grid.img_height + "px"; + e.setAttribute( + "title", + "[" + (img_index + 1) + "] " + _via_img_metadata[img_id].filename + ); + e.addEventListener("load", image_grid_on_img_load, false); + e.addEventListener("error", image_grid_on_img_error, false); + e.classList.add("img-thumbnail"); + + document.getElementById("image_grid_content_img").appendChild(e); +} + +function image_grid_on_img_load(e) { + var img = e.target; + var img_index = image_grid_parse_html_img_id(img.id); + project.fileLoadOnSuccess(img_index); + + image_grid_add_img_if_possible(img); +} + +function image_grid_on_img_error(e) { + var img = e.target; + var img_index = image_grid_parse_html_img_id(img.id); + project.fileLoadOnFail(img_index); + image_grid_add_img_if_possible(img); +} + +function image_grid_add_img_if_possible(img) { + let img_index = image_grid_parse_html_img_id(img.id); + + let p = document.getElementById("image_grid_content_img"); + let img_bottom_right_corner = parseInt(img.offsetTop) + parseInt(img.height); + if (p.clientHeight < img_bottom_right_corner) { + // stop as addition of this image caused overflow of parent container + let img_container = document.getElementById("image_grid_content_img"); + img_container.removeChild(img); + + if (_via_settings.ui.image_grid.show_region_shape) { + image_grid_page_show_all_regions(); + } + _via_image_grid_load_ongoing = false; + + var index = _via_image_grid_page_img_index_list.indexOf(img_index); + _via_image_grid_page_last_index = index; + + // setup prev, next navigation + var info = document.getElementById("image_grid_nav"); + var html = []; + var first_index = _via_image_grid_page_first_index; + var last_index = _via_image_grid_page_last_index - 1; + html.push( + '<span class="input-group-text">Showing ' + + (first_index + 1) + + " to " + + (last_index + 1) + + " :</span>" + ); + if (_via_image_grid_stack_prev_page.length) { + html.push( + '<button type="button" class="btn btn-sm btn-outline-secondary" onclick="image_grid_page_prev()">Prev</button>' + ); + } else { + html.push( + '<button type="button" class="btn btn-sm btn-outline-secondary disabled">Prev</button>' + ); + } + html.push( + '<button type="button" class="btn btn-sm btn-outline-secondary" onclick="image_grid_page_next()">Next</button>' + ); + + info.innerHTML = html.join(""); + } else { + // process this image and trigger addition of next image in sequence + var img_fn_list_index = + _via_image_grid_page_img_index_list.indexOf(img_index); + var next_img_fn_list_index = img_fn_list_index + 1; + + _via_image_grid_visible_img_index_list.push(img_index); + var is_selected = + _via_image_grid_selected_img_index_list.indexOf(img_index) !== -1; + if (!is_selected) { + image_grid_update_img_select(img_index, "unselect"); + } + + if (next_img_fn_list_index !== _via_image_grid_page_img_index_list.length) { + if (_via_image_grid_load_ongoing) { + image_grid_content_append_img(img_fn_list_index + 1); + } else { + // image grid load operation was cancelled + _via_image_grid_page_last_index = _via_image_grid_page_first_index; // load this page again + + var info = document.getElementById("image_grid_nav"); + var html = []; + html.push('<span class="input-group-text">Cancelled :</span>'); + if (_via_image_grid_stack_prev_page.length) { + html.push( + '<button type="button" class="btn btn-sm btn-outline-secondary" onclick="image_grid_page_prev()">Prev</button>' + ); + } else { + html.push( + '<button type="button" class="btn btn-sm btn-outline-secondary disabled">Prev</button>' + ); + } + html.push( + '<button type="button" class="btn btn-sm btn-outline-secondary" onclick="image_grid_page_next()">Next</button>' + ); + info.innerHTML = html.join(""); + } + } else { + // last page + var index = _via_image_grid_page_img_index_list.indexOf(img_index); + _via_image_grid_page_last_index = index; + + if (_via_settings.ui.image_grid.show_region_shape) { + image_grid_page_show_all_regions(); + } + _via_image_grid_load_ongoing = false; + + // setup prev, next navigation + var info = document.getElementById("image_grid_nav"); + var html = []; + var first_index = _via_image_grid_page_first_index; + var last_index = _via_image_grid_page_last_index; + html.push( + '<span class="input-group-text">Showing ' + + (first_index + 1) + + " to " + + (last_index + 1) + + " (end) </span>" + ); + if (_via_image_grid_stack_prev_page.length) { + html.push( + '<button type="button" class="btn btn-sm btn-outline-secondary" onclick="image_grid_page_prev()">Prev</button>' + ); + } else { + html.push( + '<button type="button" class="btn btn-sm btn-outline-secondary disabled">Prev</button>' + ); + } + html.push( + '<button type="button" class="btn btn-sm btn-outline-secondary disabled">Next</button>' + ); + + info.innerHTML = html.join(""); + } + } +} + +function image_grid_onchange_show_image_policy(policy) { + settings.image_grid_content = policy; + settings.save(); + image_grid_set_content(_via_image_grid_img_index_list); +} + +function image_grid_page_show_all_regions() { + var all_promises = []; + if (_via_settings.ui.image_grid.show_region_shape) { + var p = document.getElementById("image_grid_content_img"); + var n = p.childNodes.length; + var i; + for (i = 0; i < n; ++i) { + // draw region shape into global canvas for image grid + var img_index = image_grid_parse_html_img_id(p.childNodes[i].id); + var img_param = []; // [width, height, originalWidth, originalHeight, x, y] + img_param.push(parseInt(p.childNodes[i].width)); + img_param.push(parseInt(p.childNodes[i].height)); + img_param.push(parseInt(p.childNodes[i].naturalWidth)); + img_param.push(parseInt(p.childNodes[i].naturalHeight)); + img_param.push( + parseInt(p.childNodes[i].offsetLeft) + + parseInt(p.childNodes[i].clientLeft) + ); + img_param.push( + parseInt(p.childNodes[i].offsetTop) + + parseInt(p.childNodes[i].clientTop) + ); + var promise = image_grid_show_region_shape(img_index, img_param); + all_promises.push(promise); + } + // @todo: ensure that all promises are fulfilled + } +} + +function image_grid_is_region_in_current_group(r) { + var i, n; + n = _via_image_grid_group_var.length; + if (n === 0) { + return true; + } + + for (i = 0; i < n; ++i) { + if (_via_image_grid_group_var[i].type === "region") { + var group_value = + _via_image_grid_group_var[i].values[ + _via_image_grid_group_var[i].current_value_index + ]; + if (r[_via_image_grid_group_var[i].name] != group_value) { + return false; + } + } + } + return true; +} + +function image_grid_show_region_shape(img_index, img_param) { + return new Promise(function (ok_callback, err_callback) { + var i; + var img_id = _via_image_id_list[img_index]; + var html_img_id = image_grid_get_html_img_id(img_index); + var n = _via_img_metadata[img_id].regions.length; + var is_in_group = false; + for (i = 0; i < n; ++i) { + if ( + !image_grid_is_region_in_current_group( + _via_img_metadata[img_id].regions[i].region_attributes + ) + ) { + // skip drawing this region which is not in current group + continue; + } + + var r = _via_img_metadata[img_id].regions[i].shape_attributes; + var dimg; // region coordinates in original image space + switch (r.name) { + case VIA_REGION_SHAPE.RECT: + dimg = [r["x"], r["y"], r["x"] + r["width"], r["y"] + r["height"]]; + break; + case VIA_REGION_SHAPE.CIRCLE: + dimg = [r["cx"], r["cy"], r["cx"] + r["r"], r["cy"] + r["r"]]; + break; + case VIA_REGION_SHAPE.ELLIPSE: + dimg = [r["cx"], r["cy"], r["cx"] + r["rx"], r["cy"] + r["ry"]]; + break; + case VIA_REGION_SHAPE.POLYLINE: // handled by POLYGON + case VIA_REGION_SHAPE.POLYGON: + var j; + dimg = []; + for (j = 0; j < r["all_points_x"].length; ++j) { + dimg.push(r["all_points_x"][j]); + dimg.push(r["all_points_y"][j]); + } + break; + case VIA_REGION_SHAPE.POINT: + dimg = [r["cx"], r["cy"]]; + break; + } + var scale_factor = img_param[1] / img_param[3]; // new_height / original height + var offset_x = img_param[4]; + var offset_y = img_param[5]; + var r2 = new _via_region( + r.name, + i, + dimg, + scale_factor, + offset_x, + offset_y + ); + var r2_svg = r2.get_svg_element(); + r2_svg.setAttribute("id", image_grid_get_html_region_id(img_index, i)); + r2_svg.setAttribute("class", html_img_id); + r2_svg.setAttribute("fill", _via_settings.ui.image_grid.rshape_fill); + //r2_svg.setAttribute('fill-opacity', _via_settings.ui.image_grid.rshape_fill_opacity); + r2_svg.setAttribute("stroke", _via_settings.ui.image_grid.rshape_stroke); + r2_svg.setAttribute( + "stroke-width", + _via_settings.ui.image_grid.rshape_stroke_width + ); + document.getElementById("image_grid_content_rshape").appendChild(r2_svg); + } + }); +} + +function image_grid_image_size_increase() { + var new_img_height = + _via_settings.ui.image_grid.img_height + VIA_IMAGE_GRID_IMG_HEIGHT_CHANGE; + _via_settings.ui.image_grid.img_height = new_img_height; + + _via_image_grid_page_last_index = null; + image_grid_update(); +} + +function image_grid_image_size_decrease() { + var new_img_height = + _via_settings.ui.image_grid.img_height - VIA_IMAGE_GRID_IMG_HEIGHT_CHANGE; + if (new_img_height > 1) { + _via_settings.ui.image_grid.img_height = new_img_height; + _via_image_grid_page_last_index = null; + image_grid_update(); + } +} + +function image_grid_image_size_reset() { + var new_img_height = _via_settings.ui.image_grid.img_height; + if (new_img_height > 1) { + _via_settings.ui.image_grid.img_height = new_img_height; + _via_image_grid_page_last_index = null; + image_grid_update(); + } +} + +function image_grid_mousedown_handler(e) { + e.preventDefault(); + _via_image_grid_mousedown_img_index = image_grid_parse_html_img_id( + e.target.id + ); +} + +function image_grid_mouseup_handler(e) { + e.preventDefault(); + var last_mouseup_img_index = _via_image_grid_mouseup_img_index; + _via_image_grid_mouseup_img_index = image_grid_parse_html_img_id(e.target.id); + if ( + isNaN(_via_image_grid_mousedown_img_index) || + isNaN(_via_image_grid_mouseup_img_index) + ) { + last_mouseup_img_index = _via_image_grid_img_index_list[0]; + image_grid_group_select_none(); + return; + } + + var mousedown_img_arr_index = _via_image_grid_img_index_list.indexOf( + _via_image_grid_mousedown_img_index + ); + var mouseup_img_arr_index = _via_image_grid_img_index_list.indexOf( + _via_image_grid_mouseup_img_index + ); + + var start = -1; + var end = -1; + var operation = "select"; // {'select', 'unselect', 'toggle'} + if (mousedown_img_arr_index === mouseup_img_arr_index) { + if (e.shiftKey) { + // select all elements until this element + start = + _via_image_grid_img_index_list.indexOf(last_mouseup_img_index) + 1; + end = mouseup_img_arr_index + 1; + } else { + // toggle selection of single image + start = mousedown_img_arr_index; + end = start + 1; + operation = "toggle"; + } + } else { + if (mousedown_img_arr_index < mouseup_img_arr_index) { + start = mousedown_img_arr_index; + end = mouseup_img_arr_index + 1; + } else { + start = mouseup_img_arr_index + 1; + end = mousedown_img_arr_index; + } + operation = "toggle"; + } + + if (start > end) { + return; + } + + var i, img_index; + for (i = start; i < end; ++i) { + img_index = _via_image_grid_img_index_list[i]; + image_grid_update_img_select(img_index, operation); + } + image_grid_update_sel_count_html(); + sidebar.annotation_editor_update_content(); +} + +function image_grid_update_sel_count_html() { + document.getElementById("image_grid_group_by_sel_img_count").innerHTML = + _via_image_grid_selected_img_index_list.length.toString(); //modsote +} + +// state \in {'select', 'unselect', 'toggle'} +function image_grid_update_img_select(img_index, state) { + var html_img_id = image_grid_get_html_img_id(img_index); + var is_selected = + _via_image_grid_selected_img_index_list.indexOf(img_index) !== -1; + if (state === "toggle") { + if (is_selected) { + state = "unselect"; + } else { + state = "select"; + } + } + + switch (state) { + case "select": + if (!is_selected) { + _via_image_grid_selected_img_index_list.push(img_index); + } + if (_via_image_grid_visible_img_index_list.indexOf(img_index) !== -1) { + document.getElementById(html_img_id).classList.remove("not_sel"); + } + break; + case "unselect": + if (is_selected) { + var arr_index = + _via_image_grid_selected_img_index_list.indexOf(img_index); + _via_image_grid_selected_img_index_list.splice(arr_index, 1); + } + if (_via_image_grid_visible_img_index_list.indexOf(img_index) !== -1) { + document.getElementById(html_img_id).classList.add("not_sel"); + } + break; + } +} + +function image_grid_group_select_all() { + image_grid_group_set_all_selection_state("select"); + image_grid_update_sel_count_html(); + sidebar.annotation_editor_update_content(); + show_message("Selected all images in the current group"); +} + +function image_grid_group_select_none() { + image_grid_group_set_all_selection_state("unselect"); + image_grid_update_sel_count_html(); + sidebar.annotation_editor_update_content(); + show_message("Removed selection of all images in the current group"); +} + +function image_grid_group_set_all_selection_state(state) { + var i, img_index; + for (i = 0; i < _via_image_grid_img_index_list.length; ++i) { + img_index = _via_image_grid_img_index_list[i]; + image_grid_update_img_select(img_index, state); + } +} + +function image_grid_group_toggle_select_all() { + if ( + _via_image_grid_selected_img_index_list.length === + _via_image_grid_img_index_list.length + ) { + image_grid_group_select_none(); + } else { + image_grid_group_select_all(); + } +} + +function image_grid_parse_html_img_id(html_img_id) { + let img_index = html_img_id.substring(2); + return parseInt(img_index); +} + +function image_grid_get_html_img_id(img_index) { + return "im" + img_index; +} + +function image_grid_parse_html_region_id(html_region_id) { + var chunks = html_region_id.split("_"); + if (chunks.length === 2) { + var img_index = parseInt(chunks[0].substring(2)); + var region_id = parseInt(chunks[1].substring(2)); + return { img_index: img_index, region_id: region_id }; + } else { + console.log("image_grid_parse_html_region_id(): invalid html_region_id"); + return {}; + } +} + +function image_grid_get_html_region_id(img_index, region_id) { + return image_grid_get_html_img_id(img_index) + "_rs" + region_id; +} + +function image_grid_dblclick_handler(e) { + _via_image_index = image_grid_parse_html_img_id(e.target.id); + show_single_image_view(); +} + +function image_grid_toolbar_update_group_by_select() { + var p = document.getElementById("image_grid_toolbar_group_by_select"); + p.innerHTML = ""; + + var o = document.createElement("option"); + o.setAttribute("value", ""); + o.setAttribute("selected", "selected"); + o.innerHTML = "All Images"; + p.appendChild(o); + + // add file attributes + var fattr; + for (fattr in project.attributes.file) { + var o = document.createElement("option"); + o.setAttribute( + "value", + image_grid_toolbar_group_by_select_get_html_id("file", fattr) + ); + o.innerHTML = "[file] " + fattr; + p.appendChild(o); + } + + // add region attributes + var rattr; + for (rattr in project.attributes.region) { + var o = document.createElement("option"); + o.setAttribute( + "value", + image_grid_toolbar_group_by_select_get_html_id("region", rattr) + ); + o.innerHTML = "[region] " + rattr; + p.appendChild(o); + } +} + +function image_grid_toolbar_group_by_select_get_html_id(type, name) { + if (type === "file") { + return "f_" + name; + } + if (type === "region") { + return "r_" + name; + } +} + +function image_grid_toolbar_group_by_select_parse_html_id(id) { + if (id.startsWith("f_")) { + return { attr_type: "file", attr_name: id.substring(2) }; + } + if (id.startsWith("r_")) { + return { attr_type: "region", attr_name: id.substring(2) }; + } +} + +function image_grid_toolbar_onchange_group_by_select(p) { + if (p.options[p.selectedIndex].value === "") { + image_grid_show_all_project_images(); + return; + } + + var v = image_grid_toolbar_group_by_select_parse_html_id( + p.options[p.selectedIndex].value + ); + var attr_type = v.attr_type; + var attr_name = v.attr_name; + image_grid_group_by(attr_type, attr_name); + + image_grid_group_by_select_set_disabled(attr_type, attr_name, true); + p.blur(); // to avoid adding new groups using keyboard keys as dropdown is still in focus +} + +function image_grid_remove_html_group_panel(d) { + var p = document.getElementById("group_toolbar_" + d.group_index); + document.getElementById("image_grid_group_panel").removeChild(p); + //if image_grid_group_panel is empty, hide it + if ($("#image_grid_group_panel").children().length === 0) { + $("#image_grid_group_panel_card").addClass("d-none"); + } +} + +function image_grid_add_html_group_panel(d) { + //create div elment with jquery + let panel = $( + '<div class="input-group input-group-sm" id="group_toolbar_' + + d.group_index + + '"></div>' + ); + let del = $( + '<button class="btn btn-outline-secondary" onclick="image_grid_remove_group_by(this)"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-x" viewBox="0 0 16 16">\n' + + ' <path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/>\n' + + "</svg></button>" + ); + panel.append(del); + + let prev = $( + '<button class="btn btn-outline-secondary" value="' + + d.group_index + + '" onclick="image_grid_group_prev(this)"><</button>' + ); + panel.append(prev); + let sel = $( + '<select class="form-select form-select-sm" id="' + + image_grid_group_select_get_html_id(d.group_index) + + '" onchange="image_grid_group_value_onchange(this)"></select>' + ); + let i, value; + + let n = d.values.length; + let current_value = d.values[d.current_value_index]; + + for (i = 0; i < n; ++i) { + value = d.values[i]; + let o = $( + '<option value="' + + value + + '">' + + (i + 1) + + "/" + + n + + ": " + + d.name + + " = " + + value + + "</option>" + ); + if (value === current_value) { + o.attr("selected", "selected"); + } + sel.append(o); + } + panel.append(sel); + + let next = $( + '<button class="btn btn-outline-secondary" value="' + + d.group_index + + '" onclick="image_grid_group_next(this)">></button>' + ); + panel.append(next); + + //add panel to image_grid_group_panel + $("#image_grid_group_panel").append(panel); + $("#image_grid_group_panel_card").removeClass("d-none"); +} + +function image_grid_group_panel_set_selected_value(group_index) { + var sel = document.getElementById( + image_grid_group_select_get_html_id(group_index) + ); + sel.selectedIndex = + _via_image_grid_group_var[group_index].current_value_index; +} + +function image_grid_group_panel_set_options(group_index) { + var sel = document.getElementById( + image_grid_group_select_get_html_id(group_index) + ); + sel.innerHTML = ""; + + var i, value; + if (_via_image_grid_group_var[group_index] === undefined) { + return; + } + var n = _via_image_grid_group_var[group_index].values.length; + var name = _via_image_grid_group_var[group_index].name; + var current_value = + _via_image_grid_group_var[group_index].values[ + _via_image_grid_group_var[group_index].current_value_index + ]; + for (i = 0; i < n; ++i) { + value = _via_image_grid_group_var[group_index].values[i]; + var o = document.createElement("option"); + o.setAttribute("value", value); + o.innerHTML = i + 1 + "/" + n + ": " + name + " = " + value; + if (value === current_value) { + o.setAttribute("selected", "selected"); + } + sel.appendChild(o); + } +} + +function image_grid_group_select_get_html_id(group_index) { + return "gi_" + group_index; +} + +function image_grid_group_select_parse_html_id(id) { + return parseInt(id.substring(3)); +} + +function image_grid_group_by_select_set_disabled(type, name, is_disabled) { + var p = document.getElementById("image_grid_toolbar_group_by_select"); + var sel_option_value = image_grid_toolbar_group_by_select_get_html_id( + type, + name + ); + + var n = p.options.length; + var option_value; + var i; + for (i = 0; i < n; ++i) { + if (sel_option_value === p.options[i].value) { + if (is_disabled) { + p.options[i].setAttribute("disabled", "disabled"); + } else { + p.options[i].removeAttribute("disabled"); + } + break; + } + } +} + +function image_grid_remove_group_by(p) { + var prefix = "group_toolbar_"; + var group_index = parseInt(p.parentNode.id.substring(prefix.length)); + + if (group_index === 0) { + image_grid_show_all_project_images(); + } else { + // merge all groups that are child of group_index + image_grid_group_by_merge(_via_image_grid_group, 0, group_index); + + var n = _via_image_grid_group_var.length; + var p = document.getElementById("image_grid_group_panel"); + var group_panel_id; + var i; + for (i = group_index; i < n; ++i) { + image_grid_remove_html_group_panel(_via_image_grid_group_var[i]); + image_grid_group_by_select_set_disabled( + _via_image_grid_group_var[i].type, + _via_image_grid_group_var[i].name, + false + ); + } + _via_image_grid_group_var.splice(group_index); + + image_grid_set_content_to_current_group(); + } +} + +function image_grid_group_by(type, name) { + if (Object.keys(_via_image_grid_group).length === 0) { + // first group + var img_index_array = []; + var n = _via_img_fn_list_img_index_list.length; + var i; + for (i = 0; i < n; ++i) { + img_index_array.push(_via_img_fn_list_img_index_list[i]); + } + + _via_image_grid_group = image_grid_split_array_to_group( + img_index_array, + type, + name + ); + var new_group_values = Object.keys(_via_image_grid_group); + _via_image_grid_group_var = []; + _via_image_grid_group_var.push({ + type: type, + name: name, + current_value_index: 0, + values: new_group_values, + group_index: 0, + }); + + image_grid_add_html_group_panel(_via_image_grid_group_var[0]); + } else { + image_grid_group_split_all_arrays(_via_image_grid_group, type, name); + + var i, n, value; + var current_group_value = _via_image_grid_group; + n = _via_image_grid_group_var.length; + + for (i = 0; i < n; ++i) { + value = + _via_image_grid_group_var[i].values[ + _via_image_grid_group_var[i].current_value_index + ]; + current_group_value = current_group_value[value]; + } + var new_group_values = Object.keys(current_group_value); + var group_var_index = _via_image_grid_group_var.length; + _via_image_grid_group_var.push({ + type: type, + name: name, + current_value_index: 0, + values: new_group_values, + group_index: group_var_index, + }); + image_grid_add_html_group_panel(_via_image_grid_group_var[group_var_index]); + } + + image_grid_set_content_to_current_group(); +} + +function image_grid_group_by_merge(group, current_level, target_level) { + var child_value; + var group_data = []; + if (current_level === target_level) { + return image_grid_group_by_collapse(group); + } else { + for (child_value in group) { + group[child_value] = image_grid_group_by_merge( + group[child_value], + current_level + 1, + target_level + ); + } + } +} + +function image_grid_group_by_collapse(group) { + var child_value; + var child_collapsed_value; + var group_data = []; + for (child_value in group) { + if (Array.isArray(group[child_value])) { + group_data = group_data.concat(group[child_value]); + } else { + group_data = group_data.concat( + image_grid_group_by_collapse(group[child_value]) + ); + } + } + return group_data; +} + +// recursively collapse all arrays to list +function image_grid_group_split_all_arrays(group, type, name) { + if (Array.isArray(group)) { + return image_grid_split_array_to_group(group, type, name); + } else { + var group_value; + for (group_value in group) { + if (Array.isArray(group[group_value])) { + group[group_value] = image_grid_split_array_to_group( + group[group_value], + type, + name + ); + } else { + image_grid_group_split_all_arrays(group[group_value], type, name); + } + } + } +} + +function image_grid_split_array_to_group( + img_index_array, + attr_type, + attr_name +) { + var grp = {}; + var img_index, img_id, i; + var n = img_index_array.length; + var attr_value; + + switch (attr_type) { + case "file": + for (i = 0; i < n; ++i) { + img_index = img_index_array[i]; + img_id = _via_image_id_list[img_index]; + if ( + _via_img_metadata[img_id].file_attributes.hasOwnProperty(attr_name) + ) { + attr_value = _via_img_metadata[img_id].file_attributes[attr_name]; + + if (!grp.hasOwnProperty(attr_value)) { + grp[attr_value] = []; + } + grp[attr_value].push(img_index); + } + } + break; + case "region": + var j; + var region_count; + for (i = 0; i < n; ++i) { + img_index = img_index_array[i]; + img_id = _via_image_id_list[img_index]; + region_count = _via_img_metadata[img_id].regions.length; + for (j = 0; j < region_count; ++j) { + if ( + _via_img_metadata[img_id].regions[ + j + ].region_attributes.hasOwnProperty(attr_name) + ) { + attr_value = + _via_img_metadata[img_id].regions[j].region_attributes[attr_name]; + + if (!grp.hasOwnProperty(attr_value)) { + grp[attr_value] = []; + } + if (grp[attr_value].includes(img_index)) { + } else { + grp[attr_value].push(img_index); + } + } + } + } + break; + } + return grp; +} + +function image_grid_group_next(p) { + var group_index = parseInt(p.value); + if (_via_image_grid_group_var[group_index] === undefined) { + return; + } + var group_value_list = _via_image_grid_group_var[group_index].values; + var n = group_value_list.length; + var current_index = + _via_image_grid_group_var[group_index].current_value_index; + var next_index = current_index + 1; + if (next_index >= n) { + if (group_index === 0) { + next_index = next_index - n; + image_grid_jump_to_group(group_index, next_index); + } else { + // next of parent group + var parent_group_index = group_index - 1; + var parent_current_val_index = + _via_image_grid_group_var[parent_group_index].current_value_index; + var parent_next_val_index = parent_current_val_index + 1; + while (parent_group_index !== 0) { + if ( + parent_next_val_index >= + _via_image_grid_group_var[parent_group_index].values.length + ) { + parent_group_index = group_index - 1; + parent_current_val_index = + _via_image_grid_group_var[parent_group_index].current_value_index; + parent_next_val_index = parent_current_val_index + 1; + } else { + break; + } + } + + if ( + parent_next_val_index >= + _via_image_grid_group_var[parent_group_index].values.length + ) { + parent_next_val_index = 0; + } + image_grid_jump_to_group(parent_group_index, parent_next_val_index); + } + } else { + image_grid_jump_to_group(group_index, next_index); + } + image_grid_set_content_to_current_group(); +} + +function image_grid_group_prev(p) { + var group_index = parseInt(p.value); + var group_value_list = _via_image_grid_group_var[group_index].values; + var n = group_value_list.length; + var current_index = + _via_image_grid_group_var[group_index].current_value_index; + var prev_index = current_index - 1; + if (prev_index < 0) { + if (group_index === 0) { + prev_index = n + prev_index; + image_grid_jump_to_group(group_index, prev_index); + } else { + // prev of parent group + var parent_group_index = group_index - 1; + var parent_current_val_index = + _via_image_grid_group_var[parent_group_index].current_value_index; + var parent_prev_val_index = parent_current_val_index - 1; + while (parent_group_index !== 0) { + if (parent_prev_val_index < 0) { + parent_group_index = group_index - 1; + parent_current_val_index = + _via_image_grid_group_var[parent_group_index].current_value_index; + parent_prev_val_index = parent_current_val_index - 1; + } else { + break; + } + } + + if (parent_prev_val_index < 0) { + parent_prev_val_index = + _via_image_grid_group_var[parent_group_index].values.length - 1; + } + image_grid_jump_to_group(parent_group_index, parent_prev_val_index); + } + } else { + image_grid_jump_to_group(group_index, prev_index); + } + image_grid_set_content_to_current_group(); +} + +function image_grid_group_value_onchange(p) { + var group_index = image_grid_group_select_parse_html_id(p.id); + image_grid_jump_to_group(group_index, p.selectedIndex); + image_grid_set_content_to_current_group(); +} + +function image_grid_jump_to_group(group_index, value_index) { + var n = _via_image_grid_group_var[group_index].values.length; + if (value_index >= n || value_index < 0) { + return; + } + + _via_image_grid_group_var[group_index].current_value_index = value_index; + image_grid_group_panel_set_selected_value(group_index); + + // reset the value of lower groups + var i, value; + if (group_index + 1 < _via_image_grid_group_var.length) { + var e = _via_image_grid_group; + for (i = 0; i <= group_index; ++i) { + value = + _via_image_grid_group_var[i].values[ + _via_image_grid_group_var[i].current_value_index + ]; + e = e[value]; + } + + for (i = group_index + 1; i < _via_image_grid_group_var.length; ++i) { + _via_image_grid_group_var[i].values = Object.keys(e); + if (_via_image_grid_group_var[i].values.length === 0) { + _via_image_grid_group_var[i].current_value_index = -1; + _via_image_grid_group_var.splice(i); + image_grid_group_panel_set_options(i); + break; + } else { + _via_image_grid_group_var[i].current_value_index = 0; + value = _via_image_grid_group_var[i].values[0]; + e = e[value]; + image_grid_group_panel_set_options(i); + } + } + } +} + +function image_grid_set_content_to_current_group() { + var n = _via_image_grid_group_var.length; + + if (n === 0) { + image_grid_show_all_project_images(); + } else { + var group_img_index_list = []; + var img_index_list = _via_image_grid_group; + var i, n, value, current_value_index; + for (i = 0; i < n; ++i) { + value = + _via_image_grid_group_var[i].values[ + _via_image_grid_group_var[i].current_value_index + ]; + img_index_list = img_index_list[value]; + } + + if (Array.isArray(img_index_list)) { + image_grid_set_content(img_index_list); + } else { + console.log( + "Error: image_grid_set_content_to_current_group(): expected array while got " + + typeof img_index_list + ); + } + } +} + +function image_grid_page_next() { + _via_image_grid_stack_prev_page.push(_via_image_grid_page_first_index); + _via_image_grid_page_first_index = _via_image_grid_page_last_index; + + image_grid_clear_content(); + _via_image_grid_load_ongoing = true; + image_grid_page_nav_show_cancel(); + image_grid_content_append_img(_via_image_grid_page_first_index); +} + +function image_grid_page_prev() { + _via_image_grid_page_first_index = _via_image_grid_stack_prev_page.pop(); + _via_image_grid_page_last_index = -1; + + image_grid_clear_content(); + _via_image_grid_load_ongoing = true; + image_grid_page_nav_show_cancel(); + image_grid_content_append_img(_via_image_grid_page_first_index); +} + +function image_grid_page_nav_show_cancel() { + var info = document.getElementById("image_grid_nav"); + var html = []; + html.push("<span>Loading images ... </span>"); + html.push( + '<span class="text_button" onclick="image_grid_cancel_load_ongoing()">Cancel</span>' + ); + info.innerHTML = html.join(""); +} + +function image_grid_cancel_load_ongoing() { + _via_image_grid_load_ongoing = false; +} + +// everything to do with image zooming + +// +// hooks for sub-modules +// implemented by sub-modules +// +//function _via_hook_next_image() {} +//function _via_hook_prev_image() {} + +//////////////////////////////////////////////////////////////////////////////// +// +// Code borrowed from via2 branch +// - in future, the <canvas> based reigon shape drawing will be replaced by <svg> +// because svg allows independent manipulation of individual regions without +// requiring to clear the canvas every time some region is updated. +// +//////////////////////////////////////////////////////////////////////////////// + +//////////////////////////////////////////////////////////////////////////////// +// +// @file _via_region.src +// @description Implementation of region shapes like rectangle, circle, etc. +// @author Abhishek Dutta <adutta@robots.ox.ac.uk> +// @date 17 June 2017 +// +//////////////////////////////////////////////////////////////////////////////// + +function _via_region( + shape, + id, + data_img_space, + view_scale_factor, + view_offset_x, + view_offset_y +) { + // Note the following terminology: + // view space : + // - corresponds to the x-y plane on which the scaled version of original image is shown to the user + // - all the region query operations like is_inside(), is_on_edge(), etc are performed in view space + // - all svg draw operations like get_svg() are also in view space + // + // image space : + // - corresponds to the x-y plane which corresponds to the spatial space of the original image + // - region save, export, git push operations are performed in image space + // - to avoid any rounding issues (caused by floating scale factor), + // * user drawn regions in view space is first converted to image space + // * this region in image space is now used to initialize region in view space + // + // The two spaces are related by _via_model.now.tform.scale which is computed by the method + // _via_ctrl.compute_view_panel_to_nowfile_tform() + // and applied as follows: + // x coordinate in image space = scale_factor * x coordinate in view space + // + // shape : {rect, circle, ellipse, line, polyline, polygon, point} + // id : unique region-id + // d[] : (in view space) data whose meaning depend on region shape as follows: + // rect : d[x1,y1,x2,y2] or d[corner1_x, corner1_y, corner2_x, corner2_y] + // circle : d[x1,y1,x2,y2] or d[center_x, center_y, circumference_x, circumference_y] + // ellipse : d[x1,y1,x2,y2,transform] + // line : d[x1,y1,x2,y2] + // polyline : d[x1,y1,...,xn,yn] + // polygon : d[x1,y1,...,xn,yn] + // point : d[cx,cy] + // scale_factor : for conversion from view space to image space + // + // Note: no svg data are stored with prefix "_". For example: _scale_factor, _x2 + this.shape = shape; + this.id = id; + this.scale_factor = view_scale_factor; + this.offset_x = view_offset_x; + this.offset_y = view_offset_y; + this.recompute_svg = false; + this.attributes = {}; + + var n = data_img_space.length; + var i; + this.dview = new Array(n); + this.dimg = new Array(n); + + if (n !== 0) { + // IMPORTANT: + // to avoid any rounding issues (caused by floating scale factor), we stick to + // the principal that image space coordinates are the ground truth for every region. + // Hence, we proceed as: + // * user drawn regions in view space is first converted to image space + // * this region in image space is now used to initialize region in view space + for (i = 0; i < n; i++) { + this.dimg[i] = data_img_space[i]; + + var offset = this.offset_x; + if (i % 2 !== 0) { + // y coordinate + offset = this.offset_y; + } + this.dview[i] = Math.round(this.dimg[i] * this.scale_factor) + offset; + } + } + + // set svg attributes for each shape + switch (this.shape) { + case "rect": + _via_region_rect.call(this); + this.svg_attributes = ["x", "y", "width", "height"]; + break; + case "circle": + _via_region_circle.call(this); + this.svg_attributes = ["cx", "cy", "r"]; + break; + case "ellipse": + _via_region_ellipse.call(this); + this.svg_attributes = ["cx", "cy", "rx", "ry", "transform"]; + break; + case "line": + _via_region_line.call(this); + this.svg_attributes = ["x1", "y1", "x2", "y2"]; + break; + case "polyline": + _via_region_polyline.call(this); + this.svg_attributes = ["points"]; + break; + case "polygon": + _via_region_polygon.call(this); + this.svg_attributes = ["points"]; + break; + case "point": + _via_region_point.call(this); + // point is a special circle with minimal radius required for visualization + this.shape = "circle"; + this.svg_attributes = ["cx", "cy", "r"]; + break; + } + + this.initialize(); +} + +_via_region.prototype.prepare_svg_element = function () { + var _VIA_SVG_NS = "http://www.w3.org/2000/svg"; + this.svg_element = document.createElementNS(_VIA_SVG_NS, this.shape); + this.svg_string = "<" + this.shape; + this.svg_element.setAttributeNS(null, "id", this.id); + + var n = this.svg_attributes.length; + for (var i = 0; i < n; i++) { + this.svg_element.setAttributeNS( + null, + this.svg_attributes[i], + this[this.svg_attributes[i]] + ); + this.svg_string += + " " + this.svg_attributes[i] + '="' + this[this.svg_attributes[i]] + '"'; + } + this.svg_string += "/>"; +}; + +_via_region.prototype.get_svg_element = function () { + if (this.recompute_svg) { + this.prepare_svg_element(); + this.recompute_svg = false; + } + return this.svg_element; +}; + +_via_region.prototype.get_svg_string = function () { + if (this.recompute_svg) { + this.prepare_svg_element(); + this.recompute_svg = false; + } + return this.svg_string; +}; + +/// +/// Region shape : rectangle +/// +function _via_region_rect() { + this.is_inside = _via_region_rect.prototype.is_inside; + this.is_on_edge = _via_region_rect.prototype.is_on_edge; + this.move = _via_region_rect.prototype.move; + this.resize = _via_region_rect.prototype.resize; + this.initialize = _via_region_rect.prototype.initialize; + this.dist_to_nearest_edge = _via_region_rect.prototype.dist_to_nearest_edge; +} + +_via_region_rect.prototype.initialize = function () { + // ensure that this.(x,y) corresponds to top-left corner of rectangle + // Note: this.(x2,y2) is defined for convenience in calculations + if (this.dview[0] < this.dview[2]) { + this.x = this.dview[0]; + this.x2 = this.dview[2]; + } else { + this.x = this.dview[2]; + this.x2 = this.dview[0]; + } + if (this.dview[1] < this.dview[3]) { + this.y = this.dview[1]; + this.y2 = this.dview[3]; + } else { + this.y = this.dview[3]; + this.y2 = this.dview[1]; + } + this.width = this.x2 - this.x; + this.height = this.y2 - this.y; + this.recompute_svg = true; +}; + +/// +/// Region shape : circle +/// +function _via_region_circle() { + this.is_inside = _via_region_circle.prototype.is_inside; + this.is_on_edge = _via_region_circle.prototype.is_on_edge; + this.move = _via_region_circle.prototype.move; + this.resize = _via_region_circle.prototype.resize; + this.initialize = _via_region_circle.prototype.initialize; + this.dist_to_nearest_edge = _via_region_circle.prototype.dist_to_nearest_edge; +} + +_via_region_circle.prototype.initialize = function () { + this.cx = this.dview[0]; + this.cy = this.dview[1]; + var dx = this.dview[2] - this.dview[0]; + var dy = this.dview[3] - this.dview[1]; + this.r = Math.round(Math.sqrt(dx * dx + dy * dy)); + this.r2 = this.r * this.r; + this.recompute_svg = true; +}; + +/// +/// Region shape : ellipse +/// +function _via_region_ellipse() { + this.is_inside = _via_region_ellipse.prototype.is_inside; + this.is_on_edge = _via_region_ellipse.prototype.is_on_edge; + this.move = _via_region_ellipse.prototype.move; + this.resize = _via_region_ellipse.prototype.resize; + this.initialize = _via_region_ellipse.prototype.initialize; + this.dist_to_nearest_edge = + _via_region_ellipse.prototype.dist_to_nearest_edge; +} + +_via_region_ellipse.prototype.initialize = function () { + this.cx = this.dview[0]; + this.cy = this.dview[1]; + this.rx = Math.abs(this.dview[2] - this.dview[0]); + this.ry = Math.abs(this.dview[3] - this.dview[1]); + + this.inv_rx2 = 1 / (this.rx * this.rx); + this.inv_ry2 = 1 / (this.ry * this.ry); + + this.recompute_svg = true; +}; + +/// +/// Region shape : line +/// +function _via_region_line() { + this.is_inside = _via_region_line.prototype.is_inside; + this.is_on_edge = _via_region_line.prototype.is_on_edge; + this.move = _via_region_line.prototype.move; + this.resize = _via_region_line.prototype.resize; + this.initialize = _via_region_line.prototype.initialize; + this.dist_to_nearest_edge = _via_region_line.prototype.dist_to_nearest_edge; +} + +_via_region_line.prototype.initialize = function () { + this.x1 = this.dview[0]; + this.y1 = this.dview[1]; + this.x2 = this.dview[2]; + this.y2 = this.dview[3]; + this.dx = this.x1 - this.x2; + this.dy = this.y1 - this.y2; + this.mconst = this.x1 * this.y2 - this.x2 * this.y1; + + this.recompute_svg = true; +}; + +/// +/// Region shape : polyline +/// +function _via_region_polyline() { + this.is_inside = _via_region_polyline.prototype.is_inside; + this.is_on_edge = _via_region_polyline.prototype.is_on_edge; + this.move = _via_region_polyline.prototype.move; + this.resize = _via_region_polyline.prototype.resize; + this.initialize = _via_region_polyline.prototype.initialize; + this.dist_to_nearest_edge = + _via_region_polyline.prototype.dist_to_nearest_edge; +} + +_via_region_polyline.prototype.initialize = function () { + var n = this.dview.length; + var points = new Array(n / 2); + var points_index = 0; + for (var i = 0; i < n; i += 2) { + points[points_index] = this.dview[i] + " " + this.dview[i + 1]; + points_index++; + } + this.points = points.join(","); + this.recompute_svg = true; +}; + +/// +/// Region shape : polygon +/// +function _via_region_polygon() { + this.is_inside = _via_region_polygon.prototype.is_inside; + this.is_on_edge = _via_region_polygon.prototype.is_on_edge; + this.move = _via_region_polygon.prototype.move; + this.resize = _via_region_polygon.prototype.resize; + this.initialize = _via_region_polygon.prototype.initialize; + this.dist_to_nearest_edge = + _via_region_polygon.prototype.dist_to_nearest_edge; +} + +_via_region_polygon.prototype.initialize = function () { + var n = this.dview.length; + var points = new Array(n / 2); + var points_index = 0; + for (var i = 0; i < n; i += 2) { + points[points_index] = this.dview[i] + " " + this.dview[i + 1]; + points_index++; + } + this.points = points.join(","); + this.recompute_svg = true; +}; + +/// +/// Region shape : point +/// +function _via_region_point() { + this.is_inside = _via_region_point.prototype.is_inside; + this.is_on_edge = _via_region_point.prototype.is_on_edge; + this.move = _via_region_point.prototype.move; + this.resize = _via_region_point.prototype.resize; + this.initialize = _via_region_point.prototype.initialize; + this.dist_to_nearest_edge = _via_region_point.prototype.dist_to_nearest_edge; +} + +_via_region_point.prototype.initialize = function () { + this.cx = this.dview[0]; + this.cy = this.dview[1]; + this.r = 2; + this.r2 = this.r * this.r; + this.recompute_svg = true; +}; + +// +// find location of file +// + +function _via_file_resolve_all_to_default_filepath() { + var img_id; + for (img_id in _via_img_metadata) { + if (_via_img_metadata.hasOwnProperty(img_id)) { + _via_file_resolve_file_to_default_filepath(img_id); + } + } +} + +function _via_file_resolve_file_to_default_filepath(img_id) { + if (_via_img_metadata.hasOwnProperty(img_id)) { + if ( + typeof _via_img_fileref[img_id] === "undefined" || + !_via_img_fileref[img_id] instanceof File + ) { + if (is_url(_via_img_metadata[img_id].filename)) { + _via_img_src[img_id] = _via_img_metadata[img_id].filename; + } else { + let file_path = "file:/" + settings.defaultPath + _via_img_metadata[img_id].filename; + _via_img_src[img_id] = file_path + + } + } + } +} + +function _via_file_resolve_all() { + return new Promise(function (ok_callback, err_callback) { + var all_promises = []; + + var search_path_list = _via_file_get_search_path_list(); + var i, img_id; + for (i = 0; i < _via_img_count; ++i) { + img_id = _via_image_id_list[i]; + if ( + typeof _via_img_src[img_id] === "undefined" || + _via_img_src[img_id] === "" + ) { + var p = _via_file_resolve(i, search_path_list); + all_promises.push(p); + } + } + + Promise.all(all_promises).then( + function (ok_file_index_list) { + console.log(ok_file_index_list); + ok_callback(); + //project_file_load_on_success(ok_file_index); + }, + function (err_file_index_list) { + console.log(err_file_index_list); + err_callback(); + //project_file_load_on_fail(err_file_index); + } + ); + }); +} + +function _via_file_get_search_path_list() { + var search_path_list = []; + var path; + for (path in _via_settings.core.filepath) { + if (_via_settings.core.filepath[path] !== 0) { + search_path_list.push(path); + } + } + return search_path_list; +} + +function _via_file_resolve(file_index, search_path_list) { + return new Promise(function (ok_callback, err_callback) { + var path_index = 0; + var p = _via_file_resolve_check_path( + file_index, + path_index, + search_path_list + ).then( + function (ok) { + ok_callback(ok); + }, + function (err) { + err_callback(err); + } + ); + }, false); +} + +function _via_file_resolve_check_path( + file_index, + path_index, + search_path_list +) { + return new Promise(function (ok_callback, err_callback) { + var img_id = _via_image_id_list[file_index]; + var img = new Image(0, 0); + + var img_path = + search_path_list[path_index] + _via_img_metadata[img_id].filename; + if (is_url(_via_img_metadata[img_id].filename)) { + if (search_path_list[path_index] !== "") { + // we search for the the image filename pointed by URL in local search paths + img_path = + search_path_list[path_index] + + get_filename_from_url(_via_img_metadata[img_id].filename); + } + } + + img.setAttribute("src", img_path); + + img.addEventListener( + "load", + function () { + _via_img_src[img_id] = img_path; + ok_callback(file_index); + }, + false + ); + img.addEventListener("abort", function () { + err_callback(file_index); + }); + img.addEventListener( + "error", + function () { + var new_path_index = path_index + 1; + if (new_path_index < search_path_list.length) { + _via_file_resolve_check_path( + file_index, + new_path_index, + search_path_list + ).then( + function (ok) { + ok_callback(file_index); + }, + function (err) { + err_callback(file_index); + } + ); + } else { + err_callback(file_index); + } + }, + false + ); + }, false); +} + +// +// page 404 (file not found) +// +function show_page_404(img_index) { + $("#loading").addClass("d-none"); + buffer.hideCurrentImage(); + + set_display_area_content(VIA_DISPLAY_AREA_CONTENT_NAME.PAGE_404); + $(`#selection_panel`).hide(); + + _via_image_index = img_index; + _via_image_id = _via_image_id_list[_via_image_index]; + buffer.imgLoaded = false; + sidebar.img_fn_list_ith_entry_selected(_via_image_index, true); + + document.getElementById("page_404_filename").innerHTML = + "[" + + (_via_image_index + 1) + + "]" + + _via_img_metadata[_via_image_id].filename; +} + +// +// utils +// + +function is_url(s) { + // @todo: ensure that this is sufficient to capture all image url + return !!( + s.startsWith("http://") || + s.startsWith("https://") || + s.startsWith("www.") + ); +} + +function get_filename_from_url(url) { + return url.substring(url.lastIndexOf("/") + 1); +} + +function fixfloat(x) { + return parseFloat(x.toFixed(VIA_FLOAT_PRECISION)); +} + +function shape_attribute_fixfloat(sa) { + for (var attr in sa) { + switch (attr) { + case "x": + case "y": + case "width": + case "height": + case "r": + case "rx": + case "ry": + sa[attr] = fixfloat(sa[attr]); + break; + case "all_points_x": + case "all_points_y": + for (var i in sa[attr]) { + sa[attr][i] = fixfloat(sa[attr][i]); + } + } + } +} + +// start with the array having smallest number of elements +// check the remaining arrays if they all contain the elements of this shortest array +function array_intersect(array_list) { + if (array_list.length === 0) { + return []; + } + if (array_list.length === 1) { + return array_list[0]; + } + + var shortest_array = array_list[0]; + var shortest_array_index = 0; + var i; + for (i = 1; i < array_list.length; ++i) { + if (array_list[i].length < shortest_array.length) { + shortest_array = array_list[i]; + shortest_array_index = i; + } + } + + var intersect = []; + var element_count = {}; + + var array_index_i; + for (i = 0; i < array_list.length; ++i) { + if (i === 0) { + // in the first iteration, process the shortest element array + array_index_i = shortest_array_index; + } else { + array_index_i = i; + } + + var j; + for (j = 0; j < array_list[array_index_i].length; ++j) { + if (element_count[array_list[array_index_i][j]] === i - 1) { + if (i === array_list.length - 1) { + intersect.push(array_list[array_index_i][j]); + element_count[array_list[array_index_i][j]] = 0; + } else { + element_count[array_list[array_index_i][j]] = i; + } + } else { + element_count[array_list[array_index_i][j]] = 0; + } + } + } + return intersect; +} + +function generate_img_index_list(input) { + var all_img_index_list = []; + + // condition: count format a,b + var count_format_img_index_list = []; + if (input.prev_next_count.value !== "") { + var prev_next_split = input.prev_next_count.value.split(","); + if (prev_next_split.length === 2) { + var prev = parseInt(prev_next_split[0]); + var next = parseInt(prev_next_split[1]); + var i; + for (i = _via_image_index - prev; i <= _via_image_index + next; i++) { + count_format_img_index_list.push(i); + } + } + } + if (count_format_img_index_list.length !== 0) { + all_img_index_list.push(count_format_img_index_list); + } + + //condition: image index list expression + var expr_img_index_list = []; + if (input.img_index_list.value !== "") { + var img_index_expr = input.img_index_list.value.split(","); + if (img_index_expr.length !== 0) { + var i; + for (i = 0; i < img_index_expr.length; ++i) { + if (img_index_expr[i].includes("-")) { + var ab = img_index_expr[i].split("-"); + var a = parseInt(ab[0]) - 1; // 0 based indexing + var b = parseInt(ab[1]) - 1; + var j; + for (j = a; j <= b; ++j) { + expr_img_index_list.push(j); + } + } else { + expr_img_index_list.push(parseInt(img_index_expr[i]) - 1); + } + } + } + } + if (expr_img_index_list.length !== 0) { + all_img_index_list.push(expr_img_index_list); + } + + // condition: regular expression + var regex_img_index_list = []; + if (input.regex.value !== "") { + var regex = input.regex.value; + for (var i = 0; i < _via_image_filename_list.length; ++i) { + var filename = _via_image_filename_list[i]; + if (filename.match(regex) !== null) { + regex_img_index_list.push(i); + } + } + } + if (regex_img_index_list.length !== 0) { + all_img_index_list.push(regex_img_index_list); + } + + var intersect = array_intersect(all_img_index_list); + return intersect; +} + +if (!_via_is_debug_mode) { + // warn user of possible loss of data + window.onbeforeunload = function (e) { + e = e || window.event; + + // For IE and Firefox prior to version 4 + if (e) { + e.returnValue = "Did you save your data?"; + } + + // For Safari + return "Did you save your data?"; + }; +} + +// +// keep a record of image statistics (e.g. width, height, ...) +// +function img_stat_set(img_index, stat) { + if (stat.length) { + _via_img_stat[img_index] = stat; + } else { + delete _via_img_stat[img_index]; + } +} + +function img_stat_set_all() { + return new Promise( + function (ok_callback, err_callback) { + var promise_list = []; + var img_id; + for (var img_index in _via_image_id_list) { + if (!_via_img_stat.hasOwnProperty(img_index)) { + img_id = _via_image_id_list[img_index]; + if ( + _via_img_metadata[img_id].file_attributes.hasOwnProperty("width") && + _via_img_metadata[img_id].file_attributes.hasOwnProperty("height") + ) { + _via_img_stat[img_index] = [ + _via_img_metadata[img_id].file_attributes["width"], + _via_img_metadata[img_id].file_attributes["height"], + ]; + } else { + promise_list.push(img_stat_get(img_index)); + } + } + } + if (promise_list.length) { + Promise.all(promise_list).then( + function (ok) { + ok_callback(); + }.bind(this), + function (err) { + console.warn("Failed to read statistics of all images!"); + err_callback(); + } + ); + } else { + ok_callback(); + } + }.bind(this) + ); +} + +function img_stat_get(img_index) { + return new Promise( + function (ok_callback, err_callback) { + var img_id = _via_image_id_list[img_index]; + var tmp_img = document.createElement("img"); + var tmp_file_object_url = null; + tmp_img.addEventListener( + "load", + function () { + _via_img_stat[img_index] = [ + tmp_img.naturalWidth, + tmp_img.naturalHeight, + ]; + if (tmp_file_object_url !== null) { + URL.revokeObjectURL(tmp_file_object_url); + } + ok_callback(); + }.bind(this) + ); + tmp_img.addEventListener( + "error", + function () { + _via_img_stat[img_index] = [-1, -1]; + if (tmp_file_object_url !== null) { + URL.revokeObjectURL(tmp_file_object_url); + } + ok_callback(); + }.bind(this) + ); + + if (_via_img_fileref[img_id] instanceof File) { + tmp_file_object_url = URL.createObjectURL(_via_img_fileref[img_id]); + tmp_img.src = tmp_file_object_url; + } else { + tmp_img.src = _via_img_src[img_id]; + } + }.bind(this) + ); +} + +// pts = [x0,y0,x1,y1,....] +function polygon_to_bbox(pts) { + var xmin = +Infinity; + var xmax = -Infinity; + var ymin = +Infinity; + var ymax = -Infinity; + for (var i = 0; i < pts.length; i = i + 2) { + if (pts[i] > xmax) { + xmax = pts[i]; + } + if (pts[i] < xmin) { + xmin = pts[i]; + } + if (pts[i + 1] > ymax) { + ymax = pts[i + 1]; + } + if (pts[i + 1] < ymin) { + ymin = pts[i + 1]; + } + } + return [xmin, ymin, xmax - xmin, ymax - ymin]; +} \ No newline at end of file diff --git a/src/js/zoom.js b/src/js/zoom.js new file mode 100644 index 0000000..eeefdb6 --- /dev/null +++ b/src/js/zoom.js @@ -0,0 +1,134 @@ +// zoom.js +// Description: This file contains the code for zooming in and out of the image. +// Zoom class is responsible for handling the zooming functionality and controlling external dependency called Panzoom. +class Zoom { + constructor() { + this.panzoom = Panzoom(image_panel, { + maxScale: 12, + minScale: 0.1, + disablePan: true, + }); + this.addEventListeners(); + this.isZooming = false; + this.init = true; + } + + setStartScale(scale) { + let startX = 0; + if (_via_current_image_width > _via_current_image_height) { + startX = _via_current_image_width / 2 - _via_current_image_width * scale / 2 + } else { + startX = _via_current_image_height / 2 - _via_current_image_height * scale / 2 + } + this.panzoom.setOptions({ + startScale: scale, + startX: -startX, + }); + // fix translation + + } + + addEventListeners() { + image_panel.addEventListener("wheel", (e) => { + e.preventDefault(); + this.zoomWithWheel(e); + }); + image_panel.addEventListener("panzoomzoom", (_) => { + if (this.init) { + this.init = false; + } else { + this.startPanzoomOperation(); + } + }); + image_panel.addEventListener("panzoomend", (_) => { + this.endPanzoomOperation(); + }); + + } + startPanzoomOperation() { + this.isZooming = true; + } + + endPanzoomOperation() { + if (this.isZooming) { + this.fixCanvasBlur(); + this.isZooming = false; + } + } + + showFixCanvasBlurButton() { + let button = document.createElement("button"); + button.innerHTML = "Fix canvas blur"; + button.style.position = "absolute"; + button.style.color = "white"; + button.style.top = "8px"; + button.style.right = "80px"; + button.style.zIndex = "100"; + button.classList.add("btn"); + button.classList.add("btn-primary"); + button.onclick = () => { + this.fixCanvasBlur(); + document.body.removeChild(button); + }; + setTimeout(() => { + document.body.removeChild(button); + }, 5000); + document.body.appendChild(button); + } + + zoomWithWheel(e) { + this.panzoom.zoomWithWheel(e); + } + + handleMoseDown(e) { + this.panzoom.handleDown(e); + } + + handleMoseUp(e) { + this.panzoom.handleUp(e); + } + + handleMoseMove(e) { + this.panzoom.handleMove(e); + } + + fixCanvasBlur() { + if (!buffer.imgLoaded) { + return; + } + requestAnimationFrame(() => { + let currentScale = this.panzoom.getScale(); + if (currentScale > 1) { + currentScale = 1; + } + drawing.setBoundarySize(Math.round(4 / currentScale)); + drawing.setFontSize(Math.round(6 / currentScale)); + drawing.redrawRegCanvas(); + _via_reg_canvas.focus(); + }); + } + + zoomIn() { + this.panzoom.zoomIn(); + } + + zoomOut() { + this.panzoom.zoomOut(); + } + + resetZoom() { + this.panzoom.reset(); + image.scaleImage(1); + _via_reg_canvas.style.transform = "scale(1)"; + _via_canvas_scale = 1; + } + + enablePan() { + this.panzoom.setOptions({ disablePan: false }); + } + + disablePan() { + this.panzoom.setOptions({ disablePan: true }); + } +} +const zoom = new Zoom(); \ No newline at end of file diff --git a/src/style/main.css b/src/style/main.css new file mode 100644 index 0000000..63edc5c --- /dev/null +++ b/src/style/main.css @@ -0,0 +1,247 @@ +@import url("https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"); +@import url("https://fonts.googleapis.com/css2?family=Open+Sans:wght@200;300;400;500;600;700&display=swap"); +@import url("https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"); + +/* image grid view */ +#image_grid_panel #image_grid_content { position:relative; overflow:hidden; margin:0; padding:0; outline:none; } +#image_grid_panel #image_grid_content #image_grid_content_img img { margin:0.3em; padding:0; border: 2px solid #3db4c4;} +#image_grid_panel #image_grid_content #image_grid_content_img .not_sel { opacity:0.6; border: none;} +#image_grid_panel #image_grid_content #image_grid_content_img { position:absolute; top:0; left:0; width:100%; height:100%; } +#image_grid_panel #image_grid_content #image_grid_content_rshape { position:absolute; top:0; left:0; width:100%; height:100%; pointer-events:none; } +#image_grid_panel #image_grid_content img { float:left; margin:0; } + +::-webkit-scrollbar { + width: 10px; + height: 10px; +} + +::-webkit-scrollbar-track { + background-color: #e4e4e4; + border-radius: 50px; +} + +::-webkit-scrollbar-thumb { + background-color: gray; + border-radius: 50px; +} + + +.btn-primary { + color: #fff; + background-color: #3db4c4; + border-color: #3db4c4; +} + +.btn-primary:hover { + color: #fff; + background-color: #1c4073; + border-color: #272b2d; +} + +.nav-tabs .nav-link { + color: #1c4073; +} + +.form-range::-webkit-slider-thumb { + background-color: #3db4c4; + border: none; +} + +.form-check .form-switch:checked { + background-color: #3db4c4; +} + +.form-check .form-switch { + color: #3db4c4; +} + +.landing-text-card { + color: #fff; +} + +h1, h2 { + color: #3db4c4; +} + +.text-primary { + color: #3db4c4 !important; +} + +.bg-primary { + background-color: #3db4c4 !important; +} + +.bg-secondary { + background-color: #bdf283 !important; +} + +.underline { + text-decoration: underline; + text-decoration-color: #bdf283; + border-radius: 10px; +} + +.navbar-pharma { + background-color: #3db4c4; +} + +.navbar-pharma .nav-link { + color: #fff; +} + +body { + background-color: #f8f9fa !important; +} + +.navbar { + background-color: #f8f9fa !important; +} + +.navbar .nav-link { + color: #1c4073; +} + +.landing-text { + color: #1c4073; + font-weight: 600; +} + +.navbar .nav-link:hover { + color: #1c4073; +} + +.navbar .navbar-brand { + color: #fff; +} + +.navbar .navbar-brand:hover { + color: #fff; +} + +.navbar .nav-link.disabled { + color: #8a8484; +} + +.navbar .button { + color: #fff; + background-color: #3db4c4; + border-color: #3db4c4; +} + +.btn-outline-primary { + color: #3db4c4; + border-color: #3db4c4; +} + +.btn-outline-primary.active { + color: #fff; + background-color: #3db4c4; + border-color: #3db4c4; +} + +.btn-outline-primary:hover { + color: #fff; + background-color: #1c4073; + border-color: #3db4c4; +} + +.card-link { + color: #1c4073; +} + +.accordion-button:not(.collapsed) { + color: #fff; + background-color: #3db4c4; + border-color: #3db4c4; +} + +.accordion-button:not(.collapsed)::after { + color: #fff; + background-color: #3db4c4; + border-color: #3db4c4; +} + +.accordion-button:not(.collapsed):hover { + color: #fff; + background-color: #1c4073; + border-color: #3db4c4; +} + +.accordion-button:not(.collapsed):hover::after { + color: #fff; + background-color: #1c4073; + border-color: #3db4c4; +} + +.accordion-button:not(.collapsed):focus { + box-shadow: 0 0 0 0.25rem rgba(61, 180, 196, 0.5); +} + +.accordion-button:not(.collapsed):focus::after { + box-shadow: 0 0 0 0.25rem rgba(61, 180, 196, 0.5); +} + +.accordion-button:not(.collapsed):active { + color: #fff; + background-color: #1c4073; + border-color: #3db4c4; +} + + +.list-group-item.active { + color: #fff; + background-color: #3db4c4; + border-color: #3db4c4; +} + +.list-group-item.active:hover { + color: #fff; + background-color: #1c4073; + border-color: #3db4c4; +} + +.list-group-item.active:focus { + z-index: 2; + color: #fff; + background-color: #1c4073; + border-color: #3db4c4; +} + +.list-group-item.active.focus { + z-index: 2; + color: #fff; + background-color: #1c4073; + border-color: #3db4c4; +} + + +.btn-check:checked + .btn-outline-primary { + color: #fff; + background-color: #3db4c4; + border-color: #3db4c4; +} + +/* Hover state */ +.btn-outline-primary:hover { + color: #fff; + background-color: #1c4073; /* A darker shade of the primary color */ + border-color: #3db4c4; +} + +/* Active state (when the button is pressed) */ +.btn-outline-primary:active { + color: #fff; + background-color: #1c4073; /* A darker shade of the primary color */ + border-color: #3db4c4; +} + +/* For disabled buttons, to slightly change appearance on hover (optional) */ +.btn-outline-secondary:disabled:hover { + color: #3db4c4; + background-color: #fff; + border-color: #3db4c4; +} + +.table { + --bs-table-bg: transparent; /* Set to your desired default table background color */ +} \ No newline at end of file -- GitLab