001/*
002 *                    BioJava development code
003 *
004 * This code may be freely distributed and modified under the
005 * terms of the GNU Lesser General Public Licence.  This should
006 * be distributed with the code.  If you do not have a copy,
007 * see:
008 *
009 *      http://www.gnu.org/copyleft/lesser.html
010 *
011 * Copyright for this code is held jointly by the individual
012 * authors.  These should be listed in @author doc comments.
013 *
014 * For more information on the BioJava project and its aims,
015 * or to join the biojava-l mailing list, visit the home page
016 * at:
017 *
018 *      http://www.biojava.org/
019 *
020 */
021
022package org.biojava.bio.alignment;
023
024import java.util.ArrayList;
025import java.util.Hashtable;
026import java.util.Iterator;
027import java.util.List;
028import java.util.Map;
029import java.util.NoSuchElementException;
030import java.util.TreeSet;
031import java.util.Vector;
032
033import org.biojava.bio.BioError;
034import org.biojava.bio.BioException;
035import org.biojava.bio.symbol.Alphabet;
036import org.biojava.bio.symbol.AlphabetManager;
037import org.biojava.bio.symbol.Edit;
038import org.biojava.bio.symbol.GappedSymbolList;
039import org.biojava.bio.symbol.IllegalSymbolException;
040import org.biojava.bio.symbol.Location;
041import org.biojava.bio.symbol.RangeLocation;
042import org.biojava.bio.symbol.SimpleGappedSymbolList;
043import org.biojava.bio.symbol.Symbol;
044import org.biojava.bio.symbol.SymbolList;
045import org.biojava.utils.ChangeEvent;
046import org.biojava.utils.ChangeSupport;
047import org.biojava.utils.ChangeVetoException;
048
049/**
050 * <p>
051 * FlexibleAlignment is a class which implements UnequalLengthAlignment,
052 * ARAlignment and EditableAlignment <b>It places no restriction on where any
053 * sequence can be in the alignment so there could be gaps in the alignment. You
054 * tell it where to put the sequence, it will do it. I think I will be adding an
055 * Exception NonContinuousAlignmentException. STILL UNDER CONSTRUCTION.
056 * seqString does not work because there it does not seem to support
057 * tokenization 'token' this is true for SimpleAlignment too.</b>
058 * 
059 * @author David Waring
060 * @author Matthew Pocock
061 */
062public class FlexibleAlignment extends AbstractULAlignment implements
063                ARAlignment, EditableAlignment {
064
065        protected Map<Object, AlignmentElement> data;
066        protected List<String> labelOrder;
067        protected Location alignmentRange;
068        List<Alphabet> alphaList = new ArrayList<Alphabet>();
069
070        /**
071         * construct this object with the reference sequence which can either be a
072         * gappedSymbolList or not label in all cases refers to an object that holds
073         * the display name (generally just a String). since more than one sequence
074         * in an alignment could have the same name this works as long as the labels
075         * are different objects even though they may hold the same name.
076         */
077
078        public FlexibleAlignment(List<AlignmentElement> seqList)
079                        throws BioException {
080                data = new Hashtable<Object, AlignmentElement>();
081                labelOrder = new Vector<String>();
082                alignmentRange = new RangeLocation(1, 1);
083
084                int k = 0;
085                // go through the list make sure that all seqs are GappedSymbolLists
086                for (Iterator<AlignmentElement> i = seqList.iterator(); i.hasNext();) {
087                        AlignmentElement ae = i.next();
088                        String label = ae.getLabel();
089                        Location loc = ae.getLoc();
090                        SymbolList seq = ae.getSymbolList();
091                        alphaList.add(seq.getAlphabet());
092                        if (!(seq instanceof GappedSymbolList)) {
093                                seq = new SimpleGappedSymbolList(seq);
094                                ae = new SimpleAlignmentElement(label, seq, loc);
095                        }
096                        data.put(label, ae);
097                        labelOrder.add(label);
098                        int min = lesser(alignmentRange.getMin(), loc.getMin());
099                        int max = greater(alignmentRange.getMax(), loc.getMax());
100                        alignmentRange = new RangeLocation(min, max);
101                        k++;
102                }
103                this.alphabet = AlphabetManager.getCrossProductAlphabet(alphaList);
104                try {
105                        resetRange();
106                } catch (ChangeVetoException e) {
107                        throw new BioError("Should not have a problem here");
108                }
109
110        }
111
112        private int getOrder(Object label) throws Exception {
113                for (int i = 0; i < labelOrder.size(); i++) {
114                        if (labelOrder.get(i).equals(label))
115                                return i;
116                }
117                throw new Exception("did not find label");
118        }
119
120        /**
121         * add a new a alignment usings a location to the reference sequence. This
122         * should either contain no gaps or it should be relative to a reference
123         * sequence that already has the gaps added
124         */
125
126        public synchronized void addSequence(AlignmentElement ae)
127                        throws ChangeVetoException, BioException {
128                ChangeSupport cs;
129                ChangeEvent cevt;
130
131                String label = ae.getLabel();
132                SymbolList seq = ae.getSymbolList();
133                Location loc = ae.getLoc();
134
135                // give the listeners a change to veto this
136                // create a new change event ->the EDIT is a static final variable of
137                // type ChangeType in SymbolList interface
138                cevt = new ChangeEvent(this, ARAlignment.ADD_LABEL, label);
139                cs = getChangeSupport(ARAlignment.ADD_LABEL);
140
141                // let the listeners know what we want to do
142                cs.firePreChangeEvent(cevt);
143                if (!(seq instanceof GappedSymbolList)) {
144                        seq = new SimpleGappedSymbolList(seq);
145                        ae = new SimpleAlignmentElement(label, seq, loc);
146                }
147                data.put(label, ae);
148                labelOrder.add(label);
149                alphaList.add(seq.getAlphabet());
150                this.alphabet = AlphabetManager.getCrossProductAlphabet(alphaList);
151
152                int min = lesser(alignmentRange.getMin(), loc.getMin());
153                int max = greater(alignmentRange.getMax(), loc.getMax());
154                alignmentRange = new RangeLocation(min, max);
155                resetRange();
156
157                cs.firePostChangeEvent(cevt);
158
159        }
160
161        public synchronized void removeSequence(Object label)
162                        throws ChangeVetoException {
163
164                ChangeSupport cs;
165                ChangeEvent cevt;
166
167                // give the listeners a change to veto this
168                // create a new change event ->the EDIT is a static final variable of
169                // type ChangeType in SymbolList interface
170                cevt = new ChangeEvent(this, ARAlignment.REMOVE_LABEL, label);
171                cs = getChangeSupport(ARAlignment.REMOVE_LABEL);
172
173                // let the listeners know what we want to do
174                cs.firePreChangeEvent(cevt);
175                try {
176                        alphaList.remove(getOrder(label));
177                        this.alphabet = AlphabetManager.getCrossProductAlphabet(alphaList);
178                } catch (Throwable e) {
179                        e.printStackTrace();
180                }
181                data.remove(label);
182                labelOrder.remove(label);
183                resetRange();
184                cs.firePostChangeEvent(cevt);
185
186        }
187
188        // ///////////////////////
189        // methods from Interface UnequalLengthAlignment
190        // ///////////////////////
191
192        /**
193         * The location of an individual SymbolList relative to overall Alignment
194         */
195        public Location locInAlignment(Object label) throws NoSuchElementException {
196                return getAE(label).getLoc();
197        }
198
199        public List<Object> getLabelsAt(int column)
200                        throws IndexOutOfBoundsException {
201                if (column < 1 || column > this.length())
202                        throw new IndexOutOfBoundsException();
203                List<Object> labelList = new ArrayList<Object>();
204                Location loc;
205                Object label;
206                for (Iterator<Object> labelIterator = data.keySet().iterator(); labelIterator
207                                .hasNext();) {
208                        label = labelIterator.next();
209                        loc = getAE(label).getLoc();
210                        if (loc.contains(column))
211                                labelList.add(label);
212                }
213                return labelList;
214        }
215
216        // ///////////////////////
217        // methods from Interface Alignment
218        // //////////////////////
219
220        public synchronized int length() {
221                return alignmentRange.getMax() - alignmentRange.getMin() + 1;
222        }
223
224        public Alphabet getAlphabet() {
225                return alphabet;
226        }
227
228        /**
229         * getLabels will return a list of labels in left to right order
230         */
231
232        public synchronized List<String> getLabels() {
233                TreeSet<String> sorted = new TreeSet<String>(
234                                new LeftRightLocationComparator<String>());
235                sorted.addAll(labelOrder);
236                return new Vector<String>(sorted);
237        }
238
239        /**
240         * This gets the symbol for an individual sequence at position in the
241         * overall alignment If the sequence is not aligned at that location it
242         * returns null
243         */
244
245        public synchronized Symbol symbolAt(String label, int column)
246                        throws NoSuchElementException, IndexOutOfBoundsException {
247
248                SymbolList seq = symbolListForLabel(label);
249                int cloc = posInSeq(label, column);
250                Symbol symbol = null;
251                // debug (label.toString() + " " + column + ":" + cloc);
252                if (seq == null) {
253                        // debug("seq is null");
254                }
255
256                try {
257                        symbol = seq.symbolAt(cloc);
258                } catch (IndexOutOfBoundsException e) {
259                        // leave symbol == null
260                }
261                return symbol;
262        }
263
264        /**
265         * 
266         * @param label
267         * @return
268         * @throws NoSuchElementException
269         */
270        public synchronized SymbolList symbolListForLabel(String label)
271                        throws NoSuchElementException {
272                return getAE(label).getSymbolList();
273        }
274
275        // methods from interface EditableAlignment
276
277        public synchronized void edit(Object label, Edit edit)
278                        throws ChangeVetoException {
279                throw new BioError("Not implemented yet");
280        }
281
282        /**
283         * loc in this case is the Alignment Location
284         */
285        public synchronized void shiftAtAlignmentLoc(Object label, Location loc,
286                        int offset) throws ChangeVetoException,
287                        IllegalAlignmentEditException, IndexOutOfBoundsException {
288
289                Location sourceLoc = locInSeq(label, loc);
290                shiftAtSequenceLoc(label, sourceLoc, offset);
291
292        }
293
294        /**
295         * loc in this case is the SymbolList Location
296         */
297        public synchronized void shiftAtSequenceLoc(Object label, Location loc,
298                        int offset) throws ChangeVetoException,
299                        IllegalAlignmentEditException, IndexOutOfBoundsException {
300
301                ChangeSupport csgap;
302                ChangeEvent cegap;
303                ChangeSupport csloc;
304                ChangeEvent celoc;
305                celoc = new ChangeEvent(this, EditableAlignment.LOCATION, label);
306                csloc = getChangeSupport(EditableAlignment.LOCATION);
307                cegap = new ChangeEvent(this, EditableAlignment.GAPS, label);
308                csgap = getChangeSupport(EditableAlignment.GAPS);
309
310                int caseValue = 0;
311                int absOffset = Math.abs(offset);
312                Location seqLoc = locInAlignment(label);
313                AlignmentElement ae = getAE(label);
314                SymbolList seq = ae.getSymbolList();
315                Location newLoc;
316                int min = loc.getMin();
317                int max = loc.getMax();
318                if (min < 1 || max > seq.length()) {
319                        throw new IndexOutOfBoundsException();
320                }
321                if (offset == 0) {
322                        return;
323                }
324                if (offset > 1)
325                        caseValue += 1;
326                if (min == 1)
327                        caseValue += 2;
328                if (max == seq.length())
329                        caseValue += 4;
330
331                switch (caseValue) {
332
333                case 0: // internal shift to left
334                        if (!allGaps(seq, min + offset, min - 1)) {
335                                throw new IllegalAlignmentEditException();
336                        }
337                        csgap.firePreChangeEvent(cegap);
338
339                        ((GappedSymbolList) seq).addGapsInView(max + 1, absOffset);
340                        removeGaps((GappedSymbolList) seq, min - absOffset, absOffset);
341                        csgap.firePostChangeEvent(cegap);
342                        break;
343
344                case 1: // internal shift to right
345                        if (!allGaps(seq, max + 1, max + offset)) {
346                                throw new IllegalAlignmentEditException();
347                        }
348                        csgap.firePreChangeEvent(cegap);
349                        removeGaps((GappedSymbolList) seq, max + 1, offset);
350                        ((GappedSymbolList) seq).addGapsInView(min, offset);
351                        csgap.firePostChangeEvent(cegap);
352                        break;
353
354                case 2: // left end shift to left
355                        csgap.firePreChangeEvent(cegap);
356                        csloc.firePreChangeEvent(celoc);
357                        ((GappedSymbolList) seq).addGapsInView(max + 1, absOffset);
358                        newLoc = new RangeLocation(seqLoc.getMin() - absOffset, seqLoc
359                                        .getMax());
360                        ae.setLoc(newLoc);
361                        resetRange();
362                        csloc.firePostChangeEvent(celoc);
363                        csgap.firePostChangeEvent(cegap);
364                        break;
365
366                case 3: // left end shift to right
367                        if (!allGaps(seq, max + 1, max + offset)) {
368                                throw new IllegalAlignmentEditException();
369                        }
370                        csgap.firePreChangeEvent(cegap);
371                        csloc.firePreChangeEvent(celoc);
372                        removeGaps((GappedSymbolList) seq, max + 1, offset);
373                        newLoc = new RangeLocation(seqLoc.getMin() + offset, seqLoc
374                                        .getMax());
375                        ae.setLoc(newLoc);
376                        resetRange();
377                        csloc.firePostChangeEvent(celoc);
378                        csgap.firePostChangeEvent(cegap);
379                        break;
380
381                case 4: // right end shift to left
382                        if (!allGaps(seq, min - absOffset, min - 1)) {
383                                throw new IllegalAlignmentEditException();
384                        }
385                        csgap.firePreChangeEvent(cegap);
386                        csloc.firePreChangeEvent(celoc);
387                        removeGaps((GappedSymbolList) seq, min - absOffset, absOffset);
388                        newLoc = new RangeLocation(seqLoc.getMin(), seqLoc.getMax()
389                                        + offset);
390                        ae.setLoc(newLoc);
391                        resetRange();
392                        csloc.firePostChangeEvent(celoc);
393                        csgap.firePostChangeEvent(cegap);
394                        break;
395
396                case 5: // right end shift to right
397                        csgap.firePreChangeEvent(cegap);
398                        csloc.firePreChangeEvent(celoc);
399                        ((GappedSymbolList) seq).addGapsInView(min, offset);
400                        newLoc = new RangeLocation(seqLoc.getMin(), seqLoc.getMax()
401                                        + offset);
402                        ae.setLoc(newLoc);
403                        resetRange();
404                        csloc.firePostChangeEvent(celoc);
405                        csgap.firePostChangeEvent(cegap);
406                        break;
407
408                case 6: // whole seq shift to left
409                        debug("Shifting all to left " + absOffset);
410                        shift(label, offset);
411                        break;
412
413                case 7: // whole seq shift to right
414                        debug("Shifting all to right " + absOffset);
415                        shift(label, offset);
416                        break;
417
418                default:
419                        debug("OOOPS something is wrong " + loc.toString() + " "
420                                        + absOffset);
421                        return;
422                }
423        }
424
425        /**
426         * because there is a bug in GappedSymbolList
427         */
428
429        public synchronized void removeGaps(GappedSymbolList seq, int start,
430                        int length) {
431                try {
432                        // seq.removeGaps (start , length);
433                        // because there is a bug in GappedSymbolList we do it one at a time
434                        for (int i = 1; i <= length; i++) {
435                                seq.removeGap(start);
436                        }
437                } catch (IllegalSymbolException e) {
438                        throw new BioError("We should have tested for this already");
439                }
440        }
441
442        /**
443         * make sure that all Symbols in this range are gaps
444         */
445
446        protected synchronized boolean allGaps(SymbolList seq, int start, int end) {
447
448                Symbol gs = seq.getAlphabet().getGapSymbol();
449                for (int i = start; i <= end; i++) {
450                        if (!(seq.symbolAt(i).equals(gs))) {
451                                return false;
452                        }
453                }
454                return true;
455        }
456
457        /**
458         * check that begining is at 1 otherwise shift everything over
459         */
460        protected synchronized void resetRange() throws ChangeVetoException {
461
462                int min = 0;// just for the compiler
463                int max = 0;// just for the compiler
464                int lMin;
465                int lMax;
466                int count = 1;
467                // get the current range from all labels
468                for (Iterator<String> i = getLabels().iterator(); i.hasNext();) {
469                        Object label = i.next();
470                        lMin = locInAlignment(label).getMin();
471                        lMax = locInAlignment(label).getMax();
472                        if (count == 1) {
473                                min = lMin;
474                        } else {
475                                min = lesser(min, lMin);
476                        }
477                        if (count == 1) {
478                                max = lMax;
479                        } else {
480                                max = greater(max, lMax);
481                        }
482                        count++;
483                }
484                alignmentRange = new RangeLocation(min, max);
485                if (min != 1) {
486                        int offset = 1 - alignmentRange.getMin();
487                        shiftAll(offset);
488                        alignmentRange = new RangeLocation(
489                                        alignmentRange.getMin() + offset, alignmentRange.getMax()
490                                                        + offset);
491                }
492        }
493
494        protected synchronized void shiftAll(int offset) throws ChangeVetoException {
495                List<String> lList = getLabels();
496                for (Iterator<String> i = lList.iterator(); i.hasNext();) {
497                        Object label = i.next();
498                        shift(label, offset);
499                }
500        }
501
502        /**
503         * moves the whole sequence
504         */
505
506        protected synchronized void shift(Object label, int offset)
507                        throws ChangeVetoException {
508                ChangeSupport csloc;
509                ChangeEvent celoc;
510                celoc = new ChangeEvent(this, EditableAlignment.LOCATION, label);
511                csloc = getChangeSupport(EditableAlignment.LOCATION);
512                Location oLoc = locInAlignment(label);
513                Location nLoc = new RangeLocation(oLoc.getMin() + offset, oLoc.getMax()
514                                + offset);
515                csloc.firePreChangeEvent(celoc);
516                debug("shifting " + label.toString());
517                getAE(label).setLoc(nLoc);
518                resetRange();
519                debug("shifted " + label);
520                csloc.firePostChangeEvent(celoc);
521        }
522
523        // utility methods
524        protected int greater(int x, int y) {
525                int greatest = (x > y) ? x : y;
526                return greatest;
527        }
528
529        protected int lesser(int x, int y) {
530                int least = (x < y) ? x : y;
531                return least;
532        }
533
534        protected AlignmentElement getAE(Object label)
535                        throws NoSuchElementException {
536                if (!(data.containsKey(label)))
537                        ;
538                return data.get(label);
539        }
540
541        /**
542         * get the position in the sequence corresponding to the postion within the
543         * alignment
544         */
545
546        protected synchronized int posInSeq(Object label, int column)
547                        throws NoSuchElementException, IndexOutOfBoundsException {
548                if (column < 1 || column > this.length()) {
549                        throw new IndexOutOfBoundsException();
550                }
551                Location loc = locInAlignment(label);
552                return (column - loc.getMin() + 1);
553        }
554
555        protected synchronized Location locInSeq(Object label, Location viewLoc)
556                        throws NoSuchElementException, IndexOutOfBoundsException {
557                int min = posInSeq(label, viewLoc.getMin());
558                int max = posInSeq(label, viewLoc.getMax());
559                return new RangeLocation(min, max);
560        }
561}