public class Cluster extends ZoomableCompoundGroup implements CompoundGroupWithProperties, DoubleNameElement,
		Comparable<Cluster>
{
	private ClusterData clusterData;

	HashMap<String, List<Compound>> compoundsOrderedByPropterty = new HashMap<String, List<Compound>>();

	private boolean watched;
	private CompoundProperty highlightProp;
	private HighlightSorting highlightSorting;
	private boolean showLabel = false;

	public Cluster(org.chesmapper.map.dataInterface.ClusterData clusterData)
	{
		this.clusterData = clusterData;
		List<Compound> c = new ArrayList<Compound>();
		int count = 0;
		for (CompoundData d : clusterData.getCompounds())
			c.add(new Compound(clusterData.getCompoundClusterIndices().get(count++), d));
		setCompounds(c);

		displayName.name = getName() + " (#" + getNumCompounds() + ")";
		displayName.compareIndex = clusterData.getOrigIndex();

		if (View.instance != null) // for export without graphics
			update();
	}

	public void setFilter(CompoundFilter filter)
	{
		super.setFilter(filter);
		List<Integer> origIndices = new ArrayList<Integer>();
		if (filter != null)
			for (Compound c : getCompounds())
				if (filter.accept(c))
					origIndices.add(c.getOrigIndex());
		clusterData.setFilter(filter == null ? null : origIndices);
		updateDisplayName();
	}

	boolean alignedCompoundsCalibrated = false;

	public void updatePositions()
	{
		// the actual compound position that is stored in compound is never changed (depends only on scaling)
		// this method moves all compounds to the cluster position

		// recalculate non-superimposed diameter
		update();

		if (!clusterData.isAligned())
		{
			// the compounds have not been aligned
			// the compounds may have a center != 0, calibrate to 0
			for (Compound m : getCompounds())
				m.moveTo(new Vector3f(0f, 0f, 0f));
		}
		else
		{
			// the compounds are aligned, cannot calibrate to 0, this would brake the alignment
			// however, the compound center may have an offset, calculate and remove
			if (!alignedCompoundsCalibrated)
			{
				Vector3f[] origCenters = new Vector3f[getCompounds().size()];
				for (int i = 0; i < origCenters.length; i++)
					origCenters[i] = getCompounds().get(i).origCenter;
				Vector3f center = Vector3fUtil.center(origCenters);
				for (int i = 0; i < origCenters.length; i++)
					getCompounds().get(i).origCenter.sub(center);
				alignedCompoundsCalibrated = true;
			}
			for (Compound m : getCompounds())
				m.moveTo(m.origCenter);
		}

		// translate compounds to the cluster position
		View.instance.setAtomCoordRelative(getCenter(true), getBitSet());
	}

	private DisplayName displayName = new DisplayName();
	private Color highlightColorText;

	public String getName()
	{
		return clusterData.getName();
	}

	@Override
	public String toString()
	{
		return getName();
	}

	public String toStringWithValue()
	{
		return displayName.toString(false, null);
	}

	@Override
	public String getFirstName()
	{
		return displayName.name;
	}

	@Override
	public String getSecondName()
	{
		if (ObjectUtil.equals(displayName.valDisplay, displayName.name))
			return null;
		else
			return displayName.valDisplay;
	}

	public String getSummaryStringValue(CompoundProperty property, boolean html)
	{
		return clusterData.getSummaryStringValue(property, html);
	}

	public CountedSet<String> getNominalSummary(NominalProperty p)
	{
		return clusterData.getNominalSummary(p);
	}

	public String getAlignAlgorithm()
	{
		return clusterData.getAlignAlgorithm();
	}

	protected void update()
	{
		if (getOrigSize() != getNumCompounds())
			displayName.name = getName() + " (#" + getNumCompounds() + "/" + getOrigSize() + ")";
		else
			displayName.name = getName() + " (#" + getNumCompounds() + ")";
		super.update();
	}

	public List<Compound> getCompoundsInOrder(final CompoundProperty property, HighlightSorting sorting)
	{
		String key = property + "_" + sorting;
		if (!compoundsOrderedByPropterty.containsKey(key))
		{
			List<Compound> c = new ArrayList<Compound>();
			for (Compound compound : getCompounds())
				c.add(compound);
			final HighlightSorting finalSorting;
			if (sorting == HighlightSorting.Median)
				finalSorting = HighlightSorting.Max;
			else
				finalSorting = sorting;
			Collections.sort(c, new Comparator<Compound>()
			{
				@Override
				public int compare(Compound o1, Compound o2)
				{
					int res;
					if (o1 == null)
					{
						if (o2 == null)
							res = 0;
						else
							res = 1;
					}
					else if (o2 == null)
						res = -1;
					else if (property instanceof NumericProperty)
					{
						Double d1 = o1.getDoubleValue((NumericProperty) property);
						Double d2 = o2.getDoubleValue((NumericProperty) property);
						if (d1 == null)
						{
							if (d2 == null)
								res = 0;
							else
								res = 1;
						}
						else if (d2 == null)
							res = -1;
						else
							res = d1.compareTo(d2);
					}
					else
						res = (o1.getStringValue((NominalProperty) property) + "").compareTo(o2
								.getStringValue((NominalProperty) property) + "");
					return (finalSorting == HighlightSorting.Max ? -1 : 1) * res;
				}
			});
			if (sorting == HighlightSorting.Median)
			{
				//				Settings.LOGGER.warn("max order: ");
				//				for (Compound mm : m)
				//					Settings.LOGGER.warn(mm.getStringValue(property) + " ");
				//				Settings.LOGGER.warn();

				/**
				 * median sorting:
				 * - first order by max to compute median
				 * - create a dist-to-median array, sort compounds according to that array
				 */
				Compound medianCompound = c.get(c.size() / 2);
				//				Settings.LOGGER.warn(medianCompound.getStringValue(property));
				double distToMedian[] = new double[c.size()];
				if (property instanceof NumericProperty)
				{
					Double med = medianCompound.getDoubleValue((NumericProperty) property);
					for (int i = 0; i < distToMedian.length; i++)
					{
						Double d = c.get(i).getDoubleValue((NumericProperty) property);
						if (med == null)
						{
							if (d == null)
								distToMedian[i] = 0;
							else
								distToMedian[i] = Double.MAX_VALUE;
						}
						else if (d == null)
							distToMedian[i] = Double.MAX_VALUE;
						else
							distToMedian[i] = Math.abs(med - d);
					}
				}
				else
				{
					String medStr = medianCompound.getStringValue((NominalProperty) property);
					for (int i = 0; i < distToMedian.length; i++)
						distToMedian[i] = Math.abs((c.get(i).getStringValue((NominalProperty) property) + "")
								.compareTo(medStr + ""));
				}
				int order[] = ArrayUtil.getOrdering(distToMedian, true);
				Compound a[] = new Compound[c.size()];
				Compound s[] = ArrayUtil.sortAccordingToOrdering(order, c.toArray(a));
				c = ArrayUtil.toList(s);

				//				Settings.LOGGER.warn("med order: ");
				//				for (Compound mm : m)
				//					Settings.LOGGER.warn(mm.getStringValue(property) + " ");
				//				Settings.LOGGER.warn();
			}
			compoundsOrderedByPropterty.put(key, c);
		}
		//		Settings.LOGGER.warn("in order: ");
		//		for (Compound m : order.get(key))
		//			Settings.LOGGER.warn(m.getCompoundOrigIndex() + " ");
		//		Settings.LOGGER.warn("");
		return compoundsOrderedByPropterty.get(key);
	}

	public String getSubstructureSmarts(SubstructureSmartsType type)
	{
		return clusterData.getSubstructureSmarts(type);
	}

	public void removeWithJmolIndices(int[] compoundJmolIndices)
	{
		System.out.println("to remove from cluster: " + ArrayUtil.toString(compoundJmolIndices));
		List<Compound> toDel = new ArrayList<Compound>();
		int[] toDelIndex = new int[compoundJmolIndices.length];

		int count = 0;
		for (int i : compoundJmolIndices)
		{
			Compound c = getCompoundWithJmolIndex(i);
			toDel.add(c);
			toDelIndex[count++] = getIndex(c);
		}
		BitSet bs = new BitSet();
		for (Compound m : toDel)
		{
			bs.or(m.getBitSet());
			getCompounds().remove(m);
		}
		View.instance.hide(bs);

		compoundsOrderedByPropterty.clear();
		clusterData.remove(toDelIndex);
		update();
	}

	public boolean isWatched()
	{
		return watched;
	}

	public void setWatched(boolean watched)
	{
		this.watched = watched;
	}

	public void setHighlighProperty(CompoundProperty highlightProp, Color highlightColorText)
	{
		this.highlightProp = highlightProp;
		this.highlightColorText = highlightColorText;
		updateDisplayName();
	}

	private void updateDisplayName()
	{
		displayName.valDisplay = null;
		displayName.valCompare = null;
		if (highlightProp != null)
		{
			if (highlightProp instanceof NumericProperty)
				displayName.valCompare = new Double[] { getDoubleValue((NumericProperty) highlightProp) };
			else
			{
				String mode = getNominalSummary((NominalProperty) highlightProp).getMode(false);
				String domain[] = ((NominalProperty) highlightProp).getDomain();
				boolean invertSecondBinaryVal = false;
				if (domain.length == 2 && ArrayUtil.indexOf(domain, mode) == 1)
					invertSecondBinaryVal = true;
				CountedSet<String> set = getNominalSummary((NominalProperty) highlightProp);
				/**
				 * Clusters with nominal feature values should be sorted as follows:
				 * 1. according to the mode (the most common feature value)
				 * 2. within equal modes, according to how pure the cluster is with respect to the mode (ratio of compounds with this feature value)
				 * 3. within equal ratios, according to size (and therefore according to number of compounds with this feature value)
				 * 4. within equal size, according to cluster index   
				 */
				displayName.valCompare = new Comparable[] { mode,
						(invertSecondBinaryVal ? 1 : -1) * (set.getMaxCount(false) / (double) (set.getSum(false))),
						(invertSecondBinaryVal ? 1 : -1) * set.getSum(false) };
			}
			displayName.valDisplay = getFormattedValue(highlightProp);
		}
	}

	public CompoundProperty getHighlightProperty()
	{
		return highlightProp;
	}

	public void setHighlightSorting(HighlightSorting highlightSorting)
	{
		this.highlightSorting = highlightSorting;
	}

	public HighlightSorting getHighlightSorting()
	{
		return highlightSorting;
	}

	public String[] getStringValues(NominalProperty property, Compound excludeCompound)
	{
		return getStringValues(property, excludeCompound, false);
	}

	public String[] getStringValues(NominalProperty property, Compound excludeCompound, boolean formatted)
	{
		List<String> l = new ArrayList<String>();
		for (Compound c : getCompounds())
			if (c != excludeCompound && c.getStringValue(property) != null)
				l.add(formatted ? c.getFormattedValue(property) : c.getStringValue(property));
		String v[] = new String[l.size()];
		return l.toArray(v);
	}

	public Double[] getDoubleValues(NumericProperty property)
	{
		Double v[] = new Double[getCompounds().size()];
		for (int i = 0; i < v.length; i++)
			v[i] = getCompounds().get(i).getDoubleValue(property);
		return v;
	}

	public void setShowLabel(boolean showLabel)
	{
		this.showLabel = showLabel;
	}

	public boolean isShowLabel()
	{
		return showLabel;
	}

	public int numMissingValues(CompoundProperty p)
	{
		return clusterData.numMissingValues(p);
	}

	public boolean containsNotClusteredCompounds()
	{
		return clusterData.containsNotClusteredCompounds();
	}

	@Override
	public Double getDoubleValue(NumericProperty p)
	{
		return clusterData.getDoubleValue(p);
	}

	@Override
	public String getFormattedValue(CompoundProperty p)
	{
		return clusterData.getFormattedValue(p);
	}

	public Color getHighlightColorText()
	{
		return highlightColorText;
	}

	@Override
	public int compareTo(Cluster m)
	{
		return displayName.compareTo(m.displayName);
	}

}
 public interface ClusterController
{
	public void clearClusterActive(boolean animate, boolean clearCompoundActive);

	public void clearClusterWatched();

	public void clearCompoundActive(boolean animate);

	public void clearCompoundWatched();

	public void setClusterActive(Cluster c, boolean animate, boolean clearCompoundActive);

	public void setClusterWatched(Cluster c);

	public void setCompoundActive(Compound c, boolean animate);

	public void setCompoundActive(Compound[] c, boolean animate);

	public void toggleCompoundActive(Compound c);

	public void setCompoundWatched(Compound... c);

	public CompoundFilter getCompoundFilter();

	public void applyCompoundFilter(List<Compound> compounds, boolean accept);

	public void setCompoundFilter(CompoundFilter filter, boolean animate);

	public void useSelectedCompoundsAsFilter(boolean animate);

	public void removeCompounds(Compound[] c);

	public void removeCluster(Cluster... c);

	public void chooseClustersToRemove();

	public void chooseCompoundsToRemove();

	public void chooseClustersToFilter();

	public void chooseCompoundsToFilter();

	public void newClustering();

} public interface Clustering extends CompoundGroupWithProperties
{
	void addListener(PropertyChangeListener propertyChangeListener);

	int getNumClusters();

	int numClusters();

	List<Cluster> getClusters();

	boolean isClusterActive();

	boolean isClusterWatched();

	Cluster getCluster(int i);

	int indexOf(Cluster cluster);

	Cluster getActiveCluster();

	Cluster getWatchedCluster();

	int getActiveClusterIdx();

	int getWatchedClusterIdx();

	boolean isCompoundActive();

	boolean isCompoundWatched();

	boolean isCompoundActive(Compound c);

	Compound[] getActiveCompounds();

	Compound getActiveCompound();

	int[] getActiveCompoundsJmolIdx();

	Compound getWatchedCompound();

	Compound[] getWatchedCompounds();

	int[] getWatchedCompoundsJmolIdx();

	Cluster getClusterForCompound(Compound c);

	List<CompoundProperty> selectPropertiesAndFeaturesWithDialog(String title, CompoundProperty preselected,
			boolean addSmiles, boolean addEmbeddingStress, boolean addActivityCliffs, boolean addDistanceTo);

	List<CompoundProperty> getPropertiesAndFeatures();

	List<CompoundProperty> getProperties();

	List<CompoundProperty> getFeatures();

	List<Compound> getCompounds(boolean includingMultiClusteredCompounds);

	Cluster getUniqueClusterForCompounds(Compound[] c);

	String getOrigLocalPath();

	String getOrigSDFile();

	String getSDFile();

	boolean isClusterAlgorithmDisjoint();

	String getClusterAlgorithm();

	int getClusterIndexForCompound(Compound m);

	List<CompoundData> getCompounds();

	void chooseClustersToExport(CompoundProperty compoundDescriptor);

	void chooseCompoundsToExport(CompoundProperty compoundDescriptor);

	Double[] getDoubleValues(NumericProperty p);

	String[] getStringValues(NominalProperty p, Compound m);

	String getSummaryStringValue(CompoundProperty p, boolean b);

	int numMissingValues(CompoundProperty p);

	String getName();

	Double getNormalizedLogDoubleValue(CompoundPropertyOwner m, NumericProperty p);

	Double getNormalizedDoubleValue(CompoundPropertyOwner m, NumericProperty p);

	double getSpecificity(Compound compound, CompoundProperty p);

	double getSpecificity(Cluster cluster, CompoundProperty p);

	double getSpecificity(CompoundSelection sel, CompoundProperty p);

	int getNumCompounds(boolean includingMultiClusteredCompounds);

	int getNumUnfilteredCompounds(boolean includingMultiClusteredCompounds);

	String getEmbedAlgorithm();

	String getEmbedQuality();

	List<CompoundProperty> getAdditionalProperties();

	CorrelationProperty getEmbeddingQualityProperty();

	EqualPositionProperty getEqualPosProperty();

	Compound getCompoundWithJmolIndex(int convertRowIndexToModel);

	int numDistinctValues(CompoundProperty p);

	public void addSelectionListener(SelectionListener l);

	public abstract static class SelectionListener
	{
		public void clusterActiveChanged(Cluster c)
		{
		}

		public void clusterWatchedChanged(Cluster c)
		{
		}

		public void compoundActiveChanged(Compound c[])
		{
		}

		public void compoundWatchedChanged(Compound c[])
		{
		}
	}

	boolean isBMBFRealEndpointDataset(boolean b);

	CompoundProperty addDistanceToCompoundFeature(Compound c);

	CompoundProperty addSALIFeatures(CompoundProperty c);

	void predict();

	void addPredictionFeature(CompoundProperty clazz, PredictionResult p);

	NumericProperty addLogFeature(NumericProperty p);

	public CompoundSelection getCompoundSelection(Compound[] c);

	boolean isRandomEmbedding();

	CompoundProperty getHighlightProperty();

	Color getHighlightColorText();

	Double getFeatureDistance(int origIndex, int origIndex2);

	boolean isSkippingRedundantFeatures();

	boolean isBigDataMode();

	void computeAppDomain();

	boolean doCheSMappingWarningsExist();

	void showCheSMappingWarnings();

} public class ClusteringImpl implements Zoomable, Clustering
{
	private Vector<Cluster> clusters;
	private ClusteringData clusteringData;

	SelectionModel clusterActive;
	SelectionModel clusterWatched;
	SelectionModel compoundActive;
	SelectionModel compoundWatched;

	boolean suppresAddEvent = false;
	Vector<PropertyChangeListener> listeners;

	public static final String CLUSTER_ADDED = "cluster_added";
	public static final String CLUSTER_REMOVED = "cluster_removed";
	public static final String CLUSTER_MODIFIED = "cluster_modified";
	public static final String CLUSTER_NEW = "cluster_new";
	public static final String CLUSTER_CLEAR = "cluster_clear";
	public static final String PROPERTY_ADDED = "property_added";

	boolean dirty = true;
	int numCompounds = -1;
	private boolean superimposed = false;
	float superimposedDiameter;
	float nonSuperimposedDiameter;

	BitSet bitSetAll;
	HashMap<Cluster, Integer> clusterIndices;
	HashMap<Integer, Compound> jmolCompoundIndexToCompound;
	HashMap<Integer, Cluster> jmolCompoundIndexToCluster;
	HashMap<CompoundProperty, Integer> numMissingValues;
	HashMap<CompoundProperty, Integer> numDistinctValues;

	ClusteringValues clusteringValues = new ClusteringValues(this);

	List<Compound> filteredCompoundList;
	List<Compound> filteredCompoundListIncludingMultiClusteredCompounds;

	Vector3f superimposedCenter;
	Vector3f nonSuperimposedCenter;

	Boolean endpointDataset;
	Boolean filledEndpointDataset;

	CompoundProperty highlightProperty;
	Color highlightColorText;

	JitteringProvider jittering;

	public ClusteringImpl()
	{
		listeners = new Vector<PropertyChangeListener>();
		init();
	}

	public void init()
	{
		clusterActive = new SelectionModel();
		clusterWatched = new SelectionModel();
		compoundActive = new SelectionModel(true);
		compoundWatched = new SelectionModel(true);
		clusters = new Vector<Cluster>();
		numMissingValues = new HashMap<CompoundProperty, Integer>();
		numDistinctValues = new HashMap<CompoundProperty, Integer>();
	}

	public void addListener(PropertyChangeListener l)
	{
		listeners.add(l);
	}

	public void addListenerFirst(PropertyChangeListener l)
	{
		listeners.insertElementAt(l, 0);
	}

	public void fire(String event, Object oldValue, Object newValue)
	{
		if (!suppresAddEvent)
			for (PropertyChangeListener l : listeners)
				l.propertyChange(new PropertyChangeEvent(this, event, oldValue, newValue));
	}

	private Cluster addSingleCluster(ClusterData clusterData)
	{
		Cluster c = new Cluster(clusterData);
		@SuppressWarnings("unchecked")
		Vector<Cluster> old = (Vector<Cluster>) VectorUtil.clone(ClusteringImpl.this.clusters);
		clusters.add(c);
		dirty = true;
		fire(CLUSTER_ADDED, old, clusters);
		return c;
	}

	public synchronized void update()
	{
		if (!dirty)
			return;

		numCompounds = 0;
		for (Cluster c : clusters)
			numCompounds += c.getNumCompounds();

		if (View.instance != null) // for export without graphics
		{
			bitSetAll = new BitSet();
			for (Cluster c : clusters)
				bitSetAll.or(c.getBitSet());
		}

		int count = 0;
		clusterIndices = new HashMap<Cluster, Integer>();
		jmolCompoundIndexToCluster = new HashMap<Integer, Cluster>();
		jmolCompoundIndexToCompound = new HashMap<Integer, Compound>();
		for (Cluster c : clusters)
		{
			clusterIndices.put(c, count++);
			for (Compound m : c.getCompounds())
			{
				if (jmolCompoundIndexToCompound.get(m.getJmolIndex()) != null)
					throw new Error("duplicate compund index! " + m.getJmolIndex());
				jmolCompoundIndexToCompound.put(m.getJmolIndex(), m);
				jmolCompoundIndexToCluster.put(m.getJmolIndex(), c);
			}
		}

		filteredCompoundListIncludingMultiClusteredCompounds = new ArrayList<Compound>();
		for (Cluster c : clusters)
			for (Compound mm : c.getCompounds())
				filteredCompoundListIncludingMultiClusteredCompounds.add(mm);

		filteredCompoundList = new ArrayList<Compound>();
		HashSet<Integer> compoundIndex = new HashSet<Integer>();
		for (Cluster c : clusters)
			for (Compound mm : c.getCompounds())
			{
				if (!compoundIndex.contains(mm.getOrigIndex()))
				{
					filteredCompoundList.add(mm);
					compoundIndex.add(mm.getOrigIndex());
				}
			}

		numMissingValues.clear();
		numDistinctValues.clear();

		clusteringValues.clear();
		compoundSelections.clear();

		endpointDataset = null;
		filledEndpointDataset = null;

		dirty = false;
	}

	public boolean isClusterActive()
	{
		return clusterActive.getSelected() != -1;
	}

	public boolean isClusterWatched()
	{
		return clusterWatched.getSelected() != -1;
	}

	public boolean isCompoundWatched()
	{
		return compoundWatched.getSelected() != -1;
	}

	public boolean isCompoundActiveFromCluster(int cluster)
	{
		int sel[] = compoundActive.getSelectedIndices();
		if (sel.length == 0)
			return false;
		for (int compoundJmolIndex : sel)
			if (getClusterIndexForJmolIndex(compoundJmolIndex) == cluster)
				return true;
		return false;
	}

	public boolean isCompoundWatchedFromCluster(int cluster)
	{
		int sel[] = compoundWatched.getSelectedIndices();
		if (sel.length == 0)
			return false;
		for (int compoundJmolIndex : sel)
			if (getClusterIndexForJmolIndex(compoundJmolIndex) == cluster)
				return true;
		return false;
	}

	public SelectionModel getClusterActive()
	{
		return clusterActive;
	}

	public SelectionModel getClusterWatched()
	{
		return clusterWatched;
	}

	public SelectionModel getCompoundActive()
	{
		return compoundActive;
	}

	public SelectionModel getCompoundWatched()
	{
		return compoundWatched;
	}

	public Cluster getClusterForCompound(Compound compound)
	{
		update();
		return jmolCompoundIndexToCluster.get(compound.getJmolIndex());
	}

	public Cluster getClusterForJmolIndex(int jmolIndex)
	{
		update();
		return jmolCompoundIndexToCluster.get(jmolIndex);
	}

	public int indexOf(Cluster cluster)
	{
		update();
		return clusterIndices.get(cluster);
	}

	public int getClusterIndexForCompound(Compound compound)
	{
		return indexOf(getClusterForCompound(compound));
	}

	public int getClusterIndexForJmolIndex(int jmolIndex)
	{
		return indexOf(getClusterForJmolIndex(jmolIndex));
	}

	public Compound getCompoundWithJmolIndex(int jmolIndex)
	{
		update();
		return jmolCompoundIndexToCompound.get(jmolIndex);
	}

	public Compound[] getCompoundsWithJmolIndices(int[] idx)
	{
		Compound c[] = new Compound[idx.length];
		for (int i = 0; i < c.length; i++)
			c[i] = getCompoundWithJmolIndex(idx[i]);
		return c;
	}

	public int[] getJmolIndicesWithCompounds(Compound[] c)
	{
		int idx[] = new int[c.length];
		for (int i = 0; i < c.length; i++)
			idx[i] = c[i].getJmolIndex();
		return idx;
	}

	public synchronized int numMissingValues(CompoundProperty p)
	{
		update();
		if (!numMissingValues.containsKey(p))
		{
			int num = 0;
			for (Cluster c : clusters)
				num += c.numMissingValues(p);
			numMissingValues.put(p, num);
		}
		return numMissingValues.get(p);
	}

	public synchronized int numDistinctValues(CompoundProperty p)
	{
		update();
		if (!numDistinctValues.containsKey(p))
		{
			int numDistinct;
			if (p instanceof NumericProperty)
				numDistinct = CompoundPropertyUtil.computeNumDistinct(getDoubleValues((NumericProperty) p));
			else
				numDistinct = CompoundPropertyUtil.computeNumDistinct(getStringValues((NominalProperty) p, null));
			numDistinctValues.put(p, numDistinct);
		}
		return numDistinctValues.get(p);
	}

	@Override
	public int getNumCompounds()
	{
		return numCompounds();
	}

	public int numCompounds()
	{
		update();
		return numCompounds;
	}

	@Override
	public CountedSet<String> getNominalSummary(NominalProperty p)
	{
		return clusteringValues.getNominalSummary(p);
	}

	public int numClusters()
	{
		return clusters.size();
	}

	public BitSet getBitSetAll()
	{
		update();
		return bitSetAll;
	}

	public synchronized void clear()
	{
		checkNoSelection();

		@SuppressWarnings("unchecked")
		final Vector<Cluster> old = (Vector<Cluster>) VectorUtil.clone(ClusteringImpl.this.clusters);
		clusters.removeAllElements();
		clusteringData = null;
		View.instance.zap(true, true, true);
		filteredCompoundList.clear();
		filteredCompoundListIncludingMultiClusteredCompounds.clear();
		clusteringValues.clear();
		compoundSelections.clear();
		dirty = true;

		jittering = null;

		getClusterActive().clearSelection();
		fire(CLUSTER_CLEAR, old, clusters);
	}

	public void removeCluster(final Cluster... clusters)
	{
		checkNoSelection();

		@SuppressWarnings("unchecked")
		Vector<Cluster> old = (Vector<Cluster>) VectorUtil.clone(ClusteringImpl.this.clusters);
		for (Cluster c : clusters)
		{
			View.instance.hide(c.getBitSet());
			ClusteringImpl.this.clusters.remove(c);
		}
		dirty = true;
		updatePositions();
		if (getNumClusters() == 1)
			getClusterActive().setSelected(0);
		fire(CLUSTER_REMOVED, old, clusters);
	}

	public Cluster getCluster(int clusterIndex)
	{
		if (clusterIndex < 0)
			return null;
		return clusters.get(clusterIndex);
	}

	public Vector<Cluster> getClusters()
	{
		return clusters;
	}

	@SuppressWarnings("unchecked")
	public void newClustering(ClusteringData d)
	{
		//		@SuppressWarnings("unchecked")
		Vector<Cluster> old = (Vector<Cluster>) VectorUtil.clone(ClusteringImpl.this.clusters);
		suppresAddEvent = true;

		clusteringData = d;

		{
			String filename = d.getSDF();
			TaskProvider.debug("Load dataset into Jmol");

			if (View.instance != null) // for export without graphics
			{
				View.instance.loadCompoundFromFile(null, filename, null, null, false, null, null, 0);
				if (d.getNumCompounds(true) != View.instance.getCompoundCount())
					throw new Error("illegal num compounds, loaded by Jmol: " + View.instance.getCompoundCount()
							+ " != from wizard: " + d.getNumCompounds(true));
			}
		}

		for (int i = 0; i < d.getNumClusters(); i++)
			addSingleCluster(d.getCluster(i));

		{
			//data sanity checks
			List<Integer> jmol = new ArrayList<Integer>();
			List<Integer> orig = new ArrayList<Integer>();
			for (int i = 0; i < d.getNumClusters(); i++)
			{
				jmol = ListUtil.concat(jmol, d.getCluster(i).getCompoundClusterIndices());
				orig = ListUtil.concat(orig, d.getCluster(i).getCompoundOrigIndices());
			}
			int num_mult = clusteringData.getNumCompounds(true);
			int num_uniq = clusteringData.getNumCompounds(false);
			if (jmol.size() != orig.size())
				throw new IllegalStateException("internal error, num compounds #1 " + jmol.size() + ", " + orig.size());
			if (jmol.size() != num_mult)
				throw new IllegalStateException("internal error, num compounds #2 " + jmol.size() + ", " + num_mult);
			if (isClusterAlgorithmDisjoint())
			{
				if (num_mult != num_uniq)
					throw new IllegalStateException("internal error, num compounds #3 " + num_mult + ", " + num_uniq);
			}
			else
			{
				if (num_mult <= num_uniq)
					throw new IllegalStateException("internal error, num compounds #4 " + num_mult + ", " + num_uniq);
				orig = ListUtil.uniqValue(orig);
				if (orig.size() != num_uniq)
					throw new IllegalStateException("internal error, num compounds #5 " + orig.size() + ", " + num_uniq);
				jmol = ListUtil.uniqValue(jmol);
				if (jmol.size() != num_mult)
					throw new IllegalStateException("internal error, num compounds #6 " + jmol.size() + ", " + num_mult);
			}
			Collections.sort(jmol);
			Collections.sort(orig);
			int i;
			for (i = 0; i < num_uniq; i++)
				if (i != jmol.get(i) || i != orig.get(i))
					throw new IllegalStateException("internal error, num compounds #7 " + i + ", " + jmol.get(i) + ", "
							+ orig.get(i));
			if (!isClusterAlgorithmDisjoint())
				for (; i < num_mult; i++)
					if (i != jmol.get(i))
						throw new IllegalStateException("internal error, num compounds #8 " + i + ", " + jmol.get(i));
		}

		update();
		if (View.instance != null) // for export without graphics
		{
			TaskProvider.update(90, "Loading graphics");
			updatePositions();
		}

		suppresAddEvent = false;

		fire(CLUSTER_ADDED, old, clusters);
		fire(CLUSTER_NEW, old, clusters);

		if (View.instance != null) // for export without graphics
			View.instance.scriptWait("hover off");
	}

	public synchronized void updatePositions()
	{
		update();

		ClusteringUtil.updateScaleFactor(this);

		//		SwingUtil.invokeAndWait(new Runnable()
		//		{
		//			public void run()
		//			{
		getClusterWatched().clearSelection();
		getCompoundWatched().clearSelection();
		//			}
		//		});

		View.instance.suspendAnimation("updating clustering positions");
		for (Cluster c : clusters)
		{
			if (!superimposed)
				setClusterOverlap(c, true, null);
			c.updatePositions();
			if (!superimposed)
				setClusterOverlap(c, false, null);
		}
		View.instance.proceedAnimation("updating clustering positions");

		Vector3f[] positions = ArrayUtil.toArray(ClusteringUtil.getClusterPositions(this));
		superimposedCenter = Vector3fUtil.centerConvexHull(positions);

		// take only cluster points into account (ignore cluster sizes)
		superimposedDiameter = Vector3fUtil.maxDist(positions);
		// needed for very small distances / only one cluster : diameter should be at least as big as cluster diameter
		for (Cluster c : clusters)
			superimposedDiameter = Math.max(superimposedDiameter, c.getDiameter(true));

		// take only compound positions into account (ignore compound sizes)
		positions = ClusteringUtil.getCompoundPositions(this);
		nonSuperimposedCenter = Vector3fUtil.centerConvexHull(positions);
		nonSuperimposedDiameter = Vector3fUtil.maxDist(positions);

		//if all is filtered apart from compounds that share a single position nonSuperimposeDiameter would be 0
		nonSuperimposedDiameter = Math.max(nonSuperimposedDiameter, superimposedDiameter);
	}

	/**
	 * toggles compound positions between compound position (overlap=false) and cluster position (overlap=true) 
	 * 
	 * @param clusters
	 * @param overlap
	 * @param anim
	 */
	public void setClusterOverlap(List<Cluster> clusters, boolean overlap, View.AnimationSpeed anim)
	{
		List<Vector3f> compoundPositions = new ArrayList<Vector3f>();
		List<BitSet> bitsets = new ArrayList<BitSet>();

		for (Cluster cluster : clusters)
		{
			if (cluster.isSuperimposed() != overlap)
			{
				for (int i = 0; i < cluster.getNumCompounds(); i++)
				{
					bitsets.add(cluster.getCompound(i).getBitSet());

					// destination is compound position
					Vector3f pos = cluster.getCompound(i).getPosition();
					// compound is already at cluster position, sub to get relative vector
					pos.sub(cluster.getCenter(true));
					if (overlap)
						pos.scale(-1);
					compoundPositions.add(pos);
				}
			}
			cluster.setSuperimposed(overlap);
		}
		View.instance.setAtomCoordRelative(compoundPositions, bitsets, anim);
	}

	/**
	 * not animated
	 * 
	 * @param comp
	 * @param enable
	 */
	public void moveForDotMode(Compound comp, boolean enable)
	{
		// move to compound center ...
		Vector3f pos = new Vector3f(comp.origCenter);
		// ... from single atom position
		pos.sub(comp.origDotPosition);
		// reverse if neccessary
		if (!enable)
			pos.scale(-1);
		View.instance.setAtomCoordRelative(pos, comp.getDotModeDisplayBitSet());
	}

	public void setClusterOverlap(Cluster cluster, boolean overlap, View.AnimationSpeed anim)
	{
		List<Cluster> l = new ArrayList<Cluster>();
		l.add(cluster);
		setClusterOverlap(l, overlap, anim);
	}

	public int[] clusterChooser(String title, String description)
	{
		int clusterIndex = getClusterActive().getSelected();
		if (clusterIndex == -1)
			clusterIndex = getClusterWatched().getSelected();

		Cluster c[] = new Cluster[numClusters()];
		for (int i = 0; i < c.length; i++)
			c[i] = getCluster(i);
		boolean b[] = new boolean[numClusters()];
		if (clusterIndex != -1)
			b[clusterIndex] = true;

		return CheckBoxSelectDialog.selectIndices(Settings.TOP_LEVEL_FRAME, title, description, c, b);
	}

	public int[] selectJmolIndicesWithCompoundChooser(String title, String description)
	{
		int clusterIndex = getClusterActive().getSelected();
		if (clusterIndex == -1)
			clusterIndex = getClusterWatched().getSelected();

		List<Compound> l = new ArrayList<Compound>();
		List<Boolean> lb = new ArrayList<Boolean>();

		for (int i = 0; i < numClusters(); i++)
		{
			Cluster c = getCluster(i);
			for (int j = 0; j < c.getNumCompounds(); j++)
			{
				l.add(c.getCompound(j));
				lb.add(clusterIndex == -1 || clusterIndex == i);
			}
		}
		Compound m[] = new Compound[l.size()];
		int selectedIndices[] = CheckBoxSelectDialog.selectIndices(Settings.TOP_LEVEL_FRAME, title, description,
				l.toArray(m), ArrayUtil.toPrimitiveBooleanArray(lb));
		return selectedIndices;
	}

	public void chooseClustersToExport(CompoundProperty compoundDescProp)
	{
		int[] indices = clusterChooser("Export Cluster/s",
				"Select the clusters you want to export. The compounds will be stored in a single SDF/CSV file.");
		if (indices != null)
			ExportData.exportClusters(this, indices, compoundDescProp);
	}

	public void chooseCompoundsToExport(CompoundProperty compoundDescProp)
	{
		int indices[] = selectJmolIndicesWithCompoundChooser("Export Compounds/s",
				"Select the compounds you want to export. The compounds will be stored in a single SDF/CSV file.");
		if (indices == null)
			return;
		List<Integer> l = new ArrayList<Integer>();
		for (int i = 0; i < indices.length; i++)
			l.add(getCompoundWithJmolIndex(indices[i]).getOrigIndex());
		ExportData.exportCompoundsWithOrigIndices(this, l, compoundDescProp);
	}

	public String getName()
	{
		if (clusteringData != null)
			return clusteringData.getName();
		else
			return null;
	}

	public String getFullName()
	{
		if (clusteringData != null)
			return clusteringData.getFullName();
		else
			return null;
	}

	public String getOrigSDFile()
	{
		return clusteringData.getOrigSDF();
	}

	public String getSDFile()
	{
		return clusteringData.getSDF();
	}

	private void checkNoSelection()
	{
		if (compoundActive.getSelected() != -1 || compoundWatched.getSelected() != -1
				|| clusterWatched.getSelected() != -1 || (clusterActive.getSelected() != -1 && getNumClusters() > 1))
			throw new IllegalStateException("clear selection before");
	}

	public void removeCompoundsWithJmolIndices(int jmolIndices[])
	{
		checkNoSelection();

		LinkedHashMap<Cluster, List<Integer>> toDel = new LinkedHashMap<Cluster, List<Integer>>();

		// assign indices to clusters
		for (int i = 0; i < jmolIndices.length; i++)
		{
			Cluster c = getCluster(getClusterIndexForJmolIndex(jmolIndices[i]));
			List<Integer> l = toDel.get(c);
			if (l == null)
			{
				l = new ArrayList<Integer>();
				toDel.put(c, l);
			}
			l.add(jmolIndices[i]);
		}

		// delete clusterwise
		boolean clusterModified = false;
		Cluster clusToDel[] = new Cluster[0];
		for (Cluster c : toDel.keySet())
		{
			int indices[] = ArrayUtil.toPrimitiveIntArray(toDel.get(c));
			if (indices.length == c.getNumCompounds())
				clusToDel = ArrayUtil.concat(Cluster.class, clusToDel, new Cluster[] { c });
			else
			{
				c.removeWithJmolIndices(indices);
				dirty = true;
				clusterModified = true;
			}
		}
		if (clusToDel.length > 0)
			removeCluster(clusToDel);
		if (clusterModified)
		{
			updatePositions();
			fire(CLUSTER_MODIFIED, null, null);
		}
	}

	public SubstructureSmartsType getSubstructureSmartsType()
	{
		return clusteringData.getThreeDAligner().getSubstructureSmartsType();
	}

	public List<CompoundProperty> getFeatures()
	{
		return clusteringData.getFeatures();
	}

	public List<CompoundProperty> getProperties()
	{
		return clusteringData.getProperties();
	}

	public List<CompoundData> getCompounds()
	{
		return clusteringData.getCompounds();
	}

	public int getNumClusters()
	{
		return clusters.size();
	}

	public int getNumCompounds(boolean includingMultiClusteredCompounds)
	{
		return getCompounds(includingMultiClusteredCompounds).size();
	}

	@Override
	public int getNumUnfilteredCompounds(boolean includingMultiClusteredCompounds)
	{
		return clusteringData.getNumCompounds(includingMultiClusteredCompounds);
	}

	public List<Compound> getCompounds(boolean includingMultiClusteredCompounds)
	{
		update();
		if (includingMultiClusteredCompounds)
			return filteredCompoundListIncludingMultiClusteredCompounds;
		else
			return filteredCompoundList;
	}

	public String[] getStringValues(NominalProperty property, Compound excludeCompound)
	{
		return getStringValues(property, excludeCompound, false);
	}

	public String[] getStringValues(NominalProperty property, Compound excludeCompound, boolean formatted)
	{
		List<String> l = new ArrayList<String>();
		for (Compound m : getCompounds(false))
			if (m != excludeCompound && m.getStringValue(property) != null)
				l.add(formatted ? m.getFormattedValue(property) : m.getStringValue(property));
		String v[] = new String[l.size()];
		return l.toArray(v);
	}

	public Double[] getDoubleValues(NumericProperty property)
	{
		Double v[] = new Double[getNumCompounds(false)];
		int i = 0;
		for (Compound m : getCompounds(false))
			v[i++] = m.getDoubleValue(property);
		return v;
	}

	public String getClusterAlgorithm()
	{
		return clusteringData.getDatasetClusterer().getName();
	}

	public boolean isClusterAlgorithmDisjoint()
	{
		return clusteringData.getDatasetClusterer().isDisjointClusterer();
	}

	public String getEmbedAlgorithm()
	{
		return clusteringData.getThreeDEmbedder().getName();
	}

	public String getEmbedQuality()
	{
		return clusteringData.getEmbedQuality();
	}

	@Override
	public boolean isRandomEmbedding()
	{
		return clusteringData != null && clusteringData.getThreeDEmbedder() instanceof Random3DEmbedder;
	}

	@Override
	public Vector3f getCenter(boolean superimposed)
	{
		if (superimposed)
			return superimposedCenter;
		else
			return nonSuperimposedCenter;
	}

	@Override
	public float getDiameter(boolean superimposed)
	{
		if (superimposed)
			return superimposedDiameter;
		else
			return nonSuperimposedDiameter;
	}

	@Override
	public boolean isSuperimposed()
	{
		return superimposed;
	}

	public void setSuperimposed(boolean superimposed)
	{
		this.superimposed = superimposed;
	}

	public synchronized Double getNormalizedDoubleValue(CompoundPropertyOwner m, NumericProperty p)
	{
		return clusteringValues.getNormalizedDoubleValue(m, p);
	}

	public synchronized Double getNormalizedLogDoubleValue(CompoundPropertyOwner m, NumericProperty p)
	{
		return clusteringValues.getNormalizedLogDoubleValue(m, p);
	}

	@Override
	public Double getDoubleValue(NumericProperty p)
	{
		return clusteringValues.getDoubleValue(p);
	}

	@Override
	public String getFormattedValue(CompoundProperty p)
	{
		return getSummaryStringValue(p, false);
	}

	public String getOrigLocalPath()
	{
		return clusteringData.getOrigLocalPath();
	}

	@Override
	public List<CompoundProperty> getAdditionalProperties()
	{
		return clusteringData.getAdditionalProperties();
	}

	public CorrelationProperty getEmbeddingQualityProperty()
	{
		return clusteringData.getEmbeddingQualityProperty();
	}

	@Override
	public EqualPositionProperty getEqualPosProperty()
	{
		return clusteringData.getEqualPosProperty();
	}

	//	public CompoundProperty[] getAppDomainProperties()
	//	{
	//		return clusteringData.getAppDomainProperties();
	//	}
	//
	//	public List<CompoundProperty> getDistanceToProperties()
	//	{
	//		return clusteringData.getDistanceToProperties();
	//	}

	@Override
	public double getSpecificity(CompoundSelection c, CompoundProperty p)
	{
		return clusteringValues.getSpecificity(c, p);
	}

	@Override
	public double getSpecificity(Cluster c, CompoundProperty p)
	{
		return clusteringValues.getSpecificity(c, p);
	}

	public double getSpecificity(Compound m, CompoundProperty p)
	{
		return clusteringValues.getSpecificity(m, p);
	}

	public String getSummaryStringValue(CompoundProperty p, boolean html)
	{
		return clusteringValues.getSummaryStringValue(p, html);
	}

	public void initFeatureNormalization()
	{
		clusteringValues.initFeatureNormalization();
	}

	public Cluster getUniqueClusterForCompounds(Compound[] c)
	{
		Cluster c1 = null;
		for (int i = 0; i < c.length; i++)
		{
			Cluster c2 = getClusterForCompound(c[i]);
			if (c1 == null || c1 == c2)
				c1 = c2;
			else
				return null;
		}
		return c1;
	}

	public Cluster getUniqueClusterForJmolIndices(int[] c)
	{
		Cluster c1 = null;
		for (int i = 0; i < c.length; i++)
		{
			Cluster c2 = getClusterForJmolIndex(c[i]);
			if (c1 == null || c1 == c2)
				c1 = c2;
			else
				return null;
		}
		return c1;
	}

	public Cluster getExactClusterForCompounds(List<Compound> compounds)
	{
		int idx[] = new int[compounds.size()];
		for (int i = 0; i < idx.length; i++)
			idx[i] = compounds.get(i).getJmolIndex();
		return getExactClusterForJmolIndices(idx);
	}

	public Cluster getExactClusterForJmolIndices(int[] jmolIndices)
	{
		Cluster c1 = null;
		for (int i = 0; i < jmolIndices.length; i++)
		{
			Cluster c2 = getClusterForJmolIndex(jmolIndices[i]);
			if (c1 == null || c1 == c2)
				c1 = c2;
			else
				return null;
		}
		if (c1.getNumCompounds() == jmolIndices.length)
			return c1;
		else
			return null;
	}

	/**
	 * warning: requires to update compound positions after compounds are made visible again
	 * 
	 * @param filter
	 */
	public void setCompoundFilter(CompoundFilter filter)
	{
		for (Cluster c : clusters)
			c.setFilter(filter);
		clusteringValues.clear();
		dirty = true;
		//		updatePositions();
	}

	public int getNumOrigCompounds(boolean includingMultiClusteredCompounds)
	{
		return clusteringData.getNumCompounds(includingMultiClusteredCompounds);
	}

	@Override
	public Cluster getActiveCluster()
	{
		return getCluster(getClusterActive().getSelected());
	}

	@Override
	public int getActiveClusterIdx()
	{
		return getClusterActive().getSelected();
	}

	@Override
	public Cluster getWatchedCluster()
	{
		return getCluster(getClusterWatched().getSelected());
	}

	@Override
	public int getWatchedClusterIdx()
	{
		return getClusterWatched().getSelected();
	}

	@Override
	public boolean isCompoundActive()
	{
		return getCompoundActive().getSelected() != -1;
	}

	@Override
	public boolean isCompoundActive(Compound c)
	{
		return getCompoundActive().isSelected(c.getJmolIndex());
	}

	@Override
	public Compound[] getActiveCompounds()
	{
		return getCompoundsWithJmolIndices(getCompoundActive().getSelectedIndices());
	}

	@Override
	public Compound getActiveCompound()
	{
		return getCompoundWithJmolIndex(getCompoundActive().getSelected());
	}

	@Override
	public int[] getActiveCompoundsJmolIdx()
	{
		return getCompoundActive().getSelectedIndices();
	}

	@Override
	public Compound getWatchedCompound()
	{
		return getCompoundWithJmolIndex(getCompoundWatched().getSelected());
	}

	@Override
	public Compound[] getWatchedCompounds()
	{
		return getCompoundsWithJmolIndices(getCompoundWatched().getSelectedIndices());
	}

	@Override
	public int[] getWatchedCompoundsJmolIdx()
	{
		return getCompoundWatched().getSelectedIndices();
	}

	@Override
	public List<CompoundProperty> getPropertiesAndFeatures()
	{
		return ListUtil.concat(getProperties(), getFeatures());
	}

	@Override
	public List<CompoundProperty> selectPropertiesAndFeaturesWithDialog(String title, CompoundProperty preselected,
			boolean addSmiles, boolean addEmbeddingStress, boolean addActivityCliffs, boolean addDistanceTo)
	{
		List<CompoundProperty> props = new ArrayList<CompoundProperty>();
		for (CompoundProperty p : getAdditionalProperties())
		{
			if (addEmbeddingStress && p instanceof CorrelationProperty)
				props.add(p);
			else if (addActivityCliffs && p instanceof SALIProperty)
				props.add(p);
			else if (addDistanceTo && p instanceof DistanceToProperty)
				props.add(p);
		}
		for (CompoundProperty p : getProperties())
			if (addSmiles || (p.getCompoundPropertySet() == null || !p.getCompoundPropertySet().isSmiles()))
				props.add(p);
		for (CompoundProperty p : getFeatures())
			props.add(p);
		if (props.size() == 0)
			return props;
		else
		{
			boolean selection[] = new boolean[props.size()];
			if (preselected == null)
				Arrays.fill(selection, true);
			else
			{
				for (int i = 0; i < props.size(); i++)
					if (preselected == props.get(i))
						selection[i] = true;
			}
			Object sel[] = CheckBoxSelectDialog.select(Settings.TOP_LEVEL_FRAME, title, null,
					ArrayUtil.toArray(CompoundProperty.class, props), selection);
			if (sel == null)
				return null;
			return ArrayUtil.toList(ArrayUtil.cast(CompoundProperty.class, sel));
		}
	}

	@Override
	public void addSelectionListener(final SelectionListener l)
	{
		getClusterActive().addListener(new PropertyChangeListener()
		{
			@Override
			public void propertyChange(PropertyChangeEvent evt)
			{
				l.clusterActiveChanged(getCluster(getClusterActive().getSelected()));
			}
		});
		getClusterWatched().addListener(new PropertyChangeListener()
		{
			@Override
			public void propertyChange(PropertyChangeEvent evt)
			{
				l.clusterWatchedChanged(getCluster(getClusterWatched().getSelected()));
			}
		});
		getCompoundActive().addListener(new PropertyChangeListener()
		{
			@Override
			public void propertyChange(PropertyChangeEvent evt)
			{
				l.compoundActiveChanged(getCompoundsWithJmolIndices(getCompoundActive().getSelectedIndices()));
			}
		});
		getCompoundWatched().addListener(new PropertyChangeListener()
		{
			@Override
			public void propertyChange(PropertyChangeEvent evt)
			{
				l.compoundWatchedChanged(getCompoundsWithJmolIndices(getCompoundWatched().getSelectedIndices()));
			}
		});
	}

	// hack for BMBF datasets
	@Override
	public boolean isBMBFRealEndpointDataset(boolean filtered)
	{
		if (endpointDataset == null)
		{
			Set<String> realProps = new HashSet<String>();
			Set<String> filledProps = new HashSet<String>();
			Set<String> otherProps = new HashSet<String>();
			for (CompoundProperty p : getProperties())
			{
				String n = p.getName();
				if (n.endsWith("_real"))
					realProps.add(n.substring(0, n.length() - 5));
				else if (n.endsWith("_filled"))
					filledProps.add(n.substring(0, n.length() - 7));
				else
					otherProps.add(n);
			}
			endpointDataset = realProps.size() > 0;
			filledEndpointDataset = filledProps.size() > 0 && filledProps.size() == realProps.size();
			for (String n : realProps)
			{
				if (!otherProps.contains(n))
				{
					endpointDataset = false;
					filledEndpointDataset = false;
					break;
				}
				if (!filledProps.contains(n))
					filledEndpointDataset = false;
			}
		}
		if (filtered)
			return filledEndpointDataset;
		else
			return endpointDataset;
	}

	@Override
	public CompoundProperty addSALIFeatures(CompoundProperty p)
	{
		CompoundProperty oldProps[] = new CompoundProperty[0];
		for (CompoundProperty prop : getAdditionalProperties())
			if (prop instanceof SALIProperty)
				oldProps = ArrayUtil.push(CompoundProperty.class, oldProps, prop);

		Double d[] = new Double[getNumCompounds(true)];
		String domain[] = null;
		if (p instanceof NominalProperty)
		{
			domain = ((NominalProperty) p).getDomain();
			if (domain.length != 2)
				throw new IllegalArgumentException("not yet implemented");
		}

		for (int i = 0; i < getNumCompounds(false); i++)
		{
			Compound c = null;
			for (Compound comp : getCompounds(false))
			{
				if (comp.getOrigIndex() == i)
				{
					c = comp;
					break;
				}
			}
			if (p instanceof NumericProperty)
			{
				if (c.getDoubleValue((NumericProperty) p) != null)
					d[i] = getNormalizedDoubleValue(c, (NumericProperty) p);
			}
			else
			{
				if (c.getStringValue((NominalProperty) p) != null)
					d[i] = (double) ArrayUtil.indexOf(domain, c.getStringValue((NominalProperty) p));
			}
		}

		List<SALIProperty> l = SALIProperty.create(d, clusteringData.getFeatureDistanceMatrix().getValues(),
				p.toString());
		addNewAdditionalProperties(ListUtil.toArray(l), oldProps);
		return l.get(0);
	}

	@Override
	public Double getFeatureDistance(int origIndex, int origIndex2)
	{
		return clusteringData.getFeatureDistance(origIndex, origIndex2);
	}

	@Override
	public CompoundProperty addDistanceToCompoundFeature(Compound comp)
	{
		for (CompoundProperty prop : getAdditionalProperties())
		{
			if (prop instanceof DistanceToProperty && ((DistanceToProperty) prop).getCompound() == comp)
				return prop;
		}
		Double d[] = new Double[getNumUnfilteredCompounds(true)];
		for (int i = 0; i < d.length; i++)
			d[i] = clusteringData.getFeatureDistance(comp.getOrigIndex(), getCompounds().get(i).getOrigIndex());

		DistanceToProperty dp = new DistanceToProperty(comp, clusteringData.getEmbeddingDistanceMeasure(), d);
		addNewAdditionalProperty(dp, null);
		return dp;
	}

	HashMap<NumericProperty, NumericProperty> logProps = new HashMap<NumericProperty, NumericProperty>();

	public static class LogProperty extends DefaultNumericProperty
	{
		public LogProperty(NumericProperty p)
		{
			super("Log(" + p.getName() + ")", "Log conversion (to base 10) of " + p.getName(), ArrayUtil.log(p
					.getDoubleValues()));
		}
	}

	@Override
	public NumericProperty addLogFeature(NumericProperty p)
	{
		if (!logProps.containsKey(p))
		{
			NumericProperty l = new LogProperty(p);
			addNewAdditionalProperty(l, null);
			logProps.put(p, l);
		}
		return logProps.get(p);
	}

	CompoundProperty pred;
	CompoundProperty predMis;

	@Override
	public void predict()
	{
		CompoundProperty clazz = null;
		boolean classification = true;
		for (CompoundProperty p : getProperties())
		{
			if (p.toString().matches("(?i).*activity.*"))
				clazz = p;
			else if (p.toString().equals("LC50_mmol_log"))
			{
				clazz = p;
				classification = false;
			}
		}
		if (clazz == null)
			throw new Error();
		addPredictionFeature(clazz, Predictor.predict(getCompounds(), getFeatures(), clazz, classification));
	}

	@Override
	public void computeAppDomain()
	{
		List<NumericProperty> feats = new ArrayList<NumericProperty>();
		for (CompoundProperty p : getFeatures())
			if (p instanceof NumericProperty && p.numMissingValues() == 0 && p.numDistinctValues() >= 2)
				feats.add((NumericProperty) p);

		//AppDomainComputer appDomain[] = new AppDomainComputer[] { AppDomainHelper.select() };

		List<CompoundProperty> props = new ArrayList<CompoundProperty>();

		AppDomainComputer appDomain[] = AppDomainComputer.APP_DOMAIN_COMPUTERS;
		if (appDomain != null && appDomain.length > 0)
		{
			for (AppDomainComputer ad : appDomain)
			{
				ad.computeAppDomain(getCompounds(), feats, clusteringData.getFeatureDistanceMatrix().getValues());
				props.add(ad.getInsideAppDomainProperty());
				props.add(ad.getPropabilityAppDomainProperty());
			}
		}

		addNewAdditionalProperties(ListUtil.toArray(CompoundProperty.class, props), null);
	}

	@Override
	public void addPredictionFeature(CompoundProperty clazz, PredictionResult p)
	{
		CompoundProperty pred = p.createFeature();
		addNewAdditionalProperty(pred, this.pred);
		this.pred = pred;

		CompoundProperty predMis = p.createMissclassifiedFeature();
		addNewAdditionalProperty(predMis, this.predMis);
		this.predMis = predMis;
	}

	private void addNewAdditionalProperty(CompoundProperty p, CompoundProperty old)
	{
		addNewAdditionalProperties(new CompoundProperty[] { p }, new CompoundProperty[] { old });
	}

	private void addNewAdditionalProperties(CompoundProperty props[], CompoundProperty oldProps[])
	{
		for (CompoundProperty p : props)
		{
			int i = 0;
			for (CompoundData cc : getCompounds())
			{
				CompoundDataImpl c = (CompoundDataImpl) cc;
				if (p instanceof NumericProperty)
				{
					c.setDoubleValue(p, ((NumericProperty) p).getDoubleValues()[i]);
					c.setNormalizedValueCompleteDataset(p, ((NumericProperty) p).getNormalizedValues()[i]);
				}
				else if (p instanceof NominalProperty)
				{
					c.setStringValue(p, ((NominalProperty) p).getStringValues()[i]);
				}
				i++;
			}
		}
		if (oldProps != null)
			for (CompoundProperty old : oldProps)
				clusteringData.getAdditionalProperties().remove(old);
		for (CompoundProperty p : props)
			clusteringData.getAdditionalProperties().add(p);
		dirty = true;
		if (View.instance != null) // for export
			fire(PROPERTY_ADDED, true, false);
	}

	private HashMap<String, CompoundSelection> compoundSelections = new HashMap<String, CompoundSelection>();

	public CompoundSelection getCompoundSelection(Compound[] c)
	{
		StringBuffer key = new StringBuffer();
		for (Compound compound : c)
			key.append(compound.getJmolIndex());
		String k = key.toString();
		if (!compoundSelections.containsKey(k))
		{
			CompoundSelection sel = new CompoundSelection(c);
			clusteringValues.initSelectionNormalization(sel);
			compoundSelections.put(k, sel);
		}
		return compoundSelections.get(k);
	}

	@Override
	public CompoundProperty getHighlightProperty()
	{
		return highlightProperty;
	}

	public void setHighlighProperty(CompoundProperty prop, Color highlightColorText)
	{
		this.highlightProperty = prop;
		this.highlightColorText = highlightColorText;
	}

	@Override
	public Color getHighlightColorText()
	{
		return highlightColorText;
	}

	@Override
	public boolean isSkippingRedundantFeatures()
	{
		return clusteringData.isSkippingRedundantFeatures();
	}

	@Override
	public boolean isBigDataMode()
	{
		return Settings.BIG_DATA;
	}

	public void updateJittering(int level, Set<Compound> compounds)
	{
		if (jittering == null)
			jittering = new JitteringProvider(this);
		jittering.updateJittering(level, compounds);
	}

	public int getJitteringResetLevel(Set<Compound> compounds)
	{
		if (jittering == null)
			return -1;
		return jittering.getJitteringResetLevel(compounds);
	}

	@Override
	public boolean doCheSMappingWarningsExist()
	{
		return clusteringData != null && clusteringData.doCheSMappingWarningsExist();
	}

	@Override
	public void showCheSMappingWarnings()
	{
		clusteringData.showCheSMappingWarnings();
	}
}
 public class ClusteringUtil
{
	public static List<Vector3f> getClusterPositions(ClusteringImpl c)
	{
		List<Vector3f> list = new ArrayList<Vector3f>();
		for (Cluster cc : c.getClusters())
			if (cc.getNumCompounds() > 0)
				list.add(cc.getCenter(true));
		return list;
	}

	public static List<Vector3f> getCompoundPositions(ZoomableCompoundGroup c)
	{
		List<Vector3f> list = new ArrayList<Vector3f>();
		for (Compound m : c.getCompounds())
			list.add(m.getPosition());
		return list;
	}

	public static Vector3f[] getCompoundPositions(ClusteringImpl c)
	{
		return getCompoundPositions(c, true);
	}

	private static Vector3f[] getCompoundPositions(ClusteringImpl c, boolean scale)
	{
		Vector3f list[] = new Vector3f[c.getNumCompounds(true)];
		int i = 0;
		for (Compound m : c.getCompounds(true))
			list[i++] = m.getPosition(scale);
		return list;
	}

	public static int COMPOUND_SIZE_MAX = 40;
	public static int COMPOUND_SIZE = 20;
	public static float SCALE = 0;

	/**
	 * scale factor is used to scale the original 3d mapping values
	 * 
	 * @param v
	 * @return
	 */
	public static void updateScaleFactor(ClusteringImpl c)
	{
		Vector3f[] v = ClusteringUtil.getCompoundPositions(c, false);

		// the average min distance mean distance of each compound to its closest neighbor compound
		float d = Vector3fUtil.avgMinDist(v);
		if (d == 0)
			d = 1;

		// the smaller the distance, the higher the scale factor
		// the neigbhor should be on average 30units away
		float s = 1 / d * 30;
		//Settings.LOGGER.debug("min avg distance: " + d + " -> scale: " + s);

		// we want to limit the scale based on the max dist
		float maxD = Vector3fUtil.maxDist(v);
		float max_scale = 100 / maxD;
		if (max_scale < s)
		{
			//Settings.LOGGER.debug("override scale\nmax distance: " + maxD + " -> scale: " + max_scale);
			s = max_scale;
		}

		if (COMPOUND_SIZE < 0 || COMPOUND_SIZE > COMPOUND_SIZE_MAX)
			throw new Error("illegal compound size");
		// convert "int range 0 - COMPOUND_SIZE_MAX" to "float range 4.0 - 0.1"  
		float density = (float) (((1 - COMPOUND_SIZE / ((double) COMPOUND_SIZE_MAX)) * 3.9f) + 0.1f);
		//Settings.LOGGER.debug("compound size: " + ClusteringUtil.COMPOUND_SIZE + " -> scale multiplier: " + density);

		// scale is multiplied with the DENSITY, which is configurable by the user
		SCALE = s * density;
	}

} public class Compound implements Zoomable, Comparable<Compound>, DoubleNameElement, SingleCompoundPropertyOwner
{
	private BitSet bitSet;
	private BitSet dotModeHideBitSet;
	private BitSet dotModeDisplayBitSet;

	private int jmolIndex;
	private CompoundData compoundData;

	private Translucency translucency = Translucency.None;
	private String label = null;
	private boolean showHoverBox = false;
	private boolean showActiveBox = false;
	private String smarts = null;

	private HashMap<String, BitSet> smartsMatches;

	private String compoundColor;
	private String highlightColorString;
	private Color highlightColorText;
	private String lastHighlightColorString;

	private Vector3f spherePosition;
	private CompoundProperty highlightCompoundProperty;
	private Style style;
	private CompoundProperty descriptorProperty = null;
	private boolean sphereVisible;
	private boolean lastFeatureSphereVisible;
	private boolean visible = true;
	private boolean featureSortingEnabled = true;

	private float diameter = -1;

	public final Vector3f origCenter;
	public final Vector3f origDotPosition;

	public Compound(int jmolIndex, CompoundData compoundData)
	{
		this.jmolIndex = jmolIndex;
		this.compoundData = compoundData;
		if (View.instance != null)
		{
			bitSet = View.instance.getCompoundBitSet(getJmolIndex());
			dotModeHideBitSet = View.instance.getDotModeHideBitSet(bitSet);
			dotModeDisplayBitSet = View.instance.getDotModeDisplayBitSet(bitSet);
			origCenter = new Vector3f(View.instance.getAtomSetCenter(bitSet));
			origDotPosition = new Vector3f(View.instance.getAtomSetCenter(getDotModeDisplayBitSet()));
		}
		else
		{
			//for export without graphics
			origCenter = null;
			origDotPosition = null;
		}
		smartsMatches = new HashMap<String, BitSet>();
		setDescriptor(ViewControler.COMPOUND_INDEX_PROPERTY);
	}

	public String getFormattedValue(CompoundProperty property)
	{
		return compoundData.getFormattedValue(property);
	}

	@Override
	public String getStringValue(NominalProperty property)
	{
		return compoundData.getStringValue(property);
	}

	public Double getDoubleValue(NumericProperty property)
	{
		return compoundData.getDoubleValue(property);
	}

	public int getJmolIndex()
	{
		return jmolIndex;
	}

	public int getOrigIndex()
	{
		return compoundData.getOrigIndex();
	}

	public static class DisplayName implements Comparable<DisplayName>
	{
		String valDisplay;
		@SuppressWarnings("rawtypes")
		Comparable valCompare[];
		Integer compareIndex;
		String name;

		public String toString(boolean html, Color highlightColor)
		{
			StringBuffer b = new StringBuffer();
			if (html)
				b.append(StringEscapeUtils.escapeHtml4(name));
			else
				b.append(name);
			if (valDisplay != null && !valDisplay.equals(name))
			{
				if (html)
				{
					b.append(":&nbsp;");
					if (highlightColor != null)
						b.append("<font color='" + ColorUtil.toHtml(highlightColor) + "'>");
					b.append("<i>");
					b.append(StringEscapeUtils.escapeHtml4(valDisplay));
					b.append("</i>");
					if (highlightColor != null)
						b.append("</font>");
				}
				else
				{
					b.append(": ");
					b.append(valDisplay);
				}
			}
			return b.toString();
		}

		@Override
		public int compareTo(DisplayName d)
		{
			return compareTo(d, true);
		}

		public int compareTo(DisplayName d, boolean featureSortingEnabled)
		{
			if (featureSortingEnabled && valCompare != null)
			{
				for (int j = 0; j < valCompare.length; j++)
				{
					int i = ObjectUtil.compare(valCompare[j], d.valCompare[j]);
					if (i != 0)
						return i;
				}
			}
			if (compareIndex != null)
				return compareIndex.compareTo(d.compareIndex);
			// if nothing is selected, compound should be sorted according to identifier
			return name.compareTo(d.name);
		}
	}

	private DisplayName displayName = new DisplayName();

	@Override
	public String toString()
	{
		return getFirstName();
	}

	public String toStringWithValue()
	{
		return displayName.toString(false, null);
	}

	@Override
	public String getFirstName()
	{
		return displayName.name;
	}

	@Override
	public String getSecondName()
	{
		if (ObjectUtil.equals(displayName.valDisplay, displayName.name))
			return null;
		else
			return displayName.valDisplay;
	}

	@Override
	public int compareTo(Compound m)
	{
		return displayName.compareTo(m.displayName, featureSortingEnabled);
	}

	public String getSmiles()
	{
		return compoundData.getSmiles();
	}

	public Translucency getTranslucency()
	{
		return translucency;
	}

	public void setTranslucency(Translucency translucency)
	{
		this.translucency = translucency;
	}

	public String getLabel()
	{
		return label;
	}

	public void setLabel(String label)
	{
		this.label = label;
	}

	public boolean isShowHoverBox()
	{
		return showHoverBox;
	}

	public void setShowHoverBox(boolean showBox)
	{
		this.showHoverBox = showBox;
	}

	public boolean isShowActiveBox()
	{
		return showActiveBox;
	}

	public void setShowActiveBox(boolean showBox)
	{
		this.showActiveBox = showBox;
	}

	public String getHighlightedSmarts()
	{
		return smarts;
	}

	public void setHighlightedSmarts(String smarts)
	{
		this.smarts = smarts;
	}

	public void moveTo(Vector3f clusterPos)
	{
		Vector3f center = new Vector3f(View.instance.getAtomSetCenter(getBitSet()));
		Vector3f dest = new Vector3f(clusterPos);
		dest.sub(center);
		View.instance.setAtomCoordRelative(dest, getBitSet());
	}

	public Vector3f getPosition(boolean scaled)
	{
		Vector3f v = new Vector3f(JitteringProvider.getPosition(compoundData));
		if (scaled)
			v.scale(ClusteringUtil.SCALE);
		return v;
	}

	public Vector3f getPosition()
	{
		return getPosition(true);
	}

	public BitSet getSmartsMatch(String smarts)
	{
		//compute match dynamically
		if (!smartsMatches.containsKey(smarts))
		{
			Settings.LOGGER.info("smarts-matching smarts: " + smarts + " smiles: " + getSmiles());
			smartsMatches.put(smarts, View.instance.getSmartsMatch(smarts, bitSet));
		}
		return smartsMatches.get(smarts);
	}

	public void setCompoundColor(String colorString)
	{
		this.compoundColor = colorString;
	}

	public String getCompoundColor()
	{
		return compoundColor;
	}

	public void setHighlightColor(String colorString, Color colorText)
	{
		if (!ObjectUtil.equals(highlightColorString, colorString) || lastHighlightColorString == null)
		{
			this.lastHighlightColorString = highlightColorString;
			this.highlightColorString = colorString;
			this.highlightColorText = colorText;
		}
	}

	public Color getHighlightColorText()
	{
		return highlightColorText;
	}

	public String getHighlightColorString()
	{
		return highlightColorString;
	}

	public String getLastHighlightColorString()
	{
		return lastHighlightColorString;
	}

	public Vector3f getSpherePosition()
	{
		return spherePosition;
	}

	public void setSpherePosition(Vector3f spherePosition)
	{
		this.spherePosition = spherePosition;
	}

	public void setHighlightCompoundProperty(CompoundProperty highlightCompoundProperty)
	{
		if (this.highlightCompoundProperty != highlightCompoundProperty)
		{
			displayName.valDisplay = null;
			displayName.valCompare = null;
			if (highlightCompoundProperty != null)
			{
				if (highlightCompoundProperty instanceof NumericProperty)
					displayName.valCompare = new Double[] { getDoubleValue((NumericProperty) highlightCompoundProperty) };
				else
					displayName.valCompare = new String[] { getStringValue((NominalProperty) highlightCompoundProperty) };
				displayName.valDisplay = getFormattedValue(highlightCompoundProperty);
			}
			this.highlightCompoundProperty = highlightCompoundProperty;
			lastHighlightColorString = null;
		}
	}

	public Object getHighlightCompoundProperty()
	{
		return highlightCompoundProperty;
	}

	public void setStyle(Style style)
	{
		this.style = style;
	}

	public Style getStyle()
	{
		return style;
	}

	@Override
	public Vector3f getCenter(boolean superimposed)
	{
		return new Vector3f(View.instance.getAtomSetCenter(bitSet));
	}

	@Override
	public float getDiameter(boolean superimposed)
	{
		return getDiameter();
	}

	public float getDiameter()
	{
		if (diameter == -1)
			diameter = View.instance.getDiameter(bitSet);
		return diameter;
	}

	@Override
	public boolean isSuperimposed()
	{
		return false;
	}

	public ImageIcon getIcon(boolean backgroundBlack, int width, int height, boolean translucent)
	{
		return compoundData.getIcon(backgroundBlack, width, height, translucent);
	}

	public void setDescriptor(CompoundProperty descriptorProperty)
	{
		if (this.descriptorProperty != descriptorProperty)
		{
			displayName.compareIndex = null;
			if (descriptorProperty == ViewControler.COMPOUND_INDEX_PROPERTY)
			{
				displayName.compareIndex = getOrigIndex();
				displayName.name = "Compound " + (getOrigIndex() + 1);
			}
			else if (descriptorProperty == ViewControler.COMPOUND_SMILES_PROPERTY)
				displayName.name = getSmiles();
			else
				displayName.name = getFormattedValue(descriptorProperty);
			this.descriptorProperty = descriptorProperty;
		}
	}

	public boolean isSphereVisible()
	{
		return sphereVisible;
	}

	public void setSphereVisible(boolean sphereVisible)
	{
		this.sphereVisible = sphereVisible;
	}

	public boolean isLastFeatureSphereVisible()
	{
		return lastFeatureSphereVisible;
	}

	public void setLastFeatureSphereVisible(boolean s)
	{
		this.lastFeatureSphereVisible = s;
	}

	public BitSet getBitSet()
	{
		return bitSet;
	}

	public BitSet getDotModeHideBitSet()
	{
		return dotModeHideBitSet;
	}

	public BitSet getDotModeDisplayBitSet()
	{
		return dotModeDisplayBitSet;
	}

	public DisplayName getDisplayName()
	{
		return displayName;
	}

	public boolean isVisible()
	{
		return visible;
	}

	public void setVisible(boolean visible)
	{
		this.visible = visible;
	}

	public void setFeatureSortingEnabled(boolean featureSortingEnabled)
	{
		this.featureSortingEnabled = featureSortingEnabled;
	}

	public CompoundData getCompoundData()
	{
		return compoundData;
	}
}
 public interface CompoundFilter
{
	public boolean accept(Compound c);
} public class CompoundFilterImpl implements CompoundFilter
{
	private String desc;
	private List<Compound> compounds;

	public CompoundFilterImpl(Clustering clustering, List<Compound> compounds, String additionalDesc)
	{
		this.compounds = compounds;
		desc = "Show " + compounds.size() + "/" + clustering.getNumUnfilteredCompounds(false) + " compounds";
		if (additionalDesc != null)
			desc += " (" + additionalDesc + ")";
	}

	public static CompoundFilterImpl combine(Clustering clustering, CompoundFilter filter1, CompoundFilter filter2)
	{
		return new CompoundFilterImpl(clustering, ListUtil.cut2(((CompoundFilterImpl) filter1).compounds,
				((CompoundFilterImpl) filter2).compounds), null);
	}

	public String toString()
	{
		return desc;
	}

	public boolean accept(Compound c)
	{
		return compounds.contains(c);
	}

} public class JitteringProvider
{
	/**
	 * stores the compound-set that has been used for this jittering level
	 */
	private List<Set<Compound>> sets = new ArrayList<Set<Compound>>();
	/**
	 * stores the level that has last been used for a compound-set
	 */
	private HashMap<Set<Compound>, Integer> levels = new HashMap<Set<Compound>, Integer>();
	private DoubleKeyHashMap<CompoundData, Integer, Vector3f> positions = new DoubleKeyHashMap<CompoundData, Integer, Vector3f>();
	private int currentLevel;
	private float[] minDistances;
	private static JitteringProvider staticInstance;
	public static final int STEPS = 10;

	public JitteringProvider(Clustering c)
	{
		staticInstance = null;

		/*
		 * jittering in ches-mapper has fixed number of steps
		 * each step has a min-distance that compounds should have
		 * the min-distance is computed based on the entire data, within interval:
		 * [ min-min-dist , min-min-dist + delta(max-min-distance,min-min-dist)/2 ]
		 * the interval chunks are not equi-distant but using a log-scale  
		 */
		minDistances = new float[STEPS + 1];
		Vector3f v[] = new Vector3f[c.getCompounds().size()];
		for (int i = 0; i < v.length; i++)
			v[i] = c.getCompounds().get(i).getPosition();
		NNComputer nn = new NNComputer(v);
		nn.computeNaive();
		float dist = nn.getMinMinDist();
		float add = (nn.getMaxMinDist() - nn.getMinMinDist()) * 0.5f;
		double log[] = ArrayUtil.toPrimitiveDoubleArray(MathUtil.logBinning(STEPS, 1.2));
		for (int i = 1; i <= STEPS; i++)
			minDistances[i] = dist + add * (float) log[i];
		Settings.LOGGER
				.info("Initiated min-distances per level for jittering: " + ArrayUtil.toNiceString(minDistances));
	}

	public Vector3f getPosition(CompoundData c, int level)
	{
		if (level == 0)
			return c.getPosition();
		if (positions.containsKeyPair(c, level))
			return positions.get(c, level);
		else
			return c.getPosition();
	}

	public static Vector3f getPosition(CompoundData c)
	{
		if (staticInstance == null)
			return c.getPosition();
		else
			return staticInstance.getPosition(c, staticInstance.currentLevel);
	}

	private Jittering createFromCompounds(List<Compound> compounds, int level, float minDist)
	{
		List<CompoundData> d = new ArrayList<CompoundData>();
		for (Compound c : compounds)
			d.add(c.getCompoundData());
		return create(d, level, minDist);
	}

	private Jittering create(List<CompoundData> d, int level, float minDist)
	{
		if (level <= 0)
			throw new IllegalArgumentException();
		Vector3f v[] = new Vector3f[d.size()];
		if (level == 1)
			for (int i = 0; i < v.length; i++)
				v[i] = new Vector3f(d.get(i).getPosition());
		else
			for (int i = 0; i < v.length; i++)
				v[i] = new Vector3f(JitteringProvider.this.getPosition(d.get(i), level - 1));
		return new Jittering(v, minDist, new Random());
	}

	public void updateJittering(int level, Set<Compound> compounds)
	{
		Settings.LOGGER.info("Set jittering level to " + level);
		if (level == 0)
		{
			//do nothing, use orig positions
		}
		else if (sets.size() > level && SetUtil.isSubSet(sets.get(level), compounds))
		{
			//do nothing, positions have already been computed
		}
		else
		{
			jitter(compounds, level);
			while (sets.size() < level + 1)
				sets.add(null);
			sets.set(level, compounds);
		}
		while (sets.size() > level + 1)
			sets.remove(level + 1);
		levels.put(compounds, level);

		staticInstance = this;
		currentLevel = level;
	}

	private void jitter(Set<Compound> c, int level)
	{
		jitter(new ArrayList<Compound>(c), level);
	}

	private void jitter(List<Compound> c, final int level)
	{
		Settings.LOGGER.info("Compute jittering for " + c.size() + " compounds for level " + level);
		Jittering j = createFromCompounds(c, level, minDistances[level]);
		j.jitter();
		for (int i = 0; i < c.size(); i++)
			positions.put(c.get(i).getCompoundData(), level, j.getPosition(i));
	}

	public int getJitteringResetLevel(Set<Compound> compounds)
	{
		int currentLevel = Math.max(0, (sets.size() - 1));
		//		System.err.println("current jittering level is " + currentLevel);
		int lastLevelForSet = levels.containsKey(compounds) ? levels.get(compounds) : 0;
		//		System.err.println("last jittering level for this set is " + lastLevelForSet);
		if (lastLevelForSet == currentLevel)
		{
			if (lastLevelForSet == 0)
				return -1;
			//			System.err.println("current jittering level is equal, check if sets are complient");
			if (SetUtil.isSubSet(sets.get(currentLevel), compounds))
			{
				//				System.err.println("yes, nothing todo");
				return -1;
			}
			else
			{
				//				System.err.println("no, check lower level");
				currentLevel = -1;
			}
		}
		//		else
		//			System.err.println("current jittering level differs");
		for (int j = currentLevel; j >= 1; j--)
		{
			//			System.err.println("check if this set is subset of level " + j);
			if (SetUtil.isSubSet(sets.get(j), compounds))
			{
				//				System.err.println("yes, use level " + j);
				return j;
			}
		}
		//		System.err.println("not compatible, reset to 0");
		return 0;
	}

	private static boolean showWarning = true;

	public static void showJitterWarning()
	{
		if (showWarning)
		{
			MessagePanel p = new MessagePanel();
			p.addWarning(Settings.text("spread.warning"), Settings.text("spread.warning.details"));
			SwingUtil.showInDialog(p, "Warning", new Dimension(600, 300), null, Settings.TOP_LEVEL_FRAME);
			showWarning = false;
		}
	}

	public static void main(String[] args)
	{
		LaunchCheSMapper.init();
		showJitterWarning();
	}
} public static enum AnimationSpeed
{
	SLOW, FAST
}
 public class ComponentFactory
{
	public static Color BACKGROUND;
	public static Color FOREGROUND;
	public static Color FOREGROUND_DISABLED;
	public static Color BORDER_FOREGROUND;
	public static Color LIST_SELECTION_FOREGROUND;
	public static Color LIST_ACTIVE_BACKGROUND;
	public static Color LIST_WATCH_BACKGROUND;

	public static Color FOREGROUND_FOR_BACKGROUND_BLACK = new Color(250, 250, 250);
	public static Color FOREGROUND_FOR_BACKGROUND_WHITE = new Color(5, 5, 5);
	public static final int DISABLED_TRANSPARENT = 100; //0 = fully transparent, 255= opaque

	public static Color LIST_ACTIVE_BACKGROUND_BLACK = new Color(51, 102, 255);
	public static Color LIST_WATCH_BACKGROUND_BLACK = LIST_ACTIVE_BACKGROUND_BLACK.darker().darker();
	public static Color LIST_ACTIVE_BACKGROUND_WHITE = new Color(101, 152, 255);
	public static Color LIST_WATCH_BACKGROUND_WHITE = LIST_ACTIVE_BACKGROUND_WHITE.brighter().brighter();

	private static boolean backgroundBlack = true;

	public static boolean isBackgroundBlack()
	{
		return backgroundBlack;
	}

	static
	{
		setBackgroundBlack(backgroundBlack);
	}

	public static void setBackgroundBlack(Boolean b)
	{
		backgroundBlack = b;
		FOREGROUND = getForeground(b);
		FOREGROUND_DISABLED = ColorUtil.transparent(FOREGROUND, DISABLED_TRANSPARENT);
		if (b)
		{
			BACKGROUND = Color.BLACK;
			BORDER_FOREGROUND = FOREGROUND;
			LIST_SELECTION_FOREGROUND = Color.WHITE;
			LIST_ACTIVE_BACKGROUND = LIST_ACTIVE_BACKGROUND_BLACK;
			LIST_WATCH_BACKGROUND = LIST_WATCH_BACKGROUND_BLACK;
		}
		else
		{
			BACKGROUND = Color.WHITE;
			BORDER_FOREGROUND = Color.LIGHT_GRAY;
			LIST_SELECTION_FOREGROUND = Color.BLACK;
			LIST_ACTIVE_BACKGROUND = LIST_ACTIVE_BACKGROUND_WHITE;
			LIST_WATCH_BACKGROUND = LIST_WATCH_BACKGROUND_WHITE;
		}
		updateComponents();
	}

	public static Color getForeground(boolean blackBackground)
	{
		if (blackBackground)
			return FOREGROUND_FOR_BACKGROUND_BLACK;
		else
			return FOREGROUND_FOR_BACKGROUND_WHITE;
	}

	private static List<PropertyChangeListener> updateUIListeners = new ArrayList<PropertyChangeListener>();

	public static void updateComponents()
	{
		if (updateUIListeners != null)
			for (PropertyChangeListener p : updateUIListeners)
				p.propertyChange(new PropertyChangeEvent(Settings.TOP_LEVEL_FRAME, "updateComponents", false, true));
		if (Settings.TOP_LEVEL_FRAME != null)
			SwingUtilities.updateComponentTreeUI(Settings.TOP_LEVEL_FRAME);
	}

	//	static class UIUtil
	//	{
	//		static HashMap<String, Color> orig = new HashMap<String, Color>();
	//
	//		public static void set(String v, Color c)
	//		{
	//			if (!orig.containsKey(v))
	//				orig.put(v, UIManager.getColor(v));
	//			UIManager.put(v, new ColorUIResource(c));
	//		}
	//		public static void unset()
	//		{
	//			for (String v : orig.keySet())
	//				UIManager.put(v, new ColorUIResource(orig.get(v)));
	//		}
	//	}
	//	private static void setViewUIColors()
	//	{
	//		UIUtil.set("ScrollBar.background", BACKGROUND);
	//		UIUtil.set("ScrollBar.foreground", BORDER_FOREGROUND);
	//		UIUtil.set("ScrollBar.thumbHighlight", BORDER_FOREGROUND);
	//		UIUtil.set("ScrollBar.thumbShadow", BORDER_FOREGROUND.darker());
	//		UIUtil.set("ScrollBar.thumbDarkShadow", BORDER_FOREGROUND.darker().darker());
	//		UIUtil.set("ScrollBar.thumb", BACKGROUND);
	//		UIUtil.set("ScrollBar.track", BACKGROUND);
	//		UIUtil.set("ScrollBar.trackHighlight", BACKGROUND);
	//		UIUtil.set("ComboBox.buttonBackground", BACKGROUND);
	//		UIUtil.set("ComboBox.buttonShadow", BORDER_FOREGROUND.darker());
	//		UIUtil.set("ComboBox.buttonDarkShadow", BORDER_FOREGROUND.darker().darker());
	//		UIUtil.set("ComboBox.buttonHighlight", BORDER_FOREGROUND);
	//	}
	//	private static void unsetViewUIColors()
	//	{
	//		UIUtil.unset();
	//	}

	public static JTextField createUneditableViewTextField()
	{
		JTextField l = new JTextField()
		{
			public void updateUI()
			{
				super.updateUI();
				setForeground(FOREGROUND);
				setFont(getFont().deriveFont((float) ScreenSetup.INSTANCE.getFontSize()));
				setBorder(null);
			}

			@Override
			public void setText(String t)
			{
				super.setText(t);
				setToolTipText(t);
			}
		};
		l.setFocusable(false);
		l.setEditable(false);
		l.setBorder(null);
		l.setOpaque(false);
		return l;
	}

	public static JLabel createTransparentViewLabel()
	{
		return new TransparentViewLabel();
	}

	public static JLabel createViewLabel()
	{
		return createViewLabel("");
	}

	public static JLabel createViewLabel(String text)
	{
		return createViewLabel(text, null);
	}

	public static JLabel createViewLabel(String text, ImageIcon iconBlack, ImageIcon iconWhite)
	{
		return createViewLabel(text, null, iconBlack, iconWhite);
	}

	public static JLabel createViewLabel(String text, DimensionProvider preferredSize)
	{
		return createViewLabel(text, preferredSize, null, null);
	}

	public static interface DimensionProvider
	{
		public Dimension getPreferredSize(Dimension orig);
	}

	public static JLabel createViewLabel(String text, final DimensionProvider preferredSize, final ImageIcon iconBlack,
			final ImageIcon iconWhite)
	{
		JLabel l = new JLabel(text)
		{
			public void updateUI()
			{
				super.updateUI();
				setForeground(FOREGROUND);
				setFont(getFont().deriveFont((float) ScreenSetup.INSTANCE.getFontSize()));
				if (iconBlack != null)
					setIcon(isBackgroundBlack() ? iconBlack : iconWhite);
			}

			@Override
			public void setText(String text)
			{
				super.setText(text);
				setToolTipText(text);
			}

			@Override
			public Dimension getPreferredSize()
			{
				if (preferredSize == null)
					return super.getPreferredSize();
				else
					return preferredSize.getPreferredSize(super.getPreferredSize());
			}
		};
		if (iconBlack != null)
			l.setIcon(isBackgroundBlack() ? iconBlack : iconWhite);
		l.setToolTipText(text);
		return l;
	}

	//	public static Border createThinBorder()
	//	{
	//		return new MatteBorder(1, 1, 1, 1, FOREGROUND);
	//	}
	//
	//	public static Border createLineBorder(int thickness)
	//	{
	//		return new LineBorder(FOREGROUND, thickness);
	//	}

	public static LinkButton createViewLinkButton(String text)
	{
		LinkButton l = new LinkButton(text)
		{
			public void updateUI()
			{
				super.updateUI();
				setForegroundColor(FOREGROUND);
				setSelectedForegroundColor(LIST_SELECTION_FOREGROUND);
				setFont(getFont().deriveFont((float) ScreenSetup.INSTANCE.getFontSize()));
				setSelectedForegroundFont(getFont().deriveFont((float) ScreenSetup.INSTANCE.getFontSize()));
			}

			@Override
			public void setText(String text)
			{
				super.setText(text);
				setToolTipText(text);
			}
		};
		l.setForegroundColor(FOREGROUND);
		l.setSelectedForegroundColor(LIST_SELECTION_FOREGROUND);
		l.setSelectedForegroundFont(l.getFont());
		l.setFocusable(false);
		return l;
	}

	public static JCheckBox createViewCheckBox(String text)
	{
		JCheckBox c = new JCheckBox(text)
		{
			public void updateUI()
			{
				super.updateUI();
				setForeground(FOREGROUND);
				setFont(getFont().deriveFont((float) ScreenSetup.INSTANCE.getFontSize()));
			}
		};
		c.setFocusable(false);
		return c;
	}

	//	public static JRadioButton createViewRadioButton(String text)
	//	{
	//		JRadioButton r = new JRadioButton(text)
	//		{
	//			public void updateUI()
	//			{
	//				super.updateUI();
	//				setForeground(FOREGROUND);
	//			}
	//		};
	//		return r;
	//	}

	public static class StyleButton extends JRadioButton
	{
		public Style style;

		public StyleButton(String text, boolean selected, Style style)
		{
			super(text, selected);
			this.style = style;
		}

		public void updateUI()
		{
			super.updateUI();
			setForeground(FOREGROUND);
			setFont(getFont().deriveFont((float) ScreenSetup.INSTANCE.getFontSize()));
		}
	}

	@SuppressWarnings("unchecked")
	public static <T> JComboBox<T> createViewComboBox(Class<T> type)
	{
		return createViewComboBox((T[]) Array.newInstance(type, 0));
	}

	public static <T> JComboBox<T> createViewComboBox(T[] items)
	{
		final Font f = new JLabel().getFont();
		DescriptionListCellRenderer r = new DescriptionListCellRenderer()
		{
			@SuppressWarnings("rawtypes")
			@Override
			public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected,
					boolean cellHasFocus)
			{
				list.setSelectionBackground(LIST_ACTIVE_BACKGROUND);
				list.setSelectionForeground(LIST_SELECTION_FOREGROUND);
				list.setForeground(FOREGROUND);
				list.setBackground(BACKGROUND);
				list.setFont(f.deriveFont((float) ScreenSetup.INSTANCE.getFontSize()));
				return super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
			}
		};

		JComboBox<T> c = new JComboBox<T>()
		{
			public void updateUI()
			{
				setUI(new BasicComboBoxUI()
				{
					protected JButton createArrowButton()
					{
						JButton button = new BasicArrowButton(BasicArrowButton.SOUTH, BACKGROUND,
								BORDER_FOREGROUND.darker(), BORDER_FOREGROUND.darker().darker(), BORDER_FOREGROUND);
						button.setName("ComboBox.arrowButton");
						button.setFont(f.deriveFont((float) ScreenSetup.INSTANCE.getFontSize()));
						return button;
					}

					protected ComboPopup createPopup()
					{
						return new BasicComboPopup(comboBox)
						{
							protected JScrollPane createScroller()
							{
								JScrollPane sp = new JScrollPane(list,
										ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED,
										ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
								sp.setHorizontalScrollBar(null);
								sp.setVerticalScrollBar(new JScrollBar(JScrollBar.VERTICAL)
								{
									public void updateUI()
									{
										setUI(new MyScrollBarUI());
									}
								});
								return sp;
							}

							protected void configurePopup()
							{
								super.configurePopup();
								setBorder(new EtchedBorder(BORDER_FOREGROUND, BORDER_FOREGROUND.darker()));
							}
						};
					}
				});
				setForeground(FOREGROUND);
				setBackground(BACKGROUND);
				setBorder(new EtchedBorder(BORDER_FOREGROUND, BORDER_FOREGROUND.darker()));
				setFont(f.deriveFont((float) ScreenSetup.INSTANCE.getFontSize()));
				if (getRenderer() instanceof DescriptionListCellRenderer)
					((DescriptionListCellRenderer) getRenderer())
							.setDescriptionForeground(FOREGROUND.darker().darker());
			}
		};
		for (T object : items)
			c.addItem(object);
		c.setOpaque(false);
		c.setForeground(FOREGROUND);
		c.setBackground(BACKGROUND);
		c.setFont(f.deriveFont((float) ScreenSetup.INSTANCE.getFontSize()));
		r.setDescriptionForeground(FOREGROUND.darker().darker());
		c.setRenderer(r);
		c.setFocusable(false);
		return c;
	}

	public static class FactoryTableCellRenderer extends DefaultTableCellRenderer
	{
		boolean halfTransparent;
		protected boolean notPainting = false;

		public FactoryTableCellRenderer(boolean halfTransparent)
		{
			this.halfTransparent = halfTransparent;
		}

		public void setNotPainting(boolean notPainting)
		{
			this.notPainting = notPainting;
		}

		@Override
		public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,
				boolean hasFocus, int row, int column)
		{
			super.getTableCellRendererComponent(table, value, isSelected, false, row, column);
			if (!notPainting)
				if (isSelected)
				{
					setBackground(LIST_ACTIVE_BACKGROUND);
					setOpaque(true);
					setForeground(LIST_SELECTION_FOREGROUND);
				}
				else
				{
					if (halfTransparent)
					{
						setBackground(new Color(BACKGROUND.getRed(), BACKGROUND.getGreen(), BACKGROUND.getBlue(), 100));
						setOpaque(true);
					}
					else
						setOpaque(false);
					setForeground(FOREGROUND);
				}
			return this;
		}
	}

	public static JTable createTable()
	{
		return createTable(false);
	}

	public static JTable createTable(boolean halfTransparent)
	{
		DefaultTableModel m = new DefaultTableModel()
		{
			public boolean isCellEditable(int row, int column)
			{
				return false;
			}
		};
		JTable t = new JTable(m)
		{
			public void updateUI()
			{
				super.updateUI();
				setFont(getFont().deriveFont((float) ScreenSetup.INSTANCE.getFontSize()));
				setRowHeight((int) (ScreenSetup.INSTANCE.getFontSize() * 1.7));
			}
		};
		t.setBorder(null);
		t.getTableHeader().setVisible(false);
		t.getTableHeader().setPreferredSize(new Dimension(-1, 0));
		t.setGridColor(new Color(0, 0, 0, 0));
		t.setOpaque(false);
		t.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
		t.setDefaultRenderer(Object.class, new FactoryTableCellRenderer(halfTransparent));
		t.setFocusable(false);
		t.updateUI();
		return t;
	}

	public static int packColumn(JTable table, int vColIndex, int margin)
	{
		return packColumn(table, vColIndex, margin, Integer.MAX_VALUE);
	}

	public static int packColumn(JTable table, int vColIndex, int margin, int max)
	{
		return packColumn(table, vColIndex, margin, max, false);
	}

	public static int packColumn(JTable table, int vColIndex, int margin, int max, boolean fixMaxWidth)
	{
		DefaultTableColumnModel colModel = (DefaultTableColumnModel) table.getColumnModel();
		TableColumn col = colModel.getColumn(vColIndex);
		int width = 0;

		// Get width of column header
		TableCellRenderer renderer = col.getHeaderRenderer();
		if (renderer == null)
		{
			renderer = table.getTableHeader().getDefaultRenderer();
		}
		Component comp = renderer.getTableCellRendererComponent(table, col.getHeaderValue(), false, false, 0, 0);
		width = comp.getPreferredSize().width;

		// Get maximum width of column data
		for (int r = 0; r < table.getRowCount(); r++)
		{
			renderer = table.getCellRenderer(r, vColIndex);
			if (renderer instanceof FactoryTableCellRenderer)
				((FactoryTableCellRenderer) renderer).setNotPainting(true);
			comp = renderer.getTableCellRendererComponent(table, table.getValueAt(r, vColIndex), false, false, r,
					vColIndex);
			if (renderer instanceof FactoryTableCellRenderer)
				((FactoryTableCellRenderer) renderer).setNotPainting(false);
			width = Math.max(width, comp.getPreferredSize().width);
		}

		// Add margin
		width += 2 * margin;
		if (width > max)
			width = max;

		// Set the width
		col.setPreferredWidth(width);
		if (fixMaxWidth)
		{
			col.setMinWidth(width);
			col.setMaxWidth(width);
		}
		return width;
	}

	static class MyScrollBarUI extends BasicScrollBarUI
	{
		protected JButton createDecreaseButton(int orientation)
		{
			return new BasicArrowButton(orientation, BACKGROUND, BORDER_FOREGROUND.darker(), BORDER_FOREGROUND.darker()
					.darker(), BORDER_FOREGROUND);
		}

		protected JButton createIncreaseButton(int orientation)
		{
			return new BasicArrowButton(orientation, BACKGROUND, BORDER_FOREGROUND.darker(), BORDER_FOREGROUND.darker()
					.darker(), BORDER_FOREGROUND);
		}

		protected void configureScrollBarColors()
		{
			scrollbar.setForeground(BORDER_FOREGROUND);
			scrollbar.setBackground(BACKGROUND);
			thumbHighlightColor = BORDER_FOREGROUND;
			thumbLightShadowColor = BORDER_FOREGROUND.darker();
			thumbDarkShadowColor = BORDER_FOREGROUND.darker().darker();
			thumbColor = BACKGROUND;
			trackColor = BACKGROUND;
			trackHighlightColor = BACKGROUND;
		}
	}

	public static void setViewScrollPaneBorder(JComponent component)
	{
		component.setBorder(new CompoundBorder(new EtchedBorder(BORDER_FOREGROUND, BORDER_FOREGROUND.darker()),
				new EmptyBorder(5, 5, 5, 5)));
	}

	public static JScrollPane createViewScrollpane(JComponent table)
	{
		JScrollPane p = new JScrollPane(table)
		{
			public void updateUI()
			{
				super.updateUI();
				setBorder(new EtchedBorder(BORDER_FOREGROUND, BORDER_FOREGROUND.darker()));
			}
		};
		p.setVerticalScrollBar(new JScrollBar(JScrollBar.VERTICAL)
		{
			public void updateUI()
			{
				setUI(new MyScrollBarUI());
			}
		});
		p.setHorizontalScrollBar(new JScrollBar(JScrollBar.HORIZONTAL)
		{
			public void updateUI()
			{
				setUI(new MyScrollBarUI());
			}
		});
		p.setOpaque(false);
		p.getViewport().setOpaque(false);
		p.setViewportBorder(new EmptyBorder(5, 5, 5, 5));

		return p;
	}

	public static interface PreferredSizeProvider
	{
		public Dimension getPreferredSize();
	}

	public static ClickableLabel createViewButton(String string)
	{
		return createViewButton(string, new Insets(5, 5, 5, 5));
	}

	public static ClickableLabel createViewButton(String string, final Insets insets)
	{
		return createViewButton(string, insets, null);
	}

	public static ClickableLabel createPlusViewButton()
	{
		return createViewButton(null, SimpleImageIcon.plusImageIcon(), new Insets(4, 4, 4, 4), null);
	}

	public static ClickableLabel createMinusViewButton()
	{
		return createViewButton(null, SimpleImageIcon.minusImageIcon(), new Insets(4, 4, 4, 4), null);
	}

	public static ClickableLabel createCrossViewButton()
	{
		return createViewButton(null, SimpleImageIcon.crossImageIcon(), new Insets(4, 4, 4, 4), null);
	}

	public static ClickableLabel createViewButton(String string, final Insets insets, final PreferredSizeProvider prov)
	{
		return createViewButton(string, null, insets, prov);
	}

	public static class ClickableLabel extends JLabel
	{
		public ClickableLabel(String text, ImageIcon icon, int horizontalAlignment)
		{
			super(text, icon, horizontalAlignment);
		}

		Boolean myEnabled = null;

		public void setEnabled(boolean enabled)
		{
			myEnabled = enabled;
			updateUI();
		}

		public void addActionListener(final ActionListener l)
		{
			addMouseListener(new MouseAdapter()
			{
				@Override
				public void mousePressed(MouseEvent e)
				{
					if (myEnabled == null || myEnabled)
						l.actionPerformed(new ActionEvent(ClickableLabel.this, 0, "button clicked"));
				}
			});
		}
	}

	public static ClickableLabel createViewButton(final String string, final SimpleImageIcon icon, final Insets insets,
			final PreferredSizeProvider prov)
	{
		final BorderImageIcon ic;
		if (icon != null)
		{
			icon.setSize((int) (ScreenSetup.INSTANCE.getFontSize() * 0.55));
			ic = new BorderImageIcon(icon, 1, FOREGROUND, insets);
		}
		else
			ic = null;
		ClickableLabel c = new ClickableLabel(string, ic, SwingConstants.LEFT)
		{
			public void updateUI()
			{
				super.updateUI();
				setBackground(BACKGROUND);
				setForeground(FOREGROUND);
				if (ic != null)
				{
					if (myEnabled == null || myEnabled)
					{
						icon.setColor(FOREGROUND);
						ic.setColor(FOREGROUND);
					}
					else
					{
						icon.setColor(FOREGROUND_DISABLED);
						ic.setColor(FOREGROUND_DISABLED);
					}
					icon.setSize((int) (ScreenSetup.INSTANCE.getFontSize() * 0.55));

				}
				else
				{
					setBorder(new CompoundBorder(new LineBorder(FOREGROUND, 1), new EmptyBorder(insets)));
					setFont(getFont().deriveFont((float) ScreenSetup.INSTANCE.getFontSize()));
				}
			}

			@Override
			public Dimension getPreferredSize()
			{
				if (prov == null)
					return super.getPreferredSize();
				else
					return prov.getPreferredSize();
			}
		};
		c.setOpaque(false);
		c.setFocusable(false);
		c.setFont(new JLabel().getFont().deriveFont((float) ScreenSetup.INSTANCE.getFontSize()));
		c.setFocusable(false);
		if (ic != null)
			c.setBorder(new EmptyBorder(0, 0, 1, 1)); // hack: otherwise right and bottom line is missing
		return c;
	}

	public static JButton createViewButton(final ImageIcon blackIcon, final ImageIcon whiteIcon)
	{
		JButton c = new JButton(isBackgroundBlack() ? blackIcon : whiteIcon)
		{
			public void updateUI()
			{
				super.updateUI();
				setForeground(FOREGROUND);
				setBackground(BACKGROUND);
				setBorder(new CompoundBorder(new LineBorder(FOREGROUND, 1), new EmptyBorder(new Insets(1, 1, 1, 1))));
				setIcon(isBackgroundBlack() ? blackIcon : whiteIcon);
			}
		};
		c.setOpaque(false);
		c.setFocusable(false);
		c.setBorder(new CompoundBorder(new LineBorder(FOREGROUND, 1), new EmptyBorder(new Insets(1, 1, 1, 1))));
		return c;
	}

	private static ImageIcon createBasicViewStringImageIcon(String s)
	{
		int size = 10; //dynamically compute size according to orig size does not work well in chartpanel
		if (ScreenSetup.INSTANCE.getFontSize() >= 15)
			size = 12;
		if (ScreenSetup.INSTANCE.getFontSize() >= 21)
			size = 14;
		StringImageIcon sIcon = new StringImageIcon(s, createViewLabel().getFont().deriveFont((float) size), FOREGROUND);
		sIcon.setDrawBorder(true);
		sIcon.setInsets(new Insets(1, 3, 1, 3));
		return sIcon;
	}

	public static Icon createViewStringImageIcon(final String string)
	{
		final ImageIcon icon = new ImageIcon(createBasicViewStringImageIcon(string).getImage());
		updateUIListeners.add(new PropertyChangeListener()
		{
			@Override
			public void propertyChange(PropertyChangeEvent evt)
			{
				icon.setImage(createBasicViewStringImageIcon(string).getImage());
			}
		});
		return icon;
	}

	public static JTextArea createViewTextArea(String text, boolean editable, boolean wrap)
	{
		JTextArea infoTextArea = new JTextArea(text)
		{
			public void updateUI()
			{
				super.updateUI();
				setForeground(FOREGROUND);
				setFont(getFont().deriveFont((float) ScreenSetup.INSTANCE.getFontSize()));
			}
		};
		//infoTextArea.setFont(infoTextArea.getFont().deriveFont(Font.BOLD));
		if (!editable)
		{
			infoTextArea.setBorder(null);
			infoTextArea.setEditable(false);
			infoTextArea.setOpaque(false);
		}
		infoTextArea.setWrapStyleWord(wrap);
		infoTextArea.setLineWrap(wrap);
		infoTextArea.setFocusable(false);
		return infoTextArea;
	}

	static class MySliderUI extends BasicSliderUI
	{
		public MySliderUI(JSlider b)
		{
			super(b);
			b.setBackground(BACKGROUND);
		}

		protected Color getShadowColor()
		{
			return BORDER_FOREGROUND.darker().darker();
		}

		protected Color getHighlightColor()
		{
			return BORDER_FOREGROUND;
		}
	}

	public static JSlider createViewSlider(int min, int max, int value)
	{
		JSlider s = new JSlider(min, max, value)
		{
			public void updateUI()
			{
				super.updateUI();
				setForeground(FOREGROUND);
				setUI(new MySliderUI(this));
			}
		};
		s.setOpaque(false);
		s.setFocusable(false);
		return s;
	}

	public static void main(String args[])
	{
		LaunchCheSMapper.init();
		ComponentFactory.setBackgroundBlack(false);

		JPanel p = new JPanel();
		p.setBackground(BACKGROUND);
		//		p.setPreferredSize(new Dimension(500, 100));
		//		p.add(ComponentFactory.createViewButton("testing"));

		ClickableLabel b = ComponentFactory.createMinusViewButton();
		b.setEnabled(false);
		p.add(b);
		p.add(ComponentFactory.createCrossViewButton());
		p.add(ComponentFactory.createPlusViewButton());

		//		p.add(ComponentFactory.createViewButton("X"), new Insets(0, 5, 0, 5));
		//		p.add(ComponentFactory.createViewButton("X"), new Insets(0, 6, 0, 6));
		//		p.add(ComponentFactory.createViewSlider(0, 100, 33));
		SwingUtil.showInDialog(p, null, null, null, null, 1);

		System.exit(0);
	}

	private static HashMap<String, BufferedImage> chesMapperStringImages = new HashMap<String, BufferedImage>();

	public static BufferedImage getCheSMapperStringImage()
	{
		float fontsize = ScreenSetup.INSTANCE.getFontSize() + 2.0f;
		StringBuffer b = new StringBuffer();
		b.append(fontsize);
		b.append("#");
		b.append(isBackgroundBlack());
		String k = b.toString();
		if (!chesMapperStringImages.containsKey(k))
		{
			StringImageIcon ic = new StringImageIcon("CheS-Mapper", ComponentFactory.createViewLabel("").getFont()
					.deriveFont(Font.BOLD, fontsize), FOREGROUND_DISABLED);
			ic.setBackground(BACKGROUND);
			ic.setInsets(new Insets(3, 3, 3, 3));
			chesMapperStringImages.put(k, (BufferedImage) ic.getImage());

			//			ImageIcon ic2 = ImageLoader.getImage(ImageLoader.Image.ches_mapper_icon);
			//			MultiImageIcon ic3 = new MultiImageIcon(ic, ic2, MultiImageIcon.Layout.horizontal,
			//					MultiImageIcon.Orientation.center, 0);
			//			BufferedImage img = new BufferedImage(ic3.getIconWidth() + 6, ic3.getIconHeight() + 6,
			//					BufferedImage.TYPE_INT_ARGB);
			//			Graphics2D g = img.createGraphics();
			//			//g.setComposite(AlphaComposite.Src);
			//			g.setColor(BACKGROUND);
			//			g.fillRect(0, 0, ic3.getIconWidth() + 6, ic3.getIconHeight() + 6);
			//			ic3.paintIcon(null, g, 3, 3);
			//			g.dispose();
			//			chesMapperStringImages.put(k, img);
		}
		return chesMapperStringImages.get(k);
	}
}
 public class CompoundPropertyHighlighter extends Highlighter
{
	private CompoundProperty prop;

	private static HashMap<CompoundProperty, CompoundPropertyHighlighter> INSTANCES = new HashMap<CompoundProperty, CompoundPropertyHighlighter>();

	public static CompoundPropertyHighlighter create(CompoundProperty prop)
	{
		if (!INSTANCES.containsKey(prop))
			INSTANCES.put(prop, new CompoundPropertyHighlighter(prop));
		return INSTANCES.get(prop);
	}

	private CompoundPropertyHighlighter(CompoundProperty prop)
	{
		super(prop.toString());
		this.prop = prop;
	}

	public CompoundProperty getProperty()
	{
		return prop;
	}
} public class HighlightAutomatic
{
	boolean automatic = false;
	ViewControler controler;
	ClusteringImpl clustering;

	public HighlightAutomatic(ViewControler controler, ClusteringImpl clustering)
	{
		this.controler = controler;
		this.clustering = clustering;

		controler.addViewListener(new PropertyChangeListener()
		{

			@Override
			public void propertyChange(PropertyChangeEvent evt)
			{
				if (evt.getPropertyName().equals(ViewControler.PROPERTY_HIGHLIGHT_CHANGED))
				{
					update();
				}
			}
		});
		init();
	}

	public void init()
	{
		automatic = true;
	}

	private void update()
	{
		if (controler.getHighlighter() == Highlighter.CLUSTER_HIGHLIGHTER && !clustering.isClusterActive())
			automatic = true;
		else
			automatic = false;
	}

	public boolean resetClusterHighlighter(boolean activeClusterChanged)
	{
		if (automatic && controler.getHighlighter() == Highlighter.DEFAULT_HIGHLIGHTER && activeClusterChanged
				&& clustering.getNumClusters() > 1 && !clustering.isClusterActive())
		{
			controler.setHighlighter(Highlighter.CLUSTER_HIGHLIGHTER, false);
			automatic = true;
			return true;
		}
		return false;
	}

	public boolean resetDefaultHighlighter(boolean activeClusterChanged)
	{
		if (automatic && controler.getHighlighter() == Highlighter.CLUSTER_HIGHLIGHTER && activeClusterChanged
				&& clustering.getNumClusters() > 1 && clustering.isClusterActive())
		{
			controler.setHighlighter(Highlighter.DEFAULT_HIGHLIGHTER, false);
			automatic = true;
			return true;
		}
		return false;
	}

} public class Highlighter
{
	public static final Highlighter DEFAULT_HIGHLIGHTER = new Highlighter("None (show atom types)");
	public static final Highlighter CLUSTER_HIGHLIGHTER = new Highlighter("Cluster");

	private String name;

	public Highlighter(String name)
	{
		this.name = name;
	}

	public String toString()
	{
		return name;
	}
} public class SortFilterDialog extends JDialog
{

	private SortFilterDialog(final ViewControler viewControler, Clustering clustering)
	{
		super(Settings.TOP_LEVEL_FRAME, "Sort and filter features", true);

		DefaultFormBuilder builder = new DefaultFormBuilder(new FormLayout("fill:p:grow,10px,fill:p:grow"));

		TextPanel tp1 = new TextPanel("The feature list on the right side of the viewer can be sorted and filtered.\n");
		builder.append(tp1, 3);

		final JCheckBox sortBox = new JCheckBox("Sort features according to specificity ("
				+ Actions.getToggleSortingKey() + ")");
		sortBox.setSelected(viewControler.isFeatureSortingEnabled());
		sortBox.addActionListener(new ActionListener()
		{
			@Override
			public void actionPerformed(ActionEvent e)
			{
				viewControler.setFeatureSortingEnabled(sortBox.isSelected());
			}
		});
		builder.append(sortBox, 3);

		TextPanel tp2 = new TextPanel(
				"(The sorting is only available if clusters or compounds are selected, not for the entire dataset. "
						+ "The features are sorted according to specficity, i.e. the most important features are at the top. See online documentation for details.)\n");
		builder.append(tp2, 3);

		final JComboBox<FeatureFilter> filterCombo = new JComboBox<FeatureFilter>(FeatureFilter.validValues(clustering));
		filterCombo.setSelectedItem(viewControler.getFeatureFilter());
		filterCombo.addActionListener(new ActionListener()
		{
			@Override
			public void actionPerformed(ActionEvent e)
			{
				viewControler.setFeatureFilter((FeatureFilter) filterCombo.getSelectedItem());
			}
		});
		filterCombo.setRenderer(new DefaultListCellRenderer()
		{
			@Override
			public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected,
					boolean cellHasFocus)
			{
				return super.getListCellRendererComponent(list, ((FeatureFilter) value).niceString(), index,
						isSelected, cellHasFocus);
			}
		});
		builder.append(new JLabel("Filter features (" + Actions.getFilterFeaturesKey() + "):"));
		builder.append(filterCombo);

		JButton close = new JButton("Close");
		close.addActionListener(new ActionListener()
		{
			@Override
			public void actionPerformed(ActionEvent e)
			{
				SortFilterDialog.this.setVisible(false);
			}
		});
		builder.append(ButtonBarFactory.buildCloseBar(close));
		builder.setBorder(new EmptyBorder(10, 10, 10, 10));

		setLayout(new BorderLayout());
		add(builder.getPanel());

		viewControler.addViewListener(new PropertyChangeListener()
		{

			@Override
			public void propertyChange(PropertyChangeEvent evt)
			{
				if (!SortFilterDialog.this.isVisible())
					return;
				if (evt.getPropertyName().equals(ViewControler.PROPERTY_FEATURE_SORTING_CHANGED))
					sortBox.setSelected(viewControler.isFeatureSortingEnabled());
				else if (evt.getPropertyName().equals(ViewControler.PROPERTY_FEATURE_FILTER_CHANGED))
					filterCombo.setSelectedItem(viewControler.getFeatureFilter());
			}
		});

		pack();
		tp1.setPreferredWith(Math.max(300, sortBox.getPreferredSize().width));
		tp2.setPreferredWith(Math.max(300, sortBox.getPreferredSize().width));
		pack();
		setLocationRelativeTo(getOwner());
	}

	public static void showDialog(ViewControler viewControler, Clustering clustering)
	{
		SortFilterDialog d = new SortFilterDialog(viewControler, clustering);
		d.setVisible(true);
	}
} public class SubstructureHighlighter extends Highlighter
{
	private SubstructureSmartsType type;

	private static HashMap<SubstructureSmartsType, SubstructureHighlighter> INSTANCES = new HashMap<SubstructureSmartsType, SubstructureHighlighter>();

	public static SubstructureHighlighter create(SubstructureSmartsType type)
	{
		if (!INSTANCES.containsKey(type))
			INSTANCES.put(type, new SubstructureHighlighter(type));
		return INSTANCES.get(type);
	}

	private SubstructureHighlighter(SubstructureSmartsType type)
	{
		super(type.getName());
		this.type = type;
	}

	public SubstructureSmartsType getType()
	{
		return type;
	}
}
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.SwingUtilities;
import javax.vecmath.Vector3f;
import org.chesmapper.map.data.ClusteringData;
import org.chesmapper.map.dataInterface.CompoundGroupWithProperties;
import org.chesmapper.map.dataInterface.CompoundProperty;
import org.chesmapper.map.dataInterface.CompoundPropertyOwner;
import org.chesmapper.map.dataInterface.CompoundPropertyUtil;
import org.chesmapper.map.dataInterface.CompoundPropertyUtil.NominalColoring;
import org.chesmapper.map.dataInterface.FragmentProperty;
import org.chesmapper.map.dataInterface.NominalProperty;
import org.chesmapper.map.dataInterface.NumericProperty;
import org.chesmapper.map.dataInterface.SingleCompoundPropertyOwner;
import org.chesmapper.map.dataInterface.SubstructureSmartsType;
import org.chesmapper.map.gui.CheSMapperWizard;
import org.chesmapper.map.main.ScreenSetup;
import org.chesmapper.map.main.Settings;
import org.chesmapper.map.main.TaskProvider;
import org.chesmapper.view.cluster.Cluster;
import org.chesmapper.view.cluster.ClusterController;
import org.chesmapper.view.cluster.Clustering;
import org.chesmapper.view.cluster.ClusteringImpl;
import org.chesmapper.view.cluster.ClusteringUtil;
import org.chesmapper.view.cluster.Compound;
import org.chesmapper.view.cluster.CompoundFilter;
import org.chesmapper.view.cluster.CompoundFilterImpl;
import org.chesmapper.view.cluster.JitteringProvider;
import org.chesmapper.view.gui.View.AnimationSpeed;
import org.chesmapper.view.gui.swing.ComponentFactory;
import org.chesmapper.view.gui.util.CompoundPropertyHighlighter;
import org.chesmapper.view.gui.util.HighlightAutomatic;
import org.chesmapper.view.gui.util.Highlighter;
import org.chesmapper.view.gui.util.SortFilterDialog;
import org.chesmapper.view.gui.util.SubstructureHighlighter;
import org.jmol.adapter.smarter.SmarterJmolAdapter;
import org.jmol.api.JmolAdapter;
import org.jmol.api.JmolSimpleViewer;
import org.mg.javalib.gui.property.ColorGradient;
import org.mg.javalib.task.Task;
import org.mg.javalib.task.TaskDialog;
import org.mg.javalib.util.ArrayUtil;
import org.mg.javalib.util.ColorUtil;
import org.mg.javalib.util.ObjectUtil;
import org.mg.javalib.util.StringUtil;
import org.mg.javalib.util.SwingUtil;
import org.mg.javalib.util.ThreadUtil;


	@Override
	public void setCompoundSize(int compoundSize)
	{
		if (ClusteringUtil.COMPOUND_SIZE != compoundSize)
		{
			boolean increased = ClusteringUtil.COMPOUND_SIZE < compoundSize;
			ClusteringUtil.COMPOUND_SIZE = compoundSize;
			updateDensity(increased);
		}
	}

	@Override
	public HighlightMode getHighlightMode()
	{
		return highlightMode;
	}

	@Override
	public void setHighlightMode(HighlightMode mode)
	{
		if (highlightMode != mode)
		{
			highlightMode = mode;
			if (selectedHighlighter != Highlighter.DEFAULT_HIGHLIGHTER)
				updateAllClustersAndCompounds(true);
			fireViewChange(PROPERTY_HIGHLIGHT_MODE_CHANGED);
			if (mode == HighlightMode.Spheres)
				guiControler.showMessage("Highlight compound feature values with spheres.");
			else if (mode == HighlightMode.ColorCompounds)
				guiControler.showMessage("Highlight compound feature values by changing atom and bond colors.");
		}
	}

	@Override
	public boolean isHighlightLastFeatureEnabled()
	{
		return highlightLastFeatureEnabled;
	}

	@Override
	public void setHighlightLastFeatureEnabled(boolean b)
	{
		if (highlightLastFeatureEnabled != b)
		{
			highlightLastFeatureEnabled = b;
			if (highlightMode == HighlightMode.Spheres && selectedHighlighter != Highlighter.DEFAULT_HIGHLIGHTER)
				updateAllClustersAndCompounds(true);
			fireViewChange(PROPERTY_HIGHLIGHT_LAST_FEATURE);
			String lastMsg = ".";
			if (highlightLastFeatureEnabled && lastSelectedHighlighter != null
					&& lastSelectedHighlighter != Highlighter.DEFAULT_HIGHLIGHTER)
			{
				if (lastSelectedHighlighter == Highlighter.CLUSTER_HIGHLIGHTER)
					lastMsg = " (flattened spheroid highlights cluster assignement).";
				else if (lastSelectedHighlighter instanceof CompoundPropertyHighlighter)
					lastMsg = " (flattened spheroid highlights '" + lastSelectedHighlighter + "').";
			}
			guiControler.showMessage((b ? "Enable" : "Disable") + " highlighting of last selected feature" + lastMsg);
		}
	}

	@Override
	public void setSphereSize(double size)
	{
		if (view.sphereSize != size)
		{
			boolean increase = view.sphereSize < size;
			view.sphereSize = size;
			if (highlightMode == HighlightMode.Spheres && selectedHighlighter != Highlighter.DEFAULT_HIGHLIGHTER)
				updateAllClustersAndCompounds(true);
			guiControler.showMessage((increase ? "Increase" : "Descrease") + " sphere size to "
					+ StringUtil.formatDouble(size) + ".");
		}
	}

	@Override
	public void setSphereTranslucency(double translucency)
	{
		if (view.sphereTranslucency != translucency)
		{
			boolean increase = view.sphereTranslucency < translucency;
			view.sphereTranslucency = translucency;
			if (highlightMode == HighlightMode.Spheres && selectedHighlighter != Highlighter.DEFAULT_HIGHLIGHTER)
				updateAllClustersAndCompounds(true);
			guiControler.showMessage((increase ? "Increase" : "Descrease") + " sphere translucency to "
					+ StringUtil.formatDouble(translucency) + ".");
		}
	}

	@Override
	public int getCompoundSizeMax()
	{
		return ClusteringUtil.COMPOUND_SIZE_MAX;
	}

	@Override
	public int getCompoundSize()
	{
		return ClusteringUtil.COMPOUND_SIZE;
	}

	int jitteringLevel = 0;

	@Override
	public int getJitteringLevel()
	{
		return jitteringLevel;
	}

	private void resetJittering(HashSet<Compound> compounds, boolean skipPositionUpdate)
	{
		int idx = clustering.getJitteringResetLevel(compounds);
		if (idx != -1)
			setJitteringLevel(idx, compounds, true, skipPositionUpdate);
	}

	@Override
	public void setJitteringLevel(int level)
	{

		JitteringProvider.showJitterWarning();
