diff --git a/.gitignore b/.gitignore index d0990d6dc47420c48da2cf26392051f02c7f5d06..47cea172a9aaddf34d7d9cdba081ea79a2f9718d 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,7 @@ build/ .ipynb_checkpoints/ # MacOS finder files -*.DS_Store \ No newline at end of file +*.DS_Store + +# Installed itk-vtk-viewer files +itk_vtk_viewer/ \ No newline at end of file diff --git a/docs/assets/screenshots/itk-vtk-viewer.gif b/docs/assets/screenshots/itk-vtk-viewer.gif new file mode 100644 index 0000000000000000000000000000000000000000..04c1c08725afa483acc225c40dcf6bd7d1186feb Binary files /dev/null and b/docs/assets/screenshots/itk-vtk-viewer.gif differ diff --git a/docs/cli.md b/docs/cli.md index 4b0ed8a8ce388db4700db38a5aeb0c0c730b7cd6..6295a98c2d38e575a45a8efc863d854c88375b43 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -4,7 +4,7 @@ The library also includes a command line interface for easy access to some funct This offers quick interactions, making it ideal for tasks that require efficiency or when certain functionalities need to run on a server. !!! Example - ```bash + ``` title="Command" qim3d gui --data-explorer ```  @@ -38,7 +38,7 @@ This offers quick interactions, making it ideal for tasks that require efficienc Here's an example of how to open the [Data Explorer](gui.md#data_explorer) - ``` + ``` title="Command" qim3d gui --data-explorer ``` ``` title="Output" @@ -54,7 +54,7 @@ This offers quick interactions, making it ideal for tasks that require efficienc Or for the local thickness GUI: - ``` + ``` title="Command" qim3d gui --local-thickness --host 127.0.0.1 --platform ``` @@ -98,23 +98,47 @@ The command line interface also allows you to easily visualize data. ### `qim3d viz` !!! quote "Reference" - Volumetric visualization uses K3D: - [Github page](https://github.com/K3D-tools/K3D-jupyter) + Volumetric visualization uses [itk-vtk-viewer](https://kitware.github.io/itk-vtk-viewer/docs/index.html) and [K3D](https://github.com/K3D-tools/K3D-jupyter). +You can launch volumetric visualizations directly from the command line. By default, it will launch the `itk-vtk-viewer`, but `k3d` can be selected by passing the argument `--method k3d`. -It is possible to launch the k3d visualization directly from the command line. +!!! info + If `itk-vtk-viewer` is not installed, you will be prompted for automatic installation via the `qim3d` library. + +| Argument | Description | +| ---------------- | --------------- | +| `source` | Path to the volume file or OME-Zarr store (any image format supported by `qim3d.io.load()`). | +| `--method` | Visualization method: `itk-vtk-viewer` (default) or `k3d`. | +| `--destination` | Custom `.html` file to be saved when using `k3d`. By default, `k3d.html` is saved. An file is aways saved when calling `k3d`. | +| `--no-browser` | Prevent the file from opening automatically when finished.| -| Arguments | Description | -| --------- | ----------- | -| `--source` | Path to the volume file (Any image format supported by `qim3d.io.load()`) | -| `--destination` | Path to the `html` file to be saved. By default, `k3d.html` is saved where the command is run. | -| `--no-browser` | Do not open the file when finished. | +!!! Example "Visualization of a OME-Zarr store" + ``` title="Command" + qim3d viz Okinawa_Foram_1.zarr/ + ``` + ``` title="Output" + itk-vtk-viewer + => Serving /home/fima/Notebooks/Qim3d on port 3000 -!!! Example + enp0s31f6 => http://10.52.0.158:3000/ + wlp0s20f3 => http://10.197.104.229:3000/ + + Serving directory '/home/fima/Notebooks/Qim3d' + http://localhost:8042/ + + Visualization url: + http://localhost:3000/?rotate=false&fileToLoad=http://localhost:8042/Okinawa_Foram_1.zarr ``` - qim3d viz --source blobs_256x256x256.tif + + A new tab in the default browser will be open with the visualization: + +  + +!!! Example "Example using k3d" + ``` title="Command" + qim3d viz blobs_256x256x256.tif --method k3d ``` ``` title="Output" @@ -131,8 +155,8 @@ It is possible to launch the k3d visualization directly from the command line. Or an specific path for destination can be used. We can also choose to not open the browser: -!!! Example - ``` +!!! Example "Example using k3d, saving html to custom path" + ``` title="Command" qim3d viz --source blobs_256x256x256.tif --destination my_plot.html --no-browser ``` @@ -148,7 +172,7 @@ Or an specific path for destination can be used. We can also choose to not open ## File preview Command line interface, which allows users to preview 3D structers or 2D images directly in command line. -### `qim3d preview <filename>` +### `qim3d preview` | Arguments | Description | | --------- | ----------- | | `--axis` | Specifies from which axis the slice will be taken. If the object is 2D image, then this is ignored. Defaults to 0.| @@ -157,35 +181,35 @@ Command line interface, which allows users to preview 3D structers or 2D images | `--absolute_values` |If values are low the image might be just black square. By default maximum value is set to 255. This flag turns this behaviour off.| !!! Example - ``` + ``` title="Command" qim3d preview blobs_256x256x256.tif ``` { width="512" } !!! Example - ``` + ``` title="Command" qim3d preview blobs_256x256x256.tif --resolution 30 ``` { width="512" } !!! Example - ``` + ``` title="Command" qim3d preview blobs_256x256x256.tif --resolution 50 --axis 1 ``` { width="512" } !!! Example - ``` + ``` title="Command" qim3d preview blobs_256x256x256.tif --resolution 50 --axis 2 --slice 0 ``` { width="512" } !!! Example - ``` + ``` title="Command" qim3d preview qim_logo.png --resolution 40 ``` diff --git a/docs/viz.md b/docs/viz.md index f5f2490acc9c5d89b783fd042eafd22a9adc2457..6a38714001dca3771ed99f9746175b738dc945fa 100644 --- a/docs/viz.md +++ b/docs/viz.md @@ -8,6 +8,7 @@ The `qim3d` library aims to provide easy ways to explore and get insights from v - slicer - orthogonal - vol + - itk_vtk - mesh - local_thickness - vectors diff --git a/qim3d/cli.py b/qim3d/cli.py index 032751b6ccddcc0c0f9a537f0c08abcbf4bd8d91..672bbaafa8ac35fba38f8150d69a2bb68f66cc21 100644 --- a/qim3d/cli.py +++ b/qim3d/cli.py @@ -1,7 +1,9 @@ import argparse import webbrowser +import platform import outputformat as ouf import qim3d +import os QIM_TITLE = ouf.rainbow( f"\n _ _____ __ \n ____ _(_)___ ___ |__ /____/ / \n / __ `/ / __ `__ \ /_ </ __ / \n/ /_/ / / / / / / /__/ / /_/ / \n\__, /_/_/ /_/ /_/____/\__,_/ \n /_/ v{qim3d.__version__}\n\n", @@ -42,7 +44,15 @@ def main(): # Viz viz_parser = subparsers.add_parser("viz", help="Volumetric visualization.") - viz_parser.add_argument("filename", default=False, help="Path to the image file") + viz_parser.add_argument("source", help="Path to the image file") + viz_parser.add_argument( + "-m", + "--method", + type=str, + metavar="METHOD", + default="itk-vtk", + help="Which method is used to display file.", + ) viz_parser.add_argument( "--destination", default="k3d.html", help="Path to save html file." ) @@ -142,19 +152,35 @@ def main(): interface.launch(inbrowser=inbrowser, force_light_mode=False) elif args.subcommand == "viz": - # Load the data - print(f"Loading data from {args.filename}") - volume = qim3d.io.load(str(args.filename)) - print(f"Done, volume shape: {volume.shape}") - - # Make k3d plot - print("\nGenerating k3d plot...") - qim3d.viz.vol(volume, show=False, save=str(args.destination)) - print(f"Done, plot available at <{args.destination}>") - - if not args.no_browser: - print("Opening in default browser...") - webbrowser.open_new_tab(args.destination) + + if args.method == "itk-vtk": + try: + # We need the full path to the file for the viewer + current_dir = os.getcwd() + full_path = os.path.normpath(os.path.join(current_dir, args.source)) + qim3d.viz.itk_vtk(full_path, open_browser=not args.no_browser) + + except qim3d.viz.NotInstalledError as err: + print(err) + message = "Itk-vtk-viewer is not installed or qim3d can not find it.\nYou can either:\n\to Use 'qim3d viz SOURCE -m k3d' to display data using different method\n\to Install itk-vtk-viewer yourself following https://kitware.github.io/itk-vtk-viewer/docs/cli.html#Installation\n\to Let QIM3D install itk-vtk-viewer now (it will also install node.js in qim3d library)\nDo you want QIM3D to install itk-vtk-viewer now?" + print(message) + answer = input("[Y/n]:") + if answer in "Yy": + qim3d.viz.Installer().install() + qim3d.viz.itk_vtk(full_path) + + elif args.method == "k3d": + volume = qim3d.io.load(str(args.source)) + print("\nGenerating k3d plot...") + qim3d.viz.vol(volume, show=False, save=str(args.destination)) + print(f"Done, plot available at <{args.destination}>") + if not args.no_browser: + print("Opening in default browser...") + webbrowser.open_new_tab(args.destination) + else: + raise NotImplementedError( + f"Method '{args.method}' is not valid. Try 'k3d' or default 'itk-vtk-viewer'" + ) elif args.subcommand == "preview": diff --git a/qim3d/utils/__init__.py b/qim3d/utils/__init__.py index 61f688060d76da9a2f7064770d4ef2d98e8659f2..edccc17593d06cb9307a0b04c2ab9c1e21734726 100644 --- a/qim3d/utils/__init__.py +++ b/qim3d/utils/__init__.py @@ -13,3 +13,5 @@ from .misc import ( downscale_img, scale_to_float16, ) + +from .server import start_http_server \ No newline at end of file diff --git a/qim3d/utils/server.py b/qim3d/utils/server.py new file mode 100644 index 0000000000000000000000000000000000000000..65344f1b8483d4066a99c32ad50a9870914184ab --- /dev/null +++ b/qim3d/utils/server.py @@ -0,0 +1,73 @@ +import os +from http.server import SimpleHTTPRequestHandler, HTTPServer +import threading +from qim3d.utils.logger import log + +class CustomHTTPRequestHandler(SimpleHTTPRequestHandler): + def end_headers(self): + """Add CORS headers to each response.""" + # Allow requests from any origin, or restrict to specific domains by specifying the origin + self.send_header("Access-Control-Allow-Origin", "*") + # Allow specific methods + self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS") + # Allow specific headers (if needed) + self.send_header("Access-Control-Allow-Headers", "X-Requested-With, Content-Type") + super().end_headers() + + def list_directory(self, path): + """Helper to produce a directory listing, includes hidden files.""" + try: + file_list = os.listdir(path) + except OSError: + self.send_error(404, "No permission to list directory") + return None + + # Sort the file list + file_list.sort(key=lambda a: a.lower()) + + # Format the list with hidden files included + displaypath = os.path.basename(path) + r = ['<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">'] + r.append(f"<html>\n<title>Directory listing for {displaypath}</title>\n") + r.append(f"<body>\n<h2>Directory listing for {displaypath}</h2>\n") + r.append("<hr>\n<ul>") + for name in file_list: + fullname = os.path.join(path, name) + displayname = linkname = name + + # Append the files and directories to the HTML list + if os.path.isdir(fullname): + displayname = name + "/" + linkname = name + "/" + r.append(f'<li><a href="{linkname}">{displayname}</a></li>') + r.append("</ul>\n<hr>\n</body>\n</html>\n") + encoded = "\n".join(r).encode('utf-8', 'surrogateescape') + self.send_response(200) + self.send_header("Content-Type", "text/html; charset=utf-8") + self.send_header("Content-Length", str(len(encoded))) + self.end_headers() + # Write the encoded HTML directly to the response + self.wfile.write(encoded) + +def start_http_server(directory, port=8000): + """ + Starts an HTTP server serving the specified directory on the given port with CORS enabled. + + Parameters: + directory (str): The directory to serve. + port (int): The port number to use (default is 8000). + """ + # Change the working directory to the specified directory + os.chdir(directory) + + # Create the server + server = HTTPServer(("", port), CustomHTTPRequestHandler) + + # Run the server in a separate thread so it doesn't block execution + thread = threading.Thread(target=server.serve_forever) + thread.daemon = True + thread.start() + + log.info(f"Serving directory '{directory}'\nhttp://localhost:{port}/") + + return server diff --git a/qim3d/viz/__init__.py b/qim3d/viz/__init__.py index bc23a7cbb07e1e093e9122c75ff4f592d809f0e0..28a9ed9257c921e03e3c3eb5c515b8eb2a0769c7 100644 --- a/qim3d/viz/__init__.py +++ b/qim3d/viz/__init__.py @@ -7,6 +7,7 @@ from .explore import ( slicer, slices, ) +from .itk_vtk_viewer import itk_vtk, Installer, NotInstalledError from .k3d import vol, mesh from .local_thickness_ import local_thickness from .structure_tensor import vectors diff --git a/qim3d/viz/itk_vtk_viewer/__init__.py b/qim3d/viz/itk_vtk_viewer/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..87678ceee22f43cf8673a4a8cfaf6510014a1726 --- /dev/null +++ b/qim3d/viz/itk_vtk_viewer/__init__.py @@ -0,0 +1,3 @@ +from .installation import Installer +from .run import itk_vtk +from .helpers import NotInstalledError \ No newline at end of file diff --git a/qim3d/viz/itk_vtk_viewer/helpers.py b/qim3d/viz/itk_vtk_viewer/helpers.py new file mode 100644 index 0000000000000000000000000000000000000000..bd426b11ffb0ba217e68b4bd64932107f5ff50b3 --- /dev/null +++ b/qim3d/viz/itk_vtk_viewer/helpers.py @@ -0,0 +1,75 @@ +from pathlib import Path +import os +import platform +from typing import Callable + +import qim3d + +class NotInstalledError(Exception): pass + +SOURCE_FNM = "fnm env --use-on-cd | Out-String | Invoke-Expression;" + +LINUX = 'Linux' +WINDOWS = 'Windows' +MAC = 'Darwin' + +def get_itk_dir() -> Path: + qim_dir = Path(qim3d.__file__).parents[0] #points to .../qim3d/qim3d/ + dir = qim_dir.joinpath("viz/itk_vtk_viewer") + return dir + +def get_nvm_dir(dir:Path = None) -> Path: + if platform.system() in [LINUX, MAC]: + following_folder = ".nvm" + elif platform.system() == WINDOWS: + following_folder = '' + return dir.joinpath(following_folder) if dir is not None else get_qim_dir().joinpath(following_folder) + +def get_node_binaries_dir(nvm_dir:Path = None) -> Path: + """ + Versions could change in time. This makes sure we use the newest one. + + For windows we have to pass the argument nvm_dir and it is the itk-vtk_dir + """ + if platform.system() in [LINUX, MAC]: + following_folder = "versions/node" + binaries_folder = 'bin' + elif platform.system() == WINDOWS: + following_folder = 'node-versions' + binaries_folder = 'installation' + + node_folder = nvm_dir.joinpath(following_folder) if nvm_dir is not None else get_nvm_dir().joinpath(following_folder) + + # We don't wanna throw an error + # Instead we return None and check the returned value in run.py + if not os.path.isdir(node_folder): + return None + l = sorted(os.listdir(node_folder)) + + for name in l[::-1]: + path = node_folder.joinpath(name) + if os.path.isdir(path): + return path.joinpath(binaries_folder) + +def get_viewer_dir(dir:Path = None) -> Path: + following_folder = "viewer_app" + return dir.joinpath(following_folder) if dir is not None else get_qim_dir().joinpath(following_folder) + +def get_viewer_binaries(viewer_dir:Path = None) -> Path: + following_folder1 = 'node_modules' + following_folder2 = '.bin' + if viewer_dir is None: + viewer_dir = get_viewer_dir() + return viewer_dir.joinpath(following_folder1).joinpath(following_folder2) + +def run_for_platform(linux_func:Callable, windows_func:Callable, macos_func:Callable): + this_platform = platform.system() + if this_platform == LINUX: + return linux_func() + elif this_platform == WINDOWS: + return windows_func() + elif this_platform == MAC: + return macos_func() + +def lambda_raise(err): + raise err diff --git a/qim3d/viz/itk_vtk_viewer/install_nvm.sh b/qim3d/viz/itk_vtk_viewer/install_nvm.sh new file mode 100755 index 0000000000000000000000000000000000000000..40c1cdf6aba9f898155fce4486cd5a7d9428d571 --- /dev/null +++ b/qim3d/viz/itk_vtk_viewer/install_nvm.sh @@ -0,0 +1,495 @@ +#!/usr/bin/env bash + +{ # this ensures the entire script is downloaded # + +nvm_has() { + type "$1" > /dev/null 2>&1 +} + +nvm_echo() { + command printf %s\\n "$*" 2>/dev/null +} + +if [ -z "${BASH_VERSION}" ] || [ -n "${ZSH_VERSION}" ]; then + # shellcheck disable=SC2016 + nvm_echo >&2 'Error: the install instructions explicitly say to pipe the install script to `bash`; please follow them' + exit 1 +fi + +nvm_grep() { + GREP_OPTIONS='' command grep "$@" +} + +nvm_default_install_dir() { + [ -z "${XDG_CONFIG_HOME-}" ] && printf %s "${HOME}/.nvm" || printf %s "${XDG_CONFIG_HOME}/nvm" +} + +nvm_install_dir() { + if [ -n "$NVM_DIR" ]; then + printf %s "${NVM_DIR}" + else + nvm_default_install_dir + fi +} + +nvm_latest_version() { + nvm_echo "v0.39.7" +} + +nvm_profile_is_bash_or_zsh() { + local TEST_PROFILE + TEST_PROFILE="${1-}" + case "${TEST_PROFILE-}" in + *"/.bashrc" | *"/.bash_profile" | *"/.zshrc" | *"/.zprofile") + return + ;; + *) + return 1 + ;; + esac +} + +# +# Outputs the location to NVM depending on: +# * The availability of $NVM_SOURCE +# * The presence of $NVM_INSTALL_GITHUB_REPO +# * The method used ("script" or "git" in the script, defaults to "git") +# NVM_SOURCE always takes precedence unless the method is "script-nvm-exec" +# +nvm_source() { + local NVM_GITHUB_REPO + NVM_GITHUB_REPO="${NVM_INSTALL_GITHUB_REPO:-nvm-sh/nvm}" + if [ "${NVM_GITHUB_REPO}" != 'nvm-sh/nvm' ]; then + { nvm_echo >&2 "$(cat)" ; } << EOF +@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ +@ WARNING: REMOTE REPO IDENTIFICATION HAS CHANGED! @ +@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ +IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY! + +The default repository for this install is \`nvm-sh/nvm\`, +but the environment variables \`\$NVM_INSTALL_GITHUB_REPO\` is +currently set to \`${NVM_GITHUB_REPO}\`. + +If this is not intentional, interrupt this installation and +verify your environment variables. +EOF + fi + local NVM_VERSION + NVM_VERSION="${NVM_INSTALL_VERSION:-$(nvm_latest_version)}" + local NVM_METHOD + NVM_METHOD="$1" + local NVM_SOURCE_URL + NVM_SOURCE_URL="$NVM_SOURCE" + if [ "_$NVM_METHOD" = "_script-nvm-exec" ]; then + NVM_SOURCE_URL="https://raw.githubusercontent.com/${NVM_GITHUB_REPO}/${NVM_VERSION}/nvm-exec" + elif [ "_$NVM_METHOD" = "_script-nvm-bash-completion" ]; then + NVM_SOURCE_URL="https://raw.githubusercontent.com/${NVM_GITHUB_REPO}/${NVM_VERSION}/bash_completion" + elif [ -z "$NVM_SOURCE_URL" ]; then + if [ "_$NVM_METHOD" = "_script" ]; then + NVM_SOURCE_URL="https://raw.githubusercontent.com/${NVM_GITHUB_REPO}/${NVM_VERSION}/nvm.sh" + elif [ "_$NVM_METHOD" = "_git" ] || [ -z "$NVM_METHOD" ]; then + NVM_SOURCE_URL="https://github.com/${NVM_GITHUB_REPO}.git" + else + nvm_echo >&2 "Unexpected value \"$NVM_METHOD\" for \$NVM_METHOD" + return 1 + fi + fi + nvm_echo "$NVM_SOURCE_URL" +} + +# +# Node.js version to install +# +nvm_node_version() { + nvm_echo "$NODE_VERSION" +} + +nvm_download() { + if nvm_has "curl"; then + curl --fail --compressed -q "$@" + elif nvm_has "wget"; then + # Emulate curl with wget + ARGS=$(nvm_echo "$@" | command sed -e 's/--progress-bar /--progress=bar /' \ + -e 's/--compressed //' \ + -e 's/--fail //' \ + -e 's/-L //' \ + -e 's/-I /--server-response /' \ + -e 's/-s /-q /' \ + -e 's/-sS /-nv /' \ + -e 's/-o /-O /' \ + -e 's/-C - /-c /') + # shellcheck disable=SC2086 + eval wget $ARGS + fi +} + +install_nvm_from_git() { + local INSTALL_DIR + INSTALL_DIR="$(nvm_install_dir)" + local NVM_VERSION + NVM_VERSION="${NVM_INSTALL_VERSION:-$(nvm_latest_version)}" + if [ -n "${NVM_INSTALL_VERSION:-}" ]; then + # Check if version is an existing ref + if command git ls-remote "$(nvm_source "git")" "$NVM_VERSION" | nvm_grep -q "$NVM_VERSION" ; then + : + # Check if version is an existing changeset + elif ! nvm_download -o /dev/null "$(nvm_source "script-nvm-exec")"; then + nvm_echo >&2 "Failed to find '$NVM_VERSION' version." + exit 1 + fi + fi + + local fetch_error + if [ -d "$INSTALL_DIR/.git" ]; then + # Updating repo + nvm_echo "=> nvm is already installed in $INSTALL_DIR, trying to update using git" + command printf '\r=> ' + fetch_error="Failed to update nvm with $NVM_VERSION, run 'git fetch' in $INSTALL_DIR yourself." + else + fetch_error="Failed to fetch origin with $NVM_VERSION. Please report this!" + nvm_echo "=> Downloading nvm from git to '$INSTALL_DIR'" + command printf '\r=> ' + mkdir -p "${INSTALL_DIR}" + if [ "$(ls -A "${INSTALL_DIR}")" ]; then + # Initializing repo + command git init "${INSTALL_DIR}" || { + nvm_echo >&2 'Failed to initialize nvm repo. Please report this!' + exit 2 + } + command git --git-dir="${INSTALL_DIR}/.git" remote add origin "$(nvm_source)" 2> /dev/null \ + || command git --git-dir="${INSTALL_DIR}/.git" remote set-url origin "$(nvm_source)" || { + nvm_echo >&2 'Failed to add remote "origin" (or set the URL). Please report this!' + exit 2 + } + else + # Cloning repo + command git clone "$(nvm_source)" --depth=1 "${INSTALL_DIR}" || { + nvm_echo >&2 'Failed to clone nvm repo. Please report this!' + exit 2 + } + fi + fi + # Try to fetch tag + if command git --git-dir="$INSTALL_DIR"/.git --work-tree="$INSTALL_DIR" fetch origin tag "$NVM_VERSION" --depth=1 2>/dev/null; then + : + # Fetch given version + elif ! command git --git-dir="$INSTALL_DIR"/.git --work-tree="$INSTALL_DIR" fetch origin "$NVM_VERSION" --depth=1; then + nvm_echo >&2 "$fetch_error" + exit 1 + fi + command git -c advice.detachedHead=false --git-dir="$INSTALL_DIR"/.git --work-tree="$INSTALL_DIR" checkout -f --quiet FETCH_HEAD || { + nvm_echo >&2 "Failed to checkout the given version $NVM_VERSION. Please report this!" + exit 2 + } + if [ -n "$(command git --git-dir="$INSTALL_DIR"/.git --work-tree="$INSTALL_DIR" show-ref refs/heads/master)" ]; then + if command git --no-pager --git-dir="$INSTALL_DIR"/.git --work-tree="$INSTALL_DIR" branch --quiet 2>/dev/null; then + command git --no-pager --git-dir="$INSTALL_DIR"/.git --work-tree="$INSTALL_DIR" branch --quiet -D master >/dev/null 2>&1 + else + nvm_echo >&2 "Your version of git is out of date. Please update it!" + command git --no-pager --git-dir="$INSTALL_DIR"/.git --work-tree="$INSTALL_DIR" branch -D master >/dev/null 2>&1 + fi + fi + + nvm_echo "=> Compressing and cleaning up git repository" + if ! command git --git-dir="$INSTALL_DIR"/.git --work-tree="$INSTALL_DIR" reflog expire --expire=now --all; then + nvm_echo >&2 "Your version of git is out of date. Please update it!" + fi + if ! command git --git-dir="$INSTALL_DIR"/.git --work-tree="$INSTALL_DIR" gc --auto --aggressive --prune=now ; then + nvm_echo >&2 "Your version of git is out of date. Please update it!" + fi + return +} + +# +# Automatically install Node.js +# +nvm_install_node() { + local NODE_VERSION_LOCAL + NODE_VERSION_LOCAL="$(nvm_node_version)" + + if [ -z "$NODE_VERSION_LOCAL" ]; then + return 0 + fi + + nvm_echo "=> Installing Node.js version $NODE_VERSION_LOCAL" + nvm install "$NODE_VERSION_LOCAL" + local CURRENT_NVM_NODE + + CURRENT_NVM_NODE="$(nvm_version current)" + if [ "$(nvm_version "$NODE_VERSION_LOCAL")" == "$CURRENT_NVM_NODE" ]; then + nvm_echo "=> Node.js version $NODE_VERSION_LOCAL has been successfully installed" + else + nvm_echo >&2 "Failed to install Node.js $NODE_VERSION_LOCAL" + fi +} + +install_nvm_as_script() { + local INSTALL_DIR + INSTALL_DIR="$(nvm_install_dir)" + local NVM_SOURCE_LOCAL + NVM_SOURCE_LOCAL="$(nvm_source script)" + local NVM_EXEC_SOURCE + NVM_EXEC_SOURCE="$(nvm_source script-nvm-exec)" + local NVM_BASH_COMPLETION_SOURCE + NVM_BASH_COMPLETION_SOURCE="$(nvm_source script-nvm-bash-completion)" + + # Downloading to $INSTALL_DIR + mkdir -p "$INSTALL_DIR" + if [ -f "$INSTALL_DIR/nvm.sh" ]; then + nvm_echo "=> nvm is already installed in $INSTALL_DIR, trying to update the script" + else + nvm_echo "=> Downloading nvm as script to '$INSTALL_DIR'" + fi + nvm_download -s "$NVM_SOURCE_LOCAL" -o "$INSTALL_DIR/nvm.sh" || { + nvm_echo >&2 "Failed to download '$NVM_SOURCE_LOCAL'" + return 1 + } & + nvm_download -s "$NVM_EXEC_SOURCE" -o "$INSTALL_DIR/nvm-exec" || { + nvm_echo >&2 "Failed to download '$NVM_EXEC_SOURCE'" + return 2 + } & + nvm_download -s "$NVM_BASH_COMPLETION_SOURCE" -o "$INSTALL_DIR/bash_completion" || { + nvm_echo >&2 "Failed to download '$NVM_BASH_COMPLETION_SOURCE'" + return 2 + } & + for job in $(jobs -p | command sort) + do + wait "$job" || return $? + done + chmod a+x "$INSTALL_DIR/nvm-exec" || { + nvm_echo >&2 "Failed to mark '$INSTALL_DIR/nvm-exec' as executable" + return 3 + } +} + +nvm_try_profile() { + if [ -z "${1-}" ] || [ ! -f "${1}" ]; then + return 1 + fi + nvm_echo "${1}" +} + +# +# Detect profile file if not specified as environment variable +# (eg: PROFILE=~/.myprofile) +# The echo'ed path is guaranteed to be an existing file +# Otherwise, an empty string is returned +# +nvm_detect_profile() { + if [ "${PROFILE-}" = '/dev/null' ]; then + # the user has specifically requested NOT to have nvm touch their profile + return + fi + + if [ -n "${PROFILE}" ] && [ -f "${PROFILE}" ]; then + nvm_echo "${PROFILE}" + return + fi + + local DETECTED_PROFILE + DETECTED_PROFILE='' + + if [ "${SHELL#*bash}" != "$SHELL" ]; then + if [ -f "$HOME/.bashrc" ]; then + DETECTED_PROFILE="$HOME/.bashrc" + elif [ -f "$HOME/.bash_profile" ]; then + DETECTED_PROFILE="$HOME/.bash_profile" + fi + elif [ "${SHELL#*zsh}" != "$SHELL" ]; then + if [ -f "$HOME/.zshrc" ]; then + DETECTED_PROFILE="$HOME/.zshrc" + elif [ -f "$HOME/.zprofile" ]; then + DETECTED_PROFILE="$HOME/.zprofile" + fi + fi + + if [ -z "$DETECTED_PROFILE" ]; then + for EACH_PROFILE in ".profile" ".bashrc" ".bash_profile" ".zprofile" ".zshrc" + do + if DETECTED_PROFILE="$(nvm_try_profile "${HOME}/${EACH_PROFILE}")"; then + break + fi + done + fi + + if [ -n "$DETECTED_PROFILE" ]; then + nvm_echo "$DETECTED_PROFILE" + fi +} + +# +# Check whether the user has any globally-installed npm modules in their system +# Node, and warn them if so. +# +nvm_check_global_modules() { + local NPM_COMMAND + NPM_COMMAND="$(command -v npm 2>/dev/null)" || return 0 + [ -n "${NVM_DIR}" ] && [ -z "${NPM_COMMAND%%"$NVM_DIR"/*}" ] && return 0 + + local NPM_VERSION + NPM_VERSION="$(npm --version)" + NPM_VERSION="${NPM_VERSION:--1}" + [ "${NPM_VERSION%%[!-0-9]*}" -gt 0 ] || return 0 + + local NPM_GLOBAL_MODULES + NPM_GLOBAL_MODULES="$( + npm list -g --depth=0 | + command sed -e '/ npm@/d' -e '/ (empty)$/d' + )" + + local MODULE_COUNT + MODULE_COUNT="$( + command printf %s\\n "$NPM_GLOBAL_MODULES" | + command sed -ne '1!p' | # Remove the first line + wc -l | command tr -d ' ' # Count entries + )" + + if [ "${MODULE_COUNT}" != '0' ]; then + # shellcheck disable=SC2016 + nvm_echo '=> You currently have modules installed globally with `npm`. These will no' + # shellcheck disable=SC2016 + nvm_echo '=> longer be linked to the active version of Node when you install a new node' + # shellcheck disable=SC2016 + nvm_echo '=> with `nvm`; and they may (depending on how you construct your `$PATH`)' + # shellcheck disable=SC2016 + nvm_echo '=> override the binaries of modules installed with `nvm`:' + nvm_echo + + command printf %s\\n "$NPM_GLOBAL_MODULES" + nvm_echo '=> If you wish to uninstall them at a later point (or re-install them under your' + # shellcheck disable=SC2016 + nvm_echo '=> `nvm` Nodes), you can remove them from the system Node as follows:' + nvm_echo + nvm_echo ' $ nvm use system' + nvm_echo ' $ npm uninstall -g a_module' + nvm_echo + fi +} + +nvm_do_install() { + if [ -n "${NVM_DIR-}" ] && ! [ -d "${NVM_DIR}" ]; then + if [ -e "${NVM_DIR}" ]; then + nvm_echo >&2 "File \"${NVM_DIR}\" has the same name as installation directory." + exit 1 + fi + + if [ "${NVM_DIR}" = "$(nvm_default_install_dir)" ]; then + mkdir "${NVM_DIR}" + else + nvm_echo >&2 "You have \$NVM_DIR set to \"${NVM_DIR}\", but that directory does not exist. Check your profile files and environment." + exit 1 + fi + fi + # Disable the optional which check, https://www.shellcheck.net/wiki/SC2230 + # shellcheck disable=SC2230 + if nvm_has xcode-select && [ "$(xcode-select -p >/dev/null 2>/dev/null ; echo $?)" = '2' ] && [ "$(which git)" = '/usr/bin/git' ] && [ "$(which curl)" = '/usr/bin/curl' ]; then + nvm_echo >&2 'You may be on a Mac, and need to install the Xcode Command Line Developer Tools.' + # shellcheck disable=SC2016 + nvm_echo >&2 'If so, run `xcode-select --install` and try again. If not, please report this!' + exit 1 + fi + if [ -z "${METHOD}" ]; then + # Autodetect install method + if nvm_has git; then + install_nvm_from_git + elif nvm_has curl || nvm_has wget; then + install_nvm_as_script + else + nvm_echo >&2 'You need git, curl, or wget to install nvm' + exit 1 + fi + elif [ "${METHOD}" = 'git' ]; then + if ! nvm_has git; then + nvm_echo >&2 "You need git to install nvm" + exit 1 + fi + install_nvm_from_git + elif [ "${METHOD}" = 'script' ]; then + if ! nvm_has curl && ! nvm_has wget; then + nvm_echo >&2 "You need curl or wget to install nvm" + exit 1 + fi + install_nvm_as_script + else + nvm_echo >&2 "The environment variable \$METHOD is set to \"${METHOD}\", which is not recognized as a valid installation method." + exit 1 + fi + + nvm_echo + + local NVM_PROFILE + NVM_PROFILE="$(nvm_detect_profile)" + local PROFILE_INSTALL_DIR + PROFILE_INSTALL_DIR="$(nvm_install_dir | command sed "s:^$HOME:\$HOME:")" + + SOURCE_STR="\\nexport NVM_DIR=\"${PROFILE_INSTALL_DIR}\"\\n[ -s \"\$NVM_DIR/nvm.sh\" ] && \\. \"\$NVM_DIR/nvm.sh\" # This loads nvm\\n" + + # shellcheck disable=SC2016 + COMPLETION_STR='[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion\n' + BASH_OR_ZSH=false + + if [ -z "${NVM_PROFILE-}" ] ; then + local TRIED_PROFILE + if [ -n "${PROFILE}" ]; then + TRIED_PROFILE="${NVM_PROFILE} (as defined in \$PROFILE), " + fi + nvm_echo "=> Profile not found. Tried ${TRIED_PROFILE-}~/.bashrc, ~/.bash_profile, ~/.zprofile, ~/.zshrc, and ~/.profile." + nvm_echo "=> Create one of them and run this script again" + nvm_echo " OR" + nvm_echo "=> Append the following lines to the correct file yourself:" + command printf "${SOURCE_STR}" + nvm_echo + else + if nvm_profile_is_bash_or_zsh "${NVM_PROFILE-}"; then + BASH_OR_ZSH=true + fi + if ! command grep -qc '/nvm.sh' "$NVM_PROFILE"; then + nvm_echo "=> Appending nvm source string to $NVM_PROFILE" + command printf "${SOURCE_STR}" >> "$NVM_PROFILE" + else + nvm_echo "=> nvm source string already in ${NVM_PROFILE}" + fi + # shellcheck disable=SC2016 + if ${BASH_OR_ZSH} && ! command grep -qc '$NVM_DIR/bash_completion' "$NVM_PROFILE"; then + nvm_echo "=> Appending bash_completion source string to $NVM_PROFILE" + command printf "$COMPLETION_STR" >> "$NVM_PROFILE" + else + nvm_echo "=> bash_completion source string already in ${NVM_PROFILE}" + fi + fi + if ${BASH_OR_ZSH} && [ -z "${NVM_PROFILE-}" ] ; then + nvm_echo "=> Please also append the following lines to the if you are using bash/zsh shell:" + command printf "${COMPLETION_STR}" + fi + + # Source nvm + # shellcheck source=/dev/null + \. "$(nvm_install_dir)/nvm.sh" + + nvm_check_global_modules + + nvm_install_node + + nvm_reset + + nvm_echo "=> Close and reopen your terminal to start using nvm or run the following to use it now:" + command printf "${SOURCE_STR}" + if ${BASH_OR_ZSH} ; then + command printf "${COMPLETION_STR}" + fi +} + +# +# Unsets the various functions defined +# during the execution of the install script +# +nvm_reset() { + unset -f nvm_has nvm_install_dir nvm_latest_version nvm_profile_is_bash_or_zsh \ + nvm_source nvm_node_version nvm_download install_nvm_from_git nvm_install_node \ + install_nvm_as_script nvm_try_profile nvm_detect_profile nvm_check_global_modules \ + nvm_do_install nvm_reset nvm_default_install_dir nvm_grep +} + +[ "_$NVM_ENV" = "_testing" ] || nvm_do_install + +} # this ensures the entire script is downloaded # diff --git a/qim3d/viz/itk_vtk_viewer/installation.py b/qim3d/viz/itk_vtk_viewer/installation.py new file mode 100644 index 0000000000000000000000000000000000000000..79729955a210c57dcef3bfa50601c0dd316ffe82 --- /dev/null +++ b/qim3d/viz/itk_vtk_viewer/installation.py @@ -0,0 +1,162 @@ +from pathlib import Path +import subprocess +import os +import platform + +from .helpers import get_itk_dir, get_nvm_dir, get_node_binaries_dir, get_viewer_dir, SOURCE_FNM, NotInstalledError, run_for_platform + + + +class Installer: + """ + Implements installation procedure of itk-vtk-viewer for each OS. + Also goes for minimal installation: checking if the necessary binaries aren't already installed + """ + def __init__(self): + self.platform = platform.system() + self.install_functions = (self.install_node_manager, self.install_node, self.install_viewer) + + self.dir = get_itk_dir() # itk_vtk_viewer folder within qim3d.viz + + # If nvm was already installed, there should be this environment variable + # However it could have also been installed via our process, or user deleted the folder but didn't adjusted the bashrc, that's why we check again + self.os_nvm_dir = os.getenv('NVM_DIR') + if self.os_nvm_dir is not None: + self.os_nvm_dir = Path(self.os_nvm_dir) + self.qim_nvm_dir = get_nvm_dir(self.dir) + if not os.path.isdir(self.qim_nvm_dir): + os.mkdir(self.qim_nvm_dir) + + @property + def is_node_manager_already_installed(self) -> bool: + """ + Checks for global and local installation of nvm (Node Version Manager) + """ + def _linux() -> bool: + command_f = lambda nvmsh: F'/bin/bash -c "source {nvmsh} && nvm"' + if self.os_nvm_dir is not None: + nvmsh = self.os_nvm_dir.joinpath('nvm.sh') + output = subprocess.run(command_f(nvmsh), shell = True, capture_output = True) + if not output.stderr: + self.nvm_dir = self.os_nvm_dir + return True + + nvmsh = self.qim_nvm_dir.joinpath('nvm.sh') + output = subprocess.run(command_f(nvmsh), shell = True, capture_output = True) + self.nvm_dir = self.qim_nvm_dir + return not bool(output.stderr) # If there is an error running the above command then it is not installed (not in expected location) + + def _windows() -> bool: + output = subprocess.run(['powershell.exe', 'fnm --version'], capture_output=True) + return not bool(output.stderr) + + return run_for_platform(linux_func=_linux, windows_func=_windows,macos_func= _linux) + + @property + def is_node_already_installed(self) -> bool: + """ + Checks for global and local installation of Node.js and npm (Node Package Manager) + """ + def _linux() -> bool: + # get_node_binaries_dir might return None if the folder is not there + # In that case there is 'None' added to the PATH, thats not a problem + # the command will return an error to the output and it will be evaluated as not installed + command = F'export PATH="$PATH:{get_node_binaries_dir(self.nvm_dir)}" && npm version' + + output = subprocess.run(command, shell = True, capture_output = True) + return not bool(output.stderr) + + def _windows() -> bool: + # Didn't figure out how to install the viewer and run it properly when using global npm + return False + + return run_for_platform(linux_func=_linux,windows_func= _windows,macos_func= _linux) + + + + def install(self): + """ + First check if some of the binaries are not installed already. + If node.js is already installed (it was able to call npm without raising an error) + it only has to install the viewer and doesn't have to go through the process + """ + if self.is_node_manager_already_installed: + self.install_status = 1 + print("Node manager already installed") + if self.is_node_already_installed: + self.install_status = 2 + print("Node.js already installed") + + else: + self.install_status = 0 + + for install_function in self.install_functions[self.install_status:]: + install_function() + + def install_node_manager(self): + def _linux(): + print(F'Installing Node manager into {self.nvm_dir}...') + _ = subprocess.run([F'export NVM_DIR={self.nvm_dir} && bash {self.dir.joinpath("install_nvm.sh")}'], shell = True, capture_output=True) + + def _windows(): + print("Installing node manager...") + subprocess.run(["powershell.exe", F'$env:XDG_DATA_HOME = "{self.dir}";', "winget install Schniz.fnm"]) + + + # self._run_for_platform(_linux, None, _windows) + run_for_platform(linux_func=_linux,windows_func= _windows,macos_func= _linux) + print("Node manager installed") + + def install_node(self): + def _linux(): + """ + If nvm was already installed, terminal should have environemnt variable 'NVM_DIR' where is nvm.sh + We have to source that file either way, to be able to call nvm function + If it was't installed before, we need to export NVM_DIR in order to install npm to correct location + """ + print(F'Installing node.js into {self.nvm_dir}...') + if self.install_status == 0: + nvm_dir = self.nvm_dir + prefix = F'export NVM_DIR={nvm_dir} && ' + + elif self.install_status == 1: + nvm_dir = self.os_nvm_dir + prefix = '' + + nvmsh = Path(nvm_dir).joinpath('nvm.sh') + command = f'{prefix}/bin/bash -c "source {nvmsh} && nvm install 22"' + output = subprocess.run(command, shell = True, capture_output=True) + + def _windows(): + subprocess.run(["powershell.exe", SOURCE_FNM, F"fnm use --fnm-dir {self.dir} --install-if-missing 22"]) + + print(F'Installing node.js...') + run_for_platform(linux_func = _linux, windows_func=_windows, macos_func=_linux) + print("Node.js installed") + + def install_viewer(self): + def _linux(): + # Adds local binaries to the path in case we had to install node first (locally into qim folder), but shouldnt interfere even if + # npm is installed globally + command = F'export PATH="$PATH:{get_node_binaries_dir(self.nvm_dir)}" && npm install --prefix {self.viewer_dir} itk-vtk-viewer' + output = subprocess.run([command], shell=True, capture_output=True) + # print(output.stderr) + + def _windows(): + try: + node_bin = get_node_binaries_dir(self.dir) + print(F'Installing into {self.viewer_dir}') + subprocess.run(["powershell.exe", F'$env:PATH=$env:PATH + \';{node_bin}\';', F"npm install --prefix {self.viewer_dir} itk-vtk-viewer"], capture_output=True) + except NotInstalledError: # Not installed in qim + subprocess.run(["powershell.exe", SOURCE_FNM, F"npm install itk-vtk-viewer"], capture_output=True) + + + self.viewer_dir = get_viewer_dir(self.dir) + if not os.path.isdir(self.viewer_dir): + os.mkdir(self.viewer_dir) + + print(F"Installing itk-vtk-viewer...") + run_for_platform(linux_func=_linux, windows_func=_windows, macos_func=_linux) + print("Itk-vtk-viewer installed") + + \ No newline at end of file diff --git a/qim3d/viz/itk_vtk_viewer/run.py b/qim3d/viz/itk_vtk_viewer/run.py new file mode 100644 index 0000000000000000000000000000000000000000..867dabe41fbf71ef1135bd1bf0e6c7ad7e7c06b0 --- /dev/null +++ b/qim3d/viz/itk_vtk_viewer/run.py @@ -0,0 +1,209 @@ +import subprocess +import platform +from pathlib import Path +import os +import qim3d.utils +from qim3d.utils.logger import log + +# from .helpers import get_qim_dir, get_nvm_dir, get_viewer_binaries, get_viewer_dir, get_node_binaries_dir, NotInstalledError, SOURCE_FNM +from .helpers import * +import webbrowser +import threading +import time + + +# Start viewer +START_COMMAND = "itk-vtk-viewer -s" + +# Lock, so two threads can safely read and write to is_installed +c = threading.Condition() +is_installed = True + + +def run_global(port=3000): + linux_func = lambda: subprocess.run( + START_COMMAND+f" -p {port}", shell=True, stderr=subprocess.DEVNULL + ) + + # First sourcing the node.js, if sourcing via fnm doesnt help and user would have to do it any other way, it would throw an error and suggest to install viewer to qim library + windows_func = lambda: subprocess.run( + ["powershell.exe", SOURCE_FNM, START_COMMAND+f" -p {port}"], + shell=True, + stderr=subprocess.DEVNULL, + ) + + run_for_platform( + linux_func=linux_func, windows_func=windows_func, macos_func=linux_func + ) + + +def run_within_qim_dir(port=3000): + dir = get_itk_dir() + viewer_dir = get_viewer_dir(dir) + viewer_bin = get_viewer_binaries(viewer_dir) + + def linux_func(): + # Looks for node binaries installed in qim3d/viz/itk_vtk_viewer/.nvm + node_bin = get_node_binaries_dir(get_nvm_dir(dir)) + if node_bin is None: + # Didn't find node binaries there so it looks for enviroment variable to tell it where is nvm folder + node_bin = get_node_binaries_dir(Path(str(os.getenv("NVM_DIR")))) + + if node_bin is not None: + subprocess.run( + f'export PATH="$PATH:{viewer_bin}:{node_bin}" && {START_COMMAND+f" -p {port}"}', + shell=True, + stderr=subprocess.DEVNULL, + ) + + def windows_func(): + node_bin = get_node_binaries_dir(dir) + if node_bin is not None: + subprocess.run( + [ + "powershell.exe", + f"$env:PATH = $env:PATH + ';{viewer_bin};{node_bin}';", + START_COMMAND+f" -p {port}", + ], + stderr=subprocess.DEVNULL, + ) + + run_for_platform( + linux_func=linux_func, windows_func=windows_func, macos_func=linux_func + ) + + +def itk_vtk( + filename: str = None, + open_browser: bool = True, + file_server_port: int = 8042, + viewer_port: int = 3000, +): + """ + Opens a visualization window using the itk-vtk-viewer. Works both for common file types (Tiff, Nifti, etc.) and for **OME-Zarr stores**. + + This function starts the itk-vtk-viewer, either using a global + installation or a local installation within the QIM package. It also starts + an HTTP server to serve the file to the viewer. Optionally, it can + automatically open a browser window to display the viewer. If the viewer + is not installed, it raises a NotInstalledError. + + Args: + filename (str, optional): Path to the file or OME-Zarr store to be visualized. Trailing slashes in + the path are normalized. Defaults to None. + open_browser (bool, optional): If True, opens the visualization in a new browser tab. + Defaults to True. + file_server_port (int, optional): The port number for the local file server that hosts + the store. Defaults to 8042. + viewer_port (int, optional): The port number for the itk-vtk-viewer server. Defaults to 3000. + + Raises: + NotInstalledError: Raised if the itk-vtk-viewer is not installed in the expected location. + + Example: + ```python + import qim3d + + # Download data + downloader = qim3d.io.Downloader() + data = downloader.Okinawa_Forams.Okinawa_Foram_1(load_file=True, virtual_stack=True) + + # Export to OME-Zarr + qim3d.io.export_ome_zarr("Okinawa_Foram_1.zarr", data) + + # Start visualization + qim3d.viz.itk_vtk("Okinawa_Foram_1.zarr") + ``` + <pre style="margin-left: 12px; margin-right: 12px; color:#454545"> + Downloading Okinawa_Foram_1.tif + https://archive.compute.dtu.dk/download/public/projects/viscomp_data_repository/Okinawa_Forams/Okinawa_Foram_1.tif + 1.85GB [00:17, 111MB/s] + + Loading Okinawa_Foram_1.tif + Loading: 100% + 1.85GB/1.85GB [00:02<00:00, 762MB/s] + Loaded shape: (995, 1014, 984) + Using virtual stack + Exporting data to OME-Zarr format at Okinawa_Foram_1.zarr + Number of scales: 5 + Creating a multi-scale pyramid + - Scale 0: (995, 1014, 984) + - Scale 1: (498, 507, 492) + - Scale 2: (249, 254, 246) + - Scale 3: (124, 127, 123) + - Scale 4: (62, 63, 62) + Writing data to disk + All done! + + itk-vtk-viewer + => Serving /home/fima/Notebooks/Qim3d on port 3000 + + enp0s31f6 => http://10.52.0.158:3000/ + wlp0s20f3 => http://10.197.104.229:3000/ + + Serving directory '/home/fima/Notebooks/Qim3d' + http://localhost:8042/ + + Visualization url: + http://localhost:3000/?rotate=false&fileToLoad=http://localhost:8042/Okinawa_Foram_1.zarr + + </pre> + +  + + """ + + global is_installed + # This might seem redundant but is here in case we have to go through the installation first + # If we have to install first this variable is set to False and doesn't disappear + # So when we want to run the newly installed viewer it would still be false and webbrowser wouldnt open + c.acquire() + is_installed = True + c.release() + + # We do a delay open for the browser, just so that the itk-vtk-viewer has time to start. + # Timing is not critical, this is just so that the user does not see the "server cannot be reached" page + def delayed_open(): + time.sleep(3) + global is_installed + c.acquire() + if is_installed: + + # Normalize the filename. This is necessary for trailing slashes by the end of the path + filename_norm = os.path.normpath(os.path.abspath(filename)) + + # Start the http server + qim3d.utils.start_http_server( + os.path.dirname(filename_norm), port=file_server_port + ) + + viz_url = f"http://localhost:{viewer_port}/?rotate=false&fileToLoad=http://localhost:{file_server_port}/{os.path.basename(filename_norm)}" + + if open_browser: + webbrowser.open_new_tab(viz_url) + + log.info(f"\nVisualization url:\n{viz_url}\n") + c.release() + + # Start the delayed open in a separate thread + delayed_window = threading.Thread(target=delayed_open) + delayed_window.start() + + # First try if the user doesn't have it globally + run_global(port=viewer_port) + + # Then try to also find node.js installed in qim package + run_within_qim_dir(port=viewer_port) + + # If we got to this part, it means that the viewer is not installed and we don't want to + # open browser with non-working window + # We sat the flag is_installed to False which will be read in the other thread to let it know not to open the browser + c.acquire() + is_installed = False + c.release() + + delayed_window.join() + + # If we still get an error, it is not installed in location we expect it to be installed and have to raise an error + # which will be caught in the command line and it will ask for installation + raise NotInstalledError