/** * Copyright (C) 2002-2012 The FreeCol Team * * This file is part of FreeCol. * * FreeCol 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. * * FreeCol 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 FreeCol. If not, see <http://www.gnu.org/licenses/>. */ package net.sf.freecol.common.model; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.HashMap; import java.util.Iterator; import java.util.NoSuchElementException; import java.util.PriorityQueue; import java.util.Random; import java.util.logging.Logger; import org.freecolandroid.xml.stream.XMLStreamConstants; import org.freecolandroid.xml.stream.XMLStreamException; import org.freecolandroid.xml.stream.XMLStreamReader; import org.freecolandroid.xml.stream.XMLStreamWriter; import net.sf.freecol.common.model.pathfinding.CostDecider; import net.sf.freecol.common.model.pathfinding.CostDeciders; import net.sf.freecol.common.model.pathfinding.GoalDecider; import net.sf.freecol.common.util.Utils; /** * A rectangular isometric map. The map is represented as a * two-dimensional array of tiles. Off-map destinations, such as * {@link Europe}, can be reached via the {@link HighSeas}. * * <br/><br/> * * In theory, a {@link Game} might contain several Map instances * connected by the HighSeas. */ public class Map extends FreeColGameObject implements Location { private static final Logger logger = Logger.getLogger(Map.class.getName()); public final static int POLAR_HEIGHT = 2; /** * The layers included in the map. The RIVERS layer includes all * natural tile improvements that are not resources. The NATIVES * layer includes Lost City Rumours as well as settlements. */ public static enum Layer { NONE, LAND, TERRAIN, RIVERS, RESOURCES, NATIVES, ALL; }; /** * The directions a Unit can move to. Includes deltas for moving * to adjacent squares, which are required due to the isometric * map. Starting north and going clockwise. */ public static enum Direction { N ( 0, -2, 0, -2), NE ( 1, -1, 0, -1), E ( 1, 0, 1, 0), SE ( 1, 1, 0, 1), S ( 0, 2, 0, 2), SW ( 0, 1, -1, 1), W (-1, 0, -1, 0), NW ( 0, -1, -1, -1); public final static int NUMBER_OF_DIRECTIONS = values().length; public final static Direction[] longSides = new Direction[] { Direction.NE, Direction.SE, Direction.SW, Direction.NW }; public static final Direction[] corners = new Direction[] { Direction.N, Direction.E, Direction.S, Direction.W }; private int oddDX, oddDY, evenDX, evenDY; Direction(int oddDX, int oddDY, int evenDX, int evenDY) { this.oddDX = oddDX; this.oddDY = oddDY; this.evenDX = evenDX; this.evenDY = evenDY; } public int getOddDX() { return oddDX; } public int getOddDY() { return oddDY; } public int getEvenDX() { return evenDX; } public int getEvenDY() { return evenDY; } /** * Gets this direction rotated by n places. * * @param n The number of places to rotate. * @return The rotated direction. */ private Direction rotate(int n) { return values()[(ordinal() + n + NUMBER_OF_DIRECTIONS) % NUMBER_OF_DIRECTIONS]; } /** * Get the next direction after this one (clockwise). * * @return The next <code>Direction</code>. */ public Direction getNextDirection() { return rotate(1); } /** * Get the previous direction after this one (anticlockwise). * * @return The previous <code>Direction</code>. */ public Direction getPreviousDirection() { return rotate(-1); } /** * Returns the reverse direction of the given direction. * * @return The reverse <code>Direction</code>. */ public Direction getReverseDirection() { return rotate(NUMBER_OF_DIRECTIONS/2); } public String getNameKey() { return "direction." + this.toString(); } /** * Gets a random Direction. * * @param logMe A string to log with the random results. * @param random A <code>Random</code> number source. * @return A random <code>Direction</code> value. */ public static Direction getRandomDirection(String logMe, Random random) { return values()[Utils.randomInt(logger, logMe, random, NUMBER_OF_DIRECTIONS)]; } /** * Creates an array of the eight directions in a random order. * * @param logMe A string to log with the random results. * @param random A <code>Random</code> number source. * @return An array of the <code>Direction</code>s in a random order. */ public static Direction[] getRandomDirections(String logMe, Random random) { Direction[] directions = Direction.values(); for (int i = 0; i < directions.length; i++) { int i2 = Utils.randomInt(logger, logMe, random, NUMBER_OF_DIRECTIONS); if (i2 != i) { Direction temp = directions[i2]; directions[i2] = directions[i]; directions[i] = temp; } } return directions; } /** * Creates an array of the directions in an order that favours * a supplied one. Entry 0 will be the supplied direction, * entry 1+2 will be those immediately to the left and right * of it (chosen randomly), and so on until the last entry * will be the complete reverse of the supplied direction. * * Useful if we to step in a particular direction, but if this * fails to be able to try the closest other directions to the * original one in order. * * @param logMe A string to log with the random results. * @param random A <code>Random</code> number source. * @return An array of the <code>Direction</code>s favouring this one. */ public Direction[] getClosestDirections(String logMe, Random random) { // Will need 3 bits of randomness --- 2 directions are known, // need one bit to randomize each remaining pair. final int nbits = (NUMBER_OF_DIRECTIONS - 2) / 2; final int r = Utils.randomInt(logger, logMe, random, 1 << nbits); Direction[] ret = new Direction[NUMBER_OF_DIRECTIONS]; ret[0] = this; int step = 1, mask = 1, ord = this.ordinal(); for (int i = 1; i < NUMBER_OF_DIRECTIONS; i += 2) { Direction dr = this.rotate(step); Direction dl = this.rotate(NUMBER_OF_DIRECTIONS - step); ret[i] = ((r & mask) == 0) ? dr : dl; ret[i+1] = ((r & mask) == 0) ? dl : dr; step += 1; mask *= 2; } ret[NUMBER_OF_DIRECTIONS-1] = this.getReverseDirection(); return ret; } } /** The infinity cost as used by {@link #findPath(Unit, Tile, Tile)}. */ public static final int COST_INFINITY = Integer.MIN_VALUE; private Tile[][] tiles; /** * The highest map layer included. */ private Layer layer; /** * The latitude of the northern edge of the map. A negative value * indicates northern latitude, a positive value southern * latitude. Thus, -30 equals 30°N, and 40 equals 40°S. */ private int minimumLatitude = -90; /** * The latitude of the southern edge of the map. A negative value * indicates northern latitude, a positive value southern * latitude. Thus, -30 equals 30°N, and 40 equals 40°S. */ private int maximumLatitude = 90; /** * Variable used to convert rows to latitude. */ private float latitudePerRow; private final java.util.Map<String, Region> regions = new HashMap<String, Region>(); /** * Create a new <code>Map</code> from a collection of tiles. * * @param game * The <code>Game</code> this map belongs to. * @param tiles * The 2D array of tiles. */ public Map(Game game, Tile[][] tiles) { super(game); this.tiles = tiles; setLayer(Layer.RESOURCES); calculateLatitudePerRow(); } /** * Create a new <code>Map</code> from an <code>Element</code> in a * DOM-parsed XML-tree. * * @param game * The <code>Game</code> this map belongs to. * @param in * The input stream containing the XML. * @throws XMLStreamException * if a problem was encountered during parsing. */ public Map(Game game, XMLStreamReader in) throws XMLStreamException { super(game, in); readFromXML(in); } /** * Initiates a new <code>Map</code> with the given ID. The object should * later be initialized by calling either * {@link #readFromXML(XMLStreamReader)} or * {@link #readFromXMLElement(Element)}. * * @param game * The <code>Game</code> in which this object belong. * @param id * The unique identifier for this object. */ public Map(Game game, String id) { super(game, id); } /** * Returns a Collection containing all map regions. * * @return a Collection containing all map regions */ public Collection<Region> getRegions() { return regions.values(); } /** * Get the <code>Layer</code> value. * * @return a <code>Layer</code> value */ public final Layer getLayer() { return layer; } /** * Set the <code>Layer</code> value. * * @param newLayer The new Layer value. */ public final void setLayer(final Layer newLayer) { this.layer = newLayer; } /** * Get the <code>MinimumLatitude</code> value. * * @return an <code>int</code> value */ public final int getMinimumLatitude() { return minimumLatitude; } /** * Set the <code>MinimumLatitude</code> value. * * @param newMinimumLatitude The new MinimumLatitude value. */ public final void setMinimumLatitude(final int newMinimumLatitude) { this.minimumLatitude = newMinimumLatitude; calculateLatitudePerRow(); } /** * Get the <code>MaximumLatitude</code> value. * * @return an <code>int</code> value */ public final int getMaximumLatitude() { return maximumLatitude; } /** * Set the <code>MaximumLatitude</code> value. * * @param newMaximumLatitude The new MaximumLatitude value. */ public final void setMaximumLatitude(final int newMaximumLatitude) { this.maximumLatitude = newMaximumLatitude; calculateLatitudePerRow(); } /** * Get the <code>LatitudePerRow</code> value. * * @return a <code>float</code> value */ public final float getLatitudePerRow() { return latitudePerRow; } /** * Calculates the <code>LatitudePerRow</code> value. */ private final void calculateLatitudePerRow() { this.latitudePerRow = 1f * (maximumLatitude - minimumLatitude) / (getHeight() - 1); } /** * Get the latitude of the given map row. * * @param row an <code>int</code> value * @return an <code>int</code> value */ public int getLatitude(int row) { return minimumLatitude + (int) (row * latitudePerRow); } /** * Get the map row with the given latitude. * * @param latitude an <code>int</code> value * @return an <code>int</code> value */ public int getRow(int latitude) { return (int) ((latitude - minimumLatitude) / latitudePerRow); } /** * Returns the <code>Region</code> with the given ID. * * @param id a <code>String</code> value * @return a <code>Region</code> value */ public Region getRegion(final String id) { return regions.get(id); } /** * Returns the <code>Region</code> with the given name. * * @param id a <code>String</code> value * @return a <code>Region</code> value */ public Region getRegionByName(final String id) { for (Region region : regions.values()) { if (id.equals(region.getName())) { return region; } } return null; } /** * Describe <code>setRegion</code> method here. * * @param region a <code>Region</code> value */ public void setRegion(final Region region) { regions.put(region.getNameKey(), region); } /** * Is a tile in the map in a polar region? * * @param tile The <code>Tile</code> to examine. * @return True if the tile is in a polar region. */ public boolean isPolar(Tile tile) { return tile.getY() <= POLAR_HEIGHT || tile.getY() >= getHeight() - POLAR_HEIGHT - 1; } /** * Gets the list of tiles that might be claimable by a settlement. * We can not do a simple iteration of the rings because this * allows settlements to claim tiles across unclaimable gaps * (e.g. Aztecs owning tiles on nearby islands). So we have to * only allow tiles that are adjacent to a known connected tile. * * @param player The <code>Player</code> that intends to found a settlement. * @param centerTile The intended settlement center <code>Tile</code>. * @param radius The radius of the settlement. * @return A list of potentially claimable tiles. */ public List<Tile> getClaimableTiles(Player player, Tile centerTile, int radius) { List<Tile> tiles = new ArrayList<Tile>(); List<Tile> layer = new ArrayList<Tile>(); if (player.canClaimToFoundSettlement(centerTile)) { layer.add(centerTile); for (int r = 1; r <= radius; r++) { List<Tile> lastLayer = new ArrayList<Tile>(layer); tiles.addAll(layer); layer.clear(); for (Tile have : lastLayer) { for (Tile next : have.getSurroundingTiles(1)) { if (!tiles.contains(next) && player.canClaimForSettlement(next)) { layer.add(next); } } } } tiles.addAll(layer); } return tiles; } /** * Finds a shortest path between the given tiles. The tile at the * end will not be checked for validity. * * Using A* with the Manhatten distance as the heuristics. * * The data structure for the open list is a combined structure: using a * HashMap for membership tests and a PriorityQueue for getting the node * with the minimal f (cost+heuristics). This gives O(1) on membership * test and O(log N) for remove-best and insertions. * * The data structure for the closed list is simply a HashMap. * * @param unit The <code>Unit</code> to find the path for. * @param start The <code>Tile</code> in which the path starts from. * @param end The <code>Tile</code> at the end of the path. * @param carrier An optional carrier <code>Unit</code> that * currently holds the <code>unit</code>, or null if it is * not presently on a carrier. * @param costDecider An optional <code>CostDecider</code> for * determining the movement costs (uses default cost deciders * for the unit/s if not provided). * @return A <code>PathNode</code> for the first tile in the * path, or null if none found. * @exception IllegalArgumentException * If <code>start</code>, <code>end</code>, or <code>unit</code * are <code>null</code>, or start equals end. */ public PathNode findPath(final Unit unit, final Tile start, final Tile end, final Unit carrier, CostDecider costDecider) { if (unit == null) { throw new IllegalArgumentException("unit must not be null."); } if (start == null) { throw new IllegalArgumentException("start must not be null."); } if (end == null) { throw new IllegalArgumentException("end must not be null."); } if (start == end) { throw new IllegalArgumentException("start == end"); } // What unit starts the path? Unit currentUnit = (carrier != null) ? carrier : unit; final PathNode firstNode; if (currentUnit != null) { firstNode = new PathNode(start, 0, start.getDistanceTo(end), Direction.N, currentUnit.getMovesLeft(), 0); firstNode.setOnCarrier(carrier != null); } else { firstNode = new PathNode(start, 0, start.getDistanceTo(end), Direction.N, -1, -1); } final HashMap<String, PathNode> openList = new HashMap<String, PathNode>(); final HashMap<String, PathNode> closedList = new HashMap<String, PathNode>(); final PriorityQueue<PathNode> openListQueue = new PriorityQueue<PathNode>(1024, new Comparator<PathNode>() { public int compare(PathNode o, PathNode p) { return o.getF() - p.getF(); } }); openList.put(firstNode.getTile().getId(), firstNode); openListQueue.offer(firstNode); while (!openList.isEmpty()) { // Choose the node with the lowest f. PathNode currentNode = openListQueue.poll(); final Tile currentTile = currentNode.getTile(); openList.remove(currentTile.getId()); closedList.put(currentTile.getId(), currentNode); // Found the goal? if (currentTile == end) { while (currentNode.previous != null) { currentNode.previous.next = currentNode; currentNode = currentNode.previous; } return currentNode.next; } // Reset current unit to that of this node. currentUnit = (currentNode.isOnCarrier()) ? carrier : unit; // Only check further along a path (i.e. ignore initial // node) if it is possible to transit *through* it // (isProgress()). if (currentNode.previous != null) { Tile previousTile = currentNode.previous.getTile(); if (!currentUnit.getSimpleMoveType(previousTile, currentTile).isProgress()) { continue; } } // Collect the parameters for the current node. final int currentCost = currentNode.getCost(); final int currentMoves = currentNode.getMovesLeft(); final int currentTurns = currentNode.getTurns(); final boolean currentOnCarrier = currentNode.isOnCarrier(); // Try the tiles in each direction for (Direction direction : Direction.values()) { final Tile moveTile = currentTile.getNeighbourOrNull(direction); if (moveTile == null) continue; // If the new tile is the tile we just visited, skip // it. We can use == because PathNode.getTile() and // getNeighborOrNull both return references to the // actual Tile in tiles[][]. if (currentNode.previous != null && currentNode.previous.getTile() == moveTile) { continue; } if (closedList.containsKey(moveTile.getId())) { continue; } // Check for disembarkation on new tile, setting // moveUnit to the unit that would actually move. boolean moveOnCarrier = carrier != null && currentOnCarrier && (!moveTile.isLand() || (moveTile.getSettlement() != null && moveTile.getSettlement().getOwner() == currentUnit.getOwner())); Unit moveUnit = (moveOnCarrier) ? carrier : unit; int moveCost = currentCost; int moveMoves = (currentOnCarrier && !moveOnCarrier) ? moveUnit.getInitialMovesLeft() : currentMoves; int moveTurns = currentTurns; CostDecider moveDecider = (costDecider != null) ? costDecider : CostDeciders.defaultCostDeciderFor(moveUnit); // Update parameters for the new tile. int extraCost = moveDecider.getCost(moveUnit, currentTile, moveTile, moveMoves); if (extraCost == CostDecider.ILLEGAL_MOVE) { // Do not let the CostDecider (which may be // conservative) block the final destination if it // is still a legal move. if (moveTile == end && moveUnit.getSimpleMoveType(currentTile, moveTile) .isLegal()) { moveCost += moveUnit.getInitialMovesLeft(); moveMoves = 0; } else { continue; } } else { moveCost += extraCost; moveMoves = moveDecider.getMovesLeft(); if (moveDecider.isNewTurn()) moveTurns++; } // Is this an improvement? If not, ignore. PathNode successor = openList.get(moveTile.getId()); if (successor != null) { if (successor.getTurns() < moveTurns || (successor.getTurns() == moveTurns && successor.getCost() <= moveCost)) { continue; } openList.remove(successor.getTile().getId()); openListQueue.remove(successor); } // Queue new node with updated parameters. final int f = moveTurns * 100 + moveCost + moveTile.getDistanceTo(end); successor = new PathNode(moveTile, moveCost, f, direction, moveMoves, moveTurns); successor.previous = currentNode; successor.setOnCarrier(carrier != null && moveUnit == carrier); openList.put(moveTile.getId(), successor); openListQueue.offer(successor); } } return null; } /** * Finds the best path to <code>Europe</code>. * * @param unit The <code>Unit</code> that should be used to determine * whether or not a path is legal. * @param start The starting <code>Tile</code>. * @param costDecider An optional <code>CostDecider</code> * responsible for determining the path cost. * @return The path to Europe, or null if not found. * @see Europe */ public PathNode findPathToEurope(Unit unit, Tile start, CostDecider costDecider) { GoalDecider gd = new GoalDecider() { private PathNode goal = null; public PathNode getGoal() { return goal; } public boolean hasSubGoals() { return false; } public boolean check(Unit u, PathNode pathNode) { if (pathNode.getTile().canMoveToEurope()) { goal = pathNode; return true; } return false; } }; return search(unit, start, gd, costDecider, INFINITY, null); } /** * Finds the best path to <code>Europe</code> independently of any * unit. This method is meant to be executed by the server/AI * code, with complete knowledge of the map * * @param start The starting <code>Tile</code>. * @return The path to the target or <code>null</code> if no target can be * found. * @see Europe */ public PathNode findPathToEurope(Tile start) { final GoalDecider gd = new GoalDecider() { private PathNode goal = null; public PathNode getGoal() { return goal; } public boolean hasSubGoals() { return false; } public boolean check(Unit u, PathNode pathNode) { Tile t = pathNode.getTile(); if (t.canMoveToEurope()) { goal = pathNode; return true; } return false; } }; final CostDecider cd = new CostDecider() { public int getCost(Unit unit, Tile oldTile, Tile newTile, int movesLeft) { if (newTile.isLand()) { return ILLEGAL_MOVE; } else { return 1; } } public int getMovesLeft() { return 0; } public boolean isNewTurn() { return false; } }; return search(null, start, gd, cd, INFINITY, null); } /** * Finds a path to a goal determined by the given <code>GoalDecider</code>. * * A <code>GoalDecider</code> is typically defined inline to serve a * specific need. * * Using Dijkstra's algorithm with a closedList for marking the * visited nodes and using a PriorityQueue for getting the next * edge with the least cost. This implementation could be improved * by having the visited attribute stored on each Tile in order to * avoid both of the HashMaps currently being used to serve this * purpose. * * @param unit The <code>Unit</code> to find a path for. * @param startTile The <code>Tile</code> to start the search from. * @param gd The object responsible for determining whether a * given <code>PathNode</code> is a goal or not. * @param costDecider An optional <code>CostDecider</code> * responsible for determining the path cost. * @param maxTurns The maximum number of turns the given * <code>Unit</code> is allowed to move. This is the * maximum search range for a goal. * @param carrier The carrier the <code>unit</code> is currently * onboard or <code>null</code> if the <code>unit</code> is * either not onboard a carrier or should not use the * carrier while finding the path. * @return The path to a goal determined by the given * <code>GoalDecider</code>. */ public PathNode search(final Unit unit, final Tile startTile, final GoalDecider gd, final CostDecider costDecider, final int maxTurns, final Unit carrier) { if (startTile == null) { throw new IllegalArgumentException("startTile must not be null."); } // What unit starts the path? Unit currentUnit = (carrier != null) ? carrier : unit; final HashMap<String, PathNode> openList = new HashMap<String, PathNode>(); final HashMap<String, PathNode> closedList = new HashMap<String, PathNode>(); final PriorityQueue<PathNode> openListQueue = new PriorityQueue<PathNode>(1024, new Comparator<PathNode>() { public int compare(PathNode o, PathNode p) { return o.getCost() - p.getCost(); } }); final PathNode firstNode = new PathNode(startTile, 0, 0, Direction.N, (currentUnit != null) ? currentUnit.getMovesLeft() : -1, 0); firstNode.setOnCarrier(carrier != null); openList.put(startTile.getId(), firstNode); openListQueue.offer(firstNode); while (!openList.isEmpty()) { // Choose the node with the lowest cost. final PathNode currentNode = openListQueue.poll(); final Tile currentTile = currentNode.getTile(); openList.remove(currentTile.getId()); closedList.put(currentTile.getId(), currentNode); // Reset current unit to that of this node. currentUnit = (currentNode.isOnCarrier()) ? carrier : unit; // Check for simple success. if (gd.check(currentUnit, currentNode) && !gd.hasSubGoals()) { break; } // Stop if reached the turn limit. if (currentNode.getTurns() > maxTurns) { break; } // Only check further along a path (i.e. ignore initial // node) if it is possible to transit *through* it // (isProgress()). if (currentUnit != null && currentNode.previous != null) { Tile previousTile = currentNode.previous.getTile(); if (!currentUnit.getSimpleMoveType(previousTile, currentTile).isProgress()) { continue; } } // Collect the parameters for the current node. int currentCost = currentNode.getCost(); int currentMoves = currentNode.getMovesLeft(); int currentTurns = currentNode.getTurns(); boolean currentOnCarrier = currentNode.isOnCarrier(); // Try the tiles in each direction for (Direction direction : Direction.values()) { final Tile moveTile = currentTile.getNeighbourOrNull(direction); if (moveTile == null) continue; // If the new tile is the tile we just visited, skip // it. We can use == because PathNode.getTile() and // getNeighborOrNull both return references to the // actual Tile in tiles[][]. if (currentNode.previous != null && currentNode.previous.getTile() == moveTile) { continue; } if (closedList.containsKey(moveTile.getId())) { continue; } // Check for disembarkation on new tile, setting // moveUnit to the unit that would actually move. boolean moveOnCarrier = carrier != null && currentOnCarrier && (!moveTile.isLand() || (moveTile.getSettlement() != null && moveTile.getSettlement().getOwner() == currentUnit.getOwner())); Unit moveUnit = (moveOnCarrier) ? carrier : unit; int moveCost = currentCost; int moveMoves = (currentOnCarrier && !moveOnCarrier) ? moveUnit.getInitialMovesLeft() : currentMoves; int moveTurns = currentTurns; CostDecider moveDecider = (costDecider != null) ? costDecider : CostDeciders.defaultCostDeciderFor(moveUnit); // Update parameters for the new tile. int extraCost = moveDecider.getCost(moveUnit, currentTile, moveTile, moveMoves); if (extraCost == CostDecider.ILLEGAL_MOVE) continue; moveCost += extraCost; moveMoves = moveDecider.getMovesLeft(); if (moveDecider.isNewTurn()) moveTurns++; // Is this an improvement? If not, ignore. PathNode successor = openList.get(moveTile.getId()); if (successor != null) { if (successor.getTurns() < moveTurns || (successor.getTurns() == moveTurns && successor.getCost() <= moveCost)) { continue; } openList.remove(successor.getTile().getId()); openListQueue.remove(successor); } // Queue new node with updated parameters. final int f = moveTurns * 100 + moveCost; successor = new PathNode(moveTile, moveCost, f, direction, moveMoves, moveTurns); successor.previous = currentNode; successor.setOnCarrier(carrier != null && moveUnit == carrier); openList.put(moveTile.getId(), successor); openListQueue.offer(successor); } } PathNode bestTarget = gd.getGoal(); if (bestTarget != null) { while (bestTarget.previous != null) { bestTarget.previous.next = bestTarget; bestTarget = bestTarget.previous; } return bestTarget.next; } return null; } /** * Searches for land within the given radius. * * @param x * X-component of the position to search from. * @param y * Y-component of the position to search from. * @param distance * The radius that should be searched for land, given in number * of {@link Tile tiles}. * @return <code>true</code> if there is {@link Tile#isLand land} within * the given radius and <code>false</code> otherwise. */ public boolean isLandWithinDistance(int x, int y, int distance) { Iterator<Position> i = getCircleIterator(new Position(x, y), true, distance); while (i.hasNext()) { if (getTile(i.next()).isLand()) { return true; } } return false; } /** * Returns the Tile at a requested position. * * @param p * The position. * @return The Tile at the given position. */ public Tile getTile(Position p) { return getTile(p.getX(), p.getY()); } /** * Returns the Tile at position (x, y). 'x' specifies a column and 'y' * specifies a row. (0, 0) is the Tile at the top-left corner of the Map. * * @param x * The x-coordinate of the <code>Tile</code>. * @param y * The y-coordinate of the <code>Tile</code>. * @return The Tile at position (x, y) or <code>null</code> if the * position is invalid. */ public Tile getTile(int x, int y) { if (isValid(x, y)) { return tiles[x][y]; } else { return null; } } /** * Sets the given tile the the given coordinates. * * @param x * The x-coordinate of the <code>Tile</code>. * @param y * The y-coordinate of the <code>Tile</code>. * @param tile * The <code>Tile</code>. */ public void setTile(Tile tile, int x, int y) { tiles[x][y] = tile; } /** * Returns the width of this Map. * * @return The width of this Map. */ public int getWidth() { if (tiles == null) { return 0; } else { return tiles.length; } } /** * Returns the height of this Map. * * @return The height of this Map. */ public int getHeight() { if (tiles == null) { return 0; } else { return tiles[0].length; } } /** * Returns the direction a unit needs to move in * order to get from <code>t1</code> to <code>t2</code> * * @param t1 The tile to move from. * @param t2 The target tile if moving from <code>t1</code> * in the direction returned by this method. * @return The direction you need to move from <code>t1</code> * in order to reach <code>t2</code>, or null if the two * specified tiles are not neighbours. */ public Direction getDirection(Tile t1, Tile t2) { for (Direction d : Direction.values()) { if (t1.getNeighbourOrNull(d) == t2) { return d; } } return null; } /** * Gets an <code>Iterator</code> of every <code>Tile</code> on the map. * * @return the <code>Iterator</code>. */ public WholeMapIterator getWholeMapIterator() { return new WholeMapIterator(); } /** * Get an adjacent iterator. * * @param centerPosition * The center position to iterate around * @return Iterator */ public Iterator<Position> getAdjacentIterator(Position centerPosition) { return new AdjacentIterator(centerPosition); } /** * Get a border adjacent iterator. * * @param centerPosition * The center position to iterate around * @return Iterator */ public Iterator<Position> getBorderAdjacentIterator(Position centerPosition) { return new BorderAdjacentIterator(centerPosition); } /** * Get a flood fill iterator. * * @param centerPosition * The center position to iterate around * @return Iterator */ public Iterator<Position> getFloodFillIterator(Position centerPosition) { return new CircleIterator(centerPosition, true, INFINITY); } /** * Get a circle iterator. * * @param center * The center position to iterate around * @param isFilled * True to get all of the positions in the circle * @param radius * Radius of circle * @return Iterator */ public CircleIterator getCircleIterator(Position center, boolean isFilled, int radius) { return new CircleIterator(center, isFilled, radius); } /** * Checks whether a position is valid (within the map limits). * * @param position * The position * @return True if it is valid */ public boolean isValid(Position position) { return isValid(position.x, position.y, getWidth(), getHeight()); } /** * Checks whether a position is valid (within the map limits). * * @param x * X coordinate * @param y * Y coordinate * @return True if it is valid */ public boolean isValid(int x, int y) { return isValid(x, y, getWidth(), getHeight()); } /** * Checks whether a position is valid. * * @param position The position * @param width The width of the map. * @param height The height of the map. * @return <code>true</code> if the given position is * within the bounds of the map and <code>false</code> otherwise */ public static boolean isValid(Position position, int width, int height) { return isValid(position.x, position.y, width, height); } /** * Checks if the given position is valid. * * @param x The x-coordinate of the position. * @param y The y-coordinate of the position. * @param width The width of the map. * @param height The height of the map. * @return <code>true</code> if the given position is * within the bounds of the map and <code>false</code> otherwise */ public static boolean isValid(int x, int y, int width, int height) { return x >= 0 && x < width && y >= 0 && y < height; } /** * Represents a position on the Map. */ public static final class Position { public final int x, y; /** * Creates a new object with the given position. * * @param posX * The x-coordinate for this position. * @param posY * The y-coordinate for this position. */ public Position(int posX, int posY) { x = posX; y = posY; } /** * Returns the x-coordinate of this Position. * * @return The x-coordinate of this Position. */ public int getX() { return x; } /** * Returns the y-coordinate of this Position. * * @return The y-coordinate of this Position. */ public int getY() { return y; } /** * Compares the other Position based on the coordinates. * * @param other the reference object with which to compare. * @return true iff the coordinates match. */ @Override public boolean equals(Object other) { if (this == other) { return true; } else if (other == null) { return false; } else if (!(other instanceof Position)) { return false; } else { return x == ((Position) other).x && y == ((Position) other).y; } } /** * Returns a hash code value. The current implementation (which may * change at any time) works well as long as the maximum coordinates fit * in 16 bits. * * @return a hash code value for this object. */ @Override public int hashCode() { return x | (y << 16); } /** * Returns a string representation of the object. * * @return a string representation of the object. */ @Override public String toString() { return "(" + x + ", " + y + ")"; } /** * Gets the position adjacent to a given position, in a given * direction. * * @param direction The direction (N, NE, E, etc.) * @return Adjacent position */ public Position getAdjacent(Direction direction) { int x = this.x + ((this.y & 1) != 0 ? direction.getOddDX() : direction.getEvenDX()); int y = this.y + ((this.y & 1) != 0 ? direction.getOddDY() : direction.getEvenDY()); return new Position(x, y); } /** * Gets the distance in tiles between two map positions. With an isometric * map this is a non-trivial task. The formula below has been developed * largely through trial and error. It should cover all cases, but I * wouldn't bet my life on it. * * @param position * The second position. * @return Distance */ public int getDistance(Position position) { int ay = getY(); int by = position.getY(); int r = position.getX() - getX() - (ay - by) / 2; if (by > ay && ay % 2 == 0 && by % 2 != 0) { r++; } else if (by < ay && ay % 2 != 0 && by % 2 == 0) { r--; } return Math.max(Math.abs(ay - by + r), Math.abs(r)); } } /** * Base class for internal iterators. */ private abstract class MapIterator implements Iterator<Position> { protected Direction[] directions = Direction.values(); /** * Get the next position as a position rather as an object. * * @return position. * @throws NoSuchElementException * if iterator is exhausted. */ public abstract Position nextPosition() throws NoSuchElementException; /** * Returns the next element in the iteration. * * @return the next element in the iteration. * @exception NoSuchElementException * iteration has no more elements. */ public Position next() { return nextPosition(); } /** * Removes from the underlying collection the last element returned by * the iterator (optional operation). * * @exception UnsupportedOperationException * no matter what. */ public void remove() { throw new UnsupportedOperationException(); } } public final class WholeMapIterator extends MapIterator { private int x; private int y; /** * Default constructor. */ public WholeMapIterator() { x = 0; y = 0; } /** * Determine if the iterator has another position in it. * * @return True of there is another position */ public boolean hasNext() { return y < getHeight(); } /** * Obtain the next position to iterate over. * * @return Next position * @throws java.util.NoSuchElementException * if last position already returned */ @Override public Position nextPosition() throws NoSuchElementException { if (y < getHeight()) { Position newPosition = new Position(x, y); x++; if (x == getWidth()) { x = 0; y++; } return newPosition; } throw new NoSuchElementException("Iterator exhausted"); } } private final class AdjacentIterator extends MapIterator { // The starting tile position private Position basePosition; // Index into the list of adjacent tiles private int x = 0; /** * The constructor to use. * * @param basePosition * The position around which to iterate */ public AdjacentIterator(Position basePosition) { this.basePosition = basePosition; } /** * Determine if the iterator has another position in it. * * @return True of there is another position */ public boolean hasNext() { for (int i = x; i < 8; i++) { Position newPosition = basePosition.getAdjacent(directions[i]); if (isValid(newPosition)) return true; } return false; } /** * Obtain the next position to iterate over. * * @return Next position * @throws NoSuchElementException * if last position already returned */ @Override public Position nextPosition() throws NoSuchElementException { for (int i = x; i < 8; i++) { Position newPosition = basePosition.getAdjacent(directions[i]); if (isValid(newPosition)) { x = i + 1; return newPosition; } } throw new NoSuchElementException("Iterator exhausted"); } } /** * An interator returning positions in a spiral starting at a given center * tile. The center tile is never included in the positions returned, and * all returned positions are valid. * * @see Map.Position */ public final class CircleIterator extends MapIterator { private int radius; private int currentRadius; private Position nextPosition = null; // The current position in the circle with the current radius: private int n; /** * The constructor to use. * * @param center * The center of the circle * @param isFilled * True to get all of the positions within the circle * @param radius * The radius of the circle */ public CircleIterator(Position center, boolean isFilled, int radius) { this.radius = radius; if (center == null) { throw new IllegalArgumentException("center must not be null."); } n = 0; if (isFilled || radius == 1) { nextPosition = center.getAdjacent(Direction.NE); currentRadius = 1; } else { currentRadius = radius; nextPosition = center; for (int i = 1; i < radius; i++) { nextPosition = nextPosition.getAdjacent(Direction.N); } nextPosition = nextPosition.getAdjacent(Direction.NE); } if (!isValid(nextPosition)) { determineNextPosition(); } } /** * Returns the current radius of the circle. * * @return The distance from the center tile this * <code>CircleIterator</code> was initialized with. */ public int getCurrentRadius() { return currentRadius; } /** * Finds the next position. */ private void determineNextPosition() { boolean positionReturned = (n != 0); do { n++; final int width = currentRadius * 2; if (n >= width * 4) { currentRadius++; if (currentRadius > radius) { nextPosition = null; } else if (!positionReturned) { nextPosition = null; } else { n = 0; positionReturned = false; nextPosition = nextPosition.getAdjacent(Direction.NE); } } else { int i = n / width; Direction direction; switch (i) { case 0: direction = Direction.SE; break; case 1: direction = Direction.SW; break; case 2: direction = Direction.NW; break; case 3: direction = Direction.NE; break; default: throw new IllegalStateException("i=" + i + ", n=" + n + ", width=" + width); } nextPosition = nextPosition.getAdjacent(direction); } } while (nextPosition != null && !isValid(nextPosition)); } /** * Determine if the iterator has another position in it. * * @return <code>true</code> of there is another position and * <code>false</code> otherwise. */ public boolean hasNext() { return nextPosition != null; } /** * Obtains the next position. * * @return The next position. This position is guaranteed to be * {@link Map#isValid(net.sf.freecol.common.model.Map.Position) valid}. */ @Override public Position nextPosition() { if (nextPosition != null) { final Position p = nextPosition; determineNextPosition(); return p; } else { return null; } } } /** * Make the map usable as a parameter in the for-loop. * * Returns all Tiles based on the order of the WholeMapIterator. * * @return An Iterable that can be used to get an iterator for all tiles of the map. */ public Iterable<Tile> getAllTiles() { return new Iterable<Tile>(){ public Iterator<Tile> iterator(){ final WholeMapIterator m = getWholeMapIterator(); return new Iterator<Tile>(){ public boolean hasNext() { return m.hasNext(); } public Tile next() { return getTile(m.next()); } public void remove() { m.remove(); } }; } }; } private final class BorderAdjacentIterator extends MapIterator { // The starting tile position private Position basePosition; // Index into the list of adjacent tiles private int index; /** * The constructor to use. * * @param basePosition * The position around which to iterate */ public BorderAdjacentIterator(Position basePosition) { this.basePosition = basePosition; index = 1; } /** * Determine if the iterator has another position in it. * * @return True of there is another position */ public boolean hasNext() { for (int i = index; i < 8; i += 2) { Position newPosition = basePosition.getAdjacent(directions[i]); if (isValid(newPosition)) return true; } return false; } /** * Obtain the next position to iterate over. * * @return Next position * @throws NoSuchElementException * if last position already returned */ @Override public Position nextPosition() throws NoSuchElementException { for (int i = index; i < 8; i += 2) { Position newPosition = basePosition.getAdjacent(directions[i]); if (isValid(newPosition)) { index = i + 2; return newPosition; } } throw new NoSuchElementException("Iterator exhausted"); } } // Location interface /** * Returns <code>null</code>. * * @return <code>null</code> */ public Tile getTile() { return null; } /** * Returns the name of this location. * * @return The name of this location. */ public StringTemplate getLocationName() { return StringTemplate.key("NewWorld"); } /** * Returns the name of this location for a particular player. * * @param player The <code>Player</code> to return the name for. * @return The name of this location. */ public StringTemplate getLocationNameFor(Player player) { String name = player.getNewLandName(); if (name == null) { return getLocationName(); } else { return StringTemplate.name(name); } } /** * Adds a <code>Locatable</code> to this Location. It the given * Locatable is a Unit, its location is set to its entry location, * otherwise nothing happens. * * @param locatable * The <code>Locatable</code> to add to this Location. */ public boolean add(Locatable locatable) { if (locatable instanceof Unit) { Unit unit = (Unit) locatable; unit.setLocation(unit.getEntryLocation()); return true; } return false; } /** * Removes a <code>Locatable</code> from this Location. * * @param locatable * The <code>Locatable</code> to remove from this Location. */ public boolean remove(Locatable locatable) { if (locatable instanceof Unit) { Tile tile = ((Unit) locatable).getTile(); if (tile != null) { return tile.remove(locatable); } } return false; } /** * Checks if this <code>Location</code> contains the specified * <code>Locatable</code>. * * @param locatable * The <code>Locatable</code> to test the presence of. * @return * <ul> * <li><i>true</i> if the specified <code>Locatable</code> is * on this <code>Location</code> and * <li><i>false</i> otherwise. * </ul> */ public boolean contains(Locatable locatable) { if (locatable.getLocation() == null) { return false; } else if (locatable.getLocation().getTile() == null) { return false; } else { return true; } } /** * Checks whether or not the specified locatable may be added to this * <code>Location</code>. * * @param locatable * The <code>Locatable</code> to add. * @return The result. */ public boolean canAdd(Locatable locatable) { return (locatable instanceof Unit); } /** * Returns <code>-1</code> * * @return <code>-1</code> */ public int getUnitCount() { return -1; } /** * Returns an empty list. * * @return an empty list */ public List<Unit> getUnitList() { return Collections.emptyList(); } /** * Returns an <code>Iterator</code> for an empty list. * * @return The <code>Iterator</code>. */ public Iterator<Unit> getUnitIterator() { return getUnitList().iterator(); } /** * Gets the <code>GoodsContainer</code> this <code>Location</code> use * for storing it's goods. * * @return The <code>GoodsContainer</code> or <code>null</code> if the * <code>Location</code> cannot store any goods. */ public GoodsContainer getGoodsContainer() { return null; } /** * Returns <code>null</code>. * * @return <code>null</code> */ public Settlement getSettlement() { return null; } /** * Returns <code>null</code>. * * @return <code>null</code> */ public Colony getColony() { return null; } /** * This method writes an XML-representation of this object to the given * stream. * * <br> * <br> * * Only attributes visible to the given <code>Player</code> will be added * to that representation if <code>showAll</code> is set to * <code>false</code>. * * @param out * The target stream. * @param player * The <code>Player</code> this XML-representation should be * made for, or <code>null</code> if * <code>showAll == true</code>. * @param showAll * Only attributes visible to <code>player</code> will be added * to the representation if <code>showAll</code> is set to * <i>false</i>. * @param toSavedGame * If <code>true</code> then information that is only needed * when saving a game is added. * @throws XMLStreamException * if there are any problems writing to the stream. */ @Override protected void toXMLImpl(XMLStreamWriter out, Player player, boolean showAll, boolean toSavedGame) throws XMLStreamException { out.writeStartElement(getXMLElementTagName()); writeAttributes(out, player, showAll, toSavedGame); writeChildren(out, player, showAll, toSavedGame); out.writeEndElement(); } protected void writeAttributes(XMLStreamWriter out, Player player, boolean showAll, boolean toSavedGame) throws XMLStreamException { out.writeAttribute(ID_ATTRIBUTE, getId()); out.writeAttribute("width", Integer.toString(getWidth())); out.writeAttribute("height", Integer.toString(getHeight())); out.writeAttribute("layer", layer.toString()); out.writeAttribute("minimumLatitude", Integer.toString(minimumLatitude)); out.writeAttribute("maximumLatitude", Integer.toString(maximumLatitude)); } protected void writeChildren(XMLStreamWriter out, Player player, boolean showAll, boolean toSavedGame) throws XMLStreamException { for (Region region : regions.values()) { region.toXML(out); } for (Tile tile: getAllTiles()) { if (showAll || toSavedGame || player.hasExplored(tile)) { tile.toXML(out, player, showAll, toSavedGame); } else { tile.toXMLMinimal(out); } } } /** * Initialize this object from an XML-representation of this object. * * @param in * The input stream with the XML. */ @Override protected void readFromXMLImpl(XMLStreamReader in) throws XMLStreamException { setId(in.getAttributeValue(null, ID_ATTRIBUTE)); setLayer(Layer.valueOf(getAttribute(in, "layer", "ALL"))); if (tiles == null) { int width = Integer.parseInt(in.getAttributeValue(null, "width")); int height = Integer.parseInt(in.getAttributeValue(null, "height")); tiles = new Tile[width][height]; } minimumLatitude = getAttribute(in, "minimumLatitude", -90); maximumLatitude = getAttribute(in, "maximumLatitude", 90); calculateLatitudePerRow(); while (in.nextTag() != XMLStreamConstants.END_ELEMENT) { if (in.getLocalName().equals(Tile.getXMLElementTagName())) { Tile t = updateFreeColGameObject(in, Tile.class); setTile(t, t.getX(), t.getY()); } else if (in.getLocalName().equals(Region.getXMLElementTagName())) { setRegion(updateFreeColGameObject(in, Region.class)); } else { logger.warning("Unknown tag: " + in.getLocalName() + " loading map"); in.nextTag(); } } } /** * Returns the tag name of the root element representing this object. * * @return "map". */ public static String getXMLElementTagName() { return "map"; } }