Copied PCA files to PaSiMap analogues and changed all names
authorMorellThomas <morellth@yahoo.co.jp>
Sun, 9 Jul 2023 16:41:58 +0000 (18:41 +0200)
committerMorellThomas <morellth@yahoo.co.jp>
Sun, 9 Jul 2023 16:41:58 +0000 (18:41 +0200)
resources/lang/Messages.properties
resources/lang/Messages_es.properties
src/jalview/analysis/PaSiMap.java [new file with mode: 0755]
src/jalview/gui/CalculationChooser.java
src/jalview/gui/PaSiMapPanel.java [new file with mode: 0644]
src/jalview/viewmodel/PaSiMapModel.java [new file with mode: 0644]

index 980e9bc..a6eee7e 100644 (file)
@@ -443,6 +443,7 @@ label.selection_output_command = Selection output - {0}
 label.annotation_for_displayid = <p><h2>Annotation for {0} </h2></p><p>
 label.pdb_sequence_mapping = PDB - Sequence Mapping
 label.pca_details = PCA details
+label.pasimap_details = PaSiMap details
 label.redundancy_threshold_selection = Redundancy threshold selection
 label.user_defined_colours = User defined colours
 label.jalviewLite_release = JalviewLite - Release {0}
@@ -1004,6 +1005,8 @@ label.add_new_sbrs_service = Add a new Simple Bioinformatics Rest Service
 label.edit_sbrs_entry = Edit Simple Bioinformatics Rest Service entry
 label.pca_recalculating = Recalculating PCA
 label.pca_calculating = Calculating PCA
+label.pasimap_recalculating = Recalculating PaSiMap
+label.pasimap_calculating = Calculating PaSiMap
 label.select_foreground_colour = Choose foreground colour
 label.select_colour_for_text = Select Colour for Text
 label.adjust_foreground_text_colour_threshold = Adjust Foreground Text Colour Threshold
@@ -1330,6 +1333,7 @@ label.annotation_description = Annotation Description
 label.edit_annotation_name_description = Edit Annotation Name/Description
 label.alignment = alignment
 label.pca = PCA
+label.pasimap = PaSiMap
 label.create_image_of = Create {0} image of {1}
 label.click_to_edit = Click to edit, right-click for menu
 label.backupfiles_confirm_delete = Confirm delete
index 547d95b..74b3674 100644 (file)
@@ -922,6 +922,8 @@ label.add_new_sbrs_service = A
 label.edit_sbrs_entry = Editar entrada SBRS
 label.pca_recalculating = Recalculando ACP
 label.pca_calculating = Calculando ACP
+label.pasimap_recalculating = Recalculando PaSiMap
+label.pasimap_calculating = Calculando PaSiMap
 label.select_foreground_colour = Escoger color del primer plano
 label.select_colour_for_text = Seleccione el color del texto
 label.adjust_foreground_text_colour_threshold = Ajustar el umbral del color del texto en primer plano
@@ -1320,6 +1322,7 @@ label.annotation_description = Descripci
 label.edit_annotation_name_description = Editar el nombre/descripción de la anotación
 label.alignment = alineamiento
 label.pca = ACP
+label.pasimap = PaSiMap
 label.create_image_of = Crear imagen {0} de {1}
 label.click_to_edit = Haga clic para editar, clic en el botón derecho para ver el menú  
 label.backupfiles_confirm_delete = Confirmar borrar
diff --git a/src/jalview/analysis/PaSiMap.java b/src/jalview/analysis/PaSiMap.java
new file mode 100755 (executable)
index 0000000..7714be4
--- /dev/null
@@ -0,0 +1,302 @@
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ * 
+ * This file is part of Jalview.
+ * 
+ * Jalview 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 3
+ * of the License, or (at your option) any later version.
+ *  
+ * Jalview 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 Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
+package jalview.analysis;
+
+import jalview.api.analysis.ScoreModelI;
+import jalview.api.analysis.SimilarityParamsI;
+import jalview.bin.Console;
+import jalview.datamodel.AlignmentView;
+import jalview.datamodel.Point;
+import jalview.math.MatrixI;
+
+import java.io.PrintStream;
+
+/**
+ * Performs Principal Component Analysis on given sequences
+ */
+public class PaSiMap implements Runnable
+{
+  /*
+   * inputs
+   */
+  final private AlignmentView seqs;
+
+  final private ScoreModelI scoreModel;
+
+  final private SimilarityParamsI similarityParams;
+
+  /*
+   * outputs
+   */
+  private MatrixI pairwiseScores;
+
+  private MatrixI tridiagonal;
+
+  private MatrixI eigenMatrix;
+
+  /**
+   * Constructor given the sequences to compute for, the similarity model to
+   * use, and a set of parameters for sequence comparison
+   * 
+   * @param sequences
+   * @param sm
+   * @param options
+   */
+  public PaSiMap(AlignmentView sequences, ScoreModelI sm,
+          SimilarityParamsI options)
+  {
+    this.seqs = sequences;
+    this.scoreModel = sm;
+    this.similarityParams = options;
+  }
+
+  /**
+   * Returns Eigenvalue
+   * 
+   * @param i
+   *          Index of diagonal within matrix
+   * 
+   * @return Returns value of diagonal from matrix
+   */
+  public double getEigenvalue(int i)
+  {
+    return eigenMatrix.getD()[i];
+  }
+
+  /**
+   * DOCUMENT ME!
+   * 
+   * @param l
+   *          DOCUMENT ME!
+   * @param n
+   *          DOCUMENT ME!
+   * @param mm
+   *          DOCUMENT ME!
+   * @param factor
+   *          DOCUMENT ME!
+   * 
+   * @return DOCUMENT ME!
+   */
+  public Point[] getComponents(int l, int n, int mm, float factor)
+  {
+    Point[] out = new Point[getHeight()];
+
+    for (int i = 0; i < getHeight(); i++)
+    {
+      float x = (float) component(i, l) * factor;
+      float y = (float) component(i, n) * factor;
+      float z = (float) component(i, mm) * factor;
+      out[i] = new Point(x, y, z);
+    }
+
+    return out;
+  }
+
+  /**
+   * DOCUMENT ME!
+   * 
+   * @param n
+   *          DOCUMENT ME!
+   * 
+   * @return DOCUMENT ME!
+   */
+  public double[] component(int n)
+  {
+    // n = index of eigenvector
+    double[] out = new double[getHeight()];
+
+    for (int i = 0; i < out.length; i++)
+    {
+      out[i] = component(i, n);
+    }
+
+    return out;
+  }
+
+  /**
+   * DOCUMENT ME!
+   * 
+   * @param row
+   *          DOCUMENT ME!
+   * @param n
+   *          DOCUMENT ME!
+   * 
+   * @return DOCUMENT ME!
+   */
+  double component(int row, int n)
+  {
+    double out = 0.0;
+
+    for (int i = 0; i < pairwiseScores.width(); i++)
+    {
+      out += (pairwiseScores.getValue(row, i) * eigenMatrix.getValue(i, n));
+    }
+
+    return out / eigenMatrix.getD()[n];
+  }
+
+  /**
+   * Answers a formatted text report of the PaSiMap calculation results (matrices
+   * and eigenvalues) suitable for display
+   * 
+   * @return
+   */
+  public String getDetails()
+  {
+    StringBuilder sb = new StringBuilder(1024);
+    sb.append("PaSiMap calculation using ").append(scoreModel.getName())
+            .append(" sequence similarity matrix\n========\n\n");
+    PrintStream ps = wrapOutputBuffer(sb);
+
+    /*
+     * pairwise similarity scores
+     */
+    sb.append(" --- OrigT * Orig ---- \n");
+    pairwiseScores.print(ps, "%8.2f");
+
+    /*
+     * tridiagonal matrix, with D and E vectors
+     */
+    sb.append(" ---Tridiag transform matrix ---\n");
+    sb.append(" --- D vector ---\n");
+    tridiagonal.printD(ps, "%15.4e");
+    ps.println();
+    sb.append("--- E vector ---\n");
+    tridiagonal.printE(ps, "%15.4e");
+    ps.println();
+
+    /*
+     * eigenvalues matrix, with D vector
+     */
+    sb.append(" --- New diagonalization matrix ---\n");
+    eigenMatrix.print(ps, "%8.2f");
+    sb.append(" --- Eigenvalues ---\n");
+    eigenMatrix.printD(ps, "%15.4e");
+    ps.println();
+
+    return sb.toString();
+  }
+
+  /**
+   * Performs the PaSiMap calculation
+   */
+  @Override
+  public void run()
+  {
+    try
+    {
+      /*
+       * sequence pairwise similarity scores
+       */
+      pairwiseScores = scoreModel.findSimilarities(seqs, similarityParams);
+
+      /*
+       * tridiagonal matrix
+       */
+      tridiagonal = pairwiseScores.copy();
+      tridiagonal.tred();
+
+      /*
+       * the diagonalization matrix
+       */
+      eigenMatrix = tridiagonal.copy();
+      eigenMatrix.tqli();
+    } catch (Exception q)
+    {
+      Console.error("Error computing PaSiMap:  " + q.getMessage());
+      q.printStackTrace();
+    }
+  }
+
+  /**
+   * Returns a PrintStream that wraps (appends its output to) the given
+   * StringBuilder
+   * 
+   * @param sb
+   * @return
+   */
+  protected PrintStream wrapOutputBuffer(StringBuilder sb)
+  {
+    PrintStream ps = new PrintStream(System.out)
+    {
+      @Override
+      public void print(String x)
+      {
+        sb.append(x);
+      }
+
+      @Override
+      public void println()
+      {
+        sb.append("\n");
+      }
+    };
+    return ps;
+  }
+
+  /**
+   * Answers the N dimensions of the NxN PaSiMap matrix. This is the number of
+   * sequences involved in the pairwise score calculation.
+   * 
+   * @return
+   */
+  public int getHeight()
+  {
+    // TODO can any of seqs[] be null?
+    return pairwiseScores.height();// seqs.getSequences().length;
+  }
+
+  /**
+   * Answers the sequence pairwise similarity scores which were the first step
+   * of the PaSiMap calculation
+   * 
+   * @return
+   */
+  public MatrixI getPairwiseScores()
+  {
+    return pairwiseScores;
+  }
+
+  public void setPairwiseScores(MatrixI m)
+  {
+    pairwiseScores = m;
+  }
+
+  public MatrixI getEigenmatrix()
+  {
+    return eigenMatrix;
+  }
+
+  public void setEigenmatrix(MatrixI m)
+  {
+    eigenMatrix = m;
+  }
+
+  public MatrixI getTridiagonal()
+  {
+    return tridiagonal;
+  }
+
+  public void setTridiagonal(MatrixI tridiagonal)
+  {
+    this.tridiagonal = tridiagonal;
+  }
+}
index 0f6c257..9e596e2 100644 (file)
@@ -115,8 +115,8 @@ public class CalculationChooser extends JPanel
    */
   private PCAPanel pcaPanel;
 
-  //&! change to PaSiMapPanel
-  private PCAPanel pasimapPanel;
+  //&! 
+  private PaSiMapPanel pasimapPanel;
 
   /**
    * Constructor
@@ -657,7 +657,7 @@ public class CalculationChooser extends JPanel
      * construct the panel and kick off its calculation thread
      */
     //&! change to PaSiMapPanel
-    pasimapPanel = new PCAPanel(af.alignPanel, modelName, params);
+    pasimapPanel = new PaSiMapPanel(af.alignPanel, modelName, params);
     new Thread(pasimapPanel).start();
 
   }
diff --git a/src/jalview/gui/PaSiMapPanel.java b/src/jalview/gui/PaSiMapPanel.java
new file mode 100644 (file)
index 0000000..e394781
--- /dev/null
@@ -0,0 +1,772 @@
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ * 
+ * This file is part of Jalview.
+ * 
+ * Jalview 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 3
+ * of the License, or (at your option) any later version.
+ *  
+ * Jalview 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 Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
+package jalview.gui;
+
+import jalview.analysis.scoremodels.ScoreModels;
+import jalview.api.AlignViewportI;
+import jalview.api.analysis.ScoreModelI;
+import jalview.api.analysis.SimilarityParamsI;
+import jalview.bin.Console;
+import jalview.datamodel.Alignment;
+import jalview.datamodel.AlignmentI;
+import jalview.datamodel.AlignmentView;
+import jalview.datamodel.HiddenColumns;
+import jalview.datamodel.SequenceI;
+import jalview.gui.ImageExporter.ImageWriterI;
+import jalview.gui.JalviewColourChooser.ColourChooserListener;
+import jalview.jbgui.GPCAPanel;
+import jalview.math.RotatableMatrix.Axis;
+import jalview.util.ImageMaker;
+import jalview.util.MessageManager;
+import jalview.viewmodel.AlignmentViewport;
+import jalview.viewmodel.PaSiMapModel;
+
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.Graphics;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.print.PageFormat;
+import java.awt.print.Printable;
+import java.awt.print.PrinterException;
+import java.awt.print.PrinterJob;
+
+import javax.swing.ButtonGroup;
+import javax.swing.JMenuItem;
+import javax.swing.JRadioButtonMenuItem;
+import javax.swing.event.InternalFrameAdapter;
+import javax.swing.event.InternalFrameEvent;
+
+/**
+ * The panel holding the Pairwise Similarity Map 3-D visualisation
+ */
+public class PaSiMapPanel extends GPCAPanel
+        implements Runnable, IProgressIndicator
+{
+  private static final int MIN_WIDTH = 470;
+
+  private static final int MIN_HEIGHT = 250;
+
+  private RotatableCanvas rc;
+
+  AlignmentPanel ap;
+
+  AlignmentViewport av;
+
+  private PaSiMapModel pasimapModel;
+
+  private int top = 0;
+
+  private IProgressIndicator progressBar;
+
+  private boolean working;
+
+  /**
+   * Constructor given sequence data, a similarity (or distance) score model
+   * name, and score calculation parameters
+   * 
+   * @param alignPanel
+   * @param modelName
+   * @param params
+   */
+  public PaSiMapPanel(AlignmentPanel alignPanel, String modelName,
+          SimilarityParamsI params)
+  {
+    super();
+    this.av = alignPanel.av;
+    this.ap = alignPanel;
+    boolean nucleotide = av.getAlignment().isNucleotide();
+
+    progressBar = new ProgressBar(statusPanel, statusBar);
+
+    addInternalFrameListener(new InternalFrameAdapter()
+    {
+      @Override
+      public void internalFrameClosed(InternalFrameEvent e)
+      {
+        close_actionPerformed();
+      }
+    });
+
+    boolean selected = av.getSelectionGroup() != null
+            && av.getSelectionGroup().getSize() > 0;
+    AlignmentView seqstrings = av.getAlignmentView(selected);
+    SequenceI[] seqs;
+    if (!selected)
+    {
+      seqs = av.getAlignment().getSequencesArray();
+    }
+    else
+    {
+      seqs = av.getSelectionGroup().getSequencesInOrder(av.getAlignment());
+    }
+
+    ScoreModelI scoreModel = ScoreModels.getInstance()
+            .getScoreModel(modelName, ap);
+    setPasimapModel(
+            new PaSiMapModel(seqstrings, seqs, nucleotide, scoreModel, params));
+    PaintRefresher.Register(this, av.getSequenceSetId());
+
+    setRotatableCanvas(new RotatableCanvas(alignPanel));
+    this.getContentPane().add(getRotatableCanvas(), BorderLayout.CENTER);
+
+    addKeyListener(getRotatableCanvas());
+    validate();
+  }
+
+  /**
+   * Ensure references to potentially very large objects (the PaSiMap matrices) are
+   * nulled when the frame is closed
+   */
+  protected void close_actionPerformed()
+  {
+    setPasimapModel(null);
+    if (this.rc != null)
+    {
+      this.rc.sequencePoints = null;
+      this.rc.setAxisEndPoints(null);
+      this.rc = null;
+    }
+  }
+
+  @Override
+  protected void bgcolour_actionPerformed()
+  {
+    String ttl = MessageManager.getString("label.select_background_colour");
+    ColourChooserListener listener = new ColourChooserListener()
+    {
+      @Override
+      public void colourSelected(Color c)
+      {
+        rc.setBgColour(c);
+        rc.repaint();
+      }
+    };
+    JalviewColourChooser.showColourChooser(this, ttl, rc.getBgColour(),
+            listener);
+  }
+
+  /**
+   * Calculates the PaSiMap and displays the results
+   */
+  @Override
+  public void run()
+  {
+    working = true;
+    long progId = System.currentTimeMillis();
+    IProgressIndicator progress = this;
+    String message = MessageManager.getString("label.pasimap_recalculating");
+    if (getParent() == null)
+    {
+      progress = ap.alignFrame;
+      message = MessageManager.getString("label.pasimap_calculating");
+    }
+    progress.setProgressBar(message, progId);
+    try
+    {
+      getPasimapModel().calculate();
+
+      xCombobox.setSelectedIndex(0);
+      yCombobox.setSelectedIndex(1);
+      zCombobox.setSelectedIndex(2);
+
+      getPasimapModel().updateRc(getRotatableCanvas());
+      // rc.invalidate();
+      setTop(getPasimapModel().getTop());
+
+    } catch (OutOfMemoryError er)
+    {
+      new OOMWarning("calculating PaSiMap", er);
+      working = false;
+      return;
+    } finally
+    {
+      progress.setProgressBar("", progId);
+    }
+
+    repaint();
+    if (getParent() == null)
+    {
+      Desktop.addInternalFrame(this,
+              MessageManager.formatMessage("label.calc_title", "PaSiMap",
+                      getPasimapModel().getScoreModelName()),
+              475, 450);
+      this.setMinimumSize(new Dimension(MIN_WIDTH, MIN_HEIGHT));
+    }
+    working = false;
+  }
+
+  /**
+   * Updates the PaSiMap display after a change of component to use for x, y or z
+   * axis
+   */
+  @Override
+  protected void doDimensionChange()
+  {
+    if (getTop() == 0)
+    {
+      return;
+    }
+
+    int dim1 = getTop() - xCombobox.getSelectedIndex();
+    int dim2 = getTop() - yCombobox.getSelectedIndex();
+    int dim3 = getTop() - zCombobox.getSelectedIndex();
+    getPasimapModel().updateRcView(dim1, dim2, dim3);
+    getRotatableCanvas().resetView();
+  }
+
+  /**
+   * Sets the selected checkbox item index for PaSiMap dimension (1, 2, 3...) for
+   * the given axis (X/Y/Z)
+   * 
+   * @param index
+   * @param axis
+   */
+  public void setSelectedDimensionIndex(int index, Axis axis)
+  {
+    switch (axis)
+    {
+    case X:
+      xCombobox.setSelectedIndex(index);
+      break;
+    case Y:
+      yCombobox.setSelectedIndex(index);
+      break;
+    case Z:
+      zCombobox.setSelectedIndex(index);
+      break;
+    default:
+    }
+  }
+
+  @Override
+  protected void outputValues_actionPerformed()
+  {
+    CutAndPasteTransfer cap = new CutAndPasteTransfer();
+    try
+    {
+      cap.setText(getPasimapModel().getDetails());
+      Desktop.addInternalFrame(cap,
+              MessageManager.getString("label.pasimap_details"), 500, 500);
+    } catch (OutOfMemoryError oom)
+    {
+      new OOMWarning("opening PaSiMap details", oom);
+      cap.dispose();
+    }
+  }
+
+  @Override
+  protected void showLabels_actionPerformed()
+  {
+    getRotatableCanvas().showLabels(showLabels.getState());
+  }
+
+  @Override
+  protected void print_actionPerformed()
+  {
+    PaSiMapPrinter printer = new PaSiMapPrinter();
+    printer.start();
+  }
+
+  /**
+   * If available, shows the data which formed the inputs for the PaSiMap as a new
+   * alignment
+   */
+  @Override
+  public void originalSeqData_actionPerformed()
+  {
+    // JAL-2647 disabled after load from project (until save to project done)
+    if (getPasimapModel().getInputData() == null)
+    {
+      Console.info(
+              "Unexpected call to originalSeqData_actionPerformed - should have hidden this menu action.");
+      return;
+    }
+    // decide if av alignment is sufficiently different to original data to
+    // warrant a new window to be created
+    // create new alignment window with hidden regions (unhiding hidden regions
+    // yields unaligned seqs)
+    // or create a selection box around columns in alignment view
+    // test Alignment(SeqCigar[])
+    char gc = '-';
+    try
+    {
+      // we try to get the associated view's gap character
+      // but this may fail if the view was closed...
+      gc = av.getGapCharacter();
+    } catch (Exception ex)
+    {
+    }
+
+    Object[] alAndColsel = getPasimapModel().getInputData()
+            .getAlignmentAndHiddenColumns(gc);
+
+    if (alAndColsel != null && alAndColsel[0] != null)
+    {
+      // AlignmentOrder origorder = new AlignmentOrder(alAndColsel[0]);
+
+      AlignmentI al = new Alignment((SequenceI[]) alAndColsel[0]);
+      AlignmentI dataset = (av != null && av.getAlignment() != null)
+              ? av.getAlignment().getDataset()
+              : null;
+      if (dataset != null)
+      {
+        al.setDataset(dataset);
+      }
+
+      if (true)
+      {
+        // make a new frame!
+        AlignFrame af = new AlignFrame(al, (HiddenColumns) alAndColsel[1],
+                AlignFrame.DEFAULT_WIDTH, AlignFrame.DEFAULT_HEIGHT);
+
+        // >>>This is a fix for the moment, until a better solution is
+        // found!!<<<
+        // af.getFeatureRenderer().transferSettings(alignFrame.getFeatureRenderer());
+
+        // af.addSortByOrderMenuItem(ServiceName + " Ordering",
+        // msaorder);
+
+        Desktop.addInternalFrame(af, MessageManager.formatMessage(
+                "label.original_data_for_params", new String[]
+                { this.title }), AlignFrame.DEFAULT_WIDTH,
+                AlignFrame.DEFAULT_HEIGHT);
+      }
+    }
+    /*
+     * CutAndPasteTransfer cap = new CutAndPasteTransfer(); for (int i = 0; i <
+     * seqs.length; i++) { cap.appendText(new jalview.util.Format("%-" + 15 +
+     * "s").form( seqs[i].getName())); cap.appendText(" " + seqstrings[i] +
+     * "\n"); }
+     * 
+     * Desktop.addInternalFrame(cap, "Original Data", 400, 400);
+     */
+  }
+
+  class PaSiMapPrinter extends Thread implements Printable
+  {
+    @Override
+    public void run()
+    {
+      PrinterJob printJob = PrinterJob.getPrinterJob();
+      PageFormat defaultPage = printJob.defaultPage();
+      PageFormat pf = printJob.pageDialog(defaultPage);
+
+      if (defaultPage == pf)
+      {
+        /*
+         * user cancelled
+         */
+        return;
+      }
+
+      printJob.setPrintable(this, pf);
+
+      if (printJob.printDialog())
+      {
+        try
+        {
+          printJob.print();
+        } catch (Exception PrintException)
+        {
+          PrintException.printStackTrace();
+        }
+      }
+    }
+
+    @Override
+    public int print(Graphics pg, PageFormat pf, int pi)
+            throws PrinterException
+    {
+      pg.translate((int) pf.getImageableX(), (int) pf.getImageableY());
+
+      getRotatableCanvas().drawBackground(pg);
+      getRotatableCanvas().drawScene(pg);
+      if (getRotatableCanvas().drawAxes)
+      {
+        getRotatableCanvas().drawAxes(pg);
+      }
+
+      if (pi == 0)
+      {
+        return Printable.PAGE_EXISTS;
+      }
+      else
+      {
+        return Printable.NO_SUCH_PAGE;
+      }
+    }
+  }
+
+  public void makePaSiMapImage(ImageMaker.TYPE type)
+  {
+    int width = getRotatableCanvas().getWidth();
+    int height = getRotatableCanvas().getHeight();
+    ImageWriterI writer = new ImageWriterI()
+    {
+      @Override
+      public void exportImage(Graphics g) throws Exception
+      {
+        RotatableCanvas canvas = getRotatableCanvas();
+        canvas.drawBackground(g);
+        canvas.drawScene(g);
+        if (canvas.drawAxes)
+        {
+          canvas.drawAxes(g);
+        }
+      }
+    };
+    String pasimap = MessageManager.getString("label.pasimap");
+    ImageExporter exporter = new ImageExporter(writer, null, type, pasimap);
+    exporter.doExport(null, this, width, height, pasimap);
+  }
+
+  @Override
+  protected void viewMenu_menuSelected()
+  {
+    buildAssociatedViewMenu();
+  }
+
+  /**
+   * Builds the menu showing the choice of possible views (for the associated
+   * sequence data) to which the PaSiMap may be linked
+   */
+  void buildAssociatedViewMenu()
+  {
+    AlignmentPanel[] aps = PaintRefresher
+            .getAssociatedPanels(av.getSequenceSetId());
+    if (aps.length == 1 && getRotatableCanvas().av == aps[0].av)
+    {
+      associateViewsMenu.setVisible(false);
+      return;
+    }
+
+    associateViewsMenu.setVisible(true);
+
+    if ((viewMenu
+            .getItem(viewMenu.getItemCount() - 2) instanceof JMenuItem))
+    {
+      viewMenu.insertSeparator(viewMenu.getItemCount() - 1);
+    }
+
+    associateViewsMenu.removeAll();
+
+    JRadioButtonMenuItem item;
+    ButtonGroup buttonGroup = new ButtonGroup();
+    int iSize = aps.length;
+
+    for (int i = 0; i < iSize; i++)
+    {
+      final AlignmentPanel panel = aps[i];
+      item = new JRadioButtonMenuItem(panel.av.getViewName(),
+              panel.av == getRotatableCanvas().av);
+      buttonGroup.add(item);
+      item.addActionListener(new ActionListener()
+      {
+        @Override
+        public void actionPerformed(ActionEvent evt)
+        {
+          selectAssociatedView(panel);
+        }
+      });
+
+      associateViewsMenu.add(item);
+    }
+
+    final JRadioButtonMenuItem itemf = new JRadioButtonMenuItem(
+            "All Views");
+
+    buttonGroup.add(itemf);
+
+    itemf.setSelected(getRotatableCanvas().isApplyToAllViews());
+    itemf.addActionListener(new ActionListener()
+    {
+      @Override
+      public void actionPerformed(ActionEvent evt)
+      {
+        getRotatableCanvas().setApplyToAllViews(itemf.isSelected());
+      }
+    });
+    associateViewsMenu.add(itemf);
+
+  }
+
+  /*
+   * (non-Javadoc)
+   * 
+   * @see
+   * jalview.jbgui.GPCAPanel#outputPoints_actionPerformed(java.awt.event.ActionEvent
+   * )
+   */
+  @Override
+  protected void outputPoints_actionPerformed()
+  {
+    CutAndPasteTransfer cap = new CutAndPasteTransfer();
+    try
+    {
+      cap.setText(getPasimapModel().getPointsasCsv(false,
+              xCombobox.getSelectedIndex(), yCombobox.getSelectedIndex(),
+              zCombobox.getSelectedIndex()));
+      Desktop.addInternalFrame(cap, MessageManager
+              .formatMessage("label.points_for_params", new String[]
+              { this.getTitle() }), 500, 500);
+    } catch (OutOfMemoryError oom)
+    {
+      new OOMWarning("exporting PaSiMap points", oom);
+      cap.dispose();
+    }
+  }
+
+  /*
+   * (non-Javadoc)
+   * 
+   * @see
+   * jalview.jbgui.GPCAPanel#outputProjPoints_actionPerformed(java.awt.event
+   * .ActionEvent)
+   */
+  @Override
+  protected void outputProjPoints_actionPerformed()
+  {
+    CutAndPasteTransfer cap = new CutAndPasteTransfer();
+    try
+    {
+      cap.setText(getPasimapModel().getPointsasCsv(true,
+              xCombobox.getSelectedIndex(), yCombobox.getSelectedIndex(),
+              zCombobox.getSelectedIndex()));
+      Desktop.addInternalFrame(cap, MessageManager.formatMessage(
+              "label.transformed_points_for_params", new String[]
+              { this.getTitle() }), 500, 500);
+    } catch (OutOfMemoryError oom)
+    {
+      new OOMWarning("exporting transformed PaSiMap points", oom);
+      cap.dispose();
+    }
+  }
+
+  /*
+   * (non-Javadoc)
+   * 
+   * @see jalview.gui.IProgressIndicator#setProgressBar(java.lang.String, long)
+   */
+  @Override
+  public void setProgressBar(String message, long id)
+  {
+    progressBar.setProgressBar(message, id);
+    // if (progressBars == null)
+    // {
+    // progressBars = new Hashtable();
+    // progressBarHandlers = new Hashtable();
+    // }
+    //
+    // JPanel progressPanel;
+    // Long lId = Long.valueOf(id);
+    // GridLayout layout = (GridLayout) statusPanel.getLayout();
+    // if (progressBars.get(lId) != null)
+    // {
+    // progressPanel = (JPanel) progressBars.get(Long.valueOf(id));
+    // statusPanel.remove(progressPanel);
+    // progressBars.remove(lId);
+    // progressPanel = null;
+    // if (message != null)
+    // {
+    // statusBar.setText(message);
+    // }
+    // if (progressBarHandlers.contains(lId))
+    // {
+    // progressBarHandlers.remove(lId);
+    // }
+    // layout.setRows(layout.getRows() - 1);
+    // }
+    // else
+    // {
+    // progressPanel = new JPanel(new BorderLayout(10, 5));
+    //
+    // JProgressBar progressBar = new JProgressBar();
+    // progressBar.setIndeterminate(true);
+    //
+    // progressPanel.add(new JLabel(message), BorderLayout.WEST);
+    // progressPanel.add(progressBar, BorderLayout.CENTER);
+    //
+    // layout.setRows(layout.getRows() + 1);
+    // statusPanel.add(progressPanel);
+    //
+    // progressBars.put(lId, progressPanel);
+    // }
+    // // update GUI
+    // // setMenusForViewport();
+    // validate();
+  }
+
+  @Override
+  public void registerHandler(final long id,
+          final IProgressIndicatorHandler handler)
+  {
+    progressBar.registerHandler(id, handler);
+    // if (progressBarHandlers == null ||
+    // !progressBars.contains(Long.valueOf(id)))
+    // {
+    // throw new
+    // Error(MessageManager.getString("error.call_setprogressbar_before_registering_handler"));
+    // }
+    // progressBarHandlers.put(Long.valueOf(id), handler);
+    // final JPanel progressPanel = (JPanel) progressBars.get(Long.valueOf(id));
+    // if (handler.canCancel())
+    // {
+    // JButton cancel = new JButton(
+    // MessageManager.getString("action.cancel"));
+    // final IProgressIndicator us = this;
+    // cancel.addActionListener(new ActionListener()
+    // {
+    //
+    // @Override
+    // public void actionPerformed(ActionEvent e)
+    // {
+    // handler.cancelActivity(id);
+    // us.setProgressBar(MessageManager.formatMessage("label.cancelled_params",
+    // new String[]{((JLabel) progressPanel.getComponent(0)).getText()}), id);
+    // }
+    // });
+    // progressPanel.add(cancel, BorderLayout.EAST);
+    // }
+  }
+
+  /**
+   * 
+   * @return true if any progress bars are still active
+   */
+  @Override
+  public boolean operationInProgress()
+  {
+    return progressBar.operationInProgress();
+  }
+
+  @Override
+  protected void resetButton_actionPerformed()
+  {
+    int t = getTop();
+    setTop(0); // ugly - prevents dimensionChanged events from being processed
+    xCombobox.setSelectedIndex(0);
+    yCombobox.setSelectedIndex(1);
+    setTop(t);
+    zCombobox.setSelectedIndex(2);
+  }
+
+  /**
+   * Answers true if PaSiMap calculation is in progress, else false
+   * 
+   * @return
+   */
+  public boolean isWorking()
+  {
+    return working;
+  }
+
+  /**
+   * Answers the selected checkbox item index for PaSiMap dimension for the X, Y or
+   * Z axis of the display
+   * 
+   * @param axis
+   * @return
+   */
+  public int getSelectedDimensionIndex(Axis axis)
+  {
+    switch (axis)
+    {
+    case X:
+      return xCombobox.getSelectedIndex();
+    case Y:
+      return yCombobox.getSelectedIndex();
+    default:
+      return zCombobox.getSelectedIndex();
+    }
+  }
+
+  public void setShowLabels(boolean show)
+  {
+    showLabels.setSelected(show);
+  }
+
+  /**
+   * Sets the input data used to calculate the PaSiMap. This is provided for
+   * 'restore from project', which does not currently support this (AL-2647), so
+   * sets the value to null, and hides the menu option for "Input Data...". J
+   * 
+   * @param data
+   */
+  public void setInputData(AlignmentView data)
+  {
+    getPasimapModel().setInputData(data);
+    originalSeqData.setVisible(data != null);
+  }
+
+  public AlignViewportI getAlignViewport()
+  {
+    return av;
+  }
+
+  public PaSiMapModel getPasimapModel()
+  {
+    return pasimapModel;
+  }
+
+  public void setPasimapModel(PaSiMapModel pasimapModel)
+  {
+    this.pasimapModel = pasimapModel;
+  }
+
+  public RotatableCanvas getRotatableCanvas()
+  {
+    return rc;
+  }
+
+  public void setRotatableCanvas(RotatableCanvas rc)
+  {
+    this.rc = rc;
+  }
+
+  public int getTop()
+  {
+    return top;
+  }
+
+  public void setTop(int top)
+  {
+    this.top = top;
+  }
+
+  /**
+   * set the associated view for this PaSiMap.
+   * 
+   * @param panel
+   */
+  public void selectAssociatedView(AlignmentPanel panel)
+  {
+    getRotatableCanvas().setApplyToAllViews(false);
+
+    ap = panel;
+    av = panel.av;
+
+    getRotatableCanvas().av = panel.av;
+    getRotatableCanvas().ap = panel;
+    PaintRefresher.Register(PaSiMapPanel.this, panel.av.getSequenceSetId());
+  }
+}
diff --git a/src/jalview/viewmodel/PaSiMapModel.java b/src/jalview/viewmodel/PaSiMapModel.java
new file mode 100644 (file)
index 0000000..94485a3
--- /dev/null
@@ -0,0 +1,271 @@
+/*
+ * Jalview - A Sequence Alignment Editor and Viewer ($$Version-Rel$$)
+ * Copyright (C) $$Year-Rel$$ The Jalview Authors
+ * 
+ * This file is part of Jalview.
+ * 
+ * Jalview 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 3
+ * of the License, or (at your option) any later version.
+ *  
+ * Jalview 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 Jalview.  If not, see <http://www.gnu.org/licenses/>.
+ * The Jalview Authors are detailed in the 'AUTHORS' file.
+ */
+package jalview.viewmodel;
+
+import jalview.analysis.PaSiMap;
+import jalview.api.RotatableCanvasI;
+import jalview.api.analysis.ScoreModelI;
+import jalview.api.analysis.SimilarityParamsI;
+import jalview.datamodel.AlignmentView;
+import jalview.datamodel.Point;
+import jalview.datamodel.SequenceI;
+import jalview.datamodel.SequencePoint;
+
+import java.util.List;
+import java.util.Vector;
+
+public class PaSiMapModel
+{
+  /*
+   * inputs
+   */
+  private AlignmentView inputData;
+
+  private final SequenceI[] seqs;
+
+  private final SimilarityParamsI similarityParams;
+
+  /*
+   * options - score model, nucleotide / protein
+   */
+  private ScoreModelI scoreModel;
+
+  private boolean nucleotide = false;
+
+  /*
+   * outputs
+   */
+  private PaSiMap pasimap;
+
+  int top;
+
+  private List<SequencePoint> points;
+
+  /**
+   * Constructor given sequence data, score model and score calculation
+   * parameter options.
+   * 
+   * @param seqData
+   * @param sqs
+   * @param nuc
+   * @param modelName
+   * @param params
+   */
+  public PaSiMapModel(AlignmentView seqData, SequenceI[] sqs, boolean nuc,
+          ScoreModelI modelName, SimilarityParamsI params)
+  {
+    inputData = seqData;
+    seqs = sqs;
+    nucleotide = nuc;
+    scoreModel = modelName;
+    similarityParams = params;
+  }
+
+  /**
+   * Performs the PaSiMap calculation (in the same thread) and extracts result data
+   * needed for visualisation by PaSiMapPanel
+   */
+  public void calculate()
+  {
+    pasimap = new PaSiMap(inputData, scoreModel, similarityParams);
+    pasimap.run(); // executes in same thread, wait for completion
+
+    // Now find the component coordinates
+    int ii = 0;
+
+    while ((ii < seqs.length) && (seqs[ii] != null))
+    {
+      ii++;
+    }
+
+    int height = pasimap.getHeight();
+    // top = pasimap.getM().height() - 1;
+    top = height - 1;
+
+    points = new Vector<>();
+    Point[] scores = pasimap.getComponents(top - 1, top - 2, top - 3, 100);
+
+    for (int i = 0; i < height; i++)
+    {
+      SequencePoint sp = new SequencePoint(seqs[i], scores[i]);
+      points.add(sp);
+    }
+  }
+
+  public void updateRc(RotatableCanvasI rc)
+  {
+    rc.setPoints(points, pasimap.getHeight());
+  }
+
+  public boolean isNucleotide()
+  {
+    return nucleotide;
+  }
+
+  public void setNucleotide(boolean nucleotide)
+  {
+    this.nucleotide = nucleotide;
+  }
+
+  /**
+   * Answers the index of the principal dimension of the PaSiMap
+   * 
+   * @return
+   */
+  public int getTop()
+  {
+    return top;
+  }
+
+  public void setTop(int t)
+  {
+    top = t;
+  }
+
+  /**
+   * Updates the 3D coordinates for the list of points to the given dimensions.
+   * Principal dimension is getTop(). Next greatest eigenvector is getTop()-1.
+   * Note - pasimap.getComponents starts counting the spectrum from rank-2 to zero,
+   * rather than rank-1, so getComponents(dimN ...) == updateRcView(dimN+1 ..)
+   * 
+   * @param dim1
+   * @param dim2
+   * @param dim3
+   */
+  public void updateRcView(int dim1, int dim2, int dim3)
+  {
+    // note: actual indices for components are dim1-1, etc (patch for JAL-1123)
+    Point[] scores = pasimap.getComponents(dim1 - 1, dim2 - 1, dim3 - 1, 100);
+
+    for (int i = 0; i < pasimap.getHeight(); i++)
+    {
+      points.get(i).coord = scores[i];
+    }
+  }
+
+  public String getDetails()
+  {
+    return pasimap.getDetails();
+  }
+
+  public AlignmentView getInputData()
+  {
+    return inputData;
+  }
+
+  public void setInputData(AlignmentView data)
+  {
+    inputData = data;
+  }
+
+  public String getPointsasCsv(boolean transformed, int xdim, int ydim,
+          int zdim)
+  {
+    StringBuffer csv = new StringBuffer();
+    csv.append("\"Sequence\"");
+    if (transformed)
+    {
+      csv.append(",");
+      csv.append(xdim);
+      csv.append(",");
+      csv.append(ydim);
+      csv.append(",");
+      csv.append(zdim);
+    }
+    else
+    {
+      for (int d = 1, dmax = pasimap.component(1).length; d <= dmax; d++)
+      {
+        csv.append("," + d);
+      }
+    }
+    csv.append("\n");
+    for (int s = 0; s < seqs.length; s++)
+    {
+      csv.append("\"" + seqs[s].getName() + "\"");
+      double fl[];
+      if (!transformed)
+      {
+        // output pasimap in correct order
+        fl = pasimap.component(s);
+        for (int d = fl.length - 1; d >= 0; d--)
+        {
+          csv.append(",");
+          csv.append(fl[d]);
+        }
+      }
+      else
+      {
+        Point p = points.get(s).coord;
+        csv.append(",").append(p.x);
+        csv.append(",").append(p.y);
+        csv.append(",").append(p.z);
+      }
+      csv.append("\n");
+    }
+    return csv.toString();
+  }
+
+  public String getScoreModelName()
+  {
+    return scoreModel == null ? "" : scoreModel.getName();
+  }
+
+  public void setScoreModel(ScoreModelI sm)
+  {
+    this.scoreModel = sm;
+  }
+
+  /**
+   * Answers the parameters configured for pairwise similarity calculations
+   * 
+   * @return
+   */
+  public SimilarityParamsI getSimilarityParameters()
+  {
+    return similarityParams;
+  }
+
+  public List<SequencePoint> getSequencePoints()
+  {
+    return points;
+  }
+
+  public void setSequencePoints(List<SequencePoint> sp)
+  {
+    points = sp;
+  }
+
+  /**
+   * Answers the object holding the values of the computed PaSiMap
+   * 
+   * @return
+   */
+  public PaSiMap getPasimapData()
+  {
+    return pasimap;
+  }
+
+  public void setPaSiMap(PaSiMap data)
+  {
+    pasimap = data;
+  }
+}