package collapse; import rescuecore2.config.Config; 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 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"; private static final int MAX_COLLAPSE = 100; private static final double REPAIR_COST_FACTOR = 0.000001; // Converts square mm to square m. private static final List<EntityID> EMPTY_ID_LIST = new ArrayList<EntityID>(0); private NumberGenerator<Double> destroyed; private NumberGenerator<Double> severe; private NumberGenerator<Double> moderate; private NumberGenerator<Double> slight; private boolean block; private double floorHeight; private NumberGenerator<Double> extent; private Map<StandardEntityConstants.BuildingCode, CollapseStats> stats; private CollapseSimulatorGUI gui; private Collection<Building> buildingCache; private Collection<Road> roadCache; @Override public JComponent getGUIComponent() { if (gui == null) { gui = new CollapseSimulatorGUI(); } return gui; } @Override public String getGUIComponentName() { return "Collapse simulator"; } @Override public String getName() { return "Basic collapse simulator"; } @Override protected void postConnect() { super.postConnect(); stats = new EnumMap<StandardEntityConstants.BuildingCode, CollapseStats>(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<Building>(); roadCache = new HashSet<Road>(); for (StandardEntity next : model) { if (next instanceof Building) { buildingCache.add((Building)next); } if (next instanceof Road) { roadCache.add((Road)next); } } model.addWorldModelListener(new WorldModelListener<StandardEntity>() { @Override public void entityAdded(WorldModel<? extends StandardEntity> model, StandardEntity e) { if (e instanceof Building) { buildingCache.add((Building)e); } if (e instanceof Road) { roadCache.add((Road)e); } } @Override public void entityRemoved(WorldModel<? extends StandardEntity> 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<Building> collapsed = doCollapse(changes, time); Map<Road, Collection<java.awt.geom.Area>> newBlock = doBlock(collapsed); // Create blockade objects Map<Road, Collection<Blockade>> blockades = createBlockadeObjects(newBlock); for (Map.Entry<Road, Collection<Blockade>> entry : blockades.entrySet()) { Road r = entry.getKey(); List<EntityID> existing = r.getBlockades(); List<EntityID> ids = new ArrayList<EntityID>(); 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<Building> doCollapse(ChangeSet changes, int time) { Collection<Building> result = new HashSet<Building>(); if (gui != null) { gui.startCollapse(buildingCache.size()); } if (time == 1) { 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; } private Map<Road, Collection<java.awt.geom.Area>> doBlock(Collection<Building> collapsed) { Map<Road, Collection<java.awt.geom.Area>> result = new LazyMap<Road, Collection<java.awt.geom.Area>>() { @Override public Collection<java.awt.geom.Area> createValue() { return new ArrayList<java.awt.geom.Area>(); } }; if (!block) { return result; } if (gui != null) { gui.startBlock(collapsed.size()); } for (Building b : collapsed) { createBlockages(b, result); if (gui != null) { gui.bumpBlock(); } } if (gui != null) { gui.endBlock(); } return result; } private Collection<Building> doEarthquakeCollapse(ChangeSet changes) { Map<StandardEntityConstants.BuildingCode, Map<CollapseDegree, Integer>> count = new EnumMap<StandardEntityConstants.BuildingCode, Map<CollapseDegree, Integer>>(StandardEntityConstants.BuildingCode.class); Map<StandardEntityConstants.BuildingCode, Integer> total = new EnumMap<StandardEntityConstants.BuildingCode, Integer>(StandardEntityConstants.BuildingCode.class); for (StandardEntityConstants.BuildingCode code : StandardEntityConstants.BuildingCode.values()) { Map<CollapseDegree, Integer> next = new EnumMap<CollapseDegree, Integer>(CollapseDegree.class); for (CollapseDegree cd : CollapseDegree.values()) { next.put(cd, 0); } count.put(code, next); total.put(code, 0); } Logger.debug("Collapsing buildings"); Collection<Building> result = new HashSet<Building>(); 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); 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<CollapseDegree, Integer> data = count.get(code); for (Map.Entry<CollapseDegree, Integer> entry : data.entrySet()) { Logger.info(" " + entry.getValue() + " " + entry.getKey().toString().toLowerCase()); } } return result; } private Collection<Building> doFireCollapse(ChangeSet changes) { Logger.debug("Checking fire damage"); Collection<Building> result = new HashSet<Building>(); for (Building b : buildingCache) { 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; } 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<Road, Collection<Blockade>> createBlockadeObjects(Map<Road, Collection<java.awt.geom.Area>> blocks) { Map<Road, Collection<Blockade>> result = new LazyMap<Road, Collection<Blockade>>() { @Override public Collection<Blockade> createValue() { return new ArrayList<Blockade>(); } }; int count = 0; for (Collection<java.awt.geom.Area> c : blocks.values()) { count += c.size(); } try { if (count != 0) { List<EntityID> newIDs = requestNewEntityIDs(count); Iterator<EntityID> it = newIDs.iterator(); Logger.debug("Creating new blockade objects"); for (Map.Entry<Road, Collection<java.awt.geom.Area>> 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<Road, Collection<java.awt.geom.Area>> roadBlockages) { Logger.debug("Creating blockages for " + b); double d = floorHeight * b.getFloors() * ((double)b.getBrokenness() / (double)MAX_COLLAPSE) * extent.nextValue(); // Place some blockages on surrounding roads List<java.awt.geom.Area> wallAreas = new ArrayList<java.awt.geom.Area>(); // 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(); for (StandardEntity e : model.getEntitiesOfType(StandardEntityURN.BLOCKADE)) { Blockade blockade = (Blockade)e; existing.add(blockadeToArea(blockade)); } // Intersect wall areas with roads Map<Road, Collection<java.awt.geom.Area>> blockadesForRoads = createRoadBlockades(fullArea, existing); // Add to roadBlockages for (Map.Entry<Road, Collection<java.awt.geom.Area>> entry : blockadesForRoads.entrySet()) { Road r = entry.getKey(); Collection<java.awt.geom.Area> c = entry.getValue(); roadBlockages.get(r).addAll(c); } } private void projectWall(Edge edge, Collection<java.awt.geom.Area> 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 first = wallLine.getOrigin(); Point2D second = wallLine.getEndPoint(); Point2D third = second.plus(offset); Point2D fourth = first.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()); 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(first.getX() - radius, first.getY() - radius, radius * 2, radius * 2); Ellipse2D ellipse2 = new Ellipse2D.Double(second.getX() - radius, second.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<Road, Collection<java.awt.geom.Area>> createRoadBlockades(java.awt.geom.Area buildingArea, java.awt.geom.Area existing) { Map<Road, Collection<java.awt.geom.Area>> result = new HashMap<Road, Collection<java.awt.geom.Area>>(); for (StandardEntity e : model.getEntitiesOfType(StandardEntityURN.ROAD)) { 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<java.awt.geom.Area> 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<Edge> 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<java.awt.geom.Area> fix(java.awt.geom.Area area) { List<java.awt.geom.Area> result = new ArrayList<java.awt.geom.Area>(); 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<Point2D> 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<Integer> apexes = new ArrayList<Integer>(); // 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); } } }