package tools.map.making; import java.awt.Dimension; import java.awt.Polygon; import java.awt.Shape; import java.awt.geom.AffineTransform; import java.awt.geom.Area; import java.awt.geom.PathIterator; import java.awt.geom.Point2D; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.swing.JLabel; import javax.swing.JOptionPane; import games.strategy.debug.ClientLogger; import games.strategy.ui.Util; import games.strategy.util.AlphanumComparator; import games.strategy.util.PointFileReaderWriter; import tools.image.FileOpen; import tools.image.FileSave; /** * Utility to find connections between polygons * Not pretty, meant only for one time use. * Inputs - a polygons.txt file * Outputs - a list of connections between the Polygons */ // TODO: get this moved to its own package tree public class ConnectionFinder { private static File s_mapFolderLocation = null; private static final String TRIPLEA_MAP_FOLDER = "triplea.map.folder"; private static final String LINE_THICKNESS = "triplea.map.lineThickness"; private static final String SCALE_PIXELS = "triplea.map.scalePixels"; private static final String MIN_OVERLAP = "triplea.map.minOverlap"; private static boolean dimensionsSet = false; private static StringBuffer territoryDefinitions = null; // how many pixels should each area become bigger in both x and y axis to see which area it overlaps? // default 8, or if LINE_THICKNESS if given 4x linethickness public static int scalePixels = 8; // how many pixels should the boundingbox of the overlapping area have for it to be considered a valid connection? // default 32, or if LINE_THICKNESS is given 16 x linethickness public static double minOverlap = 32.0; public static void main(final String[] args) { handleCommandLineArgs(args); JOptionPane.showMessageDialog(null, new JLabel("<html>" + "This is the ConnectionFinder. " + "<br>It will create a file containing the connections between territories, and optionally the territory " + "definitions as well. " + "<br>Copy and paste everything from this file into your game xml file (the 'map' section). " + "<br>The connections file can and Should Be Deleted when finished, because it is Not Needed and not read " + "by the engine. " + "</html>")); System.out.println("Select polygons.txt"); File polyFile = null; if (s_mapFolderLocation != null && s_mapFolderLocation.exists()) { polyFile = new File(s_mapFolderLocation, "polygons.txt"); } if (polyFile != null && polyFile.exists() && JOptionPane.showConfirmDialog(null, "A polygons.txt file was found in the map's folder, do you want to use it?", "File Suggestion", 1) == 0) { // yay } else { polyFile = new FileOpen("Select The polygons.txt file", s_mapFolderLocation, ".txt").getFile(); } if (polyFile == null || !polyFile.exists()) { System.out.println("No polygons.txt Selected. Shutting down."); System.exit(0); } if (s_mapFolderLocation == null && polyFile != null) { s_mapFolderLocation = polyFile.getParentFile(); } final Map<String, List<Area>> territoryAreas = new HashMap<>(); Map<String, List<Polygon>> mapOfPolygons = null; try { final FileInputStream in = new FileInputStream(polyFile); mapOfPolygons = PointFileReaderWriter.readOneToManyPolygons(in); for (final String territoryName : mapOfPolygons.keySet()) { final List<Polygon> listOfPolygons = mapOfPolygons.get(territoryName); final List<Area> listOfAreas = new ArrayList<>(); for (final Polygon p : listOfPolygons) { listOfAreas.add(new Area(p)); } territoryAreas.put(territoryName, listOfAreas); } } catch (final IOException ex) { ClientLogger.logQuietly(ex); } if (!dimensionsSet) { final String lineWidth = JOptionPane.showInputDialog(null, "Enter the width of territory border lines on your map? \r\n(eg: 1, or 2, etc.)"); try { final int lineThickness = Integer.parseInt(lineWidth); scalePixels = lineThickness * 4; minOverlap = scalePixels * 4; dimensionsSet = true; } catch (final NumberFormatException ex) { // ignore malformed input } } if (JOptionPane.showConfirmDialog(null, "Scale set to " + scalePixels + " pixels larger, and minimum overlap set to " + minOverlap + " pixels. \r\n" + "Do you wish to continue with this? \r\n" + "Select Yes to continue, Select No to override and change the size.", "Scale and Overlap Size", JOptionPane.YES_NO_OPTION) == 1) { final String scale = JOptionPane.showInputDialog(null, "Enter the number of pixels larger each territory should become? \r\n" + "(Normally 4x bigger than the border line width. eg: 4, or 8, etc)"); try { scalePixels = Integer.parseInt(scale); } catch (final NumberFormatException ex) { // ignore malformed input } final String overlap = JOptionPane.showInputDialog(null, "Enter the minimum number of overlapping pixels for a connection? \r\n" + "(Normally 16x bigger than the border line width. eg: 16, or 32, etc.)"); try { minOverlap = Integer.parseInt(overlap); } catch (final NumberFormatException ex) { // ignore malformed input } } final Map<String, Collection<String>> connections = new HashMap<>(); System.out.println("Now Scanning for Connections"); // sort so that they are in alphabetic order (makes xml's prettier and easier to update in future) final List<String> allTerritories = mapOfPolygons == null ? new ArrayList<>() : new ArrayList<>(mapOfPolygons.keySet()); Collections.sort(allTerritories, new AlphanumComparator()); final List<String> allAreas = new ArrayList<>(territoryAreas.keySet()); Collections.sort(allAreas, new AlphanumComparator()); for (final String territory : allTerritories) { final Set<String> thisTerritoryConnections = new LinkedHashSet<>(); final List<Polygon> currentPolygons = mapOfPolygons.get(territory); for (final Polygon currentPolygon : currentPolygons) { final Shape scaledShape = scale(currentPolygon, scalePixels); for (final String otherTerritory : allAreas) { if (otherTerritory.equals(territory)) { continue; } if (thisTerritoryConnections.contains(otherTerritory)) { continue; } if (connections.get(otherTerritory) != null && connections.get(otherTerritory).contains(territory)) { continue; } for (final Area otherArea : territoryAreas.get(otherTerritory)) { final Area testArea = new Area(scaledShape); testArea.intersect(otherArea); if (!testArea.isEmpty() && sizeOfArea(testArea) > minOverlap) { thisTerritoryConnections.add(otherTerritory); } } } connections.put(territory, thisTerritoryConnections); } } if (JOptionPane.showConfirmDialog(null, "Do you also want to create the Territory Definitions?", "Territory Definitions", 1) == 0) { final String waterString = JOptionPane.showInputDialog(null, "Enter a string or regex that determines if the territory is Water? \r\n(e.g.: " + Util.TERRITORY_SEA_ZONE_INFIX + ")", Util.TERRITORY_SEA_ZONE_INFIX); territoryDefinitions = doTerritoryDefinitions(allTerritories, waterString); } try { final String fileName = new FileSave("Where To Save connections.txt ? (cancel to print to console)", "connections.txt", s_mapFolderLocation).getPathString(); final StringBuffer connectionsString = convertToXML(connections); if (fileName == null) { System.out.println(); if (territoryDefinitions != null) { System.out.println(territoryDefinitions.toString()); } System.out.println(connectionsString.toString()); } else { final FileOutputStream out = new FileOutputStream(fileName); if (territoryDefinitions != null) { out.write(String.valueOf(territoryDefinitions).getBytes()); } out.write(String.valueOf(connectionsString).getBytes()); out.flush(); out.close(); System.out.println("Data written to :" + new File(fileName).getCanonicalPath()); } } catch (final Exception ex) { ex.printStackTrace(); } } // end main /** * Creates the xml territory definitions. * * @param waterString * a substring contained in a TerritoryName to define a Sea Zone or a regex expression that indicates that a * territory is water * @return StringBuffer containing XML representing these connections */ private static StringBuffer doTerritoryDefinitions(final List<String> allTerritoryNames, final String waterString) { // sort for pretty xml's Collections.sort(allTerritoryNames, new AlphanumComparator()); final StringBuffer output = new StringBuffer(); output.append("<!-- Territory Definitions -->\r\n"); final Pattern waterPattern = Pattern.compile(waterString); for (final String t : allTerritoryNames) { final Matcher matcher = waterPattern.matcher(t); if (matcher.find()) { // <territory name="sea zone 1" water="true"/> output.append("<territory name=\"").append(t).append("\" water=\"true\"/>\r\n"); } else { // <territory name="neutral territory 2"/> output.append("<territory name=\"").append(t).append("\"/>\r\n"); } } output.append("\r\n"); return output; } /** * Converts a map of connections to XML formatted text with the connections. * * @param connections * a map of connections between Territories * @return a StringBuffer containing XML representing these connections */ private static StringBuffer convertToXML(final Map<String, Collection<String>> connections) { final StringBuffer output = new StringBuffer(); output.append("<!-- Territory Connections -->\r\n"); // sort for pretty xml's final List<String> allTerritories = new ArrayList<>(connections.keySet()); Collections.sort(allTerritories, new AlphanumComparator()); for (final String t1 : allTerritories) { for (final String t2 : connections.get(t1)) { output.append("<connection t1=\"").append(t1).append("\" t2=\"").append(t2).append("\"/>\r\n"); } } return output; } /** * Returns the size of the area of the bounding box of the polygon. * * @param area * the area of which the boundingbox size is measured * @return the size of the area of the boundingbox of this area */ public static double sizeOfArea(final Area area) { final Dimension d = area.getBounds().getSize(); return d.getHeight() * d.getWidth(); } /** * from: eu.hansolo.steelseries.tools.Scaler.java * Returns a double that represents the area of the given point array of a polygon * * @return a double that represents the area of the given point array of a polygon */ private static double calcSignedPolygonArea(final Point2D[] pointArray) { final int N = pointArray.length; int i; int j; double area = 0; for (i = 0; i < N; i++) { j = (i + 1) % N; area += pointArray[i].getX() * pointArray[j].getY(); area -= pointArray[i].getY() * pointArray[j].getX(); } area /= 2.0; return (area); } /** * from: eu.hansolo.steelseries.tools.Scaler.java * Returns a Point2D object that represents the center of mass of the given point array which represents a * polygon. * * @return a Point2D object that represents the center of mass of the given point array */ private static Point2D calcCenterOfMass(final Point2D[] pointArray) { final int N = pointArray.length; double cx = 0; double cy = 0; double area = calcSignedPolygonArea(pointArray); final Point2D centroid = new Point2D.Double(); int i; int j; double factor = 0; for (i = 0; i < N; i++) { j = (i + 1) % N; factor = (pointArray[i].getX() * pointArray[j].getY() - pointArray[j].getX() * pointArray[i].getY()); cx += (pointArray[i].getX() + pointArray[j].getX()) * factor; cy += (pointArray[i].getY() + pointArray[j].getY()) * factor; } area *= 6.0f; factor = 1 / area; cx *= factor; cy *= factor; centroid.setLocation(cx, cy); return centroid; } /** * from: eu.hansolo.steelseries.tools.Scaler.java * Returns a Point2D object that represents the center of mass of the given shape. * * @return a Point2D object that represents the center of mass of the given shape */ private static Point2D getCentroid(final Shape currentShape) { final ArrayList<Point2D> pointList = new ArrayList<>(32); final PathIterator pathIterator = currentShape.getPathIterator(null); int lastMoveToIndex = -1; while (!pathIterator.isDone()) { final double[] coordinates = new double[6]; switch (pathIterator.currentSegment(coordinates)) { case PathIterator.SEG_MOVETO: pointList.add(new Point2D.Double(coordinates[0], coordinates[1])); lastMoveToIndex++; break; case PathIterator.SEG_LINETO: pointList.add(new Point2D.Double(coordinates[0], coordinates[1])); break; case PathIterator.SEG_QUADTO: pointList.add(new Point2D.Double(coordinates[0], coordinates[1])); pointList.add(new Point2D.Double(coordinates[2], coordinates[3])); break; case PathIterator.SEG_CUBICTO: pointList.add(new Point2D.Double(coordinates[0], coordinates[1])); pointList.add(new Point2D.Double(coordinates[2], coordinates[3])); pointList.add(new Point2D.Double(coordinates[4], coordinates[5])); break; case PathIterator.SEG_CLOSE: if (lastMoveToIndex >= 0) { pointList.add(pointList.get(lastMoveToIndex)); } break; } pathIterator.next(); } final Point2D[] pointArray = new Point2D[pointList.size()]; pointList.toArray(pointArray); return (calcCenterOfMass(pointArray)); } public static Shape scale(final Shape currentShape, final int pixels) { final Dimension d = currentShape.getBounds().getSize(); final double scalefactorX = 1.0 + (1 / ((double) d.width)) * pixels; final double scalefactorY = 1.0 + (1 / ((double) d.height)) * pixels; return scale(currentShape, scalefactorX, scalefactorY); } /** * from: eu.hansolo.steelseries.tools.Scaler.java * Returns a scaled version of the given shape, calculated by the given scale factor. * The scaling will be calculated around the centroid of the shape. * * @param sx * how much to scale on the x-axis * @param sy * how much to scale on the y-axis * @return a scaled version of the given shape, calculated around the centroid by the given scale factors. */ private static Shape scale(final Shape currentPolygon, final double sx, final double sy) { final Point2D centroid = getCentroid(currentPolygon); final AffineTransform transform = AffineTransform.getTranslateInstance((1.0 - sx) * centroid.getX(), (1.0 - sy) * centroid.getY()); transform.scale(sx, sy); final Shape shape = transform.createTransformedShape(currentPolygon); return shape; } private static void handleCommandLineArgs(final String[] args) { for (final String arg : args) { final String value = getValue(arg); if (arg.startsWith(TRIPLEA_MAP_FOLDER)) { final File mapFolder = new File(value); if (mapFolder.exists()) { s_mapFolderLocation = mapFolder; } else { System.out.println("Could not find directory: " + value); } } if (arg.startsWith(LINE_THICKNESS)) { final int lineThickness = Integer.parseInt(value); scalePixels = lineThickness * 4; minOverlap = scalePixels * 4; dimensionsSet = true; } if (arg.startsWith(MIN_OVERLAP)) { minOverlap = Integer.parseInt(value); } if (arg.startsWith(SCALE_PIXELS)) { scalePixels = Integer.parseInt(value); } } // might be set by -D if (s_mapFolderLocation == null || s_mapFolderLocation.length() < 1) { final String value = System.getProperty(TRIPLEA_MAP_FOLDER); if (value != null && value.length() > 0) { final File mapFolder = new File(value); if (mapFolder.exists()) { s_mapFolderLocation = mapFolder; } else { System.out.println("Could not find directory: " + value); } } } String value = System.getProperty(LINE_THICKNESS); if (value != null && value.length() > 0) { final int lineThickness = Integer.parseInt(value); scalePixels = lineThickness * 4; minOverlap = scalePixels * 4; dimensionsSet = true; } value = System.getProperty(MIN_OVERLAP); if (value != null && value.length() > 0) { minOverlap = Integer.parseInt(value); } value = System.getProperty(SCALE_PIXELS); if (value != null && value.length() > 0) { scalePixels = Integer.parseInt(value); } } private static String getValue(final String arg) { final int index = arg.indexOf('='); if (index == -1) { return ""; } return arg.substring(index + 1); } }