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 enum Layout {TIGHT, LOOSE, GRAPH}; private Layout layoutStyle=Layout.LOOSE; 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() { updateVisualEncoding(); layout(); } public TFModel getModel() { return model; } public int getNumTracks() { return trackList.size(); } public void layout() { ActList acts=model.getActs(); if (acts==null) return; double min= bounds.height==0 ? 0 : 30./bounds.height; double top=0; for (TimelineTrack t: trackList) { double height=Math.max(min, t.size()/(double)numShown); t.layout(top, height, this); top+=height; } fullHeight=(int)(top*bounds.height); Collections.sort(trackList); frameChanged=false; } 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); } } }