# HG changeset patch # User Omair Majid # Date 1377705789 14400 # Node ID ba8a0e50ddeeee481b86b068da5856f8bdc90ef1 # Parent 4279c0994a384db655da61b2b35a5d8433c474ea Timeline for JMX Notifications Reviewed-by: jerboaa, neugens Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2013-August/007919.html diff -r 4279c0994a38 -r ba8a0e50ddee client/swing/src/main/java/com/redhat/thermostat/client/swing/GraphicsUtils.java --- a/client/swing/src/main/java/com/redhat/thermostat/client/swing/GraphicsUtils.java Mon Aug 26 18:33:02 2013 +0200 +++ b/client/swing/src/main/java/com/redhat/thermostat/client/swing/GraphicsUtils.java Wed Aug 28 12:03:09 2013 -0400 @@ -62,6 +62,7 @@ public Graphics2D createAAGraphics(Graphics g) { Graphics2D graphics = (Graphics2D) g.create(); graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); return graphics; } diff -r 4279c0994a38 -r ba8a0e50ddee client/swing/src/main/java/com/redhat/thermostat/client/swing/components/BasicEventTimelineUI.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/swing/src/main/java/com/redhat/thermostat/client/swing/components/BasicEventTimelineUI.java Wed Aug 28 12:03:09 2013 -0400 @@ -0,0 +1,465 @@ +/* + * Copyright 2012, 2013 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * . + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.client.swing.components; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Cursor; +import java.awt.FontMetrics; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.GridLayout; +import java.awt.Insets; +import java.awt.Paint; +import java.awt.RenderingHints; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.AdjustmentEvent; +import java.awt.event.AdjustmentListener; +import java.awt.event.HierarchyBoundsListener; +import java.awt.event.HierarchyEvent; +import java.awt.event.HierarchyListener; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.util.concurrent.TimeUnit; + +import javax.swing.JButton; +import javax.swing.JPanel; + +import com.redhat.thermostat.client.swing.components.EventTimelineModel.Event; +import com.redhat.thermostat.client.swing.components.timeline.Timeline; +import com.redhat.thermostat.client.swing.internal.LocaleResources; +import com.redhat.thermostat.common.model.LongRangeNormalizer; +import com.redhat.thermostat.common.model.Range; +import com.redhat.thermostat.shared.locale.Translate; + +public class BasicEventTimelineUI extends EventTimelineUI { + + private static final Translate translate = LocaleResources.createLocalizer(); + + private static final Color DEFAULT_FILL_COLOR = new Color(0,0,0,255); + private static final Color DEFAULT_EDGE_COLOR = Color.BLACK; + private static final Color DEFAULT_MARKER_COLOR = Color.BLACK; + + private EventTimeline eventTimeline; + + private OverviewPanel overviewPanel = new OverviewPanel(); + private Timeline overviewRuler; + + private Refresher refresher = new Refresher(overviewPanel); + private JButton moveLeftButton; + private JButton moveRightButton; + private JButton zoomInButton; + private JButton zoomOutButton; + private JButton resetZoomButton; + + @Override + protected void installComponents(EventTimeline component) { + eventTimeline = component; + + overviewRuler = new Timeline(new Range(1l, 2l)); + + moveLeftButton = new JButton("<"); + moveLeftButton.setMargin(new Insets(0, 0, 0, 0)); + moveRightButton = new JButton(">"); + moveRightButton.setMargin(new Insets(0, 0, 0, 0)); + + JPanel buttonPanel = new JPanel(); + buttonPanel.setLayout(new GridLayout()); + + zoomInButton = new JButton("+"); + zoomInButton.setToolTipText(translate.localize(LocaleResources.ZOOM_IN).getContents()); + zoomInButton.setMargin(new Insets(2, 2, 2, 2)); + + buttonPanel.add(zoomInButton); + + zoomOutButton = new JButton("-"); + zoomOutButton.setToolTipText(translate.localize(LocaleResources.ZOOM_OUT).getContents()); + zoomOutButton.setMargin(new Insets(2, 2, 2, 2)); + buttonPanel.add(zoomOutButton); + + resetZoomButton = new JButton("R"); + resetZoomButton.setToolTipText(translate.localize(LocaleResources.RESET_ZOOM).getContents()); + resetZoomButton.setMargin(new Insets(2, 2, 2, 2)); + buttonPanel.add(resetZoomButton); + + GridBagLayout layout = new GridBagLayout(); + component.setLayout(layout); + overviewPanel.setLayout(layout); + + GridBagConstraints c = new GridBagConstraints(); + + c.gridx = 0; + c.gridy = 0; + c.fill = GridBagConstraints.VERTICAL; + c.weightx = 0; + c.weighty = 1; + + component.add(moveLeftButton, c); + + c.gridx = 1; + c.gridy = 0; + c.fill = GridBagConstraints.BOTH; + c.weighty = 1.0; + c.weightx = 1.0; + + component.add(overviewPanel, c); + + c.gridy++; + c.fill = GridBagConstraints.HORIZONTAL; + c.weighty = 0; + component.add(overviewRuler, c); + + c.gridx = 2; + c.gridy = 0; + c.fill = GridBagConstraints.VERTICAL; + c.weighty = 1; + c.weightx = 0; + component.add(moveRightButton, c); + + c.gridx = 0; + c.gridy = 3; + c.fill = GridBagConstraints.VERTICAL; + c.anchor = GridBagConstraints.LINE_START; + c.weightx = 1; + c.weighty = 0; + c.gridwidth = 4; + component.add(buttonPanel, c); + } + + @Override + protected void installDefaults(EventTimeline c) { + c.setSelectionEdgePaint(DEFAULT_EDGE_COLOR); + c.setSelectionFillPaint(DEFAULT_FILL_COLOR); + c.setEventPaint(DEFAULT_MARKER_COLOR); + } + + @Override + protected void installListeners(EventTimeline c) { + c.addHierarchyBoundsListener(refresher); + c.addHierarchyListener(refresher); + c.getModel().addDataChangeListener(refresher); + c.getModel().addRangeChangeListener(new EventTimelineRangeChangeListener() { + @Override + public void rangeChanged(Range overview, Range detail) { + overviewRuler.setRange(overview); + overviewRuler.repaint(); + + } + }); + moveRightButton.addActionListener(new DetailChangeListener() { + @Override + protected Range computeNewDetailRange(long min, long max) { + long diff = (long) ((max - min) * 0.1); + return new Range<>(min + diff, max + diff); + } + }); + moveLeftButton.addActionListener(new DetailChangeListener() { + protected Range computeNewDetailRange(long min, long max) { + long diff = (long) ((max - min) * 0.1); + return new Range<>(min - diff, max - diff); + }; + }); + zoomOutButton.addActionListener(new DetailChangeListener() { + protected Range computeNewDetailRange(long min, long max) { + long diff = max - min; + return new Range<>(min - diff / 2, max + diff / 2); + }; + }); + + zoomInButton.addActionListener(new DetailChangeListener() { + protected Range computeNewDetailRange(long min, long max) { + long diff = max - min; + return new Range<>(min + diff / 4, max - diff / 4); + }; + }); + + resetZoomButton.addActionListener(new DetailChangeListener() { + @Override + protected Range computeNewDetailRange(long min, long max) { + long timeDelta = max - min; + + long tenMinutesInMillis = TimeUnit.MINUTES.toMillis(10); + + if (timeDelta <= tenMinutesInMillis) { + return new Range<>(min, max); + } else { + return new Range<>(max - tenMinutesInMillis, max); + } + } + }); + } + + private abstract class DetailChangeListener implements ActionListener { + @Override + public void actionPerformed(ActionEvent arg0) { + Range range = eventTimeline.getModel().getDetailRange(); + + long min = range.getMin(); + long max = range.getMax(); + + eventTimeline.getModel().setDetailRange(computeNewDetailRange(min, max)); + + overviewPanel.refresh(); + } + + protected abstract Range computeNewDetailRange(long min, long max); + } + + protected void uninstallListeners(EventTimeline c) { + c.removeHierarchyBoundsListener(refresher); + c.removeHierarchyListener(refresher); + } + + @Override + protected void uninstallComponents(EventTimeline c) { + c.remove(overviewPanel); + + eventTimeline = null; + } + + private long positionToTimeStamp(int position) { + Range range = eventTimeline.getModel().getTotalRange(); + LongRangeNormalizer normalizer = new LongRangeNormalizer(new Range<>(0l, (long)overviewPanel.getWidth()), range); + long result = normalizer.getValueNormalized(position); + return result; + } + + private int timeStampToPosition(long timeStamp) { + Range range = eventTimeline.getModel().getTotalRange(); + LongRangeNormalizer normalizer = new LongRangeNormalizer(range, new Range<>(0l, (long)overviewPanel.getWidth())); + int result = (int) normalizer.getValueNormalized(timeStamp); + return result; + } + + private static class Refresher implements HierarchyBoundsListener, HierarchyListener, AdjustmentListener, EventTimelineDataChangeListener { + + private OverviewPanel toRefresh; + + public Refresher(OverviewPanel toRefresh) { + this.toRefresh = toRefresh; + } + + @Override + public void dataChanged() { + refresh(); + } + + @Override + public void adjustmentValueChanged(AdjustmentEvent e) { + refresh(); + } + + @Override + public void ancestorMoved(HierarchyEvent e) { + refresh(); + } + + @Override + public void ancestorResized(HierarchyEvent e) { + refresh(); + } + + @Override + public void hierarchyChanged(HierarchyEvent e) { + refresh(); + } + + private void refresh() { + toRefresh.refresh(); + } + } + + private class OverviewPanel extends JPanel { + + private int MOUSE_MARGIN = 10; + + private int left; + private int right; + + private boolean moving = false; + private boolean movingLeft = false; + private boolean movingRight = false; + + private int oldX = -1; + private int oldLeft = -1; + private int oldRight = -1; + + public OverviewPanel() { + OverviewMotionListener chartMotionListener = new OverviewMotionListener(); + addMouseMotionListener(chartMotionListener); + addMouseListener(chartMotionListener); + } + + public void refresh() { + recomputeBars(); + repaint(); + } + + private void recomputeBars() { + Range range = eventTimeline.getModel().getDetailRange(); + if (range != null) { + left = timeStampToPosition(range.getMin()); + right = timeStampToPosition(range.getMax()); + } + } + + @Override + protected void paintComponent(Graphics g) { + super.paintComponent(g); + + int width = (right - left); + g.clearRect(0, 0, getWidth(), getHeight()); + + Graphics2D g2 = (Graphics2D) g; + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + EventTimelineModel model = eventTimeline.getModel(); + + int i = 0; + for (Event event : model.getEvents()) { + paintEvent(g2, event, i); + i++; + } + + Paint fillColor = eventTimeline.getSelectionFillPaint(); + g2.setPaint(fillColor); + g2.fillRect(left, 1, width, getHeight() - 2); + + g2.setStroke(new BasicStroke(2)); + Paint edgeColor = eventTimeline.getSelectionEdgePaint(); + g2.setPaint(edgeColor); + g2.drawRect(left, 1, width, getHeight() - 2); + + g2.dispose(); + } + + private void paintEvent(Graphics2D g, Event event, int count) { + int y = getYBandPosition(count); + int x = timeStampToPosition(event.getTimeStamp()); + paintEvent(g, event.getDescription(), x, y); + } + + private int getYBandPosition(int step) { + // TODO can we do better in determining the number of 'bands' ? + int TOTAL_STEPS = 10; + step = step % TOTAL_STEPS + 1; + + return Math.round(1.0f * step * getHeight() / TOTAL_STEPS); + } + + private void paintEvent(Graphics2D g, String text, int x, int y) { + g = (Graphics2D) g.create(); + + FontMetrics metrics = g.getFontMetrics(); + int descent = metrics.getDescent(); + int stringWidth = (int) Math.round(metrics.getStringBounds(text, g).getWidth()); + + g.setPaint(eventTimeline.getEventPaint()); + g.drawLine(x, getHeight(), x, y + descent); + g.drawLine(x, y + descent, x + stringWidth, y + descent); + g.drawString(text, x, y); + + g.dispose(); + } + + class OverviewMotionListener extends MouseAdapter { + @Override + public void mouseMoved(MouseEvent e) { + if (Math.abs(e.getX() - left) < MOUSE_MARGIN) { + setCursor(Cursor.getPredefinedCursor(Cursor.W_RESIZE_CURSOR)); + } else if (Math.abs(e.getX() - right) < MOUSE_MARGIN) { + setCursor(Cursor.getPredefinedCursor(Cursor.E_RESIZE_CURSOR)); + } else if ((e.getX() > MOUSE_MARGIN + left) && (e.getX() < right - MOUSE_MARGIN)) { + setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR)); + } else { + setCursor(Cursor.getDefaultCursor()); + } + + } + + @Override + public void mouseDragged(MouseEvent e) { + if (moving || movingLeft || movingRight) { + + int newLeft = oldLeft; + int newRight = oldRight; + if (movingLeft) { + newLeft = e.getX(); + } else if (movingRight) { + newRight = e.getX(); + } else if (moving) { + long delta = e.getX() - oldX; + newLeft += delta; + newRight += delta; + } + + Range range = new Range(positionToTimeStamp(newLeft), positionToTimeStamp(newRight)); + + eventTimeline.getModel().setDetailRange(range); + + refresh(); + } + } + + @Override + public void mouseReleased(MouseEvent e) { + moving = movingLeft = movingRight = false; + oldLeft = oldRight = oldX = -1; + } + + @Override + public void mousePressed(MouseEvent e) { + if (Math.abs(e.getX() - left) < MOUSE_MARGIN) { + movingLeft = true; + } else if (Math.abs(e.getX() - right) < MOUSE_MARGIN) { + movingRight = true; + } else if ((e.getX() > left + MOUSE_MARGIN) && (e.getX() < right - MOUSE_MARGIN)) { + moving = true; + } + Range range = eventTimeline.getModel().getDetailRange(); + oldLeft = timeStampToPosition(range.getMin()); + oldRight = timeStampToPosition(range.getMax()); + oldX = e.getX(); + } + + // TODO implement wheel scrolling + } + } +} diff -r 4279c0994a38 -r ba8a0e50ddee client/swing/src/main/java/com/redhat/thermostat/client/swing/components/EventTimeline.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/swing/src/main/java/com/redhat/thermostat/client/swing/components/EventTimeline.java Wed Aug 28 12:03:09 2013 -0400 @@ -0,0 +1,105 @@ +/* + * Copyright 2012, 2013 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * . + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.client.swing.components; + +import java.awt.Paint; + +import javax.swing.JComponent; +import javax.swing.UIManager; + +public class EventTimeline extends JComponent { + + private static final String uiClassID = "EventTimelineUI"; + + private EventTimelineModel model = new EventTimelineModel(); + + private Paint selectionEdgePaint = null; + private Paint selectionFillPaint = null; + private Paint eventPaint; + + public EventTimeline() { + updateUI(); + } + + public void setUI(EventTimelineUI newUI) { + super.setUI(newUI); + } + + @Override + public void updateUI() { + if (UIManager.get(getUIClassID()) != null) { + setUI((EventTimelineUI) UIManager.getUI(this)); + } else { + setUI(new BasicEventTimelineUI()); + } + } + + @Override + public String getUIClassID() { + return uiClassID; + } + + public EventTimelineModel getModel() { + return model; + } + + public Paint getSelectionEdgePaint() { + return selectionEdgePaint; + } + + public void setSelectionEdgePaint(Paint edgePaint) { + this.selectionEdgePaint = edgePaint; + } + + public Paint getSelectionFillPaint() { + return selectionFillPaint; + } + + public void setSelectionFillPaint(Paint fillPaint) { + this.selectionFillPaint = fillPaint; + } + + public void setEventPaint(Paint eventPaint) { + this.eventPaint = eventPaint; + } + + public Paint getEventPaint() { + return eventPaint; + } + + +} diff -r 4279c0994a38 -r ba8a0e50ddee client/swing/src/main/java/com/redhat/thermostat/client/swing/components/EventTimelineDataChangeListener.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/swing/src/main/java/com/redhat/thermostat/client/swing/components/EventTimelineDataChangeListener.java Wed Aug 28 12:03:09 2013 -0400 @@ -0,0 +1,43 @@ +/* + * Copyright 2012, 2013 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * . + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.client.swing.components; + +public interface EventTimelineDataChangeListener { + + void dataChanged(); + +} diff -r 4279c0994a38 -r ba8a0e50ddee client/swing/src/main/java/com/redhat/thermostat/client/swing/components/EventTimelineModel.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/swing/src/main/java/com/redhat/thermostat/client/swing/components/EventTimelineModel.java Wed Aug 28 12:03:09 2013 -0400 @@ -0,0 +1,167 @@ +/* + * Copyright 2012, 2013 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * . + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.client.swing.components; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.TimeUnit; + +import com.redhat.thermostat.common.model.Range; + +/** + * A time line with an total and a detail range. + */ +public class EventTimelineModel { + + private final List rangeListeners = new CopyOnWriteArrayList<>(); + private final List dataListeners = new CopyOnWriteArrayList<>(); + + private Range totalRange; + private Range detailRange;; + private List events = new ArrayList<>(); + + private boolean isAdjusting = false; + + public Range getTotalRange() { + return totalRange; + } + + private void setTotalRange(Range newRange) { + if (totalRange != null && totalRange.equals(newRange)) { + return; + } + + this.totalRange = newRange; + fireRangeChanged(); + } + + public void addEvent(long eventTimeStamp, String description) { + addEvent(new Event(eventTimeStamp, description)); + } + + public void addEvent(Event event) { + long eventTimeStamp = event.getTimeStamp(); + + if (totalRange == null) { + // some heuristics to get sane initial ranges automagically + setTotalRange(new Range<>(eventTimeStamp - TimeUnit.MINUTES.toMillis(1), eventTimeStamp + TimeUnit.MINUTES.toMillis(1))); + setDetailRange(new Range<>(eventTimeStamp - TimeUnit.MINUTES.toMillis(1), eventTimeStamp + TimeUnit.MINUTES.toMillis(1))); + } else { + long delta = (long) ((totalRange.getMax() - totalRange.getMin()) * 0.1); + + if (totalRange.getMax() < eventTimeStamp + delta) { + setTotalRange(new Range<>(totalRange.getMin(), eventTimeStamp + delta)); + } else if (totalRange.getMin() + delta > eventTimeStamp) { + setTotalRange(new Range<>(eventTimeStamp - delta, totalRange.getMax())); + } + } + + events.add(event); + fireEventDataChanged(); + } + + public List getEvents() { + return events; + } + + public void clearEvents() { + events.clear(); + } + + public void setDetailRange(Range range) { + if (detailRange != null && detailRange.equals(range)) { + return; + } + + this.detailRange = range; + + fireRangeChanged(); + } + + public Range getDetailRange() { + return detailRange; + } + + public void addRangeChangeListener(EventTimelineRangeChangeListener listener) { + rangeListeners.add(listener); + } + + public void removeRangeChangeListener(EventTimelineRangeChangeListener listener) { + rangeListeners.remove(listener); + } + + private void fireRangeChanged() { + for (EventTimelineRangeChangeListener listener : rangeListeners) { + listener.rangeChanged(this.totalRange, this.detailRange); + } + } + + public void addDataChangeListener(EventTimelineDataChangeListener listener) { + dataListeners.add(listener); + } + + public void removeEventDataChangeListener(EventTimelineDataChangeListener listener) { + dataListeners.remove(listener); + } + + private void fireEventDataChanged() { + for (EventTimelineDataChangeListener listener : dataListeners) { + listener.dataChanged(); + } + } + + public static class Event { + + private final long timeStamp; + private final String description; + + public Event(long timeStamp, String description) { + this.timeStamp = timeStamp; + this.description = description; + } + + public long getTimeStamp() { + return timeStamp; + } + + public String getDescription() { + return description; + } + } + +} diff -r 4279c0994a38 -r ba8a0e50ddee client/swing/src/main/java/com/redhat/thermostat/client/swing/components/EventTimelineRangeChangeListener.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/swing/src/main/java/com/redhat/thermostat/client/swing/components/EventTimelineRangeChangeListener.java Wed Aug 28 12:03:09 2013 -0400 @@ -0,0 +1,45 @@ +/* + * Copyright 2012, 2013 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * . + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.client.swing.components; + +import com.redhat.thermostat.common.model.Range; + +public interface EventTimelineRangeChangeListener { + + void rangeChanged(Range overview, Range detail); + +} \ No newline at end of file diff -r 4279c0994a38 -r ba8a0e50ddee client/swing/src/main/java/com/redhat/thermostat/client/swing/components/EventTimelineUI.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/swing/src/main/java/com/redhat/thermostat/client/swing/components/EventTimelineUI.java Wed Aug 28 12:03:09 2013 -0400 @@ -0,0 +1,71 @@ +/* + * Copyright 2012, 2013 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * . + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.client.swing.components; + +import javax.swing.JComponent; +import javax.swing.plaf.ComponentUI; + +public abstract class EventTimelineUI extends ComponentUI { + + public void installUI(JComponent c) { + EventTimeline zc = (EventTimeline) c; + installComponents(zc); + installDefaults(zc); + installListeners(zc); + } + + protected void installComponents(EventTimeline c) {} + + protected void installDefaults(EventTimeline c) {} + + protected void installListeners(EventTimeline c) {} + + public void uninstallUI(JComponent c) { + EventTimeline zc = (EventTimeline) c; + + uninstallListeners(zc); + uninstallDefaults(zc); + uninstallComponents(zc); + } + + protected void uninstallListeners(EventTimeline c) {} + + protected void uninstallDefaults(EventTimeline c) {} + + protected void uninstallComponents(EventTimeline c) {} + +} diff -r 4279c0994a38 -r ba8a0e50ddee client/swing/src/main/java/com/redhat/thermostat/client/swing/components/LocalizedLabel.java --- a/client/swing/src/main/java/com/redhat/thermostat/client/swing/components/LocalizedLabel.java Mon Aug 26 18:33:02 2013 +0200 +++ b/client/swing/src/main/java/com/redhat/thermostat/client/swing/components/LocalizedLabel.java Wed Aug 28 12:03:09 2013 -0400 @@ -43,7 +43,8 @@ public class LocalizedLabel extends JLabel { public LocalizedLabel(LocalizedString text) { - super(text.getContents()); + // enable word-wrapping + super("" + text.getContents() + ""); } } diff -r 4279c0994a38 -r ba8a0e50ddee client/swing/src/main/java/com/redhat/thermostat/client/swing/components/timeline/Timeline.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/swing/src/main/java/com/redhat/thermostat/client/swing/components/timeline/Timeline.java Wed Aug 28 12:03:09 2013 -0400 @@ -0,0 +1,233 @@ +/* + * Copyright 2012, 2013 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * . + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.client.swing.components.timeline; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.GradientPaint; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Paint; +import java.awt.Rectangle; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import com.redhat.thermostat.client.swing.GraphicsUtils; +import com.redhat.thermostat.client.swing.components.GradientPanel; +import com.redhat.thermostat.client.ui.Palette; +import com.redhat.thermostat.common.model.LongRangeNormalizer; +import com.redhat.thermostat.common.model.Range; + +/** + * Displays a timeline between the specified start and stop ranges, dynamically + * adjusting itself as needed. The values of the ranges are interpreted as + * timestamps in milliseconds. + */ +@SuppressWarnings("serial") +public class Timeline extends GradientPanel { + + /** Default height of this component. Subclasses may use different values */ + public static final int DEFAULT_HEIGHT = 25; + + private Range range; + + public Timeline(Range range) { + + super(Palette.LIGHT_GRAY.getColor(), Palette.WHITE.getColor()); + setFont(TimelineUtils.FONT); + + this.range = range; + } + + public Range getRange() { + return range; + } + + public void setRange(Range newRange) { + this.range = newRange; + repaint(); + } + + @Override + public int getHeight() { + return DEFAULT_HEIGHT; + } + + @Override + public Dimension getMaximumSize() { + Dimension dim = super.getMaximumSize(); + dim.height = getHeight(); + return dim; + } + + @Override + public Dimension getMinimumSize() { + Dimension dim = super.getMinimumSize(); + dim.height = getHeight(); + return dim; + } + + @Override + public Dimension getPreferredSize() { + Dimension dim = super.getPreferredSize(); + dim.height = getHeight(); + return dim; + } + + @Override + public Dimension getSize() { + return getPreferredSize(); + } + + @Override + protected void paintComponent(Graphics g) { + super.paintComponent(g); + + if (range == null) { + return; + } + + Graphics2D graphics = GraphicsUtils.getInstance().createAAGraphics(g); + + Rectangle bounds = g.getClipBounds(); + + TimeUnit timeUnitForTicks = getBestTimeUnit(); + + drawTicks(graphics, bounds, timeUnitForTicks); + + graphics.setColor(Palette.THERMOSTAT_BLU.getColor()); + graphics.drawLine(bounds.x, bounds.height - 1, bounds.width, bounds.height - 1); + + graphics.dispose(); + } + + /** + * Returns a TimeUnit and the number of subticks needed for that TimeUnit + * need to display the current range + */ + private TimeUnit getBestTimeUnit() { + long min = range.getMin(); + long max = range.getMax(); + + List units = new ArrayList<>(); + units.add(TimeUnit.DAYS); + units.add(TimeUnit.HOURS); + units.add(TimeUnit.MINUTES); + units.add(TimeUnit.SECONDS); + units.add(TimeUnit.MILLISECONDS); + + /* Find the largest unit of time suitable for the range */ + for (TimeUnit unit: units) { + long millis = unit.toMillis(1); + if (Math.abs(max - min) >= millis) { + return unit; + } + } + + return null; + } + + private void drawTicks(Graphics2D graphics, Rectangle bounds, TimeUnit tickUnit) { + Font font = graphics.getFont(); + + int widthOfOneCharacter = (int) font.getStringBounds("0", graphics.getFontRenderContext()).getWidth(); + + long deltaInMilliseconds = Math.max(1, tickUnit.toMillis(1) / 10); + + // some heuristics for spacing + int targetNumberOfIntervals = (int) (getWidth() / (10 * widthOfOneCharacter) / 1.3); + + while ((range.getMax() - range.getMin()) / deltaInMilliseconds > targetNumberOfIntervals) { + deltaInMilliseconds *= 2; + + } + + long start = (range.getMin() / deltaInMilliseconds) * deltaInMilliseconds; + long end = range.getMax(); + + DateFormat df = null; + + switch (tickUnit) { + case DAYS: + df = new SimpleDateFormat("YY-MM-dd"); + break; + case HOURS: + df = new SimpleDateFormat("hh:mm a"); + break; + case MINUTES: + df = new SimpleDateFormat("hh:mm a"); + break; + case SECONDS: + df = new SimpleDateFormat("mm.ss"); + break; + default: + df = new SimpleDateFormat("hh:mm:ss a"); + } + + Paint gradient = new GradientPaint(0, 0, Palette.WHITE.getColor(), 0, + getHeight(), Palette.GRAY.getColor()); + + LongRangeNormalizer normalizer = new LongRangeNormalizer(range, new Range(0l, (long)bounds.width)); + + for (long i = start; i < end; i += deltaInMilliseconds) { + int x = (int) normalizer.getValueNormalized(i); + + graphics.setColor(Palette.THERMOSTAT_BLU.getColor()); + graphics.drawLine(x, 0, x, bounds.height); + + graphics.setPaint(gradient); + + String value = df.format(new Date(i)); + + int stringWidth = (int) font.getStringBounds(value, + graphics.getFontRenderContext()).getWidth() - 1; + int stringHeight = (int) font.getStringBounds(value, + graphics.getFontRenderContext()).getHeight(); + graphics.fillRect(x + 1, bounds.y + 5, stringWidth + 4, stringHeight + + 4); + + graphics.setColor(Color.BLACK /* Palette.THERMOSTAT_BLU.getColor() */); + graphics.drawString(value, x + 1, bounds.y + stringHeight + 5); + } + } + +} diff -r 4279c0994a38 -r ba8a0e50ddee client/swing/src/main/java/com/redhat/thermostat/client/swing/components/timeline/TimelineRulerHeader.java --- a/client/swing/src/main/java/com/redhat/thermostat/client/swing/components/timeline/TimelineRulerHeader.java Mon Aug 26 18:33:02 2013 +0200 +++ b/client/swing/src/main/java/com/redhat/thermostat/client/swing/components/timeline/TimelineRulerHeader.java Wed Aug 28 12:03:09 2013 -0400 @@ -88,7 +88,7 @@ public Range getRange() { return range; } - + @Override public int getHeight() { return DEFAULT_HEIGHT; @@ -136,7 +136,7 @@ int unitIncrement = getUnitIncrementInPixels(); - TimelineUtils.drawMarks(range, graphics, bounds, currentValue, false, unitIncrement); + TimelineUtils.drawMarks(graphics, bounds, currentValue, false, unitIncrement); drawTimelineStrings(graphics, currentValue, bounds, unitIncrement); graphics.setColor(Palette.THERMOSTAT_BLU.getColor()); diff -r 4279c0994a38 -r ba8a0e50ddee client/swing/src/main/java/com/redhat/thermostat/client/swing/components/timeline/TimelineUtils.java --- a/client/swing/src/main/java/com/redhat/thermostat/client/swing/components/timeline/TimelineUtils.java Mon Aug 26 18:33:02 2013 +0200 +++ b/client/swing/src/main/java/com/redhat/thermostat/client/swing/components/timeline/TimelineUtils.java Wed Aug 28 12:03:09 2013 -0400 @@ -41,12 +41,11 @@ import java.awt.Rectangle; import com.redhat.thermostat.client.ui.Palette; -import com.redhat.thermostat.common.model.Range; public class TimelineUtils { public static final Font FONT = new Font("SansSerif", Font.PLAIN, 10); - public static void drawMarks(Range range, Graphics2D graphics, Rectangle bounds, + public static void drawMarks(Graphics2D graphics, Rectangle bounds, int currentValue, boolean darkerTop, int increment) { int inc = currentValue % increment; diff -r 4279c0994a38 -r ba8a0e50ddee client/swing/src/main/java/com/redhat/thermostat/client/swing/internal/LocaleResources.java --- a/client/swing/src/main/java/com/redhat/thermostat/client/swing/internal/LocaleResources.java Mon Aug 26 18:33:02 2013 +0200 +++ b/client/swing/src/main/java/com/redhat/thermostat/client/swing/internal/LocaleResources.java Wed Aug 28 12:03:09 2013 -0400 @@ -42,6 +42,10 @@ HOST_PRIMARY_STATUS, VM_PRIMARY_STATUS, + + ZOOM_IN, + ZOOM_OUT, + RESET_ZOOM, ; static final String RESOURCE_BUNDLE = diff -r 4279c0994a38 -r ba8a0e50ddee client/swing/src/main/resources/com/redhat/thermostat/client/swing/internal/strings.properties --- a/client/swing/src/main/resources/com/redhat/thermostat/client/swing/internal/strings.properties Mon Aug 26 18:33:02 2013 +0200 +++ b/client/swing/src/main/resources/com/redhat/thermostat/client/swing/internal/strings.properties Wed Aug 28 12:03:09 2013 -0400 @@ -1,2 +1,6 @@ HOST_PRIMARY_STATUS = host: {0}, id: {1} VM_PRIMARY_STATUS = vm: {0}, pid: {1}, host: {2} + +ZOOM_IN = Zoom In +ZOOM_OUT = Zoom Out +RESET_ZOOM = Reset Zoom \ No newline at end of file diff -r 4279c0994a38 -r ba8a0e50ddee client/swing/src/test/java/com/redhat/thermostat/client/swing/components/EventTimelineModelTest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/client/swing/src/test/java/com/redhat/thermostat/client/swing/components/EventTimelineModelTest.java Wed Aug 28 12:03:09 2013 -0400 @@ -0,0 +1,130 @@ +/* + * Copyright 2012, 2013 Red Hat, Inc. + * + * This file is part of Thermostat. + * + * Thermostat is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published + * by the Free Software Foundation; either version 2, or (at your + * option) any later version. + * + * Thermostat is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Thermostat; see the file COPYING. If not see + * . + * + * Linking this code with other modules is making a combined work + * based on this code. Thus, the terms and conditions of the GNU + * General Public License cover the whole combination. + * + * As a special exception, the copyright holders of this code give + * you permission to link this code with independent modules to + * produce an executable, regardless of the license terms of these + * independent modules, and to copy and distribute the resulting + * executable under terms of your choice, provided that you also + * meet, for each linked independent module, the terms and conditions + * of the license of that module. An independent module is a module + * which is not derived from or based on this code. If you modify + * this code, you may extend this exception to your version of the + * library, but you are not obligated to do so. If you do not wish + * to do so, delete this exception statement from your version. + */ + +package com.redhat.thermostat.client.swing.components; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.isA; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import java.util.Arrays; + +import org.junit.Test; + +import com.redhat.thermostat.client.swing.components.EventTimelineModel.Event; +import com.redhat.thermostat.common.model.Range; + +public class EventTimelineModelTest { + + @Test + public void testEvent() { + EventTimelineModel model = new EventTimelineModel(); + assertEquals(0, model.getEvents().size()); + + model.addEvent(0, "event1"); + + assertEquals(1, model.getEvents().size()); + } + + @Test + public void testAddEventObjects() { + EventTimelineModel model = new EventTimelineModel(); + assertEquals(0, model.getEvents().size()); + + Event event1 = new EventTimelineModel.Event(0, "event1"); + Event event2 = new EventTimelineModel.Event(2, "event2"); + model.addEvent(event1); + model.addEvent(event2); + + assertEquals(Arrays.asList(event1, event2), model.getEvents()); + } + + @Test + public void testGetTotalRange() { + long START = 0; + long END = 2; + + EventTimelineModel model = new EventTimelineModel(); + + Event event1 = new EventTimelineModel.Event(START, "event1"); + Event event2 = new EventTimelineModel.Event(END, "event2"); + model.addEvent(event1); + model.addEvent(event2); + + assertTrue(model.getTotalRange().getMin() <= START); + assertTrue(model.getTotalRange().getMax() >= END); + } + + @Test + public void testDataListeners() { + long START = 0; + long END = 2; + + EventTimelineDataChangeListener listener = mock(EventTimelineDataChangeListener.class); + + EventTimelineModel model = new EventTimelineModel(); + model.addDataChangeListener(listener); + + Event event1 = new EventTimelineModel.Event(START, "event1"); + Event event2 = new EventTimelineModel.Event(END, "event2"); + model.addEvent(event1); + model.addEvent(event2); + + verify(listener, times(2)).dataChanged(); + } + + @Test + public void testRangeListeners() { + long START = 0; + long END = 20000000; + + EventTimelineRangeChangeListener listener = mock(EventTimelineRangeChangeListener.class); + + EventTimelineModel model = new EventTimelineModel(); + model.addRangeChangeListener(listener); + + Event event1 = new EventTimelineModel.Event(START, "event1"); + Event event2 = new EventTimelineModel.Event(END, "event2"); + + model.addEvent(event1); + model.addEvent(event2); + + verify(listener, times(2)).rangeChanged(isA(Range.class), isA(Range.class)); + } +} diff -r 4279c0994a38 -r ba8a0e50ddee common/core/src/main/java/com/redhat/thermostat/common/model/LongRangeNormalizer.java --- a/common/core/src/main/java/com/redhat/thermostat/common/model/LongRangeNormalizer.java Mon Aug 26 18:33:02 2013 +0200 +++ b/common/core/src/main/java/com/redhat/thermostat/common/model/LongRangeNormalizer.java Wed Aug 28 12:03:09 2013 -0400 @@ -49,8 +49,6 @@ private long maxNormalized; - private long value; - private Range range; public LongRangeNormalizer(Range range) { @@ -77,13 +75,6 @@ this.minNormalized = minNormalized; } - public long getValue() { - return value; - } - - public void setValue(long newValue) { - this.value = newValue; - } public long getMaxNormalized() { return maxNormalized; @@ -93,7 +84,7 @@ return minNormalized; } - public long getValueNormalized() { + public long getValueNormalized(long value) { double normalized = ((value - range.min) * (double)(maxNormalized - minNormalized)/(range.max - range.min)) + minNormalized; return Math.round(normalized); } diff -r 4279c0994a38 -r ba8a0e50ddee common/core/src/test/java/com/redhat/thermostat/common/model/LongRangeNormalizerTest.java --- a/common/core/src/test/java/com/redhat/thermostat/common/model/LongRangeNormalizerTest.java Mon Aug 26 18:33:02 2013 +0200 +++ b/common/core/src/test/java/com/redhat/thermostat/common/model/LongRangeNormalizerTest.java Wed Aug 28 12:03:09 2013 -0400 @@ -46,35 +46,17 @@ public class LongRangeNormalizerTest { @Test - public void testSameRange() { - - Range range = new Range<>(0l, 10l); - - LongRangeNormalizer model = new LongRangeNormalizer(range); - - model.setValue(5); - - model.setMaxNormalized(10); - model.setMinNormalized(0); - - - Assert.assertEquals((int) model.getValue(), model.getValueNormalized()); - } - - @Test public void testDoubleRange() { Range range = new Range<>(0l, 10l); LongRangeNormalizer model = new LongRangeNormalizer(range); - model.setValue(5); - model.setMaxNormalized(20); model.setMinNormalized(0); - Assert.assertEquals(10, model.getValueNormalized()); + Assert.assertEquals(10, model.getValueNormalized(5)); } @Test @@ -84,34 +66,30 @@ LongRangeNormalizer model = new LongRangeNormalizer(range); - model.setValue(5); - model.setMaxNormalized(40); model.setMinNormalized(0); - Assert.assertEquals(20, model.getValueNormalized()); + Assert.assertEquals(20, model.getValueNormalized(5)); model.setMaxNormalized(60); model.setMinNormalized(0); - Assert.assertEquals(30, model.getValueNormalized()); + Assert.assertEquals(30, model.getValueNormalized(5)); model.setMaxNormalized(200); model.setMinNormalized(100); - Assert.assertEquals(150, model.getValueNormalized()); + Assert.assertEquals(150, model.getValueNormalized(5)); range.setMax(100l); range.setMin(0l); - model.setValue(50); model.setMaxNormalized(1); model.setMinNormalized(0); - Assert.assertEquals(1, model.getValueNormalized()); + Assert.assertEquals(1, model.getValueNormalized(50)); - model.setValue(49); - Assert.assertEquals(0, model.getValueNormalized()); + Assert.assertEquals(0, model.getValueNormalized(49)); } } diff -r 4279c0994a38 -r ba8a0e50ddee thread/client-swing/src/main/java/com/redhat/thermostat/thread/client/swing/impl/timeline/TimelineComponent.java --- a/thread/client-swing/src/main/java/com/redhat/thermostat/thread/client/swing/impl/timeline/TimelineComponent.java Mon Aug 26 18:33:02 2013 +0200 +++ b/thread/client-swing/src/main/java/com/redhat/thermostat/thread/client/swing/impl/timeline/TimelineComponent.java Wed Aug 28 12:03:09 2013 -0400 @@ -107,7 +107,7 @@ int currentValue = scrollPane.getHorizontalScrollBar().getValue(); int totalInc = pixelUnitIncrement; - TimelineUtils.drawMarks(range, graphics, bounds, currentValue, false, totalInc); + TimelineUtils.drawMarks(graphics, bounds, currentValue, false, totalInc); drawBoldMarks(graphics, currentValue, bounds, totalInc); Color lastColor = drawTimeline(graphics, currentValue, bounds); @@ -157,21 +157,17 @@ TimelineInfo info1 = infos[i]; TimelineInfo info2 = infos[i + 1]; - normalizer.setValue(info1.getTimeStamp()); - int x0 = (int) normalizer.getValueNormalized(); + int x0 = (int) normalizer.getValueNormalized(info1.getTimeStamp()); - normalizer.setValue(info2.getTimeStamp()); - int x1 = (int) normalizer.getValueNormalized(); + int x1 = (int) normalizer.getValueNormalized(info2.getTimeStamp()); graphics.setColor(info1.getColor().getColor()); graphics.fillRect(x0, 5, x1 - x0 + 1, 5); } - normalizer.setValue(infos[infos.length - 1].getTimeStamp()); - int x0 = (int) normalizer.getValueNormalized(); + int x0 = (int) normalizer.getValueNormalized(infos[infos.length - 1].getTimeStamp()); - normalizer.setValue(infos[infos.length - 1].getTimeStamp() + 250); - int x1 = (int) normalizer.getValueNormalized(); + int x1 = (int) normalizer.getValueNormalized(infos[infos.length - 1].getTimeStamp() + 250); graphics.setColor(lastColor); graphics.fillRect(x0, 5, x1 - x0 + 1, 5); diff -r 4279c0994a38 -r ba8a0e50ddee vm-heap-analysis/client-swing/src/main/java/com/redhat/thermostat/vm/heap/analysis/client/swing/internal/stats/HeapChartPanelLayout.java --- a/vm-heap-analysis/client-swing/src/main/java/com/redhat/thermostat/vm/heap/analysis/client/swing/internal/stats/HeapChartPanelLayout.java Mon Aug 26 18:33:02 2013 +0200 +++ b/vm-heap-analysis/client-swing/src/main/java/com/redhat/thermostat/vm/heap/analysis/client/swing/internal/stats/HeapChartPanelLayout.java Wed Aug 28 12:03:09 2013 -0400 @@ -95,8 +95,7 @@ Dimension preferredSize = child.getIconCenter(); - normaliser.setValue(child.getTimestamp()); - x = (int) normaliser.getValueNormalized() - preferredSize.width; + x = (int) normaliser.getValueNormalized(child.getTimestamp()) - preferredSize.width; preferredSize = child.getPreferredSize(); Rectangle bounds = new Rectangle(x, y, preferredSize.width, preferredSize.height); diff -r 4279c0994a38 -r ba8a0e50ddee vm-jmx/agent/src/main/java/com/redhat/thermostat/vm/jmx/agent/internal/JmxBackend.java --- a/vm-jmx/agent/src/main/java/com/redhat/thermostat/vm/jmx/agent/internal/JmxBackend.java Mon Aug 26 18:33:02 2013 +0200 +++ b/vm-jmx/agent/src/main/java/com/redhat/thermostat/vm/jmx/agent/internal/JmxBackend.java Wed Aug 28 12:03:09 2013 -0400 @@ -220,11 +220,10 @@ JmxNotification data = new JmxNotification(); data.setVmId(idAndPid.vmId); - data.setImportance("unknown"); data.setTimeStamp(notification.getTimeStamp()); data.setSourceBackend(JmxBackend.class.getName()); - data.setSourceDetails("dunno"); - data.setContents(notification.toString()); + data.setSourceDetails(((ObjectName) notification.getSource()).getCanonicalName()); + data.setContents(notification.getMessage()); dao.addNotification(data); } diff -r 4279c0994a38 -r ba8a0e50ddee vm-jmx/agent/src/test/java/com/redhat/thermostat/vm/jmx/agent/internal/JmxBackendTest.java --- a/vm-jmx/agent/src/test/java/com/redhat/thermostat/vm/jmx/agent/internal/JmxBackendTest.java Mon Aug 26 18:33:02 2013 +0200 +++ b/vm-jmx/agent/src/test/java/com/redhat/thermostat/vm/jmx/agent/internal/JmxBackendTest.java Wed Aug 28 12:03:09 2013 -0400 @@ -180,6 +180,7 @@ Notification notification = mock(Notification.class); when(notification.toString()).thenReturn("testing"); + when(notification.getSource()).thenReturn(name1); listener.handleNotification(notification, handbackCaptor.getValue()); diff -r 4279c0994a38 -r ba8a0e50ddee vm-jmx/client-swing/src/main/java/com/redhat/thermostat/vm/jmx/client/swing/internal/JmxNotificationsSwingView.java --- a/vm-jmx/client-swing/src/main/java/com/redhat/thermostat/vm/jmx/client/swing/internal/JmxNotificationsSwingView.java Mon Aug 26 18:33:02 2013 +0200 +++ b/vm-jmx/client-swing/src/main/java/com/redhat/thermostat/vm/jmx/client/swing/internal/JmxNotificationsSwingView.java Wed Aug 28 12:03:09 2013 -0400 @@ -36,20 +36,30 @@ package com.redhat.thermostat.vm.jmx.client.swing.internal; +import java.awt.Color; import java.awt.Component; +import java.awt.FontMetrics; +import java.awt.Graphics; +import java.awt.Graphics2D; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; +import java.awt.RenderingHints; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; import java.util.List; +import java.util.Timer; +import java.util.TimerTask; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import javax.swing.AbstractButton; -import javax.swing.BorderFactory; import javax.swing.ButtonModel; -import javax.swing.DefaultListModel; -import javax.swing.JList; +import javax.swing.JFrame; import javax.swing.JOptionPane; import javax.swing.JPanel; -import javax.swing.JScrollPane; import javax.swing.SwingUtilities; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; @@ -58,10 +68,16 @@ import com.redhat.thermostat.client.swing.IconResource; import com.redhat.thermostat.client.swing.SwingComponent; import com.redhat.thermostat.client.swing.components.ActionToggleButton; +import com.redhat.thermostat.client.swing.components.EventTimeline; +import com.redhat.thermostat.client.swing.components.EventTimelineRangeChangeListener; +import com.redhat.thermostat.client.swing.components.HeaderPanel; import com.redhat.thermostat.client.swing.components.LocalizedLabel; -import com.redhat.thermostat.client.swing.components.HeaderPanel; +import com.redhat.thermostat.client.swing.components.timeline.Timeline; +import com.redhat.thermostat.client.ui.Palette; import com.redhat.thermostat.common.ActionEvent; import com.redhat.thermostat.common.ActionListener; +import com.redhat.thermostat.common.model.LongRangeNormalizer; +import com.redhat.thermostat.common.model.Range; import com.redhat.thermostat.shared.locale.LocalizedString; import com.redhat.thermostat.shared.locale.Translate; import com.redhat.thermostat.vm.jmx.client.core.JmxNotificationsView; @@ -74,15 +90,18 @@ private List> listeners = new CopyOnWriteArrayList<>(); private final HeaderPanel visiblePanel; - private final DefaultListModel listModel = new DefaultListModel<>(); private ActionToggleButton toolbarButton; + private List notifications = new ArrayList<>(); + + private DetailPanel timelineDetails; + private Timeline ruler; + private EventTimeline timeline; + public JmxNotificationsSwingView() { LocalizedLabel description = new LocalizedLabel(translate.localize(LocaleResources.NOTIFICATIONS_DESCRIPTION)); - description.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); - JList issuesList = new JList<>(listModel); JPanel contents = new JPanel(); contents.setLayout(new GridBagLayout()); @@ -94,11 +113,45 @@ contents.add(description, c); c.gridy++; + c.fill = GridBagConstraints.BOTH; + c.anchor = GridBagConstraints.FIRST_LINE_START; + c.weightx = 1; c.weighty = 1; - c.fill = GridBagConstraints.BOTH; - c.anchor = GridBagConstraints.FIRST_LINE_START; - contents.add(new JScrollPane(issuesList), c); + + timelineDetails = new DetailPanel(); + contents.add(timelineDetails, c); + + c.gridy++; + c.weightx = 0; + c.weighty = 0; + + ruler = new Timeline(new Range(1l, 2l)); + + contents.add(ruler, c); + + c.gridy++; + c.weightx = 0.25; + c.weighty = 0.25; + + timeline = new EventTimeline(); + Color edgeColor = Palette.THERMOSTAT_BLU.getColor(); + Color fillColor = new Color(edgeColor.getRed(), edgeColor.getGreen(), edgeColor.getBlue(), 25); + Color eventColor = Palette.THERMOSTAT_RED.getColor(); + timeline.setSelectionEdgePaint(edgeColor); + timeline.setSelectionFillPaint(fillColor); + timeline.setEventPaint(eventColor); + + timeline.getModel().addRangeChangeListener(new EventTimelineRangeChangeListener() { + @Override + public void rangeChanged(Range overview, Range detail) { + if (detail != null) { + plotDetails(detail.getMin(), detail.getMax()); + } + } + }); + + contents.add(timeline, c); contents.addHierarchyListener(new ComponentVisibleListener() { @Override @@ -141,6 +194,11 @@ visiblePanel.setContent(contents); } + protected void plotDetails(long start, long end) { + ruler.setRange(new Range(start, end)); + timelineDetails.setDisplayRange(start, end); + } + @Override public void addNotificationActionListener(ActionListener listener) { listeners.add(listener); @@ -172,7 +230,8 @@ SwingUtilities.invokeLater(new Runnable() { @Override public void run() { - listModel.clear(); + notifications.clear(); + timeline.getModel().clearEvents(); } }); } @@ -182,7 +241,8 @@ SwingUtilities.invokeLater(new Runnable() { @Override public void run() { - listModel.add(listModel.size(), data.getContents()); + notifications.add(data); + timeline.getModel().addEvent(data.getTimeStamp(), data.getSourceDetails()); } }); } @@ -202,4 +262,120 @@ }); } + private class DetailPanel extends JPanel { + + private long start = 0; + private long end = 0; + + public void setDisplayRange(long start, long end) { + this.start = start; + this.end = end; + + repaint(); + } + + @Override + protected void paintComponent(Graphics g) { + super.paintComponent(g); + + long startTimeStamp = start; + long endTimeStamp = end; + + int step = 0; + + for (JmxNotification not : notifications) { + step++; + if ((not.getTimeStamp() >= startTimeStamp) && (not.getTimeStamp() <= endTimeStamp)) { + paintNotification((Graphics2D) g, not, step); + } + } + } + + private void paintNotification(Graphics2D g, JmxNotification not, int step) { + long startTimeStamp = start; + long endTimeStamp = end; + LongRangeNormalizer normalizer = new LongRangeNormalizer(new Range<>(startTimeStamp, endTimeStamp), 0, getWidth()); + int xPos = (int) normalizer.getValueNormalized(not.getTimeStamp()); + + paintNotificationDetails(not, g, xPos, getYBandPosition(step)); + } + + private int getYBandPosition(int step) { + // TODO can we do better in determining the number of 'bands' ? + int TOTAL_STEPS = 10; + step = step % TOTAL_STEPS; + + return Math.round(1.0f * step * getHeight() / TOTAL_STEPS); + } + + private void paintNotificationDetails(JmxNotification notification, Graphics2D g, int x, int y) { + g = (Graphics2D) g.create(); + + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + + FontMetrics metrics = g.getFontMetrics(); + double textWidth = 0; + + textWidth = Math.max(textWidth, metrics.getStringBounds(notification.getSourceDetails(), g).getWidth()); + textWidth = Math.max(textWidth, metrics.getStringBounds(notification.getContents(), g).getWidth()); + + int lines = 3; + int lineHeight = metrics.getHeight(); + + int textHeight = lineHeight * lines; + + final int TEXT_PADDING = 10; + + g.setColor(Palette.PALE_RED.getColor()); + + g.drawLine(x, getHeight(), x, y); + g.fillRect(x, y, (int) textWidth + (2 * TEXT_PADDING), textHeight + (lines * TEXT_PADDING)); + + g.setColor(Color.WHITE); + DateFormat df = new SimpleDateFormat("HH:mm:ss"); + g.drawString(df.format(new Date(notification.getTimeStamp())), x + TEXT_PADDING, y + TEXT_PADDING + (lineHeight * 1)); + g.drawString(notification.getSourceDetails(), x + TEXT_PADDING, y + TEXT_PADDING + (lineHeight * 2)); + g.drawString(notification.getContents(), x + TEXT_PADDING, y + TEXT_PADDING + (lineHeight * 3)); + + g.dispose(); + } + + } + + public static void main(String[] args) { + final JmxNotificationsSwingView view = new JmxNotificationsSwingView(); + + SwingUtilities.invokeLater(new Runnable() { + + @Override + public void run() { + JFrame frame = new JFrame(); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + frame.add(view.getUiComponent()); + + frame.setVisible(true); + } + }); + + final long time = System.currentTimeMillis(); + + final AtomicInteger i = new AtomicInteger(0); + Timer t = new Timer(); + TimerTask task = new TimerTask() { + @Override + public void run() { + int c = i.incrementAndGet(); + JmxNotification data = new JmxNotification(); + data.setTimeStamp(time + (c * TimeUnit.MINUTES.toMillis(1))); + data.setSourceBackend("foo"); + data.setSourceDetails("GarbageCollection " + c); + data.setContents(c + ": PS Scavenge on 'old' gen. 10 s"); + view.addNotification(data); + } + }; + + t.schedule(task, 0, TimeUnit.SECONDS.toMillis(1)); + } + } diff -r 4279c0994a38 -r ba8a0e50ddee vm-jmx/common/src/main/java/com/redhat/thermostat/vm/jmx/common/JmxNotification.java --- a/vm-jmx/common/src/main/java/com/redhat/thermostat/vm/jmx/common/JmxNotification.java Mon Aug 26 18:33:02 2013 +0200 +++ b/vm-jmx/common/src/main/java/com/redhat/thermostat/vm/jmx/common/JmxNotification.java Wed Aug 28 12:03:09 2013 -0400 @@ -46,7 +46,6 @@ private long timeStamp; private String vmId; - private String importance; private String sourceBackend; private String sourceDetails; private String contents; @@ -62,16 +61,6 @@ } @Persist - public String getImportance() { - return importance; - } - - @Persist - public void setImportance(String importance) { - this.importance = importance; - } - - @Persist public String getSourceBackend() { return sourceBackend; } diff -r 4279c0994a38 -r ba8a0e50ddee vm-jmx/common/src/main/java/com/redhat/thermostat/vm/jmx/common/internal/JmxNotificationDAOImpl.java --- a/vm-jmx/common/src/main/java/com/redhat/thermostat/vm/jmx/common/internal/JmxNotificationDAOImpl.java Mon Aug 26 18:33:02 2013 +0200 +++ b/vm-jmx/common/src/main/java/com/redhat/thermostat/vm/jmx/common/internal/JmxNotificationDAOImpl.java Wed Aug 28 12:03:09 2013 -0400 @@ -66,17 +66,14 @@ new Category<>("vm-jmx-notification-status", JmxNotificationStatus.class, Key.AGENT_ID, Key.VM_ID, Key.TIMESTAMP, NOTIFICATIONS_ENABLED); - // TODO: private static final Key IMPORTANCE = new Key<>("importance", - // false); - private static final Key SOURCE_BACKEND = new Key<>("sourceBackend"); - private static final Key SOURCE_DESCRPTION = new Key<>("sourceDescription"); + private static final Key SOURCE_DETAILS = new Key<>("sourceDetails"); private static final Key CONTENTS = new Key<>("contents"); static final Category NOTIFICATIONS = new Category<>("vm-jmx-notification", JmxNotification.class, Key.AGENT_ID, Key.VM_ID, Key.TIMESTAMP, - SOURCE_BACKEND, SOURCE_DESCRPTION, CONTENTS); + SOURCE_BACKEND, SOURCE_DETAILS, CONTENTS); static final String QUERY_LATEST_NOTIFICATION_STATUS = "QUERY " + NOTIFICATION_STATUS.getName() + " WHERE '"