/* * Copyright 2016 Cel Skeggs * * This file is part of the CCRE, the Common Chicken Runtime Engine. * * The CCRE is free software: you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License as published by the Free * Software Foundation, either version 3 of the License, or (at your option) any * later version. * * The CCRE 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 Lesser General Public License for more * details. * * You should have received a copy of the GNU Lesser General Public License * along with the CCRE. If not, see <http://www.gnu.org/licenses/>. */ package ccre.timeline; import java.awt.Color; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.Shape; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseWheelEvent; import java.awt.geom.AffineTransform; import java.util.Arrays; import javax.swing.JPanel; import ccre.log.Logger; import ccre.time.Time; /** * A base display panel used in timeline panels. * * @author skeggsc */ public final class TimelinePanel extends JPanel { private static final long serialVersionUID = 7927046605855742517L; private static final int TOOLBAR_HEIGHT = 30; private static final int TIME_HEIGHT = 20; /** * The currently-visible channels in this timeline. */ private final Timeline timeline; /** * The XY position of the upper-left corner of the view. */ private int relativeX, relativeY; /** * The sizing of the view. */ private float widthSeconds = 2.0f, heightChannels = 5; /** * The relative position for the last drag start. */ private int relDragX, relDragY; private boolean dragModeScale; /** * The most recent position of the mouse. */ private int mouseX, mouseY; /** * Creates a new timeline display panel. * * @param timeline the timeline to display. */ public TimelinePanel(Timeline timeline) { this.timeline = timeline; } /** * Start the IntelligenceMain instance so that it runs. */ public void start() { MouseAdapter listener = new SuperCanvasMouseAdapter(); this.addMouseWheelListener(listener); this.addMouseListener(listener); this.addMouseMotionListener(listener); } /** * Console text, small and monospaced. */ public static final Font console = new Font("Monospaced", Font.PLAIN, 11); private static final int CAP_PAD = 40; @Override public void paint(Graphics go) { try { Graphics2D g = (Graphics2D) go; g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB); int w = getWidth(); int h = getHeight(); g.setFont(console); g.setColor(Color.BLACK); g.fillRect(0, 0, w, h); renderTimeline(g, w, h); } catch (Throwable thr) { Logger.severe("Exception while handling paint event", thr); } } private void renderTimeline(Graphics2D g, int w, int h) { // sections of the screen: toolbar, top seconds bar, bottom seconds bar, // timeline g.setColor(Color.CYAN); g.fillRect(0, 0, w, TOOLBAR_HEIGHT); AffineTransform oldTX = g.getTransform(); g.translate(0, TOOLBAR_HEIGHT); drawTickMarks(g, w, TIME_HEIGHT); g.setTransform(oldTX); g.translate(0, h - TIME_HEIGHT); drawTickMarks(g, w, TIME_HEIGHT); g.setTransform(oldTX); int nh = h - TOOLBAR_HEIGHT - TIME_HEIGHT * 2; Shape s = g.getClip(); g.clipRect(0, TOOLBAR_HEIGHT + TIME_HEIGHT, w, nh); oldTX = g.getTransform(); int channel_height = (int) (nh / heightChannels); int n = 0; for (TimelineChannel channel : timeline.channels) { int relY = TOOLBAR_HEIGHT + TIME_HEIGHT + channel_height * n - relativeY; g.translate(-relativeX, relY); renderChannel(channel, relativeX, g, w, channel_height); g.setTransform(oldTX); g.setColor(Color.RED); g.drawString(channel.name(), CAP_PAD, relY + 3 + g.getFontMetrics().getAscent()); n++; } g.setClip(s); } private void drawTickMarks(Graphics2D g, int w, int h) { g.setColor(Color.GREEN); g.fillRect(0, 0, w, TIME_HEIGHT); double time_duration = 1.0f; // in seconds int time_duration_power = 0; double period_width = w / widthSeconds; while (period_width < 20) { period_width *= 10; time_duration *= 10; time_duration_power++; } while (period_width > 200) { period_width /= 10; time_duration /= 10; time_duration_power--; } float time_at_left = relativeX / (w / widthSeconds); float time_at_width = (relativeX + w) / (w / widthSeconds); // Logger.finest("Tick evaluation: " + time_duration + " ~ " + // period_width + " / " + time_at_left + " - " + time_at_width); int next_tick_at_left_od = ((int) Math.ceil(time_at_left / time_duration)); g.setColor(Color.YELLOW); for (int tick_od = next_tick_at_left_od - 1; tick_od * time_duration < time_at_width; tick_od++) { int position = (int) (tick_od * time_duration * (w / widthSeconds)) - relativeX; g.drawLine(position, 1, position, h - 2); g.drawString(toPowerString(tick_od, time_duration_power), position + 1, h / 2); } } /** * Converts a time, in units of 10 microseconds, to a textual representation * that includes units and displays at most three significant digits. * * @param ticks the number of ticks, in multiples of 10 microseconds. * @return the string, either <code>xxx ys</code>, <code>xx.x ys</code>, or * <code>x.xx ys</code>, where <code>x</code> and <code>y</code> are chosen * by the code. */ public static String toTimeString(long ticks) { // we want to display the three most significant digits // xxx ys, xx.x ys, x.xx ys ticks *= 10; long secs_orig = (ticks / Time.MICROSECONDS_PER_SECOND); int shift = 0; while (ticks >= 1000) { ticks /= 10; shift++; } char c1 = (char) ((ticks / 100) + '0'); char c2 = (char) (((ticks / 10) % 10) + '0'); char c3 = (char) ((ticks % 10) + '0'); switch (shift) { case 0: return "" + c1 + c2 + c3 + " us"; case 1: return "" + c1 + "." + c2 + c3 + " ms"; case 2: return "" + c1 + c2 + "." + c3 + " ms"; case 3: return "" + c1 + c2 + c3 + " ms"; case 4: return "" + c1 + "." + c2 + c3 + " s"; case 5: return "" + c1 + c2 + "." + c3 + " s"; default: return secs_orig + " s"; } } /** * Converts a number into a string after multiplication by * <code>Math.pow(10, pow10)</code>, but without any potential overflow * errors. * * @param num the base number. * @param pow10 the power of ten. * @return the number converted to a string. */ public static String toPowerString(int num, int pow10) { if (num < 0) { return "-" + toPowerString(-num, pow10); } // tick * 10^pow10 StringBuilder sb = new StringBuilder().append(num); if (pow10 >= 0) { while (pow10-- > 0) { sb.append('0'); } } else { int count_needed = -(pow10 + sb.length()); if (count_needed >= 0) { char[] zeroes = new char[count_needed + 2]; Arrays.fill(zeroes, '0'); zeroes[1] = '.'; sb.insert(0, new String(zeroes)); } else { sb.insert(sb.length() + pow10, '.'); } } return sb.toString(); } private void renderChannel(TimelineChannel channel, int relativeX, Graphics2D g, int w, int h) { int virtual_begin_at = (int) ((w / widthSeconds) * channel.beginAt()) - CAP_PAD; int virtual_end_at = (int) ((w / widthSeconds) * channel.endAt()) + CAP_PAD; g.setColor(Color.WHITE); g.fillRect(virtual_begin_at, 0, virtual_end_at - virtual_begin_at, h); FontMetrics fm = g.getFontMetrics(); int lastLocationX = 0, lastLocationY = 0; for (int i = 0; i < channel.count(); i++) { float finc = ((w / widthSeconds) * channel.timeFor(i)); if (finc > relativeX + w * 2 || finc < relativeX - w) { continue; } int virtual_incidence = (int) finc; g.setColor(channel.colorFor(i)); g.drawLine(virtual_incidence, channel.isFloat() ? (h - h / 3) : 1, virtual_incidence, h - 2); if (i != 0) { g.setColor(channel.colorFor(i - 1)); if (channel.isFloat()) { int vloc = (int) (h / 2 - (h / 2) * channel.valueFor(i)); g.drawLine(lastLocationX, lastLocationY, virtual_incidence, vloc); lastLocationY = vloc; } else if (channel.hasContinuationChannel()) { g.drawLine(lastLocationX, h / 2, virtual_incidence, h / 2); g.drawLine(Math.max(lastLocationX, virtual_incidence - 1), 1, Math.max(lastLocationX, virtual_incidence - 1), h - 2); } } String text = channel.stringFor(i); int next_virtual_incidence = (i < channel.count() - 1) ? (int) ((w / widthSeconds) * channel.timeFor(i + 1)) : Integer.MAX_VALUE; int width_available_for_text = next_virtual_incidence - (4 + virtual_incidence); if (width_available_for_text > fm.stringWidth("M")) { g.setColor(channel.colorFor(i)); while (fm.stringWidth(text) > width_available_for_text) { text = text.substring(0, text.length() - 1); } if (!text.isEmpty()) { g.drawString(text, virtual_incidence + 2, h / 2); } } lastLocationX = virtual_incidence; } } private class SuperCanvasMouseAdapter extends MouseAdapter { SuperCanvasMouseAdapter() { } @Override public void mousePressed(MouseEvent e) { try { dragModeScale = e.isShiftDown(); if (dragModeScale) { relDragX = e.getX(); relDragY = e.getY(); } else { relDragX = relativeX + e.getX(); relDragY = relativeY + e.getY(); } repaint(); } catch (Throwable thr) { Logger.severe("Exception while handling mouse press", thr); } } @Override public void mouseReleased(MouseEvent e) { try { mouseX = e.getX(); mouseY = e.getY(); updateDrag(); } catch (Throwable thr) { Logger.severe("Exception while handling mouse release", thr); } } private void updateDrag() { if (dragModeScale) { if (mouseX - relDragX < -30) { relDragX -= 30; float centerTime = (relativeX + (getWidth() / 2)) * (widthSeconds / getWidth()); widthSeconds *= 2; relativeX = (int) (centerTime / (widthSeconds / getWidth()) - (getWidth() / 2)); } else if (mouseX - relDragX > 30) { relDragX += 30; float centerTime = (relativeX + (getWidth() / 2)) * (widthSeconds / getWidth()); widthSeconds /= 2; relativeX = (int) (centerTime / (widthSeconds / getWidth()) - (getWidth() / 2)); } if (mouseY - relDragY < -30) { relDragY -= 30; float centerTime = (relativeY + (getHeight() / 2)) * (heightChannels / getHeight()); heightChannels *= 2; relativeY = (int) (centerTime / (heightChannels / getHeight()) - (getHeight() / 2)); } else if (mouseY - relDragY > 30) { relDragY += 30; float centerTime = (relativeY + (getHeight() / 2)) * (heightChannels / getHeight()); heightChannels /= 2; relativeY = (int) (centerTime / (heightChannels / getHeight()) - (getHeight() / 2)); } } else { relativeX = relDragX - mouseX; relativeY = relDragY - mouseY; } repaint(); } @Override public void mouseWheelMoved(MouseWheelEvent e) { try { // TODO repaint(); } catch (Throwable thr) { Logger.severe("Exception while handling mouse wheel", thr); } } @Override public void mouseDragged(MouseEvent e) { try { mouseX = e.getX(); mouseY = e.getY(); updateDrag(); } catch (Throwable thr) { Logger.severe("Exception while handling mouse drag", thr); } } @Override public void mouseMoved(MouseEvent e) { try { mouseX = e.getX(); mouseY = e.getY(); // repaint(); } catch (Throwable thr) { Logger.severe("Exception while handling mouse move", thr); } } } }