From 1f491c8c9e66b3accb8bb9eae78c7d9caa63c1ac Mon Sep 17 00:00:00 2001 From: "j.procter@dundee.ac.uk" Date: Fri, 8 Aug 2014 17:39:51 +0100 Subject: [PATCH] JAL-1483 abstract feature renderer model holds render styles/order/group visibility for features provided by an alignment view and methods for discovery/update of settings --- src/jalview/api/FeatureRenderer.java | 34 +- src/jalview/viewmodel/FeatureRenderer.java | 9 - .../seqfeatures/FeatureRendererModel.java | 942 ++++++++++++++++++++ 3 files changed, 975 insertions(+), 10 deletions(-) delete mode 100644 src/jalview/viewmodel/FeatureRenderer.java create mode 100644 src/jalview/viewmodel/seqfeatures/FeatureRendererModel.java diff --git a/src/jalview/api/FeatureRenderer.java b/src/jalview/api/FeatureRenderer.java index a0552dc..4fd89c1 100644 --- a/src/jalview/api/FeatureRenderer.java +++ b/src/jalview/api/FeatureRenderer.java @@ -22,9 +22,11 @@ package jalview.api; import jalview.datamodel.SequenceFeature; import jalview.datamodel.SequenceI; -import jalview.schemes.GraduatedColor; import java.awt.Color; +import java.util.Hashtable; +import java.util.List; +import java.util.Map; /** * Abstract feature renderer interface @@ -43,4 +45,34 @@ public interface FeatureRenderer void setColour(String ft, Object ggc); + AlignViewportI getViewport(); + + FeaturesDisplayedI getFeaturesDisplayed(); + + Map getFeatureColours(); + + void findAllFeatures(boolean newMadeVisible); + + Map getDisplayedFeatureCols(); + + List getFeatureGroups(); + + List getGroups(boolean visible); + + void setGroupVisibility(List toset, boolean visible); + + void setGroupVisibility(String group, boolean visible); + + List findFeaturesAtRes(SequenceI sequence, int res); + + boolean isTransparencyAvailable(); + + String[] getDisplayedFeatureTypes(); + + String[] getDisplayedFeatureGroups(); + + void setAllVisible(List featureTypes); + + void setVisible(String featureType); + } diff --git a/src/jalview/viewmodel/FeatureRenderer.java b/src/jalview/viewmodel/FeatureRenderer.java deleted file mode 100644 index b03cedb..0000000 --- a/src/jalview/viewmodel/FeatureRenderer.java +++ /dev/null @@ -1,9 +0,0 @@ -package jalview.viewmodel; - -import jalview.datamodel.SequenceFeature; - -import java.awt.Color; - -public class FeatureRenderer -{ -} diff --git a/src/jalview/viewmodel/seqfeatures/FeatureRendererModel.java b/src/jalview/viewmodel/seqfeatures/FeatureRendererModel.java new file mode 100644 index 0000000..a53004d --- /dev/null +++ b/src/jalview/viewmodel/seqfeatures/FeatureRendererModel.java @@ -0,0 +1,942 @@ +package jalview.viewmodel.seqfeatures; + +import jalview.api.AlignViewportI; +import jalview.api.FeaturesDisplayedI; +import jalview.datamodel.AlignmentI; +import jalview.datamodel.SequenceFeature; +import jalview.datamodel.SequenceI; +import jalview.renderer.seqfeatures.FeatureRenderer; +import jalview.schemes.GraduatedColor; +import jalview.viewmodel.AlignmentViewport; + +import java.awt.Color; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.Vector; +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 Object currentColour; + + protected String[] renderOrder; + + protected PropertyChangeSupport changeSupport = new PropertyChangeSupport( + this); + + protected AlignmentViewport av; + + 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(); + java.util.Iterator fdisp = _fr.getFeaturesDisplayed() + .getVisibleFeatures(); + while (fdisp.hasNext()) + { + fd.setVisible(fdisp.next()); + } + } + } + } + } + } + + 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 Hashtable minmax = new Hashtable(); + + public Hashtable 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 = ((float[][]) 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 findFeaturesAtRes(SequenceI sequence, int res) + { + ArrayList tmp = new ArrayList(); + SequenceFeature[] features = sequence.getSequenceFeatures(); + if (features != null) + { + for (int i = 0; i < features.length; i++) + { + if (av.areFeaturesDisplayed() + || !av.getFeaturesDisplayed().isVisible( + features[i].getType())) + { + continue; + } + + if (features[i].featureGroup != null + && featureGroups != null + && featureGroups.containsKey(features[i].featureGroup) + && !((Boolean) featureGroups.get(features[i].featureGroup)) + .booleanValue()) + continue; + + if ((features[i].getBegin() <= res) + && (features[i].getEnd() >= res)) + { + tmp.add(features[i]); + } + } + } + return tmp; + } + + /** + * Searches alignment for all features and updates colours + * + * @param newMadeVisible + * if true newly added feature types will be rendered immediatly + * 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(); + + ArrayList allfeatures = new ArrayList(); + ArrayList oldfeatures = new ArrayList(); + if (renderOrder != null) + { + for (int i = 0; i < renderOrder.length; i++) + { + if (renderOrder[i] != null) + { + oldfeatures.add(renderOrder[i]); + } + } + } + if (minmax == null) + { + minmax = new Hashtable(); + } + AlignmentI alignment = av.getAlignment(); + for (int i = 0; i < alignment.getHeight(); i++) + { + SequenceI asq = alignment.getSequenceAt(i); + SequenceI dasq = asq.getDatasetSequence(); + SequenceFeature[] features = dasq != null ? dasq + .getSequenceFeatures() : asq.getSequenceFeatures(); + + if (features == null) + { + continue; + } + + int index = 0; + while (index < features.length) + { + if (!featuresDisplayed.isRegistered(features[index].getType())) + { + String fgrp = features[index].getFeatureGroup(); + if (fgrp != null) + { + Boolean groupDisplayed = featureGroups.get(fgrp); + if (groupDisplayed == null) + { + groupDisplayed = Boolean.valueOf(newMadeVisible); + featureGroups.put(fgrp, groupDisplayed); + } + if (!((Boolean) groupDisplayed).booleanValue()) + { + index++; + continue; + } + } + if (!(features[index].begin == 0 && features[index].end == 0)) + { + // If beginning and end are 0, the feature is for the whole sequence + // and we don't want to render the feature in the normal way + + if (newMadeVisible + && !oldfeatures.contains(features[index].getType())) + { + // this is a new feature type on the alignment. Mark it for + // display. + featuresDisplayed.setVisible(features[index].getType()); + setOrder(features[index].getType(), 0); + } + } + } + if (!allfeatures.contains(features[index].getType())) + { + allfeatures.add(features[index].getType()); + } + if (features[index].score != Float.NaN) + { + int nonpos = features[index].getBegin() >= 1 ? 0 : 1; + float[][] mm = (float[][]) minmax.get(features[index].getType()); + if (mm == null) + { + mm = new float[][] + { null, null }; + minmax.put(features[index].getType(), mm); + } + if (mm[nonpos] == null) + { + mm[nonpos] = new float[] + { features[index].score, features[index].score }; + + } + else + { + if (mm[nonpos][0] > features[index].score) + { + mm[nonpos][0] = features[index].score; + } + if (mm[nonpos][1] < features[index].score) + { + mm[nonpos][1] = features[index].score; + } + } + } + index++; + } + } + updateRenderOrder(allfeatures); + findingFeatures = false; + } + + 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()]; + Object mmrange, fc = null; + 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) + / (float) 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) + { + mmrange = minmax.get(oldRender[j]); + if (mmrange != null) + { + fc = featureColours.get(oldRender[j]); + if (fc != null && fc instanceof GraduatedColor + && ((GraduatedColor) fc).isAutoScale()) + { + ((GraduatedColor) fc).updateBounds( + ((float[][]) mmrange)[0][0], + ((float[][]) 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 + mmrange = minmax.get(newf[i]); + if (mmrange != null) + { + fc = featureColours.get(newf[i]); + if (fc != null && fc instanceof GraduatedColor + && ((GraduatedColor) fc).isAutoScale()) + { + ((GraduatedColor) fc).updateBounds(((float[][]) mmrange)[0][0], + ((float[][]) 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 - ((Float) 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. TODO: + * replace return type with object implementing standard abstract colour/style + * interface + * + * @param featureType + * @return java.awt.Color or GraduatedColor + */ + public Object getFeatureStyle(String featureType) + { + Object fc = featureColours.get(featureType); + if (fc == null) + { + jalview.schemes.UserColourScheme ucs = new jalview.schemes.UserColourScheme(); + Color col = ucs.createColourFromName(featureType); + featureColours.put(featureType, fc = col); + } + return fc; + } + + /** + * return a nominal colour for this feature + * + * @param featureType + * @return standard color, or maximum colour for graduated colourscheme + */ + public Color getColour(String featureType) + { + Object fc = getFeatureStyle(featureType); + + if (fc instanceof Color) + { + return (Color) fc; + } + else + { + if (fc instanceof GraduatedColor) + { + return ((GraduatedColor) fc).getMaxColor(); + } + } + throw new Error("Implementation Error: Unrecognised render object " + + fc.getClass() + " for features of type " + featureType); + } + + /** + * calculate the render colour for a specific feature using current feature + * settings. + * + * @param feature + * @return render colour for the given feature + */ + public Color getColour(SequenceFeature feature) + { + Object fc = getFeatureStyle(feature.getType()); + if (fc instanceof Color) + { + return (Color) fc; + } + else + { + if (fc instanceof GraduatedColor) + { + return ((GraduatedColor) fc).findColor(feature); + } + } + throw new Error("Implementation Error: Unrecognised render object " + + fc.getClass() + " for features of type " + feature.getType()); + } + + protected boolean showFeature(SequenceFeature sequenceFeature) + { + Object fc = getFeatureStyle(sequenceFeature.type); + if (fc instanceof GraduatedColor) + { + return ((GraduatedColor) fc).isColored(sequenceFeature); + } + else + { + return true; + } + } + + protected boolean showFeatureOfType(String type) + { + return av.getFeaturesDisplayed().isVisible(type); + } + + public void setColour(String featureType, Object col) + { + // overwrite + // Color _col = (col instanceof Color) ? ((Color) col) : (col instanceof + // GraduatedColor) ? ((GraduatedColor) col).getMaxColor() : null; + // Object c = featureColours.get(featureType); + // if (c == null || c instanceof Color || (c instanceof GraduatedColor && + // !((GraduatedColor)c).getMaxColor().equals(_col))) + { + featureColours.put(featureType, col); + } + } + + public void setTransparency(float value) + { + transparency = value; + } + + public float getTransparency() + { + return transparency; + } + + Map featureOrder = null; + + /** + * 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 ((Float) featureOrder.get(type)).floatValue(); + } + } + return -1; + } + + @Override + public Map getFeatureColours() + { + return new ConcurrentHashMap<>(featureColours); + } + + /** + * Replace current ordering with new ordering + * + * @param data + * { String(Type), Colour(Type), Boolean(Displayed) } + */ + public void setFeaturePriority(Object[][] data) + { + setFeaturePriority(data, true); + } + + /** + * + * @param data + * { String(Type), Colour(Type), Boolean(Displayed) } + * @param visibleNew + * when true current featureDisplay list will be cleared + */ + public void setFeaturePriority(Object[][] data, boolean visibleNew) + { + 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; + } + // The feature table will display high priority + // features at the top, but theses 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, data[i][1]); // todo : typesafety - feature color + // interface object + if (((Boolean) data[i][2]).booleanValue()) + { + av_featuresdisplayed.setVisible(type); + } + + renderOrder[data.length - i - 1] = type; + } + } + + } + + /** + * @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; + } + + 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 ((Boolean) 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) + { + ArrayList gp = new ArrayList(); + + for (Object grp : featureGroups.keySet()) + { + Boolean state = (Boolean) 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 != ((Boolean) st).booleanValue()); + } + } + if (rdrw) + { + // set local flag indicating redraw needed ? + } + } + } + + @Override + public Hashtable getDisplayedFeatureCols() + { + Hashtable fcols = new Hashtable(); + if (getViewport().getFeaturesDisplayed() == null) + { + return fcols; + } + Iterator en = getViewport().getFeaturesDisplayed() + .getVisibleFeatures(); + while (en.hasNext()) + { + String col = en.next(); + fcols.put(col, getColour(col)); + } + return fcols; + } + + @Override + public FeaturesDisplayedI getFeaturesDisplayed() + { + return av.getFeaturesDisplayed(); + } + + @Override + public String[] getDisplayedFeatureTypes() + { + String[] typ = null; + typ = getRenderOrder().toArray(new String[0]); + FeaturesDisplayedI feature_disp = av.getFeaturesDisplayed(); + if (feature_disp != null) + { + synchronized (feature_disp) + { + for (int i = 0; i < typ.length; i++) + { + if (feature_disp.isVisible(typ[i])) + { + typ[i] = null; + } + } + } + } + return typ; + } + + @Override + public String[] getDisplayedFeatureGroups() + { + String[] gps = null; + ArrayList _gps = new ArrayList(); + Iterator en = getFeatureGroups().iterator(); + int g = 0; + boolean valid = false; + while (en.hasNext()) + { + String gp = (String) en.next(); + if (checkGroupVisibility(gp, false)) + { + valid = true; + _gps.add(gp); + } + if (!valid) + { + return null; + } + else + { + gps = new String[_gps.size()]; + _gps.toArray(gps); + } + } + return gps; + } + +} -- 1.7.10.2