content: package tim.prune.correlate;
  
  import java.awt.FlowLayout;
  import java.awt.GridLayout;
  import java.util.ArrayList;
  import java.util.List;
  
  import javax.swing.JOptionPane;
  import javax.swing.JPanel;
  import javax.swing.JTable;
  
  import tim.prune.App;
  import tim.prune.I18nManager;
  import tim.prune.cmd.Command;
  import tim.prune.cmd.CorrelateMediaCmd;
  import tim.prune.cmd.MediaLinkType;
  import tim.prune.cmd.PointAndMedia;
  import tim.prune.data.AudioClip;
  import tim.prune.data.DataPoint;
  import tim.prune.data.MediaList;
  import tim.prune.data.MediaObject;
  import tim.prune.data.TimeDifference;
  import tim.prune.data.Timestamp;
  import tim.prune.data.TimestampUtc;
  import tim.prune.function.PointUtils;
  
  /**
   * Class to manage the automatic correlation of audio clips to points
   * which is very similar to the PhotoCorrelator apart from the clip lengths
   */
  public class AudioCorrelator extends Correlator
  {
  	private AudioTimestampSelector _fileTimesSelector = null, _correlTimesSelector = null;
  
  
  	/**
  	 * Constructor
  	 * @param inApp App object
  	 */
  	public AudioCorrelator(App inApp) {
  		super(inApp);
  	}
  
  	/** @return the name key */
  	public String getNameKey() {
  		return "function.correlateaudios";
  	}
  
  	/** @return type key */
  	protected String getMediaTypeKey() {
  		return "audio";
  	}
  
  	/** @return audio list*/
  	protected MediaList<?> getMediaList() {
  		return _app.getTrackInfo().getAudioList();
  	}
  
  
  	/**
  	 * @return first gui panel including timestamp specification (beginning, middle, end)
  	 */
  	protected JPanel makeFirstPanel()
  	{
  		// First panel for timestamp stuff
  		JPanel card1 = new JPanel();
  		card1.setLayout(new FlowLayout(FlowLayout.CENTER));
  		JPanel grid1 = new JPanel();
  		grid1.setLayout(new GridLayout(0, 1));
  		_fileTimesSelector = new AudioTimestampSelector("dialog.correlate.filetimes", "dialog.correlate.filetimes2");
  		grid1.add(_fileTimesSelector);
  		_correlTimesSelector = new AudioTimestampSelector("dialog.correlate.correltimes", null);
  		grid1.add(_correlTimesSelector);
  		card1.add(grid1);
  		return card1;
  	}
  
  
  	/**
  	 * @return array of boolean flags denoting availability of cards
  	 */
  	protected boolean[] getCardEnabledFlags()
  	{
  		boolean[] cards = super.getCardEnabledFlags();
  		cards[0] = getAudioLengthAvailability(_app.getTrackInfo().getAudioList());
  		return cards;
  	}
  
  	/**
  	 * @param inAudios AudioList object
  	 * @return true if there are any audio lengths available
  	 */
  	private static boolean getAudioLengthAvailability(MediaList<AudioClip> inAudios)
  	{
  		for (int i=0; i<inAudios.getCount(); i++)
  		{
  			AudioClip a = inAudios.get(i);
  			if (a.getLengthInSeconds() > 0) {return true;}
  		}
  		return false;
  	}
  
  	/**
  	 * Create a preview of the correlate action using the selected time difference
  	 * @param inTimeDiff TimeDifference to use for preview
  	 * @param inShowWarning true to show warning if all points out of range
  	 */
  	protected void createPreview(TimeDifference inTimeDiff, boolean inShowWarning)
  	{
  		TimeDifference timeLimit = parseTimeLimit();
  		double angDistLimit = parseDistanceLimit();
  		MediaPreviewTableModel model = new MediaPreviewTableModel("dialog.correlate.select.audioname", getTimezone());
  		MediaList<AudioClip> audios = _app.getTrackInfo().getAudioList();
  		// Loop through audios deciding whether to set correlate flag or not
  		int numAudios = audios.getCount();
  		for (int i=0; i<numAudios; i++)
  		{
  			AudioClip audio = audios.get(i);
  			PointMediaPair pair = getPointPairForMedia(_app.getTrackInfo().getTrack(), audio, inTimeDiff);
  			MediaPreviewTableRow row = new MediaPreviewTableRow(pair);
  			// Don't try to correlate audios which don't have points either side
  			boolean correlate = pair.isValid();
  			// Don't select audios which already have a point
  			if (audio.getCurrentStatus() != AudioClip.Status.NOT_CONNECTED) {correlate = false;}
  			// Check time limits, distance limits
  			if (timeLimit != null && correlate) {
  				long numSecs = pair.getMinSeconds();
  				correlate = (numSecs <= timeLimit.getTotalSeconds());
  			}
  			if (angDistLimit > 0.0 && correlate)
  			{
  				final double angDistPair = DataPoint.calculateRadiansBetween(pair.getPointBefore(), pair.getPointAfter());
  				double frac = pair.getFraction();
  				if (frac > 0.5) {frac = 1 - frac;}
  				final double angDistPhoto = angDistPair * frac;
  				correlate = (angDistPhoto < angDistLimit);
  			}
  			// Don't select audios which are already correlated to the same point
  			if (pair.getSecondsBefore() == 0L && pair.getPointBefore().isDuplicate(audio.getDataPoint())) {
  				correlate = false;
  			}
  			row.setCorrelateFlag(correlate);
  			model.addRow(row);
  		}
  		_previewTable.setModel(model);
  		// Set distance units
  		model.setDistanceUnits(getSelectedDistanceUnits());
  		// Set column widths
  		_previewTable.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);
  		final int[] colWidths = {150, 160, 100, 100, 50};
  		for (int i=0; i<model.getColumnCount(); i++) {
  			_previewTable.getColumnModel().getColumn(i).setPreferredWidth(colWidths[i]);
  		}
  		// check if any audios found
  		_okButton.setEnabled(model.hasAnySelected());
  		if (inShowWarning && !model.hasAnySelected())
  		{
  			JOptionPane.showMessageDialog(_dialog, I18nManager.getText("dialog.correlate.alloutsiderange"),
  				getName(), JOptionPane.ERROR_MESSAGE);
  		}
  	}
  
  	/**
  	 * @return modified timestamp of specified media object
  	 */
  	protected Timestamp getMediaTimestamp(MediaObject inMedia)
  	{
  		Timestamp tstamp = super.getMediaTimestamp(inMedia);
  		long mediaMillis = tstamp.getMilliseconds(getTimezone());
  		try {
  			AudioClip audio = (AudioClip) inMedia;
  			int audioLength = audio.getLengthInSeconds();
  			// Each option is worth half the length of the audio clip, so need to divide by 2
  			int secsToAdd = audioLength *
  				(_correlTimesSelector.getSelectedOption() - _fileTimesSelector.getSelectedOption()) / 2;
  			if (audioLength > 0 && secsToAdd != 0)
  			{
  				mediaMillis += (secsToAdd * 1000L);
  				tstamp = new TimestampUtc(mediaMillis);
  				// Here we create a Utc timestamp but it's only temporary for the correlation
  				// so it will never have to react to timezone changes
  			}
  		}
  		catch (ClassCastException cce) {}
  		return tstamp;
  	}
  
  	/**
  	 * Finish the correlation by creating the appropriate command
  	 * and passing it back to the App
  	 */
  	protected void finishCorrelation()
  	{
  		PointMediaPair[] pointPairs = getPointPairs();
  		if (pointPairs == null || pointPairs.length <= 0) {
  			return;
  		}
  
  		ArrayList<DataPoint> pointsToCreate = new ArrayList<>();
  		ArrayList<PointAndMedia> pointAudioPairs = new ArrayList<>();
  		fillListsForCommand(pointPairs, pointsToCreate, pointAudioPairs);
  
  		Command command = new CorrelateMediaCmd(MediaLinkType.LINK_AUDIOS, pointsToCreate, pointAudioPairs);
  		command.setDescription(makeUndoText(pointAudioPairs.size()));
  		command.setConfirmText(makeConfirmText(pointAudioPairs.size()));
  		_app.execute(command);
  	}
  
  	static void fillListsForCommand(PointMediaPair[] inPointPairs, List<DataPoint> inPointsToCreate, List<PointAndMedia> inPointAudioPairs)
  	{
  		for (PointMediaPair pair : inPointPairs)
  		{
  			if (pair != null && pair.isValid())
  			{
  				AudioClip audioToLink = (AudioClip) pair.getMedia();
  				if (pair.getMinSeconds() == 0L)
  				{
  					// exact match
  					DataPoint point = pair.getPointBefore();
  					AudioClip pointAudio = pair.getPointBefore().getAudio();
  					if (pointAudio == null && !pointAlreadyBeingConnected(point, inPointAudioPairs))
  					{
  						// audio coincides with audioless point so connect the two
  						inPointAudioPairs.add(new PointAndMedia(point, null, audioToLink));
  					}
  					else if (pointAudio != null && pointAudio.equals(pair.getMedia())) {
  						// audio is already connected, nothing to do
  					}
  					else
  					{
  						// point is already connected to a different audio, so need to clone point
  						DataPoint pointToAdd = pair.getPointBefore().clonePoint();
  						inPointsToCreate.add(pointToAdd);
  						inPointAudioPairs.add(new PointAndMedia(pointToAdd, null, audioToLink));
  					}
  				}
  				else
  				{
  					// audio time falls between two points, so need to interpolate new one
  					DataPoint pointToAdd = PointUtils.interpolate(pair.getPointBefore(), pair.getPointAfter(), pair.getFraction());
  					pointToAdd.setSegmentStart(true);
  					inPointsToCreate.add(pointToAdd);
  					inPointAudioPairs.add(new PointAndMedia(pointToAdd, null, audioToLink));
  				}
  			}
  		}
  	}
  }
  