package timeflow.vis.calendar; import timeflow.data.time.*; import timeflow.model.Display; import timeflow.vis.*; import java.util.*; import java.awt.*; import java.text.*; public class Grid { TimeUnit rowUnit, columnUnit; RoughTime startRow, endRow; Interval interval; HashMap<Long, GridCell> cells; int[] screenGridX; int cellHeight=80, cellWidth, numCols, numRows; Rectangle bounds=new Rectangle(); int dy; static final DateFormat dayOfWeek=new SimpleDateFormat("EEE"); static final DateFormat month=new SimpleDateFormat("MMM d"); static final String[] day={"SUN", "MON", "TUES", "WED", "THURS", "FRI", "SAT"}; Grid(TimeUnit rowUnit, TimeUnit columnUnit, Interval interval) { this.rowUnit=rowUnit; this.columnUnit=columnUnit; numCols=columnUnit.numUnitsIn(rowUnit); setInterval(interval); } public void setDY(int dy) { this.dy=dy; } public int getCalendarHeight() { return bounds.height+bounds.y+20; } public RoughTime getTime(int x, int y) { y+=dy; if (!bounds.contains(x,y)) return null; // find grid coordinates. int gridX=(x-bounds.x)/cellWidth; int gridY=(y-bounds.y)/cellHeight; return startRow.plus(rowUnit, gridY).plus(columnUnit, gridX); } public double getScrollFraction() { double x= (getFirstDrawnTime().getTime()-startRow.getTime())/(double) (endRow.getTime()-startRow.getTime()); if (x<0) return 0; if (x>1) return 1; return x; } public RoughTime getFirstDrawnTime() { int gridY=(dy-bounds.y)/cellHeight; return startRow.plus(rowUnit, gridY); } private Point getGridCorner(long timestamp) { int diff=(int)columnUnit.difference(timestamp, startRow.getTime()); int gridX=diff%numCols; int gridY=diff/numCols; return new Point(gridX, gridY); } public Rectangle getCell(long timestamp) { Point p=getGridCorner(timestamp); return new Rectangle(bounds.x+p.x*cellWidth, bounds.y+p.y*cellHeight-dy, cellWidth, cellHeight); } void setInterval(Interval interval) { this.interval=interval; startRow=rowUnit.roundDown(interval.start); endRow=rowUnit.roundDown(interval.end); numRows=1+(int)(rowUnit.difference(endRow.getTime(), startRow.getTime())); // the next line fixes a problem with multi-century data sets. // it works, but there's probably a better way to do this :-) if (numRows>50 && rowUnit.getRoughSize()>=TimeUnit.YEAR.getRoughSize()) numRows++; } void makeCells(java.util.List<VisualAct> visualActs) { cells=new HashMap<Long, GridCell>(); for (VisualAct v: visualActs) { if (v.getStart()==null) continue; long timestamp=v.getStart().getTime(); RoughTime timeKey=columnUnit.roundDown(timestamp); GridCell cell=cells.get(timeKey.getTime()); if (cell==null) { cell=new GridCell(timeKey); cells.put(timeKey.getTime(), cell); Point p=getGridCorner(timestamp); cell.gridX=p.x; cell.gridY=p.y; } cell.visualActs.add(v); } } void render(Graphics2D g, Display display, Rectangle screenBounds, CalendarVisuals visuals, Collection<Mouseover> objectLocations) { g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); int left=110, right=20; int padY=50; boolean shouldLabel=visuals.drawStyle==CalendarVisuals.DrawStyle.LABEL; cellWidth=(screenBounds.width-left-right)/numCols; boolean fitTight=visuals.fitStyle==CalendarVisuals.FitStyle.TIGHT; int idealHeight= fitTight ? 12 : Display.CALENDAR_CELL_HEIGHT; cellHeight=Math.max(idealHeight, (screenBounds.height-padY-10)/numRows); this.bounds.setBounds(left, padY, numCols*cellWidth, numRows*cellHeight); g.setColor(new Color(240,240,240));//Color.white); g.fill(screenBounds); g.setColor(new Color(245,245,245)); g.drawRect(bounds.x, bounds.y-dy, bounds.width, bounds.height); g.setFont(display.bold()); // draw vertical grid lines. Color gridColor=new Color(220,220,220); for (int i=0; i<=numCols; i++) { int x=bounds.x+i*cellWidth; g.setColor(gridColor); g.drawLine(x,bounds.y-dy,x,bounds.y+bounds.height-dy); if (rowUnit==TimeUnit.WEEK && i<7) { g.setColor(Color.gray); g.drawString(day[i], x, bounds.y-dy-6); } } // horizontal grid lines. RoughTime labelTime=startRow.copy(); int lastLabelY=-100; int lastYear=-1000000; FontMetrics fm=display.boldFontMetrics(); int skipped=0; for (int i=0; i<numRows; i++) { int y=bounds.y+i*cellHeight; if (y-dy>-50) { if (skipped>0) { rowUnit.addTo(labelTime, skipped); skipped=0; } g.setColor(gridColor); g.drawLine(bounds.x,y-dy,bounds.x+bounds.width,y-dy); if (y-lastLabelY>30 || lastLabelY<0) { String label=null; if (rowUnit==TimeUnit.WEEK) { int year=TimeUtils.cal(labelTime.getTime()).get(Calendar.YEAR); if (year!=lastYear) label=labelTime.format(); else label=month.format(labelTime.toDate()); lastYear=year; } else label=labelTime.format(); g.setColor(Color.gray); g.drawString(label, bounds.x-fm.stringWidth(label)-15, y+15-dy); lastLabelY=y; } if (y-dy>screenBounds.height) break; // now draw, in gray, the labels for each of the boxes. if (!fitTight) { RoughTime gridLabel=labelTime.copy(); int labelH=13; for (int j=0; j<numCols; j++) { g.setColor(Color.gray); g.setFont(display.bold()); String label=columnUnit.format(gridLabel.toDate()); g.drawString(label, bounds.x+j*cellWidth+3, y-dy+labelH); columnUnit.addTo(gridLabel); } } rowUnit.addTo(labelTime); } else skipped++; } // draw a frame around the whole thing. g.setColor(Color.darkGray); g.drawRect(bounds.x, bounds.y-dy, bounds.width, bounds.height); // draw backgrounds for (GridCell cell: cells.values()) { // are any visible? boolean visible=false; for (VisualAct v: cell.visualActs) { if (v.isVisible()) { visible=true; break; } } int cx=bounds.x+cell.gridX*cellWidth; int cy=bounds.y+cell.gridY*cellHeight-dy; if (cy<screenBounds.y-50 || cy>screenBounds.y+screenBounds.height+50) continue; // label top of cell. int labelH=0; g.setColor(new Color(240,240,240)); if (visible) { g.setColor(Color.white); g.fillRect(cx+1,cy+1,cellWidth-1,cellHeight-1); } if (cellHeight>42) { labelH=13; g.setColor(Color.darkGray); g.setFont(display.bold()); String label=columnUnit.format(cell.time.toDate()); g.drawString(label, cx+3, cy+labelH); } } // draw items. int mx=10, my=shouldLabel ? 18 : 10; for (GridCell cell: cells.values()) { int cx=bounds.x+cell.gridX*cellWidth; int cy=bounds.y+cell.gridY*cellHeight-dy; if (cy<screenBounds.y-50 || cy>screenBounds.y+screenBounds.height+50) continue; // label top of cell. int labelH=cellHeight>42 ? 13 : 0; // now draw the items in the cell. // old, non-aggregation code: // START AGGREGATION CODE ArrayList<VisualAct> visibleActs=new ArrayList<VisualAct>(); for (VisualAct v: cell.visualActs) if (v.isVisible()) visibleActs.add(v); Iterator<VisualAct> vacts= VisualActFactory.makeEmFit(visuals.model, visibleActs, new Rectangle(cx, cy, cellWidth, cellHeight)).iterator(); // END AGGREGATION CODE int leftX=6; int cdx=leftX; int topDotY=Math.min(labelH+16,cellHeight/2); int cdy=topDotY; while (vacts.hasNext()) { VisualAct v=vacts.next(); if (!v.isVisible()) continue; // set x,y, room to right. int x=cx+cdx; int y=cy+cdy; int space=cellWidth-20; v.setX(x); v.setY(y); v.setSpaceToRight(space); Mouseover o=v.draw(g, new Rectangle(cx+1,cy+labelH+1,cellWidth-2, cellHeight-2-labelH), bounds, display, shouldLabel, false); if (o!=null) objectLocations.add(o); // go to next location. if we're labeling, we do this vertically. // otherwise, left-to-right, then top-to-bottom. if (shouldLabel) { cdy+=my; if (cdy>cellHeight-2-my) { g.drawString("...", x,y+my); break; } } else { cdx+=mx; if (cdx>cellWidth-mx/2-2 && vacts.hasNext()) { cdx=leftX; cdy+=my; if (cdy>cellHeight-my/2) { break; } } } } } } }