/* * 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 . * The Jalview Authors are detailed in the 'AUTHORS' file. */ package jalview.viewmodel.seqfeatures; import jalview.api.AlignViewportI; import jalview.api.FeatureColourI; import jalview.api.FeaturesDisplayedI; import jalview.datamodel.AlignmentI; import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceI; import jalview.datamodel.features.SequenceFeatures; import jalview.renderer.seqfeatures.FeatureRenderer; import jalview.schemes.FeatureColour; import jalview.util.ColorUtils; import java.awt.Color; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.Hashtable; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; public abstract class FeatureRendererModel implements jalview.api.FeatureRenderer { /** * global transparency for feature */ protected float transparency = 1.0f; protected Map featureColours = new ConcurrentHashMap(); protected Map featureGroups = new ConcurrentHashMap(); protected String[] renderOrder; Map featureOrder = null; protected PropertyChangeSupport changeSupport = new PropertyChangeSupport( this); protected AlignViewportI av; @Override public AlignViewportI getViewport() { return av; } public FeatureRendererSettings getSettings() { return new FeatureRendererSettings(this); } public void transferSettings(FeatureRendererSettings fr) { this.renderOrder = fr.renderOrder; this.featureGroups = fr.featureGroups; this.featureColours = fr.featureColours; this.transparency = fr.transparency; this.featureOrder = fr.featureOrder; } /** * update from another feature renderer * * @param fr * settings to copy */ public void transferSettings(jalview.api.FeatureRenderer _fr) { FeatureRenderer fr = (FeatureRenderer) _fr; FeatureRendererSettings frs = new FeatureRendererSettings(fr); this.renderOrder = frs.renderOrder; this.featureGroups = frs.featureGroups; this.featureColours = frs.featureColours; this.transparency = frs.transparency; this.featureOrder = frs.featureOrder; if (av != null && av != fr.getViewport()) { // copy over the displayed feature settings if (_fr.getFeaturesDisplayed() != null) { FeaturesDisplayedI fd = getFeaturesDisplayed(); if (fd == null) { setFeaturesDisplayedFrom(_fr.getFeaturesDisplayed()); } else { synchronized (fd) { fd.clear(); for (String type : _fr.getFeaturesDisplayed() .getVisibleFeatures()) { fd.setVisible(type); } } } } } } public void setFeaturesDisplayedFrom(FeaturesDisplayedI featuresDisplayed) { av.setFeaturesDisplayed(new FeaturesDisplayed(featuresDisplayed)); } @Override public void setVisible(String featureType) { FeaturesDisplayedI fdi = av.getFeaturesDisplayed(); if (fdi == null) { av.setFeaturesDisplayed(fdi = new FeaturesDisplayed()); } if (!fdi.isRegistered(featureType)) { pushFeatureType(Arrays.asList(new String[] { featureType })); } fdi.setVisible(featureType); } @Override public void setAllVisible(List featureTypes) { FeaturesDisplayedI fdi = av.getFeaturesDisplayed(); if (fdi == null) { av.setFeaturesDisplayed(fdi = new FeaturesDisplayed()); } List nft = new ArrayList(); for (String featureType : featureTypes) { if (!fdi.isRegistered(featureType)) { nft.add(featureType); } } if (nft.size() > 0) { pushFeatureType(nft); } fdi.setAllVisible(featureTypes); } /** * push a set of new types onto the render order stack. Note - this is a * direct mechanism rather than the one employed in updateRenderOrder * * @param types */ private void pushFeatureType(List types) { int ts = types.size(); String neworder[] = new String[(renderOrder == null ? 0 : renderOrder.length) + ts]; types.toArray(neworder); if (renderOrder != null) { System.arraycopy(neworder, 0, neworder, renderOrder.length, ts); System.arraycopy(renderOrder, 0, neworder, 0, renderOrder.length); } renderOrder = neworder; } protected Map minmax = new Hashtable(); public Map getMinMax() { return minmax; } /** * normalise a score against the max/min bounds for the feature type. * * @param sequenceFeature * @return byte[] { signed, normalised signed (-127 to 127) or unsigned * (0-255) value. */ protected final byte[] normaliseScore(SequenceFeature sequenceFeature) { float[] mm = minmax.get(sequenceFeature.type)[0]; final byte[] r = new byte[] { 0, (byte) 255 }; if (mm != null) { if (r[0] != 0 || mm[0] < 0.0) { r[0] = 1; r[1] = (byte) ((int) 128.0 + 127.0 * (sequenceFeature.score / mm[1])); } else { r[1] = (byte) ((int) 255.0 * (sequenceFeature.score / mm[1])); } } return r; } boolean newFeatureAdded = false; boolean findingFeatures = false; protected boolean updateFeatures() { if (av.getFeaturesDisplayed() == null || renderOrder == null || newFeatureAdded) { findAllFeatures(); if (av.getFeaturesDisplayed().getVisibleFeatureCount() < 1) { return false; } } // TODO: decide if we should check for the visible feature count first return true; } /** * search the alignment for all new features, give them a colour and display * them. Then fires a PropertyChangeEvent on the changeSupport object. * */ protected void findAllFeatures() { synchronized (firing) { if (firing.equals(Boolean.FALSE)) { firing = Boolean.TRUE; findAllFeatures(true); // add all new features as visible changeSupport.firePropertyChange("changeSupport", null, null); firing = Boolean.FALSE; } } } @Override public List findFeaturesAtColumn(SequenceI sequence, int column) { /* * include features at the position provided their feature type is * displayed, and feature group is null or marked for display */ List result = new ArrayList(); if (!av.areFeaturesDisplayed() || getFeaturesDisplayed() == null) { return result; } Set visibleFeatures = getFeaturesDisplayed() .getVisibleFeatures(); String[] visibleTypes = visibleFeatures .toArray(new String[visibleFeatures.size()]); List features = sequence.findFeatures(column, column, visibleTypes); for (SequenceFeature sf : features) { if (!featureGroupNotShown(sf)) { result.add(sf); } } return result; } /** * Searches alignment for all features and updates colours * * @param newMadeVisible * if true newly added feature types will be rendered immediately * TODO: check to see if this method should actually be proxied so * repaint events can be propagated by the renderer code */ @Override public synchronized void findAllFeatures(boolean newMadeVisible) { newFeatureAdded = false; if (findingFeatures) { newFeatureAdded = true; return; } findingFeatures = true; if (av.getFeaturesDisplayed() == null) { av.setFeaturesDisplayed(new FeaturesDisplayed()); } FeaturesDisplayedI featuresDisplayed = av.getFeaturesDisplayed(); Set oldfeatures = new HashSet(); if (renderOrder != null) { for (int i = 0; i < renderOrder.length; i++) { if (renderOrder[i] != null) { oldfeatures.add(renderOrder[i]); } } } AlignmentI alignment = av.getAlignment(); List allfeatures = new ArrayList(); for (int i = 0; i < alignment.getHeight(); i++) { SequenceI asq = alignment.getSequenceAt(i); for (String group : asq.getFeatures().getFeatureGroups(true)) { boolean groupDisplayed = true; if (group != null) { if (featureGroups.containsKey(group)) { groupDisplayed = featureGroups.get(group); } else { groupDisplayed = newMadeVisible; featureGroups.put(group, groupDisplayed); } } if (groupDisplayed) { Set types = asq.getFeatures().getFeatureTypesForGroups( true, group); for (String type : types) { if (!allfeatures.contains(type)) // or use HashSet and no test? { allfeatures.add(type); } updateMinMax(asq, type, true); // todo: for all features? } } } } // uncomment to add new features in alphebetical order (but JAL-2575) // Collections.sort(allfeatures, String.CASE_INSENSITIVE_ORDER); if (newMadeVisible) { for (String type : allfeatures) { if (!oldfeatures.contains(type)) { featuresDisplayed.setVisible(type); setOrder(type, 0); } } } updateRenderOrder(allfeatures); findingFeatures = false; } /** * Updates the global (alignment) min and max values for a feature type from * the score for a sequence, if the score is not NaN. Values are stored * separately for positional and non-positional features. * * @param seq * @param featureType * @param positional */ protected void updateMinMax(SequenceI seq, String featureType, boolean positional) { float min = seq.getFeatures().getMinimumScore(featureType, positional); if (Float.isNaN(min)) { return; } float max = seq.getFeatures().getMaximumScore(featureType, positional); /* * stored values are * { {positionalMin, positionalMax}, {nonPositionalMin, nonPositionalMax} } */ if (minmax == null) { minmax = new Hashtable(); } synchronized (minmax) { float[][] mm = minmax.get(featureType); int index = positional ? 0 : 1; if (mm == null) { mm = new float[][] { null, null }; minmax.put(featureType, mm); } if (mm[index] == null) { mm[index] = new float[] { min, max }; } else { mm[index][0] = Math.min(mm[index][0], min); mm[index][1] = Math.max(mm[index][1], max); } } } protected Boolean firing = Boolean.FALSE; /** * replaces the current renderOrder with the unordered features in * allfeatures. The ordering of any types in both renderOrder and allfeatures * is preserved, and all new feature types are rendered on top of the existing * types, in the order given by getOrder or the order given in allFeatures. * Note. this operates directly on the featureOrder hash for efficiency. TODO: * eliminate the float storage for computing/recalling the persistent ordering * New Cability: updates min/max for colourscheme range if its dynamic * * @param allFeatures */ private void updateRenderOrder(List allFeatures) { List allfeatures = new ArrayList(allFeatures); String[] oldRender = renderOrder; renderOrder = new String[allfeatures.size()]; boolean initOrders = (featureOrder == null); int opos = 0; if (oldRender != null && oldRender.length > 0) { for (int j = 0; j < oldRender.length; j++) { if (oldRender[j] != null) { if (initOrders) { setOrder(oldRender[j], (1 - (1 + (float) j) / oldRender.length)); } if (allfeatures.contains(oldRender[j])) { renderOrder[opos++] = oldRender[j]; // existing features always // appear below new features allfeatures.remove(oldRender[j]); if (minmax != null) { float[][] mmrange = minmax.get(oldRender[j]); if (mmrange != null) { FeatureColourI fc = featureColours.get(oldRender[j]); if (fc != null && !fc.isSimpleColour() && fc.isAutoScaled()) { fc.updateBounds(mmrange[0][0], mmrange[0][1]); } } } } } } } if (allfeatures.size() == 0) { // no new features - leave order unchanged. return; } int i = allfeatures.size() - 1; int iSize = i; boolean sort = false; String[] newf = new String[allfeatures.size()]; float[] sortOrder = new float[allfeatures.size()]; for (String newfeat : allfeatures) { newf[i] = newfeat; if (minmax != null) { // update from new features minmax if necessary float[][] mmrange = minmax.get(newf[i]); if (mmrange != null) { FeatureColourI fc = featureColours.get(newf[i]); if (fc != null && !fc.isSimpleColour() && fc.isAutoScaled()) { fc.updateBounds(mmrange[0][0], mmrange[0][1]); } } } if (initOrders || !featureOrder.containsKey(newf[i])) { int denom = initOrders ? allfeatures.size() : featureOrder.size(); // new unordered feature - compute persistent ordering at head of // existing features. setOrder(newf[i], i / (float) denom); } // set order from newly found feature from persisted ordering. sortOrder[i] = 2 - featureOrder.get(newf[i]).floatValue(); if (i < iSize) { // only sort if we need to sort = sort || sortOrder[i] > sortOrder[i + 1]; } i--; } if (iSize > 1 && sort) { jalview.util.QuickSort.sort(sortOrder, newf); } sortOrder = null; System.arraycopy(newf, 0, renderOrder, opos, newf.length); } /** * get a feature style object for the given type string. Creates a * java.awt.Color for a featureType with no existing colourscheme. * * @param featureType * @return */ @Override public FeatureColourI getFeatureStyle(String featureType) { FeatureColourI fc = featureColours.get(featureType); if (fc == null) { Color col = ColorUtils.createColourFromName(featureType); fc = new FeatureColour(col); featureColours.put(featureType, fc); } return fc; } /** * Returns the configured colour for a particular feature instance. This * includes calculation of 'colour by label', or of a graduated score colour, * if applicable. It does not take into account feature visibility or colour * transparency. Returns null for a score feature whose score value lies * outside any colour threshold. * * @param feature * @return */ public Color getColour(SequenceFeature feature) { FeatureColourI fc = getFeatureStyle(feature.getType()); return fc.getColor(feature); } /** * Answers true if the feature type is currently selected to be displayed, * else false * * @param type * @return */ protected boolean showFeatureOfType(String type) { return type == null ? false : av.getFeaturesDisplayed().isVisible(type); } @Override public void setColour(String featureType, FeatureColourI col) { featureColours.put(featureType, col); } @Override public void setTransparency(float value) { transparency = value; } @Override public float getTransparency() { return transparency; } /** * analogous to colour - store a normalized ordering for all feature types in * this rendering context. * * @param type * Feature type string * @param position * normalized priority - 0 means always appears on top, 1 means * always last. */ public float setOrder(String type, float position) { if (featureOrder == null) { featureOrder = new Hashtable(); } featureOrder.put(type, new Float(position)); return position; } /** * get the global priority (0 (top) to 1 (bottom)) * * @param type * @return [0,1] or -1 for a type without a priority */ public float getOrder(String type) { if (featureOrder != null) { if (featureOrder.containsKey(type)) { return featureOrder.get(type).floatValue(); } } return -1; } @Override public Map getFeatureColours() { return featureColours; } /** * Replace current ordering with new ordering * * @param data * { String(Type), Colour(Type), Boolean(Displayed) } * @return true if any visible features have been reordered, else false */ public boolean setFeaturePriority(Object[][] data) { return setFeaturePriority(data, true); } /** * Sets the priority order for features, with the highest priority (displayed * on top) at the start of the data array * * @param data * { String(Type), Colour(Type), Boolean(Displayed) } * @param visibleNew * when true current featureDisplay list will be cleared * @return true if any visible features have been reordered or recoloured, * else false (i.e. no need to repaint) */ public boolean setFeaturePriority(Object[][] data, boolean visibleNew) { /* * note visible feature ordering and colours before update */ List visibleFeatures = getDisplayedFeatureTypes(); Map visibleColours = new HashMap( getFeatureColours()); FeaturesDisplayedI av_featuresdisplayed = null; if (visibleNew) { if ((av_featuresdisplayed = av.getFeaturesDisplayed()) != null) { av.getFeaturesDisplayed().clear(); } else { av.setFeaturesDisplayed( av_featuresdisplayed = new FeaturesDisplayed()); } } else { av_featuresdisplayed = av.getFeaturesDisplayed(); } if (data == null) { return false; } // The feature table will display high priority // features at the top, but these are the ones // we need to render last, so invert the data renderOrder = new String[data.length]; if (data.length > 0) { for (int i = 0; i < data.length; i++) { String type = data[i][0].toString(); setColour(type, (FeatureColourI) data[i][1]); if (((Boolean) data[i][2]).booleanValue()) { av_featuresdisplayed.setVisible(type); } renderOrder[data.length - i - 1] = type; } } /* * get the new visible ordering and return true if it has changed * order or any colour has changed */ List reorderedVisibleFeatures = getDisplayedFeatureTypes(); if (!visibleFeatures.equals(reorderedVisibleFeatures)) { /* * the list of ordered visible features has changed */ return true; } /* * return true if any feature colour has changed */ for (String feature : visibleFeatures) { if (visibleColours.get(feature) != getFeatureStyle(feature)) { return true; } } return false; } /** * @param listener * @see java.beans.PropertyChangeSupport#addPropertyChangeListener(java.beans.PropertyChangeListener) */ public void addPropertyChangeListener(PropertyChangeListener listener) { changeSupport.addPropertyChangeListener(listener); } /** * @param listener * @see java.beans.PropertyChangeSupport#removePropertyChangeListener(java.beans.PropertyChangeListener) */ public void removePropertyChangeListener(PropertyChangeListener listener) { changeSupport.removePropertyChangeListener(listener); } public Set getAllFeatureColours() { return featureColours.keySet(); } public void clearRenderOrder() { renderOrder = null; } public boolean hasRenderOrder() { return renderOrder != null; } /** * Returns feature types in ordering of rendering, where last means on top */ public List getRenderOrder() { if (renderOrder == null) { return Arrays.asList(new String[] {}); } return Arrays.asList(renderOrder); } public int getFeatureGroupsSize() { return featureGroups != null ? 0 : featureGroups.size(); } @Override public List getFeatureGroups() { // conflict between applet and desktop - featureGroups returns the map in // the desktop featureRenderer return (featureGroups == null) ? Arrays.asList(new String[0]) : Arrays.asList(featureGroups.keySet().toArray(new String[0])); } public boolean checkGroupVisibility(String group, boolean newGroupsVisible) { if (featureGroups == null) { // then an exception happens next.. } if (featureGroups.containsKey(group)) { return featureGroups.get(group).booleanValue(); } if (newGroupsVisible) { featureGroups.put(group, new Boolean(true)); return true; } return false; } /** * get visible or invisible groups * * @param visible * true to return visible groups, false to return hidden ones. * @return list of groups */ @Override public List getGroups(boolean visible) { if (featureGroups != null) { List gp = new ArrayList(); for (String grp : featureGroups.keySet()) { Boolean state = featureGroups.get(grp); if (state.booleanValue() == visible) { gp.add(grp); } } return gp; } return null; } @Override public void setGroupVisibility(String group, boolean visible) { featureGroups.put(group, new Boolean(visible)); } @Override public void setGroupVisibility(List toset, boolean visible) { if (toset != null && toset.size() > 0 && featureGroups != null) { boolean rdrw = false; for (String gst : toset) { Boolean st = featureGroups.get(gst); featureGroups.put(gst, new Boolean(visible)); if (st != null) { rdrw = rdrw || (visible != st.booleanValue()); } } if (rdrw) { // set local flag indicating redraw needed ? } } } @Override public Map getDisplayedFeatureCols() { Map fcols = new Hashtable(); if (getViewport().getFeaturesDisplayed() == null) { return fcols; } Set features = getViewport().getFeaturesDisplayed() .getVisibleFeatures(); for (String feature : features) { fcols.put(feature, getFeatureStyle(feature)); } return fcols; } @Override public FeaturesDisplayedI getFeaturesDisplayed() { return av.getFeaturesDisplayed(); } /** * Returns a (possibly empty) list of visible feature types, in render order * (last is on top) */ @Override public List getDisplayedFeatureTypes() { List typ = getRenderOrder(); List displayed = new ArrayList(); FeaturesDisplayedI feature_disp = av.getFeaturesDisplayed(); if (feature_disp != null) { synchronized (feature_disp) { for (String type : typ) { if (feature_disp.isVisible(type)) { displayed.add(type); } } } } return displayed; } @Override public List getDisplayedFeatureGroups() { List _gps = new ArrayList(); for (String gp : getFeatureGroups()) { if (checkGroupVisibility(gp, false)) { _gps.add(gp); } } return _gps; } /** * Answers true if the feature belongs to a feature group which is not * currently displayed, else false * * @param sequenceFeature * @return */ protected boolean featureGroupNotShown(final SequenceFeature sequenceFeature) { return featureGroups != null && sequenceFeature.featureGroup != null && sequenceFeature.featureGroup.length() != 0 && featureGroups.containsKey(sequenceFeature.featureGroup) && !featureGroups.get(sequenceFeature.featureGroup) .booleanValue(); } /** * {@inheritDoc} */ @Override public List findFeaturesAtResidue(SequenceI sequence, int resNo) { List result = new ArrayList(); if (!av.areFeaturesDisplayed() || getFeaturesDisplayed() == null) { return result; } /* * include features at the position provided their feature type is * displayed, and feature group is null or the empty string * or marked for display */ Set visibleFeatures = getFeaturesDisplayed() .getVisibleFeatures(); String[] visibleTypes = visibleFeatures .toArray(new String[visibleFeatures.size()]); List features = sequence.getFeatures().findFeatures( resNo, resNo, visibleTypes); for (SequenceFeature sf : features) { if (!featureGroupNotShown(sf)) { result.add(sf); } } return result; } /** * Removes from the list of features any that have a feature group that is not * displayed, or duplicate the location of a feature of the same type (unless * a graduated colour scheme or colour by label is applied). Should be used * only for features of the same feature colour (which normally implies the * same feature type). * * @param features * @param fc */ public void filterFeaturesForDisplay(List features, FeatureColourI fc) { if (features.isEmpty()) { return; } SequenceFeatures.sortFeatures(features, true); boolean simpleColour = fc == null || fc.isSimpleColour(); SequenceFeature lastFeature = null; Iterator it = features.iterator(); while (it.hasNext()) { SequenceFeature sf = it.next(); if (featureGroupNotShown(sf)) { it.remove(); continue; } /* * a feature is redundant for rendering purposes if it has the * same extent as another (so would just redraw the same colour); * (checking type and isContactFeature as a fail-safe here, although * currently they are guaranteed to match in this context) */ if (simpleColour) { if (lastFeature != null && sf.getBegin() == lastFeature.getBegin() && sf.getEnd() == lastFeature.getEnd() && sf.isContactFeature() == lastFeature.isContactFeature() && sf.getType().equals(lastFeature.getType())) { it.remove(); } } lastFeature = sf; } } }