1   /****************************************************************************
2    * This demo file is part of yFiles for Java 2.14.
3    * Copyright (c) 2000-2017 by yWorks GmbH, Vor dem Kreuzberg 28,
4    * 72070 Tuebingen, Germany. All rights reserved.
5    * 
6    * yFiles demo files exhibit yFiles for Java functionalities. Any redistribution
7    * of demo files in source code or binary form, with or without
8    * modification, is not permitted.
9    * 
10   * Owners of a valid software license for a yFiles for Java version that this
11   * demo is shipped with are allowed to use the demo source code as basis
12   * for their own yFiles for Java powered applications. Use of such programs is
13   * governed by the rights and conditions as set out in the yFiles for Java
14   * license agreement.
15   * 
16   * THIS SOFTWARE IS PROVIDED ''AS IS'' AND ANY EXPRESS OR IMPLIED
17   * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
18   * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
19   * NO EVENT SHALL yWorks BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
21   * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
22   * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
23   * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
24   * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25   * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26   *
27   ***************************************************************************/
28  package demo.layout.labeling;
29  
30  import demo.view.DemoBase;
31  import demo.view.DemoDefaults;
32  import y.base.Edge;
33  import y.base.EdgeCursor;
34  import y.layout.CanonicMultiStageLayouter;
35  import y.layout.EdgeLabelLayout;
36  import y.layout.FixNodeLayoutStage;
37  import y.layout.LabelLayoutTranslator;
38  import y.layout.LayoutOrientation;
39  import y.layout.Layouter;
40  import y.layout.hierarchic.IncrementalHierarchicLayouter;
41  import y.layout.labeling.AbstractLabelingAlgorithm;
42  import y.layout.labeling.GreedyMISLabeling;
43  import y.layout.labeling.MISLabelingAlgorithm;
44  import y.layout.orthogonal.OrthogonalLayouter;
45  import y.layout.router.polyline.EdgeRouter;
46  import y.layout.tree.GenericTreeLayouter;
47  import y.layout.tree.TreeReductionStage;
48  import y.option.CompoundEditor;
49  import y.option.DefaultEditorFactory;
50  import y.option.Editor;
51  import y.option.EditorFactory;
52  import y.option.ItemEditor;
53  import y.option.OptionHandler;
54  import y.util.DataProviders;
55  import y.view.EdgeLabel;
56  import y.view.EditMode;
57  import y.view.Graph2D;
58  import y.view.Graph2DLayoutExecutor;
59  import y.view.Graph2DViewActions;
60  import y.view.PopupMode;
61  import y.view.Selections;
62  import y.view.SmartEdgeLabelModel;
63  import y.view.YLabel;
64  
65  import javax.swing.AbstractAction;
66  import javax.swing.Action;
67  import javax.swing.ActionMap;
68  import javax.swing.JComboBox;
69  import javax.swing.JComponent;
70  import javax.swing.JPanel;
71  import javax.swing.JPopupMenu;
72  import javax.swing.JToolBar;
73  import java.awt.BorderLayout;
74  import java.awt.Dimension;
75  import java.awt.EventQueue;
76  import java.awt.RenderingHints;
77  import java.awt.event.ActionEvent;
78  import java.awt.event.ActionListener;
79  import java.beans.PropertyChangeEvent;
80  import java.beans.PropertyChangeListener;
81  import java.net.URL;
82  import java.util.ArrayList;
83  import java.util.Iterator;
84  import java.util.List;
85  import java.util.Locale;
86  import java.util.Map;
87  
88  /**
89   * This demo shows how to configure the <code>PreferredPlacementDescriptor</code> of labels and how this effects the
90   * label placement using different layouters.
91   * <p>
92   *   The descriptor settings can be changed for all or only for a subset of the labels and a new layout is calculated
93   *   after each change to visualize the impact of the corresponding descriptor setting.
94   * </p>
95   * <p>
96   *   Two different label painters are used to either visualize the descriptor settings of a label or its oriented bounds.
97   * </p>
98   *
99   * @see <a href="http://docs.yworks.com/yfiles/doc/api/index.html#/dguide/labeling#label_preferred_placement" target="_blank">Section Preferred Placement of Edge Labels</a> in the yFiles for Java Developer's Guide
100  */
101 public class PreferredLabelPlacementDemo extends DemoBase {
102   //option handler texts
103   private static final String VISUALIZING_DESCRIPTOR_LABELING_CONFIG_NAME = "VISUALIZING_DESCRIPTOR_LABELING_CONFIG";
104   private static final String VISUALIZING_BOUNDS_LABELING_CONFIG_NAME = "VISUALIZING_BOUNDS_LABELING_CONFIG";
105   private static final int TOOLS_PANEL_WIDTH = 350;
106 
107   private final PreferredPlacementOptionHandler optionHandler;
108   private boolean isSelectionFinished;
109   private Layouter layouter;
110   private String labelVisualization;
111 
112   public PreferredLabelPlacementDemo() {
113     this(null);
114   }
115 
116   public PreferredLabelPlacementDemo(final String helpFilePath) {
117     //set view size and create content pane
118     view.setPreferredSize(new Dimension(650, 400));
119     view.setWorldRect(0, 0, 650, 400);
120     view.setFitContentOnResize(true);
121 
122     // create the labeling option handler and the content pane
123     optionHandler = new PreferredPlacementOptionHandler();
124     optionHandler.addChildPropertyChangeListener(
125       new PropertyChangeListener() {
126         public void propertyChange(final PropertyChangeEvent evt) {
127           if (isSelectionFinished && !hasUndefinedChanged(evt)) {
128             // Prevents the option handler to commit values if only the value of "valueUndefined"
129             // changes from "TRUE" to to "FALSE" but not the value itself.
130             optionHandler.commitValues(view.getGraph2D());
131             runLayout(false);
132           }
133         }
134       });
135     contentPane.add(createToolsPanel(helpFilePath), BorderLayout.EAST);
136 
137     // update the option handler when the selection of the edge labels has been changed
138     final Selections.SelectionStateObserver sso = new Selections.SelectionStateObserver() {
139       protected void updateSelectionState(Graph2D graph) {
140         isSelectionFinished = false;
141         optionHandler.updateValues(graph);
142         isSelectionFinished = true;
143       }
144     };
145     view.getGraph2D().addGraph2DSelectionListener(sso);
146     view.getGraph2D().addGraphListener(sso);
147 
148     // fix node port stage is used to keep the bounding box of the graph in the view port
149     view.getGraph2D().addDataProvider(
150       FixNodeLayoutStage.FIXED_NODE_DPKEY,
151       DataProviders.createConstantDataProvider(Boolean.TRUE));
152 
153     //load initial graph
154     loadGraph("resource/labeledgraph.graphml");
155   }
156 
157   private static boolean hasUndefinedChanged(final PropertyChangeEvent evt) {
158     return  "valueUndefined".equals(evt.getPropertyName()) &&
159             Boolean.TRUE.equals((Boolean)evt.getOldValue()) &&
160             Boolean.FALSE.equals((Boolean)evt.getNewValue());
161   }
162 
163   /**
164    * Creates an edit mode that does not allow to change the graph except the edge labels.
165    */
166   protected EditMode createEditMode() {
167     final EditMode editMode = new EditMode();
168     editMode.allowEdgeCreation(false);
169     editMode.allowNodeCreation(false);
170     editMode.allowBendCreation(false);
171     editMode.allowNodeEditing(false);
172     editMode.allowResizeNodes(false);
173     editMode.allowMoveSelection(false);
174     return editMode;
175   }
176 
177   protected void initialize() {
178     initializeLabelRendering();
179   }
180 
181   protected boolean isClipboardEnabled() {
182     return false;
183   }
184 
185   private void initializeLabelRendering() {
186     // Workaround that better keeps the label text inside its node for different zoom levels.
187     final RenderingHints rh = view.getRenderingHints();
188     rh.put(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
189     view.setRenderingHints(rh);
190     YLabel.setFractionMetricsForSizeCalculationEnabled(true);
191   }
192 
193   private static AbstractLabelingAlgorithm createGreedyMISLabeling() {
194     // pure labeling algorithm - nodes and edges are not moved
195     final GreedyMISLabeling labeling = new GreedyMISLabeling();
196     labeling.setOptimizationStrategy(MISLabelingAlgorithm.OPTIMIZATION_BALANCED);
197     labeling.setPlaceEdgeLabels(true);
198     labeling.setPlaceNodeLabels(false);
199     return labeling;
200   }
201 
202   private Layouter createIncrementalHierarchicLayouter(final byte layoutOrientation) {
203     final IncrementalHierarchicLayouter ihl = new IncrementalHierarchicLayouter();
204     ihl.setIntegratedEdgeLabelingEnabled(true);
205     ihl.setLayoutOrientation(layoutOrientation);
206     disableAutoFlipping(ihl);
207     return ihl;
208   }
209 
210   private Layouter createGenericTreeLayouter() {
211     final TreeReductionStage reductionStage = new TreeReductionStage();
212     
213     //specify edge routing algorithm responsible for non-tree edges
214     reductionStage.setNonTreeEdgeSelectionKey(Layouter.SELECTED_EDGES);
215     final EdgeRouter nonTreeEdgeRouter = new EdgeRouter();
216     nonTreeEdgeRouter.setSphereOfAction(EdgeRouter.ROUTE_SELECTED_EDGES);
217     nonTreeEdgeRouter.setSelectedEdgesDpKey(reductionStage.getNonTreeEdgeSelectionKey());
218     reductionStage.setNonTreeEdgeRouter(nonTreeEdgeRouter);
219 
220     //specify algorithm responsible for labels of non-tree edges
221     reductionStage.setNonTreeEdgeLabelSelectionKey("NonTreeEdgeLabelSelectionKey");
222     final AbstractLabelingAlgorithm labelingAlgorithm = createGreedyMISLabeling();
223     labelingAlgorithm.setSelection(reductionStage.getNonTreeEdgeLabelSelectionKey());
224     reductionStage.setNonTreeEdgeLabelingAlgorithm(labelingAlgorithm);
225 
226     final GenericTreeLayouter genericTreeLayouter = new GenericTreeLayouter();
227     genericTreeLayouter.setIntegratedEdgeLabeling(true);
228     genericTreeLayouter.prependStage(reductionStage);
229     disableAutoFlipping(genericTreeLayouter);
230     return genericTreeLayouter;
231   }
232 
233   private Layouter createOrthogonalLayouter() {
234     final OrthogonalLayouter orthogonalLayouter = new OrthogonalLayouter();
235     orthogonalLayouter.setIntegratedEdgeLabelingEnabled(true);
236     disableAutoFlipping(orthogonalLayouter);
237     return orthogonalLayouter;
238   }
239 
240   private void disableAutoFlipping(final CanonicMultiStageLayouter canonicMultiStageLayouter) {
241     final LabelLayoutTranslator labelLayouter = (LabelLayoutTranslator) canonicMultiStageLayouter.getLabelLayouter();
242     labelLayouter.setAutoFlippingEnabled(false);
243   }
244 
245   /**
246    * Does the label placement or a complete layout depending on the selected layout algorithm.
247    *
248    * @param fitViewToContent whether or not fit the view to the content after layout has been calculated
249    */
250   void runLayout(final boolean fitViewToContent) {
251     //configure and run the layouter
252     final Graph2DLayoutExecutor layoutExecutor = new Graph2DLayoutExecutor();
253     layoutExecutor.getLayoutMorpher().setKeepZoomFactor(!fitViewToContent);
254     layoutExecutor.doLayout(view, new FixNodeLayoutStage(layouter));
255   }
256 
257   protected void configureDefaultRealizers() {
258     super.configureDefaultRealizers();
259 
260     //customize label configuration
261     final YLabel.Factory factory = EdgeLabel.getFactory();
262 
263     final Map visualizingDescriptorConfigurationMap = factory.createDefaultConfigurationMap();
264     VisualizingDescriptorLabelConfiguration visualizingDescriptorLabelConfig = new VisualizingDescriptorLabelConfiguration();
265     visualizingDescriptorLabelConfig.setAutoFlippingEnabled(false);
266     visualizingDescriptorConfigurationMap.put(YLabel.Painter.class, visualizingDescriptorLabelConfig);
267     visualizingDescriptorConfigurationMap.put(YLabel.Layout.class, visualizingDescriptorLabelConfig);
268     visualizingDescriptorConfigurationMap.put(YLabel.BoundsProvider.class, visualizingDescriptorLabelConfig);
269     factory.addConfiguration(VISUALIZING_DESCRIPTOR_LABELING_CONFIG_NAME, visualizingDescriptorConfigurationMap);
270 
271     final Map visualizeBoundsConfigurationMap = factory.createDefaultConfigurationMap();
272     VisualizingBoundsLabelConfiguration visualizeBoundsLabelConfig = new VisualizingBoundsLabelConfiguration();
273     visualizeBoundsLabelConfig.setAutoFlippingEnabled(false);
274     visualizeBoundsConfigurationMap.put(YLabel.Painter.class, visualizeBoundsLabelConfig);
275     visualizeBoundsConfigurationMap.put(YLabel.Layout.class, visualizeBoundsLabelConfig);
276     visualizeBoundsConfigurationMap.put(YLabel.BoundsProvider.class, visualizeBoundsLabelConfig);
277     factory.addConfiguration(VISUALIZING_BOUNDS_LABELING_CONFIG_NAME, visualizeBoundsConfigurationMap);
278   }
279 
280   protected EdgeLabel createEdgeLabel() {
281     final EdgeLabel label = new EdgeLabel("Label");
282     final SmartEdgeLabelModel edgeLabelModel = new SmartEdgeLabelModel();
283     label.setLabelModel(edgeLabelModel, edgeLabelModel.getDefaultParameter());
284     label.setConfiguration(labelVisualization);
285     return label;
286   }
287 
288   /**
289    * Creates the tools panel containing the settings and the help panel.
290    */
291   private JPanel createToolsPanel(final String helpFilePath) {
292     final JPanel toolsPanel = new JPanel(new BorderLayout());
293     toolsPanel.add(createOptionHandlerComponent(optionHandler), BorderLayout.NORTH);
294 
295     if (helpFilePath != null) {
296       final URL url = getResource(helpFilePath);
297       if (url == null) {
298         System.err.println("Could not locate help file: " + helpFilePath);
299       } else {
300         final JComponent helpPane = createHelpPane(url);
301         if (helpPane != null) {
302           helpPane.setMinimumSize(new Dimension(200, 200));
303           helpPane.setPreferredSize(new Dimension(TOOLS_PANEL_WIDTH, 400));
304           toolsPanel.add(helpPane, BorderLayout.CENTER);
305         }
306       }
307     }
308     return toolsPanel;
309   }
310 
311   /**
312    * Creates an EditMode and adds a popup mode that displays the demo context menu.
313    */
314   protected void registerViewModes() {
315     final EditMode mode = createEditMode();
316     mode.setPopupMode(new DemoPopupMode());
317     view.addViewMode(mode);
318   }
319 
320   /**
321    * Creates the default view actions but removes the mnemonic for label editing since it is complicated to update the
322    * model if a new label is created by such an edit.
323    */
324   protected void registerViewActions() {
325     super.registerViewActions();
326     final ActionMap actionMap = view.getCanvasComponent().getActionMap();
327     actionMap.remove(Graph2DViewActions.EDIT_LABEL);
328   }
329 
330   /**
331    * Creates a {@link DeleteSelection} that only deletes edge labels.
332    */
333   protected Action createDeleteSelectionAction() {
334     final ActionMap actionMap = view.getCanvasComponent().getActionMap();
335     final Graph2DViewActions.DeleteSelectionAction deleteAction =
336       (Graph2DViewActions.DeleteSelectionAction) actionMap.get(Graph2DViewActions.DELETE_SELECTION);
337     deleteAction.putValue(Action.SMALL_ICON, getIconResource("resource/delete.png"));
338     deleteAction.putValue(Action.SHORT_DESCRIPTION, "Delete Selection");
339     deleteAction.setDeletionMask(Graph2DViewActions.DeleteSelectionAction.TYPE_EDGE_LABEL);
340     return deleteAction;
341   }
342 
343   /**
344    * Creates the default tool bar and adds a button for label placement.
345    */
346   protected JToolBar createToolBar() {
347     final JToolBar toolBar = super.createToolBar();
348     toolBar.addSeparator();
349     toolBar.add(createLayoutersComboBox());
350     toolBar.addSeparator(TOOLBAR_SMALL_SEPARATOR);
351     toolBar.add(createLayoutButton());
352     toolBar.addSeparator();
353     toolBar.add(createLabelVisualizationsComboBox());
354     return toolBar;
355   }
356 
357   private JComboBox createLayoutersComboBox() {
358     final JComboBox comboBox = new JComboBox(
359       new Object[]{
360         "Generic Label Placement",
361         "Hierarchic Layout, Top-Down",
362         "Hierarchic Layout, Left-Right",
363         "Generic Tree Layout",
364         "Orthogonal Layout"});
365     final Layouter[] layouters = {
366       createGreedyMISLabeling(),
367       createIncrementalHierarchicLayouter(LayoutOrientation.TOP_TO_BOTTOM),
368       createIncrementalHierarchicLayouter(LayoutOrientation.LEFT_TO_RIGHT),
369       createGenericTreeLayouter(),
370       createOrthogonalLayouter()};
371 
372     comboBox.setMaximumSize(comboBox.getPreferredSize());
373     comboBox.addActionListener(
374       new ActionListener() {
375         public void actionPerformed(ActionEvent e) {
376           layouter = layouters[comboBox.getSelectedIndex()];
377           runLayout(true);
378         }
379       });
380     layouter = layouters[0];
381     return comboBox;
382   }
383 
384   private JComponent createLayoutButton() {
385     return createActionControl(
386         new AbstractAction(
387           "Layout", SHARED_LAYOUT_ICON) {
388           public void actionPerformed(ActionEvent e) {
389             runLayout(true);
390           }
391         });
392   }
393 
394   private JComboBox createLabelVisualizationsComboBox() {
395     final JComboBox comboBox = new JComboBox( new Object[]{"Visualize Label Descriptor", "Visualize Label Bounds"});
396     final String[] visualizations = {VISUALIZING_DESCRIPTOR_LABELING_CONFIG_NAME, VISUALIZING_BOUNDS_LABELING_CONFIG_NAME};
397 
398     comboBox.setMaximumSize(comboBox.getPreferredSize());
399     comboBox.addActionListener(
400       new ActionListener() {
401         public void actionPerformed(ActionEvent e) {
402           labelVisualization = visualizations[comboBox.getSelectedIndex()];
403           setSelectedLabelVisualization();
404           view.updateView();
405         }
406       });
407     labelVisualization = visualizations[0];
408     return comboBox;
409   }
410 
411   /**
412    * Loads a graph and applies the label configuration to the existing labels.
413    */
414   protected void loadGraph(URL resource) {
415     super.loadGraph(resource);
416 
417     DemoDefaults.applyRealizerDefaults(view.getGraph2D());
418     setSelectedLabelVisualization();
419 
420     runLayout(true);
421     getUndoManager().resetQueue();
422   }
423 
424   private void setSelectedLabelVisualization() {
425     final Graph2D graph = view.getGraph2D();
426     for (EdgeCursor ec = graph.edges(); ec.ok(); ec.next()) {
427       final EdgeLabelLayout[] labelLayouts = graph.getEdgeLabelLayout(ec.edge());
428       for (int i = 0; i < labelLayouts.length; i++) {
429         final EdgeLabel label = (EdgeLabel) labelLayouts[i];
430         label.setConfiguration(labelVisualization);
431       }
432     }
433   }
434 
435   /**
436    * A {@link PopupMode} that adds labels to edges and calculates a new layout.
437    */
438   class DemoPopupMode extends PopupMode {
439     public JPopupMenu getEdgePopup(final Edge edge) {
440       final JPopupMenu popupMenu = new JPopupMenu();
441       popupMenu.add(
442         new AbstractAction("Add Label") {
443           public void actionPerformed(ActionEvent e) {
444             // Insert backup for correct undo.
445             final Graph2D graph2D = view.getGraph2D();
446             graph2D.firePreEvent();
447             try {
448               graph2D.backupRealizers(graph2D.edges());
449               graph2D.backupRealizers(graph2D.nodes());
450 
451               // Create and add a new edge label to the given edge.
452               final EdgeLabel label = createEdgeLabel();
453               graph2D.getRealizer(edge).addLabel(label);
454 
455               // place the new label by running a new layout calculation
456               runLayout(true);
457             } finally {
458               graph2D.firePostEvent();
459             }
460           }
461         });
462       return popupMenu;
463     }
464   }
465 
466   /**
467    * Creates a component for the specified option handler using the default editor factory and sets all of its items to
468    * auto adopt and auto commit.
469    */
470   private static JComponent createOptionHandlerComponent(final OptionHandler oh) {
471     final EditorFactory defaultEditorFactory = new DefaultEditorFactory();
472     final Editor editor = defaultEditorFactory.createEditor(oh);
473 
474     // Propagate auto adopt and auto commit to editor and its children.
475     final List stack = new ArrayList();
476     stack.add(editor);
477     while (!stack.isEmpty()) {
478       Object editorObj = stack.remove(stack.size() - 1);
479       if (editorObj instanceof ItemEditor) {
480         ((ItemEditor) editorObj).setAutoAdopt(true);
481         ((ItemEditor) editorObj).setAutoCommit(true);
482       }
483       if (editorObj instanceof CompoundEditor) {
484         for (Iterator iter = ((CompoundEditor) editorObj).editors(); iter.hasNext();) {
485           stack.add(iter.next());
486         }
487       }
488     }
489 
490     // Build and return component.
491     final JComponent optionComponent = editor.getComponent();
492     optionComponent.setMinimumSize(new Dimension(200, 50));
493     return optionComponent;
494   }
495 
496   /**
497    * Run this demo.
498    */
499   public static void main(String[] args) {
500     EventQueue.invokeLater(new Runnable() {
501       public void run() {
502         Locale.setDefault(Locale.ENGLISH);
503         initLnF();
504         (new PreferredLabelPlacementDemo("resource/preferredlabelplacementdemohelp.html")).start("Preferred Label Placement Demo");
505       }
506     });
507   }
508 }
509