// ********************************************************************** // // <copyright> // // BBN Technologies, a Verizon Company // 10 Moulton Street // Cambridge, MA 02138 // (617) 873-8000 // // Copyright (C) BBNT Solutions LLC. All rights reserved. // // </copyright> package com.bbn.openmap.gui.time; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Graphics; import java.awt.GridLayout; import java.awt.Polygon; import java.awt.Rectangle; import java.awt.event.ActionListener; import java.awt.event.ComponentEvent; import java.awt.event.ComponentListener; import java.awt.event.MouseEvent; import java.awt.geom.Point2D; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.Timer; import java.util.TimerTask; import java.util.Vector; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.Box; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JPanel; import com.bbn.openmap.MapBean; import com.bbn.openmap.MapHandler; import com.bbn.openmap.event.CenterListener; import com.bbn.openmap.event.CenterSupport; import com.bbn.openmap.event.MapMouseListener; import com.bbn.openmap.event.OMEvent; import com.bbn.openmap.event.OMEventSelectionCoordinator; import com.bbn.openmap.gui.event.AbstractEventPresenter; import com.bbn.openmap.gui.event.EventPresenter; import com.bbn.openmap.gui.time.TimeSliderLayer.TimeDrape; import com.bbn.openmap.gui.time.TimelineLayer.SelectionArea.PlayFilterSection; import com.bbn.openmap.layer.OMGraphicHandlerLayer; import com.bbn.openmap.layer.policy.NullProjectionChangePolicy; import com.bbn.openmap.omGraphics.DrawingAttributes; import com.bbn.openmap.omGraphics.OMAction; import com.bbn.openmap.omGraphics.OMGraphic; import com.bbn.openmap.omGraphics.OMGraphicList; import com.bbn.openmap.omGraphics.OMLine; import com.bbn.openmap.omGraphics.OMRaster; import com.bbn.openmap.omGraphics.OMRect; import com.bbn.openmap.omGraphics.OMText; import com.bbn.openmap.proj.Cartesian; import com.bbn.openmap.proj.Proj; import com.bbn.openmap.proj.Projection; import com.bbn.openmap.time.Clock; import com.bbn.openmap.time.TimeBounds; import com.bbn.openmap.time.TimeBoundsEvent; import com.bbn.openmap.time.TimeBoundsListener; import com.bbn.openmap.time.TimeEvent; import com.bbn.openmap.time.TimeEventListener; import com.bbn.openmap.time.TimerStatus; import com.bbn.openmap.tools.drawing.DrawingToolRequestor; import com.bbn.openmap.tools.icon.BasicIconPart; import com.bbn.openmap.tools.icon.IconPart; import com.bbn.openmap.tools.icon.OMIconFactory; /** * Timeline layer * * Render events and allow for their selection on a variable-scale time line. */ public class TimelineLayer extends OMGraphicHandlerLayer implements ActionListener, DrawingToolRequestor, PropertyChangeListener, MapMouseListener, ComponentListener, TimeBoundsListener, TimeEventListener { /** * This property is used to signify whether any OMEvents have been * designated as play filterable, so GUI controls for the play filter can be * enabled/disabled. */ public final static String PlayFilterProperty = "playfilter"; /** * This property is used to send the current offset time where the mouse is * over the timeline. */ public final static String MouseTimeProperty = "mouseTime"; /** * This property is used to send event details that can be displayed when * the mouse is over an event in the timeline. */ public final static String EventDetailsProperty = "eventDetails"; /** * This property is used to notify listeners that the time projection * parameters have changed, and they need to contact this object to figure * out how to display those changes. */ public final static String TimeParametersProperty = "timeParameters"; public static Logger logger = Logger.getLogger("com.bbn.openmap.gui.time.TimelineLayer"); protected OMGraphicList eventGraphicList = null; protected OMGraphicList timeLinesList = null; protected PlayFilter playFilter = new PlayFilter(); protected OMGraphicList ratingAreas = new OMGraphicList(); protected SelectionArea selectionRect; protected TimeSliderLayer.TimeDrape drape; protected CenterSupport centerDelegate; private TimeSliderLayer timeSliderLayer; long currentTime = 0; long gameStartTime = 0; long gameEndTime = 0; protected EventPresenter eventPresenter; protected OMEventSelectionCoordinator aesc; protected static Color tint = new Color(0x99000000, true); protected Clock clock; private boolean realTimeMode; private boolean isNoTime = true; private Timer scrollTimer = new Timer(); private ScrollTask scrollTask = null; /** * Construct the TimelineLayer. */ public TimelineLayer() { setName("Timeline"); // This is how to set the ProjectionChangePolicy, which // dictates how the layer behaves when a new projection is // received. setProjectionChangePolicy(new NullProjectionChangePolicy()); // Making the setting so this layer receives events from the // SelectMouseMode, which has a modeID of "Gestures". Other // IDs can be added as needed. setMouseModeIDsForEvents(new String[] { "Gestures" }); centerDelegate = new CenterSupport(this); addComponentListener(this); drape = new TimeDrape(0, 0, -1, -1); drape.setFillPaint(Color.gray); drape.setVisible(true); } public void findAndInit(Object someObj) { if (someObj instanceof Clock) { clock = (Clock) someObj; // clock.addPropertyChangeListener(Clock.TIMER_STATUS_PROPERTY, // this); clock.addTimeEventListener(this); clock.addTimeBoundsListener(this); setTimeBounds(clock.getStartTime(), clock.getEndTime()); } if (someObj instanceof CenterListener) { centerDelegate.add((CenterListener) someObj); } if (someObj instanceof EventPresenter) { eventPresenter = (EventPresenter) someObj; selectionRect = null; eventPresenter.addPropertyChangeListener(this); } if (someObj instanceof OMEventSelectionCoordinator) { aesc = (OMEventSelectionCoordinator) someObj; aesc.addPropertyChangeListener(this); } if (someObj instanceof TimePanel.Wrapper) { TimePanel tp = ((TimePanel.Wrapper) someObj).getTimePanel(); tp.addPropertyChangeListener(this); addPropertyChangeListener(tp); timeSliderLayer = tp.getTimeSliderPanel().getTimeSliderLayer(); } } public void findAndUndo(Object someObj) { if (someObj == clock) { // clock.removePropertyChangeListener(Clock.TIMER_STATUS_PROPERTY, // this); clock.removeTimeEventListener(this); clock.removeTimeBoundsListener(this); } if (someObj instanceof CenterListener) { centerDelegate.remove((CenterListener) someObj); } if (someObj == eventPresenter) { eventPresenter.removePropertyChangeListener(this); eventPresenter = null; } if (someObj == aesc) { aesc.removePropertyChangeListener(this); aesc = null; } if (someObj instanceof TimePanel.Wrapper) { TimePanel tp = ((TimePanel.Wrapper) someObj).getTimePanel(); removePropertyChangeListener(tp); tp.removePropertyChangeListener(this); } } public static double forwardProjectMillis(long time) { return (double) time / 60000f; // 60000 millis per minute } public static long inverseProjectMillis(double timef) { return (long) (timef * 60000f); // 60000 millis per minute } /** * Creates the OMGraphic list with the time and event markings. */ public synchronized OMGraphicList prepare() { Projection proj = getProjection(); if (logger.isLoggable(Level.FINER)) { logger.finer("Updating projection with " + proj); } OMGraphicList graphicList = getList(); if (getHeight() > 0) { if (graphicList == null) { graphicList = new OMGraphicList(); } else { graphicList.clear(); } drape = new TimeDrape(0, 0, -1, -1); drape.setFillPaint(Color.gray); drape.setVisible(isNoTime); drape.generate(proj); graphicList.add(drape); graphicList.add(constructTimeLines(proj)); graphicList.add(getCurrentTimeMarker(proj)); OMGraphicList eventGraphicList = realTimeMode ? null : getEventGraphicList(); // if new events are fetched, new rating areas and play filters are // created here. if (eventGraphicList == null || eventGraphicList.isEmpty()) { eventGraphicList = getEventList(proj); setEventGraphicList(eventGraphicList); } else { if (logger.isLoggable(Level.FINER)) { logger.finer("don't need to re-create event lines, haven't changed with (" + eventGraphicList.size() + ") events"); } // TODO Don, why does this not seem to place event markers properly if time is advancing? eventGraphicList.generate(proj); } ratingAreas.generate(proj); playFilter.generate(proj); graphicList.add(playFilter); graphicList.add(eventGraphicList); SelectionArea selectionRenderRect = getSelectionRectangle(proj); if(selectionRenderRect != null) { graphicList.add(selectionRenderRect); } graphicList.add(ratingAreas); } return graphicList; } public synchronized OMGraphicList getEventGraphicList() { return eventGraphicList; } public synchronized void setEventGraphicList(OMGraphicList eventGraphicList) { this.eventGraphicList = eventGraphicList; } protected TimeHashFactory timeHashFactory; /** * * @return OMGraphicList new graphic list */ protected OMGraphicList constructTimeLines(Projection projection) { // if (timeLinesList == null) { OMGraphicList tll = new OMGraphicList(); timeHashFactory = new TimeHashFactory(); tll.add(timeHashFactory.getHashMarks(projection, realTimeMode, gameStartTime)); if(!isNoTime) { preTime = new SelectionArea.PreTime(0); preTime.generate(projection); tll.add(preTime); postTime = new SelectionArea.PostTime(gameEndTime - gameStartTime); postTime.generate(projection); tll.add(postTime); } timeLinesList = tll; return tll; } public SelectionArea getSelectionRectangle(Projection proj) { if (selectionRect == null) { selectionRect = new SelectionArea(); if (eventPresenter != null) { selectionRect.setFillPaint(eventPresenter.getSelectionDrawingAttributes().getSelectPaint()); } } if(selectionRect.isVisible()) { // Make a temp copy, just for painting during this render frame SelectionArea selectionRectToRender = new SelectionArea(); if (eventPresenter != null) { selectionRectToRender.setFillPaint(eventPresenter.getSelectionDrawingAttributes().getSelectPaint()); } selectionRectToRender.setLocation(selectionRect.getWestLon(), selectionRect.getEastLon()); selectionRectToRender.generate(proj); return selectionRectToRender; } else { // Not visible, so don't bother sticking it on the list return null; } } protected OMGraphicList currentTimeMarker; protected SelectionArea.PreTime preTime; protected SelectionArea.PostTime postTime; protected OMGraphic getCurrentTimeMarker(Projection proj) { currentTimeMarker = new CurrentTimeMarker(); currentTimeMarker.generate(proj); return currentTimeMarker; } protected final static String ATT_KEY_EVENT = "att_key_event"; protected OMGraphicList getEventList(Projection projection) { OMGraphicList eventGraphicList; if (eventPresenter != null) { // Hack to use optimized method if available if(eventPresenter instanceof AbstractEventPresenter) { Rectangle bounds = getBounds(null); Point2D minutesPnt0 = projection.inverse(0, 0); Point2D minutesPnt1 = projection.inverse(1, 0); double leftX = bounds.getMinX(); double rightX = bounds.getMaxX(); Point2D minutesPntLeft = projection.inverse(leftX, 0); Point2D minutesPntRight = projection.inverse(rightX, 0); double minutesPerPixel = minutesPnt1.getX() - minutesPnt0.getX(); long step = (long)(minutesPerPixel * 60 * 1000); long start = gameStartTime + (long)(minutesPntLeft.getX() * 60 * 1000); long end = gameStartTime + (long)(minutesPntRight.getX() * 60 * 1000); eventGraphicList = getEventList(((AbstractEventPresenter)eventPresenter).getActiveEvents(start, end, step), projection); } else { eventGraphicList = getEventList(eventPresenter.getActiveEvents(), projection); } // As long as we feel the need to recreate the event markers, // let's re-evaluate the annotations. evaluateEventAttributes(); if (logger.isLoggable(Level.FINE)) { logger.fine("Creating event lines with (" + eventGraphicList.size() + ") events"); } } else { logger.fine("Can't create event list for timeline display, no event presenter"); eventGraphicList = new OMGraphicList(); } return eventGraphicList; } protected OMGraphicList getEventList(Iterator<OMEvent> it, Projection projection) { OMGraphicList eventGraphicList = new OMGraphicList(); if (projection != null) { BasicStroke symbolStroke = new BasicStroke(2); while (it.hasNext()) { OMEvent event = it.next(); long time = event.getTimeStamp() - gameStartTime; float lon = (float) forwardProjectMillis(time); EventMarkerLine currentLine = new EventMarkerLine(0f, lon, 6); currentLine.setLinePaint(Color.black); currentLine.setStroke(symbolStroke); currentLine.generate(projection); currentLine.putAttribute(ATT_KEY_EVENT, event); eventGraphicList.add(currentLine); } } return eventGraphicList; } public class EventMarkerLine extends OMLine { protected int heightRatioSetting; protected byte symbolHeight; public EventMarkerLine(double lat, double lon, int heightRatioSetting) { super(lat, lon, 0, 1, 0, -1); this.heightRatioSetting = heightRatioSetting; } public boolean generate(Projection proj) { byte testSH = (byte) (proj.getHeight() * 2 / heightRatioSetting); if (testSH != symbolHeight) { int[] pts = getPts(); int symbolHeight = proj.getHeight() / heightRatioSetting; pts[1] = symbolHeight; pts[3] = -symbolHeight; this.symbolHeight = (byte) symbolHeight; } return super.generate(proj); } } // ---------------------------------------------------------------------- // GUI // ---------------------------------------------------------------------- protected Box paletteBox = null; public java.awt.Component getGUI() { if (paletteBox == null) { logger.fine("creating Palette."); paletteBox = Box.createVerticalBox(); JPanel subbox3 = new JPanel(new GridLayout(0, 1)); JButton setProperties = new JButton(i18n.get(TimelineLayer.class, "setProperties", "Preferences")); setProperties.setActionCommand(DisplayPropertiesCmd); setProperties.addActionListener(this); subbox3.add(setProperties); paletteBox.add(subbox3); } return paletteBox; } public void drawingComplete(OMGraphic omg, OMAction action) { if (!doAction(omg, action)) { // null OMGraphicList on failure, should only occur if // OMGraphic is added to layer before it's ever been // on the map. setList(new OMGraphicList()); doAction(omg, action); } repaint(); } public boolean isSelectable(OMGraphic omg) { return false; } // ---------------------------------------------------------------------- // ActionListener interface implementation // ---------------------------------------------------------------------- public String getName() { return "TimelineLayer"; } protected void setTimeBounds(long start, long end) { if (gameStartTime != start || gameEndTime != end) { gameStartTime = start; gameEndTime = end; if (logger.isLoggable(Level.FINE)) { logger.fine("gst: " + gameStartTime + ", get: " + gameEndTime + ", bounds of " + postTime); } if(realTimeMode) { // TODO Don (see above) why is this necessary? Seems like the // regenerate ought to reproject the event markers properly? setEventGraphicList(null); } if(!realTimeMode || !timeSliderLayer.getUserHasChangedScale()) { setMapBeanMaxScale(true); } } } public void updateTimeBounds(TimeBoundsEvent tbe) { TimeBounds tb = tbe.getNewTimeBounds(); if (tb != null) { long oldStartTime = gameStartTime; setTimeBounds(tb.getStartTime(), tb.getEndTime()); if(realTimeMode) { long boundsStartOffset = tb.getStartTime() - oldStartTime; currentTime -= boundsStartOffset; ((Proj)getProjection()).setCenter(0, forwardProjectMillis(currentTime)); centerDelegate.fireCenter(0, forwardProjectMillis(currentTime)); timeLinesList = null; } // Update selection (this only deals with time translation for now; no scaling) if(realTimeMode && selectionRect != null && selectionRect.isVisible()) { long boundsWestDelta = tbe.getNewTimeBounds().getStartTime() - tbe.getOldTimeBounds().getStartTime(); double selectionDelta = (double)boundsWestDelta / 60000.0; double newWest = selectionRect.getWestLon() - selectionDelta; double newEast = selectionRect.getEastLon() - selectionDelta; selectionRect.setLocation(newWest, newEast); getSelectionRectangle(getProjection()); } } else { checkAndSetForNoTime(TimeEvent.NO_TIME); timeSliderLayer.setSelectionValid(false); } if(tbe.isInduceGraphicalUpdate()) { doPrepare(); } } public void updateTime(TimeEvent te) { if (checkAndSetForNoTime(te)) { return; } Clock clock = (Clock) te.getSource(); setTimeBounds(clock.getStartTime(), clock.getEndTime()); TimerStatus timerStatus = te.getTimerStatus(); if (timerStatus.equals(TimerStatus.STEP_FORWARD) || timerStatus.equals(TimerStatus.STEP_BACKWARD) || timerStatus.equals(TimerStatus.UPDATE)) { // These TimerStatus updates reflect the current time being // specifically set to a value, as opposed to the clock running // normally. currentTime = te.getSystemTime() - gameStartTime; ((Proj)getProjection()).setCenter(0, forwardProjectMillis(currentTime)); centerDelegate.fireCenter(0, forwardProjectMillis(currentTime)); timeLinesList = null; doPrepare(); } else if (timerStatus.equals(TimerStatus.FORWARD) || timerStatus.equals(TimerStatus.BACKWARD) || timerStatus.equals(TimerStatus.STOPPED)) { // Checking for a running clock prevents a time status // update after the clock is stopped. The // AudioFileHandlers don't care about the current time // if it isn't running. // This check might be avoided if just FORWARD and BACKWARD are sent // if the clock is running. Need to check the behavior of the clock // to make sure, and figure out what the state of the clock is when // it stops. if (clock.isRunning()) { long currentTime = te.getSystemTime() - gameStartTime; if (playFilter.reactToCurrentTime(currentTime, clock, gameStartTime)) { this.currentTime = currentTime; timeLinesList = null; centerDelegate.fireCenter(0, forwardProjectMillis(currentTime)); doPrepare(); } } } else { logger.info("none of the above: " + timerStatus.toString()); } } /* * @seejava.beans.PropertyChangeListener#propertyChange(java.beans. * PropertyChangeEvent) */ public void propertyChange(PropertyChangeEvent evt) { String propertyName = evt.getPropertyName(); if (propertyName == EventPresenter.ActiveEventsProperty) { setEventGraphicList(null); logger.fine("EventPresenter updated event list, calling doPrepare() " + evt.getNewValue()); doPrepare(); } else if (propertyName == OMEventSelectionCoordinator.EventsSelectedProperty) { setSelectionRectangleToEvents(); } else if (propertyName == EventPresenter.EventAttributesUpdatedProperty) { evaluateEventAttributes(); doPrepare(); } else if (propertyName == TimePanel.PlayFilterProperty) { boolean inUse = ((Boolean) evt.getNewValue()).booleanValue(); playFilter.setInUse(inUse); firePropertyChange(PlayFilterProperty, null, new Boolean(!inUse || !playFilter.isEmpty())); } else { logger.finer("AAGGH: " + propertyName); } } protected boolean checkAndSetForNoTime(TimeEvent te) { isNoTime = te == TimeEvent.NO_TIME; return isNoTime; } double snapToEvent(double lon) { if(realTimeMode) { return lon; } double retVal = lon; double minDiff = Double.MAX_VALUE; if (eventPresenter != null) { for (Iterator<OMEvent> it = eventPresenter.getAllEvents(); it.hasNext();) { OMEvent event = it.next(); long time = event.getTimeStamp() - gameStartTime; float timeMinutes = (float) forwardProjectMillis(time); if (Math.abs(timeMinutes - lon) < minDiff) { minDiff = Math.abs(timeMinutes - lon); retVal = timeMinutes; } } } return retVal; } public MapMouseListener getMapMouseListener() { return this; } public String[] getMouseModeServiceList() { return getMouseModeIDsForEvents(); } public boolean mousePressed(MouseEvent e) { doubleClick = false; updateMouseTimeDisplay(e); // Use current projection to determine rect top and bottom Projection projection = getProjection(); // Get latLong from mouse, and then snap to nearest event... Point2D latLong = projection.inverse(e.getPoint()); Point2D ul = projection.getUpperLeft(); Point2D lr = projection.getLowerRight(); double lon = latLong.getX(); float up = (float) ul.getY(); float down = (float) lr.getY(); lon = snapToEvent(lon); selectionRect.setVisible(false); selectionRect.setLocation(up, (float) lon, down, (float) lon, OMGraphic.LINETYPE_STRAIGHT); selectionRect.generate(projection); timeSliderLayer.setSelectionValid(false); downLon = lon; return true; } protected void selectEventForMouseEvent(MouseEvent e) { // Handle a single click, select event if close OMGraphicList eventGraphicList = getEventGraphicList(); if (e != null && eventGraphicList != null) { OMGraphic omg = eventGraphicList.findClosest((int) e.getX(), (int) e.getY(), 4); if (omg != null) { OMEvent sourceEvent = (OMEvent) omg.getAttribute(ATT_KEY_EVENT); if (sourceEvent != null) { sourceEvent.putAttribute(OMEvent.ATT_KEY_SELECTED, OMEvent.ATT_VAL_SELECTED); if(aesc != null) { Vector<OMEvent> eventList = new Vector<OMEvent>(); eventList.add(sourceEvent); aesc.eventsSelected(eventList); } } } } doubleClick = false; } public boolean mouseReleased(MouseEvent e) { updateMouseTimeDisplay(e); handleEventSelection(); if(scrollTask != null) { scrollTask.cancel(); scrollTask = null; } return true; } double downLon; boolean doubleClick = false; public boolean mouseClicked(MouseEvent e) { if (e.getClickCount() >= 2) { selectEventForMouseEvent(e); doubleClick = true; return true; } double lon = updateMouseTimeDisplay(e); if (clock != null) { clock.setTime(gameStartTime + inverseProjectMillis(lon)); } timeSliderLayer.clearFixedRenderRange(); selectionRect.setLocation(lon, lon); selectionRect.setVisible(false); timeSliderLayer.setSelectionValid(false); return true; } protected double updateMouseTimeDisplay(MouseEvent e) { Projection proj = getProjection(); Point2D latLong = proj.inverse(e.getPoint()); double lon = latLong.getX(); double endTime = forwardProjectMillis(gameEndTime - gameStartTime); if (lon < 0) { lon = -1; } else if (lon > endTime) { lon = endTime; } long offsetMillis = inverseProjectMillis(lon); updateMouseTimeDisplay(new Long(offsetMillis)); return lon < 0 ? 0 : lon; } public void updateMouseTimeDisplay(Long offsetMillis) { firePropertyChange(MouseTimeProperty, null, offsetMillis); } protected void updateEventDetails(MouseEvent e) { String details = ""; OMGraphicList eventGraphicList = getEventGraphicList(); if (e != null && eventGraphicList != null) { OMGraphic omg = eventGraphicList.findClosest((int) e.getX(), (int) e.getY(), 4); if (omg != null) { OMEvent sourceEvent = (OMEvent) omg.getAttribute(ATT_KEY_EVENT); if (sourceEvent != null) { details = sourceEvent.getDescription(); } } } firePropertyChange(EventDetailsProperty, null, details); } protected void updateEventDetails() { updateEventDetails(null); } public void mouseEntered(MouseEvent e) {} public void mouseExited(MouseEvent e) { firePropertyChange(MouseTimeProperty, null, new Long(-1)); updateEventDetails(); } class ScrollTask extends TimerTask { private long delta; private MouseEvent mouseEvent; ScrollTask(long delta, MouseEvent mouseEvent) { this.delta = delta; this.mouseEvent = mouseEvent; } @Override public void run() { if (clock != null) { long newTime = clock.getTime() + delta; newTime = Math.max(gameStartTime, newTime); newTime = Math.min(gameEndTime, newTime); clock.setTime(newTime); adjustSelection(mouseEvent); doPrepare(); } } } private void adjustSelection(MouseEvent e) { updateMouseTimeDisplay(e); updateEventDetails(e); timeSliderLayer.clearFixedRenderRange(); // Get latLong from mouse, and set E side of current select rect... Projection proj = getProjection(); Point2D latLong = proj.inverse(e.getPoint()); double lon = snapToEvent(latLong.getX()); float west = (float) Math.min(downLon, lon); float east = (float) Math.max(downLon, lon); selectionRect.setVisible(true); selectionRect.setLocation(west, east); selectionRect.generate(proj); timeSliderLayer.setSelectionValid(east != west); } public boolean mouseDragged(MouseEvent e) { // First the actual selection adjustment adjustSelection(e); // Now reset the scroll timer as necessary Projection proj = getProjection(); final long scrollPeriod = 50; float baseScrollMultiplier = 0.001f; int x = e.getPoint().x; if(scrollTask != null) { scrollTask.cancel(); scrollTask = null; } float scale = proj.getScale(); if (x < 0) { if (clock != null) { final float multiplier = baseScrollMultiplier * x; long delta = (long) (multiplier * scale); scrollTask = new ScrollTask(delta, e); scrollTimer.schedule(scrollTask, 0, scrollPeriod); } } else if(x > getWidth()) { if (clock != null) { final float multiplier = baseScrollMultiplier * (x - getWidth()); long delta = (long) (multiplier * scale); scrollTask = new ScrollTask(delta, e); scrollTimer.schedule(scrollTask, 0, scrollPeriod); } } doPrepare(); return true; } public boolean mouseMoved(MouseEvent e) { updateMouseTimeDisplay(e); updateEventDetails(e); return true; } public void mouseMoved() { updateEventDetails(); } protected List<OMEvent> handleEventSelection() { List<OMEvent> eventList = null; if (aesc != null && selectionRect != null) { // The thing to be careful about here is that the selection // Rectangle isn't where the user clicked and released. It's snapped // to the visible events. It makes some weird behavior below when // you try to highlight a single event, because the time for that // event is the closest snapped time event, not the invisible event // that may be clicked on. boolean goodDrag = selectionRect.isVisible(); double lowerTime = selectionRect.getWestLon(); double upperTime = selectionRect.getEastLon(); // Convert to millis long lowerTimeStamp = inverseProjectMillis((float) lowerTime); long upperTimeStamp = inverseProjectMillis((float) upperTime); boolean sameTime = lowerTimeStamp == upperTimeStamp; goodDrag = goodDrag && !sameTime; boolean labeledRangeStart = false; OMEvent lastEventLabeled = null; for (Iterator<OMEvent> it = eventPresenter.getAllEvents(); it.hasNext();) { if (eventList == null) { eventList = new Vector<OMEvent>(); } OMEvent event = (OMEvent) it.next(); double timeStamp = event.getTimeStamp() - gameStartTime; // Don't forget, need to go through all of the events, not just // the ones lower than the upper time stamp, because we need to // set the selected flag to null for all of them and then only // reset the ones that actually are selected. event.putAttribute(OMEvent.ATT_KEY_SELECTED, null); if (goodDrag && timeStamp >= lowerTimeStamp && timeStamp <= upperTimeStamp) { eventList.add(event); // Needs to be updated to put ATT_VAL_SELECTED_START_RANGE, // ATT_VAL_SELECTED_END_RANGE, or just ATT_VAL_SELECTED if (!labeledRangeStart && lowerTimeStamp != upperTimeStamp) { event.putAttribute(OMEvent.ATT_KEY_SELECTED, OMEvent.ATT_VAL_SELECTED_START_RANGE); labeledRangeStart = true; } else { event.putAttribute(OMEvent.ATT_KEY_SELECTED, OMEvent.ATT_VAL_SELECTED); } lastEventLabeled = event; } else if (sameTime && timeStamp == lowerTimeStamp) { // This code just returns the closest visible snapped time // event. // event.putAttribute(OMEvent.ATT_KEY_SELECTED, // OMEvent.ATT_VAL_SELECTED); // eventList.add(event); // I guess this is OK when a visible event is clicked on, // but it's not when a non-visible event is clicked on. } } if (labeledRangeStart && lastEventLabeled != null) { lastEventLabeled.putAttribute(OMEvent.ATT_KEY_SELECTED, OMEvent.ATT_VAL_SELECTED_END_RANGE); } aesc.eventsSelected(eventList); } return eventList; } protected void evaluateEventAttributes() { if(realTimeMode) { return; // Never mind; we're not doing anything with attributes } ratingAreas.clear(); playFilter.clear(); SelectionArea.RatingArea currentRatingArea = null; SelectionArea.PlayFilterSection currentPlayFilter = null; if (eventPresenter != null) { for (Iterator<OMEvent> it = eventPresenter.getAllEvents(); it.hasNext();) { OMEvent aare = it.next(); String rating = (String) aare.getAttribute(OMEvent.ATT_KEY_RATING); Object playFilterObj = aare.getAttribute(OMEvent.ATT_KEY_PLAY_FILTER); long timeStamp = aare.getTimeStamp() - gameStartTime; if (rating != null) { if (currentRatingArea != null && !currentRatingArea.isRating(rating)) { currentRatingArea = null; } if (currentRatingArea == null) { currentRatingArea = new SelectionArea.RatingArea(timeStamp, rating); ratingAreas.add(currentRatingArea); } currentRatingArea.addTime(timeStamp); } else if (currentRatingArea != null) { currentRatingArea = null; } if (playFilterObj != null) { if (currentPlayFilter != null) { currentPlayFilter.addTime(timeStamp); } else { currentPlayFilter = new SelectionArea.PlayFilterSection(timeStamp); // logger.info("adding play filter section to play // filter"); playFilter.add(currentPlayFilter); } } else { currentPlayFilter = null; } } OMGraphicList list = getList(); if (list != null && list.isVisible()) { firePropertyChange(PlayFilterProperty, null, new Boolean(!playFilter.isInUse() || !playFilter.isEmpty())); } } } protected void setSelectionRectangleToEvents() { if (aesc != null) { selectionRect = getSelectionRectangle(getProjection()); double lowerTime = Double.POSITIVE_INFINITY; double upperTime = Double.NEGATIVE_INFINITY; for (Iterator<OMEvent> it = eventPresenter.getAllEvents(); it.hasNext();) { OMEvent event = it.next(); if (event.getAttribute(OMEvent.ATT_KEY_SELECTED) != null) { // Convert to minutes for selectRect bounds double timeStamp = (double) forwardProjectMillis(event.getTimeStamp() - gameStartTime); if (timeStamp < lowerTime) { lowerTime = timeStamp; } if (timeStamp > upperTime) { upperTime = timeStamp; } } } if (upperTime != Double.NEGATIVE_INFINITY && lowerTime != Double.POSITIVE_INFINITY) { selectionRect.setLocation((float) lowerTime, (float) upperTime); selectionRect.setVisible(true); selectionRect.generate(getProjection()); } else { selectionRect.setVisible(false); } } doPrepare(); } public static class SelectionArea extends com.bbn.openmap.omGraphics.OMRect { public SelectionArea() { setRenderType(OMRect.RENDERTYPE_LATLON); } public void setLocation(double left, double right) { super.setLocation(0f, left, 0f, right, OMRect.LINETYPE_STRAIGHT); } public boolean generate(Projection proj) { updateY(proj); return super.generate(proj); } protected void updateY(Projection proj) { // The difference here is that the upper and lower bounds are // determined by the projection. Point2D ul = proj.getUpperLeft(); Point2D lr = proj.getLowerRight(); lat1 = ul.getY(); lat2 = lr.getY(); } public static class PreTime extends SelectionArea { public PreTime(long time) { super(); lon2 = forwardProjectMillis(time); setFillPaint(tint); setLinePaint(tint); } public void setLocation() {} public boolean generate(Projection proj) { // The difference here is that the vertical bounds are // determined the starting time and all times before that. Point2D ul = proj.getUpperLeft(); double ulx = ul.getX(); if (ulx >= lon2) { lon1 = lon2; } else { lon1 = ulx; } return super.generate(proj); } } public static class PostTime extends SelectionArea { public PostTime(long time) { super(); lon1 = forwardProjectMillis(time); setFillPaint(tint); setLinePaint(tint); } public void setLocation() {} public boolean generate(Projection proj) { // The difference here is that the vertical bounds are // determined the end time and all times after that. Point2D lr = proj.getLowerRight(); double lrx = lr.getX(); if (lrx <= lon1) { lon2 = lon1; } else { lon2 = lrx; } return super.generate(proj); } } public static class RatingArea extends SelectionArea { protected String rating; protected static Color goodColor = new Color(0x9900ff00, true); protected static Color badColor = new Color(0x99ff0000, true); public RatingArea(long time, String rating) { super(); this.rating = rating; double timef = forwardProjectMillis(time); setLocation(timef, timef); Color ratingColor = badColor; if (rating.equals(OMEvent.ATT_VAL_GOOD_RATING)) { ratingColor = goodColor; } setLinePaint(ratingColor); setFillPaint(ratingColor); } public boolean isRating(String rating) { return this.rating.equalsIgnoreCase(rating); } public void addTime(long timeToAdd) { double time = forwardProjectMillis(timeToAdd); double east = getEastLon(); double west = getWestLon(); boolean updated = false; if (time < west) { west = time; updated = true; } if (time > east) { east = time; updated = true; } if (updated) { setLocation(west, east); } } } public static class PlayFilterSection extends SelectionArea { protected static Color color = new Color(0x99000000, true); protected String idString; public PlayFilterSection(long time) { super(); double timef = forwardProjectMillis(time); setLocation(timef, timef); setLinePaint(color); setFillPaint(color); } /** * Checks time in relation to held times. * * @param timel time in unprojected milliseconds, offset from game * start time. * @return 0 if time is within bounds, -1 if time is before bounds, * 1 if time is after bounds. */ public int isWithin(long timel) { double time = forwardProjectMillis(timel); int ret = -1; if (time >= getWestLon()) { ret++; } if (time > getEastLon()) { ret++; } return ret; } protected void updateY(Projection proj) { Point2D ul = proj.getUpperLeft(); lat1 = ul.getY(); Point2D lrpt = proj.inverse(0, proj.getHeight() / 8); lat2 = lrpt.getY(); idString = null; } public void addTime(long timeToAdd) { double time = forwardProjectMillis(timeToAdd); double east = getEastLon(); double west = getWestLon(); boolean updated = false; if (time < west) { west = time; updated = true; } if (time > east) { east = time; updated = true; } if (updated) { setLocation(west, east); idString = null; } } public String toString() { if (idString == null) { idString = "PlayFilterSection[" + getWestLon() + "," + getEastLon() + "]"; } return idString; } } } public static class CurrentTimeMarker extends OMGraphicList { protected OMRaster upperMark; protected OMRaster lowerMark; protected OMLine startingLine; int iconSize = 16; int lastHeight = 0; int lastWidth = 0; public CurrentTimeMarker() { DrawingAttributes da = new DrawingAttributes(); da.setFillPaint(tint); da.setLinePaint(tint); IconPart ip = new BasicIconPart(new Polygon(new int[] { 50, 90, 10, 50 }, new int[] { 10, 90, 90, 10 }, 4), da); ImageIcon thumbsUpImage = OMIconFactory.getIcon(iconSize, iconSize, ip); lowerMark = new OMRaster(0, 0, thumbsUpImage); ip = new BasicIconPart(new Polygon(new int[] { 10, 90, 50, 10 }, new int[] { 10, 10, 90, 10 }, 4), da); ImageIcon thumbsDownImage = OMIconFactory.getIcon(iconSize, iconSize, ip); upperMark = new OMRaster(0, 0, thumbsDownImage); startingLine = new OMLine(0, 0, 0, 0); da.setTo(startingLine); add(startingLine); add(lowerMark); add(upperMark); } public boolean generate(Projection proj) { int height = proj.getHeight(); int width = proj.getWidth(); if (height != lastHeight || width != lastWidth) { lastHeight = height; lastWidth = width; int halfX = (int) (width / 2); upperMark.setX(halfX - iconSize / 2); upperMark.setY(0); lowerMark.setX(halfX - iconSize / 2); lowerMark.setY(height - iconSize); int[] pts = startingLine.getPts(); pts[0] = halfX; pts[1] = 0 + iconSize; pts[2] = halfX; pts[3] = height - iconSize; } return super.generate(proj); } } public static class TimeHashFactory { List<TimeHashMarks> hashMarks = new ArrayList<TimeHashMarks>(5); TimeHashMarks current; public TimeHashFactory() { hashMarks.add(new TimeHashMarks.Seconds()); hashMarks.add(new TimeHashMarks.Minutes()); hashMarks.add(new TimeHashMarks.Hours()); hashMarks.add(new TimeHashMarks.Days()); hashMarks.add(new TimeHashMarks.Years()); } public OMGraphicList getHashMarks(Projection proj, boolean realTimeMode, long gameStartTimeMillis) { Point2D ul = proj.getUpperLeft(); Point2D lr = proj.getLowerRight(); // timeSpan in minutes double timeSpan = lr.getX() - ul.getX(); TimeHashMarks thm = null; for (Iterator<TimeHashMarks> it = hashMarks.iterator(); it.hasNext();) { TimeHashMarks cthm = it.next(); if (cthm.passesThreshold(timeSpan)) { thm = cthm; } else { break; } } if (current != null) { current.clear(); } if (thm != current) { current = thm; } current.generate(proj, realTimeMode, timeSpan, gameStartTimeMillis); return current; } } public static class PlayFilter extends OMGraphicList { protected boolean inUse = false; String currentlyPlaying = null; public PlayFilter() {} public boolean reactToCurrentTime(long currentTime, Clock clock, long gameStartTime) { boolean ret = !inUse; if (inUse) { // logger.info("checking " + size() + " sections"); for (Iterator<OMGraphic> it = iterator(); it.hasNext();) { PlayFilterSection pfs = (PlayFilterSection) it.next(); int where = pfs.isWithin(currentTime); if (where == 0) { ret = true; currentlyPlaying = pfs.toString(); // logger.info("where == 0, setting pfs " + // currentlyPlaying); break; } else if (where > 0) { if ((currentlyPlaying != null && (currentlyPlaying.equals(pfs.toString()))) || !it.hasNext()) { // logger.info("where > 0, same pfs, stopping clock // " + pfs); clock.setTime(gameStartTime + inverseProjectMillis(pfs.getEastLon())); clock.stopClock(); currentlyPlaying = null; break; } else { // logger.info("where > 0, not the same pfs " + pfs // + ", " + currentlyPlaying); } continue; } else { // logger.info("where < 0, jumping clock " + pfs); clock.setTime(gameStartTime + inverseProjectMillis(pfs.getWestLon())); break; } } } return ret; } public boolean isInUse() { return inUse; } public void setInUse(boolean inUse) { this.inUse = inUse; } } public abstract static class TimeHashMarks extends OMGraphicList { protected String annotation; protected double unitPerMinute; protected DateFormat dateFormat; protected TimeHashMarks(String annotation, double unitPerMinute, DateFormat dateFormat) { this.annotation = annotation; this.unitPerMinute = unitPerMinute; this.dateFormat = dateFormat; } public abstract boolean passesThreshold(double minVisibleOnTimeLine); public boolean generate(Projection proj, boolean realTimeMode, double timeSpanMinutes, long gameStartTimeMillis) { Point2D ul = proj.getUpperLeft(); Point2D lr = proj.getLowerRight(); double left = ul.getX() * unitPerMinute; double right = lr.getX() * unitPerMinute; double timeSpan = timeSpanMinutes * unitPerMinute; double num = Math.floor(timeSpan); double heightStepSize = 1; double stepSize = 1; if (num < 2) { stepSize = .25; } else if (num < 5) { stepSize = .5; } else if (num > 30) { stepSize = 10; heightStepSize = 10; } else if (num > 15) { heightStepSize = 10; } if (logger.isLoggable(Level.FINER)) { logger.finer("figure on needing " + num + annotation + ", " + stepSize + " stepsize for " + (timeSpan / stepSize) + " lines"); } int height = (int) (proj.getHeight() * .2); double anchory = lr.getY(); if(realTimeMode) { // Different approach here, since we're concerned with absolute time // -So all of the above setup still applies, but we're going to convert // once we have the start time set double millisPerUnit = (long)(60.0 * 1000.0 / unitPerMinute); double gameStartTimeUnits = (double)gameStartTimeMillis / millisPerUnit; double firstMarkerOffsetMillis = (gameStartTimeMillis % millisPerUnit); double firstMarkerOffsetUnits = (double)firstMarkerOffsetMillis / millisPerUnit; // need to do negative times. if (left < 0) { while (firstMarkerOffsetUnits > left) { firstMarkerOffsetUnits -= stepSize; } } while (firstMarkerOffsetUnits < left) { firstMarkerOffsetUnits += stepSize; } double stepStart = Math.floor(firstMarkerOffsetUnits + gameStartTimeUnits); double stepEnd = Math.ceil(right + gameStartTimeUnits); int count = 0; // i is in 'units' for (double i = stepStart; i < stepEnd; i += stepSize, count++) { double anchorx = (i - gameStartTimeUnits) / unitPerMinute; int thisHeight = height; boolean doLabel = true; if (count % heightStepSize != 0) { thisHeight /= 2; doLabel = false; } OMLine currentLine = new OMLine(anchory, anchorx, 0, 0, 0, -thisHeight); currentLine.setLinePaint(tint); currentLine.setStroke(new BasicStroke(2)); add(currentLine); if (doLabel) { Date date = new Date((long) (i*millisPerUnit)); String labelString = dateFormat.format(date); OMText label = new OMText((float) anchory, (float) anchorx, 2, -5, labelString, OMText.JUSTIFY_LEFT); label.setLinePaint(tint); add(label); } } } else { // Now, we need to baseline marks on 0, not on the left most value. double start = 0; // need to do negative times. if (left < 0) { while (start > left) { start -= stepSize; } } while (start < left) { start += stepSize; } double stepStart = Math.floor(start); double stepEnd = Math.ceil(right); for (double i = stepStart; i < stepEnd; i += stepSize) { double anchorx = i / unitPerMinute; int thisHeight = height; boolean doLabel = true; if (i % heightStepSize != 0) { thisHeight /= 2; doLabel = false; } OMLine currentLine = new OMLine(anchory, anchorx, 0, 0, 0, -thisHeight); currentLine.setLinePaint(tint); currentLine.setStroke(new BasicStroke(2)); add(currentLine); if (doLabel) { OMText label = new OMText((float) anchory, (float) anchorx, 2, -5, (int) i + annotation, OMText.JUSTIFY_LEFT); label.setLinePaint(tint); add(label); } } } return super.generate(proj); } public static class Seconds extends TimeHashMarks { public Seconds() { super("s", 60, new SimpleDateFormat("HH:mm:ss.SS")); } public boolean passesThreshold(double minVisibleOnTimeLine) { return true; } } public static class Minutes extends TimeHashMarks { public Minutes() { super("m", 1, new SimpleDateFormat("HH:mm:ss")); } public boolean passesThreshold(double minVisibleOnTimeLine) { return minVisibleOnTimeLine > 2; } } public static class Hours extends TimeHashMarks { public Hours() { super("h", (1d / 60d), new SimpleDateFormat("HH:mm:ss")); } public boolean passesThreshold(double minVisibleOnTimeLine) { return minVisibleOnTimeLine / 60 > 3; } } public static class Days extends TimeHashMarks { public Days() { super("d", (1d / 60d / 24d), TimePanel.dayFormat); } public boolean passesThreshold(double minVisibleOnTimeLine) { return minVisibleOnTimeLine / 60 / 24 > 2; } } public static class Years extends TimeHashMarks { public Years() { super("y", (1d / 60d / 24d / 365d), TimePanel.dayFormat); } public boolean passesThreshold(double minVisibleOnTimeLine) { return minVisibleOnTimeLine / 60 / 24 / 365 > 1; } } } protected void setMapBeanMaxScale(boolean setScaleToMax) { float scale = (float) (TimeSliderLayer.magicScaleFactor * (double) forwardProjectMillis(gameEndTime - gameStartTime) / getProjection().getWidth()); MapBean mb = (MapBean) ((MapHandler) getBeanContext()).get(com.bbn.openmap.MapBean.class); ((Cartesian) mb.getProjection()).setMaxScale(scale); if (setScaleToMax) { mb.setScale(scale); } } public void componentHidden(ComponentEvent e) {} public void componentMoved(ComponentEvent e) {} public void componentResized(ComponentEvent e) { setMapBeanMaxScale(false); } public void componentShown(ComponentEvent e) {} public void paint(Graphics g) { try { super.paint(g); } catch (Exception e) { if (logger.isLoggable(Level.FINE)) { logger.warning(e.getMessage()); e.printStackTrace(); } } } public void setRealTimeMode(boolean realTimeMode) { this.realTimeMode = realTimeMode; } public long getDuration() { return gameEndTime - gameStartTime; } public long getEndTime() { return gameEndTime; } public void setUserHasChangedScale(boolean userHasChangedScale) { timeSliderLayer.setUserHasChangedScale(userHasChangedScale); } public void adjustZoomFromMouseWheel(int rot) { timeSliderLayer.adjustZoomFromMouseWheel(rot); doPrepare(); } long getSelectionStart() { boolean goodDrag = selectionRect.isVisible(); double lowerTime = selectionRect.getWestLon(); double upperTime = selectionRect.getEastLon(); // Convert to millis long lowerTimeStamp = inverseProjectMillis((float) lowerTime); long upperTimeStamp = inverseProjectMillis((float) upperTime); boolean sameTime = lowerTimeStamp == upperTimeStamp; goodDrag = goodDrag && !sameTime; return goodDrag ? lowerTimeStamp + gameStartTime : -1; } long getSelectionEnd() { boolean goodDrag = selectionRect.isVisible(); double lowerTime = selectionRect.getWestLon(); double upperTime = selectionRect.getEastLon(); // Convert to millis long lowerTimeStamp = inverseProjectMillis((float) lowerTime); long upperTimeStamp = inverseProjectMillis((float) upperTime); boolean sameTime = lowerTimeStamp == upperTimeStamp; goodDrag = goodDrag && !sameTime; return goodDrag ? upperTimeStamp + gameStartTime : -1; } public void clearSelection() { selectionRect.setLocation(0, 0); selectionRect.setVisible(false); timeSliderLayer.setSelectionValid(false); timeSliderLayer.clearFixedRenderRange(); } }