1
28 package demo.layout.labeling;
29
30 import demo.view.DemoBase;
31 import demo.view.DemoDefaults;
32 import y.base.DataMap;
33 import y.base.GraphEvent;
34 import y.base.GraphListener;
35 import y.base.Node;
36 import y.base.NodeCursor;
37 import y.layout.AbstractLayoutStage;
38 import y.layout.DiscreteNodeLabelModel;
39 import y.layout.LabelCandidate;
40 import y.layout.LayoutGraph;
41 import y.layout.Layouter;
42 import y.layout.NodeLabelModel;
43 import y.layout.NodeLayout;
44 import y.layout.ProfitModel;
45 import y.layout.labeling.AbstractLabelingAlgorithm;
46 import y.layout.labeling.GreedyMISLabeling;
47 import y.layout.labeling.MISLabelingAlgorithm;
48 import y.layout.NodeLabelLayout;
49 import y.layout.BufferedLayouter;
50 import y.option.EditorFactory;
51 import y.option.OptionGroup;
52 import y.option.OptionHandler;
53 import y.option.DefaultEditorFactory;
54 import y.option.Editor;
55 import y.option.ItemEditor;
56 import y.option.CompoundEditor;
57 import y.util.DataProviderAdapter;
58 import y.util.Maps;
59 import y.view.DefaultBackgroundRenderer;
60 import y.view.EditMode;
61 import y.view.Graph2D;
62 import y.view.Graph2DLayoutExecutor;
63 import y.view.NodeLabel;
64 import y.view.NodeRealizer;
65 import y.view.PopupMode;
66 import y.view.SmartNodeLabelModel;
67 import y.view.YLabel;
68 import y.view.DefaultLabelConfiguration;
69 import y.geom.YPoint;
70 import y.geom.YRectangle;
71 import y.geom.LineSegment;
72
73 import javax.swing.AbstractAction;
74 import javax.swing.Action;
75 import javax.swing.JPopupMenu;
76 import javax.swing.JToolBar;
77 import javax.swing.JPanel;
78 import javax.swing.JComponent;
79 import java.awt.Color;
80 import java.awt.Dimension;
81 import java.awt.EventQueue;
82 import java.awt.Graphics2D;
83 import java.awt.BorderLayout;
84 import java.awt.geom.Line2D;
85 import java.awt.event.ActionEvent;
86 import java.net.URL;
87 import java.util.Arrays;
88 import java.util.HashMap;
89 import java.util.HashSet;
90 import java.util.List;
91 import java.util.Locale;
92 import java.util.Map;
93 import java.util.ArrayList;
94 import java.util.Iterator;
95 import java.beans.PropertyChangeEvent;
96 import java.beans.PropertyChangeListener;
97 import java.util.Set;
98
99
120 public class NodeLabelingDemo extends DemoBase {
121 private static final String LABELING_MODEL_STRING = "Labeling Model";
122 private static final String LABEL_SIZE_STRING = "Font Size";
123 private static final String PROPERTIES_GROUP = "Node Label Properties";
124
125 private static final String MODEL_CORNERS = "Corners";
127 private static final String MODEL_SANDWICH = "Sandwich";
128 private static final String MODEL_SIDE = "Side";
129 private static final String MODEL_FREE = "Free";
130 private static final String MODEL_EIGHT_POS = "8 Pos";
131 private static final String[] NODE_LABEL_MODELS = {
132 MODEL_CORNERS, MODEL_SANDWICH, MODEL_SIDE, MODEL_FREE, MODEL_EIGHT_POS
133 };
134
135 private static final int TOOLS_PANEL_WIDTH = 350;
136
137 private Map label2Model; private MyGenericNodeLabelingAlgorithm labelLayouter;
139 private OptionHandler optionHandler;
140
141 public NodeLabelingDemo() {
142 this(null);
143 }
144
145 public NodeLabelingDemo(final String helpFilePath) {
146 DefaultBackgroundRenderer renderer = new DefaultBackgroundRenderer(view);
148
149 URL bgImage = getSharedResource("resource/usamap.png");
151
152 renderer.setImageResource(bgImage);
153 renderer.setMode(DefaultBackgroundRenderer.DYNAMIC);
154 renderer.setColor(Color.white);
155 view.setBackgroundRenderer(renderer);
156 view.setPreferredSize(new Dimension(650, 400));
157 view.setWorldRect(0, 0, 650, 400);
158
159 contentPane.add(createToolsPanel(helpFilePath), BorderLayout.EAST);
160
161 loadGraph("resource/uscities.graphml");
162
163 view.getGraph2D().addGraphListener(new GraphListener() {
165 public void onGraphEvent(GraphEvent e) {
166 if (e.getType() == GraphEvent.NODE_CREATION) {
167 final Graph2D graph = view.getGraph2D();
168 final Node node = (Node) e.getData();
169 final NodeLabelLayout[] nll = graph.getNodeLabelLayout(node);
170
171 final int labelSize = optionHandler.getInt(LABEL_SIZE_STRING);
173 final NodeLabelModel labelingModel = getModel(
174 optionHandler.getEnum(LABELING_MODEL_STRING)); for (int i = 0; i < nll.length; i++) {
176 label2Model.put(nll[i], labelingModel);
177 ((NodeLabel) nll[i]).setFontSize(labelSize);
178 }
179
180 final Set newLabels = new HashSet(Arrays.asList(nll));
182 graph.addDataProvider("SELECTED_LABELS", new DataProviderAdapter() {
183 public boolean getBool(Object dataHolder) {
184 return newLabels.contains(dataHolder);
185 }
186 });
187 final Object oldSelectionKey = labelLayouter.getSelectionKey();
188 labelLayouter.setSelection("SELECTED_LABELS");
189 new BufferedLayouter(labelLayouter).doLayout(graph);
190 labelLayouter.setSelection(oldSelectionKey);
191 graph.removeDataProvider("SELECTED_LABELS");
192 }
193 }
194 });
195
196 doLabelPlacement();
198
199 getUndoManager().resetQueue();
200 }
201
202 protected void initialize() {
203 optionHandler = createOptionHandler();
204
205 labelLayouter = new MyGenericNodeLabelingAlgorithm();
206
207 label2Model = new HashMap();
209 view.getGraph2D().addDataProvider(AbstractLabelingAlgorithm.LABEL_MODEL_DPKEY, new DataProviderAdapter() {
210 public Object get(Object dataHolder) {
211 return label2Model.get(dataHolder);
212 }
213 });
214 }
215
216
220 static class MyGenericNodeLabelingAlgorithm extends AbstractLayoutStage {
221 private final GreedyMISLabeling coreLabeling;
222
223 public MyGenericNodeLabelingAlgorithm() {
224 this(null);
225 }
226
227 public MyGenericNodeLabelingAlgorithm(Layouter core) {
228 super(core);
229 this.coreLabeling = new GreedyMISLabeling();
230 coreLabeling.setOptimizationStrategy(MISLabelingAlgorithm.OPTIMIZATION_BALANCED);
231 coreLabeling.setPlaceEdgeLabels(false);
232 coreLabeling.setPlaceNodeLabels(true);
233 coreLabeling.setApplyPostprocessing(true);
234 }
235
236
243 public void setSelection(Object key) {
244 coreLabeling.setSelection(key);
245 }
246
247
250 public Object getSelectionKey() {
251 return coreLabeling.getSelectionKey();
252 }
253
254 public boolean canLayout(LayoutGraph graph) {
255 return canLayoutCore(graph);
256 }
257
258 public void doLayout(LayoutGraph graph) {
259 doLayoutCore(graph);
260
261 coreLabeling.setProfitModel(new PreferNorthLabelsProfitModel(graph));
263
264 coreLabeling.doLayout(graph);
266 }
267
268
272 private static class PreferNorthLabelsProfitModel implements ProfitModel {
273 private static final double MAX_DISTANCE_WITH_PROFIT = 10;
274 private static final double MAX_HORIZONTAL_CENTER_DISTANCE_WITH_PROFIT = 10;
275 private DataMap label2NodeLayout;
276
277 public PreferNorthLabelsProfitModel(final LayoutGraph graph) {
278 updateLabel2NodeLayoutMap(graph);
279 }
280
281
285 private void updateLabel2NodeLayoutMap(final LayoutGraph graph) {
286 this.label2NodeLayout = Maps.createHashedDataMap();
287 for (NodeCursor nc = graph.nodes(); nc.ok(); nc.next()) {
288 final NodeLayout nLayout = graph.getNodeLayout(nc.node());
289 final NodeLabelLayout[] nll = graph.getLabelLayout(nc.node());
290 for (int i = 0; i < nll.length; i++) {
291 label2NodeLayout.set(nll[i], nLayout);
292 }
293 }
294 }
295
296 private static boolean isLabelAboveNode(final YRectangle labelBox, final NodeLayout nodeLayout) {
297 return nodeLayout.getY() > labelBox.getY() + labelBox.getHeight();
298 }
299
300 private static double calcHorizontalCenterDistance(final YRectangle labelBox, final NodeLayout nodeLayout) {
301 return Math.abs((nodeLayout.getX() + nodeLayout.getWidth() * 0.5) - (labelBox.getX() + labelBox.getWidth() * 0.5));
302 }
303
304 private static double calcDistance(YRectangle r1, YRectangle r2) {
305 if (YRectangle.intersects(r1, r2)) {
306 return 0.0;
307 } else {
308 final double distVertical = calculateOrthogonalDifference(r1, r2, false);
309 final double distHoriztonal = calculateOrthogonalDifference(r1, r2, true);
310 return Math.sqrt(distVertical * distVertical + distHoriztonal * distHoriztonal);
311 }
312 }
313
314 private static double calculateOrthogonalDifference(YRectangle rect1, YRectangle rect2, boolean horizontal) {
315 final double rect1Min = horizontal ? rect1.getX() : rect1.getY();
316 final double rect1Max = horizontal ? rect1.getX() + rect1.getWidth() : rect1.getY() + rect1.getHeight();
317 final double rect2Min = horizontal ? rect2.getX() : rect2.getY();
318 final double rect2Max = horizontal ? rect2.getX() + rect2.getWidth() : rect2.getY() + rect2.getHeight();
319
320 if (rect2Max < rect1Min) {
321 return rect2Max - rect1Min;
323 } else if (rect1Max < rect2Min) {
324 return rect2Min - rect1Max;
326 } else {
327 return 0.0;
329 }
330 }
331
332 private static YRectangle getBox(final NodeLayout nodeLayout) {
333 return new YRectangle(nodeLayout.getX(), nodeLayout.getY(), nodeLayout.getWidth(), nodeLayout.getHeight());
334 }
335
336
342 private static double calcProfit(final YRectangle labelBox, final NodeLayout nodeLayout) {
343 if (!isLabelAboveNode(labelBox, nodeLayout)) {
344 return 0;
345 }
346
347 double profit = 0.5;
348 final double dist = calcDistance(labelBox, getBox(nodeLayout));
349 if (dist < MAX_DISTANCE_WITH_PROFIT) {
350 profit += 0.25 * (MAX_DISTANCE_WITH_PROFIT - dist) / MAX_DISTANCE_WITH_PROFIT;
352 }
353 final double horizontalCenterDist = calcHorizontalCenterDistance(labelBox, nodeLayout);
354 if (horizontalCenterDist < MAX_HORIZONTAL_CENTER_DISTANCE_WITH_PROFIT) {
355 profit += 0.25 * (MAX_HORIZONTAL_CENTER_DISTANCE_WITH_PROFIT - horizontalCenterDist) / MAX_HORIZONTAL_CENTER_DISTANCE_WITH_PROFIT;
357 }
358 return profit;
359 }
360
361 public double getProfit(LabelCandidate candidate) {
362 final NodeLayout nodeLayout = (NodeLayout) label2NodeLayout.get(candidate.getOwner());
363 return (nodeLayout == null) ? 0 : calcProfit(candidate.getBoundingBox(), nodeLayout);
364 }
365 }
366 }
367
368
372 private void doLabelPlacement() {
373 final Graph2D graph = view.getGraph2D();
375
376 final int labelSize = optionHandler.getInt(LABEL_SIZE_STRING);
378 final NodeLabelModel labelingModel = getModel(optionHandler.getEnum(LABELING_MODEL_STRING)); label2Model.clear();
380 for (NodeCursor nc = graph.nodes(); nc.ok(); nc.next()) {
381 NodeLabelLayout[] nll = graph.getNodeLabelLayout(nc.node());
382 for (int i = 0; i < nll.length; i++) {
383 label2Model.put(nll[i], labelingModel);
384 ((NodeLabel) nll[i]).setFontSize(labelSize);
385 }
386 }
387
388 final NodeRealizer defaultNodeRealizer = graph.getDefaultNodeRealizer();
390 final NodeLabel nl = defaultNodeRealizer.getLabel();
391 nl.setFontSize(labelSize);
392 final SmartNodeLabelModel model = new SmartNodeLabelModel();
393 nl.setLabelModel(model, model.getDefaultParameter());
395 new Graph2DLayoutExecutor().doLayout(view, labelLayouter);
396
397 view.updateView();
398 }
399
400
403 private OptionHandler createOptionHandler() {
404 final OptionHandler oh = new OptionHandler("Options");
405 oh.addEnum(LABELING_MODEL_STRING, NODE_LABEL_MODELS, 2);
406 oh.addInt(LABEL_SIZE_STRING, 12, 10, 25);
407
408 OptionGroup og = new OptionGroup();
409 og.setAttribute(OptionGroup.ATTRIBUTE_TITLE, PROPERTIES_GROUP);
410 og.addItem(oh.getItem(LABELING_MODEL_STRING));
411 og.addItem(oh.getItem(LABEL_SIZE_STRING));
412
413 oh.addChildPropertyChangeListener(new PropertyChangeListener() {
414 public void propertyChange(PropertyChangeEvent evt) {
415 doLabelPlacement();
417 }
418 });
419
420 return oh;
421 }
422
423
426 private static NodeLabelModel getModel(int index) {
427 if (index < 0 || index >= NODE_LABEL_MODELS.length) {
428 return new DiscreteNodeLabelModel(DiscreteNodeLabelModel.SANDWICH_MASK);
429 }
430
431 final String modelString = NODE_LABEL_MODELS[index];
432 if (MODEL_CORNERS.equals(modelString)) {
433 return new DiscreteNodeLabelModel(DiscreteNodeLabelModel.CORNER_MASK);
434 } else if (MODEL_EIGHT_POS.equals(modelString)) {
435 return new DiscreteNodeLabelModel(DiscreteNodeLabelModel.EIGHT_POS_MASK);
436 } else if (MODEL_FREE.equals(modelString)) {
437 return new SmartNodeLabelModel();
438 } else if (MODEL_SIDE.equals(modelString)) {
439 return new DiscreteNodeLabelModel(DiscreteNodeLabelModel.SIDES_MASK);
440 } else {
441 return new DiscreteNodeLabelModel(DiscreteNodeLabelModel.SANDWICH_MASK);
442 }
443 }
444
445
448 private JPanel createToolsPanel(String helpFilePath) {
449 JPanel toolsPanel = new JPanel(new BorderLayout());
450 toolsPanel.add(createOptionHandlerComponent(optionHandler), BorderLayout.NORTH);
451
452 if (helpFilePath != null) {
453 final URL url = getResource(helpFilePath);
454 if (url == null) {
455 System.err.println("Could not locate help file: " + helpFilePath);
456 } else {
457 JComponent helpPane = createHelpPane(url);
458 if (helpPane != null) {
459 helpPane.setMinimumSize(new Dimension(200, 200));
460 helpPane.setPreferredSize(new Dimension(TOOLS_PANEL_WIDTH, 400));
461 toolsPanel.add(helpPane, BorderLayout.CENTER);
462 }
463 }
464 }
465
466 return toolsPanel;
467 }
468
469 protected void configureDefaultRealizers() {
470 super.configureDefaultRealizers();
471
472 final YLabel.Factory factory = NodeLabel.getFactory();
474 final Map implementationsMap = factory.createDefaultConfigurationMap();
475 implementationsMap.put(YLabel.Painter.class, new MyPainter());
476 factory.addConfiguration("Customized", implementationsMap);
477
478 NodeRealizer nodeRealizer = view.getGraph2D().getDefaultNodeRealizer();
480 nodeRealizer.setSize(10.0, 10.0);
481 nodeRealizer.getLabel().setText("City");
482 nodeRealizer.getLabel().setConfiguration("Customized");
483 }
484
485 protected EditMode createEditMode() {
486 final EditMode mode = super.createEditMode();
488 mode.allowEdgeCreation(false);
489 mode.allowMoveSelection(false);
490 mode.setSnappingEnabled(false);
491 mode.allowResizeNodes(false);
492 mode.setPopupMode(new DemoPopupMode());
493 return mode;
494 }
495
496 protected JToolBar createToolBar() {
497 JToolBar bar = super.createToolBar();
498 bar.addSeparator();
499 bar.add(createActionControl(new LayoutAction()));
500 return bar;
501 }
502
503
506 protected void loadGraph(URL resource) {
507 super.loadGraph(resource);
508
509 final Graph2D graph2D = view.getGraph2D();
510 DemoDefaults.applyRealizerDefaults(graph2D);
511 for (NodeCursor nc = graph2D.nodes(); nc.ok(); nc.next()) {
512 final NodeLabelLayout[] nll = graph2D.getNodeLabelLayout(nc.node());
513 for (int i = 0; i < nll.length; i++) {
514 ((NodeLabel) nll[i]).setConfiguration("Customized");
515 }
516 }
517 }
518
519
522 class LayoutAction extends AbstractAction {
523 LayoutAction() {
524 super("Place Labels", SHARED_LAYOUT_ICON);
525 putValue(Action.SHORT_DESCRIPTION, "Place labels");
526 }
527
528 public void actionPerformed(ActionEvent e) {
529 doLabelPlacement();
530 }
531 }
532
533
536 class DemoPopupMode extends PopupMode {
537
540 public JPopupMenu getNodePopup(Node v) {
541 JPopupMenu pm = new JPopupMenu();
542 NodeRealizer r = this.view.getGraph2D().getRealizer(v);
543 YLabel label = r.getLabel();
544 pm.add(new EditLabel(label));
545 return pm;
546 }
547
548
551 public JPopupMenu getNodeLabelPopup(NodeLabel label) {
552 JPopupMenu pm = new JPopupMenu();
553 pm.add(new EditLabel(label));
554 return pm;
555 }
556 }
557
558
561 class EditLabel extends AbstractAction {
562 YLabel label;
563
564 EditLabel(YLabel l) {
565 super("Edit Label");
566 label = l;
567 }
568
569 public void actionPerformed(ActionEvent e) {
570 view.openLabelEditor(label, label.getTextLocation().getX(), label.getTextLocation().getY());
571 }
572 }
573
574
578 static final class MyPainter extends DefaultLabelConfiguration {
579
580 public void paintBox(YLabel label, Graphics2D gfx, double x, double y, double width, double height) {
581 super.paintBox(label, gfx, x, y, width, height);
582 if (label instanceof NodeLabel) {
583 final Node node = ((NodeLabel) label).getNode();
585 final Graph2D graph2D = (Graph2D) node.getGraph();
586 final LineSegment connectingLine = new LineSegment(new YPoint(x + width * 0.5d, y + height * 0.5d),
587 graph2D.getCenter(node));
588
589 final YRectangle labelBox = new YRectangle(x, y, width, height);
591 YPoint startPoint = calcBorderIntersectionPoints(labelBox, connectingLine);
592 final YRectangle nodeBox = graph2D.getRectangle(node);
593 YPoint endPoint = calcBorderIntersectionPoints(nodeBox, connectingLine);
594
595 if (startPoint != null && endPoint != null) {
597 Line2D line = new Line2D.Double(startPoint.x, startPoint.y, endPoint.x, endPoint.y);
598 gfx.setColor(new Color(0, 0, 0, 150));
599 gfx.draw(line);
600 }
601 }
602 }
603 }
604
605
609 private static JComponent createOptionHandlerComponent(OptionHandler oh) {
610 final EditorFactory defaultEditorFactory = new DefaultEditorFactory();
611 final Editor editor = defaultEditorFactory.createEditor(oh);
612
613 final List stack = new ArrayList();
615 stack.add(editor);
616 while(!stack.isEmpty()) {
617 Object editorObj = stack.remove(stack.size() - 1);
618 if(editorObj instanceof ItemEditor) {
619 ((ItemEditor) editorObj).setAutoAdopt(true);
620 ((ItemEditor) editorObj).setAutoCommit(true);
621 }
622 if(editorObj instanceof CompoundEditor) {
623 for (Iterator iter = ((CompoundEditor) editorObj).editors(); iter.hasNext(); ) {
624 stack.add(iter.next());
625 }
626 }
627 }
628
629 JComponent optionComponent = editor.getComponent();
631 optionComponent.setMinimumSize(new Dimension(200, 50));
632 return optionComponent;
633 }
634
635
639 private static YPoint calcBorderIntersectionPoints(YRectangle r, LineSegment l) {
640 if(!r.contains(l.getFirstEndPoint()) && !r.contains(l.getSecondEndPoint())) {
641 throw new RuntimeException("Input no valid!");
642 }
643
644 final YPoint[] rCorners = new YPoint[4];
646 rCorners[0] = r.getLocation();
647 rCorners[1] = new YPoint(rCorners[0].x, rCorners[0].y + r.getHeight());
648 rCorners[2] = new YPoint(rCorners[1].x + r.getWidth(), rCorners[1].y);
649 rCorners[3] = new YPoint(rCorners[2].x, rCorners[0].y);
650 for(int i = 0; i < rCorners.length; i++) {
651 final LineSegment rSide = new LineSegment(rCorners[i], rCorners[(i + 1) % 4]);
652 YPoint intersectionPoint = LineSegment.getIntersection(rSide, l);
653 if(intersectionPoint != null) {
654 return intersectionPoint; }
656 }
657
658 for(int i = 0; i < rCorners.length; i++) {
660 if(l.intersects(rCorners[i])) {
661 return rCorners[i];
662 }
663 }
664
665 return null; }
667
668 public static void main(String[] args) {
669 EventQueue.invokeLater(new Runnable() {
670 public void run() {
671 Locale.setDefault(Locale.ENGLISH);
672 initLnF();
673 (new NodeLabelingDemo("resource/nodelabelingdemohelp.html")).start("Labeling Demo");
674 }
675 });
676 }
677 }
678
679
680
681