/* * 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.apps.viewer.controls; import java.awt.BasicStroke; import java.awt.Color; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Random; import java.util.Vector; import java.util.logging.Logger; import javax.swing.ButtonGroup; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JRadioButton; import javax.swing.JSpinner; import javax.swing.SpinnerNumberModel; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import cern.colt.matrix.impl.DenseDoubleMatrix1D; import edu.umd.cs.piccolo.PNode; import edu.umd.cs.piccolo.nodes.PPath; import edu.umd.cs.piccolo.nodes.PText; import edu.umd.cs.piccolox.nodes.PLine; import at.tuwien.ifs.somtoolbox.apps.viewer.CommonSOMViewerStateData; import at.tuwien.ifs.somtoolbox.apps.viewer.GeneralUnitPNode; import at.tuwien.ifs.somtoolbox.data.InputDatum; import at.tuwien.ifs.somtoolbox.layers.Unit; /** * @author Jakob Frank * @version $Id: AutoRoutePanel.java 3589 2010-05-21 10:42:01Z mayer $ */ public class AutoRoutePanel extends AbstractViewerControl { private static final long serialVersionUID = 1L; private float bigD; private float smallD; private PNode route; private PNode dots; private PNode lines; private SpinnerNumberModel spinnerModel; private JRadioButton rdoFlat; private JRadioButton rdoSnap; private JRadioButton rdoHigh; private JPanel mainP = null; private JCheckBox chkTSP; private JCheckBox chkDebug; private boolean debug = false; private boolean applyTSP = false; private Random rand; private LinkedList<Unit> touchedUnits; private static Logger log = Logger.getLogger("at.tuwien.ifs.somtoolbox.AutoRoute"); private SpinnerNumberModel lineModel; public AutoRoutePanel(String title, CommonSOMViewerStateData state) { super(title, state); bigD = state.mapPNode.getUnitWidth() / 3; smallD = state.mapPNode.getUnitHeight() / 6; setContentPane(getAutoRoutePanel()); } public JPanel getAutoRoutePanel() { if (mainP != null) { return mainP; } mainP = new JPanel(); mainP.setLayout(new GridBagLayout()); setContentPane(mainP); route = new PNode(); dots = new PNode(); lines = new PNode(); route.addChild(lines); route.addChild(dots); state.mapPNode.addChild(route); GridBagConstraints gbc = new GridBagConstraints(); gbc.gridy = 0; JLabel lblSteps = new JLabel("Steps: "); spinnerModel = new SpinnerNumberModel(0, 0, 100, 1); spinnerModel.addChangeListener(new ChangeListener() { @Override public void stateChanged(ChangeEvent e) { doAutoRouting(); } }); JSpinner spnSteps = new JSpinner(spinnerModel); spnSteps.setEditor(new JSpinner.NumberEditor(spnSteps, "#")); lblSteps.setLabelFor(spnSteps); mainP.add(lblSteps, gbc); mainP.add(spnSteps, gbc); gbc.gridy++; gbc.gridwidth = 2; ButtonGroup calc = new ButtonGroup(); rdoFlat = new JRadioButton("flat"); rdoFlat.setSelected(true); rdoFlat.addItemListener(new ItemListener() { @Override public void itemStateChanged(ItemEvent e) { doAutoRouting(); } }); calc.add(rdoFlat); mainP.add(rdoFlat, gbc); gbc.gridy++; rdoSnap = new JRadioButton("snapped"); rdoSnap.addItemListener(new ItemListener() { @Override public void itemStateChanged(ItemEvent e) { doAutoRouting(); } }); calc.add(rdoSnap); mainP.add(rdoSnap, gbc); gbc.gridy++; rdoHigh = new JRadioButton("highdimensional"); rdoHigh.addItemListener(new ItemListener() { @Override public void itemStateChanged(ItemEvent e) { doAutoRouting(); } }); calc.add(rdoHigh); mainP.add(rdoHigh, gbc); gbc.gridy++; gbc.gridwidth = 1; chkTSP = new JCheckBox("TSP"); chkTSP.setSelected(false); chkTSP.addItemListener(new ItemListener() { @Override public void itemStateChanged(ItemEvent e) { applyTSP = chkTSP.isSelected(); // FIXME: if it works, do autorouting here! // doAutoRouting(); } }); mainP.add(chkTSP, gbc); chkDebug = new JCheckBox("debug"); chkDebug.setSelected(debug); chkDebug.addItemListener(new ItemListener() { @Override public void itemStateChanged(ItemEvent e) { debug = chkDebug.isSelected(); doAutoRouting(); } }); mainP.add(chkDebug, gbc); gbc.gridy++; JButton btnRoute = new JButton("Route"); btnRoute.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { rand = new Random(); doAutoRouting(); } }); mainP.add(btnRoute, gbc); JButton btnPlay = new JButton("Play"); btnPlay.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { createAndPlay(); } }); mainP.add(btnPlay, gbc); JButton btnClear = new JButton("Clear"); btnClear.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { dots.removeAllChildren(); lines.removeAllChildren(); } }); mainP.add(btnClear, gbc); gbc.gridy++; gbc.gridwidth = 3; lineModel = new SpinnerNumberModel(15, 1, 100, 1); lineModel.addChangeListener(new ChangeListener() { @Override public void stateChanged(ChangeEvent e) { doAutoRouting(); } }); JSpinner spnLine = new JSpinner(lineModel); mainP.add(spnLine, gbc); return mainP; } private void createAndPlay() { state.selectionPanel.clearList(); // Create the Playlist for (Unit u : touchedUnits) { System.out.printf("%s%n", u.toString()); if (u.getNumberOfMappedInputs() > 0) { state.selectionPanel.addToList(u.getMappedInputName(0), u); } } } /** * */ private void doAutoRouting() { dots.removeAllChildren(); lines.removeAllChildren(); touchedUnits = new LinkedList<Unit>(); // Collect units to route List<GeneralUnitPNode> units = new ArrayList<GeneralUnitPNode>(); // for (int i = 0; i < state.growingLayer.getXSize(); i++) { // for (int j = 0; j < state.growingLayer.getYSize(); j++) { // GeneralUnitPNode u = state.mapPNode.getUnit(i, j); // if (u.isSelected()) { // units.add(u); // } // } // } for (Unit node : state.selectionPanel.unitsInPlaylist) { units.add(state.mapPNode.getUnit(node)); } if (applyTSP) { log.info("Solving TSP"); List<GeneralUnitPNode> tspList = new Vector<GeneralUnitPNode>(); SimpleTSPSolver s = new SimpleTSPSolver(units, rdoHigh.isSelected(), rand); s.solve(); int[] t = s.getTour(); int n = s.start; for (@SuppressWarnings("unused") int element : t) { tspList.add(units.get(n)); n = t[n]; } units = tspList; log.info(String.format("TSP solved, total length is %f%n", s.tour.length())); } if (rdoFlat.isSelected()) { doFlatRouting(units); } else if (rdoSnap.isSelected()) { doSnappedRouting(units); } else if (rdoHigh.isSelected()) { doHighDimRouting(units); } } private void doSnappedRouting(List<GeneralUnitPNode> units) { GeneralUnitPNode last = null, current = null; float lastX, lastY, currentX = 0, currentY = 0; HashMap<GeneralUnitPNode, PPath> dotCache = new HashMap<GeneralUnitPNode, PPath>(); PLine line = new PLine(); BasicStroke bs = new BasicStroke(lineModel.getNumber().floatValue(), BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND); line.setStroke(bs); lines.addChild(line); Iterator<GeneralUnitPNode> it = units.iterator(); while (it.hasNext()) { last = current; lastX = currentX; lastY = currentY; current = it.next(); currentX = (float) (current.getX() + current.getWidth() / 2); currentY = (float) (current.getY() + current.getHeight() / 2); // Dot on the unit PPath d = PPath.createEllipse(currentX - bigD / 2, currentY - bigD / 2, bigD, bigD); dotCache.put(current, d); dots.addChild(d); // Draw line and intermedate steps if (last != null) { int steps = spinnerModel.getNumber().intValue(); float dx = (currentX - lastX) / (steps + 1); float dy = (currentY - lastY) / (steps + 1); for (int i = 1; i <= steps; i++) { float ipx = (float) (last.getX() + last.getWidth() / 2 + i * dx); float ipy = (float) (last.getY() + last.getHeight() / 2 + i * dy); float oipx = ipx, oipy = ipy; // Snapping! int x = (int) (ipx / state.mapPNode.getUnitWidth()); int y = (int) (ipy / state.mapPNode.getUnitHeight()); GeneralUnitPNode u = state.mapPNode.getUnit(x, y); ipx = (float) (u.getX() + u.getWidth() / 2); ipy = (float) (u.getY() + u.getHeight() / 2); line.addPoint(line.getPointCount(), ipx, ipy); // Small dot PPath el = dotCache.get(u); @SuppressWarnings("unused") boolean newDot = false; if (el == null) { el = PPath.createEllipse(ipx - smallD / 2, ipy - smallD / 2, smallD, smallD); newDot = true; dotCache.put(u, el); dots.addChild(el); touchedUnits.add(u.getUnit()); } if (debug) { PPath sh = PPath.createLine(ipx, ipy, oipx, oipy); sh.setStrokePaint(Color.red); lines.addChild(sh); PPath od = PPath.createEllipse(oipx - smallD / 4, oipy - smallD / 4, smallD / 2, smallD / 2); od.setStrokePaint(Color.red); el.addChild(od); PText c = new PText(String.format("%d (%d/%d)", i, x, y)); c.setX(od.getX() + od.getWidth()); c.setY(od.getY()); od.addChild(c); } lastX = ipx; lastY = ipy; } } line.addPoint(line.getPointCount(), currentX, currentY); touchedUnits.add(current.getUnit()); } } private void doHighDimRouting(List<GeneralUnitPNode> units) { // TODO: Assumes Euclid distance. Consider metric used for training. GeneralUnitPNode last = null, current = null; double[] lastW = null, currentW = null; float currentX = 0, currentY = 0; PLine line = new PLine(); BasicStroke bs = new BasicStroke(lineModel.getNumber().floatValue(), BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND); line.setStroke(bs); lines.addChild(line); HashMap<GeneralUnitPNode, PPath> dotCache = new HashMap<GeneralUnitPNode, PPath>(); int steps = spinnerModel.getNumber().intValue(); Iterator<GeneralUnitPNode> it = units.iterator(); int uc = 0; while (it.hasNext()) { last = current; lastW = currentW; current = it.next(); uc++; // Where are we? currentW = current.getUnit().getWeightVector(); currentX = (float) (current.getX() + current.getWidth() / 2); currentY = (float) (current.getY() + current.getHeight() / 2); // Dot on the unit PPath d = PPath.createEllipse(currentX - bigD / 2, currentY - bigD / 2, bigD, bigD); if (debug) { PText t = new PText(String.format("%d", uc)); t.setX(currentX - t.getWidth() / 2); t.setY(currentY - t.getHeight() / 2); d.addChild(t); } dotCache.put(current, d); dots.addChild(d); if (last != null) { double[] dw = new double[currentW.length]; for (int i = 0; i < dw.length; i++) { dw[i] = (currentW[i] - lastW[i]) / (steps + 1); } double[] bW = lastW; for (int i = 1; i <= steps; i++) { bW = wgtAdd(bW, dw); Unit u = state.growingLayer.getWinner(new InputDatum("tmp", new DenseDoubleMatrix1D(bW))); GeneralUnitPNode gu = state.mapPNode.getUnit(u); if (dotCache.get(gu) != null) { continue; } float ipx = (float) (gu.getX() + gu.getWidth() / 2); float ipy = (float) (gu.getY() + gu.getHeight() / 2); PPath dot = PPath.createEllipse(ipx - smallD / 2, ipy - smallD / 2, smallD, smallD); dotCache.put(gu, dot); dots.addChild(dot); touchedUnits.add(u); line.addPoint(line.getPointCount(), ipx, ipy); } } touchedUnits.add(current.getUnit()); line.addPoint(line.getPointCount(), currentX, currentY); } } private double[] wgtAdd(double[] a, double[] b) { double[] c = new double[a.length]; for (int i = 0; i < c.length; i++) { c[i] = a[i] + b[i]; } return c; } private void doFlatRouting(List<GeneralUnitPNode> units) { GeneralUnitPNode last = null, current = null; float lastX, lastY, currentX = 0, currentY = 0; PLine line = new PLine(); BasicStroke bs = new BasicStroke(lineModel.getNumber().floatValue(), BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND); line.setStroke(bs); lines.addChild(line); Iterator<GeneralUnitPNode> it = units.iterator(); int uc = 0; while (it.hasNext()) { last = current; lastX = currentX; lastY = currentY; current = it.next(); uc++; currentX = (float) (current.getX() + current.getWidth() / 2); currentY = (float) (current.getY() + current.getHeight() / 2); // Dot on the unit PPath d = PPath.createEllipse(currentX - bigD / 2, currentY - bigD / 2, bigD, bigD); if (debug) { PText t = new PText(String.format("%d", uc)); t.setX(currentX - t.getWidth() / 2); t.setY(currentY - t.getHeight() / 2); d.addChild(t); } dots.addChild(d); // Draw line and intermedate steps if (last != null) { int steps = spinnerModel.getNumber().intValue(); float dx = (currentX - lastX) / (steps + 1); float dy = (currentY - lastY) / (steps + 1); for (int i = 1; i <= steps; i++) { float ipx = lastX + dx; float ipy = lastY + dy; // Small dot dots.addChild(PPath.createEllipse(ipx - smallD / 2, ipy - smallD / 2, smallD, smallD)); // Line line.addPoint(line.getPointCount(), ipx, ipy); lastX = ipx; lastY = ipy; } } line.addPoint(line.getPointCount(), currentX, currentY); } } public class SimpleTSPSolver { private Tour tour; private int start; @SuppressWarnings("unused") private int end; public SimpleTSPSolver(List<GeneralUnitPNode> units) { this(units, false, new Random()); } public SimpleTSPSolver(List<GeneralUnitPNode> units, boolean routeHighDim, Random rand) { Graph g = new Graph(units.size()); // TODO Hard-coded Metric! Uses L2 only for (int i = 0; i < units.size(); i++) { for (int j = 0; j < units.size(); j++) { double[] v1, v2; if (routeHighDim) { v1 = units.get(i).getUnit().getWeightVector(); v2 = units.get(j).getUnit().getWeightVector(); } else { v1 = new double[] { units.get(i).getX(), units.get(i).getY() }; v2 = new double[] { units.get(j).getX(), units.get(j).getY() }; } double dist = 0; for (int k = 0; k < v2.length; k++) { dist += Math.pow(v1[k] - v2[k], 2); } dist = Math.sqrt(dist); g.connect(i, j, dist); } } tour = new Tour(g); tour.random(rand); } public void solve() { // TODO: Solve better than local? tour.localoptimize(); double maxDist = 0; int toi = -1, fromi = -1; for (int i = 0; i < tour.to.length; i++) { if (maxDist < tour.g.distance(i, tour.to[i])) { maxDist = tour.g.distance(i, tour.to[i]); toi = i; fromi = tour.to[i]; } } tour.to[toi] = -1; end = toi; tour.from[fromi] = -1; start = fromi; } public int[] getTour() { return tour.to; } /** * contains a Matrix of distances for a graph. */ class Graph { protected int n; // N double dist[][]; // M final static double INFTY = Double.MAX_VALUE; public Graph(int n) { this.n = n; dist = new double[n][n]; int i, j; // initially disconnect all points for (i = 0; i < n; i++) { for (j = 0; j < n; j++) { if (i != j) { connect(i, j, INFTY); } else { connect(i, j, 0); } } } } void connect(int i, int j, double x) { dist[i][j] = x; } final double distance(int i, int j) { return dist[i][j]; } final int size() { return n; } } /** * A path in a graph. From[i] is the index of the point leading to i. To[i] the index of the point after i. The * path can optimize itself in a graph. */ class Tour { Graph g; int n; double l; public int from[], to[]; public Tour(Graph g) { n = g.size(); this.g = g; from = new int[n]; to = new int[n]; } // return a clone path @Override public Object clone() { Tour p = new Tour(g); p.l = l; int i; for (i = 0; i < n; i++) { p.from[i] = from[i]; p.to[i] = to[i]; } return p; } /** Create a random route. random path. */ public void random(Random r) { int i, j, i0, j0, k; for (i = 0; i < n; i++) { to[i] = -1; } for (i0 = i = 0; i < n - 1; i++) { j = (int) (r.nextLong() % (n - i)); to[i0] = 0; for (j0 = k = 0; k < j; k++) { j0++; while (to[j0] != -1) { j0++; } } while (to[j0] != -1) { j0++; } to[i0] = j0; from[j0] = i0; i0 = j0; } to[i0] = 0; from[0] = i0; getlength(); } /** * The length of this route. * * @return The length */ public double length() { return l; } /** * try to find another path with shorter length using removals of points j and inserting i,j,i+1 * * @return <code>true</code> if an improvement has been found, <code>false</code> otherwise. */ public boolean improve() { int i, j, h; double d1, d2; double H[] = new double[n]; for (i = 0; i < n; i++) { H[i] = -g.distance(from[i], i) - g.distance(i, to[i]) + g.distance(from[i], to[i]); } for (i = 0; i < n; i++) { d1 = -g.distance(i, to[i]); j = to[to[i]]; while (j != i) { d2 = H[j] + g.distance(i, j) + g.distance(j, to[i]) + d1; if (d2 < -1e-10) { h = from[j]; to[h] = to[j]; from[to[j]] = h; h = to[i]; to[i] = j; to[j] = h; from[h] = j; from[j] = i; getlength(); return true; } j = to[j]; } } return false; } /** * improve the path locally, using replacements of i,i+1 and j,j+1 with i,j and i+1,j+1 * * @return <code>true</code> if an improvement has been found, <code>false</code> otherwise. */ public boolean improvecross() { int i, j, h, h1, hj; double d1, d2, d; for (i = 0; i < n; i++) { d1 = -g.distance(i, to[i]); j = to[to[i]]; d2 = 0; d = 0; while (to[j] != i) { d += g.distance(j, from[j]) - g.distance(from[j], j); d2 = d1 + g.distance(i, j) + d + g.distance(to[i], to[j]) - g.distance(j, to[j]); if (d2 < -1e-10) { h = to[i]; h1 = to[j]; to[i] = j; to[h] = h1; from[h1] = h; hj = i; while (j != h) { h1 = from[j]; to[j] = h1; from[j] = hj; hj = j; j = h1; } from[j] = hj; getlength(); return true; } j = to[j]; } } return false; } /** * compute the length of the path */ void getlength() { l = 0; int i; for (i = 0; i < n; i++) { l += g.distance(i, to[i]); } } /** * find a local optimum starting from this path */ void localoptimize() { do { while (improve()) { // ... } } while (improvecross()); } } } }