/*
* Copyright 2004-2010 Information & Software Engineering Group (188/1)
* Institute of Software Technology and Interactive Systems
* Vienna University of Technology, Austria
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.ifs.tuwien.ac.at/dm/somtoolbox/license.html
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package at.tuwien.ifs.somtoolbox.visualization;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Graphics2D;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;
import javax.swing.ButtonGroup;
import javax.swing.DefaultComboBoxModel;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.JScrollPane;
import javax.swing.JSpinner;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.ScrollPaneConstants;
import javax.swing.SpinnerNumberModel;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import org.apache.commons.lang.ArrayUtils;
import at.tuwien.ifs.commons.gui.controls.TitledCollapsiblePanel;
import at.tuwien.ifs.commons.gui.controls.swing.table.ClassColorTableModel;
import at.tuwien.ifs.somtoolbox.SOMToolboxException;
import at.tuwien.ifs.somtoolbox.apps.viewer.CommonSOMViewerStateData;
import at.tuwien.ifs.somtoolbox.apps.viewer.fileutils.ExportUtils;
import at.tuwien.ifs.somtoolbox.clustering.Cluster;
import at.tuwien.ifs.somtoolbox.clustering.DistanceFunctionType;
import at.tuwien.ifs.somtoolbox.clustering.WardClustering;
import at.tuwien.ifs.somtoolbox.clustering.functions.ClusterElementFunctions;
import at.tuwien.ifs.somtoolbox.clustering.functions.ComponentLine2DDistance;
import at.tuwien.ifs.somtoolbox.data.SOMLibTemplateVector;
import at.tuwien.ifs.somtoolbox.data.SOMVisualisationData;
import at.tuwien.ifs.somtoolbox.data.SharedSOMVisualisationData;
import at.tuwien.ifs.somtoolbox.data.TemplateVector;
import at.tuwien.ifs.somtoolbox.input.SOMInputReader;
import at.tuwien.ifs.somtoolbox.layers.GrowingLayer;
import at.tuwien.ifs.somtoolbox.models.GrowingSOM;
import at.tuwien.ifs.somtoolbox.structures.ComponentLine2D;
import at.tuwien.ifs.somtoolbox.structures.ElementWithIndex;
import at.tuwien.ifs.somtoolbox.util.DateUtils;
import at.tuwien.ifs.somtoolbox.util.FileUtils;
import at.tuwien.ifs.somtoolbox.util.GridBagConstraintsIFS;
import at.tuwien.ifs.somtoolbox.util.LeastRecentlyUsedCache;
import at.tuwien.ifs.somtoolbox.util.StringUtils;
import at.tuwien.ifs.somtoolbox.util.UiUtils;
import at.tuwien.ifs.somtoolbox.util.VectorTools;
import at.tuwien.ifs.somtoolbox.util.VisualisationUtils;
import at.tuwien.ifs.somtoolbox.util.comparables.ComponentRegionCount;
import at.tuwien.ifs.somtoolbox.visualization.metromap.MetroColorMap;
/**
* @author Rudolf Mayer
* @version $Id: MetroMapVisualizer.java 3939 2010-11-17 16:06:14Z frank $
*/
public class MetroMapVisualizer extends AbstractBackgroundImageVisualizer {
enum Mode {
NONE("None"), TARGET_NUMBER_OF_COMPONENTS("# comp"), THRESHOLD("threshold");
private String displayName;
private Mode(String displayName) {
this.displayName = displayName;
}
}
private static final Color STATION_FILL_COLOUR = Color.WHITE;
private static final int MIN_BINS = 2;
private static final int MAX_BINS = 99;
/** the attribute legend will have at most this many entries */
private static final int MAX_NUMBER_OF_LEGEND_ENTRIES = 100;
private static final String COMP_PREFIX = "Comp. ";
private int numberOfBins = 6;
private int[] selectedComponentIndices = null;
private SOMLibTemplateVector templateVector = null;
private int radius = 130 / 10;
private int innerRadius = radius - 2;
private int lineThickness = radius;
// these got passed around way too often
private int unitWidth;
private int unitHeight;
// That one's self explanatory now, isn't it? Well, it handles the offSet of parallel lines.
private double lineOffsetIsThisFractionOfRadius = 100d;
// shifts between parallel metro lines
private double lineOffset = lineThickness / lineOffsetIsThisFractionOfRadius;
// scaling of the ellipses for stops (better don't touch)
double scale = 1.6d;
private int dim = 1;
private int selectionTargetNumberOfComponents = dim / 2;
private double selectionThreshold = 0.5;
private int aggregationTargetNumberOfComponents = dim / 2;
private double aggregationSimilarity = 0.5;
private Mode aggregationMode = Mode.NONE;
private boolean snapping = false;
private Mode selectionMode = Mode.NONE;
protected DistanceFunctionType lineDistanceFunction = DistanceFunctionType.Euclidean;
private MetroMapControlPanel metroMapControlPanel;
protected final MetroColorMap colorMap = metroColorMap;
private static final String[] legendColumnNames = new String[] { "Component", "Color" };
private JComboBox overlayVisualisationComboBox = new JComboBox();
private LeastRecentlyUsedCache<ClusterElementFunctions<ComponentLine2D>, WardClustering<ComponentLine2D>> clusterCache = new LeastRecentlyUsedCache<ClusterElementFunctions<ComponentLine2D>, WardClustering<ComponentLine2D>>(
20);
public MetroMapVisualizer() {
NUM_VISUALIZATIONS = 1;
VISUALIZATION_NAMES = new String[] { "Metro Map" };
VISUALIZATION_SHORT_NAMES = new String[] { "MetroMap" };
VISUALIZATION_DESCRIPTIONS = new String[] { "Robert Neumayer, Rudolf Mayer, Georg Pölzlbauer, and Andreas Rauber."
+ " The metro visualisation of component planes for self-organising maps. "
+ "In Proceedings of the International Joint Conference on Neural Networks (IJCNN'07), "
+ "Orlando, FL, USA, August 12-17 2007. IEEE Computer Society." };
neededInputObjects = new String[] { SOMVisualisationData.TEMPLATE_VECTOR };
if (!(controlPanel instanceof MetroMapControlPanel)) {
// create control panel if it is a generic panel
controlPanel = new MetroMapControlPanel(this);
metroMapControlPanel = (MetroMapControlPanel) controlPanel;
}
// Scale for the MetroMapVisualizer needs to be smaller, as the visualisation is made of lines, which cannot be
// scaled too much.
preferredScaleFactor = 1;
}
@Override
public BufferedImage getVisualization(int index, GrowingSOM gsom, int width, int height) throws SOMToolboxException {
return createVisualization(index, gsom, width, height);
}
private static Color[] COLORS = { Color.BLUE, Color.CYAN, Color.DARK_GRAY, Color.GREEN, Color.LIGHT_GRAY,
Color.MAGENTA, Color.ORANGE, Color.PINK, Color.RED, Color.YELLOW };
private static final MetroColorMap metroColorMap = new MetroColorMap();
public float[][] dashPatterns = initDashPatterns();
public static float[] lineThicknessFactors = { 1, 0.75f, 0.5f, 0.3f };
private UMatrix uMatrixVisualizer;
private FlowBorderlineVisualizer flowBorderlineVisualizer;
private ThematicClassMapVisualizer thematicClassMapVisualizer;
// private DoubleMatrix2D matrix;
private BufferedImage overlayVisualisation;
// private ColourLegendTable colourLegendTable1;
//
// private ColourLegendTable colourLegendTable2;
private JTable colourLegendTable1;
private double legendColumns = 2;
private int endIndexTable1;
// TODO only compute once (after we stop debugging :-))
protected Point2D[][] binCentres;
private Hashtable<Point2D, Integer> unitsWithStopsOnThemAndHowMany;
private Hashtable<Point2D, int[]>[] allDirections;
private GrowingSOM gsom;
private AffineTransformOp op;
/** Array of all points on the SOM . */
private Point2D[] allSOMCoordinates = null;
/**
* Lookup-up matrix to check fast if two points are on a diagonal to each other. The table has the sum of the X & Y
* coordinates of each point. For a given unit, all units that are in the lower-left or upper-right diagonal will
* have the same value. As an example, for a 6x6 SOM, it would thus have values as follows:
*
* <pre>
* 0 1 2 3 4 5 6
* 1 2 3 4 5 6 7
* 2 3 4 5 6 7 8
* 3 4 5 6 7 8 9
* 4 5 6 7 8 9 10
* 5 6 7 8 9 10 11
* 6 7 8 9 10 11 12
* </pre>
*/
private double[] allSOMCoordinatesSumValues = null;
/**
* Similar matrix as {@link #allSOMCoordinatesSumValues}, but containing the difference of the X&Y coordinates, for
* upper-left and lower-right diagonal units. As an example, for a 6x6 SOM, it would thus have values as follows:
*
* <pre>
* 0 1 2 3 4 5 6
* -1 0 1 2 3 4 5
* -2 -1 0 1 2 3 4
* -3 -2 -1 0 1 2 3
* -4 -3 -2 -1 0 1 2
* -5 -4 -3 -2 -1 0 1
* -6 -5 -4 -3 -2 -1 0
* </pre>
*/
private double[] allSOMCoordinatesDiffValues = null;
// these guys store the indices for the single component colouring where it is looked up again
private List<? extends Cluster<ComponentLine2D>> clusters;
private List<ComponentRegionCount> selectedComponents;
private Palette binPalette;
@Override
protected String getCacheKey(GrowingSOM gsom, int index, int width, int height) {
return appendToCacheKey(gsom, index, width, height, snapping, aggregationMode, aggregationSimilarity,
aggregationTargetNumberOfComponents, numberOfBins);
}
@Override
public BufferedImage createVisualization(int index, GrowingSOM gsom, int width, int height)
throws SOMToolboxException {
if (templateVector == null || this.gsom != gsom) {
// do some init
this.gsom = gsom;
templateVector = gsom.getSharedInputObjects().getTemplateVector();
if (templateVector == null) {
throw new SOMToolboxException("You need to specify the " + neededInputObjects[0]);
}
metroMapControlPanel.initLegendTableNormal();
final int scrollBarWidth = metroMapControlPanel.colourLegendScrollPane.getVerticalScrollBar().getWidth() + 5;
final int scrollBarHeight = metroMapControlPanel.colourLegendScrollPane.getHorizontalScrollBar().getHeight() + 5;
metroMapControlPanel.colourLegendScrollPane.setMinimumSize(new Dimension(
CommonSOMViewerStateData.getInstance().controlElementsWidth - scrollBarWidth, Math.max(
colourLegendTable1.getPreferredSize().height + scrollBarHeight, 50)));
metroMapControlPanel.colourLegendScrollPane.setPreferredSize(new Dimension(
CommonSOMViewerStateData.getInstance().controlElementsWidth - scrollBarWidth, Math.max(
colourLegendTable1.getPreferredSize().height + scrollBarHeight, 50)));
}
if (allSOMCoordinates == null) {
initNeighbourhoodLookup(gsom);
}
return createMetromapImage(index, gsom, width, height);
}
public void initNeighbourhoodLookup(GrowingSOM gsom) {
// init the data structures for neighbourhood lookup
int xSize = gsom.getLayer().getXSize();
int ySize = gsom.getLayer().getYSize();
allSOMCoordinates = new Point2D[xSize * ySize];
// TODO this is highly confusing, what about initialising an index = 0 first and then use this one?
// or maybe x2 * xSize or something?
for (int i2 = 0; i2 < xSize; i2++) {
for (int j = 0; j < ySize; j++) {
allSOMCoordinates[i2 * ySize + j] = new Point2D.Double(i2, j);
}
}
allSOMCoordinatesSumValues = new double[allSOMCoordinates.length];
allSOMCoordinatesDiffValues = new double[allSOMCoordinates.length];
for (int i = 0; i < allSOMCoordinatesDiffValues.length; i++) {
allSOMCoordinatesSumValues[i] = allSOMCoordinates[i].getX() + allSOMCoordinates[i].getY();
allSOMCoordinatesDiffValues[i] = allSOMCoordinates[i].getX() - allSOMCoordinates[i].getY();
}
}
/** Will create the metro map image for the given params. */
protected BufferedImage createMetromapImage(int index, GrowingSOM gsom, int width, int height)
throws SOMToolboxException {
GrowingLayer layer = gsom.getLayer();
// we do this every time now, check for existing binCentres and correct sizes
// thereof inside computeFinalComponentLines
binCentres = computeFinalComponentLines(layer);
BufferedImage res = createOverlayVisualisation(width, height);
Graphics2D g = (Graphics2D) res.getGraphics();
unitWidth = width / layer.getXSize();
unitHeight = height / layer.getYSize();
binPalette = new Palette("RGB, " + numberOfBins + " colors", "RGB_" + numberOfBins + "_colors", "",
ColorGradientFactory.RGBGradient(), numberOfBins);
g.setStroke(new BasicStroke(lineThickness, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 1f,
new float[] { 5f }, 5f));
// reset legend table (TODO: do this only if it is not in reset mode
// FIXME: rudi? I think this is not needed because I don't miss it
if (selectionMode == Mode.NONE && aggregationMode == Mode.NONE) {
// metroMapControlPanel.initLegendTableNormal();
}
// draw normal layout plus line
if (!snapping) {
createLayout(g, layer, true);
} else { // do parallel layouting in here (for snapped centres only, that's why we check the snapping boolean)
createLayout(g, layer, false);
createSnappedMetroLayout(g, layer);
}
return res;
}
public BufferedImage createMetromapImage(int index, GrowingSOM gsom, int width, int height, int component) {
// FIXME: this could be a parameter
int numberOfBins = 6;
GrowingLayer layer = gsom.getLayer();
int[][] binAssignment = layer.getBinAssignment(component, numberOfBins);
unitWidth = width / gsom.getLayer().getXSize();
unitHeight = height / gsom.getLayer().getYSize();
BufferedImage res = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics2D g = (Graphics2D) res.getGraphics();
colourUnits(g, binAssignment, new Palette("RGB, " + numberOfBins + " colors",
"RGB_" + numberOfBins + "_colors", "", ColorGradientFactory.RGBGradient(), numberOfBins).getColors());
// special stroke for export mode, size pretty hard-coded
g.setStroke(new BasicStroke(5, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
drawLine(g, component, layer.getBinCentres(numberOfBins));
VisualisationUtils.drawUnitGrid((Graphics2D) res.getGraphics(), gsom, width, height);
return res;
}
/**
* Creates the selected overlay visualisation.
*
* @param width width of the overlay
* @param height of the overlay
* @return overlay image
*/
private BufferedImage createOverlayVisualisation(int width, int height) throws SOMToolboxException {
BufferedImage res;
Graphics2D g;
String visName = overlayVisualisationComboBox.getSelectedItem().toString();
String visOption = null;
int indexOf = visName.indexOf("/");
if (indexOf != -1) {
visOption = visName.substring(indexOf + 1);
visName = visName.substring(0, indexOf);
}
if (StringUtils.equalsAny(visName, UMatrix.UMATRIX_SHORT_NAMES)) {
if (uMatrixVisualizer == null) {
uMatrixVisualizer = new UMatrix();
}
Palette riverPalette = Palettes.getPaletteByName("MetroMap");
uMatrixVisualizer.setPalette(riverPalette);
overlayVisualisation = uMatrixVisualizer.createVisualization(visName, gsom,
width / uMatrixVisualizer.getPreferredScaleFactor(),
height / uMatrixVisualizer.getPreferredScaleFactor());
double scaleX = (double) width / (double) overlayVisualisation.getWidth();
double scaleY = (double) height / (double) overlayVisualisation.getHeight();
op = new AffineTransformOp(AffineTransform.getScaleInstance(scaleX, scaleY), new RenderingHints(
RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC));
res = op.filter(overlayVisualisation, null);
g = (Graphics2D) res.getGraphics();
} else if (StringUtils.equalsAny(visName, FlowBorderlineVisualizer.FLOWBORDER_SHORT_NAMES)) {
if (flowBorderlineVisualizer == null) {
flowBorderlineVisualizer = new FlowBorderlineVisualizer();
}
overlayVisualisation = flowBorderlineVisualizer.createVisualization(visName, gsom, width
/ flowBorderlineVisualizer.getPreferredScaleFactor(),
height / flowBorderlineVisualizer.getPreferredScaleFactor());
double scaleX = (double) width / (double) overlayVisualisation.getWidth();
double scaleY = (double) height / (double) overlayVisualisation.getHeight();
op = new AffineTransformOp(AffineTransform.getScaleInstance(scaleX, scaleY), new RenderingHints(
RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC));
res = op.filter(overlayVisualisation, null);
g = (Graphics2D) res.getGraphics();
} else if (visName.equals(ThematicClassMapVisualizer.CLASSMAP_SHORT_NAME)) {
if (thematicClassMapVisualizer == null) {
thematicClassMapVisualizer = new ThematicClassMapVisualizer();
}
thematicClassMapVisualizer.setInputObjects(inputObjects);
if (org.apache.commons.lang.StringUtils.equals(visOption, "Chess")) {
thematicClassMapVisualizer.setInitialParams(true, true, 0);
}
try {
overlayVisualisation = thematicClassMapVisualizer.createVisualization(0, gsom, width
/ thematicClassMapVisualizer.getPreferredScaleFactor(),
height / thematicClassMapVisualizer.getPreferredScaleFactor());
} catch (SOMToolboxException e) {
e.printStackTrace();
}
double scaleX = (double) width / (double) overlayVisualisation.getWidth();
double scaleY = (double) height / (double) overlayVisualisation.getHeight();
op = new AffineTransformOp(AffineTransform.getScaleInstance(scaleX, scaleY), new RenderingHints(
RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC));
res = op.filter(overlayVisualisation, null);
g = (Graphics2D) res.getGraphics();
}
// otherwise create an empty image
else {
res = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
g = (Graphics2D) res.getGraphics();
g.setColor(Color.WHITE);
g.fillRect(0, 0, width, height);
}
return res;
}
/**
* Colours SOM units according to the given bin assignment.
*
* @param g thingy to draw on
* @param binAssignment assignment of single bins
* @param binPalette the palette to draw the bins with
*/
private void colourUnits(Graphics2D g, int[][] binAssignment, Color[] binPalette) {
for (int j = 0; j < binAssignment.length; j++) {
for (int k = 0; k < binAssignment[j].length; k++) {
if (binAssignment[j][k] != -1) {
g.setColor(binPalette[binAssignment[j][k]]);
} else {
g.setColor(Color.WHITE);
}
g.fillRect(j * unitWidth, k * unitWidth, unitWidth, unitHeight);
}
}
}
/**
* Draw the line for the current line or component index. Note that this one's not snapped.
*
* @param g thingy to draw on
* @param lineIndex the line to draw
*/
private void drawLineAndStation(Graphics2D g, int lineIndex) {
for (int stopIndex = 0; stopIndex < binCentres[lineIndex].length; stopIndex++) {
if (stopIndex < binCentres[lineIndex].length - 1) {
drawComponentLineSegment(binCentres[lineIndex][stopIndex], binCentres[lineIndex][stopIndex + 1], g,
lineIndex, false);
}
// TODO make drawing stations a function this is used twice (at least)
g.setColor(Color.BLACK);
int x = (int) (binCentres[lineIndex][stopIndex].getX() * new Double(unitWidth).doubleValue() + new Double(
unitWidth).doubleValue() / 2);
int y = (int) (binCentres[lineIndex][stopIndex].getY() * new Double(unitHeight).doubleValue() + new Double(
unitHeight).doubleValue() / 2);
g.fillOval(new Double(x).intValue() - radius, new Double(y).intValue() - radius, radius * 2, radius * 2);
g.setColor(colorMap.getColor(lineIndex));
// make this one a station if the last one had the same coordinates (i.e. multiple consecutive stops on the
// same coordinates)
if (stopIndex > 1 && binCentres[lineIndex][stopIndex].getX() == binCentres[lineIndex][stopIndex - 1].getX()
&& binCentres[lineIndex][stopIndex].getY() == binCentres[lineIndex][stopIndex - 1].getY()) {
g.setColor(Color.WHITE);
}
g.fillOval(new Double(x).intValue() - innerRadius, new Double(y).intValue() - innerRadius, innerRadius * 2,
innerRadius * 2);
}
}
private void drawLine(Graphics2D g, int lineIndex, Point2D[][] binCentres) {
for (int stopIndex = 0; stopIndex < binCentres[lineIndex].length - 1; stopIndex++) {
drawComponentLineSegment(binCentres[lineIndex][stopIndex], binCentres[lineIndex][stopIndex + 1], g,
lineIndex, true);
}
}
/**
* Create the layout for the metro lines (not snapped). This method possibly colours the units of the SOM according
* to their bin assignments, that is if only one component or component group is selected.
*
* @param g piece of paper
* @param layer layer of the SOM
* @param drawLine colouring only or drawing the actual lines too (you always have a choice after all)
*/
private void createLayout(Graphics2D g, GrowingLayer layer, boolean drawLine) {
ComponentLine2DDistance dist = new ComponentLine2DDistance(lineDistanceFunction);
for (int i = 0; i < binCentres.length; i++) {
// if selection -> draw only selected lines
// draw component plane if we have only one component selected, and no selection / aggregation was done
// FIXME: we should also draw the component plane in case of aggregation, but then we need to derive the
// "real" selected component index
// FIXME: still -- we now assign the bin assignment of the closest line to the cluster centre, probably not
// the best choice but it will do for now
if (ArrayUtils.isEmpty(selectedComponentIndices) || ArrayUtils.contains(selectedComponentIndices, i)) {
int binAssignmentIndex = -1;
if (clusters != null) {
Cluster<ComponentLine2D> cluster = clusters.get(i);
binAssignmentIndex = cluster.get(dist.getIndexOfLineClosestToMean(cluster)).getIndex();
}
if (selectedComponents != null) {
binAssignmentIndex = selectedComponents.get(i).getIndex().intValue();
}
// colour units in here (only if a single component is selected)
if (!ArrayUtils.isEmpty(selectedComponentIndices) && selectedComponentIndices.length == 1) { // &&
// selectionMode
// ==
// Mode.NONE){
binAssignmentIndex = binAssignmentIndex == -1 ? i : binAssignmentIndex;
int[][] binAssignment = layer.getBinAssignment(binAssignmentIndex, numberOfBins);
colourUnits(g, binAssignment, binPalette.getColors());
}
if (drawLine) {
drawLineAndStation(g, i);
}
}
}
}
/**
* Get a Hastable[] containing the per-dimension directions to draw on each unit of the som. This includes units not
* having a stop, i.e. this also handles long links across empty units (and if you think it's a mess now, you
* should've seen it earlier on).
*
* @return Hashtable[] containing the outgoing lines in each unit for all dimensions
*/
// FIXME other way around this? Rudi?
@SuppressWarnings("unchecked")
private Hashtable<Point2D, int[]>[] getAllDirections() {
// with crap like this you never know
boolean debug = false;
long start = System.currentTimeMillis();
Hashtable<Point2D, int[]>[] allDirections = new Hashtable[binCentres.length];
for (int l = 0; l < binCentres.length; l++) { // for each component
Hashtable<Point2D, int[]> outgoingLineTable = new Hashtable<Point2D, int[]>(); // find counts for parallel
// lines
Point2D[] snappedLine = binCentres[l];
// ok, heres the deal:
// for each line stop on the line we store the in and outgoing directions (that's two for each stop)
// then we go look for units that have a line on them but no stop (this can happen in both directions
// on each stop and include stops that lie on the same coordinates which is annoying like hell)
// we also add units found this way
for (int m = 0; m < snappedLine.length; m++) {
Point2D currentPoint = snappedLine[m];
Point2D backwardPoint = null;
Point2D backwardCoords = null;
Point2D forwardPoint = null;
Point2D forwardCoords = null;
int[] forwardDirections = null;
int[] backwardDirections = null;
// now we initialise some of these guys depending on
// whether we're on the first or last stop or somewhere
// along the line
if (m > 0) {
backwardPoint = snappedLine[m - 1];
backwardCoords = new Point2D.Double();
backwardDirections = getDirectionArray(currentPoint, backwardPoint);
backwardCoords = getNextCoords(currentPoint, backwardDirections);
if (!(m < snappedLine.length - 1)) {
if (outgoingLineTable.get(currentPoint) != null) {
outgoingLineTable.put(currentPoint,
VectorTools.mergeArrays(backwardDirections, outgoingLineTable.get(currentPoint)));
} else {
outgoingLineTable.put(currentPoint, backwardDirections);
}
}
}
if (m < snappedLine.length - 1) {
forwardPoint = snappedLine[m + 1];
forwardCoords = new Point2D.Double();
forwardDirections = getDirectionArray(currentPoint, forwardPoint);
forwardCoords = getNextCoords(currentPoint, forwardDirections);
if (!(m > 0)) {
if (outgoingLineTable.get(currentPoint) != null) {
outgoingLineTable.put(currentPoint,
VectorTools.mergeArrays(forwardDirections, outgoingLineTable.get(currentPoint)));
} else {
outgoingLineTable.put(currentPoint, forwardDirections);
}
}
}
// for this component we need no processing other than that
// it can only go in two directions when lying on a line (needed to say that)
// of it's not an endpoint of a line, add the line itself
if (backwardPoint != null && forwardPoint != null) {
int[] mergedDirections = VectorTools.mergeArrays(forwardDirections, backwardDirections);
if (outgoingLineTable.get(currentPoint) != null) {
mergedDirections = VectorTools.mergeArrays(mergedDirections,
outgoingLineTable.get(currentPoint));
}
if (debug) {
System.out.println(l + " " + currentPoint + " " + ArrayUtils.toString(mergedDirections)
+ "(direct add - no border stop)");
}
outgoingLineTable.put(currentPoint, mergedDirections);
}
// then go for the forward stops and units in between stops
if (forwardCoords != null) {
while (!(forwardPoint.getX() == forwardCoords.getX() && forwardPoint.getY() == forwardCoords.getY())) {
if (outgoingLineTable.get(forwardCoords) != null) {
if (debug) {
System.out.println(l
+ " adding: "
+ forwardCoords
+ " "
+ ArrayUtils.toString(VectorTools.mergeArrays(forwardDirections,
outgoingLineTable.get(forwardCoords))) + "(up found)");
}
outgoingLineTable.put(forwardCoords,
VectorTools.mergeArrays(forwardDirections, outgoingLineTable.get(forwardCoords)));
} else {
outgoingLineTable.put(forwardCoords, forwardDirections);
if (debug) {
System.out.println(l + " adding: " + forwardCoords + " "
+ ArrayUtils.toString(forwardDirections) + "(up new)");
System.out.println(currentPoint);
System.out.println(forwardPoint);
System.out.println(forwardCoords);
}
}
forwardCoords = getNextCoords(forwardCoords, forwardDirections);
}
}
// then go for the backward stops and units in between stops
if (backwardCoords != null) {
backwardCoords = getNextCoords(currentPoint, backwardDirections);
while (!(backwardPoint.getX() == backwardCoords.getX() && backwardPoint.getY() == backwardCoords.getY())) {
if (outgoingLineTable.get(backwardCoords) != null) {
if (debug) {
System.out.println(l
+ " adding: "
+ backwardCoords
+ " "
+ ArrayUtils.toString(VectorTools.mergeArrays(backwardDirections,
outgoingLineTable.get(backwardCoords))) + "(down found)");
}
outgoingLineTable.put(backwardCoords,
VectorTools.mergeArrays(backwardDirections, outgoingLineTable.get(backwardCoords)));
} else {
outgoingLineTable.put(backwardCoords, backwardDirections);
if (debug) {
System.out.println(l + " adding: " + backwardCoords + " "
+ ArrayUtils.toString(backwardDirections) + "(down new)");
}
}
backwardCoords = getNextCoords(backwardCoords, backwardDirections);
}
}
}
allDirections[l] = outgoingLineTable;
}
if (debug) {
System.out.println(DateUtils.formatDuration(System.currentTimeMillis() - start));
}
return allDirections;
}
/** Get a Hashtable of unit coordinates and the number of centres which lie on it. */
private Hashtable<Point2D, Integer> getUnitsWithStopsOnThemAndHowMany() {
Hashtable<Point2D, Integer> unitsWithStopsOnThemAndHowMany = new Hashtable<Point2D, Integer>();
for (int i_bin = 0; i_bin < numberOfBins; i_bin++) {
for (int i_components = 0; i_components < allDirections.length; i_components++) {
// check for selected components
if (!ArrayUtils.isEmpty(selectedComponentIndices)
&& !ArrayUtils.contains(selectedComponentIndices, i_components)) {
continue;
}
Point2D currentCentre = binCentres[i_components][i_bin];
if (unitsWithStopsOnThemAndHowMany.get(currentCentre) != null) {
unitsWithStopsOnThemAndHowMany.put(currentCentre,
unitsWithStopsOnThemAndHowMany.get(currentCentre) + 1);
} else {
unitsWithStopsOnThemAndHowMany.put(currentCentre, new Integer(1));
}
}
}
return unitsWithStopsOnThemAndHowMany;
}
/** Gets components which have links in the given direction. */
private int[] getComponentsInDirection(Point2D currentSOMUnit, int i_direction) {
int[] componentsInDirection = new int[allDirections.length];
// get all four on this unit
// then get the number of links in directions per component
// mcgyver together the number of links per component
// only draw for selected components
for (int i_components = 0; i_components < allDirections.length; i_components++) {
Hashtable<Point2D, int[]> outgoingLineTable = allDirections[i_components];
int[] directions = outgoingLineTable.get(currentSOMUnit);
// check for selected indices and if so only take into account the selected ones
if (directions == null || !ArrayUtils.isEmpty(selectedComponentIndices)
&& !ArrayUtils.contains(selectedComponentIndices, i_components)) {
componentsInDirection[i_components] = 0;
continue;
}
componentsInDirection[i_components] = directions[i_direction];
}
return componentsInDirection;
}
/**
* Organises all metro drawing for snapped lines, i.e. the layouting for parallel lines.
*
* @param g thingy to draw on
*/
private void createSnappedMetroLayout(Graphics2D g, GrowingLayer layer) {
// first initialise our caches
allDirections = getAllDirections();
unitsWithStopsOnThemAndHowMany = getUnitsWithStopsOnThemAndHowMany();
// int[][] directionsCache = new int[8][allDirections.length];
for (Point2D currentSOMUnit : allSOMCoordinates) { // we do this for each unit of the som
// we now draw for all directions so that stops can be handled in this (one) loop
for (int i_direction = 0; i_direction < 8; i_direction++) {
// adasfdafs
int compCounter = 0; // counts the number of components to be drawn on this unit
int[] componentsInDirection = getComponentsInDirection(currentSOMUnit, i_direction);
double numberOfLines = VectorTools.sum(componentsInDirection);
double multiLineOffset = (numberOfLines - 1) * lineOffset / 2;
double xDir = 0;
double yDir = 0;
double xOffset = 0;
double yOffset = 0;
// TODO this is flipped, doesn't hurt, but is kind of strange and makes it differ from matlab
// for (int i_components = componentsInDirection.length - 1; i_components >= 0; i_components--) {
for (int i_components = 0; i_components < componentsInDirection.length; i_components++) {
if (componentsInDirection[i_components] == 0) {
continue;
}
if (i_direction == 0) {
xDir = 0;
yDir = -1;
xOffset = lineOffset * compCounter - multiLineOffset;
yOffset = 0;
}
if (i_direction == 1) {
xDir = 1;
yDir = -1;
xOffset = (lineOffset * compCounter - multiLineOffset) / Math.sqrt(2);
yOffset = (lineOffset * compCounter - multiLineOffset) / Math.sqrt(2);
}
if (i_direction == 2) {
xDir = 1;
yDir = 0;
xOffset = 0;
yOffset = lineOffset * compCounter - multiLineOffset;
}
if (i_direction == 3) {
xDir = 1;
yDir = 1;
xOffset = +(lineOffset * compCounter - multiLineOffset) / Math.sqrt(2);
yOffset = -(lineOffset * compCounter - multiLineOffset) / Math.sqrt(2);
}
// Finally, draw the line
Point2D startPoint = new Point2D.Double(currentSOMUnit.getX() + xOffset, currentSOMUnit.getY()
+ yOffset);
Point2D endPoint = new Point2D.Double(currentSOMUnit.getX() + xDir + xOffset, currentSOMUnit.getY()
+ yDir + yOffset);
drawComponentLineSegment(startPoint, endPoint, g, i_components, false);
compCounter++;
// handle stops (i.e. region centres)
for (int i_bin = 0; i_bin < numberOfBins; i_bin++) {
Point2D currentCentre = binCentres[i_components][i_bin];
// draw no stops if this is gonna be a station anyways (stops with more than two centres on them
// are stations)
if (!(currentCentre.getX() == currentSOMUnit.getX() && currentCentre.getY() == currentSOMUnit.getY())
|| unitsWithStopsOnThemAndHowMany.get(currentSOMUnit) != null
&& unitsWithStopsOnThemAndHowMany.get(currentSOMUnit) > 1
|| yOffset == 0
&& xOffset == 0 && numberOfLines > 1) {
continue;
}
int x = (int) (currentCentre.getX() * unitWidth + unitWidth / 2);
int y = (int) ((currentCentre.getY() + yOffset) * unitHeight + unitHeight / 2);
// filter out the units that have several lines but only one centre of them
g.setColor(Color.BLACK);
g.fillOval(new Double(x).intValue() - radius, new Double(y).intValue() - radius, radius * 2,
radius * 2);
g.setColor(colorMap.getColor(i_components));
g.fillOval(x - innerRadius, y - innerRadius, innerRadius * 2, innerRadius * 2);
}
}
}
}
// now go for the stations, i.e. direction changes for more than two lines
// or more than two stops on one coordinate
createStations(g);
}
/**
* Draw metro stations on top of existing maps. A metro station is drawn as a large ellipse with dark border. In
* contrast to normal stops which are drawn on the bin centres, stations are drawn whenever: 1) several bin centres
* are located on the same unit 2) lines that were parallel are not so anymore (merging of lines) 3) several lines
* have stops on the same unit
*
* @param g thingy to draw on
*/
private void createStations(Graphics2D g) {
for (Point2D currentSOMUnit : allSOMCoordinates) { // we do this for each unit of the som
int[][] directionsCache = new int[8][allDirections.length];
boolean isStation = false;
for (int i_direction = 0; i_direction < 8; i_direction++) {
int[] componentInDirections = getComponentsInDirection(currentSOMUnit, i_direction);
directionsCache[i_direction] = componentInDirections;
double numberOfLines = VectorTools.sum(componentInDirections);
if (numberOfLines > 0) {
// do we have more than one bincentre on this unit? if so it's gonna be a stop anyways
if (unitsWithStopsOnThemAndHowMany.get(currentSOMUnit) != null
&& unitsWithStopsOnThemAndHowMany.get(currentSOMUnit) > 1) {
isStation = true;
continue;
}
int matches = 0;
for (int i_direction_comparison = i_direction + 1; i_direction_comparison < 8; i_direction_comparison++) {
int[] componentsInDirectionComparison = getComponentsInDirection(currentSOMUnit,
i_direction_comparison);
// double numberOfLinesComparison = sum(directionsPerComponentComparison);
if (allDirections.length == VectorTools.calculateArrayOverlaps(componentInDirections,
componentsInDirectionComparison)) {
matches++;
} else if (VectorTools.sum(componentsInDirectionComparison) > 0) {
isStation = true;
continue;
}
}
if (matches > 1) {
isStation = true;
}
}
}
if (isStation) {
g.setColor(Color.WHITE);
int x = (int) (currentSOMUnit.getX() * unitWidth + unitWidth / 2);
int y = (int) (currentSOMUnit.getY() * unitHeight + unitHeight / 2);
g.fillOval(x - radius, y - radius, radius * 2, radius * 2);
// g.setColor(colorMap.getColor(i_component));
g.fillOval(x - innerRadius, y - innerRadius, innerRadius * 2, innerRadius * 2);
int[] stationLayout = new int[] { 0, 0, 0, 0 };
stationLayout[0] = Math.max(VectorTools.sum(directionsCache[2]), VectorTools.sum(directionsCache[6]));
stationLayout[1] = Math.max(VectorTools.sum(directionsCache[3]), VectorTools.sum(directionsCache[7]));
stationLayout[2] = Math.max(VectorTools.sum(directionsCache[0]), VectorTools.sum(directionsCache[4]));
stationLayout[3] = Math.max(VectorTools.sum(directionsCache[1]), VectorTools.sum(directionsCache[5]));
int i_length = VectorTools.getIndexOfMaxValue(stationLayout);
int i_width = 0;
if (i_length == 0) {
i_width = 2;
} else if (i_length == 2) {
i_width = 0;
} else if (i_length == 1) {
i_width = 3;
} else if (i_length == 3) {
i_width = 1;
// }
}
double len = stationLayout[i_length];
double wid = Math.max(1, stationLayout[i_width]);
double x_offset = 0;
double y_offset = 0;
double len_draw = len * .6;
// % the rotation of the ellipse
double rotation = 0;
double tmp;
if (i_length == 0) { // 0 -- up
x_offset = 0;
y_offset = lineOffset * len_draw;
} else if (i_length == 1) { // 1 -- right upper
x_offset = lineOffset * len_draw / Math.sqrt(2);
y_offset = lineOffset * len_draw / Math.sqrt(2);
rotation = 3;
tmp = wid;
wid = len;
len = tmp;
} else if (i_length == 2) { // 2 -- right
x_offset = lineOffset * len / 2;
y_offset = 0;
tmp = wid;
wid = len;
len = tmp;
} else if (i_length == 3) { // 3 -- right lower
x_offset = lineOffset * len_draw / Math.sqrt(2);
y_offset = -(lineOffset * len_draw / Math.sqrt(2));
rotation = 1;
tmp = wid;
wid = len;
len = tmp;
}
double hhh = (x_offset + wid) / (lineThickness * scale);
double www = (y_offset + len) / (lineThickness * scale);
// make ellipses bigger than underlying stops
scale *= 1.2d;
// % this is the outer black ellipse
g.setColor(Color.BLACK);
Ellipse2D ellipse = new Ellipse2D.Double(x - hhh * radius * radius * scale / 2, y - www * radius
* radius * scale / 2, hhh * radius * radius * scale, www * radius * radius * scale);
// ok, here's how rotation works:
// 1. rotate the whole world
// 2. draw the ellipse (not rotated)
// 3. backrotate the whole world
AffineTransform tx = new AffineTransform();
tx.rotate(Math.toRadians(45 * rotation), x, y);
g.draw(tx.createTransformedShape(ellipse));
g.setColor(STATION_FILL_COLOUR);
g.fill(tx.createTransformedShape(ellipse));
tx.rotate(Math.toRadians(-45 * rotation), x, y);
}
}
}
/** Computes the line selection, returns the selected lines */
private Point2D[][] doSelection(Point2D[][] binCentres, GrowingLayer layer) throws SOMToolboxException {
Logger.getLogger("at.tuwien.ifs.somtoolbox").info(
"Starting component selection, mode " + selectionMode.displayName);
ArrayList<ComponentRegionCount> regions = layer.getNumberOfRegions(numberOfBins);
Collections.sort(regions);
if (selectionMode == Mode.TARGET_NUMBER_OF_COMPONENTS) {
selectedComponents = regions.subList(0, selectionTargetNumberOfComponents);
} else if (selectionMode == Mode.THRESHOLD) {
// find the max value
double maxRegionFactor = -Double.MAX_VALUE;
for (int i = 0; i < regions.size(); i++) {
if (regions.get(i).getFactor(numberOfBins) > maxRegionFactor) {
maxRegionFactor = regions.get(i).getFactor(numberOfBins);
}
}
selectedComponents = new ArrayList<ComponentRegionCount>();
for (int i = 0; i < regions.size(); i++) {
ComponentRegionCount region = regions.get(i);
if (region.getFactor(numberOfBins) <= selectionThreshold * maxRegionFactor) {
selectedComponents.add(region);
}
}
} else {
throw new SOMToolboxException("Illegal selection method");
}
Point2D[][] selectedBinCentres = new Point2D[selectedComponents.size()][];
for (int i = 0; i < selectedComponents.size(); i++) {
int regionIndex = selectedComponents.get(i).getIndex().intValue();
selectedBinCentres[i] = binCentres[regionIndex];
}
if (this.binCentres.length != selectedBinCentres.length) {
metroMapControlPanel.initLegendTableAfterSelection(selectedComponents);
}
return selectedBinCentres;
}
/**
* Clustering of metro lines is done in here (i.e. aggregation step).
*
* @return new, aggregated bin centres
*/
private Point2D[][] doAggregation(ArrayList<ComponentLine2D> binCentresAsList, GrowingLayer layer)
throws SOMToolboxException {
Logger.getLogger("at.tuwien.ifs.somtoolbox").info(
"Starting component aggregation, mode " + aggregationMode.displayName);
ClusterElementFunctions<ComponentLine2D> dist = new ComponentLine2DDistance(lineDistanceFunction);
// check if we have the clustering in the cache
// System.out.println("Cluster cache contains: " + clusterCache.keySet());
WardClustering<ComponentLine2D> wardClustering = null;
if (clusterCache.containsKey(dist)) {
wardClustering = clusterCache.get(dist);
Logger.getLogger("at.tuwien.ifs.somtoolbox").info(
"Retrieving clustering for key '" + dist.getClass().getName() + "' from cache.");
} else { // make a new clustering
wardClustering = new WardClustering<ComponentLine2D>(dist);
// do multi-threading
int cpus = Runtime.getRuntime().availableProcessors();
if (cpus > 1) { // use 2 on dual core, 3 on quad core, and n-2 on higher
wardClustering.setNumberOfCPUs(cpus == 2 ? cpus : cpus == 4 ? 3 : cpus - 2);
}
Logger.getLogger("at.tuwien.ifs.somtoolbox").info(
"Starting clustering for key '" + dist.getClass().getName() + "'.");
wardClustering.doCluster(binCentresAsList);
clusterCache.put(dist, wardClustering);
Logger.getLogger("at.tuwien.ifs.somtoolbox").info(
"Adding clustering for key '" + dist.getClass().getName() + "' to cache.");
}
if (aggregationMode == Mode.TARGET_NUMBER_OF_COMPONENTS) {
clusters = wardClustering.getClustersAtLevel(aggregationTargetNumberOfComponents);
} else if (aggregationMode == Mode.THRESHOLD) {
// compute absolute threshold value
clusters = wardClustering.getClustersByRelativeThreshold(aggregationSimilarity);
} else {
throw new SOMToolboxException("Illegal aggregion method");
}
Point2D[][] aggregatedBinCentres = new Point2D[clusters.size()][numberOfBins];
for (int i = 0; i < aggregatedBinCentres.length; i++) {
Cluster<ComponentLine2D> cluster = clusters.get(i);
aggregatedBinCentres[i] = dist.meanObject(cluster).getPoints();
}
if (this.binCentres.length != aggregatedBinCentres.length || ArrayUtils.isEmpty(selectedComponentIndices)) {
metroMapControlPanel.initLegendTableAfterAggregation(clusters);
}
return aggregatedBinCentres;
}
/** Performs the snapping step, returns snapped lines */
private Point2D[][] doSnapping(Point2D[][] binCentres, GrowingLayer layer) throws SOMToolboxException {
Logger.getLogger("at.tuwien.ifs.somtoolbox").info("Starting snapping process");
Point2D[][] snappedBinCentres = new Point2D[binCentres.length][numberOfBins];
for (int i_components = 0; i_components < binCentres.length; i_components++) {
snappedBinCentres[i_components] = snap(binCentres[i_components], layer.getXSize(), layer.getYSize());
}
Point2D[][] snappedCentres = snappedBinCentres;
Hashtable<Point2D, int[]> outgoingLineTable = new Hashtable<Point2D, int[]>(); // find counts for parallel lines
for (Point2D[] snappedLine : snappedCentres) { // for each component
outgoingLineTable = this.countOutgoingLines(snappedLine, outgoingLineTable);
}
return snappedBinCentres;
}
/**
* Performs the computation of the new bin centres, therefore component aggregation, selection as well as snapping
* are handled
*
* @return new Point[][] of bin centres
*/
private Point2D[][] computeFinalComponentLines(GrowingLayer layer) throws SOMToolboxException {
// that's the original bin centres (no snapping, no selection, no aggregation)
Point2D[][] binCentres = layer.getBinCentres(numberOfBins);
ArrayList<ComponentLine2D> binCentresAsList = layer.getBinCentresAsList(numberOfBins);
// reset colour legend if there is no aggregation & selection, but we
// obviously had one before (and thus different array lengths)
if (aggregationMode == Mode.NONE && selectionMode == Mode.NONE && this.binCentres != null
&& binCentres.length != this.binCentres.length) {
metroMapControlPanel.initLegendTableNormal();
}
// do component selection
if (selectionMode != Mode.NONE) {
binCentres = doSelection(binCentres, layer);
} else {
selectedComponents = null;
}
// do component aggregation
if (aggregationMode != Mode.NONE) {
binCentres = doAggregation(binCentresAsList, layer);
} else {
clusters = null;
}
// do snapping
if (snapping) {
binCentres = doSnapping(binCentres, layer);
}
return binCentres;
}
/**
* Draws a line segment for the given component.
*
* @param begin begin coordinates
* @param end end coordinates
* @param component does it for a given component
* @param keepCurrentStroke whether or not to keep the current line stroke (and not modify it to use e.g. dashes,
* ...; used when for exporting small images)
*/
private void drawComponentLineSegment(Point2D begin, Point2D end, Graphics2D g, int component,
boolean keepCurrentStroke) {
Path2D path = new Path2D.Double();
g.setColor(colorMap.getColor(component));
Point2D p = begin;
int x = (int) (p.getX() * unitWidth + unitWidth / 2);
int y = (int) (p.getY() * unitHeight + unitHeight / 2);
Point2D lastP = end;
int xLast = (int) (lastP.getX() * unitWidth + unitWidth / 2);
int yLast = (int) (lastP.getY() * unitHeight + unitHeight / 2);
path.append(new Line2D.Double(new Point2D.Double(x, y), new Point2D.Double(xLast, yLast)), false);
// handle dashed lines
float lineThicknessFactor = 1;
if (component % colorMap.getColourCount() == 0) {
int j = component / colorMap.getColourCount();
if (j + 1 > dashPatterns.length) {
j = dashPatterns.length - 1;
lineThicknessFactor = lineThicknessFactors[j % lineThicknessFactors.length];
}
float[] ds = dashPatterns[j];
if (!keepCurrentStroke) {
g.setStroke(new BasicStroke(lineThickness * lineThicknessFactor, BasicStroke.CAP_ROUND,
BasicStroke.JOIN_ROUND, 1f, ds, ds[0] / 2));
}
}
g.drawLine(x, y, xLast, yLast);
if (!keepCurrentStroke) {
// set stroke back to normal (believe me it's necessary)
g.setStroke(new BasicStroke(lineThickness, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 1f,
new float[] { 5f }, 5f));
}
}
/**
* return the coordinates on the som grid for the next unit in the given direction
*
* @param currentPoint start point
* @param directions directions to next point
* @return next point as directed to in directions
*/
private Point2D getNextCoords(Point2D currentPoint, int[] directions) {
Point2D nextPoint = new Point2D.Double(currentPoint.getX(), currentPoint.getY());
if (directions[0] == 1) {
nextPoint.setLocation(currentPoint.getX() + 0, currentPoint.getY() - 1);
}
if (directions[1] == 1) {
nextPoint.setLocation(currentPoint.getX() + 1, currentPoint.getY() - 1);
}
if (directions[2] == 1) {
nextPoint.setLocation(currentPoint.getX() + 1, currentPoint.getY() + 0);
}
if (directions[3] == 1) {
nextPoint.setLocation(currentPoint.getX() + 1, currentPoint.getY() + 1);
}
if (directions[4] == 1) {
nextPoint.setLocation(currentPoint.getX() + 0, currentPoint.getY() + 1);
}
if (directions[5] == 1) {
nextPoint.setLocation(currentPoint.getX() - 1, currentPoint.getY() + 1);
}
if (directions[6] == 1) {
nextPoint.setLocation(currentPoint.getX() - 1, currentPoint.getY() + 0);
}
if (directions[7] == 1) {
nextPoint.setLocation(currentPoint.getX() - 1, currentPoint.getY() - 1);
}
return nextPoint;
}
/**
* returns the direction between two nodes based on the following scheme: 0 7 left up up right up 1 \ | / 6 left - *
* - right 6 / | \ 5 left down down right down 3 4
*
* @param current current node
* @param next next node to go to
* @return dir from current to next
*/
private int getDirection(Point2D current, Point2D next) {
// this handles the case when this annoying function is called with identical parameters
int dir = -1;
// up
if (current.getX() == next.getX() && current.getY() > next.getY()) {
dir = 0;
}
// right up
if (current.getX() < next.getX() && current.getY() > next.getY()) {
dir = 1;
}
// right
if (current.getX() < next.getX() && current.getY() == next.getY()) {
dir = 2;
}
// right down
if (current.getX() < next.getX() && current.getY() < next.getY()) {
dir = 3;
}
// down
if (current.getX() == next.getX() && current.getY() < next.getY()) {
dir = 4;
}
// left down
if (current.getX() > next.getX() && current.getY() < next.getY()) {
dir = 5;
}
// left
if (current.getX() > next.getX() && current.getY() == next.getY()) {
dir = 6;
}
// left up
if (current.getX() > next.getX() && current.getY() > next.getY()) {
dir = 7;
}
return dir;
}
/**
* get a direction array for the given points its indices are set to on for outgoing directions and it takes the
* form given in
*
* @see #getDirection(Point2D, Point2D)
* @param current current node
* @param next next node
* @return array for outgoing lines (boolean in this case)
*/
private int[] getDirectionArray(Point2D current, Point2D next) {
int[] directions = new int[] { 0, 0, 0, 0, 0, 0, 0, 0 };
// handle identical arguments
if (getDirection(current, next) == -1) {
return directions;
}
directions[getDirection(current, next)] = 1;
return directions;
}
/**
* update a given direction array for the given points its indices are set to on for outgoing directions and it
* takes the form given in
*
* @see #getDirection(Point2D, Point2D)
* @param current current node
* @param next next node
* @param directions -
* @return array for outgoing lines (counts for outgoing directions this time)
*/
private int[] getDirectionArray(Point2D current, Point2D next, int[] directions) {
int dir = this.getDirection(current, next);
// again, if this is called with identical arguments
if (dir == -1) {
return directions;
}
if (directions[dir] == 0) {
directions[dir] = 1;
} else {
directions[dir] = directions[dir]++;
}
return directions;
}
private Hashtable<Point2D, int[]> countOutgoingLines(Point2D[] line, Hashtable<Point2D, int[]> outgoingLineTable) {
int[] directions = { 0, 0, 0, 0, 0, 0, 0, 0 };
for (int i = 0; i < line.length - 1; i++) {
int[] storedDirections = outgoingLineTable.get(line[i]);
if (storedDirections != null) {
directions = storedDirections;
}
directions = getDirectionArray(line[i], line[i + 1], directions);
outgoingLineTable.put(line[i], directions);
}
// TODO loop line in opposite direction to cover all outgoing thingys -- check whether this is realised by
// checking all 8 directions now, I'd
// guess it it
return outgoingLineTable;
}
/**
* Compute matrix for {@link UMatrix} visualisation. // TODO adjust to new overlay visualisation function private
* DoubleMatrix2D computeUMatrix(GrowingSOM gsom) { uMatrixVisualizer = new UMatrix(); Palette riverPalette =
* Palettes.getPaletteByName("MetroMap"); uMatrixVisualizer.initCache(Palettes.getAvailablePalettes().length);
* uMatrixVisualizer.setPalette(Palettes.getPaletteIndex(riverPalette), riverPalette); DoubleMatrix2D umatrix =
* uMatrixVisualizer.createUMatrix(gsom); VectorTools.normalise(umatrix); return umatrix; }
*/
/*
* private void drawComponentLine(Point2D[] binCentres, Graphics2D g, int component) { Path2D path = new Path2D.Double(); // draw lines
* g.setColor(colorMap.getColor(component)); for (int i = 1; i < binCentres.length; i++) { Point2D p = binCentres[i]; int x = (int) (p.getX()
* unitWidth + unitWidth / 2); int y = (int) (p.getY() unitHeight + unitHeight / 2); Point2D lastP = binCentres[i - 1]; int xLast = (int)
* (lastP.getX() unitWidth + unitWidth / 2); int yLast = (int) (lastP.getY() unitHeight + unitHeight / 2); path.append(new Line2D.Double(new
* Point2D.Double(x, y), new Point2D.Double(xLast, yLast)), false); // g.drawLine(x, y, xLast, yLast); } g.draw(path); // draw stops (on top of
* line) for (int i = 0; i < binCentres.length; i++) { Point2D p = binCentres[i]; int x = (int) (p.getX() unitWidth + unitWidth / 2); int y =
* (int) (p.getY() unitHeight + unitHeight / 2); g.setColor(Color.BLACK); g.fillOval(x - radius, y - radius, radius 2, radius 2);
* g.setColor(colorMap.getColor(component)); g.fillOval(x - innerRadius, y - innerRadius, innerRadius 2, innerRadius 2); } }
*/
/**
* Returns a snapped line of the given line. Snapping the metro lines means to find a line as similar as possible to
* the given line, which has all bin centres in the unit centres, and line segments are connected in multiples of
* 45° degree angles to each other.<br>
* TODO: Consider disallowing 135° / 315° as too sharp turns.
*/
private Point2D[] snap(Point2D[] line, int xSize, int ySize) {
// Snapping process
// 1. For each bin centre, find the 4 neighbouring Unit locations, thus resulting in bins * 4 points
// 2. For each point: consider the point as fixed, and find a line that is correctly snapped (only 45°) angles
// and as near as possible to the
// original line
// 3. From the resulting bins * 4 lines, chose the one closest to the original line
ArrayList<Point2D[]> allSnappedLines = new ArrayList<Point2D[]>();
for (int i = 0; i < line.length; i++) {
Point2D[] neighbouringPoints = getNeighbouringUnits(line[i]);
for (Point2D neighbouringPoint : neighbouringPoints) {
// find the snapped points forward and backwards from the current bin point
// this means we will have lines e.g. as follows (for 6 bins, and i == 3)
// lineSegmentForward = (0/0 0/0 0/0 0/0 x5/y5 x6/y6)
// lineSegmentBackward = (x1/y1 x2/y2, x3/y3 0/0 0/0 0/0)
Point2D[] lineSegmentForward = snapPoint(neighbouringPoint, line, i + 1, +1, xSize, ySize, numberOfBins);
Point2D[] lineSegmentBackward = snapPoint(neighbouringPoint, line, i - 1, -1, xSize, ySize,
numberOfBins);
// then merge them to one line, and set the fixed point
Point2D[] mergedLine = new Point2D[lineSegmentForward.length];
for (int k = 0; k < lineSegmentBackward.length; k++) {
mergedLine[k] = new Point2D.Double(lineSegmentForward[k].getX() + lineSegmentBackward[k].getX(),
lineSegmentForward[k].getY() + lineSegmentBackward[k].getY());
}
mergedLine[i] = neighbouringPoint;
// now if that point is the same as the last one, don't add that one
// for some strange reason this is required additionally to the condition in the recursion
if (i > 1 && !(line[i].getX() == line[i - 1].getX() && line[i].getY() == line[i - 1].getY())) {
allSnappedLines.add(mergedLine);
}
}
}
// find the closest snapped line
double minDist = Double.MAX_VALUE;
Point2D[] minDistLine = null;
for (int i = 0; i < allSnappedLines.size(); i++) {
Point2D[] currentLine = allSnappedLines.get(i);
double dist = new ComponentLine2DDistance(lineDistanceFunction).distance(line, currentLine);
if (dist < minDist) {
minDist = dist;
minDistLine = currentLine;
}
}
return minDistLine;
}
/**
* Snaps the next point on the line.
*
* @param startPoint the point to start from
* @param line the line to snap
* @param currentPosition the current position on the line
* @param direction forward (1) or backwards (-1)
* @param xSize x-size of the map
* @param ySize y-size of the map
* @param bins number of bins
* @return a snapped line
*/
private Point2D[] snapPoint(Point2D startPoint, Point2D[] line, int currentPosition, int direction, int xSize,
int ySize, int bins) {
Point2D[] result = new Point2D[bins];
if (currentPosition == -1 && direction == -1 || currentPosition == bins && direction == 1) {
for (int i = 0; i < result.length; i++) {
result[i] = new Point2D.Double(0, 0);
}
return result;
}
int startPointCoordinatesSum = (int) (startPoint.getX() + startPoint.getY());
int startPointCoordinatesDifference = (int) (startPoint.getX() - startPoint.getY());
double minDistance = Double.MAX_VALUE;
Point2D closestPoint = null;
for (int i = 0; i < allSOMCoordinates.length; i++) {
// find units that are either in the same row (x equal), same column (y equal) or are in a diagonal (sum or
// diff values equal)
if (allSOMCoordinates[i].getX() == startPoint.getX() || allSOMCoordinates[i].getY() == startPoint.getY()
|| allSOMCoordinatesSumValues[i] == startPointCoordinatesSum
|| allSOMCoordinatesDiffValues[i] == startPointCoordinatesDifference) {
double currentDistance = allSOMCoordinates[i].distance(line[currentPosition]);
if (currentDistance < minDistance) {
closestPoint = allSOMCoordinates[i];
minDistance = currentDistance;
}
}
}
// compare this startpoint to the last one and check for idendity and don't consider the closest but the point
// itself for further processing
// if so
if (currentPosition > 1 && line[currentPosition].getX() == line[currentPosition - direction].getX()
&& line[currentPosition].getY() == line[currentPosition - direction].getY()) {
result = snapPoint(startPoint, line, currentPosition + direction, direction, xSize, ySize, bins);
result[currentPosition] = startPoint;
} else {
result = snapPoint(closestPoint, line, currentPosition + direction, direction, xSize, ySize, bins);
result[currentPosition] = closestPoint;
}
return result;
}
/** ***************** VISUALISATION IMPROVEMENTS *********************** */
/*
* //TODO hm? private Point2D[][] pullOverlapping(Point2D[][] binCentres) { for (int i = 0; i < binCentres.length; i++) { for (int j = 0; j <
* binCentres[i].length; j++) { Point2D point2D = binCentres[i][j]; for (int k = i; k < binCentres.length; k++) { for (int l = j; l <
* binCentres[k].length; l++) { Point2D point2D2 = binCentres[k][l]; if (point2D.equals(point2D2) && point2D != point2D2) {
* System.out.println("found overlapping points: " + point2D + ", " + point2D2); Point2D other2; if (l == 0) { other2 = binCentres[k][l + 1]; }
* else { other2 = binCentres[k][l - 1]; } double changeX = other2.getX() - point2D2.getX(); double changeY = other2.getY() - point2D2.getY();
* System.out.println("changes: " + changeX + ", " + changeY); double d = (lineThickness 1.5 / 130d); if (changeX > changeY) { // we are moving
* vertical --> dislocate point rightwards point2D2.setLocation(point2D2.getX() + d, point2D2.getY()); } else { // otherwise downwards
* point2D2.setLocation(point2D2.getX(), point2D2.getY() + d); } System.out.println("moved: " + point2D2); } } } } } return binCentres; }
*/
private String getComponentName(int i) {
if (inputObjects != null && inputObjects.getTemplateVector() != null) {
return inputObjects.getTemplateVector().getLabel(i);
} else {
return COMP_PREFIX + (i + 1);
}
}
private String[] getComponentNames() {
String[] items = new String[dim];
for (int i = 0; i < dim; i++) {
items[i] = getComponentName(i);
}
return items;
}
/*
* private int getComponentIndex(String label) { if (inputObjects != null && inputObjects.getTemplateVector() != null) { return
* inputObjects.getTemplateVector().getIndex(label); } else { // assume generated component name String s =
* label.substring(label.indexOf(COMP_PREFIX) + COMP_PREFIX.length()).trim(); return Integer.parseInt(s); } }
*/
/**
* A control panel extending the generic {@link AbstractBackgroundImageVisualizer.VisualizationControlPanel}, adding
* additionally a {@link JList} and a {@link JTextField} for selecting a component from the {@link TemplateVector}.
*
* @author Rudolf Mayer
*/
private class MetroMapControlPanel extends VisualizationControlPanel implements ActionListener, ChangeListener,
TableModelListener {// ,
// ListSelectionListener {
private static final long serialVersionUID = 1L;
private JSpinner binSpinner;
private JSpinner thickNessSpinner;
private JCheckBox boxSnapping;
private JRadioButton buttonAggregationSimilarity;
private JRadioButton buttonAggregationTargetNumberComponents;
private JSpinner spinnerAggregationTargetNumberComponents;
private JSpinner spinnerAggregationSimilarity;
private JRadioButton buttonSelectionTargetNumberComponents;
private JSpinner spinnerSelectionTargetNumberComponents;
private JRadioButton buttonSelectionSimilarity;
private JSpinner spinnerSelectionThreshold;
private JRadioButton buttonAggregationNone;
private JRadioButton buttonSelectionNone;
private JScrollPane colourLegendScrollPane;
JComboBox distanceFunctionComboBox;
/**
* Constructs a new metro-map control panel
*
* @param vis The MetroMapVisualizer listening to updates from the list box.
*/
private MetroMapControlPanel(MetroMapVisualizer vis) {
super("Metro Map Control");
distanceFunctionComboBox = new JComboBox(DistanceFunctionType.values());
distanceFunctionComboBox.setToolTipText("Function used for line distance computation, for snapping and component line aggregation");
distanceFunctionComboBox.setFont(smallerFont);
distanceFunctionComboBox.setSelectedItem(lineDistanceFunction);
distanceFunctionComboBox.addActionListener(this);
binSpinner = new JSpinner(new SpinnerNumberModel(numberOfBins, MIN_BINS, MAX_BINS, 1));
binSpinner.setFont(smallerFont);
binSpinner.addChangeListener(this);
JPanel distanceAndBinsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
distanceAndBinsPanel.add(new JLabel("Distance"));
distanceAndBinsPanel.add(distanceFunctionComboBox);
distanceAndBinsPanel.add(new JLabel("Bins"));
distanceAndBinsPanel.add(binSpinner);
// snapping checkbox
boxSnapping = new JCheckBox("Snapping");
boxSnapping.setSelected(snapping);
boxSnapping.addActionListener(this);
// overlay vis combobox
ArrayList<String> overlayVisShortnames = new ArrayList<String>();
overlayVisShortnames.add("Empty");
overlayVisShortnames.addAll(Arrays.asList(UMatrix.UMATRIX_SHORT_NAMES));
overlayVisShortnames.addAll(Arrays.asList(FlowBorderlineVisualizer.FLOWBORDER_SHORT_NAMES));
overlayVisShortnames.add(ThematicClassMapVisualizer.CLASSMAP_SHORT_NAME);
overlayVisShortnames.add(ThematicClassMapVisualizer.CLASSMAP_SHORT_NAME + "/Chess");
overlayVisualisationComboBox.setModel(new DefaultComboBoxModel(overlayVisShortnames.toArray()));
overlayVisualisationComboBox.addActionListener(this);
overlayVisualisationComboBox.setFont(smallerFont);
overlayVisualisationComboBox.setToolTipText("Overlay visualisation");
JLabel overlayLabel = UiUtils.makeLabelWithTooltip("Overlay", overlayVisualisationComboBox.getToolTipText());
thickNessSpinner = new JSpinner(new SpinnerNumberModel(lineThickness, 1, (int) (radius * 1.5), 1));
thickNessSpinner.addChangeListener(this);
thickNessSpinner.setFont(smallerFont);
thickNessSpinner.setToolTipText("Line thickness");
JLabel thicknessLabel = UiUtils.makeLabelWithTooltip("Lines", thickNessSpinner.getToolTipText());
TitledCollapsiblePanel displayPanel = new TitledCollapsiblePanel("Display options", new GridBagLayout());
GridBagConstraintsIFS gbc = new GridBagConstraintsIFS();
displayPanel.add(UiUtils.makeAndFillPanel(boxSnapping, overlayLabel, overlayVisualisationComboBox), gbc);
displayPanel.add(UiUtils.makeAndFillPanel(thicknessLabel, thickNessSpinner), gbc.nextRow());
/* component aggregation panel */
// no aggregation
buttonAggregationNone = new JRadioButton(Mode.NONE.displayName);
buttonAggregationNone.setFont(smallerFont);
buttonAggregationNone.addActionListener(this);
buttonAggregationNone.setSelected(true);
// target number
buttonAggregationTargetNumberComponents = new JRadioButton(Mode.TARGET_NUMBER_OF_COMPONENTS.displayName);
buttonAggregationTargetNumberComponents.setFont(smallerFont);
buttonAggregationTargetNumberComponents.addActionListener(this);
spinnerAggregationTargetNumberComponents = new JSpinner();
spinnerAggregationTargetNumberComponents.setFont(smallerFont);
spinnerAggregationTargetNumberComponents.setEnabled(false);
spinnerAggregationTargetNumberComponents.addChangeListener(this);
spinnerAggregationTargetNumberComponents.setToolTipText("Absolute number of aggregrated lines to obtain");
// similarity threshold
buttonAggregationSimilarity = new JRadioButton(Mode.THRESHOLD.displayName);
buttonAggregationSimilarity.setFont(smallerFont);
buttonAggregationSimilarity.addActionListener(this);
spinnerAggregationSimilarity = new JSpinner(new SpinnerNumberModel(0.5, 0, 1, 0.01));
spinnerAggregationSimilarity.setFont(smallerFont);
spinnerAggregationSimilarity.setEnabled(false);
spinnerAggregationSimilarity.addChangeListener(this);
spinnerAggregationSimilarity.setToolTipText("Aggregate lines with a similarity relative to most distant lines");
ButtonGroup aggregationMethod = new ButtonGroup();
aggregationMethod.add(buttonAggregationNone);
aggregationMethod.add(buttonAggregationTargetNumberComponents);
aggregationMethod.add(buttonAggregationSimilarity);
final TitledCollapsiblePanel aggregationPanel = new TitledCollapsiblePanel("Component Aggregation",
new GridBagLayout());
GridBagConstraintsIFS c2 = new GridBagConstraintsIFS(GridBagConstraints.NORTHWEST,
GridBagConstraints.HORIZONTAL);
c2.ipadx = 2;
aggregationPanel.add(buttonAggregationNone, c2);
aggregationPanel.add(buttonAggregationTargetNumberComponents, c2.nextCol());
aggregationPanel.add(buttonAggregationSimilarity, c2.nextCol());
aggregationPanel.add(new JPanel(), c2.nextRow());
aggregationPanel.add(spinnerAggregationTargetNumberComponents, c2.nextCol());
aggregationPanel.add(spinnerAggregationSimilarity, c2.nextCol());
aggregationPanel.addPropertyChangeListener("enabled", new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
boolean v = evt.getNewValue().equals(Boolean.TRUE);
buttonAggregationNone.setEnabled(v);
buttonAggregationTargetNumberComponents.setEnabled(v);
buttonAggregationSimilarity.setEnabled(v);
}
});
/* component selection panel */
// no selection
buttonSelectionNone = new JRadioButton(Mode.NONE.displayName);
buttonSelectionNone.setFont(smallerFont);
buttonSelectionNone.addActionListener(this);
buttonSelectionNone.setSelected(true);
buttonSelectionNone.addChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
aggregationPanel.setEnabled(buttonSelectionNone.isSelected());
}
});
// target number
buttonSelectionTargetNumberComponents = new JRadioButton(Mode.TARGET_NUMBER_OF_COMPONENTS.displayName);
buttonSelectionTargetNumberComponents.setFont(smallerFont);
buttonSelectionTargetNumberComponents.addActionListener(this);
spinnerSelectionTargetNumberComponents = new JSpinner();
spinnerSelectionTargetNumberComponents.setFont(smallerFont);
spinnerSelectionTargetNumberComponents.setEnabled(false);
spinnerSelectionTargetNumberComponents.addChangeListener(this);
spinnerSelectionTargetNumberComponents.setToolTipText("Absolute number of lines to selected");
// similarity threshold
buttonSelectionSimilarity = new JRadioButton(Mode.THRESHOLD.displayName);
buttonSelectionSimilarity.setFont(smallerFont);
buttonSelectionSimilarity.addActionListener(this);
spinnerSelectionThreshold = new JSpinner(new SpinnerNumberModel(0.5, 0, 1, 0.01));
spinnerSelectionThreshold.setFont(smallerFont);
spinnerSelectionThreshold.setEnabled(false);
spinnerSelectionThreshold.addChangeListener(this);
spinnerSelectionThreshold.setToolTipText("Select lines with a goodness relative to the worst region");
ButtonGroup selectionMethod = new ButtonGroup();
selectionMethod.add(buttonSelectionNone);
selectionMethod.add(buttonSelectionTargetNumberComponents);
selectionMethod.add(buttonSelectionSimilarity);
final TitledCollapsiblePanel selectionPanel = new TitledCollapsiblePanel("Component Selection",
new GridBagLayout());
c2.reset();
selectionPanel.add(buttonSelectionNone, c2);
selectionPanel.add(buttonSelectionTargetNumberComponents, c2.nextCol());
selectionPanel.add(buttonSelectionSimilarity, c2.nextCol());
selectionPanel.add(new JPanel(), c2.nextRow());
selectionPanel.add(spinnerSelectionTargetNumberComponents, c2.nextCol());
selectionPanel.add(spinnerSelectionThreshold, c2.nextCol());
selectionPanel.addPropertyChangeListener("enabled", new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
boolean v = evt.getNewValue().equals(Boolean.TRUE);
buttonSelectionNone.setEnabled(v);
buttonSelectionTargetNumberComponents.setEnabled(v);
buttonSelectionSimilarity.setEnabled(v);
}
});
buttonAggregationNone.addChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
selectionPanel.setEnabled(buttonAggregationNone.isSelected());
}
});
// colourLegendTable1 = new ColourLegendTable(legendColumnNames, this);
// colourLegendTable2 = new ColourLegendTable(legendColumnNames, this);
final ClassColorTableModel theModel = new ClassColorTableModel();
theModel.setColumnName(ClassColorTableModel.NAME_COLUMN_INDEX,
legendColumnNames[ClassColorTableModel.NAME_COLUMN_INDEX]);
theModel.setColumnName(ClassColorTableModel.COLOR_COLUMN_INDEX,
legendColumnNames[ClassColorTableModel.COLOR_COLUMN_INDEX]);
colourLegendTable1 = ClassColorTableModel.createColorLegendTable(theModel);
theModel.addTableModelListener(this);
// colourLegendTable1.getSelectionModel().addListSelectionListener(this);
JButton buttonExport = new JButton("Export Legend");
buttonExport.setMargin(new Insets(2, 2, 1, 2));
buttonExport.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
File file = ExportUtils.getFilePath(metroMapControlPanel,
CommonSOMViewerStateData.getInstance().getFileChooser(), "Export class legend to");
if (file != null) {
BufferedImage image = new BufferedImage(colourLegendTable1.getWidth()
// + colourLegendTable2.getWidth()
, colourLegendTable1.getHeight(), BufferedImage.TYPE_INT_ARGB);
Graphics2D g = image.createGraphics();
// FIXME: Image export broken!
// g.drawImage(colourLegendTable1.asBufferedImage(), 0, 0, null);
// g.drawImage(colourLegendTable2.asBufferedImage(), colourLegendTable1.getWidth(), 0, null);
try {
FileUtils.saveImageToFile(file.getAbsolutePath(), image);
} catch (SOMToolboxException e1) {
Logger.getLogger("at.tuwien.ifs.somtoolbox").severe(
"Could not write class legend to file '" + file.getAbsolutePath() + "': "
+ e1.getMessage());
}
}
}
});
JPanel colourLegendPanel = new JPanel(new GridBagLayout());
c2.reset();
colourLegendPanel.add(colourLegendTable1, c2);
// colourLegendPanel.add(colourLegendTable2, c2.nextCol());
colourLegendScrollPane = new JScrollPane(colourLegendTable1,
ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED,
ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED);
JPanel metroPanel = new JPanel(new GridBagLayout());
GridBagConstraintsIFS constr = new GridBagConstraintsIFS(GridBagConstraints.NORTHWEST,
GridBagConstraints.HORIZONTAL);
constr.insets = new Insets(2, 2, 2, 2);
constr.weightx = 1.0;
metroPanel.add(distanceAndBinsPanel, constr);
metroPanel.add(displayPanel, constr.nextRow());
metroPanel.add(aggregationPanel, constr.nextRow());
metroPanel.add(selectionPanel, constr.nextRow());
metroPanel.add(colourLegendScrollPane, constr.nextRow());
metroPanel.add(buttonExport,
constr.setFill(GridBagConstraints.NONE).setAnchor(GridBagConstraints.CENTER).nextRow());
add(metroPanel, c);
}
private void setLegendTableData(final String[] names) {
endIndexTable1 = (int) Math.round(names.length / legendColumns);
if (endIndexTable1 > MAX_NUMBER_OF_LEGEND_ENTRIES) {
return;
}
// colourLegendTable1.setData(names, colorMap.getColors(names.length), 0, endIndexTable1);
final ClassColorTableModel theModel = new ClassColorTableModel(names, colorMap.getColors(names.length));
theModel.setColumnName(ClassColorTableModel.NAME_COLUMN_INDEX,
legendColumnNames[ClassColorTableModel.NAME_COLUMN_INDEX]);
theModel.setColumnName(ClassColorTableModel.COLOR_COLUMN_INDEX,
legendColumnNames[ClassColorTableModel.COLOR_COLUMN_INDEX]);
colourLegendTable1.setModel(theModel);
theModel.addTableModelListener(this);
if (names.length > 1) { // only use second legend if there is more than one element
// colourLegendTable2.setData(names, colorMap.getColors(names.length), endIndexTable1,
// (int) (names.length / legendColumns));
}
// colourLegendTable2.setVisible(names.length > 1);
}
private void initLegendTableNormal() {
setLegendTableData(getComponentNames());
}
private void initLegendTableAfterAggregation(List<? extends Cluster<ComponentLine2D>> clusters) {
String[] items = new String[clusters.size()];
for (int i = 0; i < clusters.size(); i++) {
Cluster<ComponentLine2D> cluster = clusters.get(i);
String mergedName = "";
for (int j = 0; j < cluster.size(); j++) {
ElementWithIndex line = cluster.get(j);
mergedName += getComponentName(line.getIndex());
if (j + 1 < cluster.size()) {
mergedName += " / ";
}
}
items[i] = mergedName;
}
setLegendTableData(items);
}
private void initLegendTableAfterSelection(List<ComponentRegionCount> selectedComponents) {
String[] items = new String[selectedComponents.size()];
for (int i = 0; i < selectedComponents.size(); i++) {
ComponentRegionCount region = selectedComponents.get(i);
String componentName = getComponentName(region.getIndex().intValue());
items[i] = componentName;
}
setLegendTableData(items);
}
@Override
public void actionPerformed(ActionEvent e) {
Object source = e.getSource();
try {
// aggregation radio buttons
if (source == buttonAggregationNone && aggregationMode != Mode.NONE) {
spinnerAggregationSimilarity.setEnabled(false);
spinnerAggregationTargetNumberComponents.setEnabled(false);
aggregationMode = Mode.NONE;
updateVis();
} else if (source == buttonAggregationTargetNumberComponents
&& aggregationMode != Mode.TARGET_NUMBER_OF_COMPONENTS) {
spinnerAggregationSimilarity.setEnabled(false);
spinnerAggregationTargetNumberComponents.setEnabled(true);
aggregationMode = Mode.TARGET_NUMBER_OF_COMPONENTS;
updateVis();
} else if (source == buttonAggregationSimilarity && aggregationMode != Mode.THRESHOLD) {
spinnerAggregationSimilarity.setEnabled(true);
spinnerAggregationTargetNumberComponents.setEnabled(false);
aggregationMode = Mode.THRESHOLD;
updateVis();
// selection radio buttons
} else if (source == buttonSelectionNone && selectionMode != Mode.NONE) {
spinnerSelectionThreshold.setEnabled(false);
spinnerSelectionTargetNumberComponents.setEnabled(false);
selectionMode = Mode.NONE;
updateVis();
} else if (source == buttonSelectionTargetNumberComponents
&& selectionMode != Mode.TARGET_NUMBER_OF_COMPONENTS) {
spinnerSelectionThreshold.setEnabled(false);
spinnerSelectionTargetNumberComponents.setEnabled(true);
selectionMode = Mode.TARGET_NUMBER_OF_COMPONENTS;
updateVis();
} else if (source == buttonSelectionSimilarity && selectionMode != Mode.THRESHOLD) {
spinnerSelectionThreshold.setEnabled(true);
spinnerSelectionTargetNumberComponents.setEnabled(false);
selectionMode = Mode.THRESHOLD;
updateVis();
} else if (source == boxSnapping && snapping != boxSnapping.isSelected()) {
snapping = boxSnapping.isSelected();
updateVis();
} /*
* else if (source == boxUMatrix && uMatrix != boxUMatrix.isSelected()) { uMatrix = boxUMatrix.isSelected(); updateVis(); }
*/
// distance function box
else if (source == distanceFunctionComboBox) {
lineDistanceFunction = (DistanceFunctionType) distanceFunctionComboBox.getSelectedItem();
if (snapping || selectionMode != Mode.NONE || aggregationMode != Mode.NONE) {
updateVis();
}
} else if (source == overlayVisualisationComboBox) {
updateVis();
}
} catch (SOMToolboxException ex) {
Logger.getLogger("at.tuwien.ifs.somtoolbox").severe(
"Error creating metro map visualisation: " + ex.getMessage());
}
}
private void updateVis() throws SOMToolboxException {
binCentres = computeFinalComponentLines(gsom.getLayer());
// TODO put rest in here
reInitVis();
}
@Override
public void stateChanged(ChangeEvent e) {
JSpinner spinner = (JSpinner) e.getSource();
SpinnerNumberModel model = (SpinnerNumberModel) spinner.getModel();
Number number = model.getNumber();
try {
if (spinner == binSpinner) {
if (number.intValue() != numberOfBins) {
numberOfBins = number.intValue();
updateVis();
}
} else if (spinner == thickNessSpinner) {
if (number.intValue() != lineThickness) {
lineThickness = number.intValue();
dashPatterns = initDashPatterns();
updateVis();
}
} else if (spinner == spinnerAggregationTargetNumberComponents) {
if (number.intValue() != aggregationTargetNumberOfComponents) {
aggregationTargetNumberOfComponents = number.intValue();
updateVis();
}
} else if (spinner == spinnerSelectionTargetNumberComponents) {
if (number.intValue() != selectionTargetNumberOfComponents) {
selectionTargetNumberOfComponents = number.intValue();
updateVis();
}
} else if (spinner == spinnerAggregationSimilarity) {
if (number.doubleValue() != aggregationSimilarity) {
aggregationSimilarity = number.doubleValue();
updateVis();
}
} else if (spinner == spinnerSelectionThreshold) {
if (number.doubleValue() != selectionThreshold) {
selectionThreshold = number.doubleValue();
updateVis();
}
}
} catch (SOMToolboxException ex) {
Logger.getLogger("at.tuwien.ifs.somtoolbox").severe(
"Error creating metro map visualisation: " + ex.getMessage());
}
}
/* (non-Javadoc)
* @see javax.swing.event.TableModelListener#tableChanged(javax.swing.event.TableModelEvent)
*/
@Override
public void tableChanged(TableModelEvent e) {
int col = e.getColumn();
if (col == ClassColorTableModel.SELECT_COLUMN_INDEX || col == TableModelEvent.ALL_COLUMNS) {
MetroMapVisualizer.this.selectedComponentIndices = ((ClassColorTableModel) colourLegendTable1.getModel()).getSelectedClassIndices();
reInitVis();
}
}
// @Override
public void valueChanged(ListSelectionEvent e) {
final int[] selectedRows = colourLegendTable1.getSelectedRows();
final int[] selectedRows2 = null; // colourLegendTable2.getSelectedRows();
// for (int i = 0; i < selectedRows2.length; i++) {
// selectedRows2[i] = selectedRows2[i] + endIndexTable1;
// }
MetroMapVisualizer.this.selectedComponentIndices = ArrayUtils.addAll(selectedRows, selectedRows2);
reInitVis();
}
}
private void reInitVis() {
if (visualizationUpdateListener != null) {
visualizationUpdateListener.updateVisualization();
}
}
private float[][] initDashPatterns() {
return new float[][] { { 1 }, { 25, lineThickness + 2 }, { 21, lineThickness + 2, 3, lineThickness + 2 },
{ 50, (lineThickness + 2) * 2 }, { 42, lineThickness + 2, 6, lineThickness + 2 } };
}
public Color[] getColours(int components) {
Color[] colors = new Color[components];
System.arraycopy(COLORS, 0, colors, 0, components);
return colors;
}
public Color getColour(int component) {
return COLORS[component % COLORS.length];
}
/** Finds the four units around the given point. */
public Point2D[] getNeighbouringUnits(Point2D p) {
// FIXME what about setting all x values to x if x % 1 == 0?
Point2D leftUpper = new Point2D.Double((int) p.getX(), (int) p.getY());
Point2D rightUpper = new Point2D.Double(leftUpper.getX() + 1, leftUpper.getY());
Point2D leftLower = new Point2D.Double(leftUpper.getX(), leftUpper.getY() + 1);
Point2D rightLower = new Point2D.Double(leftUpper.getX() + 1, leftUpper.getY() + 1);
if (p.getX() % 1 == 0) {
rightUpper.setLocation(leftUpper.getX(), leftUpper.getY());
rightLower.setLocation(leftUpper.getX(), leftUpper.getY() + 1);
}
if (p.getY() % 1 == 0) {
leftLower.setLocation(leftUpper.getX(), leftUpper.getY());
rightLower.setLocation(leftUpper.getX() + 1, leftUpper.getY());
}
if (p.getX() % 1 == 0 && p.getY() % 1 == 0) {
rightLower.setLocation(leftUpper.getX(), leftUpper.getY());
}
// the order of the points returned here is the same as in the matlab version
return new Point2D[] { leftUpper, leftLower, rightUpper, rightLower };
}
@Override
public void setInputObjects(SharedSOMVisualisationData inputObjects) {
super.setInputObjects(inputObjects);
metroMapControlPanel.initLegendTableNormal();
}
@Override
public void setSOMData(SOMInputReader reader) {
super.setSOMData(reader);
dim = reader.getDim();
int initialValueSpinner = (int) Math.min(10, Math.round(dim / 2d));
selectionTargetNumberOfComponents = initialValueSpinner;
aggregationTargetNumberOfComponents = initialValueSpinner;
int minValue = Math.min(1, dim);
if (dim > 1) {
metroMapControlPanel.spinnerAggregationTargetNumberComponents.setModel(new SpinnerNumberModel(
aggregationTargetNumberOfComponents, minValue, dim, 1));
metroMapControlPanel.spinnerSelectionTargetNumberComponents.setModel(new SpinnerNumberModel(
selectionTargetNumberOfComponents, minValue, dim, 1));
}
metroMapControlPanel.buttonAggregationSimilarity.setEnabled(dim > 1);
metroMapControlPanel.buttonAggregationTargetNumberComponents.setEnabled(dim > 1);
metroMapControlPanel.buttonSelectionSimilarity.setEnabled(dim > 1);
metroMapControlPanel.buttonSelectionSimilarity.setEnabled(dim > 1);
metroMapControlPanel.initLegendTableNormal();
}
@Override
public HashMap<String, BufferedImage> getVisualizationFlavours(int index, GrowingSOM gsom, int width, int height)
throws SOMToolboxException {
// FIXME: implement this
return getVisualizationFlavours(index, gsom, width, height, -1);
}
@Override
public HashMap<String, BufferedImage> getVisualizationFlavours(int index, GrowingSOM gsom, int width, int height,
int maxFlavours) throws SOMToolboxException {
HashMap<String, BufferedImage> res = new HashMap<String, BufferedImage>();
// iterate over bins
int currentNumberOfBins = numberOfBins;
boolean currentSnapping = snapping;
for (numberOfBins = 3; numberOfBins < 10; numberOfBins++) {
String key = "_bins" + numberOfBins;
snapping = false;
res.put(key, getVisualization(index, gsom, width, height));
snapping = true;
res.put(key + "_snapped", getVisualization(index, gsom, width, height));
}
numberOfBins = currentNumberOfBins;
snapping = currentSnapping;
return res;
}
@Override
public HashMap<String, BufferedImage> getVisualizationFlavours(int index, GrowingSOM gsom, int width, int height,
Map<String, String> flavourParameters) throws SOMToolboxException {
// FIXME: implement this
return getVisualizationFlavours(index, gsom, width, height, -1);
}
}