package timeflow.vis.timeline;
|
|
import timeflow.data.db.*;
|
import timeflow.data.db.filter.*;
|
import timeflow.data.time.*;
|
import timeflow.model.*;
|
import timeflow.vis.*;
|
|
import java.util.*;
|
import java.awt.*;
|
|
/*
|
* A VisualEncoding takes the info about which fields to translate to
|
* which visual aspects, and applies that to particular Acts.
|
*/
|
public class TimelineVisuals
|
{
|
|
private Map<String, TimelineTrack> trackTable = new HashMap<String, TimelineTrack>();
|
ArrayList<TimelineTrack> trackList = new ArrayList<TimelineTrack>();
|
private TimeScale timeScale = new TimeScale();
|
private Rectangle bounds = new Rectangle();
|
private boolean frameChanged;
|
private int numShown = 0;
|
private Interval globalInterval;
|
|
public double scale = 1;
|
|
public enum Layout
|
{
|
|
TIGHT, LOOSE, GRAPH
|
};
|
private Layout layoutStyle = Layout.TIGHT;
|
private VisualEncoder encoder;
|
private TFModel model;
|
private int fullHeight;
|
|
public int getFullHeight()
|
{
|
return fullHeight;
|
}
|
|
public TimelineVisuals(TFModel model)
|
{
|
this.model = model;
|
encoder = new VisualEncoder(model);
|
}
|
|
public TimeScale getTimeScale()
|
{
|
return timeScale;
|
}
|
|
public Rectangle getBounds()
|
{
|
return bounds;
|
}
|
|
public void setBounds(int x, int y, int w, int h)
|
{
|
bounds.setBounds(x, y, w, h);
|
timeScale.setLow(x);
|
timeScale.setHigh(x + w);
|
frameChanged = true;
|
}
|
|
public Layout getLayoutStyle()
|
{
|
return layoutStyle;
|
}
|
|
public void setLayoutStyle(Layout style)
|
{
|
layoutStyle = style;
|
layout();
|
}
|
|
public Interval getFitToVisibleRange()
|
{
|
ActList acts = model.getActs();
|
|
// add a little bit to the right so we can see labels...
|
ActDB db = getModel().getDB();
|
Field endField = db.getField(VirtualField.END);
|
Interval i = null;
|
if (endField == null)
|
{
|
i = DBUtils.range(acts, VirtualField.START);
|
} else
|
{
|
i = DBUtils.range(acts, new Field[]
|
{
|
db.getField(VirtualField.START), endField
|
});
|
}
|
if (i.length() == 0)
|
{
|
i.expand(globalInterval.length() / 20);
|
}
|
i = i.subinterval(-.05, 1.1);
|
i.intersection(globalInterval);
|
return i;
|
}
|
|
public void fitToVisible()
|
{
|
Interval i = getFitToVisibleRange();
|
setTimeBounds(i.start, i.end);
|
}
|
|
public void zoomOut()
|
{
|
setTimeBounds(globalInterval.start, globalInterval.end);
|
}
|
|
public void setTimeBounds(long first, long last)
|
{
|
timeScale.setDateRange(first, last);
|
frameChanged = true;
|
model.setViewInterval(new Interval(first, last));
|
}
|
|
public Interval getGlobalInterval()
|
{
|
if (globalInterval == null && model != null && model.getDB() != null)
|
{
|
createGlobalInterval();
|
}
|
return globalInterval;
|
}
|
|
public void createGlobalInterval()
|
{
|
globalInterval = DBUtils.range(model.getDB().all(), VirtualField.START).subinterval(-.05, 1.1);
|
}
|
|
public Interval getViewInterval()
|
{
|
return timeScale.getInterval();
|
}
|
|
public java.util.List<VisualAct> getVisualActs()
|
{
|
return encoder.getVisualActs();
|
}
|
|
public void layoutIfChanged()
|
{
|
if (frameChanged)
|
{
|
layout();
|
}
|
}
|
|
public void init(boolean majorChange)
|
{
|
note(new TFEvent(majorChange ? TFEvent.Type.DATABASE_CHANGE : TFEvent.Type.ACT_CHANGE, null));
|
}
|
|
public void note(TFEvent e)
|
{
|
ActList all = null;
|
if (e.type == TFEvent.Type.DATABASE_CHANGE)
|
{
|
all = model.getDB().all();
|
createGlobalInterval();
|
Interval i = guessInitialViewInterval(all, globalInterval);
|
setTimeBounds(i.start, i.end);
|
}
|
if (e.affectsRowSet())
|
{
|
all = model.getDB().all();
|
encoder.createVisualActs();
|
createGlobalInterval();
|
} else
|
{
|
encoder.createVisualActs();
|
}
|
Interval v = model.getViewInterval();
|
if (v != null && v.start != timeScale.getInterval().start)
|
{
|
timeScale.getInterval().translateTo(v.start);
|
}
|
updateVisuals();
|
}
|
|
private Interval guessInitialViewInterval(ActList acts, Interval fullRange)
|
{
|
if (acts.size() < 50)
|
{
|
return fullRange.copy();
|
}
|
|
Interval best = null;
|
int most = -1;
|
double d = Math.max(.1, 50. / acts.size());
|
d = Math.min(1. / 3, d);
|
for (double x = 0; x < 1 - d; x += d / 4)
|
{
|
Interval i = fullRange.subinterval(x, x + d);
|
TimeIntervalFilter f = new TimeIntervalFilter(i, getModel().getDB().getField(VirtualField.START));
|
int num = 0;
|
for (Act a : acts)
|
{
|
if (f.accept(a))
|
{
|
num++;
|
}
|
}
|
if (num > most)
|
{
|
most = num;
|
best = i;
|
}
|
}
|
return best;
|
}
|
|
public void updateVisuals()
|
{
|
scale = 1;
|
updateVisualEncoding();
|
layout();
|
}
|
|
public TFModel getModel()
|
{
|
return model;
|
}
|
|
public int getNumTracks()
|
{
|
return trackList.size();
|
}
|
|
public double layout()
|
{
|
ActList acts = model.getActs();
|
if (acts == null)
|
{
|
return scale;
|
}
|
|
double labelHeight = 30;
|
|
double min = bounds.height == 0 ? 0 : labelHeight / bounds.height;
|
double cellH = (double)getModel().getDisplay().getInt("timeline.item.height.min") / bounds.height;
|
|
double maxCount = 0;
|
|
// Set the minimum scale
|
for (TimelineTrack t : trackList)
|
{
|
//double height = t.size() * scale / (double) numShown;
|
|
maxCount = Math.max(t.size(), maxCount);
|
}
|
|
scale = Math.min(cellH * numShown, scale);
|
scale = Math.max(min * numShown / maxCount, scale);
|
|
double top = 0;
|
for (TimelineTrack t : trackList)
|
{
|
double height = Math.max(min, t.size() * scale / (double) numShown);
|
t.layout(top, height, this);
|
top += height;
|
}
|
fullHeight = (int) (top * bounds.height);
|
|
Collections.sort(trackList);
|
frameChanged = false;
|
|
return scale;
|
}
|
|
private void updateVisualEncoding()
|
{
|
java.util.List<VisualAct> acts = encoder.apply();
|
|
// now arrange on tracks
|
trackTable = new HashMap<String, TimelineTrack>();
|
trackList = new ArrayList<TimelineTrack>();
|
numShown = 0;
|
for (VisualAct v : acts)
|
{
|
if (!v.isVisible())
|
{
|
continue;
|
}
|
numShown++;
|
String s = v.getTrackString();
|
TimelineTrack t = trackTable.get(s);
|
if (t == null)
|
{
|
t = new TimelineTrack(s);
|
trackTable.put(s, t);
|
trackList.add(t);
|
}
|
t.add(v);
|
v.setTrack(t);
|
}
|
|
/*
|
// the following code is no longer used, but could come in handy again one day...
|
|
// If there is more than one "small" track, then we will coalesce them into
|
// one bigger "miscellaneous" track.
|
int minSize=numShown/30;//Math.max(3,numShown/30);
|
ArrayList<TimelineTrack> small=new ArrayList<TimelineTrack>();
|
for (TimelineTrack t: trackList)
|
{
|
if (t.size()<minSize)
|
small.add(t);
|
}
|
if (small.size()>1)
|
{
|
// create a new Track for "miscellaneous."
|
TimelineTrack misc=new TimelineTrack(Display.MISC_CODE);
|
trackList.add(misc);
|
trackTable.put(misc.label, misc);
|
|
// remove the old tracks.
|
for (TimelineTrack t:small)
|
{
|
trackList.remove(t);
|
trackTable.remove(t.label);
|
for (VisualAct v: t.visualActs)
|
{
|
v.setTrack(misc);
|
misc.add(v);
|
}
|
}
|
// sort miscellaneous items in time order.
|
//Collections.sort(misc.visualActs);
|
}
|
*/
|
|
for (TimelineTrack t : trackList)
|
{
|
Collections.sort(t.visualActs);
|
}
|
}
|
}
|