001 /*
002 * Copyright (c) 1998-2009, The University of Sheffield and Ontotext.
003 *
004 * This file is part of GATE (see http://gate.ac.uk/), and is free
005 * software, licenced under the GNU Library General Public License,
006 * Version 2, June 1991 (in the distribution as file licence.html,
007 * and also available at http://gate.ac.uk/gate/licence.html).
008 *
009 * Thomas Heitz - 7 July 2009
010 *
011 * $Id$
012 */
013
014 package gate.gui.docview;
015
016 import gate.Node;
017 import gate.FeatureMap;
018 import gate.util.Strings;
019 import gate.annotation.NodeImpl;
020
021 import javax.swing.*;
022 import javax.swing.event.MouseInputAdapter;
023 import javax.swing.border.CompoundBorder;
024 import javax.swing.border.EtchedBorder;
025 import javax.swing.border.EmptyBorder;
026 import java.util.*;
027 import java.awt.*;
028
029 /**
030 * Stack of annotations in a JPanel.
031 * <br><br>
032 * To use, respect this order:<br><code>
033 * AnnotationStack stackPanel = new AnnotationStack(...);<br>
034 * stackPanel.set...(...);<br>
035 * stackPanel.clearAllRows();<br>
036 * stackPanel.addRow(...);<br>
037 * stackPanel.addAnnotation(...);<br>
038 * stackPanel.drawStack();</code>
039 */
040 public class AnnotationStack extends JPanel {
041
042 public AnnotationStack() {
043 super();
044 init();
045 }
046
047 /**
048 * @param maxTextLength maximum number of characters for the text,
049 * if too long an ellipsis is added in the middle
050 * @param maxFeatureValueLength maximum number of characters
051 * for a feature value
052 */
053 public AnnotationStack(int maxTextLength, int maxFeatureValueLength) {
054 super();
055 this.maxTextLength = maxTextLength;
056 this.maxFeatureValueLength = maxFeatureValueLength;
057 init();
058 }
059
060 void init() {
061 setLayout(new GridBagLayout());
062 setOpaque(true);
063 setBackground(Color.WHITE);
064 stackRows = new ArrayList<StackRow>();
065 textMouseListener = new StackMouseListener();
066 headerMouseListener = new StackMouseListener();
067 annotationMouseListener = new StackMouseListener();
068 }
069
070 /**
071 * Add a row to the annotation stack.
072 *
073 * @param set set name for the annotation, may be null
074 * @param type annotation type
075 * @param feature feature name, may be null
076 * @param lastColumnButton button at the end of the column, may be null
077 * @param shortcut replace the header of the row, may be null
078 * @param crop how to crop the text for the annotation if too long, one of
079 * {@link #CROP_START}, {@link #CROP_MIDDLE} or {@link #CROP_END}
080 */
081 public void addRow(String set, String type, String feature,
082 JButton lastColumnButton, String shortcut, int crop) {
083 stackRows.add(
084 new StackRow(set, type, feature, lastColumnButton, shortcut, crop));
085 }
086
087 /**
088 * Add an annotation to the current stack row.
089 *
090 * @param startOffset document offset where starts the annotation
091 * @param endOffset document offset where ends the annotation
092 * @param type annotation type
093 * @param features annotation features map
094 */
095 public void addAnnotation(int startOffset, int endOffset,
096 String type, FeatureMap features) {
097 stackRows.get(stackRows.size()-1).addAnnotation(
098 StackAnnotation.createAnnotation(startOffset, endOffset, type, features));
099 }
100
101 /**
102 * Add an annotation to the current stack row.
103 *
104 * @param annotation annotation to add to the current stack row
105 */
106 public void addAnnotation(gate.Annotation annotation) {
107 stackRows.get(stackRows.size()-1).addAnnotation(
108 StackAnnotation.createAnnotation(annotation));
109 }
110
111 /**
112 * Clear all rows in the stack. To be called before adding the first row.
113 */
114 public void clearAllRows() {
115 stackRows.clear();
116 }
117
118 /**
119 * Draw the annotation stack in a JPanel with a GridBagLayout.
120 */
121 public void drawStack() {
122
123 // clear the panel
124 removeAll();
125
126 boolean textTooLong = text.length() > maxTextLength;
127 int upperBound = text.length() - (maxTextLength/2);
128
129 GridBagConstraints gbc = new GridBagConstraints();
130 gbc.gridx = 0;
131 gbc.gridy = 0;
132 gbc.fill = GridBagConstraints.BOTH;
133
134 /**********************
135 * First row of text *
136 *********************/
137
138 gbc.gridwidth = 1;
139 gbc.insets = new java.awt.Insets(10, 10, 10, 10);
140 JLabel labelTitle = new JLabel("Context");
141 labelTitle.setOpaque(true);
142 labelTitle.setBackground(Color.WHITE);
143 labelTitle.setBorder(new CompoundBorder(
144 new EtchedBorder(EtchedBorder.LOWERED,
145 new Color(250, 250, 250), new Color(250, 250, 250).darker()),
146 new EmptyBorder(new Insets(0, 2, 0, 2))));
147 labelTitle.setToolTipText("Expression and its context.");
148 add(labelTitle, gbc);
149 gbc.insets = new java.awt.Insets(10, 0, 10, 0);
150
151 int expressionStart = contextBeforeSize;
152 int expressionEnd = text.length() - contextAfterSize;
153
154 // for each character
155 for (int charNum = 0; charNum < text.length(); charNum++) {
156
157 gbc.gridx = charNum + 1;
158 if (textTooLong) {
159 if (charNum == maxTextLength/2) {
160 // add ellipsis dots in case of a too long text displayed
161 add(new JLabel("..."), gbc);
162 // skip the middle part of the text if too long
163 charNum = upperBound + 1;
164 continue;
165 } else if (charNum > upperBound) {
166 gbc.gridx -= upperBound - (maxTextLength/2) + 1;
167 }
168 }
169
170 // set the text and color of the feature value
171 JLabel label = new JLabel(text.substring(charNum, charNum+1));
172 if (charNum >= expressionStart && charNum < expressionEnd) {
173 // this part is matched by the pattern, color it
174 label.setBackground(new Color(240, 201, 184));
175 } else {
176 // this part is the context, no color
177 label.setBackground(Color.WHITE);
178 }
179 label.setOpaque(true);
180
181 // get the word from which belongs the current character charNum
182 int start = text.lastIndexOf(" ", charNum);
183 int end = text.indexOf(" ", charNum);
184 String word = text.substring(
185 (start == -1) ? 0 : start,
186 (end == -1) ? text.length() : end);
187 // add a mouse listener that modify the query
188 label.addMouseListener(textMouseListener.createListener(word));
189 add(label, gbc);
190 }
191
192 /************************************
193 * Subsequent rows with annotations *
194 ************************************/
195
196 // for each row to display
197 for (StackRow stackRow : stackRows) {
198 String type = stackRow.getType();
199 String feature = stackRow.getFeature();
200 if (feature == null) { feature = ""; }
201 String shortcut = stackRow.getShortcut();
202 if (shortcut == null) { shortcut = ""; }
203
204 gbc.gridy++;
205 gbc.gridx = 0;
206 gbc.gridwidth = 1;
207 gbc.insets = new Insets(0, 0, 3, 0);
208
209 // add the header of the row
210 JLabel annotationTypeAndFeature = new JLabel();
211 String typeAndFeature = type + (feature.equals("") ? "" : ".") + feature;
212 annotationTypeAndFeature.setText(!shortcut.equals("") ?
213 shortcut : stackRow.getSet() != null ?
214 stackRow.getSet() + "#" + typeAndFeature : typeAndFeature);
215 annotationTypeAndFeature.setOpaque(true);
216 annotationTypeAndFeature.setBackground(Color.WHITE);
217 annotationTypeAndFeature.setBorder(new CompoundBorder(
218 new EtchedBorder(EtchedBorder.LOWERED,
219 new Color(250, 250, 250), new Color(250, 250, 250).darker()),
220 new EmptyBorder(new Insets(0, 2, 0, 2))));
221 if (feature.equals("")) {
222 annotationTypeAndFeature.addMouseListener(
223 headerMouseListener.createListener(type));
224 } else {
225 annotationTypeAndFeature.addMouseListener(
226 headerMouseListener.createListener(type, feature));
227 }
228 gbc.insets = new java.awt.Insets(0, 10, 3, 10);
229 add(annotationTypeAndFeature, gbc);
230 gbc.insets = new java.awt.Insets(0, 0, 3, 0);
231
232 // add all annotations for this row
233 HashMap<Integer,TreeSet<Integer>> gridSet =
234 new HashMap<Integer,TreeSet<Integer>>();
235 int gridyMax = gbc.gridy;
236 for(StackAnnotation ann : stackRow.getAnnotations()) {
237 gbc.gridx = ann.getStartNode().getOffset().intValue()
238 - expressionStartOffset + contextBeforeSize + 1;
239 gbc.gridwidth = ann.getEndNode().getOffset().intValue()
240 - ann.getStartNode().getOffset().intValue();
241 if (gbc.gridx == 0) {
242 // column 0 is already the row header
243 gbc.gridwidth -= 1;
244 gbc.gridx = 1;
245 } else if (gbc.gridx < 0) {
246 // annotation starts before displayed text
247 gbc.gridwidth += gbc.gridx - 1;
248 gbc.gridx = 1;
249 }
250 if (gbc.gridx + gbc.gridwidth > text.length()) {
251 // annotation ends after displayed text
252 gbc.gridwidth = text.length() - gbc.gridx + 1;
253 }
254 if(textTooLong) {
255 if(gbc.gridx > (upperBound + 1)) {
256 // x starts after the hidden middle part
257 gbc.gridx -= upperBound - (maxTextLength / 2) + 1;
258 }
259 else if(gbc.gridx > (maxTextLength / 2)) {
260 // x starts in the hidden middle part
261 if(gbc.gridx + gbc.gridwidth <= (upperBound + 3)) {
262 // x ends in the hidden middle part
263 continue; // skip the middle part of the text
264 }
265 else {
266 // x ends after the hidden middle part
267 gbc.gridwidth -= upperBound - gbc.gridx + 2;
268 gbc.gridx = (maxTextLength / 2) + 2;
269 }
270 }
271 else {
272 // x starts before the hidden middle part
273 if(gbc.gridx + gbc.gridwidth < (maxTextLength / 2)) {
274 // x ends before the hidden middle part
275 // do nothing
276 }
277 else if(gbc.gridx + gbc.gridwidth < upperBound) {
278 // x ends in the hidden middle part
279 gbc.gridwidth = (maxTextLength / 2) - gbc.gridx + 1;
280 }
281 else {
282 // x ends after the hidden middle part
283 gbc.gridwidth -= upperBound - (maxTextLength / 2) + 1;
284 }
285 }
286 }
287 if(gbc.gridwidth == 0) {
288 gbc.gridwidth = 1;
289 }
290
291 JLabel label = new JLabel();
292 Object object = ann.getFeatures().get(feature);
293 String value = (object == null) ? " " : Strings.toString(object);
294 if(value.length() > maxFeatureValueLength) {
295 // show the full text in the tooltip
296 label.setToolTipText((value.length() > 500) ?
297 "<html><textarea rows=\"30\" cols=\"40\" readonly=\"readonly\">"
298 + value.replaceAll("(.{50,60})\\b", "$1\n") + "</textarea></html>" :
299 ((value.length() > 100) ?
300 "<html><table width=\"500\" border=\"0\" cellspacing=\"0\">"
301 + "<tr><td>" + value.replaceAll("\n", "<br>")
302 + "</td></tr></table></html>" :
303 value));
304 if(stackRow.getCrop() == CROP_START) {
305 value = "..." + value.substring(
306 value.length() - maxFeatureValueLength - 1);
307 }
308 else if(stackRow.getCrop() == CROP_END) {
309 value = value.substring(0, maxFeatureValueLength - 2) + "...";
310 }
311 else {// cut in the middle
312 value = value.substring(0, maxFeatureValueLength / 2) + "..."
313 + value.substring(value.length() - (maxFeatureValueLength / 2));
314 }
315 }
316 label.setText(value);
317 label.setBackground(AnnotationSetsView.getColor(stackRow.getSet(),ann.getType()));
318 label.setBorder(BorderFactory.createLineBorder(Color.BLACK, 1));
319 label.setOpaque(true);
320 if(feature.equals("")) {
321 label.addMouseListener(annotationMouseListener.createListener(
322 stackRow.getSet(), type, String.valueOf(ann.getId())));
323 // show the feature values in the tooltip
324 String width = (Strings.toString(ann.getFeatures()).length() > 100) ?
325 "500" : "100%";
326 String toolTip = "<html><table width=\"" + width
327 + "\" border=\"0\" cellspacing=\"0\" cellpadding=\"4\">";
328 Color color = (Color) UIManager.get("ToolTip.background");
329 float[] hsb = Color.RGBtoHSB(
330 color.getRed(), color.getGreen(), color.getBlue(), null);
331 color = Color.getHSBColor(hsb[0], hsb[1],
332 Math.max(0f, hsb[2] - hsb[2]*0.075f)); // darken the color
333 String hexColor = Integer.toHexString(color.getRed()) +
334 Integer.toHexString(color.getGreen()) +
335 Integer.toHexString(color.getBlue());
336 boolean odd = false; // alternate background color every other row
337 for(Map.Entry<Object, Object> map : ann.getFeatures().entrySet()) {
338 toolTip +="<tr align=\"left\""
339 + (odd?" bgcolor=\"#"+hexColor+"\"":"")+"><td><strong>"
340 + map.getKey() + "</strong></td><td>"
341 + ((Strings.toString(map.getValue()).length() > 500) ?
342 "<textarea rows=\"20\" cols=\"40\" cellspacing=\"0\">"
343 + (Strings.toString(map.getValue())).replaceAll("(.{50,60})\\b", "$1\n")
344 + "</textarea>" :
345 Strings.toString(map.getValue()).replaceAll("\n", "<br>"))
346 + "</td></tr>";
347 odd = !odd;
348 }
349 label.setToolTipText(toolTip + "</table></html>");
350
351 } else {
352 label.addMouseListener(annotationMouseListener.createListener(
353 stackRow.getSet(), type, feature, Strings.toString(
354 ann.getFeatures().get(feature)), String.valueOf(ann.getId())));
355 }
356 // find the first empty row span for this annotation
357 int oldGridy = gbc.gridy;
358 for(int y = oldGridy; y <= (gridyMax + 1); y++) {
359 // for each cell of this row where spans the annotation
360 boolean xSpanIsEmpty = true;
361 for(int x = gbc.gridx;
362 (x < (gbc.gridx + gbc.gridwidth)) && xSpanIsEmpty; x++) {
363 xSpanIsEmpty = !(gridSet.containsKey(x)
364 && gridSet.get(x).contains(y));
365 }
366 if(xSpanIsEmpty) {
367 gbc.gridy = y;
368 break;
369 }
370 }
371 // save the column x and row y of the current value
372 TreeSet<Integer> ts;
373 for(int x = gbc.gridx; x < (gbc.gridx + gbc.gridwidth); x++) {
374 ts = gridSet.get(x);
375 if(ts == null) {
376 ts = new TreeSet<Integer>();
377 }
378 ts.add(gbc.gridy);
379 gridSet.put(x, ts);
380 }
381 add(label, gbc);
382 gridyMax = Math.max(gridyMax, gbc.gridy);
383 gbc.gridy = oldGridy;
384 }
385
386 // add a button at the end of the row
387 gbc.gridwidth = 1;
388 if (stackRow.getLastColumnButton() != null) {
389 // last cell of the row
390 gbc.gridx = Math.min(text.length(), maxTextLength) + 1;
391 gbc.insets = new Insets(0, 10, 3, 0);
392 gbc.fill = GridBagConstraints.NONE;
393 gbc.anchor = GridBagConstraints.WEST;
394 add(stackRow.getLastColumnButton(), gbc);
395 gbc.insets = new Insets(0, 0, 3, 0);
396 gbc.fill = GridBagConstraints.BOTH;
397 gbc.anchor = GridBagConstraints.CENTER;
398 }
399
400 // set the new gridy to the maximum row we put a value
401 gbc.gridy = gridyMax;
402 }
403
404 if (lastRowButton != null) {
405 // add a configuration button on the last row
406 gbc.insets = new java.awt.Insets(0, 10, 0, 10);
407 gbc.gridx = 0;
408 gbc.gridy++;
409 add(lastRowButton, gbc);
410 }
411
412 // add an empty cell that takes all remaining space to
413 // align the visible cells at the top-left corner
414 gbc.gridy++;
415 gbc.gridx = Math.min(text.length(), maxTextLength) + 1;
416 gbc.gridwidth = GridBagConstraints.REMAINDER;
417 gbc.gridheight = GridBagConstraints.REMAINDER;
418 gbc.weightx = 1;
419 gbc.weighty = 1;
420 add(new JLabel(""), gbc);
421
422 validate();
423 updateUI();
424 }
425
426 /**
427 * Extension of a MouseInputAdapter that adds a method
428 * to create new Listeners from it.<br>
429 * You must overriden the createListener method.
430 */
431 public static class StackMouseListener extends MouseInputAdapter {
432 /**
433 * There is 3 cases for the parameters of createListener:
434 * <ol>
435 * <li>first line of text -> createListener(word)
436 * <li>first column, header -> createListener(type),
437 * createListener(type, feature)
438 * <li>annotation -> createListener(set, type, annotationId),
439 * createListener(set, type, feature, value, annotationId)
440 * </ol>
441 * @param parameters see above
442 * @return a MouseInputAdapter for the text, header or annotation
443 */
444 public MouseInputAdapter createListener(String... parameters) {
445 return null;
446 }
447 }
448
449 /**
450 * Annotation that doesn't belong to an annotation set
451 * and with id always equal to -1.<br>
452 * Allows to create an annotation without document, nodes, annotation set,
453 * and keep compatibility with gate.Annotation.
454 * <br>
455 * This class is only for AnnotationStack internal use
456 * as it won't work with most of the methods that use gate.Annotation.
457 */
458 private static class StackAnnotation extends gate.annotation.AnnotationImpl {
459 StackAnnotation(Integer id, Node start, Node end, String type,
460 FeatureMap features) {
461 super(id, start, end, type, features);
462 }
463 static StackAnnotation createAnnotation(int startOffset,
464 int endOffset, String type, FeatureMap features) {
465 Node startNode = new NodeImpl(-1, (long) startOffset);
466 Node endNode = new NodeImpl(-1, (long) endOffset);
467 return new StackAnnotation(-1, startNode, endNode, type, features);
468 }
469 static StackAnnotation createAnnotation(gate.Annotation annotation) {
470 return new StackAnnotation(annotation.getId(), annotation.getStartNode(),
471 annotation.getEndNode(), annotation.getType(), annotation.getFeatures());
472 }
473 }
474
475 /**
476 * A row of annotations in the stack.
477 */
478 class StackRow {
479 StackRow(String set, String type, String feature,
480 JButton lastColumnButton, String shortcut, int crop) {
481 this.set = set;
482 this.type = type;
483 this.feature = feature;
484 this.annotations = new HashSet<StackAnnotation>();
485 this.lastColumnButton = lastColumnButton;
486 this.shortcut = shortcut;
487 this.crop = crop;
488 }
489
490 public String getSet() {
491 return set;
492 }
493 public String getType() {
494 return type;
495 }
496 public String getFeature() {
497 return feature;
498 }
499 public Set<StackAnnotation> getAnnotations() {
500 return annotations;
501 }
502 public JButton getLastColumnButton() {
503 return lastColumnButton;
504 }
505 public String getShortcut() {
506 return shortcut;
507 }
508 public int getCrop() {
509 return crop;
510 }
511 public void addAnnotation(StackAnnotation annotation) {
512 annotations.add(annotation);
513 }
514
515 String set;
516 String type;
517 String feature;
518 Set<StackAnnotation> annotations;
519 JButton lastColumnButton;
520 String shortcut;
521 int crop;
522 }
523
524 public void setLastRowButton(JButton lastRowButton) {
525 this.lastRowButton = lastRowButton;
526 }
527
528 /** @param text first line of text that contains the expression
529 * and its context */
530 public void setText(String text) {
531 this.text = text;
532 }
533
534 /** @param expressionStartOffset document offset where starts the expression */
535 public void setExpressionStartOffset(int expressionStartOffset) {
536 this.expressionStartOffset = expressionStartOffset;
537 }
538
539 /** @param expressionEndOffset document offset where ends the expression */
540 public void setExpressionEndOffset(int expressionEndOffset) {
541 this.expressionEndOffset = expressionEndOffset;
542 }
543
544 /** @param contextBeforeSize number of characters before the expression */
545 public void setContextBeforeSize(int contextBeforeSize) {
546 this.contextBeforeSize = contextBeforeSize;
547 }
548
549 /** @param contextAfterSize number of characters after the expression */
550 public void setContextAfterSize(int contextAfterSize) {
551 this.contextAfterSize = contextAfterSize;
552 }
553
554 /** @param expressionTooltip optional tooltip for the expression */
555 public void setExpressionTooltip(String expressionTooltip) {
556 this.expressionTooltip = expressionTooltip;
557 }
558
559 /** @param textMouseListener optional listener for the first line of text */
560 public void setTextMouseListener(StackMouseListener textMouseListener) {
561 this.textMouseListener = textMouseListener;
562 }
563
564 /** @param headerMouseListener optional listener for the first column */
565 public void setHeaderMouseListener(StackMouseListener headerMouseListener) {
566 this.headerMouseListener = headerMouseListener;
567 }
568
569 /** @param annotationMouseListener optional listener for the annotations */
570 public void setAnnotationMouseListener(StackMouseListener annotationMouseListener) {
571 this.annotationMouseListener = annotationMouseListener;
572 }
573
574 /** rows of annotations that are displayed in the stack*/
575 ArrayList<StackRow> stackRows;
576 /** maximum number of characters for the text,
577 * if too long an ellipsis is added in the middle */
578 int maxTextLength = 150;
579 /** maximum number of characters for a feature value */
580 int maxFeatureValueLength = 30;
581 JButton lastRowButton;
582 String text = "";
583 int expressionStartOffset = 0;
584 int expressionEndOffset = 0;
585 /** number of characters before the expression */
586 int contextBeforeSize = 10;
587 /** number of characters after the expression */
588 int contextAfterSize = 10;
589 String expressionTooltip = "";
590 StackMouseListener textMouseListener;
591 StackMouseListener headerMouseListener;
592 StackMouseListener annotationMouseListener;
593 public final static int CROP_START = 0;
594 public final static int CROP_MIDDLE = 1;
595 public final static int CROP_END = 2;
596 }
|