package org.opensourcephysics.cabrillo.tracker; import java.awt.BasicStroke; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Container; import java.awt.Dimension; import java.awt.Font; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.GridLayout; import java.awt.Point; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.Shape; import java.awt.event.MouseListener; import java.awt.geom.GeneralPath; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import javax.swing.BorderFactory; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JSpinner; import javax.swing.SpinnerModel; import javax.swing.SpinnerNumberModel; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import org.opensourcephysics.display.DataClip; import org.opensourcephysics.display.DrawingPanel; import org.opensourcephysics.display.Interactive; import org.opensourcephysics.media.core.DataTrack; import org.opensourcephysics.media.core.VideoClip; import org.opensourcephysics.media.core.VideoPanel; public class DataTrackClipControl extends JPanel implements PropertyChangeListener { static Color videoColor=Color.WHITE; static Color dataColor=videoColor; static Color dataClipColor=new Color(51, 200, 51); static Color unavailableDataColor=new Color(51, 200, 51, 127); static Color availableDataColor=unavailableDataColor; static int graphicHeight = 100; protected DataTrack dataTrack; protected DrawingPanel drawingPanel; protected JPanel spinnerPanel; protected Interactive mappingGraphic; protected JLabel videoInLabel, dataInLabel, dataClipLengthLabel, dataStrideLabel; protected JSpinner videoInSpinner, dataInSpinner, dataClipLengthSpinner, dataStrideSpinner; protected boolean refreshing, drawVideoClip=false; /** * Constructor. * * @param model the DataTrack */ public DataTrackClipControl(DataTrack model) { super(new BorderLayout()); dataTrack = model; dataTrack.addPropertyChangeListener(this); createGUI(); refreshSpinners(); refreshGUI(); } /** * Creates the GUI. */ protected void createGUI() { // create labels videoInLabel = new JLabel(); videoInLabel.setBorder(BorderFactory.createEmptyBorder()); dataInLabel = new JLabel(); dataInLabel.setBorder(BorderFactory.createEmptyBorder()); dataClipLengthLabel = new JLabel(); dataClipLengthLabel.setBorder(BorderFactory.createEmptyBorder()); dataStrideLabel = new JLabel(); dataStrideLabel.setBorder(BorderFactory.createEmptyBorder()); // create spinners SpinnerModel spinModel = new SpinnerNumberModel(0, 0, 20, 1); videoInSpinner = new MySpinner(spinModel); ChangeListener listener = new ChangeListener() { public void stateChanged(ChangeEvent e) { if (refreshing) return; int in = (Integer)videoInSpinner.getValue(); if (in==dataTrack.getStartFrame()) { return; } dataTrack.setStartFrame(in); videoInSpinner.setValue(dataTrack.getStartFrame()); repaint(); videoInSpinner.requestFocusInWindow(); } }; videoInSpinner.addChangeListener(listener); spinModel = new SpinnerNumberModel(0, 0, 20, 1); dataInSpinner = new MySpinner(spinModel); listener = new ChangeListener() { public void stateChanged(ChangeEvent e) { if (refreshing) return; int in = (Integer)dataInSpinner.getValue(); if (in==dataTrack.getDataClip().getStartIndex()) { return; } dataTrack.getDataClip().setStartIndex(in); dataInSpinner.setValue(dataTrack.getDataClip().getStartIndex()); repaint(); dataInSpinner.requestFocusInWindow(); } }; dataInSpinner.addChangeListener(listener); spinModel = new SpinnerNumberModel(1, 1, 20, 1); dataClipLengthSpinner = new MySpinner(spinModel); listener = new ChangeListener() { public void stateChanged(ChangeEvent e) { if (refreshing) return; int length = (Integer)dataClipLengthSpinner.getValue(); if (length==dataTrack.getDataClip().getClipLength()) { return; } dataTrack.getDataClip().setClipLength(length); dataClipLengthSpinner.setValue(dataTrack.getDataClip().getClipLength()); repaint(); dataClipLengthSpinner.requestFocusInWindow(); } }; dataClipLengthSpinner.addChangeListener(listener); spinModel = new SpinnerNumberModel(1, 1, 10, 1); dataStrideSpinner = new MySpinner(spinModel); listener = new ChangeListener() { public void stateChanged(ChangeEvent e) { if (refreshing) return; int n = (Integer)dataStrideSpinner.getValue(); if (n==dataTrack.getDataClip().getStride()) { return; } dataTrack.getDataClip().setStride(n); dataStrideSpinner.setValue(dataTrack.getDataClip().getStride()); repaint(); dataStrideSpinner.requestFocusInWindow(); } }; dataStrideSpinner.addChangeListener(listener); // assemble drawingPanel = new GraphicPanel(); drawingPanel.setBorder(BorderFactory.createEtchedBorder()); drawingPanel.addDrawable(new MappingGraphic()); add(drawingPanel, BorderLayout.CENTER); spinnerPanel = new JPanel(new GridLayout(1, 4)); add(spinnerPanel, BorderLayout.SOUTH); JPanel singleSpinnerPanel = new JPanel(new BorderLayout()); JPanel panel = new JPanel(); panel.add(videoInSpinner); singleSpinnerPanel.add(panel, BorderLayout.NORTH); panel = new JPanel(); panel.add(videoInLabel); singleSpinnerPanel.add(panel, BorderLayout.SOUTH); spinnerPanel.add(singleSpinnerPanel); singleSpinnerPanel = new JPanel(new BorderLayout()); panel = new JPanel(); panel.add(dataClipLengthSpinner); singleSpinnerPanel.add(panel, BorderLayout.NORTH); panel = new JPanel(); panel.add(dataClipLengthLabel); singleSpinnerPanel.add(panel, BorderLayout.SOUTH); spinnerPanel.add(singleSpinnerPanel); singleSpinnerPanel = new JPanel(new BorderLayout()); panel = new JPanel(); panel.add(dataInSpinner); singleSpinnerPanel.add(panel, BorderLayout.NORTH); panel = new JPanel(); panel.add(dataInLabel); singleSpinnerPanel.add(panel, BorderLayout.SOUTH); spinnerPanel.add(singleSpinnerPanel); singleSpinnerPanel = new JPanel(new BorderLayout()); panel = new JPanel(); panel.add(dataStrideSpinner); singleSpinnerPanel.add(panel, BorderLayout.NORTH); panel = new JPanel(); panel.add(dataStrideLabel); singleSpinnerPanel.add(panel, BorderLayout.SOUTH); spinnerPanel.add(singleSpinnerPanel); } /** * Refreshes the spinners. */ protected void refreshSpinners() { VideoPanel vidPanel = dataTrack.getVideoPanel(); if (vidPanel==null) return; DataClip dataClip = dataTrack.getDataClip(); VideoClip videoClip = vidPanel.getPlayer().getVideoClip(); // data start index int clipLength = dataClip.getClipLength(); int dataLength = dataClip.getDataLength(); int max = Math.max(0, dataLength-1); SpinnerModel spinModel = new SpinnerNumberModel(dataClip.getStartIndex(), 0, max, 1); dataInSpinner.setModel(spinModel); // data stride max = Math.max(1, dataLength-1); max = Math.max(max, dataClip.getStride()); spinModel = new SpinnerNumberModel(dataClip.getStride(), 1, max, 1); dataStrideSpinner.setModel(spinModel); if (videoClip!=null) { // video start frame int first = videoClip.getFirstFrameNumber(); int last = videoClip.getLastFrameNumber(); int startFrame = dataTrack.getStartFrame(); startFrame = Math.max(startFrame, first); startFrame = Math.min(startFrame, last); spinModel = new SpinnerNumberModel(startFrame, first, last, 1); videoInSpinner.setModel(spinModel); } // frame count (clip length) max = Math.max(1, dataLength); spinModel = new SpinnerNumberModel(clipLength, 1, max, 1); dataClipLengthSpinner.setModel(spinModel); Container c = this.getTopLevelAncestor(); if (c instanceof ModelBuilder) { ModelBuilder builder = (ModelBuilder)c; builder.refreshSpinners(); } } /** * Refreshes the GUI. */ protected void refreshGUI() { setBorder(BorderFactory.createTitledBorder(TrackerRes.getString("DataTrackClipControl.Border.Title"))); //$NON-NLS-1$ videoInLabel.setText(TrackerRes.getString("DataTrackClipControl.Label.VideoStart")); //$NON-NLS-1$ dataClipLengthLabel.setText(TrackerRes.getString("DataTrackClipControl.Label.FrameCount")); //$NON-NLS-1$ dataInLabel.setText(TrackerRes.getString("DataTrackClipControl.Label.DataStart")); //$NON-NLS-1$ dataStrideLabel.setText(TrackerRes.getString("DataTrackClipControl.Label.Stride")); //$NON-NLS-1$ } /** * Adds a mouse listener to all JPanels associated with this control. */ protected void addMouseListenerToAll(MouseListener listener) { this.addMouseListener(listener); drawingPanel.addMouseListener(listener); spinnerPanel.addMouseListener(listener); } @Override public Dimension getMaximumSize() { Dimension dim = super.getMaximumSize(); dim.height = getPreferredSize().height; return dim; } @Override public void propertyChange(PropertyChangeEvent e) { refreshSpinners(); // if (e.getPropertyName().equals("dataclip")) { //$NON-NLS-1$ // refreshSpinners(); // } // else if (e.getPropertyName().equals("videoclip")) { //$NON-NLS-1$ // refreshSpinners(); // } } /** * Gets the last displayed clip index. An index is displayed if its * corresponding frame is less than or equal to the video's last frame number. * * @return the index */ private int getLastDisplayedClipIndex() { int stepCount = dataTrack.getDataClip().getClipLength(); VideoPanel vidPanel = dataTrack.getVideoPanel(); if (vidPanel==null) return stepCount; VideoClip clip = vidPanel.getPlayer().getVideoClip(); int last = clip.getLastFrameNumber(); for (int i = stepCount-1; i>0; i--) { // determine corresponding frame number and index int frame = dataTrack.getStartFrame()+i; int index = dataTrack.getDataClip().stepToIndex(i); // look for first step with frame<=last frame and index<data length if (frame<=last && index<dataTrack.getDataClip().getDataLength()) { // return the index return index; } } return dataTrack.getDataClip().stepToIndex(0); } /** * Sets the interactive graphic element that displays data elements and video frames. * * @param graphic the graphic element */ public void setGraphic(Interactive graphic) { if (graphic==null) return; if (mappingGraphic!=null) { drawingPanel.removeDrawable(mappingGraphic); } mappingGraphic = graphic; drawingPanel.addDrawable(mappingGraphic); } /** * An Interactive that displays and maps data elements to video frames. */ class MappingGraphic implements Interactive { GeneralPath path = new GeneralPath(); @Override public void draw(DrawingPanel panel, Graphics g) { VideoPanel vidPanel = dataTrack.getVideoPanel(); if (vidPanel==null) return; Graphics2D g2 = (Graphics2D)g; int strokeWidth = 8; g2.setStroke(new BasicStroke(strokeWidth, BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND)); // fill background Rectangle rect = new Rectangle(drawingPanel.getSize()); g2.setColor(new Color(200, 200, 200)); g2.fill(rect); int yVideoLine = 30; int yDataLine = 70; // draw video and data labels g2.setColor(Color.DARK_GRAY); g2.setFont(videoInLabel.getFont()); RenderingHints rh = g2.getRenderingHints(); rh.put(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); rh.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); int largeGap = 10, smallGap = 4; int fontDrop = g2.getFontMetrics().getHeight()/4; String s = TrackerRes.getString("DataTrackClipControl.Label.Video"); //$NON-NLS-1$ int labelSpace = g2.getFontMetrics().stringWidth(s); g2.drawString(s, largeGap, yVideoLine+fontDrop); s = TrackerRes.getString("DataTrackClipControl.Label.Data"); //$NON-NLS-1$ labelSpace = Math.max(labelSpace, g2.getFontMetrics().stringWidth(s)); g2.drawString(s, largeGap, yDataLine+fontDrop); labelSpace += 6; VideoClip videoClip = vidPanel.getPlayer().getVideoClip(); DataClip dataClip = dataTrack.getDataClip(); s = "0"; //$NON-NLS-1$ int frontSpace = g2.getFontMetrics().stringWidth(s); frontSpace = Math.max(frontSpace, g2.getFontMetrics().stringWidth(s)); s = String.valueOf(videoClip.getLastFrameNumber()); int endSpace = g2.getFontMetrics().stringWidth(s); s = String.valueOf(dataClip.getDataLength()-1); endSpace = Math.max(endSpace, g2.getFontMetrics().stringWidth(s)); int videoClipFrames = videoClip.getLastFrameNumber()-videoClip.getFirstFrameNumber()+1; if (drawVideoClip) { videoClipFrames = videoClip.getFrameCount(); } int dataClipFrames = dataClip.getDataLength(); int maxFrames = Math.max(videoClipFrames, dataClipFrames); double maxLength = panel.getWidth()-labelSpace-frontSpace-endSpace-3*largeGap-2*smallGap; double lengthPerFrame = maxLength/maxFrames; // draw video line double lineLength = maxLength*videoClipFrames/maxFrames; double leftEnd= labelSpace+2*largeGap+frontSpace; double rightVideoEnd = leftEnd+lineLength; path.reset(); path.moveTo(leftEnd, yVideoLine); path.lineTo(rightVideoEnd, yVideoLine); g2.setColor(videoColor); g2.draw(path); // draw dataclip on video line--full clip first as "unavailable" double fullClipLength = lengthPerFrame*(dataClip.getClipLength()); int frame0 = drawVideoClip? videoClip.getStartFrameNumber(): videoClip.getFirstFrameNumber(); double videoStartClip = leftEnd+lengthPerFrame*(dataTrack.getStartFrame()-frame0); double videoEndClip = videoStartClip + fullClipLength; videoStartClip = Math.max(videoStartClip, leftEnd); videoEndClip = Math.min(videoEndClip, leftEnd+lineLength); path.reset(); path.moveTo(videoStartClip, yVideoLine); path.lineTo(videoEndClip, yVideoLine); g2.setColor(unavailableDataColor); g2.draw(path); // draw dataclip on video line--available clip videoStartClip = leftEnd+lengthPerFrame*(dataTrack.getStartFrame()-frame0); double clipLength = lengthPerFrame*(dataClip.getAvailableClipLength()); videoEndClip = videoStartClip + clipLength; videoStartClip = Math.max(videoStartClip, leftEnd); videoEndClip = Math.min(videoEndClip, leftEnd+lineLength); // adjust clip length in case video truncated it clipLength = videoEndClip - videoStartClip; path.reset(); path.moveTo(videoStartClip, yVideoLine); path.lineTo(videoEndClip, yVideoLine); g2.setColor(dataClipColor); g2.draw(path); // draw the data line lineLength = maxLength*dataClipFrames/maxFrames; double rightDataEnd = leftEnd+lineLength; path.reset(); path.moveTo(leftEnd, yDataLine); path.lineTo(rightDataEnd, yDataLine); g2.setColor(dataColor); g2.draw(path); // draw data clip on data line if (dataClip.getStride()>1) { float frameWidth = Math.max(1, (float)lengthPerFrame); g2.setStroke(new BasicStroke(strokeWidth, BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND, 8, new float[] {frameWidth, frameWidth*(dataClip.getStride()-1)}, 0)); } double dataStartClip = leftEnd + lengthPerFrame*dataClip.getStartIndex(); double dataEndClip = dataStartClip + fullClipLength*dataClip.getStride(); dataEndClip = Math.min(dataEndClip, leftEnd+lineLength); path.reset(); path.moveTo(dataStartClip, yDataLine); path.lineTo(dataEndClip, yDataLine); g2.setColor(availableDataColor); g2.draw(path); dataEndClip = dataStartClip + clipLength*dataClip.getStride(); path.reset(); path.moveTo(dataStartClip, yDataLine); path.lineTo(dataEndClip, yDataLine); g2.setColor(dataClipColor); g2.draw(path); // connect with lines dataEndClip = dataEndClip - (dataClip.getStride()-1)*lengthPerFrame; g2.setColor(Color.BLACK); g2.setStroke(new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_ROUND)); path.reset(); path.moveTo(dataStartClip, yDataLine-strokeWidth/2); path.lineTo(videoStartClip, yVideoLine+strokeWidth/2); g2.draw(path); path.reset(); path.moveTo(dataEndClip, yDataLine-strokeWidth/2); path.lineTo(videoEndClip, yVideoLine+strokeWidth/2); g2.draw(path); // draw frame and index numbers g2.setFont(g2.getFont().deriveFont(Font.PLAIN)); int verticalOffset = fontDrop; // start index s = "0"; //$NON-NLS-1$ g2.drawString(s, (int)(leftEnd-frontSpace-smallGap), yDataLine+verticalOffset); // end index s = String.valueOf(dataClipFrames-1); g2.drawString(s, (int)(rightDataEnd+smallGap), yDataLine+verticalOffset); // first video frame s = String.valueOf(videoClip.getFirstFrameNumber()); g2.drawString(s, (int)(leftEnd-frontSpace-smallGap), yVideoLine+verticalOffset); // last video frame s = String.valueOf(videoClip.getLastFrameNumber()); g2.drawString(s, (int)(rightVideoEnd+smallGap), yVideoLine+verticalOffset); verticalOffset = -2-strokeWidth/2; s = String.valueOf(dataTrack.getStartFrame()); g2.drawString(s, (int)(videoStartClip), yVideoLine+verticalOffset); int n = dataTrack.getStartFrame()+dataClip.getAvailableClipLength()-1; n = Math.min(n, videoClip.getLastFrameNumber()); if (dataTrack.getStartFrame()!=n) { s = String.valueOf(n); int space = g2.getFontMetrics().stringWidth(s); g2.drawString(s, (int)(videoEndClip-space), yVideoLine+verticalOffset); } verticalOffset = fontDrop+strokeWidth+3; s = String.valueOf(dataTrack.getDataClip().getStartIndex()); g2.drawString(s, (int)(dataStartClip), yDataLine+verticalOffset); n = getLastDisplayedClipIndex(); if (n-dataTrack.getDataClip().getStartIndex()>0) { s = String.valueOf(n); int space = g2.getFontMetrics().stringWidth(s); g2.drawString(s, (int)(dataEndClip-space), yDataLine+verticalOffset); } } /** * Gets the hit shapes associated with the sliders. * * @return an array of hit shapes */ public Shape[] getHitShapes() { return null; } /** * Gets the sliders mark. * * @param points a Point array * @return the mark */ public Mark getMark(Point[] points) { return new Mark() { @Override public void draw(Graphics2D g, boolean highlighted) { } @Override public Rectangle getBounds(boolean highlighted) { return null; } }; } @Override public double getXMin() { return 0; } @Override public double getXMax() { return 100; } @Override public double getYMin() { return 0; } @Override public double getYMax() { return 100; } @Override public boolean isMeasured() { return true; } @Override public Interactive findInteractive(DrawingPanel panel, int _xpix, int _ypix) { return null; } @Override public void setEnabled(boolean enabled) {} @Override public boolean isEnabled() { return true; } @Override public void setXY(double x, double y) {} @Override public void setX(double x) { } @Override public void setY(double y) {} @Override public double getX() { return 0; } @Override public double getY() { return 0; } } class MySpinner extends JSpinner { public MySpinner (SpinnerModel model) { super(model); } public Dimension getPreferredSize() { Dimension dim = super.getPreferredSize(); if (dataTrack instanceof ParticleDataTrack) { ParticleDataTrack pdt = (ParticleDataTrack)dataTrack; dim.height = pdt.trackerPanel.getModelBuilder().getSpinnerHeight(); } else { dim.height += 6; } dim.width += 6; return dim; } } class GraphicPanel extends DrawingPanel { GraphicPanel() { // remove the interactive panel mouse controller removeMouseListener(mouseController); removeMouseMotionListener(mouseController); } @Override public Dimension getPreferredSize() { Dimension dim = super.getPreferredSize(); dim.height = graphicHeight; return dim; } } }