// This file is part of PleoCommand: // Interactively control Pleo with psychobiological parameters // // Copyright (C) 2010 Oliver Hoffmann - Hoffmann_Oliver@gmx.de // // This program is free software; you can redistribute it and/or // modify it under the terms of the GNU General Public License // as published by the Free Software Foundation; either version 2 // of the License, or (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program; if not, write to the Free Software // Foundation, Inc., 51 Franklin Street, Boston, USA. package pleocmd.itfc.gui; import java.awt.Dimension; import java.awt.Point; import java.awt.Rectangle; import java.awt.geom.Line2D; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.NavigableMap; import java.util.Set; import java.util.TreeMap; import pleocmd.ImmutableRectangle; import pleocmd.pipe.PipePart; import pleocmd.pipe.cvt.Converter; import pleocmd.pipe.in.Input; import pleocmd.pipe.out.Output; final class BoardAutoLayouter { private static final int MAX_SPREADING = 1000000; private static final int STEPS_WO_CHANGE_1 = 10000; private static final int STEPS_WO_CHANGE_2 = 1000; private static final int STEPS_BETWEEN_UPDATE = 100; private final NavigableMap<Double, List<Layout>> fringe; private final Set<Layout> found; private final PipeConfigBoard board; private boolean considerSpreading; private boolean interrupted; private int steps; private int lastIntersections; private int bestSpreading; @SuppressWarnings("synthetic-access") public BoardAutoLayouter(final PipeConfigBoard board) { fringe = new TreeMap<Double, List<Layout>>(); found = new HashSet<Layout>(); this.board = board; Layout.border1 = board.getPainter().getBorder1(false); Layout.border2 = board.getPainter().getBorder2(false); Layout.bounds = new Dimension(); final double s = board.getPainter().getScale(); Layout.bounds.width = (int) (board.getWidth() / s) - 1; Layout.bounds.height = (int) (board.getHeight() / s) - 1; } public void start() { considerSpreading = false; final Map<PipePart, ImmutableRectangle> parts = new HashMap<PipePart, ImmutableRectangle>(); for (final PipePart pp : board.getPainter().getSet()) parts.put(pp, pp.getGuiPosition()); Layout res = treeSearch(new Layout(null, parts)); considerSpreading = true; res = treeSearch(res); if (res != null) res.accept(); } public void interrupt() { interrupted = true; } public double getProgress() { return considerSpreading ? 0.5 + steps / (double) STEPS_WO_CHANGE_2 / 2 : steps / (double) STEPS_WO_CHANGE_1 / 2; } public String getProgressText() { return String.format( "%s phase: %d of %d iterations [%d intersection%s, " + "spreading is %d]", considerSpreading ? "Second" : "First", steps, considerSpreading ? STEPS_WO_CHANGE_2 : STEPS_WO_CHANGE_1, lastIntersections, lastIntersections == 1 ? "" : "s", bestSpreading); } private Layout treeSearch(final Layout root) { if (root == null || interrupted) return null; steps = 0; lastIntersections = 0; bestSpreading = Integer.MAX_VALUE; fringe.clear(); found.clear(); insertInFringe(root, 0); Layout lay = root; while (!fringe.isEmpty()) { lay = removeFromFringe(); if (lastIntersections != lay.getIntersections()) { lastIntersections = lay.getIntersections(); steps = 0; } if (bestSpreading > lay.getSpreading()) { bestSpreading = lay.getSpreading(); steps = 0; } if (steps % STEPS_BETWEEN_UPDATE == 0) { // we can't find a better match during phase 1 if (!considerSpreading && lastIntersections == 0) return lay; if (interrupted) break; lay.accept(); board.repaint(); try { Thread.sleep(100); } catch (final InterruptedException e) { interrupted = true; break; } } lay.expand(this); if (++steps >= (considerSpreading ? STEPS_WO_CHANGE_2 : STEPS_WO_CHANGE_1)) break; } return interrupted ? null : lay; } /** * Inserts a new candidate if it has not already been checked nor is in the * list currently. * * @param layout * a possible layout of the board * @param cost * between 0 and 1 - the higher the cost, the later this layout * will be tested and other layouts with equal heuristics will be * preferred but lower cost instead */ protected void insertInFringe(final Layout layout, final double cost) { // already tried this layout or will be trying it soon if (found.contains(layout)) return; found.add(layout); // strategy: // 1. want to have least possible crossings // 2. want to have best possible spreading // 3. want to try deeper paths with more modifications first // 4. want to randomize the list generated by expand() a bit final double g = -layout.getDepth() / 1000.0; // 0.0001 .. 1 final double c = cost / 1000.0; // 0 .. 0.0001 final double h = MAX_SPREADING * layout.getIntersections() // 1000000 .. - (considerSpreading ? layout.getSpreading() : 0); final double f = g + c + h; List<Layout> l = fringe.get(f); if (l == null) { l = new ArrayList<Layout>(1); fringe.put(f, l); } l.add(layout); } private Layout removeFromFringe() { final Entry<Double, List<Layout>> e = fringe.firstEntry(); final List<Layout> l = e.getValue(); final Layout res = l.remove(l.size() - 1); if (l.isEmpty()) fringe.remove(e.getKey()); return res; } private static final class Layout { private static final int MIN_STEP = 5; private static final int MAX_STEP = 100; private static int border1; private static int border2; private static Dimension bounds; private final Map<PipePart, ImmutableRectangle> parts; private final Map<PipePart, Double> candidateValues; private final int depth; private int intersections = -1; private int spreading = -1; protected Layout(final Layout org, final Map<PipePart, ImmutableRectangle> parts) { this.parts = parts; candidateValues = new HashMap<PipePart, Double>(); depth = org == null ? 0 : org.depth + 1; } protected void accept() { for (final Entry<PipePart, ImmutableRectangle> e : parts.entrySet()) e.getKey().setGuiPosition(e.getValue().createCopy()); } /** * Creates a new layout with simulated moving of one PipePart in one * direction. * * @param bal * to this layouter's fringe the new Layout will be added * @param pp * the PipePart for which so simulate moving * @param rect * current position of the path in the layout * @param xd * between 0 and 1 * @param yd * between 0 and 1 * @param c * between >0 and 1 */ private void expandPart(final BoardAutoLayouter bal, final PipePart pp, final ImmutableRectangle rect, final double xd, final double yd, final double c) { final Rectangle r = rect.createCopy(); r.x += MIN_STEP * (int) (1 + MAX_STEP / MIN_STEP * xd); r.y += (int) (yd * MAX_STEP); check(r, pp); // quick-check if later found.contains() would return true anyway if (rect.equalsRect(r)) return; // clone list but modify pp's value final Map<PipePart, ImmutableRectangle> copy; copy = new HashMap<PipePart, ImmutableRectangle>(parts); copy.put(pp, new ImmutableRectangle(r)); // found a (new) candidate bal.insertInFringe(new Layout(this, copy), c * Math.random()); } protected void expand(final BoardAutoLayouter bal) { for (final Entry<PipePart, ImmutableRectangle> e : parts.entrySet()) { // Parts which are involved in many intersections will // get a slightly higher priority final Double v0 = candidateValues.get(e.getKey()); final double v = v0 == null ? 1.0 : Math.max(0.01, Math.min(1.0, v0)); expandPart(bal, e.getKey(), e.getValue(), Math.random(), 0, v); expandPart(bal, e.getKey(), e.getValue(), -Math.random(), 0, v); expandPart(bal, e.getKey(), e.getValue(), 0, Math.random(), v); expandPart(bal, e.getKey(), e.getValue(), 0, -Math.random(), v); } } protected int getDepth() { return depth; } private void incCandidateValue(final PipePart pp) { final Double v = candidateValues.get(pp); candidateValues.put(pp, v == null ? 0.1 : v + 0.1); } private static final class ConnLine { private final Line2D line; private final PipePart pp1; private final PipePart pp2; public ConnLine(final Line2D line, final PipePart pp1, final PipePart pp2) { this.line = line; this.pp1 = pp1; this.pp2 = pp2; } public Line2D getLine() { return line; } public PipePart getPP1() { return pp1; } public PipePart getPP2() { return pp2; } } protected int getIntersections() { if (intersections != -1) return intersections; // intersection of connections with PipeParts and other ones is bad int its = 0; final List<ConnLine> conns = new ArrayList<ConnLine>(); for (final Entry<PipePart, ImmutableRectangle> e : parts.entrySet()) for (final PipePart ppTrg : e.getKey().getConnectedPipeParts()) { final Point ps = new Point(); final Point pt = new Point(); BoardPainter.calcConnectorPositions(e.getValue(), parts.get(ppTrg), ps, pt); final Line2D line = new Line2D.Float(ps, pt); for (final ConnLine conn : conns) { final Line2D line2 = conn.getLine(); if (conn.getLine().intersectsLine(line) && !ps.equals(line2.getP1()) && !ps.equals(line2.getP2()) && !pt.equals(line2.getP1()) && !pt.equals(line2.getP2())) { ++its; // bad for both ends of both connections // which intersect incCandidateValue(e.getKey()); incCandidateValue(ppTrg); incCandidateValue(conn.getPP1()); incCandidateValue(conn.getPP2()); } } for (final Entry<PipePart, ImmutableRectangle> e2 : parts .entrySet()) { boolean intersects; if (e2.getKey() == e.getKey() || e2.getKey() == ppTrg) { final Rectangle r = e2.getValue().createCopy(); r.grow(-2, -2); intersects = r.intersectsLine(line); } else intersects = e2.getValue().intersectsLine(line); if (intersects) { ++its; // bad for the PipePart and both ends of the // connection which intersects incCandidateValue(e.getKey()); incCandidateValue(ppTrg); incCandidateValue(e2.getKey()); } } conns.add(new ConnLine(line, e.getKey(), ppTrg)); } intersections = its; return intersections; } public int getSpreading() { if (spreading != -1) return spreading; double spread = .0; // the more spread, the better for (final Entry<PipePart, ImmutableRectangle> e1 : parts .entrySet()) { for (final Entry<PipePart, ImmutableRectangle> e2 : parts .entrySet()) if (e1 != e2) spread += getDistance(e1.getValue(), e2.getValue()); spread += getDistance(e1.getValue(), new Line2D.Float(0, 0, bounds.width, 0)); spread += getDistance(e1.getValue(), new Line2D.Float( bounds.width, 0, bounds.width, bounds.height)); spread += getDistance(e1.getValue(), new Line2D.Float(0, bounds.height, bounds.width, bounds.height)); spread += getDistance(e1.getValue(), new Line2D.Float(0, 0, 0, bounds.height)); } spreading = Math.min(MAX_SPREADING - 1, (int) (spread + 0.5)); return spreading; } private double getDistance(final ImmutableRectangle r1, final ImmutableRectangle r2) { final int cx1 = r1.getX() + r1.getWidth() / 2; final int cy1 = r1.getY() + r1.getHeight() / 2; final int cx2 = r2.getX() + r2.getWidth() / 2; final int cy2 = r2.getY() + r2.getHeight() / 2; final int dx = cx1 - cx2; final int dy = cy1 - cy2; return Math.log(Math.sqrt(dx * dx + dy * dy)); } private double getDistance(final ImmutableRectangle r1, final Line2D l2) { return Math.log(l2.ptLineDist(r1.getX() + r1.getWidth() / 2, r1.getY() + r1.getHeight() / 2)); } private void check(final Rectangle r, final PipePart pp) { final int xMin; final int xMax; final int yMin = 1; final int yMax = bounds.height; if (Input.class.isInstance(pp)) { xMin = 1; xMax = border1 - 1; } else if (Converter.class.isInstance(pp)) { xMin = border1 + 1; xMax = border2 - 1; } else if (Output.class.isInstance(pp)) { xMin = border2 + 1; xMax = bounds.width; } else { xMin = 1; xMax = bounds.width; } if (r.x < xMin) r.x = xMin; if (r.y < yMin) r.y = yMin; if (r.x + r.width > xMax) r.x = xMax - r.width; if (r.y + r.height > yMax) r.y = yMax - r.height; for (final Entry<PipePart, ImmutableRectangle> e : parts.entrySet()) if (e.getKey() != pp && e.getValue().intersects(r)) { // move r, so it doesn't intersect anymore final ImmutableRectangle rO = e.getValue(); final Rectangle i = rO.intersection(r); final int x0 = r.x + r.width / 2; final int y0 = r.y + r.height / 2; final int x1 = rO.getX() + rO.getWidth() / 2; final int y1 = rO.getY() + rO.getHeight() / 2; if (i.width < i.height && r.x - i.width >= xMin && r.x + r.width + i.width <= xMax) { if (x0 > x1) // move right r.translate(i.width, 0); else // move left r.translate(-i.width, 0); } else if (y0 > y1) { if (r.y + r.height + i.height > yMax) // move up instead of down r.translate(0, i.height - r.height - rO.getHeight()); else r.translate(0, i.height); // move down } else if (r.y - i.height < yMin) // move down instead of up r.translate(0, r.height + rO.getHeight() - i.height); else r.translate(0, -i.height); // move up // check bounds again // (overlapping is better than being out of bounds) if (r.x < xMin) r.x = xMin; if (r.y < yMin) r.y = yMin; if (r.x + r.width > xMax) r.x = xMax - r.width; if (r.y + r.height > yMax) r.y = yMax - r.height; } } @Override public boolean equals(final Object other) { return other instanceof Layout && parts.equals(((Layout) other).parts); } @Override public int hashCode() { return parts.hashCode(); } } }