/* SignalPlotColumnHeader.java created 2007-10-15 * */ package org.signalml.app.view.signal; import static org.signalml.app.util.i18n.SvarogI18n._; import static org.signalml.app.util.i18n.SvarogI18n._R; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.ItemSelectable; import java.awt.Point; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.event.ActionEvent; import java.awt.event.MouseEvent; import java.util.Calendar; import java.util.Formatter; import java.util.List; import java.util.SortedSet; import java.util.TimeZone; import javax.swing.AbstractAction; import javax.swing.JComponent; import javax.swing.JPopupMenu; import org.signalml.app.document.TagDocument; import org.signalml.app.view.tag.TagRenderer; import org.signalml.app.view.tag.comparison.TagDifferenceRenderer; import org.signalml.domain.tag.StyledTagSet; import org.signalml.domain.tag.TagDifference; import org.signalml.domain.tag.TagDifferenceSet; import org.signalml.plugin.export.signal.SignalSelection; import org.signalml.plugin.export.signal.SignalSelectionType; import org.signalml.plugin.export.signal.Tag; import org.signalml.plugin.export.signal.TagStyle; /** SignalPlotColumnHeader * * * @author Michal Dobaczewski © 2007-2008 CC Otwarte Systemy Komputerowe Sp. z o.o. */ public class SignalPlotColumnHeader extends JComponent { private static final int TOP_OFFSET = 3; private static final long serialVersionUID = 1L; private boolean calculated = false; private int componentHeight = 0; private int pageTagAreaHeight = 0; private String columnUnitLabel; private double pixelPerColumnUnit; private double pixelPerSecond; private int maxSampleCount; private double timeZoomFactor; private float pageSize; private double pixelPerPage; private float samplingFrequency; private boolean pageLinesVisible; int timeScaleY; private TagRenderer tagRenderer; private TagDifferenceRenderer tagDifferenceRenderer; private SignalPlot plot; private SignalPlotPopupProvider signalPlotPopupProvider; private Font font; private FontMetrics fontMetrics; private Rectangle tempBounds = new Rectangle(); private boolean compact; private SetCompactAction setCompactAction; public SignalPlotColumnHeader(SignalPlot plot) { super(); this.plot = plot; } public void reset() { calculated = false; } private void calculate(Graphics2D g) { if (calculated) { return; } if (compact) { font = g.getFont().deriveFont(Font.PLAIN, 9); } else { font = g.getFont(); } fontMetrics = g.getFontMetrics(font); samplingFrequency = plot.getSamplingFrequency(); pixelPerSecond = plot.getPixelPerSecond(); pageSize = plot.getPageSize(); pixelPerPage = plot.getPixelPerPage(); timeZoomFactor = plot.getTimeZoomFactor(); maxSampleCount = plot.getMaxSampleCount(); pageLinesVisible = plot.isPageLinesVisible(); if (pixelPerSecond > 6) { pixelPerColumnUnit = pixelPerSecond; columnUnitLabel = "1 s"; } else if (pixelPerSecond > (6F / 60F)) { pixelPerColumnUnit = pixelPerSecond * 60F; columnUnitLabel = "1 min"; } else { pixelPerColumnUnit = pixelPerSecond * 3600F; columnUnitLabel = "1 h"; } if (compact) { pageTagAreaHeight = 2 * fontMetrics.getAscent() + 2; } else { if (plot.getView().isComparingTags()) { pageTagAreaHeight = 40; } else { pageTagAreaHeight = Math.min(100, Math.max(40, plot.getDocument().getTagDocuments().size() * 20)); } } componentHeight = TOP_OFFSET + pageTagAreaHeight + 2 + 3 + SignalPlot.SCALE_TO_SIGNAL_GAP; if (!compact) { componentHeight += (2 + fontMetrics.getAscent()); } if (compact) { timeScaleY = TOP_OFFSET; } else { timeScaleY = TOP_OFFSET + pageTagAreaHeight / 2; } calculated = true; } public Rectangle getPixelPageTagBounds(SignalSelection tag, int tagCnt, int tagNumber, boolean comparing, Rectangle useRect) { Rectangle rect; if (useRect == null) { rect = new Rectangle(); } else { rect = useRect; } double position = tag.getPosition(); rect.x = ((int)(position * pixelPerSecond)); if (rect.x > 0 && pageLinesVisible && pixelPerPage > 4) { // avoid obscuring by page lines if visible int linePosition = (int)((int)((position / pageSize)) * pixelPerPage); if (linePosition == rect.x) { rect.x++; } } int endX = (int)((position+tag.getLength()) * pixelPerSecond); rect.width = endX-rect.x; if (compact) { rect.y = TOP_OFFSET; rect.height = pageTagAreaHeight; } else if (comparing) { // 0 - top, 1 - bottom, 2 - comparison int tagHeight = (pageTagAreaHeight-SignalPlot.COMPARISON_STRIP_HEIGHT) / 2; if (tagNumber == 0) { rect.y = TOP_OFFSET; rect.height = tagHeight; } else if (tagNumber == 1) { rect.y = TOP_OFFSET + SignalPlot.COMPARISON_STRIP_HEIGHT + tagHeight; rect.height = tagHeight; } else { rect.y = TOP_OFFSET + tagHeight + SignalPlot.COMPARISON_STRIP_MARGIN; rect.height = SignalPlot.COMPARISON_STRIP_HEIGHT - (2 * SignalPlot.COMPARISON_STRIP_MARGIN); } } else { float pixerPerTag = ((float) pageTagAreaHeight) / tagCnt; rect.y = TOP_OFFSET + (int)(((float) tagNumber) * pixerPerTag); if ((tagCnt % 2 == 0) && (tagNumber == (tagCnt/2))) { // avoid obscuring axis rect.y++; } int endY = TOP_OFFSET + (int)(((float)(tagNumber+1)) * pixerPerTag); rect.height = endY - rect.y; } return rect; } protected void paintPageTags(Graphics2D g) { List<TagDocument> tagDocuments = plot.getDocument().getTagDocuments(); int tagCnt = tagDocuments.size(); if (tagCnt == 0) { return; } if (tagRenderer == null) { tagRenderer = new TagRenderer(); } Rectangle clip = g.getClipBounds(); StyledTagSet tagSet; SortedSet<Tag> tagsToDraw; SortedSet<Tag> activeTags = null; TagStyle style; SignalSelectionType type; Component tagRendererComponent; Rectangle tagBounds; int cnt = 0; float start = (float)(clip.x / pixelPerSecond); float end = (float)((clip.x+clip.width) / pixelPerSecond); boolean active; boolean showActivity = (tagDocuments.size() > 1); boolean comparing = plot.getView().isComparingTags(); TagDocument[] comparedTags = null; if (comparing) { comparedTags = plot.getView().getComparedTags(); } for (TagDocument tagDocument : tagDocuments) { active = (tagDocument == plot.getDocument().getActiveTag()); if (compact && !active) { // in compact mode paint only the active tag continue; } if (comparing && tagDocument != comparedTags[0] && tagDocument != comparedTags[1]) { // in comparing mode paint only the compared tags continue; } tagSet = tagDocument.getTagSet(); tagsToDraw = tagSet.getTagsBetween(start, end); if (active) { activeTags = tagsToDraw; } active = (!compact) && showActivity && active; for (Tag tag : tagsToDraw) { style = tag.getStyle(); type = style.getType(); if (type == SignalSelectionType.PAGE) { tagBounds = getPixelPageTagBounds(tag, tagCnt, cnt, comparing, tempBounds); } else { // channel and block tags drawn in the main plot continue; } tagRendererComponent = tagRenderer.getTagRendererComponent(tag.getStyle(), active, false); tagRendererComponent.setBounds(tagBounds); tagRendererComponent.paint(g.create(tagBounds.x, tagBounds.y, tagBounds.width, tagBounds.height)); } cnt++; } // draw differences if (comparing && !compact) { if (tagDifferenceRenderer == null) { tagDifferenceRenderer = new TagDifferenceRenderer(); } TagDifferenceSet differenceSet = plot.getView().getDifferenceSet(); if (differenceSet != null) { SortedSet<TagDifference> differencesToDraw = differenceSet.getDifferencesBetween(start,end); for (TagDifference difference : differencesToDraw) { if (difference.getType() == SignalSelectionType.PAGE) { tagBounds = getPixelPageTagBounds(difference, 0, 2, true, tempBounds); } else { continue; } tagRendererComponent = tagDifferenceRenderer.getTagDifferenceRendererComponent(difference.getDifferenceType()); tagRendererComponent.setBounds(tagBounds); tagRendererComponent.paint(g.create(tagBounds.x, tagBounds.y, tagBounds.width, tagBounds.height)); } } } // draw page tag names if (activeTags != null) { g.setColor(Color.GRAY); g.setFont(font); for (Tag tag : activeTags) { style = tag.getStyle(); type = style.getType(); if (type == SignalSelectionType.PAGE) { int x = (int)(pixelPerSecond * tag.getPosition()); String text = style.getDescriptionOrName(); if (text.length() > 20) { text = text.substring(0, 18) + "..."; } if (compact) { g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g.drawString(text, x + 2, timeScaleY + (2 * fontMetrics.getAscent())); g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF); } else { g.drawString(text, x + 2, timeScaleY - (fontMetrics.getDescent() + 1)); } } else { // do nothing } } } } private void paintSelectedTagSelectionBox(Graphics2D g, PositionedTag tagSelection) { SignalSelectionType type; Component tagRendererComponent; Rectangle tagBounds; int cnt = tagSelection.tagPositionIndex; TagDocument tagDocument = plot.getDocument().getTagDocuments().get(cnt); boolean active = (tagDocument == plot.getDocument().getActiveTag()); if (compact && !active) { return; } type = tagSelection.tag.getType(); if (type == SignalSelectionType.PAGE) { tagBounds = getPixelPageTagBounds(tagSelection.tag, plot.getDocument().getTagDocuments().size(), cnt, plot.getView().isComparingTags(), tempBounds); } else { tagBounds = null; } if (tagBounds != null) { tagRendererComponent = tagRenderer.getTagSelectionRendererComponent(); tagRendererComponent.setBounds(tagBounds); tagRendererComponent.paint(g.create(tagBounds.x, tagBounds.y, tagBounds.width, tagBounds.height)); } } @Override protected void paintComponent(Graphics gOrig) { Graphics2D g = (Graphics2D)gOrig; calculate(g); Point viewportPoint = plot.getViewport().getViewPosition(); Dimension viewportSize = plot.getViewport().getExtentSize(); Dimension size = getSize(); Rectangle clip = g.getClipBounds(); g.setColor(getBackground()); g.fillRect(clip.x,clip.y,clip.width,clip.height); g.setColor(Color.WHITE); g.fillRect(clip.x, TOP_OFFSET, clip.width, pageTagAreaHeight); paintPageTags(g); int clipEndX = clip.x + clip.width - 1; size.height -= SignalPlot.SCALE_TO_SIGNAL_GAP; int i; int x; // this draws second ticks g.setColor(Color.GRAY); g.drawLine(viewportPoint.x, size.height-4, viewportPoint.x+viewportSize.width, size.height-4); int tickCnt = 1 + ((int)(((float)(viewportSize.width+1)) / pixelPerColumnUnit)); for (i=0; i<tickCnt; i++) { x = viewportPoint.x + ((int)(i*pixelPerColumnUnit)); g.drawLine(x, size.height-3, x , size.height-1); } if (pageLinesVisible && pixelPerPage > 4) { // this draws page boundaries int startPage = (int) Math.floor(clip.x / pixelPerPage); if (startPage == 0) { startPage++; } int endPage = (int) Math.ceil(clipEndX / pixelPerPage); g.setColor(Color.RED); for (i=startPage; i <= endPage; i++) { x = (int)(i * pixelPerPage); g.drawLine(x, TOP_OFFSET, x, TOP_OFFSET + pageTagAreaHeight - 1); } } // this draws time axis g.setColor(Color.GRAY); g.setFont(font); g.drawLine(clip.x, timeScaleY, clipEndX, timeScaleY); // XXX this must be offset to prevent parts of labels from disappearing // The offset of "5" is arbitrary // It could be recoded into a clener and well calculated offset int startUnit = (int) Math.max(0, Math.floor(clip.x / pixelPerColumnUnit) - 5); int endUnit = (int) Math.ceil(clipEndX / pixelPerColumnUnit) + 1; int second, minute, hour; Formatter formatter; String label; for (i=startUnit; i <= endUnit; i++) { x = (int)(i * pixelPerColumnUnit); if ((i % 10) != 0) { g.drawLine(x, (compact ? timeScaleY : timeScaleY-1), x, timeScaleY+1); } else { long firstSampleTimestamp = plot.getFirstSampleTimestamp(); g.drawLine(x, (compact ? timeScaleY : timeScaleY-2), x, timeScaleY+2); second = (int) Math.round((i * pixelPerColumnUnit) / pixelPerSecond); if (firstSampleTimestamp > 0) { TimeZone timeZone = Calendar.getInstance().getTimeZone(); // TimeZone API uses timestamp in milliseconds int offset = timeZone.getOffset(1000 * firstSampleTimestamp) / 1000; second = (int) ((firstSampleTimestamp + second + offset) % 86400); } hour = second / 3600; minute = (second % 3600) / 60; second = second % 60; formatter = new Formatter(); if (firstSampleTimestamp > 0 || maxSampleCount / samplingFrequency > 3600) { formatter.format("%02d:", hour); } formatter.format("%02d:%02d", minute, second); label = formatter.toString(); if (compact) { g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); } g.drawString(label, x + 3, timeScaleY + fontMetrics.getAscent() + 1); if (compact) { g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF); } } } if (!compact) { g.setColor(Color.GRAY); g.drawString(columnUnitLabel, viewportPoint.x+3, size.height-6); } PositionedTag tagSelection = plot.getView().getTagSelection(plot); if (tagSelection != null) { paintSelectedTagSelectionBox(g, tagSelection); } } @Override public Dimension getPreferredSize() { calculate((Graphics2D) getGraphics()); return new Dimension((int)(maxSampleCount*timeZoomFactor),componentHeight); } @Override public Dimension getMaximumSize() { return getPreferredSize(); } @Override public Dimension getMinimumSize() { return getPreferredSize(); } @Override public boolean isOpaque() { return true; } @Override public JPopupMenu getComponentPopupMenu() { if (plot.getView().isToolEngaged()) { return null; } if (signalPlotPopupProvider == null) { return null; } return signalPlotPopupProvider.getColumnHeaderPopupMenu(); } @Override public String getToolTipText(MouseEvent event) { if (!plot.isTagToolTipsVisible()) { return null; } Point p = event.getPoint(); PositionedTag tag = getSelectableTagAtPoint(p); if (tag == null) { return null; } String locationMessage = _R("T: {0} [P: {1}, B: {2}]", plot.toTimeSpace(p), plot.toPageSpace(p), plot.toBlockSpace(p)); return plot.getTagToolTip(locationMessage, tag); } // note that this returns the first page tag encountered // this is based on assumption that page tags do not overlap public PositionedTag getSelectableTagAtPoint(Point point) { List<TagDocument> tagDocuments = plot.getDocument().getTagDocuments(); int tagCnt = tagDocuments.size(); if (tagCnt == 0) { return null; } if (point.y < TOP_OFFSET || point.y > (TOP_OFFSET+pageTagAreaHeight)) { return null; } SortedSet<Tag> tagSet = null; float time = plot.toTimeSpace(point); TagDocument pageTagDocument = null; Tag pageTag = null; boolean comparing = plot.getView().isComparingTags(); TagDocument[] comparedTags = null; if (comparing) { comparedTags = plot.getView().getComparedTags(); } int cnt = 0; Rectangle tagBounds; for (TagDocument tagDocument : tagDocuments) { if (compact && (tagDocument != plot.getDocument().getActiveTag())) { // in compact mode scan only the active tag continue; } if (comparing && tagDocument != comparedTags[0] && tagDocument != comparedTags[1]) { // in comparing mode scan only the compared tags continue; } tagSet = tagDocument.getTagSet().getTagsBetween(time, time); for (Tag tag : tagSet) { if (!tag.getStyle().isVisible()) continue; if (tag.getStyle().getType() == SignalSelectionType.PAGE) { if (time >= tag.getPosition() && time < tag.getEndPosition()) { tagBounds = getPixelPageTagBounds(tag, tagCnt, cnt, comparing, tempBounds); if (tagBounds.contains(point)) { pageTag = tag; pageTagDocument = tagDocument; break; } } } } cnt++; } if (pageTag == null) { return null; } else { return new PositionedTag(pageTag, tagDocuments.indexOf(pageTagDocument)); } } public SignalPlotPopupProvider getSignalViewPopupProvider() { return signalPlotPopupProvider; } public void setSignalViewPopupProvider(SignalPlotPopupProvider signalPlotPopupProvider) { this.signalPlotPopupProvider = signalPlotPopupProvider; } public SignalPlot getPlot() { return plot; } public boolean isCompact() { return compact; } public void setCompact(boolean compact) { if (this.compact != compact) { reset(); revalidate(); repaint(); this.compact = compact; getSetCompactAction().putValue(AbstractAction.SELECTED_KEY, compact); } } public SetCompactAction getSetCompactAction() { if (setCompactAction == null) { setCompactAction = new SetCompactAction(); setCompactAction.putValue(AbstractAction.SELECTED_KEY, isCompact()); } return setCompactAction; } public class SetCompactAction extends AbstractAction { private static final long serialVersionUID = 1L; public SetCompactAction() { super(_("Compact mode")); } public void actionPerformed(ActionEvent ev) { Object source = ev.getSource(); if (source instanceof ItemSelectable) { setSelected(((ItemSelectable) source).getSelectedObjects() != null); } } private void setSelected(boolean selected) { if (selected) { // remove any tag-selection if it a page tag and not in active document (otherwise it would disappear) PositionedTag pTag = plot.getView().getTagSelection(plot); if (pTag != null) { if (pTag.tag.getType().isPage()) { TagDocument activeDocument = plot.getDocument().getActiveTag(); if (activeDocument == null || pTag.tagPositionIndex != plot.getDocument().getTagDocuments().indexOf(activeDocument)) { plot.getView().clearTagSelection(); } } } } setCompact(selected); putValue(AbstractAction.SELECTED_KEY, selected); } } }