/*
 * File:     TranscriptionImpl.java
 * Project:  MPI Linguistic Application
 * Date:     03 April 2006
 *
 * Copyright (C) 2001-2006  Max Planck Institute for Psycholinguistics
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package mpi.eudico.server.corpora.clomimpl.abstr;

import mpi.eudico.server.corpora.clom.Annotation;
import mpi.eudico.server.corpora.clom.MediaObject;
import mpi.eudico.server.corpora.clom.MetaTime;
import mpi.eudico.server.corpora.clom.Tier;
import mpi.eudico.server.corpora.clom.TimeOrder;
import mpi.eudico.server.corpora.clom.TimeSlot;
import mpi.eudico.server.corpora.clom.Transcription;

import mpi.eudico.server.corpora.clomimpl.dobes.ACM23TranscriptionStore;
import mpi.eudico.server.corpora.clomimpl.type.Constraint;
import mpi.eudico.server.corpora.clomimpl.type.ControlledVocabulary;
import mpi.eudico.server.corpora.clomimpl.type.LinguisticType;

import mpi.eudico.server.corpora.location.LocatorManager;

import mpi.eudico.server.corpora.util.ACMEditEvent;
import mpi.eudico.server.corpora.util.ACMEditListener;
import mpi.eudico.server.corpora.util.ACMEditableObject;
import mpi.eudico.server.corpora.util.DataTreeNode;

import mpi.eudico.tool.ToolCondition;

import mpi.eudico.util.TranscriptionUtil;

import java.io.File;
import java.io.InputStream;

import java.net.URL;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.TreeSet;
import java.util.Vector;


/**
 * TranscriptionImpl implements Transcription.
 *
 * @author Hennie Brugman
 * @author Albert Russel
 * @version 22-Jun-1999
 *
 *
 */

// modified Daan Broeder 23-10-2000
// added url atribute + getFullPath() method
// added url parameter to constructor
//
public class TranscriptionImpl implements Transcription {
    /** Holds value of property DOCUMENT ME! */
    public static final String UNDEFINED_FILE_NAME = "aishug294879ryshfda9763afo8947a5gf";
    private ArrayList listeners;

    /**
     * The list of Tiers in this Transcription.
     */
    protected Vector tiers;

    /**
     * The media file or stream associated with this Transcription
     * - deprecated since ELAN 2.0
     */
    protected MediaObject mediaObject;

    /**
     * Descriptors for the media files or streams associated with this Transcription
     */
    protected Vector mediaDescriptors;

    /**
     * Descriptors for secondary associated files. I.e. non-audio/video files, or files
     * representing sources that are not a primary subject of transcription.
     */
    protected Vector linkedFileDescriptors;

    /**
     * The url of the transcription (if applicable)
     */
    protected String url;

    /**
     * The content type of the transcription (if applicable)
     * default impl. is "text/plain"
     */
    protected String content_type = "text/plain";

    /**
     * Transcription name
     */
    protected String name;
    protected String owner;
    private DataTreeNode parent; // back reference, used for deletion of Transcriptions
    protected LocatorManager locatorManager;
    protected MetaTime metaTime;
    protected TimeOrder timeOrder;
    protected Vector linguisticTypes; // contains id strings for all types
    protected String author;
    protected boolean isLoaded;
    private boolean changed = false;
    private int timeChangePropagationMode = Transcription.NORMAL;
    private TimeProposer timeProposer;

    /**
     * Holds associated ControlledVocabulary objects.
     * @since jun 04
     */
    protected Vector controlledVocabularies;

    /**
     * A flag to temporarily turn of unnecessary notification of ACMEditListeners,
     * e.g. when a number of modifications is performed in a batch.
     * @since oct 04
     */
    protected boolean isNotifying;
    protected String fileName;
    protected String mediafileName;
    protected String svgFile;

    /**
     * New constructor for unknown file name
     */
    public TranscriptionImpl() {
        this(UNDEFINED_FILE_NAME);
    }

    /**
     * New constructor with only the full file path
     *
     * @param eafFilePath DOCUMENT ME!
     */
    public TranscriptionImpl(String eafFilePath) {
        this(eafFilePath.substring(eafFilePath.lastIndexOf(System.getProperty(
                        "file.separator")) + 1), null, null,
            "file:" + eafFilePath);

        initialize(eafFilePath.substring(eafFilePath.lastIndexOf(
                    System.getProperty("file.separator")) + 1), eafFilePath);
    }

    /**
     * MK:02/06/19
     * @param myURL unclear, hack from Dobes or Chat, often set null.
     */
    public TranscriptionImpl(String theName, DataTreeNode theParent,
        LocatorManager theLocatorManager, String myURL) {
        name = theName;
        parent = theParent;
        url = myURL;
        locatorManager = theLocatorManager;

        tiers = new Vector();

        metaTime = new FastMetaTime();
        listeners = new ArrayList();

        timeOrder = new TimeOrderImpl(this);
        linguisticTypes = new Vector();
        mediaDescriptors = new Vector();
        linkedFileDescriptors = new Vector();
        controlledVocabularies = new Vector();

        isLoaded = false;
        isNotifying = true;
        timeProposer = new TimeProposer();
    }

    public void addACMEditListener(ACMEditListener l) {
        if (!listeners.contains(l)) {
            listeners.add(l);
        }
    }

    public void removeACMEditListener(ACMEditListener l) {
        listeners.remove(l);
    }

    public void notifyListeners(ACMEditableObject source, int operation,
        Object modification) {
        Iterator i = listeners.iterator();
        ACMEditEvent event = new ACMEditEvent(source, operation, modification);

        while (i.hasNext()) {
            ((ACMEditListener) i.next()).ACMEdited(event);
        }
    }

    public void modified(int operation, Object modification) {
        handleModification(this, operation, modification);
    }

    public void handleModification(ACMEditableObject source, int operation,
        Object modification) {
        if (changed == false) {
            changed = true;
        }

        timeProposer.correctProposedTimes(this, source, operation, modification);

        if (isNotifying) {
            notifyListeners(source, operation, modification);
        }
    }

    /**
     * Sets the notification flag.
     * When set to false ACMEditListeners are no longer notified of modification.
     * When set to true listeners are notified of an CHANGE_ANNOTATIONS
     * ACMEditEvent. Every modification will then be followed by a notification.
     *
     * @param notify the new notification flag
     */
    public void setNotifying(boolean notify) {
        isNotifying = notify;

        if (isNotifying) {
            modified(ACMEditEvent.CHANGE_ANNOTATIONS, null);
        }
    }

    /**
     * Returns the notifying flag.
     * @return true when ACMEditListeners are notified of every modification,
     *     false otherwise
     */
    public boolean isNotifying() {
        return isNotifying;
    }

    /**
     * @param name HAS TO BE THE SAME AS THE XML NAME PREFIX
     * @param fileName the absolute path of the XML transcription file.
     * @param parent the parent DataTreeNode.
     */
    private void initialize(String name, String fileName) {
        if (fileName.startsWith("file:")) {
            fileName = fileName.substring(5);
        }

        author = ""; // make sure that it is initialized to empty string

        File fff = new File(fileName);

        if (!fff.exists()) {
            isLoaded = true; // prevent loading
        } else {
            isLoaded = false;
        }

        this.fileName = fileName;

        // we don't know if it's wav or mpg or mov. We have to try.
        String mimeType = "";
        this.mediafileName = null;

        // try mpg first!
        // replace *.??? by *.mpg
        if (this.mediafileName == null) {
            String test = this.fileName.substring(0, this.fileName.length() -
                    3) + "mpg";

            if (test.startsWith("file:")) {
                test = test.substring(5);
            }

            if ((new File(test)).exists()) {
                this.mediafileName = test;
                mimeType = MediaDescriptor.MPG_MIME_TYPE;
            }
        }

        // replace *.??? by *.wav
        if (this.mediafileName == null) {
            String test = this.fileName.substring(0, this.fileName.length() -
                    3) + "wav";

            if (test.startsWith("file:")) {
                test = test.substring(5);
            }

            if ((new File(test)).exists()) {
                this.mediafileName = test;
                mimeType = MediaDescriptor.WAV_MIME_TYPE;
            }
        }

        // HS 21-11-2001 mov added
        // replace *.??? by *.mov
        if (this.mediafileName == null) {
            String test = this.fileName.substring(0, this.fileName.length() -
                    3) + "mov";

            if (test.startsWith("file:")) {
                test = test.substring(5);
            }

            if ((new File(test)).exists()) {
                this.mediafileName = test;
            }
        }

        // media object is not used anymore
        //this.mediaObject = createMediaObject(this.mediafileName);
        // HB, 3 dec 03. After this, media descriptors are instantiated, if in the EAF file
        if (!isLoaded()) {
            (new ACM23TranscriptionStore()).loadTranscription(this);

            // jul 2005: make sure the proposed times are precalculated
            timeProposer.correctProposedTimes(this, null,
                ACMEditEvent.CHANGE_ANNOTATIONS, null);
        }

        // if no media descriptors, and mediafileName is not null, create media descriptors
        if ((mediaDescriptors.size() == 0) && (mediafileName != null) &&
                (mediafileName.length() > 0)) {
            String mediaURL = pathToURLString(mediafileName);

            //		String mediaURL = "file:///" + mediafileName;
            //		mediaURL = mediaURL.replace('\\', '/');
            MediaDescriptor masterMD = new MediaDescriptor(mediaURL, mimeType);
            mediaDescriptors.add(masterMD);

            String checkFile = this.fileName.substring(0,
                    this.fileName.length() - 3) + "wav";

            if (checkFile.startsWith("file:")) {
                checkFile = checkFile.substring(5);
            }

            if ((new File(checkFile)).exists()) {
                String signalURL = pathToURLString(checkFile);

                //			String signalURL = "file:///" + checkFile;
                //			signalURL = signalURL.replace('\\', '/');
                mimeType = MediaDescriptor.WAV_MIME_TYPE;

                MediaDescriptor signalMD = new MediaDescriptor(signalURL,
                        mimeType);
                signalMD.extractedFrom = mediaURL;
                mediaDescriptors.add(signalMD);
            }
        }

        if (svgFile == null) {
            String test = this.fileName.substring(0, this.fileName.length() -
                    3) + "svg";

            if (test.startsWith("file:")) {
                test = test.substring(5);
            }

            if ((new File(test)).exists()) {
                svgFile = "file:" + test;
            }
        }
    }

    /*
     * This method should be in a Utility class or a URL class
     * Convert a path to a file URL string. Takes care of Samba related problems
     * file:///path works for all files except for samba file systems, there we need file://machine/path,
     * i.e. 2 slashes insteda of 3
     *
     * What's with relative paths?
     */
    private String pathToURLString(String path) {
        // replace all back slashes by forward slashes
        path = path.replace('\\', '/');

        // remove leading slashes and count them
        int n = 0;

        while (path.charAt(0) == '/') {
            path = path.substring(1);
            n++;
        }

        // add the file:// or file:/// prefix
        if (n == 2) {
            return "file://" + path;
        } else {
            return "file:///" + path;
        }
    }

    /**
     * Returns the name of the Transcription
     *
     * @return    name of Transcription
     */
    public String getName() {
        return name;
    }

    public void setName(String theName) {
        name = theName;
    }

    /**
     * MK:02/06/19 implementing method from interface Transcription
     *
     * @return    locatorManager
     */
    public LocatorManager getLocatorManager() {
        return locatorManager;
    }

    /**
     * Returns the url of the Transcription
     *
     * @return    url string of Transcription
     */
    public String getFullPath() {
        return url;
    }

    /**
     * getContentType()
     * @returns The (semi) mime-type for the content of the resource
     *
     * default impl returns "text/plain"
     */
    public String getContentType() {
        return content_type;
    }

    /**
     * getContentStream()
     * @returns The InputStream for the content of the resource
     */
    public InputStream getContentStream() {
        InputStream is = null;

        if (url != null) {
            try {
                is = (new URL(url)).openStream();
            } catch (Exception e) {
                is = null;
            }
        }

        return is;
    }

    public String getOwner() {
        return owner;
    }

    /**
     * Returns a list of all Tags in theTiers, sorted according to MetaTime ordering.
     *
     * @param theTiers    list of Tiers whose Tags should be returned.
     * @return        an ordered list of Tags.
     */
    public Vector getTagsForTiers(Vector theTiers) {
        Vector tagList = null;
        Vector tierTags = null;
        TreeSet allTags = new TreeSet();
        Vector tagsToCompare = null;

        loadTags();

        Iterator tierIter = theTiers.iterator();

        while (tierIter.hasNext()) {
            Tier t = (Tier) tierIter.next();
            tierTags = t.getTags();

            allTags.addAll(tierTags);
        }

        tagList = new Vector(allTags);

        return tagList;
    }

    public MediaObject getMediaObject() {
        return mediaObject;
    }

    public Vector getMediaDescriptors() {
        return mediaDescriptors;
    }

    public void setMediaDescriptors(Vector theMediaDescriptors) {
        mediaDescriptors = theMediaDescriptors;
    }

    /**
     * Returns the collection of linked file descriptors
     * @return the linked file descriptors
     */
    public Vector getLinkedFileDescriptors() {
        return linkedFileDescriptors;
    }

    /**
     * Sets the collection of linked files descriptors.
     * NB: could check for null here
     * @param descriptors the new descriptors
     */
    public void setLinkedFileDescriptors(Vector descriptors) {
        linkedFileDescriptors = descriptors;
    }

    public MetaTime getMetaTime() {
        return metaTime;
    }

    public void printStatistics() {
        System.out.println("");
        System.out.println(">>> Name: " + name);
        System.out.println(">>> Number of tiers: " + tiers.size());
    }

    // TreeViewable interface methods
    public String getNodeName() {
        return getName();
    }

    public boolean isTreeViewableLeaf() {
        return true;
    }

    public Vector getChildren() {
        return new Vector();
    }

    // SharedDataObject interface method(s), via Transcription interface
    // Unreferenced interface method
    public void unreferenced() {
        // corpus should store the only reference to transcription, so
        // removing this reference results in deletion by GC
        getParent().removeChild(this);
    }

    // ToolAdministrator interface method
    public Vector getAvailableTools() {
        return ToolDatabaseImpl.Instance().getAvailableTools(this);
    }

    /**
    * Returns the parent object in the hierarchy of Corpus data objects.
    *
    * @return    the parent DataTreeNode or null, if no parent exists
    */
    public DataTreeNode getParent() {
        return parent;
    }

    /**
     * Removes a child in the Corpus data hierarchy by deleting the reference
     * to the child. The garbage collector will then do the actual deletion.
     * Children for GestureTranscription are Tiers. Removing Tiers is not yet
     * implemented, therefore removeChild does nothing yet.
     *
     * @param theChild    the child to be deleted
     */
    public void removeChild(DataTreeNode theChild) {
        removeTier((Tier) theChild);
    }

    public Object getConcreteData() {
        return null;
    }

    // HB, 17-oct-01, migrated methods from DobesTranscription to here.
    public boolean isLoaded() {
        return isLoaded;
    }

    public void setLoaded(boolean loaded) {
        isLoaded = loaded;

        // jul 2005: useless here: tiers and annotations have not yet been added 
        // at this point

        /*
        if (loaded) {
            timeProposer.correctProposedTimes(this, null,
                ACMEditEvent.CHANGE_ANNOTATIONS, null);
        }
        */
    }

    public void addTier(Tier theTier) {
        tiers.add(theTier);

        if (isLoaded()) {
            modified(ACMEditEvent.ADD_TIER, theTier);
        }
    }

    public void removeTier(Tier theTier) {
        ((TierImpl) theTier).removeAllAnnotations();

        Vector deletedTiers = new Vector();
        deletedTiers.add(theTier);

        // loop over tiers, remove all tiers where:
        // - number of annotations is 0
        // - and tier.hasAncestor(theTier)
        Iterator tierIter = tiers.iterator();

        while (tierIter.hasNext()) {
            TierImpl t = (TierImpl) tierIter.next();

            if ((t.getNumberOfAnnotations() == 0) &&
                    (t.hasAncestor((TierImpl) theTier))) {
                deletedTiers.add(t);
            }
        }

        tiers.removeAll(deletedTiers);

        modified(ACMEditEvent.REMOVE_TIER, theTier);
    }

    public TimeOrder getTimeOrder() {
        return timeOrder;
    }

    public void pruneAnnotations() {
        // remove all annotations that are marked deleted
        Iterator tierIter = tiers.iterator();

        while (tierIter.hasNext()) {
            ((TierImpl) tierIter.next()).pruneAnnotations();
        }

        // HB, 9 aug 02: moved from tier to transcription to because delete
        // usually concerns more than one tier.
        modified(ACMEditEvent.REMOVE_ANNOTATION, null);
    }

    /**
     * Refined prune method; only the 'source' tier and its dependent tiers will
     * be asked to prune their annotations.
     *
     * @param fromTier the tier that might have ben changed
     */
    public void pruneAnnotations(Tier fromTier) {
        if (fromTier instanceof TierImpl) {
            Vector depTiers = ((TierImpl) fromTier).getDependentTiers();
            depTiers.add(0, fromTier);

            Iterator tierIter = depTiers.iterator();

            while (tierIter.hasNext()) {
                ((TierImpl) tierIter.next()).pruneAnnotations();
            }

            modified(ACMEditEvent.REMOVE_ANNOTATION, null);
        }
    }

    public void setAuthor(String theAuthor) {
        author = theAuthor;
    }

    public String getAuthor() {
        return author;
    }

    public void setLinguisticTypes(Vector theTypes) {
        linguisticTypes = theTypes;
    }

    public Vector getLinguisticTypes() {
        return linguisticTypes;
    }

    public LinguisticType getLinguisticTypeByName(String name) {
        LinguisticType lt = null;

        if (linguisticTypes != null) {
            Iterator typeIt = linguisticTypes.iterator();
            LinguisticType ct = null;

            while (typeIt.hasNext()) {
                ct = (LinguisticType) typeIt.next();

                if (ct.getLinguisticTypeName().equals(name)) {
                    lt = ct;

                    break;
                }
            }
        }

        return lt;
    }

    public void addLinguisticType(LinguisticType theType) {
        linguisticTypes.add(theType);

        modified(ACMEditEvent.ADD_LINGUISTIC_TYPE, theType);
    }

    public void removeLinguisticType(LinguisticType theType) {
        linguisticTypes.remove(theType);

        modified(ACMEditEvent.REMOVE_LINGUISTIC_TYPE, theType);
    }

    public void changeLinguisticType(LinguisticType linType,
        String newTypeName, Vector constraints, String cvName,
        boolean newTimeAlignable, boolean newGraphicsAllowed) {
        linType.setLinguisticTypeName(newTypeName);
        linType.removeConstraints();

        if (constraints != null) {
            Iterator cIter = constraints.iterator();

            while (cIter.hasNext()) {
                Constraint constraint = (Constraint) cIter.next();
                linType.addConstraint(constraint);
            }
        }

        linType.setControlledVocabularyName(cvName);
        linType.setTimeAlignable(newTimeAlignable);
        linType.setGraphicReferences(newGraphicsAllowed);

        modified(ACMEditEvent.CHANGE_LINGUISTIC_TYPE, linType);
    }

    public Vector getTiersWithLinguisticType(String typeID) {
        Vector matchingTiers = new Vector();

        Iterator tierIter = tiers.iterator();

        while (tierIter.hasNext()) {
            TierImpl t = (TierImpl) tierIter.next();

            if (t.getLinguisticType().getLinguisticTypeName().equals(typeID)) {
                matchingTiers.add(t);
            }
        }

        return matchingTiers;
    }

    public Vector getCandidateParentTiers(Tier forTier) {
        // in the future, this method could take constraints on linguistic types into account.
        // for now, it returns all tiers except forTier itself and tiers that have forTier as
        // an ancestor
        Vector candidates = new Vector();
        Enumeration e = null;

        e = getTiers().elements();

        while (e.hasMoreElements()) {
            try {
                TierImpl dTier = (TierImpl) e.nextElement();

                if (dTier.hasAncestor(forTier)) {
                    break;
                }

                if (dTier != forTier) {
                    candidates.add(dTier);
                }
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }

        return candidates;
    }

    //    public abstract void setMainMediaFile(String pathName);

    /**
     * <p>MK:02/06/12<br>
     * Implementing method from interface Transription.
     * <p>
     * @param theTierId see there!
     * @return see there!
     */
    public Tier getTierWithId(String theTierId) {
        Tier t = null;
        Tier result = null;

        Iterator tierIter = tiers.iterator();

        while (tierIter.hasNext()) {
            t = (Tier) tierIter.next();

            if (t.getName().equals(theTierId)) {
                result = t;

                break;
            }
        }

        return result;
    }

    /**
     * <p>MK:02/06/12<br>
     * Where the name of all tiers are unique for a transcription, this method
     * returns the tier with the given name.
     * If no tier matches the given name, null is returned.<br>
     * Unless tier IDs are introduced, this method and getTierWithId() are identical.
     * <br>
     * Non-unique tiernames must be introduced.
     * </p>
     * @param name name of tier, as in tier.getName()
     * @return first tier in transription with given name, or null.
     */
    protected final Tier getTierByUniqueName(String name) {
        if (tiers == null) {
            return null;
        }

        Enumeration all = this.tiers.elements();

        while (all.hasMoreElements()) {
            Tier t = (Tier) all.nextElement();
            String n = (String) t.getName();

            if (n.equals(name)) {
                return t;
            }
        }

        return null;
    }

    public Vector getAnnotationsUsingTimeSlot(TimeSlot theSlot) {
        Vector resultAnnots = new Vector();

        Iterator tierIter = tiers.iterator();

        while (tierIter.hasNext()) {
            TierImpl t = (TierImpl) tierIter.next();

            resultAnnots.addAll(t.getAnnotationsUsingTimeSlot(theSlot));
        }

        return resultAnnots;
    }

    public Vector getAnnotsBeginningAtTimeSlot(TimeSlot theSlot) {
        Vector resultAnnots = new Vector();

        Iterator tierIter = tiers.iterator();

        while (tierIter.hasNext()) {
            TierImpl t = (TierImpl) tierIter.next();

            resultAnnots.addAll(t.getAnnotsBeginningAtTimeSlot(theSlot));
        }

        return resultAnnots;
    }

    /**
     * Refined version of getAnnotsBeginningAtTimeSlot(TimeSlot); here only the specified tier
     * (optional) and its depending tiers are polled.
     * @param theSlot the TimeSlot
     * @param forTier the tier
     * @param includeThisTier if true annotations on this tier will also be included
     * @return a Vector containing annotations
     */
    public Vector getAnnotsBeginningAtTimeSlot(TimeSlot theSlot, Tier forTier,
        boolean includeThisTier) {
        Vector resultAnnots = new Vector();

        Vector depTiers = ((TierImpl) forTier).getDependentTiers();

        if (includeThisTier) {
            depTiers.add(0, forTier);
        }

        Iterator tierIter = depTiers.iterator();

        while (tierIter.hasNext()) {
            TierImpl t = (TierImpl) tierIter.next();

            resultAnnots.addAll(t.getAnnotsBeginningAtTimeSlot(theSlot));
        }

        return resultAnnots;
    }

    public Vector getAnnotsEndingAtTimeSlot(TimeSlot theSlot) {
        Vector resultAnnots = new Vector();

        Iterator tierIter = tiers.iterator();

        while (tierIter.hasNext()) {
            TierImpl t = (TierImpl) tierIter.next();

            resultAnnots.addAll(t.getAnnotsEndingAtTimeSlot(theSlot));
        }

        return resultAnnots;
    }

    /**
     * Refined version of getAnnotsEndingAtTimeSlot(TimeSlot); here only the specified tier
     * (optional) and its depending tiers are polled.
     * @param theSlot the TimeSlot
     * @param forTier the tier
     * @param includeThisTier if true annotations on this tier will also be included
     * @return a Vector containing annotations
     */
    public Vector getAnnotsEndingAtTimeSlot(TimeSlot theSlot, Tier forTier,
        boolean includeThisTier) {
        Vector resultAnnots = new Vector();

        Vector depTiers = ((TierImpl) forTier).getDependentTiers();

        if (includeThisTier) {
            depTiers.add(0, forTier);
        }

        Iterator tierIter = depTiers.iterator();

        while (tierIter.hasNext()) {
            TierImpl t = (TierImpl) tierIter.next();

            resultAnnots.addAll(t.getAnnotsEndingAtTimeSlot(theSlot));
        }

        return resultAnnots;
    }

    public Vector getAnnotationIdsAtTime(long time) {
        Vector resultAnnots = new Vector();

        Iterator tierIter = tiers.iterator();

        while (tierIter.hasNext()) {
            TierImpl t = (TierImpl) tierIter.next();
            Annotation ann = t.getAnnotationAtTime(time);

            if (ann != null) {
                resultAnnots.add(ann.getId());
            }
        }

        return resultAnnots;
    }

    public Annotation getAnnotation(String id) {
        if (id == null) {
            return null;
        }

        Iterator tierIter = tiers.iterator();
        Annotation a;

        while (tierIter.hasNext()) {
            TierImpl t = (TierImpl) tierIter.next();
            a = t.getAnnotation(id);

            if (a != null) {
                return a;
            }
        }

        return null;
    }

    public long getLatestTime() {
        long latestTime = 0;

        Enumeration elmts = getTimeOrder().elements();

        while (elmts.hasMoreElements()) {
            long t = ((TimeSlot) elmts.nextElement()).getTime();

            if (t > latestTime) {
                latestTime = t;
            }
        }

        return latestTime;
    }

    /**
     * This method returns all child annotations for a given annotation,
     * irrespective of which tier it is on. There exists an alternative method
     * Annotation.getChildrenOnTier(). The main difference is, that getChildAnnotationsOf
     * does not base itself on ParentAnnotationListeners for the case of AlignableAnnotations.
     * This is essential during deletion of annotations.
     * THEREFORE: DO NOT REPLACE THIS METHOD WITH getChildrenOnTier. DELETION OF ANNOTATIONS
     * WILL THEN FAIL !!!
     * */
    public Vector getChildAnnotationsOf(Annotation theAnnot) {
        Vector children = new Vector();

        //Tier annotsTier = theAnnot.getTier();
        if (theAnnot instanceof RefAnnotation) {
            children.addAll(((RefAnnotation) theAnnot).getParentListeners());
        } else { // theAnnot is AlignableAnnotation

            // HB, 6-5-03, to take closed annotation graphs into account
            TreeSet connectedAnnots = new TreeSet();

            //TreeSet connectedTimeSlots = new TreeSet();
            //	getConnectedAnnots(connectedAnnots, connectedTimeSlots, ((AlignableAnnotation) theAnnot).getBegin());
            // HS mar 06: pass the top tier as well, to prevent iterations over unrelated tiers
            getConnectedSubtree(connectedAnnots,
                ((AlignableAnnotation) theAnnot).getBegin(),
                ((AlignableAnnotation) theAnnot).getEnd(), theAnnot.getTier());

            Vector connAnnotVector = new Vector(connectedAnnots); // 'contains' on TreeSet seems to go wrong in rare cases
                                                                  // I don't understand this, but it works - HB

            Vector descTiers = new Vector();

            Iterator tierIter = tiers.iterator();

            while (tierIter.hasNext()) {
                TierImpl t = (TierImpl) tierIter.next();

                //if (t.getParentTier() == theAnnot.getTier()) {
                if (t.hasAncestor(theAnnot.getTier())) {
                    descTiers.add(t);
                }
            }

            // on these descendant tiers, check all annots if they have theAnnot as parent
            Iterator descTierIter = descTiers.iterator();

            while (descTierIter.hasNext()) {
                TierImpl descT = (TierImpl) descTierIter.next();

                Iterator annIter = descT.getAnnotations().iterator();

                while (annIter.hasNext()) {
                    Annotation a = (Annotation) annIter.next();

                    if (a instanceof RefAnnotation) {
                        // HS 29 jul 04: added test on the size of the Vector to prevent 
                        // NoSuchElementException
                        if ((((RefAnnotation) a).getReferences().size() > 0) &&
                                (((RefAnnotation) a).getReferences()
                                      .firstElement() == theAnnot)) {
                            children.add(a);
                        }
                    } else if (a instanceof AlignableAnnotation) {
                        if (connAnnotVector.contains(a)) {
                            children.add(a);
                        } else if (descT.getLinguisticType().getConstraints()
                                            .getStereoType() == Constraint.INCLUDED_IN) {
                            // feb 2006: special case for Included_In tier, child annotations are not 
                            // found in the graph tree, test overlap, or more precise inclusion
                            if ((a.getBeginTimeBoundary() >= theAnnot.getBeginTimeBoundary()) &&
                                    (a.getEndTimeBoundary() <= theAnnot.getEndTimeBoundary())) {
                                children.add(a);
                            }

                            if (a.getBeginTimeBoundary() > theAnnot.getEndTimeBoundary()) {
                                break;
                            }
                        }
                    }
                }
            }
        }

        return children;
    }

    /**
     * Annotation based variant of the graph based getConnectedAnnots(TreeSet, TreeSet, TimeSlot).
     * Annotations on "Included_In" tiers are not discovered in the graph based way.
     *
     * @param connectedAnnots storage for annotations found
     * @param connectedTimeSlots storage for slots found
     * @param fromAnn the parent annotation
     */
    public void getConnectedAnnots(TreeSet connectedAnnots,
        TreeSet connectedTimeSlots, AlignableAnnotation fromAnn) {
        // first get the annotations and slots, graph based
        getConnectedAnnots(connectedAnnots, connectedTimeSlots,
            fromAnn.getBegin());

        // then find Included_In dependent tiers
        TierImpl t = (TierImpl) fromAnn.getTier();
        Vector depTiers = t.getDependentTiers();
        Iterator dtIt = depTiers.iterator();
        TierImpl tt;
        Vector anns;

        while (dtIt.hasNext()) {
            tt = (TierImpl) dtIt.next();

            if (tt.getLinguisticType().getConstraints().getStereoType() == Constraint.INCLUDED_IN) {
                //anns = tt.getOverlappingAnnotations(fromAnn.getBeginTimeBoundary(), fromAnn.getEndTimeBoundary());
                anns = tt.getAnnotations();

                AlignableAnnotation aa;
                ;

                Iterator anIt = anns.iterator(); // iterations over all annotations on the tier...

                while (anIt.hasNext()) {
                    aa = (AlignableAnnotation) anIt.next();

                    if (fromAnn.isAncestorOf(aa)) {
                        getConnectedAnnots(connectedAnnots, connectedTimeSlots,
                            aa);
                    }

                    //getConnectedAnnots(connectedAnnots, connectedTimeSlots, 
                    //        ((AlignableAnnotation) anIt.next()).getBegin());
                }
            }
        }
    }

    public void getConnectedAnnots(TreeSet connectedAnnots,
        TreeSet connectedTimeSlots, TimeSlot startingFromTimeSlot) {
        Vector annots = null;
        connectedTimeSlots.add(startingFromTimeSlot);

        // annots = getAnnotsBeginningAtTimeSlot(startingFromTimeSlot);
        annots = getAnnotationsUsingTimeSlot(startingFromTimeSlot);

        if (annots != null) {
            connectedAnnots.addAll(annots);

            Iterator aIter = annots.iterator();

            while (aIter.hasNext()) {
                AlignableAnnotation aa = (AlignableAnnotation) aIter.next();

                if (!connectedTimeSlots.contains(aa.getBegin())) {
                    getConnectedAnnots(connectedAnnots, connectedTimeSlots,
                        aa.getBegin());
                }

                if (!connectedTimeSlots.contains(aa.getEnd())) {
                    getConnectedAnnots(connectedAnnots, connectedTimeSlots,
                        aa.getEnd());
                }
            }
        }
    }

    /**
     * Find all annotations part of the (sub) graph, time slot based.
     * HS mar 06: the top level tier for the search can be specified to prevent
     * iterations over unrelated tiers.
     *
     * @param connectedAnnots TreeSet to add found annotations to (in/out)
     * @param startingFromTimeSlot begin time slot of the subtree
     * @param stopTimeSlot end time slot of the sub tree
     * @param topTier the top level tier for the subtree search (can be a dependent tier though)
     * @return true when the end time slot has been reached, false otherwise
     */
    public boolean getConnectedSubtree(TreeSet connectedAnnots,
        TimeSlot startingFromTimeSlot, TimeSlot stopTimeSlot, Tier topTier) {
        Vector annots = null;
        boolean endFound = false;

        if (topTier == null) {
            annots = getAnnotsBeginningAtTimeSlot(startingFromTimeSlot);
        } else {
            annots = getAnnotsBeginningAtTimeSlot(startingFromTimeSlot,
                    topTier, true);
        }

        if (annots != null) {
            Iterator aIter = annots.iterator();

            while (aIter.hasNext()) {
                AlignableAnnotation aa = (AlignableAnnotation) aIter.next();

                if (aa.getEnd() != stopTimeSlot) {
                    endFound = getConnectedSubtree(connectedAnnots,
                            aa.getEnd(), stopTimeSlot, topTier);

                    if (endFound) {
                        connectedAnnots.add(aa);
                    }
                } else {
                    endFound = true;
                    connectedAnnots.add(aa);
                }
            }
        }

        return endFound;
    }

    public boolean eachRootHasSubdiv() {
        boolean eachRootHasSubdiv = true;

        Vector topTiers = TranscriptionUtil.getTopTiers(this);
        Iterator tierIter = topTiers.iterator();

        while (tierIter.hasNext()) {
            TierImpl t = (TierImpl) tierIter.next();

            Vector annots = t.getAnnotations();
            Iterator aIter = annots.iterator();

            while (aIter.hasNext()) {
                AbstractAnnotation a = (AbstractAnnotation) aIter.next();

                ArrayList children = a.getParentListeners();

                if ((children == null) || (children.size() == 0)) {
                    return false;
                } else {
                    boolean subdivFound = false;
                    Iterator childIter = children.iterator();

                    while (childIter.hasNext()) {
                        AbstractAnnotation ch = (AbstractAnnotation) childIter.next();
                        Constraint constraint = ((TierImpl) ch.getTier()).getLinguisticType()
                                                 .getConstraints();

                        if ((constraint.getStereoType() == Constraint.SYMBOLIC_SUBDIVISION) ||
                                (constraint.getStereoType() == Constraint.TIME_SUBDIVISION)) {
                            subdivFound = true;

                            break;
                        }
                    }

                    if (!subdivFound) {
                        return false;
                    }
                }
            }
        }

        return eachRootHasSubdiv;
    }

    /**
     * Returns whether any modifications are made since the last reset (when saving)
     */
    public boolean isChanged() {
        return changed;
    }

    /**
     * Resets 'changed' status to unchanged
     */
    public void setUnchanged() {
        changed = false;
    }

    /**
     * Sets 'changed' status to changed
     */
    public void setChanged() {
        changed = true;
    }

    /**
     * Returns time Change Propagation Mode (normal, bulldozer or shift)
     *
     * @author hennie
     */
    public int getTimeChangePropagationMode() {
        return timeChangePropagationMode;
    }

    /**
     * Set Time Change Propagation Mode (normal, bulldozer or shift)
     * @author hennie
     */
    public void setTimeChangePropagationMode(int theMode) {
        timeChangePropagationMode = theMode;
    }

    /**
     * Propagate time changes by shifting all next annotations to later times,
     * maintaining gaps. Algorithm: shift all time slots after end of fixedAnnotation
     * over distance newEnd minus oldEnd.
     *
     * @param fixedAnnotation the source annotation
     * @param fixedSlots slots connected to the source annotation that should not be shifted
     * @param oldBegin the old begin time of the annotation
     * @param oldEnd the old end time of the annotation
     */
    public void correctOverlapsByShifting(AlignableAnnotation fixedAnnotation,
        Vector fixedSlots, long oldBegin, long oldEnd) {
        long newEnd = fixedAnnotation.getEnd().getTime();

        // right shift
        if (newEnd > oldEnd) { // implies that end is time aligned

            long shift = newEnd - oldEnd;

            getTimeOrder().shift(oldEnd, shift, fixedAnnotation.getEnd(),
                fixedSlots);
        }
    }

    /**
     * Propagate time changes by shifting time slots.<br>
     * <b>Note: </b> this method is intended only to be used to
     * reverse the effects of an earlier call to <code>correctOverlapsByShifting
     * </code>, as in an undo operation!
     *
     * @see #correctOverlapsByShifting(AlignableAnnotation, long, long)
     * @param from starting point for shifting
     * @param amount the distance to shift the timeslots
     */
    public void shiftBackward(long from, long amount) {
        if (amount < 0) {
            getTimeOrder().shift(from, amount, null, null);

            // let listeners know the whole transcription 
            // could be changed
            modified(ACMEditEvent.CHANGE_ANNOTATIONS, null);
        }
    }

    /**
     * Shifts all aligned timeslots with the specified amount of ms.<br>
     * When an attempt is made to shift slots such that one or more slots
     * would have a negative time value an exception is thrown.
     * Slots (and annotations) will never implicitely be deleted.
     * The shift operation is delegated to th TimeOrder object.
     *
     * @param shiftValue the number of ms to add to the time value of aligned timeslots,
     *    can be less than zero
     * @throws IllegalArgumentException when the shift value is such that any aligned
     *    slot would get a negative time value
     */
    public void shiftAllAnnotations(long shiftValue)
        throws IllegalArgumentException {
        timeOrder.shiftAll(shiftValue);

        // notify
        modified(ACMEditEvent.CHANGE_ANNOTATIONS, null);
    }

    /**
         * Markus Note: THE ARGUEMENT IS IGNORED
         * HS aug 2005: MediaObject not used at all in TranscriptionImpl,
         * but still in some extending classes.
         *
         * @param mediaFileName DOCUMENT ME!
         *
         * @return DOCUMENT ME!
         */

    /*
    public MediaObject createMediaObject(String mediaFileName) {
        // Markus note: the easiest way to ignore the argument:
        // HS feb 2005 old MediaObject stuff should be removed...

        //mediaFileName = this.mediafileName;

        // System.out.println(" createDobesMediaObject: "+     mediaFileName);
        MediaObject mediaObject = null;

        if (mediaFileName != null) {
            File f = new File(mediaFileName);

            if (f.exists()) {
                mediaObject = new MediaObjectImpl(mediaFileName, 0, null,
                        "file:" + mediaFileName);
            } else {
                mediaObject = null;
            }
        }

        return mediaObject;
    }
    */

    /**
         * Returns all Tiers from this Transcription.
         *
         * @return DOCUMENT ME!
         */
    public Vector getTiers() {
        if (!isLoaded()) {
            //			(new MinimalTranscriptionStore()).loadTranscription(this, identity);
            (new ACM23TranscriptionStore()).loadTranscription(this);
        }

        return tiers;
    }

    /**
         * This ToolAdministrator must be able to answer quetions regarding its
         * condition.
         *
         * @param condition the Condition that must be checked.
         *
         * @return DOCUMENT ME!
         */
    public boolean checkToolCondition(ToolCondition condition) {
        // HIER MOET EEN GOEDE TEST KOMEN VOOR AANWEZIGHEID VAN MEDIA DATA
        // WAARSCHIJNLIJK IS HET HET BESTE ALS ER EEN TOOL KOMT DIE ALTIJD
        // GESTART KAN WORDEN MAAR PAS VRAAGT OM MEDIA DATA ALS DIE NODIG IS.
        return true;
    }

    /**
         * Check if an Object is equal to this Transcription. For DOBES equality is
         * true if the names of two Transcriptions are the same.
         *
         * @param obj DOCUMENT ME!
         *
         * @return DOCUMENT ME!
         */
    public boolean equals(Object obj) {
        if (obj instanceof TranscriptionImpl &&
                (((TranscriptionImpl) obj).getName() == name)) {
            return true;
        }

        return false;
    }

    /**
         * DOCUMENT ME!
         *
         * @param identity DOCUMENT ME!
         */
    public void loadTags() {
        loadAnnotations();
    }

    /**
         * DOCUMENT ME!
         *
         * @return DOCUMENT ME!
         */
    public String getPathName() {
        return fileName;
    }

    /**
         * DOCUMENT ME!
         *
         * @param theFileName DOCUMENT ME!
         */
    public void setPathName(String theFileName) {
        if (theFileName.startsWith("file:")) {
            theFileName = theFileName.substring(5);
        }

        fileName = theFileName;
        url = pathToURLString(fileName);
    }

    /**
         * Load the Annotations into the Tiers.
         *
         * @param identity DOCUMENT ME!
         */
    public void loadAnnotations() {
        if (!isLoaded()) {
            (new ACM23TranscriptionStore()).loadTranscription(this);
        }
    }

    /**
         * DOCUMENT ME!
         *
         * @param pathName DOCUMENT ME!
         */
    public void setMainMediaFile(String pathName) {
        // HS feb 05:
        // let the auto detected media file name precede
        if (mediafileName == null) {
            this.mediafileName = pathName;

            //mediaObject = createMediaObject(mediafileName);
        }
    }

    /**
         * DOCUMENT ME!
         *
         * @param fileName DOCUMENT ME!
         */
    public void setSVGFile(String fileName) {
        svgFile = fileName;
    }

    /**
         * DOCUMENT ME!
         *
         * @return DOCUMENT ME!
         */
    public String getSVGFile() {
        return svgFile;
    }

    /******** support for controlled vocabularies  ******/
    /**
     * Sets the collection of ControlledVocabularies known to this Transcription.
     *
     * @param controlledVocabs the CV's for this transcription
     */
    public void setControlledVocabularies(Vector controlledVocabs) {
        if (controlledVocabs != null) {
            controlledVocabularies = controlledVocabs;
        }

        //called at parse/construction time, don't call modified 		
    }

    /**
     * Returns the collection of controlled vocabularies known to this Transcription.
     * If there are no CV's an empty Vector is returned.
     *
     * @return the list of associated cv's
     */
    public Vector getControlledVocabularies() {
        return controlledVocabularies;
    }

    /**
     * Returns the ControlledVocabulary with the specified name if it exists
     * in the Transcription or null otherwise.
     *
     * @param name the name of the cv
     *
     * @return the CV with the specified name or <code>null</code>
     */
    public ControlledVocabulary getControlledVocabulary(String name) {
        if (name == null) {
            return null;
        }

        ControlledVocabulary conVoc = null;

        for (int i = 0; i < controlledVocabularies.size(); i++) {
            conVoc = (ControlledVocabulary) controlledVocabularies.get(i);

            if (conVoc.getName().equalsIgnoreCase(name)) {
                break;
            } else {
                conVoc = null;
            }
        }

        return conVoc;
    }

    /**
     * Adds the specified CV to the list of cv's if it is not already in the list
     * and if there is not already a cv with the same name in the list.
     *
     * @param cv the ControlledVocabulary to add
     */
    public void addControlledVocabulary(ControlledVocabulary cv) {
        if (cv == null) {
            return;
        }

        ControlledVocabulary conVoc;

        for (int i = 0; i < controlledVocabularies.size(); i++) {
            conVoc = (ControlledVocabulary) controlledVocabularies.get(i);

            if ((conVoc == cv) ||
                    conVoc.getName().equalsIgnoreCase(cv.getName())) {
                return;
            }
        }

        controlledVocabularies.add(cv);

        // register as a listener for modifications
        cv.setACMEditableObject(this);

        if (isLoaded) {
            modified(ACMEditEvent.CHANGE_CONTROLLED_VOCABULARY, cv);
        }
    }

    /**
     * Removes the specified CV from the list.<br>
     * Any LinguisticTypes that reference the specified ControlledVocabulary will
     * have their refernce set to <code>null</code>.
     *
     * @param cv the CV to remove from the list
     */
    public void removeControlledVocabulary(ControlledVocabulary cv) {
        if (cv == null) {
            return;
        }

        Vector types = getLinguisticTypesWithCV(cv.getName());

        for (int i = 0; i < types.size(); i++) {
            ((LinguisticType) types.get(i)).setControlledVocabularyName(null);
        }

        controlledVocabularies.remove(cv);

        modified(ACMEditEvent.CHANGE_CONTROLLED_VOCABULARY, cv);
    }

    /**
     * Updates the name and description of the specified CV and updates the Linguistic
     * Types referencing this CV, if any.<br> The contents of a ControlledVocabulary is changed
     * directly in an editor; the CV class insures it's integrity. THe ditor class
     * should call setChanged on the Transcription object.<br><br>
     * Pending: The moment updating existing annotation values is offered as an option,
     * this implementation has to been changed.
     *
     * @param cv the cv that has been changed or is to be changed.
     * @param name the new name of the cv
     * @param description the description for the cv
     */
    public void changeControlledVocabulary(ControlledVocabulary cv,
        String name, String description /*, Vector entries*/) {
        boolean newChange = false;
        String oldName = cv.getName();
        String oldDescription = cv.getDescription();

        // doublecheck on the name
        ControlledVocabulary conVoc;

        for (int i = 0; i < controlledVocabularies.size(); i++) {
            conVoc = (ControlledVocabulary) controlledVocabularies.get(i);

            if ((conVoc != cv) && conVoc.getName().equalsIgnoreCase(name)) {
                return;
            }
        }

        if (!oldName.equals(name)) {
            newChange = true;

            Vector types = getLinguisticTypesWithCV(oldName);

            for (int i = 0; i < types.size(); i++) {
                ((LinguisticType) types.get(i)).setControlledVocabularyName(name);
            }

            cv.setName(name);
        }

        if (!oldDescription.equals(description)) {
            cv.setDescription(description);
            newChange = true;
        }

        if (newChange) {
            modified(ACMEditEvent.CHANGE_CONTROLLED_VOCABULARY, cv);
        }
    }

    /**
     * Finds the LinguisticTypes that hold a reference to the CV with
     * the specified name and returns them in a Vector.
     *
     * @param name the identifier of the ControlledVocabulary
     * @return a list of linguistic types
     */
    public Vector getLinguisticTypesWithCV(String name) {
        Vector matchingTypes = new Vector();
        LinguisticType lt;
        String cvName;

        for (int i = 0; i < linguisticTypes.size(); i++) {
            lt = (LinguisticType) linguisticTypes.get(i);
            cvName = lt.getControlledVocabylaryName();

            if ((cvName != null) && cvName.equalsIgnoreCase(name)) {
                matchingTypes.add(lt);
            }
        }

        return matchingTypes;
    }

    /**
     * Finds the tiers with a linguistic type that references the Controlled Vocabulary
     * with the specified name.
     *
     * @see #getLinguisticTypes(String)
     *
     * @param name the identifier of the ControlledVocabulary
     *
     * @return a list of all tiers using the specified ControlledVocabulary
     */
    public Vector getTiersWithCV(String name) {
        Vector matchingTiers = new Vector();

        if ((name == null) || (name.length() == 0)) {
            return matchingTiers;
        }

        Vector types = getLinguisticTypesWithCV(name);

        if (types.size() > 0) {
            Vector tv;
            LinguisticType type;

            for (int i = 0; i < types.size(); i++) {
                type = (LinguisticType) types.get(i);
                tv = getTiersWithLinguisticType(type.getLinguisticTypeName());

                if (tv.size() > 0) {
                    matchingTiers.addAll(tv);
                }
            }
        }

        return matchingTiers;
    }

    public boolean allRootAnnotsUnaligned() {
        // used when importing unaligned files from for example Shoebox or CHAT		
        Vector topTiers = TranscriptionUtil.getTopTiers(this);
        Iterator tierIter = topTiers.iterator();

        while (tierIter.hasNext()) {
            TierImpl t = (TierImpl) tierIter.next();

            Vector annots = t.getAnnotations();
            Iterator aIter = annots.iterator();

            while (aIter.hasNext()) {
                AlignableAnnotation a = (AlignableAnnotation) aIter.next();

                if (a.getBegin().isTimeAligned() || a.getEnd().isTimeAligned()) {
                    return false;
                }
            }
        }

        return true;
    }

    public void alignRootAnnots() {
        Vector rootTimeSlots = new Vector();

        // collect timeslots of root annotations
        Vector topTiers = TranscriptionUtil.getTopTiers(this);
        Iterator tierIter = topTiers.iterator();

        while (tierIter.hasNext()) {
            TierImpl t = (TierImpl) tierIter.next();

            Vector annots = t.getAnnotations();
            Iterator aIter = annots.iterator();

            while (aIter.hasNext()) {
                AlignableAnnotation a = (AlignableAnnotation) aIter.next();

                rootTimeSlots.add(a.getBegin());
                rootTimeSlots.add(a.getEnd());
            }
        }

        // align at regular intervals. Assume that slots belong to one
        // annotation pairwise		
        int cnt = 0;

        Object[] tsArray = rootTimeSlots.toArray();
        Arrays.sort(tsArray);

        for (int i = 0; i < tsArray.length; i++) {
            TimeSlot ts = (TimeSlot) tsArray[i];

            if ((i % 2) == 0) {
                ts.setTime(1000 * cnt++);
            } else {
                ts.setTime(1000 * cnt);
            }
        }
    }
}
