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; +}