/* * Carrot2 project. * * Copyright (C) 2002-2016, Dawid Weiss, Stanisław Osiński. * All rights reserved. * * Refer to the full license file "carrot2.LICENSE" * in the root folder of the repository checkout or at: * http://www.carrot2.org/carrot2.LICENSE */ package org.carrot2.workbench.vis; import java.io.StringWriter; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import org.apache.commons.lang.StringUtils; import org.carrot2.core.Cluster; import org.carrot2.core.ProcessingResult; import org.carrot2.workbench.core.WorkbenchCorePlugin; import org.carrot2.workbench.core.helpers.PostponableJob; import org.carrot2.workbench.core.helpers.Utils; import org.carrot2.workbench.core.ui.BrowserFacade; import org.carrot2.workbench.core.ui.SearchEditor; import org.carrot2.workbench.core.ui.SearchEditorSelectionProvider; import org.carrot2.workbench.core.ui.SearchResultListenerAdapter; import org.eclipse.core.runtime.IAdapterManager; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Platform; import org.eclipse.core.runtime.Status; import org.eclipse.jface.viewers.ISelection; import org.eclipse.jface.viewers.ISelectionChangedListener; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.SelectionChangedEvent; import org.eclipse.swt.SWT; import org.eclipse.swt.browser.Browser; import org.eclipse.swt.browser.BrowserFunction; import org.eclipse.swt.browser.LocationAdapter; import org.eclipse.swt.browser.LocationEvent; import org.eclipse.swt.events.ControlAdapter; import org.eclipse.swt.events.ControlEvent; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.events.DisposeListener; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import org.eclipse.ui.browser.IWorkbenchBrowserSupport; import org.eclipse.ui.part.Page; import org.eclipse.ui.progress.UIJob; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.carrotsearch.hppc.IntStack; import org.carrot2.shaded.guava.common.collect.Lists; public abstract class AbstractBrowserVisualizationViewPage extends Page { /** * Delay between the update event and refreshing the browser view. */ protected static final int BROWSER_MODEL_UPDATE_RETRY = 750; protected static final int BROWSER_MODEL_UPDATE_INITIAL = 250; /** * Delay between the selection event and refreshing the browser view. */ protected static final int BROWSER_SELECTION_DELAY = 250; /** * Delay between the sizing event after the container is initialized. */ protected static final int BROWSER_CLIENTSIZE_DELAY = 250; /** * The editor associated with this page. */ public final SearchEditor editor; /** * Internal HTML browser. */ private Browser browser; /** * A flag indicating the browser's applet has finished loading. */ private volatile boolean browserInitialized; /** * Visualization entry page URI. */ private final String entryPageUri; private final AtomicInteger view = new AtomicInteger(); /** * This visualization's logger. */ private final Logger logger = LoggerFactory.getLogger(this.getClass().getName() + ".view_" + view.incrementAndGet()); /** * Update model XML. */ private class UpdateModelJob extends PostponableJob { private AtomicReference<ProcessingResult> currentModel = new AtomicReference<>(); private ProcessingResult updatedModel; public UpdateModelJob() { setJob(new UIJob("Visualization model update") { public IStatus runInUIThread(IProgressMonitor monitor) { if (getBrowser().isDisposed()) { logger.warn("Browser disposed."); return Status.OK_STATUS; } // If the page has not finished loading, reschedule. if (!browserInitialized) { if (browser.isVisible()) { logger.debug("Model update delayed (browser not ready)."); reschedule(BROWSER_MODEL_UPDATE_RETRY); } else { logger.debug("Model update delayed (browser invisible)."); } return Status.OK_STATUS; } if (updatedModel == currentModel.getAndSet(updatedModel) || updatedModel == null) { // Same model, or no model, ignore. return Status.OK_STATUS; } doUpdateModel(currentModel.get()); return Status.OK_STATUS; } private void doUpdateModel(ProcessingResult pr) { try { StringWriter sw = new StringWriter(); pr.serializeJson(sw, "updateDataJson", true, false, true, false); String json = sw.toString(); String jsonLeader = StringUtils.abbreviate(json, 180); logger.info("Updating view XML: " + jsonLeader); if (!browser.execute("javascript:" + json)) { logger.warn("Failed to update the data model: " + jsonLeader); } } catch (Exception e) { logger.warn("Browser model update error.", e); } } }); } public void updateModel(ProcessingResult model) { if (browser.isDisposed()) { // Browser disposed. return; } updatedModel = model; reschedule(BROWSER_MODEL_UPDATE_INITIAL); } }; private final UpdateModelJob updateModelJob = new UpdateModelJob(); /** * Client size update job. */ private class UpdateClientSizeJob extends PostponableJob { private volatile Rectangle clientArea; public UpdateClientSizeJob() { setJob(new UIJob("UpdateClientSize") { public IStatus runInUIThread(IProgressMonitor monitor) { if (getBrowser().isDisposed()) { logger.warn("Browser disposed."); return Status.OK_STATUS; } Rectangle r = clientArea; if (r == null) { logger.warn("Area is null?"); return Status.OK_STATUS; } if (isBrowserInitialized()) { logger.info("updateSize(): " + clientArea); getBrowser().execute( "javascript:updateSize(" + clientArea.width + ", " + clientArea.height + ")"); } else { reschedule(BROWSER_CLIENTSIZE_DELAY); } return Status.OK_STATUS; } }); } public void update(Rectangle clientArea) { this.clientArea = clientArea; reschedule(BROWSER_CLIENTSIZE_DELAY); } }; private UpdateClientSizeJob updateClientSizeJob = new UpdateClientSizeJob(); /** * Selection refresh job. */ private PostponableJob selectionJob = new PostponableJob(new UIJob( "Browser (selection)...") { public IStatus runInUIThread(IProgressMonitor monitor) { return doSelectionRefresh(); } /* * */ protected IStatus doSelectionRefresh() { if (browser.isDisposed() || !browserInitialized) { return Status.OK_STATUS; } final IStructuredSelection sel = (IStructuredSelection) editor.getSite() .getSelectionProvider().getSelection(); @SuppressWarnings("unchecked") final List<Cluster> selected = (List<Cluster>) sel.toList(); IntStack ids = new IntStack(selected.size()); for (Cluster cluster : selected) { ids.push(cluster.getId()); } browser.execute("javascript:selectGroupsById(" + Arrays.toString(ids.toArray()) + ");"); return Status.OK_STATUS; } }); /** * Sync with search result updated event. */ private final SearchResultListenerAdapter editorSyncListener = new SearchResultListenerAdapter() { public void processingResultUpdated(ProcessingResult result) { updateModelJob.updateModel(result); } }; /** * Editor selection listener. */ private final ISelectionChangedListener selectionListener = new ISelectionChangedListener() { /* */ public void selectionChanged(SelectionChangedEvent event) { if (!browserInitialized) return; final ISelection selection = event.getSelection(); if (selection != null && selection instanceof IStructuredSelection) { final IStructuredSelection ss = (IStructuredSelection) selection; logger.debug("Selection, editor->visualization: " + ss); final IAdapterManager mgr = Platform.getAdapterManager(); final ArrayList<Cluster> selectedGroups = Lists.newArrayList(); final Object [] selected = ss.toArray(); for (Object ob : selected) { final Cluster cluster = (Cluster) mgr.getAdapter(ob, Cluster.class); if (cluster != null) selectedGroups.add(cluster); } selectionJob.reschedule(BROWSER_SELECTION_DELAY); } } }; /* * */ public AbstractBrowserVisualizationViewPage(SearchEditor editor, String entryPageUri) { this.entryPageUri = entryPageUri; this.editor = editor; } /** * */ @Override public void createControl(final Composite parent) { /* * Open the browser and redirect it to the internal HTTP server. */ browser = BrowserFacade.createNew(parent, SWT.NONE); final Activator plugin = Activator.getInstance(); final String refreshURL = plugin.getFullURL(entryPageUri); /* * Register custom callback functions. */ new BrowserFunction(browser, "swt_onGroupSelectionChanged") { public Object function(Object [] arguments) { if (!browserInitialized) return null; Object [] ids = (Object[]) arguments[0]; int [] groupIds = new int [ids.length]; for (int i = 0; i < groupIds.length; i++) { groupIds[i] = (int) Double.parseDouble(ids[i].toString()); } doGroupSelection(groupIds); return null; } }; new BrowserFunction(browser, "swt_onModelChanged") { public Object function(Object [] arguments) { if (!browserInitialized) return null; selectionJob.reschedule(BROWSER_SELECTION_DELAY); return null; } }; new BrowserFunction(browser, "swt_onVisualizationLoaded") { public Object function(Object [] arguments) { browserInitialized = true; onBrowserReady(); updateModelJob.updateModel(getProcessingResult()); return null; } }; new BrowserFunction(browser, "swt_log") { public Object function(Object [] arguments) { logger.info("JS->SWT log: " + Arrays.toString(arguments)); return null; } }; browserInitialized = false; browser.setUrl(refreshURL); browser.addLocationListener(new LocationAdapter() { @Override public void changing(LocationEvent event) { if (!event.location.startsWith("file:")) { event.doit = false; openURL(event.location); } } }); browser.addDisposeListener(new DisposeListener() { @Override public void widgetDisposed(DisposeEvent e) { logger.debug("Browser disposed."); } }); browser.addControlListener(new ControlAdapter() { @Override public void controlResized(ControlEvent e) { updateClientSize(); } }); editor.getSearchResult().addListener(editorSyncListener); editor.getSite().getSelectionProvider().addSelectionChangedListener(selectionListener); } /** * Invoked when the browser successfully embedded the visualization. */ protected void onBrowserReady() { updateClientSize(); } private void updateClientSize() { Rectangle clientArea = browser.getClientArea(); logger.debug("Updating client size: " + clientArea); updateClientSizeJob.update(clientArea); } @Override public Control getControl() { return browser; } @Override public void setFocus() { browser.setFocus(); } @Override public void dispose() { editor.getSearchResult().removeListener(editorSyncListener); editor.getSite().getSelectionProvider() .removeSelectionChangedListener(selectionListener); browser.dispose(); super.dispose(); } private void doGroupSelection(int [] selectedGroups) { logger.debug("Selection visualization->editor: " + Arrays.toString(selectedGroups)); SearchEditorSelectionProvider prov = (SearchEditorSelectionProvider) editor.getSite().getSelectionProvider(); prov.setSelected(selectedGroups, selectionListener); } /** * Returns the current processing result (must be called from the GUI thread). */ private ProcessingResult getProcessingResult() { assert Display.getCurrent() != null; final ProcessingResult pr = editor.getSearchResult().getProcessingResult(); if (pr == null || pr.getClusters() == null) return null; return pr; } /** * Make the browser available. */ protected Browser getBrowser() { return browser; } public boolean isBrowserInitialized() { return browser != null && browserInitialized; } /** * */ private static void openURL(String location) { try { WorkbenchCorePlugin .getDefault().getWorkbench().getBrowserSupport() .createBrowser( IWorkbenchBrowserSupport.AS_EDITOR | IWorkbenchBrowserSupport.LOCATION_BAR | IWorkbenchBrowserSupport.NAVIGATION_BAR | IWorkbenchBrowserSupport.STATUS, null, null, null) .openURL(new URL(location)); } catch (Exception e) { Utils.logError("Couldn't open internal browser", e, false); } } }