All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.metsci.glimpse.layers.LayeredGui Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2020, Metron, Inc.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of Metron, Inc. nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL METRON, INC. BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.metsci.glimpse.layers;

import static com.metsci.glimpse.docking.DockingFrameCloseOperation.DISPOSE_ALL_FRAMES;
import static com.metsci.glimpse.docking.DockingThemes.defaultDockingTheme;
import static com.metsci.glimpse.docking.DockingUtils.attachPopupMenu;
import static com.metsci.glimpse.docking.DockingUtils.loadDockingArrangement;
import static com.metsci.glimpse.docking.DockingUtils.requireIcon;
import static com.metsci.glimpse.docking.DockingUtils.saveDockingArrangement;
import static com.metsci.glimpse.docking.DockingWindowTitlers.createDefaultWindowTitler;
import static com.metsci.glimpse.docking.Side.RIGHT;
import static com.metsci.glimpse.docking.ViewCloseOption.VIEW_AUTO_CLOSEABLE;
import static com.metsci.glimpse.docking.ViewCloseOption.VIEW_CUSTOM_CLOSEABLE;
import static com.metsci.glimpse.docking.ViewCloseOption.VIEW_NOT_CLOSEABLE;
import static com.metsci.glimpse.docking.group.ArrangementUtils.findArrTileContaining;
import static com.metsci.glimpse.layers.FpsOption.findFps;
import static com.metsci.glimpse.layers.StandardGuiOption.HIDE_LAYERS_PANEL;
import static com.metsci.glimpse.layers.StandardViewOption.HIDE_CLONE_BUTTON;
import static com.metsci.glimpse.layers.StandardViewOption.HIDE_CLOSE_BUTTON;
import static com.metsci.glimpse.layers.StandardViewOption.HIDE_FACETS_MENU;
import static com.metsci.glimpse.layers.StandardViewOption.REQUEST_CLOSE_BUTTON;
import static com.metsci.glimpse.layers.misc.UiUtils.bindButtonText;
import static com.metsci.glimpse.layers.misc.UiUtils.bindToggleButton;
import static com.metsci.glimpse.util.ImmutableCollectionUtils.listMinus;
import static com.metsci.glimpse.util.ImmutableCollectionUtils.listPlus;
import static com.metsci.glimpse.util.ImmutableCollectionUtils.mapWith;
import static com.metsci.glimpse.util.ImmutableCollectionUtils.setMinus;
import static com.metsci.glimpse.util.ImmutableCollectionUtils.setPlus;
import static com.metsci.glimpse.util.PredicateUtils.notNull;
import static com.metsci.glimpse.util.var.VarUtils.addElementAddedListener;
import static com.metsci.glimpse.util.var.VarUtils.addElementRemovedListener;
import static com.metsci.glimpse.util.var.VarUtils.addMapVarListener;
import static javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED;
import static javax.swing.ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED;

import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.regex.Pattern;

import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JToggleButton;

import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.jogamp.opengl.GLAnimatorControl;
import com.metsci.glimpse.core.support.swing.SwingEDTAnimator;
import com.metsci.glimpse.docking.DockingFrameCloseOperation;
import com.metsci.glimpse.docking.DockingGroup;
import com.metsci.glimpse.docking.DockingGroupAdapter;
import com.metsci.glimpse.docking.DockingTheme;
import com.metsci.glimpse.docking.ViewCloseOption;
import com.metsci.glimpse.docking.group.ViewPlacementRule;
import com.metsci.glimpse.docking.group.frame.DockingGroupMultiframe;
import com.metsci.glimpse.docking.xml.DockerArrangementTile;
import com.metsci.glimpse.docking.xml.GroupArrangement;
import com.metsci.glimpse.layers.misc.LayerCardsPanel;
import com.metsci.glimpse.util.var.Disposable;
import com.metsci.glimpse.util.var.DisposableGroup;
import com.metsci.glimpse.util.var.Var;

/**
 * A {@link LayeredGui} represents a number of {@link View}s and a number of {@link Layer}s.
 * Each layer is given an opportunity to add a representation of itself to each view. Layers
 * and views can be added and removed dynamically.
 * 

* A layer typically has a different representation in each view -- for example, a TracksLayer * can show a spatial representation in a geo view, and a temporal representation in a timeline * view. The representation of a particular layer on a particular view is called a {@link Facet}. *

* There can be more than one view of a given type -- for example, two different geo views. * A layer may display itself differently on each of these views. For example, if there are * two geo views with different projections, the TracksLayer might have separate facets for * the two views: one facet showing the track points projected one way, the other facet showing * the same track points projected a different way. *

* A view can be configured by modifying its {@link Trait}s, which inform the view's facets * about what they should be displaying. For example, a geo trait might contain a projection, * and a time trait might contain a selected time. The facets for each view can base their * rendering on these traits -- so facets on geo-view #1 can use projection #1 and selected * time #1, while facets on geo-view #2 use projection #2 and selected time #2. *

* It is important to note that every view has every trait. A geo view has both a geo * trait and a time trait. A timeline view also has both a geo trait and a time trait. *

* It is possible to define a custom trait, and add it to the LayeredGui. Every view will then * have its own instance of the custom trait, so that a facet on any view can consult that trait. * For example, if a scatter plot has been added, there may be a scatter trait that contains * the bounds of the selection window in the XY space of the scatter plot. A layer can then * have a facet on a geo view which highlights the geo locations of the points that are selected * in the scatter plot. *

* Trait instances can be linked to each other. For example, two timeline views can have their * time traits linked together, so that adjusting the selection window in one timeline affects * the selection window in the other timeline as well. For another example, a geo view can have * its time trait linked to the time trait of a timeline view, so that a facet on the geo can * highlight points inside the time window that is selected on the timeline. *

* To be linked, two trait instances must be compatible with each other. Compatibility is checked * using the {@link Var#validateFn} of the {@link Trait#parent} var. *

* For flexibility, traits that are owned by views are not linked directly to each other, but * are instead linked to a common "linkage." A linkage is simply a trait object that is used * as a shared parent for some number of child traits. *

* When a new view is added, if it is missing a trait for which there is an existing linkage, * the new view's trait is populated by making a copy of the linkage. This can be used to set * default values for a trait: first add a linkage with the desired settings, and then any views * subsequently added, which don't already have that trait defined, will inherit the default * values from the linkage. *

* When a new view is added, an attempt is made to link each of its traits to an existing linkage. * If no compatible linkage is found, the trait is left unlinked -- however, it may end up linked * automatically later on, if another view with a compatible trait is added. */ public class LayeredGui { public static final Icon cloneIcon = requireIcon( LayeredGui.class.getResource( "icons/fugue/cards.png" ) ); public static final Icon layersIcon = requireIcon( LayeredGui.class.getResource( "icons/fugue/category.png" ) ); public static final String layerCardsViewId = "com.metsci.glimpse.layers.geo.LayeredGui.layerCardsView"; // Model public final Var>> linkages; public final Var>> linkageNames; public final Var> views; public final Var> layers; // View protected final Map viewDisposables; protected final DockingGroup dockingGroup; protected final GLAnimatorControl animator; protected String dockingAppName; protected final Map dockingViewIdCounters; protected final BiMap dockingViews; public LayeredGui( String frameTitleRoot, GuiOption... guiOptions ) { this( frameTitleRoot, defaultDockingTheme( ), guiOptions ); } public LayeredGui( String frameTitleRoot, DockingTheme theme, GuiOption... guiOptions ) { this( frameTitleRoot, theme, DISPOSE_ALL_FRAMES, guiOptions ); } public LayeredGui( String frameTitleRoot, DockingTheme theme, DockingFrameCloseOperation closeOperation, GuiOption... guiOptions ) { this( frameTitleRoot, theme, closeOperation, ImmutableSet.copyOf( guiOptions ) ); } public LayeredGui( String frameTitleRoot, DockingTheme theme, DockingFrameCloseOperation closeOperation, Collection guiOptions ) { this( frameTitleRoot, new DockingGroupMultiframe( closeOperation, theme ), guiOptions ); } public LayeredGui( String frameTitleRoot, DockingGroup dockingGroup, GuiOption... guiOptions ) { this( frameTitleRoot, dockingGroup, ImmutableSet.copyOf( guiOptions ) ); } public LayeredGui( String frameTitleRoot, DockingGroup dockingGroup, Collection guiOptions ) { // Model // this.linkages = new Var<>( ImmutableMap.of( ), notNull ); this.linkageNames = new Var<>( ImmutableMap.of( ), notNull ); this.views = new Var<>( ImmutableSet.of( ), notNull ); this.layers = new Var<>( ImmutableList.of( ), notNull ); // View // this.viewDisposables = new HashMap<>( ); this.dockingGroup = dockingGroup; this.dockingGroup.addListener( createDefaultWindowTitler( frameTitleRoot ) ); // Don't start the animator here, since we might not ever get any views that // use it -- see the javadocs for {@link View#setGLAnimator(GLAnimatorControl)} double fps = findFps( guiOptions, 60 ); this.animator = new SwingEDTAnimator( fps ); this.dockingViewIdCounters = new HashMap<>( ); this.dockingViews = HashBiMap.create( ); this.dockingGroup.addListener( new DockingGroupAdapter( ) { @Override public void disposingAllWindows( DockingGroup dockingGroup ) { if ( dockingAppName != null ) { saveDockingArrangement( dockingAppName, dockingGroup.captureArrangement( ) ); } views.set( ImmutableSet.of( ) ); animator.stop( ); } @Override public void closingView( DockingGroup dockingGroup, com.metsci.glimpse.docking.View dockingView ) { // If dockingViews still has this entry, then the layeredView hasn't been removed yet View view = dockingViews.inverse( ).remove( dockingView ); if ( view != null ) { views.update( ( v ) -> setMinus( v, view ) ); } } } ); if ( !guiOptions.contains( HIDE_LAYERS_PANEL ) ) { LayerCardsPanel layerCardsPanel = new LayerCardsPanel( this.layers ); JScrollPane layerCardsScroller = new JScrollPane( layerCardsPanel, VERTICAL_SCROLLBAR_AS_NEEDED, HORIZONTAL_SCROLLBAR_AS_NEEDED ); com.metsci.glimpse.docking.View layersView = new com.metsci.glimpse.docking.View( layerCardsViewId, layerCardsScroller, "Layers", VIEW_NOT_CLOSEABLE, null, layersIcon, null ); this.dockingGroup.addView( layersView ); } // Controller // addElementRemovedListener( this.views, this::handleViewRemoved ); addElementAddedListener( this.views, true, this::handleViewAdded ); addElementRemovedListener( this.layers, this::handleLayerRemoved ); addElementAddedListener( this.layers, true, this::handleLayerAdded ); addMapVarListener( this.linkageNames, false, ( ev, k, vOld, vNew ) -> { if ( vNew == null ) { this.pruneLinkages( ); } } ); } public DockingGroup getDockingGroup( ) { return this.dockingGroup; } public com.metsci.glimpse.docking.View getDockingView( View view ) { return this.dockingViews.get( view ); } public View getViewFromDocking( com.metsci.glimpse.docking.View view ) { return this.dockingViews.inverse( ).get( view ); } public void stopAnimator( ) { animator.stop( ); } public void startAnimator( ) { animator.start( ); } public boolean isVisible( ) { return this.dockingGroup.isVisible( ); } public void setVisible( boolean visible ) { this.dockingGroup.setVisible( visible ); } public void arrange( String appName, URL defaultArrUrl ) { GroupArrangement groupArr = loadDockingArrangement( appName, defaultArrUrl ); this.dockingGroup.setArrangement( groupArr ); this.dockingAppName = appName; } public Trait addLinkage( String traitKey, String name, Trait template ) { Trait linkage = template.copy( true ); if ( name != null ) { this.putLinkageName( traitKey, name, linkage ); } ImmutableList oldLinkages = this.linkages.v( ).get( traitKey ); ImmutableList newLinkages; if ( oldLinkages == null ) { newLinkages = ImmutableList.of( linkage ); } else { // WIP: What should insertion index be? newLinkages = listPlus( oldLinkages, linkage ); } this.linkages.update( ( v ) -> mapWith( v, traitKey, newLinkages ) ); return linkage; } protected void putLinkageName( String traitKey, String name, Trait linkage ) { ImmutableMap oldNames = this.linkageNames.v( ).get( traitKey ); ImmutableMap newNames; if ( oldNames == null ) { newNames = ImmutableMap.of( linkage, name ); } else { newNames = mapWith( oldNames, linkage, name ); } this.linkageNames.update( ( v ) -> mapWith( v, traitKey, newNames ) ); } /** * {@link LayeredGui} generates a docking viewId near the end of the process * of adding a {@link View}. Sometimes, though, we want to know before we add * a {@link View} what viewId it will have -- e.g. to pre-insert a {@link ViewPlacement} * in the docking {@link GroupArrangement}. *

* This method returns the docking viewId that will be used when the given {@link View} * gets added to this {@link LayeredGui} -- assuming no other {@link View}s get added in * the meantime. */ public String predictDockingViewId( View view ) { return this.genDockingViewId( view, false ); } protected String claimDockingViewId( View view ) { return this.genDockingViewId( view, true ); } protected String genDockingViewId( View view, boolean claim ) { String stem = dockingViewIdStem( view.getClass( ) ); int number = this.dockingViewIdCounters.getOrDefault( stem, 0 ); if ( claim ) { this.dockingViewIdCounters.put( stem, number + 1 ); } return ( stem + ":" + number ); } public static String dockingViewIdStem( Class viewClass ) { return viewClass.getName( ); } public static Pattern dockingViewIdPattern( Class viewClass ) { String stem = dockingViewIdStem( viewClass ); return Pattern.compile( "^" + Pattern.quote( stem ) + ":[0-9]+$" ); } public static boolean dockingViewIdMatches( Class viewClass, String viewId ) { return dockingViewIdPattern( viewClass ).matcher( viewId ).matches( ); } public void addView( View view ) { this.addView( view, null ); } /** * If {@code placementRule} is non-null, it will be called to choose a docking destination * for {@code view}. */ public void addView( View view, ViewPlacementRule placementRule ) { if ( placementRule != null ) { String viewId = this.predictDockingViewId( view ); this.dockingGroup.addViewPlacement( viewId, placementRule ); } this.views.update( ( v ) -> setPlus( v, view ) ); } public void cloneView( View view ) { Map newTraits = new LinkedHashMap<>( ); for ( Entry en : view.traits.v( ).entrySet( ) ) { String traitKey = en.getKey( ); Trait oldTrait = en.getValue( ); if ( oldTrait.parent.v( ) == null ) { // Unnamed linkage -- will get removed if all its children get removed Trait linkage = this.addLinkage( traitKey, null, oldTrait ); oldTrait.parent.set( linkage ); } Trait newTrait = oldTrait.copy( false ); newTrait.parent.set( oldTrait.parent.v( ) ); newTraits.put( traitKey, newTrait ); } View newView = view.copy( ); newView.setTraits( newTraits ); this.addView( newView, ( planArr, existingViewIds, placer ) -> { // Split the original tile, and put the clone in the right half of the split String viewId = this.dockingViews.get( view ).viewId; DockerArrangementTile tile = findArrTileContaining( planArr, viewId ); return placer.addBesideNeighbor( null, tile, RIGHT, 0.5 ); } ); } public void removeView( View view ) { this.views.update( ( v ) -> setMinus( v, view ) ); } public void addLayer( Layer layer ) { this.layers.update( ( v ) -> listPlus( v, layer ) ); } public void removeLayer( Layer layer ) { this.layers.update( ( v ) -> listMinus( v, layer ) ); } protected void handleViewAdded( View view ) { // Keep track of disposables that need to run when this view gets removed DisposableGroup disposables = new DisposableGroup( ); // Fill in traits the view doesn't already have Map newTraits = new LinkedHashMap<>( view.traits.v( ) ); for ( String traitKey : this.linkages.v( ).keySet( ) ) { newTraits.computeIfAbsent( traitKey, ( k ) -> { // The default linkage is the one at index 0 return this.linkages.v( ).get( traitKey ).get( 0 ).copy( false ); } ); } view.setTraits( newTraits ); // Link traits that aren't already linked disposables.add( view.traits.addListener( true, ( ) -> { for ( Entry en : view.traits.v( ).entrySet( ) ) { String traitKey = en.getKey( ); Trait trait = en.getValue( ); // If the trait doesn't have a parent, look for a compatible linkage if ( trait.parent.v( ) == null ) { for ( Trait linkage : this.linkages.v( ).get( traitKey ) ) { if ( trait.parent.isValid( linkage ) ) { trait.parent.set( linkage ); break; } } } // If the trait still doesn't have a parent, create an unnamed linkage if ( trait.parent.v( ) == null ) { // Unnamed linkage -- will get removed if all its children get removed Trait linkage = this.addLinkage( traitKey, null, trait ); trait.parent.set( linkage ); } } } ) ); for ( Layer layer : this.layers.v( ) ) { view.addLayer( layer ); } view.setGLAnimator( this.animator ); if ( !view.viewOptions.contains( HIDE_CLONE_BUTTON ) ) { JButton cloneButton = new JButton( cloneIcon ); cloneButton.setToolTipText( "Clone This View" ); cloneButton.addActionListener( ( ev ) -> { this.cloneView( view ); } ); view.toolbar.add( cloneButton ); } if ( !view.viewOptions.contains( HIDE_FACETS_MENU ) ) { JToggleButton facetsButton = new JToggleButton( layersIcon ); facetsButton.setToolTipText( "Show Layers" ); JPopupMenu facetsPopup = new JPopupMenu( ); attachPopupMenu( facetsButton, facetsPopup ); DisposableGroup facetDisposables = disposables.add( new DisposableGroup( ) ); disposables.add( view.facets.addListener( true, ( ) -> { facetDisposables.dispose( ); facetDisposables.clear( ); facetsPopup.removeAll( ); for ( Entry en : view.facets.v( ).entrySet( ) ) { Layer layer = en.getKey( ); Facet facet = en.getValue( ); JMenuItem facetToggle = new JCheckBoxMenuItem( ); facetDisposables.add( bindButtonText( facetToggle, layer.title ) ); facetDisposables.add( bindToggleButton( facetToggle, facet.isVisible ) ); facetsPopup.add( facetToggle ); // Redo popup menu layout after a layer title change facetDisposables.add( layer.title.addListener( true, ( ) -> { facetsPopup.pack( ); facetsPopup.repaint( ); } ) ); } } ) ); view.toolbar.add( facetsButton ); } String viewId = this.claimDockingViewId( view ); ViewCloseOption closeOption = ( view.viewOptions.contains( HIDE_CLOSE_BUTTON ) ? VIEW_NOT_CLOSEABLE : ( view.viewOptions.contains( REQUEST_CLOSE_BUTTON ) ? VIEW_CUSTOM_CLOSEABLE : VIEW_AUTO_CLOSEABLE ) ); com.metsci.glimpse.docking.View dockingView = new com.metsci.glimpse.docking.View( viewId, view.getComponent( ), "", closeOption, view.getTooltip( ), view.getIcon( ), view.toolbar ); disposables.add( view.title.addListener( true, ( ) -> { dockingView.title.set( view.title.v( ) ); } ) ); this.dockingGroup.addView( dockingView ); // When the user closes a dockingView, we will need to know the corresponding view this.dockingViews.put( view, dockingView ); disposables.add( view::dispose ); this.viewDisposables.put( view, disposables ); } protected void handleViewRemoved( View view ) { this.viewDisposables.remove( view ).dispose( ); // If dockingViews still has this entry, then the dockingView hasn't been closed yet com.metsci.glimpse.docking.View dockingView = this.dockingViews.remove( view ); if ( dockingView != null ) { this.dockingGroup.closeView( dockingView ); } this.pruneLinkages( ); } protected void pruneLinkages( ) { ImmutableMap> prunedLinkages = pruneLinkages( this.linkages.v( ), this.linkageNames.v( ), this.views.v( ) ); this.linkages.set( prunedLinkages ); } protected static ImmutableMap> pruneLinkages( Map> linkages, Map> linkageNames, Collection views ) { Map> result = new LinkedHashMap<>( ); for ( String traitKey : linkages.keySet( ) ) { List linkagesToKeep = pruneLinkages( traitKey, linkages.get( traitKey ), linkageNames.get( traitKey ), views ); result.put( traitKey, ImmutableList.copyOf( linkagesToKeep ) ); } return ImmutableMap.copyOf( result ); } protected static List pruneLinkages( String traitKey, Collection linkages, Map linkageNames, Collection views ) { Set linkagesInUse = findLinkagesInUse( views, traitKey ); List linkagesToKeep = new ArrayList<>( ); for ( Trait linkage : linkages ) { // Never auto-remove a named linkage boolean isNamed = ( linkageNames != null && linkageNames.containsKey( linkage ) ); if ( isNamed || linkagesInUse.contains( linkage ) ) { linkagesToKeep.add( linkage ); } } return linkagesToKeep; } protected static Set findLinkagesInUse( Collection views, String traitKey ) { Set linkages = new LinkedHashSet<>( ); for ( View view : views ) { Trait trait = view.traits.v( ).get( traitKey ); if ( trait != null ) { Trait linkage = trait.parent.v( ); if ( linkage != null ) { linkages.add( linkage ); } } } return linkages; } protected void handleLayerAdded( Layer layer ) { for ( View view : this.views.v( ) ) { view.addLayer( layer ); } } protected void handleLayerRemoved( Layer layer ) { for ( View view : this.views.v( ) ) { view.removeLayer( layer, false ); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy