/* Copyright 2009-2015 David Hadka * * This file is part of the MOEA Framework. * * The MOEA Framework is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or (at your * option) any later version. * * The MOEA Framework 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 Lesser General Public * License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with the MOEA Framework. If not, see <http://www.gnu.org/licenses/>. */ package org.moeaframework.examples.ga.tsplib; import java.awt.BasicStroke; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Insets; import java.awt.Paint; import java.awt.RenderingHints; import java.awt.Stroke; import java.awt.geom.Ellipse2D; import java.awt.geom.Line2D; import java.io.File; import java.io.IOException; import java.util.LinkedHashMap; import java.util.Map; import java.util.Map.Entry; import javax.swing.JFrame; import javax.swing.JPanel; /** * Panel for displaying TSPLIB problem instances and tours. */ public class TSPPanel extends JPanel { private static final long serialVersionUID = -9001874665477567840L; /** * The TSPLIB problem instance. */ private final TSPInstance problem; /** * The displayed tours and their display settings. */ private final Map<Tour, TourDisplaySetting> tours; /** * The width of nodes in the graphical display. */ private double nodeWidth; /** * The border around the graphical display. This border should be at least * half the node width to ensure nodes are fully contained inside the panel. */ private Insets insets; /** * {@code true} if this graphical display should automatically repaint when * the displayed tours are changed; {@code false} otherwise. */ private boolean autoRepaint; /** * Constructs a new panel for displaying a TSPLIB problem instance. * * @param problem the TSPLIB problem instance */ public TSPPanel(TSPInstance problem) { super(); this.problem = problem; if (DisplayDataType.NO_DISPLAY.equals(problem.getDisplayDataType())) { throw new IllegalArgumentException("problem instance does not support a graphical display"); } tours = new LinkedHashMap<Tour, TourDisplaySetting>(); nodeWidth = 4.0; insets = new Insets((int)nodeWidth, (int)nodeWidth, (int)nodeWidth, (int)nodeWidth); autoRepaint = true; setBackground(Color.WHITE); setForeground(Color.BLACK); } /** * Set to {@code true} if this graphical display should automatically * repaint when the displayed tours are changed; {@code false} otherwise. * When {@code false}, the display will only change when {@link #repaint()} * is invoked or the component automatically repaints the panel. This is * used to avoid unnecessary repaints when making many changes to this * display. * * @param autoRepaint {@code true} if this graphical display should * automatically repaint when the displayed tours are changed; * {@code false} otherwise */ public void setAutoRepaint(boolean autoRepaint) { this.autoRepaint = autoRepaint; } /** * Adds a tour to this graphical display. The tour will be displayed using * the default color. * * @param tour the tour to display */ public void displayTour(Tour tour) { synchronized (tours) { tours.put(tour, new TourDisplaySetting()); } if (autoRepaint) { repaint(); } } /** * Adds a tour to this graphical display with the specified paint settings. * * @param tour the tour to display * @param paint the paint settings */ public void displayTour(Tour tour, Paint paint) { synchronized (tours) { tours.put(tour, new TourDisplaySetting(paint)); } if (autoRepaint) { repaint(); } } /** * Adds a tour to this graphical display with the specified paint and * stroke settings. * * @param tour the tour to display * @param paint the paint settings * @param stroke the line stroke settings */ public void displayTour(Tour tour, Paint paint, Stroke stroke) { synchronized (tours) { tours.put(tour, new TourDisplaySetting(paint, stroke)); } if (autoRepaint) { repaint(); } } /** * Removes all tours shown in this display. */ public void clearTours() { synchronized (tours) { tours.clear(); } if (autoRepaint) { repaint(); } } /** * Removes the specified tour from this display. * * @param tour the tour to remove */ public void removeTour(Tour tour) { synchronized (tours) { tours.remove(tour); } if (autoRepaint) { repaint(); } } /** * Sets the width of nodes in the graphical display. * * @param nodeWidth the width of nodes in the graphical display */ public void setNodeWidth(double nodeWidth) { this.nodeWidth = nodeWidth; if (autoRepaint) { repaint(); } } /** * Sets the border around the graphical display. This border should be at * least half the node width to ensure nodes are fully contained inside the * panel. * * @param insets the border around the graphical display */ public void setInsets(Insets insets) { this.insets = insets; if (autoRepaint) { repaint(); } } /** * Converts the node coordinates into display coordinates on the screen. * If this problem uses geographical weights, then the latitude/longitude * coordinates are projected on the screen using the Mercator projection. * * @param node the node whose display coordinates are calculated * @param isGeographical {@code true} if the coordinates are geographical; * {@code false} otherwise * @return the node coordinates into display coordinates on the screen */ private double[] toDisplayCoordinates(Node node, boolean isGeographical) { double[] position = node.getPosition(); double x = position[1]; double y = position[0]; if (isGeographical) { x = GeographicalDistance.toGeographical(x); y = GeographicalDistance.toGeographical(y); x = 0.5 * Math.log((1.0 + Math.sin(x)) / (1.0 - Math.sin(x))); } return new double[] { x, y }; } @Override public void paint(Graphics g) { synchronized(tours) { super.paint(g); } } @Override protected synchronized void paintComponent(Graphics g) { super.paintComponent(g); Graphics2D g2 = (Graphics2D)g; g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // get the display data NodeCoordinates displayData = null; if (DisplayDataType.COORD_DISPLAY.equals(problem.getDisplayDataType())) { displayData = (NodeCoordinates)problem.getDistanceTable(); } else { displayData = problem.getDisplayData(); } // first determine bounds of the data boolean isGeographical = EdgeWeightType.GEO.equals(problem.getEdgeWeightType()); double left = Double.POSITIVE_INFINITY; double right = Double.NEGATIVE_INFINITY; double bottom = Double.POSITIVE_INFINITY; double top = Double.NEGATIVE_INFINITY; for (int i = 1; i <= displayData.size(); i++) { Node node = displayData.get(i); double[] position = toDisplayCoordinates(node, isGeographical); left = Math.min(left, position[0]); right = Math.max(right, position[0]); bottom = Math.min(bottom, position[1]); top = Math.max(top, position[1]); } // calculate the bounds of the drawing int displayWidth = getWidth(); int displayHeight = getHeight(); double scaleX = (displayWidth - insets.right - insets.left) / (right - left); double scaleY = (displayHeight - insets.top - insets.bottom) / (top - bottom); double scale = Math.min(scaleX, scaleY); double offsetX = (displayWidth - insets.right - insets.left - scale * (right - left)) / 2.0; double offsetY = (displayHeight - insets.top - insets.bottom - scale * (top - bottom)) / 2.0; // draw the tours for (Entry<Tour, TourDisplaySetting> entry : tours.entrySet()) { Tour tour = entry.getKey(); TourDisplaySetting displaySettings = entry.getValue(); g2.setPaint(displaySettings.getPaint()); g2.setStroke(displaySettings.getStroke()); for (int i = 0; i < tour.size(); i++) { Node node1 = displayData.get(tour.get(i)); Node node2 = displayData.get(tour.get(i+1)); double[] position1 = toDisplayCoordinates(node1, isGeographical); double[] position2 = toDisplayCoordinates(node2, isGeographical); Line2D line = new Line2D.Double( displayWidth - (offsetX + scale * (position1[0] - left) + insets.left), displayHeight - (offsetY + scale * (position1[1] - bottom) + insets.bottom), displayWidth - (offsetX + scale * (position2[0] - left) + insets.left), displayHeight - (offsetY + scale * (position2[1] - bottom) + insets.bottom)); g2.draw(line); } } // draw the nodes g2.setColor(getForeground()); for (int i = 1; i <= displayData.size(); i++) { Node node = displayData.get(i); double[] position = toDisplayCoordinates(node, isGeographical); Ellipse2D point = new Ellipse2D.Double( displayWidth - (offsetX + scale * (position[0] - left) + insets.left) - (nodeWidth / 2.0), displayHeight - (offsetY + scale * (position[1] - bottom) + insets.bottom) - (nodeWidth / 2.0), nodeWidth, nodeWidth); g2.fill(point); g2.draw(point); } } public static void main(String[] args) throws IOException { TSPInstance problem = new TSPInstance(new File("./data/tsp/gr120.tsp")); problem.addTour(new File("./data/tsp/gr120.opt.tour")); TSPPanel panel = new TSPPanel(problem); for (int i=0; i<20; i++) { panel.displayTour(Tour.createRandomTour(problem.getDimension()), new Color(128, 128, 128, 64)); } panel.displayTour(problem.getTours().get(0), Color.RED, new BasicStroke(2.0f)); JFrame frame = new JFrame(problem.getName()); frame.getContentPane().setLayout(new BorderLayout()); frame.getContentPane().add(panel, BorderLayout.CENTER); frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); frame.setSize(500, 400); frame.setLocationRelativeTo(null); frame.setVisible(true); } /** * The inner class storing tour display settings. These settings control * the paint and line stroke when rendering the tour. */ private class TourDisplaySetting { /** * The paint/color used when rendering the tour. */ private final Paint paint; /** * The line stroke used when rendering the tour. */ private final Stroke stroke; /** * Constructs a new, default tour display setting. */ public TourDisplaySetting() { this(Color.RED); } /** * Constructs a new tour display setting with the specified paint. * * @param paint the paint/color used when rendering the tour */ public TourDisplaySetting(Paint paint) { this(paint, new BasicStroke()); } /** * Constructs a new tour display setting with the specified paint and * line stroke. * * @param paint the paint/color used when rendering the tour * @param stroke the line stroke used when rendering the tour */ public TourDisplaySetting(Paint paint, Stroke stroke) { super(); this.paint = paint; this.stroke = stroke; } /** * Returns the paint/color used when rendering the tour. * * @return the paint/color used when rendering the tour */ public Paint getPaint() { return paint; } /** * Returns the line stroke used when rendering the tour. * * @return the line stroke used when rendering the tour */ public Stroke getStroke() { return stroke; } } }