2 * Jalview - A Sequence Alignment Editor and Viewer (Version 2.8)
3 * Copyright (C) 2011 J Procter, AM Waterhouse, J Engelhardt, LM Lui, G Barton, M Clamp, S Searle
5 * This file is part of Jalview.
7 * Jalview is free software: you can redistribute it and/or
8 * modify it under the terms of the GNU General Public License
9 * as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
11 * Jalview is distributed in the hope that it will be useful, but
12 * WITHOUT ANY WARRANTY; without even the implied warranty
13 * of MERCHANTABILITY or FITNESS FOR A PARTICULAR
14 * PURPOSE. See the GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License along with Jalview. If not, see <http://www.gnu.org/licenses/>.
18 package jalview.viewmodel;
20 import jalview.analysis.Conservation;
21 import jalview.api.AlignCalcManagerI;
22 import jalview.api.AlignViewportI;
23 import jalview.api.AlignmentViewPanel;
24 import jalview.datamodel.AlignmentAnnotation;
25 import jalview.datamodel.AlignmentI;
26 import jalview.datamodel.AlignmentView;
27 import jalview.datamodel.Annotation;
28 import jalview.datamodel.ColumnSelection;
29 import jalview.datamodel.Sequence;
30 import jalview.datamodel.SequenceGroup;
31 import jalview.datamodel.SequenceI;
32 import jalview.schemes.ClustalxColourScheme;
33 import jalview.schemes.ColourSchemeI;
34 import jalview.schemes.ResidueProperties;
35 import jalview.workers.AlignCalcManager;
36 import jalview.workers.ConsensusThread;
37 import jalview.workers.ConservationThread;
38 import jalview.workers.StrucConsensusThread;
40 import java.util.Hashtable;
41 import java.util.Vector;
44 * base class holding visualization and analysis attributes and common logic for
45 * an active alignment view displayed in the GUI
50 public abstract class AlignmentViewport implements AlignViewportI
53 * alignment displayed in the viewport. Please use get/setter
55 protected AlignmentI alignment;
57 protected String sequenceSetID;
60 * probably unused indicator that view is of a dataset rather than an
63 protected boolean isDataset = false;
65 private Hashtable hiddenRepSequences;
67 protected ColumnSelection colSel = new ColumnSelection();
69 public boolean autoCalculateConsensus = true;
71 protected boolean autoCalculateStrucConsensus = true;
73 protected boolean ignoreGapsInConsensusCalculation = false;
75 protected ColourSchemeI globalColourScheme = null;
78 public void setGlobalColourScheme(ColourSchemeI cs)
80 globalColourScheme = cs;
83 public ColourSchemeI getGlobalColourScheme()
85 return globalColourScheme;
88 protected AlignmentAnnotation consensus;
90 protected AlignmentAnnotation strucConsensus;
92 protected AlignmentAnnotation conservation;
94 protected AlignmentAnnotation quality;
96 protected AlignmentAnnotation[] groupConsensus;
98 protected AlignmentAnnotation[] groupConservation;
101 * results of alignment consensus analysis for visible portion of view
103 protected Hashtable[] hconsensus = null;
106 * results of secondary structure base pair consensus for visible portion of
109 protected Hashtable[] hStrucConsensus = null;
112 * percentage gaps allowed in a column before all amino acid properties should
113 * be considered unconserved
115 int ConsPercGaps = 25; // JBPNote : This should be a scalable property!
117 public int getConsPercGaps()
122 public void setSequenceConsensusHash(Hashtable[] hconsensus)
124 this.hconsensus = hconsensus;
129 public Hashtable[] getSequenceConsensusHash()
135 public Hashtable[] getRnaStructureConsensusHash()
137 return hStrucConsensus;
141 public void setRnaStructureConsensusHash(Hashtable[] hStrucConsensus)
143 this.hStrucConsensus = hStrucConsensus;
148 public AlignmentAnnotation getAlignmentQualityAnnot()
154 public AlignmentAnnotation getAlignmentConservationAnnotation()
160 public AlignmentAnnotation getAlignmentConsensusAnnotation()
166 public AlignmentAnnotation getAlignmentStrucConsensusAnnotation()
168 return strucConsensus;
171 protected AlignCalcManagerI calculator = new AlignCalcManager();
174 * trigger update of conservation annotation
176 public void updateConservation(final AlignmentViewPanel ap)
178 // see note in mantis : issue number 8585
179 if (alignment.isNucleotide() || conservation == null
180 || !autoCalculateConsensus)
185 .startRegisteredWorkersOfClass(jalview.workers.ConservationThread.class))
187 calculator.registerWorker(new jalview.workers.ConservationThread(
193 * trigger update of consensus annotation
195 public void updateConsensus(final AlignmentViewPanel ap)
197 // see note in mantis : issue number 8585
198 if (consensus == null || !autoCalculateConsensus)
202 if (!calculator.startRegisteredWorkersOfClass(ConsensusThread.class))
204 calculator.registerWorker(new ConsensusThread(this, ap));
208 // --------START Structure Conservation
209 public void updateStrucConsensus(final AlignmentViewPanel ap)
211 if (autoCalculateStrucConsensus && strucConsensus == null
212 && alignment.isNucleotide() && alignment.hasRNAStructure())
217 // see note in mantis : issue number 8585
218 if (strucConsensus == null || !autoCalculateStrucConsensus)
223 .startRegisteredWorkersOfClass(StrucConsensusThread.class))
225 calculator.registerWorker(new StrucConsensusThread(this, ap));
229 public boolean isCalcInProgress()
231 return calculator.isWorking();
234 public boolean isCalculationInProgress(
235 AlignmentAnnotation alignmentAnnotation)
237 if (!alignmentAnnotation.autoCalculated)
239 if (calculator.workingInvolvedWith(alignmentAnnotation))
241 // System.err.println("grey out ("+alignmentAnnotation.label+")");
248 public boolean isClosed()
250 // TODO: check that this isClosed is only true after panel is closed, not
251 // before it is fully constructed.
252 return alignment == null;
256 public AlignCalcManagerI getCalcManager()
262 * should conservation rows be shown for groups
264 protected boolean showGroupConservation = false;
267 * should consensus rows be shown for groups
269 protected boolean showGroupConsensus = false;
272 * should consensus profile be rendered by default
274 protected boolean showSequenceLogo = false;
276 * should consensus profile be rendered normalised to row height
278 protected boolean normaliseSequenceLogo = false;
280 * should consensus histograms be rendered by default
282 protected boolean showConsensusHistogram = true;
285 * @return the showConsensusProfile
287 public boolean isShowSequenceLogo()
289 return showSequenceLogo;
293 * @param showSequenceLogo
296 public void setShowSequenceLogo(boolean showSequenceLogo)
298 if (showSequenceLogo != this.showSequenceLogo)
300 // TODO: decouple settings setting from calculation when refactoring
301 // annotation update method from alignframe to viewport
302 this.showSequenceLogo = showSequenceLogo;
303 calculator.updateAnnotationFor(ConsensusThread.class);
304 calculator.updateAnnotationFor(StrucConsensusThread.class);
306 this.showSequenceLogo = showSequenceLogo;
310 * @param showConsensusHistogram
311 * the showConsensusHistogram to set
313 public void setShowConsensusHistogram(boolean showConsensusHistogram)
315 this.showConsensusHistogram = showConsensusHistogram;
319 * @return the showGroupConservation
321 public boolean isShowGroupConservation()
323 return showGroupConservation;
327 * @param showGroupConservation
328 * the showGroupConservation to set
330 public void setShowGroupConservation(boolean showGroupConservation)
332 this.showGroupConservation = showGroupConservation;
336 * @return the showGroupConsensus
338 public boolean isShowGroupConsensus()
340 return showGroupConsensus;
344 * @param showGroupConsensus
345 * the showGroupConsensus to set
347 public void setShowGroupConsensus(boolean showGroupConsensus)
349 this.showGroupConsensus = showGroupConsensus;
354 * @return flag to indicate if the consensus histogram should be rendered by
357 public boolean isShowConsensusHistogram()
359 return this.showConsensusHistogram;
363 * show non-conserved residues only
365 protected boolean showUnconserved = false;
369 * when set, updateAlignment will always ensure sequences are of equal length
371 private boolean padGaps = false;
374 * when set, alignment should be reordered according to a newly opened tree
376 public boolean sortByTree = false;
378 public boolean getShowUnconserved()
380 return showUnconserved;
383 public void setShowUnconserved(boolean showunconserved)
385 showUnconserved = showunconserved;
389 * @param showNonconserved
390 * the showUnconserved to set
392 public void setShowunconserved(boolean displayNonconserved)
394 this.showUnconserved = displayNonconserved;
400 * @return null or the currently selected sequence region
402 public SequenceGroup getSelectionGroup()
404 return selectionGroup;
408 * Set the selection group for this window.
411 * - group holding references to sequences in this alignment view
414 public void setSelectionGroup(SequenceGroup sg)
419 public void setHiddenColumns(ColumnSelection colsel)
421 this.colSel = colsel;
422 if (colSel.getHiddenColumns() != null)
424 hasHiddenColumns = true;
428 public ColumnSelection getColumnSelection()
433 public void setColumnSelection(ColumnSelection colSel)
435 this.colSel = colSel;
437 public Hashtable getHiddenRepSequences()
439 return hiddenRepSequences;
441 public void setHiddenRepSequences(Hashtable hiddenRepSequences)
443 this.hiddenRepSequences = hiddenRepSequences;
445 protected boolean hasHiddenColumns = false;
447 public void updateHiddenColumns()
449 hasHiddenColumns = colSel.getHiddenColumns() != null;
452 protected boolean hasHiddenRows = false;
454 public boolean hasHiddenRows()
456 return hasHiddenRows;
459 protected SequenceGroup selectionGroup;
461 public void setSequenceSetId(String newid)
463 if (sequenceSetID != null)
466 .println("Warning - overwriting a sequenceSetId for a viewport!");
468 sequenceSetID = new String(newid);
471 public String getSequenceSetId()
473 if (sequenceSetID == null)
475 sequenceSetID = alignment.hashCode() + "";
478 return sequenceSetID;
482 * unique viewId for synchronizing state (e.g. with stored Jalview Project)
485 protected String viewId = null;
487 public String getViewId()
491 viewId = this.getSequenceSetId() + "." + this.hashCode() + "";
496 public void setIgnoreGapsConsensus(boolean b, AlignmentViewPanel ap)
498 ignoreGapsInConsensusCalculation = b;
502 if (globalColourScheme != null)
504 globalColourScheme.setThreshold(globalColourScheme.getThreshold(),
505 ignoreGapsInConsensusCalculation);
511 private long sgrouphash = -1, colselhash = -1;
514 * checks current SelectionGroup against record of last hash value, and
518 * update the record of last hash value
520 * @return true if SelectionGroup changed since last call (when b is true)
522 public boolean isSelectionGroupChanged(boolean b)
524 int hc = (selectionGroup == null || selectionGroup.getSize() == 0) ? -1
525 : selectionGroup.hashCode();
526 if (hc != -1 && hc != sgrouphash)
538 * checks current colsel against record of last hash value, and optionally
542 * update the record of last hash value
543 * @return true if colsel changed since last call (when b is true)
545 public boolean isColSelChanged(boolean b)
547 int hc = (colSel == null || colSel.size() == 0) ? -1 : colSel
549 if (hc != -1 && hc != colselhash)
560 public boolean getIgnoreGapsConsensus()
562 return ignoreGapsInConsensusCalculation;
565 // / property change stuff
567 // JBPNote Prolly only need this in the applet version.
568 private java.beans.PropertyChangeSupport changeSupport = new java.beans.PropertyChangeSupport(
571 protected boolean showConservation = true;
573 protected boolean showQuality = true;
575 protected boolean showConsensus = true;
579 * Property change listener for changes in alignment
584 public void addPropertyChangeListener(
585 java.beans.PropertyChangeListener listener)
587 changeSupport.addPropertyChangeListener(listener);
596 public void removePropertyChangeListener(
597 java.beans.PropertyChangeListener listener)
599 changeSupport.removePropertyChangeListener(listener);
603 * Property change listener for changes in alignment
612 public void firePropertyChange(String prop, Object oldvalue,
615 changeSupport.firePropertyChange(prop, oldvalue, newvalue);
618 // common hide/show column stuff
621 public void hideSelectedColumns()
623 if (colSel.size() < 1)
628 colSel.hideSelectedColumns();
629 setSelectionGroup(null);
631 hasHiddenColumns = true;
634 public void hideColumns(int start, int end)
638 colSel.hideColumns(start);
642 colSel.hideColumns(start, end);
645 hasHiddenColumns = true;
648 public void showColumn(int col)
650 colSel.revealHiddenColumns(col);
651 if (colSel.getHiddenColumns() == null)
653 hasHiddenColumns = false;
657 public void showAllHiddenColumns()
659 colSel.revealAllHiddenColumns();
660 hasHiddenColumns = false;
664 // common hide/show seq stuff
665 public void showAllHiddenSeqs()
667 if (alignment.getHiddenSequences().getSize() > 0)
669 if (selectionGroup == null)
671 selectionGroup = new SequenceGroup();
672 selectionGroup.setEndRes(alignment.getWidth() - 1);
674 Vector tmp = alignment.getHiddenSequences().showAll(
676 for (int t = 0; t < tmp.size(); t++)
678 selectionGroup.addSequence((SequenceI) tmp.elementAt(t), false);
681 hasHiddenRows = false;
682 hiddenRepSequences = null;
684 firePropertyChange("alignment", null, alignment.getSequences());
685 // used to set hasHiddenRows/hiddenRepSequences here, after the property
691 public void showSequence(int index)
693 Vector tmp = alignment.getHiddenSequences().showSequence(index,
697 if (selectionGroup == null)
699 selectionGroup = new SequenceGroup();
700 selectionGroup.setEndRes(alignment.getWidth() - 1);
703 for (int t = 0; t < tmp.size(); t++)
705 selectionGroup.addSequence((SequenceI) tmp.elementAt(t), false);
707 // JBPNote: refactor: only update flag if we modified visiblity (used to
708 // do this regardless)
709 if (alignment.getHiddenSequences().getSize() < 1)
711 hasHiddenRows = false;
713 firePropertyChange("alignment", null, alignment.getSequences());
720 public void hideAllSelectedSeqs()
722 if (selectionGroup == null || selectionGroup.getSize() < 1)
727 SequenceI[] seqs = selectionGroup.getSequencesInOrder(alignment);
731 setSelectionGroup(null);
735 public void hideSequence(SequenceI[] seq)
739 for (int i = 0; i < seq.length; i++)
741 alignment.getHiddenSequences().hideSequence(seq[i]);
743 hasHiddenRows = true;
744 firePropertyChange("alignment", null, alignment.getSequences());
748 public void hideRepSequences(SequenceI repSequence, SequenceGroup sg)
750 int sSize = sg.getSize();
756 if (hiddenRepSequences == null)
758 hiddenRepSequences = new Hashtable();
761 hiddenRepSequences.put(repSequence, sg);
763 // Hide all sequences except the repSequence
764 SequenceI[] seqs = new SequenceI[sSize - 1];
766 for (int i = 0; i < sSize; i++)
768 if (sg.getSequenceAt(i) != repSequence)
770 if (index == sSize - 1)
775 seqs[index++] = sg.getSequenceAt(i);
778 sg.setSeqrep(repSequence); // note: not done in 2.7applet
779 sg.setHidereps(true); // note: not done in 2.7applet
784 public boolean isHiddenRepSequence(SequenceI seq)
786 return hiddenRepSequences != null
787 && hiddenRepSequences.containsKey(seq);
790 public SequenceGroup getRepresentedSequences(SequenceI seq)
792 return (SequenceGroup) (hiddenRepSequences == null ? null
793 : hiddenRepSequences.get(seq));
796 public int adjustForHiddenSeqs(int alignmentIndex)
798 return alignment.getHiddenSequences().adjustForHiddenSeqs(
802 // Selection manipulation
804 * broadcast selection to any interested parties
806 public abstract void sendSelection();
809 public void invertColumnSelection()
811 colSel.invertColumnSelection(0, alignment.getWidth());
815 * This method returns an array of new SequenceI objects derived from the
816 * whole alignment or just the current selection with start and end points
819 * @note if you need references to the actual SequenceI objects in the
820 * alignment or currently selected then use getSequenceSelection()
821 * @return selection as new sequenceI objects
823 public SequenceI[] getSelectionAsNewSequence()
825 SequenceI[] sequences;
826 // JBPNote: Need to test jalviewLite.getSelectedSequencesAsAlignmentFrom -
827 // this was the only caller in the applet for this method
828 // JBPNote: in applet, this method returned references to the alignment
829 // sequences, and it did not honour the presence/absence of annotation
830 // attached to the alignment (probably!)
831 if (selectionGroup == null)
833 sequences = alignment.getSequencesArray();
834 AlignmentAnnotation[] annots = alignment.getAlignmentAnnotation();
835 for (int i = 0; i < sequences.length; i++)
837 sequences[i] = new Sequence(sequences[i], annots); // construct new
845 sequences = selectionGroup.getSelectionAsNewSequences(alignment);
852 * get the currently selected sequence objects or all the sequences in the
855 * @return array of references to sequence objects
857 public SequenceI[] getSequenceSelection()
859 SequenceI[] sequences = null;
860 if (selectionGroup != null)
862 sequences = selectionGroup.getSequencesInOrder(alignment);
864 if (sequences == null)
866 sequences = alignment.getSequencesArray();
873 * This method returns the visible alignment as text, as seen on the GUI, ie
874 * if columns are hidden they will not be returned in the result. Use this for
875 * calculating trees, PCA, redundancy etc on views which contain hidden
880 public jalview.datamodel.CigarArray getViewAsCigars(
881 boolean selectedRegionOnly)
883 return new jalview.datamodel.CigarArray(alignment,
884 (hasHiddenColumns ? colSel : null),
885 (selectedRegionOnly ? selectionGroup : null));
889 * return a compact representation of the current alignment selection to pass
890 * to an analysis function
892 * @param selectedOnly
893 * boolean true to just return the selected view
894 * @return AlignmentView
896 public jalview.datamodel.AlignmentView getAlignmentView(
897 boolean selectedOnly)
899 return getAlignmentView(selectedOnly, false);
903 * return a compact representation of the current alignment selection to pass
904 * to an analysis function
906 * @param selectedOnly
907 * boolean true to just return the selected view
909 * boolean true to annotate the alignment view with groups on the
910 * alignment (and intersecting with selected region if selectedOnly
912 * @return AlignmentView
914 public jalview.datamodel.AlignmentView getAlignmentView(
915 boolean selectedOnly, boolean markGroups)
917 return new AlignmentView(alignment, colSel, selectionGroup,
918 hasHiddenColumns, selectedOnly, markGroups);
923 * This method returns the visible alignment as text, as seen on the GUI, ie
924 * if columns are hidden they will not be returned in the result. Use this for
925 * calculating trees, PCA, redundancy etc on views which contain hidden
930 public String[] getViewAsString(boolean selectedRegionOnly)
932 String[] selection = null;
933 SequenceI[] seqs = null;
935 int start = 0, end = 0;
936 if (selectedRegionOnly && selectionGroup != null)
938 iSize = selectionGroup.getSize();
939 seqs = selectionGroup.getSequencesInOrder(alignment);
940 start = selectionGroup.getStartRes();
941 end = selectionGroup.getEndRes() + 1;
945 iSize = alignment.getHeight();
946 seqs = alignment.getSequencesArray();
947 end = alignment.getWidth();
950 selection = new String[iSize];
951 if (hasHiddenColumns)
953 selection = colSel.getVisibleSequenceStrings(start, end, seqs);
957 for (i = 0; i < iSize; i++)
959 selection[i] = seqs[i].getSequenceAsString(start, end);
967 * return visible region boundaries within given column range
970 * first column (inclusive, from 0)
972 * last column (exclusive)
973 * @return int[][] range of {start,end} visible positions
975 public int[][] getVisibleRegionBoundaries(int min, int max)
977 Vector regions = new Vector();
983 if (hasHiddenColumns)
987 start = colSel.adjustForHiddenColumns(start);
990 end = colSel.getHiddenBoundaryRight(start);
1001 regions.addElement(new int[]
1004 if (hasHiddenColumns)
1006 start = colSel.adjustForHiddenColumns(end);
1007 start = colSel.getHiddenBoundaryLeft(start) + 1;
1009 } while (end < max);
1011 int[][] startEnd = new int[regions.size()][2];
1013 regions.copyInto(startEnd);
1019 * @return the padGaps
1021 public boolean isPadGaps()
1028 * the padGaps to set
1030 public void setPadGaps(boolean padGaps)
1032 this.padGaps = padGaps;
1036 * apply any post-edit constraints and trigger any calculations needed after
1037 * an edit has been performed on the alignment
1041 public void alignmentChanged(AlignmentViewPanel ap)
1045 alignment.padGaps();
1047 if (autoCalculateConsensus)
1049 updateConsensus(ap);
1051 if (hconsensus != null && autoCalculateConsensus)
1053 updateConservation(ap);
1055 if (autoCalculateStrucConsensus)
1057 updateStrucConsensus(ap);
1060 // Reset endRes of groups if beyond alignment width
1061 int alWidth = alignment.getWidth();
1062 Vector groups = alignment.getGroups();
1065 for (int i = 0; i < groups.size(); i++)
1067 SequenceGroup sg = (SequenceGroup) groups.elementAt(i);
1068 if (sg.getEndRes() > alWidth)
1070 sg.setEndRes(alWidth - 1);
1075 if (selectionGroup != null && selectionGroup.getEndRes() > alWidth)
1077 selectionGroup.setEndRes(alWidth - 1);
1080 resetAllColourSchemes();
1081 calculator.restartWorkers();
1082 // alignment.adjustSequenceAnnotations();
1087 * reset scope and do calculations for all applied colourschemes on alignment
1089 void resetAllColourSchemes()
1091 ColourSchemeI cs = globalColourScheme;
1094 cs.alignmentChanged(alignment);
1095 // TODO: fold all recalc events for clustalX into alignmentChanged
1096 if (cs instanceof ClustalxColourScheme)
1098 ((ClustalxColourScheme) cs).resetClustalX(alignment.getSequences(),
1099 alignment.getWidth());
1102 cs.setConsensus(hconsensus);
1103 if (cs.conservationApplied())
1105 cs.setConservation(Conservation.calculateConservation("All",
1106 ResidueProperties.propHash, 3, alignment.getSequences(), 0,
1107 alignment.getWidth(), false, getConsPercGaps(), false));
1111 int s, sSize = alignment.getGroups().size();
1112 for (s = 0; s < sSize; s++)
1114 SequenceGroup sg = (SequenceGroup) alignment.getGroups().elementAt(s);
1115 if (sg.cs != null && sg.cs instanceof ClustalxColourScheme)
1117 ((ClustalxColourScheme) sg.cs).resetClustalX(sg
1118 .getSequences(hiddenRepSequences), sg.getWidth());
1120 sg.recalcConservation();
1124 protected void initAutoAnnotation()
1126 // TODO: add menu option action that nulls or creates consensus object
1127 // depending on if the user wants to see the annotation or not in a
1128 // specific alignment
1130 if (hconsensus == null && !isDataset)
1132 if (!alignment.isNucleotide())
1134 if (showConservation)
1136 if (conservation == null)
1138 conservation = new AlignmentAnnotation("Conservation",
1139 "Conservation of total alignment less than "
1140 + getConsPercGaps() + "% gaps",
1141 new Annotation[1], 0f, 11f,
1142 AlignmentAnnotation.BAR_GRAPH);
1143 conservation.hasText = true;
1144 conservation.autoCalculated = true;
1145 alignment.addAnnotation(conservation);
1150 if (quality == null)
1152 quality = new AlignmentAnnotation("Quality",
1153 "Alignment Quality based on Blosum62 scores",
1154 new Annotation[1], 0f, 11f,
1155 AlignmentAnnotation.BAR_GRAPH);
1156 quality.hasText = true;
1157 quality.autoCalculated = true;
1158 alignment.addAnnotation(quality);
1164 if (alignment.hasRNAStructure())
1166 strucConsensus = new AlignmentAnnotation("StrucConsensus", "PID",
1167 new Annotation[1], 0f, 100f,
1168 AlignmentAnnotation.BAR_GRAPH);
1169 strucConsensus.hasText = true;
1170 strucConsensus.autoCalculated = true;
1174 consensus = new AlignmentAnnotation("Consensus", "PID",
1175 new Annotation[1], 0f, 100f, AlignmentAnnotation.BAR_GRAPH);
1176 consensus.hasText = true;
1177 consensus.autoCalculated = true;
1181 alignment.addAnnotation(consensus);
1182 if (strucConsensus != null)
1184 alignment.addAnnotation(strucConsensus);