Java tutorial
/******************************************************************************* * Copyright (C) 2014 Stefan Schroeder * * This library 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.0 of the License, or (at your option) any later version. * * This library 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 this library. If not, see <http://www.gnu.org/licenses/>. ******************************************************************************/ package jspritTest.util; import jsprit.core.problem.VehicleRoutingProblem; import jsprit.core.problem.job.*; import jsprit.core.problem.solution.VehicleRoutingProblemSolution; import jsprit.core.problem.solution.route.VehicleRoute; import jsprit.core.problem.solution.route.activity.TourActivity; import jsprit.core.problem.vehicle.Vehicle; import jsprit.core.util.Coordinate; import jsprit.core.util.Locations; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.jfree.chart.*; import org.jfree.chart.axis.NumberAxis; import org.jfree.chart.labels.XYItemLabelGenerator; import org.jfree.chart.plot.XYPlot; import org.jfree.chart.renderer.xy.XYItemRenderer; import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer; import org.jfree.chart.title.LegendTitle; import org.jfree.data.Range; import org.jfree.data.xy.XYDataItem; import org.jfree.data.xy.XYDataset; import org.jfree.data.xy.XYSeries; import org.jfree.data.xy.XYSeriesCollection; import org.jfree.ui.RectangleEdge; import org.jfree.util.ShapeUtilities; import java.awt.*; import java.awt.geom.Ellipse2D; import java.io.File; import java.io.IOException; import java.util.*; /** * Visualizes problem and solution. * <p>Note that every item to be rendered need to have coordinates. * * @author schroeder */ public class Plotter { private final static Color START_COLOR = Color.RED; private final static Color END_COLOR = Color.RED; private final static Color PICKUP_COLOR = Color.GREEN; private final static Color DELIVERY_COLOR = Color.BLUE; private final static Color SERVICE_COLOR = Color.BLUE; private final static Shape ELLIPSE = new Ellipse2D.Double(-3, -3, 6, 6); private static class MyActivityRenderer extends XYLineAndShapeRenderer { /** * */ private static final long serialVersionUID = 1L; private XYSeriesCollection seriesCollection; private Map<XYDataItem, Activity> activities; private Set<XYDataItem> firstActivities; public MyActivityRenderer(XYSeriesCollection seriesCollection, Map<XYDataItem, Activity> activities, Set<XYDataItem> firstActivities) { super(false, true); this.seriesCollection = seriesCollection; this.activities = activities; this.firstActivities = firstActivities; super.setSeriesOutlinePaint(0, Color.DARK_GRAY); super.setUseOutlinePaint(true); } @Override public Shape getItemShape(int seriesIndex, int itemIndex) { XYDataItem dataItem = seriesCollection.getSeries(seriesIndex).getDataItem(itemIndex); if (firstActivities.contains(dataItem)) { return ShapeUtilities.createUpTriangle(4.0f); } return ELLIPSE; } @Override public Paint getItemOutlinePaint(int seriesIndex, int itemIndex) { XYDataItem dataItem = seriesCollection.getSeries(seriesIndex).getDataItem(itemIndex); if (firstActivities.contains(dataItem)) { return Color.BLACK; } return super.getItemOutlinePaint(seriesIndex, itemIndex); } @Override public Paint getItemPaint(int seriesIndex, int itemIndex) { XYDataItem dataItem = seriesCollection.getSeries(seriesIndex).getDataItem(itemIndex); Activity activity = activities.get(dataItem); if (activity.equals(Activity.PICKUP)) return PICKUP_COLOR; if (activity.equals(Activity.DELIVERY)) return DELIVERY_COLOR; if (activity.equals(Activity.SERVICE)) return SERVICE_COLOR; if (activity.equals(Activity.START)) return START_COLOR; if (activity.equals(Activity.END)) return END_COLOR; throw new IllegalStateException( "activity at " + dataItem.toString() + " cannot be assigned to a color"); } } private static class BoundingBox { double minX; double minY; double maxX; double maxY; public BoundingBox(double minX, double minY, double maxX, double maxY) { super(); this.minX = minX; this.minY = minY; this.maxX = maxX; this.maxY = maxY; } } private enum Activity { START, END, PICKUP, DELIVERY, SERVICE } private static Logger log = LogManager.getLogger(Plotter.class); /** * Label to label ID (=jobId), SIZE (=jobSize=jobCapacityDimensions) * * @author schroeder */ public static enum Label { ID, SIZE, @SuppressWarnings("UnusedDeclaration") NO_LABEL } private Label label = Label.SIZE; private VehicleRoutingProblem vrp; private boolean plotSolutionAsWell = false; private boolean plotShipments = true; private Collection<VehicleRoute> routes; private BoundingBox boundingBox = null; private Map<XYDataItem, Activity> activitiesByDataItem = new HashMap<XYDataItem, Plotter.Activity>(); private Map<XYDataItem, String> labelsByDataItem = new HashMap<XYDataItem, String>(); private XYSeries activities; private Set<XYDataItem> firstActivities = new HashSet<XYDataItem>(); private boolean containsPickupAct = false; private boolean containsDeliveryAct = false; private boolean containsServiceAct = false; private double scalingFactor = 1.; private boolean invert = false; /** * Constructs Plotter with problem. Thus only the problem can be rendered. * * @param vrp the routing problem */ public Plotter(VehicleRoutingProblem vrp) { super(); this.vrp = vrp; } /** * Constructs Plotter with problem and solution to render them both. * * @param vrp the routing problem * @param solution the solution */ public Plotter(VehicleRoutingProblem vrp, VehicleRoutingProblemSolution solution) { super(); this.vrp = vrp; this.routes = solution.getRoutes(); plotSolutionAsWell = true; } /** * Constructs Plotter with problem and routes to render individual routes. * * @param vrp the routing problem * @param routes routes */ public Plotter(VehicleRoutingProblem vrp, Collection<VehicleRoute> routes) { super(); this.vrp = vrp; this.routes = routes; plotSolutionAsWell = true; } @SuppressWarnings("UnusedDeclaration") public Plotter setScalingFactor(double scalingFactor) { this.scalingFactor = scalingFactor; return this; } /** * Sets a label. * * @param label of jobs * @return plotter */ public Plotter setLabel(Label label) { this.label = label; return this; } public Plotter invertCoordinates(boolean invert) { this.invert = invert; return this; } /** * Sets a bounding box to zoom in to certain areas. * * @param minX lower left x * @param minY lower left y * @param maxX upper right x * @param maxY upper right y * @return */ @SuppressWarnings("UnusedDeclaration") public Plotter setBoundingBox(double minX, double minY, double maxX, double maxY) { boundingBox = new BoundingBox(minX, minY, maxX, maxY); return this; } /** * Flag that indicates whether shipments should be rendered as well. * * @param plotShipments flag to plot shipment * @return the plotter */ public Plotter plotShipments(boolean plotShipments) { this.plotShipments = plotShipments; return this; } /** * Plots problem and/or solution/routes. * * @param pngFileName - path and filename * @param plotTitle - title that appears on top of image */ public void plot(String pngFileName, String plotTitle) { String filename = pngFileName; if (!pngFileName.endsWith(".png")) filename += ".png"; if (plotSolutionAsWell) { log.info("1st plot condition"); plot(vrp, routes, filename, plotTitle); } else if (!(vrp.getInitialVehicleRoutes().isEmpty())) { log.info("2nd plot condition"); plot(vrp, vrp.getInitialVehicleRoutes(), filename, plotTitle); } else { plot(vrp, null, filename, plotTitle); } } private void plot(VehicleRoutingProblem vrp, final Collection<VehicleRoute> routes, String pngFile, String title) { log.info("plot to {}", pngFile); XYSeriesCollection problem; XYSeriesCollection solution = null; final XYSeriesCollection shipments; try { retrieveActivities(vrp); //this is where the activities are set if (activities == null) log.info("activities is null"); problem = new XYSeriesCollection(activities); shipments = makeShipmentSeries(vrp.getJobs().values()); if (routes != null) solution = makeSolutionSeries(vrp, routes); } catch (NoLocationFoundException e) { log.warn("cannot plot vrp, since coord is missing"); return; } final XYPlot plot = createPlot(problem, shipments, solution); JFreeChart chart = new JFreeChart(title, plot); LegendTitle legend = createLegend(routes, shipments, plot); chart.removeLegend(); chart.addLegend(legend); save(chart, pngFile); } private LegendTitle createLegend(final Collection<VehicleRoute> routes, final XYSeriesCollection shipments, final XYPlot plot) { LegendItemSource lis = new LegendItemSource() { public LegendItemCollection getLegendItems() { LegendItemCollection lic = new LegendItemCollection(); LegendItem vehLoc = new LegendItem("vehLoc", Color.RED); vehLoc.setShape(ELLIPSE); vehLoc.setShapeVisible(true); lic.add(vehLoc); if (containsServiceAct) { LegendItem item = new LegendItem("service", Color.BLUE); item.setShape(ELLIPSE); item.setShapeVisible(true); lic.add(item); } if (containsPickupAct) { LegendItem item = new LegendItem("pickup", Color.GREEN); item.setShape(ELLIPSE); item.setShapeVisible(true); lic.add(item); } if (containsDeliveryAct) { LegendItem item = new LegendItem("delivery", Color.BLUE); item.setShape(ELLIPSE); item.setShapeVisible(true); lic.add(item); } if (routes != null) { LegendItem item = new LegendItem("firstActivity", Color.BLACK); Shape upTriangle = ShapeUtilities.createUpTriangle(3.0f); item.setShape(upTriangle); item.setOutlinePaint(Color.BLACK); item.setLine(upTriangle); item.setLinePaint(Color.BLACK); item.setShapeVisible(true); lic.add(item); } if (!shipments.getSeries().isEmpty()) { lic.add(plot.getRenderer(1).getLegendItem(1, 0)); } if (routes != null) { lic.addAll(plot.getRenderer(2).getLegendItems()); } return lic; } }; LegendTitle legend = new LegendTitle(lis); legend.setPosition(RectangleEdge.BOTTOM); return legend; } private XYItemRenderer getShipmentRenderer(XYSeriesCollection shipments) { XYItemRenderer shipmentsRenderer = new XYLineAndShapeRenderer(true, false); // Shapes only for (int i = 0; i < shipments.getSeriesCount(); i++) { shipmentsRenderer.setSeriesPaint(i, Color.DARK_GRAY); shipmentsRenderer.setSeriesStroke(i, new BasicStroke(1.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND, 1.f, new float[] { 4.0f, 4.0f }, 0.0f)); } return shipmentsRenderer; } private MyActivityRenderer getProblemRenderer(final XYSeriesCollection problem) { MyActivityRenderer problemRenderer = new MyActivityRenderer(problem, activitiesByDataItem, firstActivities); problemRenderer.setBaseItemLabelGenerator(new XYItemLabelGenerator() { public String generateLabel(XYDataset arg0, int arg1, int arg2) { XYDataItem item = problem.getSeries(arg1).getDataItem(arg2); return labelsByDataItem.get(item); } }); problemRenderer.setBaseItemLabelsVisible(true); problemRenderer.setBaseItemLabelPaint(Color.BLACK); return problemRenderer; } private Range getRange(final XYSeriesCollection seriesCol) { if (this.boundingBox == null) return seriesCol.getRangeBounds(false); else return new Range(boundingBox.minY, boundingBox.maxY); } private Range getDomainRange(final XYSeriesCollection seriesCol) { if (this.boundingBox == null) return seriesCol.getDomainBounds(true); else return new Range(boundingBox.minX, boundingBox.maxX); } private XYPlot createPlot(final XYSeriesCollection problem, XYSeriesCollection shipments, XYSeriesCollection solution) { XYPlot plot = new XYPlot(); plot.setBackgroundPaint(Color.LIGHT_GRAY); plot.setRangeGridlinePaint(Color.LIGHT_GRAY); plot.setDomainGridlinePaint(Color.LIGHT_GRAY); XYLineAndShapeRenderer problemRenderer = getProblemRenderer(problem); plot.setDataset(0, problem); plot.setRenderer(0, problemRenderer); XYItemRenderer shipmentsRenderer = getShipmentRenderer(shipments); plot.setDataset(1, shipments); plot.setRenderer(1, shipmentsRenderer); if (solution != null) { XYItemRenderer solutionRenderer = getRouteRenderer(solution); plot.setDataset(2, solution); plot.setRenderer(2, solutionRenderer); } NumberAxis xAxis = new NumberAxis(); NumberAxis yAxis = new NumberAxis(); if (boundingBox == null) { xAxis.setRangeWithMargins(getDomainRange(problem)); yAxis.setRangeWithMargins(getRange(problem)); } else { xAxis.setRangeWithMargins(new Range(boundingBox.minX, boundingBox.maxX)); yAxis.setRangeWithMargins(new Range(boundingBox.minY, boundingBox.maxY)); } plot.setDomainAxis(xAxis); plot.setRangeAxis(yAxis); return plot; } private XYItemRenderer getRouteRenderer(XYSeriesCollection solutionColl) { XYItemRenderer solutionRenderer = new XYLineAndShapeRenderer(true, false); // Lines only for (int i = 0; i < solutionColl.getSeriesCount(); i++) { XYSeries s = solutionColl.getSeries(i); XYDataItem firstCustomer = s.getDataItem(1); firstActivities.add(firstCustomer); } return solutionRenderer; } private void save(JFreeChart chart, String pngFile) { try { ChartUtilities.saveChartAsPNG(new File(pngFile), chart, 1000, 600); } catch (IOException e) { log.error("cannot plot"); log.error(e); e.printStackTrace(); } } private XYSeriesCollection makeSolutionSeries(VehicleRoutingProblem vrp, Collection<VehicleRoute> routes) throws NoLocationFoundException { Locations locations = retrieveLocations(vrp); XYSeriesCollection coll = new XYSeriesCollection(); int counter = 1; for (VehicleRoute route : routes) { if (route.isEmpty()) continue; XYSeries series = new XYSeries(counter, false, true); Coordinate startCoord = getCoordinate(locations.getCoord(route.getStart().getLocation().getId())); series.add(startCoord.getX() * scalingFactor, startCoord.getY() * scalingFactor); for (TourActivity act : route.getTourActivities().getActivities()) { Coordinate coord = getCoordinate(locations.getCoord(act.getLocation().getId())); series.add(coord.getX() * scalingFactor, coord.getY() * scalingFactor); } Coordinate endCoord = getCoordinate(locations.getCoord(route.getEnd().getLocation().getId())); series.add(endCoord.getX() * scalingFactor, endCoord.getY() * scalingFactor); coll.addSeries(series); counter++; } return coll; } private XYSeriesCollection makeShipmentSeries(Collection<Job> jobs) throws NoLocationFoundException { XYSeriesCollection coll = new XYSeriesCollection(); if (!plotShipments) return coll; int sCounter = 1; String ship = "shipment"; boolean first = true; for (Job job : jobs) { if (!(job instanceof Shipment)) { continue; } Shipment shipment = (Shipment) job; XYSeries shipmentSeries; if (first) { first = false; shipmentSeries = new XYSeries(ship, false, true); } else { shipmentSeries = new XYSeries(sCounter, false, true); sCounter++; } Coordinate pickupCoordinate = getCoordinate(shipment.getPickupLocation().getCoordinate()); Coordinate delCoordinate = getCoordinate(shipment.getDeliveryLocation().getCoordinate()); shipmentSeries.add(pickupCoordinate.getX() * scalingFactor, pickupCoordinate.getY() * scalingFactor); shipmentSeries.add(delCoordinate.getX() * scalingFactor, delCoordinate.getY() * scalingFactor); coll.addSeries(shipmentSeries); } return coll; } private void addJob(XYSeries activities, Job job) { if (job instanceof Shipment) { Shipment s = (Shipment) job; Coordinate pickupCoordinate = getCoordinate(s.getPickupLocation().getCoordinate()); XYDataItem dataItem = new XYDataItem(pickupCoordinate.getX() * scalingFactor, pickupCoordinate.getY() * scalingFactor); activities.add(dataItem); addLabel(s, dataItem); markItem(dataItem, Activity.PICKUP); containsPickupAct = true; Coordinate deliveryCoordinate = getCoordinate(s.getDeliveryLocation().getCoordinate()); XYDataItem dataItem2 = new XYDataItem(deliveryCoordinate.getX() * scalingFactor, deliveryCoordinate.getY() * scalingFactor); activities.add(dataItem2); addLabel(s, dataItem2); markItem(dataItem2, Activity.DELIVERY); containsDeliveryAct = true; } else if (job instanceof Pickup) { Pickup service = (Pickup) job; Coordinate coord = getCoordinate(service.getLocation().getCoordinate()); XYDataItem dataItem = new XYDataItem(coord.getX() * scalingFactor, coord.getY() * scalingFactor); activities.add(dataItem); addLabel(service, dataItem); markItem(dataItem, Activity.PICKUP); containsPickupAct = true; } else if (job instanceof Delivery) { Delivery service = (Delivery) job; Coordinate coord = getCoordinate(service.getLocation().getCoordinate()); XYDataItem dataItem = new XYDataItem(coord.getX() * scalingFactor, coord.getY() * scalingFactor); activities.add(dataItem); addLabel(service, dataItem); markItem(dataItem, Activity.DELIVERY); containsDeliveryAct = true; } else if (job instanceof Service) { Service service = (Service) job; //down casting to specific Service class Coordinate coord = getCoordinate(service.getLocation().getCoordinate()); //get the service coordinate XYDataItem dataItem = new XYDataItem(coord.getX() * scalingFactor, coord.getY() * scalingFactor); activities.add(dataItem); if (this.label.equals(Label.SIZE)) addLabel(service, dataItem); //let's see if we can extend this functionality to include additional labels else { String windowStart = String.valueOf(service.getTimeWindow().getStart()); String windowEnd = String.valueOf(service.getTimeWindow().getEnd()); String label = "{ID=" + job.getId() + ":" + windowStart + "-" + windowEnd + "}"; labelsByDataItem.put(dataItem, label); } markItem(dataItem, Activity.SERVICE); containsServiceAct = true; } else { throw new IllegalStateException( "job instanceof " + job.getClass().toString() + ". this is not supported."); } } private void addLabel(Job job, XYDataItem dataItem) { if (this.label.equals(Label.SIZE)) { labelsByDataItem.put(dataItem, getSizeString(job)); } else if (this.label.equals(Label.ID)) { labelsByDataItem.put(dataItem, String.valueOf(job.getId())); } } private String getSizeString(Job job) { StringBuilder builder = new StringBuilder(); builder.append("("); boolean firstDim = true; for (int i = 0; i < job.getSize().getNuOfDimensions(); i++) { if (firstDim) { builder.append(String.valueOf(job.getSize().get(i))); firstDim = false; } else { builder.append(","); builder.append(String.valueOf(job.getSize().get(i))); } } builder.append(")"); return builder.toString(); } private Coordinate getCoordinate(Coordinate coordinate) { if (invert) { return Coordinate.newInstance(coordinate.getY(), coordinate.getX()); } return coordinate; } //instantiates the activities reference private void retrieveActivities(VehicleRoutingProblem vrp) throws NoLocationFoundException { activities = new XYSeries("activities", false, true); //add the vehicles for (Vehicle v : vrp.getVehicles()) { //get all the vehicles from the vrp instance and iterate throught the vehicles Coordinate start_coordinate = getCoordinate(v.getStartLocation().getCoordinate()); //get the start coordinate if (start_coordinate == null) throw new NoLocationFoundException(); //throw exception if the start coordinate is null XYDataItem item = new XYDataItem(start_coordinate.getX() * scalingFactor, start_coordinate.getY() * scalingFactor); markItem(item, Activity.START); //mark the Item as a Start activities.add(item); //if the current vehicle's start location does not equal its end location's, mark the end coordinate as an xyDataItem if (!v.getStartLocation().getId().equals(v.getEndLocation().getId())) { Coordinate end_coordinate = getCoordinate(v.getEndLocation().getCoordinate()); if (end_coordinate == null) throw new NoLocationFoundException(); XYDataItem end_item = new XYDataItem(end_coordinate.getX() * scalingFactor, end_coordinate.getY() * scalingFactor); markItem(end_item, Activity.END); activities.add(end_item); } } //services, pickups, deliveries are all instanceof job interface for (Job job : vrp.getJobs().values()) { addJob(activities, job); } for (VehicleRoute r : vrp.getInitialVehicleRoutes()) { for (Job job : r.getTourActivities().getJobs()) { addJob(activities, job); } } } private void markItem(XYDataItem item, Activity activity) { activitiesByDataItem.put(item, activity); } private Locations retrieveLocations(VehicleRoutingProblem vrp) throws NoLocationFoundException { return vrp.getLocations(); } }