Skip to content

Commit

Permalink
Freeform selection in the grapher views.
Browse files Browse the repository at this point in the history
Fix #220
  • Loading branch information
tinevez committed Oct 18, 2024
1 parent ea4d0a9 commit 5368f63
Show file tree
Hide file tree
Showing 3 changed files with 336 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,20 @@
*/
package org.mastodon.mamut.views.grapher;

import net.imglib2.loops.LoopBuilder;
import static org.mastodon.app.ui.ViewMenuBuilder.item;
import static org.mastodon.app.ui.ViewMenuBuilder.separator;
import static org.mastodon.mamut.MamutMenuBuilder.colorMenu;
import static org.mastodon.mamut.MamutMenuBuilder.colorbarMenu;
import static org.mastodon.mamut.MamutMenuBuilder.editMenu;
import static org.mastodon.mamut.MamutMenuBuilder.fileMenu;
import static org.mastodon.mamut.MamutMenuBuilder.tagSetMenu;
import static org.mastodon.mamut.MamutMenuBuilder.viewMenu;

import java.util.function.BiConsumer;

import javax.swing.ActionMap;
import javax.swing.JPanel;

import org.apache.commons.lang3.function.TriFunction;
import org.mastodon.Ref;
import org.mastodon.app.ui.MastodonFrameViewActions;
Expand Down Expand Up @@ -69,25 +82,15 @@
import org.mastodon.views.grapher.display.DataDisplayPanel;
import org.mastodon.views.grapher.display.DataDisplayZoom;
import org.mastodon.views.grapher.display.FeatureGraphConfig;
import org.mastodon.views.grapher.display.FreeformSelectionBehaviour;
import org.mastodon.views.grapher.display.OffsetAxes;
import org.mastodon.views.grapher.display.style.DataDisplayStyle;
import org.mastodon.views.grapher.display.style.DataDisplayStyleManager;
import org.mastodon.views.trackscheme.display.TrackSchemeNavigationActions;
import org.scijava.ui.behaviour.util.Actions;
import org.scijava.ui.behaviour.util.Behaviours;

import javax.swing.ActionMap;
import javax.swing.JPanel;
import java.util.function.BiConsumer;

import static org.mastodon.app.ui.ViewMenuBuilder.item;
import static org.mastodon.app.ui.ViewMenuBuilder.separator;
import static org.mastodon.mamut.MamutMenuBuilder.colorMenu;
import static org.mastodon.mamut.MamutMenuBuilder.colorbarMenu;
import static org.mastodon.mamut.MamutMenuBuilder.editMenu;
import static org.mastodon.mamut.MamutMenuBuilder.fileMenu;
import static org.mastodon.mamut.MamutMenuBuilder.tagSetMenu;
import static org.mastodon.mamut.MamutMenuBuilder.viewMenu;
import net.imglib2.loops.LoopBuilder;

public class GrapherInitializer< V extends Vertex< E > & HasTimepoint & HasLabel & Ref< V >, E extends Edge< V > & Ref< E > >
{
Expand Down Expand Up @@ -254,6 +257,7 @@ void installActions( final Actions viewActions, final Behaviours viewBehaviours
EditTagActions.install( viewActions, frame.getKeybindings(), frame.getTriggerbindings(), model.getTagSetModel(),
appModel.getSelectionModel(), viewGraph.getLock(), panel, panel.getDisplay(), model );
DataDisplayZoom.install( viewBehaviours, panel );
FreeformSelectionBehaviour.install( viewBehaviours, layout, viewGraph, selectionModel, focusModel, panel );
ExportViewActions.install( viewActions, panel.getDisplay(), frame, frame.getTitle() );

panel.getNavigationActions().install( viewActions, TrackSchemeNavigationActions.NavigatorEtiquette.FINDER_LIKE );
Expand Down
63 changes: 63 additions & 0 deletions src/main/java/org/mastodon/ui/util/RamerDouglasPeucker.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package org.mastodon.ui.util;

import java.awt.Point;
import java.util.List;

public class RamerDouglasPeucker
{

public static void simplifyPath( final List< Point > points, final double epsilon )
{
if ( points == null || points.size() < 3 )
return; // No need to simplify if there are fewer than 3 points

simplifySection( points, 0, points.size() - 1, epsilon );
}

private static void simplifySection( final List< Point > points, final int start, final int end, final double epsilon )
{
if ( end <= start + 1 )
return;

double maxDistance = 0;
int index = start;

for ( int i = start + 1; i < end; i++ )
{
final double distance = perpendicularDistance( points.get( i ), points.get( start ), points.get( end ) );
if ( distance > maxDistance )
{
index = i;
maxDistance = distance;
}
}

if ( maxDistance > epsilon )
{
// Simplify left half
simplifySection( points, start, index, epsilon );
// Simplify right half
simplifySection( points, index, end, epsilon );
}
else
{
for ( int i = end - 1; i > start; i-- )
points.remove( i );
}
}

private static double perpendicularDistance( final Point point, final Point lineStart, final Point lineEnd )
{
final double dx = lineEnd.x - lineStart.x;
final double dy = lineEnd.y - lineStart.y;

if ( dx == 0 && dy == 0 )
return point.distance( lineStart );

// Calculate the perpendicular distance using the cross product method
final double numerator = Math.abs( dy * point.x - dx * point.y + lineEnd.x * lineStart.y - lineEnd.y * lineStart.x );
final double denominator = Math.sqrt( dx * dx + dy * dy );

return numerator / denominator;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
package org.mastodon.views.grapher.display;

import java.awt.Color;
import java.awt.Component;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.geom.Path2D;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.mastodon.collection.RefSet;
import org.mastodon.graph.Edge;
import org.mastodon.graph.Vertex;
import org.mastodon.model.FocusModel;
import org.mastodon.model.HasLabel;
import org.mastodon.model.SelectionModel;
import org.mastodon.spatial.HasTimepoint;
import org.mastodon.ui.keymap.KeyConfigContexts;
import org.mastodon.ui.keymap.KeyConfigScopes;
import org.mastodon.ui.util.RamerDouglasPeucker;
import org.mastodon.views.grapher.datagraph.DataEdge;
import org.mastodon.views.grapher.datagraph.DataGraph;
import org.mastodon.views.grapher.datagraph.DataGraphLayout;
import org.mastodon.views.grapher.datagraph.DataVertex;
import org.mastodon.views.grapher.datagraph.ScreenTransform;
import org.mastodon.views.grapher.display.OffsetAxes.OffsetAxesListener;
import org.scijava.plugin.Plugin;
import org.scijava.ui.behaviour.DragBehaviour;
import org.scijava.ui.behaviour.io.gui.CommandDescriptionProvider;
import org.scijava.ui.behaviour.io.gui.CommandDescriptions;
import org.scijava.ui.behaviour.util.Behaviours;

import bdv.viewer.OverlayRenderer;
import bdv.viewer.TransformListener;
import net.imglib2.RealLocalizable;

public class FreeformSelectionBehaviour implements DragBehaviour, OverlayRenderer, OffsetAxesListener, TransformListener< ScreenTransform >
{

public static final String FREEFORM_SELECTION = "freeform selection";

public static final String FREEFORM_SELECTION_ADD = "freeform add to selection";

private static final String[] FREEFORM_SELECTION_KEYS = new String[] { "ctrl button1" };

private static final String[] FREEFORM_SELECTION_ADD_KEYS = new String[] { "ctrl shift button1" };

public static < V extends Vertex< E > & HasTimepoint & HasLabel, E extends Edge< V > > void
install( final Behaviours behaviours,
final DataGraphLayout< ?, ? > layout,
final DataGraph< ?, ? > graph,
final SelectionModel< DataVertex, DataEdge > selection,
final FocusModel< DataVertex > focus,
final DataDisplayPanel< ?, ? > panel )
{
final FreeformSelectionBehaviour select = new FreeformSelectionBehaviour( layout, graph, selection, focus, panel, false );
select.transformChanged( panel.getScreenTransform().get() );
select.updateAxesSize( panel.getOffsetAxes().getWidth(), panel.getOffsetAxes().getHeight() );
behaviours.behaviour( select, FREEFORM_SELECTION, FREEFORM_SELECTION_KEYS );

final FreeformSelectionBehaviour selectAdd = new FreeformSelectionBehaviour( layout, graph, selection, focus, panel, true );
selectAdd.transformChanged( panel.getScreenTransform().get() );
selectAdd.updateAxesSize( panel.getOffsetAxes().getWidth(), panel.getOffsetAxes().getHeight() );
behaviours.behaviour( selectAdd, FREEFORM_SELECTION_ADD, FREEFORM_SELECTION_ADD_KEYS );

panel.getScreenTransform().listeners().add( select );
panel.getScreenTransform().listeners().add( selectAdd );

panel.getOffsetAxes().listeners().add( select );
panel.getOffsetAxes().listeners().add( selectAdd );

panel.getDisplay().overlays().add( select );
panel.getDisplay().overlays().add( selectAdd );
}

private final boolean addToSelection;

private final List< Point > polygon = new ArrayList<>();

private boolean isDrawing = false;

private final Component panel;

private final SelectionModel< DataVertex, DataEdge > selection;

private final DataGraphLayout< ?, ? > layout;

private final DataGraph< ?, ? > graph;

private final FocusModel< DataVertex > focus;

private final ScreenTransform screenTransform;

private int axesWidth;

public FreeformSelectionBehaviour(
final DataGraphLayout< ?, ? > layout,
final DataGraph< ?, ? > graph,
final SelectionModel< DataVertex, DataEdge > selection,
final FocusModel< DataVertex > focus,
final Component panel,
final boolean addToSelection )
{
this.layout = layout;
this.graph = graph;
this.selection = selection;
this.focus = focus;
this.panel = panel;
this.addToSelection = addToSelection;
this.screenTransform = new ScreenTransform();
}

@Override
public void init( final int x, final int y )
{
polygon.clear();
polygon.add( new Point( x, y ) );
isDrawing = true;
}

@Override
public void drag( final int x, final int y )
{
if ( isDrawing )
{
polygon.add( new Point( x, y ) );
RamerDouglasPeucker.simplifyPath( polygon, 0.1 );
panel.repaint();
}
}

@Override
public void end( final int x, final int y )
{
isDrawing = false;
polygon.add( new Point( x, y ) );
select();
}

private void select()
{
selection.pauseListeners();

if ( !addToSelection )
selection.clearSelection();

// Fetch data points in bounding-box.
final double x1 = polygon.stream().mapToDouble( Point::getX ).min().getAsDouble();
final double x2 = polygon.stream().mapToDouble( Point::getX ).max().getAsDouble();
final double y1 = polygon.stream().mapToDouble( Point::getY ).min().getAsDouble();
final double y2 = polygon.stream().mapToDouble( Point::getY ).max().getAsDouble();
final RefSet< DataVertex > vs = layout.getDataVerticesWithin( x1, y1, x2, y2 );

// Test if these points are in polygon.
final DataVertex vertexRef = graph.vertexRef();
for ( final DataVertex v : vs )
{
if ( isPointInsidePolygon( v ) )
{
selection.setSelected( v, true );
for ( final DataEdge e : v.outgoingEdges() )
{
final DataVertex t = e.getTarget( vertexRef );
if ( vs.contains( t ) )
selection.setSelected( e, true );
}
}
}
graph.releaseRef( vertexRef );

final Iterator< DataVertex > it = vs.iterator();
if ( it.hasNext() )
focus.focusVertex( it.next() );

selection.resumeListeners();
}

private boolean isPointInsidePolygon( final RealLocalizable point )
{
final int n = polygon.size();
boolean inside = false;

final double xl = point.getDoublePosition( 0 );
final double yl = point.getDoublePosition( 1 );
final double xs = screenTransform.layoutToScreenX( xl ) + axesWidth;
final double ys = screenTransform.layoutToScreenY( yl );

for ( int i = 0, j = n - 1; i < n; j = i++ )
{
final Point pi = polygon.get( i );
final Point pj = polygon.get( j );

if ( ( pi.y > ys ) != ( pj.y > ys ) &&
( xs < ( pj.x - pi.x ) * ( ys - pi.y ) / ( pj.y - pi.y ) + pi.x ) )
{
inside = !inside;
}
}
return inside;
}

private final Path2D path = new Path2D.Double();

@Override
public void drawOverlays( final Graphics g )
{
if ( !isDrawing )
return;

path.reset();
path.moveTo( polygon.get( 0 ).x, polygon.get( 0 ).y );
for ( int i = 1; i < polygon.size(); i++ )
path.lineTo( polygon.get( i ).x, polygon.get( i ).y );
path.closePath();

g.setColor( Color.RED );
final Graphics2D g2 = ( Graphics2D ) g;
g2.draw( path );
}

@Override
public void transformChanged( final ScreenTransform transform )
{
synchronized ( screenTransform )
{
screenTransform.set( transform );
}
}

@Override
public void updateAxesSize( final int width, final int height )
{
axesWidth = width;
}

/*
* Command descriptions for all provided commands
*/
@Plugin( type = CommandDescriptionProvider.class )
public static class Descriptions extends CommandDescriptionProvider
{
public Descriptions()
{
super( KeyConfigScopes.MASTODON, KeyConfigContexts.GRAPHER );
}

@Override
public void getCommandDescriptions( final CommandDescriptions descriptions )
{
descriptions.add( FREEFORM_SELECTION, FREEFORM_SELECTION_KEYS, "Freeform selection in the grapher." );
descriptions.add( FREEFORM_SELECTION_ADD, FREEFORM_SELECTION_ADD_KEYS, "Freeform add to selection in the grapher." );
}
}
}

0 comments on commit 5368f63

Please sign in to comment.