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 */
021package org.biojavax.bio.phylo.io.nexus;
022
023import java.io.IOException;
024import java.io.Writer;
025import java.util.ArrayList;
026import java.util.Collection;
027import java.util.Collections;
028import java.util.Iterator;
029import java.util.LinkedHashMap;
030import java.util.List;
031import java.util.Map;
032import java.util.Set;
033
034import org.biojava.bio.seq.io.ParseException;
035
036/**
037 * Represents Nexus characters blocks.
038 * 
039 * @author Richard Holland
040 * @author Tobias Thierer
041 * @author Jim Balhoff
042 * @since 1.6
043 */
044public class CharactersBlock extends NexusBlock.Abstract {
045
046        /**
047         * A constant representing the name of Characters blocks.
048         */
049        public static final String CHARACTERS_BLOCK = "CHARACTERS";
050
051        private int dimensionsNTax = 0;
052
053        private int dimensionsNChar = 0;
054
055        private String dataType = "STANDARD";
056
057        private boolean respectCase = false;
058
059        private String missing = "?";
060
061        private String gap;
062
063        private List symbols = new ArrayList();
064
065        private Map equate = new LinkedHashMap(); // values are lists
066
067        private String matchChar;
068
069        private boolean labels = true;
070
071        private boolean transposed = false;
072
073        private boolean interleaved = false;
074
075        private List items = new ArrayList();
076
077        private String statesFormat = "STATESPRESENT";
078
079        private boolean tokens = false;
080
081        private int eliminateStart = 0;
082
083        private int eliminateEnd = 0;
084
085        private List taxLabels = new ArrayList();
086
087        // map containing two-value arrays where second value is list
088        private Map charStateLabels = new LinkedHashMap();
089
090        private List charLabels = new ArrayList();
091
092        private Map stateLabels = new LinkedHashMap(); // values are lists
093
094        // values are lists, containing nested mix of strings and lists and sets
095        private Map matrix = new LinkedHashMap();
096
097        private List comments = new ArrayList();
098
099        /**
100         * Delegates to NexusBlock.Abstract constructor using
101         * CharactersBlock.CHARACTERS_BLOCK as the name.
102         */
103        public CharactersBlock() {
104                this(CharactersBlock.CHARACTERS_BLOCK);
105        }
106
107        /**
108         * For the DATA block subclass.
109         * 
110         * @param replacementLabel
111         *            the different label to use.
112         */
113        protected CharactersBlock(final String replacementLabel) {
114                super(replacementLabel);
115        }
116
117        /**
118         * Set the NTAX value.
119         * 
120         * @param dimensionsNTax
121         *            the NTAX value.
122         */
123        public void setDimensionsNTax(int dimensionsNTax) {
124                this.dimensionsNTax = dimensionsNTax;
125        }
126
127        /**
128         * Get the NTAX value.
129         * 
130         * @return the NTAX value.
131         */
132        public int getDimensionsNTax() {
133                return this.dimensionsNTax;
134        }
135
136        /**
137         * Set the NCHAR value.
138         * 
139         * @param dimensionsNChar
140         *            the NCHAR value.
141         */
142        public void setDimensionsNChar(int dimensionsNChar) {
143                this.dimensionsNChar = dimensionsNChar;
144        }
145
146        /**
147         * Get the NCHAR value.
148         * 
149         * @return the NCHAR value.
150         */
151        public int getDimensionsNChar() {
152                return this.dimensionsNChar;
153        }
154
155        public void setDataType(final String dataType) {
156                this.dataType = dataType;
157        }
158
159        public String getDataType() {
160                return this.dataType;
161        }
162
163        public void setRespectCase(final boolean respectCase) {
164                this.respectCase = respectCase;
165        }
166
167        public boolean isRespectCase() {
168                return this.respectCase;
169        }
170
171        public void setMissing(final String missing) {
172                this.missing = missing;
173        }
174
175        public String getMissing() {
176                return this.missing;
177        }
178
179        public void setGap(final String gap) {
180                this.gap = gap;
181        }
182
183        public String getGap() {
184                return this.gap;
185        }
186
187        public void addSymbol(final String symbol) {
188                if (!this.symbols.contains(symbol))
189                        this.symbols.add(symbol);
190        }
191
192        public void removeSymbol(final String symbol) {
193                this.symbols.remove(symbol);
194        }
195
196        public void removeAllSymbols() {
197                this.symbols.clear();
198        }
199
200        public List getSymbols() {
201                return this.symbols;
202        }
203
204        public void addEquate(final String symbol, final List symbols) {
205                this.equate.put(symbol, symbols);
206        }
207
208        public void removeEquate(final String symbol) {
209                this.equate.remove(symbol);
210        }
211
212        public Map getEquates() {
213                return this.equate;
214        }
215
216        public void setMatchChar(final String matchChar) {
217                this.matchChar = matchChar;
218        }
219
220        public String getMatchChar() {
221                return this.matchChar;
222        }
223
224        public void setLabels(final boolean labels) {
225                this.labels = labels;
226        }
227
228        public boolean isLabels() {
229                return this.labels;
230        }
231
232        public void setTransposed(final boolean transposed) {
233                this.transposed = transposed;
234        }
235
236        public boolean isTransposed() {
237                return this.transposed;
238        }
239
240        public void setInterleaved(final boolean interleaved) {
241                this.interleaved = interleaved;
242        }
243
244        public boolean isInterleaved() {
245                return this.interleaved;
246        }
247
248        public void addItem(final String item) {
249                if (!this.items.contains(item))
250                        this.items.add(item);
251        }
252
253        public void removeItem(final String item) {
254                this.items.remove(item);
255        }
256
257        public void removeAllItems() {
258                this.items.clear();
259        }
260
261        public List getItems() {
262                return this.items;
263        }
264
265        public void setStatesFormat(final String statesFormat) {
266                this.statesFormat = statesFormat;
267        }
268
269        public String getStatesFormat() {
270                return this.statesFormat;
271        }
272
273        public void setTokens(final boolean tokens) {
274                this.tokens = tokens;
275        }
276
277        public boolean isTokens() {
278                return this.tokens;
279        }
280
281        public void setEliminateStart(final int eliminateStart) {
282                this.eliminateStart = eliminateStart;
283        }
284
285        public int getEliminateStart() {
286                return this.eliminateStart;
287        }
288
289        public void setEliminateEnd(final int eliminateEnd) {
290                this.eliminateEnd = eliminateEnd;
291        }
292
293        public int getEliminateEnd() {
294                return this.eliminateEnd;
295        }
296
297        /**
298         * Add a TAXLABEL. If it already exists, or is a number that refers to an
299         * index position that already exists, an exception is thrown.
300         * 
301         * @param taxLabel
302         *            the label to add.
303         * @throws ParseException
304         *             if the label cannot be added.
305         */
306        public void addTaxLabel(final String taxLabel) throws ParseException {
307                if (this.taxLabels.contains(taxLabel))
308                        throw new ParseException("Duplicate taxa label: " + taxLabel);
309                else
310                        try {
311                                // Try it as a number to see if it refers to
312                                // position we already have.
313                                final int i = Integer.parseInt(taxLabel);
314                                if (i <= this.taxLabels.size() + 1)
315                                        throw new ParseException("Taxa label " + i
316                                                        + " refers to already extant taxa position");
317                        } catch (NumberFormatException e) {
318                                // It is not a number, so ignore.
319                        } catch (ParseException e) {
320                                // Throw it.
321                                throw e;
322                        }
323                this.taxLabels.add(taxLabel);
324        }
325
326        /**
327         * Removes the given TAXLABEL.
328         * 
329         * @param taxLabel
330         *            the label to remove.
331         */
332        public void removeTaxLabel(final String taxLabel) {
333                this.taxLabels.remove(taxLabel);
334        }
335
336        /**
337         * Checks to see if we contain the given TAXLABEL.
338         * 
339         * @param taxLabel
340         *            the label to check for.
341         * @return <tt>true</tt> if we already contain it.
342         */
343        public boolean containsTaxLabel(final String taxLabel) {
344                if (this.taxLabels.contains(taxLabel))
345                        return true;
346                else
347                        try {
348                                // Try it as a number to see if it refers to
349                                // position we already have.
350                                final int i = Integer.parseInt(taxLabel);
351                                if (i <= this.taxLabels.size() + 1)
352                                        return true;
353                        } catch (NumberFormatException e) {
354                                // It is not a number, so ignore.
355                        }
356                return false;
357        }
358
359        /**
360         * Get the TAXLABEL values added so far.
361         * 
362         * @return this labels so far.
363         */
364        public List getTaxLabels() {
365                return this.taxLabels;
366        }
367
368        public void addCharState(final String charState) {
369                this.charStateLabels.put(charState, new Object[] { null,
370                                new ArrayList() });
371        }
372
373        public void setCharStateLabel(final String charState, final String label) {
374                if (!this.charStateLabels.containsKey(charState))
375                        this.addCharState(charState);
376                ((Object[]) this.charStateLabels.get(charState))[0] = label;
377        }
378
379        public void addCharStateKeyword(final String charState, final String keyword) {
380                if (!this.charStateLabels.containsKey(charState))
381                        this.addCharState(charState);
382                ((List) ((Object[]) this.charStateLabels.get(charState))[1])
383                                .add(keyword);
384        }
385
386        public String getCharStateLabel(final String charState) {
387                return (String) (((Object[]) this.charStateLabels.get(charState))[0]);
388        }
389
390        public List getCharStateLabelKeywords(final String charState) {
391                return (List) (((Object[]) this.charStateLabels.get(charState))[1]);
392        }
393
394        public void removeCharState(final String charState) {
395                this.charStateLabels.remove(charState);
396        }
397
398        public Set getAllCharStates() {
399                return this.charStateLabels.keySet();
400        }
401
402        public void addCharLabel(final String charLabel) {
403                this.charLabels.add(charLabel);
404        }
405
406        public void removeCharLabel(final String charLabel) {
407                this.charLabels.remove(charLabel);
408        }
409
410        public boolean containsCharLabel(final String charLabel) {
411                return this.charLabels.contains(charLabel);
412        }
413
414        public List getCharLabels() {
415                return this.charLabels;
416        }
417
418        public void addState(final String state) {
419                this.stateLabels.put(state, new ArrayList());
420        }
421
422        public void addStateLabel(final String state, final String label) {
423                if (!this.stateLabels.containsKey(state))
424                        this.addState(state);
425                ((List) this.stateLabels.get(state)).add(label);
426        }
427
428        public List getStateLabels(final String state) {
429                return (List) this.stateLabels.get(state);
430        }
431
432        public void removeState(final String state) {
433                this.stateLabels.remove(state);
434        }
435
436        public void addMatrixEntry(final String taxa) {
437                if (!this.matrix.containsKey(taxa))
438                        this.matrix.put(taxa, new ArrayList());
439        }
440
441        public void appendMatrixData(final String taxa, final Object data) {
442                ((List) this.matrix.get(taxa)).add(data);
443        }
444
445        public List getMatrixData(final String taxa) {
446                return (List) this.matrix.get(taxa);
447        }
448        
449        public Collection getMatrixLabels() {
450                return Collections.unmodifiableSet(this.matrix.keySet());
451        }
452
453        /**
454         * Adds a comment.
455         * 
456         * @param comment
457         *            the comment to add.
458         */
459        public void addComment(final NexusComment comment) {
460                this.comments.add(comment);
461        }
462
463        /**
464         * Removes a comment.
465         * 
466         * @param comment
467         *            the comment to remove.
468         */
469        public void removeComment(final NexusComment comment) {
470                this.comments.remove(comment);
471        }
472
473        /**
474         * Returns all comments.
475         * 
476         * @return all the selected comments.
477         */
478        public List getComments() {
479                return this.comments;
480        }
481
482        protected void writeBlockContents(Writer writer) throws IOException {
483                for (final Iterator i = this.comments.iterator(); i.hasNext();) {
484                        ((NexusComment) i.next()).writeObject(writer);
485                        writer.write(NexusFileFormat.NEW_LINE);
486                }
487                writer.write(" DIMENSIONS ");
488                if (!this.taxLabels.isEmpty())
489                        writer.write("NEWTAXA ");
490                if (this.dimensionsNTax > 0)
491                        writer.write("NTAX=" + this.dimensionsNTax + " ");
492                writer.write("NCHAR=" + this.dimensionsNChar + ";"
493                                + NexusFileFormat.NEW_LINE);
494
495                writer.write(" FORMAT DATATYPE=");
496                this.writeToken(writer, this.dataType);
497                if (this.respectCase && "STANDARD".equals(this.dataType))
498                        writer.write(" RESPECTCASE");
499                writer.write(" MISSING=");
500                this.writeToken(writer, this.missing);
501                writer.write(" GAP=");
502                this.writeToken(writer, this.gap);
503                writer.write(" SYMBOLS=\"");
504                if (this.symbols.isEmpty()) {
505                        this.symbols.add("0");
506                        this.symbols.add("1");
507                }
508                for (final Iterator i = this.symbols.iterator(); i.hasNext();)
509                        this.writeToken(writer, (String) i.next());
510                writer.write('"');
511                if (!this.equate.isEmpty()) {
512                        writer.write(" EQUATE=\"");
513                        for (final Iterator i = this.equate.entrySet().iterator(); i
514                                        .hasNext();) {
515                                final Map.Entry entry = (Map.Entry) i.next();
516                                this.writeToken(writer, "" + entry.getKey());
517                                writer.write("=(");
518                                for (final Iterator j = ((List) entry.getValue()).iterator(); j
519                                                .hasNext();)
520                                        this.writeToken(writer, "" + j.next());
521                                writer.write(')');
522                                if (i.hasNext())
523                                        writer.write(' ');
524                        }
525                        writer.write('"');
526                }
527                if (this.matchChar != null) {
528                        writer.write(" MATCHCHAR=");
529                        this.writeToken(writer, this.matchChar);
530                }
531                writer.write(this.labels ? " LABELS" : " NOLABELS");
532                if (this.transposed)
533                        writer.write(" TRANSPOSED");
534                // FIXME Output files, for now, are never interleaved.
535                // if (this.interleaved)
536                // writer.write(" INTERLEAVED");
537                writer.write(" ITEMS=");
538                if (this.items.isEmpty())
539                        this.items.add("STATES");
540                if (this.items.size() > 1)
541                        writer.write('(');
542                for (final Iterator i = this.items.iterator(); i.hasNext();) {
543                        this.writeToken(writer, "" + i.next());
544                        if (i.hasNext())
545                                writer.write(' ');
546                }
547                if (this.items.size() > 1)
548                        writer.write(')');
549                writer.write(" STATESFORMAT=");
550                this.writeToken(writer, this.statesFormat);
551                final boolean reallyUseTokens = (this.tokens || "CONTINUOUS"
552                                .equals(this.dataType))
553                                && !("DNA".equals(this.dataType) || "RNA".equals(this.dataType) || "NUCLEOTIDE"
554                                                .equals(this.dataType));
555                writer.write(reallyUseTokens ? " TOKENS" : " NOTOKENS");
556                writer.write(";" + NexusFileFormat.NEW_LINE);
557
558                if (this.eliminateStart > 0 && this.eliminateEnd > 0) {
559                        writer.write(" ELIMINATE " + this.eliminateStart + "-"
560                                        + this.eliminateEnd);
561                        writer.write(";" + NexusFileFormat.NEW_LINE);
562                }
563
564                if (this.taxLabels.size() > 0) {
565                        writer.write(" TAXLABELS");
566                        for (final Iterator i = this.taxLabels.iterator(); i.hasNext();) {
567                                writer.write(' ');
568                                this.writeToken(writer, (String) i.next());
569                        }
570                        writer.write(";" + NexusFileFormat.NEW_LINE);
571                }
572
573                if (!this.charStateLabels.isEmpty()
574                                && !"CONTINUOUS".equals(this.dataType)) {
575                        writer.write(" CHARSTATELABELS" + NexusFileFormat.NEW_LINE);
576                        for (final Iterator i = this.charStateLabels.entrySet().iterator(); i
577                                        .hasNext();) {
578                                final Map.Entry topEntry = (Map.Entry) i.next();
579                                writer.write('\t');
580                                this.writeToken(writer, "" + topEntry.getKey());
581                                writer.write('\t');
582                                final Object[] parts = (Object[]) topEntry.getValue();
583                                this.writeToken(writer, "" + parts[0]);
584                                final List names = (List) parts[1];
585                                if (!names.isEmpty()) {
586                                        writer.write('/');
587                                        for (final Iterator k = names.iterator(); k.hasNext();) {
588                                                this.writeToken(writer, "" + k.next());
589                                                if (k.hasNext())
590                                                        writer.write(' ');
591                                        }
592                                }
593                                if (!i.hasNext())
594                                        writer.write(';');
595                                else
596                                        writer.write(',');
597                                writer.write(NexusFileFormat.NEW_LINE);
598                        }
599                }
600
601                if (!this.charLabels.isEmpty() && !this.transposed) {
602                        writer.write(" CHARLABELS" + NexusFileFormat.NEW_LINE);
603                        writer.write('\t');
604                        for (final Iterator i = this.charLabels.iterator(); i.hasNext();) {
605                                this.writeToken(writer, "" + i.next());
606                                if (i.hasNext())
607                                        writer.write(' ');
608                        }
609                        writer.write(";" + NexusFileFormat.NEW_LINE);
610                }
611
612                if (!this.stateLabels.isEmpty() && !"CONTINUOUS".equals(this.dataType)) {
613                        writer.write(" STATELABELS" + NexusFileFormat.NEW_LINE);
614                        for (final Iterator i = this.stateLabels.entrySet().iterator(); i
615                                        .hasNext();) {
616                                final Map.Entry topEntry = (Map.Entry) i.next();
617                                writer.write('\t');
618                                this.writeToken(writer, "" + topEntry.getKey());
619                                writer.write('\t');
620                                final List names = (List) topEntry.getValue();
621                                for (final Iterator k = names.iterator(); k.hasNext();) {
622                                        this.writeToken(writer, "" + k.next());
623                                        if (k.hasNext())
624                                                writer.write(' ');
625                                }
626                                if (!i.hasNext())
627                                        writer.write(';');
628                                else
629                                        writer.write(',');
630                                writer.write(NexusFileFormat.NEW_LINE);
631                        }
632                }
633
634                // if statesformat=statespresent and items=1, bracket only multi values,
635                // otherwise bracket all values
636                // only space tokens if reallyUseTokens=true
637                writer.write(" MATRIX" + NexusFileFormat.NEW_LINE);
638                for (final Iterator i = this.matrix.entrySet().iterator(); i.hasNext();) {
639                        final Map.Entry entry = (Map.Entry) i.next();
640                        writer.write('\t');
641                        this.writeToken(writer, "" + entry.getKey());
642                        writer.write('\t');
643                        for (final Iterator j = ((List) entry.getValue()).iterator(); j
644                                        .hasNext();) {
645                                this.writeMatrixEntry(writer, j.next(), reallyUseTokens);
646                                if (reallyUseTokens && j.hasNext())
647                                        writer.write(' ');
648                        }
649                        writer.write(NexusFileFormat.NEW_LINE);
650                }
651                writer.write(";" + NexusFileFormat.NEW_LINE);
652
653        }
654
655        private void writeMatrixEntry(final Writer writer, final Object obj,
656                        final boolean reallyUseTokens) throws IOException {
657                if (obj == null)
658                        this.writeToken(writer, this.missing);
659                else if (obj instanceof String)
660                        this.writeToken(writer, (String) obj);
661                else if (obj instanceof List) {
662                        writer.write('(');
663                        for (final Iterator k = ((List) obj).iterator(); k.hasNext();) {
664                                this.writeMatrixEntry(writer, k.next(), reallyUseTokens);
665                                if (k.hasNext() && reallyUseTokens)
666                                        writer.write(' ');
667                        }
668                        writer.write(')');
669                } else if (obj instanceof Set) {
670                        writer.write('{');
671                        for (final Iterator k = ((Set) obj).iterator(); k.hasNext();) {
672                                this.writeMatrixEntry(writer, k.next(), reallyUseTokens);
673                                if (k.hasNext() && reallyUseTokens)
674                                        writer.write(' ');
675                        }
676                        writer.write('}');
677                }
678        }
679}