1 /*
2 * ParallelDisplay.java
3 *
4 * Created on 18. November 2001, 19:49
5 *
6 * Copyright 2001 Flo Ledermann flo@subnet.at
7 *
8 * Licensed under GNU General Public License (GPL).
9 * See http://www.gnu.org/copyleft/gpl.html
10 */
11
12 package edu.psu.geovista.app.parvis.gui;
13
14 import edu.psu.geovista.app.parvis.model.*;
15
16 import edu.psu.geovista.ui.event.*;
17 import edu.psu.geovista.ui.*;
18
19 import java.awt.*;
20 import java.awt.event.*;
21 import java.awt.geom.Rectangle2D;
22
23 import javax.swing.*;
24 import javax.swing.event.*;
25
26 import java.util.*;
27
28
29 /***
30 * The swing GUI Component for displaying a parallel coordinate visualisation.
31 * Note that the actual rendering is done by the UI delegate, ParallelDisplayUI
32 * (with its single subclass BasiParallelDisplayUI). This class is used to store
33 * the state of the component and interact with the environment.
34 *
35 * @author Flo Ledermann flo@subnet.at
36 * @version 0.1
37 */
38 public class ParallelDisplay extends JComponent implements ChangeListener, IndicationListener,
39 SelectionListener,
40 ExcentricLabelClient{
41
42
43 private transient ExcentricLabels exLabels;
44 /*** Scale values for the axes.*/
45 private float axisScale[] = null;
46 /*** Offset values for the axes.*/
47 private float axisOffset[] = null;
48
49 /*** axis -> dimension linking. */
50 protected int axisOrder[] = null;
51
52 /*** brushed values of records */
53 protected float brushValues[] = null;
54
55 protected Color recordColor = Color.black;
56 protected Color brushedColor = Color.blue;
57
58 /*** Our model. */
59 private ParallelSpaceModel model;
60
61 /*** The mode of interaction we are in.*/
62 protected int editMode = 0;
63
64 public static final int REORDER = 0;
65 public static final int SCALE = 1;
66 public static final int TRANSLATE = 2;
67 /*** used to invert axis orientation */
68 public static final int INVERT = 3;
69 public static final int BRUSH = 4;
70
71 /*** Whether we have to redraw the whole background. This is usually only
72 needed if the model changes. */
73 public boolean deepRepaint = true;
74
75 static {
76 UIManager.put("edu.psu.geovista.app.parvis.gui.ParallelDisplayUI", "edu.psu.geovista.app.parvis.gui.BasicParallelDisplayUI");
77 }
78 /***
79 * Creates a new ParallelDisplay.
80 */
81 public ParallelDisplay() {
82 init(null);
83 }
84
85 /***
86 * Creates a new ParallelDisplay with the given model.
87 *
88 * @param model The model to display.
89 */
90 public ParallelDisplay(ParallelSpaceModel model){
91 init(model);
92 }
93
94 /***
95 * Initializes the component with the given model.
96 *
97 * @param model The model to use.
98 */
99 protected void init(ParallelSpaceModel model){
100
101 //System.out.println("Initializing ParallelDisplay Component");
102 setModel(model);
103
104 setMinimumSize(new Dimension(100, 100));
105 setPreferredSize(new Dimension(700,400));
106
107 setBackground(Color.white);
108 setDoubleBuffered(false);
109 setOpaque(true);
110
111 setDefaultPreferences();
112
113 updateUI();
114 }
115
116 /***
117 * Returns the dimension associated with the given axis. Note that the assignment
118 * axisNumber (in the display) => dimensionNumber (in the model) is only the default
119 * setting, the axes might be reordered, or some dimensions may even be left out
120 * or displayed twice. So be sure to always call this function before querying the model
121 * to "convert" the axis number to the right deímension value.
122 *
123 * @param order The number of the axis to get the dimension number for.
124 *
125 * @return The number of the dimnesion to display on the given axis.
126 */
127 protected int getAxis(int order){
128 return axisOrder[order];
129 }
130
131 /*** Returns the number of axes to display. Note that this is not necessarily
132 * equal to the number of dimensions in the model (see above: getAxis()).
133 *
134 * @return The number of axes to display.
135 */
136 public int getNumAxes(){
137 if (axisOrder != null)
138 return axisOrder.length;
139 else
140 return 0;
141 }
142
143 /***
144 * Swaps two axes. This means the dimensions assigned to the two axes are swapped.
145 *
146 * @param axis1 The first axis.
147 * @param axis2 The second axis.
148 */
149 public void swapAxes(int axis1, int axis2){
150 int temp = axisOrder[axis1];
151
152 axisOrder[axis1] = axisOrder[axis2];
153 axisOrder[axis2] = temp;
154 }
155
156 /***
157 * Sets up the axis -> dimension assignment. See above: getAxis();
158 *
159 * @param order An array containing the int ids of the dimensions to be displayed
160 * on the axes.
161 */
162 public void setAxisOrder(int[] order){
163 axisOrder = order;
164 }
165
166 /***
167 * Sets the model to display.
168 *
169 * @param model The model to display.
170 */
171 public void setModel(ParallelSpaceModel model){
172 if (this.model != null) {
173 this.model.removeChangeListener(this);
174
175 axisOffset = null;
176 axisScale = null;
177
178 brushValues = null;
179 }
180
181 this.model = model;
182
183 if (model != null){
184 model.addChangeListener(this);
185
186
187 axisOffset = new float[model.getNumDimensions()];
188 axisScale = new float[model.getNumDimensions()];
189 axisOrder = new int[model.getNumDimensions()];
190
191 brushValues = new float[model.getNumRecords()];
192
193 for (int i=0; i<model.getNumDimensions(); i++){
194 // initialize scaling of axis to show maximum detail
195 axisOffset[i] = model.getMaxValue(i);
196 axisScale[i] = model.getMinValue(i) - axisOffset[i];
197 axisOrder[i] = i;
198 }
199 }
200
201 brushCount = 0;
202 deepRepaint = true;
203 repaint();
204
205 }
206
207 /***
208 * Sets the mode for user interaction with the display.
209 *
210 * @param mode The interaction mode to use.
211 */
212 public void setEditMode(int mode){
213 editMode = mode;
214
215 resetCursor();
216 }
217
218 void resetCursor(){
219 switch (editMode){
220 case BRUSH:
221 setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
222 break;
223 default:
224 setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
225 }
226 }
227 /***
228 * Returns the currently active interaction mode.
229 *
230 * @return The currently active interaction mode.
231 */
232 public int getEditMode(){
233 return editMode;
234 }
235
236 /***
237 * Returns the model.
238 * Actually the model should be hidden, because of the confusion that might
239 * occur by mistaking axes and dimensions (see above: getAxis()). This requires
240 * a rewrite of some parts of the code, so this is marked to do!
241 *
242 * @return The model that is currently displayed ba the component.
243 */
244 protected ParallelSpaceModel getModel(){
245 return model;
246 }
247
248 /***
249 * Returns the number of Records in the model.
250 */
251 public int getNumRecords(){
252 if (model != null)
253 return model.getNumRecords();
254 else
255 return 0;
256 }
257
258 public String getRecordLabel(int num){
259 if (model != null)
260 return model.getRecordLabel(num);
261 else
262 return null;
263 }
264
265 public float getValue(int recordNum, int axisNum){
266 if (model != null) {
267 return model.getValue(recordNum, axisOrder[axisNum]);
268 }
269 else {
270 return 0;
271 }
272 }
273
274 private int brushCount = 0;
275 public int[] findSelection(){
276 int numSelected = 0;
277 for (int i = 0; i < brushValues.length; i++) {
278 if (brushValues[i] > 0f) {
279 numSelected++;
280 }
281 }
282 int[] selection = new int[numSelected];
283 int counter = 0;
284 for (int i = 0; i < brushValues.length; i++) {
285 if (brushValues[i] > 0f) {
286 selection[counter] = i;
287 counter++;
288 }
289 }
290 return selection;
291 }
292 public float getBrushValue(int num){
293 if (brushValues != null)
294 return brushValues[num];
295 else
296 return 0.0f;
297 }
298
299 public void setBrushValue(int num, float val){
300 if (brushValues != null)
301 brushValues[num] = val;
302 if (val > 0.0f) brushCount = 1;
303 }
304
305 public int getBrushedCount(){
306 return brushCount;
307 }
308 public int[] getRecordsByValueRange(int axisnum, float min, float max){
309
310 int ids[] = new int[getNumRecords()];
311 int count = 0;
312
313 for (int i=0; i<getNumRecords(); i++){
314 float val = getValue(i, axisnum);
315 if ((val >= min) && (val <= max))
316 ids[count++] = i;
317 }
318
319 if (count > 0) {
320 int newids[] = new int[count];
321 System.arraycopy(ids,0,newids,0,count);
322 return newids;
323 }
324 else return new int[0];
325 }
326
327 public Color getRecordColor(){
328 return recordColor;
329 }
330
331 public Color getBrushedColor(){
332 return brushedColor;
333 }
334
335 /***
336 * Sets the user interface delegate for the component.
337 */
338 public void setUI(ParallelDisplayUI ui){
339 super.setUI(ui);
340 }
341
342 public ParallelDisplayUI getUI(){
343 ParallelDisplayUI pUI = (ParallelDisplayUI)this.ui;
344 return pUI;
345 }
346
347 /***
348 * Invalidates the component and causes a complete repaint.
349 */
350 public void invalidate(){
351 super.invalidate();
352 deepRepaint = true;
353 }
354
355 /***
356 * Swing method.
357 */
358 public void updateUI(){
359 try {
360 setUI((ParallelDisplayUI)UIManager.getUI(this));
361 }
362 catch (ClassCastException ccex){
363 }
364 invalidate();
365 }
366
367 /***
368 * Swing method.
369 */
370 public String getUIClassID(){
371 ////System.out.println("retrieving classID");
372 return "edu.psu.geovista.app.parvis.gui.ParallelDisplayUI";
373 }
374
375 /***
376 * Invoked when the model has changed its state.
377 *
378 * @param e A ChangeEvent object
379 */
380 public void stateChanged(ChangeEvent e) {
381 repaint();
382 }
383
384 /***
385 * Added by Frank Hardisty 19 July 2002
386
387 */
388 public void indicationChanged(IndicationEvent e) {
389 int indication = e.getIndication();
390 this.indication = indication;
391 //model.
392 //BasicParallelDisplayUI bui = (BasicParallelDisplayUI)UIManager.getUI(this);
393 //can't use previous line b/c this makes a new UI when called
394 //bui.setHoverRecord(indication,this);
395 repaint();
396
397 }
398 public int indication = -1;
399
400 public void selectionChanged(SelectionEvent e){
401 this.getUI().createBrushImage(this);
402
403 //first zero out old one
404 for (int i = 0; i < brushValues.length; i++) {
405 this.setBrushValue(i,0f);
406 }
407
408 int[] selection = e.getSelection();
409 if (selection.length == 0) {
410 this.brushCount = 0;
411 this.deepRepaint = true;
412 repaint();
413 return;
414 }
415 //then put new one in
416 for (int i = 0; i < selection.length; i++) {
417 int selVal = selection[i];
418 this.setBrushValue(selVal,1f);
419 }
420 this.getUI().renderBrush();
421
422 repaint();
423 }
424
425 /***
426 * Returns the current offset (translation in axis units) for the axis.
427 *
428 * @param num The axis number.
429 *
430 * @return The offset value.
431 **/
432 public float getAxisOffset(int num){
433 if (axisOffset != null){
434 return axisOffset[axisOrder[num]];
435 }
436 else return 0;
437 }
438
439 /***
440 * Returns the current scale (visible region in axis units) for the axis.
441 *
442 * @param num The axis number.
443 *
444 * @return The scale value.
445 **/
446 public float getAxisScale(int num){
447 if (axisScale != null){
448 return axisScale[axisOrder[num]];
449 }
450 else return 0;
451 }
452
453 /***
454 * Returns a String label for a specific axis.
455 *
456 * @param num The axis number.
457 *
458 * @return A Human-readable label for the axis.
459 */
460
461 public String getAxisLabel(int num){
462 if (model != null){
463 String label = model.getAxisLabel(axisOrder[num]);
464 if (label != null) return label;
465 else return ("X" + axisOrder[num]);
466 }
467 else {
468 return null;
469 }
470 }
471
472 /***
473 * Sets the offset (translation in axis units) for the axis.
474 *
475 * @param axis The axis number.
476 * @param offset The offset value.
477 **/
478 public void setAxisOffset(int axis, float offset){
479 if (axisOffset != null && axisOffset.length > axisOrder[axis]){
480 axisOffset[axisOrder[axis]] = offset;
481 }
482
483 repaint();
484 }
485
486 /***
487 * Sets the scale (visible region in axis units) for the axis.
488 *
489 * @param axis The axis number.
490 * @param scale The scale value.
491 **/
492 public void setAxisScale(int axis, float scale){
493 if (axisScale != null && axisScale.length > axisOrder[axis]){
494 axisScale[axisOrder[axis]] = scale;
495 }
496
497 repaint();
498 }
499
500 /***
501 * Configures (scales, translates) all axes to show all values between its
502 * minimum and its maximum on a maximum scale.
503 */
504 public void minMaxScale(){
505 for (int i=0; i<getNumAxes(); i++){
506 // initialize scaling of axis to show maximum detail
507 axisOffset[i] = model.getMaxValue(i);
508 axisScale[i] = model.getMinValue(i) - axisOffset[i];
509 }
510
511 deepRepaint = true;
512 repaint();
513 }
514
515 /***
516 * Configures (scales, translates) all axes to show all values between the
517 * minimum and its maximum of the given variable.
518 */
519 public void varMinMaxScale(int var){
520 float min = model.getMinValue(var);
521 float max = model.getMaxValue(var);
522 float range = max - min;
523 //float range = model.getMinValue(var) - axisOffset[var];
524 for (int i=0; i<getNumAxes(); i++){
525 //here we have a problem if the scales are too wildly different
526 //specifically, if thisScale is big and range is small
527 //in such a case we just move everything off the screen range
528
529 float thisMax = model.getMaxValue(i);
530 float thisMin = model.getMinValue(i);
531 float scaleFactor = this.getHeight()/range;
532 float endMax = (thisMax - min) * scaleFactor;
533 float endMin = (thisMin - min) * scaleFactor;
534
535 //System.out.println(i);
536 //System.out.println(endMin);
537 //System.out.println(endMax);
538 endMin = Math.abs(endMin);
539 endMax = Math.abs(endMax);
540 //endMax = Math.abs(endMax);
541 if (endMin > 300000 || endMax > 300000){
542 //System.out.println("fudge!");
543 //System.out.println();
544 if (max > 0) {
545 axisOffset[i] = Math.abs(thisMax) * 2f;
546 } else {
547 axisOffset[i] = Math.abs(thisMax) * -2f;
548 }
549
550 axisScale[i] = thisMax - thisMin;
551 } else {
552 axisOffset[i] = max;
553 axisScale[i] = min - max;
554 }
555 }
556
557 deepRepaint = true;
558 repaint();
559 }
560 private float findScreenY(float value, int axis, float scale){
561 float newValue = value;
562 BasicParallelDisplayUI comp = (BasicParallelDisplayUI)this.getUI();
563
564 value -= this.getAxisOffset(axis);;
565 value *= (this.getHeight() - 2 * comp.borderV) / scale;
566
567 return newValue;
568 }
569 /***
570 * Configures (scales, translates) all axes to show all values between zero
571 * and its maximum on a maximum scale.
572 */
573 public void zeroMaxScale(){
574 for (int i=0; i<getNumAxes(); i++){
575 // initialize scaling of axis to show maximum detail
576 axisOffset[i] = model.getMaxValue(i);
577 axisScale[i] = -1 * axisOffset[i];
578 }
579
580 deepRepaint = true;
581 repaint();
582 }
583
584 /***
585 * Configures (scales, translates) all axes to show values between zero
586 * (or the nagative minimum of all axes) and the maximum value of all axes
587 * on a maximum scale.
588 */
589 public void minMaxAbsScale(){
590 int i;
591
592 float absmax = Float.NEGATIVE_INFINITY;
593 float absmin = 0.0f;
594
595 for (i=0; i<getNumAxes(); i++){
596 // initialize scaling of axis to show maximum detail
597 float val = model.getMaxValue(i);
598 if (val > absmax) absmax = val;
599 val = model.getMinValue(i);
600 if (val < absmin) absmin = val;
601 }
602
603 for (i=0; i<getNumAxes(); i++){
604 axisOffset[i] = absmax;
605 axisScale[i] = absmin - absmax;
606 }
607
608 deepRepaint = true;
609 repaint();
610 }
611
612 private Vector progressListeners = new Vector();
613
614 public void addProgressListener(ProgressListener l){
615 progressListeners.add(l);
616 }
617
618 public void removeProgressListener(ProgressListener l){
619 progressListeners.remove(l);
620 }
621
622 public void fireProgressEvent(ProgressEvent e){
623 Vector list = (Vector)progressListeners.clone();
624 for (int i=0; i<list.size(); i++){
625 ProgressListener l = (ProgressListener)list.elementAt(i);
626 l.processProgressEvent(e);
627 }
628 }
629
630 Hashtable preferences = new Hashtable();
631
632 public void setDefaultPreferences(){
633 preferences.put("brushRadius",new Float(0.2f));
634 preferences.put("softBrush",new Boolean(true));
635 preferences.put("hoverText",new Boolean(true));
636 preferences.put("hoverLine",new Boolean(true));
637 }
638
639 public void setFloatPreference(String key, float val){
640 Object obj = new Float(val);
641 preferences.put(key, obj);
642 }
643
644 public void setBoolPreference(String key, boolean val){
645 Object obj = new Boolean(val);
646 preferences.put(key, obj);
647 }
648
649 public boolean getBoolPreference(String key){
650 Object obj = preferences.get(key);
651 if ((obj != null) && (obj instanceof Boolean)){
652 return ((Boolean)obj).booleanValue();
653 }
654 // we should throw an exception here;
655 else return false;
656 }
657
658 public float getFloatPreference(String key){
659 Object obj = preferences.get(key);
660 if ((obj != null) && (obj instanceof Float)){
661 return ((Float)obj).floatValue();
662 }
663 // we should throw an exception here;
664 else return 0.0f;
665 }
666
667 //start excentric labeling stuff
668 private void initExcentricLabels(){
669 this.exLabels = new ExcentricLabels();
670 exLabels.setComponent(this);
671 exLabels.setOpaque(true);
672 Color halfWhite = new Color(255,255,255,123);
673 exLabels.setBackgroundColor(halfWhite);
674 this.addMouseListener(exLabels);
675 }
676 public String getObservationLabel(int i){
677 String label = model.getRecordLabel(i);
678 return label;
679 }
680
681 public Shape getShapeAt(int i){
682
683 Shape shp = new Rectangle2D.Float(0,0,0,0);
684 return shp;
685 }
686 public int[] pickAll(Rectangle2D hitBox){
687
688 int[] selObs = null;//ls.findSelection(hitBox);
689 return selObs;
690 }
691 //end excentric labeling stuff
692 /***
693 * adds an IndicationListener
694 */
695 public void addIndicationListener (IndicationListener l) {
696 listenerList.add(IndicationListener.class, l);
697 }
698 /***
699 * removes an IndicationListener from the component
700 */
701 public void removeIndicationListener (IndicationListener l) {
702 listenerList.remove(IndicationListener.class, l);
703 }
704 /***
705 * Notify all listeners that have registered interest for
706 * notification on this event type. The event instance
707 * is lazily created using the parameters passed into
708 * the fire method.
709 * @see EventListenerList
710 */
711 public void fireIndicationChanged (int newIndication) {
712
713 // Guaranteed to return a non-null array
714 Object[] listeners = listenerList.getListenerList();
715 IndicationEvent e = null;
716 // Process the listeners last to first, notifying
717 // those that are interested in this event
718 for (int i = listeners.length - 2; i >= 0; i -= 2) {
719 if (listeners[i] == IndicationListener.class) {
720 // Lazily create the event:
721 if (e == null) {
722 e = new IndicationEvent(this, newIndication);
723 }
724 ((IndicationListener)listeners[i + 1]).indicationChanged(e);
725 }
726 }//next i
727
728 }
729
730 /***
731 * adds an SelectionListener
732 */
733 public void addSelectionListener (SelectionListener l) {
734 listenerList.add(SelectionListener.class, l);
735 }
736 /***
737 * removes an SelectionListener from the component
738 */
739 public void removeSelectionListener (SelectionListener l) {
740 listenerList.remove(SelectionListener.class, l);
741 }
742 /***
743 * Notify all listeners that have registered interest for
744 * notification on this event type. The event instance
745 * is lazily created using the parameters passed into
746 * the fire method.
747 * @see EventListenerList
748 */
749 public void fireSelectionChanged () {
750
751 // Guaranteed to return a non-null array
752 Object[] listeners = listenerList.getListenerList();
753 SelectionEvent e = null;
754 // Process the listeners last to first, notifying
755 // those that are interested in this event
756 for (int i = listeners.length - 2; i >= 0; i -= 2) {
757 if (listeners[i] == SelectionListener.class) {
758 // Lazily create the event:
759 if (e == null) {
760 int[] newSelection = this.findSelection();
761 e = new SelectionEvent(this, newSelection);
762 }
763 ((SelectionListener)listeners[i + 1]).selectionChanged(e);
764 }
765 }//next i
766
767 }
768
769 }
This page was automatically generated by Maven