diff --git a/modules/clear/src/clear/ClearSimulator.java b/modules/clear/src/clear/ClearSimulator.java new file mode 100644 index 0000000000000000000000000000000000000000..34687f47c77020fb8f01335b7f0ccaa1ed11dd06 --- /dev/null +++ b/modules/clear/src/clear/ClearSimulator.java @@ -0,0 +1,377 @@ +package clear; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import rescuecore2.log.Logger; +import rescuecore2.messages.Command; +import rescuecore2.messages.control.KSCommands; +import rescuecore2.misc.geometry.GeometryTools2D; +import rescuecore2.misc.geometry.Line2D; +import rescuecore2.misc.geometry.Point2D; +import rescuecore2.standard.components.StandardSimulator; +import rescuecore2.standard.entities.Area; +import rescuecore2.standard.entities.Blockade; +import rescuecore2.standard.entities.PoliceForce; +import rescuecore2.standard.entities.Road; +import rescuecore2.standard.entities.StandardEntity; +import rescuecore2.standard.messages.AKClear; +import rescuecore2.standard.messages.AKClearArea; +import rescuecore2.worldmodel.ChangeSet; +import rescuecore2.worldmodel.EntityID; + +/** + * The area model clear simulator. This simulator processes AKClear messages. + */ +public class ClearSimulator extends StandardSimulator { + + private static final String SIMULATOR_NAME = "Area Model Clear Simulator"; + + private static final String REPAIR_RATE_KEY = "clear.repair.rate"; + private static final String REPAIR_RAD_KEY = "clear.repair.rad"; + private static final String REPAIR_DISTANCE_KEY = "clear.repair.distance"; + + // Converts square mm to square m. + private static final double REPAIR_COST_FACTOR = 0.000001; + + private int repairRate; + private int repairRadius; + private int repairDistance; + + @Override + public String getName() { + return SIMULATOR_NAME; + } + + + @Override + protected void postConnect() { + this.repairRate = config.getIntValue(REPAIR_RATE_KEY); + this.repairRadius = config.getIntValue(REPAIR_RAD_KEY); + this.repairDistance = config.getIntValue(REPAIR_DISTANCE_KEY); + } + + + @Override + protected void processCommands(KSCommands c, ChangeSet changes) { + long start = System.currentTimeMillis(); + int time = c.getTime(); + Logger.info("Timestep " + time); + Map partiallyCleared = new HashMap<>(); + Set cleared = new HashSet<>(); + for (Command command : c.getCommands()) { + if ((command instanceof AKClear clear) && isValid(clear, cleared)) { + Logger.debug("Processing " + clear); + EntityID blockadeID = clear.getTarget(); + Blockade blockade = (Blockade) model.getEntity(blockadeID); + Area area = (Area) model.getEntity(blockade.getPosition()); + int cost = blockade.getRepairCost(); + Logger.debug("Blockade repair cost: " + cost); + Logger.debug("Blockade repair rate: " + this.repairRate); + if (this.repairRate >= cost) { + // Remove the blockade entirely + List ids = new ArrayList<>(area.getBlockades()); + ids.remove(blockadeID); + area.setBlockades(ids); + model.removeEntity(blockadeID); + changes.addChange(area, area.getBlockadesProperty()); + changes.entityDeleted(blockadeID); + partiallyCleared.remove(blockade); + cleared.add(blockadeID); + Logger.debug("Cleared " + blockade); + } else { + // Update the repair cost + if (!partiallyCleared.containsKey(blockade)) { + partiallyCleared.put(blockade, cost); + } + cost -= this.repairRate; + blockade.setRepairCost(cost); + changes.addChange(blockade, blockade.getRepairCostProperty()); + } + } else if ((command instanceof AKClearArea clearArea) + && (isValid(clearArea))) { + processClearArea(clearArea, changes); + Logger.debug("Processing " + clearArea); + } + } + // Shrink partially cleared blockades + for (Map.Entry next : partiallyCleared.entrySet()) { + Blockade b = next.getKey(); + double original = next.getValue(); + double current = b.getRepairCost(); + // d is the new size relative to the old size + double d = current / original; + Logger.debug("Partially cleared " + b); + Logger.debug("Original repair cost: " + original); + Logger.debug("New repair cost: " + current); + Logger.debug("Proportion left: " + d); + int[] apexes = b.getApexes(); + double cx = b.getX(); + double cy = b.getY(); + // Move each apex towards the centre + for (int i = 0; i < apexes.length; i += 2) { + double x = apexes[i]; + double y = apexes[i + 1]; + double dx = x - cx; + double dy = y - cy; + // Shift both x and y so they are now d * dx from the centre + double newX = cx + (dx * d); + double newY = cy + (dy * d); + apexes[i] = (int) newX; + apexes[i + 1] = (int) newY; + } + b.setApexes(apexes); + changes.addChange(b, b.getApexesProperty()); + } + long end = System.currentTimeMillis(); + Logger.info("Timestep " + time + " took " + (end - start) + " ms"); + } + + + private void processClearArea(AKClearArea clear, ChangeSet changes) { + PoliceForce agent = (PoliceForce) model.getEntity(clear.getAgentID()); + int targetX = clear.getDestinationX(); + int targetY = clear.getDestinationY(); + + int length = this.repairDistance; + + Map blockades = new HashMap<>(); + for (StandardEntity entity : model.getObjectsInRange(agent.getX(), + agent.getY(), length)) { + if ((entity instanceof Area area) && (area.isBlockadesDefined())) { + for (EntityID blockadeID : area.getBlockades()) { + Blockade blockade = (Blockade) model.getEntity(blockadeID); + if (blockade != null) { + if (blockade.getShape() == null) { + Logger.debug("Blockade Shape is null"); + } + blockades.put(blockade, + new java.awt.geom.Area(blockade.getShape())); + } + } + } + } + + int counter = 0; + int min = 0; + int max = 2 * length; + while (true) { + counter++; + length = (min + max) / 2; + java.awt.geom.Area area = Geometry.getClearArea(agent, targetX, targetY, + length, this.repairRadius); + + double firstSurface = Geometry.surface(area); + for (java.awt.geom.Area blockade : blockades.values()) + area.subtract(blockade); + double surface = Geometry.surface(area); + double clearedSurface = firstSurface - surface; + + if ((clearedSurface * REPAIR_COST_FACTOR) > this.repairRate) { + max = length; + } else if ((counter != 1) && (counter < 15) && ((max - min) > 5)) { + min = length; + } else { + break; + } + } + + java.awt.geom.Area area = Geometry.getClearArea(agent, targetX, targetY, + length, this.repairRadius); + for (Map.Entry entry : blockades.entrySet()) { + Blockade blockade = entry.getKey(); + java.awt.geom.Area blockadeArea = entry.getValue(); + Road road = (Road) model.getEntity(blockade.getPosition()); + double firstSurface = Geometry.surface(blockadeArea); + blockadeArea.subtract(area); + double surface = Geometry.surface(blockadeArea); + if (surface < firstSurface) { + changes.addChange(blockade, blockade.getApexesProperty()); + List areas = Geometry.getAreas(blockadeArea); + if (areas.size() == 1) { + Blockade backupBlockade = blockade; + blockade = updateBlockadeApexes(blockade, areas.get(0)); + if (blockade == null) { + blockade = backupBlockade; + areas.clear(); + } else { + changes.addChange(blockade, blockade.getApexesProperty()); + changes.addChange(blockade, blockade.getXProperty()); + changes.addChange(blockade, blockade.getYProperty()); + changes.addChange(blockade, blockade.getRepairCostProperty()); + } + } + if (areas.size() != 1) { + try { + List newIDs = requestNewEntityIDs(areas.size()); + Iterator it = newIDs.iterator(); + List newBlockades = new ArrayList<>(); + if (!areas.isEmpty()) + Logger.debug("Creating new blockade objects for " + + blockade.getID().getValue() + " " + areas.size()); + for (int[] apexes : areas) { + EntityID id = it.next(); + Blockade b = makeBlockade(id, apexes, road.getID()); + if (b != null) + newBlockades.add(b); + } + List existing = road.getBlockades(); + List ids = new ArrayList<>(); + if (existing != null) + ids.addAll(existing); + for (Blockade b : newBlockades) { + ids.add(b.getID()); + } + ids.remove(blockade.getID()); + road.setBlockades(ids); + changes.addAll(newBlockades); + + model.removeEntity(blockade.getID()); + changes.addChange(road, road.getBlockadesProperty()); + changes.entityDeleted(blockade.getID()); + } catch (InterruptedException e) { + Logger.error("Interrupted while requesting IDs"); + } + } + } + } + } + + + private Blockade updateBlockadeApexes(Blockade blockade, int[] apexes) { + List points = GeometryTools2D.vertexArrayToPoints(apexes); + if (points.size() >= 2) { + Point2D centroid = GeometryTools2D.computeCentroid(points); + blockade.setApexes(apexes); + blockade.setX((int) centroid.getX()); + blockade.setY((int) centroid.getY()); + int cost = (int) (GeometryTools2D.computeArea(points) + * REPAIR_COST_FACTOR); + if (cost != 0) { + blockade.setRepairCost(cost); + return blockade; + } + } + + return null; + } + + + private Blockade makeBlockade(EntityID id, int[] apexes, EntityID roadID) { + Blockade blockade = new Blockade(id); + blockade.setPosition(roadID); + return updateBlockadeApexes(blockade, apexes); + } + + + private boolean isValid(AKClear clear, Set cleared) { + // Check Target + StandardEntity target = model.getEntity(clear.getTarget()); + if (target == null) { + Logger + .info("Rejecting clear command " + clear + ": target does not exist"); + return false; + } else if (cleared.contains(clear.getTarget())) { + Logger.info("Ignoring clear command " + clear + + ": target already cleared in this timestep"); + return false; + } else if (!(target instanceof Blockade)) { + Logger.info( + "Rejecting clear command " + clear + ": target is not a blockade"); + return false; + } + + // Check Agent + StandardEntity agent = model.getEntity(clear.getAgentID()); + if (agent == null) { + Logger + .info("Rejecting clear command " + clear + ": agent does not exist"); + return false; + } else if (!(agent instanceof PoliceForce)) { + Logger.info( + "Rejecting clear command " + clear + ": agent is not a PoliceForce"); + return false; + } + + // Check PoliceForce + PoliceForce police = (PoliceForce) agent; + StandardEntity agentPosition = police.getPosition(model); + if (agentPosition == null) { + Logger.info( + "Rejecting clear command " + clear + ": could not locate the agent"); + return false; + } else if (!police.isHPDefined() || police.getHP() <= 0) { + Logger.info("Rejecting clear command " + clear + ": agent is dead"); + return false; + } else if (police.isBuriednessDefined() && police.getBuriedness() > 0) { + Logger.info("Rejecting clear command " + clear + ": agent is buried"); + return false; + } + + // Check Blockade + Blockade targetBlockade = (Blockade) target; + if (!targetBlockade.isPositionDefined()) { + Logger.info( + "Rejecting clear command " + clear + ": blockade position undefined"); + return false; + } else if (!targetBlockade.isRepairCostDefined()) { + Logger.info( + "Rejecting clear command " + clear + ": blockade has no repair cost"); + return false; + } + + // Check Any Blockade to Clear + Point2D agentLocation = new Point2D(police.getX(), police.getY()); + double bestDistance = Double.MAX_VALUE; + for (Line2D line : GeometryTools2D.pointsToLines( + GeometryTools2D.vertexArrayToPoints(targetBlockade.getApexes()), + true)) { + Point2D closest = GeometryTools2D.getClosestPointOnSegment(line, + agentLocation); + double distance = GeometryTools2D.getDistance(agentLocation, closest); + if (distance < this.repairDistance) { + return true; + } else if (bestDistance > distance) { + bestDistance = distance; + } + } + Logger.info("Rejecting clear command " + clear + + ": agent is not adjacent to a target: closest blockade is " + + bestDistance); + return false; + } + + + private boolean isValid(AKClearArea clear) { + StandardEntity agent = model.getEntity(clear.getAgentID()); + if (agent == null) { + Logger + .info("Rejecting clear command " + clear + ": agent does not exist"); + return false; + } else if (!(agent instanceof PoliceForce)) { + Logger.info("Rejecting clear command " + clear + + ": agent is not a police officer"); + return false; + } + + // Check PoliceForce + PoliceForce police = (PoliceForce) agent; + StandardEntity agentPosition = police.getPosition(model); + if (!(agentPosition instanceof Area)) { + Logger.info( + "Rejecting clear command " + clear + " : could not locate agent"); + return false; + } else if (!police.isHPDefined() || police.getHP() <= 0) { + Logger.info("Rejecting clear command " + clear + " : agent is dead"); + return false; + } else if (police.isBuriednessDefined() && police.getBuriedness() > 0) { + Logger.info("Rejecting clear command " + clear + " : agent is buried"); + return false; + } + return true; + } +} \ No newline at end of file diff --git a/modules/clear/src/clear/Geometry.java b/modules/clear/src/clear/Geometry.java new file mode 100644 index 0000000000000000000000000000000000000000..69953e337eaa8baef2072f343d15f8cd45926e2b --- /dev/null +++ b/modules/clear/src/clear/Geometry.java @@ -0,0 +1,99 @@ +package clear; + +import java.awt.Polygon; +import java.awt.geom.Area; +import java.awt.geom.PathIterator; +import java.util.ArrayList; +import java.util.List; +import rescuecore2.misc.geometry.Line2D; +import rescuecore2.misc.geometry.Vector2D; +import rescuecore2.standard.entities.Human; + +public class Geometry { + + public static Area getClearArea(Human agent, int targetX, int targetY, + int clearLength, int clearRad) { + Vector2D agentToTarget = new Vector2D(targetX - agent.getX(), + targetY - agent.getY()); + + if (agentToTarget.getLength() > clearLength) + agentToTarget = agentToTarget.normalised().scale(clearLength); + agentToTarget = agentToTarget.normalised() + .scale(agentToTarget.getLength() + 510); + + Vector2D backAgent = (new Vector2D(agent.getX(), agent.getY())) + .add(agentToTarget.normalised().scale(-510)); + Line2D line = new Line2D(backAgent.getX(), backAgent.getY(), + agentToTarget.getX(), agentToTarget.getY()); + + Vector2D dir = agentToTarget.normalised().scale(clearRad); + Vector2D perpend1 = new Vector2D(-dir.getY(), dir.getX()); + Vector2D perpend2 = new Vector2D(dir.getY(), -dir.getX()); + + rescuecore2.misc.geometry.Point2D points[] = new rescuecore2.misc.geometry.Point2D[] { + line.getOrigin().plus(perpend1), line.getEndPoint().plus(perpend1), + line.getEndPoint().plus(perpend2), line.getOrigin().plus(perpend2)}; + int[] xPoints = new int[points.length]; + int[] yPoints = new int[points.length]; + for (int i = 0; i < points.length; i++) { + xPoints[i] = (int) points[i].getX(); + yPoints[i] = (int) points[i].getY(); + } + return new Area(new Polygon(xPoints, yPoints, points.length)); + } + + + public static double surface(Area area) { + PathIterator iter = area.getPathIterator(null); + + double sum_all = 0; + while (!iter.isDone()) { + List points = new ArrayList(); + while (!iter.isDone()) { + double point[] = new double[2]; + int type = iter.currentSegment(point); + iter.next(); + if (type == PathIterator.SEG_CLOSE) { + if (points.size() > 0) + points.add(points.get(0)); + break; + } + points.add(point); + } + + double sum = 0; + for (int i = 0; i < points.size() - 1; i++) + sum += points.get(i)[0] * points.get(i + 1)[1] + - points.get(i)[1] * points.get(i + 1)[0]; + + sum_all += Math.abs(sum) / 2; + } + + return sum_all; + } + + + public static List getAreas(Area area) { + PathIterator iter = area.getPathIterator(null); + List areas = new ArrayList(); + ArrayList list = new ArrayList(); + while (!iter.isDone()) { + double point[] = new double[2]; // x, y + int type = iter.currentSegment(point); + if (type == PathIterator.SEG_CLOSE) { + if (list.size() > 0) { + int[] newArea = new int[list.size()]; + for (int i = 0; i < list.size(); i++) + newArea[i] = list.get(i); + areas.add(newArea); + list = new ArrayList(); + } + } else { + list.add((int) point[0]); + list.add((int) point[1]); + } + iter.next(); + } + return areas; + } +} \ No newline at end of file diff --git a/modules/collapse/src/collapse/CSBuilding.java b/modules/collapse/src/collapse/CSBuilding.java new file mode 100644 index 0000000000000000000000000000000000000000..65ef6534b8d27dd305ae7f0c95dfe6a91a07cf60 --- /dev/null +++ b/modules/collapse/src/collapse/CSBuilding.java @@ -0,0 +1,89 @@ +package collapse; + +import rescuecore2.standard.entities.Building; + +/** + * Collapse Simulator Building (CSBuilding) is a wrapper for the Standard + * Building class that contains extra variables created, updated and used by the + * Collapse Simulator only. This class is created in order to prevented + * unnecessary changes to the Standard Building class. + * + * @author Salim + * + */ +public class CSBuilding { + /** + * The reference to the real building class + */ + private final Building real; + + /** + * Collapse Ratio shows the percent that the building has been collapsed so + * far. + */ + private float collapsedRatio = 0; + /** + * This shows whether the building has fire damage in the last cycle or not + */ + private boolean hasFireDamage = false; + + public CSBuilding(Building building) { + real = building; + } + + /** + * Returns the building's collapse ratio + * + * @return + */ + public float getCollapsedRatio() { + return collapsedRatio; + } + + /** + * Changes the collapse ratio of the building to the input ratio + * + * @param collapsedRatio + * is a float + */ + + public void setCollapsedRatio(float collapsedRatio) { + this.collapsedRatio = collapsedRatio; + } + + /** + * Adds the input ratio to the building's collapse ratio + * + * @param ratio + * is a float that represents the increased value of the collapse + * ratio + */ + public void increaseCollapseRatio(float ratio) { + setCollapsedRatio(getCollapsedRatio() + ratio); + + } + + public Building getReal() { + return real; + } + + /** + * Returns the extent that is still possible to collapse. + * + * @return a float representing the extent + */ + public double getRemainingToCollapse(double floorHeight) { + return floorHeight * real.getFloors() * (1 - getCollapsedRatio()); + } + + public boolean hasFireDamage() { + return false; + } + + public void setHasFireDamage(boolean hasFireDamage) { + this.hasFireDamage = hasFireDamage; + } + public double getTotalCollapse(double floorHeight){ + return floorHeight*real.getFloors(); + } +} diff --git a/modules/collapse/src/collapse/CollapseSimulator.java b/modules/collapse/src/collapse/CollapseSimulator.java new file mode 100755 index 0000000000000000000000000000000000000000..3ce6fa189bd8526692032d5d61be7b8bbf80698c --- /dev/null +++ b/modules/collapse/src/collapse/CollapseSimulator.java @@ -0,0 +1,864 @@ +package collapse; + +import rescuecore2.config.Config; +import rescuecore2.messages.Message; +import rescuecore2.messages.control.KSAfterShocksInfo; +import rescuecore2.messages.control.KSCommands; +import rescuecore2.worldmodel.ChangeSet; +import rescuecore2.worldmodel.EntityID; +import rescuecore2.worldmodel.WorldModelListener; +import rescuecore2.worldmodel.WorldModel; +import rescuecore2.misc.geometry.Point2D; +import rescuecore2.misc.geometry.Line2D; +import rescuecore2.misc.geometry.Vector2D; +import rescuecore2.misc.geometry.GeometryTools2D; +import rescuecore2.misc.collections.LazyMap; +import rescuecore2.log.Logger; +import rescuecore2.GUIComponent; + +import rescuecore2.standard.components.StandardSimulator; +import rescuecore2.standard.entities.StandardEntity; +import rescuecore2.standard.entities.StandardEntityURN; +import rescuecore2.standard.entities.StandardEntityConstants; +import rescuecore2.standard.entities.Building; +import rescuecore2.standard.entities.Road; +import rescuecore2.standard.entities.Edge; +import rescuecore2.standard.entities.Blockade; +import rescuecore2.standard.entities.StandardWorldModel; + +import org.uncommons.maths.random.GaussianGenerator; +import org.uncommons.maths.random.ContinuousUniformGenerator; +import org.uncommons.maths.number.NumberGenerator; +import org.uncommons.maths.Maths; + +import java.util.Map; +import java.util.HashMap; +import java.util.HashSet; +import java.util.EnumMap; +import java.util.Iterator; +import java.util.List; +import java.util.ArrayList; +import java.util.Collection; + +import java.awt.geom.Path2D; +import java.awt.geom.Ellipse2D; +import java.awt.geom.PathIterator; + +import javax.swing.JComponent; + +/** + * A simple collapse simulator. + */ +public class CollapseSimulator extends StandardSimulator implements + GUIComponent { + private static final String CONFIG_PREFIX = "collapse."; + private static final String DESTROYED_SUFFIX = ".p-destroyed"; + private static final String SEVERE_SUFFIX = ".p-severe"; + private static final String MODERATE_SUFFIX = ".p-moderate"; + private static final String SLIGHT_SUFFIX = ".p-slight"; + private static final String NONE_SUFFIX = ".p-none"; + + private static final String DESTROYED_MEAN_SUFFIX = "destroyed.mean"; + private static final String DESTROYED_SD_SUFFIX = "destroyed.sd"; + private static final String SEVERE_MEAN_SUFFIX = "severe.mean"; + private static final String SEVERE_SD_SUFFIX = "severe.sd"; + private static final String MODERATE_MEAN_SUFFIX = "moderate.mean"; + private static final String MODERATE_SD_SUFFIX = "moderate.sd"; + private static final String SLIGHT_MEAN_SUFFIX = "slight.mean"; + private static final String SLIGHT_SD_SUFFIX = "slight.sd"; + + private static final String BLOCK_KEY = "collapse.create-road-blockages"; + + private static final String FLOOR_HEIGHT_KEY = "collapse.floor-height"; + private static final String WALL_COLLAPSE_EXTENT_MIN_KEY = "collapse.wall-extent.min"; + private static final String WALL_COLLAPSE_EXTENT_MAX_KEY = "collapse.wall-extent.max"; + + /* Aftershock Requirment 2013 */ + private static final String RANDOM_AFTERSHOCK = "collapse.aftershock.random"; + + /* Aftershock Requirment 2013 */ + // enum IntensityType { + // ON_DAMAGE, ON_DISTANCE; + // public String toString() { + // if (this.equals(ON_DAMAGE)) + // return "damage"; + // else + // return "distance"; + // }; + // + // } + enum CollapsePolicy { + MERGE_BLOCKADES, DONT_MERGE_BLOCKADES; + public String toString() { + if (this.equals(MERGE_BLOCKADES)) + return "merge"; + else + return "dont_merge"; + }; + } + + CollapsePolicy policy = CollapsePolicy.DONT_MERGE_BLOCKADES; + + private static final int MAX_COLLAPSE = 100; + + private static final double REPAIR_COST_FACTOR = 0.000001; // Converts + // square mm to + // square m. + + public static final String NAME = "Collapse Simulator v1.1"; + + private static final List EMPTY_ID_LIST = new ArrayList( + 0); + + private NumberGenerator destroyed; + private NumberGenerator severe; + private NumberGenerator moderate; + private NumberGenerator slight; + + private boolean block; + + private double floorHeight; + private NumberGenerator extent; + + private Map stats; + + private CollapseSimulatorGUI gui; + private Collection buildingCache; + private Collection roadCache; + + public CollapseSimulator() { + } + + /** + * This method instantiate a CollapseWorldModel instance. + */ + @Override + protected StandardWorldModel createWorldModel() { + return new CollapseWorldModel();/* Aftershocks Requirement:2013 */ + } + + @Override + public JComponent getGUIComponent() { + if (gui == null) { + gui = new CollapseSimulatorGUI(); + } + return gui; + } + + @Override + public String getGUIComponentName() { + return "Collapse simulator"; + } + + @Override + public String getName() { + return NAME; + } + + @Override + protected void postConnect() { + super.postConnect(); + stats = new EnumMap( + StandardEntityConstants.BuildingCode.class); + for (StandardEntityConstants.BuildingCode code : StandardEntityConstants.BuildingCode + .values()) { + stats.put(code, new CollapseStats(code, config)); + } + slight = new GaussianGenerator(config.getFloatValue(CONFIG_PREFIX + + SLIGHT_MEAN_SUFFIX), config.getFloatValue(CONFIG_PREFIX + + SLIGHT_SD_SUFFIX), config.getRandom()); + moderate = new GaussianGenerator(config.getFloatValue(CONFIG_PREFIX + + MODERATE_MEAN_SUFFIX), config.getFloatValue(CONFIG_PREFIX + + MODERATE_SD_SUFFIX), config.getRandom()); + severe = new GaussianGenerator(config.getFloatValue(CONFIG_PREFIX + + SEVERE_MEAN_SUFFIX), config.getFloatValue(CONFIG_PREFIX + + SEVERE_SD_SUFFIX), config.getRandom()); + destroyed = new GaussianGenerator(config.getFloatValue(CONFIG_PREFIX + + DESTROYED_MEAN_SUFFIX), config.getFloatValue(CONFIG_PREFIX + + DESTROYED_SD_SUFFIX), config.getRandom()); + block = config.getBooleanValue(BLOCK_KEY); + floorHeight = config.getFloatValue(FLOOR_HEIGHT_KEY) * 1000; + extent = new ContinuousUniformGenerator( + config.getFloatValue(WALL_COLLAPSE_EXTENT_MIN_KEY), + config.getFloatValue(WALL_COLLAPSE_EXTENT_MAX_KEY), + config.getRandom()); + buildingCache = new HashSet(); + roadCache = new HashSet(); + for (StandardEntity next : model) { + if (next instanceof Building) { + buildingCache.add((Building) next); + } + if (next instanceof Road) { + roadCache.add((Road) next); + } + } + model.addWorldModelListener(new WorldModelListener() { + @Override + public void entityAdded(WorldModel model, + StandardEntity e) { + if (e instanceof Building) { + buildingCache.add((Building) e); + } + if (e instanceof Road) { + roadCache.add((Road) e); + } + } + + @Override + public void entityRemoved( + WorldModel model, StandardEntity e) { + if (e instanceof Building) { + buildingCache.remove((Building) e); + } + if (e instanceof Road) { + roadCache.remove((Road) e); + } + } + }); + } + + @Override + protected void processCommands(KSCommands c, ChangeSet changes) { + long start = System.currentTimeMillis(); + int time = c.getTime(); + Logger.info("Timestep " + time); + if (gui != null) { + gui.timestep(time); + } + Collection collapsed = doCollapse(changes, time); + Map> newBlock = doBlock(collapsed, + time); + // Create blockade objects + Map> blockades = createBlockadeObjects(newBlock); + for (Map.Entry> entry : blockades.entrySet()) { + Road r = entry.getKey(); + List existing = r.getBlockades(); + List ids = new ArrayList(); + if (existing != null) { + ids.addAll(existing); + } + for (Blockade b : entry.getValue()) { + ids.add(b.getID()); + } + r.setBlockades(ids); + changes.addAll(entry.getValue()); + changes.addChange(r, r.getBlockadesProperty()); + } + // If any roads have undefined blockades then set the blockades property + // to the empty list + for (Road next : roadCache) { + if (!next.isBlockadesDefined()) { + next.setBlockades(EMPTY_ID_LIST); + changes.addChange(next, next.getBlockadesProperty()); + } + } + long end = System.currentTimeMillis(); + Logger.info("Timestep " + time + " took " + (end - start) + " ms"); + } + + private Collection doCollapse(ChangeSet changes, int time) { + Collection result = new HashSet(); + if (gui != null) { + gui.startCollapse(buildingCache.size()); + } + if (time == 1) { + result.addAll(doEarthquakeCollapse(changes)); + } + /* Aftershocks Requirement:2013 */ + if (time != 1 && model().aftershockHappens(time)) { + result.addAll(doEarthquakeCollapse(changes)); + } + if (gui != null) { + gui.endCollapse(); + } + if (gui != null) { + gui.startFire(buildingCache.size()); + } + // result.addAll(doFireCollapse(changes)); + if (gui != null) { + gui.endFire(); + } + return result; + } + + static long t1; + static long t2; + static long t3; + + private Map> doBlock( + Collection collapsed, int time) { + Map> result = new LazyMap>() { + @Override + public Collection createValue() { + return new ArrayList(); + } + }; + if (!block) { + return result; + } + if (gui != null) { + gui.startBlock(collapsed.size()); + } + for (Building b : collapsed) { + createBlockages(b, result, time); + if (gui != null) { + gui.bumpBlock(); + } + } + if (gui != null) { + gui.endBlock(); + } + return result; + } + + private Collection doEarthquakeCollapse(ChangeSet changes) { + Map> count = new EnumMap>( + StandardEntityConstants.BuildingCode.class); + Map total = new EnumMap( + StandardEntityConstants.BuildingCode.class); + + for (StandardEntityConstants.BuildingCode code : StandardEntityConstants.BuildingCode + .values()) { + Map next = new EnumMap( + CollapseDegree.class); + for (CollapseDegree cd : CollapseDegree.values()) { + next.put(cd, 0); + } + count.put(code, next); + total.put(code, 0); + } + + Logger.debug("Collapsing buildings"); + Collection result = new HashSet(); + for (Building b : buildingCache) { + StandardEntityConstants.BuildingCode code = b.getBuildingCodeEnum(); + int damage = code == null ? 0 : stats.get(code).damage(); + damage = Maths.restrictRange(damage, 0, MAX_COLLAPSE); + int lastDamage = b.getBrokenness(); + /* + * Aftershock Requirement 2013: this ignores the new damage of it is + * less than the previous + */ + if (damage < lastDamage) { + damage = lastDamage; + } + /* Aftershock Requirement 2013 */ + b.setBrokenness(damage); + changes.addChange(b, b.getBrokennessProperty()); + + CollapseDegree degree = CollapseDegree.get(damage); + count.get(code).put(degree, count.get(code).get(degree) + 1); + total.put(code, total.get(code) + 1); + + if (damage > 0) { + result.add(b); + } + if (gui != null) { + gui.bumpCollapse(); + } + } + Logger.info("Finished collapsing buildings: "); + for (StandardEntityConstants.BuildingCode code : StandardEntityConstants.BuildingCode + .values()) { + Logger.info("Building code " + code + ": " + total.get(code) + + " buildings"); + Map data = count.get(code); + for (Map.Entry entry : data.entrySet()) { + Logger.info(" " + entry.getValue() + " " + + entry.getKey().toString().toLowerCase()); + } + } + return result; + } + + private Collection doFireCollapse(ChangeSet changes) { + Logger.debug("Checking fire damage"); + Collection result = new HashSet(); + for (Building b : buildingCache) { + /* + * Aftershock Requirement @ 2103: it is used in doBlock to check + * whether apply the aftershock intensity or not + */ + model().getCSBuiding(b).setHasFireDamage(false); + + if (!b.isFierynessDefined()) { + if (gui != null) { + gui.bumpFire(); + } + continue; + } + int minDamage = 0; + switch (b.getFierynessEnum()) { + case HEATING: + minDamage = slight.nextValue().intValue(); + break; + case BURNING: + minDamage = moderate.nextValue().intValue(); + break; + case INFERNO: + minDamage = severe.nextValue().intValue(); + break; + case BURNT_OUT: + minDamage = destroyed.nextValue().intValue(); + break; + default: + break; + } + /* Aftershock Requirement @ 2103 */ + if (minDamage != 0) { + // indicates that the building has fire damage + model().getCSBuiding(b).setHasFireDamage(true); + } + minDamage = Maths.restrictRange(minDamage, 0, MAX_COLLAPSE); + int damage = b.isBrokennessDefined() ? b.getBrokenness() : 0; + if (damage < minDamage) { + Logger.info(b + " damaged by fire. New brokenness: " + + minDamage); + b.setBrokenness(minDamage); + changes.addChange(b, b.getBrokennessProperty()); + result.add(b); + } + if (gui != null) { + gui.bumpFire(); + } + } + Logger.debug("Finished checking fire damage"); + return result; + } + + private Map> createBlockadeObjects( + Map> blocks) { + Map> result = new LazyMap>() { + @Override + public Collection createValue() { + return new ArrayList(); + } + }; + int count = 0; + for (Collection c : blocks.values()) { + count += c.size(); + } + try { + if (count != 0) { + List newIDs = requestNewEntityIDs(count); + Iterator it = newIDs.iterator(); + Logger.debug("Creating new blockade objects"); + for (Map.Entry> entry : blocks + .entrySet()) { + Road r = entry.getKey(); + for (java.awt.geom.Area area : entry.getValue()) { + EntityID id = it.next(); + Blockade blockade = makeBlockade(id, area, r.getID()); + if (blockade != null) { + result.get(r).add(blockade); + } + } + } + } + } catch (InterruptedException e) { + Logger.error("Interrupted while requesting IDs"); + } + return result; + } + + private void createBlockages(Building b, + Map> roadBlockages, int time) { + long t = System.currentTimeMillis(); + Logger.debug("Creating blockages for " + b); + /* + * Aftershock's Requirement@2103: this checks the ratio of the + * building's floors that is possible to collapsed + */ + double remainingfloors = model().getCSBuiding(b) + .getRemainingToCollapse(floorHeight); + double currentExtent = extent.nextValue(); + double damage = b.getBrokenness(); + /* + * Aftershock Requirement 2013: applies the intensity of the aftershock. + * This only applies if there is an aftershock and time is >1 and the + * building doesn't have any fire damage. In case of any fire damage the + * building creates the max possible collapse. + */ + if (time != 1 && model().aftershockHappens(time) + && !model().getCSBuiding(b).hasFireDamage()) { + float intensity = model().aftershockIntensity(time); + damage = (int) ((float) damage * Math.min(1, intensity)); + } + /* Aftershock's Requirement: 2013 */ + double d = remainingfloors * (damage / (double) MAX_COLLAPSE) + * currentExtent; + t1 += System.currentTimeMillis() - t; + t = System.currentTimeMillis(); + /* Aftershock's Requirement: 2013 */ + model().getCSBuiding(b).increaseCollapseRatio( + (float) (d / model().getCSBuiding(b).getTotalCollapse( + floorHeight))); + // Place some blockages on surrounding roads + List wallAreas = new ArrayList(); + // Project each wall out and build a list of wall areas + for (Edge edge : b.getEdges()) { + projectWall(edge, wallAreas, d); + } + java.awt.geom.Area fullArea = new java.awt.geom.Area(); + for (java.awt.geom.Area wallArea : wallAreas) { + fullArea.add(wallArea); + } + + /* + * new ShapeDebugFrame().show("Collapsed building", new + * ShapeDebugFrame.AWTShapeInfo(b.getShape(), "Original building area", + * Color.RED, true), new ShapeDebugFrame.AWTShapeInfo(fullArea, + * "Expanded building area (d = " + d + ")", Color.BLACK, false) ); + */ + // Find existing blockade areas + java.awt.geom.Area existing = new java.awt.geom.Area(); + if (policy.equals(CollapsePolicy.MERGE_BLOCKADES)) { + for (StandardEntity e : model + .getEntitiesOfType(StandardEntityURN.BLOCKADE)) { + Blockade blockade = (Blockade) e; + existing.add(blockadeToArea(blockade)); + } + } + t2 += System.currentTimeMillis() - t; + // Intersect wall areas with roads + t = System.currentTimeMillis(); + Map> blockadesForRoads = createRoadBlockades( + fullArea, existing); + t3 += System.currentTimeMillis() - t; + // Add to roadBlockages + for (Map.Entry> entry : blockadesForRoads + .entrySet()) { + Road r = entry.getKey(); + Collection c = entry.getValue(); + roadBlockages.get(r).addAll(c); + } + } + + private void projectWall(Edge edge, + Collection areaList, double d) { + Line2D wallLine = new Line2D(edge.getStartX(), edge.getStartY(), + edge.getEndX() - edge.getStartX(), edge.getEndY() + - edge.getStartY()); + Vector2D wallDirection = wallLine.getDirection(); + Vector2D offset = wallDirection.getNormal().normalised().scale(-d); + Path2D path = new Path2D.Double(); + + Point2D right = wallLine.getOrigin(); + Point2D left = wallLine.getEndPoint(); + + Point2D first = left.plus(offset.scale(-1)); + Point2D second = right.plus(offset.scale(-1)); + Point2D third = right.plus(offset); + Point2D fourth = left.plus(offset); + + path.moveTo(first.getX(), first.getY()); + path.lineTo(second.getX(), second.getY()); + path.lineTo(third.getX(), third.getY()); + path.lineTo(fourth.getX(), fourth.getY()); + path.closePath(); + + java.awt.geom.Area wallArea = new java.awt.geom.Area(path); + areaList.add(wallArea); + // Also add circles at each corner + double radius = offset.getLength(); + Ellipse2D ellipse1 = new Ellipse2D.Double(right.getX() - radius, + right.getY() - radius, radius * 2, radius * 2); + Ellipse2D ellipse2 = new Ellipse2D.Double(left.getX() - radius, + left.getY() - radius, radius * 2, radius * 2); + areaList.add(new java.awt.geom.Area(ellipse1)); + areaList.add(new java.awt.geom.Area(ellipse2)); + // Logger.info("Edge from " + wallLine + " expanded to " + first + ", " + // + second + ", " + third + ", " + fourth); + // debug.show("Collapsed building", + // new ShapeDebugFrame.AWTShapeInfo(buildingArea, + // "Original building area", Color.RED, true), + // new ShapeDebugFrame.Line2DShapeInfo(wallLine, "Wall edge", + // Color.WHITE, true, true), + // new ShapeDebugFrame.AWTShapeInfo(wallArea, "Wall area (d = " + d + + // ")", Color.GREEN, false), + // new ShapeDebugFrame.AWTShapeInfo(ellipse1, "Ellipse 1", Color.BLUE, + // false), + // new ShapeDebugFrame.AWTShapeInfo(ellipse2, "Ellipse 2", Color.ORANGE, + // false) + // ); + } + + private Map> createRoadBlockades( + java.awt.geom.Area buildingArea, java.awt.geom.Area existing) { + Map> result = new HashMap>(); + java.awt.Rectangle rectangle = buildingArea.getBounds(); + Collection roads = model.getObjectsInRectangle( + (int) rectangle.getMinX(), (int) rectangle.getMinY(), + (int) rectangle.getMaxX(), (int) rectangle.getMaxY()); + for (StandardEntity e : roads) { + if (!(e instanceof Road)) { + continue; + } + Road r = (Road) e; + java.awt.geom.Area roadArea = areaToGeomArea(r); + java.awt.geom.Area intersection = new java.awt.geom.Area(roadArea); + intersection.intersect(buildingArea); + intersection.subtract(existing); + if (intersection.isEmpty()) { + continue; + } + existing.add(intersection); + List blockadeAreas = fix(intersection); + result.put(r, blockadeAreas); + // debug.show("Road blockage", + // new ShapeDebugFrame.AWTShapeInfo(buildingArea, "Building area", + // Color.BLACK, false), + // new ShapeDebugFrame.AWTShapeInfo(roadArea, "Road area", + // Color.BLUE, false), + // new ShapeDebugFrame.AWTShapeInfo(intersection, "Intersection", + // Color.GREEN, true) + // ); + } + return result; + } + + private java.awt.geom.Area areaToGeomArea( + rescuecore2.standard.entities.Area area) { + Path2D result = new Path2D.Double(); + Iterator it = area.getEdges().iterator(); + Edge e = it.next(); + result.moveTo(e.getStartX(), e.getStartY()); + result.lineTo(e.getEndX(), e.getEndY()); + while (it.hasNext()) { + e = it.next(); + result.lineTo(e.getEndX(), e.getEndY()); + } + return new java.awt.geom.Area(result); + } + + private List fix(java.awt.geom.Area area) { + List result = new ArrayList(); + if (area.isSingular()) { + result.add(area); + return result; + } + PathIterator it = area.getPathIterator(null); + Path2D current = null; + // CHECKSTYLE:OFF:MagicNumber + double[] d = new double[6]; + while (!it.isDone()) { + switch (it.currentSegment(d)) { + case PathIterator.SEG_MOVETO: + if (current != null) { + result.add(new java.awt.geom.Area(current)); + } + current = new Path2D.Double(); + current.moveTo(d[0], d[1]); + break; + case PathIterator.SEG_LINETO: + current.lineTo(d[0], d[1]); + break; + case PathIterator.SEG_QUADTO: + current.quadTo(d[0], d[1], d[2], d[3]); + break; + case PathIterator.SEG_CUBICTO: + current.curveTo(d[0], d[1], d[2], d[3], d[4], d[5]); + break; + case PathIterator.SEG_CLOSE: + current.closePath(); + break; + default: + throw new RuntimeException( + "Unexpected result from PathIterator.currentSegment: " + + it.currentSegment(d)); + } + it.next(); + } + // CHECKSTYLE:ON:MagicNumber + if (current != null) { + result.add(new java.awt.geom.Area(current)); + } + return result; + } + + private Blockade makeBlockade(EntityID id, java.awt.geom.Area area, + EntityID roadID) { + if (area.isEmpty()) { + return null; + } + Blockade result = new Blockade(id); + int[] apexes = getApexes(area); + List points = GeometryTools2D.vertexArrayToPoints(apexes); + if (points.size() < 2) { + return null; + } + int cost = (int) (GeometryTools2D.computeArea(points) * REPAIR_COST_FACTOR); + if (cost == 0) { + return null; + } + Point2D centroid = GeometryTools2D.computeCentroid(points); + result.setApexes(apexes); + result.setPosition(roadID); + result.setX((int) centroid.getX()); + result.setY((int) centroid.getY()); + result.setRepairCost((int) cost); + return result; + } + + private int[] getApexes(java.awt.geom.Area area) { + // Logger.debug("getApexes"); + List apexes = new ArrayList(); + // CHECKSTYLE:OFF:MagicNumber + PathIterator it = area.getPathIterator(null, 100); + double[] d = new double[6]; + int moveX = 0; + int moveY = 0; + int lastX = 0; + int lastY = 0; + boolean finished = false; + while (!finished && !it.isDone()) { + int x = 0; + int y = 0; + switch (it.currentSegment(d)) { + case PathIterator.SEG_MOVETO: + x = (int) d[0]; + y = (int) d[1]; + moveX = x; + moveY = y; + // Logger.debug("Move to " + x + ", " + y); + break; + case PathIterator.SEG_LINETO: + x = (int) d[0]; + y = (int) d[1]; + // Logger.debug("Line to " + x + ", " + y); + if (x == moveX && y == moveY) { + finished = true; + } + break; + case PathIterator.SEG_QUADTO: + x = (int) d[2]; + y = (int) d[3]; + // Logger.debug("Quad to " + x + ", " + y); + if (x == moveX && y == moveY) { + finished = true; + } + break; + case PathIterator.SEG_CUBICTO: + x = (int) d[4]; + y = (int) d[5]; + // Logger.debug("Cubic to " + x + ", " + y); + if (x == moveX && y == moveY) { + finished = true; + } + break; + case PathIterator.SEG_CLOSE: + // Logger.debug("Close"); + finished = true; + break; + default: + throw new RuntimeException( + "Unexpected result from PathIterator.currentSegment: " + + it.currentSegment(d)); + } + // Logger.debug(x + ", " + y); + if (!finished && (x != lastX || y != lastY)) { + apexes.add(x); + apexes.add(y); + } + lastX = x; + lastY = y; + it.next(); + } + // CHECKSTYLE:ON:MagicNumber + int[] result = new int[apexes.size()]; + int i = 0; + for (Integer next : apexes) { + result[i++] = next; + } + return result; + } + + private java.awt.geom.Area blockadeToArea(Blockade b) { + Path2D result = new Path2D.Double(); + int[] apexes = b.getApexes(); + result.moveTo(apexes[0], apexes[1]); + for (int i = 2; i < apexes.length; i += 2) { + result.lineTo(apexes[i], apexes[i + 1]); + } + result.closePath(); + return new java.awt.geom.Area(result); + } + + private class CollapseStats { + private double pDestroyed; + private double pSevere; + private double pModerate; + private double pSlight; + + CollapseStats(StandardEntityConstants.BuildingCode code, Config config) { + String s = CONFIG_PREFIX + code.toString().toLowerCase(); + pDestroyed = config.getFloatValue(s + DESTROYED_SUFFIX); + pSevere = pDestroyed + config.getFloatValue(s + SEVERE_SUFFIX); + pModerate = pSevere + config.getFloatValue(s + MODERATE_SUFFIX); + pSlight = pModerate + config.getFloatValue(s + SLIGHT_SUFFIX); + } + + int damage() { + double d = random.nextDouble(); + if (d < pDestroyed) { + return destroyed.nextValue().intValue(); + } + if (d < pSevere) { + return severe.nextValue().intValue(); + } + if (d < pModerate) { + return moderate.nextValue().intValue(); + } + if (d < pSlight) { + return slight.nextValue().intValue(); + } + return 0; + } + } + + private enum CollapseDegree { + NONE(0), SLIGHT(25), MODERATE(50), SEVERE(75), DESTROYED(100); + + private int max; + + private CollapseDegree(int max) { + this.max = max; + } + + public static CollapseDegree get(int d) { + for (CollapseDegree next : values()) { + if (d <= next.max) { + return next; + } + } + throw new IllegalArgumentException( + "Don't know what to do with a damage value of " + d); + } + } + + // /* Aftershocks Requirement:2013 */ + // /** + // * Handles KAAftershocksInfo and calls the parent processMessage. + // */ + // @Override + // protected void processMessage(Message msg) { + // + // } + + /* Aftershocks Requirement:2013 */ + public CollapseWorldModel model() { + return (CollapseWorldModel) model; + } + + @Override + protected void processMessage(Message msg) { + if (msg instanceof KSAfterShocksInfo) { + model().updateAftershocks((KSAfterShocksInfo) msg); + } else { + super.processMessage(msg); + } + } + + public static void main(String[] args) { + } +} diff --git a/modules/collapse/src/collapse/CollapseSimulatorGUI.java b/modules/collapse/src/collapse/CollapseSimulatorGUI.java new file mode 100755 index 0000000000000000000000000000000000000000..4afb0a8e3c93880f90175599b62b00965e8517a8 --- /dev/null +++ b/modules/collapse/src/collapse/CollapseSimulatorGUI.java @@ -0,0 +1,197 @@ +package collapse; + +import javax.swing.JPanel; +import javax.swing.JProgressBar; +import javax.swing.JLabel; +import javax.swing.SwingUtilities; +import java.awt.GridLayout; + +/** + GUI for the collapse simulator. +*/ +public class CollapseSimulatorGUI extends JPanel { + private JLabel timeLabel; + private JLabel statusLabel; + private JProgressBar collapseProgress; + private JProgressBar fireProgress; + private JProgressBar blockadeProgress; + + private int collapse; + private int fire; + private int block; + + /** + Construct a collapse simulator GUI. + */ + public CollapseSimulatorGUI() { + super(new GridLayout(0, 2)); + + timeLabel = new JLabel("Not started"); + statusLabel = new JLabel("Not started"); + collapseProgress = new JProgressBar(0, 1); + fireProgress = new JProgressBar(0, 1); + blockadeProgress = new JProgressBar(0, 1); + + collapseProgress.setStringPainted(true); + fireProgress.setStringPainted(true); + blockadeProgress.setStringPainted(true); + + add(new JLabel("Timestep")); + add(timeLabel); + add(new JLabel("Status")); + add(statusLabel); + add(new JLabel("Collapsing buildings")); + add(collapseProgress); + add(new JLabel("Fire damage")); + add(fireProgress); + add(new JLabel("Creating blockades")); + add(blockadeProgress); + } + + /** + Notify the gui that a new timestep has started. + @param time The timestep. + */ + void timestep(final int time) { + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + timeLabel.setText(String.valueOf(time)); + collapseProgress.setValue(0); + fireProgress.setValue(0); + blockadeProgress.setValue(0); + collapse = 0; + fire = 0; + block = 0; + } + }); + } + + /** + Notify the gui that collapse computation has begun. + @param buildingCount The number of buildings to process. + */ + void startCollapse(final int buildingCount) { + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + statusLabel.setText("Collapsing buildings"); + collapseProgress.setMaximum(buildingCount); + collapseProgress.setValue(0); + collapse = 0; + } + }); + } + + /** + Notify the gui that a building collapse has been processed. + */ + void bumpCollapse() { + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + collapseProgress.setValue(++collapse); + } + }); + } + + /** + Notify the gui that building collapse computation is complete. + */ + void endCollapse() { + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + collapseProgress.setValue(collapseProgress.getMaximum()); + } + }); + } + + /** + Notify the gui that fire collapse computation has begun. + @param buildingCount The number of buildings to process. + */ + void startFire(final int buildingCount) { + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + statusLabel.setText("Fire damage"); + fireProgress.setMaximum(buildingCount); + fireProgress.setValue(0); + fire = 0; + } + }); + } + + /** + Notify the gui that a fire collapse has been processed. + */ + void bumpFire() { + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + fireProgress.setValue(++fire); + } + }); + } + + /** + Notify the gui that fire collapse computation is complete. + */ + void endFire() { + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + fireProgress.setValue(fireProgress.getMaximum()); + } + }); + } + + /** + Notify the gui that blockade generation has begun. + @param buildingCount The number of buildings to process. + */ + void startBlock(final int buildingCount) { + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + statusLabel.setText("Computing blockades"); + if (buildingCount == 0) { + blockadeProgress.setMaximum(1); + blockadeProgress.setValue(1); + } + else { + blockadeProgress.setMaximum(buildingCount); + blockadeProgress.setValue(0); + block = 0; + } + } + }); + } + + /** + Notify the gui that blockade generation for a building has been processed. + */ + void bumpBlock() { + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + blockadeProgress.setValue(++block); + } + }); + } + + /** + Notify the gui that blockade generation is complete. + */ + void endBlock() { + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + blockadeProgress.setValue(blockadeProgress.getMaximum()); + statusLabel.setText("Done"); + } + }); + } + +} \ No newline at end of file diff --git a/modules/collapse/src/collapse/CollapseWorldModel.java b/modules/collapse/src/collapse/CollapseWorldModel.java new file mode 100644 index 0000000000000000000000000000000000000000..6a12b23fc27d1fd78554c70294dbdaf40a84a6fd --- /dev/null +++ b/modules/collapse/src/collapse/CollapseWorldModel.java @@ -0,0 +1,92 @@ +package collapse; + + +import java.util.HashMap; + +import rescuecore2.messages.control.KSAfterShocksInfo; +import rescuecore2.standard.entities.Building; +import rescuecore2.standard.entities.StandardEntity; +import rescuecore2.standard.entities.StandardEntityURN; +import rescuecore2.standard.entities.StandardWorldModel; +import rescuecore2.worldmodel.ChangeSet; +import rescuecore2.worldmodel.EntityID; + +/** + * + * Collapse simulator's world model contains aftershocks' information that other + * simulators don't need. + * + * @author Salim + * + */ +public class CollapseWorldModel extends StandardWorldModel { + private HashMap aftershocks; + private HashMap collapseBuildings; + + public CollapseWorldModel() { + this.aftershocks = new HashMap(); + collapseBuildings = new HashMap(); + } + + @Override + public void merge(ChangeSet changes) { + super.merge(changes); + } + + /** + * Changes the list in the world model with the new input aftershock list + * + * @param msg + * instance of KSAfterShocksInfo + */ + public void updateAftershocks(KSAfterShocksInfo msg) { + aftershocks = msg.getAftershocks(); + } + + public boolean aftershockHappens(int time) { + return aftershocks.get(time) != null; + } + + public float aftershockIntensity(int time) { + return aftershocks.get(time); + } + + public HashMap getCollapseBuildings() { + if (collapseBuildings.size() == 0) + createCollapseBuildings(); + return collapseBuildings; + } + + /** + * Creates Collapse Simulator Buildings using the Standard Buildings + */ + private void createCollapseBuildings() { + for (StandardEntity entity : this) { + if(entity instanceof Building) + collapseBuildings.put((Building) entity, new CSBuilding((Building) entity)); + } + + } + + /** + * Returns a specific CSBuilding by its EntityID + * + * @param id + * is an EntityID + * @return the corresponding CSBuilding to id + */ + public CSBuilding getCSBuiding(EntityID id) { + return getCollapseBuildings().get((Building) getEntity(id)); + } + + /** + * Returns a specific CSBuilding by its Building + * + * @param building + * is an Building + * @return the corresponding CSBuilding to building + */ + public CSBuilding getCSBuiding(Building building) { + return getCollapseBuildings().get(building); + } +} diff --git a/modules/gis2/src/gis2/GISServer.java b/modules/gis2/src/gis2/GISServer.java new file mode 100644 index 0000000000000000000000000000000000000000..a23cd479ffea96d2cfbc26bcfbc72aa7402a9e2b --- /dev/null +++ b/modules/gis2/src/gis2/GISServer.java @@ -0,0 +1,139 @@ +package gis2; + +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; + +import org.apache.log4j.Logger; + +import rescuecore2.Constants; +import rescuecore2.config.Config; +import rescuecore2.connection.Connection; +import rescuecore2.connection.ConnectionException; +import rescuecore2.connection.ConnectionListener; +import rescuecore2.connection.TCPConnection; +import rescuecore2.messages.Message; +import rescuecore2.messages.control.GKConnectOK; +import rescuecore2.messages.control.KGConnect; +import rescuecore2.messages.control.Shutdown; +import rescuecore2.misc.CommandLineOptions; +import rescuecore2.misc.java.LoadableTypeProcessor; +import rescuecore2.registry.Registry; +import rescuecore2.worldmodel.Entity; +import rescuecore2.worldmodel.WorldModel; + +/** + * This class is used for starting a remote GIS server. + */ +public final class GISServer { + private static final long WAIT_TIME = 1000; + + private ServerSocket server; + private WorldModel world; + private volatile boolean running; + + private static final Logger LOG = Logger.getLogger(GISServer.class); + + private GISServer(int port, WorldModel world) throws IOException { + server = new ServerSocket(port); + this.world = world; + running = true; + } + + /** + * Start the GIS server. + * + * @param args Command line arguments: <-c config file> + */ + public static void main(String[] args) { + Config config = new Config(); + try { + CommandLineOptions.processArgs(args, config); + int port = config.getIntValue(Constants.GIS_PORT_NUMBER_KEY, Constants.DEFAULT_GIS_PORT_NUMBER); + processJarFiles(config); + GMLWorldModelCreator creator = new GMLWorldModelCreator(); + new GISServer(port, creator.buildWorldModel(config)).run(); + LOG.info("GIS server listening on port " + port); + } catch (Exception e) { + LOG.fatal("Error starting GIS server", e); + } + } + + private static void processJarFiles(Config config) throws IOException { + LoadableTypeProcessor processor = new LoadableTypeProcessor(config); + processor.addFactoryRegisterCallbacks(Registry.SYSTEM_REGISTRY); + processor.process(); + } + + /** + * Run the GIS server. + */ + public void run() { + while (running) { + try { + Socket socket = server.accept(); + new ServerThread(socket).start(); + } catch (IOException e) { + LOG.error("Error accepting connection", e); + running = false; + } + } + } + + private class ServerThread extends Thread implements ConnectionListener { + private Socket socket; + private boolean dead; + + public ServerThread(Socket socket) { + this.socket = socket; + dead = false; + } + + @Override + public void run() { + TCPConnection c = null; + try { + c = new TCPConnection(socket); + } catch (IOException e) { + LOG.error("Error starting TCPConnection", e); + return; + } + c.startup(); + c.addConnectionListener(this); + synchronized (this) { + while (!dead) { + try { + this.wait(WAIT_TIME); + } catch (InterruptedException e) { + dead = true; + } + } + } + c.shutdown(); + } + + @Override + public void messageReceived(Connection c, Message msg) { + if (msg instanceof KGConnect) { + // Send a GKConnectOK + try { + c.sendMessage(new GKConnectOK(world.getAllEntities())); + } catch (ConnectionException e) { + LOG.fatal("Error sending message", e); + die(); + } + } + if (msg instanceof Shutdown) { + die(); + } + } + + private void die() { + synchronized (this) { + dead = true; + notifyAll(); + } + running = false; + } + } +} \ No newline at end of file diff --git a/modules/gis2/src/gis2/GMLWorldModelCreator.java b/modules/gis2/src/gis2/GMLWorldModelCreator.java new file mode 100755 index 0000000000000000000000000000000000000000..d3e5b38e2583b49c23559dd39f9b0566bc3f05c0 --- /dev/null +++ b/modules/gis2/src/gis2/GMLWorldModelCreator.java @@ -0,0 +1,273 @@ +package gis2; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import kernel.KernelException; +import kernel.WorldModelCreator; +import maps.CoordinateConversion; +import maps.MapException; +import maps.MapReader; +import maps.ScaleConversion; +import maps.gml.GMLBuilding; +import maps.gml.GMLCoordinates; +import maps.gml.GMLDirectedEdge; +import maps.gml.GMLMap; +import maps.gml.GMLRoad; +import maps.gml.GMLShape; +import org.apache.log4j.Logger; +import org.dom4j.Document; +import org.dom4j.DocumentException; +import org.dom4j.io.SAXReader; +import rescuecore2.config.Config; +import rescuecore2.misc.geometry.GeometryTools2D; +import rescuecore2.misc.geometry.Point2D; +import rescuecore2.scenario.exceptions.ScenarioException; +import rescuecore2.standard.entities.Building; +import rescuecore2.standard.entities.Edge; +import rescuecore2.standard.entities.Road; +import rescuecore2.standard.entities.StandardWorldModel; +import rescuecore2.worldmodel.Entity; +import rescuecore2.worldmodel.EntityID; +import rescuecore2.worldmodel.WorldModel; + +/** + * A WorldModelCreator that reads a GML file and scenario descriptor. + */ +public class GMLWorldModelCreator implements WorldModelCreator { + + private static final String MAP_DIRECTORY_KEY = "gis.map.dir"; + private static final String MAP_FILE_KEY = "gis.map.file"; + private static final String DEFAULT_MAP_FILE = "map.gml"; + private static final String SCENARIO_FILE_KEY = "gis.map.scenario"; + private static final String DEFAULT_SCENARIO_FILE = "scenario.xml"; + private static final String MAX_FLOOR = "gis.map.max-floor"; + private static final String FLOOR_PLACEMENT_TYPE = "gis.map.floor-placement.random"; + private static final String RANDOM_FLOOR_RATE = "gis.map.floor-placement.random.floor-rate."; + private static final String BUILDING_CODE_PLACEMENT_TYPE = "gis.map.building-code-placement.random"; + private static final String RANDOM_BUILDING_CODE_RATE = "gis.map.building-code-placement.random.code-rate."; + private static final String MAX_BUILDING_CODE = "gis.map.max-building-code"; + + private static final double SQ_MM_TO_SQ_M = 0.000001; + + private GisScenario scenario; + private static final Logger LOG = Logger + .getLogger(GMLWorldModelCreator.class); + + // private ShapeDebugFrame debug; + + private int nextID; + + @Override + public String toString() { + return "GML world model creator"; + } + + + @Override + public WorldModel buildWorldModel(Config config) + throws KernelException { + try { + StandardWorldModel result = new StandardWorldModel(); + File dir = new File(config.getValue(MAP_DIRECTORY_KEY)); + File mapFile = new File(dir, + config.getValue(MAP_FILE_KEY, DEFAULT_MAP_FILE)); + File scenarioFile = new File(dir, + config.getValue(SCENARIO_FILE_KEY, DEFAULT_SCENARIO_FILE)); + readMapData(mapFile, result, config); + readScenarioAndApply(scenarioFile, result, config); + for (Entity e : result) { + nextID = Math.max(nextID, e.getID().getValue()); + } + ++nextID; + result.index(); + return result; + } catch (MapException e) { + throw new KernelException("Couldn't read GML file", e); + } catch (DocumentException e) { + throw new KernelException("Couldn't read scenario file", e); + } catch (ScenarioException e) { + throw new KernelException("Invalid scenario file", e); + } + } + + + @Override + public EntityID generateID() { + return new EntityID(nextID++); + } + + + private void readMapData(File mapFile, StandardWorldModel result, + Config config) throws MapException { + int maxFloor = config.getIntValue(MAX_FLOOR, 3); + boolean randomfloorPlacement = config.getBooleanValue(FLOOR_PLACEMENT_TYPE, + false); + int[] floorRates = null; + int[] floorRatesCumulative = null; + if (randomfloorPlacement) { + floorRates = new int[maxFloor + 1]; + floorRatesCumulative = new int[maxFloor + 1]; + for (int i = 1; i <= maxFloor; i++) { + floorRates[i] = config.getIntValue(RANDOM_FLOOR_RATE + i); + floorRatesCumulative[i] = floorRatesCumulative[i - 1] + floorRates[i]; + } + } + + int maxBuildingCode = config.getIntValue(MAX_BUILDING_CODE, 2); + boolean randomBuildingCodePlacement = config + .getBooleanValue(BUILDING_CODE_PLACEMENT_TYPE, false); + int[] buildingCodeRates = null; + int[] buildingCodesCumulative = null; + if (randomBuildingCodePlacement) { + buildingCodeRates = new int[maxBuildingCode + 1]; + buildingCodesCumulative = new int[maxBuildingCode + 1]; + for (int i = 0; i <= maxBuildingCode; i++) { + buildingCodeRates[i] = config + .getIntValue(RANDOM_BUILDING_CODE_RATE + i); + buildingCodesCumulative[i] = (i > 0 ? buildingCodesCumulative[i - 1] + : 0) + buildingCodeRates[i]; + } + } + + GMLMap map = (GMLMap) MapReader.readMap(mapFile); + CoordinateConversion conversion = getCoordinateConversion(map); + LOG.debug("Creating entities"); + LOG.debug(map.getBuildings().size() + " buildings"); + LOG.debug(map.getRoads().size() + " roads"); + + for (GMLBuilding next : map.getBuildings()) { + // Create a new Building entity + EntityID id = new EntityID(next.getID()); + Building b = new Building(id); + List vertices = convertShapeToPoints(next, conversion); + double area = GeometryTools2D.computeArea(vertices) * SQ_MM_TO_SQ_M; + Point2D centroid = GeometryTools2D.computeCentroid(vertices); + + // Building properties + int floors = Math.min(maxFloor, next.getFloors()); + if (randomfloorPlacement) { + int rnd = config.getRandom().nextInt(floorRatesCumulative[maxFloor]) + + 1; + for (int i = 1; i <= maxFloor; i++) { + if (rnd <= floorRatesCumulative[i]) { + floors = i; + break; + } + } + } + + int code = Math.min(maxBuildingCode, next.getCode()); + if (randomBuildingCodePlacement) { + int rnd = config.getRandom() + .nextInt(buildingCodesCumulative[maxBuildingCode]) + 1; + for (int i = 0; i <= maxBuildingCode; i++) { + if (rnd <= buildingCodesCumulative[i]) { + code = i; + break; + } + } + } + + b.setFloors(floors); + b.setFieryness(0); + b.setBrokenness(0); + b.setBuildingCode(code); + b.setBuildingAttributes(0); + b.setGroundArea((int) Math.abs(area)); + b.setTotalArea(((int) Math.abs(area)) * b.getFloors()); + b.setImportance(next.getImportance()); + b.setCapacity(0); + // Area properties + b.setEdges(createEdges(next, conversion)); + b.setX((int) centroid.getX()); + b.setY((int) centroid.getY()); + result.addEntity(b); + } + for (GMLRoad next : map.getRoads()) { + // Create a new Road entity + EntityID id = new EntityID(next.getID()); + Road r = new Road(id); + List vertices = convertShapeToPoints(next, conversion); + Point2D centroid = GeometryTools2D.computeCentroid(vertices); + + // Road properties: None + // Area properties + r.setX((int) centroid.getX()); + r.setY((int) centroid.getY()); + r.setEdges(createEdges(next, conversion)); + result.addEntity(r); + } + } + + + private void readScenarioAndApply(File scenarioFile, + StandardWorldModel result, Config config) + throws DocumentException, ScenarioException { + if (scenarioFile.exists()) { + readScenario(scenarioFile, config); + LOG.debug("Applying scenario"); + scenario.apply(result, config); + } + } + + + private void readScenario(File scenarioFile, Config config) + throws DocumentException, ScenarioException { + if (scenarioFile.exists()) { + SAXReader reader = new SAXReader(); + LOG.debug("Reading scenario"); + Document doc = reader.read(scenarioFile); + scenario = new GisScenario(doc, config); + } + } + + + private List createEdges(GMLShape s, CoordinateConversion conversion) { + List result = new ArrayList(); + for (GMLDirectedEdge edge : s.getEdges()) { + GMLCoordinates start = edge.getStartCoordinates(); + GMLCoordinates end = edge.getEndCoordinates(); + Integer neighbourID = s.getNeighbour(edge); + EntityID id = neighbourID == null ? null : new EntityID(neighbourID); + double sx = conversion.convertX(start.getX()); + double sy = conversion.convertY(start.getY()); + double ex = conversion.convertX(end.getX()); + double ey = conversion.convertY(end.getY()); + result.add(new Edge((int) sx, (int) sy, (int) ex, (int) ey, id)); + } + return result; + } + + + private List convertShapeToPoints(GMLShape shape, + CoordinateConversion conversion) { + List points = new ArrayList(); + for (GMLCoordinates next : shape.getCoordinates()) { + points.add(new Point2D(conversion.convertX(next.getX()), + conversion.convertY(next.getY()))); + } + return points; + } + + + private CoordinateConversion getCoordinateConversion(GMLMap map) { + return new ScaleConversion(map.getMinX(), map.getMinY(), 1000, 1000); + } + + + public GisScenario getScenario(Config config) throws DocumentException { + + if (scenario == null) { + File dir = new File(config.getValue(MAP_DIRECTORY_KEY)); + File scenarioFile = new File(dir, + config.getValue(SCENARIO_FILE_KEY, DEFAULT_SCENARIO_FILE)); + try { + readScenario(scenarioFile, config); + } catch (ScenarioException e) { + e.printStackTrace(); + } + } + return scenario; + } +} \ No newline at end of file diff --git a/modules/gis2/src/gis2/GisScenario.java b/modules/gis2/src/gis2/GisScenario.java new file mode 100755 index 0000000000000000000000000000000000000000..282c0bb4373fdda59a2f3042ac221c65993e927b --- /dev/null +++ b/modules/gis2/src/gis2/GisScenario.java @@ -0,0 +1,919 @@ +package gis2; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import maps.gml.GMLRefuge; + +import org.apache.log4j.Logger; +import org.dom4j.Document; +import org.dom4j.DocumentHelper; +import org.dom4j.Element; +import org.dom4j.Namespace; +import org.dom4j.QName; + +import rescuecore2.config.Config; +import rescuecore2.scenario.compatibilities.CollapseSimCompatibaleScenarioV1_1; +import rescuecore2.scenario.exceptions.ScenarioException; +import rescuecore2.standard.entities.AmbulanceCentre; +import rescuecore2.standard.entities.AmbulanceTeam; +import rescuecore2.standard.entities.Area; +import rescuecore2.standard.entities.Building; +import rescuecore2.standard.entities.Civilian; +import rescuecore2.standard.entities.FireBrigade; +import rescuecore2.standard.entities.FireStation; +import rescuecore2.standard.entities.GasStation; +import rescuecore2.standard.entities.Human; +import rescuecore2.standard.entities.Hydrant; +import rescuecore2.standard.entities.PoliceForce; +import rescuecore2.standard.entities.PoliceOffice; +import rescuecore2.standard.entities.Refuge; +import rescuecore2.standard.entities.Road; +import rescuecore2.standard.entities.StandardEntity; +import rescuecore2.standard.entities.StandardWorldModel; +import rescuecore2.worldmodel.Entity; +import rescuecore2.worldmodel.EntityID; + +/** + * This class knows how to read scenario files and apply them to + * StandardWorldModels. + */ +public class GisScenario implements rescuecore2.scenario.Scenario, CollapseSimCompatibaleScenarioV1_1 { + private static final String SCENARIO_NAMESPACE_URI = "urn:roborescue:map:scenario"; + private static final Namespace SCENARIO_NAMESPACE = DocumentHelper.createNamespace("scenario", + SCENARIO_NAMESPACE_URI); + + private static final int DEFAULT_HP = 10000; + private static final int DEFAULT_STAMINA = 10000; + private static final String WATER_QUANTITY_KEY = "fire.tank.maximum"; + + private static final QName LOCATION_QNAME = DocumentHelper.createQName("location", SCENARIO_NAMESPACE); + private static final QName TIME_QNAME = DocumentHelper.createQName("time", SCENARIO_NAMESPACE); + private static final QName INTENSITY_QNAME = DocumentHelper.createQName("intensity", SCENARIO_NAMESPACE); + private static final QName SCENARIO_QNAME = DocumentHelper.createQName("scenario", SCENARIO_NAMESPACE); + private static final QName REFUGE_QNAME = DocumentHelper.createQName("refuge", SCENARIO_NAMESPACE); + private static final QName GAS_STATION_QNAME = DocumentHelper.createQName("gasstation", SCENARIO_NAMESPACE); + private static final QName HYDRANT_QNAME = DocumentHelper.createQName("hydrant", SCENARIO_NAMESPACE); + private static final QName CIV_QNAME = DocumentHelper.createQName("civilian", SCENARIO_NAMESPACE); + private static final QName FB_QNAME = DocumentHelper.createQName("firebrigade", SCENARIO_NAMESPACE); + private static final QName AT_QNAME = DocumentHelper.createQName("ambulanceteam", SCENARIO_NAMESPACE); + private static final QName PF_QNAME = DocumentHelper.createQName("policeforce", SCENARIO_NAMESPACE); + private static final QName FS_QNAME = DocumentHelper.createQName("firestation", SCENARIO_NAMESPACE); + private static final QName AC_QNAME = DocumentHelper.createQName("ambulancecentre", SCENARIO_NAMESPACE); + private static final QName PO_QNAME = DocumentHelper.createQName("policeoffice", SCENARIO_NAMESPACE); + private static final QName FIRE_QNAME = DocumentHelper.createQName("fire", SCENARIO_NAMESPACE); + /* Aftershock requirement:2013 */ + private static final QName AFTERSHOCK_QNAME = DocumentHelper.createQName("aftershock", SCENARIO_NAMESPACE); + /* Refuge bed capacity:2020 */ + private static final QName BEDCAPACITY_QNAME = DocumentHelper.createQName("bedCapacity", SCENARIO_NAMESPACE); + /* Refuge refill capacity:2020 */ + private static final QName REFILLCAPACITY_QNAME = DocumentHelper.createQName("refillCapacity", SCENARIO_NAMESPACE); + + private Set refuges; + private Set hydrants; + private Set gasStations; + private Set fires; + /* Aftershock requirement:2013 */ + private HashMap aftershocks; + private Collection civLocations; + private Collection fbLocations; + private Collection atLocations; + private Collection pfLocations; + private Collection fsLocations; + private Collection acLocations; + private Collection poLocations; + + /* Refuge Capacity requirements: 2020 */ + private HashMap refugeBedCapacity; + private HashMap refugeRefillCapacity; + private Map gmlRefuges; + + private static final Logger LOG = Logger.getLogger(GisScenario.class); + + /** + * Create an empty scenario. + */ + public GisScenario() { + refuges = new HashSet(); + hydrants = new HashSet(); + gasStations = new HashSet(); + fires = new HashSet(); + civLocations = new ArrayList(); + fbLocations = new ArrayList(); + pfLocations = new ArrayList(); + atLocations = new ArrayList(); + fsLocations = new ArrayList(); + poLocations = new ArrayList(); + acLocations = new ArrayList(); + /* Aftershock requirement:2013 */ + aftershocks = new HashMap(); + /* Refuge Capacity requirements: 2020 */ + refugeBedCapacity = new HashMap(); + refugeRefillCapacity = new HashMap(); + gmlRefuges = new HashMap(); + } + + /** + * Create a scenario from an XML document. + * + * @param doc The document to read. + * @throws ScenarioException If the scenario is invalid. + */ + public GisScenario(Document doc, Config config) throws ScenarioException { + this(); + read(doc, config); + } + + /** + * Read scenario data from an XML document. + * + * @param doc The document to read. + * @throws ScenarioException If the scenario is invalid. + */ + public void read(Document doc, Config config) throws ScenarioException { + hydrants.clear(); + gasStations.clear(); + refuges.clear(); + fires.clear(); + civLocations.clear(); + fbLocations.clear(); + pfLocations.clear(); + atLocations.clear(); + fsLocations.clear(); + poLocations.clear(); + acLocations.clear(); + refugeBedCapacity.clear(); + refugeRefillCapacity.clear(); + + Element root = doc.getRootElement(); + if (!root.getQName().equals(SCENARIO_QNAME)) { + throw new ScenarioException( + "Scenario document has wrong root element: expecting " + SCENARIO_QNAME + "; not " + root.getQName()); + } + for (Object next : root.elements(REFUGE_QNAME)) { + Element e = (Element) next; + refuges.add(Integer.parseInt(e.attributeValue(LOCATION_QNAME))); + /* capacity requirements:2020 */ + int default_bedCapacity = config.getIntValue("gis.map.refuge.default-bedCapacity", 1000); + int default_refillCapacity = config.getIntValue("gis.map.refuge.default-refillCapacity", 1000); + int bedCapacity = e.attributeValue(BEDCAPACITY_QNAME) != null + ? Integer.parseInt(e.attributeValue(BEDCAPACITY_QNAME)) + : default_bedCapacity; + int refillCapacity = e.attributeValue(REFILLCAPACITY_QNAME) != null + ? Integer.parseInt(e.attributeValue(REFILLCAPACITY_QNAME)) + : default_refillCapacity; + refugeBedCapacity.put(Integer.parseInt(e.attributeValue(LOCATION_QNAME)), bedCapacity); + refugeRefillCapacity.put(Integer.parseInt(e.attributeValue(LOCATION_QNAME)), refillCapacity); + } + for (Object next : root.elements(HYDRANT_QNAME)) { + Element e = (Element) next; + hydrants.add(Integer.parseInt(e.attributeValue(LOCATION_QNAME))); + } + for (Object next : root.elements(GAS_STATION_QNAME)) { + Element e = (Element) next; + gasStations.add(Integer.parseInt(e.attributeValue(LOCATION_QNAME))); + } + for (Object next : root.elements(CIV_QNAME)) { + Element e = (Element) next; + civLocations.add(Integer.parseInt(e.attributeValue(LOCATION_QNAME))); + } + for (Object next : root.elements(FB_QNAME)) { + Element e = (Element) next; + fbLocations.add(Integer.parseInt(e.attributeValue(LOCATION_QNAME))); + } + for (Object next : root.elements(PF_QNAME)) { + Element e = (Element) next; + pfLocations.add(Integer.parseInt(e.attributeValue(LOCATION_QNAME))); + } + for (Object next : root.elements(AT_QNAME)) { + Element e = (Element) next; + atLocations.add(Integer.parseInt(e.attributeValue(LOCATION_QNAME))); + } + for (Object next : root.elements(FS_QNAME)) { + Element e = (Element) next; + fsLocations.add(Integer.parseInt(e.attributeValue(LOCATION_QNAME))); + } + for (Object next : root.elements(PO_QNAME)) { + Element e = (Element) next; + poLocations.add(Integer.parseInt(e.attributeValue(LOCATION_QNAME))); + } + for (Object next : root.elements(AC_QNAME)) { + Element e = (Element) next; + acLocations.add(Integer.parseInt(e.attributeValue(LOCATION_QNAME))); + } + for (Object next : root.elements(FIRE_QNAME)) { + Element e = (Element) next; + fires.add(Integer.parseInt(e.attributeValue(LOCATION_QNAME))); + } + /* + * Aftershock requirement:2013 + */ + for (Object next : root.elements(AFTERSHOCK_QNAME)) { + Element e = (Element) next; + aftershocks.put(Integer.parseInt(e.attributeValue(TIME_QNAME)), + Float.parseFloat(e.attributeValue(INTENSITY_QNAME))); + } + } + + /** + * Write scenario data to an XML document. + * + * @param doc The document to write to. + */ + public void write(Document doc) { + Element root = DocumentHelper.createElement(SCENARIO_QNAME); + doc.setRootElement(root); + for (int next : refuges) { + Element el = root.addElement(REFUGE_QNAME); + el.addAttribute(LOCATION_QNAME, String.valueOf(next)); + el.addAttribute(BEDCAPACITY_QNAME, String.valueOf(refugeBedCapacity.get(next))); + // el.addAttribute(REFILLCAPACITY_QNAME, + // String.valueOf(refugeRefillCapacity.get(next))); + } + for (int next : fires) { + root.addElement(FIRE_QNAME).addAttribute(LOCATION_QNAME, String.valueOf(next)); + } + for (int next : hydrants) { + root.addElement(HYDRANT_QNAME).addAttribute(LOCATION_QNAME, String.valueOf(next)); + } + for (int next : gasStations) { + root.addElement(GAS_STATION_QNAME).addAttribute(LOCATION_QNAME, String.valueOf(next)); + } + for (int next : civLocations) { + root.addElement(CIV_QNAME).addAttribute(LOCATION_QNAME, String.valueOf(next)); + } + for (int next : fbLocations) { + root.addElement(FB_QNAME).addAttribute(LOCATION_QNAME, String.valueOf(next)); + } + for (int next : fsLocations) { + root.addElement(FS_QNAME).addAttribute(LOCATION_QNAME, String.valueOf(next)); + } + for (int next : pfLocations) { + root.addElement(PF_QNAME).addAttribute(LOCATION_QNAME, String.valueOf(next)); + } + for (int next : poLocations) { + root.addElement(PO_QNAME).addAttribute(LOCATION_QNAME, String.valueOf(next)); + } + for (int next : atLocations) { + root.addElement(AT_QNAME).addAttribute(LOCATION_QNAME, String.valueOf(next)); + } + for (int next : acLocations) { + root.addElement(AC_QNAME).addAttribute(LOCATION_QNAME, String.valueOf(next)); + } + root.addNamespace("scenario", SCENARIO_NAMESPACE_URI); + } + + /** + * Apply this scenario to a world model. + * + * @param model The world model to alter. + * @param config The configuration. + * @throws ScenarioException if this scenario is invalid. + */ + public void apply(StandardWorldModel model, Config config) throws ScenarioException { + LOG.debug("Creating " + refuges.size() + " refuges"); + for (int next : refuges) { + LOG.debug("Converting building " + next + " to a refuge"); + Building b = (Building) model.getEntity(new EntityID(next)); + if (b == null) { + throw new ScenarioException("Building " + next + " does not exist"); + } + Refuge r = new Refuge(b); + if (refugeBedCapacity.containsKey(next)) + r.setBedCapacity(refugeBedCapacity.get(next)); + else + throw new ScenarioException("RefugeBedCapacity dose not contains " + next); + if (refugeRefillCapacity.containsKey(next)) + r.setRefillCapacity(refugeRefillCapacity.get(next)); + else + throw new ScenarioException("refugeRefillCapacity dose not contain " + next); + + r.setOccupiedBeds(0); + r.setCapacity(0);// todo + r.setWaitingListSize(0); + model.removeEntity(b); + model.addEntity(r); + LOG.debug("Converted " + b + " into " + r); + } + for (int next : gasStations) { + LOG.debug("Converting building " + next + " to a gas station"); + Building b = (Building) model.getEntity(new EntityID(next)); + if (b == null) { + throw new ScenarioException("Building " + next + " does not exist"); + } + GasStation r = new GasStation(b); + r.setImportance(5); + model.removeEntity(b); + model.addEntity(r); + LOG.debug("Converted " + b + " into " + r); + } + for (int next : hydrants) { + LOG.debug("Converting Road " + next + " to a hydrant"); + Area area = (Area) model.getEntity(new EntityID(next)); + if (area == null || !(area instanceof Road)) { + throw new ScenarioException("Road " + next + " does not exist"); + } + Hydrant h = new Hydrant((Road) area); + model.removeEntity(area); + model.addEntity(h); + LOG.debug("Converted " + area + " into " + h); + } + LOG.debug("Igniting " + fires.size() + " fires"); + for (int next : fires) { + LOG.debug("Igniting " + next); + Building b = (Building) model.getEntity(new EntityID(next)); + if (b == null) { + throw new ScenarioException("Building " + next + " does not exist"); + } + b.setIgnition(true); + } + int lastID = 0; + for (StandardEntity next : model) { + lastID = Math.max(lastID, next.getID().getValue()); + } + LOG.debug("Creating " + fbLocations.size() + " fire brigades"); + + for (int next : fbLocations) { + EntityID id = new EntityID(next); + lastID = getNextId(model, config, lastID); + FireBrigade f = new FireBrigade(new EntityID(lastID)); + setupAgent(f, id, model, config); + } + LOG.debug("Creating " + pfLocations.size() + " police forces"); + for (int next : pfLocations) { + EntityID id = new EntityID(next); + lastID = getNextId(model, config, lastID); + PoliceForce p = new PoliceForce(new EntityID(lastID)); + setupAgent(p, id, model, config); + } + LOG.debug("Creating " + atLocations.size() + " ambulance teams"); + for (int next : atLocations) { + EntityID id = new EntityID(next); + lastID = getNextId(model, config, lastID); + AmbulanceTeam a = new AmbulanceTeam(new EntityID(lastID)); + setupAgent(a, id, model, config); + } + LOG.debug("Creating " + fsLocations.size() + " fire stations"); + for (int next : fsLocations) { + EntityID id = new EntityID(next); + LOG.debug("Coverting building " + next + " to a fire station"); + Building b = (Building) model.getEntity(id); + if (b == null) { + throw new ScenarioException("Building " + next + " does not exist"); + } + FireStation f = new FireStation(b); + model.removeEntity(b); + model.addEntity(f); + LOG.debug("Converted " + b + " into " + f); + } + LOG.debug("Creating " + poLocations.size() + " police offices"); + for (int next : poLocations) { + EntityID id = new EntityID(next); + LOG.debug("Coverting building " + next + " to a police office"); + Building b = (Building) model.getEntity(id); + if (b == null) { + throw new ScenarioException("Building " + next + " does not exist"); + } + PoliceOffice p = new PoliceOffice(b); + model.removeEntity(b); + model.addEntity(p); + LOG.debug("Converted " + b + " into " + p); + } + LOG.debug("Creating " + acLocations.size() + " ambulance centres"); + for (int next : acLocations) { + EntityID id = new EntityID(next); + LOG.debug("Coverting building " + next + " to an ambulance centre"); + Building b = (Building) model.getEntity(id); + if (b == null) { + throw new ScenarioException("Building " + next + " does not exist"); + } + AmbulanceCentre a = new AmbulanceCentre(b); + model.removeEntity(b); + model.addEntity(a); + LOG.debug("Converted " + b + " into " + a); + } + LOG.debug("Creating " + civLocations.size() + " civilians"); + for (int next : civLocations) { + EntityID id = new EntityID(next); + lastID = getNextId(model, config, lastID); + Civilian c = new Civilian(new EntityID(lastID)); + setupAgent(c, id, model, config); + } + } + + private int getNextId(StandardWorldModel model, Config config, int lastId) { + boolean humanRandomId = config.getBooleanValue("senario.human.random-id", true); + if (humanRandomId) { + int newId; + do { + newId = config.getRandom().nextInt(Integer.MAX_VALUE); + } while (model.getEntity(new EntityID(newId)) != null); + return newId; + } else { + return lastId + 1; + } + } + + /** + * Get the set of fire locations. + * + * @return The set of fire locations. + */ + public Set getFires() { + return Collections.unmodifiableSet(fires); + } + + /** + * Get the set of refuge locations. + * + * @return The set of refuge locations. + */ + public Set getRefuges() { + return Collections.unmodifiableSet(refuges); + } + + /** + * Get a refuge by ID. + * + * @param id The ID to look up. + * @return The refuge with that ID or null if the ID is not found. + */ + public GMLRefuge getRefuge(int id) { + return gmlRefuges.get(id); + } + + /** + * Get the set of GMLRefuges. + * + * @return The set of GMLRefuges. + */ + public Map getGMLRefuges() { + return gmlRefuges; + } + + /** + * Get the HashMap of refuge bed capacity. + * + * @return The HashMap of refuge bed capacity. + */ + public HashMap getRefugeBedCapacity() { + return refugeBedCapacity; + } + + /** + * Get the HashMap of refuge refill capacity. + * + * @return The HashMap of refuge refill capacity. + */ + public HashMap getRefugeRefillCapacity() { + return refugeRefillCapacity; + } + + /** + * Get the set of GasStations locations. + * + * @return The set of GasStations locations. + */ + public Set getGasStations() { + return Collections.unmodifiableSet(gasStations); + } + + /** + * Get the set of hydrant locations. + * + * @return The set of hydrant locations. + */ + public Set getHydrants() { + return Collections.unmodifiableSet(hydrants); + } + + /** + * Get the list of civilian locations. + * + * @return The list of civilian locations. + */ + public Collection getCivilians() { + return Collections.unmodifiableCollection(civLocations); + } + + /** + * Get the list of fire brigade locations. + * + * @return The list of fire brigade locations. + */ + public Collection getFireBrigades() { + return Collections.unmodifiableCollection(fbLocations); + } + + /** + * Get the list of fire station locations. + * + * @return The list of fire station locations. + */ + public Collection getFireStations() { + return Collections.unmodifiableCollection(fsLocations); + } + + /** + * Get the list of police force locations. + * + * @return The list of police force locations. + */ + public Collection getPoliceForces() { + return Collections.unmodifiableCollection(pfLocations); + } + + /** + * Get the list of police office locations. + * + * @return The list of police office locations. + */ + public Collection getPoliceOffices() { + return Collections.unmodifiableCollection(poLocations); + } + + /** + * Get the list of ambulance team locations. + * + * @return The list of ambulance team locations. + */ + public Collection getAmbulanceTeams() { + return Collections.unmodifiableCollection(atLocations); + } + + /** + * Get the list of ambulance centre locations. + * + * @return The list of ambulance centre locations. + */ + public Collection getAmbulanceCentres() { + return Collections.unmodifiableCollection(acLocations); + } + + /** + * Set the set of fire locations. + * + * @param newLocations The new set of locations. + */ + public void setFires(Set newLocations) { + fires.clear(); + fires.addAll(newLocations); + } + + /** + * Set the set of refuge locations. + * + * @param newLocations The new set of locations. + */ + public void setRefuges(Set newLocations) { + refuges.clear(); + refuges.addAll(newLocations); + } + + /** + * Set the set of gas station locations. + * + * @param newLocations The new set of locations. + */ + public void setGasStations(Set newLocations) { + gasStations.clear(); + gasStations.addAll(newLocations); + } + + /** + * Set the set of hydrant locations. + * + * @param newLocations The new set of locations. + */ + public void setHydrants(Set newLocations) { + hydrants.clear(); + hydrants.addAll(newLocations); + } + + /** + * Set the list of civilian locations. + * + * @param newLocations The new list of locations. + */ + public void setCivilians(Collection newLocations) { + civLocations.clear(); + civLocations.addAll(newLocations); + } + + /** + * Set the list of fire brigade locations. + * + * @param newLocations The new list of locations. + */ + public void setFireBrigades(Collection newLocations) { + fbLocations.clear(); + fbLocations.addAll(newLocations); + } + + /** + * Set the list of fire station locations. + * + * @param newLocations The new list of locations. + */ + public void setFireStations(Collection newLocations) { + fsLocations.clear(); + fsLocations.addAll(newLocations); + } + + /** + * Set the list of police force locations. + * + * @param newLocations The new list of locations. + */ + public void setPoliceForces(Collection newLocations) { + pfLocations.clear(); + pfLocations.addAll(newLocations); + } + + /** + * Set the list of police office locations. + * + * @param newLocations The new list of locations. + */ + public void setPoliceOffices(Collection newLocations) { + poLocations.clear(); + poLocations.addAll(newLocations); + } + + /** + * Set the list of ambulance team locations. + * + * @param newLocations The new list of locations. + */ + public void setAmbulanceTeams(Collection newLocations) { + atLocations.clear(); + atLocations.addAll(newLocations); + } + + /** + * Set the list of ambulance centre locations. + * + * @param newLocations The new list of locations. + */ + public void setAmbulanceCentres(Collection newLocations) { + acLocations.clear(); + acLocations.addAll(newLocations); + } + + /** + * Add a fire. + * + * @param location The new fire location. + */ + public void addFire(int location) { + fires.add(location); + } + + /** + * Remove a fire. + * + * @param location The fire location to remove. + */ + public void removeFire(int location) { + fires.remove(location); + } + + /** + * Add a refuge. + * + * @param location The new refuge location. + */ + public void addRefuge(int location) { + refuges.add(location); + } + + public void addRefuge(int location, int bedCapacity, int refillCapacity) { + refuges.add(location); + refugeBedCapacity.put(location, bedCapacity); + refugeRefillCapacity.put(location, refillCapacity); + } + + public void addRefuge(int location, int bedCapacity) { + refuges.add(location); + refugeBedCapacity.put(location, bedCapacity); + } + + public void addGMLRefuge(GMLRefuge r) { + gmlRefuges.put(r.getID(), r); + } + + /** + * Remove a refuge. + * + * @param location The refuge location to remove. + */ + public void removeRefuge(int location) { + refuges.remove(location); + if (refugeRefillCapacity.containsKey(location)) { + refugeRefillCapacity.remove(location); + } + if (refugeBedCapacity.containsKey(location)) { + refugeBedCapacity.remove(location); + } + gmlRefuges.remove(location); + } + + /** + * Add a hydrant. + * + * @param location The new hydrant location. + */ + public void addHydrant(int location) { + hydrants.add(location); + } + + /** + * Remove a hydrant. + * + * @param location The hydrant location to remove. + */ + public void removeHydrant(int location) { + hydrants.remove(location); + } + + /** + * Remove a GasStation. + * + * @param location The GasStation location to remove. + */ + public void removeGasStation(int location) { + gasStations.remove(location); + } + + /** + * Add a GasStation. + * + * @param location The new GasStation location. + */ + public void addGasStation(int location) { + gasStations.add(location); + } + + /** + * Add a civilian. + * + * @param location The new civilian location. + */ + public void addCivilian(int location) { + civLocations.add(location); + } + + /** + * Remove a civilian. + * + * @param location The civilian location to remove. + */ + public void removeCivilian(int location) { + civLocations.remove(location); + } + + /** + * Add a fire brigade. + * + * @param location The new fire brigade location. + */ + public void addFireBrigade(int location) { + fbLocations.add(location); + } + + /** + * Remove a fire brigade. + * + * @param location The fire brigade location to remove. + */ + public void removeFireBrigade(int location) { + fbLocations.remove(location); + } + + /** + * Add a fire station. + * + * @param location The new fire station location. + */ + public void addFireStation(int location) { + fsLocations.add(location); + } + + /** + * Remove a fire station. + * + * @param location The fire station location to remove. + */ + public void removeFireStation(int location) { + fsLocations.remove(location); + } + + /** + * Add a police force. + * + * @param location The new police force location. + */ + public void addPoliceForce(int location) { + pfLocations.add(location); + } + + /** + * Remove a police force. + * + * @param location The police force location to remove. + */ + public void removePoliceForce(int location) { + pfLocations.remove(location); + } + + /** + * Add a police office. + * + * @param location The new police office location. + */ + public void addPoliceOffice(int location) { + poLocations.add(location); + } + + /** + * Remove a police office. + * + * @param location The police office location to remove. + */ + public void removePoliceOffice(int location) { + poLocations.remove(location); + } + + /** + * Add an ambulance team. + * + * @param location The new ambulance team location. + */ + public void addAmbulanceTeam(int location) { + atLocations.add(location); + } + + /** + * Remove an ambulance team. + * + * @param location The ambulance team location to remove. + */ + public void removeAmbulanceTeam(int location) { + atLocations.remove(location); + } + + /** + * Add an ambulance centre. + * + * @param location The new ambulance centre location. + */ + public void addAmbulanceCentre(int location) { + acLocations.add(location); + } + + /** + * Remove an ambulance centre. + * + * @param location The ambulance centre location to remove. + */ + public void removeAmbulanceCentre(int location) { + acLocations.remove(location); + } + + private void setupAgent(Human h, EntityID position, StandardWorldModel model, Config config) + throws ScenarioException { + Entity areaEntity = model.getEntity(position); + if (areaEntity == null) { + throw new ScenarioException("Area " + position + " does not exist"); + } + if (!(areaEntity instanceof Area)) { + throw new ScenarioException("Entity " + position + " is not an area: " + areaEntity); + } + Area area = (Area) areaEntity; + h.setX(area.getX()); + h.setY(area.getY()); + h.setPosition(position); + h.setStamina(DEFAULT_STAMINA); + h.setHP(DEFAULT_HP); + h.setDamage(0); + h.setBuriedness(0); + h.setDirection(0); + h.setTravelDistance(0); + h.setPositionHistory(new int[0]); + if (h instanceof FireBrigade) { + ((FireBrigade) h).setWater(config.getIntValue(WATER_QUANTITY_KEY)); + } + model.addEntity(h); + LOG.debug("Created " + h); + } + + @Override + public HashMap getAftershocks() { + return aftershocks; + } +} diff --git a/modules/gis2/src/gis2/ScenarioException.java b/modules/gis2/src/gis2/ScenarioException.java new file mode 100644 index 0000000000000000000000000000000000000000..1c42a80702fe66d24864428c5ba8868a9d8ecd8b --- /dev/null +++ b/modules/gis2/src/gis2/ScenarioException.java @@ -0,0 +1,41 @@ +package gis2; + +/** + * Exception class for problems with scenarios. + */ +public class ScenarioException extends Exception { + /** + * Construct a scenario exception with no information. + */ + public ScenarioException() { + super(); + } + + /** + * Construct a scenario exception with an error message. + * + * @param msg The error message. + */ + public ScenarioException(String msg) { + super(msg); + } + + /** + * Construct a scenario exception that was caused by another exception. + * + * @param cause The cause of this exception. + */ + public ScenarioException(Throwable cause) { + super(cause); + } + + /** + * Construct a scenario exception with an error message and an underlying cause. + * + * @param msg The error message. + * @param cause The cause of this exception. + */ + public ScenarioException(String msg, Throwable cause) { + super(msg, cause); + } +} \ No newline at end of file diff --git a/modules/gis2/src/gis2/scenario/AbstractFunction.java b/modules/gis2/src/gis2/scenario/AbstractFunction.java new file mode 100644 index 0000000000000000000000000000000000000000..05c3c7b7e40d0a5a368cb6fec47cb4dc821e2ac5 --- /dev/null +++ b/modules/gis2/src/gis2/scenario/AbstractFunction.java @@ -0,0 +1,18 @@ +package gis2.scenario; + +/** + * Abstract base class for scenario editing functions. + */ +public abstract class AbstractFunction implements Function { + /** The editor instance. */ + protected ScenarioEditor editor; + + /** + * Construct an AbstractFunction. + * + * @param editor The editor instance. + */ + protected AbstractFunction(ScenarioEditor editor) { + this.editor = editor; + } +} \ No newline at end of file diff --git a/modules/gis2/src/gis2/scenario/AbstractTool.java b/modules/gis2/src/gis2/scenario/AbstractTool.java new file mode 100644 index 0000000000000000000000000000000000000000..510cca0e1ae2bb14ce61b8dde8b69cb27c0a2b25 --- /dev/null +++ b/modules/gis2/src/gis2/scenario/AbstractTool.java @@ -0,0 +1,18 @@ +package gis2.scenario; + +/** + * Abstract base class for scenario editing tools. + */ +public abstract class AbstractTool implements Tool { + /** The scenario editor instance. */ + protected ScenarioEditor editor; + + /** + * Construct an AbstractTool. + * + * @param editor The scenario editor instance. + */ + protected AbstractTool(ScenarioEditor editor) { + this.editor = editor; + } +} \ No newline at end of file diff --git a/modules/gis2/src/gis2/scenario/AgentOverlay.java b/modules/gis2/src/gis2/scenario/AgentOverlay.java new file mode 100644 index 0000000000000000000000000000000000000000..7013ac951fe7ba60f358c8acd7fb9c0517b91805 --- /dev/null +++ b/modules/gis2/src/gis2/scenario/AgentOverlay.java @@ -0,0 +1,134 @@ +package gis2.scenario; + +import java.awt.Color; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.Shape; +import java.awt.geom.Ellipse2D; +import java.util.Map; + +import maps.gml.GMLShape; +import maps.gml.view.Overlay; + +import rescuecore2.misc.collections.LazyMap; +import rescuecore2.misc.gui.ScreenTransform; + +/** + * Overlay for viewing agents in a scenario. + */ +public class AgentOverlay implements Overlay { + private static final int SIZE = 11; + private static final Color CIVILIAN_COLOUR = Color.GREEN; + private static final Color FIRE_BRIGADE_COLOUR = Color.RED; + private static final Color POLICE_FORCE_COLOUR = Color.BLUE; + private static final Color AMBULANCE_TEAM_COLOUR = Color.WHITE; + private static final int OFFSET = 7; + private ScenarioEditor editor; + + /** + * Construct an AgentOverlay. + * + * @param editor The scenario editor. + */ + public AgentOverlay(ScenarioEditor editor) { + this.editor = editor; + } + + @Override + public void render(Graphics2D g, ScreenTransform transform) { + // Count agents in each location + g.setFont(new Font(g.getFont().getName(), Font.BOLD, g.getFont().getSize())); + Map civs = new LazyMap() { + @Override + public Integer createValue() { + return 0; + } + }; + Map fbs = new LazyMap() { + @Override + public Integer createValue() { + return 0; + } + }; + Map pfs = new LazyMap() { + @Override + public Integer createValue() { + return 0; + } + }; + Map ats = new LazyMap() { + @Override + public Integer createValue() { + return 0; + } + }; + for (int next : editor.getScenario().getCivilians()) { + civs.put(next, civs.get(next) + 1); + } + for (int next : editor.getScenario().getFireBrigades()) { + fbs.put(next, fbs.get(next) + 1); + } + for (int next : editor.getScenario().getPoliceForces()) { + pfs.put(next, pfs.get(next) + 1); + } + for (int next : editor.getScenario().getAmbulanceTeams()) { + ats.put(next, ats.get(next) + 1); + } + // Now draw them + for (Map.Entry next : civs.entrySet()) { + GMLShape shape = editor.getMap().getShape(next.getKey()); + int count = next.getValue(); + // int x = transform.xToScreen(shape.getCentreX()); + // int y = transform.yToScreen(shape.getCentreY()) + CIV_OFFSET; + int x = transform.xToScreen(shape.getCentreX()) + OFFSET; + int y = transform.yToScreen(shape.getCentreY()); + // g.drawString(count + " civs", x, y); + paint(g, x, y, CIVILIAN_COLOUR); + g.drawString(count + "", x, y); + + } + for (Map.Entry next : fbs.entrySet()) { + GMLShape shape = editor.getMap().getShape(next.getKey()); + int count = next.getValue(); + // int x = transform.xToScreen(shape.getCentreX()); + // int y = transform.yToScreen(shape.getCentreY()) + FB_OFFSET; + int x = transform.xToScreen(shape.getCentreX()); + int y = transform.yToScreen(shape.getCentreY()) - OFFSET; + // g.drawString(count + " fbs", x, y); + paint(g, x, y, FIRE_BRIGADE_COLOUR); + g.drawString(count + "", x, y); + } + for (Map.Entry next : pfs.entrySet()) { + GMLShape shape = editor.getMap().getShape(next.getKey()); + int count = next.getValue(); + // int x = transform.xToScreen(shape.getCentreX()); + // int y = transform.yToScreen(shape.getCentreY()) + PF_OFFSET; + int x = transform.xToScreen(shape.getCentreX()); + int y = transform.yToScreen(shape.getCentreY()) + OFFSET; + // g.drawString(count + " pfs", x, y); + paint(g, x, y, POLICE_FORCE_COLOUR); + g.drawString(count + "", x, y); + } + for (Map.Entry next : ats.entrySet()) { + GMLShape shape = editor.getMap().getShape(next.getKey()); + int count = next.getValue(); + // int x = transform.xToScreen(shape.getCentreX()); + // int y = transform.yToScreen(shape.getCentreY()) + AT_OFFSET; + int x = transform.xToScreen(shape.getCentreX()) - OFFSET; + int y = transform.yToScreen(shape.getCentreY()); + // g.drawString(count + " ats", x, y); + paint(g, x, y, AMBULANCE_TEAM_COLOUR); + g.drawString(count + "", x, y); + + } + } + + public void paint(Graphics2D g, int x, int y, Color color) { + Shape shape = new Ellipse2D.Double(x - SIZE / 4, y - SIZE, SIZE, SIZE); + g.setColor(color); + g.fill(shape); + g.draw(shape); + g.setColor(Color.black); + } + +} \ No newline at end of file diff --git a/modules/gis2/src/gis2/scenario/CancelledByUserException.java b/modules/gis2/src/gis2/scenario/CancelledByUserException.java new file mode 100644 index 0000000000000000000000000000000000000000..54a7708d36377edfd9db07353973c6a834e738eb --- /dev/null +++ b/modules/gis2/src/gis2/scenario/CancelledByUserException.java @@ -0,0 +1,12 @@ +package gis2.scenario; + +/** + * Exception for indicating the the user has cancelled an operation. + */ +public class CancelledByUserException extends Exception { + /** + * Constructor. + */ + public CancelledByUserException() { + } +} \ No newline at end of file diff --git a/modules/gis2/src/gis2/scenario/ClearAgentsFunction.java b/modules/gis2/src/gis2/scenario/ClearAgentsFunction.java new file mode 100644 index 0000000000000000000000000000000000000000..d6d340eb622d357ee123bb84f8777aafce9615c4 --- /dev/null +++ b/modules/gis2/src/gis2/scenario/ClearAgentsFunction.java @@ -0,0 +1,37 @@ +package gis2.scenario; + +import gis2.GisScenario; + +import java.util.HashSet; + +/** + * Function for removing all agents. + */ +public class ClearAgentsFunction extends AbstractFunction { + /** + * Construct a clear agents function. + * + * @param editor The editor instance. + */ + public ClearAgentsFunction(ScenarioEditor editor) { + super(editor); + } + + @Override + public String getName() { + return "Remove agents"; + } + + @Override + public void execute() { + GisScenario s = editor.getScenario(); + s.setFireBrigades(new HashSet()); + s.setFireStations(new HashSet()); + s.setPoliceForces(new HashSet()); + s.setPoliceOffices(new HashSet()); + s.setAmbulanceTeams(new HashSet()); + s.setAmbulanceCentres(new HashSet()); + editor.setChanged(); + editor.updateOverlays(); + } +} \ No newline at end of file diff --git a/modules/gis2/src/gis2/scenario/ClearAllFunction.java b/modules/gis2/src/gis2/scenario/ClearAllFunction.java new file mode 100644 index 0000000000000000000000000000000000000000..c4f4dd6e3ad0492440c3efb81a980a10558b82b8 --- /dev/null +++ b/modules/gis2/src/gis2/scenario/ClearAllFunction.java @@ -0,0 +1,42 @@ +package gis2.scenario; + +import gis2.GisScenario; + +import java.util.HashSet; + +/** + * Function for removing all agents, fires, civilians and refuges. + */ +public class ClearAllFunction extends AbstractFunction { + /** + * Construct a clear all function. + * + * @param editor The editor instance. + */ + public ClearAllFunction(ScenarioEditor editor) { + super(editor); + } + + @Override + public String getName() { + return "Remove all"; + } + + @Override + public void execute() { + GisScenario s = editor.getScenario(); + s.setFireBrigades(new HashSet()); + s.setFireStations(new HashSet()); + s.setPoliceForces(new HashSet()); + s.setPoliceOffices(new HashSet()); + s.setAmbulanceTeams(new HashSet()); + s.setAmbulanceCentres(new HashSet()); + s.setCivilians(new HashSet()); + s.setFires(new HashSet()); + s.setRefuges(new HashSet()); + s.setGasStations(new HashSet()); + s.setHydrants(new HashSet()); + editor.setChanged(); + editor.updateOverlays(); + } +} \ No newline at end of file diff --git a/modules/gis2/src/gis2/scenario/ClearFiresFunction.java b/modules/gis2/src/gis2/scenario/ClearFiresFunction.java new file mode 100644 index 0000000000000000000000000000000000000000..cb3439b10212bfa06b0e80f2d4ca56f5277ab9ab --- /dev/null +++ b/modules/gis2/src/gis2/scenario/ClearFiresFunction.java @@ -0,0 +1,32 @@ +package gis2.scenario; + +import gis2.GisScenario; + +import java.util.HashSet; + +/** + * Function for removing all fires. + */ +public class ClearFiresFunction extends AbstractFunction { + /** + * Construct a clear fires function. + * + * @param editor The editor instance. + */ + public ClearFiresFunction(ScenarioEditor editor) { + super(editor); + } + + @Override + public String getName() { + return "Remove fires"; + } + + @Override + public void execute() { + GisScenario s = editor.getScenario(); + s.setFires(new HashSet()); + editor.setChanged(); + editor.updateOverlays(); + } +} \ No newline at end of file diff --git a/modules/gis2/src/gis2/scenario/Function.java b/modules/gis2/src/gis2/scenario/Function.java new file mode 100644 index 0000000000000000000000000000000000000000..56376cda6d4933928bec9d0e31a132eb3c9fb6af --- /dev/null +++ b/modules/gis2/src/gis2/scenario/Function.java @@ -0,0 +1,18 @@ +package gis2.scenario; + +/** + * Interface for a scenario editing function. + */ +public interface Function { + /** + * Get the name of this function. + * + * @return The name of the function. + */ + String getName(); + + /** + * Execute this function. + */ + void execute(); +} \ No newline at end of file diff --git a/modules/gis2/src/gis2/scenario/ModifiedFlowLayout.java b/modules/gis2/src/gis2/scenario/ModifiedFlowLayout.java new file mode 100644 index 0000000000000000000000000000000000000000..a254a86edf4c4c6537dae0d3d262e54a2eb52195 --- /dev/null +++ b/modules/gis2/src/gis2/scenario/ModifiedFlowLayout.java @@ -0,0 +1,113 @@ +package gis2.scenario; + +import java.awt.Component; +import java.awt.Container; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.Insets; + +/** + * A modified version of FlowLayout that allows containers using this Layout to + * behave in a reasonable manner when placed inside a JScrollPane + * + * @author Babu Kalakrishnan Modifications by greearb and jzd + */ + +public class ModifiedFlowLayout extends FlowLayout { + public ModifiedFlowLayout() { + super(FlowLayout.LEFT); + } + + public ModifiedFlowLayout(int align) { + super(align); + } + + public ModifiedFlowLayout(int align, int hgap, int vgap) { + super(align, hgap, vgap); + } + + public Dimension minimumLayoutSize(Container target) { + // Size of largest component, so we can resize it in + // either direction with something like a split-pane. + return computeMinSize(target); + } + + public Dimension preferredLayoutSize(Container target) { + return computeSize(target); + } + + private Dimension computeSize(Container target) { + synchronized (target.getTreeLock()) { + int hgap = getHgap(); + int vgap = getVgap(); + int w = target.getWidth(); + + // Let this behave like a regular FlowLayout (single row) + // if the container hasn't been assigned any size yet + if (w == 0) { + w = Integer.MAX_VALUE; + } + + Insets insets = target.getInsets(); + if (insets == null) { + insets = new Insets(0, 0, 0, 0); + } + int reqdWidth = 0; + + int maxwidth = w - (insets.left + insets.right + hgap * 2); + int n = target.getComponentCount(); + int x = 0; + int y = insets.top + vgap; // FlowLayout starts by adding vgap, so + // do that here too. + int rowHeight = 0; + + for (int i = 0; i < n; i++) { + Component c = target.getComponent(i); + if (c.isVisible()) { + Dimension d = c.getPreferredSize(); + if ((x == 0) || ((x + d.width) <= maxwidth)) { + // fits in current row. + if (x > 0) { + x += hgap; + } + x += d.width; + rowHeight = Math.max(rowHeight, d.height); + } else { + // Start of new row + x = d.width; + y += vgap + rowHeight; + rowHeight = d.height; + } + reqdWidth = Math.max(reqdWidth, x); + } + } + y += rowHeight; + y += insets.bottom; + return new Dimension(reqdWidth + insets.left + insets.right, y); + } + } + + private Dimension computeMinSize(Container target) { + synchronized (target.getTreeLock()) { + int minx = Integer.MAX_VALUE; + int miny = Integer.MIN_VALUE; + boolean found_one = false; + int n = target.getComponentCount(); + + for (int i = 0; i < n; i++) { + Component c = target.getComponent(i); + if (c.isVisible()) { + found_one = true; + Dimension d = c.getPreferredSize(); + minx = Math.min(minx, d.width); + miny = Math.min(miny, d.height); + } + } + if (found_one) { + return new Dimension(minx, miny); + } + return new Dimension(0, 0); + } + } + +} \ No newline at end of file diff --git a/modules/gis2/src/gis2/scenario/PlaceAgentsFunction.java b/modules/gis2/src/gis2/scenario/PlaceAgentsFunction.java new file mode 100644 index 0000000000000000000000000000000000000000..9c8eb4ef21d767379fa1c3c98be70c84f012b1d8 --- /dev/null +++ b/modules/gis2/src/gis2/scenario/PlaceAgentsFunction.java @@ -0,0 +1,114 @@ +package gis2.scenario; + +import gis2.GisScenario; + +import java.awt.GridLayout; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +import javax.swing.JCheckBox; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JTextField; + +import maps.gml.GMLShape; + +/** + * Function for placing agents. + */ +public class PlaceAgentsFunction extends AbstractFunction { + private static final int TYPE_FIRE = 0; + private static final int TYPE_POLICE = 1; + private static final int TYPE_AMBULANCE = 2; + private static final int TYPE_CIVILIAN = 3; + + private Random random; + + /** + * Construct a place agents function. + * + * @param editor The editor instance. + */ + public PlaceAgentsFunction(ScenarioEditor editor) { + super(editor); + random = new Random(); + } + + @Override + public String getName() { + return "Place agents"; + } + + @Override + public void execute() { + JPanel panel = new JPanel(new GridLayout(3, 2)); + JTextField numberField = new JTextField("1"); + JComboBox typeCombo = new JComboBox(new String[] { "Fire", "Police", "Ambulance", "Civilian" }); + + JCheckBox buildingBox = new JCheckBox("In buildings?", false); + JCheckBox roadBox = new JCheckBox("In Roads?", true); + JPanel jp = new JPanel(); + jp.add(buildingBox); + jp.add(roadBox); + + panel.add(new JLabel("Type")); + panel.add(typeCombo); + panel.add(new JLabel("Number")); + panel.add(numberField); + panel.add(jp); + List ids = new ArrayList(); + int type = -1; + List all = new ArrayList(); + if (JOptionPane.showConfirmDialog(null, panel, "Add agents", + JOptionPane.OK_CANCEL_OPTION) == JOptionPane.OK_OPTION) { + try { + int number = Integer.parseInt(numberField.getText()); + type = typeCombo.getSelectedIndex(); + if (roadBox.isSelected()) + all.addAll(editor.getMap().getRoads()); + if (buildingBox.isSelected()) + all.addAll(editor.getMap().getBuildings()); + if (all.size() == 0) { + JOptionPane.showMessageDialog(null, "No Area to Place... Please choose In Road or Building...", "Error", + JOptionPane.ERROR_MESSAGE); + return; + } + for (int i = 0; i < number; ++i) { + ids.add(all.get(random.nextInt(all.size())).getID()); + } + } catch (NumberFormatException e) { + e.printStackTrace(); + } + } + GisScenario s = editor.getScenario(); + switch (type) { + case TYPE_FIRE: + for (int id : ids) { + s.addFireBrigade(id); + } + break; + case TYPE_POLICE: + for (int id : ids) { + s.addPoliceForce(id); + } + break; + case TYPE_AMBULANCE: + for (int id : ids) { + s.addAmbulanceTeam(id); + } + break; + case TYPE_CIVILIAN: + for (int id : ids) { + s.addCivilian(id); + } + break; + default: + throw new IllegalArgumentException("Unexpected type: " + type); + } + editor.setChanged(); + editor.updateOverlays(); + } +} \ No newline at end of file diff --git a/modules/gis2/src/gis2/scenario/PlaceAmbulanceCentreTool.java b/modules/gis2/src/gis2/scenario/PlaceAmbulanceCentreTool.java new file mode 100644 index 0000000000000000000000000000000000000000..e51f62d80d6a3a0d8a2e162ed1cef745da1e356b --- /dev/null +++ b/modules/gis2/src/gis2/scenario/PlaceAmbulanceCentreTool.java @@ -0,0 +1,60 @@ +package gis2.scenario; + +import javax.swing.undo.AbstractUndoableEdit; + +import maps.gml.GMLBuilding; +import maps.gml.GMLShape; + +/** + * Tool for placing ambulance centres. + */ +public class PlaceAmbulanceCentreTool extends ShapeTool { + /** + * Construct a PlaceAmbulanceCentreTool. + * + * @param editor The editor instance. + */ + public PlaceAmbulanceCentreTool(ScenarioEditor editor) { + super(editor); + } + + @Override + public String getName() { + return "Place ambulance centre"; + } + + @Override + protected boolean shouldHighlight(GMLShape shape) { + return shape instanceof GMLBuilding; + } + + @Override + protected void processClick(GMLShape shape) { + editor.getScenario().addAmbulanceCentre(shape.getID()); + editor.setChanged(); + editor.updateOverlays(); + editor.addEdit(new AddAmbulanceCentreEdit(shape.getID())); + } + + private class AddAmbulanceCentreEdit extends AbstractUndoableEdit { + private int id; + + public AddAmbulanceCentreEdit(int id) { + this.id = id; + } + + @Override + public void undo() { + super.undo(); + editor.getScenario().removeAmbulanceCentre(id); + editor.updateOverlays(); + } + + @Override + public void redo() { + super.redo(); + editor.getScenario().addAmbulanceCentre(id); + editor.updateOverlays(); + } + } +} \ No newline at end of file diff --git a/modules/gis2/src/gis2/scenario/PlaceAmbulanceTeamTool.java b/modules/gis2/src/gis2/scenario/PlaceAmbulanceTeamTool.java new file mode 100644 index 0000000000000000000000000000000000000000..32119990202b8118589ef49af42ec61a07dcfa54 --- /dev/null +++ b/modules/gis2/src/gis2/scenario/PlaceAmbulanceTeamTool.java @@ -0,0 +1,59 @@ +package gis2.scenario; + +import javax.swing.undo.AbstractUndoableEdit; + +import maps.gml.GMLShape; + +/** + * Tool for placing ambulance teams. + */ +public class PlaceAmbulanceTeamTool extends ShapeTool { + /** + * Construct a PlaceAmbulanceTeamTool. + * + * @param editor The editor instance. + */ + public PlaceAmbulanceTeamTool(ScenarioEditor editor) { + super(editor); + } + + @Override + public String getName() { + return "Place ambulance team"; + } + + @Override + protected boolean shouldHighlight(GMLShape shape) { + return true; + } + + @Override + protected void processClick(GMLShape shape) { + editor.getScenario().addAmbulanceTeam(shape.getID()); + editor.setChanged(); + editor.updateOverlays(); + editor.addEdit(new AddAmbulanceTeamEdit(shape.getID())); + } + + private class AddAmbulanceTeamEdit extends AbstractUndoableEdit { + private int id; + + public AddAmbulanceTeamEdit(int id) { + this.id = id; + } + + @Override + public void undo() { + super.undo(); + editor.getScenario().removeAmbulanceTeam(id); + editor.updateOverlays(); + } + + @Override + public void redo() { + super.redo(); + editor.getScenario().addAmbulanceTeam(id); + editor.updateOverlays(); + } + } +} \ No newline at end of file diff --git a/modules/gis2/src/gis2/scenario/PlaceCivilianTool.java b/modules/gis2/src/gis2/scenario/PlaceCivilianTool.java new file mode 100644 index 0000000000000000000000000000000000000000..662e59be8bcc7210c5b88bc1f35b7043b2da05cf --- /dev/null +++ b/modules/gis2/src/gis2/scenario/PlaceCivilianTool.java @@ -0,0 +1,59 @@ +package gis2.scenario; + +import javax.swing.undo.AbstractUndoableEdit; + +import maps.gml.GMLShape; + +/** + * Tool for placing civilians. + */ +public class PlaceCivilianTool extends ShapeTool { + /** + * Construct a PlaceCivilianTool. + * + * @param editor The editor instance. + */ + public PlaceCivilianTool(ScenarioEditor editor) { + super(editor); + } + + @Override + public String getName() { + return "Place civilian"; + } + + @Override + protected boolean shouldHighlight(GMLShape shape) { + return true; + } + + @Override + protected void processClick(GMLShape shape) { + editor.getScenario().addCivilian(shape.getID()); + editor.setChanged(); + editor.updateOverlays(); + editor.addEdit(new AddCivilianEdit(shape.getID())); + } + + private class AddCivilianEdit extends AbstractUndoableEdit { + private int id; + + public AddCivilianEdit(int id) { + this.id = id; + } + + @Override + public void undo() { + super.undo(); + editor.getScenario().removeCivilian(id); + editor.updateOverlays(); + } + + @Override + public void redo() { + super.redo(); + editor.getScenario().addCivilian(id); + editor.updateOverlays(); + } + } +} \ No newline at end of file diff --git a/modules/gis2/src/gis2/scenario/PlaceFireBrigadeTool.java b/modules/gis2/src/gis2/scenario/PlaceFireBrigadeTool.java new file mode 100644 index 0000000000000000000000000000000000000000..78f2e8523d44d59c000e3c9f0a4afb6d18d68c45 --- /dev/null +++ b/modules/gis2/src/gis2/scenario/PlaceFireBrigadeTool.java @@ -0,0 +1,59 @@ +package gis2.scenario; + +import javax.swing.undo.AbstractUndoableEdit; + +import maps.gml.GMLShape; + +/** + * Tool for placing fire brigades. + */ +public class PlaceFireBrigadeTool extends ShapeTool { + /** + * Construct a PlaceFireBrigadeTool. + * + * @param editor The editor instance. + */ + public PlaceFireBrigadeTool(ScenarioEditor editor) { + super(editor); + } + + @Override + public String getName() { + return "Place fire brigade"; + } + + @Override + protected boolean shouldHighlight(GMLShape shape) { + return true; + } + + @Override + protected void processClick(GMLShape shape) { + editor.getScenario().addFireBrigade(shape.getID()); + editor.setChanged(); + editor.updateOverlays(); + editor.addEdit(new AddFireBrigadeEdit(shape.getID())); + } + + private class AddFireBrigadeEdit extends AbstractUndoableEdit { + private int id; + + public AddFireBrigadeEdit(int id) { + this.id = id; + } + + @Override + public void undo() { + super.undo(); + editor.getScenario().removeFireBrigade(id); + editor.updateOverlays(); + } + + @Override + public void redo() { + super.redo(); + editor.getScenario().addFireBrigade(id); + editor.updateOverlays(); + } + } +} \ No newline at end of file diff --git a/modules/gis2/src/gis2/scenario/PlaceFireStationTool.java b/modules/gis2/src/gis2/scenario/PlaceFireStationTool.java new file mode 100644 index 0000000000000000000000000000000000000000..b4e25be0c7969f890e42a426bddb9b983dcabc23 --- /dev/null +++ b/modules/gis2/src/gis2/scenario/PlaceFireStationTool.java @@ -0,0 +1,60 @@ +package gis2.scenario; + +import javax.swing.undo.AbstractUndoableEdit; + +import maps.gml.GMLBuilding; +import maps.gml.GMLShape; + +/** + * Tool for placing fire stations. + */ +public class PlaceFireStationTool extends ShapeTool { + /** + * Construct a PlaceFireStationTool. + * + * @param editor The editor instance. + */ + public PlaceFireStationTool(ScenarioEditor editor) { + super(editor); + } + + @Override + public String getName() { + return "Place fire station"; + } + + @Override + protected boolean shouldHighlight(GMLShape shape) { + return shape instanceof GMLBuilding; + } + + @Override + protected void processClick(GMLShape shape) { + editor.getScenario().addFireStation(shape.getID()); + editor.setChanged(); + editor.updateOverlays(); + editor.addEdit(new AddFireStationEdit(shape.getID())); + } + + private class AddFireStationEdit extends AbstractUndoableEdit { + private int id; + + public AddFireStationEdit(int id) { + this.id = id; + } + + @Override + public void undo() { + super.undo(); + editor.getScenario().removeFireStation(id); + editor.updateOverlays(); + } + + @Override + public void redo() { + super.redo(); + editor.getScenario().addFireStation(id); + editor.updateOverlays(); + } + } +} \ No newline at end of file diff --git a/modules/gis2/src/gis2/scenario/PlaceFireTool.java b/modules/gis2/src/gis2/scenario/PlaceFireTool.java new file mode 100644 index 0000000000000000000000000000000000000000..00cc15ba6432369ba16e25379da260c9c2cfc316 --- /dev/null +++ b/modules/gis2/src/gis2/scenario/PlaceFireTool.java @@ -0,0 +1,60 @@ +package gis2.scenario; + +import javax.swing.undo.AbstractUndoableEdit; + +import maps.gml.GMLBuilding; +import maps.gml.GMLShape; + +/** + * Tool for placing fires. + */ +public class PlaceFireTool extends ShapeTool { + /** + * Construct a PlaceFireTool. + * + * @param editor The editor instance. + */ + public PlaceFireTool(ScenarioEditor editor) { + super(editor); + } + + @Override + public String getName() { + return "Place fire"; + } + + @Override + protected boolean shouldHighlight(GMLShape shape) { + return shape instanceof GMLBuilding; + } + + @Override + protected void processClick(GMLShape shape) { + editor.getScenario().addFire(shape.getID()); + editor.setChanged(); + editor.updateOverlays(); + editor.addEdit(new AddFireEdit(shape.getID())); + } + + private class AddFireEdit extends AbstractUndoableEdit { + private int id; + + public AddFireEdit(int id) { + this.id = id; + } + + @Override + public void undo() { + super.undo(); + editor.getScenario().removeFire(id); + editor.updateOverlays(); + } + + @Override + public void redo() { + super.redo(); + editor.getScenario().addFire(id); + editor.updateOverlays(); + } + } +} \ No newline at end of file diff --git a/modules/gis2/src/gis2/scenario/PlaceGasStationTool.java b/modules/gis2/src/gis2/scenario/PlaceGasStationTool.java new file mode 100755 index 0000000000000000000000000000000000000000..f9be232b3ea0010f2feb0b587d3be20cd43abc57 --- /dev/null +++ b/modules/gis2/src/gis2/scenario/PlaceGasStationTool.java @@ -0,0 +1,60 @@ +package gis2.scenario; + +import javax.swing.undo.AbstractUndoableEdit; + +import maps.gml.GMLBuilding; +import maps.gml.GMLShape; + +/** + * Tool for placing gasStation. + */ +public class PlaceGasStationTool extends ShapeTool { + /** + * Construct a PlaceGasStationTool. + * + * @param editor The editor instance. + */ + public PlaceGasStationTool(ScenarioEditor editor) { + super(editor); + } + + @Override + public String getName() { + return "Place gas station"; + } + + @Override + protected boolean shouldHighlight(GMLShape shape) { + return shape instanceof GMLBuilding; + } + + @Override + protected void processClick(GMLShape shape) { + editor.getScenario().addGasStation(shape.getID()); + editor.setChanged(); + editor.updateOverlays(); + editor.addEdit(new AddGasStationEdit(shape.getID())); + } + + private class AddGasStationEdit extends AbstractUndoableEdit { + private int id; + + public AddGasStationEdit(int id) { + this.id = id; + } + + @Override + public void undo() { + super.undo(); + editor.getScenario().removeGasStation(id); + editor.updateOverlays(); + } + + @Override + public void redo() { + super.redo(); + editor.getScenario().addGasStation(id); + editor.updateOverlays(); + } + } +} \ No newline at end of file diff --git a/modules/gis2/src/gis2/scenario/PlaceHydrantTool.java b/modules/gis2/src/gis2/scenario/PlaceHydrantTool.java new file mode 100644 index 0000000000000000000000000000000000000000..eb909bd45ec1beb00654ca2ee05283e47f503e53 --- /dev/null +++ b/modules/gis2/src/gis2/scenario/PlaceHydrantTool.java @@ -0,0 +1,60 @@ +package gis2.scenario; + +import javax.swing.undo.AbstractUndoableEdit; + +import maps.gml.GMLRoad; +import maps.gml.GMLShape; + +/** + * Tool for placing refuges. + */ +public class PlaceHydrantTool extends ShapeTool { + /** + * Construct a PlaceHydrantTool. + * + * @param editor The editor instance. + */ + public PlaceHydrantTool(ScenarioEditor editor) { + super(editor); + } + + @Override + public String getName() { + return "Place hydrant"; + } + + @Override + protected boolean shouldHighlight(GMLShape shape) { + return shape instanceof GMLRoad; + } + + @Override + protected void processClick(GMLShape shape) { + editor.getScenario().addHydrant(shape.getID()); + editor.setChanged(); + editor.updateOverlays(); + editor.addEdit(new AddHydrantEdit(shape.getID())); + } + + private class AddHydrantEdit extends AbstractUndoableEdit { + private int id; + + public AddHydrantEdit(int id) { + this.id = id; + } + + @Override + public void undo() { + super.undo(); + editor.getScenario().removeHydrant(id); + editor.updateOverlays(); + } + + @Override + public void redo() { + super.redo(); + editor.getScenario().addHydrant(id); + editor.updateOverlays(); + } + } +} \ No newline at end of file diff --git a/modules/gis2/src/gis2/scenario/PlacePoliceForceTool.java b/modules/gis2/src/gis2/scenario/PlacePoliceForceTool.java new file mode 100644 index 0000000000000000000000000000000000000000..706d9ce92ea9b169be412c6570ad4cad88c035c9 --- /dev/null +++ b/modules/gis2/src/gis2/scenario/PlacePoliceForceTool.java @@ -0,0 +1,59 @@ +package gis2.scenario; + +import javax.swing.undo.AbstractUndoableEdit; + +import maps.gml.GMLShape; + +/** + * Tool for placing police forces. + */ +public class PlacePoliceForceTool extends ShapeTool { + /** + * Construct a PlacePoliceForceTool. + * + * @param editor The editor instance. + */ + public PlacePoliceForceTool(ScenarioEditor editor) { + super(editor); + } + + @Override + public String getName() { + return "Place police force"; + } + + @Override + protected boolean shouldHighlight(GMLShape shape) { + return true; + } + + @Override + protected void processClick(GMLShape shape) { + editor.getScenario().addPoliceForce(shape.getID()); + editor.setChanged(); + editor.updateOverlays(); + editor.addEdit(new AddPoliceForceEdit(shape.getID())); + } + + private class AddPoliceForceEdit extends AbstractUndoableEdit { + private int id; + + public AddPoliceForceEdit(int id) { + this.id = id; + } + + @Override + public void undo() { + super.undo(); + editor.getScenario().removePoliceForce(id); + editor.updateOverlays(); + } + + @Override + public void redo() { + super.redo(); + editor.getScenario().addPoliceForce(id); + editor.updateOverlays(); + } + } +} \ No newline at end of file diff --git a/modules/gis2/src/gis2/scenario/PlacePoliceOfficeTool.java b/modules/gis2/src/gis2/scenario/PlacePoliceOfficeTool.java new file mode 100644 index 0000000000000000000000000000000000000000..49776004fc064124238a3da97a16fb34103ab1cc --- /dev/null +++ b/modules/gis2/src/gis2/scenario/PlacePoliceOfficeTool.java @@ -0,0 +1,60 @@ +package gis2.scenario; + +import javax.swing.undo.AbstractUndoableEdit; + +import maps.gml.GMLBuilding; +import maps.gml.GMLShape; + +/** + * Tool for placing police offices. + */ +public class PlacePoliceOfficeTool extends ShapeTool { + /** + * Construct a PlacePoliceOfficeTool. + * + * @param editor The editor instance. + */ + public PlacePoliceOfficeTool(ScenarioEditor editor) { + super(editor); + } + + @Override + public String getName() { + return "Place police office"; + } + + @Override + protected boolean shouldHighlight(GMLShape shape) { + return shape instanceof GMLBuilding; + } + + @Override + protected void processClick(GMLShape shape) { + editor.getScenario().addPoliceOffice(shape.getID()); + editor.setChanged(); + editor.updateOverlays(); + editor.addEdit(new AddPoliceOfficeEdit(shape.getID())); + } + + private class AddPoliceOfficeEdit extends AbstractUndoableEdit { + private int id; + + public AddPoliceOfficeEdit(int id) { + this.id = id; + } + + @Override + public void undo() { + super.undo(); + editor.getScenario().removePoliceOffice(id); + editor.updateOverlays(); + } + + @Override + public void redo() { + super.redo(); + editor.getScenario().addPoliceOffice(id); + editor.updateOverlays(); + } + } +} \ No newline at end of file diff --git a/modules/gis2/src/gis2/scenario/PlaceRefugeTool.java b/modules/gis2/src/gis2/scenario/PlaceRefugeTool.java new file mode 100755 index 0000000000000000000000000000000000000000..5282a0c4f842feb63b41aceaf9fab92f1b048090 --- /dev/null +++ b/modules/gis2/src/gis2/scenario/PlaceRefugeTool.java @@ -0,0 +1,77 @@ +package gis2.scenario; + +import java.awt.GridLayout; + +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JTextField; +import javax.swing.undo.AbstractUndoableEdit; + +import maps.gml.GMLBuilding; +import maps.gml.GMLShape; + +/** + * Tool for placing refuges. + */ +public class PlaceRefugeTool extends ShapeTool { + /** + * Construct a PlaceRefugeTool. + * + * @param editor The editor instance. + */ + public PlaceRefugeTool(ScenarioEditor editor) { + super(editor); + } + + @Override + public String getName() { + return "Place refuge"; + } + + @Override + protected boolean shouldHighlight(GMLShape shape) { + return shape instanceof GMLBuilding; + } + + @Override + protected void processClick(GMLShape shape) { + JPanel panel = new JPanel(new GridLayout(3, 2)); + JTextField bedNumberField = new JTextField("100"); + panel.add(new JLabel("Insert a bed capacity for the refuge")); + panel.add(bedNumberField); + + if (JOptionPane.showConfirmDialog(null, panel, "Refuge Capacity", + JOptionPane.OK_CANCEL_OPTION) == JOptionPane.OK_OPTION) { + int bedCapacity = Integer.parseInt(bedNumberField.getText()); + int refillCapacity = 1000;// Integer.parseInt(refillNumberField.getText()); + + editor.getScenario().addRefuge(shape.getID(), bedCapacity, refillCapacity); + editor.setChanged(); + editor.updateOverlays(); + editor.addEdit(new AddRefugeEdit(shape.getID())); + } + } + + private class AddRefugeEdit extends AbstractUndoableEdit { + private int id; + + public AddRefugeEdit(int id) { + this.id = id; + } + + @Override + public void undo() { + super.undo(); + editor.getScenario().removeRefuge(id); + editor.updateOverlays(); + } + + @Override + public void redo() { + super.redo(); + editor.getScenario().addRefuge(id); + editor.updateOverlays(); + } + } +} \ No newline at end of file diff --git a/modules/gis2/src/gis2/scenario/RandomHydrantPlacementFunction.java b/modules/gis2/src/gis2/scenario/RandomHydrantPlacementFunction.java new file mode 100644 index 0000000000000000000000000000000000000000..10df614557c92d4e9a4e904be66dd7235ff9fa9c --- /dev/null +++ b/modules/gis2/src/gis2/scenario/RandomHydrantPlacementFunction.java @@ -0,0 +1,73 @@ +package gis2.scenario; + +import gis2.GisScenario; + +import java.awt.GridLayout; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Random; + +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JTextField; + +import maps.gml.GMLMap; +import maps.gml.GMLShape; + +/** + * Function for placing agents. + */ +public class RandomHydrantPlacementFunction extends AbstractFunction { + private Random random; + + /** + * Construct a place agents function. + * + * @param editor The editor instance. + */ + public RandomHydrantPlacementFunction(ScenarioEditor editor) { + super(editor); + random = new Random(); + } + + @Override + public String getName() { + return "Random Hydrant Placement"; + } + + @Override + public void execute() { + JPanel panel = new JPanel(new GridLayout(3, 2)); + JTextField numberField = new JTextField("1"); + GMLMap map = editor.getMap(); + double height = (map.getMaxX() - map.getMinX()); + double width = (map.getMaxY() - map.getMinY()); + int suggestedCount = (int) (height * width / 30000); + panel.add(new JLabel("Number: suggested number:" + suggestedCount)); + panel.add(numberField); + HashSet selectedIds = new HashSet<>(); + List all = new ArrayList(editor.getMap().getRoads()); + if (JOptionPane.showConfirmDialog(null, panel, "Add agents", + JOptionPane.OK_CANCEL_OPTION) == JOptionPane.OK_OPTION) { + GisScenario s = editor.getScenario(); + try { + int number = Integer.parseInt(numberField.getText()); + for (int i = 0; i < number; ++i) { + int id = all.get(random.nextInt(all.size())).getID(); + if (selectedIds.contains(id)) + i--; + else { + s.addHydrant(id); + selectedIds.add(id); + } + } + } catch (NumberFormatException e) { + e.printStackTrace(); + } + } + editor.setChanged(); + editor.updateOverlays(); + } +} \ No newline at end of file diff --git a/modules/gis2/src/gis2/scenario/RandomScenarioGenerator.java b/modules/gis2/src/gis2/scenario/RandomScenarioGenerator.java new file mode 100755 index 0000000000000000000000000000000000000000..95a73511fd8ac0d07fb29c1ab0fd73d562a736cf --- /dev/null +++ b/modules/gis2/src/gis2/scenario/RandomScenarioGenerator.java @@ -0,0 +1,354 @@ +package gis2.scenario; + +import gis2.GisScenario; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Random; + +import maps.MapException; +import maps.MapReader; +import maps.gml.GMLBuilding; +import maps.gml.GMLMap; +import maps.gml.GMLShape; + +import org.dom4j.Document; +import org.dom4j.DocumentHelper; +import org.dom4j.io.OutputFormat; +import org.dom4j.io.XMLWriter; + +/** + * A class for generating random scenarios. + */ +public class RandomScenarioGenerator { + private static final int DEFAULT_MIN_CIVS = 50; + private static final int DEFAULT_MAX_CIVS = 200; + private static final int DEFAULT_MIN_PLATOONS = 0; + private static final int DEFAULT_MAX_PLATOONS = 30; + private static final int DEFAULT_MIN_CENTRES = 0; + private static final int DEFAULT_MAX_CENTRES = 5; + private static final int DEFAULT_MIN_REFUGES = 0; + private static final int DEFAULT_MAX_REFUGES = 5; + private static final int DEFAULT_MIN_FIRES = 1; + private static final int DEFAULT_MAX_FIRES = 10; + + private int minCivs; + private int maxCivs; + private int minFBs; + private int maxFBs; + private int minFSs; + private int maxFSs; + private int minPOs; + private int maxPOs; + private int minPFs; + private int maxPFs; + private int minATs; + private int maxATs; + private int minACs; + private int maxACs; + private int minFires; + private int maxFires; + private int minRefuges; + private int maxRefuges; + + /** + * Construct a RandomScenarioGenerator with default parameters. + */ + public RandomScenarioGenerator() { + minCivs = DEFAULT_MIN_CIVS; + maxCivs = DEFAULT_MAX_CIVS; + minFBs = DEFAULT_MIN_PLATOONS; + maxFBs = DEFAULT_MAX_PLATOONS; + minPFs = DEFAULT_MIN_PLATOONS; + maxPFs = DEFAULT_MAX_PLATOONS; + minATs = DEFAULT_MIN_PLATOONS; + maxATs = DEFAULT_MAX_PLATOONS; + minFSs = DEFAULT_MIN_CENTRES; + maxFSs = DEFAULT_MAX_CENTRES; + minPOs = DEFAULT_MIN_CENTRES; + maxPOs = DEFAULT_MAX_CENTRES; + minACs = DEFAULT_MIN_CENTRES; + maxACs = DEFAULT_MAX_CENTRES; + minFires = DEFAULT_MIN_FIRES; + maxFires = DEFAULT_MAX_FIRES; + minRefuges = DEFAULT_MIN_REFUGES; + maxRefuges = DEFAULT_MAX_REFUGES; + } + + /** + * Entry point. + * + * @param args Command line arguments: [-civ min max] [-fb min + * max] [-fs min max] [-pf min max] [-po min max] [-at min max] [-ac + * min max] [-refuge min max] [-fire min max]. + */ + public static void main(String[] args) { + + if (args.length < 1) { + printUsage(); + return; + } + + String dirName = args[0]; + RandomScenarioGenerator generator = new RandomScenarioGenerator(); + for (int i = 1; i < args.length; ++i) { + if ("-civ".equals(args[i])) { + int min = Integer.parseInt(args[i + 1]); + int max = Integer.parseInt(args[i + 2]); + i += 2; + generator.setCivilians(min, max); + } else if ("-fb".equals(args[i])) { + int min = Integer.parseInt(args[i + 1]); + int max = Integer.parseInt(args[i + 2]); + i += 2; + generator.setFireBrigades(min, max); + } else if ("-fs".equals(args[i])) { + int min = Integer.parseInt(args[i + 1]); + int max = Integer.parseInt(args[i + 2]); + i += 2; + generator.setFireStations(min, max); + } else if ("-pf".equals(args[i])) { + int min = Integer.parseInt(args[i + 1]); + int max = Integer.parseInt(args[i + 2]); + i += 2; + generator.setPoliceForces(min, max); + } else if ("-po".equals(args[i])) { + int min = Integer.parseInt(args[i + 1]); + int max = Integer.parseInt(args[i + 2]); + i += 2; + generator.setPoliceOffices(min, max); + } else if ("-at".equals(args[i])) { + int min = Integer.parseInt(args[i + 1]); + int max = Integer.parseInt(args[i + 2]); + i += 2; + generator.setAmbulanceTeams(min, max); + } else if ("-ac".equals(args[i])) { + int min = Integer.parseInt(args[i + 1]); + int max = Integer.parseInt(args[i + 2]); + i += 2; + generator.setAmbulanceCentres(min, max); + } else if ("-refuge".equals(args[i])) { + int min = Integer.parseInt(args[i + 1]); + int max = Integer.parseInt(args[i + 2]); + i += 2; + generator.setRefuges(min, max); + } else if ("-fire".equals(args[i])) { + int min = Integer.parseInt(args[i + 1]); + int max = Integer.parseInt(args[i + 2]); + i += 2; + generator.setFires(min, max); + } + } + try { + File dir = new File(dirName); + GMLMap map = (GMLMap) MapReader.readMap(new File(dir, "map.gml")); + GisScenario s = generator.makeRandomScenario(map, new Random()); + Document doc = DocumentHelper.createDocument(); + s.write(doc); + XMLWriter writer = new XMLWriter(new FileOutputStream(new File(dir, "scenario.xml")), + OutputFormat.createPrettyPrint()); + writer.write(doc); + writer.flush(); + writer.close(); + } catch (IOException e) { + e.printStackTrace(); + } catch (MapException e) { + e.printStackTrace(); + } + } + + private static void printUsage() { + System.out.println("Usage: Launch Random Scenario Generator [map] [options]"); + System.out.println(); + System.out.println("[map] Map directory to generate random scenario"); + System.out.println(); + System.out.println("[options]"); + System.out.println("-civ\tmin max\tSet the minimum and maximum number of civilians"); + System.out.println("-fb\tmin max\tSet the minimum and maximum number of fire brigades"); + System.out.println("-fs\tmin max\tSet the minimum and maximum number of fire stations"); + System.out.println("-pf\tmin max\tSet the minimum and maximum number of police forces"); + System.out.println("-po\tmin max\tSet the minimum and maximum number of police offices"); + System.out.println("-at\tmin max\tSet the minimum and maximum number of ambulance teams"); + System.out.println("-ac\tmin max\tSet the minimum and maximum number of ambulance centers"); + System.out.println("-refuge\tmin max\tSet the minimum and maximum number of refuges"); + System.out.println("-fire\tmin max\tSet the minimum and maximum number of fires"); + } + + /** + * Set the minimum and maximum number of civilians. + * + * @param min The new minimum. + * @param max The new maximum. + */ + public void setCivilians(int min, int max) { + minCivs = min; + maxCivs = max; + } + + /** + * Set the minimum and maximum number of fire brigades. + * + * @param min The new minimum. + * @param max The new maximum. + */ + public void setFireBrigades(int min, int max) { + minFBs = min; + maxFBs = max; + } + + /** + * Set the minimum and maximum number of fire stations. + * + * @param min The new minimum. + * @param max The new maximum. + */ + public void setFireStations(int min, int max) { + minFSs = min; + maxFSs = max; + } + + /** + * Set the minimum and maximum number of police forces. + * + * @param min The new minimum. + * @param max The new maximum. + */ + public void setPoliceForces(int min, int max) { + minPFs = min; + maxPFs = max; + } + + /** + * Set the minimum and maximum number of police offices. + * + * @param min The new minimum. + * @param max The new maximum. + */ + public void setPoliceOffices(int min, int max) { + minPOs = min; + maxPOs = max; + } + + /** + * Set the minimum and maximum number of ambulance teams. + * + * @param min The new minimum. + * @param max The new maximum. + */ + public void setAmbulanceTeams(int min, int max) { + minATs = min; + maxATs = max; + } + + /** + * Set the minimum and maximum number of ambulance centres. + * + * @param min The new minimum. + * @param max The new maximum. + */ + public void setAmbulanceCentres(int min, int max) { + minACs = min; + maxACs = max; + } + + /** + * Set the minimum and maximum number of refuges. + * + * @param min The new minimum. + * @param max The new maximum. + */ + public void setRefuges(int min, int max) { + minRefuges = min; + maxRefuges = max; + } + + /** + * Set the minimum and maximum number of fires. + * + * @param min The new minimum. + * @param max The new maximum. + */ + public void setFires(int min, int max) { + minFires = min; + maxFires = max; + } + + /** + * Generate a random scenario. + * + * @param map The map to generate a scenario for. + * @param random A source of randomness. + * @return A new Scenario. + */ + public GisScenario makeRandomScenario(GMLMap map, Random random) { + GisScenario result = new GisScenario(); + int civ = random.nextInt(maxCivs - minCivs + 1) + minCivs; + int fb = random.nextInt(maxFBs - minFBs + 1) + minFBs; + int fs = random.nextInt(maxFSs - minFSs + 1) + minFSs; + int pf = random.nextInt(maxPFs - minPFs + 1) + minPFs; + int po = random.nextInt(maxPOs - minPOs + 1) + minPOs; + int at = random.nextInt(maxATs - minATs + 1) + minATs; + int ac = random.nextInt(maxACs - minACs + 1) + minACs; + int fire = random.nextInt(maxFires - minFires + 1) + minFires; + int refuge = random.nextInt(maxRefuges - minRefuges + 1) + minRefuges; + List buildings = new ArrayList(map.getBuildings()); + Collections.shuffle(buildings, random); + Iterator it = buildings.iterator(); + placeRefuges(it, result, refuge); + placeCentres(it, result, fs, po, ac); + placeFires(it, result, fire); + placeAgents(map, result, random, fb, pf, at, civ); + return result; + } + + private void placeRefuges(Iterator it, GisScenario result, int num) { + for (int i = 0; i < num; ++i) { + result.addRefuge(it.next().getID(), 1000, 1000); + } + } + + private void placeCentres(Iterator it, GisScenario result, int fire, int police, int ambulance) { + for (int i = 0; i < fire; ++i) { + result.addFireStation(it.next().getID()); + } + for (int i = 0; i < police; ++i) { + result.addPoliceOffice(it.next().getID()); + } + for (int i = 0; i < ambulance; ++i) { + result.addAmbulanceCentre(it.next().getID()); + } + } + + private void placeFires(Iterator it, GisScenario result, int num) { + for (int i = 0; i < num; ++i) { + result.addFire(it.next().getID()); + } + } + + private void placeAgents(GMLMap map, GisScenario result, Random random, int fire, int police, int ambulance, + int civ) { + List all = new ArrayList(map.getAllShapes()); + List buildings = new ArrayList(map.getBuildings()); + for (int i = 0; i < fire; ++i) { + int id = all.get(random.nextInt(all.size())).getID(); + result.addFireBrigade(id); + } + for (int i = 0; i < police; ++i) { + int id = all.get(random.nextInt(all.size())).getID(); + result.addPoliceForce(id); + } + for (int i = 0; i < ambulance; ++i) { + int id = all.get(random.nextInt(all.size())).getID(); + result.addAmbulanceTeam(id); + } + for (int i = 0; i < civ; ++i) { + int id = buildings.get(random.nextInt(buildings.size())).getID(); + result.addCivilian(id); + } + } +} \ No newline at end of file diff --git a/modules/gis2/src/gis2/scenario/RandomiseFunction.java b/modules/gis2/src/gis2/scenario/RandomiseFunction.java new file mode 100644 index 0000000000000000000000000000000000000000..7fb1dcb9a327e8005d2436c438a333f6727b3cd5 --- /dev/null +++ b/modules/gis2/src/gis2/scenario/RandomiseFunction.java @@ -0,0 +1,40 @@ +package gis2.scenario; + +import gis2.GisScenario; + +import java.util.Random; + +/** + * Function for randomizing a scenario. + */ +public class RandomiseFunction extends AbstractFunction { + private Random random; + + /** + * Construct a randomizer function. + * + * @param editor The editor instance. + */ + public RandomiseFunction(ScenarioEditor editor) { + super(editor); + random = new Random(); + } + + @Override + public String getName() { + return "Randomise"; + } + + @Override + public void execute() { + RandomScenarioGenerator generator = new RandomScenarioGenerator(); + GisScenario s = generator.makeRandomScenario(editor.getMap(), random); + try { + editor.setScenario(editor.getMap(), s); + editor.setChanged(); + editor.updateOverlays(); + } catch (CancelledByUserException e) { + // Ignore + } + } +} \ No newline at end of file diff --git a/modules/gis2/src/gis2/scenario/RemoveAmbulanceCentreTool.java b/modules/gis2/src/gis2/scenario/RemoveAmbulanceCentreTool.java new file mode 100644 index 0000000000000000000000000000000000000000..3f905e62b18bd6927259177725bdb4fa9b2ef775 --- /dev/null +++ b/modules/gis2/src/gis2/scenario/RemoveAmbulanceCentreTool.java @@ -0,0 +1,60 @@ +package gis2.scenario; + +import javax.swing.undo.AbstractUndoableEdit; + +import maps.gml.GMLBuilding; +import maps.gml.GMLShape; + +/** + * Tool for removing ambulance centres. + */ +public class RemoveAmbulanceCentreTool extends ShapeTool { + /** + * Construct a RemoveAmbulanceCentreTool. + * + * @param editor The editor instance. + */ + public RemoveAmbulanceCentreTool(ScenarioEditor editor) { + super(editor); + } + + @Override + public String getName() { + return "Remove ambulance centre"; + } + + @Override + protected boolean shouldHighlight(GMLShape shape) { + return shape instanceof GMLBuilding; + } + + @Override + protected void processClick(GMLShape shape) { + editor.getScenario().removeAmbulanceCentre(shape.getID()); + editor.setChanged(); + editor.updateOverlays(); + editor.addEdit(new RemoveAmbulanceCentreEdit(shape.getID())); + } + + private class RemoveAmbulanceCentreEdit extends AbstractUndoableEdit { + private int id; + + public RemoveAmbulanceCentreEdit(int id) { + this.id = id; + } + + @Override + public void undo() { + super.undo(); + editor.getScenario().addAmbulanceCentre(id); + editor.updateOverlays(); + } + + @Override + public void redo() { + super.redo(); + editor.getScenario().removeAmbulanceCentre(id); + editor.updateOverlays(); + } + } +} \ No newline at end of file diff --git a/modules/gis2/src/gis2/scenario/RemoveAmbulanceTeamTool.java b/modules/gis2/src/gis2/scenario/RemoveAmbulanceTeamTool.java new file mode 100644 index 0000000000000000000000000000000000000000..3094b41e84c952230a4aeeb03291633e069b47d6 --- /dev/null +++ b/modules/gis2/src/gis2/scenario/RemoveAmbulanceTeamTool.java @@ -0,0 +1,59 @@ +package gis2.scenario; + +import javax.swing.undo.AbstractUndoableEdit; + +import maps.gml.GMLShape; + +/** + * Tool for removing ambulance teams. + */ +public class RemoveAmbulanceTeamTool extends ShapeTool { + /** + * Construct a RemoveAmbulanceTeamTool. + * + * @param editor The editor instance. + */ + public RemoveAmbulanceTeamTool(ScenarioEditor editor) { + super(editor); + } + + @Override + public String getName() { + return "Remove ambulance team"; + } + + @Override + protected boolean shouldHighlight(GMLShape shape) { + return true; + } + + @Override + protected void processClick(GMLShape shape) { + editor.getScenario().removeAmbulanceTeam(shape.getID()); + editor.setChanged(); + editor.updateOverlays(); + editor.addEdit(new RemoveAmbulanceTeamEdit(shape.getID())); + } + + private class RemoveAmbulanceTeamEdit extends AbstractUndoableEdit { + private int id; + + public RemoveAmbulanceTeamEdit(int id) { + this.id = id; + } + + @Override + public void undo() { + super.undo(); + editor.getScenario().addAmbulanceTeam(id); + editor.updateOverlays(); + } + + @Override + public void redo() { + super.redo(); + editor.getScenario().removeAmbulanceTeam(id); + editor.updateOverlays(); + } + } +} \ No newline at end of file diff --git a/modules/gis2/src/gis2/scenario/RemoveCivilianTool.java b/modules/gis2/src/gis2/scenario/RemoveCivilianTool.java new file mode 100644 index 0000000000000000000000000000000000000000..5d87dd740b8d7b2004dacf8d57353b6325bda9d2 --- /dev/null +++ b/modules/gis2/src/gis2/scenario/RemoveCivilianTool.java @@ -0,0 +1,59 @@ +package gis2.scenario; + +import javax.swing.undo.AbstractUndoableEdit; + +import maps.gml.GMLShape; + +/** + * Tool for removing civilians. + */ +public class RemoveCivilianTool extends ShapeTool { + /** + * Construct a RemoveCivilianTool. + * + * @param editor The editor instance. + */ + public RemoveCivilianTool(ScenarioEditor editor) { + super(editor); + } + + @Override + public String getName() { + return "Remove civilian"; + } + + @Override + protected boolean shouldHighlight(GMLShape shape) { + return true; + } + + @Override + protected void processClick(GMLShape shape) { + editor.getScenario().removeCivilian(shape.getID()); + editor.setChanged(); + editor.updateOverlays(); + editor.addEdit(new RemoveCivilianEdit(shape.getID())); + } + + private class RemoveCivilianEdit extends AbstractUndoableEdit { + private int id; + + public RemoveCivilianEdit(int id) { + this.id = id; + } + + @Override + public void undo() { + super.undo(); + editor.getScenario().addCivilian(id); + editor.updateOverlays(); + } + + @Override + public void redo() { + super.redo(); + editor.getScenario().removeCivilian(id); + editor.updateOverlays(); + } + } +} \ No newline at end of file diff --git a/modules/gis2/src/gis2/scenario/RemoveFireBrigadeTool.java b/modules/gis2/src/gis2/scenario/RemoveFireBrigadeTool.java new file mode 100644 index 0000000000000000000000000000000000000000..26d513aa0ee4fb98d986b7649ae7839fe50782a1 --- /dev/null +++ b/modules/gis2/src/gis2/scenario/RemoveFireBrigadeTool.java @@ -0,0 +1,59 @@ +package gis2.scenario; + +import javax.swing.undo.AbstractUndoableEdit; + +import maps.gml.GMLShape; + +/** + * Tool for removing fire brigades. + */ +public class RemoveFireBrigadeTool extends ShapeTool { + /** + * Construct a RemoveFireBrigadeTool. + * + * @param editor The editor instance. + */ + public RemoveFireBrigadeTool(ScenarioEditor editor) { + super(editor); + } + + @Override + public String getName() { + return "Remove fire brigade"; + } + + @Override + protected boolean shouldHighlight(GMLShape shape) { + return true; + } + + @Override + protected void processClick(GMLShape shape) { + editor.getScenario().removeFireBrigade(shape.getID()); + editor.setChanged(); + editor.updateOverlays(); + editor.addEdit(new RemoveFireBrigadeEdit(shape.getID())); + } + + private class RemoveFireBrigadeEdit extends AbstractUndoableEdit { + private int id; + + public RemoveFireBrigadeEdit(int id) { + this.id = id; + } + + @Override + public void undo() { + super.undo(); + editor.getScenario().addFireBrigade(id); + editor.updateOverlays(); + } + + @Override + public void redo() { + super.redo(); + editor.getScenario().removeFireBrigade(id); + editor.updateOverlays(); + } + } +} \ No newline at end of file diff --git a/modules/gis2/src/gis2/scenario/RemoveFireStationTool.java b/modules/gis2/src/gis2/scenario/RemoveFireStationTool.java new file mode 100644 index 0000000000000000000000000000000000000000..433dfaf6e45edc7bfa9ffca2bcb9201a0f1fcf56 --- /dev/null +++ b/modules/gis2/src/gis2/scenario/RemoveFireStationTool.java @@ -0,0 +1,60 @@ +package gis2.scenario; + +import javax.swing.undo.AbstractUndoableEdit; + +import maps.gml.GMLBuilding; +import maps.gml.GMLShape; + +/** + * Tool for removing fire stations. + */ +public class RemoveFireStationTool extends ShapeTool { + /** + * Construct a RemoveFireStationTool. + * + * @param editor The editor instance. + */ + public RemoveFireStationTool(ScenarioEditor editor) { + super(editor); + } + + @Override + public String getName() { + return "Remove fire station"; + } + + @Override + protected boolean shouldHighlight(GMLShape shape) { + return shape instanceof GMLBuilding; + } + + @Override + protected void processClick(GMLShape shape) { + editor.getScenario().removeFireStation(shape.getID()); + editor.setChanged(); + editor.updateOverlays(); + editor.addEdit(new RemoveFireStationEdit(shape.getID())); + } + + private class RemoveFireStationEdit extends AbstractUndoableEdit { + private int id; + + public RemoveFireStationEdit(int id) { + this.id = id; + } + + @Override + public void undo() { + super.undo(); + editor.getScenario().addFireStation(id); + editor.updateOverlays(); + } + + @Override + public void redo() { + super.redo(); + editor.getScenario().removeFireStation(id); + editor.updateOverlays(); + } + } +} \ No newline at end of file diff --git a/modules/gis2/src/gis2/scenario/RemoveFireTool.java b/modules/gis2/src/gis2/scenario/RemoveFireTool.java new file mode 100644 index 0000000000000000000000000000000000000000..b6115a568436a89205abff3e9a9d4a8db5d2b725 --- /dev/null +++ b/modules/gis2/src/gis2/scenario/RemoveFireTool.java @@ -0,0 +1,60 @@ +package gis2.scenario; + +import javax.swing.undo.AbstractUndoableEdit; + +import maps.gml.GMLBuilding; +import maps.gml.GMLShape; + +/** + * Tool for removing fires. + */ +public class RemoveFireTool extends ShapeTool { + /** + * Construct a RemoveFireTool. + * + * @param editor The editor instance. + */ + public RemoveFireTool(ScenarioEditor editor) { + super(editor); + } + + @Override + public String getName() { + return "Remove fire"; + } + + @Override + protected boolean shouldHighlight(GMLShape shape) { + return shape instanceof GMLBuilding; + } + + @Override + protected void processClick(GMLShape shape) { + editor.getScenario().removeFire(shape.getID()); + editor.setChanged(); + editor.updateOverlays(); + editor.addEdit(new RemoveFireEdit(shape.getID())); + } + + private class RemoveFireEdit extends AbstractUndoableEdit { + private int id; + + public RemoveFireEdit(int id) { + this.id = id; + } + + @Override + public void undo() { + super.undo(); + editor.getScenario().addFire(id); + editor.updateOverlays(); + } + + @Override + public void redo() { + super.redo(); + editor.getScenario().removeFire(id); + editor.updateOverlays(); + } + } +} \ No newline at end of file diff --git a/modules/gis2/src/gis2/scenario/RemoveGasStationTool.java b/modules/gis2/src/gis2/scenario/RemoveGasStationTool.java new file mode 100644 index 0000000000000000000000000000000000000000..b4160f73861db849e47b6a07ac61112eb0a8d96e --- /dev/null +++ b/modules/gis2/src/gis2/scenario/RemoveGasStationTool.java @@ -0,0 +1,60 @@ +package gis2.scenario; + +import javax.swing.undo.AbstractUndoableEdit; + +import maps.gml.GMLBuilding; +import maps.gml.GMLShape; + +/** + * Tool for removing refuges. + */ +public class RemoveGasStationTool extends ShapeTool { + /** + * Construct a RemoveGasStationTool. + * + * @param editor The editor instance. + */ + public RemoveGasStationTool(ScenarioEditor editor) { + super(editor); + } + + @Override + public String getName() { + return "Remove gas station"; + } + + @Override + protected boolean shouldHighlight(GMLShape shape) { + return shape instanceof GMLBuilding; + } + + @Override + protected void processClick(GMLShape shape) { + editor.getScenario().removeGasStation(shape.getID()); + editor.setChanged(); + editor.updateOverlays(); + editor.addEdit(new RemoveGasStationEdit(shape.getID())); + } + + private class RemoveGasStationEdit extends AbstractUndoableEdit { + private int id; + + public RemoveGasStationEdit(int id) { + this.id = id; + } + + @Override + public void undo() { + super.undo(); + editor.getScenario().addGasStation(id); + editor.updateOverlays(); + } + + @Override + public void redo() { + super.redo(); + editor.getScenario().removeGasStation(id); + editor.updateOverlays(); + } + } +} \ No newline at end of file diff --git a/modules/gis2/src/gis2/scenario/RemoveHydrantTool.java b/modules/gis2/src/gis2/scenario/RemoveHydrantTool.java new file mode 100644 index 0000000000000000000000000000000000000000..d4ddc256c0abf714173ae02d7acf0ba5528de947 --- /dev/null +++ b/modules/gis2/src/gis2/scenario/RemoveHydrantTool.java @@ -0,0 +1,60 @@ +package gis2.scenario; + +import javax.swing.undo.AbstractUndoableEdit; + +import maps.gml.GMLRoad; +import maps.gml.GMLShape; + +/** + * Tool for removing refuges. + */ +public class RemoveHydrantTool extends ShapeTool { + /** + * Construct a RemoveHydrantTool. + * + * @param editor The editor instance. + */ + public RemoveHydrantTool(ScenarioEditor editor) { + super(editor); + } + + @Override + public String getName() { + return "Remove hydrant"; + } + + @Override + protected boolean shouldHighlight(GMLShape shape) { + return shape instanceof GMLRoad; + } + + @Override + protected void processClick(GMLShape shape) { + editor.getScenario().removeHydrant(shape.getID()); + editor.setChanged(); + editor.updateOverlays(); + editor.addEdit(new RemoveHydrantEdit(shape.getID())); + } + + private class RemoveHydrantEdit extends AbstractUndoableEdit { + private int id; + + public RemoveHydrantEdit(int id) { + this.id = id; + } + + @Override + public void undo() { + super.undo(); + editor.getScenario().addHydrant(id); + editor.updateOverlays(); + } + + @Override + public void redo() { + super.redo(); + editor.getScenario().removeHydrant(id); + editor.updateOverlays(); + } + } +} \ No newline at end of file diff --git a/modules/gis2/src/gis2/scenario/RemovePoliceForceTool.java b/modules/gis2/src/gis2/scenario/RemovePoliceForceTool.java new file mode 100644 index 0000000000000000000000000000000000000000..a872a495b6a79db93da3d814fe9113bbeede34fc --- /dev/null +++ b/modules/gis2/src/gis2/scenario/RemovePoliceForceTool.java @@ -0,0 +1,59 @@ +package gis2.scenario; + +import javax.swing.undo.AbstractUndoableEdit; + +import maps.gml.GMLShape; + +/** + * Tool for removing police forces. + */ +public class RemovePoliceForceTool extends ShapeTool { + /** + * Construct a RemovePoliceForceTool. + * + * @param editor The editor instance. + */ + public RemovePoliceForceTool(ScenarioEditor editor) { + super(editor); + } + + @Override + public String getName() { + return "Remove police force"; + } + + @Override + protected boolean shouldHighlight(GMLShape shape) { + return true; + } + + @Override + protected void processClick(GMLShape shape) { + editor.getScenario().removePoliceForce(shape.getID()); + editor.setChanged(); + editor.updateOverlays(); + editor.addEdit(new RemovePoliceForceEdit(shape.getID())); + } + + private class RemovePoliceForceEdit extends AbstractUndoableEdit { + private int id; + + public RemovePoliceForceEdit(int id) { + this.id = id; + } + + @Override + public void undo() { + super.undo(); + editor.getScenario().addPoliceForce(id); + editor.updateOverlays(); + } + + @Override + public void redo() { + super.redo(); + editor.getScenario().removePoliceForce(id); + editor.updateOverlays(); + } + } +} \ No newline at end of file diff --git a/modules/gis2/src/gis2/scenario/RemovePoliceOfficeTool.java b/modules/gis2/src/gis2/scenario/RemovePoliceOfficeTool.java new file mode 100644 index 0000000000000000000000000000000000000000..d24cd360ed943bf2cd5285bde803cc932d49f447 --- /dev/null +++ b/modules/gis2/src/gis2/scenario/RemovePoliceOfficeTool.java @@ -0,0 +1,60 @@ +package gis2.scenario; + +import javax.swing.undo.AbstractUndoableEdit; + +import maps.gml.GMLBuilding; +import maps.gml.GMLShape; + +/** + * Tool for removing police offices. + */ +public class RemovePoliceOfficeTool extends ShapeTool { + /** + * Construct a RemovePoliceOfficeTool. + * + * @param editor The editor instance. + */ + public RemovePoliceOfficeTool(ScenarioEditor editor) { + super(editor); + } + + @Override + public String getName() { + return "Remove police office"; + } + + @Override + protected boolean shouldHighlight(GMLShape shape) { + return shape instanceof GMLBuilding; + } + + @Override + protected void processClick(GMLShape shape) { + editor.getScenario().removePoliceOffice(shape.getID()); + editor.setChanged(); + editor.updateOverlays(); + editor.addEdit(new RemovePoliceOfficeEdit(shape.getID())); + } + + private class RemovePoliceOfficeEdit extends AbstractUndoableEdit { + private int id; + + public RemovePoliceOfficeEdit(int id) { + this.id = id; + } + + @Override + public void undo() { + super.undo(); + editor.getScenario().addPoliceOffice(id); + editor.updateOverlays(); + } + + @Override + public void redo() { + super.redo(); + editor.getScenario().removePoliceOffice(id); + editor.updateOverlays(); + } + } +} \ No newline at end of file diff --git a/modules/gis2/src/gis2/scenario/RemoveRefugeTool.java b/modules/gis2/src/gis2/scenario/RemoveRefugeTool.java new file mode 100644 index 0000000000000000000000000000000000000000..04de06d89fa9d5eb76d09872ce63fabb54cc008a --- /dev/null +++ b/modules/gis2/src/gis2/scenario/RemoveRefugeTool.java @@ -0,0 +1,60 @@ +package gis2.scenario; + +import javax.swing.undo.AbstractUndoableEdit; + +import maps.gml.GMLBuilding; +import maps.gml.GMLShape; + +/** + * Tool for removing refuges. + */ +public class RemoveRefugeTool extends ShapeTool { + /** + * Construct a RemoveRefugeTool. + * + * @param editor The editor instance. + */ + public RemoveRefugeTool(ScenarioEditor editor) { + super(editor); + } + + @Override + public String getName() { + return "Remove refuge"; + } + + @Override + protected boolean shouldHighlight(GMLShape shape) { + return shape instanceof GMLBuilding; + } + + @Override + protected void processClick(GMLShape shape) { + editor.getScenario().removeRefuge(shape.getID()); + editor.setChanged(); + editor.updateOverlays(); + editor.addEdit(new RemoveRefugeEdit(shape.getID())); + } + + private class RemoveRefugeEdit extends AbstractUndoableEdit { + private int id; + + public RemoveRefugeEdit(int id) { + this.id = id; + } + + @Override + public void undo() { + super.undo(); + editor.getScenario().addRefuge(id); + editor.updateOverlays(); + } + + @Override + public void redo() { + super.redo(); + editor.getScenario().removeRefuge(id); + editor.updateOverlays(); + } + } +} \ No newline at end of file diff --git a/modules/gis2/src/gis2/scenario/ScenarioEditor.java b/modules/gis2/src/gis2/scenario/ScenarioEditor.java new file mode 100755 index 0000000000000000000000000000000000000000..5ae933fc2f6627104dbb2092e6ee513ecbffaf61 --- /dev/null +++ b/modules/gis2/src/gis2/scenario/ScenarioEditor.java @@ -0,0 +1,852 @@ +package gis2.scenario; + +import gis2.GisScenario; +import gis2.ScenarioException; +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.event.ActionEvent; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.IOException; +import javax.swing.AbstractAction; +import javax.swing.Action; +import javax.swing.ButtonGroup; +import javax.swing.JCheckBoxMenuItem; +import javax.swing.JFileChooser; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JMenu; +import javax.swing.JMenuBar; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JSplitPane; +import javax.swing.JToggleButton; +import javax.swing.JToolBar; +import javax.swing.SwingUtilities; +import javax.swing.WindowConstants; +import javax.swing.filechooser.FileFilter; +import javax.swing.undo.CannotUndoException; +import javax.swing.undo.UndoManager; +import javax.swing.undo.UndoableEdit; +import maps.MapException; +import maps.MapReader; +import maps.gml.GMLMap; +import maps.gml.GMLRefuge; +import maps.gml.view.DecoratorOverlay; +import maps.gml.view.FilledShapeDecorator; +import maps.gml.view.GMLMapViewer; +import maps.gml.view.GMLObjectInspector; +import org.dom4j.Document; +import org.dom4j.DocumentException; +import org.dom4j.DocumentHelper; +import org.dom4j.io.OutputFormat; +import org.dom4j.io.SAXReader; +import org.dom4j.io.XMLWriter; +import rescuecore2.config.Config; + +/** + * A component for editing scenarios. + */ +public class ScenarioEditor extends JPanel { + + private static final int VIEWER_PREFERRED_SIZE = 500; + private static final int INSPECTOR_PREFERRED_WIDTH = 300; + private static final int INSPECTOR_PREFERRED_HEIGHT = 500; + + private static final Color FIRE_COLOUR = new Color(255, 0, 0, 128); + private static final Color FIRE_STATION_COLOUR = new Color(255, 255, 0); + private static final Color POLICE_OFFICE_COLOUR = new Color(0, 0, 255); + private static final Color AMBULANCE_CENTRE_COLOUR = new Color(255, 255, 255); + private static final Color REFUGE_COLOUR = new Color(0, 128, 0); + private static final Color HYDRANT_COLOUR = new Color(128, 128, 0); + private static final Color GAS_STATION_COLOUR = new Color(255, 128, 0); + + private GMLMap map; + private GMLMapViewer viewer; + private GMLObjectInspector inspector; + private DecoratorOverlay fireOverlay; + private DecoratorOverlay centreOverlay; + private transient AgentOverlay agentOverlay; + private GisScenario scenario; + private Tool currentTool; + private JLabel statusLabel; + + private boolean changed; + + private UndoManager undoManager; + private transient Action undoAction; + private transient Action redoAction; + + private File baseDir; + private File saveFile; + + private FilledShapeDecorator fireDecorator = new FilledShapeDecorator( + FIRE_COLOUR, null, null); + private FilledShapeDecorator fireStationDecorator = new FilledShapeDecorator( + FIRE_STATION_COLOUR, null, null); + private FilledShapeDecorator policeOfficeDecorator = new FilledShapeDecorator( + POLICE_OFFICE_COLOUR, null, null); + private FilledShapeDecorator ambulanceCentreDecorator = new FilledShapeDecorator( + AMBULANCE_CENTRE_COLOUR, null, null); + private FilledShapeDecorator refugeDecorator = new FilledShapeDecorator( + REFUGE_COLOUR, null, null); + private FilledShapeDecorator gasStationDecorator = new FilledShapeDecorator( + GAS_STATION_COLOUR, null, null); + private FilledShapeDecorator hydrantDecorator = new FilledShapeDecorator(null, + HYDRANT_COLOUR, null); + + /** + * Construct a new ScenarioEditor. + * + * @param menuBar + * The menu bar to add menus to. + */ + public ScenarioEditor(JMenuBar menuBar) { + this(menuBar, null, null); + } + + + /** + * Construct a new ScenarioEditor. + * + * @param menuBar + * The menu bar to add menus to. + * @param map + * The GMLMap to view. + * @param scenario + * The scenario to edit. + */ + public ScenarioEditor(JMenuBar menuBar, GMLMap map, GisScenario scenario) { + super(new BorderLayout()); + this.map = map; + this.scenario = scenario; + viewer = new GMLMapViewer(map); + viewer.setPaintNodes(false); + statusLabel = new JLabel("Status"); + fireOverlay = new DecoratorOverlay(); + centreOverlay = new DecoratorOverlay(); + agentOverlay = new AgentOverlay(this); + viewer.addOverlay(fireOverlay); + viewer.addOverlay(centreOverlay); + viewer.addOverlay(agentOverlay); + inspector = new GMLObjectInspector(map); + undoManager = new UndoManager(); + viewer.setPreferredSize( + new Dimension(VIEWER_PREFERRED_SIZE, VIEWER_PREFERRED_SIZE)); + inspector.setPreferredSize( + new Dimension(INSPECTOR_PREFERRED_WIDTH, INSPECTOR_PREFERRED_HEIGHT)); + viewer.setBackground(Color.GRAY); + viewer.getPanZoomListener().setPanOnRightMouse(); + changed = false; + JToolBar fileToolbar = new JToolBar("File"); + JToolBar editToolbar = new JToolBar("Edit"); + JToolBar toolsToolbar = new JToolBar("Tools"); + toolsToolbar.setLayout(new ModifiedFlowLayout()); + JToolBar functionsToolbar = new JToolBar("Functions"); + JMenu fileMenu = new JMenu("File", false); + JMenu editMenu = new JMenu("Edit", false); + JMenu toolsMenu = new JMenu("Tools", false); + JMenu functionsMenu = new JMenu("Functions", false); + + createFileActions(fileMenu, fileToolbar); + createEditActions(editMenu, editToolbar); + createToolActions(toolsMenu, toolsToolbar); + createFunctionActions(functionsMenu, functionsToolbar); + + JSplitPane split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, viewer, + inspector); + add(split, BorderLayout.CENTER); + JPanel toolbars = new JPanel(new ModifiedFlowLayout()); + toolbars.add(fileToolbar); + toolbars.add(editToolbar); + toolbars.add(functionsToolbar); + toolbars.add(toolsToolbar); + add(toolbars, BorderLayout.NORTH); + add(statusLabel, BorderLayout.SOUTH); + menuBar.add(fileMenu); + menuBar.add(editMenu); + menuBar.add(toolsMenu); + menuBar.add(functionsMenu); + + baseDir = new File(System.getProperty("user.dir")); + saveFile = null; + } + + + /** + * Entry point. + * + * @param args + * Command line arguments. + */ + public static void main(String[] args) { + final JFrame frame = new JFrame("Scenario Editor"); + JMenuBar menuBar = new JMenuBar(); + final ScenarioEditor editor = new ScenarioEditor(menuBar); + if (args.length > 0 && args[0].length() > 0) { + try { + editor.load(args[0]); + } catch (CancelledByUserException e) { + return; + } catch (MapException e) { + e.printStackTrace(); + } catch (ScenarioException e) { + e.printStackTrace(); + } catch (rescuecore2.scenario.exceptions.ScenarioException e) { + e.printStackTrace(); + } + } + + frame.setJMenuBar(menuBar); + frame.setContentPane(editor); + frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); + frame.pack(); + frame.addWindowListener(new WindowAdapter() { + + @Override + public void windowClosing(WindowEvent e) { + try { + editor.close(); + frame.setVisible(false); + frame.dispose(); + System.exit(0); + } catch (CancelledByUserException ex) { + frame.setVisible(true); + } + } + }); + frame.setVisible(true); + } + + + /** + * Load a map and scenario by showing a file chooser dialog. + * + * @throws CancelledByUserException + * If the user cancels + * the change due to + * unsaved changes. + * @throws MapException + * If there is a + * problem reading the + * map. + * @throws ScenarioException + * If there is a + * problem reading the + * scenario. + * @throws rescuecore2.scenario.exceptions.ScenarioException + */ + public void load() throws CancelledByUserException, MapException, + ScenarioException, rescuecore2.scenario.exceptions.ScenarioException { + JFileChooser chooser = new JFileChooser(baseDir); + chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + chooser.setFileFilter(new FileFilter() { + + @Override + public boolean accept(File f) { + return f.isDirectory(); + } + + + @Override + public String getDescription() { + return "Directories"; + } + }); + if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) { + load(chooser.getSelectedFile()); + } + } + + + /** + * Load a map and scenario from a directory. + * + * @param filename + * The name of the file to read. + * + * @throws CancelledByUserException + * If the user cancels + * the change due to + * unsaved changes. + * @throws MapException + * If there is a + * problem reading the + * map. + * @throws ScenarioException + * If there is a + * problem reading the + * scenario. + * @throws rescuecore2.scenario.exceptions.ScenarioException + */ + public void load(String filename) + throws CancelledByUserException, MapException, ScenarioException, + rescuecore2.scenario.exceptions.ScenarioException { + load(new File(filename)); + } + + + /** + * Load a map and scenario from a directory. + * + * @param dir + * The directory to read. + * + * @throws CancelledByUserException + * If the user cancels + * the change due to + * unsaved changes. + * @throws MapException + * If there is a + * problem reading the + * map. + * @throws ScenarioException + * If there is a + * problem reading the + * scenario. + * @throws rescuecore2.scenario.exceptions.ScenarioException + */ + public void load(File dir) throws CancelledByUserException, MapException, + ScenarioException, rescuecore2.scenario.exceptions.ScenarioException { + + try (FileReader r = new FileReader(new File(dir, "scenario.xml"))) { + GMLMap newMap = (GMLMap) MapReader.readMap(new File(dir, "map.gml")); + + SAXReader saxReader = new SAXReader(); + Document doc = saxReader.read(r); + GisScenario newScenario = new GisScenario(doc, new Config()); + setScenario(newMap, newScenario); + baseDir = dir; + saveFile = new File(dir, "scenario.xml"); + } catch (IOException | DocumentException e) { + throw new ScenarioException(e); + } + } + + + /** + * Set the map and scenario. + * + * @param newMap + * The new map. + * @param newScenario + * The new scenario. + * + * @throws CancelledByUserException + * If the user cancels the change due to + * unsaved changes. + */ + public void setScenario(GMLMap newMap, GisScenario newScenario) + throws CancelledByUserException { + checkForChanges(); + if (!checkScenario(newMap, newScenario)) { + JOptionPane.showMessageDialog(null, + "The scenario file contained errors."); + return; + } + map = newMap; + scenario = newScenario; + changed = false; + viewer.setMap(map); + inspector.setMap(map); + updateOverlays(); + } + + + public void updateGMLRefuges() { + for (int next : scenario.getRefuges()) { + GMLRefuge refuge = new GMLRefuge(next, map.getBuilding(next).getEdges()); + refuge.setBedCapacity(scenario.getRefugeBedCapacity().get(next)); + refuge.setRefillCapacity(scenario.getRefugeRefillCapacity().get(next)); + scenario.addGMLRefuge(refuge); + } + } + + + /** + * Get the map. + * + * @return The map. + */ + public GMLMap getMap() { + return map; + } + + + /** + * Get the scenario. + * + * @return The scenario. + */ + public GisScenario getScenario() { + return scenario; + } + + + /** + * Save the scenario. + * + * @throws ScenarioException + * If there is a problem saving the scenario. + */ + public void save() throws ScenarioException { + if (saveFile == null) { + saveAs(); + } + if (saveFile != null) { + Document doc = DocumentHelper.createDocument(); + scenario.write(doc); + try { + if (!saveFile.exists()) { + File parent = saveFile.getParentFile(); + if ((!parent.exists()) && (!saveFile.getParentFile().mkdirs())) { + throw new ScenarioException( + "Couldn't create file " + saveFile.getPath()); + } + if (!saveFile.createNewFile()) { + throw new ScenarioException( + "Couldn't create file " + saveFile.getPath()); + } + } + XMLWriter writer = new XMLWriter(new FileOutputStream(saveFile), + OutputFormat.createPrettyPrint()); + writer.write(doc); + writer.flush(); + writer.close(); + } catch (IOException e) { + throw new ScenarioException(e); + } + baseDir = saveFile.getParentFile(); + changed = false; + } + } + + + /** + * Save the scenario. + * + * @throws ScenarioException + * If there is a problem saving the scenario. + */ + public void saveAs() throws ScenarioException { + JFileChooser chooser = new JFileChooser(baseDir); + if (chooser.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) { + saveFile = chooser.getSelectedFile(); + save(); + } + } + + + /** + * Close the editor. + * + * @throws CancelledByUserException + * If the user cancels the close due to unsaved + * changes." + */ + public void close() throws CancelledByUserException { + checkForChanges(); + } + + + /** + * Get the map viewer. + * + * @return The map viewer. + */ + public GMLMapViewer getViewer() { + return viewer; + } + + + /** + * Get the object inspector. + * + * @return The object inspector. + */ + public GMLObjectInspector getInspector() { + return inspector; + } + + + /** + * Register a change to the map. + */ + public void setChanged() { + changed = true; + } + + + /** + * Register an undoable edit. + * + * @param edit + * The edit to add. + */ + public void addEdit(UndoableEdit edit) { + undoManager.addEdit(edit); + undoAction.setEnabled(undoManager.canUndo()); + redoAction.setEnabled(undoManager.canRedo()); + } + + + /** + * Update the overlay views. + */ + public void updateOverlays() { + updateGMLRefuges(); + updateFireOverlay(); + updateCentreOverlay(); + updateAgentOverlay(); + updateStatusLabel(); + viewer.repaint(); + } + + + private void checkForChanges() throws CancelledByUserException { + if (changed) { + switch (JOptionPane.showConfirmDialog(null, + "The current scenario has changes. Do you want to save them?")) { + case JOptionPane.YES_OPTION: + try { + save(); + } catch (ScenarioException e) { + JOptionPane.showMessageDialog(null, e); + throw new CancelledByUserException(); + } + break; + + case JOptionPane.NO_OPTION: + changed = false; + return; + + case JOptionPane.CANCEL_OPTION: + throw new CancelledByUserException(); + + default: + throw new RuntimeException( + "JOptionPane.showConfirmDialog returned something weird"); + } + } + } + + + private void createFileActions(JMenu menu, JToolBar toolbar) { + Action newAction = new AbstractAction("New") { + + @Override + public void actionPerformed(ActionEvent e) { + try { + checkForChanges(); + setScenario(map, new GisScenario()); + } catch (CancelledByUserException ex) { + } + } + }; + Action loadAction = new AbstractAction("Load") { + + @Override + public void actionPerformed(ActionEvent e) { + try { + checkForChanges(); + load(); + } catch (CancelledByUserException ex) { + } catch (MapException | ScenarioException ex) { + JOptionPane.showMessageDialog(null, ex); + } catch (rescuecore2.scenario.exceptions.ScenarioException ex) { + ex.printStackTrace(); + } + } + }; + Action saveAction = new AbstractAction("Save") { + + @Override + public void actionPerformed(ActionEvent e) { + try { + save(); + } catch (ScenarioException ex) { + JOptionPane.showMessageDialog(null, ex); + } + } + }; + Action saveAsAction = new AbstractAction("Save as") { + + @Override + public void actionPerformed(ActionEvent e) { + try { + saveAs(); + } catch (ScenarioException ex) { + JOptionPane.showMessageDialog(null, ex); + } + } + }; + toolbar.add(newAction); + toolbar.add(loadAction); + toolbar.add(saveAction); + toolbar.add(saveAsAction); + menu.add(newAction); + menu.add(loadAction); + menu.add(saveAction); + menu.add(saveAsAction); + } + + + private void createEditActions(JMenu menu, JToolBar toolbar) { + undoAction = new AbstractAction("Undo") { + + @Override + public void actionPerformed(ActionEvent e) { + try { + undoManager.undo(); + } catch (CannotUndoException ex) { + JOptionPane.showMessageDialog(null, ex); + } + setEnabled(undoManager.canUndo()); + redoAction.setEnabled(undoManager.canRedo()); + } + }; + redoAction = new AbstractAction("Redo") { + + @Override + public void actionPerformed(ActionEvent e) { + try { + undoManager.redo(); + } catch (CannotUndoException ex) { + JOptionPane.showMessageDialog(null, ex); + } + setEnabled(undoManager.canRedo()); + undoAction.setEnabled(undoManager.canUndo()); + } + }; + undoAction.setEnabled(false); + redoAction.setEnabled(false); + toolbar.add(undoAction); + toolbar.add(redoAction); + menu.add(undoAction); + menu.add(redoAction); + } + + + private void createToolActions(JMenu menu, JToolBar toolbar) { + ButtonGroup toolbarGroup = new ButtonGroup(); + ButtonGroup menuGroup = new ButtonGroup(); + menu.addSeparator(); + toolbar.addSeparator(); + addTool(new PlaceFireTool(this), menu, toolbar, menuGroup, toolbarGroup); + addTool(new RemoveFireTool(this), menu, toolbar, menuGroup, toolbarGroup); + addTool(new PlaceRefugeTool(this), menu, toolbar, menuGroup, toolbarGroup); + addTool(new RemoveRefugeTool(this), menu, toolbar, menuGroup, toolbarGroup); + addTool(new PlaceGasStationTool(this), menu, toolbar, menuGroup, + toolbarGroup); + addTool(new RemoveGasStationTool(this), menu, toolbar, menuGroup, + toolbarGroup); + addTool(new PlaceHydrantTool(this), menu, toolbar, menuGroup, toolbarGroup); + addTool(new RemoveHydrantTool(this), menu, toolbar, menuGroup, + toolbarGroup); + addTool(new PlaceCivilianTool(this), menu, toolbar, menuGroup, + toolbarGroup); + addTool(new RemoveCivilianTool(this), menu, toolbar, menuGroup, + toolbarGroup); + menu.addSeparator(); + toolbar.addSeparator(); + addTool(new PlaceFireBrigadeTool(this), menu, toolbar, menuGroup, + toolbarGroup); + addTool(new RemoveFireBrigadeTool(this), menu, toolbar, menuGroup, + toolbarGroup); + addTool(new PlacePoliceForceTool(this), menu, toolbar, menuGroup, + toolbarGroup); + addTool(new RemovePoliceForceTool(this), menu, toolbar, menuGroup, + toolbarGroup); + addTool(new PlaceAmbulanceTeamTool(this), menu, toolbar, menuGroup, + toolbarGroup); + addTool(new RemoveAmbulanceTeamTool(this), menu, toolbar, menuGroup, + toolbarGroup); + menu.addSeparator(); + toolbar.addSeparator(); + addTool(new PlaceFireStationTool(this), menu, toolbar, menuGroup, + toolbarGroup); + addTool(new RemoveFireStationTool(this), menu, toolbar, menuGroup, + toolbarGroup); + addTool(new PlacePoliceOfficeTool(this), menu, toolbar, menuGroup, + toolbarGroup); + addTool(new RemovePoliceOfficeTool(this), menu, toolbar, menuGroup, + toolbarGroup); + addTool(new PlaceAmbulanceCentreTool(this), menu, toolbar, menuGroup, + toolbarGroup); + addTool(new RemoveAmbulanceCentreTool(this), menu, toolbar, menuGroup, + toolbarGroup); + } + + + private void createFunctionActions(JMenu menu, JToolBar toolbar) { + addFunction(new RandomiseFunction(this), menu, toolbar); + addFunction(new ClearFiresFunction(this), menu, toolbar); + addFunction(new ClearAgentsFunction(this), menu, toolbar); + addFunction(new ClearAllFunction(this), menu, toolbar); + addFunction(new PlaceAgentsFunction(this), menu, toolbar); + addFunction(new RandomHydrantPlacementFunction(this), menu, toolbar); + } + + + private void addTool(final Tool t, JMenu menu, JToolBar toolbar, + ButtonGroup menuGroup, ButtonGroup toolbarGroup) { + final JToggleButton toggle = new JToggleButton(); + final JCheckBoxMenuItem check = new JCheckBoxMenuItem(); + Action action = new AbstractAction(t.getName()) { + + @Override + public void actionPerformed(ActionEvent e) { + if (currentTool != null) { + currentTool.deactivate(); + } + currentTool = t; + toggle.setSelected(true); + check.setSelected(true); + currentTool.activate(); + } + }; + toggle.setAction(action); + check.setAction(action); + menu.add(check); + toolbar.add(toggle); + menuGroup.add(check); + toolbarGroup.add(toggle); + } + + + private void addFunction(final Function f, JMenu menu, JToolBar toolbar) { + Action action = new AbstractAction(f.getName()) { + + @Override + public void actionPerformed(ActionEvent e) { + f.execute(); + } + }; + toolbar.add(action); + menu.add(action); + } + + + private boolean checkScenario(GMLMap newMap, GisScenario newScenario) { + boolean valid = true; + for (int id : newScenario.getFires()) { + if (newMap.getBuilding(id) == null) { + valid = false; + } + } + for (int id : newScenario.getRefuges()) { + if (newMap.getBuilding(id) == null) { + valid = false; + } + } + for (int id : newScenario.getHydrants()) { + if (newMap.getRoad(id) == null) { + valid = false; + } + } + for (int id : newScenario.getFireStations()) { + if (newMap.getBuilding(id) == null) { + valid = false; + } + } + for (int id : newScenario.getAmbulanceCentres()) { + if (newMap.getBuilding(id) == null) { + valid = false; + } + } + for (int id : newScenario.getPoliceOffices()) { + if (newMap.getBuilding(id) == null) { + valid = false; + } + } + + for (int id : newScenario.getCivilians()) { + if (newMap.getShape(id) == null) { + valid = false; + } + } + for (int id : newScenario.getFireBrigades()) { + if (newMap.getShape(id) == null) { + valid = false; + } + } + for (int id : newScenario.getAmbulanceTeams()) { + if (newMap.getShape(id) == null) { + valid = false; + } + } + for (int id : newScenario.getPoliceForces()) { + if (newMap.getShape(id) == null) { + valid = false; + } + } + return valid; + } + + + private void updateStatusLabel() { + SwingUtilities.invokeLater(new Runnable() { + + @Override + public void run() { + statusLabel.setText(scenario.getFires().size() + " fires, " + + scenario.getRefuges().size() + " refuges, " + + scenario.getHydrants().size() + " hydrants, " + + scenario.getGasStations().size() + " gas stations, " + + scenario.getCivilians().size() + " civilians, " + + scenario.getFireBrigades().size() + " fb, " + + scenario.getFireStations().size() + " fs, " + + scenario.getPoliceForces().size() + " pf, " + + scenario.getPoliceOffices().size() + " po, " + + scenario.getAmbulanceTeams().size() + " at, " + + scenario.getAmbulanceCentres().size() + " ac"); + } + }); + } + + + private void updateFireOverlay() { + fireOverlay.clearAllBuildingDecorators(); + for (int next : scenario.getFires()) { + fireOverlay.setBuildingDecorator(fireDecorator, map.getBuilding(next)); + } + } + + + private void updateCentreOverlay() { + centreOverlay.clearAllBuildingDecorators(); + centreOverlay.clearAllRoadDecorators(); + for (int next : scenario.getFireStations()) { + centreOverlay.setBuildingDecorator(fireStationDecorator, + map.getBuilding(next)); + } + for (int next : scenario.getPoliceOffices()) { + centreOverlay.setBuildingDecorator(policeOfficeDecorator, + map.getBuilding(next)); + } + for (int next : scenario.getAmbulanceCentres()) { + centreOverlay.setBuildingDecorator(ambulanceCentreDecorator, + map.getBuilding(next)); + } + for (int next : scenario.getRefuges()) { + + centreOverlay.setBuildingDecorator(refugeDecorator, + scenario.getRefuge(next)); + } + for (int next : scenario.getGasStations()) { + centreOverlay.setBuildingDecorator(gasStationDecorator, + map.getBuilding(next)); + } + for (int next : scenario.getHydrants()) { + centreOverlay.setRoadDecorator(hydrantDecorator, map.getRoad(next)); + } + } + + + private void updateAgentOverlay() { + } +} \ No newline at end of file diff --git a/modules/gis2/src/gis2/scenario/ShapeTool.java b/modules/gis2/src/gis2/scenario/ShapeTool.java new file mode 100644 index 0000000000000000000000000000000000000000..22a108a7222e975124072dada4e5d34e42d3171b --- /dev/null +++ b/modules/gis2/src/gis2/scenario/ShapeTool.java @@ -0,0 +1,145 @@ +package gis2.scenario; + +import java.awt.Color; +import java.awt.Insets; +import java.awt.Point; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.event.MouseMotionListener; + +import maps.gml.GMLBuilding; +import maps.gml.GMLCoordinates; +import maps.gml.GMLRoad; +import maps.gml.GMLShape; +import maps.gml.GMLSpace; +import maps.gml.view.FilledShapeDecorator; + +/** + * Abstract base class for tools that operate on GML shapes. + */ +public abstract class ShapeTool extends AbstractTool { + private static final Color HIGHLIGHT_COLOUR = new Color(0, 0, 255, 128); + + private Listener listener; + private FilledShapeDecorator highlight; + + private GMLShape highlightShape; + + /** + * Construct a ShapeTool. + * + * @param editor The editor instance. + */ + public ShapeTool(ScenarioEditor editor) { + super(editor); + listener = new Listener(); + highlight = new FilledShapeDecorator(HIGHLIGHT_COLOUR, HIGHLIGHT_COLOUR, HIGHLIGHT_COLOUR); + } + + @Override + public void activate() { + editor.getViewer().addMouseListener(listener); + editor.getViewer().addMouseMotionListener(listener); + highlightShape = null; + } + + @Override + public void deactivate() { + editor.getViewer().removeMouseListener(listener); + editor.getViewer().removeMouseMotionListener(listener); + editor.getViewer().clearAllBuildingDecorators(); + editor.getViewer().clearAllRoadDecorators(); + editor.getViewer().clearAllSpaceDecorators(); + editor.getViewer().repaint(); + } + + /** + * Handle a click on a shape. + * + * @param shape The shape that was clicked. + */ + protected abstract void processClick(GMLShape shape); + + /** + * Find out if a shape should be highlighted or not. Only highlighted shapes can + * be clicked. + * + * @param shape The shape to check. + * @return True if the shape should be highlighted, false otherwise. + */ + protected abstract boolean shouldHighlight(GMLShape shape); + + private void highlight(GMLShape newShape) { + if (!shouldHighlight(newShape)) { + return; + } + if (highlightShape == newShape) { + return; + } + if (highlightShape != null) { + if (highlightShape instanceof GMLBuilding) { + editor.getViewer().clearBuildingDecorator((GMLBuilding) highlightShape); + } + if (highlightShape instanceof GMLRoad) { + editor.getViewer().clearRoadDecorator((GMLRoad) highlightShape); + } + if (highlightShape instanceof GMLSpace) { + editor.getViewer().clearSpaceDecorator((GMLSpace) highlightShape); + } + } + highlightShape = newShape; + if (highlightShape != null) { + if (highlightShape instanceof GMLBuilding) { + editor.getViewer().setBuildingDecorator(highlight, (GMLBuilding) highlightShape); + } + if (highlightShape instanceof GMLRoad) { + editor.getViewer().setRoadDecorator(highlight, (GMLRoad) highlightShape); + } + if (highlightShape instanceof GMLSpace) { + editor.getViewer().setSpaceDecorator(highlight, (GMLSpace) highlightShape); + } + } + editor.getViewer().repaint(); + } + + private class Listener implements MouseListener, MouseMotionListener { + @Override + public void mouseClicked(MouseEvent e) { + if (highlightShape != null && e.getButton() == MouseEvent.BUTTON1) { + processClick(highlightShape); + } + } + + @Override + public void mouseMoved(MouseEvent e) { + Point p = fixEventPoint(e.getPoint()); + GMLCoordinates c = editor.getViewer().getCoordinatesAtPoint(p.x, p.y); + highlight(editor.getMap().findShapeUnder(c.getX(), c.getY())); + } + + @Override + public void mousePressed(MouseEvent e) { + } + + @Override + public void mouseReleased(MouseEvent e) { + } + + @Override + public void mouseDragged(MouseEvent e) { + } + + @Override + public void mouseEntered(MouseEvent e) { + } + + @Override + public void mouseExited(MouseEvent e) { + } + + private Point fixEventPoint(Point p) { + Insets insets = editor.getViewer().getInsets(); + return new Point(p.x - insets.left, p.y - insets.top); + } + } +} \ No newline at end of file diff --git a/modules/gis2/src/gis2/scenario/Tool.java b/modules/gis2/src/gis2/scenario/Tool.java new file mode 100644 index 0000000000000000000000000000000000000000..b0264d7ee0f434fb144d0829377a3bac124aab03 --- /dev/null +++ b/modules/gis2/src/gis2/scenario/Tool.java @@ -0,0 +1,23 @@ +package gis2.scenario; + +/** + * Interface for a scenario editing tool. + */ +public interface Tool { + /** + * Get the name of this tool. + * + * @return The name of the tool. + */ + String getName(); + + /** + * Activate this tool. + */ + void activate(); + + /** + * Deactivate this tool. + */ + void deactivate(); +} \ No newline at end of file diff --git a/modules/human/src/human/ControlledAgentGUI.java b/modules/human/src/human/ControlledAgentGUI.java new file mode 100644 index 0000000000000000000000000000000000000000..e2f52dd710cc7a14bf3b00b555a0b44f2923694d --- /dev/null +++ b/modules/human/src/human/ControlledAgentGUI.java @@ -0,0 +1,350 @@ +package human; + +import rescuecore2.Constants; +import rescuecore2.misc.CommandLineOptions; +import rescuecore2.config.Config; +import rescuecore2.config.ConfigException; +import rescuecore2.connection.ConnectionException; +import rescuecore2.components.Agent; +import rescuecore2.components.ComponentLauncher; +import rescuecore2.components.TCPComponentLauncher; +import rescuecore2.components.ComponentConnectionException; +import rescuecore2.view.ViewComponent; +import rescuecore2.view.ViewListener; +import rescuecore2.view.RenderedObject; +import rescuecore2.messages.control.KVTimestep; +import rescuecore2.log.Logger; + +import rescuecore2.standard.entities.Human; +import rescuecore2.standard.entities.Building; +import rescuecore2.standard.entities.Road; +import rescuecore2.standard.view.StandardWorldModelViewer; +import rescuecore2.standard.components.StandardViewer; + +import java.awt.Dimension; +import java.awt.GridLayout; +import java.awt.BorderLayout; +import javax.swing.JFrame; +import javax.swing.JList; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.BorderFactory; +import javax.swing.AbstractListModel; + +import java.util.List; +import java.util.ArrayList; + +import java.io.IOException; + +/** + GUI for controlled agents. + */ +public class ControlledAgentGUI extends JPanel { + private static final int VIEW_SIZE = 500; + + private List fbs; + private ListListModel fbListModel; + private JList fbList; + private List pfs; + private ListListModel pfListModel; + private JList pfList; + private List ats; + private ListListModel atListModel; + private JList atList; + + /** + Construct a ControlledAgentGUI. + @param view The view of the world. + */ + public ControlledAgentGUI(StandardWorldModelViewer view) { + super(new BorderLayout()); + fbs = new ArrayList(); + fbListModel = new ListListModel(fbs); + fbList = new JList(fbListModel); + pfs = new ArrayList(); + pfListModel = new ListListModel(pfs); + pfList = new JList(pfListModel); + ats = new ArrayList(); + atListModel = new ListListModel(ats); + atList = new JList(atListModel); + // CHECKSTYLE:OFF:MagicNumber + JPanel agents = new JPanel(new GridLayout(3, 1)); + // CHECKSTYLE:ON:MagicNumber + JScrollPane scroll = new JScrollPane(fbList); + scroll.setBorder(BorderFactory.createTitledBorder("Fire brigades")); + agents.add(scroll); + scroll = new JScrollPane(pfList); + scroll.setBorder(BorderFactory.createTitledBorder("Police forces")); + agents.add(scroll); + scroll = new JScrollPane(atList); + scroll.setBorder(BorderFactory.createTitledBorder("Ambulance teams")); + agents.add(scroll); + add(agents, BorderLayout.WEST); + add(view, BorderLayout.CENTER); + view.addViewListener(new ViewListener() { + @Override + public void objectsClicked(ViewComponent view, List objects) { + handleClick(objects); + } + @Override + public void objectsRollover(ViewComponent view, List objects) { + } + }); + } + + /** + Entry point. + @param args Command-line arguments. + */ + public static void main(String[] args) { + Config config = new Config(); + try { + CommandLineOptions.processArgs(args, config); + } + catch (ConfigException e) { + Logger.error("Configuration error", e); + System.exit(-1); + } + catch (IOException e) { + Logger.error("Configuration error", e); + System.exit(-1); + } + StandardWorldModelViewer view = new StandardWorldModelViewer(); + view.setPreferredSize(new Dimension(VIEW_SIZE, VIEW_SIZE)); + ControlledAgentGUI gui = new ControlledAgentGUI(view); + JFrame frame = new JFrame("Controlled agents"); + frame.add(gui); + frame.pack(); + frame.setVisible(true); + + // Connect a viewer and agents + int port = config.getIntValue(Constants.KERNEL_PORT_NUMBER_KEY, Constants.DEFAULT_KERNEL_PORT_NUMBER); + String host = config.getValue(Constants.KERNEL_HOST_NAME_KEY, Constants.DEFAULT_KERNEL_HOST_NAME); + ComponentLauncher launcher = new TCPComponentLauncher(host, port, config); + ControlViewer viewer = new ControlViewer(view, gui); + try { + launcher.connect(viewer); + } + catch (InterruptedException e) { + Logger.error("Interrupted", e); + System.exit(-1); + } + catch (ConnectionException e) { + Logger.error("Viewer connection failed", e); + System.exit(-1); + } + catch (ComponentConnectionException e) { + Logger.error("Viewer connection failed", e); + System.exit(-1); + } + gui.launchAgents(launcher); + } + + private void launchAgents(ComponentLauncher launcher) { + // Connect as many fire brigades, police forces and ambulance teams as possible, but do it in a new thread. + new AgentConnector(launcher).start(); + } + + private void refreshLists() { + fbListModel.refresh(); + pfListModel.refresh(); + atListModel.refresh(); + } + + private void handleClick(List clicked) { + handleFBClick(clicked); + handlePFClick(clicked); + handleATClick(clicked); + } + + private void handleFBClick(List clicked) { + for (RenderedObject next : clicked) { + if (next.getObject() instanceof Building) { + Building b = (Building)next.getObject(); + for (ControlledFireBrigade agent : getSelectedFireBrigades()) { + agent.setTarget(b); + } + break; + } + } + fbListModel.refresh(); + } + + private void handlePFClick(List clicked) { + for (RenderedObject next : clicked) { + if (next.getObject() instanceof Road) { + Road r = (Road)next.getObject(); + for (ControlledPoliceForce agent : getSelectedPoliceForces()) { + agent.setTarget(r); + } + break; + } + } + pfListModel.refresh(); + } + + private void handleATClick(List clicked) { + for (RenderedObject next : clicked) { + if (next.getObject() instanceof Human) { + Human h = (Human)next.getObject(); + for (ControlledAmbulanceTeam agent : getSelectedAmbulanceTeams()) { + agent.setTarget(h); + } + break; + } + } + atListModel.refresh(); + } + + private List getSelectedFireBrigades() { + int[] selected = fbList.getSelectedIndices(); + List agents = new ArrayList(selected.length); + for (int next : selected) { + agents.add(fbs.get(next)); + } + return agents; + } + + private List getSelectedPoliceForces() { + int[] selected = pfList.getSelectedIndices(); + List agents = new ArrayList(selected.length); + for (int next : selected) { + agents.add(pfs.get(next)); + } + return agents; + } + + private List getSelectedAmbulanceTeams() { + int[] selected = atList.getSelectedIndices(); + List agents = new ArrayList(selected.length); + for (int next : selected) { + agents.add(ats.get(next)); + } + return agents; + } + + private static class ControlViewer extends StandardViewer { + private StandardWorldModelViewer view; + private ControlledAgentGUI gui; + + public ControlViewer(StandardWorldModelViewer view, ControlledAgentGUI gui) { + this.view = view; + this.gui = gui; + } + + @Override + protected void postConnect() { + view.view(model); + } + + @Override + protected void handleTimestep(KVTimestep t) { + super.handleTimestep(t); + view.repaint(); + gui.refreshLists(); + } + } + + private static class ListListModel extends AbstractListModel { + private List data; + + public ListListModel(List data) { + this.data = data; + } + + @Override + public int getSize() { + return data.size(); + } + + @Override + public Object getElementAt(int index) { + return data.get(index); + } + + public void refresh() { + fireContentsChanged(this, 0, data.size()); + } + } + + private class AgentConnector extends Thread { + private ComponentLauncher launcher; + + public AgentConnector(ComponentLauncher launcher) { + this.launcher = launcher; + } + + @Override + public void run() { + connectAgents(new FireBrigadeAgentType(), fbs, fbListModel); + connectAgents(new PoliceForceAgentType(), pfs, pfListModel); + connectAgents(new AmbulanceTeamAgentType(), ats, atListModel); + } + + private void connectAgents(AgentType type, List list, ListListModel model) { + int count = 0; + while (true) { + ++count; + T agent = type.createAgent(); + try { + launcher.connect(agent); + list.add(agent); + } + catch (ComponentConnectionException e) { + break; + } + catch (InterruptedException e) { + break; + } + catch (ConnectionException e) { + break; + } + } + model.refresh(); + } + } + + private interface AgentType { + /** + Create an Agent of the right type. + @return A new Agent implementation. + */ + T createAgent(); + } + + private static class FireBrigadeAgentType implements AgentType { + @Override + public ControlledFireBrigade createAgent() { + return new ControlledFireBrigade(); + } + + @Override + public String toString() { + return "fire brigade"; + } + } + + private static class PoliceForceAgentType implements AgentType { + @Override + public ControlledPoliceForce createAgent() { + return new ControlledPoliceForce(); + } + + @Override + public String toString() { + return "police force"; + } + } + + private static class AmbulanceTeamAgentType implements AgentType { + @Override + public ControlledAmbulanceTeam createAgent() { + return new ControlledAmbulanceTeam(); + } + + @Override + public String toString() { + return "ambulance team"; + } + } +} diff --git a/modules/human/src/human/ControlledAmbulanceTeam.java b/modules/human/src/human/ControlledAmbulanceTeam.java new file mode 100644 index 0000000000000000000000000000000000000000..8059c915ed8c13f3950eec3791627b499adf5add --- /dev/null +++ b/modules/human/src/human/ControlledAmbulanceTeam.java @@ -0,0 +1,135 @@ +package human; + +import static rescuecore2.misc.Handy.objectsToIDs; + +import rescuecore2.worldmodel.EntityID; +import rescuecore2.worldmodel.ChangeSet; +import rescuecore2.messages.Command; +import rescuecore2.log.Logger; + +import rescuecore2.standard.entities.StandardEntity; +import rescuecore2.standard.entities.StandardEntityURN; +import rescuecore2.standard.entities.Refuge; +import rescuecore2.standard.entities.Human; +import rescuecore2.standard.entities.AmbulanceTeam; +import rescuecore2.standard.components.StandardAgent; + +import sample.SampleSearch; + +import java.util.Collection; +import java.util.List; +import java.util.EnumSet; + +/** + A basic ambulance team agent that will try to rescue a given target. Once the target is unburied this agent will attempt to load it and transport it to a refuge. If there is no target then this agent does nothing. + */ +public class ControlledAmbulanceTeam extends StandardAgent { + private SampleSearch search; + private Human target; + + /** + Set the target of this ambulance team. + @param target The new target. + */ + public void setTarget(Human target) { + this.target = target; + } + + @Override + protected void think(int time, ChangeSet changed, Collection heard) { + if (target == null) { + Logger.info("Nothing to do."); + return; + } + else { + // Is the target on board? + if (target.getPosition().equals(getID())) { + // Yes + // Are we at a refuge? + if (location() instanceof Refuge) { + sendUnload(time); + return; + } + else { + List path = search.breadthFirstSearch(me().getPosition(), objectsToIDs(model.getEntitiesOfType(StandardEntityURN.REFUGE))); + if (path != null) { + sendMove(time, path); + return; + } + else { + Logger.info("Couldn't plan a path to refuge."); + return; + } + } + } + else { + if (target.getPosition().equals(me().getPosition())) { + // We're at the same location + if (target.getBuriedness() != 0) { + sendRescue(time, target.getID()); + return; + } + else { + // Unburied: try to load + sendLoad(time, target.getID()); + return; + } + } + else { + // Plan a path + List path = search.breadthFirstSearch(me().getPosition(), target.getID()); + if (path != null) { + sendMove(time, path); + return; + } + else { + Logger.info("Couldn't plan a path to target."); + } + } + } + } + } + + @Override + protected EnumSet getRequestedEntityURNsEnum() { + return EnumSet.of(StandardEntityURN.AMBULANCE_TEAM); + } + + /** + Get the location of the entity controlled by this agent. + @return The location of the entity controlled by this agent. + */ + protected StandardEntity location() { + AmbulanceTeam me = me(); + return me.getPosition(model); + } + + @Override + protected void postConnect() { + super.postConnect(); + search = new SampleSearch(model); + } + + @Override + public String toString() { + if (me() == null) { + return "Human controlled ambulance team"; + } + StringBuilder result = new StringBuilder(); + result.append("Human controlled ambulance team "); + result.append(getID()); + result.append(" "); + if (target == null) { + result.append("(no target)"); + } + else { + result.append("target: human "); + result.append(target.getID()); + if (target.getPosition().equals(getID())) { + result.append(" (loaded)"); + } + } + return result.toString(); + } +} + diff --git a/modules/human/src/human/ControlledFireBrigade.java b/modules/human/src/human/ControlledFireBrigade.java new file mode 100644 index 0000000000000000000000000000000000000000..c8c17fefd0cbe5d264869f8302665f19b2c5d764 --- /dev/null +++ b/modules/human/src/human/ControlledFireBrigade.java @@ -0,0 +1,114 @@ +package human; + +import static rescuecore2.misc.Handy.objectsToIDs; + +import rescuecore2.worldmodel.EntityID; +import rescuecore2.worldmodel.ChangeSet; +import rescuecore2.messages.Command; +import rescuecore2.log.Logger; + +import rescuecore2.standard.entities.StandardEntity; +import rescuecore2.standard.entities.StandardEntityURN; +import rescuecore2.standard.entities.Building; +import rescuecore2.standard.entities.Refuge; +import rescuecore2.standard.entities.FireBrigade; +import rescuecore2.standard.components.StandardAgent; + +import sample.SampleSearch; + +import java.util.List; +import java.util.Collection; +import java.util.EnumSet; + +/** + A basic fire brigade agent that will try to extinguish a given target. If the target is a refuge then the fire brigade will attempt to enter the building to replenish water. If there is no target then this agent does nothing. + */ +public class ControlledFireBrigade extends StandardAgent { + private static final int MAX_WATER = 15000; + private static final int EXTINGUISH_DISTANCE = 30000; + private static final int EXTINGUISH_POWER = 1000; + + private SampleSearch search; + private Building target; + + /** + Set the target of this fire brigade. + @param target The new target. + */ + public void setTarget(Building target) { + this.target = target; + } + + @Override + protected void think(int time, ChangeSet changed, Collection heard) { + if (target == null) { + Logger.info("Nothing to do"); + return; + } + if (target instanceof Refuge) { + // Just go there + List path = search.breadthFirstSearch(me().getPosition(), target.getID()); + if (path != null) { + sendMove(time, path); + return; + } + else { + Logger.info("Couldn't plan a path to refuge."); + } + } + // Are we close enough to extinguish? + int distance = model.getDistance(me(), target); + if (distance < EXTINGUISH_DISTANCE) { + sendExtinguish(time, target.getID(), EXTINGUISH_POWER); + return; + } + // Otherwise plan a path + if (!target.equals(location())) { + List path = planPathToFire(); + if (path != null) { + sendMove(time, path); + return; + } + else { + Logger.info("Couldn't plan a path to target."); + } + } + } + + private List planPathToFire() { + // Try to get to anything within EXTINGUISH_DISTANCE of the target + Collection targets = model.getObjectsInRange(target, EXTINGUISH_DISTANCE); + if (targets.isEmpty()) { + return null; + } + return search.breadthFirstSearch(me().getPosition(), objectsToIDs(targets)); + } + + @Override + protected EnumSet getRequestedEntityURNsEnum() { + return EnumSet.of(StandardEntityURN.FIRE_BRIGADE); + } + + /** + Get the location of the entity controlled by this agent. + @return The location of the entity controlled by this agent. + */ + protected StandardEntity location() { + FireBrigade me = me(); + return me.getPosition(model); + } + + @Override + protected void postConnect() { + super.postConnect(); + search = new SampleSearch(model); + } + + @Override + public String toString() { + if (me() == null) { + return "Human controlled fire brigade"; + } + return "Human controlled fire brigade " + me().getID() + " (" + me().getWater() + " water)" + (target == null ? " (no target)" : " target: building " + target.getID()); + } +} \ No newline at end of file diff --git a/modules/human/src/human/ControlledPoliceForce.java b/modules/human/src/human/ControlledPoliceForce.java new file mode 100644 index 0000000000000000000000000000000000000000..72a1f2a9f8479cf73c1f862ff6dd608336f6dc2b --- /dev/null +++ b/modules/human/src/human/ControlledPoliceForce.java @@ -0,0 +1,125 @@ +package human; + +import rescuecore2.worldmodel.EntityID; +import rescuecore2.worldmodel.ChangeSet; +import rescuecore2.messages.Command; +import rescuecore2.misc.Pair; +import rescuecore2.log.Logger; + +import rescuecore2.standard.entities.StandardEntity; +import rescuecore2.standard.entities.StandardEntityURN; +import rescuecore2.standard.entities.Area; +import rescuecore2.standard.entities.Road; +import rescuecore2.standard.entities.PoliceForce; +import rescuecore2.standard.components.StandardAgent; + +import sample.SampleSearch; + +import java.util.Collection; +import java.util.List; +import java.util.EnumSet; + +/** + A basic police force agent that will try to clear a given target. Fully-blocked roads encountered along the way are also cleared. If there is no target then this agent does nothing. +*/ +public class ControlledPoliceForce extends StandardAgent { + private SampleSearch search; + private Road target; + + /** + Set the target for this police force. + @param target The new target. + */ + public void setTarget(Road target) { + this.target = target; + } + + @Override + protected void think(int time, ChangeSet changed, Collection heard) { + if (location() instanceof Road) { + Road r = (Road)location(); + EntityID nearest = getNearestBlockade(); + if (nearest != null) { + sendClear(time, nearest); + return; + } + } + if (target == null) { + Logger.info("Nothing to do."); + return; + } + List path = search.breadthFirstSearch(me().getPosition(), target.getID()); + if (path != null) { + sendMove(time, path); + return; + } + else { + Logger.info("Couldn't plan a path to target."); + } + } + + @Override + protected EnumSet getRequestedEntityURNsEnum() { + return EnumSet.of(StandardEntityURN.POLICE_FORCE); + } + + /** + Get the location of the entity controlled by this agent. + @return The location of the entity controlled by this agent. + */ + protected StandardEntity location() { + PoliceForce me = me(); + return me.getPosition(model); + } + + @Override + protected void postConnect() { + super.postConnect(); + search = new SampleSearch(model); + } + + @Override + public String toString() { + if (me() == null) { + return "Human controlled police force"; + } + return "Human controlled police force " + getID() + (target == null ? " (no target)" : " target: road " + target.getID() + " with " + (target.isBlockadesDefined() ? " unknown" : String.valueOf(target.getBlockades().size())) + " blockades"); + } + + /** + Get the blockade that is nearest this agent. + @return The EntityID of the nearest blockade, or null if there are no blockades in the agents current location. + */ + public EntityID getNearestBlockade() { + return getNearestBlockade((Area)location(), me().getX(), me().getY()); + } + + /** + Get the blockade that is nearest a point. + @param area The area to check. + @param x The X coordinate to look up. + @param y The X coordinate to look up. + @return The EntityID of the nearest blockade, or null if there are no blockades in this area. + */ + public EntityID getNearestBlockade(Area area, int x, int y) { + double bestDistance = 0; + EntityID best = null; + if (area.isBlockadesDefined()) { + for (EntityID blockadeID : area.getBlockades()) { + StandardEntity entity = model.getEntity(blockadeID); + Pair location = entity.getLocation(model); + if (location == null) { + continue; + } + double dx = location.first() - x; + double dy = location.second() - y; + double distance = Math.hypot(dx, dy); + if (best == null || distance < bestDistance) { + bestDistance = distance; + best = entity.getID(); + } + } + } + return best; + } +} diff --git a/modules/ignition/src/ignition/IgnitionModel.java b/modules/ignition/src/ignition/IgnitionModel.java new file mode 100644 index 0000000000000000000000000000000000000000..a8232be89cd049ceae0e340800b14a28698ccf11 --- /dev/null +++ b/modules/ignition/src/ignition/IgnitionModel.java @@ -0,0 +1,19 @@ +package ignition; + +import rescuecore2.standard.entities.StandardWorldModel; +import rescuecore2.standard.entities.Building; + +import java.util.Set; + +/** + A model for determining which buildings ignite at any timestep. + */ +public interface IgnitionModel { + /** + Find out which buildings have ignited. + @param world The world model. + @param time The current time. + @return A list of newly ignited buildings. + */ + Set findIgnitionPoints(StandardWorldModel world, int time); +} diff --git a/modules/ignition/src/ignition/IgnitionSimulator.java b/modules/ignition/src/ignition/IgnitionSimulator.java new file mode 100644 index 0000000000000000000000000000000000000000..c0df3584f5a7c7380000956842a609bff4616bde --- /dev/null +++ b/modules/ignition/src/ignition/IgnitionSimulator.java @@ -0,0 +1,83 @@ +package ignition; + +import rescuecore2.worldmodel.ChangeSet; +import rescuecore2.messages.control.KSCommands; +import rescuecore2.log.Logger; + +import rescuecore2.standard.components.StandardSimulator; +import rescuecore2.standard.entities.Building; +import rescuecore2.standard.entities.GasStation; +import rescuecore2.standard.entities.StandardEntity; +import rescuecore2.standard.entities.StandardEntityURN; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +/** + A simulator that determines when new building fires begin. +*/ +public class IgnitionSimulator extends StandardSimulator { + private IgnitionModel ignitionModel; + private int GAS_STATION_EXPLOSION_RANG; + private List notIgnaitedGasStations; + @Override + protected void postConnect() { + super.postConnect(); + ignitionModel = new RandomIgnitionModel(model, config); + GAS_STATION_EXPLOSION_RANG=config.getIntValue("ignition.gas_station.explosion.range"); + notIgnaitedGasStations=new ArrayList(); + for (StandardEntity entity : model.getEntitiesOfType(StandardEntityURN.GAS_STATION)) { + notIgnaitedGasStations.add((GasStation) entity); + } + } + + @Override + protected void processCommands(KSCommands c, ChangeSet changes) { + long start = System.currentTimeMillis(); + int time = c.getTime(); + Logger.info("Timestep " + time); + + explosionGasStations(changes); + + Logger.info("Ignating after shock "); + // Find out which buildings have ignited. + Set buildings = ignitionModel.findIgnitionPoints(model, c.getTime()); + for (Building next : buildings) { + Logger.info("Igniting " + next); + next.setIgnition(true); + changes.addChange(next, next.getIgnitionProperty()); + } + long end = System.currentTimeMillis(); + Logger.info("Timestep " + time + " took " + (end - start) + " ms"); + } + + private void explosionGasStations(ChangeSet changes) { + Logger.info("explosion Gas Stations "); + for (Iterator iterator= notIgnaitedGasStations.iterator(); iterator.hasNext();) { + GasStation gasStation = iterator.next(); + if(gasStation.isFierynessDefined()&&gasStation.getFieryness()==1){ + explode(gasStation,changes); + iterator.remove(); + } + } + } + + private void explode(GasStation gasStation, ChangeSet changes) { + Logger.info(gasStation+" Ignited ==> explosion" ); + for (StandardEntity rangeEntity : model.getObjectsInRange(gasStation, GAS_STATION_EXPLOSION_RANG)) { + if(rangeEntity instanceof Building){ + Building rangeBuilding = (Building)rangeEntity; + Logger.info("Igniting " + rangeBuilding); + rangeBuilding.setIgnition(true); + changes.addChange(rangeBuilding, rangeBuilding.getIgnitionProperty()); + } + } + } + + @Override + public String getName() { + return "Ignition simulator"; + } +} \ No newline at end of file diff --git a/modules/ignition/src/ignition/RandomIgnitionModel.java b/modules/ignition/src/ignition/RandomIgnitionModel.java new file mode 100644 index 0000000000000000000000000000000000000000..3655dc3d36f8d35c7aa289b188ee65cb59bd64bf --- /dev/null +++ b/modules/ignition/src/ignition/RandomIgnitionModel.java @@ -0,0 +1,58 @@ +package ignition; + +import rescuecore2.config.Config; +import rescuecore2.log.Logger; + +import rescuecore2.standard.entities.StandardWorldModel; +import rescuecore2.standard.entities.StandardEntity; +import rescuecore2.standard.entities.Building; + +import java.util.List; +import java.util.ArrayList; +import java.util.Set; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Collections; + +import org.uncommons.maths.random.PoissonGenerator; + +/** + An IgnitionModel that ignites unburnt buildings in a random order. The number of ignitions per timestep is drawn from a Poisson distribution. +*/ +public class RandomIgnitionModel implements IgnitionModel { + private static final String MEAN_KEY = "ignition.random.lambda"; + + private PoissonGenerator generator; + private Iterator it; + + /** + Construct a RandomIgnitionModel. + @param world The world model. + @param config The system configuration. + */ + public RandomIgnitionModel(StandardWorldModel world, Config config) { + List unburnt = new ArrayList(); + for (StandardEntity next : world) { + if (next instanceof Building) { + unburnt.add((Building)next); + } + } + Collections.shuffle(unburnt, config.getRandom()); + double mean = config.getFloatValue(MEAN_KEY); + generator = new PoissonGenerator(mean, config.getRandom()); + it = unburnt.iterator(); + } + + @Override + public Set findIgnitionPoints(StandardWorldModel world, int time) { + Set result = new HashSet(); + if (it.hasNext()) { + int number = generator.nextValue(); + Logger.debug("Igniting " + number + " buildings"); + for (int i = 0; i < number && it.hasNext(); ++i) { + result.add(it.next()); + } + } + return result; + } +} diff --git a/modules/kernel/src/kernel/AbstractCommandFilter.java b/modules/kernel/src/kernel/AbstractCommandFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..caa582c24347f95ce04198fde116c603b756e2d9 --- /dev/null +++ b/modules/kernel/src/kernel/AbstractCommandFilter.java @@ -0,0 +1,33 @@ +package kernel; + +import java.util.Collection; +import java.util.Iterator; + +import rescuecore2.config.Config; +import rescuecore2.messages.Command; + +/** + An abstract base class for command filters. + */ +public abstract class AbstractCommandFilter implements CommandFilter { + @Override + public void initialise(Config config) { + } + + @Override + public void filter(Collection commands, KernelState state) { + for (Iterator it = commands.iterator(); it.hasNext();) { + if (!allowed(it.next(), state)) { + it.remove(); + } + } + } + + /** + Find out if a particular command is allowed. + @param command The command. + @param state The kernel state. + @return True iff the command is allowed. + */ + protected abstract boolean allowed(Command command, KernelState state); +} diff --git a/modules/kernel/src/kernel/AbstractCommunicationModel.java b/modules/kernel/src/kernel/AbstractCommunicationModel.java new file mode 100644 index 0000000000000000000000000000000000000000..c8b308c0cdb0a4d68864413375ec06a34d949d4b --- /dev/null +++ b/modules/kernel/src/kernel/AbstractCommunicationModel.java @@ -0,0 +1,65 @@ +package kernel; + +import java.util.Collection; +import java.util.List; +import java.util.LinkedList; +import java.util.Map; +import java.util.Arrays; + +import rescuecore2.messages.Command; +import rescuecore2.config.Config; +import rescuecore2.worldmodel.Entity; +import rescuecore2.worldmodel.WorldModel; +import rescuecore2.misc.collections.LazyMap; + +/** + Abstract base class for communication models. + */ +public abstract class AbstractCommunicationModel implements CommunicationModel { + private Map> hearing; + + /** + Construct an AbstractCommunicationModel. + */ + public AbstractCommunicationModel() { + hearing = new LazyMap>() { + @Override + public List createValue() { + return new LinkedList(); + } + }; + } + + @Override + public void initialise(Config config, WorldModel model) { + hearing.clear(); + } + + @Override + public void process(int time, Collection agentCommands) { + hearing.clear(); + } + + @Override + public Collection getHearing(Entity agent) { + return hearing.get(agent); + } + + /** + Register a set of heard messages for an agent. + @param agent The agent. + @param c The messages heard. + */ + protected void addHearing(Entity agent, Command... c) { + addHearing(agent, Arrays.asList(c)); + } + + /** + Register a set of heard messages for an agent. + @param agent The agent. + @param c The messages heard. + */ + protected void addHearing(Entity agent, Collection c) { + hearing.get(agent).addAll(c); + } +} diff --git a/modules/kernel/src/kernel/AbstractKernelComponent.java b/modules/kernel/src/kernel/AbstractKernelComponent.java new file mode 100644 index 0000000000000000000000000000000000000000..8f3030b59311c4a970d9dbdfb8ad067516d2b225 --- /dev/null +++ b/modules/kernel/src/kernel/AbstractKernelComponent.java @@ -0,0 +1,65 @@ +package kernel; + +import rescuecore2.messages.Message; +import rescuecore2.messages.control.Shutdown; +import rescuecore2.connection.Connection; +import rescuecore2.connection.ConnectionException; +import rescuecore2.log.Logger; + +import java.util.Collection; +import java.util.Collections; + +/** + Abstract base class for KernelComponent implementations. + */ +public abstract class AbstractKernelComponent implements KernelComponent { + private Connection connection; + private String name; + + /** + Construct a new abstract component. + @param name The name of this component. + @param c The connection this component is using. + */ + protected AbstractKernelComponent(String name, Connection c) { + this.name = name; + this.connection = c; + } + + @Override + public void send(Collection messages) { + if (!connection.isAlive()) { + return; + } + try { + connection.sendMessages(messages); + } + catch (ConnectionException e) { + Logger.error("Error sending message", e); + } + } + + @Override + public Connection getConnection() { + return connection; + } + + @Override + public void shutdown() { + send(new Shutdown()); + connection.shutdown(); + } + + @Override + public String getName() { + return name; + } + + /** + Send a single message. + @param message The message to send. + */ + protected void send(Message message) { + send(Collections.singleton(message)); + } +} diff --git a/modules/kernel/src/kernel/AgentProxy.java b/modules/kernel/src/kernel/AgentProxy.java new file mode 100644 index 0000000000000000000000000000000000000000..a919da5dc43426528eb2df943ed16aa4f637f086 --- /dev/null +++ b/modules/kernel/src/kernel/AgentProxy.java @@ -0,0 +1,117 @@ +package kernel; + +import java.util.Collection; +import java.util.Map; +import java.util.ArrayList; + +import rescuecore2.connection.Connection; +import rescuecore2.connection.ConnectionListener; +import rescuecore2.messages.Message; +import rescuecore2.messages.Command; +import rescuecore2.messages.control.KASense; +import rescuecore2.worldmodel.Entity; +import rescuecore2.worldmodel.EntityID; +import rescuecore2.worldmodel.ChangeSet; +import rescuecore2.log.Logger; +import rescuecore2.misc.collections.LazyMap; +import rescuecore2.registry.Registry; + +/** + This class is the kernel interface to an agent. + */ +public class AgentProxy extends AbstractKernelComponent { + private Entity entity; + private Map> commands; + + /** + Construct an agent. + @param name The name of the controlling agent. + @param e The entity controlled by the agent. + @param c The connection to the agent. + */ + public AgentProxy(String name, Entity e, Connection c) { + super(name, c); + this.entity = e; + commands = new LazyMap>() { + @Override + public Collection createValue() { + return new ArrayList(); + } + }; + c.addConnectionListener(new AgentConnectionListener()); + } + + @Override + public String toString() { + return getName() + ": " + Registry.getCurrentRegistry().toPrettyName(entity.getURN()) + " " + entity.getID(); + } + + /** + Get the entity controlled by this agent. + @return The entity controlled by this agent. + */ + public Entity getControlledEntity() { + return entity; + } + + /** + Get all agent commands at a particular time. + @param timestep The current timestep. + @return A collection of messages representing the commands + */ + public Collection getAgentCommands(int timestep) { + Collection result; + synchronized (commands) { + result = new ArrayList<>(commands.get(timestep)); + } + Logger.trace(entity.toString() + " getAgentCommands(" + timestep + ") returning " + result); + return result; + } + + /** + Notify the of a perception update. + @param time The current timestep. + @param visible The set of visible changes. + @param heard The set of communication messages that the agent heard. + */ + public void sendPerceptionUpdate(int time, ChangeSet visible, Collection heard) { + KASense sense = new KASense(getControlledEntity().getID(), time, visible, heard); + send(sense); + } + + /** + Register an agent command received. + @param c The command that was received. + */ + protected void commandReceived(Command c) { + // Check that the command is for the right agent + if (!c.getAgentID().equals(entity.getID())) { + Logger.warn("Ignoring bogus command: Agent " + entity.getID() + " tried to send a command for agent " + c.getAgentID()); + return; + } + int time = c.getTime(); + Logger.trace("AgentProxy " + entity + " received " + c); + synchronized (commands) { + Collection result = commands.get(time); + result.add(c); + commands.notifyAll(); + } + } + + private class AgentConnectionListener implements ConnectionListener { + @Override + public void messageReceived(Connection c, Message msg) { + if (msg instanceof Command) { + EntityID id = ((Command)msg).getAgentID(); + if (id.equals(getControlledEntity().getID())) { + commandReceived((Command)msg); + } + } + } + } + + @Override + public int hashCode() { + return entity.getID().hashCode(); + } +} diff --git a/modules/kernel/src/kernel/AgentRegistrar.java b/modules/kernel/src/kernel/AgentRegistrar.java new file mode 100644 index 0000000000000000000000000000000000000000..00448211a9222a883092a24faf856c613ca1cb60 --- /dev/null +++ b/modules/kernel/src/kernel/AgentRegistrar.java @@ -0,0 +1,19 @@ +package kernel; + +import rescuecore2.worldmodel.WorldModel; +import rescuecore2.worldmodel.Entity; +import rescuecore2.config.Config; + +/** + Implementations of this decide which entities are controlled by agents and what each agent can see on startup. + */ +public interface AgentRegistrar { + /** + Process a WorldModel and Config and tell the ComponentManager which entities are agent-controlled and what they can see on connection. + @param world The WorldModel. + @param config The Config. + @param manager The ComponentManager. + @throws KernelException If there is a problem registering agents. + */ + void registerAgents(WorldModel world, Config config, ComponentManager manager) throws KernelException; +} diff --git a/modules/kernel/src/kernel/ChainedCommandFilter.java b/modules/kernel/src/kernel/ChainedCommandFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..9b7b23ff5d1fe1451f87152274b3b72788e35107 --- /dev/null +++ b/modules/kernel/src/kernel/ChainedCommandFilter.java @@ -0,0 +1,52 @@ +package kernel; + +import java.util.Collection; +import java.util.List; +import java.util.ArrayList; + +import rescuecore2.config.Config; +import rescuecore2.messages.Command; + +/** + A CommandFilter that chains together a set of filters. + */ +public class ChainedCommandFilter implements CommandFilter { + private List filters; + + /** + Construct an empty ChainedCommandFilter. + */ + public ChainedCommandFilter() { + filters = new ArrayList(); + } + + /** + Add a CommandFilter to the chain. + @param filter The filter to add. + */ + public void addFilter(CommandFilter filter) { + filters.add(filter); + } + + /** + Remove a CommandFilter from the chain. + @param filter The filter to remove. + */ + public void removeFilter(CommandFilter filter) { + filters.remove(filter); + } + + @Override + public void initialise(Config config) { + for (CommandFilter next : filters) { + next.initialise(config); + } + } + + @Override + public void filter(Collection commands, KernelState state) { + for (CommandFilter next : filters) { + next.filter(commands, state); + } + } +} diff --git a/modules/kernel/src/kernel/CommandCollector.java b/modules/kernel/src/kernel/CommandCollector.java new file mode 100644 index 0000000000000000000000000000000000000000..3a66d22abb7501833386f08a38f9d777c05ae0be --- /dev/null +++ b/modules/kernel/src/kernel/CommandCollector.java @@ -0,0 +1,26 @@ +package kernel; + +import rescuecore2.config.Config; +import rescuecore2.messages.Command; + +import java.util.Collection; + +/** + The CommandCollector gathers commands from agents. +*/ +public interface CommandCollector { + /** + Collect all commands from agents. + @param agents The agents. + @param timestep The timestep. + @return All agent commands. + @throws InterruptedException If the thread is interrupted. + */ + Collection getAgentCommands(Collection agents, int timestep) throws InterruptedException; + + /** + Initialise this command collector. + @param config The kernel configuration. + */ + void initialise(Config config); +} diff --git a/modules/kernel/src/kernel/CommandFilter.java b/modules/kernel/src/kernel/CommandFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..d41060128d427603c930ab032ddc465890829f21 --- /dev/null +++ b/modules/kernel/src/kernel/CommandFilter.java @@ -0,0 +1,24 @@ +package kernel; + +import java.util.Collection; + +import rescuecore2.config.Config; +import rescuecore2.messages.Command; + +/** + An interface for allowing the kernel to filter out agent commands. + */ +public interface CommandFilter { + /** + Initialise this filter. + @param config The kernel configuration. + */ + void initialise(Config config); + + /** + Filter a set of agent commands. Any illegal commands should be removed from the given collection. + @param commands The commands to filter. This collection should be modified to remove any illegal commands. + @param state The state of the kernel. + */ + void filter(Collection commands, KernelState state); +} diff --git a/modules/kernel/src/kernel/CommunicationModel.java b/modules/kernel/src/kernel/CommunicationModel.java new file mode 100644 index 0000000000000000000000000000000000000000..f3ee6cda148f6c6e8f8e9c57899395499faf6f2c --- /dev/null +++ b/modules/kernel/src/kernel/CommunicationModel.java @@ -0,0 +1,34 @@ +package kernel; + +import java.util.Collection; + +import rescuecore2.config.Config; +import rescuecore2.messages.Command; +import rescuecore2.worldmodel.WorldModel; +import rescuecore2.worldmodel.Entity; + +/** + A model of communication. Implementers are responsible for determining what communications are received by each agent in the world. + */ +public interface CommunicationModel { + /** + Initialise this communication model. + @param config The kernel configuration. + @param world The world model. + */ + void initialise(Config config, WorldModel world); + + /** + Process a set of agent commands and work out what communications each agent can hear. + @param time The current time. + @param agentCommands The set of all agent commands this timestep. + */ + void process(int time, Collection agentCommands); + + /** + Get the set of hear commands an agent can hear. + @param agent The agent controlled entity. + @return Set set of hear commands the agent can hear. + */ + Collection getHearing(Entity agent); +} diff --git a/modules/kernel/src/kernel/ComponentManager.java b/modules/kernel/src/kernel/ComponentManager.java new file mode 100644 index 0000000000000000000000000000000000000000..a9f7a49c72fed90c4414905df2843f13994ee0e9 --- /dev/null +++ b/modules/kernel/src/kernel/ComponentManager.java @@ -0,0 +1,575 @@ +package kernel; + +import java.util.Set; +import java.util.HashSet; +import java.util.Queue; +import java.util.List; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.Map.Entry; +import java.util.HashMap; + +import rescuecore2.config.Config; +import rescuecore2.connection.Connection; +import rescuecore2.connection.ConnectionException; +import rescuecore2.connection.ConnectionListener; +import rescuecore2.connection.ConnectionManagerListener; +import rescuecore2.messages.Message; +import rescuecore2.messages.control.KSAfterShocksInfo; +import rescuecore2.messages.control.VKConnect; +import rescuecore2.registry.Registry; +import rescuecore2.messages.control.VKAcknowledge; +import rescuecore2.messages.control.KVConnectOK; +import rescuecore2.messages.control.SKConnect; +import rescuecore2.messages.control.SKAcknowledge; +import rescuecore2.messages.control.KSConnectOK; +import rescuecore2.messages.control.AKConnect; +import rescuecore2.messages.control.AKAcknowledge; +import rescuecore2.messages.control.KAConnectError; +import rescuecore2.messages.control.KAConnectOK; +import rescuecore2.scenario.Scenario; +import rescuecore2.scenario.exceptions.UncompatibleScenarioException; +import rescuecore2.worldmodel.Entity; +import rescuecore2.worldmodel.EntityID; +import rescuecore2.worldmodel.WorldModel; +import rescuecore2.GUIComponent; +import rescuecore2.log.Logger; + +import kernel.ui.ComponentManagerGUI; + +import javax.swing.JComponent; + +/** + * Class that manages connecting components (agents, simulators, viewers) to the + * kernel. + */ +public class ComponentManager implements ConnectionManagerListener, + GUIComponent { + private static final int STARTING_ID = 1; + + private static final int WAIT_TIME = 10000; + + private Kernel kernel; + private ComponentManagerGUI gui; + + // Entities that have no controller yet. Map from type to list of entities. + private Map> uncontrolledEntities; + + // Connected agents + private Set agentsToAcknowledge; + + // Connected simulators + private Set simsToAcknowledge; + private int nextID; + + // Connected viewers + private Set viewersToAcknowledge; + + // World information + private WorldModel world; + + private Config config; + + /** Lock objects. */ + private final Object agentLock = new Object(); + private final Object simLock = new Object(); + private final Object viewerLock = new Object(); + private final Object idLock = new Object(); + + private final Scenario scenario; + + /** + * Create a ComponentManager. + * + * @param kernel + * The kernel. + * @param world + * The world model. + * @param config + * The kernel configuration. + */ + public ComponentManager(Kernel kernel, WorldModel world, + Config config, Scenario scenario) { + this.kernel = kernel; + this.world = world; + this.config = config; + this.scenario = scenario; + uncontrolledEntities = new HashMap>(); + agentsToAcknowledge = new HashSet(); + simsToAcknowledge = new HashSet(); + viewersToAcknowledge = new HashSet(); + nextID = STARTING_ID; + gui = new ComponentManagerGUI(); + } + + /** + * Register an agent-controlled entity. + * + * @param entity + * The entity that is agent-controlled. + * @param visibleOnStartup + * The set of entities that the agent should be sent on startup. + * If this is null then all entities will be sent. + * @param agentConfig + * A view of the system configuration that should be shared with + * the agent. + */ + public void registerAgentControlledEntity(Entity entity, + Collection visibleOnStartup, Config agentConfig) { + Logger.info("Agent controlled entity registered: " + entity); + synchronized (agentLock) { + Queue q = uncontrolledEntities.get(entity + .getURN()); + if (q == null) { + q = new LinkedList(); + uncontrolledEntities.put(entity.getURN(), q); + } + if (visibleOnStartup == null) { + visibleOnStartup = world.getAllEntities(); + } + q.add(new ControlledEntityInfo(entity, visibleOnStartup, + agentConfig)); + } + updateGUIUncontrolledAgents(); + } + + /** + * Wait for all agents to connect. This method will block until all agent + * entities have controllers. + * + * @throws InterruptedException + * If the thread is interrupted. + */ + public void waitForAllAgents() throws InterruptedException { + synchronized (agentLock) { + boolean done = false; + do { + done = true; + for (Entry> next : uncontrolledEntities + .entrySet()) { + if (!next.getValue().isEmpty()) { + done = false; + Logger.info("Waiting for " + next.getValue().size() + + " entities of type " + next.getKey()); + } + } + if (!agentsToAcknowledge.isEmpty()) { + done = false; + Logger.info("Waiting for " + agentsToAcknowledge.size() + + " agents to acknowledge"); + } + if (!done) { + agentLock.wait(WAIT_TIME); + } + } while (!done); + } + } + + /** + * Wait until all simulators have acknowledged. + * + * @throws InterruptedException + * If the thread is interrupted. + */ + public void waitForAllSimulators() throws InterruptedException { + synchronized (simLock) { + while (!simsToAcknowledge.isEmpty()) { + simLock.wait(WAIT_TIME); + Logger.info("Waiting for " + simsToAcknowledge.size() + + " simulators to acknowledge"); + } + } + } + + /** + * Wait until all viewers have acknowledged. + * + * @throws InterruptedException + * If the thread is interrupted. + */ + public void waitForAllViewers() throws InterruptedException { + synchronized (viewerLock) { + while (!viewersToAcknowledge.isEmpty()) { + viewerLock.wait(WAIT_TIME); + Logger.info("Waiting for " + viewersToAcknowledge.size() + + " viewers to acknowledge"); + } + } + } + + @Override + public void newConnection(Connection c) { + c.addConnectionListener(new ComponentConnectionListener()); + } + + @Override + public JComponent getGUIComponent() { + return gui; + } + + @Override + public String getGUIComponentName() { + return "Component manager"; + } + + private boolean agentAcknowledge(int requestID, EntityID agentID, + Connection c) { + synchronized (agentLock) { + for (AgentAck next : agentsToAcknowledge) { + if (next.requestID == requestID && next.agentID.equals(agentID) + && next.connection == c) { + agentsToAcknowledge.remove(next); + kernel.addAgent(next.agent); + agentLock.notifyAll(); + return true; + } + } + return false; + } + } + + private boolean simAcknowledge(int requestID, int simulatorID, Connection c) { + synchronized (simLock) { + for (SimulatorAck next : simsToAcknowledge) { + if (next.requestID == requestID + && next.simulatorID == simulatorID + && next.connection == c) { + simsToAcknowledge.remove(next); + kernel.addSimulator(next.sim); + simLock.notifyAll(); + return true; + } + } + return false; + } + } + + private boolean viewerAcknowledge(int requestID, int viewerID, Connection c) { + synchronized (viewerLock) { + for (ViewerAck next : viewersToAcknowledge) { + if (next.requestID == requestID && next.viewerID == viewerID + && next.connection == c) { + viewersToAcknowledge.remove(next); + kernel.addViewer(next.viewer); + viewerLock.notifyAll(); + return true; + } + } + return false; + } + } + + private int getNextSimulatorID() { + synchronized (idLock) { + return nextID++; + } + } + + private int getNextViewerID() { + synchronized (idLock) { + return nextID++; + } + } + + private ControlledEntityInfo findEntityToControl(List types) { + Logger.debug("Finding entity to control. Requested types: " + types); + for (Integer next : types) { + Queue q = uncontrolledEntities.get(next); + Logger.debug("Uncontrolled entities of type " + next + ": " + q); + if (q != null) { + ControlledEntityInfo info = q.poll(); + if (info != null) { + return info; + } + } + } + return null; + } + + private void updateGUIUncontrolledAgents() { + List data = new ArrayList(); + synchronized (agentLock) { + for (Queue q : uncontrolledEntities.values()) { + for (ControlledEntityInfo info : q) { + data.add(Registry.SYSTEM_REGISTRY.toPrettyName(info.entity.getURN()) + " " + info.entity.getID()); + } + } + } + gui.updateUncontrolledAgents(data); + } + + private void updateGUIAgentAck() { + List data = new ArrayList(); + synchronized (agentLock) { + for (AgentAck ack : agentsToAcknowledge) { + data.add(ack.toString()); + } + } + gui.updateAgentAck(data); + } + + private void updateGUISimulatorAck() { + List data = new ArrayList(); + synchronized (simLock) { + for (SimulatorAck ack : simsToAcknowledge) { + data.add(ack.toString()); + } + } + gui.updateSimulatorAck(data); + } + + private void updateGUIViewerAck() { + List data = new ArrayList(); + synchronized (viewerLock) { + for (ViewerAck ack : viewersToAcknowledge) { + data.add(ack.toString()); + } + } + gui.updateViewerAck(data); + } + + private class ComponentConnectionListener implements ConnectionListener { + @Override + public void messageReceived(Connection connection, Message msg) { + if (msg instanceof AKConnect) { + handleAKConnect((AKConnect) msg, connection); + } + if (msg instanceof AKAcknowledge) { + handleAKAcknowledge((AKAcknowledge) msg, connection); + } + try { + if (msg instanceof SKConnect) { + handleSKConnect((SKConnect) msg, connection); + } + } catch (UncompatibleScenarioException e) { + e.printStackTrace(); + } + if (msg instanceof SKAcknowledge) { + handleSKAcknowledge((SKAcknowledge) msg, connection); + } + if (msg instanceof VKConnect) { + handleVKConnect((VKConnect) msg, connection); + } + if (msg instanceof VKAcknowledge) { + handleVKAcknowledge((VKAcknowledge) msg, connection); + } + } + + private void handleAKConnect(AKConnect connect, Connection connection) { + // Pull out the request ID and requested entity type list + int requestID = connect.getRequestID(); + List types = connect.getRequestedEntityTypes(); + // See if we can find an entity for this agent to control. + Message reply = null; + Logger.debug("AKConnect received: " + types); + synchronized (agentLock) { + ControlledEntityInfo result = findEntityToControl(types); + if (result == null) { + Logger.debug("No suitable entities found"); + // Send an error + reply = new KAConnectError(requestID, "No more agents"); + } else { + Logger.debug("Found entity to control: " + result); + Entity entity = result.entity; + AgentProxy agent = new AgentProxy(connect.getAgentName(), + entity, connection); + agentsToAcknowledge.add(new AgentAck(agent, entity.getID(), + requestID, connection)); + Logger.info("Agent '" + connect.getAgentName() + "' id " + + entity.getID() + " (" + connection + + " request ID " + requestID + ") connected"); + // Send an OK + reply = new KAConnectOK(requestID, entity.getID(), + result.visibleSet, result.config); + } + } + if (reply != null) { + try { + connection.sendMessage(reply); + } catch (ConnectionException e) { + Logger.error("Error sending reply", e); + } + } + updateGUIUncontrolledAgents(); + updateGUIAgentAck(); + } + + private void handleAKAcknowledge(AKAcknowledge msg, + Connection connection) { + int requestID = msg.getRequestID(); + EntityID agentID = msg.getAgentID(); + if (agentAcknowledge(requestID, agentID, connection)) { + Logger.info("Agent " + agentID + " (" + connection + + " request ID " + requestID + ") acknowledged"); + } else { + Logger.warn("Unexpected acknowledge from agent " + agentID + + " (request ID " + requestID + ")"); + } + updateGUIAgentAck(); + } + + private void handleSKConnect(SKConnect msg, Connection connection) + throws UncompatibleScenarioException { + int simID = getNextSimulatorID(); + int requestID = msg.getRequestID(); + Logger.info("Simulator '" + msg.getSimulatorName() + "' id " + + simID + " (" + connection + " request ID " + requestID + + ") connected"); + SimulatorProxy sim = new SimulatorProxy(msg.getSimulatorName(), + simID, connection); + synchronized (simLock) { + simsToAcknowledge.add(new SimulatorAck(sim, simID, requestID, + connection)); + } + // Send an OK + sim.send(Collections.singleton(new KSConnectOK(simID, requestID, + world.getAllEntities(), config))); + sendAdditionalInfoToSim(sim); + updateGUISimulatorAck(); + } + + /** + * Used to send info other than world model's to simulators. Information + * such as Aftershocks' properties and etc. + * + * @param sim + * @throws UncompatibleScenarioException + */ + private void sendAdditionalInfoToSim(SimulatorProxy sim) + throws UncompatibleScenarioException { + sim.send(Collections.singleton(new KSAfterShocksInfo(scenario))); + } + + private void handleSKAcknowledge(SKAcknowledge msg, + Connection connection) { + int requestID = msg.getRequestID(); + int simID = msg.getSimulatorID(); + if (simAcknowledge(requestID, simID, connection)) { + Logger.info("Simulator " + simID + " (" + connection + + " request ID " + requestID + ") acknowledged"); + } else { + Logger.warn("Unexpected acknowledge from simulator " + simID + + " (request ID " + requestID + ")"); + } + updateGUISimulatorAck(); + } + + private void handleVKConnect(VKConnect msg, Connection connection) { + int requestID = msg.getRequestID(); + int viewerID = getNextViewerID(); + Logger.info("Viewer '" + msg.getViewerName() + "' id " + viewerID + + " (" + connection + " request ID " + requestID + + ") connected"); + ViewerProxy viewer = new ViewerProxy(msg.getViewerName(), viewerID, + connection); + synchronized (viewerLock) { + viewersToAcknowledge.add(new ViewerAck(viewer, viewerID, + requestID, connection)); + } + // Send an OK + viewer.send(Collections.singleton(new KVConnectOK(viewerID, + requestID, world.getAllEntities(), config))); + updateGUIViewerAck(); + } + + private void handleVKAcknowledge(VKAcknowledge msg, + Connection connection) { + int requestID = msg.getRequestID(); + int viewerID = msg.getViewerID(); + if (viewerAcknowledge(requestID, viewerID, connection)) { + Logger.info("Viewer " + viewerID + " (" + connection + + " request ID " + requestID + ") acknowledged"); + } else { + Logger.warn("Unexpected acknowledge from viewer " + viewerID + + " (" + requestID + ")"); + } + updateGUIViewerAck(); + } + } + + private static class AgentAck { + AgentProxy agent; + EntityID agentID; + int requestID; + Connection connection; + + public AgentAck(AgentProxy agent, EntityID agentID, int requestID, + Connection c) { + this.agent = agent; + this.agentID = agentID; + this.requestID = requestID; + this.connection = c; + } + + @Override + public String toString() { + return agent.getName() + ": " + + Registry.SYSTEM_REGISTRY.toPrettyName(agent.getControlledEntity().getURN()) + " " + + agent.getControlledEntity().getID() + "(" + connection + + " request ID " + requestID + ")"; + } + } + + private static class SimulatorAck { + SimulatorProxy sim; + int simulatorID; + int requestID; + Connection connection; + + public SimulatorAck(SimulatorProxy sim, int simID, int requestID, + Connection c) { + this.sim = sim; + this.simulatorID = simID; + this.requestID = requestID; + this.connection = c; + } + + @Override + public String toString() { + return sim + " " + simulatorID + "(connection request ID " + + requestID + ")"; + } + } + + private static class ViewerAck { + ViewerProxy viewer; + int viewerID; + int requestID; + Connection connection; + + public ViewerAck(ViewerProxy viewer, int viewerID, int requestID, + Connection c) { + this.viewer = viewer; + this.viewerID = viewerID; + this.requestID = requestID; + this.connection = c; + } + + @Override + public String toString() { + return viewer + " " + viewerID + "(connection request ID " + + requestID + ")"; + } + } + + private static class ControlledEntityInfo { + Entity entity; + Collection visibleSet; + Config config; + + public ControlledEntityInfo(Entity entity, + Collection visibleSet, Config config) { + this.entity = entity; + this.visibleSet = visibleSet; + this.config = config; + } + + @Override + public String toString() { + return entity.toString(); + } + } +} diff --git a/modules/kernel/src/kernel/CompositeCommandCollector.java b/modules/kernel/src/kernel/CompositeCommandCollector.java new file mode 100644 index 0000000000000000000000000000000000000000..1f5a088248e732390369c926cafab051660302b7 --- /dev/null +++ b/modules/kernel/src/kernel/CompositeCommandCollector.java @@ -0,0 +1,109 @@ +package kernel; + +import rescuecore2.config.Config; +import rescuecore2.log.Logger; +import rescuecore2.messages.Command; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorCompletionService; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +/** + A CommandCollector that waits for any of a set of child CommandCollectors to return a result. +*/ +public class CompositeCommandCollector implements CommandCollector { + private Set children; + private ExecutorService executorService; + + /** + Construct a CompositeCommandCollector with no children. + */ + public CompositeCommandCollector() { + children = new HashSet(); + } + + @Override + public void initialise(Config config) { + for (CommandCollector next : children) { + next.initialise(config); + } + executorService = Executors.newFixedThreadPool(children.size()); + } + + @Override + public Collection getAgentCommands(Collection agents, int timestep) throws InterruptedException { + Collection result = new ArrayList(); + if (agents.size() == 0) { + return result; + } + ExecutorCompletionService> service = new ExecutorCompletionService>(executorService); + Set>> futures = new HashSet>>(); + for (CommandCollector next : children) { + futures.add(service.submit(new ChildCommandsFetcher(next, agents, timestep))); + } + try { + for (int i = 0; i < children.size(); ++i) { + try { + result = service.take().get(); + break; + } + catch (ExecutionException e) { + Logger.error("Error while getting agent commands", e); + } + } + } + finally { + for (Future> next : futures) { + next.cancel(true); + } + } + return result; + } + + /** + Add a child command collector. + @param child The child to add. + */ + public void addCommandCollector(CommandCollector child) { + children.add(child); + } + + @Override + public String toString() { + StringBuilder result = new StringBuilder(); + result.append("CompositeCommandCollector ["); + for (Iterator it = children.iterator(); it.hasNext();) { + result.append(it.next()); + if (it.hasNext()) { + result.append(", "); + } + } + result.append("]"); + return result.toString(); + } + + private static final class ChildCommandsFetcher implements Callable> { + private CommandCollector child; + private Collection agents; + private int timestep; + + ChildCommandsFetcher(CommandCollector child, Collection agents, int timestep) { + this.child = child; + this.agents = agents; + this.timestep = timestep; + } + + @Override + public Collection call() throws Exception { + return child.getAgentCommands(agents, timestep); + } + } +} diff --git a/modules/kernel/src/kernel/EntityIDGenerator.java b/modules/kernel/src/kernel/EntityIDGenerator.java new file mode 100644 index 0000000000000000000000000000000000000000..64cecc9ff409d1c84a1480d9800b609d0da4bbd7 --- /dev/null +++ b/modules/kernel/src/kernel/EntityIDGenerator.java @@ -0,0 +1,14 @@ +package kernel; + +import rescuecore2.worldmodel.EntityID; + +/** + Interface for objects that can generate new EntityIDs. + */ +public interface EntityIDGenerator { + /** + Create a new EntityID. + @return A new EntityID. + */ + EntityID generateID(); +} diff --git a/modules/kernel/src/kernel/FrozenAgentsCommandFilter.java b/modules/kernel/src/kernel/FrozenAgentsCommandFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..29e35dfd1a655ce655c9895609099907cbb1bfcd --- /dev/null +++ b/modules/kernel/src/kernel/FrozenAgentsCommandFilter.java @@ -0,0 +1,28 @@ +package kernel; + +import rescuecore2.config.Config; +import rescuecore2.messages.Command; +import rescuecore2.log.Logger; + +import java.util.Collection; + +/** + A CommandFilter that ignores agent commands for some number of timesteps. + */ +public class FrozenAgentsCommandFilter implements CommandFilter { + private int freezeTime; + + @Override + public void initialise(Config config) { + freezeTime = config.getIntValue(KernelConstants.IGNORE_AGENT_COMMANDS_KEY, 0); + } + + @Override + public void filter(Collection commands, KernelState state) { + int time = state.getTime(); + if (time < freezeTime) { + Logger.info("Ignoring early commands: " + time + " < " + freezeTime); + commands.clear(); + } + } +} diff --git a/modules/kernel/src/kernel/InlineComponentLauncher.java b/modules/kernel/src/kernel/InlineComponentLauncher.java new file mode 100644 index 0000000000000000000000000000000000000000..d03b78e89a37c973aba5a5580d495bf6c5de5b5c --- /dev/null +++ b/modules/kernel/src/kernel/InlineComponentLauncher.java @@ -0,0 +1,34 @@ +package kernel; + +import rescuecore2.config.Config; +import rescuecore2.components.ComponentLauncher; +import rescuecore2.connection.Connection; +import rescuecore2.connection.StreamConnection; +import rescuecore2.connection.ConnectionException; +import rescuecore2.misc.Pair; + +/** + A class that knows how to connect components to the kernel using inline streams. + */ +public class InlineComponentLauncher extends ComponentLauncher { + private ComponentManager manager; + + /** + Construct a new InlineComponentLauncher. + @param manager The component manager. + @param config The system configuration. + */ + public InlineComponentLauncher(ComponentManager manager, Config config) { + super(config); + this.manager = manager; + } + + @Override + protected Connection makeConnection() throws ConnectionException { + Pair connections = StreamConnection.createConnectionPair(); + connections.first().setRegistry(getDefaultRegistry()); + connections.first().startup(); + manager.newConnection(connections.first()); + return connections.second(); + } +} diff --git a/modules/kernel/src/kernel/Kernel.java b/modules/kernel/src/kernel/Kernel.java new file mode 100644 index 0000000000000000000000000000000000000000..7c0b743fe53e20532884c95be813c0d27666e90a --- /dev/null +++ b/modules/kernel/src/kernel/Kernel.java @@ -0,0 +1,604 @@ +package kernel; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import rescuecore2.Constants; +import rescuecore2.Timestep; +import rescuecore2.config.Config; +import rescuecore2.log.CommandsRecord; +import rescuecore2.log.ConfigRecord; +import rescuecore2.log.EndLogRecord; +//import rescuecore2.log.FileLogWriter; +import rescuecore2.log.InitialConditionsRecord; +import rescuecore2.log.LogException; +import rescuecore2.log.LogWriter; +import rescuecore2.log.Logger; +import rescuecore2.log.PerceptionRecord; +import rescuecore2.log.RCRSLogFactory; +import rescuecore2.log.StartLogRecord; +import rescuecore2.log.UpdatesRecord; +import rescuecore2.messages.Command; +import rescuecore2.score.ScoreFunction; +//import rescuecore2.misc.gui.ChangeSetComponent; +import rescuecore2.worldmodel.ChangeSet; +import rescuecore2.worldmodel.Entity; +import rescuecore2.worldmodel.EntityID; +import rescuecore2.worldmodel.WorldModel; + +/** + * The Robocup Rescue kernel. + */ +public class Kernel { + /** The log context for kernel log messages. */ + public static final String KERNEL_LOG_CONTEXT = "kernel"; + + private Config config; + private Perception perception; + private CommunicationModel communicationModel; + private WorldModel worldModel; + private LogWriter log; + + private Set listeners; + + private Collection agents; + private Collection sims; + private Collection viewers; + private int time; + private Timestep previousTimestep; + + private EntityIDGenerator idGenerator; + private CommandFilter commandFilter; + + private TerminationCondition termination; + private ScoreFunction score; + private CommandCollector commandCollector; + + private boolean isShutdown; + + // private ChangeSetComponent simulatorChanges; + + /** + * Construct a kernel. + * + * @param config The configuration to use. + * @param perception A perception calculator. + * @param communicationModel A communication model. + * @param worldModel The world model. + * @param idGenerator An EntityIDGenerator. + * @param commandFilter An optional command filter. This may be null. + * @param termination The termination condition. + * @param score The score function. + * @param collector The CommandCollector to use. + * @throws KernelException If there is a problem constructing the kernel. + */ + public Kernel(Config config, Perception perception, + CommunicationModel communicationModel, + WorldModel worldModel, + EntityIDGenerator idGenerator, CommandFilter commandFilter, + TerminationCondition termination, ScoreFunction score, + CommandCollector collector) throws KernelException { + try { + Logger.pushLogContext(KERNEL_LOG_CONTEXT); + this.config = config; + this.perception = perception; + this.communicationModel = communicationModel; + this.worldModel = worldModel; + this.commandFilter = commandFilter; + this.score = score; + this.termination = termination; + this.commandCollector = collector; + this.idGenerator = idGenerator; + listeners = new HashSet(); + agents = new TreeSet(new Comparator() { + @Override + public int compare(AgentProxy o1, AgentProxy o2) { + return Integer.compare(o1.hashCode(), o2.hashCode()); + } + }); + sims = new HashSet(); + viewers = new HashSet(); + time = 0; + try { + String logName = config.getValue("kernel.logname"); + Logger.info("Logging to " + logName); + File logFile = new File(logName); + if (logFile.getParentFile().mkdirs()) { + Logger.info("Created log directory: " + + logFile.getParentFile().getAbsolutePath()); + } + if (logFile.createNewFile()) { + Logger.info( + "Created log file: " + logFile.getAbsolutePath()); + } + log = RCRSLogFactory.getLogWriter(logFile); + log.writeRecord(new StartLogRecord()); + log.writeRecord(new InitialConditionsRecord(worldModel)); + log.writeRecord(new ConfigRecord(config)); + } catch (IOException e) { + throw new KernelException("Couldn't open log file for writing", + e); + } catch (LogException e) { + throw new KernelException("Couldn't open log file for writing", + e); + } + config.setValue(Constants.COMMUNICATION_MODEL_KEY, + communicationModel.getClass().getName()); + config.setValue(Constants.PERCEPTION_KEY, + perception.getClass().getName()); + + // simulatorChanges = new ChangeSetComponent(); + + // Initialise + perception.initialise(config, worldModel); + communicationModel.initialise(config, worldModel); + commandFilter.initialise(config); + score.initialise(worldModel, config); + termination.initialise(config); + commandCollector.initialise(config); + + isShutdown = false; + + Logger.info("Kernel initialised"); + Logger.info("Perception module: " + perception); + Logger.info("Communication module: " + communicationModel); + Logger.info("Command filter: " + commandFilter); + Logger.info("Score function: " + score); + Logger.info("Termination condition: " + termination); + Logger.info("Command collector: " + collector); + } finally { + Logger.popLogContext(); + } + } + + /** + * Get the kernel's configuration. + * + * @return The configuration. + */ + public Config getConfig() { + return config; + } + + /** + * Get a snapshot of the kernel's state. + * + * @return A new KernelState snapshot. + */ + public KernelState getState() { + return new KernelState(getTime(), getWorldModel()); + } + + /** + * Add an agent to the system. + * + * @param agent The agent to add. + */ + public void addAgent(AgentProxy agent) { + synchronized (this) { + agents.add(agent); + } + fireAgentAdded(agent); + } + + /** + * Remove an agent from the system. + * + * @param agent The agent to remove. + */ + public void removeAgent(AgentProxy agent) { + synchronized (this) { + agents.remove(agent); + } + fireAgentRemoved(agent); + } + + /** + * Get all agents in the system. + * + * @return An unmodifiable view of all agents. + */ + public Collection getAllAgents() { + synchronized (this) { + return Collections.unmodifiableCollection(agents); + } + } + + /** + * Add a simulator to the system. + * + * @param sim The simulator to add. + */ + public void addSimulator(SimulatorProxy sim) { + synchronized (this) { + sims.add(sim); + sim.setEntityIDGenerator(idGenerator); + } + fireSimulatorAdded(sim); + } + + /** + * Remove a simulator from the system. + * + * @param sim The simulator to remove. + */ + public void removeSimulator(SimulatorProxy sim) { + synchronized (this) { + sims.remove(sim); + } + fireSimulatorRemoved(sim); + } + + /** + * Get all simulators in the system. + * + * @return An unmodifiable view of all simulators. + */ + public Collection getAllSimulators() { + synchronized (this) { + return Collections.unmodifiableCollection(sims); + } + } + + /** + * Add a viewer to the system. + * + * @param viewer The viewer to add. + */ + public void addViewer(ViewerProxy viewer) { + synchronized (this) { + viewers.add(viewer); + } + fireViewerAdded(viewer); + } + + /** + * Remove a viewer from the system. + * + * @param viewer The viewer to remove. + */ + public void removeViewer(ViewerProxy viewer) { + synchronized (this) { + viewers.remove(viewer); + } + fireViewerRemoved(viewer); + } + + /** + * Get all viewers in the system. + * + * @return An unmodifiable view of all viewers. + */ + public Collection getAllViewers() { + synchronized (this) { + return Collections.unmodifiableCollection(viewers); + } + } + + /** + * Add a KernelListener. + * + * @param l The listener to add. + */ + public void addKernelListener(KernelListener l) { + synchronized (listeners) { + listeners.add(l); + } + } + + /** + * Remove a KernelListener. + * + * @param l The listener to remove. + */ + public void removeKernelListener(KernelListener l) { + synchronized (listeners) { + listeners.remove(l); + } + } + + /** + * Get the current time. + * + * @return The current time. + */ + public int getTime() { + synchronized (this) { + return time; + } + } + + /** + * Get the world model. + * + * @return The world model. + */ + public WorldModel getWorldModel() { + return worldModel; + } + + /** + * Find out if the kernel has terminated. + * + * @return True if the kernel has terminated, false otherwise. + */ + public boolean hasTerminated() { + synchronized (this) { + return isShutdown || termination.shouldStop(getState()); + } + } + + /** + * Run a single timestep. + * + * @throws InterruptedException If this thread is interrupted during the + * timestep. + * @throws KernelException If there is a problem executing the + * timestep. + * @throws LogException If there is a problem writing the log. + */ + public void timestep() + throws InterruptedException, KernelException, LogException { + try { + Logger.pushLogContext(KERNEL_LOG_CONTEXT); + synchronized (this) { + if (time == 0) { + fireStarted(); + } + if (isShutdown) { + return; + } + ++time; + // Work out what the agents can see and hear (using the commands + // from the previous timestep). + // Wait for new commands + // Send commands to simulators and wait for updates + // Collate updates and broadcast to simulators + // Send perception, commands and updates to viewers + Timestep nextTimestep = new Timestep(time); + Logger.info("Timestep " + time); + Logger.debug("Sending agent updates"); + long start = System.currentTimeMillis(); + sendAgentUpdates(nextTimestep, + previousTimestep == null ? new HashSet() + : previousTimestep.getCommands()); + long perceptionTime = System.currentTimeMillis(); + Logger.debug("Waiting for commands"); + Collection commands = waitForCommands(time); + nextTimestep.setCommands(commands); + log.writeRecord(new CommandsRecord(time, commands)); + long commandsTime = System.currentTimeMillis(); + Logger.debug("Broadcasting commands"); + ChangeSet changes = sendCommandsToSimulators(time, commands); + // simulatorUpdates.show(changes); + nextTimestep.setChangeSet(changes); + log.writeRecord(new UpdatesRecord(time, changes)); + long updatesTime = System.currentTimeMillis(); + // Merge updates into world model + worldModel.merge(changes); + long mergeTime = System.currentTimeMillis(); + Logger.debug("Broadcasting updates"); + sendUpdatesToSimulators(time, changes); + sendToViewers(nextTimestep); + long broadcastTime = System.currentTimeMillis(); + Logger.debug("Computing score"); + double s = score.score(worldModel, nextTimestep); + long scoreTime = System.currentTimeMillis(); + nextTimestep.setScore(s); + Logger.info("Timestep " + time + " complete"); + Logger.debug("Score: " + s); + Logger.debug("Perception took : " + + (perceptionTime - start) + "ms"); + Logger.debug("Agent commands took : " + + (commandsTime - perceptionTime) + "ms"); + Logger.debug("Simulator updates took : " + + (updatesTime - commandsTime) + "ms"); + Logger.debug("World model merge took : " + + (mergeTime - updatesTime) + "ms"); + Logger.debug("Update broadcast took : " + + (broadcastTime - mergeTime) + "ms"); + Logger.debug("Score calculation took : " + + (scoreTime - broadcastTime) + "ms"); + Logger.debug("Total time : " + (scoreTime - start) + + "ms"); + fireTimestepCompleted(nextTimestep); + previousTimestep = nextTimestep; + Logger.debug("Commands: " + commands); + Logger.debug( + "Timestep commands: " + previousTimestep.getCommands()); + } + } finally { + Logger.popLogContext(); + } + } + + /** + * Shut down the kernel. This method will notify all + * agents/simulators/viewers of the shutdown. + */ + public void shutdown() { + synchronized (this) { + if (isShutdown) { + return; + } + Logger.info("Kernel is shutting down"); + ExecutorService service = Executors.newFixedThreadPool( + agents.size() + sims.size() + viewers.size()); + List> callables = new ArrayList>(); + for (AgentProxy next : agents) { + final AgentProxy proxy = next; + callables.add(Executors.callable(new Runnable() { + @Override + public void run() { + proxy.shutdown(); + } + })); + } + for (SimulatorProxy next : sims) { + final SimulatorProxy proxy = next; + callables.add(Executors.callable(new Runnable() { + @Override + public void run() { + proxy.shutdown(); + } + })); + } + for (ViewerProxy next : viewers) { + final ViewerProxy proxy = next; + callables.add(Executors.callable(new Runnable() { + @Override + public void run() { + proxy.shutdown(); + } + })); + } + try { + service.invokeAll(callables); + } catch (InterruptedException e) { + Logger.warn("Interrupted during shutdown"); + } + try { + log.writeRecord(new EndLogRecord()); + log.close(); + } catch (LogException e) { + Logger.error("Error closing log", e); + } + Logger.info("Kernel has shut down"); + isShutdown = true; + fireShutdown(); + } + } + + private void sendAgentUpdates(Timestep timestep, + Collection commandsLastTimestep) + throws InterruptedException, KernelException, LogException { + perception.setTime(time); + communicationModel.process(time, commandsLastTimestep); + for (AgentProxy next : agents) { + if (Thread.interrupted()) { + throw new InterruptedException(); + } + ChangeSet visible = perception.getVisibleEntities(next); + Collection heard = communicationModel + .getHearing(next.getControlledEntity()); + EntityID id = next.getControlledEntity().getID(); + timestep.registerPerception(id, visible, heard); + log.writeRecord(new PerceptionRecord(time, id, visible, heard)); + next.sendPerceptionUpdate(time, visible, heard); + } + } + + private Collection waitForCommands(int timestep) + throws InterruptedException { + Collection commands = commandCollector.getAgentCommands(agents, + timestep); + Logger.debug("Raw commands: " + commands); + commandFilter.filter(commands, getState()); + Logger.debug("Filtered commands: " + commands); + return commands; + } + + /** + * Send commands to all simulators and return which entities have been + * updated by the simulators. + */ + private ChangeSet sendCommandsToSimulators(int timestep, + Collection commands) throws InterruptedException { + for (SimulatorProxy next : sims) { + next.sendAgentCommands(timestep, commands); + } + // Wait until all simulators have sent updates + ChangeSet result = new ChangeSet(); + for (SimulatorProxy next : sims) { + Logger.debug("Fetching updates from " + next); + result.merge(next.getUpdates(timestep)); + } + return result; + } + + private void sendUpdatesToSimulators(int timestep, ChangeSet updates) + throws InterruptedException { + for (SimulatorProxy next : sims) { + next.sendUpdate(timestep, updates); + } + } + + private void sendToViewers(Timestep timestep) { + for (ViewerProxy next : viewers) { + next.sendTimestep(timestep); + } + } + + private Set getListeners() { + Set result; + synchronized (listeners) { + result = new HashSet(listeners); + } + return result; + } + + private void fireStarted() { + for (KernelListener next : getListeners()) { + next.simulationStarted(this); + } + } + + private void fireShutdown() { + for (KernelListener next : getListeners()) { + next.simulationEnded(this); + } + } + + private void fireTimestepCompleted(Timestep timestep) { + for (KernelListener next : getListeners()) { + next.timestepCompleted(this, timestep); + } + } + + private void fireAgentAdded(AgentProxy agent) { + for (KernelListener next : getListeners()) { + next.agentAdded(this, agent); + } + } + + private void fireAgentRemoved(AgentProxy agent) { + for (KernelListener next : getListeners()) { + next.agentRemoved(this, agent); + } + } + + private void fireSimulatorAdded(SimulatorProxy sim) { + for (KernelListener next : getListeners()) { + next.simulatorAdded(this, sim); + } + } + + private void fireSimulatorRemoved(SimulatorProxy sim) { + for (KernelListener next : getListeners()) { + next.simulatorRemoved(this, sim); + } + } + + private void fireViewerAdded(ViewerProxy viewer) { + for (KernelListener next : getListeners()) { + next.viewerAdded(this, viewer); + } + } + + private void fireViewerRemoved(ViewerProxy viewer) { + for (KernelListener next : getListeners()) { + next.viewerRemoved(this, viewer); + } + } +} diff --git a/modules/kernel/src/kernel/KernelComponent.java b/modules/kernel/src/kernel/KernelComponent.java new file mode 100644 index 0000000000000000000000000000000000000000..fcca62baf4e4d5b48d39967c7467e1ce51bd7a18 --- /dev/null +++ b/modules/kernel/src/kernel/KernelComponent.java @@ -0,0 +1,34 @@ +package kernel; + +import rescuecore2.connection.Connection; +import rescuecore2.messages.Message; + +import java.util.Collection; + +/** + This class is the kernel interface to components (agents, viewers, simulators). + */ +public interface KernelComponent { + /** + Send a set of messages to this component. + @param m The messages to send. + */ + void send(Collection m); + + /** + Shut this component down. + */ + void shutdown(); + + /** + Get this component's connection. + @return The connection to the component. + */ + Connection getConnection(); + + /** + Get the name of this component. + @return The name of the component. + */ + String getName(); +} diff --git a/modules/kernel/src/kernel/KernelConstants.java b/modules/kernel/src/kernel/KernelConstants.java new file mode 100644 index 0000000000000000000000000000000000000000..19a172dd07aa973934a18fd0ac403f70236002f1 --- /dev/null +++ b/modules/kernel/src/kernel/KernelConstants.java @@ -0,0 +1,35 @@ +package kernel; + +/** + Some useful constants for the kernel. +*/ +public final class KernelConstants { + /** The config key for gis implementations. */ + public static final String GIS_KEY = "kernel.gis"; + + /** The config key for perception implementations. */ + public static final String PERCEPTION_KEY = "kernel.perception"; + + /** The config key for communication model implementations. */ + public static final String COMMUNICATION_MODEL_KEY = "kernel.communication"; + + /** The config key for agent implementations. */ + public static final String AGENTS_KEY = "kernel.agents"; + + /** The config key for simulator implementations. */ + public static final String SIMULATORS_KEY = "kernel.simulators"; + + /** The config key for viewer implementations. */ + public static final String VIEWERS_KEY = "kernel.viewers"; + + /** The config key for component implementations. */ + public static final String COMPONENTS_KEY = "kernel.components"; + + /** Whether to run the kernel in inline-only mode. */ + public static final String INLINE_ONLY_KEY = "kernel.inline-only"; + + /** The config key for ignoring agent commands at the start of the simulation. */ + public static final String IGNORE_AGENT_COMMANDS_KEY = "kernel.agents.ignoreuntil"; + + private KernelConstants() {} +} diff --git a/modules/kernel/src/kernel/KernelException.java b/modules/kernel/src/kernel/KernelException.java new file mode 100644 index 0000000000000000000000000000000000000000..e1620fdb8426c93a8459ed0b90412e573eac5ce4 --- /dev/null +++ b/modules/kernel/src/kernel/KernelException.java @@ -0,0 +1,39 @@ +package kernel; + +/** + Root of the kernel exception heirarchy. + */ + +public class KernelException extends Exception { + /** + Construct a kernel exception with no information. + */ + public KernelException() { + super(); + } + + /** + Construct a kernel exception with an error message. + @param msg The error message. + */ + public KernelException(String msg) { + super(msg); + } + + /** + Construct a kernel exception that was caused by another exception. + @param cause The cause of this exception. + */ + public KernelException(Throwable cause) { + super(cause); + } + + /** + Construct a kernel exception with an error message and an underlying cause. + @param msg The error message. + @param cause The cause of this exception. + */ + public KernelException(String msg, Throwable cause) { + super(msg, cause); + } +} diff --git a/modules/kernel/src/kernel/KernelListener.java b/modules/kernel/src/kernel/KernelListener.java new file mode 100644 index 0000000000000000000000000000000000000000..da5e5d0555dea7b02fc9c1a858e27b7187c6b8a1 --- /dev/null +++ b/modules/kernel/src/kernel/KernelListener.java @@ -0,0 +1,69 @@ +package kernel; + +import rescuecore2.Timestep; + +/** + Interface for objects that are interested in kernel events. + */ +public interface KernelListener { + /** + Notification that the kernel has started the simulation. + @param kernel The kernel. + */ + void simulationStarted(Kernel kernel); + + /** + Notification that the kernel has ended the simulation and shut down. + @param kernel The kernel. + */ + void simulationEnded(Kernel kernel); + + /** + Notification that a timestep has been completed. + @param kernel The kernel. + @param time The timestep that has just been completed. + */ + void timestepCompleted(Kernel kernel, Timestep time); + + /** + Notification that an agent has been added. + @param kernel The kernel. + @param agent The agent that was added. + */ + void agentAdded(Kernel kernel, AgentProxy agent); + + /** + Notification that an agent has been removed. + @param kernel The kernel. + @param agent The agent that was removed. + */ + void agentRemoved(Kernel kernel, AgentProxy agent); + + /** + Notification that a simulator has been added. + @param kernel The kernel. + @param simulator The simulator that was added. + */ + void simulatorAdded(Kernel kernel, SimulatorProxy simulator); + + /** + Notification that a simulator has been removed. + @param kernel The kernel. + @param simulator The simulator that was removed. + */ + void simulatorRemoved(Kernel kernel, SimulatorProxy simulator); + + /** + Notification that a viewer has been added. + @param kernel The kernel. + @param viewer The viewer that was added. + */ + void viewerAdded(Kernel kernel, ViewerProxy viewer); + + /** + Notification that a viewer has been removed. + @param kernel The kernel. + @param viewer The viewer that was removed. + */ + void viewerRemoved(Kernel kernel, ViewerProxy viewer); +} diff --git a/modules/kernel/src/kernel/KernelListenerAdapter.java b/modules/kernel/src/kernel/KernelListenerAdapter.java new file mode 100644 index 0000000000000000000000000000000000000000..dc56055b5f11574f0900043b49feacbe36d5db92 --- /dev/null +++ b/modules/kernel/src/kernel/KernelListenerAdapter.java @@ -0,0 +1,35 @@ +package kernel; + +import rescuecore2.Timestep; + +/** + Abstract class for objects that want to implement a subset of the KernelListener interface. All default method implementations do nothing. + */ +public class KernelListenerAdapter implements KernelListener { + @Override + public void simulationStarted(Kernel kernel) {} + + @Override + public void simulationEnded(Kernel kernel) {} + + @Override + public void timestepCompleted(Kernel kernel, Timestep time) {} + + @Override + public void agentAdded(Kernel kernel, AgentProxy agent) {} + + @Override + public void agentRemoved(Kernel kernel, AgentProxy agent) {} + + @Override + public void simulatorAdded(Kernel kernel, SimulatorProxy simulator) {} + + @Override + public void simulatorRemoved(Kernel kernel, SimulatorProxy simulator) {} + + @Override + public void viewerAdded(Kernel kernel, ViewerProxy viewer) {} + + @Override + public void viewerRemoved(Kernel kernel, ViewerProxy viewer) {} +} diff --git a/modules/kernel/src/kernel/KernelStartupOptions.java b/modules/kernel/src/kernel/KernelStartupOptions.java new file mode 100644 index 0000000000000000000000000000000000000000..e43ef97ee183e9fc58f7e9e4c0a56658b7b7a247 --- /dev/null +++ b/modules/kernel/src/kernel/KernelStartupOptions.java @@ -0,0 +1,305 @@ +package kernel; + +import static rescuecore2.misc.java.JavaTools.instantiate; + +import java.util.List; +import java.util.ArrayList; +import java.util.Set; +import java.util.HashSet; +import java.util.Map; +import java.util.HashMap; +import java.util.Collection; +import java.util.Collections; + +import rescuecore2.misc.Pair; +import rescuecore2.config.Config; +import rescuecore2.components.Component; +import rescuecore2.components.Simulator; +import rescuecore2.components.Viewer; +import rescuecore2.components.Agent; +import rescuecore2.log.Logger; + +/** + Container class for all kernel startup options. +*/ +public class KernelStartupOptions { + private static final String AUTO_SUFFIX = ".auto"; + + private List worldOptions; + private List perceptionOptions; + private List commsOptions; + + private Map sims; + private Map viewers; + private Map agents; + private Map other; + + private WorldModelCreator world; + private Perception perception; + private CommunicationModel comms; + + /** + Create a KernelStartupOptions. + @param config The system configuration. + */ + public KernelStartupOptions(Config config) { + Pair, Integer> w = createOptions(config, KernelConstants.GIS_KEY, WorldModelCreator.class); + worldOptions = w.first(); + world = worldOptions.get(w.second()); + + Pair, Integer> p = createOptions(config, KernelConstants.PERCEPTION_KEY, Perception.class); + perceptionOptions = p.first(); + perception = perceptionOptions.get(p.second()); + + Pair, Integer> c = createOptions(config, KernelConstants.COMMUNICATION_MODEL_KEY, CommunicationModel.class); + commsOptions = c.first(); + comms = commsOptions.get(c.second()); + + sims = createComponentOptions(config, KernelConstants.SIMULATORS_KEY, Simulator.class); + viewers = createComponentOptions(config, KernelConstants.VIEWERS_KEY, Viewer.class); + agents = createComponentOptions(config, KernelConstants.AGENTS_KEY, Agent.class); + other = createComponentOptions(config, KernelConstants.COMPONENTS_KEY, Component.class); + } + + /** + Get the names of all components that should be started inline. + @return All inline component class names and the requested number of each. + */ + public Collection> getInlineComponents() { + List> result = new ArrayList>(); + for (Map.Entry next : sims.entrySet()) { + result.add(new Pair(next.getKey().getClass().getName(), next.getValue())); + } + for (Map.Entry next : viewers.entrySet()) { + result.add(new Pair(next.getKey().getClass().getName(), next.getValue())); + } + for (Map.Entry next : agents.entrySet()) { + result.add(new Pair(next.getKey().getClass().getName(), next.getValue())); + } + for (Map.Entry next : other.entrySet()) { + result.add(new Pair(next.getKey().getClass().getName(), next.getValue())); + } + return result; + } + + /** + Get the WorldModelCreator the kernel should use. + @return The selected WorldModelCreator. + */ + public WorldModelCreator getWorldModelCreator() { + return world; + } + + /** + Set the WorldModelCreator the kernel should use. + @param creator The selected WorldModelCreator. + */ + public void setWorldModelCreator(WorldModelCreator creator) { + this.world = creator; + } + + /** + Get the list of available WorldModelCreator implementations. + @return All known WorldModelCreators. + */ + public List getAvailableWorldModelCreators() { + return Collections.unmodifiableList(worldOptions); + } + + /** + Get the Perception module the kernel should use. + @return The selected Perception. + */ + public Perception getPerception() { + return perception; + } + + /** + Set the Perception module the kernel should use. + @param p The selected Perception. + */ + public void setPerception(Perception p) { + perception = p; + } + + /** + Get the list of available Perception implementations. + @return All known Perceptions. + */ + public List getAvailablePerceptions() { + return Collections.unmodifiableList(perceptionOptions); + } + + /** + Get the CommunicationModel the kernel should use. + @return The selected CommunicationModel. + */ + public CommunicationModel getCommunicationModel() { + return comms; + } + + /** + Set the CommunicationModel the kernel should use. + @param c The selected CommunicationModel. + */ + public void setCommunicationModel(CommunicationModel c) { + comms = c; + } + + /** + Get the list of available CommunicationModel implementations. + @return All known CommunicationModels. + */ + public List getAvailableCommunicationModels() { + return Collections.unmodifiableList(commsOptions); + } + + /** + Get the list of available Simulator components. + @return All known Simulators. + */ + public Collection getAvailableSimulators() { + return Collections.unmodifiableSet(sims.keySet()); + } + + /** + Get the list of available Viewer components. + @return All known Viewers. + */ + public Collection getAvailableViewers() { + return Collections.unmodifiableSet(viewers.keySet()); + } + + /** + Get the list of available Agent components. + @return All known Agents. + */ + public Collection getAvailableAgents() { + return Collections.unmodifiableSet(agents.keySet()); + } + + /** + Get the list of available components that are not simulators, viewers or agents. + @return All known Components that are not simulators, viewers or agents. + */ + public Collection getAvailableComponents() { + return Collections.unmodifiableSet(other.keySet()); + } + + /** + Get the number of instances of a type of component to start. + @param c The component type. + @return The number of instances to start. + */ + public int getInstanceCount(Component c) { + if (sims.containsKey(c)) { + return sims.get(c); + } + if (viewers.containsKey(c)) { + return viewers.get(c); + } + if (agents.containsKey(c)) { + return agents.get(c); + } + if (other.containsKey(c)) { + return other.get(c); + } + throw new IllegalArgumentException("Component " + c + " not recognised"); + } + + /** + Set the number of instances of a type of component to start. + @param c The component type. + @param count The number of instances to start. + */ + public void setInstanceCount(Component c, int count) { + if (c instanceof Simulator) { + sims.put((Simulator)c, count); + } + else if (c instanceof Viewer) { + viewers.put((Viewer)c, count); + } + else if (c instanceof Agent) { + agents.put((Agent)c, count); + } + else { + other.put(c, count); + } + } + + private Pair, Integer> createOptions(Config config, String key, Class expectedClass) { + List instances = new ArrayList(); + int index = 0; + int selectedIndex = 0; + Logger.trace("Loading options: " + key); + List classNames = config.getArrayValue(key); + String auto = config.getValue(key + AUTO_SUFFIX, null); + boolean autoFound = false; + for (String next : classNames) { + Logger.trace("Option found: '" + next + "'"); + T t = instantiate(next, expectedClass); + if (t != null) { + instances.add(t); + if (next.equals(auto)) { + selectedIndex = index; + autoFound = true; + } + ++index; + } + } + if (auto != null && !autoFound) { + Logger.warn("Could not find class " + auto + " in config key " + key + ". Values found: " + classNames); + } + return new Pair, Integer>(instances, selectedIndex); + } + + private Map createComponentOptions(Config config, String key, Class expectedClass) { + Logger.trace("Loading component options: " + key); + Map result = new HashMap(); + List classNames = config.getArrayValue(key, ""); + List autoClassNames = config.getArrayValue(key + AUTO_SUFFIX, ""); + Set allClassNames = new HashSet(classNames); + allClassNames.addAll(strip(autoClassNames)); + for (String next : allClassNames) { + Logger.trace("Option found: '" + next + "'"); + T t = instantiate(next, expectedClass); + if (t != null) { + int count = getStartCount(next, autoClassNames); + result.put(t, count); + } + } + return result; + } + + private int getStartCount(String className, List auto) { + for (String next : auto) { + if (next.startsWith(className)) { + int index = next.indexOf("*"); + if (index == -1) { + return 1; + } + String arg = next.substring(index + 1); + if ("n".equals(arg)) { + return Integer.MAX_VALUE; + } + return Integer.parseInt(arg); + } + } + return 0; + } + + private List strip(List autoClassNames) { + List result = new ArrayList(autoClassNames.size()); + // Remove any trailing *n + for (String s : autoClassNames) { + int index = s.indexOf("*"); + if (index != -1) { + result.add(s.substring(0, index)); + } + else { + result.add(s); + } + } + return result; + } +} diff --git a/modules/kernel/src/kernel/KernelState.java b/modules/kernel/src/kernel/KernelState.java new file mode 100644 index 0000000000000000000000000000000000000000..2e6ba2505445c5eda840f059806580edba6f7cf1 --- /dev/null +++ b/modules/kernel/src/kernel/KernelState.java @@ -0,0 +1,38 @@ +package kernel; + +import rescuecore2.worldmodel.WorldModel; +import rescuecore2.worldmodel.Entity; + +/** + A class for obtaining information about the state of the kernel. +*/ +public class KernelState { + private int time; + private WorldModel model; + + /** + Construct a snapshot of the kernel state. + @param time The current time. + @param model The world model snapshot. + */ + public KernelState(int time, WorldModel model) { + this.time = time; + this.model = model; + } + + /** + Get the current time. + @return The current time. + */ + public int getTime() { + return time; + } + + /** + Get the world model. + @return The world model. + */ + public WorldModel getWorldModel() { + return model; + } +} diff --git a/modules/kernel/src/kernel/OrTerminationCondition.java b/modules/kernel/src/kernel/OrTerminationCondition.java new file mode 100644 index 0000000000000000000000000000000000000000..4ccefd8e13912587a0b9b0f570489b9e54df7fbf --- /dev/null +++ b/modules/kernel/src/kernel/OrTerminationCondition.java @@ -0,0 +1,53 @@ +package kernel; + +import rescuecore2.config.Config; + +import java.util.Collection; +import java.util.Iterator; + +/** + A TerminationCondition that returns true if any of its children return true. +*/ +public class OrTerminationCondition implements TerminationCondition { + private Collection children; + + /** + Construct a new OrTerminationCondition. + @param children The child conditions. This must have at least one element. + */ + public OrTerminationCondition(Collection children) { + if (children == null || children.size() == 0) { + throw new IllegalArgumentException("Must have at least one child"); + } + this.children = children; + } + + @Override + public boolean shouldStop(KernelState state) { + for (TerminationCondition next : children) { + if (next.shouldStop(state)) { + return true; + } + } + return false; + } + + @Override + public void initialise(Config config) { + for (TerminationCondition next : children) { + next.initialise(config); + } + } + + @Override + public String toString() { + StringBuilder result = new StringBuilder(); + for (Iterator it = children.iterator(); it.hasNext();) { + result.append(it.next()); + if (it.hasNext()) { + result.append(" | "); + } + } + return result.toString(); + } +} diff --git a/modules/kernel/src/kernel/Perception.java b/modules/kernel/src/kernel/Perception.java new file mode 100644 index 0000000000000000000000000000000000000000..6508a165a09a5949a20e5aafc145a16d3b76cc2b --- /dev/null +++ b/modules/kernel/src/kernel/Perception.java @@ -0,0 +1,31 @@ +package kernel; + +import rescuecore2.config.Config; +import rescuecore2.worldmodel.Entity; +import rescuecore2.worldmodel.WorldModel; +import rescuecore2.worldmodel.ChangeSet; + +/** + Implementations of this interface are responsible for determining what entities/properties each agent can see. + */ +public interface Perception { + /** + Initialise this perception object. + @param config The kernel configuration. + @param world The world model. + */ + void initialise(Config config, WorldModel world); + + /** + Determine what Entities are visible to a particular agent. The returned Entities should be copies of Entities in the ground-truth WorldModel. Only visible properties should have defined values. + @param agent The agent that is perceiving the world. + @return A collection of entities that the agent can perceive. + */ + ChangeSet getVisibleEntities(AgentProxy agent); + + /** + Notify this perception object of the current time. + @param timestep The current timestep. + */ + void setTime(int timestep); +} diff --git a/modules/kernel/src/kernel/RemoteGISWorldModelCreator.java b/modules/kernel/src/kernel/RemoteGISWorldModelCreator.java new file mode 100644 index 0000000000000000000000000000000000000000..875adb7afb0d18c832445a5e021dd4dae2a44c45 --- /dev/null +++ b/modules/kernel/src/kernel/RemoteGISWorldModelCreator.java @@ -0,0 +1,121 @@ +package kernel; + + + +import java.io.IOException; +import java.util.concurrent.CountDownLatch; + +import org.dom4j.DocumentException; + +import rescuecore2.Constants; +import rescuecore2.config.Config; +import rescuecore2.connection.Connection; +import rescuecore2.connection.ConnectionException; +import rescuecore2.connection.TCPConnection; +import rescuecore2.connection.ConnectionListener; +import rescuecore2.scenario.Scenario; +import rescuecore2.worldmodel.Entity; +import rescuecore2.worldmodel.EntityID; +import rescuecore2.worldmodel.WorldModel; +import rescuecore2.worldmodel.DefaultWorldModel; +import rescuecore2.messages.Message; +import rescuecore2.messages.control.GKConnectOK; +import rescuecore2.messages.control.GKConnectError; +import rescuecore2.messages.control.KGConnect; +import rescuecore2.messages.control.KGAcknowledge; +import rescuecore2.log.Logger; + +/** + * A WorldModelCreator that talks to a remote GIS. + */ +public class RemoteGISWorldModelCreator implements WorldModelCreator { + private int nextID; + + @Override + public WorldModel buildWorldModel(Config config) + throws KernelException { + Logger.info("Connecting to remote GIS..."); + DefaultWorldModel world = DefaultWorldModel.create(); + CountDownLatch latch = new CountDownLatch(1); + int gisPort = config.getIntValue(Constants.GIS_PORT_NUMBER_KEY, + Constants.DEFAULT_GIS_PORT_NUMBER); + Connection conn; + try { + conn = new TCPConnection(gisPort); + conn.addConnectionListener(new GISConnectionListener(latch, world)); + conn.startup(); + conn.sendMessage(new KGConnect(1)); + } catch (IOException e) { + throw new KernelException("Couldn't connect to GIS", e); + } catch (ConnectionException e) { + throw new KernelException("Couldn't connect to GIS", e); + } + // Wait for a reply + try { + latch.await(); + } catch (InterruptedException e) { + throw new KernelException("Interrupted while connecting to GIS", e); + } + conn.shutdown(); + return world; + } + + @Override + public String toString() { + return "Remote GIS"; + } + + @Override + public EntityID generateID() { + synchronized (this) { + return new EntityID(nextID++); + } + } + + /** + * Listener for the GIS connection. + */ + private class GISConnectionListener implements ConnectionListener { + private CountDownLatch latch; + private DefaultWorldModel model; + + public GISConnectionListener(CountDownLatch latch, + DefaultWorldModel model) { + this.latch = latch; + this.model = model; + } + + public void messageReceived(Connection c, Message m) { + if (m instanceof GKConnectOK) { + try { + // Update the internal world model + model.removeAllEntities(); + model.addEntities(((GKConnectOK) m).getEntities()); + // Send an acknowledgement + c.sendMessage(new KGAcknowledge()); + Logger.info("GIS connected OK"); + // Trigger the countdown latch + latch.countDown(); + nextID = 0; + for (Entity next : model) { + nextID = Math.max(nextID, next.getID().getValue()); + } + ++nextID; + } catch (ConnectionException e) { + Logger.error("RemoteGISWorldModelCreator.messageReceived", + e); + } + } + if (m instanceof GKConnectError) { + Logger.error("Error connecting to remote GIS: " + + ((GKConnectError) m).getReason()); + latch.countDown(); + } + } + } + + @Override + public Scenario getScenario(Config config) throws DocumentException{ + return null;// TODO Salim implement + } +} diff --git a/modules/kernel/src/kernel/SimulatorProxy.java b/modules/kernel/src/kernel/SimulatorProxy.java new file mode 100644 index 0000000000000000000000000000000000000000..af1c33699f35cbbaa48dc09e140f7daafa833d9f --- /dev/null +++ b/modules/kernel/src/kernel/SimulatorProxy.java @@ -0,0 +1,146 @@ +package kernel; + +import rescuecore2.connection.Connection; +import rescuecore2.connection.ConnectionListener; +import rescuecore2.messages.Message; +import rescuecore2.messages.Command; +import rescuecore2.messages.control.SKUpdate; +import rescuecore2.messages.control.KSUpdate; +import rescuecore2.messages.control.KSCommands; +import rescuecore2.messages.control.EntityIDRequest; +import rescuecore2.messages.control.EntityIDResponse; +import rescuecore2.worldmodel.ChangeSet; +import rescuecore2.worldmodel.EntityID; +import rescuecore2.log.Logger; + +import java.util.Collection; +import java.util.Map; +import java.util.HashMap; +import java.util.List; +import java.util.ArrayList; + +/** + This class is the kernel interface to a simulator. + */ +public class SimulatorProxy extends AbstractKernelComponent { + private Map updates; + private int id; + private EntityIDGenerator idGenerator; + + /** + Construct a new simulator. + @param name The name of the simulator. + @param id The ID of the simulator. + @param c The connection this simulator is using. + */ + public SimulatorProxy(String name, int id, Connection c) { + super(name, c); + this.id = id; + updates = new HashMap(); + c.addConnectionListener(new SimulatorConnectionListener()); + } + + /** + Get updates from this simulator. This method may block until updates are available. + @param time The timestep to get updates for. + @return A ChangeSet representing the updates from this simulator. + @throws InterruptedException If this thread is interrupted while waiting for updates. + */ + public ChangeSet getUpdates(int time) throws InterruptedException { + ChangeSet result = null; + synchronized (updates) { + while (result == null) { + result = updates.get(time); + if (result == null) { + updates.wait(1000); + } + } + } + return result; + } + + /** + Send an update message to this simulator. + @param time The simulation time. + @param update The updated entities. + */ + public void sendUpdate(int time, ChangeSet update) { + send(new KSUpdate(id, time, update)); + } + + /** + Send a set of agent commands to this simulator. + @param time The current time. + @param commands The agent commands to send. + */ + public void sendAgentCommands(int time, Collection commands) { + send(new KSCommands(id, time, commands)); + } + + @Override + public String toString() { + return getName() + " (" + id + "): " + getConnection().toString(); + } + + /** + Set the EntityIDGenerator. + @param generator The new EntityIDGenerator. + */ + public void setEntityIDGenerator(EntityIDGenerator generator) { + idGenerator = generator; + } + + /** + Register an update from the simulator. + @param time The timestep of the update. + @param changes The set of changes. + */ + protected void updateReceived(int time, ChangeSet changes) { + synchronized (updates) { + ChangeSet c = updates.get(time); + if (c == null) { + c = new ChangeSet(); + updates.put(time, c); + } + c.merge(changes); + updates.notifyAll(); + } + } + + private class SimulatorConnectionListener implements ConnectionListener { + @Override + public void messageReceived(Connection connection, Message msg) { + Logger.pushLogContext(Kernel.KERNEL_LOG_CONTEXT); + try { + if (msg instanceof SKUpdate) { + SKUpdate update = (SKUpdate)msg; + if (update.getSimulatorID() == id) { + updateReceived(update.getTime(), update.getChangeSet()); + } + } + if (msg instanceof EntityIDRequest) { + EntityIDRequest req = (EntityIDRequest)msg; + Logger.debug("Simulator proxy " + id + " received entity ID request: " + msg); + if (req.getSimulatorID() == id) { + int requestID = req.getRequestID(); + int count = req.getCount(); + List result = new ArrayList(count); + for (int i = 0; i < count; ++i) { + result.add(idGenerator.generateID()); + } + Logger.debug("Simulator proxy " + id + " sending new IDs: " + result); + send(new EntityIDResponse(id, requestID, result)); + } + } + } + finally { + Logger.popLogContext(); + } + } + } + + @Override + public int hashCode() { + return Integer.hashCode(id); + } +} diff --git a/modules/kernel/src/kernel/StartKernel.java b/modules/kernel/src/kernel/StartKernel.java new file mode 100644 index 0000000000000000000000000000000000000000..cfd0ac3e124945330f31e4978bdd18a28115b0ec --- /dev/null +++ b/modules/kernel/src/kernel/StartKernel.java @@ -0,0 +1,512 @@ +package kernel; + +import static rescuecore2.misc.java.JavaTools.instantiate; +import static rescuecore2.misc.java.JavaTools.instantiateFactory; + +import java.awt.BorderLayout; +import java.awt.FlowLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import javax.swing.JButton; +import javax.swing.JDialog; +import javax.swing.JFrame; +import javax.swing.JPanel; + +import kernel.ui.KernelGUI; +import kernel.ui.KernelStartupPanel; +import kernel.ui.ScoreGraph; +import kernel.ui.ScoreTable; + +import org.dom4j.DocumentException; + +import rescuecore2.Constants; +import rescuecore2.GUIComponent; +import rescuecore2.components.Component; +import rescuecore2.components.ComponentConnectionException; +import rescuecore2.components.ComponentInitialisationException; +import rescuecore2.components.ComponentLauncher; +import rescuecore2.config.ClassNameSetValueConstraint; +import rescuecore2.config.ClassNameValueConstraint; +import rescuecore2.config.Config; +import rescuecore2.config.ConfigException; +import rescuecore2.config.IntegerValueConstraint; +import rescuecore2.connection.ConnectionException; +import rescuecore2.connection.ConnectionManager; +import rescuecore2.log.LogException; +import rescuecore2.log.Logger; +import rescuecore2.misc.CommandLineOptions; +import rescuecore2.misc.MutableBoolean; +import rescuecore2.misc.Pair; +import rescuecore2.misc.java.LoadableType; +import rescuecore2.misc.java.LoadableTypeProcessor; +import rescuecore2.registry.Factory; +import rescuecore2.registry.Registry; +import rescuecore2.scenario.Scenario; +import rescuecore2.score.ScoreFunction; +import rescuecore2.worldmodel.Entity; +import rescuecore2.worldmodel.WorldModel; + +/** + * A class for launching the kernel. + */ +public final class StartKernel { + private static final String NO_STARTUP_MENU = "--nomenu"; + private static final String NO_GUI = "--nogui"; + private static final String AUTORUN = "--autorun"; + + private static final String GIS_MANIFEST_KEY = "Gis"; + private static final String PERCEPTION_MANIFEST_KEY = "Perception"; + private static final String COMMUNICATION_MANIFEST_KEY = "CommunicationModel"; + + private static final String COMMAND_COLLECTOR_KEY = "kernel.commandcollectors"; + + private static final String TERMINATION_KEY = "kernel.termination"; + + private static final String GIS_REGEX = "(.+WorldModelCreator).class"; + private static final String PERCEPTION_REGEX = "(.+Perception).class"; + private static final String COMMUNICATION_REGEX = "(.+CommunicationModel).class"; + + private static final LoadableType GIS_LOADABLE_TYPE = new LoadableType(GIS_MANIFEST_KEY, GIS_REGEX, + WorldModelCreator.class); + private static final LoadableType PERCEPTION_LOADABLE_TYPE = new LoadableType(PERCEPTION_MANIFEST_KEY, + PERCEPTION_REGEX, Perception.class); + private static final LoadableType COMMUNICATION_LOADABLE_TYPE = new LoadableType(COMMUNICATION_MANIFEST_KEY, + COMMUNICATION_REGEX, CommunicationModel.class); + + private static final String KERNEL_STARTUP_TIME_KEY = "kernel.startup.connect-time"; + + private static final String COMMAND_FILTERS_KEY = "kernel.commandfilters"; + private static final String AGENT_REGISTRAR_KEY = "kernel.agents.registrar"; + private static final String GUI_COMPONENTS_KEY = "kernel.ui.components"; + + /** Utility class: private constructor. */ + private StartKernel() { + } + + /** + * Start a kernel. + * + * @param args Command line arguments. + * @throws DocumentException + */ + public static void main(String[] args) throws DocumentException { + Config config = new Config(); + boolean showStartupMenu = true; + boolean showGUI = true; + boolean autorun = false; + Logger.setLogContext("startup"); + try { + args = CommandLineOptions.processArgs(args, config); + for (String arg : args) { + if (arg.equalsIgnoreCase(NO_GUI)) { + showGUI = false; + } else if (arg.equalsIgnoreCase(NO_STARTUP_MENU)) { + showStartupMenu = false; + } else if (arg.equalsIgnoreCase(AUTORUN)) { + autorun = true; + } else { + Logger.warn("Unrecognised option: " + arg); + } + } + // Process jar files + processJarFiles(config); + Registry localRegistry = new Registry("Kernel local registry"); + // Register preferred message, entity and property factories + for (String next : config.getArrayValue(Constants.FACTORY_KEY, "")) { + Factory factory = instantiateFactory(next, Factory.class); + if (factory != null) { + localRegistry.registerFactory(factory); + Logger.info("Registered local factory: " + next); + } + } + + config.addConstraint(new IntegerValueConstraint(Constants.KERNEL_PORT_NUMBER_KEY, 1, 65535)); + config.addConstraint(new IntegerValueConstraint(KERNEL_STARTUP_TIME_KEY, 0, Integer.MAX_VALUE)); + config.addConstraint(new ClassNameSetValueConstraint(Constants.FACTORY_KEY, Factory.class)); + config.addConstraint(new ClassNameSetValueConstraint(COMMAND_FILTERS_KEY, CommandFilter.class)); + config.addConstraint(new ClassNameSetValueConstraint(TERMINATION_KEY, TerminationCondition.class)); + config.addConstraint(new ClassNameSetValueConstraint(COMMAND_COLLECTOR_KEY, CommandCollector.class)); + config.addConstraint(new ClassNameSetValueConstraint(GUI_COMPONENTS_KEY, GUIComponent.class)); + config.addConstraint(new ClassNameValueConstraint(AGENT_REGISTRAR_KEY, AgentRegistrar.class)); + config.addConstraint(new ClassNameValueConstraint(Constants.SCORE_FUNCTION_KEY, ScoreFunction.class)); + + Logger.setLogContext("kernel"); + final KernelInfo kernelInfo = createKernel(config, showStartupMenu); + if (kernelInfo == null) { + System.exit(0); + } + KernelGUI gui = null; + if (showGUI) { + gui = new KernelGUI(kernelInfo.kernel, kernelInfo.componentManager, config, localRegistry, !autorun); + for (GUIComponent next : kernelInfo.guiComponents) { + gui.addGUIComponent(next); + if (next instanceof KernelListener) { + kernelInfo.kernel.addKernelListener((KernelListener) next); + } + } + JFrame frame = new JFrame("Kernel GUI"); + frame.setExtendedState(JFrame.MAXIMIZED_BOTH); + frame.getContentPane().add(gui); + frame.pack(); + frame.addWindowListener(new WindowAdapter() { + public void windowClosing(WindowEvent e) { + kernelInfo.kernel.shutdown(); + System.exit(0); + } + }); + frame.setVisible(true); + } + initialiseKernel(kernelInfo, config, localRegistry); + autostartComponents(kernelInfo, localRegistry, gui, config); + if (!showGUI || autorun) { + waitForComponentManager(kernelInfo, config); + Kernel kernel = kernelInfo.kernel; + while (!kernel.hasTerminated()) { + kernel.timestep(); + } + kernel.shutdown(); + } + } catch (ConfigException e) { + Logger.fatal("Couldn't start kernel", e); + } catch (KernelException e) { + Logger.fatal("Couldn't start kernel", e); + } catch (IOException e) { + Logger.fatal("Couldn't start kernel", e); + } catch (LogException e) { + Logger.fatal("Couldn't write log", e); + } catch (InterruptedException e) { + Logger.fatal("Kernel interrupted"); + } catch (DocumentException e) { + Logger.fatal("Document Exception ", e); + } + } + + private static KernelInfo createKernel(Config config, boolean showMenu) throws KernelException, DocumentException { + KernelStartupOptions options = new KernelStartupOptions(config); + // Show the chooser GUI + if (showMenu) { + JFrame frame = new JFrame() { + private static final long serialVersionUID = 1L; + { + setUndecorated(true); + setVisible(true); + setLocationRelativeTo(null); + setTitle("RCRS Start options"); + setVisible(true); + java.awt.Toolkit.getDefaultToolkit().beep(); + setAlwaysOnTop(true); + setAlwaysOnTop(false); + } + }; + final JDialog dialog = new JDialog(frame, "Setup kernel options", true) { + private static final long serialVersionUID = 1L; + + @Override + public void setVisible(boolean b) { + super.setVisible(b); + if (!isVisible()) + frame.dispose(); + } + }; + KernelStartupPanel panel = new KernelStartupPanel(config, options); + JButton okButton = new JButton("OK"); + JButton cancelButton = new JButton("Cancel"); + JPanel buttons = new JPanel(new FlowLayout()); + buttons.add(okButton); + buttons.add(cancelButton); + dialog.getContentPane().add(panel, BorderLayout.CENTER); + dialog.getContentPane().add(buttons, BorderLayout.SOUTH); + final MutableBoolean ok = new MutableBoolean(true); + okButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + ok.set(true); + dialog.setVisible(false); + dialog.dispose(); + } + }); + cancelButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + ok.set(false); + dialog.setVisible(false); + dialog.dispose(); + } + }); + dialog.pack(); + dialog.setAlwaysOnTop(true); + dialog.setAlwaysOnTop(false); + dialog.setVisible(true); + if (!ok.get()) { + return null; + } + } + WorldModelCreator gis = options.getWorldModelCreator(); + Perception perception = options.getPerception(); + CommunicationModel comms = options.getCommunicationModel(); + CommandFilter filter = makeCommandFilter(config); + TerminationCondition termination = makeTerminationCondition(config); + ScoreFunction score = makeScoreFunction(config); + CommandCollector collector = makeCommandCollector(config); + + // Get the world model + WorldModel worldModel = gis.buildWorldModel(config); + Scenario scenario = gis.getScenario(config); + // Create the kernel + ScoreGraph graph = new ScoreGraph(score); + Kernel kernel = new Kernel(config, perception, comms, worldModel, gis, filter, termination, graph, collector); + // Create the component manager + ComponentManager componentManager = new ComponentManager(kernel, worldModel, config, scenario); + KernelInfo result = new KernelInfo(kernel, options, componentManager, + makeGUIComponents(config, componentManager, perception, comms, termination, filter, graph, collector, score)); + return result; + } + + private static void initialiseKernel(KernelInfo kernel, Config config, Registry registry) throws KernelException { + registerInitialAgents(config, kernel.componentManager, kernel.kernel.getWorldModel()); + if (!config.getBooleanValue(KernelConstants.INLINE_ONLY_KEY, false)) { + // Start the connection manager + ConnectionManager connectionManager = new ConnectionManager(); + try { + connectionManager.listen(config.getIntValue(Constants.KERNEL_PORT_NUMBER_KEY), registry, + kernel.componentManager); + } catch (IOException e) { + throw new KernelException("Couldn't open kernel port", e); + } + } + } + + private static void waitForComponentManager(final KernelInfo kernel, Config config) throws KernelException { + // Wait for all connections + // Set up a CountDownLatch + final CountDownLatch latch = new CountDownLatch(1); + final long timeout = config.getIntValue(KERNEL_STARTUP_TIME_KEY, -1); + Thread timeoutThread = null; + if (timeout > 0) { + timeoutThread = new Thread() { + public void run() { + try { + Thread.sleep(timeout); + latch.countDown(); + } catch (InterruptedException e) { + } + } + }; + } + Thread waitThread = new Thread() { + public void run() { + try { + kernel.componentManager.waitForAllAgents(); + kernel.componentManager.waitForAllSimulators(); + kernel.componentManager.waitForAllViewers(); + } catch (InterruptedException e) { + } + latch.countDown(); + } + }; + waitThread.start(); + if (timeoutThread != null) { + timeoutThread.start(); + } + // Wait at the latch until either everything is connected or the + // connection timeout expires + Logger.info("Waiting for all agents, simulators and viewers to connect."); + if (timeout > -1) { + Logger.info("Connection timeout is " + timeout + "ms"); + } + try { + latch.await(); + } catch (InterruptedException e) { + waitThread.interrupt(); + if (timeoutThread != null) { + timeoutThread.interrupt(); + } + throw new KernelException("Interrupted"); + } + } + + private static void autostartComponents(KernelInfo info, Registry registry, KernelGUI gui, Config config) + throws InterruptedException { + KernelStartupOptions options = info.options; + Collection> all = new ArrayList>(); + Config launchConfig = new Config(config); + launchConfig.removeExcept(Constants.RANDOM_SEED_KEY, Constants.RANDOM_CLASS_KEY); + for (Pair next : options.getInlineComponents()) { + if (next.second() > 0) { + all.add(new ComponentStarter(next.first(), info.componentManager, next.second(), registry, gui, launchConfig)); + } + } + ExecutorService service = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); + service.invokeAll(all); + } + + private static void registerInitialAgents(Config config, ComponentManager c, WorldModel model) + throws KernelException { + AgentRegistrar ar = instantiate(config.getValue(AGENT_REGISTRAR_KEY), AgentRegistrar.class); + if (ar == null) { + throw new KernelException("Couldn't instantiate agent registrar"); + } + ar.registerAgents(model, config, c); + } + + private static CommandFilter makeCommandFilter(Config config) { + ChainedCommandFilter result = new ChainedCommandFilter(); + List classNames = config.getArrayValue(COMMAND_FILTERS_KEY, null); + for (String next : classNames) { + Logger.debug("Command filter found: '" + next + "'"); + CommandFilter f = instantiate(next, CommandFilter.class); + if (f != null) { + result.addFilter(f); + } + } + return result; + } + + private static TerminationCondition makeTerminationCondition(Config config) { + List result = new ArrayList(); + for (String next : config.getArrayValue(TERMINATION_KEY, null)) { + TerminationCondition t = instantiate(next, TerminationCondition.class); + if (t != null) { + result.add(t); + } + } + return new OrTerminationCondition(result); + } + + private static ScoreFunction makeScoreFunction(Config config) { + String className = config.getValue(Constants.SCORE_FUNCTION_KEY); + ScoreFunction result = instantiate(className, ScoreFunction.class); + return new ScoreTable(result); + } + + private static CommandCollector makeCommandCollector(Config config) { + List classNames = config.getArrayValue(COMMAND_COLLECTOR_KEY); + CompositeCommandCollector result = new CompositeCommandCollector(); + for (String next : classNames) { + CommandCollector c = instantiate(next, CommandCollector.class); + if (c != null) { + result.addCommandCollector(c); + } + } + return result; + } + + private static List makeGUIComponents(Config config, ComponentManager componentManager, + Object... objectsToTest) { + List result = new ArrayList(); + result.add(componentManager); + List classNames = config.getArrayValue(GUI_COMPONENTS_KEY, null); + for (String next : classNames) { + Logger.debug("GUI component found: '" + next + "'"); + GUIComponent c = instantiate(next, GUIComponent.class); + if (c != null) { + result.add(c); + } + } + for (Object next : objectsToTest) { + if (next instanceof GUIComponent) { + result.add((GUIComponent) next); + } + } + return result; + } + + private static void processJarFiles(Config config) throws IOException { + LoadableTypeProcessor processor = new LoadableTypeProcessor(config); + processor.addFactoryRegisterCallbacks(Registry.SYSTEM_REGISTRY); + processor.addConfigUpdater(LoadableType.AGENT, config, KernelConstants.AGENTS_KEY); + processor.addConfigUpdater(LoadableType.SIMULATOR, config, KernelConstants.SIMULATORS_KEY); + processor.addConfigUpdater(LoadableType.VIEWER, config, KernelConstants.VIEWERS_KEY); + processor.addConfigUpdater(LoadableType.COMPONENT, config, KernelConstants.COMPONENTS_KEY); + processor.addConfigUpdater(GIS_LOADABLE_TYPE, config, KernelConstants.GIS_KEY); + processor.addConfigUpdater(PERCEPTION_LOADABLE_TYPE, config, KernelConstants.PERCEPTION_KEY); + processor.addConfigUpdater(COMMUNICATION_LOADABLE_TYPE, config, KernelConstants.COMMUNICATION_MODEL_KEY); + Logger.info("Looking for gis, perception, communication, agent, simulator and viewer implementations"); + processor.process(); + } + + private static class ComponentStarter implements Callable { + private String className; + private ComponentManager componentManager; + private int count; + private Registry registry; + private KernelGUI gui; + private Config config; + + public ComponentStarter(String className, ComponentManager componentManager, int count, Registry registry, + KernelGUI gui, Config config) { + this.className = className; + this.componentManager = componentManager; + this.count = count; + this.registry = registry; + this.gui = gui; + this.config = config; + Logger.debug("New ComponentStarter: " + className + " * " + count); + } + + public Void call() throws InterruptedException { + Logger.debug("ComponentStarter running: " + className + " * " + count); + ComponentLauncher launcher = new InlineComponentLauncher(componentManager, config); + launcher.setDefaultRegistry(registry); + Logger.info("Launching " + count + " instances of component '" + className + "'..."); + for (int i = 0; i < count; ++i) { + Component c = instantiate(className, Component.class); + if (c == null) { + break; + } + Logger.info("Launching " + className + " instance " + (i + 1) + "..."); + try { + c.initialise(); + launcher.connect(c); + if (gui != null && c instanceof GUIComponent) { + gui.addGUIComponent((GUIComponent) c); + } + Logger.info(className + "instance " + (i + 1) + " launched successfully"); + } catch (ComponentConnectionException e) { + Logger.info(className + "instance " + (i + 1) + " failed: " + e.getMessage()); + break; + } catch (ComponentInitialisationException e) { + Logger.info(className + "instance " + (i + 1) + " failed", e); + } catch (ConnectionException e) { + Logger.info(className + "instance " + (i + 1) + " failed", e); + } + } + return null; + } + } + + private static class KernelInfo { + Kernel kernel; + KernelStartupOptions options; + ComponentManager componentManager; + List guiComponents; + + public KernelInfo(Kernel kernel, KernelStartupOptions options, ComponentManager componentManager, + List otherComponents) { + this.kernel = kernel; + this.options = options; + this.componentManager = componentManager; + guiComponents = new ArrayList(otherComponents); + } + } + + public class DummyFrame extends JFrame { + public DummyFrame(String title) { + super(title); + setUndecorated(true); + setVisible(true); + setLocationRelativeTo(null); + } + } +} \ No newline at end of file diff --git a/modules/kernel/src/kernel/TerminationCondition.java b/modules/kernel/src/kernel/TerminationCondition.java new file mode 100644 index 0000000000000000000000000000000000000000..d90b3a536914efab1bb962dee2b8fa3bd4cd218f --- /dev/null +++ b/modules/kernel/src/kernel/TerminationCondition.java @@ -0,0 +1,21 @@ +package kernel; + +import rescuecore2.config.Config; + +/** + Termination conditions tell the kernel when to stop running a simulation. + */ +public interface TerminationCondition { + /** + Initialise this termination condition. + @param config The configuration. + */ + void initialise(Config config); + + /** + Return whether this termination condition has been met. + @param state The state of the kernel. + @return True if this termination condition has been met and the simulation should stop, false otherwise. + */ + boolean shouldStop(KernelState state); +} diff --git a/modules/kernel/src/kernel/TimedCommandCollector.java b/modules/kernel/src/kernel/TimedCommandCollector.java new file mode 100644 index 0000000000000000000000000000000000000000..e347973e35ecd0918ca8128e9f2d22ca6746f9bd --- /dev/null +++ b/modules/kernel/src/kernel/TimedCommandCollector.java @@ -0,0 +1,48 @@ +package kernel; + +import rescuecore2.config.Config; +import rescuecore2.messages.Command; +import rescuecore2.log.Logger; + +import java.util.Collection; +import java.util.ArrayList; + +/** + A CommandCollector that waits for a certain amount of time before returning agent commands. +*/ +public class TimedCommandCollector implements CommandCollector { + private static final int DEFAULT_TIME = 1000; + private static final String TIME_KEY = "kernel.agents.think-time"; + + private long time; + + @Override + public void initialise(Config config) { + time = config.getIntValue(TIME_KEY, DEFAULT_TIME); + } + + @Override + public Collection getAgentCommands(Collection agents, int timestep) throws InterruptedException { + long now = System.currentTimeMillis(); + long end = now + time; + while (now < end) { + long diff = end - now; + Logger.trace(this + " waiting for " + diff + "ms"); + Thread.sleep(diff); + now = System.currentTimeMillis(); + } + Collection result = new ArrayList(); + for (AgentProxy next : agents) { + Collection commands = next.getAgentCommands(timestep); + result.addAll(commands); + } + Logger.trace(this + " returning " + result.size() + " commands"); + Logger.trace(this + " returning " + result); + return result; + } + + @Override + public String toString() { + return "Timed command collector"; + } +} diff --git a/modules/kernel/src/kernel/TimestepTerminationCondition.java b/modules/kernel/src/kernel/TimestepTerminationCondition.java new file mode 100644 index 0000000000000000000000000000000000000000..e6517d9768c028e657a9d2a5da34e318bea6987e --- /dev/null +++ b/modules/kernel/src/kernel/TimestepTerminationCondition.java @@ -0,0 +1,35 @@ +package kernel; + +import rescuecore2.config.Config; +import rescuecore2.log.Logger; + +/** + A TerminationCondition that terminates the simulation after a specified timestep. +*/ +public class TimestepTerminationCondition implements TerminationCondition { + /** + The config key describing the number of timesteps to run. + */ + private static final String TIMESTEPS_KEY = "kernel.timesteps"; + + private int time; + + @Override + public void initialise(Config config) { + time = config.getIntValue(TIMESTEPS_KEY); + } + + @Override + public boolean shouldStop(KernelState state) { + if (state.getTime() >= time) { + Logger.info("TimestepTerminationCondition fired: " + state.getTime() + " >= " + time); + return true; + } + return false; + } + + @Override + public String toString() { + return "Timestep >= " + time + ""; + } +} diff --git a/modules/kernel/src/kernel/ViewerProxy.java b/modules/kernel/src/kernel/ViewerProxy.java new file mode 100644 index 0000000000000000000000000000000000000000..f3b036d8837f13bf47b485701d9460da7b359599 --- /dev/null +++ b/modules/kernel/src/kernel/ViewerProxy.java @@ -0,0 +1,41 @@ +package kernel; + +import rescuecore2.connection.Connection; +import rescuecore2.messages.control.KVTimestep; +import rescuecore2.Timestep; + +/** + This class is the kernel interface to a viewer. + */ +public class ViewerProxy extends AbstractKernelComponent { + private int id; + + /** + Construct a viewer. + @param name The name of the viewer. + @param id The ID of the viewer. + @param c The connection to the viewer. + */ + public ViewerProxy(String name, int id, Connection c) { + super(name, c); + this.id = id; + } + + /** + Send a Timestep structure to this viewer. + @param time The Timestep to send. + */ + public void sendTimestep(Timestep time) { + send(new KVTimestep(id, time.getTime(), time.getCommands(), time.getChangeSet())); + } + + @Override + public String toString() { + return getName() + " (" + id + "): " + getConnection().toString(); + } + + @Override + public int hashCode() { + return Integer.hashCode(id); + } +} diff --git a/modules/kernel/src/kernel/WorldModelCreator.java b/modules/kernel/src/kernel/WorldModelCreator.java new file mode 100644 index 0000000000000000000000000000000000000000..9e526dec8272ae5d0f49df9202036171114a3870 --- /dev/null +++ b/modules/kernel/src/kernel/WorldModelCreator.java @@ -0,0 +1,35 @@ +package kernel; + + + +import org.dom4j.DocumentException; + +import rescuecore2.scenario.Scenario; +import rescuecore2.worldmodel.WorldModel; +import rescuecore2.worldmodel.Entity; +import rescuecore2.config.Config; + +/** + * The interface for world model creators, e.g. GIS systems. + */ +public interface WorldModelCreator extends EntityIDGenerator { + /** + * Create a new WorldModel. + * + * @param config + * The config to use. + * @return A new world model. + * @throws KernelException + * If there is a problem building the world model. + */ + WorldModel buildWorldModel(Config config) + throws KernelException; + + /** + * Returns the scenario of the simulation + * + * @param config + * @return a scenario + */ + public Scenario getScenario(Config config) throws DocumentException; +} diff --git a/modules/kernel/src/kernel/WrongTimeCommandFilter.java b/modules/kernel/src/kernel/WrongTimeCommandFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..18a6050f435a50ab77fd3d72536e9b8009ee298b --- /dev/null +++ b/modules/kernel/src/kernel/WrongTimeCommandFilter.java @@ -0,0 +1,18 @@ +package kernel; + +import rescuecore2.messages.Command; +import rescuecore2.log.Logger; + +/** + A CommandFilter that ignores agent commands that have the wrong timestamp. + */ +public class WrongTimeCommandFilter extends AbstractCommandFilter { + @Override + protected boolean allowed(Command command, KernelState state) { + if (command.getTime() == state.getTime()) { + return true; + } + Logger.info("Ignoring command " + command + ": Wrong timestamp: " + command.getTime() + " should be " + state.getTime()); + return false; + } +} diff --git a/modules/kernel/src/kernel/ui/ComponentManagerGUI.java b/modules/kernel/src/kernel/ui/ComponentManagerGUI.java new file mode 100644 index 0000000000000000000000000000000000000000..430e9c5f21ce0458824a048f5a5b23bb38c082af --- /dev/null +++ b/modules/kernel/src/kernel/ui/ComponentManagerGUI.java @@ -0,0 +1,105 @@ +package kernel.ui; + +import javax.swing.JPanel; +import javax.swing.JList; +import javax.swing.JScrollPane; +import javax.swing.BorderFactory; +import javax.swing.SwingUtilities; +import java.awt.GridLayout; + +import java.util.List; + +import rescuecore2.misc.gui.ListModelList; + +/** + A user interface component for viewing that state of the ComponentManager. + */ +public class ComponentManagerGUI extends JPanel { + private JList uncontrolledAgents; + private JList agentAck; + private JList simulatorAck; + private JList viewerAck; + private ListModelList uncontrolledAgentsModel; + private ListModelList agentAckModel; + private ListModelList simulatorAckModel; + private ListModelList viewerAckModel; + + /** + Construct a new ComponentManagerGUI. + */ + public ComponentManagerGUI() { + // CHECKSTYLE:OFF:MagicNumber + super(new GridLayout(4, 1)); + // CHECKSTYLE:ON:MagicNumber + uncontrolledAgentsModel = new ListModelList(); + agentAckModel = new ListModelList(); + simulatorAckModel = new ListModelList(); + viewerAckModel = new ListModelList(); + uncontrolledAgents = new JList(uncontrolledAgentsModel); + agentAck = new JList(agentAckModel); + simulatorAck = new JList(simulatorAckModel); + viewerAck = new JList(viewerAckModel); + add(uncontrolledAgents, "Agents with no controller"); + add(agentAck, "Agents that have not acknowledged"); + add(simulatorAck, "Simulators that have not acknowledged"); + add(viewerAck, "Viewers that have not acknowledged"); + } + + /** + Update the list of uncontrolled agents. + @param data A list of uncontrolled agent descriptions. This list will be displated verbatim. + */ + public void updateUncontrolledAgents(final List data) { + SwingUtilities.invokeLater(new Runnable() { + public void run() { + uncontrolledAgentsModel.clear(); + uncontrolledAgentsModel.addAll(data); + } + }); + } + + /** + Update the list of agents that have not acknowledged the connection. + @param data A list of unacknowledged agent descriptions. This list will be displayed verbatim. + */ + public void updateAgentAck(final List data) { + SwingUtilities.invokeLater(new Runnable() { + public void run() { + agentAckModel.clear(); + agentAckModel.addAll(data); + } + }); + } + + /** + Update the list of simulators that have not acknowledged the connection. + @param data A list of unacknowledged simulator descriptions. This list will be displayed verbatim. + */ + public void updateSimulatorAck(final List data) { + SwingUtilities.invokeLater(new Runnable() { + public void run() { + simulatorAckModel.clear(); + simulatorAckModel.addAll(data); + } + }); + } + + /** + Update the list of viewers that have not acknowledged the connection. + @param data A list of unacknowledged viewer descriptions. This list will be displayed verbatim. + */ + public void updateViewerAck(final List data) { + SwingUtilities.invokeLater(new Runnable() { + public void run() { + viewerAckModel.clear(); + viewerAckModel.addAll(data); + } + }); + } + + private void add(JList list, String title) { + JScrollPane scroll = new JScrollPane(list); + scroll.setBorder(BorderFactory.createTitledBorder(title)); + add(scroll); + } +} diff --git a/modules/kernel/src/kernel/ui/KernelControlPanel.java b/modules/kernel/src/kernel/ui/KernelControlPanel.java new file mode 100644 index 0000000000000000000000000000000000000000..e3e5693e9fcdf38e2df2fc4622fcff5d3ae2ab06 --- /dev/null +++ b/modules/kernel/src/kernel/ui/KernelControlPanel.java @@ -0,0 +1,325 @@ +package kernel.ui; + +import static rescuecore2.misc.java.JavaTools.instantiate; + +import java.awt.GridLayout; +import java.awt.event.ActionListener; +import java.awt.event.ActionEvent; + +import javax.swing.JPanel; +import javax.swing.JButton; +import javax.swing.SwingUtilities; +import javax.swing.JOptionPane; + +import java.util.Collection; +import java.util.List; +import java.util.ArrayList; + +import kernel.Kernel; +import kernel.KernelException; +import kernel.ComponentManager; +import kernel.InlineComponentLauncher; + +import rescuecore2.misc.WorkerThread; +import rescuecore2.config.Config; +import rescuecore2.config.NoSuchConfigOptionException; +import rescuecore2.connection.ConnectionException; +import rescuecore2.components.Component; +import rescuecore2.components.ComponentLauncher; +import rescuecore2.components.ComponentInitialisationException; +import rescuecore2.components.ComponentConnectionException; +import rescuecore2.log.LogException; +import rescuecore2.log.Logger; +import rescuecore2.registry.Registry; + +/** + A JComponent containing various controls for the kernel GUI. + */ +public class KernelControlPanel extends JPanel { + private Kernel kernel; + private Config config; + private Registry registry; + private ComponentManager componentManager; + private ComponentLauncher launcher; + private Collection controlButtons; + private JButton stepButton; + private JButton runButton; + + private volatile boolean running; + private volatile boolean step; + private RunThread runThread; + + private final Object runLock = new Object(); + + /** + Create a KernelControlPanel component. + @param kernel The kernel to control. + @param config The kernel configuration. + @param componentManager The kernel component manager. + @param registry The registry to use for new connections. + */ + public KernelControlPanel(Kernel kernel, Config config, ComponentManager componentManager, Registry registry) { + super(new GridLayout(0, 1)); + this.kernel = kernel; + this.config = config; + this.componentManager = componentManager; + this.registry = registry; + controlButtons = new ArrayList(); + JButton addAgent = new JButton("Add agent"); + JButton removeAgent = new JButton("Remove agent"); + JButton addSim = new JButton("Add simulator"); + JButton removeSim = new JButton("Remove simulator"); + JButton addViewer = new JButton("Add viewer"); + JButton removeViewer = new JButton("Remove viewer"); + stepButton = new JButton("Step"); + runButton = new JButton("Run"); + add(addAgent); + add(removeAgent); + add(addSim); + add(removeSim); + add(addViewer); + add(removeViewer); + add(stepButton); + add(runButton); + controlButtons.add(addAgent); + controlButtons.add(removeAgent); + controlButtons.add(addSim); + controlButtons.add(removeSim); + controlButtons.add(addViewer); + controlButtons.add(removeViewer); + addAgent.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + addAgent(); + } + }); + removeAgent.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + removeAgent(); + } + }); + addSim.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + addSim(); + } + }); + removeSim.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + removeSim(); + } + }); + addViewer.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + addViewer(); + } + }); + removeViewer.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + removeViewer(); + } + }); + stepButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + stepButtonPressed(); + } + }); + runButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + runButtonPressed(); + } + }); + runThread = new RunThread(); + running = false; + } + + /** + Activate this control panel. + */ + public void activate() { + runThread.start(); + launcher = new InlineComponentLauncher(componentManager, config); + launcher.setDefaultRegistry(Registry.getCurrentRegistry()); + } + + private void addAgent() { + Component[] as = createComponents("agents"); + addComponent(as, "agent"); + } + + private void removeAgent() { + } + + private void addSim() { + Component[] ss = createComponents("simulators"); + addComponent(ss, "simulator"); + } + + private void removeSim() { + } + + private void addViewer() { + Component[] vs = createComponents("viewers"); + addComponent(vs, "viewer"); + } + + private void removeViewer() { + } + + private void addComponent(Component[] options, String type) { + if (options.length == 0) { + return; + } + Component c = (Component)JOptionPane.showInputDialog(this, "Choose a " + type, "Choose a " + type, JOptionPane.QUESTION_MESSAGE, null, options, options[0]); + if (c == null) { + return; + } + try { + c.initialise(); + launcher.connect(c); + } + catch (NoSuchConfigOptionException e) { + JOptionPane.showMessageDialog(this, "Adding " + type + " failed: " + e); + } + catch (ComponentInitialisationException e) { + JOptionPane.showMessageDialog(this, "Adding " + type + " failed: " + e); + } + catch (ComponentConnectionException e) { + JOptionPane.showMessageDialog(this, "Adding " + type + " failed: " + e.getMessage()); + } + catch (ConnectionException e) { + JOptionPane.showMessageDialog(this, "Adding " + type + " failed: " + e); + } + catch (InterruptedException e) { + JOptionPane.showMessageDialog(this, "Adding " + type + " failed: " + e); + } + } + + private void stepButtonPressed() { + // This method should only be called on the event dispatch thread so it's OK to update the GUI. + // Do a sanity check just in case. + if (!SwingUtilities.isEventDispatchThread()) { + throw new RuntimeException("stepButtonPressed called by thread " + Thread.currentThread() + ", not the event dispatch thread."); + } + synchronized (runLock) { + if (!running) { + step = true; + stepButton.setText("Working"); + stepButton.setEnabled(false); + runButton.setEnabled(false); + setControlButtonsEnabled(false); + runLock.notifyAll(); + } + } + } + + private void endStep() { + step = false; + stepButton.setText("Step"); + stepButton.setEnabled(true); + runButton.setEnabled(true); + setControlButtonsEnabled(true); + } + + private void runButtonPressed() { + // This method should only be called on the event dispatch thread so it's OK to update the GUI. + // Do a sanity check just in case. + if (!SwingUtilities.isEventDispatchThread()) { + throw new RuntimeException("runButtonPressed called by thread " + Thread.currentThread() + ", not the event dispatch thread."); + } + synchronized (runLock) { + if (running) { + setControlButtonsEnabled(true); + stepButton.setEnabled(true); + runButton.setText("Run"); + } + else { + setControlButtonsEnabled(false); + stepButton.setEnabled(false); + runButton.setText("Stop"); + } + running = !running; + runLock.notifyAll(); + } + } + + private void setControlButtonsEnabled(boolean b) { + for (JButton next : controlButtons) { + next.setEnabled(b); + } + } + + private void disableAllButtons() throws InterruptedException { + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + setControlButtonsEnabled(false); + stepButton.setEnabled(false); + runButton.setEnabled(false); + } + }); + } + catch (java.lang.reflect.InvocationTargetException e) { + // Should never happen + Logger.error("KernelControlPanel.disableAllButtons", e); + } + } + + private boolean shouldStep() { + synchronized (runLock) { + return running || step; + } + } + + private Component[] createComponents(String type) { + List classNames = config.getArrayValue("kernel." + type, null); + List instances = new ArrayList(); + for (String next : classNames) { + Component c = instantiate(next, Component.class); + if (c != null) { + instances.add(c); + } + } + return instances.toArray(new Component[0]); + } + + private class RunThread extends WorkerThread { + @Override + public boolean work() throws InterruptedException { + if (shouldStep()) { + if (!kernel.hasTerminated()) { + try { + kernel.timestep(); + } + catch (KernelException e) { + Logger.error("Kernel error", e); + kernel.shutdown(); + disableAllButtons(); + return false; + } + catch (LogException e) { + Logger.error("Log error", e); + kernel.shutdown(); + disableAllButtons(); + return false; + } + synchronized (runLock) { + if (step) { + endStep(); + } + } + return true; + } + else { + kernel.shutdown(); + disableAllButtons(); + return false; + } + } + else { + synchronized (runLock) { + runLock.wait(1000); + } + return true; + } + } + } +} diff --git a/modules/kernel/src/kernel/ui/KernelGUI.java b/modules/kernel/src/kernel/ui/KernelGUI.java new file mode 100644 index 0000000000000000000000000000000000000000..ebcb16022ba0f25e7ccef1842d623363f83289bf --- /dev/null +++ b/modules/kernel/src/kernel/ui/KernelGUI.java @@ -0,0 +1,68 @@ +package kernel.ui; + +import java.awt.BorderLayout; +import java.awt.Dimension; +import javax.swing.JPanel; +import javax.swing.JTabbedPane; +import javax.swing.SwingUtilities; + +import kernel.Kernel; +import kernel.ComponentManager; + +import rescuecore2.config.Config; +import rescuecore2.registry.Registry; +import rescuecore2.GUIComponent; + +/** + A GUI for the kernel. + */ +public class KernelGUI extends JPanel { + private static final int STATUS_SIZE = 300; + + private Kernel kernel; + private KernelStatus status; + private KernelControlPanel control; + private JTabbedPane tabs; + private Config config; + + /** + Construct a KernelGUI component. + @param kernel The kernel to watch. + @param componentManager The kernel component manager. + @param config The kernel configuration. + @param registry The registry to use for new connections. + @param controls Whether to show the control panel or not. + */ + public KernelGUI(Kernel kernel, ComponentManager componentManager, Config config, Registry registry, boolean controls) { + super(new BorderLayout()); + this.kernel = kernel; + this.config = config; + status = new KernelStatus(kernel); + status.setPreferredSize(new Dimension(STATUS_SIZE, STATUS_SIZE)); + add(status, BorderLayout.EAST); + tabs = new JTabbedPane(); + add(tabs, BorderLayout.CENTER); + if (controls) { + control = new KernelControlPanel(kernel, config, componentManager, registry); + add(control, BorderLayout.WEST); + control.activate(); + } + addGUIComponent(componentManager); + + } + + /** + Add a kernel GUI component. + @param c The GUI component to add. + */ + public void addGUIComponent(final GUIComponent c) { + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + if (c.getGUIComponent() != null) { + tabs.addTab(c.getGUIComponentName(), c.getGUIComponent()); + } + } + }); + } +} diff --git a/modules/kernel/src/kernel/ui/KernelStartupPanel.java b/modules/kernel/src/kernel/ui/KernelStartupPanel.java new file mode 100644 index 0000000000000000000000000000000000000000..912ebf2ae8e43a6339e4708c3127987a9e1b4d3f --- /dev/null +++ b/modules/kernel/src/kernel/ui/KernelStartupPanel.java @@ -0,0 +1,241 @@ +package kernel.ui; + +import javax.swing.JPanel; +import javax.swing.JCheckBox; +import javax.swing.JLabel; +import javax.swing.JSpinner; +import javax.swing.SpinnerNumberModel; +import javax.swing.JScrollPane; +import javax.swing.BorderFactory; +import javax.swing.JComboBox; +import javax.swing.JSplitPane; + +import javax.swing.event.ChangeListener; +import javax.swing.event.ChangeEvent; + +import java.awt.BorderLayout; +import java.awt.GridBagLayout; +import java.awt.GridBagConstraints; +import java.awt.event.ActionListener; +import java.awt.event.ActionEvent; + +import java.util.Collection; +import java.util.List; + +import kernel.WorldModelCreator; +import kernel.Perception; +import kernel.CommunicationModel; +import kernel.KernelStartupOptions; + +import rescuecore2.config.Config; +import rescuecore2.components.Component; +import rescuecore2.misc.gui.ConfigTree; + +/** + A JPanel for displaying and editing kernel startup options. +*/ +public class KernelStartupPanel extends JPanel { + private static final String AUTO_SUFFIX = ".auto"; + + /** + Create a kernel launch GUI. + @param config The system configuration. + @param options The kernel startup options. + */ + public KernelStartupPanel(Config config, final KernelStartupOptions options) { + super(new BorderLayout()); + final JComboBox gis = createComboBox(options.getAvailableWorldModelCreators(), options.getWorldModelCreator()); + final JComboBox perception = createComboBox(options.getAvailablePerceptions(), options.getPerception()); + final JComboBox comms = createComboBox(options.getAvailableCommunicationModels(), options.getCommunicationModel()); + CheckboxPanel simulators = new CheckboxPanel(options.getAvailableSimulators(), options); + CheckboxPanel viewers = new CheckboxPanel(options.getAvailableViewers(), options); + SpinnerPanel agents = new SpinnerPanel(options.getAvailableAgents(), options); + CheckboxPanel otherComponents = new CheckboxPanel(options.getAvailableComponents(), options); + JScrollPane simulatorsScroll = new JScrollPane(simulators); + simulatorsScroll.setBorder(BorderFactory.createTitledBorder("Simulators")); + JScrollPane viewersScroll = new JScrollPane(viewers); + viewersScroll.setBorder(BorderFactory.createTitledBorder("Viewers")); + JScrollPane agentsScroll = new JScrollPane(agents); + agentsScroll.setBorder(BorderFactory.createTitledBorder("Agents")); + JScrollPane componentsScroll = new JScrollPane(otherComponents); + componentsScroll.setBorder(BorderFactory.createTitledBorder("Other components")); + ConfigTree configTree = new ConfigTree(config); + JScrollPane configTreeScroll = new JScrollPane(configTree); + configTreeScroll.setBorder(BorderFactory.createTitledBorder("Config")); + + GridBagLayout layout = new GridBagLayout(); + GridBagConstraints c = new GridBagConstraints(); + JPanel optionsPanel = new JPanel(layout); + JSplitPane top = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, optionsPanel, configTreeScroll); + add(top, BorderLayout.CENTER); + + c.gridx = 0; + c.gridy = 0; + c.gridwidth = 1; + c.gridheight = 1; + c.weightx = 0; + c.weighty = 0; + c.fill = GridBagConstraints.BOTH; + c.anchor = GridBagConstraints.CENTER; + JLabel l = new JLabel("GIS:"); + layout.setConstraints(l, c); + optionsPanel.add(l); + c.gridy = 1; + l = new JLabel("Perception:"); + layout.setConstraints(l, c); + optionsPanel.add(l); + c.gridy = 2; + l = new JLabel("Communication model:"); + layout.setConstraints(l, c); + optionsPanel.add(l); + c.gridy = 0; + c.gridx = 1; + c.weightx = 1; + layout.setConstraints(gis, c); + optionsPanel.add(gis); + c.gridy = 1; + layout.setConstraints(perception, c); + optionsPanel.add(perception); + c.gridy = 2; + layout.setConstraints(comms, c); + optionsPanel.add(comms); + + // Simulators, viewers, agents, other components + c.gridx = 0; + ++c.gridy; + c.gridwidth = 2; + c.weightx = 1; + c.weighty = 1; + layout.setConstraints(simulatorsScroll, c); + optionsPanel.add(simulatorsScroll); + ++c.gridy; + layout.setConstraints(viewersScroll, c); + optionsPanel.add(viewersScroll); + ++c.gridy; + layout.setConstraints(agentsScroll, c); + optionsPanel.add(agentsScroll); + ++c.gridy; + layout.setConstraints(componentsScroll, c); + optionsPanel.add(componentsScroll); + + // Event listeners + gis.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + options.setWorldModelCreator((WorldModelCreator)gis.getSelectedItem()); + } + }); + perception.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + options.setPerception((Perception)perception.getSelectedItem()); + } + }); + comms.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + options.setCommunicationModel((CommunicationModel)comms.getSelectedItem()); + } + }); + } + + private JComboBox createComboBox(List options, T selected) { + Object[] choices = options.toArray(); + JComboBox result = new JComboBox(choices); + result.setSelectedItem(selected); + result.setEnabled(choices.length > 1); + return result; + } + + private static final class CheckboxPanel extends JPanel { + private CheckboxPanel(Collection available, final KernelStartupOptions options) { + GridBagLayout layout = new GridBagLayout(); + GridBagConstraints c = new GridBagConstraints(); + c.gridx = 0; + c.gridy = 0; + c.weightx = 1; + c.weighty = 1; + c.gridwidth = 1; + c.gridheight = 1; + c.fill = GridBagConstraints.BOTH; + c.anchor = GridBagConstraints.CENTER; + setLayout(layout); + for (Component t : available) { + c.gridx = 0; + c.weightx = 1; + JLabel l = new JLabel(t.getName()); + layout.setConstraints(l, c); + add(l); + c.gridx = 1; + c.weightx = 0; + final JCheckBox check = new JCheckBox(); + check.setSelected(options.getInstanceCount(t) > 0); + options.setInstanceCount(t, check.isSelected() ? 1 : 0); + layout.setConstraints(check, c); + add(check); + ++c.gridy; + final Component comp = t; + check.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + options.setInstanceCount(comp, check.isSelected() ? 1 : 0); + } + }); + } + } + } + + private static final class SpinnerPanel extends JPanel { + private SpinnerPanel(Collection available, final KernelStartupOptions options) { + GridBagLayout layout = new GridBagLayout(); + GridBagConstraints c = new GridBagConstraints(); + c.gridx = 0; + c.gridy = 0; + c.weightx = 1; + c.weighty = 1; + c.fill = GridBagConstraints.BOTH; + c.anchor = GridBagConstraints.CENTER; + setLayout(layout); + for (Component t : available) { + c.gridx = 0; + c.weightx = 1; + JLabel l = new JLabel(t.getName()); + layout.setConstraints(l, c); + add(l); + int count = options.getInstanceCount(t); + boolean all = count == Integer.MAX_VALUE; + final JSpinner spinner = new JSpinner(new SpinnerNumberModel(count == Integer.MAX_VALUE ? 0 : count, 0, Integer.MAX_VALUE, 1)); + final JCheckBox check = new JCheckBox("Maximum"); + check.setSelected(all); + spinner.setEnabled(!all); + final Component comp = t; + check.addChangeListener(new ChangeListener() { + @Override + public void stateChanged(ChangeEvent e) { + spinner.setEnabled(!check.isSelected()); + if (check.isSelected()) { + options.setInstanceCount(comp, Integer.MAX_VALUE); + } + else { + options.setInstanceCount(comp, ((Number)spinner.getValue()).intValue()); + } + } + }); + spinner.addChangeListener(new ChangeListener() { + @Override + public void stateChanged(ChangeEvent e) { + options.setInstanceCount(comp, ((Number)spinner.getValue()).intValue()); + } + }); + c.gridx = 1; + c.weightx = 0; + layout.setConstraints(spinner, c); + add(spinner); + c.gridx = 2; + layout.setConstraints(check, c); + add(check); + ++c.gridy; + } + } + } +} diff --git a/modules/kernel/src/kernel/ui/KernelStatus.java b/modules/kernel/src/kernel/ui/KernelStatus.java new file mode 100644 index 0000000000000000000000000000000000000000..1205f53783223570f94186e40d4e86be6a4ee564 --- /dev/null +++ b/modules/kernel/src/kernel/ui/KernelStatus.java @@ -0,0 +1,128 @@ +package kernel.ui; + +import java.util.ArrayList; +import java.awt.BorderLayout; +import java.awt.GridLayout; +import javax.swing.JPanel; +import javax.swing.JList; +import javax.swing.JLabel; +import javax.swing.JScrollPane; +import javax.swing.BorderFactory; +import javax.swing.SwingUtilities; + +import kernel.KernelListener; +import kernel.AgentProxy; +import kernel.SimulatorProxy; +import kernel.ViewerProxy; +import kernel.Kernel; + +import rescuecore2.misc.gui.ListModelList; +import rescuecore2.Timestep; + +/** + A status panel for the kernel. + */ +public class KernelStatus extends JPanel implements KernelListener { + private Kernel kernel; + + private ListModelList agents; + private ListModelList simulators; + private ListModelList viewers; + + private JList agentsList; + private JList simulatorsList; + private JList viewersList; + private JLabel timeLabel; + private JLabel scoreLabel; + + /** + Construct a KernelStatus component. + @param kernel The Kernel to watch. + */ + public KernelStatus(Kernel kernel) { + super(new BorderLayout()); + this.kernel = kernel; + agents = new ListModelList(new ArrayList()); + simulators = new ListModelList(new ArrayList()); + viewers = new ListModelList(new ArrayList()); + kernel.addKernelListener(this); + agentsList = new JList(agents); + simulatorsList = new JList(simulators); + viewersList = new JList(viewers); + // CHECKSTYLE:OFF:MagicNumber + JPanel lists = new JPanel(new GridLayout(3, 1)); + // CHECKSTYLE:ON:MagicNumber + JScrollPane agentsScroll = new JScrollPane(agentsList); + JScrollPane simulatorsScroll = new JScrollPane(simulatorsList); + JScrollPane viewersScroll = new JScrollPane(viewersList); + agentsScroll.setBorder(BorderFactory.createTitledBorder("Agents")); + simulatorsScroll.setBorder(BorderFactory.createTitledBorder("Simulators")); + viewersScroll.setBorder(BorderFactory.createTitledBorder("Viewers")); + lists.add(agentsScroll); + lists.add(simulatorsScroll); + lists.add(viewersScroll); + add(lists, BorderLayout.CENTER); + timeLabel = new JLabel("Time: not started", JLabel.CENTER); + scoreLabel = new JLabel("Score: not started", JLabel.CENTER); + JPanel labels = new JPanel(new GridLayout(1, 2)); + labels.add(timeLabel); + labels.add(scoreLabel); + add(labels, BorderLayout.NORTH); + agents.addAll(kernel.getAllAgents()); + simulators.addAll(kernel.getAllSimulators()); + viewers.addAll(kernel.getAllViewers()); + } + + @Override + public void simulationStarted(Kernel k) { + } + + @Override + public void simulationEnded(Kernel k) { + SwingUtilities.invokeLater(new Runnable() { + public void run() { + timeLabel.setText("Time: ended"); + } + }); + } + + @Override + public void timestepCompleted(Kernel k, final Timestep time) { + SwingUtilities.invokeLater(new Runnable() { + public void run() { + timeLabel.setText("Time: " + time.getTime()); + scoreLabel.setText("Score: " + time.getScore()); + } + }); + } + + @Override + public void agentAdded(Kernel k, AgentProxy info) { + agents.add(info); + } + + @Override + public void agentRemoved(Kernel k, AgentProxy info) { + agents.remove(info); + } + + @Override + public void simulatorAdded(Kernel k, SimulatorProxy info) { + simulators.add(info); + } + + @Override + public void simulatorRemoved(Kernel k, SimulatorProxy info) { + simulators.remove(info); + } + + @Override + public void viewerAdded(Kernel k, ViewerProxy info) { + viewers.add(info); + } + + @Override + public void viewerRemoved(Kernel k, ViewerProxy info) { + viewers.remove(info); + } +} diff --git a/modules/kernel/src/kernel/ui/ScoreGraph.java b/modules/kernel/src/kernel/ui/ScoreGraph.java new file mode 100644 index 0000000000000000000000000000000000000000..e017c34ad72093ca2c8de7451cf9a3ab9abe0ecc --- /dev/null +++ b/modules/kernel/src/kernel/ui/ScoreGraph.java @@ -0,0 +1,129 @@ +package kernel.ui; + +import rescuecore2.score.ScoreFunction; +import rescuecore2.score.CompositeScoreFunction; +import rescuecore2.score.DelegatingScoreFunction; +import rescuecore2.worldmodel.Entity; +import rescuecore2.worldmodel.WorldModel; +import rescuecore2.config.Config; +import rescuecore2.Timestep; +import rescuecore2.GUIComponent; + +import javax.swing.JComponent; +import javax.swing.JPanel; +import javax.swing.Box; +import javax.swing.JCheckBox; +import javax.swing.event.ChangeListener; +import javax.swing.event.ChangeEvent; + +import java.awt.BorderLayout; + +import java.util.Set; +import java.util.List; +import java.util.ArrayList; + +import org.jfree.chart.ChartPanel; +import org.jfree.chart.JFreeChart; +import org.jfree.chart.ChartFactory; +import org.jfree.chart.plot.PlotOrientation; +import org.jfree.chart.plot.XYPlot; +import org.jfree.chart.renderer.xy.XYItemRenderer; +import org.jfree.data.xy.XYSeriesCollection; +import org.jfree.data.xy.XYSeries; + +/** + A ScoreFunction that also provides a components for graphing the components of the score. + */ +public class ScoreGraph extends DelegatingScoreFunction implements GUIComponent { + private JFreeChart chart; + private List allSeries; + + /** + Construct a ScoreGraph that wraps a child score function. + @param child The child score function. + */ + public ScoreGraph(ScoreFunction child) { + super("Score graph", child); + } + + @Override + public void initialise(WorldModel world, Config config) { + super.initialise(world, config); + allSeries = new ArrayList(); + XYSeriesCollection data = new XYSeriesCollection(); + createSeries(child, data); + PlotOrientation orientation = PlotOrientation.VERTICAL; + chart = ChartFactory.createXYLineChart("Score", "Time", "Score", data, orientation, true, false, false); + } + + @Override + public double score(WorldModel world, Timestep timestep) { + update(world, timestep); + return super.score(world, timestep); + } + + @Override + public JComponent getGUIComponent() { + JComponent selectionPanel = Box.createVerticalBox(); + final XYItemRenderer renderer = ((XYPlot)chart.getPlot()).getRenderer(); + for (SeriesInfo next : allSeries) { + final ScoreFunction f = next.function; + final int index = next.index; + final JCheckBox checkBox = new JCheckBox(f.getName(), true); + selectionPanel.add(checkBox); + checkBox.addChangeListener(new ChangeListener() { + public void stateChanged(ChangeEvent e) { + boolean selected = checkBox.isSelected(); + renderer.setSeriesVisible(index, selected); + } + }); + } + JPanel result = new JPanel(); + result.add(new ChartPanel(chart), BorderLayout.CENTER); + result.add(selectionPanel, BorderLayout.EAST); + return result; + } + + @Override + public String getGUIComponentName() { + return "Score chart"; + } + + private void createSeries(ScoreFunction root, XYSeriesCollection data) { + if (!(root instanceof ScoreTable || root instanceof ScoreGraph)) { + XYSeries next = new XYSeries(root.getName()); + allSeries.add(new SeriesInfo(root, next, allSeries.size())); + data.addSeries(next); + } + if (root instanceof DelegatingScoreFunction) { + createSeries(((DelegatingScoreFunction)root).getChildFunction(), data); + } + if (root instanceof CompositeScoreFunction) { + Set children = ((CompositeScoreFunction)root).getChildFunctions(); + for (ScoreFunction f : children) { + createSeries(f, data); + } + } + } + + private void update(WorldModel world, Timestep timestep) { + for (SeriesInfo next : allSeries) { + ScoreFunction f = next.function; + XYSeries data = next.series; + double d = f.score(world, timestep); + data.add(timestep.getTime(), d); + } + } + + private static class SeriesInfo { + ScoreFunction function; + XYSeries series; + int index; + + public SeriesInfo(ScoreFunction function, XYSeries series, int index) { + this.function = function; + this.series = series; + this.index = index; + } + } +} diff --git a/modules/kernel/src/kernel/ui/ScoreTable.java b/modules/kernel/src/kernel/ui/ScoreTable.java new file mode 100644 index 0000000000000000000000000000000000000000..3fd7de65db7fd8e08f7b972b4cf5fba3691e4391 --- /dev/null +++ b/modules/kernel/src/kernel/ui/ScoreTable.java @@ -0,0 +1,190 @@ +package kernel.ui; + +import rescuecore2.score.ScoreFunction; +import rescuecore2.score.CompositeScoreFunction; +import rescuecore2.score.DelegatingScoreFunction; +import rescuecore2.worldmodel.Entity; +import rescuecore2.worldmodel.WorldModel; +import rescuecore2.config.Config; +import rescuecore2.Timestep; +import rescuecore2.GUIComponent; + +import javax.swing.JComponent; +import javax.swing.JTable; +import javax.swing.JList; +import javax.swing.JLabel; +import javax.swing.JScrollPane; +import javax.swing.AbstractListModel; +import javax.swing.UIManager; +import javax.swing.ListCellRenderer; +import javax.swing.table.AbstractTableModel; +import javax.swing.table.JTableHeader; + +import java.util.Map; +import java.util.HashMap; +import java.util.List; +import java.util.ArrayList; +import java.util.Set; + +/** + A ScoreFunction that also provides a JTable for viewing the components of the score. + */ +public class ScoreTable extends DelegatingScoreFunction implements GUIComponent { + private ScoreModel model; + + /** + Construct a ScoreTable that wraps a child score function. + @param child The child score function. + */ + public ScoreTable(ScoreFunction child) { + super("Score Table", child); + } + + @Override + public void initialise(WorldModel world, Config config) { + super.initialise(world, config); + model = new ScoreModel(child); + } + + @Override + public double score(WorldModel world, Timestep timestep) { + model.update(world, timestep); + return super.score(world, timestep); + } + + @Override + public JComponent getGUIComponent() { + JTable table = new JTable(model.table); + table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); + JScrollPane scroll = new JScrollPane(table); + JList rowHeader = new JList(model.list); + rowHeader.setFixedCellHeight(table.getRowHeight()); + rowHeader.setCellRenderer(new RowHeaderRenderer(table)); + rowHeader.setBackground(table.getBackground()); + rowHeader.setOpaque(true); + scroll.setRowHeaderView(rowHeader); + return scroll; + } + + @Override + public String getGUIComponentName() { + return "Score"; + } + + private static class RowHeaderRenderer extends JLabel implements ListCellRenderer { + RowHeaderRenderer(JTable table) { + JTableHeader header = table.getTableHeader(); + setOpaque(true); + setBorder(UIManager.getBorder("TableHeader.cellBorder")); + setHorizontalAlignment(LEFT); + setForeground(header.getForeground()); + setBackground(header.getBackground()); + setFont(header.getFont()); + } + + @Override + public JLabel getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { + setText((value == null) ? "" : value.toString()); + return this; + } + } + + private static class ScoreModel { + ScoreTableModel table; + ScoreListModel list; + private int steps; + private List entries; + + ScoreModel(ScoreFunction root) { + steps = 0; + entries = new ArrayList(); + populateEntries(root, ""); + table = new ScoreTableModel(); + list = new ScoreListModel(); + } + + private void populateEntries(ScoreFunction root, String prefix) { + String suffix = ""; + if (!(root instanceof ScoreTable || root instanceof ScoreGraph)) { + entries.add(new ScoreFunctionEntry(root, prefix)); + suffix = "--"; + } + if (root instanceof DelegatingScoreFunction) { + populateEntries(((DelegatingScoreFunction)root).getChildFunction(), prefix + suffix); + } + if (root instanceof CompositeScoreFunction) { + Set children = ((CompositeScoreFunction)root).getChildFunctions(); + for (ScoreFunction next : children) { + populateEntries(next, prefix + suffix); + } + } + } + + void update(WorldModel world, Timestep timestep) { + for (ScoreFunctionEntry next : entries) { + next.update(world, timestep); + } + steps = timestep.getTime(); + table.fireTableStructureChanged(); + } + + private class ScoreTableModel extends AbstractTableModel { + @Override + public String getColumnName(int col) { + return String.valueOf(col + 1); + } + + @Override + public int getRowCount() { + return entries.size(); + } + + @Override + public int getColumnCount() { + return steps; + } + + @Override + public Object getValueAt(int row, int column) { + return entries.get(row).getScore(column + 1); + } + } + + private class ScoreListModel extends AbstractListModel { + @Override + public int getSize() { + return entries.size(); + } + + @Override + public Object getElementAt(int row) { + return entries.get(row).getScoreFunctionName(); + } + } + + private static class ScoreFunctionEntry { + private ScoreFunction function; + private String prefix; + private Map scores; + + public ScoreFunctionEntry(ScoreFunction f, String prefix) { + function = f; + this.prefix = prefix; + scores = new HashMap(); + } + + public String getScoreFunctionName() { + return prefix + function.getName(); + } + + public double getScore(int step) { + return scores.containsKey(step) ? scores.get(step) : Double.NaN; + } + + void update(WorldModel world, Timestep timestep) { + double d = function.score(world, timestep); + scores.put(timestep.getTime(), d); + } + } + } +} diff --git a/modules/maps/src/maps/ConstantConversion.java b/modules/maps/src/maps/ConstantConversion.java new file mode 100644 index 0000000000000000000000000000000000000000..5c14909e91060d53809caa64d95aa9508c02797f --- /dev/null +++ b/modules/maps/src/maps/ConstantConversion.java @@ -0,0 +1,26 @@ +package maps; + +/** + A coordinate conversion that multiplies by a number. +*/ +public class ConstantConversion implements CoordinateConversion { + private double constant; + + /** + Construct a ConstantConversion. + @param c The constant. + */ + public ConstantConversion(double c) { + constant = c; + } + + @Override + public double convertX(double x) { + return x * constant; + } + + @Override + public double convertY(double y) { + return y * constant; + } +} diff --git a/modules/maps/src/maps/CoordinateConversion.java b/modules/maps/src/maps/CoordinateConversion.java new file mode 100644 index 0000000000000000000000000000000000000000..4f8ec565cb6af85a8a93578a1d97323e54d583b6 --- /dev/null +++ b/modules/maps/src/maps/CoordinateConversion.java @@ -0,0 +1,20 @@ +package maps; + +/** + Interface for converting coordinates from one system to another. +*/ +public interface CoordinateConversion { + /** + Convert an X coordinate. + @param x The coordinate to convert. + @return The converted coordinate. + */ + double convertX(double x); + + /** + Convert a Y coordinate. + @param y The coordinate to convert. + @return The converted coordinate. + */ + double convertY(double y); +} diff --git a/modules/maps/src/maps/IdentityConversion.java b/modules/maps/src/maps/IdentityConversion.java new file mode 100644 index 0000000000000000000000000000000000000000..3c61a00cfdd2752c15b934f19251b778f7ca007b --- /dev/null +++ b/modules/maps/src/maps/IdentityConversion.java @@ -0,0 +1,16 @@ +package maps; + +/** + A no-op coordinate conversion. +*/ +public class IdentityConversion implements CoordinateConversion { + @Override + public double convertX(double x) { + return x; + } + + @Override + public double convertY(double y) { + return y; + } +} diff --git a/modules/maps/src/maps/Map.java b/modules/maps/src/maps/Map.java new file mode 100644 index 0000000000000000000000000000000000000000..f8457cc8c6c1ad6186adecc830fe2e8c255ae733 --- /dev/null +++ b/modules/maps/src/maps/Map.java @@ -0,0 +1,7 @@ +package maps; + +/** + Top-level interface for all types of map. +*/ +public interface Map { +} \ No newline at end of file diff --git a/modules/maps/src/maps/MapException.java b/modules/maps/src/maps/MapException.java new file mode 100644 index 0000000000000000000000000000000000000000..6ba6312f7fa804cb4c5c250829b599917dc7f23c --- /dev/null +++ b/modules/maps/src/maps/MapException.java @@ -0,0 +1,38 @@ +package maps; + +/** + Exceptions related to maps. +*/ +public class MapException extends Exception { + /** + Construct a MapException with no error message. + */ + public MapException() { + super(); + } + + /** + Construct a MapException with an error message. + @param msg The error message. + */ + public MapException(String msg) { + super(msg); + } + + /** + Construct a MapException with an underlying cause. + @param cause The cause. + */ + public MapException(Throwable cause) { + super(cause); + } + + /** + Construct a MapException with an error message and underlying cause. + @param msg The error message. + @param cause The cause. + */ + public MapException(String msg, Throwable cause) { + super(msg, cause); + } +} diff --git a/modules/maps/src/maps/MapFormat.java b/modules/maps/src/maps/MapFormat.java new file mode 100644 index 0000000000000000000000000000000000000000..ac9d4528cf87f8b9b49058c9d163f746ab39cb01 --- /dev/null +++ b/modules/maps/src/maps/MapFormat.java @@ -0,0 +1,32 @@ +package maps; + +import java.io.File; + +/** + Interface for different types of map format. +*/ +public interface MapFormat { + /** + Read a File and return a Map. + @param file The file to read. + @return A new Map. + @throws MapException If there is a problem reading the map. + */ + Map read(File file) throws MapException; + + /** + Write a map to a file. + @param map The map to write. + @param file The file to write to. + @throws MapException If there is a problem writing the map. + */ + void write(Map map, File file) throws MapException; + + /** + Find out if a file looks valid to this format. + @param file The file to check. + @return True if this format can probably read the file, false otherwise. + @throws MapException If there is a problem reading the file. + */ + boolean canRead(File file) throws MapException; +} diff --git a/modules/maps/src/maps/MapReader.java b/modules/maps/src/maps/MapReader.java new file mode 100644 index 0000000000000000000000000000000000000000..33d4edac200958addc4ddf91bfde04d4f92fba88 --- /dev/null +++ b/modules/maps/src/maps/MapReader.java @@ -0,0 +1,95 @@ +package maps; + +import maps.gml.formats.RobocupFormat; +import maps.gml.formats.OrdnanceSurveyFormat; +import maps.gml.formats.MeijoFormat; +import maps.gml.formats.GeospatialInformationAuthorityFormat; + +import java.io.File; + +import java.util.List; +import java.util.ArrayList; + +import rescuecore2.log.Logger; + +/** + A utility class for reading maps. +*/ +public final class MapReader { + private static final List ALL_FORMATS = new ArrayList(); + + static { + ALL_FORMATS.add(RobocupFormat.INSTANCE); + ALL_FORMATS.add(MeijoFormat.INSTANCE); + ALL_FORMATS.add(OrdnanceSurveyFormat.INSTANCE); + ALL_FORMATS.add(GeospatialInformationAuthorityFormat.INSTANCE); + } + + private MapReader() { + } + + /** + Read a Map from a file and guess the format. + @param file The name of the file to read. + @return A Map. + @throws MapException If there is a problem reading the map. + */ + public static Map readMap(String file) throws MapException { + return readMap(file, null); + } + + /** + Read a Map from a file using a particular format. + @param file The name of the file to read. + @param format The format to use. If this is null then the format will be guessed. + @return A Map. + @throws MapException If there is a problem reading the map. + */ + public static Map readMap(String file, MapFormat format) throws MapException { + return readMap(new File(file), format); + } + + /** + Read a Map from a file and guess the format. + @param file The file to read. + @return A Map. + @throws MapException If there is a problem reading the map. + */ + public static Map readMap(File file) throws MapException { + return readMap(file, null); + } + + /** + Read a Map from a file using a particular format. + @param file The file to read. + @param format The format to use. If this is null then the format will be guessed. + @return A Map. + @throws MapException If there is a problem reading the map. + */ + public static Map readMap(File file, MapFormat format) throws MapException { + if (format == null) { + format = guessFormat(file); + } + if (format == null) { + throw new MapException("Unrecognised format"); + } + Logger.debug("Reading " + format.toString() + " format"); + return format.read(file); + } + + /** + Guess the format for a Map. + @param file The file to guess the format of. + @return The most likely format or null if the file type is unrecognised. + @throws MapException If there is a problem reading the file. + */ + public static MapFormat guessFormat(File file) throws MapException { + Logger.debug("Guessing format"); + for (MapFormat next : ALL_FORMATS) { + if (next.canRead(file)) { + return next; + } + } + return null; + } +} diff --git a/modules/maps/src/maps/MapTools.java b/modules/maps/src/maps/MapTools.java new file mode 100644 index 0000000000000000000000000000000000000000..88618a0724812082baf29480c38c421faa658669 --- /dev/null +++ b/modules/maps/src/maps/MapTools.java @@ -0,0 +1,28 @@ +package maps; + +import javax.measure.unit.NonSI; +import javax.measure.unit.SI; +import org.jscience.geography.coordinates.UTM; +import org.jscience.geography.coordinates.LatLong; +import org.jscience.geography.coordinates.crs.ReferenceEllipsoid; + +/** + Utility class for dealing with maps. +*/ +public final class MapTools { + private MapTools() { + } + + /** + Compute the size of one metre in latitude/longitude relative to a reference point. + @param lat The latitude of the reference point. + @param lon The longitude of the reference point. + @return The size of one metre at the reference point. + */ + public static double sizeOf1Metre(double lat, double lon) { + UTM centre = UTM.latLongToUtm(LatLong.valueOf(lat, lon, NonSI.DEGREE_ANGLE), ReferenceEllipsoid.WGS84); + UTM offset = UTM.valueOf(centre.longitudeZone(), centre.latitudeZone(), centre.eastingValue(SI.METRE), centre.northingValue(SI.METRE) + 1, SI.METRE); + LatLong result = UTM.utmToLatLong(offset, ReferenceEllipsoid.WGS84); + return Math.abs(result.latitudeValue(NonSI.DEGREE_ANGLE) - lat); + } +} diff --git a/modules/maps/src/maps/MapWriter.java b/modules/maps/src/maps/MapWriter.java new file mode 100644 index 0000000000000000000000000000000000000000..75b5956ea1f9acf4a72d0876252b69709971407e --- /dev/null +++ b/modules/maps/src/maps/MapWriter.java @@ -0,0 +1,33 @@ +package maps; + +import java.io.File; + +/** + A class for writing maps. +*/ +public final class MapWriter { + private MapWriter() { + } + + /** + Write a map to a file. + @param map The map to write. + @param file The name of the file to write to. + @param format The MapFormat to write. + @throws MapException If there is a problem writing the map. + */ + public static void writeMap(Map map, String file, MapFormat format) throws MapException { + writeMap(map, new File(file), format); + } + + /** + Write a Map to a file. + @param map The map to write. + @param file The file to write to. + @param format The MapFormat to write. + @throws MapException If there is a problem writing the map. + */ + public static void writeMap(Map map, File file, MapFormat format) throws MapException { + format.write(map, file); + } +} diff --git a/modules/maps/src/maps/ScaleConversion.java b/modules/maps/src/maps/ScaleConversion.java new file mode 100644 index 0000000000000000000000000000000000000000..7b1b2e68745817f7359e0a25914089736498c0db --- /dev/null +++ b/modules/maps/src/maps/ScaleConversion.java @@ -0,0 +1,35 @@ +package maps; + +/** + A coordinate conversion that scales and translates coordinates. +*/ +public class ScaleConversion implements CoordinateConversion { + private double xOrigin; + private double yOrigin; + private double xScale; + private double yScale; + + /** + Construct a ScaleConversion. + @param xOrigin The x coordinate of the new origin. + @param yOrigin The y coordinate of the new origin. + @param xScale The scale factor for x coordinates. + @param yScale The scale factor for y coordinates. + */ + public ScaleConversion(double xOrigin, double yOrigin, double xScale, double yScale) { + this.xOrigin = xOrigin; + this.yOrigin = yOrigin; + this.xScale = xScale; + this.yScale = yScale; + } + + @Override + public double convertX(double x) { + return (x - xOrigin) * xScale; + } + + @Override + public double convertY(double y) { + return (y - yOrigin) * yScale; + } +} diff --git a/modules/maps/src/maps/convert/Convert.java b/modules/maps/src/maps/convert/Convert.java new file mode 100644 index 0000000000000000000000000000000000000000..0840ba8b159919737c42e38e03239f80ca6b8104 --- /dev/null +++ b/modules/maps/src/maps/convert/Convert.java @@ -0,0 +1,90 @@ +package maps.convert; + +import maps.MapWriter; +import maps.osm.OSMMap; +import maps.osm.OSMMapViewer; +import maps.osm.OSMException; +import maps.gml.GMLMap; +import maps.gml.view.GMLMapViewer; + +import maps.convert.osm2gml.Convertor; +import maps.gml.formats.RobocupFormat; + +import org.dom4j.DocumentException; + +import java.awt.GridLayout; +import java.awt.Dimension; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.BorderFactory; +import java.io.File; +import java.io.IOException; + +/** + This class converts maps from one format to another. +*/ +public final class Convert { + // Nodes that are close are deemed to be co-located. + private static final double NEARBY_NODE_THRESHOLD = 0.000001; + + private static final int PROGRESS_WIDTH = 200; + private static final int PROGRESS_HEIGHT = 10; + private static final int VIEWER_SIZE = 500; + private static final int STATUS_WIDTH = 500; + private static final int STATUS_HEIGHT = 10; + private static final int MARGIN = 4; + + // private ShapeDebugFrame debug; + // private List allOSMNodes; + // private List allGMLNodes; + + private Convert() { + } + + /** + Run the map convertor. + @param args Command line arguments: osm-mapname gml-mapname. + */ + public static void main(String[] args) { + if (args.length < 2) { + System.out.println("Usage: Convert "); + return; + } + try { + OSMMap osmMap = readOSMMap(args[0]); + OSMMapViewer osmViewer = new OSMMapViewer(osmMap); + Convertor convert = new Convertor(); + GMLMap gmlMap = convert.convert(osmMap); + MapWriter.writeMap(gmlMap, args[1], RobocupFormat.INSTANCE); + GMLMapViewer gmlViewer = new GMLMapViewer(gmlMap); + JFrame frame = new JFrame("Convertor"); + JPanel main = new JPanel(new GridLayout(1, 2)); + osmViewer.setPreferredSize(new Dimension(VIEWER_SIZE, VIEWER_SIZE)); + gmlViewer.setPreferredSize(new Dimension(VIEWER_SIZE, VIEWER_SIZE)); + osmViewer.setBorder(BorderFactory.createTitledBorder("OSM map")); + gmlViewer.setBorder(BorderFactory.createTitledBorder("GML map")); + main.add(osmViewer); + main.add(gmlViewer); + frame.setContentPane(main); + frame.pack(); + frame.setVisible(true); + frame.addWindowListener(new WindowAdapter() { + public void windowClosing(WindowEvent e) { + System.exit(0); + } + }); + } + // CHECKSTYLE:OFF:IllegalCatch + catch (Exception e) { + e.printStackTrace(); + } + // CHECKSTYLE:ON:IllegalCatch + } + + private static OSMMap readOSMMap(String file) throws OSMException, IOException, DocumentException { + File f = new File(file); + return new OSMMap(f); + } +} diff --git a/modules/maps/src/maps/convert/ConvertStep.java b/modules/maps/src/maps/convert/ConvertStep.java new file mode 100644 index 0000000000000000000000000000000000000000..e59de093ea970d25c7062baefec347b34a83cb08 --- /dev/null +++ b/modules/maps/src/maps/convert/ConvertStep.java @@ -0,0 +1,154 @@ +package maps.convert; + +import javax.swing.JProgressBar; +import javax.swing.JLabel; +import javax.swing.JComponent; +import javax.swing.SwingUtilities; + +import rescuecore2.misc.gui.ShapeDebugFrame; +import rescuecore2.log.Logger; + +/** + A step in the map conversion process. +*/ +public abstract class ConvertStep { + /** A ShapeDebugFrame for use by subclasses. */ + protected ShapeDebugFrame debug; + + private JProgressBar progress; + private JLabel status; + + /** + Construct a ConvertStep. + */ + protected ConvertStep() { + this.progress = new JProgressBar(); + this.status = new JLabel(); + progress.setString(""); + progress.setStringPainted(true); + debug = new ShapeDebugFrame(); + } + + /** + Set the progress level. + @param amount The new progress. + */ + protected void setProgress(final int amount) { + SwingUtilities.invokeLater(new Runnable() { + public void run() { + progress.setValue(amount); + progress.setString(progress.getValue() + " / " + progress.getMaximum()); + } + }); + } + + /** + Increase the progress level by one. + */ + protected void bumpProgress() { + SwingUtilities.invokeLater(new Runnable() { + public void run() { + progress.setValue(progress.getValue() + 1); + progress.setString(progress.getValue() + " / " + progress.getMaximum()); + } + }); + } + + /** + Increase the maximum progress level by one. + */ + protected void bumpMaxProgress() { + bumpMaxProgress(1); + } + + /** + Increase the maximum progress level by some amount. + @param amount The amount to increase the maximum progress level. + */ + protected void bumpMaxProgress(final int amount) { + SwingUtilities.invokeLater(new Runnable() { + public void run() { + progress.setMaximum(progress.getMaximum() + amount); + progress.setString(progress.getValue() + " / " + progress.getMaximum()); + } + }); + } + + /** + Set the progress maximum. + @param max The new progress maximum. + */ + protected void setProgressLimit(final int max) { + SwingUtilities.invokeLater(new Runnable() { + public void run() { + progress.setIndeterminate(false); + progress.setMaximum(max); + progress.setString(progress.getValue() + " / " + progress.getMaximum()); + } + }); + } + + /** + Set the status label. + @param s The new status label. + */ + protected void setStatus(final String s) { + SwingUtilities.invokeLater(new Runnable() { + public void run() { + status.setText(s); + } + }); + } + + /** + Get the JProgressBar component for this step. + @return The progress bar component. + */ + public JProgressBar getProgressBar() { + return progress; + } + + /** + Get the status component for this step. + @return The status component. + */ + public JComponent getStatusComponent() { + return status; + } + + /** + Perform the conversion step. + */ + public final void doStep() { + try { + Logger.pushLogContext(getClass().getName()); + SwingUtilities.invokeLater(new Runnable() { + public void run() { + progress.setIndeterminate(true); + } + }); + step(); + SwingUtilities.invokeLater(new Runnable() { + public void run() { + progress.setIndeterminate(false); + progress.setValue(progress.getMaximum()); + } + }); + debug.deactivate(); + } + finally { + Logger.popLogContext(); + } + } + + /** + Get a user-friendly description of this step. + @return A description string. + */ + public abstract String getDescription(); + + /** + Perform the step. + */ + protected abstract void step(); +} diff --git a/modules/maps/src/maps/convert/legacy2gml/BuildingInfo.java b/modules/maps/src/maps/convert/legacy2gml/BuildingInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..4a28f40abe17a936eaf409e97d92dd5886ed5e70 --- /dev/null +++ b/modules/maps/src/maps/convert/legacy2gml/BuildingInfo.java @@ -0,0 +1,209 @@ +package maps.convert.legacy2gml; + +import maps.legacy.LegacyBuilding; +import maps.gml.GMLMap; +import maps.gml.GMLBuilding; +import maps.gml.GMLRoad; +import maps.gml.GMLNode; +import maps.gml.GMLDirectedEdge; + +import java.util.List; +import java.util.ArrayList; +import java.util.Map; +import java.util.Collection; +import java.util.HashSet; + +import rescuecore2.misc.geometry.Point2D; +import rescuecore2.misc.geometry.Line2D; +import rescuecore2.misc.geometry.GeometryTools2D; +import rescuecore2.misc.geometry.Vector2D; +import rescuecore2.misc.Pair; +import rescuecore2.log.Logger; + +/** + Container for building information during conversion. +*/ +public class BuildingInfo { + /** + * Width of a generated entrance road. + */ + public static final int ENTRANCE_SIZE = 3000; + + private Point2D roadLeft; + private Point2D roadRight; + + private LegacyBuilding building; + private List apexes; + + /** + Construct a BuildingInfo object. + @param b The LegacyBuilding to store info about. + */ + public BuildingInfo(LegacyBuilding b) { + building = b; + } + + /** + Set the left corner at the road end. + @param newRoadLeft The new head-left corner. + */ + public void setRoadLeft(Point2D newRoadLeft) { + roadLeft = newRoadLeft; + } + + /** + Set the right corner at the road end. + @param newRoadRight The new head-right corner. + */ + public void setRoadRight(Point2D newRoadRight) { + roadRight = newRoadRight; + } + + /** + Process the building and create a GMLBuilding. + @param gml The GML map. + @param nodeInfo Information about the legacy nodes. + @param roadInfo Information about the legacy roads. + */ + public void process(GMLMap gml, Map nodeInfo, Map roadInfo) { + apexes = new ArrayList(); + int[] coords = building.getApexes(); + for (int i = 0; i < coords.length; i += 2) { + int x = coords[i]; + int y = coords[i + 1]; + GMLNode node = gml.createNode(x, y); + apexes.add(node); + } + GMLBuilding b = gml.createBuildingFromNodes(apexes); + b.setFloors(building.getFloors()); + b.setCode(building.getCode()); + // Create an entrance + // Logger.debug("Creating entrance for " + building); + if (building.getEntrances().length == 0) { + return; + } + Point2D entrancePoint = nodeInfo.get(building.getEntrances()[0]).getLocation(); + Point2D centre = new Point2D(building.getX(), building.getY()); + Line2D centreLine = new Line2D(centre, entrancePoint); + Vector2D centreVector = centreLine.getDirection(); + Vector2D leftVector = centreVector.getNormal().normalised().scale(ENTRANCE_SIZE / 2.0); + Vector2D rightVector = leftVector.scale(-1); + Line2D left = new Line2D(centre.plus(leftVector), centreVector); + Line2D right = new Line2D(centre.plus(rightVector), centreVector); + // Logger.debug("Entrance point: " + entrancePoint); + // Logger.debug("Building centre: " + centre); + // Logger.debug("Left line: " + left); + // Logger.debug("Right line: " + right); + + Pair leftEntrance = getEntrancePoint(left, centreLine, b.getEdges(), true, true); + if (leftEntrance == null) { + Logger.warn(b + ": Left entrance line does not intersect any walls"); + return; + } + GMLNode leftNode = gml.createNode(leftEntrance.first().getOrigin().getX(), leftEntrance.first().getOrigin().getY()); + // Logger.debug("New left node: " + leftNode); + gml.splitEdge(leftEntrance.second().getEdge(), leftNode); + Pair rightEntrance = getEntrancePoint(right, centreLine, b.getEdges(), false, true); + if (rightEntrance == null) { + Logger.warn(b + ": Right entrance line does not intersect any walls"); + return; + } + GMLNode rightNode = gml.createNode(rightEntrance.first().getOrigin().getX(), rightEntrance.first().getOrigin().getY()); + // Logger.debug("New right node: " + rightNode); + gml.splitEdge(rightEntrance.second().getEdge(), rightNode); + gml.removeEdge(leftEntrance.second().getEdge()); + gml.removeEdge(rightEntrance.second().getEdge()); + // Now create the new road segment +// Pair leftRoad = getEntrancePoint(left, centreLine, getAllRoadEdges(nodeInfo.values(), roadInfo.values()), true, false); +// if (leftRoad == null) { +// Logger.warn(b + ": Left entrance line does not intersect any roads"); +// return; +// } + GMLNode roadLeftNode = gml.createNode(roadLeft.getX(), roadLeft.getY()); + // Logger.debug("New road left node: " + roadLeftNode); +// gml.splitEdge(leftRoad.second().getEdge(), roadLeftNode); +// Pair rightRoad = getEntrancePoint(right, centreLine, getAllRoadEdges(nodeInfo.values(), roadInfo.values()), false, false); +// if (rightRoad == null) { +// Logger.warn(b + ": Right entrance line does not intersect any roads"); +// return; +// } + GMLNode roadRightNode = gml.createNode(roadRight.getX(), roadRight.getY()); + // Logger.debug("New road left node: " + roadRightNode); +// gml.splitEdge(rightRoad.second().getEdge(), roadRightNode); + // Create the road + gml.createRoad(gml.apexesToEdges(roadLeftNode, roadRightNode, rightNode, leftNode)); + } + + /** + Trim a line to a set of walls and return the trimmed line and intersecting wall. Returns null if the line does not intersect any walls. + @param line The line to trim. + @param walls The walls to trim to. + @param trimStart True to trim the start of the line, false to trim the end. + */ + private Pair trim(Line2D line, Collection walls, boolean trimStart) { + GMLDirectedEdge wall = null; + for (GMLDirectedEdge next : walls) { + Point2D p = GeometryTools2D.getSegmentIntersectionPoint(line, Tools.gmlDirectedEdgeToLine(next)); + if (p != null) { + if (trimStart) { + line = new Line2D(p, line.getEndPoint()); + } + else { + line = new Line2D(line.getOrigin(), p); + } + wall = next; + } + } + if (wall == null) { + return null; + } + return new Pair(line, wall); + } + + private Pair getEntrancePoint(Line2D line, Line2D centre, Collection walls, boolean left, boolean trimStart) { + Pair trimmed = trim(line, walls, trimStart); + if (trimmed == null) { + // Entrance line does not intersect with the building outline. Snap it back to the endpoint of the wall the centre line intersects with. + trimmed = trim(centre, walls, trimStart); + if (trimmed == null) { + // Entrance does not intersect with any walls! + return null; + } + GMLDirectedEdge wall = trimmed.second(); + Line2D wallLine = Tools.gmlDirectedEdgeToLine(wall); + Vector2D centreNormal = centre.getDirection().getNormal().normalised(); + if (!left) { + centreNormal = centreNormal.scale(-1); + } + Vector2D wallDirection = wallLine.getDirection().normalised(); + Point2D end; + if (wallDirection.dot(centreNormal) > 0) { + // Same direction: end point is the end of the wall + end = wallLine.getEndPoint(); + } + else { + // Opposite directions: end point is the start of the wall + end = wallLine.getOrigin(); + } + return new Pair(new Line2D(end, line.getEndPoint()), wall); + } + else { + return trimmed; + } + } + + private Collection getAllRoadEdges(Collection nodes, Collection roads) { + Collection all = new HashSet(); + for (NodeInfo next : nodes) { + GMLRoad r = next.getRoad(); + if (r != null) { + all.addAll(r.getEdges()); + } + } + for (RoadInfo next : roads) { + all.addAll(next.getRoad().getEdges()); + } + return all; + } +} + diff --git a/modules/maps/src/maps/convert/legacy2gml/LegacyToGML.java b/modules/maps/src/maps/convert/legacy2gml/LegacyToGML.java new file mode 100644 index 0000000000000000000000000000000000000000..0907b34a8e74565b53675ac2779a8a4262206118 --- /dev/null +++ b/modules/maps/src/maps/convert/legacy2gml/LegacyToGML.java @@ -0,0 +1,109 @@ +package maps.convert.legacy2gml; + +import maps.legacy.LegacyMap; +import maps.legacy.LegacyMapFormat; +import maps.legacy.LegacyRoad; +import maps.legacy.LegacyNode; +import maps.legacy.LegacyBuilding; +import maps.gml.GMLMap; +import maps.gml.formats.RobocupFormat; +import maps.ScaleConversion; + +import java.io.File; + +import java.util.Map; +import java.util.HashMap; + +import rescuecore2.log.Logger; + +/** + This class converts maps from the legacy format to GML. +*/ +public final class LegacyToGML { + private static final double MM_TO_M = 0.001; + + private LegacyToGML() {} + + /** + Run the map convertor. + @param args Command line arguments: legacy-mapdir gml-mapname. + */ + public static void main(String[] args) { + if (args.length < 2) { + System.out.println("Usage: LegacyToGML "); + return; + } + try { + Logger.info("Reading legacy map"); + LegacyMap legacy = LegacyMapFormat.INSTANCE.read(new File(args[0])); + GMLMap gml = new GMLMap(); + Logger.info("Converting"); + convert(legacy, gml); + Logger.info("Writing GML map"); + RobocupFormat.INSTANCE.write(gml, new File(args[1])); + Logger.info("Done"); + } + // CHECKSTYLE:OFF:IllegalCatch + catch (Exception e) { + e.printStackTrace(); + } + // CHECKSTYLE:ON:IllegalCatch + System.exit(0); + } + + private static void convert(LegacyMap legacy, GMLMap gml) { + Map roadInfo = new HashMap(); + Map nodeInfo = new HashMap(); + Map buildingInfo = new HashMap(); + Logger.debug("Reading roads"); + for (LegacyRoad r : legacy.getRoads()) { + roadInfo.put(r.getID(), new RoadInfo()); + } + Logger.debug("Removing duplicate roads"); + for (LegacyNode n : legacy.getNodes()) { + Map roadToFarNode = new HashMap(); + for (int rid : n.getEdges()) { + LegacyRoad road = legacy.getRoad(rid); + if (road == null) { + continue; + } + int farNodeId = (n.getID() == road.getHead()) ? road.getTail() : road.getHead(); + + // Use the widest road + LegacyRoad existingRoad = roadToFarNode.get(farNodeId); + if (existingRoad != null && road.getWidth() <= existingRoad.getWidth()) { + roadInfo.remove(road.getID()); + } + else if (existingRoad != null) { + roadInfo.remove(existingRoad.getID()); + roadToFarNode.put(farNodeId, road); + } + else { + roadToFarNode.put(farNodeId, road); + } + } + } + Logger.debug("Reading nodes"); + for (LegacyNode n : legacy.getNodes()) { + nodeInfo.put(n.getID(), new NodeInfo(n)); + } + Logger.debug("Reading buildings"); + for (LegacyBuilding b : legacy.getBuildings()) { + buildingInfo.put(b.getID(), new BuildingInfo(b)); + } + Logger.debug("Creating intersections"); + for (NodeInfo n : nodeInfo.values()) { + n.process(legacy, gml, roadInfo, buildingInfo); + } + Logger.debug("Creating roads"); + for (RoadInfo r : roadInfo.values()) { + r.process(gml); + } + Logger.debug("Creating buildings"); + for (BuildingInfo b : buildingInfo.values()) { + b.process(gml, nodeInfo, roadInfo); + } + // Rescale to m + gml.convertCoordinates(new ScaleConversion(gml.getMinX(), gml.getMinY(), MM_TO_M, MM_TO_M)); + } +} diff --git a/modules/maps/src/maps/convert/legacy2gml/NodeInfo.java b/modules/maps/src/maps/convert/legacy2gml/NodeInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..6b93d033feceec506672402677b6edae3c4c3c7c --- /dev/null +++ b/modules/maps/src/maps/convert/legacy2gml/NodeInfo.java @@ -0,0 +1,343 @@ +package maps.convert.legacy2gml; + + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import maps.gml.GMLMap; +import maps.gml.GMLNode; +import maps.gml.GMLRoad; +import maps.legacy.LegacyBuilding; +import maps.legacy.LegacyMap; +import maps.legacy.LegacyNode; +import maps.legacy.LegacyObject; +import maps.legacy.LegacyRoad; +import rescuecore2.misc.geometry.GeometryTools2D; +import rescuecore2.misc.geometry.Line2D; +import rescuecore2.misc.geometry.Point2D; +import rescuecore2.misc.geometry.Vector2D; + +/** + Container for node information during conversion. +*/ +public class NodeInfo { + private LegacyNode node; + private Point2D centre; + private List apexes; + private GMLRoad road; + + /** + Construct a NodeInfo object. + @param node The LegacyNode to store info about. + */ + public NodeInfo(LegacyNode node) { + this.node = node; + centre = new Point2D(node.getX(), node.getY()); + } + + /** + Get the LegacyNode. + @return The LegacyNode. + */ + public LegacyNode getNode() { + return node; + } + + /** + Get the node location. + @return The node location. + */ + public Point2D getLocation() { + return centre; + } + + /** + Get the generated GMLRoad. + @return The generated road or null if this node did not generate a road segment. + */ + public GMLRoad getRoad() { + return road; + } + + /** + Process the node and create GMLRoad objects if required. + @param legacy The legacy map. + @param gml The GML map. + @param roadInfo A map from road ID to RoadInfo. + @param buildingInfo A map from building ID to BuildingInfo. + */ + public void process(LegacyMap legacy, GMLMap gml, Map roadInfo, Map buildingInfo) { + apexes = new ArrayList(); + List edges = new ArrayList(); + for (int id : node.getEdges()) { + LegacyRoad lRoad = legacy.getRoad(id); + if (lRoad != null && roadInfo.containsKey(id)) { + edges.add(new RoadAspect(lRoad, node, legacy, roadInfo.get(id))); + continue; + } + LegacyBuilding lBuilding = legacy.getBuilding(id); + if (lBuilding != null && buildingInfo.containsKey(id)) { + edges.add(new BuildingAspect(lBuilding, node, legacy, buildingInfo.get(id))); + } + } + if (edges.size() == 1) { + EdgeAspect aspect = edges.get(0); + findRoadEdges(aspect, centre); + } + else { + // Sort the roads + CounterClockwiseSort sort = new CounterClockwiseSort(centre); + Collections.sort(edges, sort); + // Now build the apex list + Iterator it = edges.iterator(); + EdgeAspect first = it.next(); + EdgeAspect prev = first; + EdgeAspect next; + while (it.hasNext()) { + next = it.next(); + Point2D[] newApexes = findIncomingEdgeIntersection(prev, next, centre); + for (Point2D apex : newApexes) { + apexes.add(gml.createNode(apex.getX(), apex.getY())); + } + prev = next; + } + Point2D[] newApexes = findIncomingEdgeIntersection(prev, first, centre); + for (Point2D apex : newApexes) { + apexes.add(gml.createNode(apex.getX(), apex.getY())); + } + } + if (apexes.size() > 2) { + road = gml.createRoadFromNodes(apexes); + } + } + + /** + Process two incoming roads and find the intersection of the right edge of the first road and the left edge of the second road. + @param first The road endpoint to check the right edge of. + @param second The road endpoint to check the left edge of. + @param centrePoint The centre of the intersection. + @return The intersection of the two roads. + */ + private Point2D[] findIncomingEdgeIntersection(EdgeAspect first, EdgeAspect second, Point2D centrePoint) { + LegacyObject firstNode = first.getFarNode(); + LegacyObject secondNode = second.getFarNode(); + Point2D firstPoint = new Point2D(firstNode.getX(), firstNode.getY()); + Point2D secondPoint = new Point2D(secondNode.getX(), secondNode.getY()); + // Find the intersection of the incoming road edges + Vector2D firstVector = centrePoint.minus(firstPoint); + Vector2D secondVector = centrePoint.minus(secondPoint); + Vector2D firstNormal = firstVector.getNormal().normalised().scale(-first.getRoadWidth() / 2.0); + Vector2D secondNormal = secondVector.getNormal().normalised().scale(second.getRoadWidth() / 2.0); + Point2D start1Point = firstPoint.plus(firstNormal); + Point2D start2Point = secondPoint.plus(secondNormal); + Line2D line1 = new Line2D(start1Point, firstVector); + Line2D line2 = new Line2D(start2Point, secondVector); + Point2D intersection = GeometryTools2D.getIntersectionPoint(line1, line2); + if (intersection == null) { + // Lines are parallel + // This means the normals are parallel, so we can just add a normal to the centre point to generate an intersection point + intersection = centrePoint.plus(firstNormal); + } + + double maxWidth = Math.max(first.getRoadWidth(), second.getRoadWidth()); + double distance = GeometryTools2D.getDistance(centrePoint, intersection); + Vector2D intersectionVector = intersection.minus(centrePoint).normalised(); + double dp1 = firstVector.normalised().dot(intersectionVector); + double dp2 = secondVector.normalised().dot(intersectionVector); + + if (distance > maxWidth && dp1 > 0 && dp2 > 0) { + //Cap spikes on acute angles + Vector2D cutoffVector = intersectionVector.getNormal().scale(maxWidth); + Point2D cutoffStart = centrePoint.plus(intersectionVector.scale(maxWidth)).plus(cutoffVector); + // CHECKSTYLE:OFF:MagicNumber + Line2D cutoffLine = new Line2D(cutoffStart, cutoffVector.scale(-2.0)); + // CHECKSTYLE:ON:MagicNumber + Point2D end1 = GeometryTools2D.getIntersectionPoint(line1, cutoffLine); + Point2D end2 = GeometryTools2D.getIntersectionPoint(line2, cutoffLine); + first.setRightEnd(end1); + second.setLeftEnd(end2); + return new Point2D[] {end1, end2}; + } + else if (distance > maxWidth && (dp1 > 0 || dp2 > 0)) { + // Prevent too distant intersections on obtuse angles + // Those usually happen on intersections between roads with (very) different widths. + // For now, just use the intersection that would have occured between two roads + // of the averages width + double avgWidth = (first.getRoadWidth() + second.getRoadWidth()) / 2.0; + firstNormal = firstVector.getNormal().normalised().scale(-avgWidth / 2.0); + secondNormal = secondVector.getNormal().normalised().scale(avgWidth / 2.0); + start1Point = firstPoint.plus(firstNormal); + start2Point = secondPoint.plus(secondNormal); + line1 = new Line2D(start1Point, firstVector); + line2 = new Line2D(start2Point, secondVector); + intersection = GeometryTools2D.getIntersectionPoint(line1, line2); + + first.setRightEnd(intersection); + second.setLeftEnd(intersection); + } + else if (distance > maxWidth) { + // Inner acute angle: just cut off at twice the max. road width. + intersection = centrePoint.plus(intersectionVector.scale(maxWidth * 2.0)); + first.setRightEnd(intersection); + second.setLeftEnd(intersection); + } + else { + first.setRightEnd(intersection); + second.setLeftEnd(intersection); + } + return new Point2D[] {intersection}; + } + + private void findRoadEdges(EdgeAspect aspect, Point2D centrePoint) { + LegacyObject farNode = aspect.getFarNode(); + Point2D roadPoint = new Point2D(farNode.getX(), farNode.getY()); + Vector2D vector = centrePoint.minus(roadPoint); + Vector2D leftNormal = vector.getNormal().normalised().scale(aspect.getRoadWidth() / 2.0); + Vector2D rightNormal = leftNormal.scale(-1); + Point2D left = centrePoint.plus(leftNormal); + Point2D right = centrePoint.plus(rightNormal); + aspect.setLeftEnd(left); + aspect.setRightEnd(right); + } + + private interface EdgeAspect { + int getRoadWidth(); + LegacyObject getFarNode(); + void setLeftEnd(Point2D p); + void setRightEnd(Point2D p); + } + + private static class RoadAspect implements EdgeAspect { + private boolean forward; + private LegacyNode farNode; + private RoadInfo info; + private int width; + + RoadAspect(LegacyRoad road, LegacyNode intersection, LegacyMap map, RoadInfo info) { + forward = intersection.getID() == road.getTail(); + farNode = map.getNode(forward ? road.getHead() : road.getTail()); + width = road.getWidth(); + this.info = info; + } + + public int getRoadWidth() { + return width; + } + + public LegacyObject getFarNode() { + return farNode; + } + + public void setLeftEnd(Point2D p) { + if (forward) { + info.setHeadLeft(p); + } + else { + info.setTailRight(p); + } + } + + public void setRightEnd(Point2D p) { + if (forward) { + info.setHeadRight(p); + } + else { + info.setTailLeft(p); + } + } + } + + private static class BuildingAspect implements EdgeAspect { + private LegacyBuilding building; + private BuildingInfo info; + + BuildingAspect(LegacyBuilding building, LegacyNode intersection, LegacyMap map, BuildingInfo info) { + this.building = building; + this.info = info; + } + + public int getRoadWidth() { + return BuildingInfo.ENTRANCE_SIZE; + } + + public LegacyObject getFarNode() { + return building; + } + + public void setLeftEnd(Point2D p) { + info.setRoadLeft(p); + } + + public void setRightEnd(Point2D p) { + info.setRoadRight(p); + } + } + + private static class CounterClockwiseSort implements Comparator { + private Point2D centre; + + /** + Construct a CounterClockwiseSort with a reference point. + @param centre The reference point. + */ + public CounterClockwiseSort(Point2D centre) { + this.centre = centre; + } + + @Override + public int compare(EdgeAspect first, EdgeAspect second) { + double d1 = score(first); + double d2 = score(second); + if (d1 < d2) { + return 1; + } + else if (d1 > d2) { + return -1; + } + else { + return 0; + } + } + + /** + Compute the score for a RoadAspect - the amount of clockwiseness from 12 o'clock. + @param aspect The RoadAspect. + @return The amount of clockwiseness. This will be in the range [0..4) with 0 representing 12 o'clock, 1 representing 3 o'clock and so on. + */ + public double score(EdgeAspect aspect) { + LegacyObject node = aspect.getFarNode(); + Point2D point = new Point2D(node.getX(), node.getY()); + Vector2D v = point.minus(centre); + double sin = v.getX() / v.getLength(); + double cos = v.getY() / v.getLength(); + if (Double.isNaN(sin) || Double.isNaN(cos)) { + System.out.println(v); + System.out.println(v.getLength()); + } + return convert(sin, cos); + } + + // CHECKSTYLE:OFF:MagicNumber + private double convert(double sin, double cos) { + if (sin >= 0 && cos >= 0) { + return sin; + } + if (sin >= 0 && cos < 0) { + return 2 - sin; + } + if (sin < 0 && cos < 0) { + return 2 - sin; + } + if (sin < 0 && cos >= 0) { + return 4 + sin; + } + throw new IllegalArgumentException("This should be impossible! What's going on? sin=" + sin + ", cos=" + cos); + } + // CHECKSTYLE:ON:MagicNumber + } +} + diff --git a/modules/maps/src/maps/convert/legacy2gml/RoadInfo.java b/modules/maps/src/maps/convert/legacy2gml/RoadInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..7fb1629c413947ab9b4f94d3265131401d8a6587 --- /dev/null +++ b/modules/maps/src/maps/convert/legacy2gml/RoadInfo.java @@ -0,0 +1,81 @@ +package maps.convert.legacy2gml; + +import rescuecore2.misc.geometry.Point2D; + +import maps.gml.GMLMap; +import maps.gml.GMLNode; +import maps.gml.GMLRoad; + +import java.util.List; +import java.util.ArrayList; + +/** + Container for road information during conversion. +*/ +public class RoadInfo { + private Point2D headLeft; + private Point2D headRight; + private Point2D tailLeft; + private Point2D tailRight; + private GMLRoad road; + + /** + Construct a RoadInfo. + */ + public RoadInfo() { + } + + /** + Set the left corner at the head end. + @param newHeadLeft The new head-left corner. + */ + public void setHeadLeft(Point2D newHeadLeft) { + headLeft = newHeadLeft; + } + + /** + Set the right corner at the head end. + @param newHeadRight The new head-right corner. + */ + public void setHeadRight(Point2D newHeadRight) { + headRight = newHeadRight; + } + + /** + Set the left corner at the tail end. + @param newTailLeft The new tail-left corner. + */ + public void setTailLeft(Point2D newTailLeft) { + tailLeft = newTailLeft; + } + + /** + Set the right corner at the tail end. + @param newTailRight The new tail-right corner. + */ + public void setTailRight(Point2D newTailRight) { + tailRight = newTailRight; + } + + /** + Get the generated GMLRoad. + @return The generated road. + */ + public GMLRoad getRoad() { + return road; + } + + /** + Process this RoadInfo and generate a GMLRoad object. + @param gml The GML map. + */ + public void process(GMLMap gml) { + List apexes = new ArrayList(); + apexes.add(gml.createNode(headLeft.getX(), headLeft.getY())); + apexes.add(gml.createNode(tailLeft.getX(), tailLeft.getY())); + apexes.add(gml.createNode(tailRight.getX(), tailRight.getY())); + apexes.add(gml.createNode(headRight.getX(), headRight.getY())); + road = gml.createRoadFromNodes(apexes); + } +} + diff --git a/modules/maps/src/maps/convert/legacy2gml/Tools.java b/modules/maps/src/maps/convert/legacy2gml/Tools.java new file mode 100644 index 0000000000000000000000000000000000000000..d567a1586c1db1aa58d70b6a1a7642efb7f0bba4 --- /dev/null +++ b/modules/maps/src/maps/convert/legacy2gml/Tools.java @@ -0,0 +1,50 @@ +package maps.convert.legacy2gml; + +import java.util.List; +import java.util.ArrayList; +import java.util.Iterator; + +import rescuecore2.misc.geometry.Point2D; +import rescuecore2.misc.geometry.Line2D; + +import maps.gml.GMLNode; +import maps.gml.GMLDirectedEdge; + +/** + Utilities for the legacy2gml covertor. +*/ +public final class Tools { + private Tools() {} + + /** + Convert a list of GML nodes to Line2D objects. + @param nodes The node list. + @return A new Line2D list. + */ + public static List nodeListToLineList(List nodes) { + List result = new ArrayList(nodes.size()); + Iterator it = nodes.iterator(); + GMLNode first = it.next(); + GMLNode prev = first; + while (it.hasNext()) { + GMLNode next = it.next(); + result.add(new Line2D(new Point2D(prev.getX(), prev.getY()), new Point2D(next.getX(), next.getY()))); + prev = next; + } + result.add(new Line2D(new Point2D(prev.getX(), prev.getY()), new Point2D(first.getX(), first.getY()))); + return result; + } + + /** + Convert a GMLDirectedEdge to a Line2D. + @param edge The edge to convert. + @return A new Line2D. + */ + public static Line2D gmlDirectedEdgeToLine(GMLDirectedEdge edge) { + GMLNode start = edge.getStartNode(); + GMLNode end = edge.getEndNode(); + Point2D origin = new Point2D(start.getX(), start.getY()); + Point2D endPoint = new Point2D(end.getX(), end.getY()); + return new Line2D(origin, endPoint); + } +} \ No newline at end of file diff --git a/modules/maps/src/maps/convert/osm2gml/CleanOSMStep.java b/modules/maps/src/maps/convert/osm2gml/CleanOSMStep.java new file mode 100644 index 0000000000000000000000000000000000000000..e2c9d619ef8492141ac4c8d7f8e9034039928fb3 --- /dev/null +++ b/modules/maps/src/maps/convert/osm2gml/CleanOSMStep.java @@ -0,0 +1,212 @@ +package maps.convert.osm2gml; + +import maps.osm.OSMMap; +import maps.osm.OSMNode; +import maps.osm.OSMRoad; +import maps.osm.OSMBuilding; +import maps.osm.OSMWay; +import maps.convert.ConvertStep; + +import java.util.List; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.Set; +import java.util.HashSet; +import java.util.Collection; +import java.util.Collections; + +import rescuecore2.misc.geometry.Line2D; +//import rescuecore2.log.Logger; + +/** + This step cleans the OpenStreetMap data by removing duplicate nodes and way, fixing degenerate ways, and fixing building edge orderings. +*/ +public class CleanOSMStep extends ConvertStep { + private TemporaryMap map; + + /** + Construct a CleanOSMStep. + @param map The TemporaryMap to clean. + */ + public CleanOSMStep(TemporaryMap map) { + this.map = map; + } + + @Override + public String getDescription() { + return "Cleaning OpenStreetMap data"; + } + + @Override + protected void step() { + OSMMap osm = map.getOSMMap(); + setProgressLimit(osm.getNodes().size() + (osm.getRoads().size() + osm.getBuildings().size()) * 2 + osm.getBuildings().size()); + setStatus("Looking for duplicate nodes"); + int nodes = fixNodes(); + setStatus("Fixing degenerate ways"); + int fixed = fixDegenerateWays(osm.getRoads()); + fixed += fixDegenerateWays(osm.getBuildings()); + setStatus("Looking for duplicate ways"); + int ways = fixDuplicateWays(osm.getRoads()); + ways += fixDuplicateWays(osm.getBuildings()); + setStatus("Fixing building direction"); + int b = fixBuildingDirection(osm.getBuildings()); + setStatus("Removed " + nodes + " duplicate nodes and " + ways + " duplicate ways, fixed " + fixed + " degenerate ways, fixed " + b + " clockwise buildings"); + } + + private int fixNodes() { + OSMMap osm = map.getOSMMap(); + int count = 0; + double threshold = ConvertTools.nearbyThreshold(osm, map.getNearbyThreshold()); + Set removed = new HashSet(); + for (OSMNode next : osm.getNodes()) { + if (removed.contains(next)) { + bumpProgress(); + continue; + } + for (OSMNode test : osm.getNodes()) { + if (next == test) { + continue; + } + if (removed.contains(test)) { + continue; + } + if (nearby(next, test, threshold)) { + // Remove the test node and replace all references to it with 'next' + osm.replaceNode(test, next); + removed.add(test); + // Logger.debug("Removed duplicate node " + test.getID()); + ++count; + } + } + bumpProgress(); + } + return count; + } + + private int fixDegenerateWays(Collection ways) { + int count = 0; + for (OSMWay way : ways) { + // Check that no nodes are listed multiple times in sequence + List ids = new ArrayList(way.getNodeIDs()); + Iterator it = ids.iterator(); + if (!it.hasNext()) { + // Empty way. Remove it. + remove(way); + ++count; + continue; + } + long last = it.next(); + boolean fixed = false; + while (it.hasNext()) { + long next = it.next(); + if (next == last) { + // Duplicate node + it.remove(); + // Logger.debug("Removed node " + next + " from way " + way.getID()); + fixed = true; + } + last = next; + } + if (fixed) { + way.setNodeIDs(ids); + ++count; + } + bumpProgress(); + } + return count; + } + + private int fixDuplicateWays(Collection ways) { + int count = 0; + Set removed = new HashSet(); + for (OSMWay next : ways) { + if (removed.contains(next)) { + bumpProgress(); + continue; + } + // Look at all other roads and see if any are subpaths of this road + for (OSMWay test : ways) { + if (next == test) { + continue; + } + if (removed.contains(test)) { + continue; + } + List testIDs = test.getNodeIDs(); + if (isSubList(testIDs, next.getNodeIDs())) { + remove(test); + removed.add(test); + ++count; + // Logger.debug("Removed way " + test.getID()); + } + else { + Collections.reverse(testIDs); + if (isSubList(testIDs, next.getNodeIDs())) { + remove(test); + removed.add(test); + ++count; + // Logger.debug("Removed way " + test.getID()); + } + } + } + bumpProgress(); + } + return count; + } + + /** + Make sure all buildings have their nodes listed in clockwise order. + */ + private int fixBuildingDirection(Collection buildings) { + OSMMap osm = map.getOSMMap(); + // Sum the angles of all right-hand turns + // If the total is +360 then order is clockwise, -360 means counterclockwise. + int count = 0; + for (OSMBuilding building : buildings) { + // Logger.debug("Building " + building + " angle sum: " + ConvertTools.getAnglesSum(building, osm)); + if (ConvertTools.isClockwise(building, osm)) { + // Reverse the order + List ids = building.getNodeIDs(); + Collections.reverse(ids); + building.setNodeIDs(ids); + ++count; + } + bumpProgress(); + } + return count; + } + + private boolean nearby(OSMNode first, OSMNode second, double threshold) { + double dx = first.getLongitude() - second.getLongitude(); + double dy = first.getLatitude() - second.getLatitude(); + return (dx >= -threshold + && dx <= threshold + && dy >= -threshold + && dy <= threshold); + } + + private boolean isSubList(List first, List second) { + return Collections.indexOfSubList(second, first) != -1; + } + + private void remove(OSMWay way) { + OSMMap osm = map.getOSMMap(); + if (way instanceof OSMRoad) { + osm.removeRoad((OSMRoad)way); + } + else if (way instanceof OSMBuilding) { + osm.removeBuilding((OSMBuilding)way); + } + else { + throw new IllegalArgumentException("Don't know how to handle this type of OSMWay: " + way.getClass().getName()); + } + } + + private Line2D makeLine(long first, long second) { + OSMMap osm = map.getOSMMap(); + OSMNode n1 = osm.getNode(first); + OSMNode n2 = osm.getNode(second); + return new Line2D(n1.getLongitude(), n1.getLatitude(), n2.getLongitude() - n1.getLongitude(), n2.getLatitude() - n1.getLatitude()); + } +} diff --git a/modules/maps/src/maps/convert/osm2gml/ComputePassableEdgesStep.java b/modules/maps/src/maps/convert/osm2gml/ComputePassableEdgesStep.java new file mode 100644 index 0000000000000000000000000000000000000000..4c66c605cc7fc0480db0faa35777300aa0bb0473 --- /dev/null +++ b/modules/maps/src/maps/convert/osm2gml/ComputePassableEdgesStep.java @@ -0,0 +1,56 @@ +package maps.convert.osm2gml; + +import maps.convert.ConvertStep; + +import java.util.Collection; + +/** + This step computes which edges are passable and sets up neighbours accordingly. +*/ +public class ComputePassableEdgesStep extends ConvertStep { + private TemporaryMap map; + + /** + Construct a ComputePassableEdgesStep. + @param map The TemporaryMap to use. + */ + public ComputePassableEdgesStep(TemporaryMap map) { + this.map = map; + } + + @Override + public String getDescription() { + return "Computing passable edges"; + } + + @Override + protected void step() { + setProgressLimit(map.getAllEdges().size()); + // For each edge see if it is shared by two road faces + // If so, make it passable. + int count = 0; + for (Edge next : map.getAllEdges()) { + int roadCount = 0; + Collection attached = map.getAttachedObjects(next); + for (TemporaryObject o : attached) { + if (o instanceof TemporaryRoad || o instanceof TemporaryIntersection) { + ++roadCount; + } + } + if (roadCount > 1) { + // Edge is passable. Make the neighbours. + for (TemporaryObject o1 : attached) { + for (TemporaryObject o2 : attached) { + if (o1 == o2) { + continue; + } + o1.setNeighbour(next, o2); + } + } + ++count; + } + bumpProgress(); + } + setStatus("Made " + count + " edges passable"); + } +} diff --git a/modules/maps/src/maps/convert/osm2gml/Constants.java b/modules/maps/src/maps/convert/osm2gml/Constants.java new file mode 100644 index 0000000000000000000000000000000000000000..1efcce73d58b0ed811adfcbd058815d89a1b06cc --- /dev/null +++ b/modules/maps/src/maps/convert/osm2gml/Constants.java @@ -0,0 +1,88 @@ +package maps.convert.osm2gml; + +import java.awt.Color; + +/** Useful OSM to GML constants. */ +public final class Constants { + /** The width of roads in m. */ + public static final float ROAD_WIDTH = 7; + + // CHECKSTYLE:OFF:JavadocVariable + public static final Color BLACK = new Color(0, 0, 0); + public static final Color WHITE = new Color(255, 255, 255); + public static final Color RED = new Color(255, 0, 0); + public static final Color MAROON = new Color(128, 0, 0); + public static final Color LIME = new Color(0, 255, 0); + public static final Color GREEN = new Color(0, 128, 0); + public static final Color BLUE = new Color(0, 0, 255); + public static final Color NAVY = new Color(0, 0, 128); + public static final Color FUSCHIA = new Color(255, 0, 255); + public static final Color GRAY = new Color(128, 128, 128); + public static final Color OLIVE = new Color(128, 128, 0); + public static final Color PURPLE = new Color(128, 0, 128); + public static final Color SILVER = new Color(192, 192, 192); + public static final Color TEAL = new Color(0, 128, 128); + public static final Color YELLOW = new Color(255, 255, 0); + public static final Color AQUA = new Color(0, 255, 255); + public static final Color ORANGE = new Color(255, 140, 0); + + public static final Color TRANSPARENT_BLACK = new Color(0, 0, 0, 128); + public static final Color TRANSPARENT_WHITE = new Color(255, 255, 255, 128); + public static final Color TRANSPARENT_RED = new Color(255, 0, 0, 128); + public static final Color TRANSPARENT_MAROON = new Color(128, 0, 0, 128); + public static final Color TRANSPARENT_LIME = new Color(0, 255, 0, 128); + public static final Color TRANSPARENT_GREEN = new Color(0, 128, 0, 128); + public static final Color TRANSPARENT_BLUE = new Color(0, 0, 255, 128); + public static final Color TRANSPARENT_NAVY = new Color(0, 0, 128, 128); + public static final Color TRANSPARENT_FUSCHIA = new Color(255, 0, 255, 128); + public static final Color TRANSPARENT_GRAY = new Color(128, 128, 128, 128); + public static final Color TRANSPARENT_OLIVE = new Color(128, 128, 0, 128); + public static final Color TRANSPARENT_PURPLE = new Color(128, 0, 128, 128); + public static final Color TRANSPARENT_SILVER = new Color(192, 192, 192, 128); + public static final Color TRANSPARENT_TEAL = new Color(0, 128, 128, 128); + public static final Color TRANSPARENT_YELLOW = new Color(255, 255, 0, 128); + public static final Color TRANSPARENT_AQUA = new Color(0, 255, 255, 128); + public static final Color TRANSPARENT_ORANGE = new Color(255, 140, 0, 128); + + public static final Color[] COLOURS = {RED, + GREEN, + BLUE, + MAROON, + LIME, + NAVY, + OLIVE, + PURPLE, + TEAL, + GRAY, + SILVER, + FUSCHIA, + YELLOW, + AQUA, + ORANGE, + BLACK, + WHITE + }; + + public static final Color[] TRANSPARENT_COLOURS = {TRANSPARENT_RED, + TRANSPARENT_GREEN, + TRANSPARENT_BLUE, + TRANSPARENT_MAROON, + TRANSPARENT_LIME, + TRANSPARENT_NAVY, + TRANSPARENT_OLIVE, + TRANSPARENT_PURPLE, + TRANSPARENT_TEAL, + TRANSPARENT_GRAY, + TRANSPARENT_SILVER, + TRANSPARENT_FUSCHIA, + TRANSPARENT_YELLOW, + TRANSPARENT_AQUA, + TRANSPARENT_ORANGE, + TRANSPARENT_BLACK, + TRANSPARENT_WHITE + }; + // CHECKSTYLE:ON:JavadocVariable + + private Constants() { + } +} diff --git a/modules/maps/src/maps/convert/osm2gml/ConvertTools.java b/modules/maps/src/maps/convert/osm2gml/ConvertTools.java new file mode 100644 index 0000000000000000000000000000000000000000..665cee2d77363396b28c35ae2ff0d48bed61ee2d --- /dev/null +++ b/modules/maps/src/maps/convert/osm2gml/ConvertTools.java @@ -0,0 +1,459 @@ +package maps.convert.osm2gml; + +import rescuecore2.misc.geometry.Point2D; +import rescuecore2.misc.geometry.Line2D; +import rescuecore2.misc.geometry.Vector2D; +import rescuecore2.misc.geometry.GeometryTools2D; +import rescuecore2.misc.gui.ShapeDebugFrame; +//import rescuecore2.log.Logger; + +import maps.gml.GMLMap; +import maps.gml.GMLNode; +import maps.gml.debug.GMLNodeShapeInfo; +import maps.gml.GMLEdge; +import maps.gml.debug.GMLEdgeShapeInfo; +//import maps.gml.GMLFace; +//import maps.gml.debug.GMLFaceShapeInfo; +import maps.gml.GMLDirectedEdge; +import maps.gml.GMLObject; +import maps.gml.GMLRoad; +import maps.gml.GMLBuilding; +import maps.gml.GMLSpace; +import maps.gml.GMLShape; +import maps.gml.debug.GMLShapeInfo; +import maps.osm.OSMMap; +import maps.osm.OSMNode; +import maps.osm.OSMBuilding; +import maps.MapTools; + +import java.util.Set; +import java.util.List; +import java.util.Collection; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; + +import java.awt.Color; + +/** + Useful tools for converting OSM to GML. + */ +public final class ConvertTools { + private static final Color BACKGROUND_BUILDING_COLOUR = new Color(0, 255, 0, 32); // Transparent lime + private static final Color BACKGROUND_INTERSECTION_COLOUR = new Color(192, 192, 192, 32); // Transparent silver + private static final Color BACKGROUND_ROAD_COLOUR = new Color(128, 128, 128, 32); // Transparent gray + private static final Color BACKGROUND_SPACE_COLOUR = new Color(0, 128, 0, 32); // Transparent green + + private static final double CLOCKWISE_SUM = -360; + private static final double THRESHOLD = 0.0001; + + private ConvertTools() {} + + /** + Compute the size of one metre in latitude/longitude for an OSMMap. + @param map The map to look up. + @return The size of one metre on the given map. + */ + public static double sizeOf1Metre(OSMMap map) { + return MapTools.sizeOf1Metre(map.getCentreLatitude(), map.getCentreLongitude()); + } + + /** + Compute the nearby-node threshold for an OSMMap in degrees. + @param map The map to look up. + @param thresholdM The desired threshold in meters. + @return The size of the nearby-node threshold for the map in degrees. + */ + public static double nearbyThreshold(OSMMap map, double thresholdM) { + return sizeOf1Metre(map) * thresholdM; + } + + /** + Convert a GMLEdge to a Line2D. + @param edge The edge to convert. + @return A new Line2D. + */ + public static Line2D gmlEdgeToLine(GMLEdge edge) { + GMLNode start = edge.getStart(); + GMLNode end = edge.getEnd(); + Point2D origin = new Point2D(start.getX(), start.getY()); + Point2D endPoint = new Point2D(end.getX(), end.getY()); + return new Line2D(origin, endPoint); + } + + /** + Convert a GMLEdge to a Line2D. + @param edge The edge to convert. + @param start The node to start from. This must be one of the endpoints of the edge. + @return A new Line2D. + */ + public static Line2D gmlEdgeToLine(GMLEdge edge, GMLNode start) { + if (!start.equals(edge.getStart()) && !start.equals(edge.getEnd())) { + throw new IllegalArgumentException("'start' must be one of the endpoints of 'edge'"); + } + GMLNode end = start.equals(edge.getStart()) ? edge.getEnd() : edge.getStart(); + Point2D origin = new Point2D(start.getX(), start.getY()); + Point2D endPoint = new Point2D(end.getX(), end.getY()); + return new Line2D(origin, endPoint); + } + + /** + Convert a GMLDirectedEdge to a Line2D. + @param edge The edge to convert. + @return A new Line2D. + */ + public static Line2D gmlDirectedEdgeToLine(GMLDirectedEdge edge) { + GMLNode start = edge.getStartNode(); + GMLNode end = edge.getEndNode(); + Point2D origin = new Point2D(start.getX(), start.getY()); + Point2D endPoint = new Point2D(end.getX(), end.getY()); + return new Line2D(origin, endPoint); + } + + /** + Find the "leftmost" turn in a particular direction, i.e. the one with the highest left angle, or the lowest right angle if there are no left turns possible. + @param from The edge we're turning from. + @param candidates The set of edges we could turn into. + @return The leftmost turn. + */ + public static Edge findLeftTurn(DirectedEdge from, Set candidates) { + return findBestTurn(from, candidates, true); + } + + /** + Find the "rightmost" turn in a particular direction, i.e. the one with the highest right angle, or the lowest left angle if there are no right turns possible. + @param from The edge we're turning from. + @param candidates The set of edges we could turn into. + @return The rightmost turn. + */ + public static Edge findRightTurn(DirectedEdge from, Set candidates) { + return findBestTurn(from, candidates, false); + } + + /** + Find the "best" turn in a particular direction. If left turns are preferred then the "best" turn is the one with the highest left angle, or the lowest right angle. For right turns, the "best" is the one with the highest right angle or the lowest left angle. + @param from The edge we're turning from. + @param candidates The set of edges we could turn into. + @param preferLeft Whether to prefer left turns or not. + @return The best turn. + */ + public static Edge findBestTurn(DirectedEdge from, Set candidates, boolean preferLeft) { + Edge mostRight = null; + Edge mostLeft = null; + Edge leastRight = null; + Edge leastLeft = null; + double mostRightAngle = 0; + double mostLeftAngle = 0; + double leastRightAngle = 0; + double leastLeftAngle = 0; + Line2D fromLine = from.getLine(); + for (Edge next : candidates) { + if (next.equals(from.getEdge())) { + continue; + } + Line2D nextLine = next.getLine(); + if (!next.getStart().equals(from.getEndNode())) { + nextLine = new Line2D(nextLine.getEndPoint(), nextLine.getOrigin()); + } + if (GeometryTools2D.isRightTurn(fromLine, nextLine)) { + double angle = GeometryTools2D.getRightAngleBetweenLines(fromLine, nextLine); + if (mostRight == null || angle > mostRightAngle) { + mostRight = next; + mostRightAngle = angle; + } + if (leastRight == null || angle < leastRightAngle) { + leastRight = next; + leastRightAngle = angle; + } + } + else { + double angle = GeometryTools2D.getLeftAngleBetweenLines(fromLine, nextLine); + if (mostLeft == null || angle > mostLeftAngle) { + mostLeft = next; + mostLeftAngle = angle; + } + if (leastLeft == null || angle < leastLeftAngle) { + leastLeft = next; + leastLeftAngle = angle; + } + } + } + if (preferLeft) { + if (mostLeft != null) { + return mostLeft; + } + return leastRight; + } + else { + if (mostRight != null) { + return mostRight; + } + return leastLeft; + } + } + + /** + Create ShapeInfo objects for all GMLShapes in a map. + @param map The map to debug. + @return A list of ShapeInfo objects. + */ + public static List getAllDebugShapes(GMLMap map) { + return createGMLShapeDebug(map.getAllShapes()); + } + + /** + Create ShapeInfo objects for all TemporaryObjects in a map. + @param map The map to debug. + @return A list of ShapeInfo objects. + */ + public static List getAllDebugShapes(TemporaryMap map) { + return createTemporaryObjectDebug(map.getAllObjects()); + } + + /** + Create ShapeInfo objects for a set of GMLShapes. + @param objects The objects to debug. + @return A list of ShapeInfo objects. + */ + public static List createGMLShapeDebug(GMLShape... objects) { + return createGMLShapeDebug(Arrays.asList(objects)); + } + + /** + Create ShapeInfo objects for a set of GMLShapes. + @param objects The objects to debug. + @return A list of ShapeInfo objects. + */ + public static List createGMLShapeDebug(Collection objects) { + List allShapes = new ArrayList(); + for (GMLShape next : objects) { + Color c = Constants.TRANSPARENT_RED; + String name = "Unknown"; + if (next instanceof GMLRoad) { + c = BACKGROUND_ROAD_COLOUR; + name = "Roads"; + } + if (next instanceof GMLBuilding) { + c = BACKGROUND_BUILDING_COLOUR; + name = "Buildings"; + } + if (next instanceof GMLSpace) { + c = BACKGROUND_SPACE_COLOUR; + name = "Spaces"; + } + allShapes.add(new GMLShapeInfo(next, name, Color.BLACK, c)); + } + return allShapes; + } + + /** + Create ShapeInfo objects for a set of GMLObjects. + @param objects The objects to debug. + @return A list of ShapeInfo objects. + */ + public static List createGMLObjectDebug(GMLObject... objects) { + return createGMLObjectDebug(Arrays.asList(objects)); + } + + /** + Create ShapeInfo objects for a set of GMLObjects. + @param objects The objects to debug. + @return A list of ShapeInfo objects. + */ + public static List createGMLObjectDebug(Collection objects) { + List allShapes = new ArrayList(); + for (GMLObject object : objects) { + if (object instanceof GMLNode) { + allShapes.add(new GMLNodeShapeInfo((GMLNode)object, "Nodes", Constants.BLACK, true)); + } + if (object instanceof GMLEdge) { + allShapes.add(new GMLEdgeShapeInfo((GMLEdge)object, "Edges", Constants.BLACK, false)); + } + /* + if (object instanceof GMLFace) { + GMLFace face = (GMLFace)object; + Color c = Constants.TRANSPARENT_RED; + String name = "Unknown"; + allShapes.add(new GMLFaceShapeInfo(face, "Faces", Constants.BLACK, Constants.TRANSPARENT_AQUA, false)); + } + */ + } + return allShapes; + } + + /** + Create ShapeInfo objects for a set of TemporaryObjects. + @param objects The objects to debug. + @return A list of ShapeInfo objects. + */ + public static List createTemporaryObjectDebug(TemporaryObject... objects) { + return createTemporaryObjectDebug(Arrays.asList(objects)); + } + + /** + Create ShapeInfo objects for a set of TemporaryObjects. + @param objects The objects to debug. + @return A list of ShapeInfo objects. + */ + public static List createTemporaryObjectDebug(Collection objects) { + List allShapes = new ArrayList(); + for (TemporaryObject next : objects) { + Color c = Constants.TRANSPARENT_RED; + String name = "Unknown"; + if (next instanceof TemporaryRoad) { + c = BACKGROUND_ROAD_COLOUR; + name = "Roads"; + } + if (next instanceof TemporaryBuilding) { + c = BACKGROUND_BUILDING_COLOUR; + name = "Buildings"; + } + if (next instanceof TemporaryIntersection) { + c = BACKGROUND_INTERSECTION_COLOUR; + name = "Intersections"; + } + allShapes.add(new TemporaryObjectInfo(next, name, Color.BLACK, c)); + } + return allShapes; + } + + /** + Is a number approximately equal to another number? + @param n The number to test. + @param expected The expected value. + @param threshold The threshold. + @return If n is within [expected - threshold, expected + threshold]. + */ + public static boolean nearlyEqual(double n, double expected, double threshold) { + return (n >= expected - threshold + && n <= expected + threshold); + } + + /** + Find out if a set of GMLDirectedEdges is convex. + @param edges The set of edges to test. + @return True iff the face is convex. + */ + public static boolean isConvex(List edges) { + Iterator it = edges.iterator(); + Line2D first = it.next().getLine(); + Line2D a = first; + Line2D b = it.next().getLine(); + boolean rightTurn = GeometryTools2D.isRightTurn(a, b); + while (it.hasNext()) { + a = b; + b = it.next().getLine(); + if (rightTurn != GeometryTools2D.isRightTurn(a, b)) { + return false; + } + } + if (rightTurn != GeometryTools2D.isRightTurn(b, first)) { + return false; + } + return true; + } + + /** + Sum the angles of all turns in a GMLFace. + @param face The face to check. + @return The sum of angles in the face. + */ + /* + public static double getAnglesSum(GMLFace face) { + double sum = 0; + Iterator it = face.getEdges().iterator(); + GMLDirectedEdge first = it.next(); + GMLDirectedEdge a = first; + while (it.hasNext()) { + GMLDirectedEdge b = it.next(); + double d = getAngle(a, b); + if (!Double.isNaN(d)) { + sum += d; + } + a = b; + } + double d = getAngle(a, first); + if (!Double.isNaN(d)) { + sum += d; + } + return sum; + } + */ + + /** + Sum the angles of all turns in an OSMBuilding. + @param building The building to check. + @param map The OSMMap the building is part of. + @return The sum of angles in the building. + */ + public static double getAnglesSum(OSMBuilding building, OSMMap map) { + double sum = 0; + Iterator it = building.getNodeIDs().iterator(); + long first = it.next(); + long second = it.next(); + long a = first; + long b = second; + while (it.hasNext()) { + long c = it.next(); + double d = getAngle(a, b, c, map); + // Logger.debug("Angle from " + a + ":" + b + ":" + c + " = " + d); + if (!Double.isNaN(d)) { + sum += d; + } + a = b; + b = c; + } + double d = getAngle(a, first, second, map); + // Logger.debug("Angle from " + a + ":" + first + ":" + second + " = " + d); + if (!Double.isNaN(d)) { + sum += d; + } + return sum; + } + + /** + Find out if a GMLFace is defined clockwise or not. + @param face The GMLFace to check. + @return True if the face is defined clockwise, false if anti-clockwise. + */ + /* + public static boolean isClockwise(GMLFace face) { + return nearlyEqual(getAnglesSum(face), CLOCKWISE_SUM, THRESHOLD); + } + */ + + /** + Find out if an OSMBuilding is defined clockwise or not. + @param building The OSMBuilding to check. + @param map The OSM map. + @return True if the building is defined clockwise, false if anti-clockwise. + */ + public static boolean isClockwise(OSMBuilding building, OSMMap map) { + return nearlyEqual(getAnglesSum(building, map), CLOCKWISE_SUM, THRESHOLD); + } + + /* + private static double getAngle(GMLDirectedEdge a, GMLDirectedEdge b) { + Vector2D v1 = new Vector2D(a.getEndNode().getX() - a.getStartNode().getX(), a.getEndNode().getY() - a.getStartNode().getY()); + Vector2D v2 = new Vector2D(b.getEndNode().getX() - b.getStartNode().getX(), b.getEndNode().getY() - b.getStartNode().getY()); + double d = GeometryTools2D.getAngleBetweenVectors(v1, v2); + if (GeometryTools2D.isRightTurn(v1, v2)) { + return -d; + } + return d; + } + */ + + private static double getAngle(long first, long second, long third, OSMMap map) { + OSMNode n1 = map.getNode(first); + OSMNode n2 = map.getNode(second); + OSMNode n3 = map.getNode(third); + Vector2D v1 = new Vector2D(n2.getLongitude() - n1.getLongitude(), n2.getLatitude() - n1.getLatitude()); + Vector2D v2 = new Vector2D(n3.getLongitude() - n2.getLongitude(), n3.getLatitude() - n2.getLatitude()); + double d = GeometryTools2D.getAngleBetweenVectors(v1, v2); + if (GeometryTools2D.isRightTurn(v1, v2)) { + return -d; + } + return d; + } +} diff --git a/modules/maps/src/maps/convert/osm2gml/Convertor.java b/modules/maps/src/maps/convert/osm2gml/Convertor.java new file mode 100644 index 0000000000000000000000000000000000000000..6beeb1b5263d84634da5608c40c0c7e942bb1a9d --- /dev/null +++ b/modules/maps/src/maps/convert/osm2gml/Convertor.java @@ -0,0 +1,115 @@ +package maps.convert.osm2gml; + +import maps.osm.OSMMap; +import maps.gml.GMLMap; +import maps.convert.ConvertStep; + +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.JLabel; +import javax.swing.JComponent; +import javax.swing.JProgressBar; +import javax.swing.Box; +import java.awt.BorderLayout; +import java.awt.GridBagLayout; +import java.awt.GridBagConstraints; +import java.awt.Dimension; +import java.awt.Insets; + +import java.util.List; +import java.util.ArrayList; + +/** + This class converts OSMMaps to GMLMaps. +*/ +public class Convertor { + private static final int PROGRESS_WIDTH = 200; + private static final int PROGRESS_HEIGHT = 10; + private static final int STATUS_WIDTH = 500; + private static final int STATUS_HEIGHT = 10; + private static final int MARGIN = 4; + + /** + Convert an OSMMap to a GMLMap. + @param map The OSMMap to convert. + @return A new GMLMap. + */ + public GMLMap convert(OSMMap map) { + GMLMap gmlMap = new GMLMap(); + + JFrame frame = new JFrame("OSM to GML converter"); + JPanel main = new JPanel(new BorderLayout()); + JComponent top = Box.createVerticalBox(); + top.add(new JLabel("Converting OSM map with " + map.getRoads().size() + " roads and " + map.getBuildings().size() + " buildings")); + top.add(new JLabel("Map size: " + (map.getMaxLongitude() - map.getMinLongitude()) + " x " + (map.getMaxLatitude() - map.getMinLatitude()))); + GridBagLayout layout = new GridBagLayout(); + GridBagConstraints c = new GridBagConstraints(); + c.gridx = 0; + c.gridy = 0; + c.gridwidth = 1; + c.gridheight = 1; + c.weightx = 1; + c.weighty = 1; + c.fill = GridBagConstraints.BOTH; + c.anchor = GridBagConstraints.CENTER; + c.insets = new Insets(MARGIN, MARGIN, MARGIN, MARGIN); + JPanel progress = new JPanel(layout); + + // Random random = new Random(); + + TemporaryMap temp = new TemporaryMap(map); + + List steps = new ArrayList(); + addStep(new CleanOSMStep(temp), steps, progress, layout, c); + addStep(new ScanOSMStep(temp), steps, progress, layout, c); + addStep(new MakeTempObjectsStep(temp), steps, progress, layout, c); + addStep(new SplitIntersectingEdgesStep(temp), steps, progress, layout, c); + addStep(new SplitShapesStep(temp), steps, progress, layout, c); + addStep(new RemoveShapesStep(temp), steps, progress, layout, c); + addStep(new MergeShapesStep(temp), steps, progress, layout, c); + addStep(new ComputePassableEdgesStep(temp), steps, progress, layout, c); + /* + addStep(new CreateBuildingsStep(temp, ConvertTools.sizeOf1Metre(osmMap), random), steps, progress, layout, c); + addStep(new CreateEntrancesStep(temp), steps, progress, layout, c); + addStep(new PruneStep(temp), steps, progress, layout, c); + */ + addStep(new MakeObjectsStep(temp, gmlMap), steps, progress, layout, c); + + main.add(top); + main.add(progress); + + frame.setContentPane(main); + frame.pack(); + frame.setVisible(true); + + for (ConvertStep next : steps) { + next.doStep(); + } + + return gmlMap; + } + + private void addStep(ConvertStep step, List steps, JComponent panel, GridBagLayout layout, GridBagConstraints c) { + JLabel title = new JLabel(step.getDescription()); + JProgressBar progress = step.getProgressBar(); + JComponent status = step.getStatusComponent(); + + c.gridx = 0; + c.weightx = 1; + layout.setConstraints(title, c); + panel.add(title); + c.gridx = 1; + c.weightx = 0; + layout.setConstraints(progress, c); + panel.add(progress); + c.gridx = 2; + c.weightx = 1; + layout.setConstraints(status, c); + panel.add(status); + ++c.gridy; + progress.setPreferredSize(new Dimension(PROGRESS_WIDTH, PROGRESS_HEIGHT)); + status.setPreferredSize(new Dimension(STATUS_WIDTH, STATUS_HEIGHT)); + + steps.add(step); + } +} diff --git a/modules/maps/src/maps/convert/osm2gml/CreateBuildingsStep.java b/modules/maps/src/maps/convert/osm2gml/CreateBuildingsStep.java new file mode 100644 index 0000000000000000000000000000000000000000..39d1dd45dff929938bc388f6a71aa0f6c37890fe --- /dev/null +++ b/modules/maps/src/maps/convert/osm2gml/CreateBuildingsStep.java @@ -0,0 +1,189 @@ +package maps.convert.osm2gml; + +import maps.gml.GMLMap; + +import maps.convert.ConvertStep; + +import java.util.Random; + +/** + This class populates a GMLMap with random buildings. +*/ +public class CreateBuildingsStep extends ConvertStep { + private static final double SIMILAR_LENGTH_THRESHOLD = 0.1; + private static final double NEARLY_PARALLEL_THRESHOLD = 0.0001; + + // private GMLMap gmlMap; + // private double sizeOf1m; + // private Random random; + + /** + Construct a CreateBuildingsStep. + @param gmlMap The GMLMap to use. + @param sizeOf1m The size of 1m in GMLMap units. + @param random The random number generator to use. + */ + public CreateBuildingsStep(GMLMap gmlMap, double sizeOf1m, Random random) { + // this.gmlMap = gmlMap; + // this.sizeOf1m = sizeOf1m; + // this.random = random; + } + + @Override + public String getDescription() { + return "Creating buildings"; + } + + @Override + protected void step() { + /* + debug.setBackground(ConvertTools.getAllGMLShapes(gmlMap)); + // Find open spaces with no buildings + setProgressLimit(gmlMap.getEdges().size()); + Set seenLeft = new HashSet(); + Set seenRight = new HashSet(); + for (GMLEdge edge : gmlMap.getEdges()) { + if (seenLeft.contains(edge) && seenRight.contains(edge)) { + bumpProgress(); + continue; + } + // Try walking from this edge looking for space on the left + if (!seenLeft.contains(edge)) { + List edges = walk(edge, true); + if (edges != null) { + processOpenSpace(edges); + for (GMLDirectedEdge dEdge : edges) { + seenLeft.add(dEdge.getEdge()); + } + } + } + if (!seenRight.contains(edge)) { + List edges = walk(edge, false); + if (edges != null) { + processOpenSpace(reverseEdgeList(edges)); + for (GMLDirectedEdge dEdge : edges) { + seenRight.add(dEdge.getEdge()); + } + } + } + bumpProgress(); + } + */ + } + + /* + private List walk(GMLEdge edge, boolean left) { + if (hasConnectedFace(edge, left)) { + return null; + } + GMLNode start = edge.getStart(); + GMLNode current = edge.getEnd(); + GMLDirectedEdge dEdge = new GMLDirectedEdge(edge, start); + List result = new ArrayList(); + result.add(dEdge); + GMLEdge last = edge; + while (current != start) { + GMLEdge next = ConvertTools.findBestTurn(dEdge, gmlMap.getAttachedEdges(current), left); + boolean forward = current == next.getStart(); + boolean lookOnLeft = (left && forward) || (!left && !forward); +// debug.show("Walking outside " + (left ? "left" : "right"), +// new GMLEdgeShapeInfo(last, "From edge", Constants.RED, true), +// new GMLEdgeShapeInfo(next, "To edge", Constants.BLUE, true)); + // See if any faces are connected on the side we care about + if (hasConnectedFace(next, lookOnLeft)) { + // There's a connected face so this walk isn't going to result in an open space. + return null; + } + dEdge = new GMLDirectedEdge(next, current); + current = dEdge.getEndNode(); + last = next; + result.add(dEdge); + } + return result; + } + + private boolean hasConnectedFace(GMLEdge edge, boolean left) { + Set faces = gmlMap.getAttachedFaces(edge); +// List shapes = new ArrayList(); +// shapes.add(new GMLEdgeShapeInfo(edge, "Test edge", Constants.BLUE, true)); +// shapes.add(new GMLNodeShapeInfo(edge.getStart(), "Start node", Constants.RED, true)); +// shapes.add(new GMLNodeShapeInfo(edge.getEnd(), "End node", Constants.NAVY, true)); +// for (GMLFace face : faces) { +// shapes.add(new GMLFaceShapeInfo(face, "Attached face", Constants.BLACK, Constants.TRANSPARENT_ORANGE, true)); +// } +// debug.show("Checking for connected faces", shapes); + for (GMLFace face : faces) { + if (FaceType.BUILDING.equals(face.getFaceType())) { + // Always exclude edges on buildings even if they're on the opposite side + return true; + } + if (left) { + if (face.isConnectedLeft(edge)) { + return true; + } + } + else { + if (face.isConnectedRight(edge)) { + return true; + } + } + } + return false; + } + + private List reverseEdgeList(List edges) { + List result = new ArrayList(edges.size()); + for (GMLDirectedEdge edge : edges) { + result.add(new GMLDirectedEdge(edge.getEdge(), !edge.isForward())); + } + Collections.reverse(result); + return result; + } + + private void processOpenSpace(List edges) { + GMLFace face = gmlMap.createFace(edges, FaceType.BUILDING); + gmlMap.removeFace(face); + if (!ConvertTools.isClockwise(face)) { + List e = new ArrayList(edges.size()); + for (GMLDirectedEdge next : edges) { + e.add(new GMLDirectedEdgeShapeInfo(next, "Open space edge", Constants.OLIVE, true, true)); + } + // Split into "nice" shapes + // if (isNearlyRectangular(face)) { + debug.show("Open space", e); + RowHousingBuildingSpaceFiller filler = new RowHousingBuildingSpaceFiller(sizeOf1m, random, debug); + filler.createBuildings(face, gmlMap); + // } + } + } + + private boolean isNearlyRectangular(GMLFace face) { + if (face.getEdges().size() != 4) { + return false; + } + // Check if the opposing faces are approximately parallel + Iterator it = face.getEdges().iterator(); + GMLDirectedEdge e1 = it.next(); + GMLDirectedEdge e2 = it.next(); + GMLDirectedEdge e3 = it.next(); + GMLDirectedEdge e4 = it.next(); + if (nearlyParallel(e1, e3) && nearlyParallel(e2, e4) && similarLength(e1, e3) && similarLength(e2, e4)) { + return true; + } + return false; + } + + private boolean nearlyParallel(GMLDirectedEdge e1, GMLDirectedEdge e2) { + Line2D l1 = ConvertTools.gmlDirectedEdgeToLine(e1); + Line2D l2 = ConvertTools.gmlDirectedEdgeToLine(e2); + double d = (l1.getDirection().getX() * l2.getDirection().getY()) - (l1.getDirection().getY() * l2.getDirection().getX()); + return ConvertTools.nearlyEqual(d, 0, NEARLY_PARALLEL_THRESHOLD); + } + + private boolean similarLength(GMLDirectedEdge e1, GMLDirectedEdge e2) { + double l1 = ConvertTools.gmlDirectedEdgeToLine(e1).getDirection().getLength(); + double l2 = ConvertTools.gmlDirectedEdgeToLine(e1).getDirection().getLength(); + return ConvertTools.nearlyEqual(l1 - l2, 0, SIMILAR_LENGTH_THRESHOLD); + } + */ +} diff --git a/modules/maps/src/maps/convert/osm2gml/CreateEntrancesStep.java b/modules/maps/src/maps/convert/osm2gml/CreateEntrancesStep.java new file mode 100644 index 0000000000000000000000000000000000000000..fb8df525cc45082e428e0cf08e4dbd20c1f67230 --- /dev/null +++ b/modules/maps/src/maps/convert/osm2gml/CreateEntrancesStep.java @@ -0,0 +1,81 @@ +package maps.convert.osm2gml; + +import maps.gml.GMLMap; + +import maps.convert.ConvertStep; + +/** + This class computes the entrances for buildings. +*/ +public class CreateEntrancesStep extends ConvertStep { + // private GMLMap gmlMap; + + /** + Construct a CreateEntrancesStep. + @param gmlMap The GMLMap to use. + */ + public CreateEntrancesStep(GMLMap gmlMap) { + super(); + // this.gmlMap = gmlMap; + } + + @Override + public String getDescription() { + return "Creating building entrances"; + } + + @Override + protected void step() { + /* + setProgressLimit(gmlMap.getFaces().size()); + int sharedCount = 0; + int corridorCount = 0; + for (GMLFace face : gmlMap.getFaces()) { + if (FaceType.BUILDING.equals(face.getFaceType())) { + // Look to see if we have any edges shared with a road + boolean found = false; + for (GMLDirectedEdge directedEdge : face.getEdges()) { + GMLEdge edge = directedEdge.getEdge(); + if (isSharedWithRoad(edge)) { + // Make the edge passable + // TO DO: Make part of the edge passable + // TO DO: Make more edges passable if this edge is too short + edge.setPassable(true); + found = true; + ++sharedCount; + break; + } + } + // If we couldn't find a shared edge then we need to create a corridor that connects an edge to a road. + if (!found) { + makeCorrider(face); + ++corridorCount; + } + } + bumpProgress(); + } + setStatus("Made " + sharedCount + " shared edges passable and created " + corridorCount + " corridors"); + */ + } + + /* + private boolean isSharedWithRoad(GMLEdge edge) { + for (GMLFace face : gmlMap.getAttachedFaces(edge)) { + if (FaceType.ROAD.equals(face.getFaceType())) { + return true; + } + } + return false; + } + + private void makeCorrider(GMLFace face) { + // Find an edge that is close to a road or intersection + GMLEdge bestBuildingEdge = null; + GMLEdge bestRoadEdge = null; + for (GMLDirectedEdge next : face.getEdges()) { + GMLEdge buildingEdge = next.getEdge(); + // Look for the nearest road or intersection edge + } + } + */ +} diff --git a/modules/maps/src/maps/convert/osm2gml/DirectedEdge.java b/modules/maps/src/maps/convert/osm2gml/DirectedEdge.java new file mode 100644 index 0000000000000000000000000000000000000000..0bfcf0b2cac156b28adad68413110467b057a5f4 --- /dev/null +++ b/modules/maps/src/maps/convert/osm2gml/DirectedEdge.java @@ -0,0 +1,126 @@ +package maps.convert.osm2gml; + +import rescuecore2.misc.geometry.Point2D; +import rescuecore2.misc.geometry.Line2D; + +/** + A DirectedEdge is an edge with an orientation. + */ +public class DirectedEdge { + private Edge edge; + private boolean forward; + private Line2D line; + + /** + Construct a directed edge. + @param edge The underlying edge. + @param forward True if this directed edge is aligned with the underlying edge direction, false otherwise. + */ + public DirectedEdge(Edge edge, boolean forward) { + this.edge = edge; + this.forward = forward; + this.line = edge.getLine(); + if (!forward) { + line = new Line2D(line.getEndPoint(), line.getOrigin()); + } + } + + /** + Construct a directed edge. + @param edge The underlying edge. + @param start The start node. + */ + public DirectedEdge(Edge edge, Node start) { + this.edge = edge; + this.forward = start.equals(edge.getStart()); + this.line = edge.getLine(); + if (!forward) { + line = new Line2D(line.getEndPoint(), line.getOrigin()); + } + } + + /** + Get the underlying edge. + @return The underlying edge. + */ + public Edge getEdge() { + return edge; + } + + /** + Get the line represented by this edge. + @return The line. + */ + public Line2D getLine() { + return line; + } + + /** + Is this directed edge in the direction of the underlying edge? + @return True if this directed edge is aligned with the underlying edge direction, false otherwise. + */ + public boolean isForward() { + return forward; + } + + /** + Get the node at the start of the underlying edge. + @return The start node. + */ + public Node getStartNode() { + return forward ? edge.getStart() : edge.getEnd(); + } + + /** + Get the node at the end of the underlying edge. + @return The end node. + */ + public Node getEndNode() { + return forward ? edge.getEnd() : edge.getStart(); + } + + /** + Get the coordinates of the start of this edge. + @return The coordinates of the start of this edge. + */ + public Point2D getStartCoordinates() { + if (forward) { + return edge.getStart().getCoordinates(); + } + else { + return edge.getEnd().getCoordinates(); + } + } + + /** + Get the coordinates of the end of this edge. + @return The coordinates of the end of this edge. + */ + public Point2D getEndCoordinates() { + if (forward) { + return edge.getEnd().getCoordinates(); + } + else { + return edge.getStart().getCoordinates(); + } + } + + @Override + public String toString() { + return "DirectedEdge" + (forward ? "" : " backwards") + " along " + edge; + } + + @Override + public int hashCode() { + return edge.hashCode() ^ (forward ? 1 : 0); + } + + @Override + public boolean equals(Object o) { + if (o instanceof DirectedEdge) { + DirectedEdge e = (DirectedEdge)o; + return this.forward == e.forward && this.edge.equals(e.edge); + } + return false; + } +} diff --git a/modules/maps/src/maps/convert/osm2gml/DirectedEdgeShapeInfo.java b/modules/maps/src/maps/convert/osm2gml/DirectedEdgeShapeInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..09bbd95b846eb8ebe2414a86231a9410b6aed770 --- /dev/null +++ b/modules/maps/src/maps/convert/osm2gml/DirectedEdgeShapeInfo.java @@ -0,0 +1,57 @@ +package maps.convert.osm2gml; + +import java.awt.Color; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; + +import rescuecore2.misc.gui.ShapeDebugFrame; + +import rescuecore2.misc.geometry.Line2D; + +/** + A ShapeInfo that knows how to draw DirectedEdges. +*/ +public class DirectedEdgeShapeInfo extends ShapeDebugFrame.Line2DShapeInfo { + private Collection edges; + + /** + Create a new DirectedEdgeShapeInfo. + @param edge The edge to draw. + @param name The name of the edge. + @param colour The colour to draw the edge. + @param thick Whether to draw the edge thick or not. + */ + public DirectedEdgeShapeInfo(DirectedEdge edge, String name, Color colour, boolean thick) { + this(Collections.singleton(edge), name, colour, thick); + } + + /** + Create a new DirectedEdgeShapeInfo. + @param edges The edges to draw. + @param name The name of the edge. + @param colour The colour to draw the edge. + @param thick Whether to draw the edge thick or not. + */ + public DirectedEdgeShapeInfo(Collection edges, String name, Color colour, boolean thick) { + super(makeLines(edges), name, colour, thick, true); + this.edges = edges; + } + + @Override + public Object getObject() { + return edges; + } + + private static Collection makeLines(Collection edges) { + if (edges == null) { + return null; + } + Collection result = new ArrayList(); + for (DirectedEdge next : edges) { + result.add(next.getLine()); + } + return result; + } +} diff --git a/modules/maps/src/maps/convert/osm2gml/Edge.java b/modules/maps/src/maps/convert/osm2gml/Edge.java new file mode 100644 index 0000000000000000000000000000000000000000..9c1175e42fb5d36be7af06da9afb953155e7d97e --- /dev/null +++ b/modules/maps/src/maps/convert/osm2gml/Edge.java @@ -0,0 +1,61 @@ +package maps.convert.osm2gml; + +import rescuecore2.misc.geometry.Line2D; + +/** + An edge. An edge is a line between two nodes. + */ +public class Edge extends ManagedObject { + private Node start; + private Node end; + private Line2D line; + + /** + Construct a new Edge. + @param id The ID of this object. + @param start The start node. + @param end The end node. + */ + public Edge(long id, Node start, Node end) { + super(id); + this.start = start; + this.end = end; + line = new Line2D(start.getCoordinates(), end.getCoordinates()); + } + + /** + Get the start node. + @return The start node. + */ + public Node getStart() { + return start; + } + + /** + Get the end node. + @return The end node. + */ + public Node getEnd() { + return end; + } + + /** + Get the line represented by this edge. + @return The line. + */ + public Line2D getLine() { + return line; + } + + @Override + public String toString() { + StringBuilder result = new StringBuilder(); + result.append("Edge "); + result.append(getID()); + result.append(" from "); + result.append(start); + result.append(" to "); + result.append(end); + return result.toString(); + } +} diff --git a/modules/maps/src/maps/convert/osm2gml/EdgeShapeInfo.java b/modules/maps/src/maps/convert/osm2gml/EdgeShapeInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..96c987cf482fe576f7260b8ceffb648fa5fae1a7 --- /dev/null +++ b/modules/maps/src/maps/convert/osm2gml/EdgeShapeInfo.java @@ -0,0 +1,59 @@ +package maps.convert.osm2gml; + +import java.awt.Color; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; + +import rescuecore2.misc.gui.ShapeDebugFrame; + +import rescuecore2.misc.geometry.Line2D; + +/** + A ShapeInfo that knows how to draw Edges. +*/ +public class EdgeShapeInfo extends ShapeDebugFrame.Line2DShapeInfo { + private Collection edges; + + /** + Create a new EdgeShapeInfo. + @param edge The edge to draw. + @param name The name of the edge. + @param colour The colour to draw the edge. + @param thick Whether to draw the edge thick or not. + @param arrow Whether to draw the edge's direction or not. + */ + public EdgeShapeInfo(Edge edge, String name, Color colour, boolean thick, boolean arrow) { + this(Collections.singleton(edge), name, colour, thick, arrow); + } + + /** + Create a new EdgeShapeInfo. + @param edges The edges to draw. + @param name The name of the edge. + @param colour The colour to draw the edge. + @param thick Whether to draw the edge thick or not. + @param arrow Whether to draw the edge's direction or not. + */ + public EdgeShapeInfo(Collection edges, String name, Color colour, boolean thick, boolean arrow) { + super(makeLines(edges), name, colour, thick, arrow); + this.edges = edges; + } + + @Override + public Object getObject() { + return edges; + } + + private static Collection makeLines(Collection edges) { + if (edges == null) { + return null; + } + Collection result = new ArrayList(); + for (Edge next : edges) { + result.add(next.getLine()); + } + return result; + } +} diff --git a/modules/maps/src/maps/convert/osm2gml/MakeObjectsStep.java b/modules/maps/src/maps/convert/osm2gml/MakeObjectsStep.java new file mode 100644 index 0000000000000000000000000000000000000000..7ffe31fdfa008500e67bdfa3528f37a7e3de5ef2 --- /dev/null +++ b/modules/maps/src/maps/convert/osm2gml/MakeObjectsStep.java @@ -0,0 +1,103 @@ +package maps.convert.osm2gml; + +import maps.gml.GMLMap; +import maps.gml.GMLNode; +import maps.gml.GMLEdge; +import maps.gml.GMLDirectedEdge; +import maps.gml.GMLShape; +import maps.convert.ConvertStep; +import maps.ScaleConversion; + +import java.util.List; +import java.util.ArrayList; +import java.util.Map; +import java.util.HashMap; +import java.util.Collection; + +/** + This step creates the final GML objects. +*/ +public class MakeObjectsStep extends ConvertStep { + private TemporaryMap map; + private GMLMap gmlMap; + + /** + Construct a MakeObjectsStep. + @param map The TemporaryMap to read. + @param gmlMap The GMLMap to populate. + */ + public MakeObjectsStep(TemporaryMap map, GMLMap gmlMap) { + super(); + this.map = map; + this.gmlMap = gmlMap; + } + + @Override + public String getDescription() { + return "Generating GML objects"; + } + + @Override + protected void step() { + double xMin = Double.POSITIVE_INFINITY; + double yMin = Double.POSITIVE_INFINITY; + for (Node next : map.getAllNodes()) { + xMin = Math.min(xMin, next.getX()); + yMin = Math.min(yMin, next.getY()); + } + double sizeOf1m = ConvertTools.sizeOf1Metre(map.getOSMMap()); + double scale = 1.0 / sizeOf1m; + ScaleConversion conversion = new ScaleConversion(xMin, yMin, scale, scale); + Collection nodes = map.getAllNodes(); + Collection edges = map.getAllEdges(); + setProgressLimit(nodes.size() + edges.size() + (map.getAllObjects().size() * 2)); + Map nodeMap = new HashMap(); + Map edgeMap = new HashMap(); + Map shapeMap = new HashMap(); + for (Node n : nodes) { + GMLNode node = gmlMap.createNode(conversion.convertX(n.getX()), conversion.convertY(n.getY())); + nodeMap.put(n, node); + bumpProgress(); + } + for (Edge e : edges) { + GMLNode first = nodeMap.get(e.getStart()); + GMLNode second = nodeMap.get(e.getEnd()); + GMLEdge edge = gmlMap.createEdge(first, second); + edgeMap.put(e, edge); + bumpProgress(); + } + for (TemporaryBuilding b : map.getBuildings()) { + shapeMap.put(b, gmlMap.createBuilding(makeEdges(b, edgeMap))); + bumpProgress(); + } + for (TemporaryRoad r : map.getRoads()) { + shapeMap.put(r, gmlMap.createRoad(makeEdges(r, edgeMap))); + bumpProgress(); + } + for (TemporaryIntersection i : map.getIntersections()) { + shapeMap.put(i, gmlMap.createRoad(makeEdges(i, edgeMap))); + bumpProgress(); + } + // Generate neighbour information + for (TemporaryObject o : map.getAllObjects()) { + GMLShape s = shapeMap.get(o); + for (DirectedEdge e : o.getEdges()) { + TemporaryObject neighbour = o.getNeighbour(e); + if (neighbour != null) { + s.setNeighbour(edgeMap.get(e.getEdge()), shapeMap.get(neighbour).getID()); + } + } + bumpProgress(); + } + setStatus("Created " + gmlMap.getRoads().size() + " roads and " + gmlMap.getBuildings().size() + " buildings"); + } + + private List makeEdges(TemporaryObject o, Map edgeMap) { + List oldEdges = o.getEdges(); + List result = new ArrayList(oldEdges.size()); + for (DirectedEdge dEdge : oldEdges) { + result.add(new GMLDirectedEdge(edgeMap.get(dEdge.getEdge()), dEdge.isForward())); + } + return result; + } +} diff --git a/modules/maps/src/maps/convert/osm2gml/MakeTempObjectsStep.java b/modules/maps/src/maps/convert/osm2gml/MakeTempObjectsStep.java new file mode 100644 index 0000000000000000000000000000000000000000..5ebe1d631f7cfe51b109e04ad8ab0f137a57d3c5 --- /dev/null +++ b/modules/maps/src/maps/convert/osm2gml/MakeTempObjectsStep.java @@ -0,0 +1,97 @@ +package maps.convert.osm2gml; + +import maps.convert.ConvertStep; + +import java.util.Collection; +import java.util.List; +import java.util.ArrayList; +import java.util.Iterator; + +import rescuecore2.misc.geometry.Point2D; + +/** + This step creates TemporaryObjects from the OSM data. +*/ +public class MakeTempObjectsStep extends ConvertStep { + private TemporaryMap map; + + /** + Construct a MakeTempObjectsStep. + @param map The TemporaryMap to populate. + */ + public MakeTempObjectsStep(TemporaryMap map) { + super(); + this.map = map; + } + + @Override + public String getDescription() { + return "Generating temporary objects"; + } + + @Override + protected void step() { + Collection roads = map.getOSMRoadInfo(); + Collection intersections = map.getOSMIntersectionInfo(); + Collection buildings = map.getOSMBuildingInfo(); + setProgressLimit(roads.size() + intersections.size() + buildings.size()); + generateRoadObjects(roads); + generateIntersectionObjects(intersections); + generateBuildingObjects(buildings); + setStatus("Created " + map.getRoads().size() + " roads, " + map.getIntersections().size() + " intersections, " + map.getBuildings().size() + " buildings"); + } + + private void generateRoadObjects(Collection roads) { + for (OSMRoadInfo road : roads) { + if (road.getArea() != null) { + List edges = generateEdges(road); + if (edges.size() > 2) { + map.addRoad(new TemporaryRoad(edges)); + } + } + bumpProgress(); + } + } + + private void generateIntersectionObjects(Collection intersections) { + for (OSMIntersectionInfo intersection : intersections) { + if (intersection.getArea() != null) { + List edges = generateEdges(intersection); + if (edges.size() > 2) { + map.addIntersection(new TemporaryIntersection(edges)); + } + } + bumpProgress(); + } + } + + private void generateBuildingObjects(Collection buildings) { + for (OSMBuildingInfo building : buildings) { + if (building.getArea() != null) { + List edges = generateEdges(building); + if (edges.size() > 2) { + map.addBuilding(new TemporaryBuilding(edges, building.getBuildingID())); + } + } + bumpProgress(); + } + } + + private List generateEdges(OSMShape s) { + List result = new ArrayList(); + Iterator it = s.getVertices().iterator(); + Node first = map.getNode(it.next()); + Node previous = first; + while (it.hasNext()) { + Node n = map.getNode(it.next()); + if (!n.equals(previous)) { + result.add(map.getDirectedEdge(previous, n)); + previous = n; + } + } + if (!previous.equals(first)) { + result.add(map.getDirectedEdge(previous, first)); + } + return result; + } +} diff --git a/modules/maps/src/maps/convert/osm2gml/ManagedObject.java b/modules/maps/src/maps/convert/osm2gml/ManagedObject.java new file mode 100644 index 0000000000000000000000000000000000000000..19caeb99f6492b9a4270f6efbf56d176830dd10d --- /dev/null +++ b/modules/maps/src/maps/convert/osm2gml/ManagedObject.java @@ -0,0 +1,37 @@ +package maps.convert.osm2gml; + +/** + A managed map object. +*/ +public abstract class ManagedObject { + private long id; + + /** + Construct a managed object. + @param id The id of the object. + */ + protected ManagedObject(long id) { + this.id = id; + } + + /** + Get this object's ID. + @return The object ID. + */ + public long getID() { + return id; + } + + @Override + public int hashCode() { + return (int)id; + } + + @Override + public boolean equals(Object o) { + if (o instanceof ManagedObject) { + return this.id == ((ManagedObject)o).id; + } + return false; + } +} diff --git a/modules/maps/src/maps/convert/osm2gml/MergeShapesStep.java b/modules/maps/src/maps/convert/osm2gml/MergeShapesStep.java new file mode 100644 index 0000000000000000000000000000000000000000..82c14aea5ab4f7c4781d131d3545d2655204a91f --- /dev/null +++ b/modules/maps/src/maps/convert/osm2gml/MergeShapesStep.java @@ -0,0 +1,159 @@ +package maps.convert.osm2gml; + +import java.util.Map; +import java.util.List; +import java.util.HashMap; +import java.util.ArrayList; +import java.util.Set; +import java.util.HashSet; +import java.util.Collection; +import java.util.Collections; + +import java.awt.Color; + +import maps.convert.ConvertStep; + +/** + This class merges adjacent shapes of the same type. +*/ +public class MergeShapesStep extends ConvertStep { + private TemporaryMap map; + + /** + Construct a MergeShapesStep. + @param map The TemporaryMap to use. + */ + public MergeShapesStep(TemporaryMap map) { + this.map = map; + } + + @Override + public String getDescription() { + return "Merging adjacent shapes"; + } + + @Override + protected void step() { + debug.setBackground(ConvertTools.getAllDebugShapes(map)); + Collection buildings = map.getBuildings(); + Collection roads = map.getRoads(); + setProgressLimit(buildings.size() + roads.size()); + // Merge any buildings with the same ID that got split earlier. + int buildingCount = 0; + int roadCount = 0; + for (TemporaryBuilding next : buildings) { + if (tryToMerge(next)) { + ++buildingCount; + } + bumpProgress(); + } + // Try merging adjacent roads + for (TemporaryRoad next : roads) { + if (tryToMerge(next)) { + ++roadCount; + } + bumpProgress(); + } + setStatus("Merged " + buildingCount + " building shapes and " + roadCount + " road shapes"); + } + + private boolean tryToMerge(TemporaryBuilding b) { + if (!map.getBuildings().contains(b)) { + return false; + } + Collection others = map.getBuildings(); + for (TemporaryBuilding other : others) { + if (b == other) { + continue; + } + if (other.getBuildingID() == b.getBuildingID()) { + List boundary = mergeShapes(b, other); + if (boundary == null) { + continue; + } + TemporaryBuilding newBuilding = new TemporaryBuilding(boundary, b.getBuildingID()); + map.addBuilding(newBuilding); + map.removeBuilding(b); + map.removeBuilding(other); + debug.show("Merged buildings", new TemporaryObjectInfo(b, "First", Color.BLACK, Color.GREEN), + new TemporaryObjectInfo(other, "Second", Color.BLACK, Color.WHITE), + new TemporaryObjectInfo(newBuilding, "New building", Color.BLUE, null)); + return true; + } + } + return false; + } + + private boolean tryToMerge(TemporaryRoad r) { + if (!map.getRoads().contains(r)) { + return false; + } + Collection others = map.getRoads(); + for (TemporaryRoad other : others) { + if (r == other) { + continue; + } + List boundary = mergeShapes(r, other); + if (boundary == null) { + continue; + } + // Check for convexity + if (ConvertTools.isConvex(boundary)) { + TemporaryRoad newRoad = new TemporaryRoad(boundary); + map.addRoad(newRoad); + map.removeRoad(r); + map.removeRoad(other); + debug.show("Merged roads", new TemporaryObjectInfo(r, "First", Color.BLACK, Color.GREEN), + new TemporaryObjectInfo(other, "Second", Color.BLACK, Color.WHITE), + new TemporaryObjectInfo(newRoad, "New road", Color.BLUE, null)); + return true; + } + } + return false; + } + + private List mergeShapes(TemporaryObject first, TemporaryObject second) { + Map edges1 = new HashMap(); + Map edges2 = new HashMap(); + for (DirectedEdge e : first.getEdges()) { + edges1.put(e.getEdge(), e); + } + for (DirectedEdge e : second.getEdges()) { + edges2.put(e.getEdge(), e); + } + if (Collections.disjoint(edges1.keySet(), edges2.keySet())) { + return null; + } + Set boundary = new HashSet(); + for (Map.Entry next : edges1.entrySet()) { + if (!edges2.containsKey(next.getKey())) { + boundary.add(next.getValue()); + } + } + for (Map.Entry next : edges2.entrySet()) { + if (!edges1.containsKey(next.getKey())) { + boundary.add(next.getValue()); + } + } + // Walk the boundary + DirectedEdge start = boundary.iterator().next(); + List result = new ArrayList(); + result.add(start); + while (!boundary.isEmpty()) { + start = findNextEdge(start, boundary); + boundary.remove(start); + result.add(start); + } + return result; + } + + private DirectedEdge findNextEdge(DirectedEdge from, Set candidates) { + Node n = from.getEndNode(); + for (DirectedEdge next : candidates) { + if (next.getStartNode().equals(n)) { + return next; + } + } + throw new IllegalArgumentException("No candidate edge starting from " + n); + } +} diff --git a/modules/maps/src/maps/convert/osm2gml/Node.java b/modules/maps/src/maps/convert/osm2gml/Node.java new file mode 100644 index 0000000000000000000000000000000000000000..2bdcd22610635d6f0e97d4bb10b2f35fa4685164 --- /dev/null +++ b/modules/maps/src/maps/convert/osm2gml/Node.java @@ -0,0 +1,59 @@ +package maps.convert.osm2gml; + +import rescuecore2.misc.geometry.Point2D; + +/** + A node object. + */ +public class Node extends ManagedObject { + private Point2D coordinates; + + /** + Construct a new node. + @param id The ID of this node. + @param x The x coordinate of this node. + @param y The y coordinate of this node. + */ + public Node(long id, double x, double y) { + this(id, new Point2D(x, y)); + } + + /** + Construct a new node. + @param id The ID of this node. + @param coordinates The coordinates of this node. + */ + public Node(long id, Point2D coordinates) { + super(id); + this.coordinates = coordinates; + } + + /** + Get the coordinates of this node. + @return The node coordinates. + */ + public Point2D getCoordinates() { + return coordinates; + } + + /** + Get the X coordinate. + @return The X coordinate. + */ + public double getX() { + return coordinates.getX(); + } + + /** + Get the Y coordinate. + @return The Y coordinate. + */ + public double getY() { + return coordinates.getY(); + } + + @Override + public String toString() { + return "Node " + getID() + " at " + coordinates; + } +} diff --git a/modules/maps/src/maps/convert/osm2gml/OSMBuildingInfo.java b/modules/maps/src/maps/convert/osm2gml/OSMBuildingInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..08e749c644b97a1add17084bea54b55a91f4b203 --- /dev/null +++ b/modules/maps/src/maps/convert/osm2gml/OSMBuildingInfo.java @@ -0,0 +1,80 @@ +package maps.convert.osm2gml; + +import maps.osm.OSMBuilding; +import maps.osm.OSMMap; +import maps.osm.OSMNode; + +import rescuecore2.misc.geometry.Point2D; + +import java.awt.geom.Area; +import java.awt.geom.Path2D; + +import java.util.List; +import java.util.ArrayList; +import java.util.Iterator; + +/** + Information about an OSM building. +*/ +public class OSMBuildingInfo implements OSMShape { + private List vertices; + private Area area; + private long buildingID; + + /** + Construct a new OSMBuildingInfo. + @param building The building. + @param map The map. + */ + public OSMBuildingInfo(OSMBuilding building, OSMMap map) { + buildingID = building.getID(); + vertices = new ArrayList(); + for (Long next : building.getNodeIDs()) { + OSMNode node = map.getNode(next); + vertices.add(new Point2D(node.getLongitude(), node.getLatitude())); + } + // Compute the area + Path2D.Double path = new Path2D.Double(); + Iterator it = vertices.iterator(); + Point2D point = it.next(); + path.moveTo(point.getX(), point.getY()); + while (it.hasNext()) { + point = it.next(); + path.lineTo(point.getX(), point.getY()); + } + path.closePath(); + area = new Area(path.createTransformedShape(null)); + } + + @Override + public List getVertices() { + return vertices; + } + + @Override + public Area getArea() { + return area; + } + + /** + Get the ID of the building. + @return The building ID. + */ + public long getBuildingID() { + return buildingID; + } + + @Override + public String toString() { + StringBuilder result = new StringBuilder(); + result.append("BuildingInfo ["); + for (Iterator it = vertices.iterator(); it.hasNext();) { + result.append(it.next().toString()); + if (it.hasNext()) { + result.append(", "); + } + } + result.append("]"); + return result.toString(); + } +} diff --git a/modules/maps/src/maps/convert/osm2gml/OSMIntersectionInfo.java b/modules/maps/src/maps/convert/osm2gml/OSMIntersectionInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..0a3174cdb11088b8eb6476e79a74c47dbf05f5bd --- /dev/null +++ b/modules/maps/src/maps/convert/osm2gml/OSMIntersectionInfo.java @@ -0,0 +1,322 @@ +package maps.convert.osm2gml; + +import java.util.List; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.Collections; +import java.util.Comparator; + +import java.awt.geom.Area; +import java.awt.geom.Path2D; + +import rescuecore2.misc.geometry.Line2D; +import rescuecore2.misc.geometry.Point2D; +import rescuecore2.misc.geometry.Vector2D; +import rescuecore2.misc.geometry.GeometryTools2D; + +//import rescuecore2.misc.gui.ShapeDebugFrame; +//import java.awt.Color; + +import maps.osm.OSMNode; + +/** + Information about an OSM intersection. +*/ +public class OSMIntersectionInfo implements OSMShape { + // private static ShapeDebugFrame debug = new ShapeDebugFrame(); + + private OSMNode centre; + private List roads; + private List vertices; + private Area area; + + /** + Create an IntersectionInfo. + @param centre The OSMNode at the centre of the intersection. + */ + public OSMIntersectionInfo(OSMNode centre) { + this.centre = centre; + roads = new ArrayList(); + } + + /** + Add an incoming road. + @param road The incoming road. + */ + public void addRoadSegment(OSMRoadInfo road) { + if (road.getFrom() == centre && road.getTo() == centre) { + System.out.println("Degenerate road found"); + } + else { + roads.add(new RoadAspect(road, centre)); + } + } + + /** + Process this intersection and determine the vertices and area it covers. + @param sizeOf1m The size of 1m in latitude/longitude. + */ + public void process(double sizeOf1m) { + vertices = new ArrayList(); + if (roads.size() > 1) { + processRoads(sizeOf1m); + } + else { + processSingleRoad(sizeOf1m); + area = null; + } + } + + /** + Get the OSMNode at the centre of this intersection. + @return The OSMNode at the centre. + */ + public OSMNode getCentre() { + return centre; + } + + @Override + public Area getArea() { + return area; + } + + @Override + public List getVertices() { + return vertices; + } + + @Override + public String toString() { + StringBuilder result = new StringBuilder(); + result.append("IntersectionInfo (centre "); + result.append(centre); + result.append(") ["); + for (Iterator it = vertices.iterator(); it.hasNext();) { + result.append(it.next().toString()); + if (it.hasNext()) { + result.append(", "); + } + } + result.append("]"); + if (area == null) { + result.append(" (degenerate)"); + } + return result.toString(); + } + + private void processRoads(double sizeOf1m) { + // Sort incoming roads counterclockwise about the centre + Point2D centrePoint = new Point2D(centre.getLongitude(), centre.getLatitude()); + CounterClockwiseSort sort = new CounterClockwiseSort(centrePoint); + Collections.sort(roads, sort); + // Go through each pair of adjacent incoming roads and compute the two intersection points + Iterator it = roads.iterator(); + RoadAspect first = it.next(); + RoadAspect previous = first; + while (it.hasNext()) { + RoadAspect next = it.next(); + Point2D p = findIncomingRoadIntersection(previous, next, centrePoint, sizeOf1m); + vertices.add(p); + previous = next; + } + Point2D p = findIncomingRoadIntersection(previous, first, centrePoint, sizeOf1m); + vertices.add(p); + // If there are multiple vertices then compute the area + if (vertices.size() > 2) { + Iterator ix = vertices.iterator(); + Point2D point = ix.next(); + Path2D.Double path = new Path2D.Double(); + path.moveTo(point.getX(), point.getY()); + while (ix.hasNext()) { + point = ix.next(); + path.lineTo(point.getX(), point.getY()); + } + path.closePath(); + area = new Area(path.createTransformedShape(null)); + } + else { + area = null; + } + } + + /** + Process two incoming roads and find the intersection of the right edge of the first road and the left edge of the second road. + @param first The road to check the right edge of. + @param second The road the check the left edge of. + @param centrePoint The centre of the intersection. + @return The intersection of the two roads. + */ + private Point2D findIncomingRoadIntersection(RoadAspect first, RoadAspect second, Point2D centrePoint, double sizeOf1m) { + OSMNode firstNode = first.getFarNode(); + OSMNode secondNode = second.getFarNode(); + // Find the intersection of the incoming road edges + Point2D firstPoint = new Point2D(firstNode.getLongitude(), firstNode.getLatitude()); + Point2D secondPoint = new Point2D(secondNode.getLongitude(), secondNode.getLatitude()); + Vector2D firstVector = centrePoint.minus(firstPoint); + Vector2D secondVector = centrePoint.minus(secondPoint); + Vector2D firstNormal = firstVector.getNormal().normalised().scale(-Constants.ROAD_WIDTH * sizeOf1m / 2); + Vector2D secondNormal = secondVector.getNormal().normalised().scale(Constants.ROAD_WIDTH * sizeOf1m / 2); + Point2D start1Point = firstPoint.plus(firstNormal); + Point2D start2Point = secondPoint.plus(secondNormal); + Line2D line1 = new Line2D(start1Point, firstVector); + Line2D line2 = new Line2D(start2Point, secondVector); + Point2D intersection = GeometryTools2D.getIntersectionPoint(line1, line2); + if (intersection == null) { + // Lines are parallel + // This means the normals are parallel, so we can just add a normal to the centre point to generate an intersection point + intersection = centrePoint.plus(firstNormal); + } + first.setRightEnd(intersection); + second.setLeftEnd(intersection); + + /* + List shapes = new ArrayList(); + shapes.add(new ShapeDebugFrame.Line2DShapeInfo(new Line2D(firstPoint, centrePoint), "First road", Color.BLUE, false, false)); + shapes.add(new ShapeDebugFrame.Line2DShapeInfo(new Line2D(firstPoint, firstNormal), "First road offset", Color.YELLOW, false, false)); + shapes.add(new ShapeDebugFrame.Point2DShapeInfo(start1Point, "Left start", Color.BLUE, true)); + shapes.add(new ShapeDebugFrame.Line2DShapeInfo(line1, "Left edge", Color.BLUE, true, false)); + shapes.add(new ShapeDebugFrame.Line2DShapeInfo(new Line2D(secondPoint, centrePoint), "Second road", Color.WHITE, false, false)); + shapes.add(new ShapeDebugFrame.Line2DShapeInfo(new Line2D(secondPoint, secondNormal), "Second road offset", Color.CYAN, false, false)); + shapes.add(new ShapeDebugFrame.Point2DShapeInfo(start2Point, "Right start", Color.WHITE, true)); + shapes.add(new ShapeDebugFrame.Line2DShapeInfo(line2, "Right edge", Color.WHITE, true, false)); + shapes.add(new ShapeDebugFrame.Point2DShapeInfo(intersection, "Intersection", Color.ORANGE, true)); + debug.show("Intersection", shapes); + */ + + return intersection; + } + + /** + This "intersection" has a single incoming road. Set the incoming road's left and right edges. + */ + private void processSingleRoad(double sizeOf1m) { + Point2D centrePoint = new Point2D(centre.getLongitude(), centre.getLatitude()); + RoadAspect road = roads.iterator().next(); + OSMNode node = road.getFarNode(); + Point2D nodePoint = new Point2D(node.getLongitude(), node.getLatitude()); + Vector2D nodeVector = centrePoint.minus(nodePoint); + Vector2D nodeNormal = nodeVector.getNormal().normalised().scale(-Constants.ROAD_WIDTH * sizeOf1m / 2); + Vector2D nodeNormal2 = nodeNormal.scale(-1); + Point2D start1Point = nodePoint.plus(nodeNormal); + Point2D start2Point = nodePoint.plus(nodeNormal2); + Line2D line1 = new Line2D(start1Point, nodeVector); + Line2D line2 = new Line2D(start2Point, nodeVector); + Point2D end1 = line1.getPoint(1); + Point2D end2 = line2.getPoint(1); + road.setRightEnd(end1); + road.setLeftEnd(end2); + + /* + List shapes = new ArrayList(); + shapes.add(new ShapeDebugFrame.Line2DShapeInfo(new Line2D(nodePoint, centrePoint), "Single road", Color.BLUE, false)); + shapes.add(new ShapeDebugFrame.Line2DShapeInfo(new Line2D(nodePoint, nodeNormal), "Offset 1", Color.YELLOW, false)); + shapes.add(new ShapeDebugFrame.Line2DShapeInfo(new Line2D(nodePoint, nodeNormal2), "Offset 2", Color.CYAN, false)); + shapes.add(new ShapeDebugFrame.Point2DShapeInfo(start1Point, "Left start", Color.BLUE, true)); + shapes.add(new ShapeDebugFrame.Line2DShapeInfo(line1, "Left edge", Color.BLUE, true)); + shapes.add(new ShapeDebugFrame.Point2DShapeInfo(start2Point, "Right start", Color.WHITE, true)); + shapes.add(new ShapeDebugFrame.Line2DShapeInfo(line2, "Right edge", Color.WHITE, true)); + shapes.add(new ShapeDebugFrame.Point2DShapeInfo(end1, "Endpoint 1", Color.ORANGE, true)); + shapes.add(new ShapeDebugFrame.Point2DShapeInfo(end2, "Endpoint 2", Color.PINK, true)); + debug.show(shapes); + */ + } + + private static class RoadAspect { + private boolean forward; + private OSMRoadInfo road; + + RoadAspect(OSMRoadInfo road, OSMNode intersection) { + this.road = road; + forward = intersection == road.getTo(); + } + + OSMRoadInfo getRoad() { + return road; + } + + OSMNode getFarNode() { + return forward ? road.getFrom() : road.getTo(); + } + + void setLeftEnd(Point2D p) { + if (forward) { + road.setToLeft(p); + } + else { + road.setFromRight(p); + } + } + + void setRightEnd(Point2D p) { + if (forward) { + road.setToRight(p); + } + else { + road.setFromLeft(p); + } + } + } + + private static class CounterClockwiseSort implements Comparator { + private Point2D centre; + + /** + Construct a CounterClockwiseSort with a reference point. + @param centre The reference point. + */ + public CounterClockwiseSort(Point2D centre) { + this.centre = centre; + } + + @Override + public int compare(RoadAspect first, RoadAspect second) { + double d1 = score(first); + double d2 = score(second); + if (d1 < d2) { + return 1; + } + else if (d1 > d2) { + return -1; + } + else { + return 0; + } + } + + /** + Compute the score for a RoadAspect - the amount of clockwiseness from 12 o'clock. + @param aspect The RoadAspect. + @return The amount of clockwiseness. This will be in the range [0..4) with 0 representing 12 o'clock, 1 representing 3 o'clock and so on. + */ + public double score(RoadAspect aspect) { + OSMNode node = aspect.getFarNode(); + Point2D point = new Point2D(node.getLongitude(), node.getLatitude()); + Vector2D v = point.minus(centre); + double sin = v.getX() / v.getLength(); + double cos = v.getY() / v.getLength(); + if (Double.isNaN(sin) || Double.isNaN(cos)) { + System.out.println(v); + System.out.println(v.getLength()); + } + return convert(sin, cos); + } + + // CHECKSTYLE:OFF:MagicNumber + private double convert(double sin, double cos) { + if (sin >= 0 && cos >= 0) { + return sin; + } + if (sin >= 0 && cos < 0) { + return 2 - sin; + } + if (sin < 0 && cos < 0) { + return 2 - sin; + } + if (sin < 0 && cos >= 0) { + return 4 + sin; + } + throw new IllegalArgumentException("This should be impossible! What's going on? sin=" + sin + ", cos=" + cos); + } + // CHECKSTYLE:ON:MagicNumber + } +} diff --git a/modules/maps/src/maps/convert/osm2gml/OSMRoadInfo.java b/modules/maps/src/maps/convert/osm2gml/OSMRoadInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..2a0c98846ccc02ca2e401b0674a18de0643e12db --- /dev/null +++ b/modules/maps/src/maps/convert/osm2gml/OSMRoadInfo.java @@ -0,0 +1,162 @@ +package maps.convert.osm2gml; + +import rescuecore2.misc.geometry.Point2D; + +import maps.osm.OSMNode; + +import java.awt.geom.Area; +import java.awt.geom.Path2D; + +import java.util.List; +import java.util.ArrayList; + +/** + Information about an OSM road. +*/ +public class OSMRoadInfo implements OSMShape { + private OSMNode from; + private OSMNode to; + private Point2D fromLeft; + private Point2D toLeft; + private Point2D fromRight; + private Point2D toRight; + private Area area; + + /** + Create an OSMRoadInfo between two nodes. + @param from The first OSMNode. + @param to The second OSMNode. + */ + public OSMRoadInfo(OSMNode from, OSMNode to) { + this.from = from; + this.to = to; + area = null; + } + + /** + Get the "from" node. + @return The "from" node. + */ + public OSMNode getFrom() { + return from; + } + + /** + Get the "to" node. + @return The "to" node. + */ + public OSMNode getTo() { + return to; + } + + /** + Set the point that is at the left side of this road at the "from" end. + @param p The from-left corner point. + */ + public void setFromLeft(Point2D p) { + fromLeft = p; + area = null; + } + + /** + Set the point that is at the right side of this road at the "from" end. + @param p The from-right corner point. + */ + public void setFromRight(Point2D p) { + fromRight = p; + area = null; + } + + /** + Set the point that is at the left side of this road at the "to" end. + @param p The to-left corner point. + */ + public void setToLeft(Point2D p) { + toLeft = p; + area = null; + } + + /** + Set the point that is at the right side of this road at the "to" end. + @param p The to-right corner point. + */ + public void setToRight(Point2D p) { + toRight = p; + area = null; + } + + /** + Get the point that is at the left side of this road at the "from" end. + @return The from-left corner point. + */ + public Point2D getFromLeft() { + return fromLeft; + } + + /** + Get the point that is at the right side of this road at the "from" end. + @return The from-right corner point. + */ + public Point2D getFromRight() { + return fromRight; + } + + /** + Get the point that is at the left side of this road at the "to" end. + @return The to-left corner point. + */ + public Point2D getToLeft() { + return toLeft; + } + + /** + Get the point that is at the right side of this road at the "to" end. + @return The to-right corner point. + */ + public Point2D getToRight() { + return toRight; + } + + @Override + public Area getArea() { + if (area == null) { + if (fromLeft == null || fromRight == null || toLeft == null || toRight == null) { + return null; + } + Path2D.Double path = new Path2D.Double(); + path.moveTo(fromLeft.getX(), fromLeft.getY()); + path.lineTo(fromRight.getX(), fromRight.getY()); + path.lineTo(toRight.getX(), toRight.getY()); + path.lineTo(toLeft.getX(), toLeft.getY()); + path.closePath(); + area = new Area(path.createTransformedShape(null)); + } + return area; + } + + @Override + public List getVertices() { + List result = new ArrayList(); + result.add(fromLeft); + result.add(fromRight); + result.add(toRight); + result.add(toLeft); + return result; + } + + @Override + public String toString() { + StringBuilder result = new StringBuilder(); + result.append("RoadInfo ["); + result.append(fromLeft); + result.append(", "); + result.append(fromRight); + result.append(", "); + result.append(toRight); + result.append(", "); + result.append(toLeft); + result.append("]"); + return result.toString(); + } +} + diff --git a/modules/maps/src/maps/convert/osm2gml/OSMShape.java b/modules/maps/src/maps/convert/osm2gml/OSMShape.java new file mode 100644 index 0000000000000000000000000000000000000000..c61efd3af7e36bcd83db237cf1d63dd3ce1cf640 --- /dev/null +++ b/modules/maps/src/maps/convert/osm2gml/OSMShape.java @@ -0,0 +1,23 @@ +package maps.convert.osm2gml; + +import java.util.List; +import java.awt.geom.Area; + +import rescuecore2.misc.geometry.Point2D; + +/** + Interface for OSM object shapes. +*/ +public interface OSMShape { + /** + Get the vertices of this shape in clockwise order. + @return The vertices of this shape. + */ + List getVertices(); + + /** + Get the area covered by this shape. + @return The area of this shape. + */ + Area getArea(); +} diff --git a/modules/maps/src/maps/convert/osm2gml/PruneStep.java b/modules/maps/src/maps/convert/osm2gml/PruneStep.java new file mode 100644 index 0000000000000000000000000000000000000000..4f99e17f9fa93bd66cd686a2870410e26018c1a7 --- /dev/null +++ b/modules/maps/src/maps/convert/osm2gml/PruneStep.java @@ -0,0 +1,54 @@ +package maps.convert.osm2gml; + +import maps.gml.GMLMap; + +import maps.convert.ConvertStep; + +/** + This step removes extra nodes and edges. +*/ +public class PruneStep extends ConvertStep { + // private GMLMap gmlMap; + + /** + Construct a PruneStep. + @param gmlMap The GMLMap to use. + */ + public PruneStep(GMLMap gmlMap) { + super(); + // this.gmlMap = gmlMap; + } + + @Override + public String getDescription() { + return "Pruning nodes and edges"; + } + + @Override + protected void step() { + /* + setProgressLimit(gmlMap.getEdges().size() + gmlMap.getNodes().size()); + int edgeCount = 0; + int nodeCount = 0; + // Any edge that is not part of a face can be pruned + setStatus("Pruning edges"); + for (GMLEdge next : gmlMap.getEdges()) { + if (gmlMap.getAttachedFaces(next).isEmpty()) { + gmlMap.removeEdge(next); + ++edgeCount; + } + bumpProgress(); + } + // Any node that is not part of an edge can be pruned + setStatus("Pruning nodes"); + for (GMLNode next : gmlMap.getNodes()) { + if (gmlMap.getAttachedEdges(next).isEmpty()) { + gmlMap.removeNode(next); + ++nodeCount; + } + bumpProgress(); + } + setStatus("Removed " + edgeCount + " edges and " + nodeCount + " nodes"); + */ + } +} diff --git a/modules/maps/src/maps/convert/osm2gml/RemoveShapesStep.java b/modules/maps/src/maps/convert/osm2gml/RemoveShapesStep.java new file mode 100644 index 0000000000000000000000000000000000000000..c2ca7004ba662cdedc9cc4a3e51a046e13b8184c --- /dev/null +++ b/modules/maps/src/maps/convert/osm2gml/RemoveShapesStep.java @@ -0,0 +1,128 @@ +package maps.convert.osm2gml; + +import java.awt.Color; + +import java.util.Set; +import java.util.HashSet; +import java.util.Collection; + +import maps.convert.ConvertStep; + +import rescuecore2.log.Logger; + +/** + This step removes shapes that are duplicates or contained entirely inside another shape. +*/ +public class RemoveShapesStep extends ConvertStep { + private TemporaryMap map; + + /** + Construct a RemoveFacesStep. + @param map The TemporaryMap to use. + */ + public RemoveShapesStep(TemporaryMap map) { + super(); + this.map = map; + } + + @Override + public String getDescription() { + return "Removing extraneous shapes"; + } + + @Override + protected void step() { + debug.setBackground(ConvertTools.getAllDebugShapes(map)); + debug.setAutozoomEnabled(false); + Collection allObjects = map.getAllObjects(); + setProgressLimit(allObjects.size() * 2); + Set removed = new HashSet(); + setStatus("Removing duplicate shapes"); + int duplicateCount = 0; + int interiorCount = 0; + Logger.debug("Removing building duplicates"); + duplicateCount += removeDuplicates(map.getBuildings(), removed, allObjects); + Logger.debug("Removing intersection duplicates"); + duplicateCount += removeDuplicates(map.getIntersections(), removed, allObjects); + Logger.debug("Removing road duplicates"); + duplicateCount += removeDuplicates(map.getRoads(), removed, allObjects); + Logger.debug("Removing interior faces"); + setStatus("Removing interior faces"); + interiorCount += removeInterior(map.getRoads(), removed, allObjects); + interiorCount += removeInterior(map.getIntersections(), removed, allObjects); + interiorCount += removeInterior(map.getBuildings(), removed, allObjects); + setStatus("Removed " + removed.size() + " faces: " + duplicateCount + " duplicates and " + interiorCount + " interior"); + debug.clearBackground(); + debug.activate(); + debug.show("Result", ConvertTools.getAllDebugShapes(map)); + } + + /** + Remove all shapes that are duplicates of a test set. + @param test The set of objects to test against. + @param removed The set of removed objects. + @param toCheck The set of shapes to check. + @return The number of objects removed. + */ + private int removeDuplicates(Collection test, Set removed, Collection toCheck) { + int count = 0; + Logger.debug(test.size() + " test objects, " + toCheck.size() + " to check, " + removed.size() + " already removed"); + for (TemporaryObject first : test) { + bumpProgress(); + if (removed.contains(first)) { + continue; + } + Logger.debug("Next test object: " + first); + for (TemporaryObject second : toCheck) { + if (removed.contains(second)) { + continue; + } + if (first == second) { + continue; + } + Logger.debug("Next check object: " + second); + if (first.isDuplicate(second)) { + map.removeTemporaryObject(second); + removed.add(second); + ++count; + Logger.debug("Removed duplicate object: " + second + " is same as " + first); + } + debug.show("Checking for duplicates", + new TemporaryObjectInfo(first, "First", Color.WHITE, Constants.TRANSPARENT_LIME), + new TemporaryObjectInfo(second, "Second", Color.WHITE, Constants.TRANSPARENT_BLUE)); + } + } + return count; + } + + /** + Remove any shapes that are entirely inside another shapes. + @param toCheck The set of objects to check. + @param removed The set of removed objects. + @param allObjects All objects. + @return The number of removed objects. + */ + private int removeInterior(Collection toCheck, Set removed, Collection allObjects) { + int count = 0; + for (TemporaryObject first : toCheck) { + bumpProgress(); + if (removed.contains(first)) { + continue; + } + for (TemporaryObject second : allObjects) { + if (removed.contains(second)) { + continue; + } + if (first == second) { + continue; + } + if (first.isEntirelyInside(second)) { + map.removeTemporaryObject(first); + removed.add(first); + ++count; + } + } + } + return count; + } +} diff --git a/modules/maps/src/maps/convert/osm2gml/ScanOSMStep.java b/modules/maps/src/maps/convert/osm2gml/ScanOSMStep.java new file mode 100644 index 0000000000000000000000000000000000000000..5cceb45521ff6f7f8f2da92f704ec85ba7405623 --- /dev/null +++ b/modules/maps/src/maps/convert/osm2gml/ScanOSMStep.java @@ -0,0 +1,102 @@ +package maps.convert.osm2gml; + +import maps.osm.OSMMap; +import maps.osm.OSMNode; +import maps.osm.OSMRoad; +import maps.osm.OSMBuilding; + +import maps.convert.ConvertStep; + +import java.util.Map; +import java.util.List; +import java.util.HashMap; +import java.util.ArrayList; +import java.util.Iterator; + +/** + This step scans the OpenStreetMap data and generates information about roads, intersections and buildings. +*/ +public class ScanOSMStep extends ConvertStep { + private TemporaryMap map; + private Map nodeToIntersection; + private List intersections; + private List roads; + private List buildings; + + /** + Construct a ScanOSMStep. + @param map The OSMMap to scan. + */ + public ScanOSMStep(TemporaryMap map) { + this.map = map; + } + + @Override + public String getDescription() { + return "Scanning OpenStreetMap data"; + } + + @Override + protected void step() { + nodeToIntersection = new HashMap(); + intersections = new ArrayList(); + roads = new ArrayList(); + buildings = new ArrayList(); + OSMMap osm = map.getOSMMap(); + setProgressLimit(osm.getRoads().size() + osm.getBuildings().size()); + setStatus("Scanning roads and buildings"); + scanRoads(); + scanBuildings(); + double sizeOf1m = ConvertTools.sizeOf1Metre(osm); + setStatus("Generating intersections"); + setProgressLimit(intersections.size()); + setProgress(0); + for (OSMIntersectionInfo next : intersections) { + next.process(sizeOf1m); + bumpProgress(); + } + setStatus("Created " + roads.size() + " roads, " + intersections.size() + " intersections, " + buildings.size() + " buildings"); + map.setOSMInfo(intersections, roads, buildings); + } + + private void scanRoads() { + OSMMap osm = map.getOSMMap(); + for (OSMRoad road : osm.getRoads()) { + Iterator it = road.getNodeIDs().iterator(); + OSMNode start = osm.getNode(it.next()); + while (it.hasNext()) { + OSMNode end = osm.getNode(it.next()); + if (start == end) { + System.out.println("Degenerate road: " + road.getID()); + continue; + } + OSMIntersectionInfo from = nodeToIntersection.get(start); + OSMIntersectionInfo to = nodeToIntersection.get(end); + if (from == null) { + from = new OSMIntersectionInfo(start); + nodeToIntersection.put(start, from); + intersections.add(from); + } + if (to == null) { + to = new OSMIntersectionInfo(end); + nodeToIntersection.put(end, to); + intersections.add(to); + } + OSMRoadInfo roadInfo = new OSMRoadInfo(start, end); + from.addRoadSegment(roadInfo); + to.addRoadSegment(roadInfo); + start = end; + roads.add(roadInfo); + } + bumpProgress(); + } + } + + private void scanBuildings() { + OSMMap osm = map.getOSMMap(); + for (OSMBuilding building : osm.getBuildings()) { + buildings.add(new OSMBuildingInfo(building, osm)); + bumpProgress(); + } + } +} diff --git a/modules/maps/src/maps/convert/osm2gml/SplitIntersectingEdgesStep.java b/modules/maps/src/maps/convert/osm2gml/SplitIntersectingEdgesStep.java new file mode 100644 index 0000000000000000000000000000000000000000..720f7aba3b04fa6cc9c44f07cb5014fb742132cf --- /dev/null +++ b/modules/maps/src/maps/convert/osm2gml/SplitIntersectingEdgesStep.java @@ -0,0 +1,283 @@ +package maps.convert.osm2gml; + +import java.util.List; +import java.util.Deque; +import java.util.ArrayDeque; +import java.util.Set; +import java.util.HashSet; + +import rescuecore2.misc.geometry.Point2D; +import rescuecore2.misc.geometry.Line2D; +import rescuecore2.misc.geometry.GeometryTools2D; +//import rescuecore2.log.Logger; + +import maps.convert.ConvertStep; + +/** + This step splits any edges that intersect. +*/ +public class SplitIntersectingEdgesStep extends ConvertStep { + private TemporaryMap map; + private int splitCount; + private int inspectedCount; + private Deque toCheck; + private Set seen; + + /** + Construct a SplitIntersectingEdgesStep. + @param map The TemporaryMap to use. + */ + public SplitIntersectingEdgesStep(TemporaryMap map) { + this.map = map; + } + + @Override + public String getDescription() { + return "Splitting intersecting edges"; + } + + @Override + protected void step() { + debug.setBackground(ConvertTools.getAllDebugShapes(map)); + toCheck = new ArrayDeque(map.getAllEdges()); + seen = new HashSet(); + setProgressLimit(toCheck.size()); + splitCount = 0; + inspectedCount = 0; + while (!toCheck.isEmpty()) { + Edge next = toCheck.pop(); + check(next); + ++inspectedCount; + setProgressLimit(toCheck.size() + inspectedCount); + bumpProgress(); + } + setStatus("Inspected " + inspectedCount + " edges and split " + splitCount); + } + + private void check(Edge e) { + if (!map.getAllEdges().contains(e)) { + // Logger.debug("Skipped edge " + e); + // debug.show("Skipped edge", new EdgeShapeInfo(e, "Skipped edge", Color.BLUE, true, false)); + return; + } + if (seen.contains(e)) { + return; + } + seen.add(e); + Line2D l1 = e.getLine(); + Set edges = new HashSet(map.getAllEdges()); + for (Edge test : edges) { + if (test.equals(e)) { + continue; + } + Line2D l2 = test.getLine(); + if (GeometryTools2D.parallel(l1, l2)) { + if (processParallelLines(e, test)) { + break; + } + } + else { + if (checkForIntersection(e, test)) { + break; + } + } + } + } + + /** + @return True if e1 was split. + */ + private boolean processParallelLines(Edge e1, Edge e2) { + // Possible cases: + // Shorter line entirely inside longer + // Shorter line overlaps longer at longer start + // Shorter line overlaps longer at longer end + // Shorter line start point is same as longer start and end point is inside + // Shorter line start point is same as longer end and end point is inside + // Shorter line end point is same as longer start and start point is inside + // Shorter line end point is same as longer end and start point is inside + Edge shorterEdge = e1; + Edge longerEdge = e2; + if (e1.getLine().getDirection().getLength() > e2.getLine().getDirection().getLength()) { + shorterEdge = e2; + longerEdge = e1; + } + Line2D shorter = shorterEdge.getLine(); + Line2D longer = longerEdge.getLine(); + boolean shortStartLongStart = shorterEdge.getStart() == longerEdge.getStart(); + boolean shortStartLongEnd = shorterEdge.getStart() == longerEdge.getEnd(); + boolean shortEndLongStart = shorterEdge.getEnd() == longerEdge.getStart(); + boolean shortEndLongEnd = shorterEdge.getEnd() == longerEdge.getEnd(); + boolean startInside = !shortStartLongStart && !shortStartLongEnd && GeometryTools2D.contains(longer, shorter.getOrigin()); + boolean endInside = !shortEndLongStart && !shortEndLongEnd && GeometryTools2D.contains(longer, shorter.getEndPoint()); + /* + if (startInside || endInside) { + ++overlapCount; + } + */ + if (startInside && endInside) { + processInternalEdge(shorterEdge, longerEdge); + return longerEdge == e1; + } + else if (startInside && !endInside) { + // Either full overlap or coincident end point + if (shortEndLongStart) { + processCoincidentNode(shorterEdge, longerEdge, shorterEdge.getEnd()); + return longerEdge == e1; + } + else if (shortEndLongEnd) { + processCoincidentNode(shorterEdge, longerEdge, shorterEdge.getEnd()); + return longerEdge == e1; + } + else { + // Full overlap + processOverlap(shorterEdge, longerEdge); + return true; + } + } + else if (endInside && !startInside) { + // Either full overlap or coincident end point + if (shortStartLongStart) { + processCoincidentNode(shorterEdge, longerEdge, shorterEdge.getStart()); + return longerEdge == e1; + } + else if (shortStartLongEnd) { + processCoincidentNode(shorterEdge, longerEdge, shorterEdge.getStart()); + return longerEdge == e1; + } + else { + // Full overlap + processOverlap(shorterEdge, longerEdge); + return true; + } + } + return false; + } + + /** + @return true if first is split. + */ + private boolean checkForIntersection(Edge first, Edge second) { + Point2D intersection = GeometryTools2D.getSegmentIntersectionPoint(first.getLine(), second.getLine()); + // System.out.println(intersection); + if (intersection == null) { + // Maybe the intersection is within the map's "nearby" tolerance? + intersection = GeometryTools2D.getIntersectionPoint(first.getLine(), second.getLine()); + /* + debug.show("Split intersection", + new ShapeDebugFrame.Line2DShapeInfo(first, "Line 1", Color.ORANGE, true, false), + new ShapeDebugFrame.Line2DShapeInfo(second, "Line 2", Color.BLUE, true, false), + new ShapeDebugFrame.Point2DShapeInfo(intersection, "Near-miss intersection", Color.BLACK, false), + new ShapeDebugFrame.Line2DShapeInfo(firstObject.getLines(), "Object 1", Color.GREEN, false, false), + new ShapeDebugFrame.Line2DShapeInfo(secondObject.getLines(), "Object 2", Color.GRAY, false, false) + ); + */ + // Was this a near miss? + if (map.isNear(intersection, first.getStart().getCoordinates()) || map.isNear(intersection, first.getEnd().getCoordinates())) { + // Check that the intersection is actually somewhere on the second segment + double d = second.getLine().getIntersection(first.getLine()); + if (d < 0 || d > 1) { + // Nope. Ignore it. + return false; + } + } + else if (map.isNear(intersection, second.getStart().getCoordinates()) || map.isNear(intersection, second.getEnd().getCoordinates())) { + // Check that the intersection is actually somewhere on the first line segment + double d = first.getLine().getIntersection(second.getLine()); + if (d < 0 || d > 1) { + // Nope. Ignore it. + return false; + } + } + else { + // Not a near miss. + return false; + } + } + Node n = map.getNode(intersection); + // Split the two edges into 4 (maybe) + // Was the first edge split? + boolean splitFirst = !n.equals(first.getStart()) && !n.equals(first.getEnd()); + boolean splitSecond = !n.equals(second.getStart()) && !n.equals(second.getEnd()); + Set newEdges = new HashSet(); + if (splitFirst) { + List e = map.splitEdge(first, n); + toCheck.addAll(e); + newEdges.addAll(e); + ++splitCount; + /* + Logger.debug("Split edge " + first); + debug.show("Split first line", + new ShapeDebugFrame.Line2DShapeInfo(first.getLine(), "Line 1", Color.ORANGE, true, false), + new EdgeShapeInfo(e, "New edges", Color.WHITE, false, true), + new ShapeDebugFrame.Point2DShapeInfo(intersection, "Intersection", Color.BLACK, true) + ); + */ + } + if (splitSecond) { + List e = map.splitEdge(second, n); + toCheck.addAll(e); + newEdges.addAll(e); + ++splitCount; + /* + Logger.debug("Split edge " + second); + debug.show("Split second line", + new ShapeDebugFrame.Line2DShapeInfo(second.getLine(), "Line 2", Color.BLUE, true, false), + new EdgeShapeInfo(e, "New edges", Color.WHITE, false, true), + new ShapeDebugFrame.Point2DShapeInfo(intersection, "Intersection", Color.BLACK, true) + ); + */ + } + // if (splitFirst || splitSecond) { + /* + Logger.debug("First line: " + first + " -> " + first.getLine()); + Logger.debug("Second line: " + second + " -> " + second.getLine()); + Logger.debug("Intersection: " + intersection); + Logger.debug("New edges"); + for (Edge next : newEdges) { + Logger.debug(" " + next + " -> " + next.getLine()); + } + */ + // debug.show("Split intersection", + // new ShapeDebugFrame.Line2DShapeInfo(first.getLine(), "Line 1", Color.ORANGE, true, false), + // new ShapeDebugFrame.Line2DShapeInfo(second.getLine(), "Line 2", Color.BLUE, true, false), + // new EdgeShapeInfo(newEdges, "New edges", Color.WHITE, false, true), + // new ShapeDebugFrame.Point2DShapeInfo(intersection, "Intersection", Color.BLACK, true) + // ); + // } + return splitFirst; + } + + private void processInternalEdge(Edge shorter, Edge longer) { + // Split longer into (up to) three chunks + double t1 = GeometryTools2D.positionOnLine(longer.getLine(), shorter.getLine().getOrigin()); + double t2 = GeometryTools2D.positionOnLine(longer.getLine(), shorter.getLine().getEndPoint()); + Node first; + Node second; + if (t1 < t2) { + first = shorter.getStart(); + second = shorter.getEnd(); + } + else { + first = shorter.getEnd(); + second = shorter.getStart(); + } + toCheck.addAll(map.splitEdge(longer, first, second)); + ++splitCount; + } + + private void processCoincidentNode(Edge shorter, Edge longer, Node coincidentPoint) { + // Split the long edge at the non-coincident point + Node cutPoint = coincidentPoint.equals(shorter.getStart()) ? shorter.getEnd() : shorter.getStart(); + toCheck.addAll(map.splitEdge(longer, cutPoint)); + ++splitCount; + } + + private void processOverlap(Edge shorter, Edge longer) { + Node shortSplit = GeometryTools2D.contains(shorter.getLine(), longer.getLine().getOrigin()) ? longer.getStart() : longer.getEnd(); + Node longSplit = GeometryTools2D.contains(longer.getLine(), shorter.getLine().getOrigin()) ? shorter.getStart() : shorter.getEnd(); + toCheck.addAll(map.splitEdge(shorter, shortSplit)); + toCheck.addAll(map.splitEdge(longer, longSplit)); + ++splitCount; + } +} diff --git a/modules/maps/src/maps/convert/osm2gml/SplitShapesStep.java b/modules/maps/src/maps/convert/osm2gml/SplitShapesStep.java new file mode 100644 index 0000000000000000000000000000000000000000..e03d014bf1b80c3bae8d355e50d52cb5f5ff96bb --- /dev/null +++ b/modules/maps/src/maps/convert/osm2gml/SplitShapesStep.java @@ -0,0 +1,115 @@ +package maps.convert.osm2gml; + +import java.util.List; +import java.util.ArrayList; +import java.util.Set; +import java.util.HashSet; +import java.util.Collection; + +import maps.convert.ConvertStep; + +import rescuecore2.log.Logger; + +/** + This step splits any shapes that overlap. +*/ +public class SplitShapesStep extends ConvertStep { + private TemporaryMap map; + + /** + Construct a SplitFacesStep. + @param map The map to use. + */ + public SplitShapesStep(TemporaryMap map) { + this.map = map; + } + + @Override + public String getDescription() { + return "Splitting overlapping shapes"; + } + + @Override + protected void step() { + Collection all = map.getAllObjects(); + setProgressLimit(all.size()); + int count = 0; + debug.setBackground(ConvertTools.getAllDebugShapes(map)); + for (TemporaryObject shape : all) { + count += splitShapeIfRequired(shape); + bumpProgress(); + } + setStatus("Added " + count + " new shapes"); + } + + private int splitShapeIfRequired(TemporaryObject shape) { + Set edgesRemaining = new HashSet(shape.getEdges()); + boolean firstShape = true; + int newShapeCount = 0; + // debug.show("Splitting shapes", new TemporaryObjectInfo(shape, "Shape", Constants.BLACK, Constants.TRANSPARENT_ORANGE)); + // Logger.debug("Splitting shape " + shape); + // Logger.debug("Edges: "); + // for (DirectedEdge e : edgesRemaining) { + // Logger.debug(" " + e); + // } + while (!edgesRemaining.isEmpty()) { + // Logger.debug(edgesRemaining.size() + " edges remaining"); + DirectedEdge dEdge = edgesRemaining.iterator().next(); + edgesRemaining.remove(dEdge); + Node start = dEdge.getStartNode(); + Node end = dEdge.getEndNode(); + List result = new ArrayList(); + result.add(dEdge); + // Now walk around + Logger.debug("Starting walk from " + dEdge); + Logger.debug("Start: " + start); + Logger.debug("End: " + end); + while (!end.equals(start)) { + Set candidates = map.getAttachedEdges(end); + // Logger.debug("From edge: " + dEdge); + // Logger.debug("Candidates: "); + // for (Edge e : candidates) { + // Logger.debug(" " + e); + // } + Edge turn = ConvertTools.findLeftTurn(dEdge, candidates); + Logger.debug("Best turn: " + turn); + DirectedEdge newDEdge = new DirectedEdge(turn, end); + // debug.show("Splitting shapes", + // new TemporaryObjectInfo(shape, "Shape", Constants.BLACK, Constants.TRANSPARENT_ORANGE), + // new ShapeDebugFrame.Line2DShapeInfo(dEdge.getLine(), "Edge", Constants.BLUE, true, true), + // new ShapeDebugFrame.Line2DShapeInfo(newDEdge.getLine(), "Turn", Constants.RED, true, true)); + dEdge = newDEdge; + end = dEdge.getEndNode(); + edgesRemaining.remove(dEdge); + result.add(dEdge); + Logger.debug("Added " + dEdge); + Logger.debug("New end: " + end); + } + if (!firstShape || !edgesRemaining.isEmpty()) { + // Didn't cover all edges so new shapes are needed. + if (firstShape) { + map.removeTemporaryObject(shape); + firstShape = false; + } + else { + ++newShapeCount; + } + TemporaryObject newObject = null; + if (shape instanceof TemporaryRoad) { + newObject = new TemporaryRoad(result); + } + if (shape instanceof TemporaryIntersection) { + newObject = new TemporaryIntersection(result); + } + if (shape instanceof TemporaryBuilding) { + newObject = new TemporaryBuilding(result, ((TemporaryBuilding)shape).getBuildingID()); + } + map.addTemporaryObject(newObject); + // debug.show("Splitting shapes", + // new TemporaryObjectInfo(shape, "Original shape", Constants.BLACK, null), + // new TemporaryObjectInfo(newObject, "New shape", Constants.RED, Constants.TRANSPARENT_RED)); + } + } + return newShapeCount; + } +} diff --git a/modules/maps/src/maps/convert/osm2gml/TemporaryBuilding.java b/modules/maps/src/maps/convert/osm2gml/TemporaryBuilding.java new file mode 100644 index 0000000000000000000000000000000000000000..f3b59738a7df71bdbec5bed0edf30a39f7fe041b --- /dev/null +++ b/modules/maps/src/maps/convert/osm2gml/TemporaryBuilding.java @@ -0,0 +1,28 @@ +package maps.convert.osm2gml; + +import java.util.List; + +/** + A temporary building during conversion. +*/ +public class TemporaryBuilding extends TemporaryObject { + private long id; + + /** + Construct a new TemporaryBuilding. + @param edges The edges of the building in counter-clockwise order. + @param id The ID of the OSM building that generated this data. + */ + public TemporaryBuilding(List edges, long id) { + super(edges); + this.id = id; + } + + /** + Get the ID of the original OSM building. + @return The OSM building ID. + */ + public long getBuildingID() { + return id; + } +} diff --git a/modules/maps/src/maps/convert/osm2gml/TemporaryIntersection.java b/modules/maps/src/maps/convert/osm2gml/TemporaryIntersection.java new file mode 100644 index 0000000000000000000000000000000000000000..c4839a1ed80427e49022b02224058f00dc30f404 --- /dev/null +++ b/modules/maps/src/maps/convert/osm2gml/TemporaryIntersection.java @@ -0,0 +1,16 @@ +package maps.convert.osm2gml; + +import java.util.List; + +/** + A temporary intersection during conversion. +*/ +public class TemporaryIntersection extends TemporaryObject { + /** + Construct a new TemporaryIntersection. + @param edges The edges of the intersection in counter-clockwise order. + */ + public TemporaryIntersection(List edges) { + super(edges); + } +} diff --git a/modules/maps/src/maps/convert/osm2gml/TemporaryMap.java b/modules/maps/src/maps/convert/osm2gml/TemporaryMap.java new file mode 100644 index 0000000000000000000000000000000000000000..307b4d3bbf06f96271f3315be3ebed6eece2bb5d --- /dev/null +++ b/modules/maps/src/maps/convert/osm2gml/TemporaryMap.java @@ -0,0 +1,444 @@ +package maps.convert.osm2gml; + +import java.util.Set; +import java.util.Map; +import java.util.HashSet; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.ArrayList; + +import maps.osm.OSMMap; + +import rescuecore2.misc.geometry.Point2D; +import rescuecore2.misc.collections.LazyMap; +//import rescuecore2.log.Logger; + +/** + This class holds all temporary information during map conversion. +*/ +public class TemporaryMap { + /** + The threshold for determining if nodes are co-located in metres. + */ + private static final double NEARBY_THRESHOLD_M = 1; + + private double threshold; + + private Set nodes; + private Set edges; + private Map> edgesAtNode; + private Map> objectsAtEdge; + private Set tempRoads; + private Set tempIntersections; + private Set tempBuildings; + private Set allObjects; + + private OSMMap osmMap; + private Collection osmIntersections; + private Collection osmRoads; + private Collection osmBuildings; + + private int nextID; + + /** + Construct a TemporaryMap. + @param osmMap The OpenStreetMap data this map is generated from. + */ + public TemporaryMap(OSMMap osmMap) { + this.osmMap = osmMap; + nextID = 0; + nodes = new HashSet(); + edges = new HashSet(); + threshold = ConvertTools.nearbyThreshold(osmMap, NEARBY_THRESHOLD_M); + tempRoads = new HashSet(); + tempIntersections = new HashSet(); + tempBuildings = new HashSet(); + allObjects = new HashSet(); + edgesAtNode = new LazyMap>() { + @Override + public Set createValue() { + return new HashSet(); + } + }; + objectsAtEdge = new LazyMap>() { + @Override + public Set createValue() { + return new HashSet(); + } + }; + } + + /** + Get the OSMMap. + @return The OSMMap. + */ + public OSMMap getOSMMap() { + return osmMap; + } + + /** + Set the OSMMap information. + @param intersections The set of intersections. + @param roads The set of roads. + @param buildings The set of buildings. + */ + public void setOSMInfo(Collection intersections, Collection roads, Collection buildings) { + osmIntersections = new HashSet(intersections); + osmRoads = new HashSet(roads); + osmBuildings = new HashSet(buildings); + } + + /** + Get the OSM intersection info. + @return The OSM intersection info. + */ + public Collection getOSMIntersectionInfo() { + return Collections.unmodifiableCollection(osmIntersections); + } + + /** + Get the OSM road info. + @return The OSM road info. + */ + public Collection getOSMRoadInfo() { + return Collections.unmodifiableCollection(osmRoads); + } + + /** + Get the OSM building info. + @return The OSM building info. + */ + public Collection getOSMBuildingInfo() { + return Collections.unmodifiableCollection(osmBuildings); + } + + /** + Add a road. + @param road The road to add. + */ + public void addRoad(TemporaryRoad road) { + tempRoads.add(road); + addObject(road); + } + + /** + Remove a road. + @param road The road to remove. + */ + public void removeRoad(TemporaryRoad road) { + tempRoads.remove(road); + removeObject(road); + } + + /** + Add an intersection. + @param intersection The intersection to add. + */ + public void addIntersection(TemporaryIntersection intersection) { + tempIntersections.add(intersection); + addObject(intersection); + } + + /** + Remove an intersection. + @param intersection The intersection to remove. + */ + public void removeIntersection(TemporaryIntersection intersection) { + tempIntersections.remove(intersection); + removeObject(intersection); + } + + /** + Add a building. + @param building The building to add. + */ + public void addBuilding(TemporaryBuilding building) { + tempBuildings.add(building); + addObject(building); + } + + /** + Remove a building. + @param building The building to remove. + */ + public void removeBuilding(TemporaryBuilding building) { + tempBuildings.remove(building); + removeObject(building); + } + + /** + Add an object. + @param object The object to add. + */ + public void addTemporaryObject(TemporaryObject object) { + if (object instanceof TemporaryRoad) { + addRoad((TemporaryRoad)object); + } + if (object instanceof TemporaryIntersection) { + addIntersection((TemporaryIntersection)object); + } + if (object instanceof TemporaryBuilding) { + addBuilding((TemporaryBuilding)object); + } + } + + /** + Remove an object. + @param object The object to remove. + */ + public void removeTemporaryObject(TemporaryObject object) { + if (object instanceof TemporaryRoad) { + removeRoad((TemporaryRoad)object); + } + if (object instanceof TemporaryIntersection) { + removeIntersection((TemporaryIntersection)object); + } + if (object instanceof TemporaryBuilding) { + removeBuilding((TemporaryBuilding)object); + } + } + + /** + Get all roads in the map. + @return All roads. + */ + public Collection getRoads() { + return new HashSet(tempRoads); + } + + /** + Get all intersections in the map. + @return All intersections. + */ + public Collection getIntersections() { + return new HashSet(tempIntersections); + } + + /** + Get all buildings in the map. + @return All buildings. + */ + public Collection getBuildings() { + return new HashSet(tempBuildings); + } + + /** + Get all objects in the map. + @return All objects. + */ + public Collection getAllObjects() { + return new HashSet(allObjects); + } + + /** + Get all nodes in the map. + @return All nodes. + */ + public Collection getAllNodes() { + return new HashSet(nodes); + } + + /** + Get all edges in the map. + @return All edges. + */ + public Collection getAllEdges() { + return new HashSet(edges); + } + + /** + Get all objects attached to an Edge. + @param e The Edge. + @return All attached TemporaryObjects. + */ + public Set getAttachedObjects(Edge e) { + return new HashSet(objectsAtEdge.get(e)); + } + + /** + Get all edges attached to a Node. + @param n The Node. + @return All attached edges. + */ + public Set getAttachedEdges(Node n) { + return new HashSet(edgesAtNode.get(n)); + } + + /** + Set the threshold for deciding if two points are the same. The {@link #isNear(Point2D, Point2D)} method uses this value to check if a new point needs to be registered. + @param t The new threshold. + */ + public void setNearbyThreshold(double t) { + threshold = t; + } + + /** + Get the threshold for deciding if two points are the same. The {@link #isNear(Point2D, Point2D)} method uses this value to check if a new point needs to be registered. + @return The nearby threshold. + */ + public double getNearbyThreshold() { + return threshold; + } + + /** + Find out if two points are nearby. + @param point1 The first point. + @param point2 The second point. + @return True iff the two points are within the nearby threshold. + */ + public boolean isNear(Point2D point1, Point2D point2) { + return isNear(point1.getX(), point1.getY(), point2.getX(), point2.getY()); + } + + /** + Find out if two points are nearby. + @param x1 The x coordinate of the first point. + @param y1 The y coordinate of the first point. + @param x2 The x coordinate of the second point. + @param y2 The y coordinate of the second point. + @return True iff the two points are within the nearby threshold. + */ + public boolean isNear(double x1, double y1, double x2, double y2) { + double dx = x2 - x1; + double dy = y2 - y1; + return (dx >= -threshold + && dx <= threshold + && dy >= -threshold + && dy <= threshold); + } + + /** + Get a Node near a point. If a Node already exists nearby then it will be returned, otherwise a new Node will be created. + @param p The node coordinates. + @return A Node. + */ + public Node getNode(Point2D p) { + return getNode(p.getX(), p.getY()); + } + + /** + Get a Node near a point. If a Node already exists nearby then it will be returned, otherwise a new Node will be created. + @param x The X coordinate. + @param y The Y coordinate. + @return A Node. + */ + public Node getNode(double x, double y) { + for (Node next : nodes) { + if (isNear(x, y, next.getX(), next.getY())) { + return next; + } + } + return createNode(x, y); + } + + /** + Get an Edge between two nodes. This will return either a new Edge or a shared instance if one already exists. + @param from The from node. + @param to The to node. + @return An Edge. + */ + public Edge getEdge(Node from, Node to) { + for (Edge next : edges) { + if (next.getStart().equals(from) && next.getEnd().equals(to) + || next.getStart().equals(to) && next.getEnd().equals(from)) { + return next; + } + } + return createEdge(from, to); + } + + /** + Get a DirectedEdge between two nodes. + @param from The from node. + @param to The to node. + @return A new DirectedEdge. + */ + public DirectedEdge getDirectedEdge(Node from, Node to) { + Edge e = getEdge(from, to); + return new DirectedEdge(e, from); + } + + /** + Replace an existing edge with a set of new edges. + @param edge The old edge. + @param newEdges The new edges. + */ + public void replaceEdge(Edge edge, Collection newEdges) { + for (TemporaryObject next : getAttachedObjects(edge)) { + next.replaceEdge(edge, newEdges); + for (Edge nextEdge : newEdges) { + objectsAtEdge.get(nextEdge).add(next); + } + } + removeEdge(edge); + } + + /** + Split an edge into chunks. + @param edge The edge to split. + @param splitPoints The points to split the line. These must be in order along the line. + @return The replacement edges. + */ + public List splitEdge(Edge edge, Node... splitPoints) { + List replacements = new ArrayList(); + Edge current = edge; + for (Node n : splitPoints) { + if (n.equals(current.getStart()) || n.equals(current.getEnd())) { + // Don't bother if the split point is at the origin or endpoint + continue; + } + replacements.add(getEdge(current.getStart(), n)); + current = getEdge(n, current.getEnd()); + } + if (!edge.equals(current)) { + replacements.add(current); + } + if (!replacements.isEmpty()) { + replaceEdge(edge, replacements); + } + return replacements; + } + + private Node createNode(double x, double y) { + Node result = new Node(nextID++, x, y); + nodes.add(result); + return result; + } + + private Edge createEdge(Node from, Node to) { + Edge result = new Edge(nextID++, from, to); + edges.add(result); + edgesAtNode.get(from).add(result); + edgesAtNode.get(to).add(result); + // Logger.debug("Created edge " + result); + return result; + } + + private void addObject(TemporaryObject object) { + allObjects.add(object); + for (DirectedEdge next : object.getEdges()) { + objectsAtEdge.get(next.getEdge()).add(object); + } + } + + private void removeNode(Node n) { + nodes.remove(n); + edgesAtNode.remove(n); + } + + private void removeEdge(Edge e) { + edges.remove(e); + edgesAtNode.get(e.getStart()).remove(e); + edgesAtNode.get(e.getEnd()).remove(e); + objectsAtEdge.remove(e); + // Logger.debug("Removed edge " + e); + } + + private void removeObject(TemporaryObject object) { + allObjects.remove(object); + for (DirectedEdge next : object.getEdges()) { + objectsAtEdge.get(next.getEdge()).remove(object); + } + } +} diff --git a/modules/maps/src/maps/convert/osm2gml/TemporaryObject.java b/modules/maps/src/maps/convert/osm2gml/TemporaryObject.java new file mode 100644 index 0000000000000000000000000000000000000000..307a43ed765e23887c8fbf7de89e3d7fc8ff5f7f --- /dev/null +++ b/modules/maps/src/maps/convert/osm2gml/TemporaryObject.java @@ -0,0 +1,246 @@ +package maps.convert.osm2gml; + +import java.util.List; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.ListIterator; +import java.util.Collection; +import java.util.Collections; +import java.util.Set; +import java.util.HashSet; +import java.util.Map; +import java.util.HashMap; + +import java.awt.geom.Rectangle2D; +import java.awt.geom.Path2D; +import java.awt.geom.Area; +import java.awt.Shape; + +import rescuecore2.misc.geometry.Point2D; +//import rescuecore2.log.Logger; + +import maps.gml.GMLCoordinates; +import maps.gml.GMLTools; + +/** + Abstract base class for temporary data structures during conversion. +*/ +public abstract class TemporaryObject { + private List edges; + private Map neighbours; + private Path2D path; + private Rectangle2D bounds; + + /** + Construct a new TemporaryObject. + @param edges The edges of the object in counter-clockwise order. + */ + protected TemporaryObject(List edges) { + this.edges = new ArrayList(edges); + neighbours = new HashMap(); + } + + /** + Get the edges of this object. + @return The edges. + */ + public List getEdges() { + return Collections.unmodifiableList(edges); + } + + /** + Get the neighbour through a particular edge. + @param edge The edge to look up. + @return The neighbour through that edge or null. + */ + public TemporaryObject getNeighbour(DirectedEdge edge) { + return neighbours.get(edge); + } + + /** + Set the neighbour through a particular edge. + @param edge The edge to set the neighbour of. + @param neighbour The new neighbour for that edge. + */ + public void setNeighbour(DirectedEdge edge, TemporaryObject neighbour) { + neighbours.put(edge, neighbour); + } + + /** + Set the neighbour through a particular edge. + @param edge The edge to set the neighbour of. + @param neighbour The new neighbour for that edge. + */ + public void setNeighbour(Edge edge, TemporaryObject neighbour) { + neighbours.put(findDirectedEdge(edge), neighbour); + } + + /** + Turn the edges into a list of coordinates. + @return A list of GMLCoordinates. + */ + public List makeGMLCoordinates() { + List result = new ArrayList(); + for (DirectedEdge next : edges) { + Point2D p = next.getStartCoordinates(); + result.add(new GMLCoordinates(p.getX(), p.getY())); + } + return result; + } + + /** + Get the bounds of this object. + @return The bounds. + */ + public Rectangle2D getBounds() { + if (bounds == null) { + bounds = GMLTools.getBounds(makeGMLCoordinates()); + } + return bounds; + } + + /** + Get the Shape of this object. + @return The shape. + */ + public Shape getShape() { + if (path == null) { + path = new Path2D.Double(); + Iterator it = edges.iterator(); + DirectedEdge d = it.next(); + path.moveTo(d.getStartCoordinates().getX(), d.getStartCoordinates().getY()); + path.lineTo(d.getEndCoordinates().getX(), d.getEndCoordinates().getY()); + while (it.hasNext()) { + d = it.next(); + path.lineTo(d.getEndCoordinates().getX(), d.getEndCoordinates().getY()); + } + } + return path; + } + + /** + Check if this object is a duplicate of another. Objects are duplicates if they contain the same list of directed edges, possibly offset. + @param other The other object to check against. + @return True if this object is a duplicate of other, false otherwise. + */ + public boolean isDuplicate(TemporaryObject other) { + List myEdges = getEdges(); + List otherEdges = other.getEdges(); + if (myEdges.size() != otherEdges.size()) { + return false; + } + Iterator it = myEdges.iterator(); + DirectedEdge start = it.next(); + // See if we can find an equivalent edge in other + Iterator ix = otherEdges.iterator(); + DirectedEdge otherStart = null; + while (ix.hasNext()) { + DirectedEdge test = ix.next(); + if (test.equals(start)) { + // Found! + otherStart = test; + break; + } + } + if (otherStart == null) { + // Edge not found in other so can't be a duplicate + return false; + } + // Check that edges are equivalent + // Walk through the edge lists starting at the beginning for me and at the equivalent edge in other. When we reach the end of other go back to the start. + while (ix.hasNext()) { + DirectedEdge a = it.next(); + DirectedEdge b = ix.next(); + if (!a.equals(b)) { + return false; + } + } + ix = otherEdges.iterator(); + while (it.hasNext()) { + DirectedEdge a = it.next(); + DirectedEdge b = ix.next(); + if (!a.equals(b)) { + return false; + } + } + return true; + } + + /** + Check if this object is a entirely inside another. + @param other The other object to check against. + @return True if this object is entirely inside the other, false otherwise. + */ + public boolean isEntirelyInside(TemporaryObject other) { + if (!this.getBounds().intersects(other.getBounds())) { + return false; + } + Area a = new Area(getShape()); + Area b = new Area(other.getShape()); + Area intersection = new Area(a); + intersection.intersect(b); + return a.equals(intersection); + } + + /** + Replace an edge with a set of replacement edges. + @param edge The edge to replace. + @param replacements The set of replacement edges. These can be in any order. + */ + protected void replaceEdge(Edge edge, Collection replacements) { + // Logger.debug(this + " replacing edge " + edge + " with " + replacements); + // Logger.debug("Old edge list: " + edges); + if (replacements.isEmpty()) { + // Just remove the edge + for (Iterator it = edges.iterator(); it.hasNext();) { + DirectedEdge next = it.next(); + if (next.getEdge().equals(edge)) { + it.remove(); + } + } + } + else { + for (ListIterator it = edges.listIterator(); it.hasNext();) { + DirectedEdge next = it.next(); + if (next.getEdge().equals(edge)) { + it.remove(); + Set replacementsSet = new HashSet(replacements); + // Create directed edges for the replacements + Node start = next.getStartNode(); + Node end = next.getEndNode(); + while (!start.equals(end)) { + DirectedEdge newEdge = findNewEdge(start, replacementsSet); + replacementsSet.remove(newEdge.getEdge()); + it.add(newEdge); + start = newEdge.getEndNode(); + } + break; + } + } + } + // Logger.debug("New edge list: " + edges); + bounds = null; + path = null; + } + + private DirectedEdge findNewEdge(Node from, Set candidates) { + for (Edge next : candidates) { + if (next.getStart().equals(from)) { + return new DirectedEdge(next, true); + } + if (next.getEnd().equals(from)) { + return new DirectedEdge(next, false); + } + } + return null; + } + + private DirectedEdge findDirectedEdge(Edge e) { + for (DirectedEdge next : edges) { + if (next.getEdge().equals(e)) { + return next; + } + } + throw new IllegalArgumentException("Edge " + e + " not found"); + } +} diff --git a/modules/maps/src/maps/convert/osm2gml/TemporaryObjectInfo.java b/modules/maps/src/maps/convert/osm2gml/TemporaryObjectInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..961aca090ad7889e55d3a5cbf766bd5a0ac061b8 --- /dev/null +++ b/modules/maps/src/maps/convert/osm2gml/TemporaryObjectInfo.java @@ -0,0 +1,90 @@ +package maps.convert.osm2gml; + +import java.awt.Color; +import java.awt.Shape; +import java.awt.Polygon; +import java.awt.Graphics2D; +import java.awt.geom.Rectangle2D; + +import java.util.List; + +import rescuecore2.misc.gui.ScreenTransform; +import rescuecore2.misc.gui.ShapeDebugFrame; + +import maps.gml.GMLCoordinates; + +/** + A ShapeInfo that knows how to draw TemporaryObjects. +*/ +public class TemporaryObjectInfo extends ShapeDebugFrame.ShapeInfo { + private TemporaryObject shape; + private Color outlineColour; + private Color fillColour; + private Rectangle2D bounds; + + /** + Create a new TemporaryObjectInfo. + @param shape The shape to draw. + @param name The name of the shape. + @param outlineColour The colour to draw the outline of the shape. This may be null to indicate that the outline should not be painted. + @param fillColour The colour to draw the interior of the shape. This may be null to indicate that the interior should not be painted. + */ + public TemporaryObjectInfo(TemporaryObject shape, String name, Color outlineColour, Color fillColour) { + super(shape, name); + this.shape = shape; + this.outlineColour = outlineColour; + this.fillColour = fillColour; + if (shape != null) { + bounds = shape.getBounds(); + } + } + + @Override + public Shape paint(Graphics2D g, ScreenTransform transform) { + if (shape == null) { + return null; + } + List coordinates = shape.makeGMLCoordinates(); + int n = coordinates.size(); + int[] xs = new int[n]; + int[] ys = new int[n]; + int i = 0; + for (GMLCoordinates next : coordinates) { + xs[i] = transform.xToScreen(next.getX()); + ys[i] = transform.yToScreen(next.getY()); + ++i; + } + Polygon p = new Polygon(xs, ys, n); + if (fillColour != null) { + g.setColor(fillColour); + g.fill(p); + } + if (outlineColour != null) { + g.setColor(outlineColour); + g.draw(p); + } + return p; + } + + @Override + public void paintLegend(Graphics2D g, int width, int height) { + if (outlineColour != null) { + g.setColor(outlineColour); + g.drawRect(0, 0, width - 1, height - 1); + } + if (fillColour != null) { + g.setColor(fillColour); + g.fillRect(0, 0, width, height); + } + } + + @Override + public Rectangle2D getBoundsShape() { + return bounds; + } + + @Override + public java.awt.geom.Point2D getBoundsPoint() { + return null; + } +} diff --git a/modules/maps/src/maps/convert/osm2gml/TemporaryRoad.java b/modules/maps/src/maps/convert/osm2gml/TemporaryRoad.java new file mode 100644 index 0000000000000000000000000000000000000000..239136df96d89048f0eb2d9d66cdc19581a43eae --- /dev/null +++ b/modules/maps/src/maps/convert/osm2gml/TemporaryRoad.java @@ -0,0 +1,16 @@ +package maps.convert.osm2gml; + +import java.util.List; + +/** + A temporary road during conversion. +*/ +public class TemporaryRoad extends TemporaryObject { + /** + Construct a new TemporaryRoad. + @param edges The edges of the road in counter-clockwise order. + */ + public TemporaryRoad(List edges) { + super(edges); + } +} diff --git a/modules/maps/src/maps/convert/osm2gml/buildings/BuildingSpaceFiller.java b/modules/maps/src/maps/convert/osm2gml/buildings/BuildingSpaceFiller.java new file mode 100644 index 0000000000000000000000000000000000000000..af6ade4ed92243eaa20b23a7d6da1d20b57ab4f1 --- /dev/null +++ b/modules/maps/src/maps/convert/osm2gml/buildings/BuildingSpaceFiller.java @@ -0,0 +1,16 @@ +package maps.convert.osm2gml.buildings; + +import maps.gml.GMLShape; +import maps.gml.GMLMap; + +/** + Interface for objects that know how to fill spaces with buildings. +*/ +public interface BuildingSpaceFiller { + /** + Populate a space with buildings. + @param space The space to fill. + @param map The GMLMap to populate. + */ + void createBuildings(GMLShape space, GMLMap map); +} diff --git a/modules/maps/src/maps/convert/osm2gml/buildings/RowHousingBuildingSpaceFiller.java b/modules/maps/src/maps/convert/osm2gml/buildings/RowHousingBuildingSpaceFiller.java new file mode 100644 index 0000000000000000000000000000000000000000..e4b918582c292b6c5f6426c10fceaaa806b45470 --- /dev/null +++ b/modules/maps/src/maps/convert/osm2gml/buildings/RowHousingBuildingSpaceFiller.java @@ -0,0 +1,102 @@ +package maps.convert.osm2gml.buildings; + +import maps.gml.GMLShape; +import maps.gml.GMLMap; +import maps.gml.GMLNode; +import maps.gml.GMLDirectedEdge; +import maps.convert.osm2gml.buildings.row.RowFiller; +import maps.convert.osm2gml.buildings.row.RectangularDuplexRowFiller; + +import java.util.Random; +import java.util.Comparator; + +//import rescuecore2.misc.gui.ShapeDebugFrame; + +/** + A BuildingSpaceFiller that fills a space with row housing. +*/ +public class RowHousingBuildingSpaceFiller implements BuildingSpaceFiller { + // private ShapeDebugFrame debug; + private double sizeOf1m; + private Random random; + + /** + Construct a new RowHousingBuildingSpaceFiller. + @param sizeOf1m The size of 1m in GMLMap units. + @param random The random number generator to use. + */ + public RowHousingBuildingSpaceFiller(double sizeOf1m, Random random/*, ShapeDebugFrame debug*/) { + // this.debug = debug; + this.sizeOf1m = sizeOf1m; + this.random = random; + } + + @Override + public void createBuildings(GMLShape space, GMLMap map) { + // Sort the edges of the space by length + /* + List allEdges = space.getEdges(); + Collections.sort(allEdges, new EdgeLengthComparator()); + Set newFaces = new HashSet(); + RowFiller filler = createRandomFiller(); + for (GMLDirectedEdge next : allEdges) { + Set edgeFaces = filler.fillRow(next, map); + // debug.show("Next row faces", ConvertTools.createGMLDebugShapes(edgeFaces)); + // Remove new faces that overlap with existing ones + for (Iterator it = edgeFaces.iterator(); it.hasNext();) { + GMLFace newFace = it.next(); + boolean good = true; + for (GMLFace testFace : map.getFaces()) { + if (testFace == newFace) { + continue; + } + if (newFace.intersects(testFace)) { + good = false; + break; + } + } + if (good) { + newFaces.add(newFace); + } + else { + map.removeFace(newFace); + it.remove(); + } + } + // debug.show("Pruned next row faces", ConvertTools.createGMLDebugShapes(edgeFaces)); + } + debug.show("All new faces", ConvertTools.createGMLDebugShapes(newFaces)); + */ + } + + private RowFiller createRandomFiller() { + if (random.nextBoolean()) { + return RectangularDuplexRowFiller.makeWideFiller(sizeOf1m, random); + } + else { + return RectangularDuplexRowFiller.makeLongFiller(sizeOf1m, random); + } + } + + private static final class EdgeLengthComparator implements Comparator, java.io.Serializable { + public int compare(GMLDirectedEdge e1, GMLDirectedEdge e2) { + GMLNode start1 = e1.getStartNode(); + GMLNode end1 = e1.getEndNode(); + GMLNode start2 = e2.getStartNode(); + GMLNode end2 = e2.getEndNode(); + double dx1 = end1.getX() - start1.getX(); + double dy1 = end1.getY() - start1.getY(); + double dx2 = end2.getX() - start2.getX(); + double dy2 = end2.getY() - start2.getY(); + double l1 = Math.hypot(dx1, dy1); + double l2 = Math.hypot(dx2, dy2); + if (l1 < l2) { + return 1; + } + if (l2 < l1) { + return -1; + } + return 0; + } + } +} diff --git a/modules/maps/src/maps/convert/osm2gml/buildings/RowHousingType.java b/modules/maps/src/maps/convert/osm2gml/buildings/RowHousingType.java new file mode 100644 index 0000000000000000000000000000000000000000..a044e62222f9b472fae182ca1581c292adb4ca39 --- /dev/null +++ b/modules/maps/src/maps/convert/osm2gml/buildings/RowHousingType.java @@ -0,0 +1,12 @@ +package maps.convert.osm2gml.buildings; + +//CHECKSTYLE:OFF:JavadocType|JavadocVariable +public enum RowHousingType { + T_SHAPE_ROW, + T_SHAPE_DUPLEX, + RECTANGULAR_DUPLEX, // Done + THIN_DUPLEX, + SQUARE_DETACHED, + L_SHAPE_DETACHED, +//CHECKSTYLE:ON:JavadocType|JavadocVariable +} diff --git a/modules/maps/src/maps/convert/osm2gml/buildings/row/RectangularDuplexRowFiller.java b/modules/maps/src/maps/convert/osm2gml/buildings/row/RectangularDuplexRowFiller.java new file mode 100644 index 0000000000000000000000000000000000000000..0c21dc8145e35dc59b45399382fb9e72ed2b6917 --- /dev/null +++ b/modules/maps/src/maps/convert/osm2gml/buildings/row/RectangularDuplexRowFiller.java @@ -0,0 +1,156 @@ +package maps.convert.osm2gml.buildings.row; + +import java.util.Set; +import java.util.HashSet; +import java.util.Random; + +import maps.gml.GMLDirectedEdge; +import maps.gml.GMLShape; +import maps.gml.GMLMap; + +/** + A RowFiller that creates rectangular duplex units. +*/ +public final class RectangularDuplexRowFiller implements RowFiller { + private static final double WIDE_LOT_WIDTH_M = 25; + private static final double WIDE_BUILDING_WIDTH_M = 21; + private static final double WIDE_BUILDING_DEPTH_M = 7; + private static final double WIDE_MIN_OFFSET_M = 2; + private static final double WIDE_MAX_OFFSET_M = 10; + + private static final double LONG_LOT_WIDTH_M = 18; + private static final double LONG_BUILDING_WIDTH_M = 15; + private static final double LONG_BUILDING_DEPTH_M = 15; + private static final double LONG_MIN_OFFSET_M = 1; + private static final double LONG_MAX_OFFSET_M = 4; + + private static final int MIN_RUN_LENGTH = 1; + private static final int MAX_RUN_LENGTH = 5; + + private final double lotWidth; + private final double buildingWidth; + private final double buildingDepth; + private final double minOffset; + private final double maxOffset; + + private final Random random; + + private RectangularDuplexRowFiller(double sizeOf1m, Random random, double lotWidth, double buildingWidth, double buildingDepth, double minOffset, double maxOffset) { + this.lotWidth = lotWidth * sizeOf1m; + this.buildingWidth = buildingWidth * sizeOf1m; + this.buildingDepth = buildingDepth * sizeOf1m; + this.minOffset = minOffset * sizeOf1m; + this.maxOffset = maxOffset * sizeOf1m; + this.random = random; + } + + /** + Create a filler that creates wide buildings. + @param sizeOf1m The size of 1m in GML units. + @param random The random number generator to use. + @return A new RectangularDuplexRowFiller. + */ + public static RectangularDuplexRowFiller makeWideFiller(double sizeOf1m, Random random) { + return new RectangularDuplexRowFiller(sizeOf1m, random, WIDE_LOT_WIDTH_M, WIDE_BUILDING_WIDTH_M, WIDE_BUILDING_DEPTH_M, WIDE_MIN_OFFSET_M, WIDE_MAX_OFFSET_M); + } + + /** + Create a filler that creates longer, narrower buildings. + @param sizeOf1m The size of 1m in GML units. + @param random The random number generator to use. + @return A new RectangularDuplexRowFiller. + */ + public static RectangularDuplexRowFiller makeLongFiller(double sizeOf1m, Random random) { + return new RectangularDuplexRowFiller(sizeOf1m, random, LONG_LOT_WIDTH_M, LONG_BUILDING_WIDTH_M, LONG_BUILDING_DEPTH_M, LONG_MIN_OFFSET_M, LONG_MAX_OFFSET_M); + } + + @Override + public Set fillRow(GMLDirectedEdge edge, GMLMap map) { + Set result = new HashSet(); + /* + Line2D edgeLine = ConvertTools.gmlDirectedEdgeToLine(edge); + Vector2D normal = edgeLine.getDirection().getNormal().normalised(); + // Create lots along the edge + double edgeLength = edgeLine.getDirection().getLength(); + int lots = (int)(edgeLength / LOT_WIDTH); + double trueLotWidth = edgeLength / lots; + System.out.println("Creating " + lots + " lots"); + double offset = getRandomOffset(); + int runLength = getRandomRunLength(); + for (int i = 0; i < lots; ++i) { + if (runLength-- == 0) { + offset = getRandomOffset(); + runLength = getRandomRunLength(); + } + double d1 = i; + double d2 = i + 1; + d1 /= lots; + d2 /= lots; + Point2D topRight = edgeLine.getPoint(d1); + Point2D topLeft = edgeLine.getPoint(d2); + Set faces = createBuildingInLot(edgeLine, topRight, topLeft, normal, offset, map); + result.addAll(faces); + } + */ + return result; + } + + /* + private Set createBuildingInLot(Line2D edgeLine, Point2D lotTopRight, Point2D lotTopLeft, Vector2D edgeNormal, double depthOffset, GMLMap map) { + // Create the building by moving in from the sides of the lot boundary + Line2D topLine = new Line2D(lotTopRight, lotTopLeft); + double lotWidth = topLine.getDirection().getLength(); + double widthSlack = ((lotWidth - BUILDING_WIDTH) / lotWidth) / 2; + Point2D topRight = topLine.getPoint(widthSlack); + Point2D topMiddle = topLine.getPoint(0.5); + Point2D topLeft = topLine.getPoint(1.0 - widthSlack); + // Offset from the top of the boundary + topRight = topRight.plus(edgeNormal.scale(depthOffset)); + topMiddle = topMiddle.plus(edgeNormal.scale(depthOffset)); + topLeft = topLeft.plus(edgeNormal.scale(depthOffset)); + // Find the other end of the building + Point2D bottomRight = topRight.plus(edgeNormal.scale(BUILDING_DEPTH)); + Point2D bottomMiddle = topMiddle.plus(edgeNormal.scale(BUILDING_DEPTH)); + Point2D bottomLeft = topLeft.plus(edgeNormal.scale(BUILDING_DEPTH)); + // Create new nodes and directed edges for the building + GMLNode n1 = map.ensureNodeNear(topRight); + GMLNode n2 = map.ensureNodeNear(topMiddle); + GMLNode n3 = map.ensureNodeNear(topLeft); + GMLNode n4 = map.ensureNodeNear(bottomLeft); + GMLNode n5 = map.ensureNodeNear(bottomMiddle); + GMLNode n6 = map.ensureNodeNear(bottomRight); + // Create two new buildings + List edges1 = new ArrayList(); + +// List edges2 = new ArrayList(); +// edges1.add(map.ensureDirectedEdge(topRight, topMiddle)); +// edges1.add(map.ensureDirectedEdge(topMiddle, bottomMiddle)); +// edges1.add(map.ensureDirectedEdge(bottomMiddle, bottomRight)); +// edges1.add(map.ensureDirectedEdge(bottomRight, topRight)); +// edges2.add(map.ensureDirectedEdge(topMiddle, topLeft)); +// edges2.add(map.ensureDirectedEdge(topLeft, bottomLeft)); +// edges2.add(map.ensureDirectedEdge(bottomLeft, bottomMiddle)); +// edges2.add(map.ensureDirectedEdge(bottomMiddle, topMiddle)); + + edges1.add(map.ensureDirectedEdge(topRight, topLeft)); + edges1.add(map.ensureDirectedEdge(topLeft, bottomLeft)); + edges1.add(map.ensureDirectedEdge(bottomLeft, bottomRight)); + edges1.add(map.ensureDirectedEdge(bottomRight, topRight)); + // TO DO: Make the entrance faces + Set result = new HashSet(); + result.add(map.createFace(edges1, FaceType.BUILDING)); + // result.add(map.createFace(edges2, FaceType.BUILDING)); + return result; + } + + private double getRandomOffset() { + double d = random.nextDouble(); + double range = MAX_OFFSET - MIN_OFFSET; + return MIN_OFFSET + (d * range); + } + + private int getRandomRunLength() { + return MIN_RUN_LENGTH + random.nextInt(MAX_RUN_LENGTH - MIN_RUN_LENGTH + 1); + } +*/ +} diff --git a/modules/maps/src/maps/convert/osm2gml/buildings/row/RowFiller.java b/modules/maps/src/maps/convert/osm2gml/buildings/row/RowFiller.java new file mode 100644 index 0000000000000000000000000000000000000000..16eed507f17bb3d4c73f59c9c87f0f62533fd877 --- /dev/null +++ b/modules/maps/src/maps/convert/osm2gml/buildings/row/RowFiller.java @@ -0,0 +1,20 @@ +package maps.convert.osm2gml.buildings.row; + +import java.util.Set; + +import maps.gml.GMLShape; +import maps.gml.GMLDirectedEdge; +import maps.gml.GMLMap; + +/** + Interface for a building generator that works by row. +*/ +public interface RowFiller { + /** + Generate buildings along an edge. + @param edge The edge to populate. + @param map The map. + @return The set of new faces. + */ + Set fillRow(GMLDirectedEdge edge, GMLMap map); +} diff --git a/modules/maps/src/maps/convert/osm2gml/buildings/row/ThinDuplexRowFiller.java b/modules/maps/src/maps/convert/osm2gml/buildings/row/ThinDuplexRowFiller.java new file mode 100644 index 0000000000000000000000000000000000000000..2b4fdcaf69b35e91fec5a16861f40da647a8d386 --- /dev/null +++ b/modules/maps/src/maps/convert/osm2gml/buildings/row/ThinDuplexRowFiller.java @@ -0,0 +1,108 @@ +package maps.convert.osm2gml.buildings.row; + +import java.util.Set; +import java.util.HashSet; +import java.util.Random; + +import maps.gml.GMLDirectedEdge; +import maps.gml.GMLShape; +import maps.gml.GMLMap; + +/** + A RowFiller that creates long, thin duplex units. +*/ +public class ThinDuplexRowFiller implements RowFiller { + private static final double BUILDING_WIDTH_M = 10; + private static final double BUILDING_DEPTH_M = 20; + private static final double MIN_OFFSET_M = 2; + private static final double MAX_OFFSET_M = 3; + private static final int MIN_RUN_LENGTH = 1; + private static final int MAX_RUN_LENGTH = 5; + + private final double buildingWidth; + private final double buildingDepth; + private final double minOffset; + private final double maxOffset; + + private final Random random; + + /** + Construct a ThinDuplexRowFiller. + @param sizeOf1m The size of 1m. + @param random A random number generator. + */ + public ThinDuplexRowFiller(double sizeOf1m, Random random) { + buildingWidth = BUILDING_WIDTH_M * sizeOf1m; + buildingDepth = BUILDING_DEPTH_M * sizeOf1m; + minOffset = MIN_OFFSET_M * sizeOf1m; + maxOffset = MAX_OFFSET_M * sizeOf1m; + this.random = random; + } + + @Override + public Set fillRow(GMLDirectedEdge edge, GMLMap map) { + Set result = new HashSet(); + /* + Line2D edgeLine = ConvertTools.gmlDirectedEdgeToLine(edge); + Vector2D normal = edgeLine.getDirection().getNormal().normalised(); + // Create buildings along the edge until we run out of room + double edgeLength = edgeLine.getDirection().getLength(); + double offset = getRandomOffset(); + int runLength = getRandomRunLength(); + double d = 0; + while (d < 1) { + if (runLength-- == 0) { + offset = getRandomOffset(); + runLength = getRandomRunLength(); + } + double d1 = d; + double d2 = d + (BUILDING_WIDTH / edgeLength); + Point2D topRight = edgeLine.getPoint(d1); + Point2D topLeft = edgeLine.getPoint(d2); + result.addAll(createBuildingInLot(edgeLine, topRight, topLeft, normal, offset, map)); + d = d2; + } + */ + return result; + } + + /* + private Set createBuildingInLot(Line2D edgeLine, Point2D topRight, Point2D topLeft, Vector2D edgeNormal, double depthOffset, GMLMap map) { + // Offset from the top of the boundary + topRight = topRight.plus(edgeNormal.scale(depthOffset)); + topLeft = topLeft.plus(edgeNormal.scale(depthOffset)); + // Find the other end of the building + Point2D bottomRight = topRight.plus(edgeNormal.scale(BUILDING_DEPTH)); + Point2D bottomLeft = topLeft.plus(edgeNormal.scale(BUILDING_DEPTH)); + // Create new nodes and directed edges for the lot + GMLNode n1 = map.ensureNodeNear(topRight); + GMLNode n2 = map.ensureNodeNear(topLeft); + GMLNode n3 = map.ensureNodeNear(bottomLeft); + GMLNode n4 = map.ensureNodeNear(bottomRight); + List edges = new ArrayList(); + GMLDirectedEdge e1 = map.ensureDirectedEdge(topRight, topLeft); + GMLDirectedEdge e2 = map.ensureDirectedEdge(topLeft, bottomLeft); + GMLDirectedEdge e3 = map.ensureDirectedEdge(bottomLeft, bottomRight); + GMLDirectedEdge e4 = map.ensureDirectedEdge(bottomRight, topRight); + edges.add(e1); + edges.add(e2); + edges.add(e3); + edges.add(e4); + GMLFace buildingFace = map.createFace(edges, FaceType.BUILDING); + // Make the entrance face + Set result = new HashSet(); + result.add(buildingFace); + return result; + } + + private double getRandomOffset() { + double d = random.nextDouble(); + double range = MAX_OFFSET - MIN_OFFSET; + return MIN_OFFSET + (d * range); + } + + private int getRandomRunLength() { + return MIN_RUN_LENGTH + random.nextInt(MAX_RUN_LENGTH - MIN_RUN_LENGTH + 1); + } + */ +} diff --git a/modules/maps/src/maps/gml/GMLBuilding.java b/modules/maps/src/maps/gml/GMLBuilding.java new file mode 100755 index 0000000000000000000000000000000000000000..e122bf00dfb763ed876f6ff3ff5f7420b8ddd773 --- /dev/null +++ b/modules/maps/src/maps/gml/GMLBuilding.java @@ -0,0 +1,109 @@ +package maps.gml; + +import java.util.List; + +/** + A building in GML space. +*/ +public class GMLBuilding extends GMLShape { + private int floors; + private int code; + private int importance; + private int capacity; + + /** + Construct a GMLBuilding. + @param id The ID of the building. + */ + public GMLBuilding(int id) { + super(id); + } + + /** + Construct a GMLBuilding. + @param id The ID of the building. + @param edges The edges of the building. + */ + public GMLBuilding(int id, List edges) { + super(id, edges); + } + + /** + Construct a GMLBuilding. + @param id The ID of the building. + @param edges The edges of the building. + @param neighbours The neighbours of each edge. + */ + public GMLBuilding(int id, List edges, List neighbours) { + super(id, edges, neighbours); + } + + @Override + public String toString() { + return "GMLBuilding " + getID(); + } + + /** + Set the capacity of this building. + @param newCapacity The new capacity of the building. + */ + public void setCapacity(int newCapacity) { + capacity = newCapacity; + } + + /** + Get the capacity of this building. + @return The the capacity of building. + */ + public int getCapacity() { + return capacity; + } + + /** + Set the number of floors in this building. + @param newFloors The new number of floors. + */ + public void setFloors(int newFloors) { + floors = newFloors; + } + + /** + Get the number of floors in this building. + @return The number of floors. + */ + public int getFloors() { + return floors; + } + + /** + Set the building code of this building. + @param newCode The new building code. + */ + public void setCode(int newCode) { + code = newCode; + } + + /** + Get the building code of this building. + @return The building code. + */ + public int getCode() { + return code; + } + + /** + Set the importance of this building. + @param newImportance The new importance. + */ + public void setImportance(int newImportance) { + importance = newImportance; + } + + /** + Get the importance of this building. + @return The importance. + */ + public int getImportance() { + return importance; + } +} diff --git a/modules/maps/src/maps/gml/GMLCoordinates.java b/modules/maps/src/maps/gml/GMLCoordinates.java new file mode 100644 index 0000000000000000000000000000000000000000..acef976ae515d026887077a818cac907a38a36eb --- /dev/null +++ b/modules/maps/src/maps/gml/GMLCoordinates.java @@ -0,0 +1,95 @@ +package maps.gml; + +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.text.NumberFormat; +import java.util.Locale; + +/** + A set of GML coordinates. These coordinates are in m. +*/ +public class GMLCoordinates { + private static final NumberFormat FORMAT = new DecimalFormat("#0.000", DecimalFormatSymbols.getInstance(Locale.US)); + + private double x; + private double y; + + /** + Create a new GMLCoordinates object. + @param x The X coordinate. + @param y The Y coordinate. + */ + public GMLCoordinates(double x, double y) { + this.x = x; + this.y = y; + } + + /** + Copy constructor. + @param other The GMLCoordinates to copy. + */ + public GMLCoordinates(GMLCoordinates other) { + this.x = other.x; + this.y = other.y; + } + + /** + Create a new GMLCoordinates object from a String of the form "x,y". + @param s The String to read. + @throws IllegalArgumentException If the string is invalid. + */ + public GMLCoordinates(String s) { + int index = s.indexOf(","); + if (index == -1) { + throw new IllegalArgumentException("'" + s + "' is not of the form 'x,y'"); + } + try { + this.x = Double.parseDouble(s.substring(0, index)); + this.y = Double.parseDouble(s.substring(index + 1)); + } + catch (NumberFormatException e) { + throw new IllegalArgumentException("'" + s + "' is not of the form 'x,y'", e); + } + } + + /** + Get the X coordinate. + @return The X coordinate. + */ + public double getX() { + return x; + } + + /** + Get the Y coordinate. + @return The y coordinate. + */ + public double getY() { + return y; + } + + /** + Set the X coordinate. + @param newX The new X coordinate. + */ + public void setX(double newX) { + x = newX; + } + + /** + Set the Y coordinate. + @param newY The new Y coordinate. + */ + public void setY(double newY) { + y = newY; + } + + @Override + public String toString() { + StringBuilder result = new StringBuilder(); + result.append(FORMAT.format(x)); + result.append(","); + result.append(FORMAT.format(y)); + return result.toString(); + } +} diff --git a/modules/maps/src/maps/gml/GMLDirectedEdge.java b/modules/maps/src/maps/gml/GMLDirectedEdge.java new file mode 100644 index 0000000000000000000000000000000000000000..18e559e895bdcd7bdcdb60da1bc5d51550e28033 --- /dev/null +++ b/modules/maps/src/maps/gml/GMLDirectedEdge.java @@ -0,0 +1,127 @@ +package maps.gml; + +import java.util.List; +import java.util.ArrayList; + +/** + A GMLDirectedEdge is an edge with an orientation. + */ +public class GMLDirectedEdge { + private GMLEdge edge; + private boolean forward; + + /** + Construct a directed GML edge. + @param edge The underlying edge. + @param forward True if this directed edge is aligned with the underlying edge direction, false otherwise. + */ + public GMLDirectedEdge(GMLEdge edge, boolean forward) { + this.edge = edge; + this.forward = forward; + } + + /** + Construct a directed GML edge. + @param edge The underlying edge. + @param start The start node. + */ + public GMLDirectedEdge(GMLEdge edge, GMLNode start) { + this.edge = edge; + this.forward = start.equals(edge.getStart()); + } + + /** + Get the underlying edge. + @return The underlying edge. + */ + public GMLEdge getEdge() { + return edge; + } + + /** + Is this directed edge in the direction of the underlying edge? + @return True if this directed edge is aligned with the underlying edge direction, false otherwise. + */ + public boolean isForward() { + return forward; + } + + /** + Reverse the direction of this edge. + */ + public void reverse() { + forward = !forward; + } + + /** + Get the node at the start of the underlying edge. + @return The start node. + */ + public GMLNode getStartNode() { + return forward ? edge.getStart() : edge.getEnd(); + } + + /** + Get the node at the end of the underlying edge. + @return The end node. + */ + public GMLNode getEndNode() { + return forward ? edge.getEnd() : edge.getStart(); + } + + /** + Get the points of the underlying edge in the right order for this directed edge. + @return The points of the underlying edge in the right order. + */ + public List getPoints() { + List result = new ArrayList(); + result.add(getStartNode().getCoordinates()); + result.add(getEndNode().getCoordinates()); + return result; + } + + /** + Get the coordinates of the start of this edge. + @return The coordinates of the start of this edge. + */ + public GMLCoordinates getStartCoordinates() { + if (forward) { + return edge.getStart().getCoordinates(); + } + else { + return edge.getEnd().getCoordinates(); + } + } + + /** + Get the coordinates of the end of this edge. + @return The coordinates of the end of this edge. + */ + public GMLCoordinates getEndCoordinates() { + if (forward) { + return edge.getEnd().getCoordinates(); + } + else { + return edge.getStart().getCoordinates(); + } + } + + @Override + public String toString() { + return "GMLDirectedEdge" + (forward ? "" : " backwards") + " along " + edge; + } + + @Override + public int hashCode() { + return edge.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (o instanceof GMLDirectedEdge) { + GMLDirectedEdge e = (GMLDirectedEdge)o; + return this.forward == e.forward && this.edge.equals(e.edge); + } + return false; + } +} diff --git a/modules/maps/src/maps/gml/GMLEdge.java b/modules/maps/src/maps/gml/GMLEdge.java new file mode 100644 index 0000000000000000000000000000000000000000..28ed9db548528813e3207486ec26d429383f4be9 --- /dev/null +++ b/modules/maps/src/maps/gml/GMLEdge.java @@ -0,0 +1,112 @@ +package maps.gml; + +import java.util.List; +import java.util.ArrayList; +import java.util.Collections; + +/** + A GML edge. An edge is a line between two nodes. + */ +public class GMLEdge extends GMLObject { + private GMLNode start; + private GMLNode end; + private boolean passable; + private List points; + + /** + Construct a new GMLEdge. + @param id The ID of this object. + @param start The start node. + @param end The end node. + @param passable True if this directed edge is passable. + */ + public GMLEdge(int id, GMLNode start, GMLNode end, boolean passable) { + super(id); + this.start = start; + this.end = end; + this.passable = passable; + points = new ArrayList(); + points.add(start.getCoordinates()); + points.add(end.getCoordinates()); + } + + /** + Get the points along the edge. + @return The coordinates along the edge. + */ + public List getPoints() { + return Collections.unmodifiableList(points); + } + + /** + Set the points along the edge. + @param newPoints The new coordinates along the edge. + */ + public void setPoints(List newPoints) { + points.clear(); + points.addAll(newPoints); + } + + /** + Get the start node. + @return The start node. + */ + public GMLNode getStart() { + return start; + } + + /** + Set the start node. + @param s The new start node. + */ + public void setStart(GMLNode s) { + start = s; + } + + /** + Get the end node. + @return The end node. + */ + public GMLNode getEnd() { + return end; + } + + /** + Set the end node. + @param e The new end node. + */ + public void setEnd(GMLNode e) { + end = e; + } + + /** + Find out if this edge is passable. + @return True iff the edge is passable. + */ + public boolean isPassable() { + return passable; + } + + /** + Set the passable flag on this edge. + @param b The new passable flag. + */ + public void setPassable(boolean b) { + passable = b; + } + + @Override + public String toString() { + StringBuilder result = new StringBuilder(); + result.append("GMLEdge "); + result.append(getID()); + result.append(" from "); + result.append(start); + result.append(" to "); + result.append(end); + if (!passable) { + result.append(" (impassable)"); + } + return result.toString(); + } +} diff --git a/modules/maps/src/maps/gml/GMLException.java b/modules/maps/src/maps/gml/GMLException.java new file mode 100644 index 0000000000000000000000000000000000000000..681ef2f368cea6f46580d3d2bd8f3eddca7858f9 --- /dev/null +++ b/modules/maps/src/maps/gml/GMLException.java @@ -0,0 +1,38 @@ +package maps.gml; + +/** + Exceptions related to GML. +*/ +public class GMLException extends Exception { + /** + Construct a GMLException with no error message. + */ + public GMLException() { + super(); + } + + /** + Construct a GMLException with an error message. + @param msg The error message. + */ + public GMLException(String msg) { + super(msg); + } + + /** + Construct a GMLException with an underlying cause. + @param cause The cause. + */ + public GMLException(Throwable cause) { + super(cause); + } + + /** + Construct a GMLException with an error message and underlying cause. + @param msg The error message. + @param cause The cause. + */ + public GMLException(String msg, Throwable cause) { + super(msg, cause); + } +} diff --git a/modules/maps/src/maps/gml/GMLMap.java b/modules/maps/src/maps/gml/GMLMap.java new file mode 100755 index 0000000000000000000000000000000000000000..c68ceb8831b160cc2ecf533aaada331946293924 --- /dev/null +++ b/modules/maps/src/maps/gml/GMLMap.java @@ -0,0 +1,1007 @@ +package maps.gml; + +import java.util.Map; +import java.util.HashMap; +import java.util.Collection; +import java.util.Collections; +import java.util.Arrays; +import java.util.Set; +import java.util.HashSet; +import java.util.List; +import java.util.ArrayList; +import java.util.Iterator; + +import maps.CoordinateConversion; + +import rescuecore2.misc.geometry.Point2D; +import rescuecore2.misc.geometry.Line2D; +import rescuecore2.misc.geometry.GeometryTools2D; + +import rescuecore2.misc.collections.LazyMap; + +/** + A GML map. All coordinates are specified in m. +*/ +public class GMLMap implements maps.Map { + private double minX; + private double maxX; + private double minY; + private double maxY; + private boolean boundsKnown; + + private Map nodes; + private Map edges; + private Map buildings; + private Map roads; + private Map spaces; + private Set allShapes; + private Set allObjects; + + private Map> attachedEdges; + private Map> attachedShapes; + + private int nextID; + + /** + Construct an empty GML map. + */ + public GMLMap() { + nodes = new HashMap(); + edges = new HashMap(); + buildings = new HashMap(); + roads = new HashMap(); + spaces = new HashMap(); + allShapes = new HashSet(); + allObjects = new HashSet(); + attachedEdges = new LazyMap>() { + @Override + public Collection createValue() { + return new HashSet(); + } + }; + attachedShapes = new LazyMap>() { + @Override + public Collection createValue() { + return new HashSet(); + } + }; + boundsKnown = false; + nextID = 0; + } + + /** + Create a new GMLNode. + @param x The X coordinate of the node in m. + @param y The Y coordinate of the node in m. + @return A new GMLNode with a unique ID. + */ + public GMLNode createNode(double x, double y) { + GMLNode n = new GMLNode(nextID++, x, y); + addNode(n); + return n; + } + + /** + Create a new GMLNode. + @param coords The coordinates of the node. + @return A new GMLNode with a unique ID. + */ + public GMLNode createNode(GMLCoordinates coords) { + GMLNode n = new GMLNode(nextID++, coords); + addNode(n); + return n; + } + + /** + Create a set of new GMLNodes. + @param coords The coordinates of the new nodes. + @return A set of new GMLNodes with unique IDs. + */ + public List createNodes(List coords) { + List result = new ArrayList(coords.size()); + for (GMLCoordinates c : coords) { + GMLNode n = new GMLNode(nextID++, c); + addNode(n); + result.add(n); + } + return result; + } + + /** + Create a set of new GMLNodes. + @param coords The coordinates of the new nodes. + @return A set of new GMLNodes with unique IDs. + */ + public List createNodesFromPoints(List coords) { + List result = new ArrayList(coords.size()); + for (Point2D p : coords) { + GMLNode n = new GMLNode(nextID++, p.getX(), p.getY()); + addNode(n); + result.add(n); + } + return result; + } + + /** + Create a new GMLEdge between two nodes. + @param first The 'start' node. + @param second The 'end' node. + @return A new GMLEdge with a unique ID. + */ + public GMLEdge createEdge(GMLNode first, GMLNode second) { + GMLEdge e = new GMLEdge(nextID++, first, second, false); + addEdge(e); + return e; + } + + /** + Create new GMLBuilding. + @param apexes The apexes of the building. + @return A new GMLBuilding with a unique ID. + */ + public GMLBuilding createBuildingFromNodes(List apexes) { + return createBuilding(apexesToEdges(apexes)); + } + + /** + Create new GMLBuilding. + @param bEdges The edges of the building. + @return A new GMLBuilding with a unique ID. + */ + public GMLBuilding createBuilding(List bEdges) { + GMLBuilding b = new GMLBuilding(nextID++, bEdges); + b.setFloors(1); + b.setCode(0); + b.setImportance(1); + b.setCapacity(0);//todo + addBuilding(b); + return b; + } + + /** + Create new GMLRoad. + @param apexes The apexes of the road. + @return A new GMLRoad with a unique ID. + */ + public GMLRoad createRoadFromNodes(List apexes) { + return createRoad(apexesToEdges(apexes)); + } + + /** + Create new GMLRoad. + @param rEdges The edges of the road. + @return A new GMLRoad with a unique ID. + */ + public GMLRoad createRoad(List rEdges) { + GMLRoad r = new GMLRoad(nextID++, rEdges); + addRoad(r); + return r; + } + + /** + Create new GMLSpace. + @param apexes The apexes of the space. + @return A new GMLSpace with a unique ID. + */ + public GMLSpace createSpaceFromNodes(List apexes) { + return createSpace(apexesToEdges(apexes)); + } + + /** + Create new GMLSpace. + @param sEdges The edges of the space. + @return A new GMLSpace with a unique ID. + */ + public GMLSpace createSpace(List sEdges) { + GMLSpace s = new GMLSpace(nextID++, sEdges); + addSpace(s); + return s; + } + + /** + Add a node. + @param n The node to add. + */ + public void addNode(GMLNode n) { + if (nodes.containsKey(n.getID())) { + return; + } + addObject(n); + nodes.put(n.getID(), n); + boundsKnown = false; + } + + /** + Add an edge. The edge's nodes will also be added if required. + @param e The edge to add. + */ + public void addEdge(GMLEdge e) { + if (edges.containsKey(e.getID())) { + return; + } + addObject(e); + edges.put(e.getID(), e); + addNode(e.getStart()); + addNode(e.getEnd()); + attachedEdges.get(e.getStart()).add(e); + attachedEdges.get(e.getEnd()).add(e); + } + + /** + Add a building. The building's edges will be added if required. + @param b The building to add. + */ + public void addBuilding(GMLBuilding b) { + if (buildings.containsKey(b.getID())) { + return; + } + addShape(b); + buildings.put(b.getID(), b); + } + + /** + Add a road. The road's edges will be added if required. + @param r The road to add. + */ + public void addRoad(GMLRoad r) { + if (roads.containsKey(r.getID())) { + return; + } + addShape(r); + roads.put(r.getID(), r); + } + + /** + Add a space. The space's edges will be added if required. + @param s The space to add. + */ + public void addSpace(GMLSpace s) { + if (spaces.containsKey(s.getID())) { + return; + } + addShape(s); + spaces.put(s.getID(), s); + } + + /** + Add an object. + @param object The object to add. + */ + public void add(GMLObject object) { + if (object instanceof GMLNode) { + addNode((GMLNode)object); + } + else if (object instanceof GMLEdge) { + addEdge((GMLEdge)object); + } + else if (object instanceof GMLRoad) { + addRoad((GMLRoad)object); + } + else if (object instanceof GMLBuilding) { + addBuilding((GMLBuilding)object); + } + else if (object instanceof GMLSpace) { + addSpace((GMLSpace)object); + } + else { + throw new IllegalArgumentException("Don't know how to add " + object + " (class: " + object.getClass().getName() + ")"); + } + } + + /** + Add a set of objects. + @param objects The objects to add. + */ + public void add(Collection objects) { + for (GMLObject next : objects) { + add(next); + } + } + + /** + Add a set of objects. + @param objects The objects to add. + */ + public void add(GMLObject... objects) { + for (GMLObject next : objects) { + add(next); + } + } + + /** + Remove a node, any edges attached to the node and any shapes attached to those edges. + @param n The node to remove. + @return All removed objects, not including n. + */ + public Collection removeNode(GMLNode n) { + Collection result = new HashSet(); + if (nodes.containsKey(n.getID())) { + removeObject(n); + nodes.remove(n.getID()); + Collection attached = new HashSet(getAttachedEdges(n)); + for (GMLEdge next : attached) { + result.add(next); + result.addAll(removeEdge(next)); + } + boundsKnown = false; + } + return result; + } + + /** + Remove an edge and any attached shapes. + @param e The edge to remove. + @return All removed objects, not including e. + */ + public Collection removeEdge(GMLEdge e) { + Collection result = new HashSet(); + if (edges.containsKey(e.getID())) { + removeObject(e); + edges.remove(e.getID()); + Collection attached = new HashSet(getAttachedShapes(e)); + for (GMLShape next : attached) { + result.add(next); + remove(next); + } + attachedEdges.get(e.getStart()).remove(e); + attachedEdges.get(e.getEnd()).remove(e); + } + return result; + } + + /** + Remove a building. + @param b The building to remove. + */ + public void removeBuilding(GMLBuilding b) { + if (buildings.containsKey(b.getID())) { + removeShape(b); + buildings.remove(b.getID()); + } + } + + /** + Remove a road. + @param r The road to remove. + */ + public void removeRoad(GMLRoad r) { + if (roads.containsKey(r.getID())) { + removeShape(r); + roads.remove(r.getID()); + } + } + + /** + Remove a space. + @param s The space to remove. + */ + public void removeSpace(GMLSpace s) { + if (spaces.containsKey(s.getID())) { + removeShape(s); + spaces.remove(s.getID()); + } + } + + /** + Remove an object. + @param object The object to remove. + */ + public void remove(GMLObject object) { + if (object instanceof GMLNode) { + removeNode((GMLNode)object); + } + else if (object instanceof GMLEdge) { + removeEdge((GMLEdge)object); + } + else if (object instanceof GMLRoad) { + removeRoad((GMLRoad)object); + } + else if (object instanceof GMLBuilding) { + removeBuilding((GMLBuilding)object); + } + else if (object instanceof GMLSpace) { + removeSpace((GMLSpace)object); + } + else { + throw new IllegalArgumentException("Don't know how to remove " + object + " (class: " + object.getClass().getName() + ")"); + } + } + + /** + Remove a set of objects. + @param objects The objects to remove. + */ + public void remove(Collection objects) { + for (GMLObject next : objects) { + remove(next); + } + } + + /** + Remove a set of objects. + @param objects The objects to remove. + */ + public void remove(GMLObject... objects) { + for (GMLObject next : objects) { + remove(next); + } + } + + /** + Remove all nodes, edges and shapes. + */ + public void removeAllNodes() { + nodes.clear(); + edges.clear(); + roads.clear(); + buildings.clear(); + spaces.clear(); + allShapes.clear(); + allObjects.clear(); + attachedEdges.clear(); + attachedShapes.clear(); + boundsKnown = false; + } + + /** + Remove all edges and shapes. + */ + public void removeAllEdges() { + edges.clear(); + roads.clear(); + buildings.clear(); + spaces.clear(); + allShapes.clear(); + allObjects.retainAll(nodes.values()); + attachedEdges.clear(); + attachedShapes.clear(); + } + + /** + Remove all buildings. + */ + public void removeAllBuildings() { + for (Map.Entry> entry : attachedShapes.entrySet()) { + entry.getValue().removeAll(buildings.values()); + } + allShapes.removeAll(buildings.values()); + allObjects.removeAll(buildings.values()); + buildings.clear(); + } + + /** + Remove all roads. + */ + public void removeAllRoads() { + for (Map.Entry> entry : attachedShapes.entrySet()) { + entry.getValue().removeAll(buildings.values()); + } + allShapes.removeAll(roads.values()); + allObjects.removeAll(roads.values()); + roads.clear(); + } + + /** + Remove all spaces. + */ + public void removeAllSpaces() { + for (Map.Entry> entry : attachedShapes.entrySet()) { + entry.getValue().removeAll(buildings.values()); + } + allShapes.removeAll(spaces.values()); + allObjects.removeAll(spaces.values()); + spaces.clear(); + } + + /** + Get a node by ID. + @param id The ID to look up. + @return The node with that ID or null if the ID is not found. + */ + public GMLNode getNode(int id) { + return nodes.get(id); + } + + /** + Get an edge by ID. + @param id The ID to look up. + @return The edge with that ID or null if the ID is not found. + */ + public GMLEdge getEdge(int id) { + return edges.get(id); + } + + /** + Get a building by ID. + @param id The ID to look up. + @return The building with that ID or null if the ID is not found. + */ + public GMLBuilding getBuilding(int id) { + return buildings.get(id); + } + + /** + Get a road by ID. + @param id The ID to look up. + @return The road with that ID or null if the ID is not found. + */ + public GMLRoad getRoad(int id) { + return roads.get(id); + } + + /** + Get a space by ID. + @param id The ID to look up. + @return The space with that ID or null if the ID is not found. + */ + public GMLSpace getSpace(int id) { + return spaces.get(id); + } + + /** + Get a shape by ID. + @param id The ID to look up. + @return The shape with that ID or null if the ID is not found. + */ + public GMLShape getShape(int id) { + GMLBuilding b = getBuilding(id); + if (b != null) { + return b; + } + GMLRoad r = getRoad(id); + if (r != null) { + return r; + } + GMLSpace s = getSpace(id); + if (s != null) { + return s; + } + return null; + } + + /** + Get an object by ID. + @param id The ID to look up. + @return The object with that ID or null if the ID is not found. + */ + public GMLObject getObject(int id) { + GMLNode n = getNode(id); + if (n != null) { + return n; + } + GMLEdge e = getEdge(id); + if (e != null) { + return e; + } + GMLBuilding b = getBuilding(id); + if (b != null) { + return b; + } + GMLRoad r = getRoad(id); + if (r != null) { + return r; + } + GMLSpace s = getSpace(id); + if (s != null) { + return s; + } + return null; + } + + /** + Get all nodes in the map. + @return All nodes. + */ + public Set getNodes() { + return new HashSet(nodes.values()); + } + + /** + Get all edges in the map. + @return All edges. + */ + public Set getEdges() { + return new HashSet(edges.values()); + } + + /** + Get all buildings in the map. + @return All buildings. + */ + public Set getBuildings() { + return new HashSet(buildings.values()); + } + + /** + Get all roads in the map. + @return All roads. + */ + public Set getRoads() { + return new HashSet(roads.values()); + } + + /** + Get all spaces in the map. + @return All spaces. + */ + public Set getSpaces() { + return new HashSet(spaces.values()); + } + + /** + Get all shapes in the map. + @return All shapes. + */ + public Set getAllShapes() { + return Collections.unmodifiableSet(allShapes); + } + + /** + Get all objects in the map. + @return All objects. + */ + public Set getAllObjects() { + return Collections.unmodifiableSet(allObjects); + } + + /** + Get the minimum x coordinate. + @return The minimum x coordinate. + */ + public double getMinX() { + calculateBounds(); + return minX; + } + + /** + Get the maximum x coordinate. + @return The maximum x coordinate. + */ + public double getMaxX() { + calculateBounds(); + return maxX; + } + + /** + Get the minimum y coordinate. + @return The minimum y coordinate. + */ + public double getMinY() { + calculateBounds(); + return minY; + } + + /** + Get the maximum y coordinate. + @return The maximum y coordinate. + */ + public double getMaxY() { + calculateBounds(); + return maxY; + } + + /** + Find out if this map has a real size or not. Maps with zero or one nodes do not have a real size. + @return True if this map has two or more nodes. + */ + public boolean hasSize() { + return nodes.size() > 1; + } + + /** + Rescale the map coordinates. + @param conversion The coordinate conversion to apply. + */ + public void convertCoordinates(CoordinateConversion conversion) { + for (GMLNode next : nodes.values()) { + next.convert(conversion); + } + boundsKnown = false; + } + + /** + Create or retrieve an existing edge between two nodes. + @param first The 'start' node. + @param second The 'end' node. + @return A new GMLEdge with a unique ID or an existing edge. The returned edge may be reversed with respect to first and second. + */ + public GMLEdge ensureEdge(GMLNode first, GMLNode second) { + for (GMLEdge next : edges.values()) { + if ((next.getStart().equals(first) && next.getEnd().equals(second)) + || (next.getStart().equals(second) && next.getEnd().equals(first)) + ) { + return next; + } + } + return createEdge(first, second); + } + + /** + Turn a list of apexes into a list of directed edges. + @param apexes The apexes to convert. + @return A list of directed edges. + */ + public List apexesToEdges(GMLNode... apexes) { + return apexesToEdges(Arrays.asList(apexes)); + } + + /** + Turn a list of apexes into a list of directed edges. + @param apexes The apexes to convert. + @return A list of directed edges. + */ + public List apexesToEdges(List apexes) { + List edgeList = new ArrayList(apexes.size()); + Iterator it = apexes.iterator(); + GMLNode first = it.next(); + GMLNode previous = first; + while (it.hasNext()) { + GMLNode next = it.next(); + GMLEdge edge = ensureEdge(previous, next); + edgeList.add(new GMLDirectedEdge(edge, previous)); + previous = next; + } + GMLEdge edge = ensureEdge(previous, first); + edgeList.add(new GMLDirectedEdge(edge, previous)); + return edgeList; + } + + /** + Get all GMLNodes inside a region. + @param xMin The lower X bound of the region. + @param yMin The lower Y bound of the region. + @param xMax The upper X bound of the region. + @param yMax The upper Y bound of the region. + @return All GMLNodes inside the region. + */ + public Collection getNodesInRegion(double xMin, double yMin, double xMax, double yMax) { + Collection result = new ArrayList(); + for (GMLNode next : nodes.values()) { + double x = next.getX(); + double y = next.getY(); + if (x >= xMin && x <= xMax && y >= yMin && y <= yMax) { + result.add(next); + } + } + return result; + } + + /** + Find the GMLNode nearest a point. + @param x The X coordinate. + @param y The Y coordinate. + @return The nearest GMLNode. + */ + public GMLNode findNearestNode(double x, double y) { + GMLNode best = null; + double bestDistance = Double.NaN; + for (GMLNode next : nodes.values()) { + double dx = x - next.getX(); + double dy = y - next.getY(); + double d = (dx * dx) + (dy * dy); + if (best == null || d < bestDistance) { + best = next; + bestDistance = d; + } + } + return best; + } + + /** + Find the GMLEdge nearest a point. + @param x The X coordinate. + @param y The Y coordinate. + @return The nearest GMLEdge. + */ + public GMLEdge findNearestEdge(double x, double y) { + return findNearestEdge(x, y, edges.values()); + } + + /** + Find the GMLEdge nearest a point from a set of possible edges. + @param x The X coordinate. + @param y The Y coordinate. + @param possible The set of possible edges. + @return The nearest GMLEdge. + */ + public GMLEdge findNearestEdge(double x, double y, Collection possible) { + GMLEdge best = null; + double bestDistance = Double.NaN; + Point2D test = new Point2D(x, y); + for (GMLEdge next : possible) { + Line2D line = GMLTools.toLine(next); + Point2D closest = GeometryTools2D.getClosestPointOnSegment(line, test); + double d = GeometryTools2D.getDistance(test, closest); + if (best == null || d < bestDistance) { + best = next; + bestDistance = d; + } + } + return best; + } + + /** + Find the GMLShape under a point. + @param x The X coordinate. + @param y The Y coordinate. + @return The shape under the point or null if no shapes are found. + */ + public GMLShape findShapeUnder(double x, double y) { + for (GMLShape next : allShapes) { + if (GMLTools.coordsToShape(next.getUnderlyingCoordinates()).contains(x, y)) { + return next; + } + } + return null; + } + + /** + Get all GMLEdges that are attached to a GMLNode. + @param node The GMLNode to look up. + @return All attached GMLEdges. + */ + public Collection getAttachedEdges(GMLNode node) { + return Collections.unmodifiableCollection(attachedEdges.get(node)); + } + + /** + Get all GMLShapes that are attached to a GMLEdge. + @param edge The GMLEdge to look up. + @return All attached GMLShapes. + */ + public Collection getAttachedShapes(GMLEdge edge) { + return Collections.unmodifiableCollection(attachedShapes.get(edge)); + } + + /** + Merge a pair of edges and form a new edge. The node and existing edges are not removed from the map. + @param edge1 The first edge. + @param edge2 The second edge. + @return The new edge. + */ + public GMLEdge mergeEdges(GMLEdge edge1, GMLEdge edge2) { + GMLNode commonNode = edge1.getStart(); + if (!commonNode.equals(edge2.getStart()) && !commonNode.equals(edge2.getEnd())) { + commonNode = edge1.getEnd(); + } + if (!commonNode.equals(edge2.getStart()) && !commonNode.equals(edge2.getEnd())) { + throw new IllegalArgumentException("Edges " + edge1 + " and " + edge2 + " do not have a common node"); + } + GMLNode start = commonNode.equals(edge1.getStart()) ? edge1.getEnd() : edge1.getStart(); + GMLNode end = commonNode.equals(edge2.getStart()) ? edge2.getEnd() : edge2.getStart(); + return ensureEdge(start, end); + } + + /** + Replace all references to a node with another node. This does not delete the old node from the map. + @param oldNode The node to replace. + @param newNode The new node. + */ + public void replaceNode(GMLNode oldNode, GMLNode newNode) { + List attached = new ArrayList(getAttachedEdges(oldNode)); + for (GMLEdge next : attached) { + if (next.getStart().equals(oldNode)) { + next.setStart(newNode); + attachedEdges.get(oldNode).remove(next); + attachedEdges.get(newNode).add(next); + } + if (next.getEnd().equals(oldNode)) { + next.setEnd(newNode); + attachedEdges.get(oldNode).remove(next); + attachedEdges.get(newNode).add(next); + } + } + } + + /** + Replace all references to an edge with another edge. This does not delete the old edge from the map. The two edges must have the same pair of start and end nodes but may be in different directions. + @param oldEdge The edge to replace. + @param newEdge The new edge. + */ + public void replaceEdge(GMLEdge oldEdge, GMLEdge newEdge) { + if ((oldEdge.getStart() != newEdge.getStart() && oldEdge.getStart() != newEdge.getEnd()) + || (oldEdge.getEnd() != newEdge.getStart() && oldEdge.getEnd() != newEdge.getEnd())) { + throw new IllegalArgumentException("oldEdge and newEdge do not share start and end nodes"); + } + Collection attached = new HashSet(getAttachedShapes(oldEdge)); + for (GMLShape next : attached) { + for (GMLDirectedEdge dEdge : next.getEdges()) { + if (dEdge.getEdge() == oldEdge) { + boolean forward; + if (oldEdge.getStart() == newEdge.getStart()) { + forward = dEdge.isForward(); + } + else { + forward = !dEdge.isForward(); + } + GMLDirectedEdge replacement = new GMLDirectedEdge(newEdge, forward); + next.replaceEdge(dEdge, replacement); + attachedShapes.get(oldEdge).remove(next); + attachedShapes.get(newEdge).add(next); + } + } + } + } + + /** + Insert a node into an edge. This method updates attached edges but does not delete the old edge. + @param edge The edge to split. + @param node The node to insert. + @return The two new edges. + */ + public Collection splitEdge(GMLEdge edge, GMLNode node) { + Collection result = new ArrayList(2); + GMLEdge first = ensureEdge(edge.getStart(), node); + GMLEdge second = ensureEdge(node, edge.getEnd()); + result.add(first); + result.add(second); + // Update any attached edges + Collection attached = new HashSet(getAttachedShapes(edge)); + for (GMLShape shape : attached) { + for (GMLDirectedEdge dEdge : shape.getEdges()) { + if (dEdge.getEdge() == edge) { + // Create two new directed edges + GMLDirectedEdge d1; + GMLDirectedEdge d2; + if (dEdge.isForward()) { + d1 = new GMLDirectedEdge(first, true); + d2 = new GMLDirectedEdge(second, true); + } + else { + d1 = new GMLDirectedEdge(second, false); + d2 = new GMLDirectedEdge(first, false); + } + shape.replaceEdge(dEdge, d1, d2); + attachedShapes.get(edge).remove(shape); + attachedShapes.get(first).add(shape); + attachedShapes.get(second).add(shape); + } + } + } + return result; + } + + private void addShape(GMLShape shape) { + addObject(shape); + allShapes.add(shape); + for (GMLDirectedEdge edge : shape.getEdges()) { + addEdge(edge.getEdge()); + attachedShapes.get(edge.getEdge()).add(shape); + } + } + + private void addObject(GMLObject object) { + allObjects.add(object); + nextID = Math.max(nextID, object.getID() + 1); + } + + private void removeShape(GMLShape shape) { + removeObject(shape); + allShapes.remove(shape); + for (GMLDirectedEdge edge : shape.getEdges()) { + attachedShapes.get(edge.getEdge()).remove(shape); + } + } + + private void removeObject(GMLObject object) { + allObjects.remove(object); + } + + private void calculateBounds() { + if (boundsKnown) { + return; + } + minX = Double.POSITIVE_INFINITY; + minY = Double.POSITIVE_INFINITY; + maxX = Double.NEGATIVE_INFINITY; + maxY = Double.NEGATIVE_INFINITY; + for (GMLNode n : nodes.values()) { + GMLCoordinates c = n.getCoordinates(); + minX = Math.min(minX, c.getX()); + maxX = Math.max(maxX, c.getX()); + minY = Math.min(minY, c.getY()); + maxY = Math.max(maxY, c.getY()); + } + boundsKnown = true; + } +} diff --git a/modules/maps/src/maps/gml/GMLMapFormat.java b/modules/maps/src/maps/gml/GMLMapFormat.java new file mode 100644 index 0000000000000000000000000000000000000000..d8ad6b3ce4d249a97f82f8b8abe93dec130feeae --- /dev/null +++ b/modules/maps/src/maps/gml/GMLMapFormat.java @@ -0,0 +1,177 @@ +package maps.gml; + +import java.io.File; +import java.io.Reader; +import java.io.FileReader; +import java.io.FileOutputStream; +import java.io.FileNotFoundException; +import java.io.IOException; + +import javax.xml.stream.XMLStreamReader; +import javax.xml.stream.XMLStreamConstants; +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamException; + +import org.dom4j.Document; +import org.dom4j.Element; +import org.dom4j.DocumentException; +import org.dom4j.io.SAXReader; +import org.dom4j.io.XMLWriter; +import org.dom4j.io.OutputFormat; + +import maps.MapFormat; +import maps.MapException; +import maps.Map; + +import rescuecore2.log.Logger; + +/** + Abstract base class for map formats that use GML. +*/ +public abstract class GMLMapFormat implements MapFormat { + @Override + public GMLMap read(File file) throws MapException { + FileReader r; + try { + r = new FileReader(file); + } + catch (FileNotFoundException e) { + throw new MapException(e); + } + try { + return read(r); + } + catch (DocumentException e) { + throw new MapException(e); + } + finally { + try { + r.close(); + } + catch (IOException e) { + Logger.warn("IOException while closing file reader", e); + } + } + } + + @Override + public void write(Map map, File file) throws MapException { + if (map == null) { + throw new IllegalArgumentException("Map must not be null"); + } + if (file == null) { + throw new IllegalArgumentException("File must not be null"); + } + if (!(map instanceof GMLMap)) { + throw new IllegalArgumentException("Map is not a GMLMap: " + map.getClass().getName()); + } + Document doc = write((GMLMap)map); + try { + if (!file.exists()) { + File parent = file.getParentFile(); + if (!parent.exists()) { + if (!file.getParentFile().mkdirs()) { + throw new MapException("Couldn't create file " + file.getPath()); + } + } + if (!file.createNewFile()) { + throw new MapException("Couldn't create file " + file.getPath()); + } + } + XMLWriter writer = new XMLWriter(new FileOutputStream(file), OutputFormat.createPrettyPrint()); + Element root = doc.getRootElement(); + for (java.util.Map.Entry next : getNamespaces().entrySet()) { + root.addNamespace(next.getKey(), next.getValue()); + } + writer.write(doc); + writer.flush(); + writer.close(); + } + catch (IOException e) { + throw new MapException(e); + } + } + + @Override + public boolean canRead(File file) throws MapException { + if (file.isDirectory() || !file.exists()) { + return false; + } + if (!file.getName().endsWith(".gml")) { + return false; + } + // Check that the XML dialect is correct by looking at the root element. + FileReader r; + try { + r = new FileReader(file); + } + catch (FileNotFoundException e) { + throw new MapException(e); + } + try { + XMLStreamReader reader = XMLInputFactory.newInstance().createXMLStreamReader(r); + while (reader.hasNext()) { + if (reader.next() == XMLStreamConstants.START_ELEMENT) { + return isCorrectRootElement(reader.getNamespaceURI(), reader.getLocalName()); + } + } + } + catch (XMLStreamException e) { + Logger.debug("Exception while reading XML stream", e); + return false; + } + finally { + try { + r.close(); + } + catch (IOException e) { + Logger.warn("IOException while closing file reader", e); + } + } + return false; + } + + /** + Read a GMLMap from a Reader. + @param reader The Reader to read. + @return A new GMLMap. + @throws DocumentException If there is a problem parsing the XML. + @throws MapException If there is a problem reading the map. + */ + public GMLMap read(Reader reader) throws DocumentException, MapException { + Logger.debug("Parsing GML"); + SAXReader saxReader = new SAXReader(); + Document doc = saxReader.read(reader); + Logger.debug("Building map"); + return read(doc); + } + + /** + Find out if the root element is correct for this format type. + @param uri The URI of the root element. + @param localName The local name of the root element. + @return True if the uri and localName are correct for this format's root element, false otherwise. + */ + protected abstract boolean isCorrectRootElement(String uri, String localName); + + /** + Read a Document and return a GMLMap. + @param doc The document to read. + @return A new GMLMap. + @throws MapException If there is a problem reading the map. + */ + protected abstract GMLMap read(Document doc) throws MapException; + + /** + Turn a GMLMap into an xml document. + @param map The map to write. + @return A new document. + */ + protected abstract Document write(GMLMap map); + + /** + Get the uris and preferred prefixes for all namespaces this format cares about. + @return A map from prefix to uri for all relevant namespaces. + */ + protected abstract java.util.Map getNamespaces(); +} diff --git a/modules/maps/src/maps/gml/GMLNode.java b/modules/maps/src/maps/gml/GMLNode.java new file mode 100644 index 0000000000000000000000000000000000000000..92e66dd05312744c1217ae1d1af24821e34cdb79 --- /dev/null +++ b/modules/maps/src/maps/gml/GMLNode.java @@ -0,0 +1,82 @@ +package maps.gml; + +import maps.CoordinateConversion; + +/** + A GML node object. + */ +public class GMLNode extends GMLObject { + private GMLCoordinates coordinates; + + /** + Construct a new GML node. + @param id The ID of this node. + @param x The x coordinate of this node. + @param y The y coordinate of this node. + */ + public GMLNode(int id, double x, double y) { + this(id, new GMLCoordinates(x, y)); + } + + /** + Construct a new GML node. + @param id The ID of this node. + @param coordinates The coordinates of this node. + */ + public GMLNode(int id, GMLCoordinates coordinates) { + super(id); + this.coordinates = coordinates; + } + + /** + Get the coordinates of this node. + @return The node coordinates. + */ + public GMLCoordinates getCoordinates() { + return coordinates; + } + + /** + Set the coordinates of this node. + @param c The new coordinates. + */ + public void setCoordinates(GMLCoordinates c) { + if (c == null) { + throw new IllegalArgumentException("Coordinates cannot be null"); + } + this.coordinates = c; + } + + /** + Get the X coordinate. + @return The X coordinate. + */ + public double getX() { + return coordinates.getX(); + } + + /** + Get the Y coordinate. + @return The Y coordinate. + */ + public double getY() { + return coordinates.getY(); + } + + /** + Apply a CoordinateConversion to this node. + @param c The conversion to apply. + */ + public void convert(CoordinateConversion c) { + double oldX = coordinates.getX(); + double oldY = coordinates.getY(); + double newX = c.convertX(oldX); + double newY = c.convertY(oldY); + coordinates = new GMLCoordinates(newX, newY); + } + + @Override + public String toString() { + return "GMLNode " + getID() + " at " + coordinates; + } +} diff --git a/modules/maps/src/maps/gml/GMLObject.java b/modules/maps/src/maps/gml/GMLObject.java new file mode 100644 index 0000000000000000000000000000000000000000..92f5f93ef50eb37bff81d0190cadc007eecc50f2 --- /dev/null +++ b/modules/maps/src/maps/gml/GMLObject.java @@ -0,0 +1,37 @@ +package maps.gml; + +/** + A GML map object. +*/ +public abstract class GMLObject { + private int id; + + /** + Construct a GML object. + @param id The id of the object. + */ + protected GMLObject(int id) { + this.id = id; + } + + /** + Get this object's ID. + @return The object ID. + */ + public int getID() { + return id; + } + + @Override + public int hashCode() { + return (int)id; + } + + @Override + public boolean equals(Object o) { + if (o instanceof GMLObject) { + return this.id == ((GMLObject)o).id; + } + return false; + } +} diff --git a/modules/maps/src/maps/gml/GMLRefuge.java b/modules/maps/src/maps/gml/GMLRefuge.java new file mode 100644 index 0000000000000000000000000000000000000000..6993f324b1f6ae8a18bb47663cad6dfa82085028 --- /dev/null +++ b/modules/maps/src/maps/gml/GMLRefuge.java @@ -0,0 +1,97 @@ +package maps.gml; + +import java.util.List; + +public class GMLRefuge extends GMLBuilding { + + private int bedCapacity; + private int refillCapacity; + + + /** + * Construct a GMLRefuge. + * + * @param id + * The ID of the Refuge. + */ + public GMLRefuge( int id ) { + super( id ); + } + + + /** + * Construct a GMLRefuge. + * + * @param id + * The ID of the Refuge. + * @param edges + * The edges of the Refuge. + */ + public GMLRefuge( int id, List edges ) { + super( id, edges ); + } + + + /** + * Construct a GMLRefuge. + * + * @param id + * The ID of the Refuge. + * @param edges + * The edges of the Refuge. + * @param neighbours + * The neighbours of each edge. + */ + public GMLRefuge( int id, List edges, List neighbours ) { + super( id, edges, neighbours ); + } + + + @Override + public String toString() { + return "GMLRefuge " + getID(); + } + + + /** + * Set the bed capacity of this Refuge. + * + * @param newCapacity + * The new bed capacity of the Refuge. + */ + public void setBedCapacity( int newCapacity ) { + bedCapacity = newCapacity; + } + + + /** + * Get the bed capacity of this Refuge. + * + * @return The the bed capacity of Refuge. + */ + public int getBedCapacity() { + return bedCapacity; + } + + + /** + * Set the refill capacity of this Refuge. + * + * @param newCapacity + * The new refill capacity of the Refuge. + */ + public void setRefillCapacity( int newCapacity ) { + refillCapacity = newCapacity; + } + + + /** + * Get the refill capacity of this Refuge. + * + * @return The the refill capacity of Refuge. + */ + public int getRefillCapacity() { + return refillCapacity; + } + +} diff --git a/modules/maps/src/maps/gml/GMLRoad.java b/modules/maps/src/maps/gml/GMLRoad.java new file mode 100644 index 0000000000000000000000000000000000000000..2b9ded80ca5a0271a131c027031d22c961c9dd5c --- /dev/null +++ b/modules/maps/src/maps/gml/GMLRoad.java @@ -0,0 +1,40 @@ +package maps.gml; + +import java.util.List; + +/** + A road in GML space. +*/ +public class GMLRoad extends GMLShape { + /** + Construct a GMLRoad. + @param id The ID of the road. + */ + public GMLRoad(int id) { + super(id); + } + + /** + Construct a GMLRoad. + @param id The ID of the road. + @param edges The edges of the road. + */ + public GMLRoad(int id, List edges) { + super(id, edges); + } + + /** + Construct a GMLRoad. + @param id The ID of the road. + @param edges The edges of the road. + @param neighbours The neighbours of each edge. + */ + public GMLRoad(int id, List edges, List neighbours) { + super(id, edges, neighbours); + } + + @Override + public String toString() { + return "GMLRoad " + getID(); + } +} diff --git a/modules/maps/src/maps/gml/GMLShape.java b/modules/maps/src/maps/gml/GMLShape.java new file mode 100644 index 0000000000000000000000000000000000000000..7b6313efe0728c4c089347b56b86fa09ed1ad740 --- /dev/null +++ b/modules/maps/src/maps/gml/GMLShape.java @@ -0,0 +1,283 @@ +package maps.gml; + +import java.util.List; +import java.util.ArrayList; +import java.util.Map; +import java.util.HashMap; +import java.util.Iterator; +import java.util.ListIterator; +import java.util.Collections; + +import java.awt.geom.Rectangle2D; + +import rescuecore2.misc.geometry.Point2D; +import rescuecore2.misc.geometry.GeometryTools2D; + +/** + Abstract base class for shapes in GML space. +*/ +public abstract class GMLShape extends GMLObject { + private List edges; + private Map neighbours; + private List points; + private Rectangle2D bounds; + private Point2D centroid; + + /** + Construct a GMLShape. + @param id The ID of the shape. + */ + protected GMLShape(int id) { + super(id); + this.edges = new ArrayList(); + neighbours = new HashMap(); + bounds = null; + centroid = null; + } + + /** + Construct a GMLShape. + @param id The ID of the shape. + @param edges The edges of the shape. + */ + protected GMLShape(int id, List edges) { + this(id); + this.edges.addAll(edges); + points = getUnderlyingCoordinates(); + } + + /** + Construct a GMLShape. + @param id The ID of the shape. + @param edges The edges of the shape. + @param neighbours The neighbours of each edge. + */ + protected GMLShape(int id, List edges, List neighbours) { + this(id, edges); + Iterator it = edges.iterator(); + Iterator ix = neighbours.iterator(); + while (it.hasNext() && ix.hasNext()) { + setNeighbour(it.next(), ix.next()); + } + points = getUnderlyingCoordinates(); + } + + /** + Get the edges of this shape. + @return The edges. + */ + public List getEdges() { + return new ArrayList(edges); + } + + /** + Set the list of edges. + @param newEdges The new edge list. + */ + public void setEdges(List newEdges) { + edges.clear(); + neighbours.clear(); + edges.addAll(newEdges); + bounds = null; + centroid = null; + points = getUnderlyingCoordinates(); + } + + /** + Reorder the list of edges. This will not clear the neighbour map or the bounds. + @param newEdges The reordered edge list. + */ + public void reorderEdges(List newEdges) { + edges.clear(); + edges.addAll(newEdges); + points = getUnderlyingCoordinates(); + centroid = null; + neighbours.keySet().retainAll(newEdges); + } + + /** + Replace a GMLDirectedEdge with a set of new edges. The neighbour of the old edge will be set for each of the new edges. + @param oldEdge The edge to replace. + @param newEdges The new edges. + */ + public void replaceEdge(GMLDirectedEdge oldEdge, GMLDirectedEdge... newEdges) { + ListIterator it = edges.listIterator(); + while (it.hasNext()) { + if (it.next() == oldEdge) { + it.remove(); + for (GMLDirectedEdge e : newEdges) { + it.add(e); + } + } + } + bounds = null; + centroid = null; + points = getUnderlyingCoordinates(); + } + + /** + Remove an edge from this shape. + @param edge The underlying edge to remove. + */ + public void removeEdge(GMLEdge edge) { + for (Iterator it = edges.iterator(); it.hasNext();) { + GMLDirectedEdge dEdge = it.next(); + if (dEdge.getEdge().equals(edge)) { + it.remove(); + neighbours.remove(dEdge); + } + } + bounds = null; + centroid = null; + points = getUnderlyingCoordinates(); + } + + /** + Get the ID of the neighbour through a particular edge. + @param edge The edge to look up. + @return The ID of the neighbour through that edge or null. + */ + public Integer getNeighbour(GMLDirectedEdge edge) { + return neighbours.get(edge); + } + + /** + Set the ID of the neighbour through a particular edge. + @param edge The edge to set the neighbour of. + @param neighbour The new neighbour ID for that edge. This may be null. + */ + public void setNeighbour(GMLDirectedEdge edge, Integer neighbour) { + if (neighbour == null) { + neighbours.remove(edge); + } + else { + neighbours.put(edge, neighbour); + } + } + + /** + Find out if an edge has a neighbour. + @param edge The edge to look up. + @return True if there is a neighbour through that edge or false otherwise. + */ + public boolean hasNeighbour(GMLDirectedEdge edge) { + return neighbours.containsKey(edge); + } + + /** + Get the ID of the neighbour through a particular edge. + @param edge The edge to look up. + @return The ID of the neighbour through that edge or null. + */ + public Integer getNeighbour(GMLEdge edge) { + return getNeighbour(findDirectedEdge(edge)); + } + + /** + Set the ID of the neighbour through a particular edge. + @param edge The edge to set the neighbour of. + @param neighbour The new neighbour ID for that edge. This may be null. + */ + public void setNeighbour(GMLEdge edge, Integer neighbour) { + setNeighbour(findDirectedEdge(edge), neighbour); + } + + /** + Find out if an edge has a neighbour. + @param edge The edge to look up. + @return True if there is a neighbour through that edge or false otherwise. + */ + public boolean hasNeighbour(GMLEdge edge) { + return neighbours.containsKey(findDirectedEdge(edge)); + } + + /** + Get the coordinates of the edges that make up this shape. + @return The underlying edge coordinates. + */ + public List getUnderlyingCoordinates() { + List result = new ArrayList(); + for (GMLDirectedEdge next : edges) { + result.add(next.getStartCoordinates()); + } + return result; + } + + /** + Get the nodes of the edges that make up this shape. + @return The underlying nodes. + */ + public List getUnderlyingNodes() { + List result = new ArrayList(); + for (GMLDirectedEdge next : edges) { + result.add(next.getStartNode()); + } + return result; + } + + /** + Get the coordinates of the apexes of this shape. + @return The apex coordinates. + */ + public List getCoordinates() { + return Collections.unmodifiableList(points); + } + + /** + Set the coordinates of the apexes of this shape. + @param newPoints The new apex coordinates. + */ + public void setCoordinates(List newPoints) { + points.clear(); + points.addAll(newPoints); + bounds = null; + centroid = null; + } + + /** + Get the x coordinate of the centroid of this shape. + @return The x coordinate of the centroid. + */ + public double getCentreX() { + return getCentroid().getX(); + } + + /** + Get the y coordinate of the centroid of this shape. + @return The y coordinate of the centroid. + */ + public double getCentreY() { + return getCentroid().getY(); + } + + /** + Get the bounds of this shape. + @return The bounds of the shape. + */ + public Rectangle2D getBounds() { + if (bounds == null) { + bounds = GMLTools.getBounds(getCoordinates()); + } + return bounds; + } + + /** + Get the centroid of this shape. + @return The centroid of the shape. + */ + public Point2D getCentroid() { + if (centroid == null) { + centroid = GeometryTools2D.computeCentroid(GMLTools.coordinatesAsPoints(getCoordinates())); + } + return centroid; + } + + private GMLDirectedEdge findDirectedEdge(GMLEdge e) { + for (GMLDirectedEdge next : edges) { + if (next.getEdge().equals(e)) { + return next; + } + } + throw new IllegalArgumentException(this + ": Edge " + e + " not found"); + } +} diff --git a/modules/maps/src/maps/gml/GMLSpace.java b/modules/maps/src/maps/gml/GMLSpace.java new file mode 100644 index 0000000000000000000000000000000000000000..a3b4be62bc7a2365328617c59cf0566d2b247dc8 --- /dev/null +++ b/modules/maps/src/maps/gml/GMLSpace.java @@ -0,0 +1,35 @@ +package maps.gml; + +import java.util.List; + +/** + An open space in GML space. +*/ +public class GMLSpace extends GMLShape { + /** + Construct a GMLSpace. + @param id The ID of the space. + */ + public GMLSpace(int id) { + super(id); + } + + /** + Construct a GMLSpace. + @param id The ID of the space. + @param edges The edges of the space. + */ + public GMLSpace(int id, List edges) { + super(id, edges); + } + + /** + Construct a GMLSpace. + @param id The ID of the space. + @param edges The edges of the space. + @param neighbours The neighbours of each edge. + */ + public GMLSpace(int id, List edges, List neighbours) { + super(id, edges, neighbours); + } +} diff --git a/modules/maps/src/maps/gml/GMLTools.java b/modules/maps/src/maps/gml/GMLTools.java new file mode 100644 index 0000000000000000000000000000000000000000..25ed148ad0f67ff3e3f95a5a8ea5519205d8e6f2 --- /dev/null +++ b/modules/maps/src/maps/gml/GMLTools.java @@ -0,0 +1,178 @@ +package maps.gml; + +import java.util.List; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.StringTokenizer; + +import java.awt.geom.Rectangle2D; +import java.awt.geom.Path2D; +import java.awt.Shape; + +import rescuecore2.misc.geometry.Point2D; +import rescuecore2.misc.geometry.Line2D; + +/** + Useful tools for manipulating GML. +*/ +public final class GMLTools { + private GMLTools() { + } + + /** + Turn a list of coordinates into a string suitable for putting into an XML document. + @param coords The coordinate list. + @return A string version of the list. + */ + public static String getCoordinatesString(List coords) { + StringBuilder result = new StringBuilder(); + for (Iterator it = coords.iterator(); it.hasNext();) { + GMLCoordinates next = it.next(); + result.append(String.valueOf(next.getX())); + result.append(","); + result.append(String.valueOf(next.getY())); + if (it.hasNext()) { + result.append(" "); + } + } + return result.toString(); + } + + /** + Turn a coordinates string into a list of GMLCoordinates. + @param coords The coordinates string. + @return A list of GMLCoordinates. + */ + public static List getCoordinatesList(String coords) { + List result = new ArrayList(); + StringTokenizer tokens = new StringTokenizer(coords, " "); + while (tokens.hasMoreTokens()) { + result.add(new GMLCoordinates(tokens.nextToken())); + } + return result; + } + + /** + Convert a list of GMLCoordinates to Point2D objects. + @param coords The GMLCoordinates to convert. + @return A list of Point2D objects. + */ + public static List coordinatesAsPoints(List coords) { + List result = new ArrayList(coords.size()); + for (GMLCoordinates next : coords) { + result.add(new Point2D(next.getX(), next.getY())); + } + return result; + } + + /** + Get the bounds of a set of coordinates. + @param coords The coordinate list. + @return The bounds of the coordinates. + */ + public static Rectangle2D getBounds(List coords) { + if (coords.isEmpty()) { + return null; + } + double minX = Double.POSITIVE_INFINITY; + double minY = Double.POSITIVE_INFINITY; + double maxX = Double.NEGATIVE_INFINITY; + double maxY = Double.NEGATIVE_INFINITY; + for (GMLCoordinates next : coords) { + minX = Math.min(minX, next.getX()); + minY = Math.min(minY, next.getY()); + maxX = Math.max(maxX, next.getX()); + maxY = Math.max(maxY, next.getY()); + } + return new Rectangle2D.Double(minX, minY, maxX - minX, maxY - minY); + } + + /** + Get the bounds of a set of gml objects. + @param objects The object list. + @return The bounds of the objects. + */ + public static Rectangle2D getObjectBounds(List objects) { + Rectangle2D result = null; + for (GMLObject next : objects) { + result = expand(result, next); + } + return result; + } + + /** + Turn a list of coordinates into a shape. + @param coords The coordinates. + @return A new shape. + */ + public static Shape coordsToShape(List coords) { + Path2D path = new Path2D.Double(); + Iterator it = coords.iterator(); + GMLCoordinates c = it.next(); + path.moveTo(c.getX(), c.getY()); + while (it.hasNext()) { + c = it.next(); + path.lineTo(c.getX(), c.getY()); + } + path.closePath(); + return path; + } + + /** + Turn a GMLNode into a Point2D. + @param node The node to convert. + @return A new Point2D. + */ + public static Point2D toPoint(GMLNode node) { + return new Point2D(node.getX(), node.getY()); + } + + /** + Turn a GMLEdge into a Line2D. + @param edge The edge to convert. + @return A new Line2D. + */ + public static Line2D toLine(GMLEdge edge) { + return new Line2D(toPoint(edge.getStart()), toPoint(edge.getEnd())); + } + + private static Rectangle2D expand(Rectangle2D rect, double x, double y) { + if (rect == null) { + return new Rectangle2D.Double(x, y, 0, 0); + } + double newMinX = Math.min(x, rect.getX()); + double newMaxX = Math.max(x, rect.getX() + rect.getWidth()); + double newMinY = Math.min(y, rect.getY()); + double newMaxY = Math.max(y, rect.getY() + rect.getHeight()); + rect.setRect(newMinX, newMinY, newMaxX - newMinX, newMaxY - newMinY); + return rect; + } + + private static Rectangle2D expand(Rectangle2D rect, GMLNode node) { + return expand(rect, node.getX(), node.getY()); + } + + private static Rectangle2D expand(Rectangle2D rect, GMLEdge edge) { + return expand(expand(rect, edge.getStart()), edge.getEnd()); + } + + private static Rectangle2D expand(Rectangle2D rect, GMLShape shape) { + for (GMLDirectedEdge next : shape.getEdges()) { + rect = expand(rect, next.getEdge()); + } + return rect; + } + + private static Rectangle2D expand(Rectangle2D rect, GMLObject object) { + if (object instanceof GMLNode) { + return expand(rect, (GMLNode)object); + } + if (object instanceof GMLEdge) { + return expand(rect, (GMLEdge)object); + } + if (object instanceof GMLShape) { + return expand(rect, (GMLShape)object); + } + return rect; + } +} diff --git a/modules/maps/src/maps/gml/ViewGMLMap.java b/modules/maps/src/maps/gml/ViewGMLMap.java new file mode 100644 index 0000000000000000000000000000000000000000..e4485765b3d027c57a874e3c0057280031546578 --- /dev/null +++ b/modules/maps/src/maps/gml/ViewGMLMap.java @@ -0,0 +1,49 @@ +package maps.gml; + +import maps.MapReader; +import maps.gml.view.GMLMapViewer; + +import java.awt.Dimension; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import javax.swing.JFrame; + +/** + A GML map viewer. +*/ +public final class ViewGMLMap { + private static final int VIEWER_SIZE = 500; + + private ViewGMLMap() { + } + + /** + Start the viewer. + @param args Command-line arguments: mapname. + */ + public static void main(String[] args) { + if (args.length < 1) { + System.out.println("Usage: ViewGMLMap "); + return; + } + try { + GMLMap map = (GMLMap)MapReader.readMap(args[0]); + GMLMapViewer gmlViewer = new GMLMapViewer(map); + JFrame frame = new JFrame("GML Map"); + gmlViewer.setPreferredSize(new Dimension(VIEWER_SIZE, VIEWER_SIZE)); + frame.setContentPane(gmlViewer); + frame.pack(); + frame.setVisible(true); + frame.addWindowListener(new WindowAdapter() { + public void windowClosing(WindowEvent e) { + System.exit(0); + } + }); + } + // CHECKSTYLE:OFF:IllegalCatch + catch (Exception e) { + e.printStackTrace(); + } + // CHECKSTYLE:ON:IllegalCatch + } +} diff --git a/modules/maps/src/maps/gml/debug/GMLDirectedEdgeShapeInfo.java b/modules/maps/src/maps/gml/debug/GMLDirectedEdgeShapeInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..246fb3522023377339c2f04881a4c82a7e112a27 --- /dev/null +++ b/modules/maps/src/maps/gml/debug/GMLDirectedEdgeShapeInfo.java @@ -0,0 +1,47 @@ +package maps.gml.debug; + +import java.awt.Color; + +import rescuecore2.misc.gui.ShapeDebugFrame; + +import rescuecore2.misc.geometry.Point2D; +import rescuecore2.misc.geometry.Line2D; + +import maps.gml.GMLDirectedEdge; +import maps.gml.GMLNode; + +/** + A ShapeInfo that knows how to draw GMLDirectedEdges. +*/ +public class GMLDirectedEdgeShapeInfo extends ShapeDebugFrame.Line2DShapeInfo { + private GMLDirectedEdge edge; + + /** + Create a new GMLDirectedEdgeShapeInfo. + @param edge The directed edge to draw. + @param name The name of the edge. + @param colour The colour to draw the edge. + @param thick Whether to draw the edge thick or not. + @param arrow Whether to draw the direction arrow or not. + */ + public GMLDirectedEdgeShapeInfo(GMLDirectedEdge edge, String name, Color colour, boolean thick, boolean arrow) { + super(gmlDirectedEdgeToLine(edge), name, colour, thick, arrow); + this.edge = edge; + } + + @Override + public Object getObject() { + return edge; + } + + private static Line2D gmlDirectedEdgeToLine(GMLDirectedEdge edge) { + if (edge == null) { + return null; + } + GMLNode start = edge.getStartNode(); + GMLNode end = edge.getEndNode(); + Point2D origin = new Point2D(start.getX(), start.getY()); + Point2D endPoint = new Point2D(end.getX(), end.getY()); + return new Line2D(origin, endPoint); + } +} diff --git a/modules/maps/src/maps/gml/debug/GMLEdgeShapeInfo.java b/modules/maps/src/maps/gml/debug/GMLEdgeShapeInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..621d1fa977c07083310ec5b10ac1c07c8c25fb4f --- /dev/null +++ b/modules/maps/src/maps/gml/debug/GMLEdgeShapeInfo.java @@ -0,0 +1,46 @@ +package maps.gml.debug; + +import java.awt.Color; + +import rescuecore2.misc.gui.ShapeDebugFrame; + +import rescuecore2.misc.geometry.Point2D; +import rescuecore2.misc.geometry.Line2D; + +import maps.gml.GMLEdge; +import maps.gml.GMLNode; + +/** + A ShapeInfo that knows how to draw GMLEdges. +*/ +public class GMLEdgeShapeInfo extends ShapeDebugFrame.Line2DShapeInfo { + private GMLEdge edge; + + /** + Create a new GMLEdgeShapeInfo. + @param edge The edge to draw. + @param name The name of the edge. + @param colour The colour to draw the edge. + @param thick Whether to draw the edge thick or not. + */ + public GMLEdgeShapeInfo(GMLEdge edge, String name, Color colour, boolean thick) { + super(gmlEdgeToLine(edge), name, colour, thick, false); + this.edge = edge; + } + + @Override + public Object getObject() { + return edge; + } + + private static Line2D gmlEdgeToLine(GMLEdge edge) { + if (edge == null) { + return null; + } + GMLNode start = edge.getStart(); + GMLNode end = edge.getEnd(); + Point2D origin = new Point2D(start.getX(), start.getY()); + Point2D endPoint = new Point2D(end.getX(), end.getY()); + return new Line2D(origin, endPoint); + } +} diff --git a/modules/maps/src/maps/gml/debug/GMLNodeShapeInfo.java b/modules/maps/src/maps/gml/debug/GMLNodeShapeInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..31fd623e33da8e929cd550100fc49a356992ab2a --- /dev/null +++ b/modules/maps/src/maps/gml/debug/GMLNodeShapeInfo.java @@ -0,0 +1,37 @@ +package maps.gml.debug; + +import java.awt.Color; + +import rescuecore2.misc.gui.ShapeDebugFrame; + +import rescuecore2.misc.geometry.Point2D; + +import maps.gml.GMLNode; + +/** + A ShapeInfo that knows how to draw GMLNodes. +*/ +public class GMLNodeShapeInfo extends ShapeDebugFrame.Point2DShapeInfo { + private GMLNode node; + + /** + Create a new GMLNodeShapeInfo. + @param node The node to draw. + @param name The name of the node. + @param colour The colour to draw the node. + @param square Whether to draw the node with a square or not. + */ + public GMLNodeShapeInfo(GMLNode node, String name, Color colour, boolean square) { + super(gmlNodeToPoint(node), name, colour, square); + this.node = node; + } + + @Override + public Object getObject() { + return node; + } + + private static Point2D gmlNodeToPoint(GMLNode node) { + return new Point2D(node.getX(), node.getY()); + } +} diff --git a/modules/maps/src/maps/gml/debug/GMLShapeInfo.java b/modules/maps/src/maps/gml/debug/GMLShapeInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..e1d5127ab3624f6b1fbb2b901399663207edc406 --- /dev/null +++ b/modules/maps/src/maps/gml/debug/GMLShapeInfo.java @@ -0,0 +1,92 @@ +package maps.gml.debug; + +import java.awt.Color; +import java.awt.Shape; +import java.awt.Polygon; +import java.awt.Graphics2D; +import java.awt.geom.Rectangle2D; + +import java.util.List; + +import rescuecore2.misc.gui.ScreenTransform; +import rescuecore2.misc.gui.ShapeDebugFrame; + +import maps.gml.GMLShape; +import maps.gml.GMLTools; +import maps.gml.GMLCoordinates; + +/** + A ShapeInfo that knows how to draw GMLShapes. +*/ +public class GMLShapeInfo extends ShapeDebugFrame.ShapeInfo { + private GMLShape shape; + private Color outlineColour; + private Color fillColour; + private Rectangle2D bounds; + + /** + Create a new GMLShapeInfo. + @param shape The shape to draw. + @param name The name of the shape. + @param outlineColour The colour to draw the outline of the shape. This may be null to indicate that the outline should not be painted. + @param fillColour The colour to draw the interior of the shape. This may be null to indicate that the interior should not be painted. + */ + public GMLShapeInfo(GMLShape shape, String name, Color outlineColour, Color fillColour) { + super(shape, name); + this.shape = shape; + this.outlineColour = outlineColour; + this.fillColour = fillColour; + if (shape != null) { + bounds = GMLTools.getBounds(shape.getCoordinates()); + } + } + + @Override + public Shape paint(Graphics2D g, ScreenTransform transform) { + if (shape == null) { + return null; + } + List coordinates = shape.getCoordinates(); + int n = coordinates.size(); + int[] xs = new int[n]; + int[] ys = new int[n]; + int i = 0; + for (GMLCoordinates next : coordinates) { + xs[i] = transform.xToScreen(next.getX()); + ys[i] = transform.yToScreen(next.getY()); + ++i; + } + Polygon p = new Polygon(xs, ys, n); + if (outlineColour != null) { + g.setColor(outlineColour); + g.draw(p); + } + if (fillColour != null) { + g.setColor(fillColour); + g.fill(p); + } + return p; + } + + @Override + public void paintLegend(Graphics2D g, int width, int height) { + if (outlineColour != null) { + g.setColor(outlineColour); + g.drawRect(0, 0, width - 1, height - 1); + } + if (fillColour != null) { + g.setColor(fillColour); + g.fillRect(0, 0, width, height); + } + } + + @Override + public Rectangle2D getBoundsShape() { + return bounds; + } + + @Override + public java.awt.geom.Point2D getBoundsPoint() { + return null; + } +} diff --git a/modules/maps/src/maps/gml/editor/AbstractFunction.java b/modules/maps/src/maps/gml/editor/AbstractFunction.java new file mode 100644 index 0000000000000000000000000000000000000000..3716cf9ca98297a29edaf81868c029f311ce73c8 --- /dev/null +++ b/modules/maps/src/maps/gml/editor/AbstractFunction.java @@ -0,0 +1,17 @@ +package maps.gml.editor; + +/** + Abstract base class for Function implementations. +*/ +public abstract class AbstractFunction implements Function { + /** The GMLEditor instance. */ + protected GMLEditor editor; + + /** + Construct an AbstractFunction. + @param editor The editor instance. + */ + protected AbstractFunction(GMLEditor editor) { + this.editor = editor; + } +} \ No newline at end of file diff --git a/modules/maps/src/maps/gml/editor/AbstractTool.java b/modules/maps/src/maps/gml/editor/AbstractTool.java new file mode 100644 index 0000000000000000000000000000000000000000..21fef75d1a90db4cf63882cd9d2ebfd59a7183a4 --- /dev/null +++ b/modules/maps/src/maps/gml/editor/AbstractTool.java @@ -0,0 +1,17 @@ +package maps.gml.editor; + +/** + Abstract base class for Tool implementations. +*/ +public abstract class AbstractTool implements Tool { + /** The GMLEditor instance. */ + protected GMLEditor editor; + + /** + Construct an AbstractTool. + @param editor The editor instance. + */ + protected AbstractTool(GMLEditor editor) { + this.editor = editor; + } +} \ No newline at end of file diff --git a/modules/maps/src/maps/gml/editor/AddNoiseFunction.java b/modules/maps/src/maps/gml/editor/AddNoiseFunction.java new file mode 100644 index 0000000000000000000000000000000000000000..fffd09cfb41b5250c5e7131e433dab8b3bf281df --- /dev/null +++ b/modules/maps/src/maps/gml/editor/AddNoiseFunction.java @@ -0,0 +1,52 @@ +package maps.gml.editor; + +import maps.gml.GMLNode; +import maps.gml.GMLCoordinates; + +import java.util.Random; + +import org.uncommons.maths.random.DiscreteUniformGenerator; +import org.uncommons.maths.random.MersenneTwisterRNG; + +/** + A function for adding noise to node coordinates. +*/ +public class AddNoiseFunction extends ProgressFunction { + private static final int RANGE = 5; + private static final double FACTOR = 0.001; + + private Random random; + + /** + Construct an AddNoiseFunction. + @param editor The editor instance. + */ + public AddNoiseFunction(GMLEditor editor) { + super(editor); + random = new MersenneTwisterRNG(); + } + + @Override + public String getName() { + return "Add noise"; + } + + @Override + protected String getTitle() { + return "Adding noise"; + } + + @Override + protected void executeImpl() { + DiscreteUniformGenerator generator = new DiscreteUniformGenerator(-RANGE, RANGE, random); + setProgressLimit(editor.getMap().getNodes().size()); + for (GMLNode next : editor.getMap().getNodes()) { + GMLCoordinates c = next.getCoordinates(); + c.setX(c.getX() + (generator.nextValue() * FACTOR)); + c.setY(c.getY() + (generator.nextValue() * FACTOR)); + bumpProgress(); + } + editor.setChanged(); + editor.getViewer().repaint(); + } +} \ No newline at end of file diff --git a/modules/maps/src/maps/gml/editor/CancelledByUserException.java b/modules/maps/src/maps/gml/editor/CancelledByUserException.java new file mode 100644 index 0000000000000000000000000000000000000000..ec539fc794400fb83f9809722dad1616b6a78620 --- /dev/null +++ b/modules/maps/src/maps/gml/editor/CancelledByUserException.java @@ -0,0 +1,11 @@ +package maps.gml.editor; + +/** + Exception for indicating the the user has cancelled an operation. +*/ +public class CancelledByUserException extends Exception { + /** + Constructor. + */ + public CancelledByUserException() {} +} \ No newline at end of file diff --git a/modules/maps/src/maps/gml/editor/ComputePassableEdgesFunction.java b/modules/maps/src/maps/gml/editor/ComputePassableEdgesFunction.java new file mode 100644 index 0000000000000000000000000000000000000000..047a54800c775b529fd456a4d230acc13e2d165a --- /dev/null +++ b/modules/maps/src/maps/gml/editor/ComputePassableEdgesFunction.java @@ -0,0 +1,88 @@ +package maps.gml.editor; + +import java.util.Collection; +import java.util.List; +import java.util.Iterator; + +import maps.gml.GMLEdge; +import maps.gml.GMLDirectedEdge; +import maps.gml.GMLShape; +import maps.gml.GMLRoad; + +import rescuecore2.log.Logger; + +/** + A function for computing passable edges. +*/ +public class ComputePassableEdgesFunction extends ProgressFunction { + /** + Construct a ComputePassableEdgesFunction. + @param editor The editor instance. + */ + public ComputePassableEdgesFunction(GMLEditor editor) { + super(editor); + } + + @Override + public String getName() { + return "Compute passable edges"; + } + + @Override + protected String getTitle() { + return "Finding neighbours"; + } + + @Override + protected void executeImpl() { + final Collection edges = editor.getMap().getEdges(); + setProgressLimit(edges.size()); + int passable = 0; + int impassable = 0; + for (GMLEdge next : edges) { + Collection shapes = editor.getMap().getAttachedShapes(next); + if (shapes.size() == 2) { + Iterator it = shapes.iterator(); + GMLShape first = it.next(); + GMLShape second = it.next(); + if (first instanceof GMLRoad || second instanceof GMLRoad + || next.isPassable()) { + next.setPassable(true); + GMLDirectedEdge firstEdge = findDirectedEdge(first.getEdges(), next); + GMLDirectedEdge secondEdge = findDirectedEdge(second.getEdges(), next); + first.setNeighbour(firstEdge, second.getID()); + second.setNeighbour(secondEdge, first.getID()); + ++passable; + } + else { + makeImpassable(next, shapes); + ++impassable; + } + } + else { + makeImpassable(next, shapes); + ++impassable; + } + bumpProgress(); + } + editor.setChanged(); + editor.getViewer().repaint(); + Logger.debug("Made " + passable + " edges passable and " + impassable + " impassable"); + } + + private void makeImpassable(GMLEdge edge, Collection attached) { + edge.setPassable(false); + for (GMLShape shape : attached) { + shape.setNeighbour(edge, null); + } + } + + private GMLDirectedEdge findDirectedEdge(List possible, GMLEdge target) { + for (GMLDirectedEdge next : possible) { + if (next.getEdge() == target) { + return next; + } + } + return null; + } +} \ No newline at end of file diff --git a/modules/maps/src/maps/gml/editor/CreateBuildingTool.java b/modules/maps/src/maps/gml/editor/CreateBuildingTool.java new file mode 100644 index 0000000000000000000000000000000000000000..c31fbdb0e51104fb59c93ae785dcf001d29dbed1 --- /dev/null +++ b/modules/maps/src/maps/gml/editor/CreateBuildingTool.java @@ -0,0 +1,55 @@ +package maps.gml.editor; + +import javax.swing.undo.AbstractUndoableEdit; +import javax.swing.undo.UndoableEdit; + +import java.util.List; + +import maps.gml.GMLNode; +import maps.gml.GMLBuilding; + +/** + A tool for creating buildings. +*/ +public class CreateBuildingTool extends CreateShapeTool { + /** + Construct a CreateBuildingTool. + @param editor The editor instance. + */ + public CreateBuildingTool(GMLEditor editor) { + super(editor); + } + + @Override + public String getName() { + return "Create building"; + } + + @Override + protected UndoableEdit finished(List nodes) { + GMLBuilding building = editor.getMap().createBuildingFromNodes(nodes); + return new CreateBuildingEdit(building); + } + + private class CreateBuildingEdit extends AbstractUndoableEdit { + private GMLBuilding building; + + public CreateBuildingEdit(GMLBuilding building) { + this.building = building; + } + + @Override + public void undo() { + super.undo(); + editor.getMap().removeBuilding(building); + editor.getViewer().repaint(); + } + + @Override + public void redo() { + super.redo(); + editor.getMap().addBuilding(building); + editor.getViewer().repaint(); + } + } +} \ No newline at end of file diff --git a/modules/maps/src/maps/gml/editor/CreateEdgeTool.java b/modules/maps/src/maps/gml/editor/CreateEdgeTool.java new file mode 100644 index 0000000000000000000000000000000000000000..97b2418033c8818d6a1ab46c6dee86bcf5cc9dab --- /dev/null +++ b/modules/maps/src/maps/gml/editor/CreateEdgeTool.java @@ -0,0 +1,202 @@ +package maps.gml.editor; + +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.event.MouseMotionListener; +import java.awt.Color; +import java.awt.Point; +import java.awt.Insets; + +import javax.swing.undo.AbstractUndoableEdit; + +import maps.gml.view.NodeDecorator; +import maps.gml.view.SquareNodeDecorator; +import maps.gml.view.LineOverlay; +import maps.gml.GMLNode; +import maps.gml.GMLEdge; +import maps.gml.GMLCoordinates; + +import rescuecore2.misc.geometry.Point2D; + +/** + A tool for creating edges. +*/ +public class CreateEdgeTool extends AbstractTool { + private static final Color HIGHLIGHT_COLOUR = Color.BLUE; + private static final int HIGHLIGHT_SIZE = 6; + + private Listener listener; + private NodeDecorator nodeHighlight; + private LineOverlay overlay; + + private GMLNode hover; + private GMLNode start; + private GMLNode end; + // private GMLEdge edge; + + /** + Construct a CreateEdgeTool. + @param editor The editor instance. + */ + public CreateEdgeTool(GMLEditor editor) { + super(editor); + listener = new Listener(); + nodeHighlight = new SquareNodeDecorator(HIGHLIGHT_COLOUR, HIGHLIGHT_SIZE); + overlay = new LineOverlay(HIGHLIGHT_COLOUR, true); + } + + @Override + public String getName() { + return "Create edge"; + } + + @Override + public void activate() { + editor.getViewer().addMouseListener(listener); + editor.getViewer().addMouseMotionListener(listener); + editor.getViewer().addOverlay(overlay); + hover = null; + start = null; + end = null; + // edge = null; + } + + @Override + public void deactivate() { + editor.getViewer().removeMouseListener(listener); + editor.getViewer().removeMouseMotionListener(listener); + editor.getViewer().clearAllNodeDecorators(); + editor.getViewer().removeOverlay(overlay); + editor.getViewer().repaint(); + } + + private void setHover(GMLNode node) { + if (hover == node) { + return; + } + if (hover != null) { + editor.getViewer().clearNodeDecorator(hover); + } + hover = node; + if (hover != null) { + editor.getViewer().setNodeDecorator(nodeHighlight, hover); + } + editor.getViewer().repaint(); + } + + private void setStart(GMLNode node) { + if (start == node) { + return; + } + if (start != null) { + editor.getViewer().clearNodeDecorator(start); + } + start = node; + if (start != null) { + editor.getViewer().setNodeDecorator(nodeHighlight, start); + } + editor.getViewer().repaint(); + } + + private void setEnd(GMLNode node) { + if (start == node || end == node) { + return; + } + if (end != null) { + editor.getViewer().clearNodeDecorator(end); + } + end = node; + if (end != null) { + editor.getViewer().setNodeDecorator(nodeHighlight, end); + } + editor.getViewer().repaint(); + } + + private class Listener implements MouseListener, MouseMotionListener { + @Override + public void mousePressed(MouseEvent e) { + if (e.getButton() == MouseEvent.BUTTON1) { + Point p = fixEventPoint(e.getPoint()); + GMLCoordinates c = editor.getViewer().getCoordinatesAtPoint(p.x, p.y); + GMLNode node = editor.getMap().findNearestNode(c.getX(), c.getY()); + overlay.setStart(new Point2D(node.getX(), node.getY())); + setStart(node); + setHover(null); + } + } + + @Override + public void mouseReleased(MouseEvent e) { + if (e.getButton() == MouseEvent.BUTTON1) { + if (start != null && end != null) { + GMLEdge edge = editor.getMap().createEdge(start, end); + editor.setChanged(); + editor.addEdit(new CreateEdgeEdit(edge)); + editor.getViewer().clearAllNodeDecorators(); + overlay.setStart(null); + overlay.setEnd(null); + editor.getViewer().repaint(); + start = null; + end = null; + hover = null; + } + } + } + + @Override + public void mouseDragged(MouseEvent e) { + if (start != null) { + Point p = fixEventPoint(e.getPoint()); + GMLCoordinates c = editor.getViewer().getCoordinatesAtPoint(p.x, p.y); + GMLNode node = editor.getMap().findNearestNode(c.getX(), c.getY()); + overlay.setEnd(new Point2D(node.getX(), node.getY())); + setEnd(node); + } + } + + @Override + public void mouseMoved(MouseEvent e) { + Point p = fixEventPoint(e.getPoint()); + GMLCoordinates c = editor.snap(editor.getViewer().getCoordinatesAtPoint(p.x, p.y)); + GMLNode node = editor.getMap().findNearestNode(c.getX(), c.getY()); + setHover(node); + } + + @Override + public void mouseClicked(MouseEvent e) { + } + @Override + public void mouseEntered(MouseEvent e) { + } + @Override + public void mouseExited(MouseEvent e) { + } + + private Point fixEventPoint(Point p) { + Insets insets = editor.getViewer().getInsets(); + return new Point(p.x - insets.left, p.y - insets.top); + } + } + + private class CreateEdgeEdit extends AbstractUndoableEdit { + private GMLEdge edge; + + public CreateEdgeEdit(GMLEdge edge) { + this.edge = edge; + } + + @Override + public void undo() { + super.undo(); + editor.getMap().removeEdge(edge); + editor.getViewer().repaint(); + } + + @Override + public void redo() { + super.redo(); + editor.getMap().addEdge(edge); + editor.getViewer().repaint(); + } + } +} \ No newline at end of file diff --git a/modules/maps/src/maps/gml/editor/CreateNodeTool.java b/modules/maps/src/maps/gml/editor/CreateNodeTool.java new file mode 100644 index 0000000000000000000000000000000000000000..a6ff18ead1b7cc3e928fc8b43b29d0857236b32f --- /dev/null +++ b/modules/maps/src/maps/gml/editor/CreateNodeTool.java @@ -0,0 +1,105 @@ +package maps.gml.editor; + +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.event.MouseMotionListener; +import java.awt.Point; +import java.awt.Insets; + +import javax.swing.undo.AbstractUndoableEdit; + +import maps.gml.GMLNode; +import maps.gml.GMLCoordinates; + +/** + A tool for creating nodes. +*/ +public class CreateNodeTool extends AbstractTool { + private Listener listener; + + /** + Construct a CreateNodeTool. + @param editor The editor instance. + */ + public CreateNodeTool(GMLEditor editor) { + super(editor); + listener = new Listener(); + } + + @Override + public String getName() { + return "Create node"; + } + + @Override + public void activate() { + editor.getViewer().addMouseListener(listener); + editor.getViewer().addMouseMotionListener(listener); + } + + @Override + public void deactivate() { + editor.getViewer().removeMouseListener(listener); + editor.getViewer().removeMouseMotionListener(listener); + } + + private class Listener implements MouseListener, MouseMotionListener { + @Override + public void mouseClicked(MouseEvent e) { + if (e.getButton() == MouseEvent.BUTTON1) { + Point p = fixEventPoint(e.getPoint()); + GMLCoordinates c = editor.snap(editor.getViewer().getCoordinatesAtPoint(p.x, p.y)); + GMLNode node = editor.getMap().createNode(c); + editor.setChanged(); + editor.addEdit(new CreateNodeEdit(node)); + editor.getViewer().repaint(); + } + } + + @Override + public void mouseMoved(MouseEvent e) { + } + @Override + public void mousePressed(MouseEvent e) { + } + @Override + public void mouseReleased(MouseEvent e) { + } + @Override + public void mouseDragged(MouseEvent e) { + } + @Override + public void mouseEntered(MouseEvent e) { + } + @Override + public void mouseExited(MouseEvent e) { + } + + private Point fixEventPoint(Point p) { + Insets insets = editor.getViewer().getInsets(); + return new Point(p.x - insets.left, p.y - insets.top); + } + } + + private class CreateNodeEdit extends AbstractUndoableEdit { + private GMLNode node; + + public CreateNodeEdit(GMLNode node) { + this.node = node; + } + + @Override + public void undo() { + super.undo(); + editor.getMap().removeNode(node); + editor.getViewer().repaint(); + } + + @Override + public void redo() { + super.redo(); + editor.getMap().addNode(node); + editor.getViewer().repaint(); + } + } +} \ No newline at end of file diff --git a/modules/maps/src/maps/gml/editor/CreateRoadTool.java b/modules/maps/src/maps/gml/editor/CreateRoadTool.java new file mode 100644 index 0000000000000000000000000000000000000000..df52814bf261b61d01a0bf5fa06f48ec517e34fd --- /dev/null +++ b/modules/maps/src/maps/gml/editor/CreateRoadTool.java @@ -0,0 +1,55 @@ +package maps.gml.editor; + +import javax.swing.undo.UndoableEdit; +import javax.swing.undo.AbstractUndoableEdit; + +import java.util.List; + +import maps.gml.GMLNode; +import maps.gml.GMLRoad; + +/** + A tool for creating roads. +*/ +public class CreateRoadTool extends CreateShapeTool { + /** + Construct a CreateRoadTool. + @param editor The editor instance. + */ + public CreateRoadTool(GMLEditor editor) { + super(editor); + } + + @Override + public String getName() { + return "Create road"; + } + + @Override + protected UndoableEdit finished(List nodes) { + GMLRoad road = editor.getMap().createRoadFromNodes(nodes); + return new CreateRoadEdit(road); + } + + private class CreateRoadEdit extends AbstractUndoableEdit { + private GMLRoad road; + + public CreateRoadEdit(GMLRoad road) { + this.road = road; + } + + @Override + public void undo() { + super.undo(); + editor.getMap().removeRoad(road); + editor.getViewer().repaint(); + } + + @Override + public void redo() { + super.redo(); + editor.getMap().addRoad(road); + editor.getViewer().repaint(); + } + } +} \ No newline at end of file diff --git a/modules/maps/src/maps/gml/editor/CreateShapeTool.java b/modules/maps/src/maps/gml/editor/CreateShapeTool.java new file mode 100644 index 0000000000000000000000000000000000000000..51ee18126aadb230457ca732e2f8b0ce92ac9dcc --- /dev/null +++ b/modules/maps/src/maps/gml/editor/CreateShapeTool.java @@ -0,0 +1,205 @@ +package maps.gml.editor; + +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.event.MouseMotionListener; +import java.awt.Color; +import java.awt.Point; +import java.awt.Insets; + +import javax.swing.undo.UndoableEdit; + +import java.util.List; +import java.util.ArrayList; +import java.util.Set; +import java.util.HashSet; + +import maps.gml.view.EdgeDecorator; +import maps.gml.view.LineEdgeDecorator; +import maps.gml.GMLNode; +import maps.gml.GMLEdge; +import maps.gml.GMLCoordinates; + +/** + A tool for creating shapes. +*/ +public abstract class CreateShapeTool extends AbstractTool { + private static final Color HOVER_COLOUR = Color.BLUE; + private static final Color SELECTED_COLOUR = Color.GREEN; + private static final Color POSSIBLE_COLOUR = Color.WHITE; + + private Listener listener; + private EdgeDecorator hoverHighlight; + private EdgeDecorator selectedHighlight; + private EdgeDecorator possibleHighlight; + + private List edges; + private List nodes; + private Set possible; + private GMLNode startNode; + private GMLNode currentNode; + private GMLEdge hover; + + /** + Construct a CreateShapeTool. + @param editor The editor instance. + */ + protected CreateShapeTool(GMLEditor editor) { + super(editor); + listener = new Listener(); + hoverHighlight = new LineEdgeDecorator(HOVER_COLOUR); + selectedHighlight = new LineEdgeDecorator(SELECTED_COLOUR); + possibleHighlight = new LineEdgeDecorator(POSSIBLE_COLOUR); + edges = new ArrayList(); + nodes = new ArrayList(); + possible = new HashSet(); + } + + @Override + public void activate() { + editor.getViewer().addMouseListener(listener); + editor.getViewer().addMouseMotionListener(listener); + clearData(); + } + + @Override + public void deactivate() { + editor.getViewer().removeMouseListener(listener); + editor.getViewer().removeMouseMotionListener(listener); + editor.getViewer().clearAllEdgeDecorators(); + editor.getViewer().repaint(); + clearData(); + } + + /** + Perform whatever shape creation tasks are needed once the shape has been closed. + @param shapeNodes The nodes of the shape. + @return An UndoableEdit for the change. + */ + protected abstract UndoableEdit finished(List shapeNodes); + + private void addEdge(GMLEdge edge) { + editor.getViewer().clearEdgeDecorator(possible); + edges.add(edge); + possible.clear(); + editor.getViewer().setEdgeDecorator(selectedHighlight, edge); + editor.getViewer().repaint(); + if (edges.size() == 1) { + startNode = edge.getStart(); + currentNode = edge.getEnd(); + possible.addAll(editor.getMap().getAttachedEdges(startNode)); + possible.addAll(editor.getMap().getAttachedEdges(currentNode)); + } + else if (edges.size() == 2) { + // Find the shared node + GMLEdge first = edges.get(0); + GMLEdge second = edges.get(1); + GMLNode shared; + if (first.getStart().equals(second.getStart()) || first.getStart().equals(second.getEnd())) { + startNode = first.getEnd(); + shared = first.getStart(); + } + else { + startNode = first.getStart(); + shared = first.getEnd(); + } + currentNode = shared.equals(second.getStart()) ? second.getEnd() : second.getStart(); + nodes.add(startNode); + nodes.add(shared); + nodes.add(currentNode); + possible.addAll(editor.getMap().getAttachedEdges(currentNode)); + } + else if (edges.size() > 2) { + // Update end node + currentNode = currentNode.equals(edge.getStart()) ? edge.getEnd() : edge.getStart(); + if (currentNode.equals(startNode)) { + // We're done + editor.addEdit(finished(nodes)); + editor.setChanged(); + clearData(); + editor.getViewer().clearAllEdgeDecorators(); + editor.getViewer().repaint(); + } + else { + nodes.add(currentNode); + possible.addAll(editor.getMap().getAttachedEdges(currentNode)); + } + } + possible.removeAll(edges); + editor.getViewer().setEdgeDecorator(possibleHighlight, possible); + if (possible.size() == 1) { + addEdge(possible.iterator().next()); + } + editor.getViewer().repaint(); + } + + private void clearData() { + nodes.clear(); + edges.clear(); + possible.clear(); + startNode = null; + currentNode = null; + hover = null; + } + + private void hover(GMLEdge edge) { + if (hover == edge) { + return; + } + if (hover != null) { + editor.getViewer().clearEdgeDecorator(hover); + if (possible.contains(hover)) { + editor.getViewer().setEdgeDecorator(possibleHighlight, hover); + } + } + hover = edge; + if (hover != null) { + editor.getViewer().setEdgeDecorator(hoverHighlight, hover); + } + editor.getViewer().repaint(); + } + + private class Listener implements MouseListener, MouseMotionListener { + @Override + public void mousePressed(MouseEvent e) { + } + @Override + public void mouseReleased(MouseEvent e) { + } + @Override + public void mouseDragged(MouseEvent e) { + } + + @Override + public void mouseMoved(MouseEvent e) { + Point p = fixEventPoint(e.getPoint()); + GMLCoordinates c = editor.snap(editor.getViewer().getCoordinatesAtPoint(p.x, p.y)); + if (edges.isEmpty()) { + hover(editor.getMap().findNearestEdge(c.getX(), c.getY())); + } + else { + hover(editor.getMap().findNearestEdge(c.getX(), c.getY(), possible)); + } + } + + @Override + public void mouseClicked(MouseEvent e) { + if (hover != null && e.getButton() == MouseEvent.BUTTON1) { + GMLEdge edge = hover; + hover(null); + addEdge(edge); + } + } + @Override + public void mouseEntered(MouseEvent e) { + } + @Override + public void mouseExited(MouseEvent e) { + } + + private Point fixEventPoint(Point p) { + Insets insets = editor.getViewer().getInsets(); + return new Point(p.x - insets.left, p.y - insets.top); + } + } +} \ No newline at end of file diff --git a/modules/maps/src/maps/gml/editor/CreateSpaceTool.java b/modules/maps/src/maps/gml/editor/CreateSpaceTool.java new file mode 100644 index 0000000000000000000000000000000000000000..e92c7092273a2e48ee6cb9e86dd7cc932157744c --- /dev/null +++ b/modules/maps/src/maps/gml/editor/CreateSpaceTool.java @@ -0,0 +1,55 @@ +package maps.gml.editor; + +import javax.swing.undo.AbstractUndoableEdit; +import javax.swing.undo.UndoableEdit; + +import java.util.List; + +import maps.gml.GMLNode; +import maps.gml.GMLSpace; + +/** + A tool for creating spaces. +*/ +public class CreateSpaceTool extends CreateShapeTool { + /** + Construct a CreateSpaceTool. + @param editor The editor instance. + */ + public CreateSpaceTool(GMLEditor editor) { + super(editor); + } + + @Override + public String getName() { + return "Create space"; + } + + @Override + protected UndoableEdit finished(List nodes) { + GMLSpace space = editor.getMap().createSpaceFromNodes(nodes); + return new CreateSpaceEdit(space); + } + + private class CreateSpaceEdit extends AbstractUndoableEdit { + private GMLSpace space; + + public CreateSpaceEdit(GMLSpace space) { + this.space = space; + } + + @Override + public void undo() { + super.undo(); + editor.getMap().removeSpace(space); + editor.getViewer().repaint(); + } + + @Override + public void redo() { + super.redo(); + editor.getMap().addSpace(space); + editor.getViewer().repaint(); + } + } +} \ No newline at end of file diff --git a/modules/maps/src/maps/gml/editor/DeleteEdgeTool.java b/modules/maps/src/maps/gml/editor/DeleteEdgeTool.java new file mode 100644 index 0000000000000000000000000000000000000000..9370cf4d4cf62794cc051acf7cd2ccdeff4cddd0 --- /dev/null +++ b/modules/maps/src/maps/gml/editor/DeleteEdgeTool.java @@ -0,0 +1,139 @@ +package maps.gml.editor; + +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.event.MouseMotionListener; +import java.awt.Color; +import java.awt.Point; +import java.awt.Insets; + +import javax.swing.undo.AbstractUndoableEdit; + +import maps.gml.view.EdgeDecorator; +import maps.gml.view.LineEdgeDecorator; +import maps.gml.GMLEdge; +import maps.gml.GMLCoordinates; +import maps.gml.GMLObject; + +import java.util.Collection; + +/** + A tool for deleting edges. +*/ +public class DeleteEdgeTool extends AbstractTool { + private static final Color HIGHLIGHT_COLOUR = Color.BLUE; + + private Listener listener; + private EdgeDecorator edgeHighlight; + + private GMLEdge edge; + + /** + Construct a DeleteEdgeTool. + @param editor The editor instance. + */ + public DeleteEdgeTool(GMLEditor editor) { + super(editor); + listener = new Listener(); + edgeHighlight = new LineEdgeDecorator(HIGHLIGHT_COLOUR); + } + + @Override + public String getName() { + return "Delete edge"; + } + + @Override + public void activate() { + editor.getViewer().addMouseListener(listener); + editor.getViewer().addMouseMotionListener(listener); + edge = null; + } + + @Override + public void deactivate() { + editor.getViewer().removeMouseListener(listener); + editor.getViewer().removeMouseMotionListener(listener); + editor.getViewer().clearAllEdgeDecorators(); + editor.getViewer().repaint(); + } + + private void highlightEdge(GMLEdge newEdge) { + if (edge == newEdge) { + return; + } + if (edge != null) { + editor.getViewer().clearEdgeDecorator(edge); + } + edge = newEdge; + if (edge != null) { + editor.getViewer().setEdgeDecorator(edgeHighlight, edge); + } + editor.getViewer().repaint(); + } + + private class Listener implements MouseListener, MouseMotionListener { + @Override + public void mouseClicked(MouseEvent e) { + if (e.getButton() == MouseEvent.BUTTON1) { + Collection deleted = editor.getMap().removeEdge(edge); + editor.getViewer().repaint(); + editor.setChanged(); + editor.addEdit(new DeleteEdgeEdit(edge, deleted)); + } + } + + @Override + public void mouseMoved(MouseEvent e) { + Point p = fixEventPoint(e.getPoint()); + GMLCoordinates c = editor.snap(editor.getViewer().getCoordinatesAtPoint(p.x, p.y)); + highlightEdge(editor.getMap().findNearestEdge(c.getX(), c.getY())); + } + + @Override + public void mousePressed(MouseEvent e) { + } + @Override + public void mouseReleased(MouseEvent e) { + } + @Override + public void mouseDragged(MouseEvent e) { + } + @Override + public void mouseEntered(MouseEvent e) { + } + @Override + public void mouseExited(MouseEvent e) { + } + + private Point fixEventPoint(Point p) { + Insets insets = editor.getViewer().getInsets(); + return new Point(p.x - insets.left, p.y - insets.top); + } + } + + private class DeleteEdgeEdit extends AbstractUndoableEdit { + private GMLEdge edge; + private Collection deleted; + + public DeleteEdgeEdit(GMLEdge edge, Collection deleted) { + this.edge = edge; + this.deleted = deleted; + } + + @Override + public void undo() { + super.undo(); + editor.getMap().addEdge(edge); + editor.getMap().add(deleted); + editor.getViewer().repaint(); + } + + @Override + public void redo() { + super.redo(); + editor.getMap().removeEdge(edge); + editor.getViewer().repaint(); + } + } +} \ No newline at end of file diff --git a/modules/maps/src/maps/gml/editor/DeleteNodeTool.java b/modules/maps/src/maps/gml/editor/DeleteNodeTool.java new file mode 100644 index 0000000000000000000000000000000000000000..219634285db580e28359dc4e01de997d6cfe6e10 --- /dev/null +++ b/modules/maps/src/maps/gml/editor/DeleteNodeTool.java @@ -0,0 +1,250 @@ +package maps.gml.editor; + +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.event.MouseMotionListener; +import java.awt.Color; +import java.awt.Point; +import java.awt.Insets; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Map; +import java.util.HashMap; + +import maps.gml.view.NodeDecorator; +import maps.gml.view.SquareNodeDecorator; +import maps.gml.view.EdgeDecorator; +import maps.gml.view.LineEdgeDecorator; +import maps.gml.view.RectangleOverlay; +import maps.gml.GMLNode; +import maps.gml.GMLEdge; +import maps.gml.GMLCoordinates; +import maps.gml.GMLObject; + +import javax.swing.undo.AbstractUndoableEdit; + +/** + A tool for deleting nodes. +*/ +public class DeleteNodeTool extends AbstractTool { + private static final Color HIGHLIGHT_COLOUR = Color.BLUE; + private static final int HIGHLIGHT_SIZE = 6; + + private static final Color OVERLAY_COLOUR = new Color(0, 0, 128, 128); + + private Listener listener; + private NodeDecorator nodeHighlight; + private EdgeDecorator edgeHighlight; + private GMLNode selected; + private Collection attachedEdges; + + private GMLCoordinates pressPoint; + private GMLCoordinates dragPoint; + + private RectangleOverlay overlay; + + /** + Construct a DeleteNodeTool. + @param editor The editor instance. + */ + public DeleteNodeTool(GMLEditor editor) { + super(editor); + listener = new Listener(); + nodeHighlight = new SquareNodeDecorator(HIGHLIGHT_COLOUR, HIGHLIGHT_SIZE); + edgeHighlight = new LineEdgeDecorator(HIGHLIGHT_COLOUR); + selected = null; + attachedEdges = new HashSet(); + overlay = new RectangleOverlay(OVERLAY_COLOUR, true); + } + + @Override + public String getName() { + return "Delete node"; + } + + @Override + public void activate() { + editor.getViewer().addMouseListener(listener); + editor.getViewer().addMouseMotionListener(listener); + selected = null; + attachedEdges.clear(); + } + + @Override + public void deactivate() { + editor.getViewer().removeMouseListener(listener); + editor.getViewer().removeMouseMotionListener(listener); + editor.getViewer().clearAllNodeDecorators(); + editor.getViewer().clearAllEdgeDecorators(); + editor.getViewer().removeOverlay(overlay); + editor.getViewer().repaint(); + } + + private void highlightNode(GMLNode node) { + if (selected == node) { + return; + } + if (selected != null) { + editor.getViewer().clearNodeDecorator(selected); + editor.getViewer().clearEdgeDecorator(attachedEdges); + } + selected = node; + attachedEdges.clear(); + if (selected != null) { + attachedEdges.addAll(editor.getMap().getAttachedEdges(selected)); + editor.getViewer().setNodeDecorator(nodeHighlight, selected); + editor.getViewer().setEdgeDecorator(edgeHighlight, attachedEdges); + } + editor.getViewer().repaint(); + } + + private void removeNodes() { + double xMin = Math.min(pressPoint.getX(), dragPoint.getX()); + double xMax = Math.max(pressPoint.getX(), dragPoint.getX()); + double yMin = Math.min(pressPoint.getY(), dragPoint.getY()); + double yMax = Math.max(pressPoint.getY(), dragPoint.getY()); + Collection nodes = editor.getMap().getNodesInRegion(xMin, yMin, xMax, yMax); + Map> deleted = new HashMap>(); + for (GMLNode next : nodes) { + deleted.put(next, editor.getMap().removeNode(next)); + } + editor.getViewer().repaint(); + editor.setChanged(); + editor.addEdit(new DeleteNodesEdit(nodes, deleted)); + } + + private class Listener implements MouseListener, MouseMotionListener { + @Override + public void mouseMoved(MouseEvent e) { + Point p = fixEventPoint(e.getPoint()); + GMLCoordinates c = editor.getViewer().getCoordinatesAtPoint(p.x, p.y); + GMLNode node = editor.getMap().findNearestNode(c.getX(), c.getY()); + highlightNode(node); + } + + @Override + public void mouseClicked(MouseEvent e) { + if (selected == null) { + return; + } + if (e.getButton() == MouseEvent.BUTTON1) { + Collection deleted = editor.getMap().removeNode(selected); + editor.getViewer().repaint(); + editor.setChanged(); + editor.addEdit(new DeleteNodeEdit(selected, deleted)); + } + } + + @Override + public void mousePressed(MouseEvent e) { + if (e.getButton() == MouseEvent.BUTTON1) { + Point p = fixEventPoint(e.getPoint()); + pressPoint = editor.getViewer().getCoordinatesAtPoint(p.x, p.y); + overlay.setLeft(pressPoint.getX()); + overlay.setBottom(pressPoint.getY()); + editor.getViewer().addOverlay(overlay); + editor.getViewer().repaint(); + } + } + + @Override + public void mouseReleased(MouseEvent e) { + if (e.getButton() == MouseEvent.BUTTON1) { + Point p = fixEventPoint(e.getPoint()); + dragPoint = editor.getViewer().getCoordinatesAtPoint(p.x, p.y); + overlay.setLeft(Double.NaN); + overlay.setRight(Double.NaN); + overlay.setBottom(Double.NaN); + overlay.setTop(Double.NaN); + editor.getViewer().removeOverlay(overlay); + editor.getViewer().repaint(); + removeNodes(); + pressPoint = null; + dragPoint = null; + } + } + + @Override + public void mouseDragged(MouseEvent e) { + if (pressPoint != null) { + Point p = fixEventPoint(e.getPoint()); + dragPoint = editor.getViewer().getCoordinatesAtPoint(p.x, p.y); + overlay.setRight(dragPoint.getX()); + overlay.setTop(dragPoint.getY()); + editor.getViewer().repaint(); + highlightNode(null); + } + } + + @Override + public void mouseEntered(MouseEvent e) { + } + @Override + public void mouseExited(MouseEvent e) { + } + + private Point fixEventPoint(Point p) { + Insets insets = editor.getViewer().getInsets(); + return new Point(p.x - insets.left, p.y - insets.top); + } + } + + private class DeleteNodeEdit extends AbstractUndoableEdit { + private GMLNode node; + private Collection deletedObjects; + + public DeleteNodeEdit(GMLNode node, Collection deletedObjects) { + this.node = node; + this.deletedObjects = deletedObjects; + } + + @Override + public void undo() { + super.undo(); + editor.getMap().addNode(node); + editor.getMap().add(deletedObjects); + editor.getViewer().repaint(); + } + + @Override + public void redo() { + super.redo(); + editor.getMap().removeNode(node); + editor.getMap().remove(deletedObjects); + editor.getViewer().repaint(); + } + } + + private class DeleteNodesEdit extends AbstractUndoableEdit { + private Collection nodes; + private Map> deletedObjects; + + public DeleteNodesEdit(Collection nodes, Map> deletedObjects) { + this.nodes = nodes; + this.deletedObjects = deletedObjects; + } + + @Override + public void undo() { + super.undo(); + for (GMLNode next : nodes) { + Collection deleted = deletedObjects.get(next); + editor.getMap().addNode(next); + editor.getMap().add(deleted); + } + editor.getViewer().repaint(); + } + + @Override + public void redo() { + super.redo(); + for (GMLNode next : nodes) { + Collection deleted = deletedObjects.get(next); + editor.getMap().removeNode(next); + editor.getMap().remove(deleted); + } + editor.getViewer().repaint(); + } + } +} \ No newline at end of file diff --git a/modules/maps/src/maps/gml/editor/DeleteShapeTool.java b/modules/maps/src/maps/gml/editor/DeleteShapeTool.java new file mode 100644 index 0000000000000000000000000000000000000000..7d2031031eb9e4e540dc0696b711032d6057625d --- /dev/null +++ b/modules/maps/src/maps/gml/editor/DeleteShapeTool.java @@ -0,0 +1,153 @@ +package maps.gml.editor; + +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.event.MouseMotionListener; +import java.awt.Color; +import java.awt.Point; +import java.awt.Insets; + +import javax.swing.undo.AbstractUndoableEdit; + +import maps.gml.view.FilledShapeDecorator; +import maps.gml.GMLRoad; +import maps.gml.GMLBuilding; +import maps.gml.GMLSpace; +import maps.gml.GMLShape; +import maps.gml.GMLCoordinates; + +/** + A tool for deleting shapes. +*/ +public class DeleteShapeTool extends AbstractTool { + private static final Color HIGHLIGHT_COLOUR = Color.BLUE; + + private Listener listener; + private FilledShapeDecorator highlight; + + private GMLShape shape; + + /** + Construct a DeleteShapeTool. + @param editor The editor instance. + */ + public DeleteShapeTool(GMLEditor editor) { + super(editor); + listener = new Listener(); + highlight = new FilledShapeDecorator(HIGHLIGHT_COLOUR, HIGHLIGHT_COLOUR, HIGHLIGHT_COLOUR); + } + + @Override + public String getName() { + return "Delete shape"; + } + + @Override + public void activate() { + editor.getViewer().addMouseListener(listener); + editor.getViewer().addMouseMotionListener(listener); + shape = null; + } + + @Override + public void deactivate() { + editor.getViewer().removeMouseListener(listener); + editor.getViewer().removeMouseMotionListener(listener); + editor.getViewer().clearAllBuildingDecorators(); + editor.getViewer().clearAllRoadDecorators(); + editor.getViewer().clearAllSpaceDecorators(); + editor.getViewer().repaint(); + } + + private void highlightShape(GMLShape newShape) { + if (shape == newShape) { + return; + } + if (shape != null) { + if (shape instanceof GMLBuilding) { + editor.getViewer().clearBuildingDecorator((GMLBuilding)shape); + } + if (shape instanceof GMLRoad) { + editor.getViewer().clearRoadDecorator((GMLRoad)shape); + } + if (shape instanceof GMLSpace) { + editor.getViewer().clearSpaceDecorator((GMLSpace)shape); + } + } + shape = newShape; + if (shape != null) { + if (shape instanceof GMLBuilding) { + editor.getViewer().setBuildingDecorator(highlight, (GMLBuilding)shape); + } + if (shape instanceof GMLRoad) { + editor.getViewer().setRoadDecorator(highlight, (GMLRoad)shape); + } + if (shape instanceof GMLSpace) { + editor.getViewer().setSpaceDecorator(highlight, (GMLSpace)shape); + } + } + editor.getViewer().repaint(); + } + + private class Listener implements MouseListener, MouseMotionListener { + @Override + public void mouseClicked(MouseEvent e) { + if (shape != null && e.getButton() == MouseEvent.BUTTON1) { + editor.getMap().remove(shape); + editor.getViewer().repaint(); + editor.setChanged(); + editor.addEdit(new DeleteShapeEdit(shape)); + } + } + + @Override + public void mouseMoved(MouseEvent e) { + Point p = fixEventPoint(e.getPoint()); + GMLCoordinates c = editor.snap(editor.getViewer().getCoordinatesAtPoint(p.x, p.y)); + highlightShape(editor.getMap().findShapeUnder(c.getX(), c.getY())); + } + + @Override + public void mousePressed(MouseEvent e) { + } + @Override + public void mouseReleased(MouseEvent e) { + } + @Override + public void mouseDragged(MouseEvent e) { + } + @Override + public void mouseEntered(MouseEvent e) { + } + @Override + public void mouseExited(MouseEvent e) { + } + + private Point fixEventPoint(Point p) { + Insets insets = editor.getViewer().getInsets(); + return new Point(p.x - insets.left, p.y - insets.top); + } + } + + private class DeleteShapeEdit extends AbstractUndoableEdit { + private GMLShape shape; + + public DeleteShapeEdit(GMLShape shape) { + this.shape = shape; + } + + @Override + public void undo() { + super.undo(); + editor.getMap().add(shape); + editor.getViewer().repaint(); + } + + @Override + public void redo() { + super.redo(); + editor.getMap().remove(shape); + editor.getViewer().repaint(); + } + } +} \ No newline at end of file diff --git a/modules/maps/src/maps/gml/editor/FixAttachedObjectsFunction.java b/modules/maps/src/maps/gml/editor/FixAttachedObjectsFunction.java new file mode 100644 index 0000000000000000000000000000000000000000..86950964b8b5d53e3aa1610957cbb5e0fcde649d --- /dev/null +++ b/modules/maps/src/maps/gml/editor/FixAttachedObjectsFunction.java @@ -0,0 +1,59 @@ +package maps.gml.editor; + +import java.util.Set; +import java.util.HashSet; + +import maps.gml.GMLShape; +import maps.gml.GMLEdge; + +/** + A function for fixing the lists of attached shapes. +*/ +public class FixAttachedObjectsFunction extends ProgressFunction { + /** + Construct a FixAttachedObjectsFunction. + @param editor The editor instance. + */ + public FixAttachedObjectsFunction(GMLEditor editor) { + super(editor); + } + + @Override + public String getName() { + return "Fix attached objects"; + } + + @Override + protected String getTitle() { + return "Fixing attached objects"; + } + + @Override + protected void executeImpl() { + // Remove and re-add all edges and shapes. + final Set shapes = new HashSet(); + final Set edges = new HashSet(); + synchronized (editor.getMap()) { + shapes.addAll(editor.getMap().getAllShapes()); + edges.addAll(editor.getMap().getEdges()); + } + setProgressLimit(shapes.size() + edges.size()); + synchronized (editor.getMap()) { + editor.getMap().removeAllEdges(); + } + for (GMLEdge next : edges) { + synchronized (editor.getMap()) { + editor.getMap().add(next); + } + bumpProgress(); + } + for (GMLShape next : shapes) { + synchronized (editor.getMap()) { + editor.getMap().add(next); + } + bumpProgress(); + } + editor.setChanged(); + editor.getViewer().repaint(); + } +} \ No newline at end of file diff --git a/modules/maps/src/maps/gml/editor/FixDegenerateShapesFunction.java b/modules/maps/src/maps/gml/editor/FixDegenerateShapesFunction.java new file mode 100644 index 0000000000000000000000000000000000000000..f499962c312a118354d41a464b6f9eb93b20c414 --- /dev/null +++ b/modules/maps/src/maps/gml/editor/FixDegenerateShapesFunction.java @@ -0,0 +1,246 @@ +package maps.gml.editor; + +import java.util.Set; +import java.util.HashSet; +import java.util.Collection; +import java.util.List; +import java.util.ArrayList; +import java.util.Iterator; + +import maps.gml.GMLShape; +import maps.gml.GMLNode; +import maps.gml.GMLEdge; +import maps.gml.GMLDirectedEdge; + +import rescuecore2.log.Logger; + +/** + A function for fixing degenerate shapes. +*/ +public class FixDegenerateShapesFunction extends ProgressFunction { + /** + Construct a FixDegenerateShapesFunction. + @param editor The editor instance. + */ + public FixDegenerateShapesFunction(GMLEditor editor) { + super(editor); + } + + @Override + public String getName() { + return "Fix degenerate shapes"; + } + + @Override + protected String getTitle() { + return "Fixing degenerate shapes"; + } + + @Override + protected void executeImpl() { + // Go through all shapes and remove any that have two or fewer edges. + Set shapes = new HashSet(); + Set edges = new HashSet(); + synchronized (editor.getMap()) { + shapes.addAll(editor.getMap().getAllShapes()); + edges.addAll(editor.getMap().getEdges()); + } + setProgressLimit(shapes.size() + edges.size()); + int shapeCount = 0; + int spurCount = 0; + int edgeCount = 0; + int outlineCount = 0; + for (GMLShape next : shapes) { + synchronized (editor.getMap()) { + removeDuplicateEdges(next); + if (checkForDegenerateShape(next)) { + ++shapeCount; + } + else { + if (checkForSpurs(next)) { + ++spurCount; + } + if (!checkOutline(next)) { + ++outlineCount; + editor.getMap().remove(next); + } + } + } + bumpProgress(); + } + for (GMLEdge next : edges) { + synchronized (editor.getMap()) { + if (checkForDegenerateEdge(next)) { + ++edgeCount; + } + } + bumpProgress(); + } + Logger.debug("Removed " + shapeCount + " degenerate shapes and " + edgeCount + " edges"); + Logger.debug("Removed " + outlineCount + " shapes with broken outlines"); + Logger.debug("Fixed " + spurCount + " spurs"); + editor.setChanged(); + editor.getViewer().repaint(); + } + + private void removeDuplicateEdges(GMLShape shape) { + List result = new ArrayList(shape.getEdges()); + Set seen = new HashSet(); + /* + Logger.debug("Checking for duplicate edges in " + shape); + Logger.debug("Edges:"); + for (GMLDirectedEdge next : result) { + Logger.debug(" " + next); + } + */ + for (Iterator it = result.iterator(); it.hasNext();) { + GMLDirectedEdge dEdge = it.next(); + GMLEdge edge = dEdge.getEdge(); + if (seen.contains(edge)) { + // Logger.debug("Duplicate found: " + dEdge); + it.remove(); + } + seen.add(edge); + } + /* + Logger.debug("Resulting edges:"); + for (GMLDirectedEdge next : result) { + Logger.debug(" " + next); + } + */ + editor.getMap().remove(shape); + shape.reorderEdges(result); + // Update attached edges by removing and re-adding + editor.getMap().add(shape); + } + + private boolean checkForSpurs(GMLShape shape) { + boolean spur = false; + List good = new ArrayList(shape.getEdges().size()); + /* + Logger.debug("Checking for spurs in " + shape); + Logger.debug("Edges:"); + for (GMLDirectedEdge next : shape.getEdges()) { + Logger.debug(" " + next); + } + */ + for (GMLDirectedEdge dEdge : shape.getEdges()) { + // This edge is good if both its nodes are part of other edges. + GMLNode start = dEdge.getStartNode(); + GMLNode end = dEdge.getEndNode(); + if (isFound(start, shape, dEdge) && isFound(end, shape, dEdge)) { + good.add(dEdge); + } + else { + // Logger.debug("Found spur edge: " + dEdge); + spur = true; + } + } + if (spur) { + editor.getMap().remove(shape); + shape.reorderEdges(good); + // Update attached edges by removing and re-adding + editor.getMap().add(shape); + } + return spur; + } + + private boolean checkOutline(GMLShape shape) { + List edges = shape.getEdges(); + List result = new ArrayList(edges.size()); + Set seen = new HashSet(); + GMLDirectedEdge dEdge = edges.get(0); + GMLNode start = dEdge.getStartNode(); + GMLNode current = dEdge.getEndNode(); + result.add(dEdge); + seen.add(dEdge.getEdge()); + /* + Logger.debug("Checking outline of " + shape); + Logger.debug("Edges:"); + for (GMLDirectedEdge next : edges) { + Logger.debug(" " + next); + } + Logger.debug("First edge: " + dEdge); + Logger.debug("Start node: " + start); + */ + while (current != start) { + // Logger.debug("Current node: " + current); + GMLDirectedEdge next = findNextEdge(current, edges, seen); + // Logger.debug("Next edge: " + next); + if (next == null) { + // Logger.debug("No next edge found!"); + return false; + } + current = next.getEndNode(); + seen.add(next.getEdge()); + result.add(next); + } + /* + Logger.debug("Finished checking outline"); + Logger.debug("New edges:"); + for (GMLDirectedEdge next : result) { + Logger.debug(" " + next); + } + */ + editor.getMap().remove(shape); + shape.reorderEdges(result); + // Update attached edges by removing and re-adding + editor.getMap().add(shape); + return true; + } + + private boolean checkForDegenerateShape(GMLShape shape) { + // CHECKSTYLE:OFF:MagicNumber + if (shape.getEdges().size() < 3) { + // CHECKSTYLE:ON:MagicNumber + editor.getMap().remove(shape); + return true; + } + return false; + } + + private boolean checkForDegenerateEdge(GMLEdge edge) { + if (edge.getStart().equals(edge.getEnd())) { + // Remove this edge from all attached shapes + Collection attached = new HashSet(editor.getMap().getAttachedShapes(edge)); + for (GMLShape shape : attached) { + editor.getMap().remove(shape); + shape.removeEdge(edge); + editor.getMap().add(shape); + } + editor.getMap().remove(edge); + return true; + } + return false; + } + + private boolean isFound(GMLNode node, GMLShape shape, GMLDirectedEdge ignore) { + for (GMLDirectedEdge edge : shape.getEdges()) { + if (edge == ignore) { + continue; + } + if (node.equals(edge.getStartNode()) || node.equals(edge.getEndNode())) { + return true; + } + } + return false; + } + + private GMLDirectedEdge findNextEdge(GMLNode start, Collection possible, Set seen) { + for (GMLDirectedEdge next : possible) { + if (next.getStartNode() == start && !seen.contains(next.getEdge())) { + return next; + } + } + // No edges found. Try reversing them. + for (GMLDirectedEdge next : possible) { + if (next.getEndNode() == start && !seen.contains(next.getEdge())) { + // Logger.debug("Reversed edge " + next); + next.reverse(); + return next; + } + } + // Nothing found. + return null; + } +} \ No newline at end of file diff --git a/modules/maps/src/maps/gml/editor/FixDuplicateEdgesFunction.java b/modules/maps/src/maps/gml/editor/FixDuplicateEdgesFunction.java new file mode 100644 index 0000000000000000000000000000000000000000..1408ef8087bdeb3901142966e6c54ebdac178d00 --- /dev/null +++ b/modules/maps/src/maps/gml/editor/FixDuplicateEdgesFunction.java @@ -0,0 +1,64 @@ +package maps.gml.editor; + +import java.util.Set; +import java.util.HashSet; +import java.util.Iterator; + +import maps.gml.GMLEdge; + +import rescuecore2.log.Logger; + +/** + A function for fixing duplicate edges. +*/ +public class FixDuplicateEdgesFunction extends ProgressFunction { + /** + Construct a FixDuplicateEdgesFunction. + @param editor The editor instance. + */ + public FixDuplicateEdgesFunction(GMLEditor editor) { + super(editor); + } + + @Override + public String getName() { + return "Fix duplicate edges"; + } + + @Override + protected String getTitle() { + return "Fixing duplicate edges"; + } + + @Override + protected void executeImpl() { + // Go through all edges and replace any duplicates + final Set remaining = new HashSet(editor.getMap().getEdges()); + setProgressLimit(remaining.size()); + int count = 0; + while (!remaining.isEmpty()) { + GMLEdge next = remaining.iterator().next(); + remaining.remove(next); + // Look at other edges for a duplicate + Iterator it = remaining.iterator(); + while (it.hasNext()) { + GMLEdge test = it.next(); + if ((test.getStart() == next.getStart() || test.getStart() == next.getEnd()) + && (test.getEnd() == next.getStart() || test.getEnd() == next.getEnd())) { + // Duplicate found + editor.getMap().replaceEdge(test, next); + editor.getMap().removeEdge(test); + it.remove(); + ++count; + bumpProgress(); + } + } + bumpProgress(); + } + if (count != 0) { + editor.setChanged(); + editor.getViewer().repaint(); + } + Logger.debug("Removed " + count + " duplicate edges"); + } +} \ No newline at end of file diff --git a/modules/maps/src/maps/gml/editor/FixLatLongTool.java b/modules/maps/src/maps/gml/editor/FixLatLongTool.java new file mode 100644 index 0000000000000000000000000000000000000000..c358b99319bfefb1ec284a4d240ce8419ca6173d --- /dev/null +++ b/modules/maps/src/maps/gml/editor/FixLatLongTool.java @@ -0,0 +1,36 @@ +package maps.gml.editor; + +import maps.ScaleConversion; +import maps.MapTools; + +/** + A tool for fixing latitude/longitude coordinates. +*/ +public class FixLatLongTool extends AbstractTool { + /** + Construct a FixLatLongTool. + @param editor The editor instance. + */ + public FixLatLongTool(GMLEditor editor) { + super(editor); + } + + @Override + public String getName() { + return "Fix lat/long"; + } + + @Override + public void activate() { + double minX = editor.getMap().getMinX(); + double minY = editor.getMap().getMinY(); + double factor = 1.0 / MapTools.sizeOf1Metre(minX, minY); + ScaleConversion c = new ScaleConversion(minX, minY, factor, factor); + editor.getMap().convertCoordinates(c); + editor.setChanged(); + } + + @Override + public void deactivate() { + } +} \ No newline at end of file diff --git a/modules/maps/src/maps/gml/editor/FixNearbyNodesFunction.java b/modules/maps/src/maps/gml/editor/FixNearbyNodesFunction.java new file mode 100644 index 0000000000000000000000000000000000000000..f50b7d23f7556b5cd9e3fe7df39109f1cb7656e7 --- /dev/null +++ b/modules/maps/src/maps/gml/editor/FixNearbyNodesFunction.java @@ -0,0 +1,80 @@ +package maps.gml.editor; + +import javax.swing.JOptionPane; + +import java.util.Set; +import java.util.HashSet; + +import maps.gml.GMLNode; + +import rescuecore2.log.Logger; + +/** + A function for fixing nearby nodes. +*/ +public class FixNearbyNodesFunction extends ProgressFunction { + private static final double DEFAULT_TOLERANCE = 0.001; + + private double tolerance; + + /** + Construct a FixNearbyNodesFunction. + @param editor The editor instance. + */ + public FixNearbyNodesFunction(GMLEditor editor) { + super(editor); + } + + @Override + public String getName() { + return "Fix nearby nodes"; + } + + @Override + protected String getTitle() { + return "Fixing nearby nodes"; + } + + @Override + public void execute() { + String s = JOptionPane.showInputDialog(editor.getViewer(), "Enter the desired tolerance (in m)", DEFAULT_TOLERANCE); + if (s == null) { + return; + } + tolerance = Double.parseDouble(s); + super.execute(); + } + + @Override + protected void executeImpl() { + // Go through all nodes and replace any nearby ones. + final Set remaining = new HashSet(editor.getMap().getNodes()); + setProgressLimit(remaining.size()); + int count = 0; + while (!remaining.isEmpty()) { + GMLNode next = remaining.iterator().next(); + remaining.remove(next); + double x = next.getX(); + double y = next.getY(); + // Logger.debug("Next node: " + next); + // Logger.debug("Finding nodes near " + x + ", " + y); + bumpProgress(); + for (GMLNode replaced : editor.getMap().getNodesInRegion(x - tolerance, y - tolerance, x + tolerance, y + tolerance)) { + if (replaced == next) { + continue; + } + // Logger.debug("Found " + replaced); + editor.getMap().replaceNode(replaced, next); + remaining.remove(replaced); + editor.getMap().removeNode(replaced); + ++count; + bumpProgress(); + } + } + if (count != 0) { + editor.setChanged(); + editor.getViewer().repaint(); + } + Logger.debug("Removed " + count + " nodes"); + } +} \ No newline at end of file diff --git a/modules/maps/src/maps/gml/editor/Function.java b/modules/maps/src/maps/gml/editor/Function.java new file mode 100644 index 0000000000000000000000000000000000000000..a151bda40b1484951858a2467eef73d9f2e93c33 --- /dev/null +++ b/modules/maps/src/maps/gml/editor/Function.java @@ -0,0 +1,17 @@ +package maps.gml.editor; + +/** + Interface for an editing function. +*/ +public interface Function { + /** + Get the name of this function. + @return The name of the function. + */ + String getName(); + + /** + Execute this function. + */ + void execute(); +} \ No newline at end of file diff --git a/modules/maps/src/maps/gml/editor/GMLEditor.java b/modules/maps/src/maps/gml/editor/GMLEditor.java new file mode 100644 index 0000000000000000000000000000000000000000..3a4c3f2483f3253dc9de4b07c3d48856f3f0fc85 --- /dev/null +++ b/modules/maps/src/maps/gml/editor/GMLEditor.java @@ -0,0 +1,638 @@ +package maps.gml.editor; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.GridLayout; +import java.awt.Insets; +import java.awt.Point; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.event.MouseMotionListener; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.io.File; +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.util.ArrayList; +import java.util.List; + +import javax.swing.AbstractAction; +import javax.swing.Action; +import javax.swing.ButtonGroup; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JCheckBoxMenuItem; +import javax.swing.JFileChooser; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JMenu; +import javax.swing.JMenuBar; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JSpinner; +import javax.swing.JSplitPane; +import javax.swing.JTextField; +import javax.swing.JToggleButton; +import javax.swing.JToolBar; +import javax.swing.SpinnerNumberModel; +import javax.swing.SwingUtilities; +import javax.swing.WindowConstants; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.swing.undo.CannotUndoException; +import javax.swing.undo.UndoManager; +import javax.swing.undo.UndoableEdit; + +import maps.MapException; +import maps.MapReader; +import maps.MapWriter; +import maps.gml.GMLCoordinates; +import maps.gml.GMLMap; +import maps.gml.GMLObject; +import maps.gml.formats.RobocupFormat; +import maps.gml.view.GMLMapViewer; +import maps.gml.view.GMLObjectInspector; + +import rescuecore2.log.Logger; + +/** + * A component for editing GML maps. + */ +public class GMLEditor extends JPanel { + private static final int VIEWER_PREFERRED_SIZE = 500; + private static final int INSPECTOR_PREFERRED_WIDTH = 300; + private static final int INSPECTOR_PREFERRED_HEIGHT = 500; + + private static final double SNAP_MIN_RESOLUTION = 0.001; + private static final double SNAP_MAX_RESOLUTION = 1000; + + private static final NumberFormat FORMAT = new DecimalFormat("#0.000"); + + private GMLMap map; + private GMLMapViewer viewer; + private GMLObjectInspector inspector; + private JLabel x; + private JLabel y; + private boolean changed; + private ViewerMouseListener viewerMouseListener; + private Tool currentTool; + + private UndoManager undoManager; + private Action undoAction; + private Action redoAction; + + private File saveFile; + private File baseDir; + + private Snap snap; + + /** + * Construct a new GMLEditor. + * + * @param menuBar The menu bar to add menus to. + */ + public GMLEditor(JMenuBar menuBar) { + super(new BorderLayout()); + map = new GMLMap(); + viewer = new GMLMapViewer(map); + inspector = new GMLObjectInspector(map); + undoManager = new UndoManager(); + viewer.setPreferredSize(new Dimension(VIEWER_PREFERRED_SIZE, VIEWER_PREFERRED_SIZE)); + inspector.setPreferredSize(new Dimension(INSPECTOR_PREFERRED_WIDTH, INSPECTOR_PREFERRED_HEIGHT)); + viewer.setBackground(Color.GRAY); + viewer.getPanZoomListener().setPanOnRightMouse(); + snap = new Snap(); + changed = false; + x = new JLabel("X: "); + y = new JLabel("Y: "); + JToolBar fileToolbar = new JToolBar("File"); + JToolBar viewToolbar = new JToolBar("View"); + JToolBar editToolbar = new JToolBar("Edit"); + JToolBar toolsToolbar = new JToolBar("Tools"); + JToolBar functionsToolbar = new JToolBar("Functions"); + JMenu fileMenu = new JMenu("File", false); + JMenu viewMenu = new JMenu("View", false); + JMenu editMenu = new JMenu("Edit", false); + JMenu toolsMenu = new JMenu("Tools", false); + JMenu functionsMenu = new JMenu("Functions", false); + + createFileActions(fileMenu, fileToolbar); + createViewActions(viewMenu, viewToolbar); + createEditActions(editMenu, editToolbar); + createToolActions(toolsMenu, toolsToolbar); + createFunctionActions(functionsMenu, functionsToolbar); + + JPanel main = new JPanel(new BorderLayout()); + JPanel labels = new JPanel(new GridLayout(1, 2)); + labels.add(x); + labels.add(y); + JSplitPane split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, viewer, inspector); + main.add(split, BorderLayout.CENTER); + main.add(labels, BorderLayout.SOUTH); + add(main, BorderLayout.CENTER); + JPanel toolbars = new JPanel(new GridLayout(0, 1)); + toolbars.add(fileToolbar); + toolbars.add(viewToolbar); + toolbars.add(editToolbar); + toolbars.add(toolsToolbar); + toolbars.add(functionsToolbar); + add(toolbars, BorderLayout.NORTH); + menuBar.add(fileMenu); + menuBar.add(viewMenu); + menuBar.add(editMenu); + menuBar.add(toolsMenu); + menuBar.add(functionsMenu); + + viewerMouseListener = new ViewerMouseListener(); + viewer.addMouseListener(viewerMouseListener); + viewer.addMouseMotionListener(viewerMouseListener); + + baseDir = new File(System.getProperty("user.dir")); + } + + /** + * Entry point. + * + * @param args Command line arguments. + */ + public static void main(String[] args) { + final JFrame frame = new JFrame("GMLEditor"); + JMenuBar menuBar = new JMenuBar(); + final GMLEditor editor = new GMLEditor(menuBar); + if (args.length > 0 && args[0].length() > 0) { + try { + editor.load(args[0]); + } catch (CancelledByUserException e) { + return; + } catch (MapException e) { + e.printStackTrace(); + } + } + + frame.setJMenuBar(menuBar); + frame.setContentPane(editor); + frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); + frame.pack(); + frame.addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent e) { + try { + editor.close(); + frame.setVisible(false); + frame.dispose(); + System.exit(0); + } catch (CancelledByUserException ex) { + frame.setVisible(true); + } + } + }); + frame.setVisible(true); + } + + /** + * Load a map by showing a file chooser dialog. + * + * @throws CancelledByUserException If the user cancels the change due to + * unsaved changes. + * @throws MapException If there is a problem reading the map. + */ + public void load() throws CancelledByUserException, MapException { + JFileChooser chooser = new JFileChooser(baseDir); + if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) { + load(chooser.getSelectedFile()); + } + } + + /** + * Load a map from a file. + * + * @param filename The name of the file to read. + * @throws CancelledByUserException If the user cancels the change due to + * unsaved changes. + * @throws MapException If there is a problem reading the map. + */ + public void load(String filename) throws CancelledByUserException, MapException { + load(new File(filename)); + } + + /** + * Load a map from a file. + * + * @param file The file to read. + * @throws CancelledByUserException If the user cancels the change due to + * unsaved changes. + * @throws MapException If there is a problem reading the map. + */ + public void load(File file) throws CancelledByUserException, MapException { + setMap((GMLMap) MapReader.readMap(file)); + saveFile = file; + baseDir = saveFile.getParentFile(); + } + + /** + * Set the map. + * + * @param newMap The new map. + * @throws CancelledByUserException If the user cancels the change due to + * unsaved changes. + */ + public void setMap(GMLMap newMap) throws CancelledByUserException { + checkForChanges(); + map = newMap; + changed = false; + viewer.setMap(map); + inspector.setMap(map); + viewer.repaint(); + } + + /** + * Get the map. + * + * @return The map. + */ + public GMLMap getMap() { + return map; + } + + /** + * Save the map. + * + * @throws MapException If there is a problem saving the map. + */ + public void save() throws MapException { + if (saveFile == null) { + JFileChooser chooser = new JFileChooser(baseDir); + if (chooser.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) { + saveFile = chooser.getSelectedFile(); + } + } + if (saveFile != null) { + Logger.debug("Saving to " + saveFile.getAbsolutePath()); + MapWriter.writeMap(map, saveFile, RobocupFormat.INSTANCE); + baseDir = saveFile.getParentFile(); + changed = false; + } + } + + /** + * Close the editor. + * + * @throws CancelledByUserException If the user cancels the close due to unsaved + * changes." + */ + public void close() throws CancelledByUserException { + checkForChanges(); + } + + /** + * Get the map viewer. + * + * @return The map viewer. + */ + public GMLMapViewer getViewer() { + return viewer; + } + + /** + * Get the object inspector. + * + * @return The object inspector. + */ + public GMLObjectInspector getInspector() { + return inspector; + } + + /** + * Register a change to the map. + */ + public void setChanged() { + changed = true; + } + + /** + * Register an undoable edit. + * + * @param edit The edit to add. + */ + public void addEdit(UndoableEdit edit) { + undoManager.addEdit(edit); + undoAction.setEnabled(undoManager.canUndo()); + redoAction.setEnabled(undoManager.canRedo()); + } + + /** + * Snap coordinates to the grid. + * + * @param c The coordinates to snap. + * @return The passed-in coordinates object. + */ + public GMLCoordinates snap(GMLCoordinates c) { + snap.snap(c); + return c; + } + + private void updatePositionLabels(final Point p) { + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + GMLCoordinates c = viewer.getCoordinatesAtPoint(p.x, p.y); + x.setText("X: " + FORMAT.format(c.getX())); + y.setText("Y: " + FORMAT.format(c.getY())); + } + }); + } + + private void checkForChanges() throws CancelledByUserException { + if (changed) { + switch (JOptionPane.showConfirmDialog(null, "The current map has changes. Do you want to save them?")) { + case JOptionPane.YES_OPTION: + try { + save(); + } catch (MapException e) { + JOptionPane.showMessageDialog(null, e); + throw new CancelledByUserException(); + } + break; + case JOptionPane.NO_OPTION: + changed = false; + return; + case JOptionPane.CANCEL_OPTION: + throw new CancelledByUserException(); + default: + throw new RuntimeException("JOptionPane.showConfirmDialog returned something weird"); + } + } + } + + private void createFileActions(JMenu menu, JToolBar toolbar) { + Action newAction = new AbstractAction("New") { + @Override + public void actionPerformed(ActionEvent e) { + try { + checkForChanges(); + setMap(new GMLMap()); + } catch (CancelledByUserException ex) { + return; + } + } + }; + Action loadAction = new AbstractAction("Load") { + @Override + public void actionPerformed(ActionEvent e) { + try { + checkForChanges(); + load(); + } catch (CancelledByUserException ex) { + return; + } catch (MapException ex) { + JOptionPane.showMessageDialog(null, ex); + } + } + }; + Action saveAction = new AbstractAction("Save") { + @Override + public void actionPerformed(ActionEvent e) { + try { + save(); + } catch (MapException ex) { + JOptionPane.showMessageDialog(null, ex); + } + } + }; + Action saveAsAction = new AbstractAction("Save as") { + @Override + public void actionPerformed(ActionEvent e) { + try { + saveFile = null; + save(); + } catch (MapException ex) { + JOptionPane.showMessageDialog(null, ex); + } + } + }; + toolbar.add(newAction); + toolbar.add(loadAction); + toolbar.add(saveAction); + toolbar.add(saveAsAction); + menu.add(newAction); + menu.add(loadAction); + menu.add(saveAction); + menu.add(saveAsAction); + } + + private void createViewActions(JMenu menu, JToolBar toolbar) { + final JCheckBox snapBox = new JCheckBox("Snap to grid", snap.isEnabled()); + final JSpinner snapSpinner = new JSpinner( + new SpinnerNumberModel(snap.getResolution(), SNAP_MIN_RESOLUTION, SNAP_MAX_RESOLUTION, SNAP_MIN_RESOLUTION)); + snapSpinner.addChangeListener(new ChangeListener() { + @Override + public void stateChanged(ChangeEvent e) { + snap.setResolution((Double) snapSpinner.getValue()); + } + }); + snapBox.addChangeListener(new ChangeListener() { + @Override + public void stateChanged(ChangeEvent e) { + snap.setEnabled(snapBox.isSelected()); + } + }); + Action gridAction = new AbstractAction("Show grid") { + @Override + public void actionPerformed(ActionEvent e) { + viewer.setGridEnabled((Boolean) getValue(Action.SELECTED_KEY)); + viewer.repaint(); + } + }; + gridAction.putValue(Action.SELECTED_KEY, false); + toolbar.add(snapSpinner); + toolbar.add(snapBox); + toolbar.add(new JToggleButton(gridAction)); + menu.add(new JCheckBoxMenuItem(gridAction)); + + // Create the "show objects" button and textfield + final JTextField showField = new JTextField(); + JButton showButton = new JButton("Show"); + showButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + String s = showField.getText(); + List objects = new ArrayList(); + for (String next : s.split(",")) { + int id = Integer.parseInt(next.trim()); + GMLObject o = map.getObject(id); + if (o != null) { + objects.add(o); + } + } + viewer.view(objects); + viewer.repaint(); + } + }); + toolbar.addSeparator(); + toolbar.add(showField); + toolbar.add(showButton); + + // Add the reset zoom action + Action resetZoom = new AbstractAction("Reset zoom") { + @Override + public void actionPerformed(ActionEvent e) { + viewer.viewAll(); + viewer.repaint(); + } + }; + toolbar.addSeparator(); + menu.addSeparator(); + toolbar.add(resetZoom); + menu.add(resetZoom); + } + + private void createEditActions(JMenu menu, JToolBar toolbar) { + undoAction = new AbstractAction("Undo") { + @Override + public void actionPerformed(ActionEvent e) { + try { + undoManager.undo(); + } catch (CannotUndoException ex) { + JOptionPane.showMessageDialog(null, ex); + } + setEnabled(undoManager.canUndo()); + redoAction.setEnabled(undoManager.canRedo()); + } + }; + redoAction = new AbstractAction("Redo") { + @Override + public void actionPerformed(ActionEvent e) { + try { + undoManager.redo(); + } catch (CannotUndoException ex) { + JOptionPane.showMessageDialog(null, ex); + } + setEnabled(undoManager.canRedo()); + undoAction.setEnabled(undoManager.canUndo()); + } + }; + undoAction.setEnabled(false); + redoAction.setEnabled(false); + toolbar.add(undoAction); + toolbar.add(redoAction); + menu.add(undoAction); + menu.add(redoAction); + } + + private void createToolActions(JMenu menu, JToolBar toolbar) { + ButtonGroup toolbarGroup = new ButtonGroup(); + ButtonGroup menuGroup = new ButtonGroup(); + addTool(new InspectTool(this), menu, toolbar, menuGroup, toolbarGroup); + menu.addSeparator(); + toolbar.addSeparator(); + addTool(new CreateNodeTool(this), menu, toolbar, menuGroup, toolbarGroup); + addTool(new CreateEdgeTool(this), menu, toolbar, menuGroup, toolbarGroup); + addTool(new CreateRoadTool(this), menu, toolbar, menuGroup, toolbarGroup); + addTool(new CreateBuildingTool(this), menu, toolbar, menuGroup, toolbarGroup); + addTool(new CreateSpaceTool(this), menu, null, menuGroup, null); + menu.addSeparator(); + toolbar.addSeparator(); + addTool(new DeleteNodeTool(this), menu, toolbar, menuGroup, toolbarGroup); + addTool(new DeleteEdgeTool(this), menu, toolbar, menuGroup, toolbarGroup); + addTool(new DeleteShapeTool(this), menu, toolbar, menuGroup, toolbarGroup); + menu.addSeparator(); + toolbar.addSeparator(); + addTool(new MoveNodeTool(this), menu, toolbar, menuGroup, toolbarGroup); + addTool(new MergeNodesTool(this), menu, toolbar, menuGroup, toolbarGroup); + addTool(new MergeLinesTool(this), menu, toolbar, menuGroup, toolbarGroup); + addTool(new SplitEdgeTool(this), menu, toolbar, menuGroup, toolbarGroup); + addTool(new SplitShapeTool(this), menu, toolbar, menuGroup, toolbarGroup); + addTool(new TogglePassableTool(this), menu, toolbar, menuGroup, toolbarGroup); + } + + private void createFunctionActions(JMenu menu, JToolBar toolbar) { + addFunction(new ScaleFunction(this), menu, toolbar); + addFunction(new FixNearbyNodesFunction(this), menu, toolbar); + addFunction(new FixDuplicateEdgesFunction(this), menu, toolbar); + addFunction(new SplitEdgesFunction(this), menu, toolbar); + addFunction(new ComputePassableEdgesFunction(this), menu, toolbar); + addFunction(new PruneOrphanNodesFunction(this), menu, toolbar); + addFunction(new PruneOrphanEdgesFunction(this), menu, toolbar); + addFunction(new FixDegenerateShapesFunction(this), menu, toolbar); + addFunction(new FixAttachedObjectsFunction(this), menu, toolbar); + addFunction(new ValidateFunction(this), menu, toolbar); + addFunction(new AddNoiseFunction(this), menu, toolbar); + } + + private void addTool(final Tool t, JMenu menu, JToolBar toolbar, ButtonGroup menuGroup, ButtonGroup toolbarGroup) { + final JToggleButton toggle = new JToggleButton(); + final JCheckBoxMenuItem check = new JCheckBoxMenuItem(); + Action action = new AbstractAction(t.getName()) { + @Override + public void actionPerformed(ActionEvent e) { + if (currentTool != null) { + currentTool.deactivate(); + } + currentTool = t; + toggle.setSelected(true); + check.setSelected(true); + currentTool.activate(); + } + }; + toggle.setAction(action); + check.setAction(action); + menu.add(check); + if (toolbar != null) { + toolbar.add(toggle); + toolbarGroup.add(toggle); + } + menuGroup.add(check); + } + + private void addFunction(final Function f, JMenu menu, JToolBar toolbar) { + Action action = new AbstractAction(f.getName()) { + @Override + public void actionPerformed(ActionEvent e) { + f.execute(); + } + }; + toolbar.add(action); + menu.add(action); + } + + private class ViewerMouseListener implements MouseListener, MouseMotionListener { + @Override + public void mouseMoved(MouseEvent e) { + updatePositionLabels(fixEventPoint(e.getPoint())); + } + + @Override + public void mouseDragged(MouseEvent e) { + updatePositionLabels(fixEventPoint(e.getPoint())); + } + + @Override + public void mouseClicked(MouseEvent e) { + } + + @Override + public void mousePressed(MouseEvent e) { + } + + @Override + public void mouseReleased(MouseEvent e) { + } + + @Override + public void mouseExited(MouseEvent e) { + } + + @Override + public void mouseEntered(MouseEvent e) { + updatePositionLabels(fixEventPoint(e.getPoint())); + } + + private Point fixEventPoint(Point p) { + Insets insets = viewer.getInsets(); + return new Point(p.x - insets.left, p.y - insets.top); + } + } +} \ No newline at end of file diff --git a/modules/maps/src/maps/gml/editor/InspectTool.java b/modules/maps/src/maps/gml/editor/InspectTool.java new file mode 100644 index 0000000000000000000000000000000000000000..f4643c621ef259c1ff9f5588566929b86ff81423 --- /dev/null +++ b/modules/maps/src/maps/gml/editor/InspectTool.java @@ -0,0 +1,174 @@ +package maps.gml.editor; + +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.event.MouseMotionListener; +import java.awt.Point; +import java.awt.Insets; +import java.awt.Color; + +import maps.gml.GMLNode; +import maps.gml.GMLEdge; +import maps.gml.GMLRoad; +import maps.gml.GMLBuilding; +import maps.gml.GMLSpace; +import maps.gml.GMLShape; +import maps.gml.GMLObject; +import maps.gml.GMLCoordinates; +import maps.gml.view.FilledShapeDecorator; +import maps.gml.view.NodeDecorator; +import maps.gml.view.EdgeDecorator; +import maps.gml.view.SquareNodeDecorator; +import maps.gml.view.LineEdgeDecorator; + +import rescuecore2.misc.geometry.Point2D; +import rescuecore2.misc.geometry.Line2D; +import rescuecore2.misc.geometry.GeometryTools2D; + +/** + A tool for inspecting objects. +*/ +public class InspectTool extends AbstractTool { + /** Distance in pixels to consider an object "nearby". */ + private static final int NEARBY = 5; + + private static final Color HIGHLIGHT_COLOUR = Color.BLUE; + private static final int NODE_SIZE = 5; + + private Listener listener; + private NodeDecorator nodeHighlight; + private EdgeDecorator edgeHighlight; + private FilledShapeDecorator shapeHighlight; + + /** + Construct an InspectTool. + @param editor The editor instance. + */ + public InspectTool(GMLEditor editor) { + super(editor); + listener = new Listener(); + nodeHighlight = new SquareNodeDecorator(HIGHLIGHT_COLOUR, NODE_SIZE); + edgeHighlight = new LineEdgeDecorator(HIGHLIGHT_COLOUR); + shapeHighlight = new FilledShapeDecorator(HIGHLIGHT_COLOUR, HIGHLIGHT_COLOUR, HIGHLIGHT_COLOUR); + } + + @Override + public String getName() { + return "Inspect object"; + } + + @Override + public void activate() { + editor.getViewer().addMouseListener(listener); + editor.getViewer().addMouseMotionListener(listener); + } + + @Override + public void deactivate() { + editor.getViewer().removeMouseListener(listener); + editor.getViewer().removeMouseMotionListener(listener); + editor.getViewer().clearAllNodeDecorators(); + editor.getViewer().clearAllEdgeDecorators(); + editor.getViewer().clearAllBuildingDecorators(); + editor.getViewer().clearAllRoadDecorators(); + editor.getViewer().clearAllSpaceDecorators(); + editor.getViewer().repaint(); + } + + private void highlight(GMLObject object) { + editor.getViewer().clearAllNodeDecorators(); + editor.getViewer().clearAllEdgeDecorators(); + editor.getViewer().clearAllBuildingDecorators(); + editor.getViewer().clearAllRoadDecorators(); + editor.getViewer().clearAllSpaceDecorators(); + if (object instanceof GMLNode) { + editor.getViewer().setNodeDecorator(nodeHighlight, (GMLNode)object); + } + if (object instanceof GMLEdge) { + editor.getViewer().setEdgeDecorator(edgeHighlight, (GMLEdge)object); + } + if (object instanceof GMLBuilding) { + editor.getViewer().setBuildingDecorator(shapeHighlight, (GMLBuilding)object); + } + if (object instanceof GMLRoad) { + editor.getViewer().setRoadDecorator(shapeHighlight, (GMLRoad)object); + } + if (object instanceof GMLSpace) { + editor.getViewer().setSpaceDecorator(shapeHighlight, (GMLSpace)object); + } + editor.getViewer().repaint(); + } + + private boolean closeEnough(GMLNode node, Point p) { + GMLCoordinates lowerLeft = editor.getViewer().getCoordinatesAtPoint(p.x - NEARBY, p.y + NEARBY); + GMLCoordinates topRight = editor.getViewer().getCoordinatesAtPoint(p.x + NEARBY, p.y - NEARBY); + return (node.getX() > lowerLeft.getX() && node.getX() < topRight.getX() && node.getY() > lowerLeft.getY() && node.getY() < topRight.getY()); + } + + private boolean closeEnough(GMLEdge edge, Point p) { + Point start = editor.getViewer().getScreenCoordinates(edge.getStart().getCoordinates()); + Point end = editor.getViewer().getScreenCoordinates(edge.getEnd().getCoordinates()); + Point2D startPoint = new Point2D(start.x, start.y); + Point2D endPoint = new Point2D(end.x, end.y); + Line2D line = new Line2D(startPoint, endPoint); + Point2D testPoint = new Point2D(p.x, p.y); + Point2D closest = GeometryTools2D.getClosestPointOnSegment(line, testPoint); + return GeometryTools2D.getDistance(testPoint, closest) < NEARBY; + } + + private class Listener implements MouseListener, MouseMotionListener { + @Override + public void mouseClicked(MouseEvent e) { + if (e.getButton() == MouseEvent.BUTTON1) { + Point p = fixEventPoint(e.getPoint()); + editor.getInspector().inspect(findNearbyObject(p)); + } + } + + @Override + public void mouseMoved(MouseEvent e) { + Point p = fixEventPoint(e.getPoint()); + highlight(findNearbyObject(p)); + } + + @Override + public void mousePressed(MouseEvent e) { + } + @Override + public void mouseReleased(MouseEvent e) { + } + @Override + public void mouseDragged(MouseEvent e) { + } + @Override + public void mouseEntered(MouseEvent e) { + } + @Override + public void mouseExited(MouseEvent e) { + } + + private GMLObject findNearbyObject(Point p) { + GMLCoordinates c = editor.getViewer().getCoordinatesAtPoint(p.x, p.y); + GMLNode node = editor.getMap().findNearestNode(c.getX(), c.getY()); + GMLEdge edge = editor.getMap().findNearestEdge(c.getX(), c.getY()); + GMLShape shape = editor.getMap().findShapeUnder(c.getX(), c.getY()); + // If the node is close enough inspect that + // Otherwise, if the edge is close enough + // Otherwise the shape + if (node != null && closeEnough(node, p)) { + return node; + } + else if (edge != null && closeEnough(edge, p)) { + return edge; + } + else { + return shape; + } + } + + private Point fixEventPoint(Point p) { + Insets insets = editor.getViewer().getInsets(); + return new Point(p.x - insets.left, p.y - insets.top); + } + } +} \ No newline at end of file diff --git a/modules/maps/src/maps/gml/editor/MergeLinesTool.java b/modules/maps/src/maps/gml/editor/MergeLinesTool.java new file mode 100644 index 0000000000000000000000000000000000000000..3ca00c1694892825161edf2599eacdcaa6a378e0 --- /dev/null +++ b/modules/maps/src/maps/gml/editor/MergeLinesTool.java @@ -0,0 +1,169 @@ +package maps.gml.editor; + +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.event.MouseMotionListener; +import java.awt.Color; +import java.awt.Point; +import java.awt.Insets; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; + +import javax.swing.undo.AbstractUndoableEdit; + +import maps.gml.view.NodeDecorator; +import maps.gml.view.SquareNodeDecorator; +import maps.gml.view.EdgeDecorator; +import maps.gml.view.LineEdgeDecorator; +import maps.gml.GMLNode; +import maps.gml.GMLEdge; +import maps.gml.GMLCoordinates; +import maps.gml.GMLObject; + +/** + A tool for merging lines by deleting a common node. +*/ +public class MergeLinesTool extends AbstractTool { + private static final Color HIGHLIGHT_COLOUR = Color.BLUE; + private static final int HIGHLIGHT_SIZE = 6; + + private Listener listener; + private NodeDecorator nodeHighlight; + private EdgeDecorator edgeHighlight; + private GMLNode selected; + private Collection attachedEdges; + + /** + Construct a MergeLinesTool. + @param editor The editor instance. + */ + public MergeLinesTool(GMLEditor editor) { + super(editor); + listener = new Listener(); + nodeHighlight = new SquareNodeDecorator(HIGHLIGHT_COLOUR, HIGHLIGHT_SIZE); + edgeHighlight = new LineEdgeDecorator(HIGHLIGHT_COLOUR); + selected = null; + attachedEdges = new HashSet(); + } + + @Override + public String getName() { + return "Merge lines"; + } + + @Override + public void activate() { + editor.getViewer().addMouseListener(listener); + editor.getViewer().addMouseMotionListener(listener); + selected = null; + attachedEdges.clear(); + } + + @Override + public void deactivate() { + editor.getViewer().removeMouseListener(listener); + editor.getViewer().removeMouseMotionListener(listener); + editor.getViewer().clearAllNodeDecorators(); + editor.getViewer().clearAllEdgeDecorators(); + editor.getViewer().repaint(); + } + + private void highlightNode(GMLNode node, Collection attached) { + if (selected == node) { + return; + } + if (selected != null) { + editor.getViewer().clearNodeDecorator(selected); + editor.getViewer().clearEdgeDecorator(attachedEdges); + } + selected = node; + attachedEdges.clear(); + if (selected != null) { + attachedEdges.addAll(attached); + editor.getViewer().setNodeDecorator(nodeHighlight, selected); + editor.getViewer().setEdgeDecorator(edgeHighlight, attachedEdges); + } + editor.getViewer().repaint(); + } + + private class Listener implements MouseListener, MouseMotionListener { + @Override + public void mouseMoved(MouseEvent e) { + Point p = fixEventPoint(e.getPoint()); + GMLCoordinates c = editor.getViewer().getCoordinatesAtPoint(p.x, p.y); + GMLNode node = editor.getMap().findNearestNode(c.getX(), c.getY()); + Collection attached = editor.getMap().getAttachedEdges(node); + if (attached.size() == 2) { + highlightNode(node, attached); + } + } + + @Override + public void mouseClicked(MouseEvent e) { + if (selected == null) { + return; + } + if (e.getButton() == MouseEvent.BUTTON1) { + Iterator it = attachedEdges.iterator(); + GMLEdge newEdge = editor.getMap().mergeEdges(it.next(), it.next()); + Collection deleted = editor.getMap().removeNode(selected); + editor.setChanged(); + editor.addEdit(new MergeEdit(selected, deleted, newEdge)); + highlightNode(null, null); + } + } + + @Override + public void mousePressed(MouseEvent e) { + } + @Override + public void mouseReleased(MouseEvent e) { + } + @Override + public void mouseDragged(MouseEvent e) { + } + @Override + public void mouseEntered(MouseEvent e) { + } + @Override + public void mouseExited(MouseEvent e) { + } + + private Point fixEventPoint(Point p) { + Insets insets = editor.getViewer().getInsets(); + return new Point(p.x - insets.left, p.y - insets.top); + } + } + + private class MergeEdit extends AbstractUndoableEdit { + private GMLNode deletedNode; + private Collection deletedObjects; + private GMLEdge newEdge; + + public MergeEdit(GMLNode node, Collection deletedObjects, GMLEdge newEdge) { + this.deletedNode = node; + this.deletedObjects = deletedObjects; + this.newEdge = newEdge; + } + + @Override + public void undo() { + super.undo(); + editor.getMap().addNode(deletedNode); + editor.getMap().add(deletedObjects); + editor.getMap().removeEdge(newEdge); + editor.getViewer().repaint(); + } + + @Override + public void redo() { + super.redo(); + editor.getMap().removeNode(deletedNode); + editor.getMap().remove(deletedObjects); + editor.getMap().addEdge(newEdge); + editor.getViewer().repaint(); + } + } +} \ No newline at end of file diff --git a/modules/maps/src/maps/gml/editor/MergeNodesTool.java b/modules/maps/src/maps/gml/editor/MergeNodesTool.java new file mode 100644 index 0000000000000000000000000000000000000000..c9b128494e4077295c31881f2092dffd42c53238 --- /dev/null +++ b/modules/maps/src/maps/gml/editor/MergeNodesTool.java @@ -0,0 +1,162 @@ +package maps.gml.editor; + +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.event.MouseMotionListener; +import java.awt.Color; +import java.awt.Point; +import java.awt.Insets; + +import maps.gml.view.NodeDecorator; +import maps.gml.view.SquareNodeDecorator; +import maps.gml.GMLNode; +import maps.gml.GMLCoordinates; + +/** + A tool for merging nodes. +*/ +public class MergeNodesTool extends AbstractTool { + private static final Color HOVER_COLOUR = Color.BLUE; + private static final Color MERGE_COLOUR = Color.RED; + private static final int HIGHLIGHT_SIZE = 6; + + private Listener listener; + private NodeDecorator hoverHighlight; + private NodeDecorator mergeHighlight; + private GMLNode hover; + private GMLNode merge; + private boolean merging; + + /** + Construct a MergeNodesTool. + @param editor The editor instance. + */ + public MergeNodesTool(GMLEditor editor) { + super(editor); + listener = new Listener(); + hoverHighlight = new SquareNodeDecorator(HOVER_COLOUR, HIGHLIGHT_SIZE); + mergeHighlight = new SquareNodeDecorator(MERGE_COLOUR, HIGHLIGHT_SIZE); + } + + @Override + public String getName() { + return "Merge nodes"; + } + + @Override + public void activate() { + editor.getViewer().addMouseListener(listener); + editor.getViewer().addMouseMotionListener(listener); + hover = null; + merge = null; + merging = false; + } + + @Override + public void deactivate() { + editor.getViewer().removeMouseListener(listener); + editor.getViewer().removeMouseMotionListener(listener); + editor.getViewer().clearAllNodeDecorators(); + editor.getViewer().repaint(); + } + + private void hover(GMLNode node) { + if (hover == node) { + return; + } + if (hover != null) { + editor.getViewer().clearNodeDecorator(hover); + } + hover = node; + if (hover != null) { + editor.getViewer().setNodeDecorator(hoverHighlight, hover); + } + editor.getViewer().repaint(); + } + + private void setMerge(GMLNode node) { + if (merge == node) { + return; + } + if (node == hover) { + return; + } + if (merge != null) { + editor.getViewer().clearNodeDecorator(merge); + } + merge = node; + if (merge != null) { + editor.getViewer().setNodeDecorator(mergeHighlight, merge); + } + editor.getViewer().repaint(); + } + + private class Listener implements MouseListener, MouseMotionListener { + @Override + public void mouseMoved(MouseEvent e) { + Point p = fixEventPoint(e.getPoint()); + GMLCoordinates c = editor.getViewer().getCoordinatesAtPoint(p.x, p.y); + GMLNode node = editor.getMap().findNearestNode(c.getX(), c.getY()); + hover(node); + } + + @Override + public void mouseDragged(MouseEvent e) { + if (hover == null) { + return; + } + if (!merging) { + return; + } + Point p = fixEventPoint(e.getPoint()); + GMLCoordinates c = editor.getViewer().getCoordinatesAtPoint(p.x, p.y); + GMLNode node = editor.getMap().findNearestNode(c.getX(), c.getY()); + setMerge(node); + } + + @Override + public void mouseReleased(MouseEvent e) { + if (e.getButton() == MouseEvent.BUTTON1) { + if (merging) { + if (hover != null && merge != null) { + editor.getMap().replaceNode(hover, merge); + editor.getMap().removeNode(hover); + editor.setChanged(); + } + if (hover != null) { + editor.getViewer().clearNodeDecorator(hover); + } + if (merge != null) { + editor.getViewer().clearNodeDecorator(merge); + } + editor.getViewer().repaint(); + hover = null; + merge = null; + merging = false; + } + } + } + + @Override + public void mousePressed(MouseEvent e) { + if (e.getButton() == MouseEvent.BUTTON1) { + merging = true; + } + } + + @Override + public void mouseClicked(MouseEvent e) { + } + @Override + public void mouseEntered(MouseEvent e) { + } + @Override + public void mouseExited(MouseEvent e) { + } + + private Point fixEventPoint(Point p) { + Insets insets = editor.getViewer().getInsets(); + return new Point(p.x - insets.left, p.y - insets.top); + } + } +} \ No newline at end of file diff --git a/modules/maps/src/maps/gml/editor/MoveNodeTool.java b/modules/maps/src/maps/gml/editor/MoveNodeTool.java new file mode 100644 index 0000000000000000000000000000000000000000..f6dc6e75f8447a884d9e81489a31f6336176b813 --- /dev/null +++ b/modules/maps/src/maps/gml/editor/MoveNodeTool.java @@ -0,0 +1,162 @@ +package maps.gml.editor; + +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.event.MouseMotionListener; +import java.awt.Color; +import java.awt.Point; +import java.awt.Insets; + +import maps.gml.view.NodeDecorator; +import maps.gml.view.SquareNodeDecorator; +import maps.gml.GMLNode; +import maps.gml.GMLCoordinates; + +import javax.swing.undo.AbstractUndoableEdit; + +/** + A tool for moving nodes. +*/ +public class MoveNodeTool extends AbstractTool { + private static final Color HIGHLIGHT_COLOUR = Color.BLACK; + private static final int HIGHLIGHT_SIZE = 6; + + private Listener listener; + private NodeDecorator highlight; + private GMLNode selected; + private GMLCoordinates pressCoords; + private GMLCoordinates originalCoords; + + /** + Construct a MoveNodeTool. + @param editor The editor instance. + */ + public MoveNodeTool(GMLEditor editor) { + super(editor); + listener = new Listener(); + highlight = new SquareNodeDecorator(HIGHLIGHT_COLOUR, HIGHLIGHT_SIZE); + selected = null; + } + + @Override + public String getName() { + return "Move node"; + } + + @Override + public void activate() { + editor.getViewer().addMouseListener(listener); + editor.getViewer().addMouseMotionListener(listener); + selected = null; + } + + @Override + public void deactivate() { + editor.getViewer().removeMouseListener(listener); + editor.getViewer().removeMouseMotionListener(listener); + editor.getViewer().clearAllNodeDecorators(); + editor.getViewer().repaint(); + } + + private void highlightNode(GMLNode node) { + if (selected == node) { + return; + } + if (selected != null) { + editor.getViewer().clearNodeDecorator(selected); + } + selected = node; + if (selected != null) { + editor.getViewer().setNodeDecorator(highlight, selected); + } + editor.getViewer().repaint(); + } + + private class Listener implements MouseListener, MouseMotionListener { + @Override + public void mouseMoved(MouseEvent e) { + Point p = fixEventPoint(e.getPoint()); + GMLCoordinates c = editor.getViewer().getCoordinatesAtPoint(p.x, p.y); + GMLNode node = editor.getMap().findNearestNode(c.getX(), c.getY()); + highlightNode(node); + } + + @Override + public void mousePressed(MouseEvent e) { + if (selected == null) { + return; + } + if (e.getButton() == MouseEvent.BUTTON1) { + Point p = fixEventPoint(e.getPoint()); + pressCoords = editor.getViewer().getCoordinatesAtPoint(p.x, p.y); + originalCoords = new GMLCoordinates(selected.getCoordinates()); + } + } + + @Override + public void mouseReleased(MouseEvent e) { + pressCoords = null; + editor.addEdit(new MoveNodeEdit(selected, originalCoords, new GMLCoordinates(selected.getCoordinates()))); + } + + @Override + public void mouseDragged(MouseEvent e) { + if (selected == null) { + return; + } + if (pressCoords == null) { + return; + } + Point p = fixEventPoint(e.getPoint()); + GMLCoordinates dragCoords = editor.getViewer().getCoordinatesAtPoint(p.x, p.y); + double dx = dragCoords.getX() - pressCoords.getX(); + double dy = dragCoords.getY() - pressCoords.getY(); + GMLCoordinates result = new GMLCoordinates(originalCoords.getX() + dx, originalCoords.getY() + dy); + editor.snap(result); + selected.setCoordinates(result); + editor.setChanged(); + editor.getViewer().repaint(); + } + + @Override + public void mouseClicked(MouseEvent e) { + } + @Override + public void mouseEntered(MouseEvent e) { + } + @Override + public void mouseExited(MouseEvent e) { + } + + private Point fixEventPoint(Point p) { + Insets insets = editor.getViewer().getInsets(); + return new Point(p.x - insets.left, p.y - insets.top); + } + } + + private class MoveNodeEdit extends AbstractUndoableEdit { + private GMLNode node; + private GMLCoordinates oldPosition; + private GMLCoordinates newPosition; + + public MoveNodeEdit(GMLNode node, GMLCoordinates oldPosition, GMLCoordinates newPosition) { + this.node = node; + this.oldPosition = oldPosition; + this.newPosition = newPosition; + } + + @Override + public void undo() { + super.undo(); + node.setCoordinates(oldPosition); + editor.getViewer().repaint(); + } + + @Override + public void redo() { + super.redo(); + node.setCoordinates(newPosition); + editor.getViewer().repaint(); + } + } +} \ No newline at end of file diff --git a/modules/maps/src/maps/gml/editor/PanZoomTool.java b/modules/maps/src/maps/gml/editor/PanZoomTool.java new file mode 100644 index 0000000000000000000000000000000000000000..308375bd4c0b011a51df4e6e7e315b0ffc0413bb --- /dev/null +++ b/modules/maps/src/maps/gml/editor/PanZoomTool.java @@ -0,0 +1,27 @@ +package maps.gml.editor; + +/** + A tool for panning and zooming the view. +*/ +public class PanZoomTool extends AbstractTool { + /** + Construct a PanZoomTool. + @param editor The editor instance. + */ + public PanZoomTool(GMLEditor editor) { + super(editor); + } + + @Override + public String getName() { + return "Pan/Zoom"; + } + + @Override + public void activate() { + } + + @Override + public void deactivate() { + } +} \ No newline at end of file diff --git a/modules/maps/src/maps/gml/editor/ProgressFunction.java b/modules/maps/src/maps/gml/editor/ProgressFunction.java new file mode 100644 index 0000000000000000000000000000000000000000..70c0ca37b9da75dc735c6577f8ac9fd69832db76 --- /dev/null +++ b/modules/maps/src/maps/gml/editor/ProgressFunction.java @@ -0,0 +1,139 @@ +package maps.gml.editor; + +import java.awt.Window; +import java.awt.Dialog; +import java.awt.BorderLayout; +import javax.swing.JDialog; +import javax.swing.JProgressBar; +import javax.swing.SwingUtilities; + +import rescuecore2.log.Logger; + +/** + Abstract base class for Function implementations that require a progress dialog. +*/ +public abstract class ProgressFunction extends AbstractFunction { + private JProgressBar progress; + + /** + Construct a ProgressFunction. + @param editor The editor instance. + */ + protected ProgressFunction(GMLEditor editor) { + super(editor); + progress = new JProgressBar(); + progress.setStringPainted(true); + } + + @Override + public void execute() { + final JDialog dialog = new JDialog((Window)editor.getViewer().getTopLevelAncestor(), getTitle(), Dialog.ModalityType.APPLICATION_MODAL); + dialog.getContentPane().add(progress, BorderLayout.CENTER); + dialog.pack(); + SwingUtilities.invokeLater(new Runnable() { + public void run() { + progress.setValue(0); + progress.setIndeterminate(true); + } + }); + Thread t = new Thread() { + @Override + public void run() { + try { + executeImpl(); + } + // CHECKSTYLE:OFF:IllegalCatch + catch (RuntimeException e) { + // CHECKSTYLE:ON:IllegalCatch + Logger.error("Error running " + this, e); + } + dialog.setVisible(false); + dialog.dispose(); + } + }; + t.start(); + dialog.setVisible(true); + } + + /** + Execute the function. + */ + protected abstract void executeImpl(); + + /** + Get the title for the progress dialog. + @return The dialog title. + */ + protected abstract String getTitle(); + + /** + Set the progress level. + @param amount The new progress. + */ + protected void setProgress(final int amount) { + SwingUtilities.invokeLater(new Runnable() { + public void run() { + progress.setValue(amount); + progress.setString(progress.getValue() + " / " + progress.getMaximum()); + } + }); + } + + /** + Increase the progress level by one. + */ + protected void bumpProgress() { + SwingUtilities.invokeLater(new Runnable() { + public void run() { + progress.setValue(progress.getValue() + 1); + progress.setString(progress.getValue() + " / " + progress.getMaximum()); + } + }); + } + + /** + Increase the maximum progress level by one. + */ + protected void bumpMaxProgress() { + bumpMaxProgress(1); + } + + /** + Increase the maximum progress level by some amount. + @param amount The amount to increase the maximum progress level. + */ + protected void bumpMaxProgress(final int amount) { + SwingUtilities.invokeLater(new Runnable() { + public void run() { + progress.setMaximum(progress.getMaximum() + amount); + progress.setString(progress.getValue() + " / " + progress.getMaximum()); + } + }); + } + + /** + Set the progress maximum. + @param max The new progress maximum. + */ + protected void setProgressLimit(final int max) { + SwingUtilities.invokeLater(new Runnable() { + public void run() { + progress.setIndeterminate(false); + progress.setMaximum(max); + progress.setString(progress.getValue() + " / " + progress.getMaximum()); + } + }); + } + + /** + Set the progress string. + @param s The new progress string. + */ + protected void setProgressString(final String s) { + SwingUtilities.invokeLater(new Runnable() { + public void run() { + progress.setString(s); + } + }); + } +} \ No newline at end of file diff --git a/modules/maps/src/maps/gml/editor/PruneOrphanEdgesFunction.java b/modules/maps/src/maps/gml/editor/PruneOrphanEdgesFunction.java new file mode 100644 index 0000000000000000000000000000000000000000..039940aa375272f595a7f90c66896351ba45bb6f --- /dev/null +++ b/modules/maps/src/maps/gml/editor/PruneOrphanEdgesFunction.java @@ -0,0 +1,69 @@ +package maps.gml.editor; + +import javax.swing.undo.AbstractUndoableEdit; + +import java.util.HashSet; +import java.util.Collection; + +import maps.gml.GMLEdge; + +import rescuecore2.log.Logger; + +/** + A function for pruning edges that are not attached to any shapes. +*/ +public class PruneOrphanEdgesFunction extends AbstractFunction { + /** + Construct a PruneOrphanEdgesFunction. + @param editor The editor instance. + */ + public PruneOrphanEdgesFunction(GMLEditor editor) { + super(editor); + } + + @Override + public String getName() { + return "Prune orphaned edges"; + } + + @Override + public void execute() { + // Go through all edges and remove any that are not attached to shapes. + final Collection remaining = new HashSet(editor.getMap().getEdges()); + final Collection deleted = new HashSet(); + for (GMLEdge next : remaining) { + if (editor.getMap().getAttachedShapes(next).isEmpty()) { + editor.getMap().removeEdge(next); + deleted.add(next); + } + } + if (!deleted.isEmpty()) { + editor.setChanged(); + editor.getViewer().repaint(); + } + Logger.debug("Removed " + deleted.size() + " edges"); + editor.addEdit(new DeleteEdgesEdit(deleted)); + } + + private class DeleteEdgesEdit extends AbstractUndoableEdit { + private Collection edges; + + public DeleteEdgesEdit(Collection edges) { + this.edges = edges; + } + + @Override + public void undo() { + super.undo(); + editor.getMap().add(edges); + editor.getViewer().repaint(); + } + + @Override + public void redo() { + super.redo(); + editor.getMap().remove(edges); + editor.getViewer().repaint(); + } + } +} \ No newline at end of file diff --git a/modules/maps/src/maps/gml/editor/PruneOrphanNodesFunction.java b/modules/maps/src/maps/gml/editor/PruneOrphanNodesFunction.java new file mode 100644 index 0000000000000000000000000000000000000000..91c18e8f7693a89c74396b68c9849c14a927cd33 --- /dev/null +++ b/modules/maps/src/maps/gml/editor/PruneOrphanNodesFunction.java @@ -0,0 +1,69 @@ +package maps.gml.editor; + +import javax.swing.undo.AbstractUndoableEdit; + +import java.util.HashSet; +import java.util.Collection; + +import maps.gml.GMLNode; + +import rescuecore2.log.Logger; + +/** + A function for pruning nodes that are not attached to any edges. +*/ +public class PruneOrphanNodesFunction extends AbstractFunction { + /** + Construct a PruneOrphanNodesFunction. + @param editor The editor instance. + */ + public PruneOrphanNodesFunction(GMLEditor editor) { + super(editor); + } + + @Override + public String getName() { + return "Prune orphaned nodes"; + } + + @Override + public void execute() { + // Go through all nodes and remove any that are not attached to edges. + final Collection remaining = new HashSet(editor.getMap().getNodes()); + final Collection deleted = new HashSet(); + for (GMLNode next : remaining) { + if (editor.getMap().getAttachedEdges(next).isEmpty()) { + editor.getMap().removeNode(next); + deleted.add(next); + } + } + if (!deleted.isEmpty()) { + editor.setChanged(); + editor.getViewer().repaint(); + } + Logger.debug("Removed " + deleted.size() + " nodes"); + editor.addEdit(new DeleteNodesEdit(deleted)); + } + + private class DeleteNodesEdit extends AbstractUndoableEdit { + private Collection nodes; + + public DeleteNodesEdit(Collection nodes) { + this.nodes = nodes; + } + + @Override + public void undo() { + super.undo(); + editor.getMap().add(nodes); + editor.getViewer().repaint(); + } + + @Override + public void redo() { + super.redo(); + editor.getMap().remove(nodes); + editor.getViewer().repaint(); + } + } +} \ No newline at end of file diff --git a/modules/maps/src/maps/gml/editor/ScaleFunction.java b/modules/maps/src/maps/gml/editor/ScaleFunction.java new file mode 100644 index 0000000000000000000000000000000000000000..59178d91a9e309335d4b381d9939dd7dfbe468d0 --- /dev/null +++ b/modules/maps/src/maps/gml/editor/ScaleFunction.java @@ -0,0 +1,38 @@ +package maps.gml.editor; + +import javax.swing.JOptionPane; + +import maps.ScaleConversion; + +/** + A function for scaling the map. +*/ +public class ScaleFunction extends AbstractFunction { + /** + Construct a ScaleFunction. + @param editor The editor instance. + */ + public ScaleFunction(GMLEditor editor) { + super(editor); + } + + @Override + public String getName() { + return "Scale map"; + } + + @Override + public void execute() { + String s = JOptionPane.showInputDialog("Enter scale factor"); + if (s != null) { + try { + double factor = Double.parseDouble(s); + editor.getMap().convertCoordinates(new ScaleConversion(editor.getMap().getMinX(), editor.getMap().getMinY(), factor, factor)); + editor.setChanged(); + } + catch (NumberFormatException e) { + e.printStackTrace(); + } + } + } +} \ No newline at end of file diff --git a/modules/maps/src/maps/gml/editor/Snap.java b/modules/maps/src/maps/gml/editor/Snap.java new file mode 100644 index 0000000000000000000000000000000000000000..2886082e116f6aebea5ee2fa543afe1934d9dd16 --- /dev/null +++ b/modules/maps/src/maps/gml/editor/Snap.java @@ -0,0 +1,73 @@ +package maps.gml.editor; + +import maps.gml.GMLCoordinates; + +/** + Class for snapping coordinates to a grid. +*/ +public class Snap { + private static final double DEFAULT_RESOLUTION = 0.001; + + private double resolution; + private boolean enabled; + + /** + Create a disabled Snap with default resolution. + */ + public Snap() { + resolution = DEFAULT_RESOLUTION; + enabled = false; + } + + /** + Set the resolution. + @param d The new resolution. + */ + public void setResolution(double d) { + resolution = d; + } + + /** + Get the resolution. + @return The resolution. + */ + public double getResolution() { + return resolution; + } + + /** + Set whether to enable snapping. + @param b Whether to enable snapping. + */ + public void setEnabled(boolean b) { + enabled = b; + } + + /** + Find out if this Snap is enabled. + @return True if enabled. + */ + public boolean isEnabled() { + return enabled; + } + + /** + Snap a set of coordinates to the grid. + @param c The coordinates to snap. + */ + public void snap(GMLCoordinates c) { + if (!enabled) { + return; + } + double x = round(c.getX()); + double y = round(c.getY()); + c.setX(x); + c.setY(y); + } + + private double round(double d) { + // CHECKSTYLE:OFF:MagicNumber + return Math.floor((d / resolution) + 0.5) * resolution; + // CHECKSTYLE:ON:MagicNumber + } +} \ No newline at end of file diff --git a/modules/maps/src/maps/gml/editor/SplitEdgeTool.java b/modules/maps/src/maps/gml/editor/SplitEdgeTool.java new file mode 100644 index 0000000000000000000000000000000000000000..e72c9a20d0675a0e959343c602ed6ddba83e2dd2 --- /dev/null +++ b/modules/maps/src/maps/gml/editor/SplitEdgeTool.java @@ -0,0 +1,148 @@ +package maps.gml.editor; + +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.event.MouseMotionListener; +import java.awt.Color; +import java.awt.Point; +import java.awt.Insets; + +import maps.gml.view.NodeDecorator; +import maps.gml.view.SquareNodeDecorator; +import maps.gml.view.EdgeDecorator; +import maps.gml.view.LineEdgeDecorator; +import maps.gml.GMLNode; +import maps.gml.GMLEdge; +import maps.gml.GMLCoordinates; +import maps.gml.GMLTools; + +import rescuecore2.misc.geometry.Point2D; +import rescuecore2.misc.geometry.Line2D; +import rescuecore2.misc.geometry.GeometryTools2D; + +/** + A tool for splitting edges. +*/ +public class SplitEdgeTool extends AbstractTool { + private static final Color EDGE_COLOUR = Color.BLUE; + private static final Color NODE_COLOUR = Color.BLACK; + private static final int NODE_SIZE = 6; + + private Listener listener; + private NodeDecorator nodeHighlight; + private EdgeDecorator edgeHighlight; + private GMLNode node; + private GMLEdge edge; + + /** + Construct a SplitEdgeTool. + @param editor The editor instance. + */ + public SplitEdgeTool(GMLEditor editor) { + super(editor); + listener = new Listener(); + nodeHighlight = new SquareNodeDecorator(NODE_COLOUR, NODE_SIZE); + edgeHighlight = new LineEdgeDecorator(EDGE_COLOUR); + edge = null; + node = null; + } + + @Override + public String getName() { + return "Split edge"; + } + + @Override + public void activate() { + editor.getViewer().addMouseListener(listener); + editor.getViewer().addMouseMotionListener(listener); + node = null; + edge = null; + } + + @Override + public void deactivate() { + editor.getViewer().removeMouseListener(listener); + editor.getViewer().removeMouseMotionListener(listener); + editor.getViewer().clearAllNodeDecorators(); + editor.getViewer().clearAllEdgeDecorators(); + editor.getViewer().repaint(); + if (node != null) { + editor.getMap().removeNode(node); + } + } + + private void update(GMLCoordinates c) { + if (node == null) { + node = editor.getMap().createNode(c); + editor.getViewer().setNodeDecorator(nodeHighlight, node); + } + GMLEdge newEdge = editor.getMap().findNearestEdge(c.getX(), c.getY()); + if (newEdge != edge) { + if (edge != null) { + editor.getViewer().clearEdgeDecorator(edge); + } + edge = newEdge; + editor.getViewer().setEdgeDecorator(edgeHighlight, edge); + } + // Snap the node coordinates to the edge + Line2D line = GMLTools.toLine(edge); + Point2D point = new Point2D(c.getX(), c.getY()); + Point2D closest = GeometryTools2D.getClosestPointOnSegment(line, point); + c.setX(closest.getX()); + c.setY(closest.getY()); + node.setCoordinates(c); + editor.getViewer().repaint(); + } + + private void split() { + if (node == null || edge == null) { + return; + } + editor.getMap().splitEdge(edge, node); + editor.getMap().removeEdge(edge); + editor.setChanged(); + editor.getViewer().clearAllNodeDecorators(); + editor.getViewer().clearAllEdgeDecorators(); + editor.getViewer().repaint(); + node = null; + edge = null; + } + + private class Listener implements MouseListener, MouseMotionListener { + @Override + public void mouseMoved(MouseEvent e) { + Point p = fixEventPoint(e.getPoint()); + GMLCoordinates c = editor.snap(editor.getViewer().getCoordinatesAtPoint(p.x, p.y)); + update(c); + } + + @Override + public void mouseClicked(MouseEvent e) { + if (e.getButton() == MouseEvent.BUTTON1) { + split(); + } + } + + @Override + public void mousePressed(MouseEvent e) { + } + @Override + public void mouseReleased(MouseEvent e) { + } + @Override + public void mouseDragged(MouseEvent e) { + } + @Override + public void mouseEntered(MouseEvent e) { + } + @Override + public void mouseExited(MouseEvent e) { + } + + private Point fixEventPoint(Point p) { + Insets insets = editor.getViewer().getInsets(); + return new Point(p.x - insets.left, p.y - insets.top); + } + } +} \ No newline at end of file diff --git a/modules/maps/src/maps/gml/editor/SplitEdgesFunction.java b/modules/maps/src/maps/gml/editor/SplitEdgesFunction.java new file mode 100644 index 0000000000000000000000000000000000000000..7b99c24eeba91170418ea749a2d39283c94a6814 --- /dev/null +++ b/modules/maps/src/maps/gml/editor/SplitEdgesFunction.java @@ -0,0 +1,98 @@ +package maps.gml.editor; + +import javax.swing.JOptionPane; + +import java.util.Queue; +import java.util.LinkedList; +import java.util.Collection; +import java.util.HashSet; + +import maps.gml.GMLNode; +import maps.gml.GMLEdge; +import maps.gml.GMLTools; + +import rescuecore2.log.Logger; +import rescuecore2.misc.geometry.Point2D; +import rescuecore2.misc.geometry.Line2D; +import rescuecore2.misc.geometry.GeometryTools2D; + +/** + A function for splitting edges that cover nearby nodes. +*/ +public class SplitEdgesFunction extends ProgressFunction { + private static final double DEFAULT_THRESHOLD = 0.001; + + private double threshold; + + /** + Construct a SplitEdgesFunction. + @param editor The editor instance. + */ + public SplitEdgesFunction(GMLEditor editor) { + super(editor); + } + + @Override + public String getName() { + return "Split edges"; + } + + @Override + public void execute() { + String s = JOptionPane.showInputDialog(editor.getViewer(), "Enter the desired distance threshold (in m)", DEFAULT_THRESHOLD); + if (s == null) { + return; + } + threshold = Double.parseDouble(s); + super.execute(); + } + + @Override + protected String getTitle() { + return "Splitting edges"; + } + + @Override + protected void executeImpl() { + // Go through all edges and split any that cover nearby nodes + final Queue remaining = new LinkedList(); + final Collection nodes = new HashSet(); + synchronized (editor.getMap()) { + remaining.addAll(editor.getMap().getEdges()); + nodes.addAll(editor.getMap().getNodes()); + } + setProgressLimit(remaining.size()); + int count = 0; + while (!remaining.isEmpty()) { + GMLEdge next = remaining.remove(); + Line2D line = GMLTools.toLine(next); + // Look for nodes that are close to the line + for (GMLNode node : nodes) { + if (node == next.getStart() || node == next.getEnd()) { + continue; + } + Point2D p = GMLTools.toPoint(node); + Point2D closest = GeometryTools2D.getClosestPointOnSegment(line, p); + if (GeometryTools2D.getDistance(p, closest) < threshold) { + // Split the edge + Collection newEdges; + synchronized (editor.getMap()) { + newEdges = editor.getMap().splitEdge(next, node); + editor.getMap().removeEdge(next); + newEdges.removeAll(editor.getMap().getEdges()); + } + remaining.addAll(newEdges); + bumpMaxProgress(newEdges.size()); + ++count; + break; + } + } + bumpProgress(); + } + if (count != 0) { + editor.setChanged(); + editor.getViewer().repaint(); + } + Logger.debug("Split " + count + " edges"); + } +} \ No newline at end of file diff --git a/modules/maps/src/maps/gml/editor/SplitShapeTool.java b/modules/maps/src/maps/gml/editor/SplitShapeTool.java new file mode 100755 index 0000000000000000000000000000000000000000..24695995035f54e8c5ab6e6e253874544394cf64 --- /dev/null +++ b/modules/maps/src/maps/gml/editor/SplitShapeTool.java @@ -0,0 +1,362 @@ +package maps.gml.editor; + +import java.awt.Color; +import java.awt.Insets; +import java.awt.Point; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.event.MouseMotionListener; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; + +import javax.swing.undo.AbstractUndoableEdit; + +import maps.gml.GMLBuilding; +import maps.gml.GMLCoordinates; +import maps.gml.GMLEdge; +import maps.gml.GMLNode; +import maps.gml.GMLRoad; +import maps.gml.GMLShape; +import maps.gml.GMLSpace; +import maps.gml.view.LineOverlay; +import maps.gml.view.NodeDecorator; +import maps.gml.view.SquareNodeDecorator; +import rescuecore2.log.Logger; +import rescuecore2.misc.Pair; +import rescuecore2.misc.geometry.GeometryTools2D; +import rescuecore2.misc.geometry.Point2D; + +/** + * A tool for creating edges. + */ +public class SplitShapeTool extends AbstractTool { + + private static final Color HIGHLIGHT_COLOUR = Color.BLUE; + private static final int HIGHLIGHT_SIZE = 6; + + private static final double THRESHOLD = 0.001; + + private Listener listener; + private NodeDecorator nodeHighlight; + private LineOverlay overlay; + + private GMLNode hover; + private GMLNode start; + private GMLNode end; + // private GMLEdge edge; + + + /** + * Construct a CreateEdgeTool. + * + * @param editor + * The editor instance. + */ + public SplitShapeTool( GMLEditor editor ) { + super( editor ); + listener = new Listener(); + nodeHighlight = new SquareNodeDecorator( HIGHLIGHT_COLOUR, HIGHLIGHT_SIZE ); + overlay = new LineOverlay( HIGHLIGHT_COLOUR, true ); + } + + + @Override + public String getName() { + return "Split shape"; + } + + + @Override + public void activate() { + editor.getViewer().addMouseListener( listener ); + editor.getViewer().addMouseMotionListener( listener ); + editor.getViewer().addOverlay( overlay ); + hover = null; + start = null; + end = null; + // edge = null; + } + + + @Override + public void deactivate() { + editor.getViewer().removeMouseListener( listener ); + editor.getViewer().removeMouseMotionListener( listener ); + editor.getViewer().clearAllNodeDecorators(); + editor.getViewer().removeOverlay( overlay ); + editor.getViewer().repaint(); + } + + + private void setHover( GMLNode node ) { + if ( hover == node ) { + return; + } + if ( hover != null ) { + editor.getViewer().clearNodeDecorator( hover ); + } + hover = node; + if ( hover != null ) { + editor.getViewer().setNodeDecorator( nodeHighlight, hover ); + } + editor.getViewer().repaint(); + } + + + private void setStart( GMLNode node ) { + if ( start == node ) { + return; + } + if ( start != null ) { + editor.getViewer().clearNodeDecorator( start ); + } + start = node; + if ( start != null ) { + editor.getViewer().setNodeDecorator( nodeHighlight, start ); + } + editor.getViewer().repaint(); + } + + + private void setEnd( GMLNode node ) { + if ( start == node || end == node ) { + return; + } + if ( end != null ) { + editor.getViewer().clearNodeDecorator( end ); + } + end = node; + if ( end != null ) { + editor.getViewer().setNodeDecorator( nodeHighlight, end ); + } + editor.getViewer().repaint(); + } + + + private class Listener implements MouseListener, MouseMotionListener { + + @Override + public void mousePressed( MouseEvent e ) { + if ( e.getButton() == MouseEvent.BUTTON1 ) { + Point p = fixEventPoint( e.getPoint() ); + GMLCoordinates c = editor.getViewer().getCoordinatesAtPoint( p.x, p.y ); + GMLNode node = editor.getMap().findNearestNode( c.getX(), c.getY() ); + overlay.setStart( new Point2D( node.getX(), node.getY() ) ); + setStart( node ); + setHover( null ); + } + } + + + @Override + public void mouseReleased( MouseEvent e ) { + if ( e.getButton() == MouseEvent.BUTTON1 ) { + if ( start != null && end != null ) { + SplitShapeEdit edit = splitByEdge(); + editor.setChanged(); + if ( edit != null ) { + editor.addEdit( edit ); + } + editor.getViewer().clearAllNodeDecorators(); + overlay.setStart( null ); + overlay.setEnd( null ); + editor.getViewer().repaint(); + start = null; + end = null; + hover = null; + } + } + } + + + private SplitShapeEdit splitByEdge() { + Collection add = new ArrayList(); + Collection delete = new ArrayList(); + + GMLEdge edge = editor.getMap().createEdge( start, end ); + Collection startEdges = editor.getMap() + .getAttachedEdges( start ); + Collection endEdges = editor.getMap().getAttachedEdges( end ); + Collection startShapes = new HashSet(); + Collection endShapes = new HashSet(); + for ( GMLEdge next : startEdges ) { + startShapes.addAll( editor.getMap().getAttachedShapes( next ) ); + } + for ( GMLEdge next : endEdges ) { + endShapes.addAll( editor.getMap().getAttachedShapes( next ) ); + } + for ( GMLShape shape : startShapes ) { + if ( endShapes.contains( shape ) ) { + Pair split = splitShape( shape, edge ); + if ( split != null ) { + add.add( split.first() ); + add.add( split.second() ); + delete.add( shape ); + } + } + } + if ( !add.isEmpty() ) { + edge.setPassable( true ); + return new SplitShapeEdit( edge, add, delete ); + } else { + editor.getMap().remove( edge ); + return null; + } + } + + + private Pair splitShape( GMLShape shape, + GMLEdge edge ) { + List nodes1 = new ArrayList(); + List nodes2 = new ArrayList(); + boolean first = true; + for ( GMLNode n : shape.getUnderlyingNodes() ) { + if ( n == edge.getStart() || n == edge.getEnd() ) { + first = !first; + nodes1.add( n ); + nodes2.add( n ); + } else if ( first ) { + nodes1.add( n ); + } else { + nodes2.add( n ); + } + } + if ( nodes1.size() <= 2 || nodes2.size() <= 2 ) { + return null; + } + + // Check if we really split an interior edge + double oldArea = area( shape.getUnderlyingNodes() ); + double area1 = area( nodes1 ); + double area2 = area( nodes2 ); + if ( area1 + area2 > oldArea + THRESHOLD ) { + return null; + } + + GMLShape s1 = null; + GMLShape s2 = null; + if ( shape instanceof GMLBuilding ) { + GMLBuilding b = (GMLBuilding) shape; + GMLBuilding b1 = editor.getMap().createBuildingFromNodes( nodes1 ); + GMLBuilding b2 = editor.getMap().createBuildingFromNodes( nodes2 ); + b1.setCode( b.getCode() ); + b2.setCode( b.getCode() ); + b1.setFloors( b.getFloors() ); + b2.setFloors( b.getFloors() ); + b1.setImportance( b.getImportance() ); + b2.setImportance( b.getImportance() ); + b1.setCapacity( b.getCapacity() ); + b2.setCapacity( b.getCapacity() ); + s1 = b1; + s2 = b2; + } else if ( shape instanceof GMLRoad ) { + // GMLBuilding b = (GMLBuilding) shape; + s1 = editor.getMap().createRoadFromNodes( nodes1 ); + s2 = editor.getMap().createRoadFromNodes( nodes2 ); + } else if ( shape instanceof GMLSpace ) { + // GMLBuilding b = (GMLBuilding) shape; + s1 = editor.getMap().createSpaceFromNodes( nodes1 ); + s2 = editor.getMap().createSpaceFromNodes( nodes2 ); + } else { + throw new IllegalArgumentException( + "Shape is not a building, road or space" ); + } + editor.getMap().remove( shape ); + return new Pair( s1, s2 ); + } + + + private double area( List nodes ) { + List vertices = new ArrayList(); + for ( GMLNode n : nodes ) { + vertices.add( new Point2D( n.getX(), n.getY() ) ); + } + return GeometryTools2D.computeArea( vertices ); + } + + + @Override + public void mouseDragged( MouseEvent e ) { + if ( start != null ) { + Point p = fixEventPoint( e.getPoint() ); + GMLCoordinates c = editor.getViewer().getCoordinatesAtPoint( p.x, p.y ); + GMLNode node = editor.getMap().findNearestNode( c.getX(), c.getY() ); + overlay.setEnd( new Point2D( node.getX(), node.getY() ) ); + setEnd( node ); + } + } + + + @Override + public void mouseMoved( MouseEvent e ) { + Point p = fixEventPoint( e.getPoint() ); + GMLCoordinates c = editor + .snap( editor.getViewer().getCoordinatesAtPoint( p.x, p.y ) ); + GMLNode node = editor.getMap().findNearestNode( c.getX(), c.getY() ); + setHover( node ); + } + + + @Override + public void mouseClicked( MouseEvent e ) { + } + + + @Override + public void mouseEntered( MouseEvent e ) { + } + + + @Override + public void mouseExited( MouseEvent e ) { + } + + + private Point fixEventPoint( Point p ) { + Insets insets = editor.getViewer().getInsets(); + return new Point( p.x - insets.left, p.y - insets.top ); + } + } + + private class SplitShapeEdit extends AbstractUndoableEdit { + + private Collection add; + private Collection remove; + private GMLEdge edge; + + + public SplitShapeEdit( GMLEdge edge, Collection add, Collection remove ) { + this.edge = edge; + this.add = add; + this.remove = remove; + } + + + @Override + public void undo() { + super.undo(); + editor.getMap().removeEdge( edge ); + editor.getMap().remove( add ); + editor.getMap().add( remove ); + editor.getViewer().repaint(); + } + + + @Override + public void redo() { + super.redo(); + editor.getMap().addEdge( edge ); + for ( GMLShape r : remove ) { + Logger.debug( "remove: " + r.toString() ); + } + for ( GMLShape r : add ) { + Logger.debug( "add: " + r.toString() ); + } + editor.getMap().remove( remove ); + editor.getMap().add( add ); + editor.getViewer().repaint(); + } + } +} \ No newline at end of file diff --git a/modules/maps/src/maps/gml/editor/TogglePassableTool.java b/modules/maps/src/maps/gml/editor/TogglePassableTool.java new file mode 100644 index 0000000000000000000000000000000000000000..39b0735cfb361cdf7067e10d2fa37a02462235d2 --- /dev/null +++ b/modules/maps/src/maps/gml/editor/TogglePassableTool.java @@ -0,0 +1,166 @@ +package maps.gml.editor; + +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.event.MouseMotionListener; +import java.awt.Color; +import java.awt.Point; +import java.awt.Insets; + +import javax.swing.undo.AbstractUndoableEdit; + +import java.util.Collection; +import java.util.Iterator; + +import maps.gml.GMLEdge; +import maps.gml.GMLShape; +import maps.gml.GMLCoordinates; +import maps.gml.view.EdgeDecorator; +import maps.gml.view.LineEdgeDecorator; + +/** + A tool for toggling the passable flag on edges. +*/ +public class TogglePassableTool extends AbstractTool { + private static final Color HIGHLIGHT_COLOUR = Color.BLUE; + + private Listener listener; + private EdgeDecorator highlight; + private GMLEdge selected; + + /** + Construct a TogglePassableTool. + @param editor The editor instance. + */ + public TogglePassableTool(GMLEditor editor) { + super(editor); + highlight = new LineEdgeDecorator(HIGHLIGHT_COLOUR); + listener = new Listener(); + } + + @Override + public String getName() { + return "Toggle passable"; + } + + @Override + public void activate() { + editor.getViewer().addMouseListener(listener); + editor.getViewer().addMouseMotionListener(listener); + selected = null; + } + + @Override + public void deactivate() { + editor.getViewer().removeMouseListener(listener); + editor.getViewer().removeMouseMotionListener(listener); + editor.getViewer().clearAllEdgeDecorators(); + editor.getViewer().repaint(); + } + + private void highlight(GMLEdge edge) { + if (selected == edge) { + return; + } + if (selected != null) { + editor.getViewer().clearEdgeDecorator(selected); + } + selected = edge; + if (selected != null) { + editor.getViewer().setEdgeDecorator(highlight, selected); + } + editor.getViewer().repaint(); + } + + private void toggle() { + boolean isPassable = !selected.isPassable(); + setPassable(selected, isPassable); + editor.addEdit(new ToggleEdit(selected, isPassable)); + } + + private void setPassable(GMLEdge edge, boolean passable) { + edge.setPassable(passable); + Collection attached = editor.getMap().getAttachedShapes(edge); + Iterator it = attached.iterator(); + GMLShape first = it.next(); + GMLShape second = it.next(); + if (passable) { + first.setNeighbour(edge, second.getID()); + second.setNeighbour(edge, first.getID()); + } + else { + first.setNeighbour(edge, null); + second.setNeighbour(edge, null); + } + editor.setChanged(); + editor.getViewer().repaint(); + } + + private class Listener implements MouseListener, MouseMotionListener { + @Override + public void mouseMoved(MouseEvent e) { + Point p = fixEventPoint(e.getPoint()); + GMLCoordinates c = editor.getViewer().getCoordinatesAtPoint(p.x, p.y); + GMLEdge edge = editor.getMap().findNearestEdge(c.getX(), c.getY()); + if (editor.getMap().getAttachedShapes(edge).size() == 2) { + highlight(edge); + } + } + + @Override + public void mouseClicked(MouseEvent e) { + if (selected == null) { + return; + } + if (e.getButton() == MouseEvent.BUTTON1) { + toggle(); + highlight(null); + } + } + + @Override + public void mousePressed(MouseEvent e) { + } + @Override + public void mouseReleased(MouseEvent e) { + } + @Override + public void mouseDragged(MouseEvent e) { + } + @Override + public void mouseEntered(MouseEvent e) { + } + @Override + public void mouseExited(MouseEvent e) { + } + + private Point fixEventPoint(Point p) { + Insets insets = editor.getViewer().getInsets(); + return new Point(p.x - insets.left, p.y - insets.top); + } + } + + private class ToggleEdit extends AbstractUndoableEdit { + private GMLEdge edge; + private boolean newPassable; + + public ToggleEdit(GMLEdge edge, boolean newPassable) { + this.edge = edge; + this.newPassable = newPassable; + } + + @Override + public void undo() { + super.undo(); + setPassable(edge, !newPassable); + editor.getViewer().repaint(); + } + + @Override + public void redo() { + super.redo(); + setPassable(edge, newPassable); + editor.getViewer().repaint(); + } + } +} \ No newline at end of file diff --git a/modules/maps/src/maps/gml/editor/Tool.java b/modules/maps/src/maps/gml/editor/Tool.java new file mode 100644 index 0000000000000000000000000000000000000000..405c59e78c9af7ad5944bc8b678588dbf91cf3aa --- /dev/null +++ b/modules/maps/src/maps/gml/editor/Tool.java @@ -0,0 +1,22 @@ +package maps.gml.editor; + +/** + Interface for an editing tool. +*/ +public interface Tool { + /** + Get the name of this tool. + @return The name of the tool. + */ + String getName(); + + /** + Activate this tool. + */ + void activate(); + + /** + Deactivate this tool. + */ + void deactivate(); +} \ No newline at end of file diff --git a/modules/maps/src/maps/gml/editor/ValidateFunction.java b/modules/maps/src/maps/gml/editor/ValidateFunction.java new file mode 100644 index 0000000000000000000000000000000000000000..31ecad85f6c7f9af923baa03559ee8f3686f9007 --- /dev/null +++ b/modules/maps/src/maps/gml/editor/ValidateFunction.java @@ -0,0 +1,109 @@ +package maps.gml.editor; + +import java.awt.Color; +import java.util.ArrayList; +import java.util.Collection; + +import maps.gml.GMLBuilding; +import maps.gml.GMLEdge; +import maps.gml.GMLMap; +import maps.gml.GMLNode; +import maps.gml.GMLObject; +import maps.gml.GMLRoad; +import maps.gml.GMLSpace; +import maps.gml.view.DecoratorOverlay; +import maps.gml.view.EdgeDecorator; +import maps.gml.view.FilledShapeDecorator; +import maps.gml.view.LineEdgeDecorator; +import maps.gml.view.NodeDecorator; +import maps.gml.view.SquareNodeDecorator; +import maps.validate.GMLMapValidator; +import maps.validate.MapValidator; +import maps.validate.ValidationError; + +import rescuecore2.log.Logger; + +/** + * Check the map for errors and highlight them on the map. + * + */ +public class ValidateFunction extends AbstractFunction { + private static final Color HIGHLIGHT_COLOUR = Color.RED; + private static final int NODE_SIZE = 5; + + private DecoratorOverlay overlay = new DecoratorOverlay(); + + private NodeDecorator nodeHighlight; + private EdgeDecorator edgeHighlight; + private FilledShapeDecorator shapeHighlight; + + /** + * Create a new ValidateFunction. + * @param editor The editor. + */ + public ValidateFunction(GMLEditor editor) { + super(editor); + nodeHighlight = new SquareNodeDecorator(HIGHLIGHT_COLOUR, NODE_SIZE); + edgeHighlight = new LineEdgeDecorator(HIGHLIGHT_COLOUR); + shapeHighlight = new FilledShapeDecorator(HIGHLIGHT_COLOUR, + HIGHLIGHT_COLOUR, HIGHLIGHT_COLOUR); + } + + @Override + public void execute() { + overlay.clearAllDecorators(); + + Collection allErrors = new ArrayList(); + for (MapValidator validator : GMLMapValidator + .getDefaultValidators()) { + Logger.info("Validating " + validator); + Collection errors = validator.validate(editor + .getMap()); + allErrors.addAll(errors); + + for (ValidationError e : errors) { + System.out.println(e); + addDecorator(e.getId()); + } + } + editor.getInspector().setErrors(allErrors); + + editor.getViewer().removeOverlay(overlay); + editor.getViewer().addOverlay(overlay); + editor.getViewer().repaint(); + + } + + /** + * Add a new error decorator for the object with the given id. + * @param id + */ + private void addDecorator(int id) { + GMLObject obj = editor.getMap().getObject(id); + if (obj == null) { + return; + } + if (obj instanceof GMLBuilding) { + overlay.setBuildingDecorator(shapeHighlight, (GMLBuilding) obj); + } + else if (obj instanceof GMLRoad) { + overlay.setRoadDecorator(shapeHighlight, (GMLRoad) obj); + } + else if (obj instanceof GMLSpace) { + overlay.setSpaceDecorator(shapeHighlight, (GMLSpace) obj); + } + else if (obj instanceof GMLEdge) { + overlay.setEdgeDecorator(edgeHighlight, (GMLEdge) obj); + } + else if (obj instanceof GMLNode) { + overlay.setNodeDecorator(nodeHighlight, (GMLNode) obj); + } + + } + + @Override + public String getName() { + return "Validate map"; + } + +} diff --git a/modules/maps/src/maps/gml/formats/Common.java b/modules/maps/src/maps/gml/formats/Common.java new file mode 100644 index 0000000000000000000000000000000000000000..a4b20a65ed252b7df315cc654dca7e07e5395dfe --- /dev/null +++ b/modules/maps/src/maps/gml/formats/Common.java @@ -0,0 +1,41 @@ +package maps.gml.formats; + +import org.dom4j.DocumentHelper; +import org.dom4j.Namespace; +import org.dom4j.QName; + +/** + A bunch of common GML format namespaces and qnames. +*/ +public final class Common { + // CHECKSTYLE:OFF:JavadocVariable + public static final String GML_NAMESPACE_URI = "http://www.opengis.net/gml"; + public static final String GML_3_2_NAMESPACE_URI = "http://www.opengis.net/gml/3.2"; + public static final String XLINK_NAMESPACE_URI = "http://www.w3.org/1999/xlink"; + + public static final Namespace GML_NAMESPACE = DocumentHelper.createNamespace("gml", GML_NAMESPACE_URI); + public static final Namespace GML_3_2_NAMESPACE = DocumentHelper.createNamespace("gml32", GML_3_2_NAMESPACE_URI); + public static final Namespace XLINK_NAMESPACE = DocumentHelper.createNamespace("xlink", XLINK_NAMESPACE_URI); + + public static final QName GML_ID_QNAME = DocumentHelper.createQName("id", GML_NAMESPACE); + public static final QName GML_NODE_QNAME = DocumentHelper.createQName("Node", GML_NAMESPACE); + public static final QName GML_EDGE_QNAME = DocumentHelper.createQName("Edge", GML_NAMESPACE); + public static final QName GML_FACE_QNAME = DocumentHelper.createQName("Face", GML_NAMESPACE); + public static final QName GML_POINT_PROPERTY_QNAME = DocumentHelper.createQName("pointProperty", GML_NAMESPACE); + public static final QName GML_POINT_QNAME = DocumentHelper.createQName("Point", GML_NAMESPACE); + public static final QName GML_COORDINATES_QNAME = DocumentHelper.createQName("coordinates", GML_NAMESPACE); + public static final QName GML_ORIENTATION_QNAME = DocumentHelper.createQName("orientation"); + public static final QName GML_DIRECTED_NODE_QNAME = DocumentHelper.createQName("directedNode", GML_NAMESPACE); + public static final QName GML_DIRECTED_EDGE_QNAME = DocumentHelper.createQName("directedEdge", GML_NAMESPACE); + public static final QName GML_DIRECTED_FACE_QNAME = DocumentHelper.createQName("directedFace", GML_NAMESPACE); + public static final QName GML_CENTRE_LINE_OF_QNAME = DocumentHelper.createQName("centerLineOf", GML_NAMESPACE); + public static final QName GML_LINE_STRING_QNAME = DocumentHelper.createQName("LineString", GML_NAMESPACE); + public static final QName GML_POLYGON_QNAME = DocumentHelper.createQName("polygon", GML_NAMESPACE); + public static final QName GML_LINEAR_RING_QNAME = DocumentHelper.createQName("LinearRing", GML_NAMESPACE); + + public static final QName XLINK_HREF_QNAME = DocumentHelper.createQName("href", XLINK_NAMESPACE); + // CHECKSTYLE:ON:JavadocVariable + + private Common() { + } +} diff --git a/modules/maps/src/maps/gml/formats/GeospatialInformationAuthorityFormat.java b/modules/maps/src/maps/gml/formats/GeospatialInformationAuthorityFormat.java new file mode 100644 index 0000000000000000000000000000000000000000..56acd0e8b8b531ab0d27016d0f01575635edea7b --- /dev/null +++ b/modules/maps/src/maps/gml/formats/GeospatialInformationAuthorityFormat.java @@ -0,0 +1,166 @@ +package maps.gml.formats; + +import maps.gml.GMLMap; +import maps.gml.GMLNode; +import maps.gml.GMLDirectedEdge; +import maps.gml.GMLCoordinates; +import maps.gml.GMLMapFormat; +import maps.MapTools; +import maps.CoordinateConversion; +import maps.ScaleConversion; + +import org.dom4j.Document; +import org.dom4j.DocumentHelper; +import org.dom4j.Element; +import org.dom4j.Namespace; +import org.dom4j.QName; + +import java.util.List; +import java.util.ArrayList; +import java.util.Map; +import java.util.HashMap; +import java.util.StringTokenizer; +import java.util.Collections; + +import rescuecore2.log.Logger; + +/** + A MapFormat that can handle maps from Japan's Geospatial Information Authority. + */ +public final class GeospatialInformationAuthorityFormat extends GMLMapFormat { + /** Singleton instance. */ + public static final GeospatialInformationAuthorityFormat INSTANCE = new GeospatialInformationAuthorityFormat(); + + private static final String FGD_NAMESPACE_URI = "http://fgd.gsi.go.jp/spec/2008/FGD_GMLSchema"; + + private static final Namespace FGD_NAMESPACE = DocumentHelper.createNamespace("fgd", FGD_NAMESPACE_URI); + + private static final QName DATASET_QNAME = DocumentHelper.createQName("Dataset", FGD_NAMESPACE); + private static final QName BUILDING_QNAME = DocumentHelper.createQName("BldL", FGD_NAMESPACE); + private static final QName ROAD_QNAME = DocumentHelper.createQName("RdEdg", FGD_NAMESPACE); + private static final QName LOC_QNAME = DocumentHelper.createQName("loc", FGD_NAMESPACE); + private static final QName CURVE_QNAME = DocumentHelper.createQName("Curve", Common.GML_3_2_NAMESPACE); + private static final QName SEGMENTS_QNAME = DocumentHelper.createQName("segments", Common.GML_3_2_NAMESPACE); + private static final QName LINE_STRING_SEGMENT_QNAME = DocumentHelper.createQName("LineStringSegment", Common.GML_3_2_NAMESPACE); + private static final QName POS_LIST_QNAME = DocumentHelper.createQName("posList", Common.GML_3_2_NAMESPACE); + + // Map from uri prefix to uri for XPath expressions and for output + private static final Map URIS = new HashMap(); + + static { + URIS.put("gml", Common.GML_NAMESPACE_URI); + URIS.put("xlink", Common.XLINK_NAMESPACE_URI); + URIS.put("fgd", FGD_NAMESPACE_URI); + } + + private GeospatialInformationAuthorityFormat() { + } + + @Override + public String toString() { + return "Japan Geospatial Information Authority"; + } + + @Override + public Map getNamespaces() { + return Collections.unmodifiableMap(URIS); + } + + @Override + public boolean isCorrectRootElement(String uri, String localName) { + return FGD_NAMESPACE_URI.equals(uri) && "Dataset".equals(localName); + } + + @Override + public GMLMap read(Document doc) { + GMLMap result = new GMLMap(); + readBuildings(doc, result); + readRoads(doc, result); + // Convert from lat/lon to metres + double scale = 1.0 / MapTools.sizeOf1Metre((result.getMinY() + result.getMaxY()) / 2, (result.getMinX() + result.getMaxX()) / 2); + CoordinateConversion conversion = new ScaleConversion(result.getMinX(), result.getMinY(), scale, scale); + result.convertCoordinates(conversion); + return result; + } + + @Override + public Document write(GMLMap map) { + // Not implemented + throw new RuntimeException("GeospatialInformationAuthorityFormat.write not implemented"); + } + + private void readBuildings(Document doc, GMLMap result) { + List elements = doc.getRootElement().elements(BUILDING_QNAME); + Logger.debug("Found " + elements.size() + " buildings"); + for (Object next : elements) { + Element e = (Element)next; + try { + Element posList = e.element(LOC_QNAME).element(CURVE_QNAME).element(SEGMENTS_QNAME).element(LINE_STRING_SEGMENT_QNAME).element(POS_LIST_QNAME); + String coords = posList.getText(); + List edges = readEdges(coords, result); + result.createBuilding(edges); + } + catch (NullPointerException ex) { + Logger.debug("Building with wonky outline found: " + ex); + } + } + } + + private void readRoads(Document doc, GMLMap result) { + List elements = doc.getRootElement().elements(ROAD_QNAME); + Logger.debug("Found " + elements.size() + " roads"); + for (Object next : elements) { + Element e = (Element)next; + try { + Element posList = e.element(LOC_QNAME).element(CURVE_QNAME).element(SEGMENTS_QNAME).element(LINE_STRING_SEGMENT_QNAME).element(POS_LIST_QNAME); + String coords = posList.getText(); + createEdges(coords, result); + } + catch (NullPointerException ex) { + Logger.debug("Road with wonky outline found: " + ex); + } + } + } + + private List readEdges(String coordinatesString, GMLMap map) { + List edges = new ArrayList(); + StringTokenizer tokens = new StringTokenizer(coordinatesString, " \t\n\r"); + GMLCoordinates lastApex = null; + GMLNode fromNode = null; + GMLNode toNode = null; + while (tokens.hasMoreTokens()) { + String north = tokens.nextToken(); + String east = tokens.nextToken(); + double x = Double.parseDouble(east); + double y = Double.parseDouble(north); + GMLCoordinates nextApex = new GMLCoordinates(x, y); + toNode = map.createNode(nextApex); + if (lastApex != null) { + edges.add(new GMLDirectedEdge(map.createEdge(fromNode, toNode), true)); + } + lastApex = nextApex; + fromNode = toNode; + } + return edges; + } + + private void createEdges(String coordinatesString, GMLMap map) { + StringTokenizer tokens = new StringTokenizer(coordinatesString, " \t\n\r"); + GMLCoordinates lastApex = null; + GMLNode fromNode = null; + GMLNode toNode = null; + while (tokens.hasMoreTokens()) { + String north = tokens.nextToken(); + String east = tokens.nextToken(); + double x = Double.parseDouble(east); + double y = Double.parseDouble(north); + GMLCoordinates nextApex = new GMLCoordinates(x, y); + toNode = map.createNode(nextApex); + if (lastApex != null) { + map.createEdge(fromNode, toNode); + } + lastApex = nextApex; + fromNode = toNode; + } + } +} diff --git a/modules/maps/src/maps/gml/formats/MeijoFormat.java b/modules/maps/src/maps/gml/formats/MeijoFormat.java new file mode 100644 index 0000000000000000000000000000000000000000..de0d73f8c3827d90cee06b9f9267a95695f44cea --- /dev/null +++ b/modules/maps/src/maps/gml/formats/MeijoFormat.java @@ -0,0 +1,418 @@ +package maps.gml.formats; + +import maps.gml.GMLMap; +import maps.gml.GMLCoordinates; +import maps.gml.GMLShape; +import maps.gml.GMLBuilding; +import maps.gml.GMLRoad; +import maps.gml.GMLNode; +import maps.gml.GMLEdge; +import maps.gml.GMLDirectedEdge; +import maps.gml.GMLTools; +import maps.gml.GMLMapFormat; + +import maps.ConstantConversion; + +import org.dom4j.Document; +import org.dom4j.Element; +import org.dom4j.Attribute; +import org.dom4j.QName; +import org.dom4j.Namespace; +import org.dom4j.XPath; +import org.dom4j.DocumentHelper; + +import org.jaxen.SimpleVariableContext; + +import java.util.List; +import java.util.ArrayList; +import java.util.Map; +import java.util.HashMap; +import java.util.Collection; +import java.util.Iterator; +import java.util.Collections; + +import rescuecore2.misc.Pair; +import rescuecore2.log.Logger; + +/** + A MapFormat that can handle GML maps from Meijo University. + */ +public final class MeijoFormat extends GMLMapFormat { + /** Singleton instance. */ + public static final MeijoFormat INSTANCE = new MeijoFormat(); + + private static final String MEIJO_NAMESPACE_URI = "http://sakura.meijo-u.ac.jp/rcrs"; + private static final String GML_APP_NAMESPACE_URI = "http://www.opengis.net/app"; + private static final Namespace RCRS_NAMESPACE = DocumentHelper.createNamespace("rcrs", MEIJO_NAMESPACE_URI); + private static final Namespace GML_APP_NAMESPACE = DocumentHelper.createNamespace("app", GML_APP_NAMESPACE_URI); + + private static final QName ROOT_QNAME = DocumentHelper.createQName("Topology", GML_APP_NAMESPACE); + private static final QName VERSION_QNAME = DocumentHelper.createQName("Version", RCRS_NAMESPACE); + private static final QName DESCRIPTION_QNAME = DocumentHelper.createQName("Description", RCRS_NAMESPACE); + private static final QName AREA_QNAME = DocumentHelper.createQName("Area", RCRS_NAMESPACE); + private static final QName NODE_LIST_QNAME = DocumentHelper.createQName("NodeList", RCRS_NAMESPACE); + private static final QName EDGE_LIST_QNAME = DocumentHelper.createQName("EdgeList", RCRS_NAMESPACE); + private static final QName FACE_LIST_QNAME = DocumentHelper.createQName("FaceList", RCRS_NAMESPACE); + private static final QName FACE_QNAME = DocumentHelper.createQName("Face", RCRS_NAMESPACE); + private static final QName BUILDING_PROPERTY_QNAME = DocumentHelper.createQName("BuildingProperty", RCRS_NAMESPACE); + + // Map from uri prefix to uri for writing XML documents + private static final Map URIS = new HashMap(); + + private static final XPath NODE_XPATH = DocumentHelper.createXPath("//app:Topology/rcrs:Area/rcrs:NodeList/gml:Node"); + private static final XPath EDGE_XPATH = DocumentHelper.createXPath("//app:Topology/rcrs:Area/rcrs:EdgeList/gml:Edge"); + private static final XPath FACE_XPATH = DocumentHelper.createXPath("//app:Topology/rcrs:Area/rcrs:FaceList/rcrs:Face"); + + private static final XPath NODE_COORDINATES_XPATH = DocumentHelper.createXPath("gml:pointProperty/gml:Point/gml:coordinates"); + private static final XPath EDGE_COORDINATES_XPATH = DocumentHelper.createXPath("gml:centerLineOf/gml:LineString/gml:coordinates"); + private static final XPath FACE_COORDINATES_XPATH = DocumentHelper.createXPath("gml:polygon/gml:LinearRing/gml:coordinates"); + + private static final XPath EDGE_START_XPATH = DocumentHelper.createXPath("gml:directedNode[1]//@xlink:href"); + private static final XPath EDGE_END_XPATH = DocumentHelper.createXPath("gml:directedNode[2]/@xlink:href"); + + private static final SimpleVariableContext FACE_NEIGHBOUR_XPATH_CONTEXT = new SimpleVariableContext(); + private static final String FACE_NEIGHBOUR_XPATH_STRING = "//rcrs:EdgeList/gml:Edge[@gml:id=\"$edgeid\"]/gml:directedFace[@xlink:href!=\"#$faceid\"]/@xlink:href"; + // private static final XPath FACE_NEIGHBOUR_XPATH = DocumentHelper.createXPath("//rcrs:EdgeList/gml:Edge[@gml:id=\"$edgeid\"]/gml:directedFace[@xlink:href!=\"#$faceid\"]/@xlink:href"); + // private static final XPath FACE_NEIGHBOUR_XPATH = DocumentHelper.createXPath("//rcrs:EdgeList/gml:Edge[@gml:id=\"$edgeid\"]/gml:directedFace", FACE_NEIGHBOUR_XPATH_CONTEXT); + + private static final double THRESHOLD = 0.0001; + + + static { + URIS.put("gml", Common.GML_NAMESPACE_URI); + URIS.put("app", GML_APP_NAMESPACE_URI); + URIS.put("xlink", Common.XLINK_NAMESPACE_URI); + URIS.put("rcrs", MEIJO_NAMESPACE_URI); + + NODE_XPATH.setNamespaceURIs(URIS); + EDGE_XPATH.setNamespaceURIs(URIS); + FACE_XPATH.setNamespaceURIs(URIS); + + NODE_COORDINATES_XPATH.setNamespaceURIs(URIS); + EDGE_COORDINATES_XPATH.setNamespaceURIs(URIS); + FACE_COORDINATES_XPATH.setNamespaceURIs(URIS); + + EDGE_START_XPATH.setNamespaceURIs(URIS); + EDGE_END_XPATH.setNamespaceURIs(URIS); + + // FACE_NEIGHBOUR_XPATH.setNamespaceURIs(URIS); + } + + private MeijoFormat() { + } + + @Override + public Map getNamespaces() { + return Collections.unmodifiableMap(URIS); + } + + @Override + public String toString() { + return "Meijo"; + } + + @Override + public boolean isCorrectRootElement(String uri, String localName) { + return MEIJO_NAMESPACE_URI.equals(uri) && "Topology".equals(localName); + } + + @Override + public GMLMap read(Document doc) { + GMLMap result = new GMLMap(); + readNodes(doc, result); + // This format has coordinates in mm, so divide by 1000 to convert to m. + // CHECKSTYLE:OFF:MagicNumber + result.convertCoordinates(new ConstantConversion(0.001)); + // CHECKSTYLE:ON:MagicNumber + readEdges(doc, result); + readFaces(doc, result); + splitMultipleEdges(result); + // checkEdgeOrderAndDirection(result); + return result; + } + + @Override + public Document write(GMLMap map) { + Element root = DocumentHelper.createElement(ROOT_QNAME); + Document result = DocumentHelper.createDocument(root); + writeNodes(map, root.addElement(NODE_LIST_QNAME)); + writeEdges(map, root.addElement(EDGE_LIST_QNAME)); + writeFaces(map, root.addElement(FACE_LIST_QNAME)); + return result; + } + + private void writeNodes(GMLMap map, Element parent) { + for (GMLNode next : map.getNodes()) { + Element e = parent.addElement(Common.GML_NODE_QNAME); + e.addAttribute(Common.GML_ID_QNAME, String.valueOf(next.getID())); + e.addElement(Common.GML_POINT_PROPERTY_QNAME).addElement(Common.GML_POINT_QNAME).addElement(Common.GML_COORDINATES_QNAME).setText(next.getCoordinates().toString()); + } + } + + private void writeEdges(GMLMap map, Element parent) { + for (GMLEdge next : map.getEdges()) { + Element e = parent.addElement(Common.GML_EDGE_QNAME); + e.addAttribute(Common.GML_ID_QNAME, String.valueOf(next.getID())); + e.addElement(Common.GML_DIRECTED_NODE_QNAME).addAttribute(Common.GML_ORIENTATION_QNAME, "+").addAttribute(Common.XLINK_HREF_QNAME, "#" + next.getStart().getID()); + e.addElement(Common.GML_DIRECTED_NODE_QNAME).addAttribute(Common.GML_ORIENTATION_QNAME, "+").addAttribute(Common.XLINK_HREF_QNAME, "#" + next.getEnd().getID()); + // Add directed faces + // This will be real slow + for (GMLShape shape : map.getAllShapes()) { + for (GMLDirectedEdge edge : shape.getEdges()) { + if (edge.getEdge() == next) { + e.addElement(Common.GML_DIRECTED_FACE_QNAME).addAttribute(Common.GML_ORIENTATION_QNAME, "+").addAttribute(Common.XLINK_HREF_QNAME, "#" + shape.getID()); + } + } + } + } + } + + private void writeFaces(GMLMap map, Element parent) { + for (GMLShape next : map.getAllShapes()) { + Element e = parent.addElement(FACE_QNAME); + if (next instanceof GMLBuilding) { + parent.addElement(BUILDING_PROPERTY_QNAME); + } + e = e.addElement(Common.GML_FACE_QNAME); + e.addAttribute(Common.GML_ID_QNAME, String.valueOf(next.getID())); + for (GMLDirectedEdge edge : next.getEdges()) { + String orientation = "-"; + if (edge.getEdge().isPassable()) { + orientation = "+"; + } + e.addElement(Common.GML_DIRECTED_EDGE_QNAME).addAttribute(Common.GML_ORIENTATION_QNAME, orientation).addAttribute(Common.XLINK_HREF_QNAME, "#" + edge.getEdge().getID()); + } + e.addElement(Common.GML_POLYGON_QNAME).addElement(Common.GML_LINEAR_RING_QNAME).addElement(Common.GML_COORDINATES_QNAME).setText(GMLTools.getCoordinatesString(next.getCoordinates())); + } + } + + private void readNodes(Document doc, GMLMap result) { + for (Object next : NODE_XPATH.selectNodes(doc)) { + Element e = (Element)next; + int id = readID(e); + String coordinates = ((Element)NODE_COORDINATES_XPATH.evaluate(e)).getText(); + GMLCoordinates c = new GMLCoordinates(coordinates); + GMLNode node = new GMLNode(id, c); + result.addNode(node); + Logger.debug("Read node " + node); + } + } + + private void readEdges(Document doc, GMLMap result) { + for (Object next : EDGE_XPATH.selectNodes(doc)) { + Element e = (Element)next; + int id = readID(e); + // Logger.debug("Children: " + e.elements()); + // Logger.debug("Start: " + EDGE_START_XPATH.evaluate(e)); + int startID = Integer.parseInt(((Attribute)EDGE_START_XPATH.evaluate(e)).getValue().substring(1)); + int endID = Integer.parseInt(((Attribute)EDGE_END_XPATH.evaluate(e)).getValue().substring(1)); + GMLEdge edge = new GMLEdge(id, result.getNode(startID), result.getNode(endID), false); + // Read the edge coordinates + edge.setPoints(GMLTools.getCoordinatesList(((Element)EDGE_COORDINATES_XPATH.evaluate(e)).getText())); + result.addEdge(edge); + Logger.debug("Read edge " + edge); + } + } + + private void readFaces(Document doc, GMLMap result) { + // Logger.debug("Reading buildings"); + for (Object next : FACE_XPATH.selectNodes(doc)) { + // Logger.debug("Reading " + next); + Element e = (Element)next; + String type = e.attributeValue("type"); + Element gmlFace = e.element(Common.GML_FACE_QNAME); + int id = readID(gmlFace); + // Logger.debug("ID = " + id); + // Logger.debug("Type = " + type); + Pair, List> edges = readEdges(gmlFace, result, id); + // Logger.debug("Edges: " + edges); + GMLShape shape = null; + if ("building".equals(type)) { + shape = new GMLBuilding(id, edges.first(), edges.second()); + } + else { + shape = new GMLRoad(id, edges.first(), edges.second()); + } + // Logger.debug("Computing coordinates"); + String coordsString = ((Element)FACE_COORDINATES_XPATH.evaluate(gmlFace)).getText(); + // Logger.debug("coordsString = " + coordsString); + List coords = GMLTools.getCoordinatesList(coordsString); + // Logger.debug("coords = " + coords); + shape.setCoordinates(coords); + result.add(shape); + Logger.debug("Read shape: " + shape); + } + } + + private Pair, List> readEdges(Element e, GMLMap map, int faceID) { + List edges = new ArrayList(); + List neighbours = new ArrayList(); + + // Logger.debug("Reading edges for face " + faceID); + for (Object next : e.elements(Common.GML_DIRECTED_EDGE_QNAME)) { + Element dEdge = (Element)next; + boolean passable = "+".equals(dEdge.attributeValue(Common.GML_ORIENTATION_QNAME)); + int edgeID = Integer.parseInt(dEdge.attributeValue(Common.XLINK_HREF_QNAME).substring(1)); + // Logger.debug("Edge ID: " + edgeID); + // Logger.debug("Passable? " + passable); + edges.add(new GMLDirectedEdge(map.getEdge(edgeID), true)); + XPath xpath = makeFaceNeighbourXPath(edgeID, faceID); + // FACE_NEIGHBOUR_XPATH_CONTEXT.setVariableValue("edgeid", String.valueOf(edgeID)); + // FACE_NEIGHBOUR_XPATH_CONTEXT.setVariableValue("faceid", String.valueOf(faceID)); + Object o = xpath.evaluate(e); + // Logger.debug("Neighbours: " + o); + if (o == null) { + neighbours.add(null); + } + else if (o instanceof Collection && ((Collection)o).isEmpty()) { + neighbours.add(null); + } + else { + int neighbourID = Integer.parseInt(((Attribute)o).getValue().substring(1)); + neighbours.add(neighbourID); + } + // Logger.debug("Edge list : " + edges); + // Logger.debug("Neighbour list: " + neighbours); + } + // Logger.debug("Finished reading edges for face " + faceID); + return new Pair, List>(edges, neighbours); + } + + private int readID(Element e) { + return Integer.parseInt(e.attributeValue(Common.GML_ID_QNAME)); + } + + private XPath makeFaceNeighbourXPath(int edgeID, int faceID) { + String path = FACE_NEIGHBOUR_XPATH_STRING.replace("$edgeid", String.valueOf(edgeID)).replace("$faceid", String.valueOf(faceID)); + // Logger.debug("Neighbour XPath: " + path); + XPath result = DocumentHelper.createXPath(path); + result.setNamespaceURIs(URIS); + return result; + } + + private void splitMultipleEdges(GMLMap map) { + // Look for edges that have more then 2 GMLCoordinates and split them into multiple edges + for (GMLEdge edge : map.getEdges()) { + if (edge.getPoints().size() != 2) { + // Split this edge + Iterator it = edge.getPoints().iterator(); + GMLCoordinates first = it.next(); + List newEdges = new ArrayList(); + while (it.hasNext()) { + GMLCoordinates second = it.next(); + GMLNode n1 = map.createNode(first); + GMLNode n2 = map.createNode(second); + GMLEdge newEdge = map.createEdge(n1, n2); + newEdges.add(newEdge); + first = second; + } + // Update any shapes that reference the old edge + for (GMLShape shape : map.getAllShapes()) { + replaceEdge(shape, edge, newEdges); + } + map.removeEdge(edge); + // Logger.debug("Split " + edge); + // Logger.debug("New edges: " + newEdges); + } + } + } + + private void replaceEdge(GMLShape shape, GMLEdge oldEdge, List newEdges) { + List newShapeEdges = new ArrayList(); + List newShapeNeighbours = new ArrayList(); + boolean found = false; + for (GMLDirectedEdge e : shape.getEdges()) { + if (e.getEdge().equals(oldEdge)) { + found = true; + GMLNode start = e.getStartNode(); + Integer neighbour = shape.getNeighbour(e); + for (GMLEdge next : newEdges) { + GMLDirectedEdge newDEdge = new GMLDirectedEdge(next, start); + newShapeEdges.add(newDEdge); + newShapeNeighbours.add(neighbour); + start = newDEdge.getEndNode(); + } + } + else { + newShapeEdges.add(e); + newShapeNeighbours.add(shape.getNeighbour(e)); + } + } + if (found) { + shape.setEdges(newShapeEdges); + Iterator it = newShapeEdges.iterator(); + Iterator ix = newShapeNeighbours.iterator(); + while (it.hasNext() && ix.hasNext()) { + shape.setNeighbour(it.next(), ix.next()); + } + } + } + + /* + private void checkEdgeOrderAndDirection(GMLMap map) { + Set remaining = new HashSet(); + List reordered = new ArrayList(); + for (GMLShape shape : map.getAllShapes()) { + remaining.clear(); + reordered.clear(); + remaining.addAll(shape.getEdges()); + // Iterator it = shape.getEdges().iterator(); + GMLDirectedEdge edge = shape.getEdges().get(0); + GMLNode start = edge.getEndNode(); + // Logger.debug("Reordering " + remaining.size() + " edges for " + shape); + // Logger.debug("Original order"); + // for (GMLDirectedEdge e : shape.getEdges()) { + // logEdge(e); + // } + // Logger.debug("First edge"); + // logEdge(edge); + remaining.remove(edge); + reordered.add(edge); + while (!remaining.isEmpty()) { + edge = null; + // Find the next edge + for (GMLDirectedEdge next : remaining) { + if (closeEnough(next.getStartNode(), start)) { + edge = next; + break; + } + if (closeEnough(next.getEndNode(), start)) { + edge = next; + edge.reverse(); + break; + } + } + if (edge == null) { + throw new RuntimeException("Failed to reorder edges: found discontinuity in shape outline"); + } + // Logger.debug("Next edge"); + // logEdge(edge); + remaining.remove(edge); + reordered.add(edge); + start = edge.getEndNode(); + } + // Logger.debug("Reordered"); + // for (GMLDirectedEdge e : reordered) { + // logEdge(e); + // } + shape.reorderEdges(reordered); + } + } + */ + + // private void logEdge(GMLDirectedEdge e) { + // Logger.debug(e.getEdge().getID() + ": " + e.getStartNode().getID() + " -> " + e.getEndNode().getID()); + // } + + private boolean closeEnough(GMLNode n1, GMLNode n2) { + if (n1 == n2) { + return true; + } + double dx = n1.getX() - n2.getX(); + double dy = n1.getY() - n2.getY(); + return (dx > -THRESHOLD + && dx < THRESHOLD + && dy > -THRESHOLD + && dy < THRESHOLD); + } +} diff --git a/modules/maps/src/maps/gml/formats/OrdnanceSurveyFormat.java b/modules/maps/src/maps/gml/formats/OrdnanceSurveyFormat.java new file mode 100644 index 0000000000000000000000000000000000000000..5f2149eec10856c194fa3300568a9b2d45d657d4 --- /dev/null +++ b/modules/maps/src/maps/gml/formats/OrdnanceSurveyFormat.java @@ -0,0 +1,171 @@ +package maps.gml.formats; + +import maps.gml.GMLMap; +import maps.gml.GMLNode; +import maps.gml.GMLDirectedEdge; +import maps.gml.GMLCoordinates; +import maps.gml.GMLBuilding; +import maps.gml.GMLRoad; +import maps.gml.GMLMapFormat; + +import org.dom4j.Document; +import org.dom4j.DocumentHelper; +import org.dom4j.Element; +import org.dom4j.Namespace; +import org.dom4j.QName; +import org.dom4j.XPath; + +import java.util.List; +import java.util.ArrayList; +import java.util.Map; +import java.util.HashMap; +import java.util.StringTokenizer; +import java.util.Collection; +import java.util.Collections; + +import rescuecore2.log.Logger; + +// TO DO: Handle inner boundaries + +/** + A MapFormat that can handle maps from the UK Ordnance Survey. + */ +public final class OrdnanceSurveyFormat extends GMLMapFormat { + /** Singleton instance. */ + public static final OrdnanceSurveyFormat INSTANCE = new OrdnanceSurveyFormat(); + + private static final String FEATURE_CODE_BUILDING = "10021"; + private static final String FEATURE_CODE_ROAD = "10172"; + private static final String FEATURE_CODE_FOOTPATH = "10183"; + + private static final String FEATURE_CODE_OPEN_SPACE = "10053"; + private static final String FEATURE_CODE_GENERAL_SPACE = "10056"; + + private static final String OSGB_NAMESPACE_URI = "http://www.ordnancesurvey.co.uk/xml/namespaces/osgb"; + + private static final Namespace OSGB_NAMESPACE = DocumentHelper.createNamespace("osgb", OSGB_NAMESPACE_URI); + + private static final QName FEATURE_COLLECTION_QNAME = DocumentHelper.createQName("FeatureCollection", OSGB_NAMESPACE); + private static final QName TOPOGRAPHIC_AREA_QNAME = DocumentHelper.createQName("TopographicArea", OSGB_NAMESPACE); + + private static final XPath BUILDING_XPATH = DocumentHelper.createXPath("//osgb:topographicMember/osgb:TopographicArea[osgb:featureCode[text()='" + FEATURE_CODE_BUILDING + "']]"); + // private static final XPath ROAD_XPATH = DocumentHelper.createXPath("//osgb:topographicMember/osgb:TopographicArea[osgb:featureCode[text()='" + FEATURE_CODE_ROAD + "' or text()='" + FEATURE_CODE_FOOTPATH + "']]"); + private static final XPath ROAD_XPATH = DocumentHelper.createXPath("//osgb:topographicMember/osgb:TopographicArea[osgb:featureCode[text()='" + FEATURE_CODE_ROAD + "']]"); + private static final XPath SPACE_XPATH = DocumentHelper.createXPath("//osgb:topographicMember/osgb:TopographicArea[osgb:featureCode[text()='" + FEATURE_CODE_OPEN_SPACE + "' or text()='" + FEATURE_CODE_GENERAL_SPACE + "']]"); + private static final XPath SHAPE_XPATH = DocumentHelper.createXPath("osgb:polygon/gml:Polygon/gml:outerBoundaryIs/gml:LinearRing/gml:coordinates"); + private static final XPath INNER_RING_XPATH = DocumentHelper.createXPath("osgb:polygon/gml:Polygon/gml:innerBoundaryIs/gml:LinearRing/gml:coordinates"); + + // Map from uri prefix to uri for XPath expressions + private static final Map URIS = new HashMap(); + + private static final int FID_PREFIX_LENGTH = 4; + + static { + URIS.put("gml", Common.GML_NAMESPACE_URI); + URIS.put("xlink", Common.XLINK_NAMESPACE_URI); + URIS.put("osgb", OSGB_NAMESPACE_URI); + + BUILDING_XPATH.setNamespaceURIs(URIS); + ROAD_XPATH.setNamespaceURIs(URIS); + SPACE_XPATH.setNamespaceURIs(URIS); + SHAPE_XPATH.setNamespaceURIs(URIS); + INNER_RING_XPATH.setNamespaceURIs(URIS); + } + + private OrdnanceSurveyFormat() { + } + + @Override + public String toString() { + return "Ordnance survey"; + } + + @Override + public Map getNamespaces() { + return Collections.unmodifiableMap(URIS); + } + + @Override + public boolean isCorrectRootElement(String uri, String localName) { + return OSGB_NAMESPACE_URI.equals(uri) && "FeatureCollection".equals(localName); + } + + @Override + public GMLMap read(Document doc) { + GMLMap result = new GMLMap(); + readBuildings(doc, result); + readRoads(doc, result); + readSpaces(doc, result); + return result; + } + + @Override + public Document write(GMLMap map) { + // Not implemented + throw new RuntimeException("OrdnanceSurveyFormat.write not implemented"); + } + + private void readBuildings(Document doc, GMLMap result) { + for (Object next : BUILDING_XPATH.selectNodes(doc)) { + Logger.debug("Found building element: " + next); + Element e = (Element)next; + // String fid = e.attributeValue("fid"); + // long id = Long.parseLong(fid.substring(FID_PREFIX_LENGTH)); // Strip off the 'osgb' prefix + String coordinatesString = ((Element)SHAPE_XPATH.evaluate(e)).getText(); + List edges = readEdges(coordinatesString, result); + GMLBuilding b = result.createBuilding(edges); + } + } + + private void readRoads(Document doc, GMLMap result) { + for (Object next : ROAD_XPATH.selectNodes(doc)) { + Logger.debug("Found road element: " + next); + Element e = (Element)next; + // String fid = e.attributeValue("fid"); + // long id = Long.parseLong(fid.substring(FID_PREFIX_LENGTH)); // Strip off the 'osgb' prefix + String coordinatesString = ((Element)SHAPE_XPATH.evaluate(e)).getText(); + Object inner = INNER_RING_XPATH.evaluate(e); + if ((inner instanceof Collection) && ((Collection)inner).isEmpty()) { + List edges = readEdges(coordinatesString, result); + GMLRoad road = result.createRoad(edges); + } + else { + Logger.debug("Inner ring found: ignoring"); + Logger.debug("Found: " + inner); + } + } + } + + private void readSpaces(Document doc, GMLMap result) { + /* + for (Object next : SPACE_XPATH.selectNodes(doc)) { + Logger.debug("Found space element: " + next); + Element e = (Element)next; + String fid = e.attributeValue("fid"); + long id = Long.parseLong(fid.substring(4)); // Strip off the 'osgb' prefix + String coordinatesString = ((Element)SHAPE_XPATH.evaluate(e)).getText(); + List edges = readEdges(coordinatesString, result); + result.createSpace(edges); + } + */ + } + + private List readEdges(String coordinatesString, GMLMap map) { + List edges = new ArrayList(); + StringTokenizer tokens = new StringTokenizer(coordinatesString, " "); + GMLCoordinates lastApex = null; + GMLNode fromNode = null; + GMLNode toNode = null; + while (tokens.hasMoreTokens()) { + String token = tokens.nextToken(); + GMLCoordinates nextApex = new GMLCoordinates(token); + toNode = map.createNode(nextApex); + if (lastApex != null) { + edges.add(new GMLDirectedEdge(map.createEdge(fromNode, toNode), true)); + } + lastApex = nextApex; + fromNode = toNode; + } + return edges; + } +} diff --git a/modules/maps/src/maps/gml/formats/RobocupFormat.java b/modules/maps/src/maps/gml/formats/RobocupFormat.java new file mode 100755 index 0000000000000000000000000000000000000000..1a4347ac3090442dea2201cf9fed1232ba739345 --- /dev/null +++ b/modules/maps/src/maps/gml/formats/RobocupFormat.java @@ -0,0 +1,367 @@ +package maps.gml.formats; + +import maps.gml.GMLMap; +import maps.gml.GMLCoordinates; +import maps.gml.GMLObject; +import maps.gml.GMLShape; +import maps.gml.GMLBuilding; +import maps.gml.GMLRoad; +import maps.gml.GMLSpace; +import maps.gml.GMLNode; +import maps.gml.GMLEdge; +import maps.gml.GMLDirectedEdge; +import maps.gml.GMLMapFormat; +import maps.MapException; + +import org.dom4j.Document; +import org.dom4j.Element; +import org.dom4j.QName; +import org.dom4j.Namespace; +import org.dom4j.DocumentHelper; + +import java.util.List; +import java.util.ArrayList; +import java.util.Map; +import java.util.HashMap; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; + +import rescuecore2.misc.Pair; +import rescuecore2.log.Logger; + +/** + A MapFormat that can handle Robocup Rescue GML maps. + */ +public final class RobocupFormat extends GMLMapFormat { + /** Singleton instance. */ + public static final RobocupFormat INSTANCE = new RobocupFormat(); + + private static final String RCR_NAMESPACE_URI = "urn:roborescue:map:gml"; + private static final Namespace RCR_NAMESPACE = DocumentHelper.createNamespace("rcr", RCR_NAMESPACE_URI); + + private static final QName RCR_ROOT_QNAME = DocumentHelper.createQName("map", RCR_NAMESPACE); + private static final QName RCR_NODE_LIST_QNAME = DocumentHelper.createQName("nodelist", RCR_NAMESPACE); + private static final QName RCR_EDGE_LIST_QNAME = DocumentHelper.createQName("edgelist", RCR_NAMESPACE); + private static final QName RCR_BUILDING_LIST_QNAME = DocumentHelper.createQName("buildinglist", RCR_NAMESPACE); + private static final QName RCR_ROAD_LIST_QNAME = DocumentHelper.createQName("roadlist", RCR_NAMESPACE); + private static final QName RCR_SPACE_LIST_QNAME = DocumentHelper.createQName("spacelist", RCR_NAMESPACE); + private static final QName RCR_NODE_QNAME = DocumentHelper.createQName("node", RCR_NAMESPACE); + private static final QName RCR_EDGE_QNAME = DocumentHelper.createQName("edge", RCR_NAMESPACE); + private static final QName RCR_BUILDING_QNAME = DocumentHelper.createQName("building", RCR_NAMESPACE); + private static final QName RCR_ROAD_QNAME = DocumentHelper.createQName("road", RCR_NAMESPACE); + private static final QName RCR_SPACE_QNAME = DocumentHelper.createQName("space", RCR_NAMESPACE); + private static final QName RCR_NEIGHBOUR_QNAME = DocumentHelper.createQName("neighbour", RCR_NAMESPACE); + + private static final QName RCR_FLOORS_QNAME = DocumentHelper.createQName("floors", RCR_NAMESPACE); + private static final QName RCR_BUILDING_CODE_QNAME = DocumentHelper.createQName("buildingcode", RCR_NAMESPACE); + private static final QName RCR_IMPORTANCE_QNAME = DocumentHelper.createQName("importance", RCR_NAMESPACE); + private static final QName RCR_CAPACITY_QNAME = DocumentHelper.createQName("capacity", RCR_NAMESPACE); + + // Map from uri prefix to uri for writing XML documents + private static final Map URIS = new HashMap(); + + private static final Comparator ID_SORTER = new Comparator() { + @Override + public int compare(GMLObject first, GMLObject second) { + if (first.getID() < second.getID()) { + return -1; + } + if (first.getID() > second.getID()) { + return 1; + } + return 0; + } + }; + + static { + URIS.put("gml", Common.GML_NAMESPACE_URI); + URIS.put("xlink", Common.XLINK_NAMESPACE_URI); + URIS.put("rcr", RCR_NAMESPACE_URI); + } + + private RobocupFormat() { + } + + @Override + public Map getNamespaces() { + return Collections.unmodifiableMap(URIS); + } + + @Override + public String toString() { + return "Robocup rescue"; + } + + @Override + public boolean isCorrectRootElement(String uri, String localName) { + return RCR_NAMESPACE_URI.equals(uri) && "map".equals(localName); + } + + @Override + public GMLMap read(Document doc) throws MapException { + GMLMap result = new GMLMap(); + readNodes(doc, result); + readEdges(doc, result); + readBuildings(doc, result); + readRoads(doc, result); + readSpaces(doc, result); + return result; + } + + @Override + public Document write(GMLMap map) { + Element root = DocumentHelper.createElement(RCR_ROOT_QNAME); + Document result = DocumentHelper.createDocument(root); + writeNodes(map, root.addElement(RCR_NODE_LIST_QNAME)); + writeEdges(map, root.addElement(RCR_EDGE_LIST_QNAME)); + writeShapes(map.getBuildings(), RCR_BUILDING_QNAME, root.addElement(RCR_BUILDING_LIST_QNAME)); + writeShapes(map.getRoads(), RCR_ROAD_QNAME, root.addElement(RCR_ROAD_LIST_QNAME)); + writeShapes(map.getSpaces(), RCR_SPACE_QNAME, root.addElement(RCR_SPACE_LIST_QNAME)); + return result; + } + + private void writeNodes(GMLMap map, Element parent) { + List nodes = new ArrayList(map.getNodes()); + Collections.sort(nodes, ID_SORTER); + for (GMLNode next : nodes) { + Element e = parent.addElement(Common.GML_NODE_QNAME); + e.addAttribute(Common.GML_ID_QNAME, String.valueOf(next.getID())); + e.addElement(Common.GML_POINT_PROPERTY_QNAME).addElement(Common.GML_POINT_QNAME).addElement(Common.GML_COORDINATES_QNAME).setText(next.getCoordinates().toString()); + } + } + + private void writeEdges(GMLMap map, Element parent) { + List edges = new ArrayList(map.getEdges()); + Collections.sort(edges, ID_SORTER); + for (GMLEdge next : edges) { + Element e = parent.addElement(Common.GML_EDGE_QNAME); + e.addAttribute(Common.GML_ID_QNAME, String.valueOf(next.getID())); + e.addElement(Common.GML_DIRECTED_NODE_QNAME).addAttribute(Common.GML_ORIENTATION_QNAME, "-").addAttribute(Common.XLINK_HREF_QNAME, "#" + next.getStart().getID()); + e.addElement(Common.GML_DIRECTED_NODE_QNAME).addAttribute(Common.GML_ORIENTATION_QNAME, "+").addAttribute(Common.XLINK_HREF_QNAME, "#" + next.getEnd().getID()); + } + } + + private void writeShapes(Collection shapes, QName qname, Element parent) { + List sorted = new ArrayList(shapes); + Collections.sort(sorted, ID_SORTER); + for (GMLShape next : sorted) { + Element e = parent.addElement(qname).addAttribute(Common.GML_ID_QNAME, String.valueOf(next.getID())).addElement(Common.GML_FACE_QNAME); + for (GMLDirectedEdge dEdge : next.getEdges()) { + String orientation = dEdge.isForward() ? "+" : "-"; + Element dEdgeElement = e.addElement(Common.GML_DIRECTED_EDGE_QNAME).addAttribute(Common.GML_ORIENTATION_QNAME, orientation).addAttribute(Common.XLINK_HREF_QNAME, "#" + dEdge.getEdge().getID()); + Integer neighbour = next.getNeighbour(dEdge); + if (neighbour != null) { + dEdgeElement.addAttribute(RCR_NEIGHBOUR_QNAME, String.valueOf(neighbour)); + } + } + if (next instanceof GMLBuilding) { + GMLBuilding b = (GMLBuilding)next; + e.addAttribute(RCR_FLOORS_QNAME, String.valueOf(b.getFloors())); + e.addAttribute(RCR_BUILDING_CODE_QNAME, String.valueOf(b.getCode())); + e.addAttribute(RCR_IMPORTANCE_QNAME, String.valueOf(b.getImportance())); + //e.addAttribute(RCR__QNAME, String.valueOf(b.getImportance())); + } + } + } + + private void readNodes(Document doc, GMLMap result) throws MapException { + Logger.debug("Reading nodes"); + for (Object next : doc.getRootElement().elements(RCR_NODE_LIST_QNAME)) { + Element nodeList = (Element)next; + for (Object nextNode : nodeList.elements(Common.GML_NODE_QNAME)) { + Element e = (Element)nextNode; + int id = readID(e); + String coordinates = readNodeCoordinates(e); + GMLCoordinates c = new GMLCoordinates(coordinates); + GMLNode node = new GMLNode(id, c); + result.addNode(node); + } + } + Logger.debug("Read " + result.getNodes().size() + " nodes"); + } + + private void readEdges(Document doc, GMLMap result) throws MapException { + Logger.debug("Reading edges"); + for (Object next : doc.getRootElement().elements(RCR_EDGE_LIST_QNAME)) { + Element edgeList = (Element)next; + for (Object nextEdge : edgeList.elements(Common.GML_EDGE_QNAME)) { + Element e = (Element)nextEdge; + int id = readID(e); + int startID = -1; + int endID = -1; + for (Object directedNode : e.elements(Common.GML_DIRECTED_NODE_QNAME)) { + Element directedNodeElement = (Element)directedNode; + if ("-".equals(directedNodeElement.attributeValue(Common.GML_ORIENTATION_QNAME))) { + if (startID != -1) { + throw new MapException("Edge has multiple start nodes: " + e); + } + startID = readHref(directedNodeElement, "start node"); + } + if ("+".equals(directedNodeElement.attributeValue(Common.GML_ORIENTATION_QNAME))) { + if (endID != -1) { + throw new MapException("Edge has multiple end nodes: " + e); + } + endID = readHref(directedNodeElement, "end node"); + } + } + GMLEdge edge = new GMLEdge(id, result.getNode(startID), result.getNode(endID), false); + result.addEdge(edge); + } + } + Logger.debug("Read " + result.getEdges().size() + " edges"); + } + + private void readBuildings(Document doc, GMLMap result) throws MapException { + Logger.debug("Reading buildings"); + for (Object next : doc.getRootElement().elements(RCR_BUILDING_LIST_QNAME)) { + Element buildingList = (Element)next; + for (Object nextBuilding : buildingList.elements(RCR_BUILDING_QNAME)) { + Element e = (Element)nextBuilding; + Pair, List> edges = readEdges(e, result); + GMLBuilding b = new GMLBuilding(readID(e), edges.first(), edges.second()); + Element f = e.element(Common.GML_FACE_QNAME); + int floors = readInt(f, RCR_FLOORS_QNAME, 1); + int code = readInt(f, RCR_BUILDING_CODE_QNAME, 0); + int importance = readInt(f, RCR_IMPORTANCE_QNAME, 1); + int capacity = readInt(f, RCR_CAPACITY_QNAME, 0); + b.setFloors(floors); + b.setCode(code); + b.setImportance(importance); + b.setCapacity(capacity); + result.addBuilding(b); + } + } + Logger.debug("Read " + result.getBuildings().size() + " buildings"); + } + + private void readRoads(Document doc, GMLMap result) throws MapException { + Logger.debug("Reading roads"); + for (Object next : doc.getRootElement().elements(RCR_ROAD_LIST_QNAME)) { + Element roadList = (Element)next; + for (Object nextRoad : roadList.elements(RCR_ROAD_QNAME)) { + Element e = (Element)nextRoad; + Pair, List> edges = readEdges(e, result); + GMLRoad r = new GMLRoad(readID(e), edges.first(), edges.second()); + result.addRoad(r); + } + } + Logger.debug("Read " + result.getRoads().size() + " roads"); + } + + private void readSpaces(Document doc, GMLMap result) throws MapException { + Logger.debug("Reading spaces"); + for (Object next : doc.getRootElement().elements(RCR_SPACE_LIST_QNAME)) { + Element spaceList = (Element)next; + for (Object nextSpace : spaceList.elements(RCR_SPACE_QNAME)) { + Element e = (Element)nextSpace; + Pair, List> edges = readEdges(e, result); + GMLSpace s = new GMLSpace(readID(e), edges.first(), edges.second()); + result.addSpace(s); + } + } + Logger.debug("Read " + result.getSpaces().size() + " spaces"); + } + + private Pair, List> readEdges(Element e, GMLMap map) throws MapException { + List edges = new ArrayList(); + List neighbours = new ArrayList(); + Element faceElement = e.element(Common.GML_FACE_QNAME); + if (faceElement == null) { + throw new MapException("Shape does not contain a gml:Face: " + e); + } + for (Object nextEdge : faceElement.elements(Common.GML_DIRECTED_EDGE_QNAME)) { + Element directedEdge = (Element)nextEdge; + // Logger.debug("Next directed edge: " + directedEdge); + int nextID = readHref(directedEdge, "underlying edge"); + String orientation = directedEdge.attributeValue(Common.GML_ORIENTATION_QNAME); + boolean forward; + if (orientation == null) { + throw new MapException("Directed edge has no orientation attribute: " + e); + } + if ("+".equals(orientation)) { + forward = true; + } + else if ("-".equals(orientation)) { + forward = false; + } + else { + throw new MapException("Directed edge has invalid orientation attribute: " + e); + } + GMLEdge edge = map.getEdge(nextID); + GMLDirectedEdge dEdge = new GMLDirectedEdge(edge, forward); + String neighbourString = directedEdge.attributeValue(RCR_NEIGHBOUR_QNAME); + Integer neighbourID = null; + if (neighbourString != null) { + try { + neighbourID = Integer.valueOf(neighbourString); + } + catch (NumberFormatException ex) { + throw new MapException("Directed edge has invalid neighbour: " + e, ex); + } + edge.setPassable(true); + } + edges.add(dEdge); + neighbours.add(neighbourID); + } + if (edges.isEmpty()) { + throw new MapException("Shape contains no edges: " + e); + } + return new Pair, List>(edges, neighbours); + } + + private int readID(Element e) throws MapException { + String s = e.attributeValue(Common.GML_ID_QNAME); + if (s == null) { + throw new MapException("No ID attribute found: " + e); + } + try { + return Integer.parseInt(s); + } + catch (NumberFormatException ex) { + throw new MapException("Couldn't parse ID attribute", ex); + } + } + + private String readNodeCoordinates(Element node) throws MapException { + Element pointProperty = node.element(Common.GML_POINT_PROPERTY_QNAME); + if (pointProperty == null) { + throw new MapException("Couldn't find gml:pointProperty child of node"); + } + Element point = pointProperty.element(Common.GML_POINT_QNAME); + if (point == null) { + throw new MapException("Couldn't find gml:Point child of node"); + } + Element coords = point.element(Common.GML_COORDINATES_QNAME); + if (coords == null) { + throw new MapException("Couldn't find gml:coordinates child of node"); + } + return coords.getText(); + } + + private int readHref(Element e, String type) throws MapException { + String href = e.attributeValue(Common.XLINK_HREF_QNAME); + if (href == null || href.length() == 0) { + throw new MapException("Edge has no " + type + " ID"); + } + try { + return Integer.parseInt(href.substring(1)); + } + catch (NumberFormatException ex) { + throw new MapException("Edge has invalid " + type + " ID"); + } + } + + private int readInt(Element e, QName attributeName, int defaultValue) throws MapException { + String s = e.attributeValue(attributeName); + if (s == null) { + return defaultValue; + } + try { + return Integer.parseInt(s); + } + catch (NumberFormatException ex) { + throw new MapException("Attribute " + attributeName + " is not an integer: " + e); + } + } +} diff --git a/modules/maps/src/maps/gml/generator/GMLMapGenerator.java b/modules/maps/src/maps/gml/generator/GMLMapGenerator.java new file mode 100644 index 0000000000000000000000000000000000000000..4c45ae8b5037f192a2b593b8115974502e03b568 --- /dev/null +++ b/modules/maps/src/maps/gml/generator/GMLMapGenerator.java @@ -0,0 +1,62 @@ +package maps.gml.generator; + +import maps.gml.GMLMap; +import maps.gml.formats.RobocupFormat; +import maps.MapWriter; +import maps.MapException; + +import rescuecore2.config.Config; +import rescuecore2.config.ConfigException; +import rescuecore2.log.Logger; + +import java.io.File; + +/** + A tool for generating GML maps. +*/ +public class GMLMapGenerator { + private static final String OUTPUT_FILE_KEY = "generator.output"; + + private Config config; + + /** + Construct a GMLMapGenerator. + @param config The configuration to use. + */ + public GMLMapGenerator(Config config) { + this.config = config; + } + + /** + Entry point. + @param args Command line arguments. + */ + public static void main(String[] args) { + try { + Config config = new Config(); + for (int i = 0; i < args.length; ++i) { + config.read(new File(args[i])); + } + GMLMap map = new GMLMapGenerator(config).generateMap(); + String outFile = config.getValue(OUTPUT_FILE_KEY); + Logger.debug("Writing generated map to " + outFile); + MapWriter.writeMap(map, outFile, RobocupFormat.INSTANCE); + } + catch (MapException e) { + e.printStackTrace(); + } + catch (ConfigException e) { + e.printStackTrace(); + } + } + + /** + Generate a new map. + @return The new map. + */ + public GMLMap generateMap() { + GMLMap result = new GMLMap(); + new ManhattanGenerator(config).populate(result); + return result; + } +} \ No newline at end of file diff --git a/modules/maps/src/maps/gml/generator/ManhattanGenerator.java b/modules/maps/src/maps/gml/generator/ManhattanGenerator.java new file mode 100644 index 0000000000000000000000000000000000000000..98f2a926a2b64ba847bc72c4069e0b18e30e9022 --- /dev/null +++ b/modules/maps/src/maps/gml/generator/ManhattanGenerator.java @@ -0,0 +1,207 @@ +package maps.gml.generator; + +import maps.gml.GMLMap; +import maps.gml.GMLNode; +//import maps.gml.GMLEdge; +import maps.gml.GMLDirectedEdge; +import maps.gml.GMLBuilding; +//import maps.gml.GMLRoad; +import maps.gml.GMLCoordinates; + +import rescuecore2.config.Config; +import rescuecore2.misc.geometry.Point2D; +import rescuecore2.misc.geometry.GeometryTools2D; +import rescuecore2.log.Logger; + +import org.uncommons.maths.number.NumberGenerator; +import org.uncommons.maths.random.Probability; +import org.uncommons.maths.random.ContinuousUniformGenerator; + +import java.util.Collection; +import java.util.List; +import java.util.ArrayList; +import java.util.HashSet; + +/** + A MapGenerator that generates a grid world. + */ +public class ManhattanGenerator implements MapGenerator { + private static final String GRID_WIDTH_KEY = "generator.manhattan.grid.width"; + private static final String GRID_HEIGHT_KEY = "generator.manhattan.grid.height"; + private static final String GRID_SIZE_KEY = "generator.manhattan.grid.size"; + private static final String ROAD_WIDTH_KEY = "generator.manhattan.road.width"; + private static final String BUILDING_WIDTH_MIN_KEY = "generator.manhattan.building.width.min"; + private static final String BUILDING_HEIGHT_MIN_KEY = "generator.manhattan.building.height.min"; + private static final String BUILDING_SEPARATION_MIN_KEY = "generator.manhattan.building.separation.min"; + private static final String BUILDING_SEPARATION_MAX_KEY = "generator.manhattan.building.separation.max"; + private static final String BUILDING_MIN_SIZE_KEY = "generator.manhattan.building.split.min-size"; + private static final String BUILDING_MAX_SIZE_KEY = "generator.manhattan.building.split.max-size"; + private static final String BUILDING_SPLIT_CHANCE_KEY = "generator.manhattan.building.split.chance"; + + private Config config; + // private NumberGenerator widthGenerator; + // private NumberGenerator heightGenerator; + private NumberGenerator separationGenerator; + private Probability split; + private double minSize; + private double maxSize; + private double minWidth; + private double minHeight; + + private GMLMap map; + + /** + Construct a ManhattanGenerator. + @param config The configuration to use. + */ + public ManhattanGenerator(Config config) { + this.config = config; + // widthGenerator = new ContinuousUniformGenerator(config.getFloatValue(BUILDING_WIDTH_MIN_KEY), config.getFloatValue(BUILDING_WIDTH_MAX_KEY), config.getRandom()); + // heightGenerator = new ContinuousUniformGenerator(config.getFloatValue(BUILDING_HEIGHT_MIN_KEY), config.getFloatValue(BUILDING_HEIGHT_MAX_KEY), config.getRandom()); + // Logger.debug("separation min: " + config.getFloatValue(BUILDING_SEPARATION_MIN_KEY)); + // Logger.debug("separation max: " + config.getFloatValue(BUILDING_SEPARATION_MAX_KEY)); + separationGenerator = new ContinuousUniformGenerator(config.getFloatValue(BUILDING_SEPARATION_MIN_KEY), config.getFloatValue(BUILDING_SEPARATION_MAX_KEY), config.getRandom()); + // Logger.debug("Generator: "+ separationGenerator); + split = new Probability(config.getFloatValue(BUILDING_SPLIT_CHANCE_KEY)); + minSize = config.getFloatValue(BUILDING_MIN_SIZE_KEY); + maxSize = config.getFloatValue(BUILDING_MAX_SIZE_KEY); + minWidth = config.getFloatValue(BUILDING_WIDTH_MIN_KEY); + minHeight = config.getFloatValue(BUILDING_HEIGHT_MIN_KEY); + } + + @Override + public void populate(GMLMap gmlMap) { + this.map = gmlMap; + int gridWidth = config.getIntValue(GRID_WIDTH_KEY); + int gridHeight = config.getIntValue(GRID_HEIGHT_KEY); + double gridSize = config.getIntValue(GRID_SIZE_KEY); + double roadWidth = config.getIntValue(ROAD_WIDTH_KEY); + Logger.debug("Generating manhattan map: grid size " + gridWidth + " x " + gridHeight); + Logger.debug("Grid cell size: " + gridSize + "m"); + Logger.debug("Road width: " + roadWidth + "m"); + Collection allBuildings = new ArrayList(); + for (int gridX = 0; gridX < gridWidth; ++gridX) { + for (int gridY = 0; gridY < gridHeight; ++gridY) { + double cellXMin = (gridX * gridSize) + roadWidth; + double cellYMin = (gridY * gridSize) + roadWidth; + double cellXMax = ((gridX + 1) * gridSize) - roadWidth; + double cellYMax = ((gridY + 1) * gridSize) - roadWidth; + GMLBuilding base = createBuilding(cellXMin, cellYMin, cellXMax, cellYMax); + allBuildings.addAll(divide(base)); + } + } + map.removeAllNodes(); + map.removeAllEdges(); + map.removeAllBuildings(); + for (GMLBuilding next : allBuildings) { + map.add(next); + for (GMLDirectedEdge edge : next.getEdges()) { + map.add(edge.getEdge()); + map.add(edge.getEdge().getStart()); + map.add(edge.getEdge().getEnd()); + } + } + } + + private Collection divide(GMLBuilding b) { + // Logger.debug("Possibly dividing building " + b); + Collection result = new HashSet(); + List vertices = coordinatesToVertices(b.getUnderlyingCoordinates()); + double area = GeometryTools2D.computeArea(vertices); + // Logger.debug("Area: " + area + " sqm"); + if (area <= minSize) { + result.add(b); + } + else { + if (area > maxSize || split.nextEvent(config.getRandom())) { + // Split the building + double xMin = b.getBounds().getMinX(); + double xMax = b.getBounds().getMaxX(); + double yMin = b.getBounds().getMinY(); + double yMax = b.getBounds().getMaxY(); + double width = xMax - xMin; + double height = yMax - yMin; + // Logger.debug("Width: " + width); + // Logger.debug("Height: " + height); + // Logger.debug("width * height = " + (width * height)); + // Logger.debug("Area = " + area); + if (height > width) { + // Logger.debug("Splitting horizontally"); + // Horizontal split + double splitY = (yMax + yMin) / 2; + double topOffset = separationGenerator.nextValue(); + double bottomOffset = separationGenerator.nextValue(); + double topY = splitY + topOffset; + double bottomY = splitY - bottomOffset; + // Logger.debug("yMin = " + yMin); + // Logger.debug("yMax = " + yMax); + // Logger.debug("splitY = " + splitY); + // Logger.debug("topOffset = " + topOffset); + // Logger.debug("bottomOffset = " + bottomOffset); + // Logger.debug("topY = " + topY); + // Logger.debug("bottomY = " + bottomY); + if (yMax - topY < minHeight || bottomY - yMin < minHeight) { + // Logger.debug("Split too thin"); + // Logger.debug("Top piece: " + (yMax - topY)); + // Logger.debug("Bottom piece: " + (bottomY - yMin)); + // Logger.debug("Minimum height: " + minHeight); + result.add(b); + } + else { + result.addAll(divide(createBuilding(xMin, yMin, xMax, bottomY))); + result.addAll(divide(createBuilding(xMin, topY, xMax, yMax))); + } + } + else { + // Logger.debug("Splitting vertically"); + // Vertical split + double splitX = (xMax + xMin) / 2; + double leftOffset = separationGenerator.nextValue(); + double rightOffset = separationGenerator.nextValue(); + double leftX = splitX - leftOffset; + double rightX = splitX + rightOffset; + // Logger.debug("xMin = " + xMin); + // Logger.debug("xMax = " + xMax); + // Logger.debug("splitX = " + splitX); + // Logger.debug("leftOffset = " + leftOffset); + // Logger.debug("rightOffset = " + rightOffset); + // Logger.debug("leftX = " + leftX); + // Logger.debug("rightX = " + rightX); + if (xMax - rightX < minWidth || leftX - xMin < minWidth) { + // Logger.debug("Split too thin"); + // Logger.debug("Left piece: " + (leftX - xMin)); + // Logger.debug("Right piece: " + (xMax - rightX)); + // Logger.debug("Minimum width: " + minWidth); + result.add(b); + } + else { + result.addAll(divide(createBuilding(xMin, yMin, leftX, yMax))); + result.addAll(divide(createBuilding(rightX, yMin, xMax, yMax))); + } + } + } + else { + // Logger.debug("Not splitting"); + result.add(b); + } + } + return result; + } + + private List coordinatesToVertices(List coords) { + List result = new ArrayList(coords.size()); + for (GMLCoordinates c : coords) { + result.add(new Point2D(c.getX(), c.getY())); + } + return result; + } + + private GMLBuilding createBuilding(double xMin, double yMin, double xMax, double yMax) { + List nodes = new ArrayList(); + nodes.add(map.createNode(xMin, yMin)); + nodes.add(map.createNode(xMax, yMin)); + nodes.add(map.createNode(xMax, yMax)); + nodes.add(map.createNode(xMin, yMax)); + return map.createBuildingFromNodes(nodes); + } +} \ No newline at end of file diff --git a/modules/maps/src/maps/gml/generator/MapGenerator.java b/modules/maps/src/maps/gml/generator/MapGenerator.java new file mode 100644 index 0000000000000000000000000000000000000000..3a06e4247afdaf1809b975c4aad80436cfc73818 --- /dev/null +++ b/modules/maps/src/maps/gml/generator/MapGenerator.java @@ -0,0 +1,14 @@ +package maps.gml.generator; + +import maps.gml.GMLMap; + +/** + Top-level interface for map generation strategies. + */ +public interface MapGenerator { + /** + Generate a map. + @param map The map object to populate. + */ + void populate(GMLMap map); +} \ No newline at end of file diff --git a/modules/maps/src/maps/gml/view/BuildingDecorator b/modules/maps/src/maps/gml/view/BuildingDecorator new file mode 100644 index 0000000000000000000000000000000000000000..2189ba2f03929400e6b47015ec9b5858eb05a16e --- /dev/null +++ b/modules/maps/src/maps/gml/view/BuildingDecorator @@ -0,0 +1,18 @@ +package maps.gml.view; + +import maps.gml.GMLBuilding; +import rescuecore2.misc.gui.ScreenTransform; +import java.awt.Graphics2D; + +/** + Interface for objects that know how to decorate GMLBuildings. +*/ +public interface BuildingDecorator { + /** + Decorate a GMLBuilding. + @param building The building to decorate. + @param g The graphics to draw on. + @param transform The screen transform. + */ + void decorate(GMLBuilding building, Graphics2D g, ScreenTransform transform); +} \ No newline at end of file diff --git a/modules/maps/src/maps/gml/view/BuildingDecorator.java b/modules/maps/src/maps/gml/view/BuildingDecorator.java new file mode 100644 index 0000000000000000000000000000000000000000..2189ba2f03929400e6b47015ec9b5858eb05a16e --- /dev/null +++ b/modules/maps/src/maps/gml/view/BuildingDecorator.java @@ -0,0 +1,18 @@ +package maps.gml.view; + +import maps.gml.GMLBuilding; +import rescuecore2.misc.gui.ScreenTransform; +import java.awt.Graphics2D; + +/** + Interface for objects that know how to decorate GMLBuildings. +*/ +public interface BuildingDecorator { + /** + Decorate a GMLBuilding. + @param building The building to decorate. + @param g The graphics to draw on. + @param transform The screen transform. + */ + void decorate(GMLBuilding building, Graphics2D g, ScreenTransform transform); +} \ No newline at end of file diff --git a/modules/maps/src/maps/gml/view/CrossNodeDecorator.java b/modules/maps/src/maps/gml/view/CrossNodeDecorator.java new file mode 100644 index 0000000000000000000000000000000000000000..0f292c7b485ea7220645d2ebf13c3c2dcf8c3d2f --- /dev/null +++ b/modules/maps/src/maps/gml/view/CrossNodeDecorator.java @@ -0,0 +1,33 @@ +package maps.gml.view; + +import maps.gml.GMLNode; +import rescuecore2.misc.gui.ScreenTransform; +import java.awt.Graphics2D; +import java.awt.Color; + +/** + A NodeDecorator that draws a cross for each node. +*/ +public class CrossNodeDecorator implements NodeDecorator { + private Color colour; + private int size; + + /** + Construct a CrossNodeDecorator. + @param colour The colour to draw the cross. + @param size The size of each arm of the cross. + */ + public CrossNodeDecorator(Color colour, int size) { + this.colour = colour; + this.size = size; + } + + @Override + public void decorate(GMLNode node, Graphics2D g, ScreenTransform transform) { + int x = transform.xToScreen(node.getX()); + int y = transform.yToScreen(node.getY()); + g.setColor(colour); + g.drawLine(x - size, y - size, x + size, y + size); + g.drawLine(x - size, y + size, x + size, y - size); + } +} \ No newline at end of file diff --git a/modules/maps/src/maps/gml/view/DecoratorOverlay.java b/modules/maps/src/maps/gml/view/DecoratorOverlay.java new file mode 100755 index 0000000000000000000000000000000000000000..19ff1a464e0d331c21140834e3ff010d4d22100d --- /dev/null +++ b/modules/maps/src/maps/gml/view/DecoratorOverlay.java @@ -0,0 +1,467 @@ +package maps.gml.view; + +import maps.gml.GMLBuilding; +import maps.gml.GMLEdge; +import maps.gml.GMLNode; +import maps.gml.GMLRoad; +import maps.gml.GMLSpace; +import maps.gml.GMLRefuge; +import rescuecore2.misc.gui.ScreenTransform; +import java.awt.Graphics2D; +import java.awt.*; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +/** + * Draws an overlay consisting of Decorators. + */ +public class DecoratorOverlay implements Overlay { + + private transient Map nodeDecorators; + private transient Map edgeDecorators; + private transient Map buildingDecorators; + private transient Map roadDecorators; + private transient Map spaceDecorators; + + + /** + * Construct a DecoratorOverlay. + */ + public DecoratorOverlay() { + nodeDecorators = new HashMap(); + edgeDecorators = new HashMap(); + buildingDecorators = new HashMap(); + roadDecorators = new HashMap(); + spaceDecorators = new HashMap(); + } + + + /** + * Set the NodeDecorator for a set of GMLNodes. + * + * @param decorator + * The decorator to set. + * @param nodes + * The nodes to set the decorator for. + */ + public void setNodeDecorator( NodeDecorator decorator, GMLNode... nodes ) { + setNodeDecorator( decorator, Arrays.asList( nodes ) ); + } + + + /** + * Set the NodeDecorator for a set of GMLNodes. + * + * @param decorator + * The decorator to set. + * @param nodes + * The nodes to set the decorator for. + */ + public void setNodeDecorator( NodeDecorator decorator, + Collection nodes ) { + for ( GMLNode next : nodes ) { + nodeDecorators.put( next, decorator ); + } + } + + + /** + * Get the NodeDecorator for a GMLNodes. + * + * @param node + * The node to look up. + * @return The NodeDecorator for that node. This will be null if no custom + * decorator has been set for that node. + */ + public NodeDecorator getNodeDecorator( GMLNode node ) { + NodeDecorator result = nodeDecorators.get( node ); + return result; + } + + + /** + * Remove any custom NodeDecorator for a set of GMLNodes. + * + * @param nodes + * The nodes to remove any custom decorator for. + */ + public void clearNodeDecorator( GMLNode... nodes ) { + clearNodeDecorator( Arrays.asList( nodes ) ); + } + + + /** + * Remove any custom NodeDecorator for a set of GMLNodes. + * + * @param nodes + * The nodes to remove any custom decorator for. + */ + public void clearNodeDecorator( Collection nodes ) { + for ( GMLNode next : nodes ) { + nodeDecorators.remove( next ); + } + } + + + /** + * Remove any custom NodeDecorators. + */ + public void clearAllNodeDecorators() { + nodeDecorators.clear(); + } + + + /** + * Set the EdgeDecorator for a set of GMLEdges. + * + * @param decorator + * The decorator to set. + * @param edges + * The edges to set the decorator for. + */ + public void setEdgeDecorator( EdgeDecorator decorator, GMLEdge... edges ) { + setEdgeDecorator( decorator, Arrays.asList( edges ) ); + } + + + /** + * Set the EdgeDecorator for a set of GMLEdges. + * + * @param decorator + * The decorator to set. + * @param edges + * The edges to set the decorator for. + */ + public void setEdgeDecorator( EdgeDecorator decorator, + Collection edges ) { + for ( GMLEdge next : edges ) { + edgeDecorators.put( next, decorator ); + } + } + + + /** + * Get the EdgeDecorator for a GMLEdge. + * + * @param edge + * The edge to look up. + * @return The EdgeDecorator for that edge. This will be null if no custom + * decorator has been set for that edge. + */ + public EdgeDecorator getEdgeDecorator( GMLEdge edge ) { + EdgeDecorator result = edgeDecorators.get( edge ); + return result; + } + + + /** + * Remove any custom EdgeDecorator for a set of GMLEdges. + * + * @param edges + * The edges to remove any custom decorator for. + */ + public void clearEdgeDecorator( GMLEdge... edges ) { + clearEdgeDecorator( Arrays.asList( edges ) ); + } + + + /** + * Remove any custom EdgeDecorator for a set of GMLEdges. + * + * @param edges + * The edges to remove any custom decorator for. + */ + public void clearEdgeDecorator( Collection edges ) { + for ( GMLEdge next : edges ) { + edgeDecorators.remove( next ); + } + } + + + /** + * Remove any custom EdgeDecorators. + */ + public void clearAllEdgeDecorators() { + edgeDecorators.clear(); + } + + + /** + * Set the BuildingDecorator for a set of GMLBuildings. + * + * @param decorator + * The decorator to set. + * @param buildings + * The buildings to set the decorator for. + */ + public void setBuildingDecorator( BuildingDecorator decorator, + GMLBuilding... buildings ) { + setBuildingDecorator( decorator, Arrays.asList( buildings ) ); + } + + + /** + * Set the BuildingDecorator for a set of GMLBuildings. + * + * @param decorator + * The decorator to set. + * @param buildings + * The buildings to set the decorator for. + */ + public void setBuildingDecorator( BuildingDecorator decorator, + Collection buildings ) { + for ( GMLBuilding next : buildings ) { + buildingDecorators.put( next, decorator ); + } + } + + + /** + * Get the BuildingDecorator for a GMLBuildings. + * + * @param building + * The building to look up. + * @return The BuildingDecorator for that building. This will be null if no + * custom decorator has been set for that building. + */ + public BuildingDecorator getBuildingDecorator( GMLBuilding building ) { + BuildingDecorator result = buildingDecorators.get( building ); + return result; + } + + + /** + * Remove any custom BuildingDecorator for a set of GMLBuildings. + * + * @param buildings + * The buildings to remove any custom decorator for. + */ + public void clearBuildingDecorator( GMLBuilding... buildings ) { + clearBuildingDecorator( Arrays.asList( buildings ) ); + } + + + /** + * Remove any custom BuildingDecorator for a set of GMLBuildings. + * + * @param buildings + * The buildings to remove any custom decorator for. + */ + public void + clearBuildingDecorator( Collection buildings ) { + for ( GMLBuilding next : buildings ) { + buildingDecorators.remove( next ); + } + } + + + /** + * Remove any custom BuildingDecorators. + */ + public void clearAllBuildingDecorators() { + buildingDecorators.clear(); + } + + + /** + * Set the RoadDecorator for a set of GMLRoads. + * + * @param decorator + * The decorator to set. + * @param roads + * The roads to set the decorator for. + */ + public void setRoadDecorator( RoadDecorator decorator, GMLRoad... roads ) { + setRoadDecorator( decorator, Arrays.asList( roads ) ); + } + + + /** + * Set the RoadDecorator for a set of GMLRoads. + * + * @param decorator + * The decorator to set. + * @param roads + * The roads to set the decorator for. + */ + public void setRoadDecorator( RoadDecorator decorator, + Collection roads ) { + for ( GMLRoad next : roads ) { + roadDecorators.put( next, decorator ); + } + } + + + /** + * Get the RoadDecorator for a GMLRoads. + * + * @param road + * The road to look up. + * @return The RoadDecorator for that road. Will return null if no custom + * decorator has been set for that road. + */ + public RoadDecorator getRoadDecorator( GMLRoad road ) { + RoadDecorator result = roadDecorators.get( road ); + return result; + } + + + /** + * Remove any custom RoadDecorator for a set of GMLRoads. + * + * @param roads + * The roads to remove any custom decorator for. + */ + public void clearRoadDecorator( GMLRoad... roads ) { + clearRoadDecorator( Arrays.asList( roads ) ); + } + + + /** + * Remove any custom RoadDecorator for a set of GMLRoads. + * + * @param roads + * The roads to remove any custom decorator for. + */ + public void clearRoadDecorator( Collection roads ) { + for ( GMLRoad next : roads ) { + roadDecorators.remove( next ); + } + } + + + /** + * Remove any custom RoadDecorators. + */ + public void clearAllRoadDecorators() { + roadDecorators.clear(); + } + + + /** + * Set the SpaceDecorator for a set of GMLSpaces. + * + * @param decorator + * The decorator to set. + * @param spaces + * The spaces to set the decorator for. + */ + public void setSpaceDecorator( SpaceDecorator decorator, + GMLSpace... spaces ) { + setSpaceDecorator( decorator, Arrays.asList( spaces ) ); + } + + + /** + * Set the SpaceDecorator for a set of GMLSpaces. + * + * @param decorator + * The decorator to set. + * @param spaces + * The spaces to set the decorator for. + */ + public void setSpaceDecorator( SpaceDecorator decorator, + Collection spaces ) { + for ( GMLSpace next : spaces ) { + spaceDecorators.put( next, decorator ); + } + } + + + /** + * Get the SpaceDecorator for a GMLSpaces. + * + * @param space + * The space to look up. + * @return The SpaceDecorator for that space. This will be null if no custom + * decorator has been set for that space. + */ + public SpaceDecorator getSpaceDecorator( GMLSpace space ) { + SpaceDecorator result = spaceDecorators.get( space ); + return result; + } + + + /** + * Remove any custom SpaceDecorator for a set of GMLSpaces. + * + * @param spaces + * The spaces to remove any custom decorator for. + */ + public void clearSpaceDecorator( GMLSpace... spaces ) { + clearSpaceDecorator( Arrays.asList( spaces ) ); + } + + + /** + * Remove any custom SpaceDecorator for a set of GMLSpaces. + * + * @param spaces + * The spaces to remove any custom decorator for. + */ + public void clearSpaceDecorator( Collection spaces ) { + for ( GMLSpace next : spaces ) { + spaceDecorators.remove( next ); + } + } + + + /** + * Remove any custom SpaceDecorators. + */ + public void clearAllSpaceDecorators() { + spaceDecorators.clear(); + } + + + /** + * Remove all types of Decorators. + */ + public void clearAllDecorators() { + clearAllBuildingDecorators(); + clearAllRoadDecorators(); + clearAllSpaceDecorators(); + clearAllEdgeDecorators(); + clearAllNodeDecorators(); + } + + + @Override + public void render( Graphics2D g, ScreenTransform transform ) { + for ( Entry e : roadDecorators.entrySet() ) { + e.getValue().decorate( e.getKey(), (Graphics2D) g.create(), transform ); + } + for ( Entry e : buildingDecorators + .entrySet() ) { + e.getValue().decorate( e.getKey(), (Graphics2D) g.create(), transform ); + + if ( e.getKey() instanceof GMLRefuge ) { + int x = transform.xToScreen( e.getKey().getCentreX() ); + int y = transform.yToScreen( e.getKey().getCentreY() ); + Graphics2D oldg = g; + g.setColor( new Color( 0, 0, 0 ) ); + g.setFont( new Font( g.getFont().getName(), Font.BOLD, + g.getFont().getSize() ) ); + g.drawString( + String + .valueOf( "C=" + ( (GMLRefuge) e.getKey() ).getBedCapacity() ), + x - 20, y ); + // g.drawString(String.valueOf("R " + + // ((GMLRefuge)e.getKey()).getRefillCapacity()), x - 10, y + 10); + g = oldg; + } + } + for ( Entry e : spaceDecorators.entrySet() ) { + e.getValue().decorate( e.getKey(), (Graphics2D) g.create(), transform ); + } + for ( Entry e : edgeDecorators.entrySet() ) { + e.getValue().decorate( e.getKey(), (Graphics2D) g.create(), transform ); + } + for ( Entry e : nodeDecorators.entrySet() ) { + e.getValue().decorate( e.getKey(), (Graphics2D) g.create(), transform ); + } + } +} diff --git a/modules/maps/src/maps/gml/view/EdgeDecorator.java b/modules/maps/src/maps/gml/view/EdgeDecorator.java new file mode 100644 index 0000000000000000000000000000000000000000..a4297c633866f85731db9c0fdb539f86e7a0a4d5 --- /dev/null +++ b/modules/maps/src/maps/gml/view/EdgeDecorator.java @@ -0,0 +1,18 @@ +package maps.gml.view; + +import maps.gml.GMLEdge; +import rescuecore2.misc.gui.ScreenTransform; +import java.awt.Graphics2D; + +/** + Interface for objects that know how to decorate GMLEdges. +*/ +public interface EdgeDecorator { + /** + Decorate a GMLEdge. + @param edge The edge to decorate. + @param g The graphics to draw on. + @param transform The screen transform. + */ + void decorate(GMLEdge edge, Graphics2D g, ScreenTransform transform); +} \ No newline at end of file diff --git a/modules/maps/src/maps/gml/view/FilledShapeDecorator.java b/modules/maps/src/maps/gml/view/FilledShapeDecorator.java new file mode 100755 index 0000000000000000000000000000000000000000..6d0c170d8a9df3cf9c59a6d98bcce8822b215fe6 --- /dev/null +++ b/modules/maps/src/maps/gml/view/FilledShapeDecorator.java @@ -0,0 +1,80 @@ +package maps.gml.view; + +import maps.gml.GMLShape; +import maps.gml.GMLRoad; +import maps.gml.GMLBuilding; +import maps.gml.GMLSpace; +import maps.gml.GMLCoordinates; + +import rescuecore2.misc.gui.ScreenTransform; + +import java.awt.Graphics2D; +import java.awt.Color; +import java.awt.Polygon; +import java.util.List; + +/** + This class knows how to decorate buildings, roads and spaces. +*/ +public class FilledShapeDecorator implements BuildingDecorator, RoadDecorator, SpaceDecorator { + private Color buildingColour; + private Color roadColour; + private Color spaceColour; + + /** + Construct a FilledShapeDecorator. + @param buildingColour The colour of buildings. + @param roadColour The colour of roads. + @param spaceColour The colour of spaces. + */ + public FilledShapeDecorator(Color buildingColour, Color roadColour, Color spaceColour) { + this.buildingColour = buildingColour; + this.roadColour = roadColour; + this.spaceColour = spaceColour; + } + + @Override + public void decorate(GMLBuilding building, Graphics2D g, ScreenTransform transform) { + if (buildingColour == null) { + return; + } + g.setColor(buildingColour); + draw(building, g, transform); + } + + @Override + public void decorate(GMLRoad road, Graphics2D g, ScreenTransform transform) { + if (roadColour == null) { + return; + } + g.setColor(roadColour); + draw(road, g, transform); + } + + @Override + public void decorate(GMLSpace space, Graphics2D g, ScreenTransform transform) { + if (spaceColour == null) { + return; + } + g.setColor(spaceColour); + draw(space, g, transform); + } + + private void draw(GMLShape shape, Graphics2D g, ScreenTransform transform) { + List coords = shape.getUnderlyingCoordinates(); + int n = coords.size(); + int[] xs = new int[n]; + int[] ys = new int[n]; + int i = 0; + for (GMLCoordinates next : coords) { + xs[i] = transform.xToScreen(next.getX()); + ys[i] = transform.yToScreen(next.getY()); + ++i; + } + g.fill(new Polygon(xs, ys, n)); + } + + public Color getBuildingColour() { + return buildingColour; + } +} \ No newline at end of file diff --git a/modules/maps/src/maps/gml/view/GMLMapViewer.java b/modules/maps/src/maps/gml/view/GMLMapViewer.java new file mode 100644 index 0000000000000000000000000000000000000000..46ee3f1e06a3019218df04399d82ac6e95962f5e --- /dev/null +++ b/modules/maps/src/maps/gml/view/GMLMapViewer.java @@ -0,0 +1,697 @@ +package maps.gml.view; + +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Color; +import java.awt.Insets; +import java.awt.Point; +import java.awt.geom.Rectangle2D; +import javax.swing.JComponent; + +import java.util.Map; +import java.util.HashMap; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.ArrayList; +import java.util.HashSet; + +import rescuecore2.misc.gui.ScreenTransform; +import rescuecore2.misc.gui.PanZoomListener; + +import maps.gml.GMLMap; +import maps.gml.GMLNode; +import maps.gml.GMLEdge; +import maps.gml.GMLBuilding; +import maps.gml.GMLRoad; +import maps.gml.GMLSpace; +import maps.gml.GMLObject; +import maps.gml.GMLCoordinates; +import maps.gml.GMLTools; + +/** + A component for viewing GML maps. +*/ +public class GMLMapViewer extends JComponent { + private static final Color BUILDING_COLOUR = new Color(67, 67, 67, 67); // Transparent dark gray + private static final Color ROAD_COLOUR = new Color(192, 192, 192, 128); // Transparent light gray + private static final Color SPACE_COLOUR = new Color(0, 128, 0, 128); // Transparent green + + private static final Color GRID_COLOUR = new Color(0, 255, 0, 128); // Transparent lime + + private static final Color NODE_COLOUR = Color.BLACK; + private static final int NODE_SIZE = 3; + + private static final Color EDGE_COLOUR = Color.BLACK; + + private static final double MIN_ZOOM_BOUNDS_SIZE = 0.1; + + private GMLMap map; + private ScreenTransform transform; + private PanZoomListener panZoom; + + private transient NodeDecorator defaultNodeDecorator; + private transient Map nodeDecorators; + + private transient EdgeDecorator defaultEdgeDecorator; + private transient Map edgeDecorators; + + private transient BuildingDecorator defaultBuildingDecorator; + private transient Map buildingDecorators; + + private transient RoadDecorator defaultRoadDecorator; + private transient Map roadDecorators; + + private transient SpaceDecorator defaultSpaceDecorator; + private transient Map spaceDecorators; + + private transient List overlays; + + private boolean grid; + private double gridResolution; + private boolean paintNodes=true; + + /** + Create a GMLMapViewer. + */ + public GMLMapViewer() { + this(null); + } + + /** + Create a GMLMapViewer. + @param map The map to view. + */ + public GMLMapViewer(GMLMap map) { + panZoom = new PanZoomListener(this); + defaultNodeDecorator = new CrossNodeDecorator(NODE_COLOUR, NODE_SIZE); + defaultEdgeDecorator = new LineEdgeDecorator(EDGE_COLOUR); + FilledShapeDecorator d = new FilledShapeDecorator(BUILDING_COLOUR, ROAD_COLOUR, SPACE_COLOUR); + defaultBuildingDecorator = d; + defaultRoadDecorator = d; + defaultSpaceDecorator = d; + nodeDecorators = new HashMap(); + edgeDecorators = new HashMap(); + buildingDecorators = new HashMap(); + roadDecorators = new HashMap(); + spaceDecorators = new HashMap(); + grid = false; + gridResolution = 1; + overlays = new ArrayList(); + setMap(map); + } + + /** + Set the map. + @param map The map to view. + */ + public void setMap(GMLMap map) { + this.map = map; + transform = null; + if (map != null) { + if (!map.hasSize()) { + // CHECKSTYLE:OFF:MagicNumber + transform = new ScreenTransform(0, 0, 100, 100); + // CHECKSTYLE:ON:MagicNumber + } + else { + transform = new ScreenTransform(map.getMinX(), map.getMinY(), map.getMaxX(), map.getMaxY()); + } + } + panZoom.setScreenTransform(transform); + } + + /** + View a particular set of objects. + @param objects The objects to view. + */ + public void view(GMLObject... objects) { + view(Arrays.asList(objects)); + } + + /** + View a particular set of objects. + @param objects The objects to view. + */ + public void view(List objects) { + if (objects == null || objects.isEmpty()) { + return; + } + Rectangle2D bounds = GMLTools.getObjectBounds(objects); + if (bounds == null) { + return; + } + if (bounds.getWidth() < MIN_ZOOM_BOUNDS_SIZE) { + bounds = new Rectangle2D.Double(bounds.getX() - MIN_ZOOM_BOUNDS_SIZE / 2, + bounds.getY(), MIN_ZOOM_BOUNDS_SIZE, bounds.getHeight()); + } + if (bounds.getHeight() < MIN_ZOOM_BOUNDS_SIZE) { + bounds = new Rectangle2D.Double(bounds.getX(), + bounds.getY() - MIN_ZOOM_BOUNDS_SIZE / 2, + bounds.getWidth(), MIN_ZOOM_BOUNDS_SIZE); + } + transform.show(bounds); + } + + /** + View all objects. + */ + public void viewAll() { + transform.resetZoom(); + } + + /** + Get the PanZoomListener for this component. + @return The PanZoomListener. + */ + public PanZoomListener getPanZoomListener() { + return panZoom; + } + + /** + Set the default node decorator. + @param defaultDecorator The new default node decorator. + */ + public void setDefaultNodeDecorator(NodeDecorator defaultDecorator) { + defaultNodeDecorator = defaultDecorator; + } + + /** + Get the default node decorator. + @return The default node decorator. + */ + public NodeDecorator getDefaultNodeDecorator() { + return defaultNodeDecorator; + } + + /** + Set the NodeDecorator for a set of GMLNodes. + @param decorator The decorator to set. + @param nodes The nodes to set the decorator for. + */ + public void setNodeDecorator(NodeDecorator decorator, GMLNode... nodes) { + setNodeDecorator(decorator, Arrays.asList(nodes)); + } + + /** + Set the NodeDecorator for a set of GMLNodes. + @param decorator The decorator to set. + @param nodes The nodes to set the decorator for. + */ + public void setNodeDecorator(NodeDecorator decorator, Collection nodes) { + for (GMLNode next : nodes) { + nodeDecorators.put(next, decorator); + } + } + + /** + Get the NodeDecorator for a GMLNodes. + @param node The node to look up. + @return The NodeDecorator for that node. This will be the default decorator if no custom decorator has been set for that node. + */ + public NodeDecorator getNodeDecorator(GMLNode node) { + NodeDecorator result = nodeDecorators.get(node); + if (result == null) { + result = defaultNodeDecorator; + } + return result; + } + + /** + Remove any custom NodeDecorator for a set of GMLNodes. + @param nodes The nodes to remove any custom decorator for. + */ + public void clearNodeDecorator(GMLNode... nodes) { + clearNodeDecorator(Arrays.asList(nodes)); + } + + /** + Remove any custom NodeDecorator for a set of GMLNodes. + @param nodes The nodes to remove any custom decorator for. + */ + public void clearNodeDecorator(Collection nodes) { + for (GMLNode next : nodes) { + nodeDecorators.remove(next); + } + } + + /** + Remove any custom NodeDecorators. + */ + public void clearAllNodeDecorators() { + nodeDecorators.clear(); + } + + /** + Set the default edge decorator. + @param defaultDecorator The new default edge decorator. + */ + public void setDefaultEdgeDecorator(EdgeDecorator defaultDecorator) { + defaultEdgeDecorator = defaultDecorator; + } + + /** + Get the default edge decorator. + @return The default edge decorator. + */ + public EdgeDecorator getDefaultEdgeDecorator() { + return defaultEdgeDecorator; + } + + /** + Set the EdgeDecorator for a set of GMLEdges. + @param decorator The decorator to set. + @param edges The edges to set the decorator for. + */ + public void setEdgeDecorator(EdgeDecorator decorator, GMLEdge... edges) { + setEdgeDecorator(decorator, Arrays.asList(edges)); + } + + /** + Set the EdgeDecorator for a set of GMLEdges. + @param decorator The decorator to set. + @param edges The edges to set the decorator for. + */ + public void setEdgeDecorator(EdgeDecorator decorator, Collection edges) { + for (GMLEdge next : edges) { + edgeDecorators.put(next, decorator); + } + } + + /** + Get the EdgeDecorator for a GMLEdge. + @param edge The edge to look up. + @return The EdgeDecorator for that edge. This will be the default decorator if no custom decorator has been set for that edge. + */ + public EdgeDecorator getEdgeDecorator(GMLEdge edge) { + EdgeDecorator result = edgeDecorators.get(edge); + if (result == null) { + result = defaultEdgeDecorator; + } + return result; + } + + /** + Remove any custom EdgeDecorator for a set of GMLEdges. + @param edges The edges to remove any custom decorator for. + */ + public void clearEdgeDecorator(GMLEdge... edges) { + clearEdgeDecorator(Arrays.asList(edges)); + } + + /** + Remove any custom EdgeDecorator for a set of GMLEdges. + @param edges The edges to remove any custom decorator for. + */ + public void clearEdgeDecorator(Collection edges) { + for (GMLEdge next : edges) { + edgeDecorators.remove(next); + } + } + + /** + Remove any custom EdgeDecorators. + */ + public void clearAllEdgeDecorators() { + edgeDecorators.clear(); + } + + /** + Set the default building decorator. + @param defaultDecorator The new default building decorator. + */ + public void setDefaultBuildingDecorator(BuildingDecorator defaultDecorator) { + defaultBuildingDecorator = defaultDecorator; + } + + /** + Get the default building decorator. + @return The default building decorator. + */ + public BuildingDecorator getDefaultBuildingDecorator() { + return defaultBuildingDecorator; + } + + /** + Set the BuildingDecorator for a set of GMLBuildings. + @param decorator The decorator to set. + @param buildings The buildings to set the decorator for. + */ + public void setBuildingDecorator(BuildingDecorator decorator, GMLBuilding... buildings) { + setBuildingDecorator(decorator, Arrays.asList(buildings)); + } + + /** + Set the BuildingDecorator for a set of GMLBuildings. + @param decorator The decorator to set. + @param buildings The buildings to set the decorator for. + */ + public void setBuildingDecorator(BuildingDecorator decorator, Collection buildings) { + for (GMLBuilding next : buildings) { + buildingDecorators.put(next, decorator); + } + } + + /** + Get the BuildingDecorator for a GMLBuildings. + @param building The building to look up. + @return The BuildingDecorator for that building. This will be the default decorator if no custom decorator has been set for that building. + */ + public BuildingDecorator getBuildingDecorator(GMLBuilding building) { + BuildingDecorator result = buildingDecorators.get(building); + if (result == null) { + result = defaultBuildingDecorator; + } + return result; + } + + /** + Remove any custom BuildingDecorator for a set of GMLBuildings. + @param buildings The buildings to remove any custom decorator for. + */ + public void clearBuildingDecorator(GMLBuilding... buildings) { + clearBuildingDecorator(Arrays.asList(buildings)); + } + + /** + Remove any custom BuildingDecorator for a set of GMLBuildings. + @param buildings The buildings to remove any custom decorator for. + */ + public void clearBuildingDecorator(Collection buildings) { + for (GMLBuilding next : buildings) { + buildingDecorators.remove(next); + } + } + + /** + Remove any custom BuildingDecorators. + */ + public void clearAllBuildingDecorators() { + buildingDecorators.clear(); + } + + /** + Set the default road decorator. + @param defaultDecorator The new default road decorator. + */ + public void setDefaultRoadDecorator(RoadDecorator defaultDecorator) { + defaultRoadDecorator = defaultDecorator; + } + + /** + Get the default road decorator. + @return The default road decorator. + */ + public RoadDecorator getDefaultRoadDecorator() { + return defaultRoadDecorator; + } + + /** + Set the RoadDecorator for a set of GMLRoads. + @param decorator The decorator to set. + @param roads The roads to set the decorator for. + */ + public void setRoadDecorator(RoadDecorator decorator, GMLRoad... roads) { + setRoadDecorator(decorator, Arrays.asList(roads)); + } + + /** + Set the RoadDecorator for a set of GMLRoads. + @param decorator The decorator to set. + @param roads The roads to set the decorator for. + */ + public void setRoadDecorator(RoadDecorator decorator, Collection roads) { + for (GMLRoad next : roads) { + roadDecorators.put(next, decorator); + } + } + + /** + Get the RoadDecorator for a GMLRoads. + @param road The road to look up. + @return The RoadDecorator for that road. This will be the default decorator if no custom decorator has been set for that road. + */ + public RoadDecorator getRoadDecorator(GMLRoad road) { + RoadDecorator result = roadDecorators.get(road); + if (result == null) { + result = defaultRoadDecorator; + } + return result; + } + + /** + Remove any custom RoadDecorator for a set of GMLRoads. + @param roads The roads to remove any custom decorator for. + */ + public void clearRoadDecorator(GMLRoad... roads) { + clearRoadDecorator(Arrays.asList(roads)); + } + + /** + Remove any custom RoadDecorator for a set of GMLRoads. + @param roads The roads to remove any custom decorator for. + */ + public void clearRoadDecorator(Collection roads) { + for (GMLRoad next : roads) { + roadDecorators.remove(next); + } + } + + /** + Remove any custom RoadDecorators. + */ + public void clearAllRoadDecorators() { + roadDecorators.clear(); + } + + /** + Set the default space decorator. + @param defaultDecorator The new default space decorator. + */ + public void setDefaultSpaceDecorator(SpaceDecorator defaultDecorator) { + defaultSpaceDecorator = defaultDecorator; + } + + /** + Get the default space decorator. + @return The default space decorator. + */ + public SpaceDecorator getDefaultSpaceDecorator() { + return defaultSpaceDecorator; + } + + /** + Set the SpaceDecorator for a set of GMLSpaces. + @param decorator The decorator to set. + @param spaces The spaces to set the decorator for. + */ + public void setSpaceDecorator(SpaceDecorator decorator, GMLSpace... spaces) { + setSpaceDecorator(decorator, Arrays.asList(spaces)); + } + + /** + Set the SpaceDecorator for a set of GMLSpaces. + @param decorator The decorator to set. + @param spaces The spaces to set the decorator for. + */ + public void setSpaceDecorator(SpaceDecorator decorator, Collection spaces) { + for (GMLSpace next : spaces) { + spaceDecorators.put(next, decorator); + } + } + + /** + Get the SpaceDecorator for a GMLSpaces. + @param space The space to look up. + @return The SpaceDecorator for that space. This will be the default decorator if no custom decorator has been set for that space. + */ + public SpaceDecorator getSpaceDecorator(GMLSpace space) { + SpaceDecorator result = spaceDecorators.get(space); + if (result == null) { + result = defaultSpaceDecorator; + } + return result; + } + + /** + Remove any custom SpaceDecorator for a set of GMLSpaces. + @param spaces The spaces to remove any custom decorator for. + */ + public void clearSpaceDecorator(GMLSpace... spaces) { + clearSpaceDecorator(Arrays.asList(spaces)); + } + + /** + Remove any custom SpaceDecorator for a set of GMLSpaces. + @param spaces The spaces to remove any custom decorator for. + */ + public void clearSpaceDecorator(Collection spaces) { + for (GMLSpace next : spaces) { + spaceDecorators.remove(next); + } + } + + /** + Remove any custom SpaceDecorators. + */ + public void clearAllSpaceDecorators() { + spaceDecorators.clear(); + } + + /** + Set whether to draw the grid or not. + @param b True to draw the grid. + */ + public void setGridEnabled(boolean b) { + grid = b; + } + + /** + Set the grid resolution. + @param resolution The new grid resolution. + */ + public void setGridResolution(double resolution) { + gridResolution = resolution; + } + + /** + Add an overlay to the view. + @param overlay The overlay to add. + */ + public void addOverlay(Overlay overlay) { + overlays.add(overlay); + } + + /** + Remove an overlay from the view. + @param overlay The overlay to remove. + */ + public void removeOverlay(Overlay overlay) { + overlays.remove(overlay); + } + + @Override + public void paintComponent(Graphics graphics) { + super.paintComponent(graphics); + Graphics copy = graphics.create(); + copy.setColor(getBackground()); + copy.fillRect(0, 0, getWidth(), getHeight()); + if (map == null) { + return; + } + Insets insets = getInsets(); + int width = getWidth() - insets.left - insets.right; + int height = getHeight() - insets.top - insets.bottom; + Graphics2D g = (Graphics2D)graphics.create(insets.left, insets.top, width + 1 , height + 1); + transform.rescale(width, height); + Collection roads; + Collection buildings; + Collection spaces; + Collection edges; + Collection nodes; + synchronized (map) { + roads = new HashSet(map.getRoads()); + buildings = new HashSet(map.getBuildings()); + spaces = new HashSet(map.getSpaces()); + edges = new HashSet(map.getEdges()); + nodes = new HashSet(map.getNodes()); + } + for (GMLRoad next : roads) { + RoadDecorator d = getRoadDecorator(next); + if (d != null) { + d.decorate(next, (Graphics2D)g.create(), transform); + } + } + for (GMLBuilding next : buildings) { + BuildingDecorator d = getBuildingDecorator(next); + if (d != null) { + d.decorate(next, (Graphics2D)g.create(), transform); + } + } + for (GMLSpace next : spaces) { + SpaceDecorator d = getSpaceDecorator(next); + if (d != null) { + d.decorate(next, (Graphics2D)g.create(), transform); + } + } + for (GMLEdge next : edges) { + EdgeDecorator e = getEdgeDecorator(next); + if (e != null) { + e.decorate(next, (Graphics2D)g.create(), transform); + } + } + for (GMLNode next : nodes) { + NodeDecorator n = getNodeDecorator(next); + if (paintNodes&&n != null) { + n.decorate(next, (Graphics2D)g.create(), transform); + } + } + for (Overlay next : overlays) { + next.render((Graphics2D)g.create(), transform); + } + if (grid) { + double xMin = roundDownToGrid(transform.screenToX(0)); + double xMax = roundUpToGrid(transform.screenToX(width)); + double yMin = roundDownToGrid(transform.screenToY(height)); + double yMax = roundUpToGrid(transform.screenToY(0)); + g.setColor(GRID_COLOUR); + for (double worldX = xMin; worldX <= xMax; worldX += gridResolution) { + int x = transform.xToScreen(worldX); + g.drawLine(x, 0, x, height); + } + for (double worldY = yMin; worldY <= yMax; worldY += gridResolution) { + int y = transform.yToScreen(worldY); + g.drawLine(0, y, width, y); + } + } + } + + @Override + public boolean isOpaque() { + return true; + } + + /** + Enable or disable the pan/zoom feature. + @param enabled Whether pan/zoom should be enabled or not. + */ + public void setPanZoomEnabled(boolean enabled) { + panZoom.setEnabled(enabled); + } + + /** + Get the coordinates of a point on screen. + @param x The screen x coordinate. + @param y The screen y coordinate. + @return The coordinates in the GML map under the screen point. + */ + public GMLCoordinates getCoordinatesAtPoint(int x, int y) { + double cx = transform.screenToX(x); + double cy = transform.screenToY(y); + return new GMLCoordinates(cx, cy); + } + + /** + Get the on-screen coordinates for a point. + @param c The GML coordinates to look up. + @return The on-screen coordinates of the point. + */ + public Point getScreenCoordinates(GMLCoordinates c) { + int x = transform.xToScreen(c.getX()); + int y = transform.yToScreen(c.getY()); + return new Point(x, y); + } + + private double roundDownToGrid(double d) { + return Math.floor(d / gridResolution) * gridResolution; + } + + private double roundUpToGrid(double d) { + return Math.ceil(d / gridResolution) * gridResolution; + } + + public void setPaintNodes(boolean paintNodes) { + this.paintNodes = paintNodes; + + } +} diff --git a/modules/maps/src/maps/gml/view/GMLObjectInspector.java b/modules/maps/src/maps/gml/view/GMLObjectInspector.java new file mode 100644 index 0000000000000000000000000000000000000000..930451aab26395fb1c8d17b8c0355959c104f716 --- /dev/null +++ b/modules/maps/src/maps/gml/view/GMLObjectInspector.java @@ -0,0 +1,381 @@ +package maps.gml.view; + +import java.awt.BorderLayout; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTable; +import javax.swing.table.AbstractTableModel; +import javax.swing.table.TableModel; + +import maps.gml.GMLDirectedEdge; +import maps.gml.GMLEdge; +import maps.gml.GMLMap; +import maps.gml.GMLNode; +import maps.gml.GMLObject; +import maps.gml.GMLShape; +import maps.validate.ValidationError; + +/** + A class for inspecting GML objects. +*/ +public class GMLObjectInspector extends JPanel { + private static final int NODE_ROW_ID = 0; + private static final int NODE_ROW_X = 1; + private static final int NODE_ROW_Y = 2; + private static final int NODE_ROW_ATTACHED_EDGES = 3; + private static final int NODE_ROWS = 4; + + private static final int EDGE_ROW_ID = 0; + private static final int EDGE_ROW_START = 1; + private static final int EDGE_ROW_END = 2; + private static final int EDGE_ROW_PASSABLE = 3; + private static final int EDGE_ROW_ATTACHED_SHAPES = 4; + private static final int EDGE_ROWS = 5; + + private static final int SHAPE_ROW_ID = 0; + private static final int SHAPE_ROW_EDGE_COUNT = 1; + private static final int SHAPE_BASE_ROWS = 2; + + private static final TableModel EMPTY_MODEL = new AbstractTableModel() { + @Override + public int getRowCount() { + return 0; + } + + @Override + public int getColumnCount() { + return 0; + } + + @Override + public Object getValueAt(int row, int col) { + return null; + } + }; + + private GMLMap map; + private JTable table; + private NodeTableModel nodeModel; + private EdgeTableModel edgeModel; + private ShapeTableModel shapeModel; + private Map> errors; + + /** + Construct a new GMLObjectInspector. + @param map The GMLMap to consult for geometry information. + */ + public GMLObjectInspector(GMLMap map) { + super(new BorderLayout()); + this.map = map; + errors = new HashMap>(); + nodeModel = new NodeTableModel(); + edgeModel = new EdgeTableModel(); + shapeModel = new ShapeTableModel(); + table = new JTable(); + JScrollPane scroll = new JScrollPane(table); + add(scroll, BorderLayout.CENTER); + } + + /** + Set the map this inspector should consult for geometry information. + @param newMap The new map. + */ + public void setMap(GMLMap newMap) { + map = newMap; + } + + /** + Inspect a GMLNode. + @param node The node to inspect. + */ + public void inspect(GMLNode node) { + table.setModel(nodeModel); + List e = (node == null) ? null : errors.get(node.getID()); + nodeModel.show(node, e); + } + + /** + Inspect a GMLEdge. + @param edge The edge to inspect. + */ + public void inspect(GMLEdge edge) { + table.setModel(edgeModel); + List e = (edge == null) ? null : errors.get(edge.getID()); + edgeModel.show(edge, e); + } + + /** + Inspect a GMLShape. + @param shape The shape to inspect. + */ + public void inspect(GMLShape shape) { + table.setModel(shapeModel); + List e = (shape == null) ? null : errors.get(shape.getID()); + shapeModel.show(shape, e); + } + + /** + Inspect a GMLObject. + @param object The object to inspect. + */ + public void inspect(GMLObject object) { + if (object == null) { + table.setModel(EMPTY_MODEL); + } + else if (object instanceof GMLNode) { + inspect((GMLNode)object); + } + else if (object instanceof GMLEdge) { + inspect((GMLEdge)object); + } + else if (object instanceof GMLShape) { + inspect((GMLShape)object); + } + else { + throw new IllegalArgumentException("Don't know how to inspect " + object); + } + } + + /** + * Set the Collection of ValidationErrors for the Inspector to display in the table. + * @param err The collection of errors. + */ + public void setErrors(Collection err) { + errors.clear(); + for (ValidationError e : err) { + if (!errors.containsKey(e.getId())) { + errors.put(e.getId(), new ArrayList()); + } + errors.get(e.getId()).add(e); + } + } + + private class NodeTableModel extends AbstractTableModel { + private GMLNode node; + private List errors; + + void show(GMLNode n, List err) { + node = n; + errors = err; + fireTableDataChanged(); + } + + @Override + public int getRowCount() { + int errorCount = (errors == null) ? 0 : errors.size(); + return NODE_ROWS + errorCount; + } + + @Override + public int getColumnCount() { + return 2; + } + + @Override + public Object getValueAt(int row, int col) { + if (col == 0) { + switch (row) { + case NODE_ROW_ID: + return "Node ID"; + case NODE_ROW_X: + return "X"; + case NODE_ROW_Y: + return "Y"; + case NODE_ROW_ATTACHED_EDGES: + return "Attached edges"; + default: + return "Error"; + } + } + else if (col == 1) { + if (node == null) { + return null; + } + switch (row) { + case NODE_ROW_ID: + return node.getID(); + case NODE_ROW_X: + return node.getX(); + case NODE_ROW_Y: + return node.getY(); + case NODE_ROW_ATTACHED_EDGES: + if (map == null) { + return ""; + } + Collection attached = map.getAttachedEdges(node); + StringBuilder result = new StringBuilder(); + for (GMLEdge next : attached) { + result.append(next.toString()); + result.append(" "); + } + return result.toString(); + default: + int errorCount = (errors == null) ? 0 : errors.size(); + int index = row - NODE_ROWS; + if (index < 0 || index >= errorCount) { + throw new IllegalArgumentException("Invalid row: " + row); + } + return errors.get(index).getMessage(); + } + } + else { + throw new IllegalArgumentException("Unrecognised column: " + col); + } + } + } + + private class EdgeTableModel extends AbstractTableModel { + private GMLEdge edge; + private List errors; + + void show(GMLEdge e, List err) { + edge = e; + errors = err; + fireTableDataChanged(); + } + + @Override + public int getRowCount() { + int errorCount = (errors == null) ? 0 : errors.size(); + return EDGE_ROWS + errorCount; + } + + @Override + public int getColumnCount() { + return 2; + } + + @Override + public Object getValueAt(int row, int col) { + if (col == 0) { + switch (row) { + case EDGE_ROW_ID: + return "Edge ID"; + case EDGE_ROW_START: + return "Start node"; + case EDGE_ROW_END: + return "End node"; + case EDGE_ROW_PASSABLE: + return "Passable"; + case EDGE_ROW_ATTACHED_SHAPES: + return "Attached shapes"; + default: + return "Error"; + } + } + else if (col == 1) { + if (edge == null) { + return null; + } + switch (row) { + case EDGE_ROW_ID: + return edge.getID(); + case EDGE_ROW_START: + return edge.getStart().getID(); + case EDGE_ROW_END: + return edge.getEnd().getID(); + case EDGE_ROW_PASSABLE: + return edge.isPassable(); + case EDGE_ROW_ATTACHED_SHAPES: + if (map == null) { + return ""; + } + Collection attached = map.getAttachedShapes(edge); + StringBuilder result = new StringBuilder(); + for (GMLShape next : attached) { + result.append(next.toString()); + result.append(" "); + } + return result.toString(); + default: + int errorCount = (errors == null) ? 0 : errors.size(); + int index = row - EDGE_ROWS; + if (index < 0 || index >= errorCount) { + throw new IllegalArgumentException("Invalid row: " + row); + } + return errors.get(index).getMessage(); + } + } + else { + throw new IllegalArgumentException("Unrecognised column: " + col); + } + } + } + + private static class ShapeTableModel extends AbstractTableModel { + private GMLShape shape; + private List errors; + + void show(GMLShape s, List err) { + shape = s; + errors = err; + fireTableDataChanged(); + } + + @Override + public int getRowCount() { + int edgeCount = (shape == null) ? 0 : shape.getEdges().size(); + int errorCount = (errors == null) ? 0 : errors.size(); + return SHAPE_BASE_ROWS + edgeCount + errorCount; + } + + @Override + public int getColumnCount() { + return 2; + } + + @Override + public Object getValueAt(int row, int col) { + if (col == 0) { + switch (row) { + case SHAPE_ROW_ID: + return "Shape ID"; + case SHAPE_ROW_EDGE_COUNT: + return "Number of edges"; + default: + int edgeCount = (shape == null) ? 0 : shape.getEdges().size(); + if (row < SHAPE_BASE_ROWS + edgeCount) { + return "Edge " + (row - SHAPE_BASE_ROWS + 1); + } + return "Error"; + } + } + else if (col == 1) { + if (shape == null) { + return null; + } + switch (row) { + case SHAPE_ROW_ID: + return shape.getID(); + case SHAPE_ROW_EDGE_COUNT: + return shape.getEdges().size(); + default: + int edgeCount = shape.getEdges().size(); + if (row < SHAPE_BASE_ROWS + edgeCount) { + List edges = shape.getEdges(); + int index = row - SHAPE_BASE_ROWS; + if (index < 0 || index >= edges.size()) { + throw new IllegalArgumentException("Invalid row: " + row); + } + return edges.get(index); + } + int errorCount = (errors == null) ? 0 : errors.size(); + int index = row - SHAPE_BASE_ROWS - edgeCount; + if (index < 0 || index >= errorCount) { + throw new IllegalArgumentException("Invalid row: " + row); + } + return errors.get(index).getMessage(); + } + } + else { + throw new IllegalArgumentException("Unrecognised column: " + col); + } + } + } +} \ No newline at end of file diff --git a/modules/maps/src/maps/gml/view/LineEdgeDecorator.java b/modules/maps/src/maps/gml/view/LineEdgeDecorator.java new file mode 100644 index 0000000000000000000000000000000000000000..88026c921b2a297f5c9d09e3859a89feb88b2fe6 --- /dev/null +++ b/modules/maps/src/maps/gml/view/LineEdgeDecorator.java @@ -0,0 +1,37 @@ +package maps.gml.view; + +import maps.gml.GMLEdge; +import rescuecore2.misc.gui.ScreenTransform; +import java.awt.Graphics2D; +import java.awt.Color; +import java.awt.Stroke; +import java.awt.BasicStroke; + +/** + An EdgeDecorator that draws a line for each edge. +*/ +public class LineEdgeDecorator implements EdgeDecorator { + private static final Stroke PASSABLE_STROKE = new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL); + private static final Stroke IMPASSABLE_STROKE = new BasicStroke(2, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL); + + private Color colour; + + /** + Construct a LineEdgeDecorator. + @param colour The colour to draw the line. + */ + public LineEdgeDecorator(Color colour) { + this.colour = colour; + } + + @Override + public void decorate(GMLEdge edge, Graphics2D g, ScreenTransform transform) { + int x1 = transform.xToScreen(edge.getStart().getX()); + int y1 = transform.yToScreen(edge.getStart().getY()); + int x2 = transform.xToScreen(edge.getEnd().getX()); + int y2 = transform.yToScreen(edge.getEnd().getY()); + g.setColor(colour); + g.setStroke(edge.isPassable() ? PASSABLE_STROKE : IMPASSABLE_STROKE); + g.drawLine(x1, y1, x2, y2); + } +} \ No newline at end of file diff --git a/modules/maps/src/maps/gml/view/LineOverlay.java b/modules/maps/src/maps/gml/view/LineOverlay.java new file mode 100644 index 0000000000000000000000000000000000000000..8dc644412f27ca104537fca824cc313f275acb5f --- /dev/null +++ b/modules/maps/src/maps/gml/view/LineOverlay.java @@ -0,0 +1,97 @@ +package maps.gml.view; + +import java.awt.Graphics2D; +import java.awt.Color; + +import rescuecore2.misc.gui.ScreenTransform; +import rescuecore2.misc.geometry.Point2D; + +/** + A line overlay. + */ +public class LineOverlay implements Overlay { + private Point2D start; + private Point2D end; + private Color colour; + private boolean useWorldCoords; + + /** + Construct a LineOverlay with no coordinates defined.. + @param colour The colour to draw the rectangle. + @param useWorldCoords Whether to convert the coordinates from world to screen. Set to false if you want to directly specify screen coordinates. + */ + public LineOverlay(Color colour, boolean useWorldCoords) { + this(null, null, colour, useWorldCoords); + } + + /** + Construct a LineOverlay. + @param start The start coordinate. + @param end The end coordinate. + @param colour The colour to draw the rectangle. + @param useWorldCoords Whether to convert the coordinates from world to screen. Set to false if you want to directly specify screen coordinates. + */ + public LineOverlay(Point2D start, Point2D end, Color colour, boolean useWorldCoords) { + this.start = start; + this.end = end; + this.colour = colour; + this.useWorldCoords = useWorldCoords; + } + + /** + Set the start coordinate. + @param p The new start coordinate. + */ + public void setStart(Point2D p) { + start = p; + } + + /** + Set the end coordinate. + @param p The new end coordinate. + */ + public void setEnd(Point2D p) { + end = p; + } + + /** + Set the colour. + @param c The new colour. + */ + public void setColour(Color c) { + colour = c; + } + + /** + Set whether to use world coordinates or not. If true, the coordinates will be converted to screen coordinates; if false the coordinates will be used as given. + @param b True to use world coordinates, false otherwise. + */ + public void setUseWorldCoordinates(boolean b) { + useWorldCoords = b; + } + + @Override + public void render(Graphics2D g, ScreenTransform transform) { + if (start == null || end == null) { + return; + } + Graphics2D graphics = (Graphics2D)g.create(); + graphics.setColor(colour); + double x1 = start.getX(); + double x2 = end.getX(); + double y1 = start.getY(); + double y2 = end.getY(); + if (useWorldCoords) { + x1 = transform.xToScreen(x1); + x2 = transform.xToScreen(x2); + /* + double temp = transform.yToScreen(y2); + y2 = transform.yToScreen(y1); + y1 = temp; + */ + y1 = transform.yToScreen(y1); + y2 = transform.yToScreen(y2); + } + graphics.drawLine((int)x1, (int)y1, (int)x2, (int)y2); + } +} \ No newline at end of file diff --git a/modules/maps/src/maps/gml/view/NodeDecorator.java b/modules/maps/src/maps/gml/view/NodeDecorator.java new file mode 100644 index 0000000000000000000000000000000000000000..ad0de3a290d6296eac496f802133d4758aa2e789 --- /dev/null +++ b/modules/maps/src/maps/gml/view/NodeDecorator.java @@ -0,0 +1,18 @@ +package maps.gml.view; + +import maps.gml.GMLNode; +import rescuecore2.misc.gui.ScreenTransform; +import java.awt.Graphics2D; + +/** + Interface for objects that know how to decorate GMLNodes. +*/ +public interface NodeDecorator { + /** + Decorate a GMLNode. + @param node The node to decorate. + @param g The graphics to draw on. + @param transform The screen transform. + */ + void decorate(GMLNode node, Graphics2D g, ScreenTransform transform); +} \ No newline at end of file diff --git a/modules/maps/src/maps/gml/view/NullBuildingDecorator.java b/modules/maps/src/maps/gml/view/NullBuildingDecorator.java new file mode 100644 index 0000000000000000000000000000000000000000..455ba4909e9c1998f083721f91215db4d74a0463 --- /dev/null +++ b/modules/maps/src/maps/gml/view/NullBuildingDecorator.java @@ -0,0 +1,14 @@ +package maps.gml.view; + +import maps.gml.GMLBuilding; +import rescuecore2.misc.gui.ScreenTransform; +import java.awt.Graphics2D; + +/** + A no-op BuildingDecorator. +*/ +public class NullBuildingDecorator implements BuildingDecorator { + @Override + public void decorate(GMLBuilding building, Graphics2D g, ScreenTransform transform) { + } +} \ No newline at end of file diff --git a/modules/maps/src/maps/gml/view/NullEdgeDecorator.java b/modules/maps/src/maps/gml/view/NullEdgeDecorator.java new file mode 100644 index 0000000000000000000000000000000000000000..9331075c1f4ed6c016132048a17ee874d14ea21a --- /dev/null +++ b/modules/maps/src/maps/gml/view/NullEdgeDecorator.java @@ -0,0 +1,14 @@ +package maps.gml.view; + +import maps.gml.GMLEdge; +import rescuecore2.misc.gui.ScreenTransform; +import java.awt.Graphics2D; + +/** + A no-op EdgeDecorator. +*/ +public class NullEdgeDecorator implements EdgeDecorator { + @Override + public void decorate(GMLEdge edge, Graphics2D g, ScreenTransform transform) { + } +} \ No newline at end of file diff --git a/modules/maps/src/maps/gml/view/NullNodeDecorator.java b/modules/maps/src/maps/gml/view/NullNodeDecorator.java new file mode 100644 index 0000000000000000000000000000000000000000..0f0f866286abecd1a44486a155d059d557553089 --- /dev/null +++ b/modules/maps/src/maps/gml/view/NullNodeDecorator.java @@ -0,0 +1,14 @@ +package maps.gml.view; + +import maps.gml.GMLNode; +import rescuecore2.misc.gui.ScreenTransform; +import java.awt.Graphics2D; + +/** + A no-op NodeDecorator. +*/ +public class NullNodeDecorator implements NodeDecorator { + @Override + public void decorate(GMLNode node, Graphics2D g, ScreenTransform transform) { + } +} \ No newline at end of file diff --git a/modules/maps/src/maps/gml/view/NullRoadDecorator.java b/modules/maps/src/maps/gml/view/NullRoadDecorator.java new file mode 100644 index 0000000000000000000000000000000000000000..c6eb2e5677fbd231de06cd4570b9507c9f53de70 --- /dev/null +++ b/modules/maps/src/maps/gml/view/NullRoadDecorator.java @@ -0,0 +1,14 @@ +package maps.gml.view; + +import maps.gml.GMLRoad; +import rescuecore2.misc.gui.ScreenTransform; +import java.awt.Graphics2D; + +/** + A no-op RoadDecorator. +*/ +public class NullRoadDecorator implements RoadDecorator { + @Override + public void decorate(GMLRoad road, Graphics2D g, ScreenTransform transform) { + } +} \ No newline at end of file diff --git a/modules/maps/src/maps/gml/view/NullSpaceDecorator.java b/modules/maps/src/maps/gml/view/NullSpaceDecorator.java new file mode 100644 index 0000000000000000000000000000000000000000..c7ddb37af14974b873571c4fcc4008d3a1bc2df2 --- /dev/null +++ b/modules/maps/src/maps/gml/view/NullSpaceDecorator.java @@ -0,0 +1,14 @@ +package maps.gml.view; + +import maps.gml.GMLSpace; +import rescuecore2.misc.gui.ScreenTransform; +import java.awt.Graphics2D; + +/** + A no-op SpaceDecorator. +*/ +public class NullSpaceDecorator implements SpaceDecorator { + @Override + public void decorate(GMLSpace space, Graphics2D g, ScreenTransform transform) { + } +} \ No newline at end of file diff --git a/modules/maps/src/maps/gml/view/Overlay.java b/modules/maps/src/maps/gml/view/Overlay.java new file mode 100644 index 0000000000000000000000000000000000000000..e0ec768ca27b80b00f3489617b49f42e41e92335 --- /dev/null +++ b/modules/maps/src/maps/gml/view/Overlay.java @@ -0,0 +1,17 @@ +package maps.gml.view; + +import java.awt.Graphics2D; + +import rescuecore2.misc.gui.ScreenTransform; + +/** + Interface for overlays that appear on the GML map viewer. +*/ +public interface Overlay { + /** + Render this overlay. + @param g The graphics to draw on. + @param transform The current screen transform. + */ + void render(Graphics2D g, ScreenTransform transform); +} \ No newline at end of file diff --git a/modules/maps/src/maps/gml/view/RectangleOverlay.java b/modules/maps/src/maps/gml/view/RectangleOverlay.java new file mode 100644 index 0000000000000000000000000000000000000000..c81614d95f96b4f3d7b8405f2dfa9535a95fb8c1 --- /dev/null +++ b/modules/maps/src/maps/gml/view/RectangleOverlay.java @@ -0,0 +1,114 @@ +package maps.gml.view; + +import java.awt.Graphics2D; +import java.awt.Color; + +import rescuecore2.misc.gui.ScreenTransform; + +/** + A rectangular overlay. + */ +public class RectangleOverlay implements Overlay { + private double left; + private double right; + private double bottom; + private double top; + private Color colour; + private boolean useWorldCoords; + + /** + Construct a RectangleOverlay with no coordinates defined.. + @param colour The colour to draw the rectangle. + @param useWorldCoords Whether to convert the coordinates from world to screen. Set to false if you want to directly specify screen coordinates. + */ + public RectangleOverlay(Color colour, boolean useWorldCoords) { + this(Double.NaN, Double.NaN, Double.NaN, Double.NaN, colour, useWorldCoords); + } + + /** + Construct a RectangleOverlay. + @param left The left-hand X coordinate. + @param right The right-hand X coordinate. + @param top The top Y coordinate. + @param bottom The bottom Y coordinate. + @param colour The colour to draw the rectangle. + @param useWorldCoords Whether to convert the coordinates from world to screen. Set to false if you want to directly specify screen coordinates. + */ + public RectangleOverlay(double left, double right, double top, double bottom, Color colour, boolean useWorldCoords) { + this.left = left; + this.right = right; + this.top = top; + this.bottom = bottom; + this.colour = colour; + this.useWorldCoords = useWorldCoords; + } + + /** + Set the left-hand X coordinate. + @param x The new coordinate. + */ + public void setLeft(double x) { + left = x; + } + + /** + Set the right-hand X coordinate. + @param x The new coordinate. + */ + public void setRight(double x) { + right = x; + } + + /** + Set the top Y coordinate. + @param y The new coordinate. + */ + public void setTop(double y) { + top = y; + } + + /** + Set the bottom Y coordinate. + @param y The new coordinate. + */ + public void setBottom(double y) { + bottom = y; + } + + /** + Set the colour. + @param c The new colour. + */ + public void setColour(Color c) { + colour = c; + } + + /** + Set whether to use world coordinates or not. If true, the coordinates will be converted to screen coordinates; if false the coordinates will be used as given. + @param b True to use world coordinates, false otherwise. + */ + public void setUseWorldCoordinates(boolean b) { + useWorldCoords = b; + } + + @Override + public void render(Graphics2D g, ScreenTransform transform) { + if (Double.isNaN(left) || Double.isNaN(right) || Double.isNaN(top) || Double.isNaN(bottom)) { + return; + } + Graphics2D graphics = (Graphics2D)g.create(); + graphics.setColor(colour); + double x1 = left < right ? left : right; + double x2 = left < right ? right : left; + double y1 = bottom < top ? bottom : top; + double y2 = bottom < top ? top : bottom; + if (useWorldCoords) { + x1 = transform.xToScreen(x1); + x2 = transform.xToScreen(x2); + double temp = transform.yToScreen(y2); + y2 = transform.yToScreen(y1); + y1 = temp; + } + graphics.fillRect((int)x1, (int)y1, (int)(x2 - x1), (int)(y2 - y1)); + } +} \ No newline at end of file diff --git a/modules/maps/src/maps/gml/view/RoadDecorator.java b/modules/maps/src/maps/gml/view/RoadDecorator.java new file mode 100644 index 0000000000000000000000000000000000000000..c0f03ed258fa05416ac6a13594de5dd48389b9ff --- /dev/null +++ b/modules/maps/src/maps/gml/view/RoadDecorator.java @@ -0,0 +1,18 @@ +package maps.gml.view; + +import maps.gml.GMLRoad; +import rescuecore2.misc.gui.ScreenTransform; +import java.awt.Graphics2D; + +/** + Interface for objects that know how to decorate GMLRoads. +*/ +public interface RoadDecorator { + /** + Decorate a GMLRoad. + @param road The road to decorate. + @param g The graphics to draw on. + @param transform The screen transform. + */ + void decorate(GMLRoad road, Graphics2D g, ScreenTransform transform); +} \ No newline at end of file diff --git a/modules/maps/src/maps/gml/view/SpaceDecorator.java b/modules/maps/src/maps/gml/view/SpaceDecorator.java new file mode 100644 index 0000000000000000000000000000000000000000..e2824d5eefca2615bef13e69c9dafc2504c35b46 --- /dev/null +++ b/modules/maps/src/maps/gml/view/SpaceDecorator.java @@ -0,0 +1,18 @@ +package maps.gml.view; + +import maps.gml.GMLSpace; +import rescuecore2.misc.gui.ScreenTransform; +import java.awt.Graphics2D; + +/** + Interface for objects that know how to decorate GMLSpaces. +*/ +public interface SpaceDecorator { + /** + Decorate a GMLSpace. + @param space The space to decorate. + @param g The graphics to draw on. + @param transform The screen transform. + */ + void decorate(GMLSpace space, Graphics2D g, ScreenTransform transform); +} \ No newline at end of file diff --git a/modules/maps/src/maps/gml/view/SquareNodeDecorator.java b/modules/maps/src/maps/gml/view/SquareNodeDecorator.java new file mode 100644 index 0000000000000000000000000000000000000000..4dd43b291e2cb21f6ce5af3d8e6ac1810b7d3b5f --- /dev/null +++ b/modules/maps/src/maps/gml/view/SquareNodeDecorator.java @@ -0,0 +1,32 @@ +package maps.gml.view; + +import maps.gml.GMLNode; +import rescuecore2.misc.gui.ScreenTransform; +import java.awt.Graphics2D; +import java.awt.Color; + +/** + A NodeDecorator that draws a square for each node. +*/ +public class SquareNodeDecorator implements NodeDecorator { + private Color colour; + private int size; + + /** + Construct a SquareNodeDecorator. + @param colour The colour to draw the square. + @param size The size of the square. + */ + public SquareNodeDecorator(Color colour, int size) { + this.colour = colour; + this.size = size; + } + + @Override + public void decorate(GMLNode node, Graphics2D g, ScreenTransform transform) { + int x = transform.xToScreen(node.getX()); + int y = transform.yToScreen(node.getY()); + g.setColor(colour); + g.fillRect(x - (size / 2), y - (size / 2), size + 1, size + 1); + } +} \ No newline at end of file diff --git a/modules/maps/src/maps/legacy/LegacyBuilding.java b/modules/maps/src/maps/legacy/LegacyBuilding.java new file mode 100644 index 0000000000000000000000000000000000000000..a935ff42f59d7ae99fdb1c3da41cf45fb2af412e --- /dev/null +++ b/modules/maps/src/maps/legacy/LegacyBuilding.java @@ -0,0 +1,78 @@ +package maps.legacy; + +import static rescuecore2.misc.EncodingTools.readInt32LE; +import static rescuecore2.misc.EncodingTools.reallySkip; + +import java.io.InputStream; +import java.io.IOException; + +/** + A legacy building. +*/ +public class LegacyBuilding extends LegacyObject { + private int floors; + private int code; + private int[] entrances; + private int[] apexes; + + @Override + public void read(InputStream in) throws IOException { + // CHECKSTYLE:OFF:MagicNumber + // Skip size + reallySkip(in, 4); + id = readInt32LE(in); + x = readInt32LE(in); + y = readInt32LE(in); + floors = readInt32LE(in); + // Skip attributes, ignition, fieryness, brokenness - 4 * 4 bytes + reallySkip(in, 16); + int numEntrances = readInt32LE(in); + entrances = new int[numEntrances]; + for (int j = 0; j < numEntrances; ++j) { + entrances[j] = readInt32LE(in); + } + // Skip shapeID, ground area, total read - 3 * 4 bytes + reallySkip(in, 12); + code = readInt32LE(in); + int numApexes = readInt32LE(in); + apexes = new int[numApexes * 2]; + for (int j = 0; j < numApexes; ++j) { + // Apexes + apexes[j * 2] = readInt32LE(in); + apexes[j * 2 + 1] = readInt32LE(in); + } + // CHECKSTYLE:ON:MagicNumber + } + + /** + Get the number of floors in this building. + @return The number of floors. + */ + public int getFloors() { + return floors; + } + + /** + Get the building code. + @return The building code. + */ + public int getCode() { + return code; + } + + /** + Get the list of entrance nodes. + @return The entrances. + */ + public int[] getEntrances() { + return entrances; + } + + /** + Get the list of apexes. + @return The apex list. + */ + public int[] getApexes() { + return apexes; + } +} \ No newline at end of file diff --git a/modules/maps/src/maps/legacy/LegacyMap.java b/modules/maps/src/maps/legacy/LegacyMap.java new file mode 100644 index 0000000000000000000000000000000000000000..cf8f7630997012920474dc78e45433f0b785ec0b --- /dev/null +++ b/modules/maps/src/maps/legacy/LegacyMap.java @@ -0,0 +1,154 @@ +package maps.legacy; + +import static rescuecore2.misc.EncodingTools.readInt32LE; +import static rescuecore2.misc.EncodingTools.reallySkip; + +import java.io.File; +import java.io.InputStream; +import java.io.FileInputStream; +import java.io.IOException; + +import java.util.Map; +import java.util.HashMap; +import java.util.Collection; +import java.util.Collections; + +import rescuecore2.log.Logger; + +/** + A legacy (version 0) RoboCup Rescue map. +*/ +public class LegacyMap implements maps.Map { + private Map nodes; + private Map roads; + private Map buildings; + + /** + Construct an empty map. + */ + public LegacyMap() { + nodes = new HashMap(); + roads = new HashMap(); + buildings = new HashMap(); + } + + /** + Construct a map and read from a directory. + @param baseDir The map directory. + @throws IOException If there is a problem reading the map. + */ + public LegacyMap(File baseDir) throws IOException { + this(); + read(baseDir); + } + + /** + Read map data from a directory. + @param baseDir The map directory. + @throws IOException If there is a problem reading the map. + */ + public void read(File baseDir) throws IOException { + nodes.clear(); + roads.clear(); + buildings.clear(); + readNodes(baseDir); + readRoads(baseDir); + readBuildings(baseDir); + } + + /** + Get all roads. + @return All roads. + */ + public Collection getRoads() { + return Collections.unmodifiableCollection(roads.values()); + } + + /** + Get a road by ID. + @param id The ID to look up. + @return The road with the given ID or null if no such road exists. + */ + public LegacyRoad getRoad(int id) { + return roads.get(id); + } + + /** + Get all nodes. + @return All nodes. + */ + public Collection getNodes() { + return Collections.unmodifiableCollection(nodes.values()); + } + + /** + Get a node by ID. + @param id The ID to look up. + @return The node with the given ID or null if no such node exists. + */ + public LegacyNode getNode(int id) { + return nodes.get(id); + } + + /** + Get all buildings. + @return All buildings. + */ + public Collection getBuildings() { + return Collections.unmodifiableCollection(buildings.values()); + } + + /** + Get a building by ID. + @param id The ID to look up. + @return The building with the given ID or null if no such building exists. + */ + public LegacyBuilding getBuilding(int id) { + return buildings.get(id); + } + + private void readNodes(File baseDir) throws IOException { + File f = new File(baseDir, "node.bin"); + InputStream in = new FileInputStream(f); + // CHECKSTYLE:OFF:MagicNumber + reallySkip(in, 12); + // CHECKSTYLE:ON:MagicNumber + int num = readInt32LE(in); + Logger.debug("Reading " + num + " nodes"); + for (int i = 0; i < num; ++i) { + LegacyNode node = new LegacyNode(); + node.read(in); + nodes.put(node.getID(), node); + } + } + + private void readRoads(File baseDir) throws IOException { + File f = new File(baseDir, "road.bin"); + InputStream in = new FileInputStream(f); + // CHECKSTYLE:OFF:MagicNumber + reallySkip(in, 12); + // CHECKSTYLE:ON:MagicNumber + int num = readInt32LE(in); + Logger.debug("Reading " + num + " roads"); + for (int i = 0; i < num; ++i) { + LegacyRoad road = new LegacyRoad(); + road.read(in); + roads.put(road.getID(), road); + } + } + + private void readBuildings(File baseDir) throws IOException { + File f = new File(baseDir, "building.bin"); + InputStream in = new FileInputStream(f); + // CHECKSTYLE:OFF:MagicNumber + reallySkip(in, 12); + // CHECKSTYLE:ON:MagicNumber + int num = readInt32LE(in); + Logger.debug("Reading " + num + " buildings"); + for (int i = 0; i < num; ++i) { + LegacyBuilding building = new LegacyBuilding(); + building.read(in); + buildings.put(building.getID(), building); + } + } +} \ No newline at end of file diff --git a/modules/maps/src/maps/legacy/LegacyMapFormat.java b/modules/maps/src/maps/legacy/LegacyMapFormat.java new file mode 100644 index 0000000000000000000000000000000000000000..adbf5929b5597af22b56fdcc427952a8a6180048 --- /dev/null +++ b/modules/maps/src/maps/legacy/LegacyMapFormat.java @@ -0,0 +1,45 @@ +package maps.legacy; + +import maps.Map; +import maps.MapFormat; +import maps.MapException; + +import java.io.File; +import java.io.IOException; + +/** + MapFormat for legacy maps. +*/ +public final class LegacyMapFormat implements MapFormat { + /** Singleton instance. */ + public static final LegacyMapFormat INSTANCE = new LegacyMapFormat(); + + private LegacyMapFormat() {} + + @Override + public LegacyMap read(File file) throws MapException { + try { + return new LegacyMap(file); + } + catch (IOException e) { + throw new MapException(e); + } + } + + @Override + public void write(Map map, File file) throws MapException { + throw new RuntimeException("LegacyMapFormat.write not implemented"); + } + + @Override + public boolean canRead(File file) throws MapException { + if (!file.exists() || !file.isDirectory()) { + return false; + } + // Look for road.bin, node.bin and building.bin files + File road = new File(file, "road.bin"); + File node = new File(file, "node.bin"); + File building = new File(file, "building.bin"); + return road.exists() && node.exists() && building.exists(); + } +} diff --git a/modules/maps/src/maps/legacy/LegacyNode.java b/modules/maps/src/maps/legacy/LegacyNode.java new file mode 100644 index 0000000000000000000000000000000000000000..0d3f28b48f4955042e850b389ce9bdb6e196fab6 --- /dev/null +++ b/modules/maps/src/maps/legacy/LegacyNode.java @@ -0,0 +1,40 @@ +package maps.legacy; + +import static rescuecore2.misc.EncodingTools.readInt32LE; +import static rescuecore2.misc.EncodingTools.reallySkip; + +import java.io.InputStream; +import java.io.IOException; + +/** + A legacy node. +*/ +public class LegacyNode extends LegacyObject { + private int[] edges; + + @Override + public void read(InputStream in) throws IOException { + // CHECKSTYLE:OFF:MagicNumber + // Skip size + reallySkip(in, 4); + id = readInt32LE(in); + x = readInt32LE(in); + y = readInt32LE(in); + int numEdges = readInt32LE(in); + edges = new int[numEdges]; + for (int j = 0; j < numEdges; ++j) { + edges[j] = readInt32LE(in); + } + // Skip signal flag, timing, pocket to turn across, shortcut to turn + reallySkip(in, (numEdges * 6 + 1) * 4); + // CHECKSTYLE:ON:MagicNumber + } + + /** + Get the list of edges, i.e. roads and buildings adjacent to this node. + @return The edge list. + */ + public int[] getEdges() { + return edges; + } +} \ No newline at end of file diff --git a/modules/maps/src/maps/legacy/LegacyObject.java b/modules/maps/src/maps/legacy/LegacyObject.java new file mode 100644 index 0000000000000000000000000000000000000000..2ab34f67044223904cf1c3832ad48f328c6dcea6 --- /dev/null +++ b/modules/maps/src/maps/legacy/LegacyObject.java @@ -0,0 +1,47 @@ +package maps.legacy; + +import java.io.InputStream; +import java.io.IOException; + +/** + Abstract base class for legacy objects. +*/ +public abstract class LegacyObject { + /** The ID of this object. */ + protected int id; + /** The X coordinate. */ + protected int x; + /** The Y coordinate. */ + protected int y; + + /** + Read the data for this object. + @param in The InputStream to read. + @throws IOException If there is a problem reading the stream. + */ + public abstract void read(InputStream in) throws IOException; + + /** + Get the ID of this object. + @return The object ID. + */ + public int getID() { + return id; + } + + /** + Get the X coordinate. + @return The X coordinate. + */ + public int getX() { + return x; + } + + /** + Get the Y coordinate. + @return The Y coordinate. + */ + public int getY() { + return y; + } +} \ No newline at end of file diff --git a/modules/maps/src/maps/legacy/LegacyRoad.java b/modules/maps/src/maps/legacy/LegacyRoad.java new file mode 100644 index 0000000000000000000000000000000000000000..2751c7795cbfca7e9b36331b5d90c5c81b78f822 --- /dev/null +++ b/modules/maps/src/maps/legacy/LegacyRoad.java @@ -0,0 +1,66 @@ +package maps.legacy; + +import static rescuecore2.misc.EncodingTools.readInt32LE; +import static rescuecore2.misc.EncodingTools.reallySkip; + +import java.io.InputStream; +import java.io.IOException; + +/** + A legacy road. +*/ +public class LegacyRoad extends LegacyObject { + private int head; + private int tail; + private int length; + private int width; + + @Override + public void read(InputStream in) throws IOException { + // CHECKSTYLE:OFF:MagicNumber + // Skip size + reallySkip(in, 4); + id = readInt32LE(in); + head = readInt32LE(in); + tail = readInt32LE(in); + length = readInt32LE(in); + // Skip roadkind, cars/humans to head/tail - 5 * 4 bytes + reallySkip(in, 20); + width = readInt32LE(in); + // Skip block, repaircost, median, lines to head/tail, width for walkers - 6 * 4 bytes + reallySkip(in, 24); + // CHECKSTYLE:ON:MagicNumber + } + + /** + Get the ID of the head node. + @return The head node id. + */ + public int getHead() { + return head; + } + + /** + Get the ID of the tail node. + @return The tail node id. + */ + public int getTail() { + return tail; + } + + /** + Get the length of this road in mm. + @return The length. + */ + public int getLength() { + return length; + } + + /** + Get the width of this road in mm. + @return The width. + */ + public int getWidth() { + return width; + } +} \ No newline at end of file diff --git a/modules/maps/src/maps/osm/OSMBuilding.java b/modules/maps/src/maps/osm/OSMBuilding.java new file mode 100644 index 0000000000000000000000000000000000000000..711b32e1456f6ab26e33018d55279d8641f1503b --- /dev/null +++ b/modules/maps/src/maps/osm/OSMBuilding.java @@ -0,0 +1,22 @@ +package maps.osm; + +import java.util.List; + +/** + A building in OSM space. +*/ +public class OSMBuilding extends OSMWay { + /** + Construct an OSMBuilding. + @param id The ID of the building. + @param ids The IDs of the apex nodes of the building. + */ + public OSMBuilding(Long id, List ids) { + super(id, ids); + } + + @Override + public String toString() { + return "OSMBuilding: id " + getID(); + } +} diff --git a/modules/maps/src/maps/osm/OSMException.java b/modules/maps/src/maps/osm/OSMException.java new file mode 100644 index 0000000000000000000000000000000000000000..05bc1319f440288b196dd699a009f73848d00dd0 --- /dev/null +++ b/modules/maps/src/maps/osm/OSMException.java @@ -0,0 +1,38 @@ +package maps.osm; + +/** + Exceptions related to OpenStreetMap. +*/ +public class OSMException extends Exception { + /** + Construct an OSMException with no error message. + */ + public OSMException() { + super(); + } + + /** + Construct an OSMException with an error message. + @param msg The error message. + */ + public OSMException(String msg) { + super(msg); + } + + /** + Construct an OSMException with an underlying cause. + @param cause The cause. + */ + public OSMException(Throwable cause) { + super(cause); + } + + /** + Construct an OSMException with an error message and underlying cause. + @param msg The error message. + @param cause The cause. + */ + public OSMException(String msg, Throwable cause) { + super(msg, cause); + } +} diff --git a/modules/maps/src/maps/osm/OSMMap.java b/modules/maps/src/maps/osm/OSMMap.java new file mode 100644 index 0000000000000000000000000000000000000000..8bec39918e5d034ce22e45f8f404fd3ad7829cf7 --- /dev/null +++ b/modules/maps/src/maps/osm/OSMMap.java @@ -0,0 +1,415 @@ +package maps.osm; + +import org.dom4j.Document; +import org.dom4j.DocumentHelper; +import org.dom4j.DocumentException; +import org.dom4j.Element; +import org.dom4j.io.SAXReader; + +import java.util.HashSet; +import java.util.Map; +import java.util.HashMap; +import java.util.List; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; + +import java.io.File; +import java.io.IOException; + +/** + An OpenStreetMap map. +*/ +public class OSMMap { + private static final Collection ROAD_MARKERS = new HashSet(); + + static { + ROAD_MARKERS.add("motorway"); + ROAD_MARKERS.add("motorway_link"); + ROAD_MARKERS.add("trunk"); + ROAD_MARKERS.add("trunk_link"); + ROAD_MARKERS.add("primary"); + ROAD_MARKERS.add("primary_link"); + ROAD_MARKERS.add("secondary"); + ROAD_MARKERS.add("secondary_link"); + ROAD_MARKERS.add("tertiary"); + ROAD_MARKERS.add("unclassified"); + ROAD_MARKERS.add("road"); + ROAD_MARKERS.add("residential"); + ROAD_MARKERS.add("living_street"); + ROAD_MARKERS.add("service"); + ROAD_MARKERS.add("track"); + ROAD_MARKERS.add("services"); + ROAD_MARKERS.add("pedestrian"); + } + + private Map nodes; + private Map roads; + private Map buildings; + + private boolean boundsCalculated; + private double minLat; + private double maxLat; + private double minLon; + private double maxLon; + + /** + Construct an empty map. + */ + public OSMMap() { + boundsCalculated = false; + nodes = new HashMap(); + roads = new HashMap(); + buildings = new HashMap(); + } + + /** + Construct a map from an XML document. + @param doc The document to read. + */ + public OSMMap(Document doc) throws OSMException { + this(); + read(doc); + } + + /** + Construct a map from an XML file. + @param file The file to read. + */ + public OSMMap(File file) throws OSMException, DocumentException, IOException { + this(); + SAXReader reader = new SAXReader(); + Document doc = reader.read(file); + read(doc); + } + + /** + Construct a copy of an OSMMap over a bounded area. + @param other The map to copy. + @param minLat The minimum latitude of the new map. + @param minLon The minimum longitude of the new map. + @param maxLat The maximum latitude of the new map. + @param maxLon The maximum longitude of the new map. + */ + public OSMMap(OSMMap other, double minLat, double minLon, double maxLat, double maxLon) { + this.minLat = minLat; + this.minLon = minLon; + this.maxLat = maxLat; + this.maxLon = maxLon; + boundsCalculated = true; + nodes = new HashMap(); + roads = new HashMap(); + buildings = new HashMap(); + // Copy all nodes inside the bounds + for (OSMNode next : other.nodes.values()) { + double lat = next.getLatitude(); + double lon = next.getLongitude(); + long id = next.getID(); + if (lat >= minLat && lat <= maxLat && lon >= minLon && lon <= maxLon) { + this.nodes.put(id, new OSMNode(id, lat, lon)); + } + } + // Now copy the bits of roads and buildings that do not have missing nodes + for (OSMRoad next : other.roads.values()) { + List ids = new ArrayList(next.getNodeIDs()); + for (Iterator it = ids.iterator(); it.hasNext();) { + Long nextID = it.next(); + if (!nodes.containsKey(nextID)) { + it.remove(); + } + } + if (!ids.isEmpty()) { + roads.put(next.getID(), new OSMRoad(next.getID(), ids)); + } + } + for (OSMBuilding next : other.buildings.values()) { + boolean allFound = true; + for (Long nextID : next.getNodeIDs()) { + if (!nodes.containsKey(nextID)) { + allFound = false; + } + } + if (allFound) { + buildings.put(next.getID(), new OSMBuilding(next.getID(), new ArrayList(next.getNodeIDs()))); + } + } + } + + /** + Read an XML document and populate this map. + @param doc The document to read. + */ + public void read(Document doc) throws OSMException { + boundsCalculated = false; + nodes = new HashMap(); + roads = new HashMap(); + buildings = new HashMap(); + Element root = doc.getRootElement(); + if (!"osm".equals(root.getName())) { + throw new OSMException("Invalid map file: root element must be 'osm', not " + root.getName()); + } + for (Object next : root.elements("node")) { + Element e = (Element)next; + OSMNode node = processNode(e); + } + for (Object next : root.elements("way")) { + Element e = (Element)next; + processWay(e); + } + } + + /** + Turn this map into XML. + @return A new XML document. + */ + public Document toXML() { + Element root = DocumentHelper.createElement("osm"); + Element bounds = root.addElement("bounds"); + calculateBounds(); + bounds.addAttribute("minlat", String.valueOf(minLat)); + bounds.addAttribute("maxlat", String.valueOf(maxLat)); + bounds.addAttribute("minlon", String.valueOf(minLon)); + bounds.addAttribute("maxlon", String.valueOf(maxLon)); + for (OSMNode next : nodes.values()) { + Element node = root.addElement("node"); + node.addAttribute("id", String.valueOf(next.getID())); + node.addAttribute("lat", String.valueOf(next.getLatitude())); + node.addAttribute("lon", String.valueOf(next.getLongitude())); + } + for (OSMRoad next : roads.values()) { + Element node = root.addElement("way"); + node.addAttribute("id", String.valueOf(next.getID())); + for (Long nextID : next.getNodeIDs()) { + node.addElement("nd").addAttribute("ref", String.valueOf(nextID)); + } + node.addElement("tag").addAttribute("k", "highway").addAttribute("v", "primary"); + } + for (OSMBuilding next : buildings.values()) { + Element node = root.addElement("way"); + node.addAttribute("id", String.valueOf(next.getID())); + for (Long nextID : next.getNodeIDs()) { + node.addElement("nd").addAttribute("ref", String.valueOf(nextID)); + } + node.addElement("tag").addAttribute("k", "building").addAttribute("v", "yes"); + } + return DocumentHelper.createDocument(root); + } + + /** + Get the minimum longitude in this map. + @return The minimum longitude. + */ + public double getMinLongitude() { + calculateBounds(); + return minLon; + } + + /** + Get the maximum longitude in this map. + @return The maximum longitude. + */ + public double getMaxLongitude() { + calculateBounds(); + return maxLon; + } + + /** + Get the centre longitude in this map. + @return The centre longitude. + */ + public double getCentreLongitude() { + calculateBounds(); + return (maxLon + minLon) / 2; + } + + /** + Get the minimum latitude in this map. + @return The minimum latitude. + */ + public double getMinLatitude() { + calculateBounds(); + return minLat; + } + + /** + Get the maximum latitude in this map. + @return The maximum latitude. + */ + public double getMaxLatitude() { + calculateBounds(); + return maxLat; + } + + /** + Get the centre latitude in this map. + @return The centre latitude. + */ + public double getCentreLatitude() { + calculateBounds(); + return (maxLat + minLat) / 2; + } + + /** + Get all nodes in the map. + @return All nodes. + */ + public Collection getNodes() { + return new HashSet(nodes.values()); + } + + /** + Remove a node. + @param node The node to remove. + */ + public void removeNode(OSMNode node) { + nodes.remove(node.getID()); + } + + /** + Get a node by ID. + @param id The ID of the node. + @return The node with the given ID or null. + */ + public OSMNode getNode(Long id) { + return nodes.get(id); + } + + /** + Get the nearest node to a point. + @param lat The latitude of the point. + @param lon The longitude of the point. + @return The nearest node. + */ + public OSMNode getNearestNode(double lat, double lon) { + double smallest = Double.MAX_VALUE; + OSMNode best = null; + for (OSMNode next : nodes.values()) { + double d1 = next.getLatitude() - lat; + double d2 = next.getLongitude() - lon; + double d = (d1 * d1) + (d2 * d2); + if (d < smallest) { + best = next; + smallest = d; + } + } + return best; + } + + /** + Replace a node and update all references. + @param old The node to replace. + @param replacement The replacement node. + */ + public void replaceNode(OSMNode old, OSMNode replacement) { + for (OSMRoad r : roads.values()) { + r.replace(old.getID(), replacement.getID()); + } + for (OSMBuilding b : buildings.values()) { + b.replace(old.getID(), replacement.getID()); + } + removeNode(old); + } + + /** + Get all roads. + @return All roads. + */ + public Collection getRoads() { + return new HashSet(roads.values()); + } + + /** + Remove a road. + @param road The road to remove. + */ + public void removeRoad(OSMRoad road) { + roads.remove(road.getID()); + } + + /** + Get all buildings. + @return All buildings. + */ + public Collection getBuildings() { + return new HashSet(buildings.values()); + } + + /** + Remove a building. + @param building The building to remove. + */ + public void removeBuilding(OSMBuilding building) { + buildings.remove(building.getID()); + } + + private void calculateBounds() { + if (boundsCalculated) { + return; + } + minLat = Double.POSITIVE_INFINITY; + maxLat = Double.NEGATIVE_INFINITY; + minLon = Double.POSITIVE_INFINITY; + maxLon = Double.NEGATIVE_INFINITY; + for (OSMNode node : nodes.values()) { + minLat = Math.min(minLat, node.getLatitude()); + maxLat = Math.max(maxLat, node.getLatitude()); + minLon = Math.min(minLon, node.getLongitude()); + maxLon = Math.max(maxLon, node.getLongitude()); + } + boundsCalculated = true; + } + + private OSMNode processNode(Element e) { + long id = Long.parseLong(e.attributeValue("id")); + double lat = Double.parseDouble(e.attributeValue("lat")); + double lon = Double.parseDouble(e.attributeValue("lon")); + OSMNode node = new OSMNode(id, lat, lon); + nodes.put(id, node); + return node; + } + + private void processWay(Element e) { + long id = Long.parseLong(e.attributeValue("id")); + List ids = new ArrayList(); + for (Object next : e.elements("nd")) { + Element nd = (Element)next; + Long nextID = Long.parseLong(nd.attributeValue("ref")); + ids.add(nextID); + } + // Is this way a road or a building? + boolean road = false; + boolean building = false; + for (Object next : e.elements("tag")) { + Element tag = (Element)next; + building = building || tagSignifiesBuilding(tag); + road = road || tagSignifiesRoad(tag); + } + if (building) { + buildings.put(id, new OSMBuilding(id, ids)); + } + else if (road) { + roads.put(id, new OSMRoad(id, ids)); + } + } + + private boolean tagSignifiesRoad(Element tag) { + String key = tag.attributeValue("k"); + String value = tag.attributeValue("v"); + if (!"highway".equals(key)) { + return false; + } + return ROAD_MARKERS.contains(value); + } + + private boolean tagSignifiesBuilding(Element tag) { + String key = tag.attributeValue("k"); + String value = tag.attributeValue("v"); + if ("building".equals(key)) { + return "yes".equals(value); + } + if ("rcr:building".equals(key)) { + return "1".equals(value); + } + return false; + } +} diff --git a/modules/maps/src/maps/osm/OSMMapExtractor.java b/modules/maps/src/maps/osm/OSMMapExtractor.java new file mode 100644 index 0000000000000000000000000000000000000000..c6f5bde4d26564ce9e82ded4553ff9b06ff0f335 --- /dev/null +++ b/modules/maps/src/maps/osm/OSMMapExtractor.java @@ -0,0 +1,158 @@ +package maps.osm; + +import javax.swing.JFrame; +import javax.swing.JComponent; + +import java.awt.Point; +import java.awt.Insets; +import java.awt.Graphics; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.event.MouseEvent; +import java.awt.event.MouseAdapter; + +import java.io.Writer; +import java.io.File; +import java.io.FileWriter; + +import org.dom4j.Document; +import org.dom4j.io.XMLWriter; +import org.dom4j.io.OutputFormat; + +/** + This class extracts a portion of an OSMMap. +*/ +public class OSMMapExtractor extends MouseAdapter { + private static final int VIEWER_SIZE = 500; + private static final Color DRAG_COLOUR = new Color(128, 128, 128, 64); + + private JComponent glass; + private Point press; + private Point drag; + private Point release; + + private OSMMap map; + private OSMMapViewer viewer; + private Writer out; + + /** + Construct an OSMMapExtractor. + @param map The map. + @param viewer The viewer. + @param out The writer to write extracted data to. + */ + public OSMMapExtractor(OSMMap map, OSMMapViewer viewer, Writer out) { + this.map = map; + this.viewer = viewer; + this.out = out; + this.glass = new DragGlass(); + } + + /** + Start the OSMMapExtractor. + @param args Command line arguments: source target. + */ + public static void main(String[] args) { + try { + OSMMap map = new OSMMap(new File(args[0])); + Writer out = new FileWriter(new File(args[1])); + OSMMapViewer viewer = new OSMMapViewer(map); + + OSMMapExtractor extractor = new OSMMapExtractor(map, viewer, out); + viewer.addMouseListener(extractor); + viewer.setPreferredSize(new Dimension(VIEWER_SIZE, VIEWER_SIZE)); + + JFrame frame = new JFrame(); + frame.setGlassPane(extractor.getGlass()); + frame.setContentPane(viewer); + frame.pack(); + frame.setVisible(true); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + } + // CHECKSTYLE:OFF:IllegalCatch + catch (Exception e) { + e.printStackTrace(); + } + // CHECKSTYLE:ON:IllegalCatch + } + + /** + Get a glass component for drawing the selection overlay. + @return A glass component. + */ + public JComponent getGlass() { + return glass; + } + + @Override + public void mousePressed(MouseEvent e) { + if (e.getButton() == MouseEvent.BUTTON3) { + Point p = e.getPoint(); + Insets insets = viewer.getInsets(); + p.translate(-insets.left, -insets.top); + press = new Point(p); + } + } + + @Override + public void mouseDragged(MouseEvent e) { + if (e.getButton() == MouseEvent.BUTTON3) { + Point p = e.getPoint(); + Insets insets = viewer.getInsets(); + p.translate(-insets.left, -insets.top); + drag = new Point(p); + glass.repaint(); + } + } + + @Override + public void mouseReleased(MouseEvent e) { + if (e.getButton() == MouseEvent.BUTTON3) { + Point p = e.getPoint(); + Insets insets = viewer.getInsets(); + p.translate(-insets.left, -insets.top); + release = new Point(p); + drag = null; + write(); + } + } + + private void write() { + double pressLat = viewer.getLatitude(press.y); + double pressLon = viewer.getLongitude(press.x); + double releaseLat = viewer.getLatitude(release.y); + double releaseLon = viewer.getLongitude(release.x); + try { + OSMMap newMap = new OSMMap(map, + Math.min(pressLat, releaseLat), + Math.min(pressLon, releaseLon), + Math.max(pressLat, releaseLat), + Math.max(pressLon, releaseLon)); + Document d = newMap.toXML(); + XMLWriter writer = new XMLWriter(out, OutputFormat.createPrettyPrint()); + writer.write(d); + writer.flush(); + writer.close(); + System.out.println("Wrote map"); + } + // CHECKSTYLE:OFF:IllegalCatch + catch (Exception ex) { + ex.printStackTrace(); + } + // CHECKSTYLE:ON:IllegalCatch + } + + private class DragGlass extends JComponent { + public void paintComponent(Graphics g) { + if (drag == null) { + return; + } + g.setColor(DRAG_COLOUR); + int x = Math.min(press.x, drag.x); + int y = Math.max(press.y, drag.y); + int width = (int)Math.abs(press.x - drag.x); + int height = (int)Math.abs(press.y - drag.y); + g.fillRect(x, y, width, height); + } + } +} diff --git a/modules/maps/src/maps/osm/OSMMapViewer.java b/modules/maps/src/maps/osm/OSMMapViewer.java new file mode 100644 index 0000000000000000000000000000000000000000..396041371b35121ae08ceafb1b3a7943755ef55c --- /dev/null +++ b/modules/maps/src/maps/osm/OSMMapViewer.java @@ -0,0 +1,130 @@ +package maps.osm; + +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Color; +import java.awt.Insets; +import java.awt.Point; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import javax.swing.JComponent; + +import rescuecore2.misc.gui.ScreenTransform; +import rescuecore2.misc.gui.PanZoomListener; + +/** + A component for viewing OSM maps. +*/ +public class OSMMapViewer extends JComponent { + private OSMMap map; + private ScreenTransform transform; + private PanZoomListener panZoom; + + /** + Create an OSMMapViewer. + */ + public OSMMapViewer() { + this(null); + } + + /** + Create an OSMMapViewer. + @param map The map to view. + */ + public OSMMapViewer(final OSMMap map) { + panZoom = new PanZoomListener(this); + setMap(map); + addMouseListener(new MouseAdapter() { + public void mouseClicked(MouseEvent e) { + if (e.getButton() == MouseEvent.BUTTON1) { + Point p = e.getPoint(); + double lon = transform.screenToX(p.x); + double lat = transform.screenToY(p.y); + OSMNode node = map.getNearestNode(lat, lon); + System.out.println("Click at " + lat + ", " + lon); + System.out.println("Nearest node: " + node); + } + } + }); + } + + /** + Set the map. + @param map The new map to view. + */ + public void setMap(OSMMap map) { + this.map = map; + transform = null; + if (map != null) { + transform = new ScreenTransform(map.getMinLongitude(), map.getMinLatitude(), map.getMaxLongitude(), map.getMaxLatitude()); + } + panZoom.setScreenTransform(transform); + } + + /** + Get the latitude of a screen coordinate. + @param y The screen coordinate. + @return The latitude at that coordinate. + */ + public double getLatitude(int y) { + return transform.screenToY(y); + } + + /** + Get the longitude of a screen coordinate. + @param x The screen coordinate. + @return The longitude at that coordinate. + */ + public double getLongitude(int x) { + return transform.screenToX(x); + } + + @Override + public void paintComponent(Graphics graphics) { + super.paintComponent(graphics); + if (map == null) { + return; + } + Insets insets = getInsets(); + int width = getWidth() - insets.left - insets.right; + int height = getHeight() - insets.top - insets.bottom; + Graphics2D g = (Graphics2D)graphics.create(insets.left, insets.top, width + 1 , height + 1); + transform.rescale(width, height); + g.setColor(Color.black); + for (OSMNode next : map.getNodes()) { + int x = transform.xToScreen(next.getLongitude()); + int y = transform.yToScreen(next.getLatitude()); + g.drawLine(x - 1, y - 1, x + 1, y + 1); + g.drawLine(x + 1, y - 1, x - 1, y + 1); + } + for (OSMRoad next : map.getRoads()) { + int lastX = -1; + int lastY = -1; + for (Long nodeID : next.getNodeIDs()) { + OSMNode node = map.getNode(nodeID); + int x = transform.xToScreen(node.getLongitude()); + int y = transform.yToScreen(node.getLatitude()); + if (lastX != -1) { + g.drawLine(lastX, lastY, x, y); + } + lastX = x; + lastY = y; + } + } + g.setColor(Color.blue); + for (OSMBuilding next : map.getBuildings()) { + int lastX = -1; + int lastY = -1; + for (Long nodeID : next.getNodeIDs()) { + OSMNode node = map.getNode(nodeID); + int x = transform.xToScreen(node.getLongitude()); + int y = transform.yToScreen(node.getLatitude()); + if (lastX != -1) { + g.drawLine(lastX, lastY, x, y); + } + lastX = x; + lastY = y; + } + } + } +} diff --git a/modules/maps/src/maps/osm/OSMNode.java b/modules/maps/src/maps/osm/OSMNode.java new file mode 100644 index 0000000000000000000000000000000000000000..5bf27c0629d01c19f13f3a39006ce1db2be3f4ff --- /dev/null +++ b/modules/maps/src/maps/osm/OSMNode.java @@ -0,0 +1,42 @@ +package maps.osm; + +/** + An OpenStreetMap node. + */ +public class OSMNode extends OSMObject { + private double lat; + private double lon; + + /** + Construct an OSMNode. + @param id The ID of the node. + @param lat The latitude of the node. + @param lon The longitude of the node. + */ + public OSMNode(long id, double lat, double lon) { + super(id); + this.lat = lat; + this.lon = lon; + } + + /** + Get the latitude of this node in degrees. + @return The latitude in degrees. + */ + public double getLatitude() { + return lat; + } + + /** + Get the longitude of this node in degrees. + @return The longitude in degrees. + */ + public double getLongitude() { + return lon; + } + + @Override + public String toString() { + return "OSMNode (" + getID() + ") at lat " + lat + ", lon " + lon; + } +} diff --git a/modules/maps/src/maps/osm/OSMObject.java b/modules/maps/src/maps/osm/OSMObject.java new file mode 100644 index 0000000000000000000000000000000000000000..2682f8dc88a35572ee5b952cb37a269b40472847 --- /dev/null +++ b/modules/maps/src/maps/osm/OSMObject.java @@ -0,0 +1,24 @@ +package maps.osm; + +/** + Abstract base class for OpenStreetMap objects. +*/ +public abstract class OSMObject { + private long id; + + /** + Construct an OSMObject. + @param id The ID of the object. + */ + public OSMObject(long id) { + this.id = id; + } + + /** + Get the ID of this object. + @return The ID of the object. + */ + public long getID() { + return id; + } +} diff --git a/modules/maps/src/maps/osm/OSMRoad.java b/modules/maps/src/maps/osm/OSMRoad.java new file mode 100644 index 0000000000000000000000000000000000000000..03c53d4451e84c1f05b4ae712c81676390c4762f --- /dev/null +++ b/modules/maps/src/maps/osm/OSMRoad.java @@ -0,0 +1,17 @@ +package maps.osm; + +import java.util.List; + +/** + An OpenStreetMap road. + */ +public class OSMRoad extends OSMWay { + /** + Construct an OSMRoad. + @param id The ID of the road. + @param ids The IDs of the apex nodes of the road. + */ + public OSMRoad(Long id, List ids) { + super(id, ids); + } +} diff --git a/modules/maps/src/maps/osm/OSMWay.java b/modules/maps/src/maps/osm/OSMWay.java new file mode 100644 index 0000000000000000000000000000000000000000..c596ac7d61998eb6ce31acde89b9d7010bf7deca --- /dev/null +++ b/modules/maps/src/maps/osm/OSMWay.java @@ -0,0 +1,47 @@ +package maps.osm; + +import java.util.List; +import java.util.ArrayList; +import java.util.Collections; + +/** + An OSM way. +*/ +public abstract class OSMWay extends OSMObject { + private List ids; + + /** + Construct an OSMWay. + @param id The ID of the way. + @param ids The IDs of the nodes of the way. + */ + public OSMWay(Long id, List ids) { + super(id); + this.ids = ids; + } + + /** + Get the IDs of the way nodes. + @return The IDs of the nodes of this way. + */ + public List getNodeIDs() { + return new ArrayList(ids); + } + + /** + Set the IDs of the way nodes. + @param newIDs The new IDs of the nodes of this way. + */ + public void setNodeIDs(List newIDs) { + ids = newIDs; + } + + /** + Replace a node ID in this way. + @param oldID The old node ID. + @param newID The new node ID. + */ + public void replace(Long oldID, Long newID) { + Collections.replaceAll(ids, oldID, newID); + } +} diff --git a/modules/maps/src/maps/osm/debug/OSMWayShapeInfo.java b/modules/maps/src/maps/osm/debug/OSMWayShapeInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..7520c2a4fa885a3743225cfcae6416bba463e720 --- /dev/null +++ b/modules/maps/src/maps/osm/debug/OSMWayShapeInfo.java @@ -0,0 +1,108 @@ +package maps.osm.debug; + +import java.awt.Color; +import java.awt.Shape; +import java.awt.Polygon; +import java.awt.Graphics2D; +import java.awt.geom.Rectangle2D; + +import java.util.List; + +import rescuecore2.misc.gui.ScreenTransform; +import rescuecore2.misc.gui.ShapeDebugFrame; +import rescuecore2.misc.gui.DrawingTools; + +import maps.osm.OSMWay; +import maps.osm.OSMMap; +import maps.osm.OSMNode; + +/** + A ShapeInfo that knows how to draw OSMWays. +*/ +public class OSMWayShapeInfo extends ShapeDebugFrame.ShapeInfo { + private OSMWay way; + private OSMMap map; + private Color colour; + private boolean drawEdgeDirections; + private Rectangle2D bounds; + + /** + Create a new OSMWayShapeInfo. + @param way The way to draw. + @param map The map the way is part of. + @param name The name of the way. + @param colour The colour to draw the way. + @param drawEdgeDirections Whether to draw edge directions or not. + */ + public OSMWayShapeInfo(OSMWay way, OSMMap map, String name, Color colour, boolean drawEdgeDirections) { + super(way, name); + this.way = way; + this.map = map; + this.colour = colour; + this.drawEdgeDirections = drawEdgeDirections; + if (way != null) { + bounds = findBounds(); + } + } + + @Override + public Shape paint(Graphics2D g, ScreenTransform transform) { + if (way == null) { + return null; + } + List points = way.getNodeIDs(); + int n = points.size(); + int[] xs = new int[n]; + int[] ys = new int[n]; + int i = 0; + for (long next : points) { + xs[i] = transform.xToScreen(map.getNode(next).getLongitude()); + ys[i] = transform.yToScreen(map.getNode(next).getLatitude()); + ++i; + } + Polygon p = new Polygon(xs, ys, n); + if (colour != null) { + g.setColor(colour); + g.draw(p); + if (drawEdgeDirections) { + for (i = 1; i < n; ++i) { + DrawingTools.drawArrowHeads(xs[i - 1], ys[i - 1], xs[i], ys[i], g); + } + } + } + return p; + } + + @Override + public void paintLegend(Graphics2D g, int width, int height) { + if (colour != null) { + g.setColor(colour); + g.drawRect(0, 0, width - 1, height - 1); + } + } + + @Override + public Rectangle2D getBoundsShape() { + return bounds; + } + + @Override + public java.awt.geom.Point2D getBoundsPoint() { + return null; + } + + private Rectangle2D findBounds() { + double xMin = Double.POSITIVE_INFINITY; + double xMax = Double.NEGATIVE_INFINITY; + double yMin = Double.POSITIVE_INFINITY; + double yMax = Double.NEGATIVE_INFINITY; + for (long next : way.getNodeIDs()) { + OSMNode n = map.getNode(next); + xMin = Math.min(xMin, n.getLongitude()); + xMax = Math.max(xMax, n.getLongitude()); + yMin = Math.min(yMin, n.getLatitude()); + yMax = Math.max(yMax, n.getLatitude()); + } + return new Rectangle2D.Double(xMin, yMin, xMax - xMin, yMax - yMin); + } +} diff --git a/modules/maps/src/maps/validate/GMLConnectivityValidator.java b/modules/maps/src/maps/validate/GMLConnectivityValidator.java new file mode 100644 index 0000000000000000000000000000000000000000..235f855695d295be65df4fb275ef2212f0955097 --- /dev/null +++ b/modules/maps/src/maps/validate/GMLConnectivityValidator.java @@ -0,0 +1,143 @@ +package maps.validate; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; +import java.util.Set; + +import maps.gml.GMLBuilding; +import maps.gml.GMLDirectedEdge; +import maps.gml.GMLMap; +import maps.gml.GMLRoad; +import maps.gml.GMLShape; + +/** + * Checks if a GML map is fully connected and if all individual shaped are + * correctly connected to each other (i.e. there are no dangling or no one-way + * connections). + */ +public class GMLConnectivityValidator implements MapValidator { + private GMLMap map = null; + + @Override + public Collection validate(GMLMap mmap) { + this.map = mmap; + List errors = new LinkedList(); + + // Check if all shapes are connected correctly (no dangling connections, + // etc...) + Set toBeChecked = new HashSet(); + for (GMLShape shape : map.getAllShapes()) { + errors.addAll(validateShape(shape)); + if (shape instanceof GMLBuilding || shape instanceof GMLRoad) { + toBeChecked.add(shape); + } + } + + Queue open = new LinkedList(); + GMLShape first = toBeChecked.iterator().next(); + open.add(first); + + // check for connectivity (only simple connectivity needs to be checked, + // as + // we made sure that there are no one-way connections + while (!open.isEmpty()) { + GMLShape next = open.remove(); + toBeChecked.remove(next); + for (GMLShape n : getNeigbours(next)) { + if (toBeChecked.contains(n)) { + open.add(n); + } + if (!(n instanceof GMLBuilding || n instanceof GMLRoad)) { + String message = "Can reach non-building, non-road shape " + + n.getID(); + errors.add(new ValidationError(next.getID(), message)); + + } + } + } + + if (!toBeChecked.isEmpty()) { + for (GMLShape unreachable : toBeChecked) { + String message = "The map is not fully connected. Shape cannot be reached from " + + first.getID(); + errors.add(new ValidationError(unreachable.getID(), message)); + } + } + + return errors; + } + + /** + * Check if all connections to neighbours are reflexive. + * + * @param shape + * @return + */ + private Collection validateShape(GMLShape shape) { + List errors = new LinkedList(); + + for (GMLDirectedEdge e : shape.getEdges()) { + if (shape.hasNeighbour(e)) { + int nId = shape.getNeighbour(e); + GMLShape neighbour = map.getShape(nId); + + if (neighbour == null) { + String message = "Connection to nonexisting id " + nId + + " via Edge " + e.getEdge().getID(); + errors.add(new ValidationError(shape.getID(), message)); + } + else if (neighbour == shape) { + String message = "Shape is connected to itself via Edge" + + e.getEdge().getID(); + errors.add(new ValidationError(shape.getID(), message)); + } + else { + GMLShape backRef = null; + try { + if (neighbour.hasNeighbour(e.getEdge())) { + backRef = map.getShape(neighbour.getNeighbour(e + .getEdge())); + } + if (backRef != shape) { + String message = "Connection to " + neighbour.getID() + + " via Edge " + e.getEdge().getID() + + " is not reflexive."; + errors.add(new ValidationError(shape.getID(), message)); + } + } + catch (IllegalArgumentException ex) { + String message = "Neigbour " + neighbour.getID() + + " does not share Edge " + e.getEdge().getID(); + errors.add(new ValidationError(shape.getID(), message)); + } + } + } + } + + return errors; + } + + /** + * Get all shapes that a shape is connected to. + * @param shape + * @return + */ + private Collection getNeigbours(GMLShape shape) { + Collection result = new ArrayList(); + for (GMLDirectedEdge edge : shape.getEdges()) { + if (shape.hasNeighbour(edge)) { + GMLShape n = map.getShape(shape.getNeighbour(edge)); + if (n != null) { + result.add(n); + } + } + } + + return result; + } + +} diff --git a/modules/maps/src/maps/validate/GMLMapValidator.java b/modules/maps/src/maps/validate/GMLMapValidator.java new file mode 100644 index 0000000000000000000000000000000000000000..fd9df5b092fb2ca21320203e1f87e5d09a09a9e1 --- /dev/null +++ b/modules/maps/src/maps/validate/GMLMapValidator.java @@ -0,0 +1,61 @@ +package maps.validate; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import maps.MapException; +import maps.MapReader; +import maps.gml.GMLMap; + +/** + * Load a map and check it for errors. + * + */ +public final class GMLMapValidator { + private static List> validators; + + static { + validators = new ArrayList>(); + + validators.add(new GMLConnectivityValidator()); + validators.add(new GMLShapeValidator()); + validators.add(new GMLTraversabilityValidator()); + } + + private GMLMapValidator() {} + + /** + * Returns a list of default MapValidators to use for GML maps. + @return List of default MapValidators. + */ + public static List> getDefaultValidators() { + return new ArrayList>(validators); + } + + /** + * @param args The command line arguments. + */ + public static void main(String[] args) { + try { + GMLMap map = (GMLMap)MapReader.readMap(args[0]); + boolean hasErrors = true; + for (MapValidator val : validators) { + Collection errors = val.validate(map); + for (ValidationError e : errors) { + System.err.println(e); + hasErrors = true; + } + } + if (!hasErrors) { + System.out.println("No errors have been found."); + } + + } + catch (MapException e) { + e.printStackTrace(); + } + + } + +} diff --git a/modules/maps/src/maps/validate/GMLShapeValidator.java b/modules/maps/src/maps/validate/GMLShapeValidator.java new file mode 100644 index 0000000000000000000000000000000000000000..4848e92be554e9a70ccf2acb8bab29eae960921a --- /dev/null +++ b/modules/maps/src/maps/validate/GMLShapeValidator.java @@ -0,0 +1,69 @@ +package maps.validate; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import maps.gml.GMLMap; +import maps.gml.GMLShape; + +import com.vividsolutions.jts.geom.Geometry; + +/** + Validate the correctness of basic shape properties. + @author goebelbe + */ +public class GMLShapeValidator implements MapValidator { + + @Override + public Collection validate(GMLMap map) { + List errors = new ArrayList(); + List polygons = new ArrayList(); + List shapes = new ArrayList(map.getAllShapes()); + + for (GMLShape shape : shapes) { + try { + Geometry polygon = checkShape(shape); + polygons.add(polygon); + } + catch (ValidationException e) { + errors.add(e.getError()); + polygons.add(null); + } + } + + for (int i = 0; i < polygons.size(); i++) { + Geometry s1 = polygons.get(i); + if (s1 == null) { + continue; + } + for (int j = i + 1; j < polygons.size(); j++) { + Geometry s2 = polygons.get(j); + if (s2 != null && s1.intersects(s2) && !s1.touches(s2)) { + int s1Id = shapes.get(i).getID(); + int s2Id = shapes.get(j).getID(); + String message = " Shape overlaps with shape " + s2Id; + errors.add(new ValidationError(s1Id, message)); + } + } + } + return errors; + } + + /** + Check if the given shape is correct. + @param shape + @return + */ + private static Geometry checkShape(GMLShape shape) throws ValidationException { + Geometry polygon = JTSTools.shapeToPolygon(shape); + if (!polygon.isValid()) { + throw new ValidationException(shape.getID(), "invalid shape"); + } + if (!polygon.contains(polygon.getCentroid())) { + throw new ValidationException(shape.getID(), "Shape doesn't contain centroid."); + } + return polygon; + } + +} diff --git a/modules/maps/src/maps/validate/GMLTraversabilityValidator.java b/modules/maps/src/maps/validate/GMLTraversabilityValidator.java new file mode 100644 index 0000000000000000000000000000000000000000..3aabbaddacb6be1a89194d18091e8ab85f0c7b85 --- /dev/null +++ b/modules/maps/src/maps/validate/GMLTraversabilityValidator.java @@ -0,0 +1,198 @@ +package maps.validate; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import maps.gml.GMLDirectedEdge; +import maps.gml.GMLMap; +import maps.gml.GMLRoad; +import maps.gml.GMLShape; + +import com.vividsolutions.jts.geom.Coordinate; +import com.vividsolutions.jts.geom.Geometry; +import com.vividsolutions.jts.geom.LineString; +import com.vividsolutions.jts.operation.linemerge.LineSequencer; + +/** + * Validator to check if the shapes of the map are traversable. + * + * For all shaped we check if they can be entered via their entrances. + * + * For road we also check if each entrance can reach the other ones via this + * road. + * + */ +public class GMLTraversabilityValidator implements MapValidator { + private static final double MIN_ROAD_WIDTH = 1.0; + private static final double SHAPE_PADDING = 0.01; + + @Override + public Collection validate(GMLMap map) { + Collection errors = new ArrayList(); + for (GMLShape shape : map.getRoads()) { + ValidationError error = checkTraversability(shape, MIN_ROAD_WIDTH); + if (error != null) { + errors.add(error); + } + } + for (GMLShape shape : map.getBuildings()) { + ValidationError error = checkTraversability(shape, MIN_ROAD_WIDTH); + if (error != null) { + errors.add(error); + } + } + return errors; + } + + /** + * Check if this shape can be traversed by an agent of width + * minWidth. + * + * @param shape + * @param agentWidth + * @return + */ + private ValidationError checkTraversability(GMLShape shape, double minWidth) { + // To check for traversability, we shrink the non-traversable edges + // of the shape by the radius of the agent. + // We then check, if all entrance edges are part of the same part + // of the resulting polygon + + try { + Geometry polygon = JTSTools.shapeToPolygon(shape); + if (!polygon.isValid()) { + return new ValidationError(shape.getID(), "invalid shape"); + } + Geometry boundary = impassableLines(shape); + Geometry buffer = boundary.buffer(((double) minWidth) / 2); + Geometry result = polygon.difference(buffer); + // make sure the intersection tests succeed + result = result.buffer(SHAPE_PADDING); + + Coordinate centroid = JTSTools.pointToCoordinate(shape.getCentroid()); + + // Build list of adjacent entrance edges + List edges = shape.getEdges(); + List> entrances = new ArrayList>(); + List entrance = new ArrayList(); + for (GMLDirectedEdge e : edges) { + if (shape.hasNeighbour(e)) { + entrance.add(e); + //Check if we have a line of sight to the centroid + LineString edge = JTSTools.edgeToLine(e); + Coordinate edgeCenter = edge.getCentroid().getCoordinate(); + Coordinate[] coords = new Coordinate[]{centroid, edgeCenter}; + LineString lineOfSight = JTSTools.getFactory().createLineString(coords); + if (lineOfSight.intersects(boundary)) { + String message = "Edge " + e.getEdge().getID() + + " has no line of sight to shape center."; + return new ValidationError(shape.getID(), message); + } + } + else { + if (!entrance.isEmpty()) { + entrances.add(entrance); + } + entrance = new ArrayList(); + } + } + if (!entrance.isEmpty()) { + // Merge first and last sequences if neccessary + if (shape.hasNeighbour(edges.get(0)) && !entrances.isEmpty()) { + entrances.get(0).addAll(entrance); + } + else { + entrances.add(entrance); + } + } + + // Check in which part of the polygon the entrances lie + GMLDirectedEdge firstEdge = null; + int firstPolygon = -1; + + for (List etr : entrances) { + int polyIndex = -1; + for (GMLDirectedEdge e : etr) { + polyIndex = findPolygonPartOfEdge(e, result); + if (polyIndex != -1) { + break; + } + } + + if (polyIndex == -1) { + // Entrance edge no longer in polygon + String message = "Edge is too narrow to pass through."; + return new ValidationError(etr.get(0).getEdge().getID(), + message); + } + if (firstEdge == null) { + firstEdge = etr.get(0); + firstPolygon = polyIndex; + } + else if (firstPolygon != polyIndex + && (shape instanceof GMLRoad)) { + // Only check traversability for roads + String message = "Can't reach edge " + + firstEdge.getEdge().getID() + " from " + + etr.get(0).getEdge().getID(); + return new ValidationError(shape.getID(), message); + } + + } + + return null; + } + catch (ValidationException e) { + return e.getError(); + } + } + + /** + * Find the index of the subgeometry the given edge is part of. Return -1 if + * the edge is not contained in the geometry at all. + * @param edge + * @param geom + * @return + */ + private static int findPolygonPartOfEdge(GMLDirectedEdge edge, Geometry geom) { + for (int i = 0; i < geom.getNumGeometries(); i++) { + if (edgePartOfPolygon(edge, geom.getGeometryN(i))) { + return i; + } + } + return -1; + } + + /** + * Checks if an edge is part (i.e intersects) of a given polygon. + * @param edge + * @param polygon + * @return + */ + private static boolean edgePartOfPolygon(GMLDirectedEdge edge, + Geometry polygon) { + // No idea if this works... + return polygon.intersects(JTSTools.edgeToLine(edge)); + } + + /** + * Return a LineString or MultiLineString of the impassable edges of a + * shape. + * @param shape + * @return + */ + private static Geometry impassableLines(GMLShape shape) { + LineSequencer seq = new LineSequencer(); + for (GMLDirectedEdge e : shape.getEdges()) { + if (!shape.hasNeighbour(e)) { + Coordinate[] coord = new Coordinate[2]; + coord[0] = JTSTools.nodeToCoordinate(e.getStartNode()); + coord[1] = JTSTools.nodeToCoordinate(e.getEndNode()); + seq.add(JTSTools.getFactory().createLineString(coord)); + } + } + + return seq.getSequencedLineStrings(); + } +} diff --git a/modules/maps/src/maps/validate/JTSTools.java b/modules/maps/src/maps/validate/JTSTools.java new file mode 100644 index 0000000000000000000000000000000000000000..3a7c62fa8d618f4d91b6dbb4dfa7856886c4453b --- /dev/null +++ b/modules/maps/src/maps/validate/JTSTools.java @@ -0,0 +1,114 @@ +package maps.validate; + +import maps.gml.GMLDirectedEdge; +import maps.gml.GMLNode; +import maps.gml.GMLShape; +import rescuecore2.misc.geometry.Point2D; + +import com.vividsolutions.jts.geom.Coordinate; +import com.vividsolutions.jts.geom.CoordinateList; +import com.vividsolutions.jts.geom.Geometry; +import com.vividsolutions.jts.geom.GeometryFactory; +import com.vividsolutions.jts.geom.LineString; +import com.vividsolutions.jts.geom.LinearRing; +import com.vividsolutions.jts.operation.linemerge.LineSequencer; +import com.vividsolutions.jts.util.AssertionFailedException; + +/** + * This class provides some conversion functions from GML map classes to JTS + * Geometry classes. + */ +public final class JTSTools { + private static GeometryFactory geomFactory = new GeometryFactory(); + + private JTSTools() { + }; + + /** + * Create a LineString from a GMLDirectedEdge. + * @param edge + * The edge to convert. + * @return LineString or MultLineString. + */ + public static LineString edgeToLine(GMLDirectedEdge edge) { + Coordinate[] coord = new Coordinate[2]; + coord[0] = nodeToCoordinate(edge.getStartNode()); + coord[1] = nodeToCoordinate(edge.getEndNode()); + return geomFactory.createLineString(coord); + } + + /** + * Create a JTS Polygon form a GMLShape. + * @param shape + * The shape to convert. + * @return Polygon geometry. + * @throws ValidationException + */ + public static Geometry shapeToPolygon(GMLShape shape) + throws ValidationException { + LineSequencer seq = new LineSequencer(); + for (GMLDirectedEdge e : shape.getEdges()) { + Coordinate[] coord = new Coordinate[2]; + coord[0] = nodeToCoordinate(e.getStartNode()); + coord[1] = nodeToCoordinate(e.getEndNode()); + if (coord[0].equals(coord[1])) { + throw new ValidationException(e.getEdge().getID(), + "Zero length edge."); + } + seq.add(geomFactory.createLineString(coord)); + } + + try { + if (!seq.isSequenceable()) { + throw new ValidationException(shape.getID(), + "Outline is not a single line."); + } + } + catch (AssertionFailedException e) { + throw new ValidationException(shape.getID(), + "Could not get outline: " + e.getMessage()); + } + Geometry line = seq.getSequencedLineStrings(); + + CoordinateList coord = new CoordinateList(line.getCoordinates()); + coord.closeRing(); + + // CHECKSTYLE:OFF:MagicNumber + if (coord.size() < 4) { + // CHECKSTYLE:ON:MagicNumber + throw new ValidationException(shape.getID(), "Degenerate Shape"); + } + + Geometry ring = geomFactory.createLinearRing(coord.toCoordinateArray()); + return geomFactory.createPolygon((LinearRing) ring, null); + } + + /** + * Create a Coordinate from a GMLNode. + * @param node + * Node to convert. + * @return Coordinate object. + */ + public static Coordinate nodeToCoordinate(GMLNode node) { + return new Coordinate(node.getX(), node.getY()); + } + + /** + * Create a Coordinate from a Point2D. + * @param point + * Point to convert. + * @return Coordinate object. + */ + public static Coordinate pointToCoordinate(Point2D point) { + return new Coordinate(point.getX(), point.getY()); + } + + /** + * Get the default GeometryFactory. + * @return The default GeometryFactory. + */ + public static GeometryFactory getFactory() { + return geomFactory; + } + +} diff --git a/modules/maps/src/maps/validate/MapValidator.java b/modules/maps/src/maps/validate/MapValidator.java new file mode 100644 index 0000000000000000000000000000000000000000..679ed6433e9a44a13f7a569bfe75aa7e25842a8f --- /dev/null +++ b/modules/maps/src/maps/validate/MapValidator.java @@ -0,0 +1,20 @@ +package maps.validate; + +import java.util.Collection; + +import maps.Map; + +/** + * Interface for classes that can validate maps. + * + * @param + */ +public interface MapValidator { + + /** + * Check if the given map is valid. If not, return a collection of errors. + * @param map Map that should be checked. + * @return Collection of errors. + */ + Collection validate(T map); +} diff --git a/modules/maps/src/maps/validate/ValidationError.java b/modules/maps/src/maps/validate/ValidationError.java new file mode 100644 index 0000000000000000000000000000000000000000..7ac295f70d902ef3d3e4e28148ab7c9080ed6d40 --- /dev/null +++ b/modules/maps/src/maps/validate/ValidationError.java @@ -0,0 +1,41 @@ +package maps.validate; + +/** + * This class encapsulates a validation error. It contains the object id and an + * error message. + */ +public class ValidationError { + private int id; + private String message; + + /** + * Create a new ValidationError object. + * @param id The id of the GMLObject containing the error. + * @param message The error message. + */ + public ValidationError(int id, String message) { + this.id = id; + this.message = message; + } + + /** + * Get the id of the object this error refers to. + * @return The id of the GMLObject containing the error. + */ + public int getId() { + return id; + } + + /** + * Get the error message of this ValidationError. + * @return The error message. + */ + public String getMessage() { + return message; + } + + @Override + public String toString() { + return "Error in object " + id + ": " + message; + } +} diff --git a/modules/maps/src/maps/validate/ValidationException.java b/modules/maps/src/maps/validate/ValidationException.java new file mode 100644 index 0000000000000000000000000000000000000000..d3f3f3d999c1c1843d20f493eada0def3d740add --- /dev/null +++ b/modules/maps/src/maps/validate/ValidationException.java @@ -0,0 +1,29 @@ +package maps.validate; + +/** + * Exception that is raised when an error in a map is detected during + * validation. + * + */ +public class ValidationException extends Exception { + private ValidationError error; + + /** + * Create a new ValidationException. + * @param id The id of the GMLObject containing the error. + * @param message The error message. + */ + public ValidationException(int id, String message) { + super(message); + error = new ValidationError(id, message); + } + + /** + * Get the underlying ValidationError for this exception. + * @return The ValidationError causing this exception. + */ + public ValidationError getError() { + return error; + } + +} diff --git a/modules/misc/src/misc/DamageType.java b/modules/misc/src/misc/DamageType.java new file mode 100755 index 0000000000000000000000000000000000000000..140c1ebdc8bdff00cc434f7c2a68cbdb4bffaf6e --- /dev/null +++ b/modules/misc/src/misc/DamageType.java @@ -0,0 +1,94 @@ +package misc; + +import rescuecore2.config.Config; + +import java.util.Random; + +import org.uncommons.maths.random.GaussianGenerator; +import org.uncommons.maths.number.NumberGenerator; + +/** + Container for information about different damage types. + */ +/* + * Implementation of Refuge Bed Capacity + * @author Farshid Faraji + * May 2020 During Covid-19 :-))) + * */ +public class DamageType { + private String type; + private double k; + private double l; + private NumberGenerator noise; + + private double damage; + + /** + Construct a DamageType. + @param type The name of this type. + @param config The system configuration. + @param Random sequence proprietary. + */ + public DamageType(String type, Config config, Random random) { + this.type = type; + k = config.getFloatValue("misc.injury." + type + ".k"); + l = config.getFloatValue("misc.injury." + type + ".l"); + double mean = config.getFloatValue("misc.injury." + type + ".noise.mean"); + double sd = config.getFloatValue("misc.injury." + type + ".noise.sd"); + noise = new GaussianGenerator(mean, sd, random); + damage = 0; + } + + /** + Get the type name. + @return The type name. + */ + public String getType() { + return type; + } + + /** + Compute damage progression for this type. + @return The new damage. + */ + public double progress() { + if (damage <= 0) { + return damage; + } + double n = noise.nextValue(); + damage = damage + (k * damage * damage) + l + n; + return damage; + } + + public double progressInRefuge() { + if (damage <= 0) { + return damage; + } + double n = noise.nextValue(); + damage = damage - (k * damage * damage) - l - (2*n); + return damage; + } + /** + Get the current damage. + @return The current damage. + */ + public double getDamage() { + return damage; + } + + /** + Set the current damage. + @param d The current damage. + */ + public void setDamage(double d) { + damage = d; + } + + /** + Add some damage. + @param d The amount to add. + */ + public void addDamage(double d) { + damage += d; + } +} diff --git a/modules/misc/src/misc/HumanAttributes.java b/modules/misc/src/misc/HumanAttributes.java new file mode 100755 index 0000000000000000000000000000000000000000..71fb6cc58ff08c66d996aba77cba5fa088c735c9 --- /dev/null +++ b/modules/misc/src/misc/HumanAttributes.java @@ -0,0 +1,170 @@ +package misc; + +import rescuecore2.config.Config; +import rescuecore2.worldmodel.EntityID; + +import rescuecore2.standard.entities.Human; + +import java.util.Random; + +/** + Class for holding information about humans. + */ +/* + * Implementation of Refuge Bed Capacity + * @author Farshid Faraji + * May 2020 During Covid-19 :-))) + * */ +public class HumanAttributes { + private Human human; + private EntityID id; + private DamageType damageFire; + private DamageType damageCollapse; + private DamageType damageBury; + private Random random; + + /** + Construct a HumanAttributes object that wraps a Human. + @param h The Human to wrap. + @param config The system configuration. + */ + public HumanAttributes(Human h, Config config) { + this.human = h; + this.id = h.getID(); + // Generate Random for each Human + this.random = new Random(config.getRandom().nextLong()); + damageFire = new DamageType("fire", config, random); + damageCollapse = new DamageType("collapse", config, random); + damageBury = new DamageType("bury", config, random); + } + + /** + Get the ID of the wrapped human. + @return The human ID. + */ + public EntityID getID() { + return id; + } + + /** + Get the wrapped human. + @return The wrapped human. + */ + public Human getHuman() { + return human; + } + + /** + Get the random sequence of the wrapped human. + @return The random sequence. + */ + public Random getRandom(){ + return random; + } + /** + Add some collapse damage. + @param d The amount of damage to add. + */ + public void addCollapseDamage(double d) { + damageCollapse.addDamage(d); + } + + /** + Get the amount of collapse damage this human has. + @return The amount of collapse damage. + */ + public double getCollapseDamage() { + return damageCollapse.getDamage(); + } + + /** + Set the amount of collapse damage this human has. + @param d The new collapse damage. + */ + public void setCollapseDamage(double d) { + damageCollapse.setDamage(d); + } + + /** + Add some buriedness damage. + @param d The amount of damage to add. + */ + public void addBuriednessDamage(double d) { + damageBury.addDamage(d); + } + + /** + Get the amount of buriedness damage this human has. + @return The amount of buriedness damage. + */ + public double getBuriednessDamage() { + return damageBury.getDamage(); + } + + /** + Set the amount of buriedness damage this human has. + @param d The new buriedness damage. + */ + public void setBuriednessDamage(double d) { + damageBury.setDamage(d); + } + + /** + Add some fire damage. + @param d The amount of damage to add. + */ + public void addFireDamage(double d) { + damageFire.addDamage(d); + } + + /** + Get the amount of fire damage this human has. + @return The amount of fire damage. + */ + public double getFireDamage() { + return damageFire.getDamage(); + } + + /** + Set the amount of fire damage this human has. + @param d The new fire damage. + */ + public void setFireDamage(double d) { + damageFire.setDamage(d); + } + + /** + Get the total damage of this human, rounded to the nearest integer. + @return The total damage. + */ + public int getTotalDamage() { + return (int)Math.round(damageCollapse.getDamage() + damageFire.getDamage() + damageBury.getDamage()); + } + + /** + Progress all damage types. + */ + public void progressDamage() { + damageCollapse.progress(); + damageFire.progress(); + damageBury.progress(); + } + + public void progressDamageInRefuge() + { + //int damage = getTotalDamage(); + damageCollapse.progressInRefuge(); + damageFire.progressInRefuge(); + damageBury.progressInRefuge(); + } + + /** + Clear all damage. + */ + public void clearDamage() { + damageCollapse.setDamage(0); + damageBury.setDamage(0); + damageFire.setDamage(0); + } +} + diff --git a/modules/misc/src/misc/MiscParameters.java b/modules/misc/src/misc/MiscParameters.java new file mode 100644 index 0000000000000000000000000000000000000000..ae9260e9a915191cbe5b1c8270a2bdf2c6e51510 --- /dev/null +++ b/modules/misc/src/misc/MiscParameters.java @@ -0,0 +1,277 @@ +package misc; + +import java.util.Random; +import java.util.Map; +import java.util.EnumMap; + +import rescuecore2.config.Config; + +import rescuecore2.standard.entities.Building; +import rescuecore2.standard.entities.Human; +import rescuecore2.standard.entities.AmbulanceTeam; +import rescuecore2.standard.entities.FireBrigade; +import rescuecore2.standard.entities.PoliceForce; +import rescuecore2.standard.entities.Refuge; + +/** + * Container for all misc simulator parameters. + * @author Maitreyi Nanjanath + * @author Cameron Skinner + */ +public class MiscParameters { + private Config config; + + // All building classes indexed by building code and degree of collapse. The BuildingClass class knows about buriedness rates, injury rates etc. + private Map> buildingClasses; + + private DamageType collapseDamage; + private DamageType buryDamage; + private DamageType fireDamage; + + /** + Create a new MiscParameters object based on a Config. + @param config The Config to read. + */ + public MiscParameters(Config config) { + this.config = config; + initBuildingData(); + initInjuryData(); + } + + /** + Find out if an agent inside a building should be buried due to collapse. + @param b The building to check. + @param hA Human Attribute for calc should bury. + @return True if an agent inside the building should be buried, false otherwise. + */ + public boolean shouldBuryAgent(Building b, HumanAttributes hA) { + if (!b.isBuildingCodeDefined() || !b.isBrokennessDefined() || b.getBrokenness() == 0||b instanceof Refuge) { + return false; + } + BuildingClass clazz = getBuildingClass(b); + return clazz.shouldBury(hA); + } + + /** + Get the buriedness of an agent inside a building. + @param b The building to check. + @return The buriedness of an agent inside the building. + */ + public int getBuriedness(Building b) { + if (!b.isBuildingCodeDefined() || !b.isBrokennessDefined() || b.getBrokenness() == 0) { + return 0; + } + BuildingClass clazz = getBuildingClass(b); + return clazz.getAgentBuriedness(); + } + + /** + Get the amount of damage an agent should take as a result of being in a collapsing building. + @param b The building. + @param hA The HumanAttributes. + @return The amount of damage to add to the agent. + */ + public int getCollapseDamage(Building b, HumanAttributes hA) { + if (!b.isBuildingCodeDefined() || !b.isBrokennessDefined() || b.getBrokenness() == 0) { + return 0; + } + BuildingClass clazz = getBuildingClass(b); + Injury injury = clazz.getCollapseInjury(hA.getRandom()); + return collapseDamage.getDamage(injury, hA.getHuman()); + } + + /** + Get the amount of damage an agent should take as a result of being buried in a collapsed building. + @param b The building. + @param hA The HumanAttributes. + @return The amount of damage to add to the agent. + */ + public int getBuryDamage(Building b, HumanAttributes hA) { + if (!b.isBuildingCodeDefined() || !b.isBrokennessDefined() || b.getBrokenness() == 0) { + return 0; + } + BuildingClass clazz = getBuildingClass(b); + Injury injury = clazz.getBuryInjury(hA.getRandom()); + return buryDamage.getDamage(injury, hA.getHuman()); + } + + /** + Get the amount of damage an agent should take as a result of being in a burning building. + @param b The building. + @param hA The HumanAttributes. + @return The amount of damage to add to the agent. + */ + public int getFireDamage(Building b, HumanAttributes hA) { + if (!b.isBuildingCodeDefined()) { + return 0; + } + BuildingClass clazz = getBuildingClass(b); + Injury injury = clazz.getFireInjury(hA.getRandom()); + return fireDamage.getDamage(injury, hA.getHuman()); + } + + private void initBuildingData() { + buildingClasses = new EnumMap>(BuildingCode.class); + for (BuildingCode code : BuildingCode.values()) { + Map codeMap = new EnumMap(BrokennessDegree.class); + for (BrokennessDegree degree : BrokennessDegree.values()) { + codeMap.put(degree, new BuildingClass(config, code, degree)); + } + buildingClasses.put(code, codeMap); + } + } + + private void initInjuryData() { + collapseDamage = new DamageType(config, "collapse"); + buryDamage = new DamageType(config, "bury"); + fireDamage = new DamageType(config, "fire"); + } + + private BuildingClass getBuildingClass(Building b) { + BuildingCode code = BuildingCode.values()[b.getBuildingCode()]; + BrokennessDegree degree = BrokennessDegree.getBrokennessDegree(b); + return buildingClasses.get(code).get(degree); + } + + private class BuildingClass { + private double buriedProbability; + private int initialBuriedness; + private Map collapseInjuryProbability; + private Map buryInjuryProbability; + private Map fireInjuryProbability; + + public BuildingClass(Config config, BuildingCode code, BrokennessDegree degree) { + buriedProbability = config.getFloatValue("misc.buriedness." + code + "." + degree + ".rate"); + initialBuriedness = config.getIntValue("misc.buriedness." + code + "." + degree + ".value"); + collapseInjuryProbability = new EnumMap(Injury.class); + buryInjuryProbability = new EnumMap(Injury.class); + fireInjuryProbability = new EnumMap(Injury.class); + collapseInjuryProbability.put(Injury.SLIGHT, config.getFloatValue("misc.injury.collapse." + code + "." + degree + ".slight")); + collapseInjuryProbability.put(Injury.SERIOUS, config.getFloatValue("misc.injury.collapse." + code + "." + degree + ".serious")); + collapseInjuryProbability.put(Injury.CRITICAL, config.getFloatValue("misc.injury.collapse." + code + "." + degree + ".critical")); + buryInjuryProbability.put(Injury.SLIGHT, config.getFloatValue("misc.injury.bury." + code + "." + degree + ".slight")); + buryInjuryProbability.put(Injury.SERIOUS, config.getFloatValue("misc.injury.bury." + code + "." + degree + ".serious")); + buryInjuryProbability.put(Injury.CRITICAL, config.getFloatValue("misc.injury.bury." + code + "." + degree + ".critical")); + fireInjuryProbability.put(Injury.SLIGHT, config.getFloatValue("misc.injury.fire." + code + "." + degree + ".slight")); + fireInjuryProbability.put(Injury.SERIOUS, config.getFloatValue("misc.injury.fire." + code + "." + degree + ".serious")); + fireInjuryProbability.put(Injury.CRITICAL, config.getFloatValue("misc.injury.fire." + code + "." + degree + ".critical")); + } + + public int getAgentBuriedness() { + return initialBuriedness ;//TODO Genrate Random; + } + + public boolean shouldBury(HumanAttributes hA) { + return hA.getRandom().nextDouble() < buriedProbability; + } + + public Injury getCollapseInjury(Random random) { + return getInjury(collapseInjuryProbability, random); + } + + public Injury getBuryInjury(Random random) { + return getInjury(buryInjuryProbability, random); + } + + public Injury getFireInjury(Random random) { + return getInjury(fireInjuryProbability, random); + } + + private Injury getInjury(Map table, Random random) { + double d = random.nextDouble(); + + double d1 = table.get(Injury.SLIGHT); + double d2 = table.get(Injury.SERIOUS) + d1; + double d3 = table.get(Injury.CRITICAL) + d2; + if (d < d1) { + return Injury.SLIGHT; + } + if (d < d2) { + return Injury.SERIOUS; + } + if (d < d3) { + return Injury.CRITICAL; + } + return Injury.NONE; + } + } + + private enum BuildingCode { + WOOD, + STEEL, + CONCRETE; + + public String toString() { + return super.toString().toLowerCase(); + } + } + + private enum BrokennessDegree { + NONE(0, 0), + PARTIAL(1, 25), + HALF(26, 50), + ALL(51, 100); + + private int min; + private int max; + + private BrokennessDegree(int min, int max) { + this.min = min; + this.max = max; + } + + @Override + public String toString() { + return super.toString().toLowerCase(); + } + + public static BrokennessDegree getBrokennessDegree(Building b) { + int brokenness = b.isBrokennessDefined() ? b.getBrokenness() : 0; + for (BrokennessDegree next : BrokennessDegree.values()) { + if (brokenness >= next.min && brokenness <= next.max) { + return next; + } + } + return BrokennessDegree.NONE; + } + } + + private enum Injury { + NONE, + SLIGHT, + SERIOUS, + CRITICAL; + } + + private static class DamageType { + private Map damage; + private double ambulanceMultiplier; + private double policeMultiplier; + private double fireMultiplier; + + public DamageType(Config config, String type) { + damage = new EnumMap(Injury.class); + damage.put(Injury.NONE, 0); + damage.put(Injury.SLIGHT, config.getIntValue("misc.injury." + type + ".slight")); + damage.put(Injury.SERIOUS, config.getIntValue("misc.injury." + type + ".serious")); + damage.put(Injury.CRITICAL, config.getIntValue("misc.injury." + type + ".critical")); + ambulanceMultiplier = config.getFloatValue("misc.injury." + type + ".multiplier.ambulance"); + policeMultiplier = config.getFloatValue("misc.injury." + type + ".multiplier.police"); + fireMultiplier = config.getFloatValue("misc.injury." + type + ".multiplier.fire"); + } + + public int getDamage(Injury injury, Human agent) { + int result = damage.get(injury); + if (agent instanceof AmbulanceTeam) { + return (int)(result * ambulanceMultiplier); + } + if (agent instanceof PoliceForce) { + return (int)(result * policeMultiplier); + } + if (agent instanceof FireBrigade) { + return (int)(result * fireMultiplier); + } + return result; + } + } +} diff --git a/modules/misc/src/misc/MiscSimulator.java b/modules/misc/src/misc/MiscSimulator.java new file mode 100755 index 0000000000000000000000000000000000000000..d0d7c95337a62dd7270a8e52c5bc90b6eaa4ef9e --- /dev/null +++ b/modules/misc/src/misc/MiscSimulator.java @@ -0,0 +1,666 @@ +package misc; + +import java.util.ArrayList; +import java.util.Deque; +import java.util.Formatter; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.Map; +import java.util.Set; + +import javax.swing.JComponent; + +import rescuecore2.GUIComponent; +import rescuecore2.log.Logger; +import rescuecore2.messages.Command; +import rescuecore2.messages.control.KSCommands; +import rescuecore2.messages.control.KSUpdate; +import rescuecore2.misc.geometry.GeometryTools2D; +import rescuecore2.misc.geometry.Point2D; +import rescuecore2.standard.components.StandardSimulator; +import rescuecore2.standard.entities.AmbulanceTeam; +import rescuecore2.standard.entities.Building; +import rescuecore2.standard.entities.Civilian; +import rescuecore2.standard.entities.FireBrigade; +import rescuecore2.standard.entities.GasStation; +import rescuecore2.standard.entities.Human; +import rescuecore2.standard.entities.PoliceForce; +import rescuecore2.standard.entities.Refuge; +import rescuecore2.standard.entities.StandardEntity; +import rescuecore2.standard.entities.StandardEntityURN; +import rescuecore2.standard.entities.StandardPropertyURN; +import rescuecore2.standard.messages.AKLoad; +import rescuecore2.standard.messages.AKRescue; +import rescuecore2.standard.messages.AKUnload; +import rescuecore2.worldmodel.ChangeSet; +import rescuecore2.worldmodel.Entity; +import rescuecore2.worldmodel.EntityID; +import rescuecore2.worldmodel.EntityListener; +import rescuecore2.worldmodel.Property; + +/** + * Implementation of the legacy misc simulator. + * + * @author Maitreyi Nanjanath + * @author Cameron Skinner + * + * Implementation of Refuge Bed Capacity + * @author Farshid Faraji + */ +public class MiscSimulator extends StandardSimulator implements GUIComponent { + + private Map humans; + private Set newlyBrokenBuildings; + private Map oldBrokenBuildingsBuriedness = new HashMap<>(); + private MiscParameters parameters; + private MiscSimulatorGUI gui; + private int GAS_STATION_EXPLOSION_RANG; + private int GAS_STATION_Buriedness_Bound; + private int GAS_STATION_Buriedness_MIN; + private int GAS_STATION_Damage_Bound; + private int GAS_STATION_Damage_MIN; + + private Set notExplosedGasStations; + private Map> waitingList; + private Map> beds; + + @Override + public JComponent getGUIComponent() { + if (gui == null) { + gui = new MiscSimulatorGUI(); + } + return gui; + } + + @Override + public String getGUIComponentName() { + return "Misc simulator"; + } + + @Override + protected void postConnect() { + super.postConnect(); + notExplosedGasStations = new HashSet<>(); + waitingList = new HashMap>(); + beds = new HashMap>(); + + parameters = new MiscParameters(config); + GAS_STATION_EXPLOSION_RANG = config.getIntValue("ignition.gas_station.explosion.range", 0); + GAS_STATION_Buriedness_Bound = config.getIntValue("misc.gas_station.Buriedness.bound", 30); + GAS_STATION_Buriedness_MIN = config.getIntValue("misc.gas_station.Buriedness.min", 0); + GAS_STATION_Damage_Bound = config.getIntValue("misc.gas_station.Damage.bound", 50); + GAS_STATION_Damage_MIN = config.getIntValue("misc.gas_station.Damage.min", 15); + + humans = new HashMap(); + newlyBrokenBuildings = new HashSet(); + Logger.info("MiscSimulator connected. World has " + model.getAllEntities().size() + " entities."); + BuildingChangeListener buildingListener = new BuildingChangeListener(); + // HumanChangeListener humanListener = new HumanChangeListener(); + for (Entity et : model.getAllEntities()) { + if (et instanceof GasStation) { + notExplosedGasStations.add(et.getID()); + } + if (et instanceof Refuge) { + Deque wlist = new LinkedList(); + waitingList.put(et.getID(), wlist); + + Deque blist = new LinkedList(); + beds.put(et.getID(), blist); + } + if (et instanceof Building) { + et.addEntityListener(buildingListener); + } else if (et instanceof Human) { + // et.addEntityListener(humanListener); + Human human = (Human) et; + HumanAttributes ha = new HumanAttributes(human, config); + humans.put(ha.getID(), ha); + } + } + } + + @Override + protected void processCommands(KSCommands c, ChangeSet changes) { + long start = System.currentTimeMillis(); + int time = c.getTime(); + Logger.info("Timestep " + time); + + for (Command com : c.getCommands()) { + + if (checkValidity(com)) { + if (com instanceof AKRescue) { + Human human = (Human) (model.getEntity(((AKRescue) com).getTarget())); + handleRescue(human, changes); + } + /* + * For the implementation of Refuge Bed Capacity + **/ + if (com instanceof AKUnload) { + handleUnload(com, changes); + } + + if (com instanceof AKLoad) { + handleLoad((AKLoad) com, changes); + } + + } else { + Logger.debug("Ignoring " + com); + } + } + + updateRefuges(); + + processBrokenBuildings(changes); + // processBurningBuildings( changes ); + // processExplodedGasStations( changes ); + updateDamage(changes); + + updateChangeSet(changes); + + // Clean up + newlyBrokenBuildings.clear(); + writeDebugOutput(c.getTime()); + if (gui != null) { + gui.refresh(humans.values()); + } + long end = System.currentTimeMillis(); + Logger.info("Timestep " + time + " took " + (end - start) + " ms"); + } + + private void processExplodedGasStations(ChangeSet changes) { + Logger.info("processExplodedGasStations for " + notExplosedGasStations); + for (Iterator iterator = notExplosedGasStations.iterator(); iterator.hasNext();) { + GasStation gasStation = (GasStation) model.getEntity(iterator.next()); + if (gasStation.isFierynessDefined() && gasStation.getFieryness() == 1) { + for (HumanAttributes hA : humans.values()) { + + Human human = hA.getHuman(); + if (!human.isXDefined() || !human.isYDefined()) + continue; + if (GeometryTools2D.getDistance(new Point2D(human.getX(), human.getY()), + new Point2D(gasStation.getX(), gasStation.getY())) < GAS_STATION_EXPLOSION_RANG) { + Logger.info(human + " getting damage from explosion..." + human); + int oldBuriedness = human.isBuriednessDefined() ? human.getBuriedness() : 0; + human.setBuriedness( + oldBuriedness + hA.getRandom().nextInt(GAS_STATION_Buriedness_Bound) + GAS_STATION_Buriedness_MIN); + changes.addChange(human, human.getBuriednessProperty()); + // Check for injury from being exploded + int damage = hA.getRandom().nextInt(GAS_STATION_Damage_Bound) + GAS_STATION_Damage_MIN; + if (damage != 0) { + hA.addCollapseDamage(damage); + } + } + + } + + iterator.remove(); + } + } + + } + + private void processBrokenBuildings(ChangeSet changes) { + for (HumanAttributes hA : humans.values()) { + Human human = hA.getHuman(); + EntityID positionID = human.getPosition(); + if (!newlyBrokenBuildings.contains(positionID)) { + continue; + } + // Human is in a newly collapsed building + // Check for buriedness + Logger.trace("Checking if human should be buried in broken building"); + Building b = (Building) human.getPosition(model); + if (parameters.shouldBuryAgent(b, hA)) { + int buriedness = parameters.getBuriedness(b) - oldBrokenBuildingsBuriedness.get(b.getID()); + + if (buriedness != 0) { + int oldBuriedness = human.isBuriednessDefined() ? human.getBuriedness() : 0; + human.setBuriedness(oldBuriedness + buriedness); + changes.addChange(human, human.getBuriednessProperty()); + // Check for injury from being buried + int damage = parameters.getBuryDamage(b, hA); + if (damage != 0) { + hA.addBuriednessDamage(damage); + } + } + } + // Now check for injury from the collapse + int damage = parameters.getCollapseDamage(b, hA); + if (damage != 0) { + hA.addCollapseDamage(damage); + } + } + } + + private void processBurningBuildings(ChangeSet changes) { + for (HumanAttributes hA : humans.values()) { + Human human = hA.getHuman(); + Entity position = human.getPosition(model); + if (position instanceof Building && ((Building) position).isOnFire()) { + // Human is in a burning building + int damage = parameters.getFireDamage((Building) position, hA); + if (damage != 0) { + hA.addFireDamage(damage); + } + } + } + } + + private void writeDebugOutput(int time) { + StringBuilder builder = new StringBuilder(); + Formatter format = new Formatter(builder); + format.format("Agents damaged or buried at timestep %1d%n", time); + format.format(" ID | HP | Damage | Bury | Collapse | Fire | Buriedness%n"); + for (HumanAttributes ha : humans.values()) { + Human h = ha.getHuman(); + int hp = h.isHPDefined() ? h.getHP() : 0; + int damage = ha.getTotalDamage(); + int buriedness = h.isBuriednessDefined() ? h.getBuriedness() : 0; + boolean isAlive = hp > 0; + boolean hasDamage = damage > 0; + boolean isBuried = buriedness > 0; + if ((hasDamage || isBuried) && isAlive) { + format.format("%1$9d | %2$6d | %3$6d | %4$8.3f | %5$8.3f | %6$8.3f | %7$6d%n", ha.getID().getValue(), hp, + damage, ha.getBuriednessDamage(), ha.getCollapseDamage(), ha.getFireDamage(), buriedness); + } + } + format.close(); + Logger.debug(builder.toString()); + } + + private void updateDamage(ChangeSet changes) { + for (HumanAttributes ha : humans.values()) { + Human h = ha.getHuman(); + int oldDamage = ha.getTotalDamage(); + if (h.isPositionDefined() && !(h.getPosition(model) instanceof Refuge)) { + updateDamage(ha); + int hp = h.isHPDefined() ? h.getHP() : 0; + int damage = ha.getTotalDamage(); + + h.setDamage(damage); + changes.addChange(ha.getHuman(), ha.getHuman().getDamageProperty()); + + // Update HP + boolean isAlive = hp > 0; + boolean hasDamage = damage > 0; + + if (isAlive && hasDamage) { + int newHP = Math.max(0, hp - damage); + h.setHP(newHP); + changes.addChange(ha.getHuman(), ha.getHuman().getHPProperty()); + } + } + /* + * For the implementation of Refuge Bed Capacity Damage increases and HP + * decreases while victim is in waiting list in Refuge While victim is on the + * bed, Damage is reducing but HP is fix human will not die while on the bed but + * it takes time to get damage to 0 + */ + + else if (h.isPositionDefined() && (h.getPosition(model) instanceof Refuge) && h.isHPDefined() && h.getHP() > 0) { + if (h instanceof FireBrigade || h instanceof AmbulanceTeam || h instanceof PoliceForce) { + ha.clearDamage(); + h.setDamage(0); + changes.addChange(ha.getHuman(), ha.getHuman().getDamageProperty()); + continue; + } + if (waitingList.get(h.getPosition()).size() > 0 && waitingList.get(h.getPosition()).contains(h.getID())) { + updateDamage(ha); + int hp = h.isHPDefined() ? h.getHP() : 0; + int damage = ha.getTotalDamage(); + + h.setDamage(damage); + changes.addChange(ha.getHuman(), ha.getHuman().getDamageProperty()); + + // Update HP + boolean isAlive = hp > 0; + boolean hasDamage = damage > 0; + + if (isAlive && hasDamage) { + int newHP = Math.max(0, hp - damage); + h.setHP(newHP); + changes.addChange(ha.getHuman(), ha.getHuman().getHPProperty()); + } + } else if (beds.get(h.getPosition()).size() > 0 && beds.get(h.getPosition()).contains(h.getID())) { + updateDamageInRefuge(ha); + h.setDamage(ha.getTotalDamage()); + changes.addChange(ha.getHuman(), ha.getHuman().getDamageProperty()); + + if (oldDamage > 0 && h.getDamage() <= 0) { + + if (beds.get(h.getPosition()).remove(h.getID())) { + ((Refuge) h.getPosition(model)).decreaseOccupiedBeds(); + } + + if (waitingList.get(h.getPosition()).size() > 0) { + beds.get(h.getPosition()).add(waitingList.get(h.getPosition()).remove()); + ((Refuge) h.getPosition(model)).increaseOccupiedBeds(); + } + } + } + } + } + } + + private void updateDamage(HumanAttributes ha) { + Human h = ha.getHuman(); + if (h.getHP() <= 0) { + return; // Agent is already dead. + } + ha.progressDamage(); + } + + /* + * For the implementation of Refuge Bed Capacity + **/ + private void updateDamageInRefuge(HumanAttributes ha) { + Human h = ha.getHuman(); + if (h.getHP() <= 0) { + return; // Agent is already dead. + } + ha.progressDamageInRefuge(); + } + + private boolean checkValidity(Command command) { + Entity e = model.getEntity(command.getAgentID()); + if (e == null) { + Logger.warn("Received a " + command.getURN() + " command from an unknown agent: " + command.getAgentID()); + return false; + } + if (command instanceof AKRescue) { + return checkRescue((AKRescue) command, e); + } + if (command instanceof AKUnload) + return checkUnload((AKUnload) command, e); + + if (command instanceof AKLoad) + return checkLoad((AKLoad) command, e); + + return false; + } + + private boolean checkRescue(AKRescue rescue, Entity agent) { + EntityID targetID = rescue.getTarget(); + Entity target = model.getEntity(targetID); + // || agent instanceof AmbulanceTeam + if (!(agent instanceof FireBrigade)) { + Logger.warn("Rejecting rescue command from agent " + agent.getID() + " who is of type " + agent.getURN()); + return false; + } + if (target == null) { + Logger.warn("Rejecting rescue command from agent " + agent.getID() + " for a non-existent target " + targetID); + return false; + } + if (!(target instanceof Human)) { + Logger.warn("Rejecting rescue command from agent " + agent.getID() + " for a non-human target: " + targetID + + " is of type " + target.getURN()); + return false; + } + Human h = (Human) target; + if (!h.isBuriednessDefined() || h.getBuriedness() == 0) { + Logger.warn("Rejecting rescue command from agent " + agent.getID() + " for a non-buried target " + targetID); + return false; + } + + // || agent instanceof AmbulanceTeam + if (agent instanceof FireBrigade) { + Human ag = (Human) agent; + if (ag.isHPDefined() && ag.getHP() <= 0) { + Logger.warn("Rejecting rescue command from agent " + agent.getID() + ": agent is dead"); + return false; + } + if (ag.isBuriednessDefined() && ag.getBuriedness() > 0) { + Logger.warn("Rejecting rescue command from agent " + agent.getID() + ": agent is buried"); + return false; + } + if (!h.isPositionDefined() || !ag.isPositionDefined() || !h.getPosition().equals(ag.getPosition())) { + Logger.warn("Rejecting rescue command from agent " + agent.getID() + " for a non-adjacent target " + targetID); + return false; + } + if (h.getID().equals(ag.getID())) { + Logger.warn("Rejecting rescue command from agent " + agent.getID() + ": tried to rescue self"); + return false; + } + } + return true; + } + + private boolean checkUnload(AKUnload unload, Entity agent) { + if (!(agent instanceof AmbulanceTeam)) { + Logger.warn("Rejecting unload command from agent " + agent.getID() + " who is of type " + agent.getURN()); + return false; + } + + AmbulanceTeam at = (AmbulanceTeam) agent; + if (at.isHPDefined() && at.getHP() <= 0) { + Logger.warn("Rejecting Unload command from agent " + agent.getID() + ": agent is dead"); + return false; + } + + return true; + } + + private boolean checkLoad(AKLoad unload, Entity agent) { + if (!(agent instanceof AmbulanceTeam)) { + Logger.warn("Rejecting load command from agent " + agent.getID() + " who is of type " + agent.getURN()); + return false; + } + + AmbulanceTeam at = (AmbulanceTeam) agent; + if (at.isHPDefined() && at.getHP() <= 0) { + Logger.warn("Rejecting Unload command from agent " + agent.getID() + ": agent is dead"); + return false; + } + + return true; + } + + private void handleRescue(Human target, ChangeSet changes) { + target.setBuriedness(Math.max(0, target.getBuriedness() - 1)); + changes.addChange(target, target.getBuriednessProperty()); + } + + /* + * For the implementation of Refuge Bed Capacity + **/ + private void handleUnload(Command command, ChangeSet changes) { + EntityID agentID = command.getAgentID(); + Entity agent = model.getEntity(agentID); + Civilian target = null; + for (Entity e : model.getEntitiesOfType(StandardEntityURN.CIVILIAN)) { + Civilian civ = (Civilian) e; + if (civ.isPositionDefined() && agentID.equals(civ.getPosition())) { + target = civ; + break; + } + } + if (target != null) { + Entity AgentPosition = ((Human) agent).getPosition(model); + if (AgentPosition != null && AgentPosition instanceof Refuge) { + addVictimToWaitingList(AgentPosition, target); + } + } + } + + private void handleLoad(AKLoad load, ChangeSet changes) { + EntityID agentID = load.getAgentID(); + Entity agent = model.getEntity(agentID); + + EntityID targetID = load.getTarget(); + Entity target = model.getEntity(targetID); + + // --------------- + if (agent == null) { + Logger.warn("Rejecting load command from agent " + agentID + ": agent does not exist"); + return; + } + if (!(agent instanceof AmbulanceTeam)) { + Logger.warn("Rejecting load command from agent " + agentID + ": agent type is " + agent.getURN()); + return; + } + if (target == null) { + Logger.warn("Rejecting load command from agent " + agentID + ": target does not exist " + targetID); + return; + } + + AmbulanceTeam at = (AmbulanceTeam) agent; + Civilian h = (Civilian) target; + + if (at.isHPDefined() && at.getHP() <= 0) { + Logger.warn("Rejecting load command from agent " + agentID + ": agent is dead"); + return; + } + + if (!h.isPositionDefined() || !at.isPositionDefined() || !h.getPosition().equals(at.getPosition())) { + Logger.warn("Rejecting load command from agent " + agentID + ": target is non-adjacent " + targetID); + return; + } + if (h.getID().equals(at.getID())) { + Logger.warn("Rejecting load command from agent " + agentID + ": tried to load self"); + return; + } + // Is there something already loaded? + for (Entity e : model.getEntitiesOfType(StandardEntityURN.CIVILIAN)) { + Civilian c = (Civilian) e; + if (c.isPositionDefined() && agentID.equals(c.getPosition())) { + Logger.warn( + "Rejecting load command from agent " + agentID + ": agent already has civilian " + c.getID() + " loaded"); + return; + } + } + // -------------------- + + Entity AgentPosition = ((Human) agent).getPosition(model); + if (AgentPosition != null && AgentPosition instanceof Refuge) { + + if (waitingList.get(h.getPosition()).size() > 0 && waitingList.get(h.getPosition()).contains(h.getID())) { + waitingList.get(h.getPosition()).remove(h.getID()); + } + + if (beds.get(h.getPosition()).size() > 0 && beds.get(h.getPosition()).contains(h.getID())) { + beds.get(h.getPosition()).remove(h.getID()); + ((Refuge) h.getPosition(model)).decreaseOccupiedBeds(); + + if (waitingList.get(h.getPosition()).size() > 0) { + beds.get(h.getPosition()).add(waitingList.get(h.getPosition()).remove()); + ((Refuge) h.getPosition(model)).increaseOccupiedBeds(); + } + } + + } + } + + private class BuildingChangeListener implements EntityListener { + + @Override + public void propertyChanged(Entity e, Property p, Object oldValue, Object newValue) { + if (!(e instanceof Building)) { + return; // we want to only look at buildings + } + + if (p.getURN() == (StandardPropertyURN.BROKENNESS.getURNId())) + checkBrokenness(e, oldValue, newValue); + + } + + private void checkBrokenness(Entity e, Object oldValue, Object newValue) { + double old = oldValue == null ? 0 : (Integer) oldValue; + double next = newValue == null ? 0 : (Integer) newValue; + if (next > old) { + newlyBrokenBuildings.add(e.getID()); + + } + } + + } + + @Override + protected void handleUpdate(KSUpdate u) { + for (StandardEntity entity : model) { + if (entity instanceof Building) { + oldBrokenBuildingsBuriedness.put(entity.getID(), parameters.getBuriedness((Building) entity)); + } + } + super.handleUpdate(u); + } + + private void addVictimToWaitingList(Entity refuge, Civilian victim) { + if (victim.getDamage() > 0) + waitingList.get(refuge.getID()).add(victim.getID()); + } + + /* + * For the implementation of Refuge Bed Capacity + **/ + private void updateRefuges() { + for (Map.Entry> e : waitingList.entrySet()) { + ArrayList tempList = new ArrayList(); + for (EntityID civ : (Deque) e.getValue()) { + if (model.getEntity(civ) instanceof Human) { + if (((Human) model.getEntity(civ)).getDamage() <= 0) { + tempList.add(civ); + } + if (((Human) model.getEntity(civ)).getHP() <= 0) { + tempList.add(civ); + } + } + } + if (tempList.size() > 0) { + ((Deque) e.getValue()).removeAll(tempList); + } + } + + for (Map.Entry> e : beds.entrySet()) { + ArrayList tempList = new ArrayList(); + for (EntityID civ : (Deque) e.getValue()) { + if (model.getEntity(civ) instanceof Human) { + if (((Human) model.getEntity(civ)).getDamage() <= 0) { + tempList.add(civ); + } + if (((Human) model.getEntity(civ)).getHP() <= 0) { + tempList.add(civ); + } + } + } + if (tempList.size() > 0) { + // ( (Deque) e.getValue() ).removeAll( tempList ); + for (EntityID id : tempList) { + if (((Deque) e.getValue()).remove(id)) { + ((Refuge) model.getEntity(e.getKey())).decreaseOccupiedBeds(); + Logger.warn("decreaseOccupiedBeds in update Refuge"); + } + } + + } + } + + for (StandardEntity e : model.getEntitiesOfType(StandardEntityURN.REFUGE)) { + if (e instanceof Refuge) { + // Logger.warn("Refuge = " + e.getID() + " bed cap = " + ( (Refuge) e + // ).getOccupiedBeds() + " beds size = " + beds.get(e.getID()).size() + " wait + // size = " + waitingList.get(e.getID()).size()); + while (((Refuge) e).getOccupiedBeds() < ((Refuge) e).getBedCapacity()) { + if (waitingList.get(e.getID()).size() > 0) { + beds.get(e.getID()).add(waitingList.get(e.getID()).remove()); + ((Refuge) e).increaseOccupiedBeds(); + } else + break; + } + } + } + } + + /* + * For the implementation of Refuge Bed Capacity + **/ + private void updateChangeSet(ChangeSet changes) { + for (StandardEntity e : model.getEntitiesOfType(StandardEntityURN.REFUGE)) + if (e instanceof Refuge) { + int size = waitingList.get(e.getID()).size(); + ((Refuge) e).setWaitingListSize(size); + changes.addChange((Refuge) e, ((Refuge) e).getOccupiedBedsProperty()); + changes.addChange((Refuge) e, ((Refuge) e).getWaitingListSizeProperty()); + } + } +} \ No newline at end of file diff --git a/modules/misc/src/misc/MiscSimulatorGUI.java b/modules/misc/src/misc/MiscSimulatorGUI.java new file mode 100644 index 0000000000000000000000000000000000000000..c77adc18259010a969ed27c0e5e52a76a4c9c2b8 --- /dev/null +++ b/modules/misc/src/misc/MiscSimulatorGUI.java @@ -0,0 +1,105 @@ +package misc; + +import javax.swing.JPanel; +import javax.swing.JTable; +import javax.swing.JScrollPane; +import javax.swing.table.AbstractTableModel; +import java.awt.BorderLayout; +import java.util.Collection; + +/** + A GUI component for viewing the misc simulator state. + */ +public class MiscSimulatorGUI extends JPanel { + private static final int COLUMN_ID = 0; + private static final int COLUMN_HP = 1; + private static final int COLUMN_DAMAGE = 2; + private static final int COLUMN_BURY = 3; + private static final int COLUMN_COLLAPSE = 4; + private static final int COLUMN_FIRE = 5; + private static final int COLUMN_BURIEDNESS = 6; + private static final int COLUMNS = 7; + + private MiscTableModel model; + + /** + Create a MiscSimulatorGUI. + */ + public MiscSimulatorGUI() { + super(new BorderLayout()); + model = new MiscTableModel(); + add(new JScrollPane(new JTable(model)), BorderLayout.CENTER); + } + + /** + Refresh the UI. + @param data The set of human data to show. + */ + public void refresh(Collection data) { + model.setData(data.toArray(new HumanAttributes[data.size()])); + } + + private static class MiscTableModel extends AbstractTableModel { + private HumanAttributes[] data; + + void setData(HumanAttributes[] data) { + this.data = data; + fireTableDataChanged(); + } + + @Override + public int getRowCount() { + return data == null ? 0 : data.length; + } + + @Override + public int getColumnCount() { + return COLUMNS; + } + + @Override + public Object getValueAt(int row, int col) { + HumanAttributes att = data[row]; + switch (col) { + case COLUMN_ID: + return att.getID(); + case COLUMN_HP: + return att.getHuman().isHPDefined() ? String.valueOf(att.getHuman().getHP()) : "undefined"; + case COLUMN_DAMAGE: + return att.getHuman().isDamageDefined() ? String.valueOf(att.getHuman().getDamage()) : "undefined"; + case COLUMN_BURY: + return att.getBuriednessDamage(); + case COLUMN_COLLAPSE: + return att.getCollapseDamage(); + case COLUMN_FIRE: + return att.getFireDamage(); + case COLUMN_BURIEDNESS: + return att.getHuman().isBuriednessDefined() ? String.valueOf(att.getHuman().getBuriedness()) : "undefined"; + default: + throw new IllegalArgumentException("Unrecognised column: " + col); + } + } + + @Override + public String getColumnName(int col) { + switch (col) { + case COLUMN_ID: + return "ID"; + case COLUMN_HP: + return "HP"; + case COLUMN_DAMAGE: + return "Total damage"; + case COLUMN_BURY: + return "Buriedness damage"; + case COLUMN_COLLAPSE: + return "Collapse damage"; + case COLUMN_FIRE: + return "Fire damage"; + case COLUMN_BURIEDNESS: + return "Buriedness"; + default: + throw new IllegalArgumentException("Unrecognised column: " + col); + } + } + } +} diff --git a/modules/oldsims/firesimulator/kernel/Kernel.java b/modules/oldsims/firesimulator/kernel/Kernel.java new file mode 100644 index 0000000000000000000000000000000000000000..dc5cba03bcb9de24938b635a81ca95c5fc80154d --- /dev/null +++ b/modules/oldsims/firesimulator/kernel/Kernel.java @@ -0,0 +1,25 @@ +package firesimulator.kernel; + +import firesimulator.simulator.Simulator; + + + +/** + * @author tn + * + */ +public interface Kernel { + + public void register(Simulator sim); + + public void establishConnection(); + + public void signalReadyness(); + + public boolean waitForNextCycle(); + + public void sendUpdate(); + + public void receiveUpdate(); + +} diff --git a/modules/oldsims/firesimulator/simulator/EnergyHistory.java b/modules/oldsims/firesimulator/simulator/EnergyHistory.java new file mode 100644 index 0000000000000000000000000000000000000000..754f3ba290f429d8b675ac7308f776d990053fa9 --- /dev/null +++ b/modules/oldsims/firesimulator/simulator/EnergyHistory.java @@ -0,0 +1,85 @@ +package firesimulator.simulator; + +import java.util.Map; +import java.util.HashMap; + +import firesimulator.world.World; +import firesimulator.world.Building; +import org.apache.log4j.Logger; + +public class EnergyHistory { + private static final Logger LOG = Logger.getLogger(EnergyHistory.class); + + private int time; + private Map initialEnergy; + private Map initialTemperature; + private Map burnEnergy; + private Map coolEnergy; + private Map exchangedWithAir; + private Map lostToRadiation; + private Map gainedByRadiation; + private Map finalEnergy; + private Map finalTemperature; + + public EnergyHistory(World world, int time) { + initialEnergy = new HashMap(); + initialTemperature = new HashMap(); + burnEnergy = new HashMap(); + coolEnergy = new HashMap(); + exchangedWithAir = new HashMap(); + lostToRadiation = new HashMap(); + gainedByRadiation = new HashMap(); + finalEnergy = new HashMap(); + finalTemperature = new HashMap(); + this.time = time; + for (Building next : world.getBuildings()) { + initialEnergy.put(next, next.getEnergy()); + initialTemperature.put(next, next.getTemperature()); + } + } + + public void registerBurn(Building b, double energy) { + burnEnergy.put(b, energy); + } + + public void registerCool(Building b, double energy) { + coolEnergy.put(b, energy); + } + + public void registerAir(Building b, double energy) { + exchangedWithAir.put(b, energy); + } + + public void registerRadiationLoss(Building b, double energy) { + lostToRadiation.put(b, energy); + } + + public void registerRadiationGain(Building b, double energy) { + double old = gainedByRadiation.containsKey(b) ? gainedByRadiation.get(b) : 0; + gainedByRadiation.put(b, old + energy); + } + + public void registerFinalEnergy(World world) { + for (Building next : world.getBuildings()) { + finalEnergy.put(next, next.getEnergy()); + finalTemperature.put(next, next.getTemperature()); + } + } + + public void logSummary() { + LOG.debug("Energy summary at time " + time); + for (Building next : initialEnergy.keySet()) { + boolean changed = burnEnergy.containsKey(next) || coolEnergy.containsKey(next) || exchangedWithAir.containsKey(next) || lostToRadiation.containsKey(next) || gainedByRadiation.containsKey(next); + if (changed && !initialEnergy.get(next).equals(finalEnergy.get(next))) { + LOG.debug("Building " + next.getID()); + LOG.debug(" Initial energy / temperature: " + initialEnergy.get(next) + " / " + initialTemperature.get(next)); + LOG.debug(" Burn energy : " + burnEnergy.get(next)); + LOG.debug(" Cool energy : " + coolEnergy.get(next)); + LOG.debug(" Exchanged with air : " + exchangedWithAir.get(next)); + LOG.debug(" Lost to radiation : " + lostToRadiation.get(next)); + LOG.debug(" Gained by radiation : " + gainedByRadiation.get(next)); + LOG.debug(" Final energy / temperature : " + finalEnergy.get(next) + " / " + finalTemperature.get(next)); + } + } + } +} \ No newline at end of file diff --git a/modules/oldsims/firesimulator/simulator/ExtinguishRequest.java b/modules/oldsims/firesimulator/simulator/ExtinguishRequest.java new file mode 100644 index 0000000000000000000000000000000000000000..79b86a8072819b0a1a83bcb90ab466220ca6e923 --- /dev/null +++ b/modules/oldsims/firesimulator/simulator/ExtinguishRequest.java @@ -0,0 +1,128 @@ +package firesimulator.simulator; + +import firesimulator.util.Configuration; +import firesimulator.world.Building; +import firesimulator.world.FireBrigade; + +import org.apache.log4j.Logger; + +/** + * @author tn + * + */ +public class ExtinguishRequest { + private static final Logger LOG = Logger.getLogger(ExtinguishRequest.class); + + public static final int REASON_OK=1; + public static final int REASON_OK_VIRTUAL=2; + public static final int REASON_FB_WAS_NULL=-1; + public static final int REASON_TO_MUCH_WATER=-2; + public static final int REASON_TANK_EMPTY=-3; + public static final int REASON_OUT_OF_RANGE=-4; + public static final int REASON_NEGATIVE_WATER=-5;//Add by Ali Modaresi To fix using kernel bug + + public static final String OK="passed all tests"; + public static final String OK_VIRT="is virtual mode"; + public static final String ER_FB_NULL="firebrigade is null"; + public static final String ER_TO_MUCH="firebrigade is exceeding limt"; + public static final String ER_EMPTY="tank is empty"; + public static final String ER_RANGE="target is out of range"; + public static final String ER_NEGATIVE_WATER="negative water request";//Add by Ali Modaresi To fix using kernel bug + public static final String UNKNOWN="unknown code "; + + public static int MAX_WATER_PER_CYCLE; + public static int MAX_DISTANCE; + private FireBrigade source; + private Building target; + private int quantity; + private static boolean DEBUG_VERBOSE=true; + + public ExtinguishRequest(FireBrigade source, Building target,int quantity){ + this.target=target; + this.source=source; + this.quantity=quantity; + } + + public void verbose(String msg){ + if(DEBUG_VERBOSE) + LOG.debug(msg); + } + + public int validate(){ + if(source==null && Configuration.isActive("virtual"))return REASON_OK_VIRTUAL; + if(source==null)return REASON_FB_WAS_NULL; + if(source.getWaterUsed()+quantity>MAX_WATER_PER_CYCLE){ + return REASON_TO_MUCH_WATER; + } + if(source.getWaterQuantity()MAX_DISTANCE) + return REASON_OUT_OF_RANGE; + + if(quantity<0){//Added by Ali Modaresi to fix using kernel bug + LOG.warn("Using kernel bug.... Extinguish with negative water");//Added by Ali Modaresi to fix using kernel bug + return REASON_NEGATIVE_WATER;//Added by Ali Modaresi to fix using kernel bug + } + return REASON_OK; + } + + public String getReason(int code){ + switch (code) { + case REASON_OK: + return OK; + case REASON_OK_VIRTUAL: + return OK_VIRT; + case REASON_FB_WAS_NULL: + return ER_FB_NULL; + case REASON_OUT_OF_RANGE: + return ER_RANGE; + case REASON_TANK_EMPTY: + return ER_EMPTY; + case REASON_TO_MUCH_WATER: + return ER_TO_MUCH; + case REASON_NEGATIVE_WATER://Added by Ali Modaresi to fix using kernel bug + return ER_NEGATIVE_WATER;//Added by Ali Modaresi to fix using kernel bug + default: + return UNKNOWN+code; + } + } + + + private double distance(FireBrigade source2, Building target2) { + double x=source2.getX()-target2.getX(); + double y=source2.getY()-target2.getY(); + return Math.sqrt((x*x)+(y*y)); + } + + public boolean execute(){ + verbose(toString()); + int result; + if((result=validate())<0){ + verbose("ERROR reason = "+getReason(result)+"\n"); + return false; + } + if(source!=null){ + source.addWaterUsed(quantity); + source.setWaterQuantity(source.getWaterQuantity()-quantity); + } + target.setWaterQuantity(target.getWaterQuantity()+quantity); + verbose("OK reason = "+getReason(result)+"\n"); + return true; + } + + public FireBrigade getSource() { + return source; + } + + public String toString(){ + String fb; + try{ + fb="fb="+source.getID(); + }catch (Exception e) { + fb="fb=null"; + } + return "extinguish request; "+fb+", target="+target.getID()+", quantity="+quantity+" -> "; + } + +} diff --git a/modules/oldsims/firesimulator/simulator/Monitor.java b/modules/oldsims/firesimulator/simulator/Monitor.java new file mode 100644 index 0000000000000000000000000000000000000000..412643484a9e73a61a41e580cb35c7114f2ddcf0 --- /dev/null +++ b/modules/oldsims/firesimulator/simulator/Monitor.java @@ -0,0 +1,14 @@ +package firesimulator.simulator; + +import firesimulator.world.World; + + +public interface Monitor { + + public void step(World world); + + public void reset(World world); + + public void done(World world); + +} diff --git a/modules/oldsims/firesimulator/simulator/Simulator.java b/modules/oldsims/firesimulator/simulator/Simulator.java new file mode 100644 index 0000000000000000000000000000000000000000..71f421f92b38ab601b5481bcafbf0a53b9f2e2ed --- /dev/null +++ b/modules/oldsims/firesimulator/simulator/Simulator.java @@ -0,0 +1,387 @@ +package firesimulator.simulator; + +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; +import java.util.Map; +import java.util.HashMap; +import firesimulator.util.Configuration; +import firesimulator.util.Rnd; +import firesimulator.world.Building; +import firesimulator.world.FireBrigade; +import firesimulator.world.World; +import org.apache.log4j.Logger; + +public class Simulator { + + private static final Logger LOG = Logger.getLogger( Simulator.class ); + + private World world; + + private WindShift windShift; + + public static float GAMMA = 0.5f; + + public static float AIR_TO_AIR_COEFFICIENT = 0.5f; + + public static float AIR_TO_BUILDING_COEFFICIENT = 45f; + + public static float WATER_COEFFICIENT = 0.5f; + + public static float ENERGY_LOSS = 0.9f; + + public static float WIND_DIRECTION = 0.9f; + + public static float WIND_RANDOM = 0f; + + public static int WIND_SPEED = 0; + + public static float RADIATION_COEFFICENT = 1.0f; + + public static float TIME_STEP_LENGTH = 1f; + + public static float WEIGHT_GRID = 0.2f; + + public static float AIR_CELL_HEAT_CAPACITY = 1f; + + private Set monitors; + + private static boolean verbose; + + private static Simulator me; + + private EnergyHistory energyHistory; + + + public Simulator( World world ) { + me = this; + monitors = new HashSet(); + verbose = true; + this.world = world; + windShift = null; + } + + + public static Simulator getSimulator() { + return me; + } + + + public void addMonitor( Monitor monitor ) { + monitors.add( monitor ); + } + + + public void removeMonitor( Monitor monitor ) { + monitors.remove( monitor ); + } + + + private void informStep() { + for ( Iterator i = monitors.iterator(); i.hasNext(); ) { + ((Monitor) i.next()).step( world ); + } + } + + + private void informDone() { + for ( Iterator i = monitors.iterator(); i.hasNext(); ) { + ((Monitor) i.next()).done( world ); + } + } + + + private void informReset() { + for ( Iterator i = monitors.iterator(); i.hasNext(); ) { + ((Monitor) i.next()).reset( world ); + } + } + + + public void step( int timestep ) { + energyHistory = new EnergyHistory( world, timestep ); + refill(); + executeExtinguishRequests(); + burn(); + cool(); + updateGrid(); + exchangeBuilding(); + // FIXED + cool(); + energyHistory.registerFinalEnergy( world ); + energyHistory.logSummary(); + } + + + private void cool() { + for ( Iterator i = world.getBuildings().iterator(); i.hasNext(); ) { + Building b = (Building) i.next(); + waterCooling( b ); + } + } + + + private void refill() { + for ( Iterator i = world.getFirebrigades().iterator(); i.hasNext(); ) { + FireBrigade fb = ((FireBrigade) i.next()); + if ( fb.refill() ) { + LOG.debug( "refilling fire brigade " + fb.getID() ); + } + } + } + + + private void executeExtinguishRequests() { + for ( Iterator i = world.getExtinguishIterator(); i.hasNext(); ) { + ExtinguishRequest er = (ExtinguishRequest) i.next(); + er.execute(); + } + world.clearExtinguishRequests(); + } + + + private void burn() { + for ( Building b : world.getBuildings() ) { + if ( b.getTemperature() >= b.getIgnitionPoint() && b.fuel > 0 + && b.isInflameable() ) { + float consumed = b.getConsum(); + if ( consumed > b.fuel ) { + consumed = b.fuel; + } + double oldFuel = b.fuel; + double oldEnergy = b.getEnergy(); + double oldTemp = b.getTemperature(); + b.setEnergy( b.getEnergy() + consumed ); + energyHistory.registerBurn( b, consumed ); + b.fuel -= consumed; + b.setPrevBurned( consumed ); + } else { + b.setPrevBurned( 0f ); + } + } + } + + + private void waterCooling( Building b ) { + double lWATER_COEFFICIENT = (b.getFieryness() > 0 && b.getFieryness() < 4 + ? WATER_COEFFICIENT + : WATER_COEFFICIENT * GAMMA); + boolean cond = false; + if ( b.getWaterQuantity() > 0 ) { + double oldEnergy = b.getEnergy(); + double oldTemp = b.getTemperature(); + double oldWater = b.getWaterQuantity(); + double dE = b.getTemperature() * b.getCapacity(); + if ( dE <= 0 ) { + return; + } + double effect = b.getWaterQuantity() * lWATER_COEFFICIENT; + int consumed = b.getWaterQuantity(); + if ( effect > dE ) { + cond = true; + double pc = 1 - ((effect - dE) / effect); + effect *= pc; + consumed *= pc; + } + b.setWaterQuantity( b.getWaterQuantity() - consumed ); + b.setEnergy( b.getEnergy() - effect ); + energyHistory.registerCool( b, effect ); + LOG.debug( "Building " + b.getID() + " water cooling" ); + LOG.debug( "Old energy: " + oldEnergy + ", old temperature: " + oldTemp + + ", old water: " + oldWater ); + LOG.debug( "Consumed " + consumed + " water: effect = " + effect ); + LOG.debug( "New energy: " + b.getEnergy() + ", new temperature: " + + b.getTemperature() + ", new water: " + b.getWaterQuantity() ); + } + } + + + private void exchangeBuilding() { + for ( Iterator i = world.getBuildings().iterator(); i.hasNext(); ) { + Building b = (Building) i.next(); + exchangeWithAir( b ); + } + double sumdt = 0; + Map radiation = new HashMap(); + for ( Iterator i = world.getBuildings().iterator(); i.hasNext(); ) { + Building b = (Building) i.next(); + double radEn = b.getRadiationEnergy(); + radiation.put( b, radEn ); + } + for ( Iterator i = world.getBuildings().iterator(); i.hasNext(); ) { + Building b = (Building) i.next(); + double radEn = radiation.get( b ); + Building[] bs = b.connectedBuilding; + float[] vs = b.connectedValues; + + for ( int c = 0; c < vs.length; c++ ) { + double oldEnergy = bs[c].getEnergy(); + double connectionValue = vs[c]; + double a = radEn * connectionValue; + double sum = oldEnergy + a; + bs[c].setEnergy( sum ); + energyHistory.registerRadiationGain( bs[c], a ); + } + b.setEnergy( b.getEnergy() - radEn ); + energyHistory.registerRadiationLoss( b, -radEn ); + } + } + + + private void exchangeWithAir( Building b ) { + // Give/take heat to/from air cells + double oldTemperature = b.getTemperature(); + double oldEnergy = b.getEnergy(); + double energyDelta = 0; + + for ( int[] nextCell : b.cells ) { + int cellX = nextCell[0]; + int cellY = nextCell[1]; + double cellCover = nextCell[2] / 100.0; + double cellTemp = world.getAirCellTemp( cellX, cellY ); + double dT = cellTemp - b.getTemperature(); + double energyTransferToBuilding = dT * AIR_TO_BUILDING_COEFFICIENT + * TIME_STEP_LENGTH * cellCover * world.SAMPLE_SIZE; + energyDelta += energyTransferToBuilding; + double newCellTemp = cellTemp - energyTransferToBuilding + / (AIR_CELL_HEAT_CAPACITY * world.SAMPLE_SIZE); + world.setAirCellTemp( cellX, cellY, newCellTemp ); + } + b.setEnergy( oldEnergy + energyDelta ); + energyHistory.registerAir( b, energyDelta ); + } + + + private void updateGrid() { + LOG.debug( "Updating air grid" ); + double[][] airtemp = world.getAirTemp(); + double[][] newairtemp = new double[airtemp.length][airtemp[0].length]; + for ( int x = 0; x < airtemp.length; x++ ) { + for ( int y = 0; y < airtemp[0].length; y++ ) { + double dt = (averageTemp( x, y ) - airtemp[x][y]); + double change = (dt * AIR_TO_AIR_COEFFICIENT * TIME_STEP_LENGTH); + newairtemp[x][y] = relTemp( airtemp[x][y] + change ); + if ( !(newairtemp[x][y] > -Double.MAX_VALUE + && newairtemp[x][y] < Double.MAX_VALUE) ) { + LOG.warn( "Value is not sensible: " + newairtemp[x][y] ); + newairtemp[x][y] = Double.MAX_VALUE * 0.75; + } + if ( newairtemp[x][y] == Double.NEGATIVE_INFINITY + || newairtemp[x][y] == Double.POSITIVE_INFINITY ) { + LOG.warn( "aha" ); + } + } + } + world.setAirTemp( newairtemp ); + // Disable on October 21, 2018 because the wind direction and speed was not + // correctly implemented. + // world.setAirTemp( getWindShift().shift( world.getAirTemp(), this ) ); + } + + + private double relTemp( double deltaT ) { + return Math.max( 0, deltaT * ENERGY_LOSS * TIME_STEP_LENGTH ); + } + + + private double averageTemp( int x, int y ) { + double rv = neighbourCellAverage( x, y ) / weightSummCells( x, y ); + return rv; + } + + + private double neighbourCellAverage( int x, int y ) { + double total = getTempAt( x + 1, y - 1 ); + total += getTempAt( x + 1, y ); + total += getTempAt( x + 1, y + 1 ); + total += getTempAt( x, y - 1 ); + total += getTempAt( x, y + 1 ); + total += getTempAt( x - 1, y - 1 ); + total += getTempAt( x - 1, y ); + total += getTempAt( x - 1, y + 1 ); + return total * WEIGHT_GRID; + } + + + private float weightSummCells( int x, int y ) { + return 8 * WEIGHT_GRID; + } + + + protected double getTempAt( int x, int y ) { + if ( x < 0 || y < 0 || x >= world.getAirTemp().length + || y >= world.getAirTemp()[0].length ) + return 0; + return world.getAirTemp()[x][y]; + } + + + public void setWind( float direction, float speed ) { + windShift = new WindShift( direction, speed, world.SAMPLE_SIZE ); + } + + + public WindShift getWindShift() { + if ( WIND_RANDOM > 0 && windShift != null ) { + float nd = (float) (windShift.direction + + windShift.direction * WIND_RANDOM * Rnd.get01()); + float ns = (float) (windShift.speed + + windShift.speed * WIND_RANDOM * Rnd.get01()); + setWind( nd, ns ); + } + if ( windShift == null || windShift.direction != WIND_DIRECTION + || windShift.speed != WIND_SPEED ) + setWind( WIND_DIRECTION, WIND_SPEED ); + return windShift; + } + + + private void loadVars() { + AIR_TO_BUILDING_COEFFICIENT = Float.parseFloat( + Configuration.getValue( "resq-fire.air_to_building_flow" ) ); + AIR_TO_AIR_COEFFICIENT = Float + .parseFloat( Configuration.getValue( "resq-fire.air_to_air_flow" ) ); + ENERGY_LOSS = Float + .parseFloat( Configuration.getValue( "resq-fire.energy_loss" ) ); + WATER_COEFFICIENT = Float.parseFloat( + Configuration.getValue( "resq-fire.water_thermal_capacity" ) ); + // Disable on October 21, 2018 because the wind direction and speed was not + // correctly implemented. + // WIND_SPEED = Integer + // .parseInt( Configuration.getValue( "resq-fire.wind_speed" ) ); + // WIND_DIRECTION = Float + // .parseFloat( Configuration.getValue( "resq-fire.wind_direction" ) ); + // WIND_RANDOM = Float + // .parseFloat( Configuration.getValue( "resq-fire.wind_random" ) ); + RADIATION_COEFFICENT = Float.parseFloat( + Configuration.getValue( "resq-fire.radiation_coefficient" ) ); + AIR_CELL_HEAT_CAPACITY = Float.parseFloat( + Configuration.getValue( "resq-fire.air_cell_heat_capacity" ) ); + ExtinguishRequest.MAX_WATER_PER_CYCLE = Integer.parseInt( + Configuration.getValue( "resq-fire.max_extinguish_power_sum" ) ); + ExtinguishRequest.MAX_DISTANCE = Integer + .parseInt( Configuration.getValue( "resq-fire.water_distance" ) ); + GAMMA = Float.parseFloat( Configuration.getValue( "resq-fire.gamma" ) ); + Rnd.setSeed( Long.parseLong( Configuration.getValue( "random.seed" ) ) ); + + } + + + public void initialize() { + try { + loadVars(); + } catch ( Exception e ) { + LOG.fatal( "invalid configuration, aborting", e ); + System.exit( -1 ); + } + + world.initialize(); + } + + + public void reset() { + loadVars(); + world.reset(); + informReset(); + } +} \ No newline at end of file diff --git a/modules/oldsims/firesimulator/simulator/WindShift.java b/modules/oldsims/firesimulator/simulator/WindShift.java new file mode 100644 index 0000000000000000000000000000000000000000..a27705ca19b56673984b922c9bc88f15bd18e293 --- /dev/null +++ b/modules/oldsims/firesimulator/simulator/WindShift.java @@ -0,0 +1,71 @@ +package firesimulator.simulator; + +/** + * @author Timo N�ssle + * + */ +public class WindShift { + + float speed; + + float direction; + + float directionDg; + + int[][] grid = new int[4][2]; + + float[] weights = new float[4]; + + + public WindShift( float direction, float speed, int gridSize ) { + this.speed = speed % gridSize; + directionDg = direction; + direction = direction % 360; + direction = (float) (direction / (360 / (2 * Math.PI))); + this.direction = direction; + float v_y = -((float) Math.cos( direction ) * speed); + float v_x = -((float) Math.sin( direction ) * speed); + float[][] points = new float[4][2]; + points[0][0] = v_x; + points[0][1] = v_y; + points[1][0] = v_x + gridSize; + points[1][1] = v_y; + points[2][0] = v_x + gridSize; + points[2][1] = v_y + gridSize; + points[3][0] = v_x; + points[3][1] = v_y + gridSize; + float areaTotal = gridSize * gridSize; + for ( int c = 0; c < 4; c++ ) { + float tx = grid[c][0] * gridSize; + float ty = grid[c][1] * gridSize; + float sx = points[0][0]; + float sy = points[0][1]; + float dx = gridSize - Math.abs( sx - tx ); + float dy = gridSize - Math.abs( sy - ty ); + float weight = (dx * dy) / areaTotal; + weights[c] = weight; + } + } + + + public float getDirection() { + return directionDg; + } + + + public double[][] shift( double[][] source, Simulator sim ) { + if ( speed == 0 ) + return source; + double[][] result = new double[source.length][source[0].length]; + for ( int x = 0; x < source.length; x++ ) + for ( int y = 0; y < source[0].length; y++ ) { + float temp = 0; + for ( int c = 0; c < 4; c++ ) { + temp += sim.getTempAt( x - grid[c][0], y - grid[c][1] ) * weights[c]; + System.out.println( weights[c] ); + } + result[x][y] = temp; + } + return result; + } +} \ No newline at end of file diff --git a/modules/oldsims/firesimulator/util/Configuration.java b/modules/oldsims/firesimulator/util/Configuration.java new file mode 100644 index 0000000000000000000000000000000000000000..70b1def77caa586215a80411edf86dfb50085770 --- /dev/null +++ b/modules/oldsims/firesimulator/util/Configuration.java @@ -0,0 +1,312 @@ +package firesimulator.util; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.IOException; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.List; +import java.util.LinkedList; +import java.util.ArrayList; +import java.util.Properties; +import java.util.StringTokenizer; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import firesimulator.world.Wall; + +import org.apache.log4j.Logger; + +public class Configuration { + private static final Logger LOG = Logger.getLogger(Configuration.class); + + private static final String PREFIX = "resq-fire."; + + + public class Prop{ + + private String name; + private String command; + private String description; + private String paramName; + private boolean paramReq; + private String value; + boolean active; + + public Prop(String name, String command, String description , String paramName, boolean paramReq,String def){ + this.name=name; + this.command=command; + this.description=description; + this.paramName=paramName; + this.paramReq=paramReq; + if(def!=null){ + value=def; + active=true; + }else{ + value=null; + active=false; + } + } + + public String getValue(){ + return value; + } + + public boolean isActive(){ + return active; + } + + public boolean validate(){ + if(paramReq) + if(value==null||value.length()==0)return false; + return true; + } + + public String getDescription(){ + if(description==null)return ""; + return "\n"+name+": "+command+" "+(paramName!=null?(!paramReq?"[":"")+"<"+paramName+">"+(!paramReq?"]":""):"")+"\n"+description+"\n"; + } + + } + + private static LinkedList Props =new LinkedList(); + private static final String CONFIG_TXT_PATH="."; + public static String VERSION="06.08.2005"; + + public void initialize(){ + Props.add(new Prop(PREFIX + "store","s","Stores the intial data from the kernel in the given file.","filename",true,null)); + Props.add(new Prop(PREFIX + "virtual","v","Use the virtual kernel instead of the rescue kernel.\nRequires a .scn file.","filename",true,null)); + Props.add(new Prop(PREFIX + "host","h","The host to connect to. Default host is localhost.","host",true,"localhost")); + Props.add(new Prop(PREFIX + "port","p","The port to connect to. Default port is 6000","port",true,"6000")); + Props.add(new Prop(PREFIX + "setup","stp","Uses the given setup file","filename",true,null)); + Props.add(new Prop(PREFIX + "csetup","cstp","Uses the given config.txt file","filename",true,null)); + Props.add(new Prop(PREFIX + "ray_rate","ray_rate","Number of emitted rays per mm while sampling. Default rate is "+Wall.RAY_RATE,"rate",true,Wall.RAY_RATE+"")); + Props.add(new Prop(PREFIX + "help","help","Prints this text and exits",null,false,null)); + //hidden parameters + Props.add(new Prop(PREFIX + "cell_size","cell_size",null,null,true,null)); + Props.add(new Prop(PREFIX + "max_ray_distance","max_ray_distance",null,null,true,null)); + Props.add(new Prop(PREFIX + "energy_loss","energy_loss",null,null,true,null)); + Props.add(new Prop(PREFIX + "air_to_air_flow","air_to_air_flow",null,null,true,null)); + Props.add(new Prop(PREFIX + "air_to_building_flow","air_to_building_flow",null,null,true,null)); + Props.add(new Prop(PREFIX + "air_cell_heat_capacity","air_cell_heat_capacity",null,null,true,null)); + Props.add(new Prop(PREFIX + "wooden_capacity","wooden_capacity",null,null,true,null)); + Props.add(new Prop(PREFIX + "wooden_energy","wooden_energy",null,null,true,null)); + Props.add(new Prop(PREFIX + "wooden_ignition","wooden_ignition",null,null,true,null)); + Props.add(new Prop(PREFIX + "wooden_burning","wooden_burning",null,null,true,null)); + Props.add(new Prop(PREFIX + "wooden_speed","wooden_speed",null,null,true,null)); + Props.add(new Prop(PREFIX + "steel_capacity","steel_capacity",null,null,true,null)); + Props.add(new Prop(PREFIX + "steel_energy","steel_energy",null,null,true,null)); + Props.add(new Prop(PREFIX + "steel_ignition","steel_ignition",null,null,true,null)); + Props.add(new Prop(PREFIX + "steel_burning","steel_burning",null,null,true,null)); + Props.add(new Prop(PREFIX + "steel_speed","steel_speed",null,null,true,null)); + Props.add(new Prop(PREFIX + "concrete_capacity","concrete_capacity",null,null,true,null)); + Props.add(new Prop(PREFIX + "concrete_energy","concrete_energy",null,null,true,null)); + Props.add(new Prop(PREFIX + "concrete_ignition","concrete_ignition",null,null,true,null)); + Props.add(new Prop(PREFIX + "concrete_burning","concrete_burning",null,null,true,null)); + Props.add(new Prop(PREFIX + "concrete_speed","concrete_speed",null,null,true,null)); + Props.add(new Prop(PREFIX + "max_extinguish_power_sum","max_extinguish_power_sum",null,null,true,null)); + Props.add(new Prop(PREFIX + "water_refill_rate","water_refill_rate",null,null,true,null)); + Props.add(new Prop(PREFIX + "water_hydrant_refill_rate","water_hydrant_refill_rate",null,null,true,null)); + Props.add(new Prop(PREFIX + "water_capacity","water_capacity",null,null,true,null)); + Props.add(new Prop(PREFIX + "water_thermal_capacity","water_thermal_capacity",null,null,true,null)); + Props.add(new Prop(PREFIX + "water_distance","water_distance",null,null,true,null)); + Props.add(new Prop(PREFIX + "radiation_coefficient","radiation_coefficient",null,null,true,null)); + Props.add(new Prop(PREFIX + "wind_speed","wind_speed",null,null,true,null)); + Props.add(new Prop(PREFIX + "wind_direction","wind_direction",null,null,true,null)); + Props.add(new Prop(PREFIX + "wind_random","wind_random",null,null,true,null)); + Props.add(new Prop(PREFIX + "randomseed","randomseed",null,null,true,null)); + Props.add(new Prop("random.seed","random.seed",null,null,true,null)); + Props.add(new Prop(PREFIX + "refuge_inflammable","refuge_inflammable",null,null,true,null)); + Props.add(new Prop(PREFIX + "fire_station_inflammable","firestation_inflammable",null,null,true,null)); + Props.add(new Prop(PREFIX + "police_office_inflammable","policeoffice_inflammable",null,null,true,null)); + Props.add(new Prop(PREFIX + "ambulance_center_inflammable","ambulancecenter_inflammable",null,null,true,null)); + Props.add(new Prop(PREFIX + "gamma","gamma",null,null,true,null)); + Props.add(new Prop(PREFIX + "rays.dir","rays",null,null,true,"rays")); + Props.add(new Prop(PREFIX + "burn-rate-average","burn-rate-average",null,null,true,"0.2")); + Props.add(new Prop(PREFIX + "burn-rate-variance","burn-rate-variance",null,null,true,"0")); + } + + public void parse(String cmdLine){ + StringTokenizer st=new StringTokenizer(cmdLine,"-"); + try{ + while(st.hasMoreTokens()){ + String tok=st.nextToken(); + int index=tok.indexOf(" "); + String cmd; + if(index==-1){ + cmd=tok.trim(); + if(cmd.length()==0)continue; + Prop p =propForCmd(cmd); + p.active=true; + }else{ + cmd=tok.substring(0,index).trim(); + if(cmd.length()==0)continue; + Prop p =propForCmd(cmd); + p.active=true; + p.value=tok.substring(index).trim(); + } + } + }catch(Exception e){printHelpAndExit();} + if(isActive("help")){ + printHelpAndExit(); + } + } + + private void printHelpAndExit(){ + System.out.println("ResQ Firesimulator"); + System.out.println(VERSION); + System.out.println("author: Timo N�ssle\nemail: nuessle@informatik.uni-freiburg.de\n"); + System.out.println("java Main [-