diff --git a/.github/workflows/build_jar.yml b/.github/workflows/build_jar.yml index ca7f0f24..101be085 100644 --- a/.github/workflows/build_jar.yml +++ b/.github/workflows/build_jar.yml @@ -9,17 +9,17 @@ build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 + - uses: actions/checkout@v4 + - name: Set up JDK 17 + uses: actions/setup-java@v4 with: - java-version: '11' + java-version: '17' distribution: 'adopt-hotspot' - name: Grant execute permission for gradlew run: chmod +x gradlew - name: Build with Gradle - run: ./gradlew build -P toolchain=11 - - uses: actions/upload-artifact@v2 + run: ./gradlew build -P toolchain=17 + - uses: actions/upload-artifact@v4 with: name: libs path: build/libs diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 0566ee69..9d1b1be2 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -1,21 +1,29 @@ # This workflow will build a Java project with Gradle # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle - name: Java CI with Gradle +name: Java CI with Gradle - on: [pull_request] +on: + push: + pull_request: - jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 - with: - java-version: '11' - distribution: 'adopt-hotspot' - - name: Grant execute permission for gradlew - run: chmod +x gradlew - - name: Build with Gradle - run: ./gradlew build -P toolchain=11 +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'adopt-hotspot' + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Build with Gradle + run: ./gradlew build -P toolchain=17 + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: qupath-extension-omero-gs + path: build/libs/*.jar + retention-days: 30 diff --git a/README.md b/README.md index 06eb033c..cb67c7cb 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,13 @@ -[![Extension docs](https://img.shields.io/badge/docs-qupath_omero-red)](https://qupath.readthedocs.io/en/stable/docs/advanced/omero.html) -[![Forum](https://img.shields.io/badge/forum-image.sc-green)](https://forum.image.sc/tag/qupath) -[![Downloads (latest release)](https://img.shields.io/github/downloads-pre/qupath/qupath-extension-omero/latest/total)](https://github.com/qupath/qupath-extension-omero/releases/latest) -[![Downloads (all releases)](https://img.shields.io/github/downloads/qupath/qupath-extension-omero/total)](https://github.com/qupath/qupath-extension-omero/releases) +# QuPath OMERO extension with raw tiles support -# QuPath OMERO extension - -Welcome to the OMERO extension for [QuPath](http://qupath.github.io)! +Welcome to the Glencoe Software's version of the OMERO extension for [QuPath](http://qupath.github.io)! This adds support for accessing images hosted on an [OMERO](https://www.openmicroscopy.org/omero/) -server through OMERO's web API. +server. If microservices are available on the OMERO server, then they will be used to retrieve +image data; all data types are supported in this case, and uncompressed image data is provided to QuPath. + +If microservices are not available, then the OMERO web API is used. In this case, only uint8 images +with 3 or fewer channels are supported. > **Important!** > @@ -29,7 +28,7 @@ The main documentation for the extension is at https://qupath.readthedocs.io/en/ ## Installing -To install the OMERO extension, download the latest `qupath-extension-omero-[version].jar` file from [releases](https://github.com/qupath/qupath-extension-omero/releases) and drag it onto the main QuPath window. +To install the OMERO extension, download the latest `qupath-extension-omero-[version].jar` file from [releases](https://github.com/glencoesoftware/qupath-extension-omero/releases) and drag it onto the main QuPath window. If you haven't installed any extensions before, you'll be prompted to select a QuPath user directory. The extension will then be copied to a location inside that directory. diff --git a/build.gradle b/build.gradle index 84b8ad31..82e51d2f 100644 --- a/build.gradle +++ b/build.gradle @@ -8,35 +8,36 @@ plugins { } ext.moduleName = 'qupath.extension.omero' -ext.qupathVersion = gradle.ext.qupathVersion ext.qupathJavaVersion = 17 - -base { - description = "QuPath extension to support image reading using OMERO's web API." - version = "0.4.0" - group = "io.github.qupath" -} +archivesBaseName = 'qupath-extension-omero' +description = "QuPath extension to support image reading using OMERO's web API." +version = "0.4.1-gs-SNAPSHOT" repositories { - + mavenLocal() mavenCentral() - + maven { - url "https://maven.scijava.org/content/repositories/releases" + url 'https://artifacts.glencoesoftware.com/artifactory/scijava-thirdparty' } - + maven { - url "https://maven.scijava.org/content/repositories/snapshots" + url 'https://artifacts.glencoesoftware.com/artifactory/ome.releases' } - + + maven { + url "https://maven.scijava.org/content/repositories/releases" + } + } dependencies { - implementation libs.commons.text - shadow "io.github.qupath:qupath-gui-fx:${qupathVersion}" - shadow libs.qupath.fxtras - - shadow libs.slf4j + implementation "org.apache.commons:commons-text:1.9" + + shadow "io.github.qupath:qupath-gui-fx:0.5.1" + shadow "io.github.qupath:qupath-fxtras:0.1.4" + shadow "ome:formats-bsd:7.0.1" + shadow "org.slf4j:slf4j-api:1.7.30" } jar { @@ -88,3 +89,15 @@ tasks.withType(org.gradle.jvm.tasks.Jar) { tasks.named('test') { useJUnitPlatform() } + +javafx { + version = "17.0.9" + modules = ["javafx.base", + "javafx.controls", + "javafx.graphics", + "javafx.media", + "javafx.fxml", + "javafx.web", + "javafx.swing"] + configuration = 'api' +} diff --git a/src/main/java/qupath/lib/images/servers/omero/OmeroExtension.java b/src/main/java/qupath/lib/images/servers/omero/OmeroExtension.java index 46261dff..d8225475 100644 --- a/src/main/java/qupath/lib/images/servers/omero/OmeroExtension.java +++ b/src/main/java/qupath/lib/images/servers/omero/OmeroExtension.java @@ -26,12 +26,19 @@ import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; +import com.google.gson.reflect.TypeToken; + +import javafx.beans.property.StringProperty; import javafx.event.Event; import javafx.event.EventHandler; import javafx.scene.control.Label; @@ -46,6 +53,7 @@ import qupath.lib.gui.actions.ActionTools; import qupath.lib.gui.extensions.GitHubProject; import qupath.lib.gui.extensions.QuPathExtension; +import qupath.lib.gui.prefs.PathPrefs; import qupath.lib.gui.tools.MenuTools; /** @@ -102,16 +110,23 @@ public String getDescription() { private static Menu createServerListMenu(QuPathGUI qupath, Menu browseServerMenu) { EventHandler validationHandler = e -> { + StringProperty usedServerProp = PathPrefs.createPersistentPreference("omero_ext.server_list", ""); + + List usedServers = getServerList(usedServerProp); + browseServerMenu.getItems().clear(); // Get all active servers var activeServers = OmeroWebClients.getAllClients(); // Populate the menu with each unique active servers + ArrayList activeURIs = new ArrayList(); for (var client: activeServers) { if (client == null) continue; + // 3 dots are appended to distinguish active connections MenuItem item = new MenuItem(client.getServerURI() + "..."); + activeURIs.add(client.getServerURI().toString()); item.setOnAction(e2 -> { var browser = browsers.get(client); if (browser == null || browser.getStage() == null) { @@ -123,6 +138,23 @@ private static Menu createServerListMenu(QuPathGUI qupath, Menu browseServerMenu }); browseServerMenu.getItems().add(item); } + + // add servers that have been connected to previously, + // but which are not currently connected + if (usedServers != null) { + for (String server : usedServers) { + final String usedServer = server.endsWith("/") ? server.substring(0, server.length() - 1) : server; + if (!usedServer.isEmpty() && !activeURIs.contains(usedServer)) { + // no suffix appended to the server name here + // distinguishes from active connections that have 3 dots appended + MenuItem item = new MenuItem(usedServer); + item.setOnAction(e2 -> { + handleLogin(qupath, usedServer); + }); + browseServerMenu.getItems().add(item); + } + } + } // Create 'New server...' MenuItem MenuItem customServerItem = new MenuItem("New server..."); @@ -140,50 +172,73 @@ private static Menu createServerListMenu(QuPathGUI qupath, Menu browseServerMenu var path = tf.getText(); if (path == null || path.isEmpty()) return; - try { - if (!path.startsWith("http:") && !path.startsWith("https:")) - throw new IOException("The input URL must contain a scheme (e.g. \"https://\")!"); - - // Make the path a URI - URI uri = new URI(path); - - // Clean the URI (in case it's a full path) - URI uriServer = OmeroTools.getServerURI(uri); - - if (uriServer == null) - throw new MalformedURLException("Could not parse server from " + uri.toString()); - - // Check if client exist and if browser is already opened - var client = OmeroWebClients.getClientFromServerURI(uriServer); - if (client == null) - client = OmeroWebClients.createClientAndLogin(uriServer); - - if (client == null) - throw new IOException("Could not parse server from " + uri.toString()); - - var browser = browsers.get(client); - if (browser == null || browser.getStage() == null) { - // Create new browser - browser = new OmeroWebImageServerBrowserCommand(qupath, client); - browsers.put(client, browser); - browser.run(); - } else // Request focus for already-existing browser - browser.getStage().requestFocus(); - - } catch (FileNotFoundException ex) { - Dialogs.showErrorMessage("OMERO web server", String.format("An error occured when trying to reach %s: %s\"", path, ex.getLocalizedMessage())); - } catch (IOException | URISyntaxException ex) { - Dialogs.showErrorMessage("OMERO web server", ex.getLocalizedMessage()); - return; - } + + handleLogin(qupath, path); + + List serverList = getServerList(usedServerProp); + if (serverList != null && !serverList.contains(path)) { + serverList.add(path); + usedServerProp.setValue((new Gson()).toJson(serverList)); + } }); MenuTools.addMenuItems(browseServerMenu, null, customServerItem); }; - + // Ensure the menu is populated (every time the parent menu is opened) browseServerMenu.getParentMenu().setOnShowing(validationHandler); return browseServerMenu; } + + private static List getServerList(StringProperty usedServerProp) { + Gson gson = new Gson(); + List usedServers = null; + try { + usedServers = gson.fromJson(usedServerProp.get(), new TypeToken<>() {}); + } catch (JsonSyntaxException ignored) {} + if (usedServers == null) { + usedServers = new ArrayList(); + } + return usedServers; + } + + static void handleLogin(QuPathGUI qupath, String path) { + try { + if (!path.startsWith("http:") && !path.startsWith("https:")) { + throw new IOException("The input URL must contain a scheme (e.g. \"https://\")!"); + } + // Make the path a URI + URI uri = new URI(path); + + // Clean the URI (in case it's a full path) + URI uriServer = OmeroTools.getServerURI(uri); + + if (uriServer == null) + throw new MalformedURLException("Could not parse server from " + uri.toString()); + + // Check if client exist and if browser is already opened + var client = OmeroWebClients.getClientFromServerURI(uriServer); + if (client == null) + client = OmeroWebClients.createClientAndLogin(uriServer); + + if (client == null) + throw new IOException("Could not parse server from " + uri.toString()); + + var browser = browsers.get(client); + if (browser == null || browser.getStage() == null) { + // Create new browser + browser = new OmeroWebImageServerBrowserCommand(qupath, client); + browsers.put(client, browser); + browser.run(); + } else // Request focus for already-existing browser + browser.getStage().requestFocus(); + + } catch (FileNotFoundException ex) { + Dialogs.showErrorMessage("OMERO web server", String.format("An error occured when trying to reach %s: %s\"", path, ex.getLocalizedMessage())); + } catch (IOException | URISyntaxException ex) { + Dialogs.showErrorMessage("OMERO web server", ex.getLocalizedMessage()); + return; + } + } /** * Return map of currently opened browsers. @@ -197,7 +252,7 @@ static Map getOpenedBrowsers( @Override public GitHubRepo getRepository() { - return GitHubRepo.create(getName(), "qupath", "qupath-extension-omero"); + return GitHubRepo.create(getName(), "glencoesoftware", "qupath-extension-omero-web"); } @Override diff --git a/src/main/java/qupath/lib/images/servers/omero/OmeroWebClient.java b/src/main/java/qupath/lib/images/servers/omero/OmeroWebClient.java index a6651300..cce735b7 100644 --- a/src/main/java/qupath/lib/images/servers/omero/OmeroWebClient.java +++ b/src/main/java/qupath/lib/images/servers/omero/OmeroWebClient.java @@ -23,6 +23,7 @@ import java.io.IOException; import java.io.InputStream; +import java.io.InputStreamReader; import java.io.OutputStream; import java.lang.reflect.Type; import java.net.Authenticator; @@ -30,6 +31,7 @@ import java.net.CookieHandler; import java.net.CookieManager; import java.net.CookiePolicy; +import java.net.HttpCookie; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.PasswordAuthentication; @@ -54,7 +56,9 @@ import org.slf4j.LoggerFactory; import com.google.gson.JsonElement; +import com.google.gson.JsonObject; import com.google.gson.JsonParser; +import com.google.gson.JsonPrimitive; import com.google.gson.JsonSyntaxException; import com.google.gson.annotations.SerializedName; import com.google.gson.reflect.TypeToken; @@ -115,7 +119,9 @@ public class OmeroWebClient { * User ID fetched from the login request response. Can be used to match {@code Owner}s in the browser. */ private int userId; - + + private String sessionId; + private boolean hasMicroservice = false; /** * Logged in property (modified by login/loggedIn/logout/timer) @@ -170,10 +176,8 @@ public void run() { String authenticate(final PasswordAuthentication authentication, final int serverID) throws Exception { var handler = CookieHandler.getDefault(); - if (handler == null) { - handler = new CookieManager(null, CookiePolicy.ACCEPT_ALL); - CookieHandler.setDefault(handler); - } + handler = new CookieManager(null, CookiePolicy.ACCEPT_ALL); + CookieHandler.setDefault(handler); if (this.token == null) this.token = getCSRFToken(); @@ -224,9 +228,56 @@ String authenticate(final PasswordAuthentication authentication, final int serve // e.printStackTrace(); // } + String rtn = null; try (InputStream input = connection.getInputStream()) { - return GeneralTools.readInputStreamAsString(input); + rtn = GeneralTools.readInputStreamAsString(input); } + + // look for session ID in existing session cookies + // this will throw an IOException if the 'sessionid' cookie is not found + // the session ID is used later when retrieving raw tiles from the microservice + sessionId = null; + List cookies = ((CookieManager) handler).getCookieStore().getCookies(); + for (HttpCookie cookie : cookies) { + if (cookie.getName().equals("sessionid")) { + sessionId = cookie.getValue(); + } + } + if (sessionId == null) { + throw new IOException("Could not find valid 'sessionid' cookie"); + } + + // check for image region microservice - enables raw tile retrieval + // see https://github.com/glencoesoftware/omero-ms-image-region + // + // the session ID retrieved above is not required to perform this check, + // but both the session ID and microservice configuration must be present in order + // to retrieve raw tiles + HttpURLConnection conn = null; + try { + URL optionsURL = new URL(serverURI.getScheme(), serverURI.getHost(), serverURI.getPort(), "/tile/"); + conn = (HttpURLConnection) optionsURL.openConnection(); + conn.setRequestMethod("OPTIONS"); + conn.connect(); + int response = conn.getResponseCode(); + if (response == HttpURLConnection.HTTP_OK) { + try (InputStreamReader reader = new InputStreamReader(conn.getInputStream())) { + JsonObject root = GsonTools.getInstance().fromJson(reader, JsonObject.class); + JsonPrimitive provider = root.getAsJsonPrimitive("provider"); + if (provider != null && provider.getAsString().equals("PixelBufferMicroservice")) { + hasMicroservice = true; + } + } + } + else { + throw new IOException("Could not check for OMERO microservice (" + response +")"); + } + } + finally { + conn.disconnect(); + } + + return rtn; } private int keepAlive() { @@ -314,6 +365,14 @@ int getUserId() { return userId; } + String getSessionId() { + return sessionId; + } + + boolean hasMicroservice() { + return hasMicroservice; + } + void setUsername(String newUsername) { username.set(newUsername); } @@ -389,7 +448,7 @@ private static int getUserId(String json) { JsonElement element = JsonParser.parseString(json); return element.getAsJsonObject().get("eventContext").getAsJsonObject().get("userId").getAsInt(); } - + /** * Check and return whether the client is logged in to its server * (not necessarily with access to all its images). @@ -441,7 +500,7 @@ else if ("--password".equals(name) || "-p".equals(name)) { // Parse login response JSON to get default Group and user ID defaultGroup = getDefaultGroup(result); userId = getUserId(result); - + // If we have previous URIs and the the username was different if (uris.size() > 0 && !usernameOld.isEmpty() && !usernameOld.equals(authentication.getUserName())) { Dialogs.showInfoNotification("OMERO login", String.format("OMERO account switched from \"%s\" to \"%s\" for %s", usernameOld, authentication.getUserName(), serverURI)); @@ -657,4 +716,4 @@ static PasswordAuthentication getPasswordAuthentication(String prompt, String ho return new PasswordAuthentication(userName, password); } } -} \ No newline at end of file +} diff --git a/src/main/java/qupath/lib/images/servers/omero/OmeroWebImageServer.java b/src/main/java/qupath/lib/images/servers/omero/OmeroWebImageServer.java index 130c9ab0..a04ff6e9 100644 --- a/src/main/java/qupath/lib/images/servers/omero/OmeroWebImageServer.java +++ b/src/main/java/qupath/lib/images/servers/omero/OmeroWebImageServer.java @@ -21,10 +21,24 @@ package qupath.lib.images.servers.omero; +import java.awt.image.BandedSampleModel; import java.awt.image.BufferedImage; +import java.awt.image.ColorModel; +import java.awt.image.DataBuffer; +import java.awt.image.DataBufferByte; +import java.awt.image.DataBufferDouble; +import java.awt.image.DataBufferFloat; +import java.awt.image.DataBufferInt; +import java.awt.image.DataBufferShort; +import java.awt.image.DataBufferUShort; +import java.awt.image.SampleModel; +import java.awt.image.WritableRaster; + + import java.io.IOException; import java.net.URI; import java.net.URL; +import java.net.URLConnection; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -45,6 +59,7 @@ import com.google.gson.JsonObject; import qupath.lib.awt.common.BufferedImageTools; +import qupath.lib.color.ColorModelFactory; import qupath.lib.images.servers.AbstractTileableImageServer; import qupath.lib.images.servers.ImageChannel; import qupath.lib.images.servers.ImageServerBuilder; @@ -56,6 +71,8 @@ import qupath.lib.objects.PathObject; import qupath.lib.objects.PathObjectReader; +import loci.formats.gui.AWTImageTools; + /** * ImageServer that reads pixels using the OMERO web API. *

@@ -156,7 +173,7 @@ public class OmeroWebImageServer extends AbstractTileableImageServer implements // Add URI to the client's list of URIs client.addURI(uri); } - + protected ImageServerMetadata buildMetadata() throws IOException { String uriQuery = uri.getQuery(); if (uriQuery != null && !uriQuery.isEmpty() && uriQuery.startsWith("show=image-")) { @@ -180,19 +197,25 @@ protected ImageServerMetadata buildMetadata() throws IOException { double pixelHeightMicrons = Double.NaN; double zSpacingMicrons = Double.NaN; PixelType pixelType = PixelType.UINT8; - boolean isRGB = true; + + // if the image region microservice is used to fetch tiles, + // each tile will be a single (non-RGB) channel + // if the microservice is not available and webgateway is used instead, + // then only 8-bit RGB data is supported and RGB tiles will be provided + // see readRenderedTile and OmeroWebImageServerBrowserCommand#isSupported + boolean isRGB = !client.hasMicroservice(); double magnification = Double.NaN; JsonObject map = OmeroRequests.requestMetadata(scheme, host, port, Integer.parseInt(id)); JsonObject size = map.getAsJsonObject("size"); + JsonObject meta = map.getAsJsonObject("meta"); sizeX = size.getAsJsonPrimitive("width").getAsInt(); sizeY = size.getAsJsonPrimitive("height").getAsInt(); sizeC = size.getAsJsonPrimitive("c").getAsInt(); sizeZ = size.getAsJsonPrimitive("z").getAsInt(); sizeT = size.getAsJsonPrimitive("t").getAsInt(); - - + pixelType = convertPixelType(meta.getAsJsonPrimitive("pixelsType").getAsString()); JsonElement pixelSizeElement = map.get("pixel_size"); if (pixelSizeElement != null) { @@ -209,27 +232,49 @@ protected ImageServerMetadata buildMetadata() throws IOException { zSpacingMicrons = zSpacing.getAsDouble(); } } - - String pixelsType = null; - - if (map.has("meta")) { - JsonObject meta = map.getAsJsonObject("meta"); - if (meta.has("imageName")) - imageName = meta.get("imageName").getAsString(); - if (meta.has("pixelsType")) - pixelsType = meta.get("pixelsType").getAsString(); + + if (meta.has("imageName")) { + imageName = meta.get("imageName").getAsString(); } - - - List channels = null; - if (sizeC == 3) - channels = ImageChannel.getDefaultRGBChannels(); -// else if (sizeC == 1) -// channels = ImageChannel.getDefaultChannelList(1); - - if (channels == null || (pixelsType != null && !"uint8".equals(pixelsType))) - throw new IOException("Only 8-bit RGB images supported! Selected image has " + sizeC + " channel(s) & pixel type " + pixelsType); - + + // copy channel names and colors from OMERO metadata + List channels = null; + if (!client.hasMicroservice()) { + if (sizeC == 3) { + channels = ImageChannel.getDefaultRGBChannels(); + } + if (channels == null || pixelType != PixelType.UINT8) { + throw new IOException("Only 8-bit RGB images supported! Selected image has " + sizeC + " channel(s) & pixel type " + pixelType); + } + } + else if (map.has("channels")) { + channels = new ArrayList(); + JsonArray allChannels = map.get("channels").getAsJsonArray(); + + for (int index=0; index channels = getMetadata().getChannels(); + ColorModel colorModel = ColorModelFactory.createColorModel(pixelType, channels); + SampleModel sampleModel = sampleModel = new BandedSampleModel(dataBuffer.getDataType(), width, height, channels.size()); + WritableRaster raster = WritableRaster.createWritableRaster(sampleModel, dataBuffer, null); + return new BufferedImage(colorModel, raster, false, null); + } @Override @@ -504,4 +645,26 @@ public boolean equals(Object obj) { return host.equals(((OmeroWebImageServer)obj).getHost()) && client.getUsername().equals(((OmeroWebImageServer)obj).getWebclient().getUsername()); } -} \ No newline at end of file + + private PixelType convertPixelType(String type) { + switch (type) { + case "int8": + return PixelType.INT8; + case "uint8": + return PixelType.UINT8; + case "int16": + return PixelType.INT16; + case "uint16": + return PixelType.UINT16; + case "int32": + return PixelType.INT32; + case "uint32": + return PixelType.UINT32; + case "float": + return PixelType.FLOAT32; + case "double": + return PixelType.FLOAT64; + } + throw new IllegalArgumentException("Unsupported pixel type: " + type); + } +} diff --git a/src/main/java/qupath/lib/images/servers/omero/OmeroWebImageServerBrowserCommand.java b/src/main/java/qupath/lib/images/servers/omero/OmeroWebImageServerBrowserCommand.java index 100dbbe0..d734932a 100644 --- a/src/main/java/qupath/lib/images/servers/omero/OmeroWebImageServerBrowserCommand.java +++ b/src/main/java/qupath/lib/images/servers/omero/OmeroWebImageServerBrowserCommand.java @@ -350,14 +350,17 @@ public Owner fromString(String string) { var selectedItem = tree.getSelectionModel().getSelectedItem(); if (selectedItem != null && selectedItem.getValue().getType() == OmeroObjectType.IMAGE && isSupported(selectedItem.getValue())) { if (qupath.getProject() == null) { - try { - qupath.openImage(qupath.getViewer(), createObjectURI(selectedItem.getValue()), true, true); - } catch (IOException ex) { - throw new RuntimeException(ex); - } - } - else + try { + qupath.openImage(qupath.getViewer(), createObjectURI(selectedItem.getValue()), true, true); + } catch (IOException ioe) { + Dialogs.showErrorMessage("Open image", "Error opening image\n" + ioe.getLocalizedMessage()); + logger.error(ioe.getMessage(), ioe); + throw new RuntimeException(ioe); + } + } + else { promptToImportOmeroImages(createObjectURI(selectedItem.getValue())); + } } } }); @@ -618,14 +621,17 @@ else if (uri.getType() == OmeroObjectType.ORPHANED_FOLDER) } if (qupath.getProject() == null) { if (validUris.length == 1) { - try { - qupath.openImage(qupath.getViewer(), validUris[0], true, true); - } catch (IOException ex) { - throw new RuntimeException(ex); - } - } - else + try { + qupath.openImage(qupath.getViewer(), validUris[0], true, true); + } catch (IOException ioe) { + Dialogs.showErrorMessage("Open image", "Error opening image\n" + ioe.getLocalizedMessage()); + logger.error(ioe.getMessage(), ioe); + throw new RuntimeException(ioe); + } + } + else { Dialogs.showErrorMessage("Open OMERO images", "If you want to handle multiple images, you need to create a project first."); // Same as D&D for images + } return; } promptToImportOmeroImages(validUris); @@ -955,13 +961,16 @@ private static WritableImage paintBufferedImageOnCanvas(BufferedImage img, Canva GuiTools.paintImage(canvas, wi); return wi; } - + /** * Return whether the image type is supported by QuPath. * @param omeroObj * @return isSupported */ - private static boolean isSupported(OmeroObject omeroObj) { + private boolean isSupported(OmeroObject omeroObj) { + if (client.hasMicroservice()) { + return true; + } if (omeroObj == null || omeroObj.getType() != OmeroObjectType.IMAGE) return true; return isUint8((Image)omeroObj) && has3Channels((Image)omeroObj); @@ -1416,7 +1425,7 @@ private Node createAnnotationsPane(String title, OmeroAnnotations omeroAnnotatio } break; case RATING: - int rating = 0; + int rating = 0; for (var ann: anns) { var ann2 = (LongAnnotation)ann; rating += ann2.getValue(); @@ -1951,4 +1960,4 @@ void shutdownPools() { executorTable.shutdownNow(); executorThumbnails.shutdownNow(); } -} \ No newline at end of file +}