diff --git a/modules/HortaTracer/src/main/java/org/janelia/horta/NeuronTracerTopComponent.java b/modules/HortaTracer/src/main/java/org/janelia/horta/NeuronTracerTopComponent.java
index 9be58509e..5e4c61043 100644
--- a/modules/HortaTracer/src/main/java/org/janelia/horta/NeuronTracerTopComponent.java
+++ b/modules/HortaTracer/src/main/java/org/janelia/horta/NeuronTracerTopComponent.java
@@ -44,6 +44,7 @@
import org.janelia.horta.loader.*;
import org.janelia.workstation.common.actions.CopyToClipboardAction;
import org.janelia.workstation.controller.dialog.NeuronColorDialog;
+import org.janelia.workstation.controller.dialog.SaveOrOpenDeepLinkDialog;
import org.janelia.workstation.controller.listener.ColorModelListener;
import org.janelia.workstation.controller.listener.UnmixingListener;
import org.janelia.workstation.controller.listener.NeuronVertexCreationListener;
@@ -1270,6 +1271,14 @@ public void actionPerformed(ActionEvent e) {
snapMenu.add(snapPanel);
+ topMenu.add(new AbstractAction("Create Deep Link") {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ SaveOrOpenDeepLinkDialog deepLinkDialog = new SaveOrOpenDeepLinkDialog();
+ deepLinkDialog.showDialog();
+ }
+ });
+
JMenu viewMenu = new JMenu("View");
topMenu.add(viewMenu);
diff --git a/modules/ViewerController/pom.xml b/modules/ViewerController/pom.xml
index 758ef7b65..edc9ce0ba 100644
--- a/modules/ViewerController/pom.xml
+++ b/modules/ViewerController/pom.xml
@@ -157,6 +157,10 @@
org.janelia.workstation
viewer3d
+
+ org.janelia.workstation
+ geometry3d
+
diff --git a/modules/ViewerController/src/main/java/org/janelia/workstation/controller/TmViewerManager.java b/modules/ViewerController/src/main/java/org/janelia/workstation/controller/TmViewerManager.java
index 58692386b..5dbd1f8cd 100644
--- a/modules/ViewerController/src/main/java/org/janelia/workstation/controller/TmViewerManager.java
+++ b/modules/ViewerController/src/main/java/org/janelia/workstation/controller/TmViewerManager.java
@@ -321,5 +321,8 @@ public void loadComplete(LoadProjectEvent event) {
// re-enable updates once the model is fully loaded
RefreshHandler.getInstance().ifPresent(rh -> rh.setReceiveUpdates(true));
+
+ PostSampleLoadEvent postLoadEvent = new PostSampleLoadEvent(currProject, currProject instanceof TmSample);
+ ViewerEventBus.postEvent(postLoadEvent);
}
}
diff --git a/modules/ViewerController/src/main/java/org/janelia/workstation/controller/action/OpenDeepLinkAction.java b/modules/ViewerController/src/main/java/org/janelia/workstation/controller/action/OpenDeepLinkAction.java
new file mode 100644
index 000000000..a5f10c73c
--- /dev/null
+++ b/modules/ViewerController/src/main/java/org/janelia/workstation/controller/action/OpenDeepLinkAction.java
@@ -0,0 +1,94 @@
+package org.janelia.workstation.controller.action;
+
+import com.google.common.eventbus.Subscribe;
+import org.janelia.geometry3d.Rotation;
+import org.janelia.geometry3d.Vantage;
+import org.janelia.model.domain.DomainObject;
+import org.janelia.model.domain.Reference;
+import org.janelia.model.domain.tiledMicroscope.TmMappedNeuron;
+import org.janelia.model.domain.tiledMicroscope.TmSample;
+import org.janelia.model.domain.tiledMicroscope.TmWorkspace;
+import org.janelia.workstation.common.actions.BaseContextualNodeAction;
+import org.janelia.workstation.controller.TmViewerManager;
+import org.janelia.workstation.controller.ViewerEventBus;
+import org.janelia.workstation.controller.eventbus.PostSampleLoadEvent;
+import org.janelia.workstation.controller.eventbus.UnloadProjectEvent;
+import org.janelia.workstation.controller.eventbus.ViewEvent;
+import org.janelia.workstation.controller.model.DeepLink;
+import org.janelia.workstation.controller.model.TmModelManager;
+import org.janelia.workstation.controller.model.TmViewState;
+import org.janelia.workstation.core.api.DomainMgr;
+import org.janelia.workstation.core.workers.SimpleWorker;
+import org.janelia.workstation.integration.util.FrameworkAccess;
+import org.openide.awt.ActionID;
+import org.openide.awt.ActionReference;
+import org.openide.awt.ActionReferences;
+import org.openide.awt.ActionRegistration;
+import org.openide.util.NbBundle;
+import org.openide.util.actions.SystemAction;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Launches the Data Viewer from a context-menu.
+ */
+@ActionID(
+ category = "actions",
+ id = "OpenDeepLinkAction"
+)
+@ActionRegistration(
+ displayName = "#CTL_OpenDeepLinkAction",
+ lazy = false
+)
+@ActionReferences({
+ @ActionReference(path = "Menu/Actions/Horta", position = 1510, separatorBefore = 1499)
+})
+@NbBundle.Messages("CTL_OpenDeepLinkAction=Open Deep Link In Horta")
+public class OpenDeepLinkAction extends BaseContextualNodeAction {
+
+ private static final Logger log = LoggerFactory.getLogger(OpenDeepLinkAction.class);
+ private DeepLink deepLink;
+
+ public static OpenDeepLinkAction get() {
+ return SystemAction.get(OpenDeepLinkAction.class);
+ }
+
+ public void setDeepLink(DeepLink link) {
+ deepLink = link;
+ }
+
+ @Subscribe
+ public void finishDeepLinkOpen(PostSampleLoadEvent event) {
+ TmViewState view = deepLink.getViewpoint();
+ TmModelManager.getInstance().getCurrentView().setCameraFocusX(view.getCameraFocusX());
+ TmModelManager.getInstance().getCurrentView().setCameraFocusY(view.getCameraFocusY());
+ TmModelManager.getInstance().getCurrentView().setCameraFocusZ(view.getCameraFocusZ());
+ float[] rot = view.getCameraRotation();
+ TmModelManager.getInstance().getCurrentView().setZoomLevel(view.getZoomLevel());
+ ViewEvent viewEvent = new ViewEvent(this,(double)view.getCameraFocusX(),
+ (double)view.getCameraFocusY(),
+ (double)view.getCameraFocusZ(),
+ view.getZoomLevel(),
+ rot,
+ false);
+ ViewerEventBus.postEvent(viewEvent);
+ }
+
+ @Override
+ public void performAction() {
+ DomainObject sample = (deepLink.getSample()==null)? deepLink.getWorkspace() : deepLink.getSample();
+ if (sample==null) {
+ log.warn("Action performed with null domain object");
+ return;
+ }
+
+ ViewerEventBus.registerForEvents(this);
+ openProject(sample);
+ }
+
+ private void
+ openProject(DomainObject sample) {
+ TmViewerManager.getInstance().loadProject(sample);
+ FrameworkAccess.getBrowsingController().updateRecentlyOpenedHistory(Reference.createFor(sample));
+ }
+}
diff --git a/modules/ViewerController/src/main/java/org/janelia/workstation/controller/dialog/SaveOrOpenDeepLinkDialog.java b/modules/ViewerController/src/main/java/org/janelia/workstation/controller/dialog/SaveOrOpenDeepLinkDialog.java
new file mode 100644
index 000000000..638bd5aa4
--- /dev/null
+++ b/modules/ViewerController/src/main/java/org/janelia/workstation/controller/dialog/SaveOrOpenDeepLinkDialog.java
@@ -0,0 +1,201 @@
+package org.janelia.workstation.controller.dialog;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonMappingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.http.NameValuePair;
+import org.apache.http.client.utils.URLEncodedUtils;
+import org.janelia.model.domain.Reference;
+import org.janelia.model.domain.tiledMicroscope.TmSample;
+import org.janelia.model.domain.tiledMicroscope.TmWorkspace;
+import org.janelia.workstation.common.gui.dialogs.ModalDialog;
+import org.janelia.workstation.controller.access.TiledMicroscopeDomainMgr;
+import org.janelia.workstation.controller.action.OpenDeepLinkAction;
+import org.janelia.workstation.controller.model.DeepLink;
+import org.janelia.workstation.controller.model.TmModelManager;
+import org.janelia.workstation.controller.model.TmViewState;
+import org.janelia.workstation.integration.util.FrameworkAccess;
+
+import javax.swing.*;
+import java.awt.*;
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.net.URLDecoder;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public class SaveOrOpenDeepLinkDialog extends ModalDialog {
+ private JTextArea deepLinkResults = new JTextArea(2,40);
+ private JTextArea loadDeepLink = new JTextArea(2,40);
+ private boolean success = false;
+
+ public SaveOrOpenDeepLinkDialog() {
+ super(FrameworkAccess.getMainFrame());
+
+ setTitle("Save or Open Deep Link");
+ setLayout(new GridBagLayout());
+
+ // Enable line wrapping
+ deepLinkResults.setLineWrap(true);
+ deepLinkResults.setWrapStyleWord(true);
+ deepLinkResults.setEditable(true);
+ loadDeepLink.setLineWrap(true);
+ loadDeepLink.setWrapStyleWord(true);
+ loadDeepLink.setEditable(true);
+
+ GridBagConstraints constraints = new GridBagConstraints();
+ constraints.fill = GridBagConstraints.HORIZONTAL;
+ constraints.insets = new Insets(10, 10, 0, 10);
+
+ constraints.gridx = 0;
+ constraints.gridy = 0;
+ add(new JLabel("Generate DeepLink For Current Location:"), constraints);
+
+ JButton generateButton = new JButton("Generate");
+ generateButton.setToolTipText("Generate Deep Link");
+ generateButton.addActionListener(e -> generateLink());
+
+ constraints.gridx = 1;
+ constraints.gridy = 0;
+ add(generateButton,constraints);
+
+ constraints.gridx = 0;
+ constraints.gridy = 1;
+ constraints.gridwidth = 2;
+ constraints.insets = new Insets(0, 5, 30, 5);
+ add(deepLinkResults,constraints);
+
+ constraints.gridx = 0;
+ constraints.gridy = 2;
+ constraints.gridwidth = 1;
+ add(new JLabel("Load DeepLink:"), constraints);
+
+ constraints.gridx = 1;
+ constraints.gridy = 2;
+ add(loadDeepLink,constraints);
+
+ constraints.gridx = 2;
+ constraints.gridy = 2;
+ JButton loadButton = new JButton("Load");
+ loadButton.setToolTipText("Load Deep Link");
+ loadButton.addActionListener(e -> loadLink());
+ add(loadButton, constraints);
+
+ JButton closeButton = new JButton("Close");
+ closeButton.setToolTipText("Close");
+ closeButton.addActionListener(e -> onCancel());
+ constraints.gridx = 0;
+ constraints.gridy = 3;
+ constraints.gridwidth = 1;
+ add(closeButton, constraints);
+
+ getRootPane().setDefaultButton(closeButton);
+ }
+
+ public void showDialog() {
+ packAndShow();
+ }
+
+ private String encodeValue(String value) throws UnsupportedEncodingException {
+ return URLEncoder.encode(value, StandardCharsets.UTF_8.toString());
+ }
+
+ private void onCancel() {
+ success = false;
+ dispose();
+ }
+
+ private void generateLink() {
+ DeepLink deepLink = new DeepLink();
+
+ Map deepLinkMap = new HashMap<>();
+ TmWorkspace workspace = TmModelManager.getInstance().getCurrentWorkspace();
+ if (workspace==null) {
+ TmSample sample = TmModelManager.getInstance().getCurrentSample();
+ if (sample == null) {
+ deepLinkResults.setText("There is no workspace or sample currently open to generate a deep link");
+ return;
+ }
+ deepLink.setSample(sample);
+ deepLinkMap.put("sample", sample.getId());
+ } else {
+ deepLink.setWorkspace(workspace);
+ deepLinkMap.put("workspace", workspace.getId());
+ }
+
+ TmViewState view = TmModelManager.getInstance().getCurrentView();
+ deepLink.setViewpoint(view);
+ deepLinkMap.put("viewFocusX", view.getCameraFocusX());
+ deepLinkMap.put("viewFocusY", view.getCameraFocusY());
+ deepLinkMap.put("viewFocusZ", view.getCameraFocusZ());
+ deepLinkMap.put("viewZoom", view.getZoomLevel());
+
+ ObjectMapper mapper = new ObjectMapper();
+ String encodedURL = deepLinkMap.keySet().stream()
+ .map(key -> {
+ try {
+ String jsonVal = mapper.writeValueAsString(deepLinkMap.get(key));
+ return key + "=" + encodeValue(jsonVal);
+ } catch (UnsupportedEncodingException | JsonProcessingException e) {
+ e.printStackTrace();
+ return "";
+ }
+ })
+ .collect(Collectors.joining("&", "deeplink:", ""));
+ deepLinkResults.setText(encodedURL);
+ repaint();
+ revalidate();
+ }
+
+ private void loadLink() {
+ String encodedParams = loadDeepLink.getText().replaceAll("deeplink:","");
+ String[] pairs = encodedParams.split("&");
+
+ HashMap params = new HashMap<>();
+ try {
+ for (String pair : pairs) {
+ int idx = pair.indexOf("=");
+ String key = null;
+ key = URLDecoder.decode(pair.substring(0, idx), StandardCharsets.UTF_8.toString());
+ String value = URLDecoder.decode(pair.substring(idx + 1), StandardCharsets.UTF_8.toString());
+ params.put(key, value);
+ }
+
+ DeepLink parsedDeepLink = new DeepLink();
+ ObjectMapper mapper = new ObjectMapper();
+ if (params.containsKey("workspace")) {
+ TmWorkspace workspace = TiledMicroscopeDomainMgr.getDomainMgr().getWorkspace(Long.parseLong(params.get("workspace")));
+ parsedDeepLink.setWorkspace(workspace);
+ } else if (params.containsKey("sample")) {
+ TmSample sample = TiledMicroscopeDomainMgr.getDomainMgr().getSample(Long.parseLong(params.get("sample")));
+ parsedDeepLink.setSample(sample);
+ }
+
+ Double focusX = mapper.readValue(params.get("viewFocusX"), Double.class);
+ Double focusY = mapper.readValue(params.get("viewFocusY"), Double.class);
+ Double focusZ = mapper.readValue(params.get("viewFocusZ"), Double.class);
+ Double zoomLevel = mapper.readValue(params.get("viewZoom"), Double.class);
+ TmViewState view = new TmViewState();
+ view.setCameraFocusX(focusX);
+ view.setCameraFocusY(focusY);
+ view.setCameraFocusZ(focusZ);
+ view.setZoomLevel(zoomLevel);
+ parsedDeepLink.setViewpoint(view);
+
+ OpenDeepLinkAction action = new OpenDeepLinkAction();
+ action.setDeepLink(parsedDeepLink);
+ action.performAction();
+ } catch (Exception e) {
+ FrameworkAccess.handleException(e);
+ }
+ }
+
+
+ public boolean isSuccess() {
+ return success;
+ }
+
+}
diff --git a/modules/ViewerController/src/main/java/org/janelia/workstation/controller/eventbus/PostSampleLoadEvent.java b/modules/ViewerController/src/main/java/org/janelia/workstation/controller/eventbus/PostSampleLoadEvent.java
new file mode 100644
index 000000000..f903bbcf4
--- /dev/null
+++ b/modules/ViewerController/src/main/java/org/janelia/workstation/controller/eventbus/PostSampleLoadEvent.java
@@ -0,0 +1,21 @@
+package org.janelia.workstation.controller.eventbus;
+
+public class PostSampleLoadEvent extends ViewerEvent {
+ private boolean sample = false;
+ private Object project;
+
+ public PostSampleLoadEvent(Object project,
+ boolean isSample) {
+ super(project);
+ this.project = project;
+ this.sample = isSample;
+ }
+
+ public boolean isSample() {
+ return sample;
+ }
+ public Object getProject() {
+ return project;
+ }
+
+}
diff --git a/modules/ViewerController/src/main/java/org/janelia/workstation/controller/model/DeepLink.java b/modules/ViewerController/src/main/java/org/janelia/workstation/controller/model/DeepLink.java
new file mode 100644
index 000000000..6a991509c
--- /dev/null
+++ b/modules/ViewerController/src/main/java/org/janelia/workstation/controller/model/DeepLink.java
@@ -0,0 +1,48 @@
+package org.janelia.workstation.controller.model;
+
+import org.janelia.geometry3d.Vantage;
+import org.janelia.model.domain.tiledMicroscope.TmSample;
+import org.janelia.model.domain.tiledMicroscope.TmWorkspace;
+
+/**
+ * This stores a global ID for locating a HC stack, loading the appropriate Sample/Workspace in the stack, and
+ * navigating to a specific viewpoint in that Sample/Workspace.
+ */
+public class DeepLink {
+ public String getHortaCloudStack() {
+ return hortaCloudStack;
+ }
+
+ public void setHortaCloudStack(String hortaCloudStack) {
+ this.hortaCloudStack = hortaCloudStack;
+ }
+
+ public TmWorkspace getWorkspace() {
+ return workspace;
+ }
+
+ public void setWorkspace(TmWorkspace workspace) {
+ this.workspace = workspace;
+ }
+
+ public TmSample getSample() {
+ return sample;
+ }
+
+ public void setSample(TmSample sample) {
+ this.sample = sample;
+ }
+
+ public TmViewState getViewpoint() {
+ return viewpoint;
+ }
+
+ public void setViewpoint(TmViewState viewpoint) {
+ this.viewpoint = viewpoint;
+ }
+
+ String hortaCloudStack;
+ TmWorkspace workspace;
+ TmSample sample;
+ TmViewState viewpoint;
+}