source: trunk/src/org/apollo/audio/structure/AudioStructureModel.java@ 1102

Last change on this file since 1102 was 1102, checked in by davidb, 6 years ago

Reworking of the code-base to separate logic from graphics. This version of Expeditee now supports a JFX graphics as an alternative to SWING

File size: 50.2 KB
Line 
1package org.apollo.audio.structure;
2
3import java.io.FileNotFoundException;
4import java.io.IOException;
5import java.util.HashMap;
6import java.util.HashSet;
7import java.util.LinkedList;
8import java.util.List;
9import java.util.Map;
10import java.util.Queue;
11import java.util.Set;
12import java.util.Stack;
13
14import javax.sound.sampled.UnsupportedAudioFileException;
15
16import org.apollo.audio.ApolloSubjectChangedEvent;
17import org.apollo.io.AudioIO;
18import org.apollo.io.AudioPathManager;
19import org.apollo.mvc.AbstractSubject;
20import org.apollo.mvc.SubjectChangedEvent;
21import org.apollo.util.ExpediteeFileTextSearch;
22import org.apollo.util.Mutable;
23import org.apollo.util.TextItemSearchResult;
24import org.apollo.widgets.LinkedTrack;
25import org.apollo.widgets.SampledTrack;
26import org.apollo.widgets.TrackWidgetCommons;
27import org.expeditee.core.BlockingRunnable;
28import org.expeditee.gio.EcosystemManager;
29import org.expeditee.gui.DisplayController;
30import org.expeditee.gui.Frame;
31import org.expeditee.gui.FrameIO;
32import org.expeditee.io.Conversion;
33import org.expeditee.items.ItemUtils;
34import org.expeditee.items.widgets.Widget;
35import org.expeditee.settings.folders.FolderSettings;
36
37/**
38 * A thread safe model of the hierarchical structure of a track graph...
39 * abstracted from Expeditee frames and audio widgets.
40 *
41 * The track widgets notify this model for keeping the structure consistent with
42 * Expeditee's data.
43 *
44 *
45 * <b>Thread safe convention:</b>
46 * OverdubbedFrame's and TrackGraphInfo are always handled on the swing thread.
47 * This class provides asynch loading routines but that is purely for loading - not
48 * handling!
49 *
50 * The track graph is a loop-free directed graph.
51 *
52 * A graph root is a graph of OverdubbedFrame's
53 *
54 * Although a {@link TrackGraphNode} or a {@link LinkedTracksGraphNode} may reside
55 * in multiple graphs, there is always only one instance of them - they
56 * are just shared.
57 *
58 * There are multiple graphs because Graphs can be mutually exlusive.
59 * Or some frames cannot be reached unless they are a starting point (aka root)
60 *
61 * @author Brook Novak
62 *
63 */
64public class AudioStructureModel extends AbstractSubject {
65
66 /**
67 * A loop-free directed graph.
68 * For each graph root, there can be no other graph root that is
69 * reachable (other than itself).
70 */
71 private Set<OverdubbedFrame> graphRoots = new HashSet<OverdubbedFrame>(); // SHARED RESOURCE
72
73 /** All overdubbed frames loaded in memory. MAP = Framename -> Overdub instance */
74 private Map<String, OverdubbedFrame> allOverdubbedFrames = new HashMap<String, OverdubbedFrame>(); // SHARED RESOURCE
75
76 /** For resources {@link AudioStructureModel#graphRoots} and {@link AudioStructureModel#allOverdubbedFrames} */
77 private Object sharedResourceLocker = new Object();
78
79 private boolean cancelFetch = false;
80
81 private DelayedModelUpdator delayedModelUpdator = null;
82
83 private static AudioStructureModel instance = new AudioStructureModel();
84
85 private AudioStructureModel() { }
86
87 public static AudioStructureModel getInstance() {
88 return instance;
89 }
90
91 /**
92 * <b>MUST NOT BE IN THE EXPEDITEE THREAD! OTHERWISE WILL DEFINITELY DEADLOCK</b>
93 *
94 * Same as {@link #fetchGraph(String)} but waits for any updates to finish before
95 * the fetch.
96 *
97 * Intention: really to highlight the need to call waitOnUpdates prior to this ... but
98 * may not bee neccessary...
99 *
100 * @param rootFrameName
101 * refer to #fetchGraph(String)
102 * @return
103 * refer to #fetchGraph(String)
104 *
105 * @throws NullPointerException
106 * if rootFrameName is null.
107 *
108 * @throws IllegalArgumentException
109 * If rootFrameName is not a valid framename.
110 *
111 * @throws InterruptedException
112 * refer to #fetchGraph(String)
113 *
114 * @throws TrackGraphLoopException
115 * refer to #fetchGraph(String) -
116 * <B>However</B> - it could also be that the reason was due to the thread being interupted while
117 * waiting on a update to finish. Thus may want to manually wait for updates prior - see
118 * {@link #waitOnUpdates()}.
119 *
120 * @see #fetchGraph(String)
121 */
122 public synchronized OverdubbedFrame fetchLatestGraph(String rootFrameName)
123 throws InterruptedException, TrackGraphLoopException {
124 waitOnUpdates();
125 return fetchGraph(rootFrameName);
126 }
127
128 /**
129 * May have to read from file if not yet loaded in memory.
130 * Thus it could take some time.
131 *
132 * Thread safe.
133 *
134 * NOTE: The intention is that this is called on a dedicated thread... other than
135 * swings thread. However once a OverdubbedFrame is returned be sure
136 * to only use it on the swing thread by convention.
137 *
138 * <b>MUST NOT NE IN THE EXPEDITEE THREAD! OTHERWISE WILL DEFINITELY DEADLOCK</b>
139 *
140 * @param rootFrameName
141 * Must not be null. Mustn't be a link - must be the <i>framename</i>
142 *
143 * @return
144 * The overdubbed frame.
145 * Null if the frame does not exist - or if it does exist but there
146 * are no track / linked-track widgets on it. Not that it can return
147 * a overdubbed frame that contains a heirarchy of linked overdubbed frames
148 * but no actual playable tracks.
149 *
150 * @throws NullPointerException
151 * if rootFrameName is null.
152 *
153 * @throws IllegalArgumentException
154 * If rootFrameName is not a valid framename.
155 *
156 * @throws InterruptedException
157 * If the fetch request was cancelled because the model
158 * has changed in some way during the read. Must retry the fetch.
159 *
160 * @throws TrackGraphLoopException
161 * If the requested root introduces loops to the current graph state.
162 * The loop trace will be provided in the exception.
163 *
164 * @see TrackGraphLoopException#getFullLoopTrace()
165 *
166 */
167 public synchronized OverdubbedFrame fetchGraph(String rootFrameName)
168 throws InterruptedException, TrackGraphLoopException {
169
170 if(rootFrameName == null) throw new NullPointerException("rootFrameName");
171 if(!FrameIO.isValidFrameName(rootFrameName))
172 throw new IllegalArgumentException("the rootFrameName \""
173 + rootFrameName +"\" is not a valid framename");
174
175 // reset flag
176 cancelFetch = false; // note that race conditions here is beside the point...meaningless
177
178 OverdubbedFrame rootODFrame = null;
179 synchronized(sharedResourceLocker) {
180 rootODFrame = allOverdubbedFrames.get(rootFrameName.toLowerCase());
181 }
182
183 if (rootODFrame != null) {
184 return rootODFrame;
185 }
186
187 // There is no overdub frame loaded for the requested frame.
188 // Thus create a new root
189 Map<String, OverdubbedFrame> newGraph = new HashMap<String, OverdubbedFrame> ();
190 rootODFrame = buildGraph(rootFrameName, newGraph); // throws InterruptedException's
191
192 // There exists no such frame or is not an actual overdubbed frame
193 if (rootODFrame == null || rootODFrame.isEmpty()) return null;
194
195 // Must run on swing thread for checking for loops before commiting the new graph to
196 // the model.
197 NewGraphCommitor commit = new NewGraphCommitor(rootODFrame, newGraph);
198 try {
199 EcosystemManager.getMiscManager().runOnGIOThread(commit);
200 } catch (Exception e) {
201 e.printStackTrace();
202 assert(false);
203 }
204
205 // Check if commit aborted
206 if (commit.abortedCommit) {
207
208 if (cancelFetch) { // Due to cancel request?
209 throw new InterruptedException();
210 }
211
212 // Must be due to loop
213 assert (!commit.loopTrace.isEmpty());
214 throw new TrackGraphLoopException(commit.loopTrace);
215 }
216
217 return rootODFrame;
218 }
219
220 /**
221 * Assumption: that this is called from another thread other than the swing thread.
222 *
223 * @param rootFrameName
224 *
225 * @param newNodes
226 *
227 * @return
228 * Null if rootFrameName does not exist
229 *
230 * @throws InterruptedException
231 */
232 private OverdubbedFrame buildGraph(String rootFrameName, Map<String, OverdubbedFrame> newNodes)
233 throws InterruptedException
234 {
235 // Must be a frame name, not a link
236 assert(rootFrameName != null);
237 assert(FrameIO.isValidFrameName(rootFrameName));
238
239 // If cancelled the immediatly abort fetch
240 if (cancelFetch) { // check for cancel request
241 throw new InterruptedException();
242 }
243
244 // Look for existing node on previously loaded graph...
245 OverdubbedFrame oframe = null;
246 synchronized(sharedResourceLocker) {
247 oframe = allOverdubbedFrames.get(rootFrameName.toLowerCase());
248 }
249
250 // Check to see if not already created this node during this recursive call
251 if (oframe == null)
252 oframe = newNodes.get(rootFrameName.toLowerCase());
253
254 if (oframe != null) return oframe;
255
256 // There are no existing nodes ... thus create a new node from searching for
257 // the frame from file
258
259 // But first .. look in expeditee's cache since the cache might not be consistent with the file system yet
260 // .. or in the common case - the root could be the current frame.
261 ExpediteeCachedTrackInfoFetcher cacheFetch = new ExpediteeCachedTrackInfoFetcher(rootFrameName);
262
263 try {
264 EcosystemManager.getMiscManager().runOnGIOThread(cacheFetch);
265 } catch (Exception e) {
266 e.printStackTrace();
267 assert(false);
268 }
269
270 /* Localfilename -> TrackModelData. */
271 Map<String, TrackModelData> tracks = null;
272
273 /* VirtualFilename -> LinkedTrackModelData. */
274 Map<String, LinkedTrackModelData> linkedTracks = null;
275
276 if (cacheFetch.tracks != null) {
277
278 tracks = cacheFetch.tracks;
279 assert(cacheFetch.linkedTracks != null);
280 linkedTracks = cacheFetch.linkedTracks;
281
282 } else { // search on file system
283
284 String trackPrefix = ItemUtils.GetTag(ItemUtils.TAG_IWIDGET) + ": ";
285 String linkedTrackPrefix = trackPrefix;
286
287 trackPrefix += SampledTrack.class.getName();
288 linkedTrackPrefix += LinkedTrack.class.getName();
289
290 String fullPath = null;
291 for (int i = 0; i < FolderSettings.FrameDirs.get().size(); i++) { // RISKY CODE - IN EXPEDITEE SPACE FROM RANDOM TRHEAD
292 String possiblePath = FolderSettings.FrameDirs.get().get(i);
293 fullPath = FrameIO.getFrameFullPathName(possiblePath, rootFrameName);
294 if (fullPath != null)
295 break;
296 }
297
298 // does even exist?
299 if (fullPath == null) {
300 return null;
301 }
302
303 try {
304
305 // Perform prefix search
306 List<TextItemSearchResult> results = ExpediteeFileTextSearch.prefixSearch(
307 fullPath,
308 new String[] {trackPrefix, linkedTrackPrefix});
309
310 // Parse search results
311 tracks = new HashMap<String, TrackModelData>();
312 linkedTracks = new HashMap<String, LinkedTrackModelData>();
313
314 for (TextItemSearchResult result : results) {
315
316 // Track widget
317 if (result.text.startsWith(trackPrefix)) {
318
319 String name = null;
320 String localFileName = null;
321 Mutable.Long initiationTime = null;
322
323 for (String data : result.data) { // read data lines
324 data = data.trim();
325
326 if (data.startsWith(SampledTrack.META_LOCALNAME_TAG) &&
327 data.length() > SampledTrack.META_LOCALNAME_TAG.length()) {
328 localFileName = data.substring(SampledTrack.META_LOCALNAME_TAG.length());
329
330 } else if (data.startsWith(TrackWidgetCommons.META_INITIATIONTIME_TAG)
331 && data.length() > TrackWidgetCommons.META_INITIATIONTIME_TAG.length()) {
332
333 try {
334 initiationTime = Mutable.createMutableLong(Long.parseLong(
335 data.substring(TrackWidgetCommons.META_INITIATIONTIME_TAG.length())));
336 } catch (NumberFormatException e) { /* Consume */ }
337
338 } else if (data.startsWith(TrackWidgetCommons.META_NAME_TAG)
339 && data.length() > TrackWidgetCommons.META_NAME_TAG.length()) {
340
341 name = data.substring(TrackWidgetCommons.META_NAME_TAG.length());
342
343 }
344 }
345
346 // Add track to map
347 if (localFileName != null) {
348 tracks.put(localFileName, new TrackModelData(
349 initiationTime, -1, name, result.position.y)); // pass -1 for running time to signify that must be read from audio file
350 }
351
352 // Linked track widget
353 } else {
354
355 assert(result.text.startsWith(linkedTrackPrefix));
356
357 // If the linked track infact has a link
358 if (result.explink != null && result.explink.length() > 0) {
359
360 Mutable.Long initiationTime = null;
361 String virtualFilename = null;
362 String name = null;
363
364 for (String data : result.data) { // read data lines
365 data = data.trim();
366
367 // OK OK, Smell in code here - duplicated from above. - sorta
368 if (data.startsWith(LinkedTrack.META_VIRTUALNAME_TAG) &&
369 data.length() > LinkedTrack.META_VIRTUALNAME_TAG.length()) {
370 virtualFilename = data.substring(LinkedTrack.META_VIRTUALNAME_TAG.length());
371
372 } else if (data.startsWith(TrackWidgetCommons.META_INITIATIONTIME_TAG)
373 && data.length() > TrackWidgetCommons.META_INITIATIONTIME_TAG.length()) {
374
375 try {
376 initiationTime = Mutable.createMutableLong(Long.parseLong(
377 data.substring(TrackWidgetCommons.META_INITIATIONTIME_TAG.length())));
378 } catch (NumberFormatException e) { /* Consume */ }
379
380 } else if (data.startsWith(TrackWidgetCommons.META_NAME_TAG)
381 && data.length() > TrackWidgetCommons.META_NAME_TAG.length()) {
382
383 name = data.substring(TrackWidgetCommons.META_NAME_TAG.length());
384
385 }
386 }
387
388 // Add linked track to map
389 if (virtualFilename != null) {
390 linkedTracks.put(virtualFilename, new LinkedTrackModelData(
391 initiationTime, result.explink, name, result.position.y)); // pass -1 for running time to signify that must be read from audio file
392 }
393
394 }
395
396
397 }
398
399 } // Proccess next result
400
401 } catch (FileNotFoundException e) {
402 e.printStackTrace();
403 return null;
404 } catch (IOException e) {
405 e.printStackTrace();
406 return null;
407 }
408
409 }
410
411 if (cancelFetch) { // check for cancel request
412 throw new InterruptedException();
413 }
414
415 assert(tracks != null);
416 assert(linkedTracks != null);
417
418 // Add new node (avoid infite recursion)
419 oframe = new OverdubbedFrame(rootFrameName);
420 newNodes.put(rootFrameName.toLowerCase(), oframe);
421
422 // Load track times from file
423 for (String localFilename : tracks.keySet()) {
424
425 if (cancelFetch) { // check for cancel request
426 throw new InterruptedException();
427 }
428
429 TrackModelData tmodel = tracks.get(localFilename);
430 if (tmodel.runningTimeMS <= 0) {
431 try {
432
433 tmodel.runningTimeMS = AudioIO.getRunningTime(AudioPathManager.AUDIO_HOME_DIRECTORY + localFilename);
434
435 } catch (IOException e) {
436 e.printStackTrace();
437 } catch (UnsupportedAudioFileException e) {
438 e.printStackTrace();
439 }
440 }
441
442 // If was able to get the runnning time ... then include in model
443 if (tmodel.runningTimeMS > 0) {
444
445 if (tmodel.initiationTime == null) {
446 tmodel.initiationTime = Mutable.createMutableLong(oframe.getFirstInitiationTime());
447 // Remember: initiation times are relative, so setting to zero
448 // could be obscure.
449 }
450
451 oframe.addTrack(new TrackGraphNode(
452 tmodel.initiationTime.value,
453 localFilename,
454 tmodel.runningTimeMS,
455 tmodel.name,
456 tmodel.ypos));
457
458 } // otheriwse omit from model.
459
460 }
461
462 // Get linked OverdubbedFrame's.. recurse
463 for (String virtualFilename : linkedTracks.keySet()) {
464
465 LinkedTrackModelData linkedTrackModel = linkedTracks.get(virtualFilename);
466
467 // At this point, the link may / maynot be absolute,
468 String linkedFrameName = linkedTrackModel.frameLink;
469 assert(linkedFrameName != null);
470
471 if (linkedFrameName.length() == 0) continue;
472
473 if (!(Character.isLetter(linkedFrameName.charAt(0)))) { // relative link
474 // Heres a trick: to get the frameset of the relative link - its just the
475 // current root framenames frameset
476 linkedFrameName = Conversion.getFramesetName(rootFrameName) + linkedFrameName; // WARNING: IN EXPEDITEE THREAD
477 }
478
479 OverdubbedFrame od = buildGraph(linkedFrameName, newNodes); // Recurse
480
481 if (od == null) { // bad link or empty frame
482 od = new OverdubbedFrame(linkedFrameName);
483 }
484
485 if (linkedTrackModel.initiationTime == null) {
486 linkedTrackModel.initiationTime = Mutable.createMutableLong(oframe.getFirstInitiationTime());
487 // Remember: initiation times are relative, so setting to zero
488 // could be obscure.
489 }
490
491 oframe.addLinkedTrack(new LinkedTracksGraphNode(
492 linkedTrackModel.initiationTime.value,
493 od,
494 virtualFilename,
495 linkedTrackModel.name,
496 linkedTrackModel.ypos));
497
498 }
499
500 return oframe;
501 }
502
503 /**
504 *
505 * @author Brook Novak
506 *
507 */
508 private class TrackModelData {
509
510 TrackModelData(Mutable.Long initiationTime, long runningTimeMS, String name, int ypos) {
511 this.initiationTime = initiationTime;
512 this.runningTimeMS = runningTimeMS;
513 this.name = name;
514 this.ypos = ypos;
515 }
516
517 /** Null if unavilable */
518 Mutable.Long initiationTime;
519
520 /** negative if unavailable */
521 long runningTimeMS;
522
523 /** Can be null. Trackname */
524 String name;
525
526 int ypos;
527 }
528
529 /**
530 *
531 * @author Brook Novak
532 *
533 */
534 private class LinkedTrackModelData {
535
536 LinkedTrackModelData(Mutable.Long initiationTime, String frameLink, String name, int ypos) {
537 assert(frameLink != null);
538 this.initiationTime = initiationTime;
539 this.frameLink = frameLink;
540 this.name = name;
541 this.ypos = ypos;
542 }
543 /** Null if unavilable */
544 Mutable.Long initiationTime;
545
546 /** The framename */
547 String frameLink;
548
549 /** Can be null. Trackname */
550 String name;
551
552 /** The y pixel position */
553 int ypos;
554 }
555
556 /**
557 * Used for fetching track info in expeditees cache
558 *
559 * @author Brook Novak
560 *
561 */
562 private class ExpediteeCachedTrackInfoFetcher extends BlockingRunnable
563 {
564 private String rootFrameName;
565
566 /** Localfilename -> TrackModelData. Not null if frame was in memory. */
567 Map<String, TrackModelData> tracks = null;
568
569 /** VirtualFilename -> LinkedTrackModelData. Not null if frame was in memory. */
570 Map<String, LinkedTrackModelData> linkedTracks = null;
571
572 ExpediteeCachedTrackInfoFetcher(String rootFrameName)
573 {
574 assert(rootFrameName != null);
575 this.rootFrameName = rootFrameName;
576 }
577
578 public void execute()
579 {
580 assert(rootFrameName != null);
581
582 // Check if the current frame
583 Frame rootFrame = null;
584 if (DisplayController.getCurrentFrame() != null && DisplayController.getCurrentFrame().getName() != null
585 && DisplayController.getCurrentFrame().getName().equals(rootFrameName)) {
586 rootFrame = DisplayController.getCurrentFrame();
587 } else {
588 // Check if in cache
589 rootFrame = FrameIO.FrameFromCache(rootFrameName);
590 }
591
592 // Frame exists in memory... rummage around for track meta data
593 if (rootFrame != null) {
594 tracks = new HashMap<String, TrackModelData>();
595 linkedTracks = new HashMap<String, LinkedTrackModelData>();
596
597 for (Widget iw : rootFrame.getInteractiveWidgets()) {
598
599 if (iw instanceof SampledTrack) {
600 SampledTrack sampledTrackWidget = (SampledTrack)iw;
601
602 TrackGraphNode tinf =
603 AudioStructureModel.getInstance().getTrackGraphInfo(
604 sampledTrackWidget.getLocalFileName(),
605 rootFrameName);
606
607 Mutable.Long initTime = (tinf != null) ? Mutable.createMutableLong(tinf.getInitiationTime()) :
608 sampledTrackWidget.getInitiationTimeFromMeta();
609
610 long runningTime = sampledTrackWidget.getRunningMSTimeFromRawAudio();
611 if (runningTime <= 0) runningTime = -1; // signify to load from file
612
613 tracks.put(sampledTrackWidget.getLocalFileName(),
614 new TrackModelData(
615 initTime,
616 runningTime,
617 sampledTrackWidget.getName(),
618 sampledTrackWidget.getY()));
619
620 // NOTE: If the audio was sotred in a recovery file - then the widget would
621 // be deleted - thus no need to consider recovery files.
622
623 } else if (iw instanceof LinkedTrack) {
624 LinkedTrack linkedTrackWidget = (LinkedTrack)iw;
625
626 LinkedTracksGraphNode ltinf =
627 AudioStructureModel.getInstance().getLinkedTrackGraphInfo(
628 linkedTrackWidget.getVirtualFilename(),
629 rootFrameName);
630
631 Mutable.Long initTime = (ltinf != null) ? Mutable.createMutableLong(ltinf.getInitiationTime()) :
632 linkedTrackWidget.getInitiationTimeFromMeta();
633
634 // Don't consider track-links without any links
635 if (linkedTrackWidget.getLink() != null) {
636 linkedTracks.put(linkedTrackWidget.getVirtualFilename(),
637 new LinkedTrackModelData(
638 initTime,
639 linkedTrackWidget.getLink(),
640 linkedTrackWidget.getName(),
641 linkedTrackWidget.getY()));
642 }
643
644 }
645
646 }
647
648 }
649 }
650 }
651
652
653 /**
654 * Used for commiting a new graph in a thread-safe way.
655 * Checks for loops in the graph before commiting.
656 *
657 * If did not commit, then {@link #abortedCommit} will be true. Otherwise it will be false.
658 *
659 * if {@link #loopTrace} is not empty after the run, then the commit
660 * was aborted because there was a loop. (And {@link #loopTrace} contains the trace).
661 * The only other reason for aborting the commit was if a cancel was requested
662 * (@see TrackGraphModel#cancelFetch)
663 *
664 * @author Brook Novak
665 *
666 *
667 */
668 private class NewGraphCommitor extends BlockingRunnable
669 {
670 private final OverdubbedFrame rootODFrame;
671 private final Map<String, OverdubbedFrame> newGraph;
672
673 private boolean abortedCommit = false;
674
675 /** If not empty after the run, then the commit */
676 Stack<OverdubbedFrame> loopTrace = new Stack<OverdubbedFrame>();
677
678 NewGraphCommitor(OverdubbedFrame rootODFrame, Map<String, OverdubbedFrame> newGraph)
679 {
680 assert(rootODFrame != null);
681 assert(newGraph != null);
682
683 this.rootODFrame = rootODFrame;
684 this.newGraph = newGraph;
685 }
686
687 public void execute()
688 {
689
690 // Theoretically this would never need to be called ... but for super safety
691 // keep sync block - since dealing with shared resources.
692 synchronized(sharedResourceLocker) {
693
694 // At anytime during this point, the new graph could become invalid
695 // during this time while officially adding it to the "consistant" model..
696 // which is obviously BAD.
697 if (cancelFetch) {
698 abortedCommit = true;
699 return; // important: while locked shared resources
700 }
701
702 // Check that the graph is loop free.
703 boolean ilf = isLoopFree(rootODFrame, loopTrace);
704 if (ilf) { // check from existing roots POV's
705 for (OverdubbedFrame existingRoot : graphRoots) {
706 loopTrace.clear();
707 ilf = isLoopFree(existingRoot, loopTrace);
708 if (!ilf) break;
709 }
710 }
711
712 if (!ilf) {
713 assert (!loopTrace.isEmpty()); // loopTrace will contain the trace
714 abortedCommit = true;
715 return;
716 } else {
717 assert (loopTrace.isEmpty());
718 }
719
720 // Since we are creating a new root, existing roots might
721 // be reachable from the new root, thus remove reachable
722 // roots ... since they are no longer roots ...
723 List<OverdubbedFrame> redundantRoots = new LinkedList<OverdubbedFrame>();
724 for (OverdubbedFrame existingRoot : graphRoots) {
725 if (rootODFrame.getChild(existingRoot.getFrameName()) != null) {
726 redundantRoots.add(existingRoot); // this root is reachable from the new root
727 }
728 }
729
730 // Get rid of the redundant roots
731 graphRoots.removeAll(redundantRoots);
732
733 // Commit the new loop-free graph to the model
734 graphRoots.add(rootODFrame); // add the mutex or superceeding root
735 allOverdubbedFrames.putAll(newGraph); // Include new graph into all allOverdubbedFrames
736 }
737 }
738 }
739//
740// /**
741// * Determines if a node is reachable from a given node.
742// *
743// * @param source
744// * Must not be null.
745// *
746// * @param target
747// * Must not be null.
748// *
749// * @param visited
750// * Must be empty. Must not be null.
751// *
752// * @return
753// * True if target is reachable via source.
754// */
755// private boolean isReachable(OverdubbedFrame source, OverdubbedFrame target, Set<OverdubbedFrame> visited) {
756// assert(source != null);
757// assert(target != null);
758// assert(visited != null);
759//
760// // Base cases
761// if (source == target) return true;
762// else if (visited.contains(source)) return false;
763//
764// // Remember visited node
765// visited.add(source);
766//
767// // Recurse
768// for (Iterator<LinkedTracksGraphInfo> itor = source.getLinkedTrackIterator(); itor.hasNext();) {
769// LinkedTracksGraphInfo lti = itor.next();
770//
771// if (isReachable(lti.getLinkedFrame(), target, visited)) {
772// return true;
773// }
774// }
775//
776// // Reached end of this nodes links - found no match
777// return false;
778// }
779
780 /**
781 * Determines if a graph is loop free from a starting point.
782 * <B>MUST BE ON THE SWING THREAD</B>
783 *
784 * @param current
785 * Where to look for loops from.
786 *
787 * @param visitedStack
788 * Must be empty. Used internally. If the
789 * function returns false, then the stack will provde a trace of the loop.
790 *
791 * @return
792 * True if the graph at current is loop free.
793 */
794 private boolean isLoopFree(OverdubbedFrame current, Stack<OverdubbedFrame> visitedStack) {
795
796 if (visitedStack.contains(current)) { // found a loop
797 visitedStack.add(current); // add to strack for loop trace.
798 return false;
799 }
800
801 visitedStack.push(current); // save current node to stack
802
803 for (LinkedTracksGraphNode tl : current.getLinkedTracksCopy()) {
804
805 boolean ilf = isLoopFree(tl.getLinkedFrame(), visitedStack);
806
807 if (!ilf) return false;
808
809 }
810
811 visitedStack.pop(); // pop the current node
812
813 return true;
814
815 }
816
817 /**
818 * Gets a TrackGraphInfo. <b>MUST BE ON EXPEDITEE THREAD</b>
819 *
820 * @param localFilename
821 * Must not be null.
822 *
823 * @param parentFrameName
824 * If null - the search will be slower.
825 *
826 * @return
827 * The TrackGraphInfo for the given track. Null if not in model
828 */
829 public TrackGraphNode getTrackGraphInfo(String localFilename, String parentFrameName) {
830
831 synchronized(sharedResourceLocker) {
832
833 assert(localFilename != null);
834
835 if (parentFrameName != null) {
836 OverdubbedFrame odframe = allOverdubbedFrames.get(parentFrameName.toLowerCase());
837
838
839 if (odframe != null) {
840 return odframe.getTrack(localFilename);
841 }
842
843 } else { // parentFrameName is null .. do search
844
845 for (OverdubbedFrame odframe : allOverdubbedFrames.values()) {
846 TrackGraphNode tinf = odframe.getTrack(localFilename);
847 if (tinf != null) return tinf;
848 }
849 }
850 }
851
852 return null;
853 }
854
855
856 /**
857 * Gets a TrackGraphInfo. <b>MUST BE ON EXPEDITEE THREAD</b>
858 *
859 * @param virtualFilename
860 * Must not be null.
861 *
862 * @param parentFrameName
863 * If null - the search will be slower.
864 *
865 * @return
866 * The LinkedTracksGraphInfo for the given virtualFilename. Null if not in model
867 */
868 public LinkedTracksGraphNode getLinkedTrackGraphInfo(String virtualFilename, String parentFrameName) {
869
870 synchronized(sharedResourceLocker) {
871
872 assert(virtualFilename != null);
873
874 if (parentFrameName != null) {
875 OverdubbedFrame odframe = allOverdubbedFrames.get(parentFrameName.toLowerCase());
876
877
878 if (odframe != null) {
879 return odframe.getLinkedTrack(virtualFilename);
880 }
881
882 } else {
883
884 for (OverdubbedFrame odframe : allOverdubbedFrames.values()) {
885 LinkedTracksGraphNode ltinf = odframe.getLinkedTrack(virtualFilename);
886 if (ltinf != null) return ltinf;
887 }
888
889 }
890 }
891
892 return null;
893 }
894
895
896 /**
897 * <B>MUST BE ON SWING THREAD</B>.
898 * Gets the parent ODFrame of a track - given the tracks local filename.
899 *
900 * @param localFilename
901 * The local filename of the track to get the frame for. Must not be null.
902 *
903 * @return
904 * The parent. Null if no parent exists.
905 */
906 public OverdubbedFrame getParentOverdubbedFrame(String localFilename) {
907 assert(localFilename != null);
908
909 synchronized (allOverdubbedFrames) {
910 for (OverdubbedFrame odf : allOverdubbedFrames.values()) {
911 if (odf.containsTrack(localFilename)) {
912 return odf;
913 }
914 }
915 }
916
917 return null;
918 }
919
920 /**
921 * Gets an overdubbed frame representation of a given frame.
922 *
923 * @param framename
924 * The name of the frame.
925 *
926 * @return
927 * The OverdubbedFrame for the given framename. Null if does not exist.
928 */
929 public OverdubbedFrame getOverdubbedFrame(String framename) {
930 assert(framename != null);
931
932 synchronized (allOverdubbedFrames) {
933 return allOverdubbedFrames.get(framename.toLowerCase());
934 }
935 }
936
937 /**
938 * <B>MUST BE ON SWING THREAD</B>.
939 * Keeps model consistent with expeditee.
940 *
941 * @param localFilename
942 * Must not be null.
943 *
944 * @param parentFrameName
945 * Can be null.
946 *
947 * @param currentRunningTime
948 * I.e. the new running time after the edit. Must be larger than zero. In milliseconds.
949 */
950 public void onTrackWidgetAudioEdited(String localFilename, String parentFrameName, long currentRunningTime) {
951
952 boolean doNotify = false;
953 TrackGraphNode tinf = null;
954
955 synchronized(sharedResourceLocker) { // IMPORTANT: Must wait for new graphs to be added to the shared resources
956
957 assert(localFilename != null);
958 assert (currentRunningTime > 0);
959
960 // Locate parent frame
961 OverdubbedFrame odframe = null;
962
963 if (parentFrameName != null) odframe = allOverdubbedFrames.get(parentFrameName.toLowerCase());
964 else odframe = getParentOverdubbedFrame(localFilename);
965
966 // adjust running time in model
967 if (odframe != null) { // is loaded?
968 tinf = odframe.getTrack(localFilename);
969 assert(tinf != null); // due to the assumption that the model is consistent
970 if (tinf.getRunningTime() != currentRunningTime) {
971 tinf.setRunningTime(currentRunningTime);
972 // Note: a fetch might be waiting on this - i.e in progress. Thus must be cancelled.
973 // It will die in its own time - and will always be cancelled because it locks
974 // the current locked object.
975 cancelFetch = doNotify = true;
976 }
977 }
978
979
980 }
981
982 // Notify observers.
983 if (doNotify)
984 fireSubjectChanged(new SubjectChangedEvent(ApolloSubjectChangedEvent.GRAPH_TRACK_EDITED, tinf));
985 }
986
987 /**
988 * <B>MUST BE ON SWING THREAD</B>.
989 * Keeps model consistent with expeditee.
990 *
991 * @param localFilename
992 * Must not be null.
993 *
994 * @param parentFrameName
995 * Can be null.
996 *
997 * @param newInitiationTime
998 * In milliseconds.Relative - i.e. can be negative
999 *
1000 * @param name
1001 * The name given to the linked track. Can be null if there is no name...
1002 *
1003 * @param ypos
1004 * The Y-pixel position of the track.
1005 *
1006 * @param currentRunningTime
1007 * Must be larger than zero. In milliseconds. Used in case the model has not been create for the widget.
1008 */
1009 public void onTrackWidgetAnchored(
1010 String localFilename, String parentFrameName,
1011 long newInitiationTime, long currentRunningTime,
1012 String name, int ypos) {
1013
1014 boolean doNotify = false;
1015 TrackGraphNode tinf = null;
1016
1017 synchronized(sharedResourceLocker) { // IMPORTANT: Must wait for new graphs to be added to the shared resources
1018
1019 assert(localFilename != null);
1020
1021 // Locate parent frame
1022 OverdubbedFrame odframe = null;
1023
1024 if (parentFrameName != null) odframe = allOverdubbedFrames.get(parentFrameName.toLowerCase());
1025 else odframe = getParentOverdubbedFrame(localFilename);
1026
1027 // adjust initiation time in model
1028 if (odframe != null) { // is loaded?
1029 tinf = odframe.getTrack(localFilename);
1030
1031 if (tinf != null && tinf.getInitiationTime() != newInitiationTime) {
1032 tinf.setInitiationTime(newInitiationTime);
1033 cancelFetch = doNotify = true;
1034
1035 } else { // if there is no model but overdub frame is in memory - then create new model for this track.
1036
1037 tinf = new TrackGraphNode(
1038 newInitiationTime,
1039 localFilename,
1040 currentRunningTime,
1041 name,
1042 ypos);
1043
1044 odframe.addTrack(tinf); // safe because on swing thread
1045
1046 // Note: a fetch might be waiting on this - i.e in progress. Thus must be cancelled.
1047 cancelFetch = doNotify = true;
1048 // It will die in its own time - and will always be cancelled because it locks
1049 // the current locked object.
1050 }
1051
1052
1053 }
1054
1055 }
1056
1057 // Notify observers.
1058 if (doNotify)
1059 fireSubjectChanged(new SubjectChangedEvent(ApolloSubjectChangedEvent.GRAPH_TRACK_ADDED, tinf));
1060
1061 }
1062
1063 /**
1064 * <B>MUST BE ON SWING THREAD</B>.
1065 * Keeps model consistent with expeditee.
1066 *
1067 * Note: Invoke when picked up - not when a frame changes... also if it
1068 * is removed due to XRaymode - in that case the whole model will be released.
1069 *
1070 * @param localFilename
1071 * Must not be null.
1072 *
1073 * @param parentFrameName
1074 * Can be null. If given will be faster.
1075 *
1076 */
1077 public void onTrackWidgetRemoved(String localFilename, String parentFrameName) {
1078 TrackGraphNode tinf = null;
1079 boolean doNotify = false;
1080
1081 synchronized(sharedResourceLocker) { // IMPORTANT: Must wait for new graphs to be added to the shared resources
1082
1083 if (DisplayController.isXRayMode()) { // discard whole model
1084
1085 // Neccessary because if the user goes into xray then moves to a new frame
1086 // then it wil screw everything up. Also note they can delete the text source items
1087 // in xray mode.
1088 // This basically will cause short load times to occur again...
1089 allOverdubbedFrames.clear();
1090 graphRoots.clear();
1091
1092 // Note: a fetch might be waiting on this - i.e in progress. Thus must be cancelled.
1093 cancelFetch = doNotify = true;
1094 // It will die in its own time - and will always be cancelled because it locks
1095 // the current locked object.
1096
1097 } else {
1098
1099 // Locate parent frame
1100 OverdubbedFrame odframe = null;
1101
1102 if (parentFrameName != null) odframe = allOverdubbedFrames.get(parentFrameName.toLowerCase());
1103 else odframe = getParentOverdubbedFrame(localFilename);
1104
1105 if (odframe != null) {
1106 tinf = odframe.removeTrack(localFilename);
1107 //assert(tinf != null);
1108 cancelFetch = doNotify = true;
1109 }
1110
1111 }
1112
1113 }
1114
1115 // Notify observers.
1116 if (doNotify)
1117 fireSubjectChanged(new SubjectChangedEvent(ApolloSubjectChangedEvent.GRAPH_TRACK_REMOVED, tinf));
1118 }
1119
1120 /**
1121 * <B>MUST BE ON SWING THREAD</B>.
1122 * Keeps model consistent with expeditee.
1123 *
1124 * @param localFilename
1125 * Must not be null.
1126 *
1127 * @param parentFrameName
1128 * Can be null.
1129 *
1130 * @param currentRunningTime
1131 * I.e. the new running time after the edit. Must be larger than zero. In milliseconds.
1132 */
1133 public void onTrackWidgetNameChanged(String localFilename, String parentFrameName, String newName) {
1134
1135 boolean doNotify = false;
1136
1137 synchronized(sharedResourceLocker) { // IMPORTANT: Must wait for new graphs to be added to the shared resources
1138
1139 assert(localFilename != null);
1140
1141 // Locate parent frame
1142 OverdubbedFrame odframe = null;
1143
1144 if (parentFrameName != null) odframe = allOverdubbedFrames.get(parentFrameName.toLowerCase());
1145 else odframe = getParentOverdubbedFrame(localFilename);
1146
1147 // adjust name in model
1148 if (odframe != null) { // is loaded?
1149 AbstractTrackGraphNode tinf = odframe.getTrack(localFilename);
1150 assert(tinf != null); // due to the assumption that the model is consistent
1151 if (tinf.getName() != newName) {
1152 tinf.setName(newName);
1153 // Note: a fetch might be waiting on this - i.e in progress. Thus must be cancelled.
1154 cancelFetch = doNotify = true;
1155 // It will die in its own time - and will always be cancelled because it locks
1156 // the current locked object.
1157 }
1158 }
1159
1160 }
1161
1162 // Notify observers.
1163 if (doNotify)
1164 fireSubjectChanged(new SubjectChangedEvent(ApolloSubjectChangedEvent.NAME_CHANGED, localFilename));
1165 }
1166
1167
1168 /**
1169 * <B>MUST BE ON SWING THREAD</B>.
1170 * Keeps model consistent with expeditee.
1171 *
1172 * @param localFilename
1173 * Must not be null.
1174 *
1175 * @param parentFrameName
1176 * Can be null.
1177 *
1178 * @param initTime
1179 * The initiation time in ms
1180 *
1181 * @param yPos
1182 * The y pixel position.
1183 *
1184 */
1185 public void onTrackWidgetPositionChanged(String localFilename, String parentFrameName, long initTime, int yPos) {
1186
1187 boolean doNotify = false;
1188 AbstractTrackGraphNode tinf = null;
1189
1190 synchronized(sharedResourceLocker) { // IMPORTANT: Must wait for new graphs to be added to the shared resources
1191
1192 assert(localFilename != null);
1193
1194 // Locate parent frame
1195 OverdubbedFrame odframe = null;
1196
1197 if (parentFrameName != null) odframe = allOverdubbedFrames.get(parentFrameName.toLowerCase());
1198 else odframe = getParentOverdubbedFrame(localFilename);
1199
1200 // adjust name in model
1201 if (odframe != null) { // is loaded?
1202 tinf = odframe.getTrack(localFilename);
1203 if (tinf != null &&
1204 (tinf.getInitiationTime() != initTime || tinf.getYPixelPosition() != yPos)) {
1205 tinf.setInitiationTime(initTime);
1206 tinf.setYPixelPosition(yPos);
1207 // Note: a fetch might be waiting on this - i.e in progress. Thus must be cancelled.
1208 cancelFetch = doNotify = true;
1209 // It will die in its own time - and will always be cancelled because it locks
1210 // the current locked object.
1211 }
1212 }
1213
1214 }
1215
1216 // Notify observers.
1217 if (doNotify)
1218 fireSubjectChanged(new SubjectChangedEvent(ApolloSubjectChangedEvent.GRAPH_TRACK_POSITION_CHANGED, tinf));
1219 }
1220
1221
1222 /**
1223 * <B>MUST BE ON SWING THREAD</B>.
1224 * Keeps model consistent with expeditee.
1225 *
1226 * @param virtualFilename
1227 * Must not be null.
1228 *
1229 * @param parentFrameName
1230 * Can be null.
1231 *
1232 * @param initTime
1233 * The initiation time in ms
1234 *
1235 * @param yPos
1236 * The y pixel position.
1237 *
1238 */
1239 public void onLinkedTrackWidgetPositionChanged(String virtualFilename, String parentFrameName, long initTime, int yPos) {
1240
1241 boolean doNotify = false;
1242 AbstractTrackGraphNode tinf = null;
1243
1244 synchronized(sharedResourceLocker) { // IMPORTANT: Must wait for new graphs to be added to the shared resources
1245
1246 assert(virtualFilename != null);
1247
1248 // Locate parent frame
1249 OverdubbedFrame odframe = null;
1250
1251 if (parentFrameName != null) odframe = allOverdubbedFrames.get(parentFrameName.toLowerCase());
1252 else odframe = getParentOverdubbedFrame(virtualFilename);
1253
1254 // adjust name in model
1255 if (odframe != null) { // is loaded?
1256 tinf = odframe.getLinkedTrack(virtualFilename);
1257 if (tinf != null &&
1258 (tinf.getInitiationTime() != initTime || tinf.getYPixelPosition() != yPos)) {
1259
1260 tinf.setInitiationTime(initTime);
1261 tinf.setYPixelPosition(yPos);
1262
1263 // Note: a fetch might be waiting on this - i.e in progress. Thus must be cancelled.
1264 cancelFetch = doNotify = true;
1265 // It will die in its own time - and will always be cancelled because it locks
1266 // the current locked object.
1267 }
1268 }
1269 }
1270
1271 // Notify observers.
1272 if (doNotify)
1273 fireSubjectChanged(new SubjectChangedEvent(ApolloSubjectChangedEvent.GRAPH_LINKED_TRACK_POSITION_CHANGED, tinf));
1274 }
1275
1276 /**
1277 * <B>MUST BE ON SWING THREAD</B>.
1278 * Keeps model consistent with expeditee.
1279 *
1280 * @param virtualFilename
1281 * Must not be null.
1282 *
1283 * @param parentFrameName
1284 * Can be null.
1285 *
1286 * @param currentRunningTime
1287 * I.e. the new running time after the edit. Must be larger than zero. In milliseconds.
1288 */
1289 public void onLinkedTrackWidgetNameChanged(String virtualFilename, String parentFrameName, String newName) {
1290
1291 boolean doNotify = false;
1292
1293 synchronized(sharedResourceLocker) { // IMPORTANT: Must wait for new graphs to be added to the shared resources
1294
1295 assert(virtualFilename != null);
1296
1297 // Locate parent frame
1298 OverdubbedFrame odframe = null;
1299
1300 if (parentFrameName != null) odframe = allOverdubbedFrames.get(parentFrameName.toLowerCase());
1301 else odframe = getParentOverdubbedFrame(virtualFilename);
1302
1303 // adjust name in model
1304 if (odframe != null) { // is loaded?
1305 AbstractTrackGraphNode tinf = odframe.getLinkedTrack(virtualFilename);
1306 if (tinf != null && tinf.getName() != newName) {
1307 tinf.setName(newName);
1308 // Note: a fetch might be waiting on this - i.e in progress. Thus must be cancelled.
1309 cancelFetch = doNotify = true;
1310 // It will die in its own time - and will always be cancelled because it locks
1311 // the current locked object.
1312 }
1313 }
1314 }
1315
1316 // Notify observers.
1317 if (doNotify)
1318 fireSubjectChanged(new SubjectChangedEvent(ApolloSubjectChangedEvent.NAME_CHANGED, virtualFilename));
1319 }
1320
1321
1322 /**
1323 * <B>MUST BE ON SWING THREAD</B>.
1324 * Keeps model consistent with expeditee.
1325 *
1326 * Doe not use for changing a the widgets link. Must first remove, then acnhor with new link -
1327 * tracklinks links are immutable.
1328 *
1329 * @param virtualFilename
1330 * Must not be null.
1331 *
1332 * @param parentFrameName
1333 * Must not be null.
1334 *
1335 * @param newInitiationTime
1336 * In milliseconds.Relative - i.e. can be negative
1337 *
1338 * @param absoluteLinkedFrame
1339 * Must not be null. Must be a valid framename (absolute).
1340 *
1341 * @param name
1342 * The name given to the linked track. Can be null if there is no name...
1343 *
1344 * @param ypos
1345 * The Y-pixel position of the track.
1346 *
1347 */
1348 public void onLinkedTrackWidgetAnchored(
1349 String virtualFilename, String parentFrameName,
1350 long newInitiationTime, String absoluteLinkedFrame,
1351 String name, int ypos) {
1352
1353 boolean doNotify = false;
1354
1355 synchronized(sharedResourceLocker) { // IMPORTANT: Must wait for new graphs to be added to the shared resources
1356
1357 assert(virtualFilename != null);
1358 assert(parentFrameName != null);
1359 assert(absoluteLinkedFrame != null);
1360 assert(FrameIO.isValidFrameName(absoluteLinkedFrame));
1361
1362 // Locate parent frame
1363 OverdubbedFrame odframe = allOverdubbedFrames.get(parentFrameName.toLowerCase());
1364
1365 if (odframe != null) { // is loaded?
1366
1367 LinkedTracksGraphNode linkInf = odframe.getLinkedTrack(virtualFilename);
1368
1369 if (linkInf != null) {
1370
1371 // Update the initation time
1372 linkInf.setInitiationTime(newInitiationTime);
1373
1374 // The link should be consistant - check for miss-use of procedure call
1375 assert(linkInf.getLinkedFrame().getFrameName().equalsIgnoreCase(absoluteLinkedFrame));
1376
1377 } else { // if there is no model but overdub frame is in memory - then create new model for this track.
1378
1379 updateLater(new LinkedTrackUpdate(
1380 newInitiationTime, absoluteLinkedFrame,
1381 virtualFilename, parentFrameName, name, ypos));
1382
1383 }
1384
1385 // Note: a fetch might be waiting on this - i.e in progress. Thus must be cancelled.
1386 cancelFetch = doNotify = true;
1387 // It will die in its own time - and will always be cancelled because it locks
1388 // the current locked object.
1389 }
1390
1391 }
1392
1393 // Notify observers.
1394 if (doNotify)
1395 fireSubjectChanged(new SubjectChangedEvent(ApolloSubjectChangedEvent.GRAPH_LINKED_TRACK_ADDED, virtualFilename));
1396
1397 }
1398
1399 /**
1400 * <B>MUST BE ON SWING THREAD</B>.
1401 * Keeps model consistent with expeditee.
1402 *
1403 * Note: Invoke when picked up - not when a frame changes... also if it
1404 * is removed due to XRaymode - in that case the whole model will be released.
1405 *
1406 * <b>Also</b> invoke if the track tracks link has changed.
1407 *
1408 * @param virtualFilename
1409 * Must not be null.
1410 *
1411 * @param parentFrameName
1412 * Can be null. If given will be faster.
1413 *
1414 */
1415 public void onLinkedTrackWidgetRemoved(String virtualFilename, String parentFrameName) {
1416
1417 boolean doNotify = false;
1418
1419 synchronized(sharedResourceLocker) { // IMPORTANT: Must wait for new graphs to be added to the shared resources
1420
1421
1422 if (DisplayController.isXRayMode()) { // discard whole model
1423
1424 // Neccessary because if the user goes into xray then moves to a new frame
1425 // then it wil screw everything up. Also note they can delete the text source items
1426 // in xray mode.
1427 // This basically will cause short load times to occur again...
1428 allOverdubbedFrames.clear();
1429 graphRoots.clear();
1430
1431
1432 // Note: a fetch might be waiting on this - i.e in progress. Thus must be cancelled.
1433 cancelFetch = doNotify = true;
1434 // It will die in its own time - and will always be cancelled because it locks
1435 // the current locked object.
1436 } else {
1437
1438 // Locate parent frame
1439 OverdubbedFrame odframe = null;
1440
1441 if (parentFrameName != null) {
1442 odframe = allOverdubbedFrames.get(parentFrameName.toLowerCase());
1443 } else {
1444 for (OverdubbedFrame odf : allOverdubbedFrames.values()) {
1445 if (odf.containsLinkedTrack(virtualFilename)) {
1446 odframe = odf;
1447 break;
1448 }
1449 }
1450 }
1451
1452 if (odframe != null) {
1453 LinkedTracksGraphNode linkInf = odframe.getLinkedTrack(virtualFilename);
1454
1455 if (linkInf != null) {
1456 boolean didRemove = odframe.removeLinkedTrack(linkInf);
1457 assert(didRemove);
1458
1459 // Track links are the actual links in the directed graph. THus if they are
1460 // removed then the graph must be checked for creating new root nodes.
1461 // That is, the removed link may have isolated a node (or group of nodes)
1462 // for which must be reachable via their own start state...
1463 boolean isReachable = false;
1464 for (OverdubbedFrame existingRoot : graphRoots) {
1465 if (existingRoot.getChild(linkInf.getLinkedFrame().getFrameName()) != null) {
1466 isReachable = true;
1467 break;
1468 }
1469
1470 }
1471
1472 // Ensure that the frame is reachable
1473 if (!isReachable) {
1474 graphRoots.add(linkInf.getLinkedFrame());
1475 }
1476
1477 // Note: a fetch might be waiting on this - i.e in progress. Thus must be cancelled.
1478 cancelFetch = doNotify = true;
1479 // It will die in its own time - and will always be cancelled because it locks
1480 // the current locked object.
1481
1482 }
1483
1484
1485 }
1486
1487 }
1488
1489 }
1490
1491 // Notify observers.
1492 if (doNotify)
1493 fireSubjectChanged(new SubjectChangedEvent(ApolloSubjectChangedEvent.GRAPH_LINKED_TRACK_REMOVED, virtualFilename));
1494 }
1495
1496 /**
1497 * @return
1498 * True if the graph model is updating.
1499 */
1500 public boolean isUpdating() {
1501 return (delayedModelUpdator != null && delayedModelUpdator.isAlive());
1502 }
1503
1504
1505 /**
1506 * Waits for updates to finish.
1507 *
1508 * <b>MUST NOT BE ON SWING THREAD - OR MAY DEADLOCK</b>
1509 *
1510 * @throws InterruptedException
1511 * if any thread has interrupted the current thread
1512 */
1513 public void waitOnUpdates() throws InterruptedException {
1514 if (delayedModelUpdator != null && delayedModelUpdator.isAlive()) {
1515 delayedModelUpdator.join();
1516 }
1517 }
1518
1519 /**
1520 * Queues an update for the consistant model to be updated later... since it may take some time.
1521 *
1522 * @param update
1523 * The update to do. Must not be null.
1524 */
1525 private void updateLater(LinkedTrackUpdate update) {
1526
1527 synchronized(updateQueue) {
1528
1529 assert(update != null);
1530
1531 // Add the update tot he queu
1532 updateQueue.add(update);
1533
1534 // Ensure that the update thread is alive
1535 if (delayedModelUpdator == null || !delayedModelUpdator.isAlive()) {
1536 delayedModelUpdator = new DelayedModelUpdator();
1537 delayedModelUpdator.start();
1538 }
1539
1540 }
1541
1542 }
1543
1544 private Queue<LinkedTrackUpdate> updateQueue = new LinkedList<LinkedTrackUpdate>(); // SHARED RESOURCE
1545
1546 /**
1547 * Used for queuing update data.
1548 *
1549 *
1550 * @author Brook Novak
1551 *
1552 */
1553 private class LinkedTrackUpdate extends LinkedTrackModelData {
1554
1555 LinkedTrackUpdate(
1556 long initiationTime,
1557 String absoluteLink,
1558 String virtualFilename,
1559 String parentFrameToAddTo,
1560 String name,
1561 int ypos) {
1562
1563 super(Mutable.createMutableLong(initiationTime), absoluteLink, name, ypos);
1564
1565 assert(virtualFilename != null);
1566 assert(parentFrameToAddTo != null);
1567 assert(FrameIO.isValidFrameName(absoluteLink));
1568
1569 this.virtualFilename = virtualFilename;
1570 this.parentFrameToAddTo = parentFrameToAddTo;
1571 }
1572
1573 String virtualFilename;
1574 String parentFrameToAddTo;
1575
1576 }
1577
1578 /**
1579 *
1580 * @author Brook Novak
1581 *
1582 */
1583 private class DelayedModelUpdator extends Thread {
1584
1585 public void run() {
1586 while (true) {
1587 LinkedTrackUpdate update;
1588 synchronized(updateQueue) {
1589 if (updateQueue.isEmpty()) return; // important: only quits when updateQueue is synched
1590 update = updateQueue.poll();
1591 }
1592
1593 assert(update != null);
1594
1595 // Keep trying the update until success or encountered loops
1596 while (true) {
1597 try {
1598
1599 OverdubbedFrame odframe = fetchGraph(update.frameLink);
1600 if (odframe == null) { // does not exist or is empty...
1601 odframe = new OverdubbedFrame(update.frameLink);
1602 }
1603
1604
1605 // add to parent only if does not already exist - also being aware of loops
1606 final LinkedTrackUpdate updateData = update;
1607 final OverdubbedFrame linkedODFrame = odframe;
1608
1609 try {
1610 EcosystemManager.getMiscManager().runOnGIOThread(new BlockingRunnable() {
1611
1612 public void execute() { // on swing thread
1613
1614 synchronized(sharedResourceLocker) {
1615
1616 try { // The fetch will be cancelled after this block - within the lock
1617
1618 OverdubbedFrame parentFrame = allOverdubbedFrames.get(updateData.parentFrameToAddTo.toLowerCase());
1619 if (parentFrame == null) {
1620 // No need to both adding - because model does not even reference this new link...
1621 // must have cleared while updating... e.g. user could have switched into xray mode.
1622 return;
1623 }
1624
1625 assert(updateData.initiationTime != null);
1626
1627 // Create the linked track instance
1628 LinkedTracksGraphNode linktInf = new LinkedTracksGraphNode(
1629 updateData.initiationTime.value,
1630 linkedODFrame,
1631 updateData.virtualFilename,
1632 updateData.name,
1633 updateData.ypos);
1634
1635 // Check that the graph will be loop free
1636 parentFrame.addLinkedTrack(linktInf); // ADDING TEMPORARILY
1637
1638 // Check from parent frame
1639 Stack<OverdubbedFrame> loopTrace = new Stack<OverdubbedFrame>();
1640 if (!isLoopFree(parentFrame, loopTrace)) {
1641 parentFrame.removeLinkedTrack(linktInf);
1642 // Ignore link - has loop
1643 return;
1644 }
1645
1646 // Check for all existing graph roots
1647 for (OverdubbedFrame existingRoot : graphRoots) {
1648 loopTrace.clear();
1649 if (!isLoopFree(existingRoot, loopTrace)) {
1650 parentFrame.removeLinkedTrack(linktInf);
1651 // Ignore link - has loop
1652 return;
1653 }
1654 }
1655
1656 // Otherwise leave the link in its place
1657
1658 // Ensure that the linked frame is in the allOverdubbedFrames set
1659 allOverdubbedFrames.put(linkedODFrame.getFrameName().toLowerCase(), linkedODFrame);
1660
1661 } finally { // IMPORTANT: CANCEL WHILE LOCKED
1662
1663 // Cancel any fetches
1664 cancelFetch = true;
1665 }
1666
1667 } // release lock
1668
1669
1670 }
1671 });
1672 } catch (Exception e) {
1673 e.printStackTrace();
1674 assert(false);
1675 }
1676
1677 // Done with this update...
1678 break;
1679
1680
1681 } catch (InterruptedException e) { // Canceled
1682 // Consume and retry
1683
1684 } catch (TrackGraphLoopException e) { // bad link
1685 // Consume - since is fine - the model will just ignore the link
1686 break;
1687 }
1688 } // retry fetch
1689
1690 } // proccess next update
1691
1692 } // finished updating
1693 }
1694
1695}
Note: See TracBrowser for help on using the repository browser.