/*
* BasicTrail.java
* de.sciss.timebased package
*
* Copyright (c) 2004-2016 Hanns Holger Rutz. All rights reserved.
*
* This software is published under the GNU General Public License v3+
*
*
* For further information, please contact Hanns Holger Rutz at
* contact@sciss.de
*/
package de.sciss.timebased;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.ConcurrentModificationException;
import java.util.Enumeration;
import java.util.List;
import javax.swing.tree.TreeNode;
import javax.swing.undo.UndoableEdit;
import de.sciss.app.BasicEvent;
import de.sciss.app.BasicUndoableEdit;
import de.sciss.app.EventManager;
import de.sciss.app.PerformableEdit;
import de.sciss.app.AbstractCompoundEdit;
import de.sciss.io.Span;
import de.sciss.util.ListEnum;
public abstract class BasicTrail
implements Trail, EventManager.Processor {
private static final boolean DEBUG = false;
protected static final Comparator<Object> startComparator = new StartComparator();
protected static final Comparator<Object> stopComparator = new StopComparator();
// private static final List collEmpty = new ArrayList( 1 );
private final List<Stake> collStakesByStart = new ArrayList<Stake>(); // sorted using StartComparator
private final List<Stake> collStakesByStop = new ArrayList<Stake>(); // sorted using StopComparator
private List<Stake> collEditByStart = null;
private List<Stake> collEditByStop = null;
private AbstractCompoundEdit currentEdit = null;
private double rate;
private EventManager elm = null; // lazy creation
private List<BasicTrail> dependants = null; // lazy creation
// Element : Trail
private final Object sync = new Object();
public BasicTrail()
{
/* empty */
}
public double getRate()
{
return rate;
}
public void setRate(double rate) {
this.rate = rate;
}
public void clear( Object source )
{
final boolean wasEmpty = isEmpty();
final Span span = getSpan();
clearIgnoreDependants();
// ____ dep ____
if (dependants != null) {
synchronized (sync) {
for (BasicTrail dependant : dependants) {
dependant.clear(source);
}
}
}
if( (source != null) && !wasEmpty ) {
dispatchModification( source, span );
}
}
protected void clearIgnoreDependants() {
Stake stake;
while (!collStakesByStart.isEmpty()) {
stake = collStakesByStart.remove(0);
// stake.setTrail( null );
stake.dispose();
}
collStakesByStop.clear();
}
public void dispose()
{
//System.err.println( "BasicTrail.dispose()" );
// ____ dep ____
// crucial here that dependants are disposed _before_ this object
// coz they might otherwise try be keep running stuff which is already disposed
if( dependants != null ) {
synchronized(sync) {
final Object[] dep = dependants.toArray();
for (Object aDep : dep) {
((BasicTrail) aDep).dispose();
}
}
}
for (Stake aCollStakesByStart : collStakesByStart) aCollStakesByStart.dispose();
collStakesByStart.clear();
collStakesByStop .clear();
}
protected List<Stake> editGetCollByStart(AbstractCompoundEdit ce) {
if ((ce == null) || (collEditByStart == null)) {
return collStakesByStart;
} else {
return collEditByStart;
}
}
protected List<Stake> editGetCollByStop(AbstractCompoundEdit ce) {
if ((ce == null) || (collEditByStop == null)) {
return collStakesByStop;
} else {
return collEditByStop;
}
}
public Span getSpan() {
return editGetSpan(null);
}
public Span editGetSpan(AbstractCompoundEdit ce) {
return new Span(editGetStart(ce), editGetStop(ce));
}
private long editGetStart(AbstractCompoundEdit ce) {
final List<Stake> coll = editGetCollByStart(ce);
return (coll.isEmpty() ? 0 : coll.get(0).getSpan().start);
}
private long editGetStop(AbstractCompoundEdit ce) {
final List<Stake> coll = editGetCollByStop(ce);
return (coll.isEmpty() ? 0 : coll.get(coll.size() - 1).getSpan().stop);
}
public void editBegin(AbstractCompoundEdit ce) {
if (currentEdit != null) {
throw new ConcurrentModificationException("Concurrent editing");
}
currentEdit = ce;
collEditByStart = null; // dispose ? XXXX
collEditByStop = null; // dispose ? XXXX
// ____ dep ____
if (dependants != null) {
synchronized (sync) {
for (BasicTrail dependant : dependants) dependant.editBegin(ce);
}
}
}
public void editEnd( AbstractCompoundEdit ce )
{
checkEdit( ce );
currentEdit = null;
collEditByStart = null; // dispose ? XXXX
collEditByStop = null; // dispose ? XXXX
// ____ dep ____
if( dependants != null ) {
synchronized(sync) {
for (BasicTrail dependant : dependants) {
dependant.editEnd(ce);
}
}
}
}
private void checkEdit( AbstractCompoundEdit ce )
{
if( currentEdit == null ) {
throw new IllegalStateException( "Missing editBegin" );
}
if( ce != currentEdit ) {
throw new ConcurrentModificationException( "Concurrent editing" );
}
}
private void ensureEditCopy()
{
if (collEditByStart == null) {
collEditByStart = new ArrayList<Stake>(collStakesByStart);
collEditByStop = new ArrayList<Stake>(collStakesByStop );
}
}
// returns stakes that intersect OR TOUCH the span
public List<Stake> getRange(Span span, boolean byStart) {
return editGetRange(span, byStart, null);
}
// returns stakes that intersect OR TOUCH the span
public List<Stake> editGetRange(Span span, boolean byStart, AbstractCompoundEdit ce) {
final List<Stake> collByStart, collByStop;
List<Stake> collResult;
int idx;
if (ce == null) {
collByStart = collStakesByStart;
collByStop = collStakesByStop;
} else {
checkEdit(ce);
collByStart = collEditByStart == null ? collStakesByStart : collEditByStart;
collByStop = collEditByStop == null ? collStakesByStop : collEditByStop;
}
if (byStart) {
idx = Collections.binarySearch(collByStop, span.start, stopComparator);
if (idx < 0) {
idx = -(idx + 1);
} else {
// "If the list contains multiple elements equal to the specified object,
// there is no guarantee which one will be found"
idx = getLeftMostIndex(collByStop, idx, false);
}
collResult = new ArrayList<Stake>(collByStop.subList(idx, collByStop.size()));
Collections.sort( collResult, startComparator );
idx = Collections.binarySearch(collResult, span.stop, startComparator);
if (idx < 0) {
idx = -(idx + 1);
} else {
idx = getRightMostIndex(collResult, idx, true) + 1;
}
collResult = collResult.subList(0, idx);
} else {
idx = Collections.binarySearch(collByStart, span.stop, startComparator);
if (idx < 0) {
idx = -(idx + 1);
} else {
idx = getRightMostIndex(collByStart, idx, true) + 1;
}
collResult = new ArrayList<Stake>(collByStart.subList(0, idx));
Collections.sort(collResult, stopComparator);
idx = Collections.binarySearch(collResult, span.start, stopComparator);
if (idx < 0) {
idx = -(idx + 1);
} else {
idx = getLeftMostIndex(collResult, idx, false);
}
collResult = collResult.subList(idx, collResult.size());
}
return collResult;
}
public void insert( Object source, Span span )
{
editInsert( source, span, getDefaultTouchMode(), null );
}
public void insert( Object source, Span span, int touchMode )
{
editInsert( source, span, touchMode, null );
}
public void editInsert(Object source, Span span, AbstractCompoundEdit ce) {
editInsert(source, span, getDefaultTouchMode(), ce);
}
public void editInsert( Object source, Span span, int touchMode, AbstractCompoundEdit ce )
{
final long start = span.start;
// final long stop = span.stop;
final long totStop = editGetStop( ce );
final long delta = span.getLength();
if( (delta == 0) || (start > totStop) ) return;
final List<Stake> collRange = editGetRange( new Span( start, totStop ), true, ce );
if( collRange.isEmpty() ) return;
final List<Stake> collToAdd = new ArrayList<Stake>();
final List<Stake> collToRemove = new ArrayList<Stake>();
final Span modSpan;
Span stakeSpan;
switch( touchMode ) {
case TOUCH_NONE:
// XXX could use binarySearch ?
for (Stake stake : collRange) {
stakeSpan = stake.getSpan();
if (stakeSpan.start >= start) {
collToRemove.add(stake);
collToAdd.add(stake.shiftVirtual(delta));
}
}
break;
case TOUCH_SPLIT:
for (Stake stake : collRange) {
stakeSpan = stake.getSpan();
if (stakeSpan.stop <= start) continue;
collToRemove.add(stake);
if (stakeSpan.start >= start) { // not splitted
collToAdd.add(stake.shiftVirtual(delta));
} else {
collToAdd.add(stake.replaceStop(start));
stake = stake.replaceStart(start);
collToAdd.add(stake.shiftVirtual(delta));
stake.dispose(); // delete temp product
}
}
break;
case TOUCH_RESIZE:
System.err.println( "BasicTrail.insert, touchmode resize : not tested" );
for (Stake stake : collRange) {
stakeSpan = stake.getSpan();
if (stakeSpan.stop > start) {
collToRemove.add(stake);
if (stakeSpan.start > start) {
collToAdd.add(stake.shiftVirtual(delta));
} else {
collToAdd.add(stake.replaceStop(stakeSpan.stop + delta));
}
}
}
break;
default:
throw new IllegalArgumentException( "TouchMode : " + touchMode );
}
modSpan = Span.union( removeAllPr( collToRemove, ce ), addAllPr( collToAdd, ce ));
// ____ dep ____
if( dependants != null ) {
synchronized(sync) {
for (BasicTrail dependant : dependants) {
dependant.editInsert(source, span, touchMode, ce);
}
}
}
if( (source != null) && (modSpan != null) ) {
if( ce != null ) {
ce.addPerform( new Edit( this, modSpan ));
} else {
dispatchModification( source, modSpan );
}
}
}
public void remove( Object source, Span span )
{
editRemove( source, span, getDefaultTouchMode(), null );
}
public void remove( Object source, Span span, int touchMode )
{
editRemove( source, span, touchMode, null );
}
public void editRemove( Object source, Span span, AbstractCompoundEdit ce )
{
editRemove( source, span, getDefaultTouchMode(), ce );
}
/**
* Removes a time span from the trail. Stakes that are included in the
* span will be removed. Stakes that begin after the end of the removed span,
* will be shifted to the left by <code>span.getLength()</code>. Stakes whose <code>stop</code> is
* <code><=</code> the start of removed span, remain unaffected. Stakes that intersect the
* removed span are traited according to the <code>touchMode</code> setting:
* <ul>
* <li><code>TOUCH_NONE</code> : intersecting stakes whose <code>start</code> is smaller than
* the removed span's start remain unaffected ; otherwise they are removed. This mode is usefull
* for markers.</li>
* <li><code>TOUCH_SPLIT</code> : the stake is cut at the removed span's start and stop ; a
* middle part (if existing) is removed ; the left part (if existing) remains as is ; the right part
* (if existing) is shifted by <code>-span.getLength()</code>. This mode is usefull for audio regions.</li>
* <li><code>TOUCH_RESIZE</code> : intersecting stakes whose <code>start</code> is smaller than
* the removed span's start, will keep their start position ; if their stop position lies within the
* removed span, it is truncated to the removed span's start. if their stop position exceeds the removed
* span's stop, the stake's length is shortened by <code>-span.getLength()</code> .
* intersecting stakes whose <code>start</code> is greater or equal to the
* the removed span's start, will by shortened by <code>(removed_span_stop - stake_start)</code> and
* shifted by <code>-span.getLength()</code> . This mode is usefull for marker regions.</li>
* </ul>
*
* @param source source object for event dispatching (or <code>null</code> for no dispatching)
* @param span the span to remove
* @param touchMode the way intersecting staks are handled (see above)
* @param ce provided to make the action undoable ; may be <code>null</code>. if a
* <code>CompoundEdit</code> is provided, disposal of removed stakes is deferred
* until the edit dies ; otherwise (<code>ce == null</code>) removed stakes are
* immediately disposed.
*/
public void editRemove( Object source, Span span, int touchMode, AbstractCompoundEdit ce )
{
final long start = span.start;
final long stop = span.stop;
final long totStop = editGetStop( ce );
final long delta = -span.getLength();
if( (delta == 0) || (start > totStop) ) return;
final List<Stake> collRange = editGetRange( new Span( start, totStop ), true, ce );
if( collRange.isEmpty() ) return;
final List<Stake> collToAdd = new ArrayList<Stake>();
final List<Stake> collToRemove = new ArrayList<Stake>();
final Span modSpan;
Span stakeSpan;
switch( touchMode ) {
case TOUCH_NONE:
// XXX could use binarySearch ?
for (Stake stake : collRange) {
stakeSpan = stake.getSpan();
if (stakeSpan.start < start) continue;
collToRemove.add(stake);
if (stakeSpan.start >= stop) {
collToAdd.add(stake.shiftVirtual(delta));
}
}
break;
case TOUCH_SPLIT:
for (Stake stake : collRange) {
stakeSpan = stake.getSpan();
if (stakeSpan.stop > start) {
collToRemove.add(stake);
if (stakeSpan.start >= start) { // start portion not splitted
if (stakeSpan.start >= stop) { // just shifted
collToAdd.add(stake.shiftVirtual(delta));
} else if (stakeSpan.stop > stop) { // stop portion splitted (otherwise completely removed!)
stake = stake.replaceStart(stop);
collToAdd.add(stake.shiftVirtual(delta));
stake.dispose(); // delete temp product
}
} else {
collToAdd.add(stake.replaceStop(start)); // start portion splitted
if (stakeSpan.stop > stop) { // stop portion splitted
stake = stake.replaceStart(stop);
collToAdd.add(stake.shiftVirtual(delta));
stake.dispose(); // delete temp product
}
}
}
}
break;
case TOUCH_RESIZE:
System.err.println( "BasicTrail.remove, touchmode resize : not tested" );
for (Stake stake : collRange) {
stakeSpan = stake.getSpan();
if (stakeSpan.stop > start) {
collToRemove.add(stake);
if (stakeSpan.start >= start) { // start portion not modified
if (stakeSpan.start >= stop) { // just shifted
collToAdd.add(stake.shiftVirtual(delta));
} else if (stakeSpan.stop > stop) { // stop portion splitted (otherwise completely removed!)
stake = stake.replaceStart(stop);
collToAdd.add(stake.shiftVirtual(delta));
stake.dispose(); // delete temp product
}
} else {
if (stakeSpan.stop <= stop) {
collToAdd.add(stake.replaceStop(start));
} else {
collToAdd.add(stake.replaceStop(stakeSpan.stop + delta));
}
}
}
}
break;
default:
throw new IllegalArgumentException( "TouchMode : " + touchMode );
}
if( DEBUG ) {
System.err.println( this.getClass().getName() + " : removing : " );
for (Stake aCollToRemove : collToRemove) {
System.err.println(" span " + aCollToRemove.getSpan());
}
System.err.println( " : adding : " );
for (Object aCollToAdd : collToAdd) {
System.err.println(" span " + ((Stake) aCollToAdd).getSpan());
}
}
modSpan = Span.union( removeAllPr( collToRemove, ce ), addAllPr( collToAdd, ce ));
// ____ dep ____
if( dependants != null ) {
synchronized(sync) {
for (BasicTrail dependant : dependants) {
dependant.editRemove(source, span, touchMode, ce);
}
}
}
if( (source != null) && !(collToRemove.isEmpty() && collToAdd.isEmpty()) ) {
if( ce != null ) {
ce.addPerform( new Edit( this, modSpan ));
} else {
dispatchModification( source, modSpan );
}
}
}
public void clear( Object source, Span span )
{
editClear( source, span, getDefaultTouchMode(), null );
}
public void clear( Object source, Span span, int touchMode )
{
editClear( source, span, touchMode, null );
}
public void editClear( Object source, Span span, AbstractCompoundEdit ce )
{
editClear( source, span, getDefaultTouchMode(), ce );
}
public void editClear( Object source, Span span, int touchMode, AbstractCompoundEdit ce )
{
final long start = span.start;
final long stop = span.stop;
final List<Stake> collRange = editGetRange( span, true, ce );
if( collRange.isEmpty() ) return;
final List<Stake> collToAdd = new ArrayList<Stake>();
final List<Stake> collToRemove = new ArrayList<Stake>();
final Span modSpan;
Span stakeSpan;
switch( touchMode ) {
case TOUCH_NONE:
for (Stake stake : collRange) {
stakeSpan = stake.getSpan();
if (stakeSpan.start >= start) {
collToRemove.add(stake);
}
}
break;
case TOUCH_SPLIT:
for (Stake stake : collRange) {
stakeSpan = stake.getSpan();
if (stakeSpan.stop > start) {
collToRemove.add(stake);
if (stakeSpan.start >= start) { // start portion not splitted
if (stakeSpan.stop > stop) { // stop portion splitted (otherwise completely removed!)
collToAdd.add(stake.replaceStart(stop));
}
} else {
collToAdd.add(stake.replaceStop(start)); // start portion splitted
if (stakeSpan.stop > stop) { // stop portion splitted
collToAdd.add(stake.replaceStart(stop));
}
}
}
}
break;
case TOUCH_RESIZE:
System.err.println( "BasicTrail.clear, touchmode resize : not tested" );
for (Stake stake : collRange) {
stakeSpan = stake.getSpan();
if (stakeSpan.stop > start) {
collToRemove.add(stake);
if (stakeSpan.start >= start) { // start portion not modified
if (stakeSpan.stop > stop) { // stop portion splitted (otherwise completely removed!)
collToAdd.add(stake.replaceStart(stop));
}
} else {
if (stakeSpan.stop <= stop) {
collToAdd.add(stake.replaceStop(start));
} else {
collToAdd.add(stake.replaceStop(stakeSpan.stop - span.getLength()));
}
}
}
}
break;
default:
throw new IllegalArgumentException( "TouchMode : " + touchMode );
}
if( DEBUG ) {
System.err.println( this.getClass().getName() + " : removing : " );
for (Stake aCollToRemove : collToRemove) {
System.err.println(" span " + aCollToRemove.getSpan());
}
System.err.println( " : adding : " );
for (Stake aCollToAdd : collToAdd) {
System.err.println(" span " + aCollToAdd.getSpan());
}
}
modSpan = Span.union( removeAllPr( collToRemove, ce ), addAllPr( collToAdd, ce ));
// ____ dep ____
if( dependants != null ) {
synchronized(sync) {
for (BasicTrail dependant : dependants) {
dependant.editClear(source, span, touchMode, ce);
}
}
}
if( (source != null) && (modSpan != null) ) {
if( ce != null ) {
ce.addPerform( new Edit( this, modSpan ));
} else {
dispatchModification( source, modSpan );
}
}
}
public Trail getCutTrail(Span span, int touchMode, long shiftVirtual)
{
final BasicTrail trail = createEmptyCopy();
final List<Stake> stakes = getCutRange(span, true, touchMode, shiftVirtual);
// trail.setRate( this.getRate() );
// Collections.sort( stakes, startComparator );
trail.collStakesByStart.addAll( stakes );
Collections.sort(stakes, stopComparator);
trail.collStakesByStop.addAll( stakes );
return trail;
}
protected abstract BasicTrail createEmptyCopy();
public static List<Stake> getCuttedRange(List<Stake> stakes, Span span, boolean byStart,
int touchMode, long shiftVirtual) {
if (stakes.isEmpty()) return stakes;
final List<Stake> collResult = new ArrayList<Stake>();
final long start = span.start;
final long stop = span.stop;
final boolean shift = shiftVirtual != 0;
Span stakeSpan;
switch( touchMode ) {
case TOUCH_NONE:
for (Stake stake : stakes) {
stakeSpan = stake.getSpan();
if (stakeSpan.start >= start) {
if (shift) {
collResult.add(stake.shiftVirtual(shiftVirtual));
} else {
collResult.add(stake.duplicate());
}
}
}
break;
case TOUCH_SPLIT:
for (Stake stake : stakes) {
stakeSpan = stake.getSpan();
if (stakeSpan.start >= start) { // start portion not splitted
if (stakeSpan.stop <= stop) { // completely included, just make a copy
if (shift) {
collResult.add(stake.shiftVirtual(shiftVirtual));
} else {
collResult.add(stake.duplicate());
}
} else { // adjust stop
stake = stake.replaceStop(stop);
if (shift) {
final Stake stake2 = stake;
stake = stake.shiftVirtual(shiftVirtual);
stake2.dispose(); // delete temp product
}
collResult.add(stake);
}
} else {
if (stakeSpan.stop <= stop) { // stop included, just adjust start
stake = stake.replaceStart(start);
if (shift) {
final Stake stake2 = stake;
stake = stake.shiftVirtual(shiftVirtual);
stake2.dispose(); // delete temp product
}
collResult.add(stake);
} else { // adjust both start and stop
final Stake stake2 = stake.replaceStart(start);
stake = stake2.replaceStop(stop);
stake2.dispose(); // delete temp product
if (shift) {
final Stake stake3 = stake;
stake = stake.shiftVirtual(shiftVirtual);
stake3.dispose(); // delete temp product
}
collResult.add(stake);
}
}
}
break;
default:
throw new IllegalArgumentException( "TouchMode : " + touchMode );
}
return collResult;
}
public List<Stake> getCutRange(Span span, boolean byStart, int touchMode, long shiftVirtual) {
return BasicTrail.getCuttedRange(getRange(span, byStart), span, byStart, touchMode, shiftVirtual);
}
public Stake get(int idx, boolean byStart) {
final List<Stake> coll = byStart ? collStakesByStart : collStakesByStop;
return coll.get(idx);
}
public int getNumStakes()
{
return collStakesByStart.size();
}
public boolean isEmpty()
{
return collStakesByStart.isEmpty();
}
public boolean contains( Stake stake )
{
return indexOf( stake, true ) >= 0;
}
public int indexOf( Stake stake, boolean byStart )
{
return editIndexOf( stake, byStart, null );
}
public int editIndexOf( Stake stake, boolean byStart, AbstractCompoundEdit ce )
{
final List<Stake> coll;
final Comparator<Object> comp = byStart ? startComparator : stopComparator;
final int idx;
if( ce == null ) {
coll = byStart ? collStakesByStart : collStakesByStop;
} else {
checkEdit( ce );
coll = byStart ? (collEditByStart == null ? collStakesByStart : collEditByStart) :
(collEditByStop == null ? collStakesByStop : collEditByStop);
}
// "If the list contains multiple elements equal to the specified object,
// there is no guarantee which one will be found"
idx = Collections.binarySearch(coll, stake, comp);
if (idx >= 0) {
Stake stake2 = coll.get(idx);
if (stake2.equals(stake)) return idx;
for (int idx2 = idx - 1; idx2 >= 0; idx2--) {
stake2 = coll.get(idx2);
if (stake2.equals(stake)) return idx2;
}
for (int idx2 = idx + 1; idx2 < coll.size(); idx2++) {
stake2 = coll.get(idx2);
if (stake2.equals(stake)) return idx2;
}
}
return idx;
}
public int indexOf( long pos, boolean byStart )
{
return editIndexOf( pos, byStart, null );
}
public int editIndexOf(long pos, boolean byStart, AbstractCompoundEdit ce) {
if (byStart) {
return Collections.binarySearch(editGetCollByStart(ce), pos, startComparator);
} else {
return Collections.binarySearch(editGetCollByStop(ce), pos, stopComparator);
}
}
public Stake editGetLeftMost(int idx, boolean byStart, AbstractCompoundEdit ce) {
if (idx < 0) {
idx = -(idx + 2);
if (idx < 0) return null;
}
final List<Stake> coll = byStart ? editGetCollByStart(ce) : editGetCollByStop(ce);
Stake lastStake = coll.get(idx);
final long pos = byStart ? lastStake.getSpan().start : lastStake.getSpan().stop;
Stake nextStake;
while (idx > 0) {
nextStake = coll.get(--idx);
if ((byStart ? nextStake.getSpan().start : nextStake.getSpan().stop) != pos) break;
lastStake = nextStake;
}
return lastStake;
}
public Stake editGetRightMost(int idx, boolean byStart, AbstractCompoundEdit ce) {
final List<Stake> coll = byStart ? editGetCollByStart(ce) : editGetCollByStop(ce);
final int sizeM1 = coll.size() - 1;
if (idx < 0) {
idx = -(idx + 1);
if (idx > sizeM1) return null;
}
Stake lastStake = coll.get(idx);
final long pos = byStart ? lastStake.getSpan().start : lastStake.getSpan().stop;
Stake nextStake;
while (idx < sizeM1) {
nextStake = coll.get(++idx);
if ((byStart ? nextStake.getSpan().start : nextStake.getSpan().stop) != pos) break;
lastStake = nextStake;
}
return lastStake;
}
public int editGetLeftMostIndex(int idx, boolean byStart, AbstractCompoundEdit ce) {
final List<Stake> coll = byStart ? editGetCollByStart(ce) : editGetCollByStop(ce);
return getLeftMostIndex(coll, idx, byStart);
}
private int getLeftMostIndex(List<Stake> coll, int idx, boolean byStart) {
if (idx < 0) {
idx = -(idx + 2);
if (idx < 0) return -1;
}
Stake stake = coll.get(idx);
final long pos = byStart ? stake.getSpan().start : stake.getSpan().stop;
while (idx > 0) {
stake = coll.get(idx - 1);
if ((byStart ? stake.getSpan().start : stake.getSpan().stop) != pos) break;
idx--;
}
return idx;
}
public int editGetRightMostIndex_(int idx, boolean byStart, AbstractCompoundEdit ce) {
final List<Stake> coll = byStart ? editGetCollByStart(ce) : editGetCollByStop(ce);
return getRightMostIndex(coll, idx, byStart);
}
private int getRightMostIndex(List<Stake> coll, int idx, boolean byStart) {
final int sizeM1 = coll.size() - 1;
if (idx < 0) {
idx = -(idx + 1);
if (idx > sizeM1) return -1;
}
Stake stake = coll.get(idx);
final long pos = byStart ? stake.getSpan().start : stake.getSpan().stop;
while (idx < sizeM1) {
stake = coll.get(idx + 1);
if ((byStart ? stake.getSpan().start : stake.getSpan().stop) != pos) break;
idx++;
}
return idx;
}
public List<Stake> getAll(boolean byStart) {
final List<Stake> coll = byStart ? collStakesByStart : collStakesByStop;
return new ArrayList<Stake>(coll);
}
public List<Stake> getAll(int startIdx, int stopIdx, boolean byStart) {
final List<Stake> coll = byStart ? collStakesByStart : collStakesByStop;
return new ArrayList<Stake>(coll.subList(startIdx, stopIdx));
}
public void add(Object source, Stake stake)
throws IOException {
editAddAll(source, Collections.singletonList(stake), null); // ____ dep ____ handled there
}
public void editAdd(Object source, Stake stake, AbstractCompoundEdit ce)
throws IOException {
editAddAll(source, Collections.singletonList(stake), ce); // ____ dep ____ handled there
}
public void addAll(Object source, List<Stake> stakes)
throws IOException {
editAddAll(source, stakes, null);
}
public void editAddAll(Object source, List<Stake> stakes, AbstractCompoundEdit ce)
throws IOException {
if (DEBUG) System.err.println("editAddAll " + stakes.size());
if (stakes.isEmpty()) return;
if (ce != null) {
checkEdit(ce);
}
final Span span = addAllPr(stakes, ce);
// ____ dep ____
if ((dependants != null) && (span != null)) {
synchronized (sync) {
for (BasicTrail dependant : dependants) dependant.addAllDep(source, stakes, ce, span);
}
}
if ((source != null) && (span != null)) {
if (ce != null) {
ce.addPerform(new Edit(this, span));
} else {
dispatchModification(source, span);
}
}
}
/**
* To be overwritten by dependants.
*/
protected void addAllDep(Object source, List<Stake> stakes, AbstractCompoundEdit ce, Span span)
throws IOException {
/* empty */
}
private Span addAllPr(List<Stake> stakes, AbstractCompoundEdit ce) {
if (stakes.isEmpty()) return null;
long start = Long.MAX_VALUE;
long stop = Long.MIN_VALUE;
final Span span;
for (Stake stake : stakes) {
sortAddStake(stake, ce);
start = Math.min(start, stake.getSpan().start);
stop = Math.max(stop , stake.getSpan().stop );
}
span = new Span(start, stop);
if (ce != null) ce.addPerform(new Edit(this, stakes, span, EDIT_ADD));
return span;
}
public void remove(Object source, Stake stake)
throws IOException {
editRemoveAll(source, Collections.singletonList(stake), null);
}
public void editRemove(Object source, Stake stake, AbstractCompoundEdit ce)
throws IOException {
editRemoveAll(source, Collections.singletonList(stake), ce); // ____ dep ____ handled there
}
public void removeAll(Object source, List<Stake> stakes)
throws IOException {
editRemoveAll(source, stakes, null);
}
public void editRemoveAll(Object source, List<Stake> stakes, AbstractCompoundEdit ce)
throws IOException {
if (stakes.isEmpty()) return;
if( ce != null ) {
checkEdit( ce );
}
final Span span = removeAllPr( stakes, ce );
// ____ dep ____
if( (dependants != null) && (span != null) ) {
synchronized(sync) {
for (BasicTrail dependant : dependants) {
dependant.removeAllDep(source, stakes, ce, span);
}
}
}
if ((source != null) && (span != null)) {
if (ce != null) {
ce.addPerform(new Edit(this, span));
} else {
dispatchModification(source, span);
}
}
}
/**
* To be overwritten by dependants.
*/
protected void removeAllDep(Object source, List<Stake> stakes, AbstractCompoundEdit ce, Span span)
throws IOException {
/* empty */
}
private Span removeAllPr(List<Stake> stakes, AbstractCompoundEdit ce) {
if (stakes.isEmpty()) return null;
long start = Long.MAX_VALUE;
long stop = Long.MIN_VALUE;
Stake stake;
final Span span;
for (Object stake1 : stakes) {
stake = (Stake) stake1;
sortRemoveStake(stake, ce);
start = Math.min(start, stake.getSpan().start);
stop = Math.max(stop, stake.getSpan().stop);
if (ce == null) stake.dispose();
}
span = new Span( start, stop );
if( ce != null ) ce.addPerform( new Edit( this, stakes, span, EDIT_REMOVE ));
return span;
}
public void debugDump() {
/* empty */
}
public void debugVerifyContiguity() {
Stake stake;
final Span totalSpan = this.getSpan();
long lastStop = totalSpan.start;
Span stakeSpan;
boolean ok = true;
System.err.println("total Span = " + totalSpan);
for (int i = 0; i < collStakesByStart.size(); i++) {
stake = collStakesByStart.get(i);
stakeSpan = stake.getSpan();
if (stakeSpan.start != lastStop) {
System.err.println("! broken contiguity for stake #" + i + " (" + stake + ") : "
+ stakeSpan + " should have start of " + lastStop);
ok = false;
}
if (stakeSpan.getLength() == 0) {
System.err.println("! warning : stake #" + i + " (" + stake + ") has zero length");
} else if (stakeSpan.getLength() < 0) {
System.err.println("! illegal span length for stake #" + i + " (" + stake + ") : " + stakeSpan);
ok = false;
}
lastStop = stakeSpan.stop;
}
System.err.println("--- result: " + (ok ? "OK." : "ERRORNEOUS!"));
}
protected void addIgnoreDependants(Stake stake) {
sortAddStake(stake, null);
}
protected void sortAddStake(Stake stake, AbstractCompoundEdit ce) {
final List<Stake> collByStart, collByStop;
int idx;
if (ce == null) {
collByStart = collStakesByStart;
collByStop = collStakesByStop;
} else {
ensureEditCopy();
collByStart = collEditByStart;
collByStop = collEditByStop;
}
idx = editIndexOf(stake.getSpan().start, true, ce); // look for position only!
if (idx < 0) idx = -(idx + 1);
collByStart.add(idx, stake);
idx = editIndexOf(stake.getSpan().stop, false, ce);
if (idx < 0) idx = -(idx + 1);
collByStop.add(idx, stake);
stake.setTrail(this); // ???
}
protected void sortRemoveStake(Stake stake, AbstractCompoundEdit ce) {
final List<Stake> collByStart, collByStop;
int idx;
if (ce == null) {
collByStart = collStakesByStart;
collByStop = collStakesByStop;
} else {
ensureEditCopy();
collByStart = collEditByStart;
collByStop = collEditByStop;
}
idx = editIndexOf(stake, true, ce);
if (idx >= 0) collByStart.remove(idx); // look for object equality!
idx = editIndexOf(stake, false, ce);
if (idx >= 0) collByStop.remove(idx);
}
public void addListener(Trail.Listener listener) {
if (elm == null) {
elm = new EventManager(this);
}
elm.addListener(listener);
}
public void removeListener(Trail.Listener listener) {
elm.removeListener(listener);
}
public void addDependant(BasicTrail sub) {
if (dependants == null) {
dependants = new ArrayList<BasicTrail>();
}
synchronized (sync) {
if (dependants.contains(sub)) {
System.err.println("BasicTrail.addDependant : WARNING : duplicate add");
}
dependants.add(sub);
}
}
public void removeDependant(BasicTrail sub) {
synchronized (sync) {
if (!dependants.remove(sub)) {
System.err.println("BasicTrail.removeDependant : WARNING : was not in list");
}
}
}
public int getNumDependants() {
if (dependants == null) {
return 0;
} else {
synchronized (sync) {
return dependants.size();
}
}
}
public BasicTrail getDependant(int i) {
synchronized (sync) {
return dependants.get(i);
}
}
protected void dispatchModification(Object source, Span span) {
if (elm != null) {
elm.dispatchEvent(new Trail.Event(this, source, span));
}
}
// ---------------- TreeNode interface ----------------
public TreeNode getChildAt( int childIndex )
{
return get( childIndex, true );
}
public int getChildCount()
{
return getNumStakes();
}
public TreeNode getParent()
{
return null;
}
public int getIndex( TreeNode node )
{
if( node instanceof Stake ) {
return indexOf( (Stake) node, true );
} else {
return -1;
}
}
public boolean getAllowsChildren()
{
return true;
}
public boolean isLeaf()
{
return false;
}
public Enumeration<?> children() {
return new ListEnum(getAll(true));
}
// --------------------- EventManager.Processor interface ---------------------
/**
* This is called by the EventManager
* if new events are to be processed. This
* will invoke the listener's <code>propertyChanged</code> method.
*/
public void processEvent(BasicEvent e) {
Trail.Listener listener;
int i;
Trail.Event te = (Trail.Event) e;
for (i = 0; i < elm.countListeners(); i++) {
listener = (Trail.Listener) elm.getListener(i);
switch (e.getID()) {
case Trail.Event.MODIFIED:
listener.trailModified(te);
break;
default:
assert false : e.getID();
break;
}
} // for( i = 0; i < this.countListeners(); i++ )
}
// --------------------- internal classes ---------------------
// undoable edits
private static final int EDIT_ADD = 0;
private static final int EDIT_REMOVE = 1;
private static final int EDIT_DISPATCH = 2;
protected static final String[] EDIT_NAMES = { "Add", "Remove", "Dispatch" };
// @todo disposal is wrong (leaks?) when edit is not performed (e.g. EDIT_ADD not performed)
// @todo dispatch should not be a separate edit but one that is sucked and collapsed through multiple EDIT_ADD / EDIT_REMOVE stages
@SuppressWarnings("serial")
private static class Edit
extends BasicUndoableEdit {
private final int cmd;
private final List<Stake> stakes;
private final String key;
private final BasicTrail trail;
// private boolean removed;
private boolean disposeWhenDying;
private Span span;
protected Edit(BasicTrail t, Span span) {
this(t, null, span, EDIT_DISPATCH, "editChangeTrail");
}
protected Edit(BasicTrail t, List<Stake> stakes, Span span, int cmd) {
this(t, stakes, span, cmd, "editChangeTrail");
}
private Edit(BasicTrail t, List<Stake> stakes, Span span, int cmd, String key) {
this.stakes = stakes;
this.cmd = cmd;
this.key = key;
this.span = span;
this.trail = t;
// removed = false;
disposeWhenDying = stakes != null;
}
private void addAll() {
for (Stake stake : stakes) {
trail.sortAddStake(stake, null);
}
disposeWhenDying = false;
}
private void removeAll() {
for (Stake stake : stakes) {
trail.sortRemoveStake(stake, null);
}
disposeWhenDying = true;
}
private void disposeAll() {
for (Stake stake : stakes) {
stake.dispose();
}
}
public void undo() {
super.undo();
switch (cmd) {
case EDIT_ADD:
removeAll();
break;
case EDIT_REMOVE:
addAll();
break;
case EDIT_DISPATCH:
trail.dispatchModification(trail, span);
break;
default:
assert false : cmd;
}
}
public void redo() {
super.redo();
perform();
}
public PerformableEdit perform() {
switch (cmd) {
case EDIT_ADD:
addAll();
break;
case EDIT_REMOVE:
removeAll();
break;
case EDIT_DISPATCH:
trail.dispatchModification(trail, span);
break;
default:
assert false : cmd;
}
return this;
}
public void die() {
super.die();
if (disposeWhenDying) {
disposeAll();
}
}
public String getPresentationName() {
return getResourceString(key);
}
public String toString() {
return (trail.getClass().getName() + "$Edit:" + EDIT_NAMES[cmd] + "; span = " + span +
"; canUndo = " + canUndo() + "; canRedo = " + canRedo() + "; isSignificant = " + isSignificant());
}
/**
* Collapses multiple successive edits
* into one single edit. The new edit is sucked off by
* the old one.
*/
public boolean addEdit(UndoableEdit anEdit) {
if (!(anEdit instanceof Edit)) return false;
final Edit old = (Edit) anEdit;
if ((old.trail == this.trail) && (old.cmd == this.cmd)) {
switch (cmd) {
case EDIT_ADD:
case EDIT_REMOVE:
this.stakes.addAll(old.stakes);
this.span = this.span.union(old.span);
break;
case EDIT_DISPATCH:
this.span = this.span.union(old.span);
break;
default:
assert false : cmd;
}
old.die();
return true;
} else {
return false;
}
}
/**
* Collapses multiple successive edits
* into one single edit. The old edit is sucked off by
* the new one.
*/
public boolean replaceEdit(UndoableEdit anEdit) {
return addEdit(anEdit); // same behaviour in this case
}
}
// ---------------- comparators ----------------
// XXX TODO - this is really horrible, allowing both `Stake` and `Long`
private static class StartComparator
implements Comparator<Object> {
protected StartComparator() { /* empty */ }
public int compare(Object o1, Object o2) {
long l1 = (o1 instanceof Stake) ? ((Stake) o1).getSpan().start : (Long) o1;
long l2 = (o2 instanceof Stake) ? ((Stake) o2).getSpan().start : (Long) o2;
return l1 < l2 ? -1 : (l1 > l2 ? 1 : 0);
}
public boolean equals(Object o) {
return ((o != null) && (o instanceof StartComparator));
}
}
private static class StopComparator
implements Comparator<Object> {
protected StopComparator() { /* empty */ }
public int compare(Object o1, Object o2) {
long l1 = (o1 instanceof Stake) ? ((Stake) o1).getSpan().stop : (Long) o1;
long l2 = (o2 instanceof Stake) ? ((Stake) o2).getSpan().stop : (Long) o2;
return l1 < l2 ? -1 : (l1 > l2 ? 1 : 0);
}
public boolean equals(Object o) {
return ((o != null) && (o instanceof StopComparator));
}
}
}