package rescuecore2.standard.kernel;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Map;
import kernel.Perception;
import kernel.AgentProxy;
import rescuecore2.worldmodel.Entity;
import rescuecore2.worldmodel.EntityID;
import rescuecore2.worldmodel.WorldModel;
import rescuecore2.worldmodel.ChangeSet;
import rescuecore2.worldmodel.properties.IntProperty;
import rescuecore2.config.Config;
import rescuecore2.view.RenderedObject;
import rescuecore2.view.ViewComponent;
import rescuecore2.view.ViewListener;
import rescuecore2.misc.Pair;
import rescuecore2.misc.collections.LazyMap;
import rescuecore2.misc.gui.ScreenTransform;
import rescuecore2.misc.geometry.Point2D;
import rescuecore2.misc.geometry.Line2D;
import rescuecore2.misc.geometry.Vector2D;
import rescuecore2.misc.geometry.GeometryTools2D;
import rescuecore2.log.Logger;
import rescuecore2.GUIComponent;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.BorderLayout;
import javax.swing.JComponent;
import javax.swing.JPanel;
import rescuecore2.standard.entities.StandardWorldModel;
import rescuecore2.standard.entities.StandardEntity;
import rescuecore2.standard.entities.Road;
import rescuecore2.standard.entities.Area;
import rescuecore2.standard.entities.Edge;
import rescuecore2.standard.entities.Blockade;
import rescuecore2.standard.entities.Building;
import rescuecore2.standard.entities.Human;
import rescuecore2.standard.entities.AmbulanceTeam;
import rescuecore2.standard.entities.FireBrigade;
import rescuecore2.standard.entities.StandardEntityURN;
import rescuecore2.standard.view.StandardWorldModelViewer;
import rescuecore2.standard.view.StandardViewLayer;
import rescuecore2.standard.view.BuildingLayer;
import rescuecore2.standard.view.RoadLayer;
import rescuecore2.standard.view.RoadBlockageLayer;
import rescuecore2.standard.view.HumanLayer;
/**
Line of sight perception.
*/
public class LineOfSightPerception implements Perception, GUIComponent {
private static final int DEFAULT_VIEW_DISTANCE = 30000;
private static final int DEFAULT_HP_PRECISION = 1000;
private static final int DEFAULT_DAMAGE_PRECISION = 100;
private static final int DEFAULT_RAY_COUNT = 720;
private static final String VIEW_DISTANCE_KEY = "perception.los.max-distance";
private static final String RAY_COUNT_KEY = "perception.los.ray-count";
private static final String HP_PRECISION_KEY = "perception.los.precision.hp";
private static final String DAMAGE_PRECISION_KEY = "perception.los.precision.damage";
private static final IntersectionSorter INTERSECTION_SORTER = new IntersectionSorter();
private int viewDistance;
private int hpPrecision;
private int damagePrecision;
private int rayCount;
private StandardWorldModel world;
private Config config;
private LOSView view;
/**
Create a LineOfSightPerception object.
*/
public LineOfSightPerception() {
}
@Override
public void initialise(Config newConfig, WorldModel<? extends Entity> model) {
world = StandardWorldModel.createStandardWorldModel(model);
this.config = newConfig;
viewDistance = config.getIntValue(VIEW_DISTANCE_KEY, DEFAULT_VIEW_DISTANCE);
hpPrecision = config.getIntValue(HP_PRECISION_KEY, DEFAULT_HP_PRECISION);
damagePrecision = config.getIntValue(DAMAGE_PRECISION_KEY, DEFAULT_DAMAGE_PRECISION);
rayCount = config.getIntValue(RAY_COUNT_KEY, DEFAULT_RAY_COUNT);
view = null;
}
@Override
public String toString() {
return "Line of sight perception";
}
@Override
public JComponent getGUIComponent() {
if (view == null) {
view = new LOSView();
view.refresh();
}
return view;
}
@Override
public String getGUIComponentName() {
return "Line of sight";
}
@Override
public void setTime(int timestep) {
if (view != null) {
view.clear();
view.refresh();
}
}
@Override
public ChangeSet getVisibleEntities(AgentProxy agent) {
StandardEntity agentEntity = (StandardEntity)agent.getControlledEntity();
Logger.debug("Finding visible entities for " + agentEntity);
ChangeSet result = new ChangeSet();
// Look for objects within range
Pair<Integer, Integer> location = agentEntity.getLocation(world);
if (location != null) {
Point2D point = new Point2D(location.first(), location.second());
Collection<StandardEntity> nearby = world.getObjectsInRange(location.first(), location.second(), viewDistance);
Collection<StandardEntity> visible = findVisible(agentEntity, point, nearby);
for (StandardEntity next : visible) {
StandardEntityURN urn = next.getStandardURN();
switch (urn) {
case ROAD:
addRoadProperties((Road)next, result);
break;
case BUILDING:
case REFUGE:
case FIRE_STATION:
case AMBULANCE_CENTRE:
case POLICE_OFFICE:
addBuildingProperties((Building)next, result);
break;
case CIVILIAN:
case FIRE_BRIGADE:
case AMBULANCE_TEAM:
case POLICE_FORCE:
// Always send all properties of the agent-controlled object
if (next == agentEntity) {
addSelfProperties((Human)next, result);
}
else {
addHumanProperties((Human)next, result);
}
break;
case BLOCKADE:
addBlockadeProperties((Blockade)next, result);
break;
default:
// Ignore other types
break;
}
}
}
if (view != null) {
view.repaint();
}
return result;
}
private void addRoadProperties(Road road, ChangeSet result) {
addAreaProperties(road, result);
// Only update blockades
result.addChange(road, road.getBlockadesProperty());
// Also update each blockade
if (road.isBlockadesDefined()) {
for (EntityID id : road.getBlockades()) {
Blockade blockade = (Blockade)world.getEntity(id);
if (blockade == null) {
Logger.error("Blockade " + id + " is null!");
Logger.error(road.getFullDescription());
}
else {
addBlockadeProperties(blockade, result);
}
}
}
}
private void addBuildingProperties(Building building, ChangeSet result) {
addAreaProperties(building, result);
// Update TEMPERATURE, FIERYNESS and BROKENNESS
result.addChange(building, building.getTemperatureProperty());
result.addChange(building, building.getFierynessProperty());
result.addChange(building, building.getBrokennessProperty());
}
private void addAreaProperties(Area area, ChangeSet result) {
}
private void addFarBuildingProperties(Building building, ChangeSet result) {
// Update FIERYNESS only
result.addChange(building, building.getFierynessProperty());
}
private void addHumanProperties(Human human, ChangeSet result) {
// Update POSITION, X, Y, DIRECTION, STAMINA, BURIEDNESS, HP, DAMAGE
result.addChange(human, human.getPositionProperty());
result.addChange(human, human.getXProperty());
result.addChange(human, human.getYProperty());
result.addChange(human, human.getDirectionProperty());
result.addChange(human, human.getStaminaProperty());
result.addChange(human, human.getBuriednessProperty());
// Round HP and damage
IntProperty hp = (IntProperty)human.getHPProperty().copy();
roundProperty(hp, hpPrecision);
result.addChange(human, hp);
IntProperty damage = (IntProperty)human.getDamageProperty().copy();
roundProperty(damage, damagePrecision);
result.addChange(human, damage);
}
private void addSelfProperties(Human human, ChangeSet result) {
// Update human properties and POSITION_HISTORY
addHumanProperties(human, result);
result.addChange(human, human.getPositionHistoryProperty());
// Un-round hp and damage
result.addChange(human, human.getHPProperty());
result.addChange(human, human.getDamageProperty());
if (human instanceof FireBrigade) {
FireBrigade fb = (FireBrigade)human;
result.addChange(fb, fb.getWaterProperty());
}
}
private void addBlockadeProperties(Blockade blockade, ChangeSet result) {
result.addChange(blockade, blockade.getXProperty());
result.addChange(blockade, blockade.getYProperty());
result.addChange(blockade, blockade.getPositionProperty());
result.addChange(blockade, blockade.getApexesProperty());
result.addChange(blockade, blockade.getRepairCostProperty());
}
private void roundProperty(IntProperty p, int precision) {
if (precision != 1 && p.isDefined()) {
p.setValue(round(p.getValue(), precision));
}
}
private int round(int value, int precision) {
int remainder = value % precision;
value -= remainder;
if (remainder >= precision / 2) {
value += precision;
}
return value;
}
private Collection<StandardEntity> findVisible(StandardEntity agentEntity, Point2D location, Collection<StandardEntity> nearby) {
Logger.debug("Finding visible entities from " + location);
Logger.debug(nearby.size() + " nearby entities");
Collection<LineInfo> lines = getAllLines(nearby);
// Cast rays
// CHECKSTYLE:OFF:MagicNumber
double dAngle = Math.PI * 2 / rayCount;
// CHECKSTYLE:ON:MagicNumber
Collection<StandardEntity> result = new HashSet<StandardEntity>();
for (int i = 0; i < rayCount; ++i) {
double angle = i * dAngle;
Vector2D vector = new Vector2D(Math.sin(angle), Math.cos(angle)).scale(viewDistance);
Ray ray = new Ray(new Line2D(location, vector), lines);
for (LineInfo hit : ray.getLinesHit()) {
StandardEntity e = hit.getEntity();
result.add(e);
}
if (view != null) {
view.addRay(agentEntity, ray);
}
}
// Now look for humans
for (StandardEntity next : nearby) {
if (next instanceof Human) {
Human h = (Human)next;
if (canSee(agentEntity, location, h, lines)) {
result.add(h);
}
}
}
// Add self
result.add(agentEntity);
Logger.debug(agentEntity + " can see " + result);
return result;
}
private boolean canSee(StandardEntity agent, Point2D location, Human h, Collection<LineInfo> lines) {
if (h.isXDefined() && h.isYDefined()) {
int x = h.getX();
int y = h.getY();
Point2D humanLocation = new Point2D(x, y);
Ray ray = new Ray(new Line2D(location, humanLocation), lines);
if (ray.getVisibleLength() >= 1) {
if (view != null) {
view.addRay(agent, ray);
}
return true;
}
}
else if (h.isPositionDefined()) {
if (h.getPosition().equals(agent.getID())) {
return true;
}
Entity e = world.getEntity(h.getPosition());
if (e instanceof AmbulanceTeam) {
return canSee(agent, location, (Human)e, lines);
}
}
return false;
}
private Collection<LineInfo> getAllLines(Collection<StandardEntity> entities) {
Collection<LineInfo> result = new HashSet<LineInfo>();
for (StandardEntity next : entities) {
if (next instanceof Building) {
for (Edge edge : ((Building)next).getEdges()) {
Line2D line = edge.getLine();
result.add(new LineInfo(line, next, !edge.isPassable()));
}
}
if (next instanceof Road) {
for (Edge edge : ((Road)next).getEdges()) {
Line2D line = edge.getLine();
result.add(new LineInfo(line, next, false));
}
}
else if (next instanceof Blockade) {
int[] apexes = ((Blockade)next).getApexes();
List<Point2D> points = GeometryTools2D.vertexArrayToPoints(apexes);
List<Line2D> lines = GeometryTools2D.pointsToLines(points, true);
for (Line2D line : lines) {
result.add(new LineInfo(line, next, false));
}
}
else {
continue;
}
}
return result;
}
private static class Ray {
/** The ray itself. */
private Line2D ray;
/** The visible length of the ray. */
private double length;
/** List of lines hit in order. */
private List<LineInfo> hit;
public Ray(Line2D ray, Collection<LineInfo> otherLines) {
this.ray = ray;
List<Pair<LineInfo, Double>> intersections = new ArrayList<Pair<LineInfo, Double>>();
// Find intersections with other lines
for (LineInfo other : otherLines) {
double d1 = ray.getIntersection(other.getLine());
double d2 = other.getLine().getIntersection(ray);
if (d2 >= 0 && d2 <= 1 && d1 > 0 && d1 <= 1) {
intersections.add(new Pair<LineInfo, Double>(other, d1));
}
}
Collections.sort(intersections, INTERSECTION_SORTER);
hit = new ArrayList<LineInfo>();
length = 1;
for (Pair<LineInfo, Double> next : intersections) {
LineInfo l = next.first();
hit.add(l);
if (l.isBlocking()) {
length = next.second();
break;
}
}
}
public Line2D getRay() {
return ray;
}
public double getVisibleLength() {
return length;
}
public List<LineInfo> getLinesHit() {
return Collections.unmodifiableList(hit);
}
}
private static class LineInfo {
private Line2D line;
private StandardEntity entity;
private boolean blocking;
public LineInfo(Line2D line, StandardEntity entity, boolean blocking) {
this.line = line;
this.entity = entity;
this.blocking = blocking;
}
public Line2D getLine() {
return line;
}
public StandardEntity getEntity() {
return entity;
}
public boolean isBlocking() {
return blocking;
}
}
private static class IntersectionSorter implements Comparator<Pair<LineInfo, Double>>, java.io.Serializable {
@Override
public int compare(Pair<LineInfo, Double> a, Pair<LineInfo, Double> b) {
double d1 = a.second();
double d2 = b.second();
if (d1 < d2) {
return -1;
}
if (d1 > d2) {
return 1;
}
return 0;
}
}
private class LOSView extends JPanel {
private transient StandardWorldModelViewer viewer;
private transient Collection<Ray> rays;
private transient Map<StandardEntity, Collection<Ray>> sources;
private transient StandardEntity selected;
public LOSView() {
super(new BorderLayout());
viewer = new StandardWorldModelViewer();
viewer.removeAllLayers();
viewer.addLayer(new BuildingLayer());
viewer.addLayer(new RoadLayer());
viewer.addLayer(new RoadBlockageLayer());
viewer.addLayer(new HumanLayer());
viewer.addLayer(new RayLayer());
rays = new ArrayList<Ray>();
sources = new LazyMap<StandardEntity, Collection<Ray>>() {
@Override
public Collection<Ray> createValue() {
return new HashSet<Ray>();
}
};
selected = null;
viewer.addViewListener(new ViewListener() {
@Override
public void objectsClicked(ViewComponent v, List<RenderedObject> objects) {
selected = null;
for (RenderedObject o : objects) {
if (o.getObject() instanceof Human) {
selected = (StandardEntity)o.getObject();
viewer.repaint();
}
}
}
@Override
public void objectsRollover(ViewComponent v, List<RenderedObject> objects) {
}
});
add(viewer, BorderLayout.CENTER);
}
public void clear() {
synchronized (rays) {
rays.clear();
sources.clear();
}
}
public void addRay(StandardEntity source, Ray ray) {
synchronized (rays) {
rays.add(ray);
sources.get(source).add(ray);
}
}
public void refresh() {
viewer.view(world);
}
private class RayLayer extends StandardViewLayer {
@Override
public Collection<RenderedObject> render(Graphics2D g, ScreenTransform transform, int width, int height) {
Collection<Ray> toDraw = new HashSet<Ray>();
synchronized (rays) {
if (selected == null) {
toDraw.addAll(rays);
}
else {
toDraw.addAll(sources.get(selected));
}
}
g.setColor(Color.CYAN);
for (Ray next : toDraw) {
Line2D line = next.getRay();
Point2D origin = line.getOrigin();
Point2D end = line.getPoint(next.getVisibleLength());
int x1 = transform.xToScreen(origin.getX());
int y1 = transform.yToScreen(origin.getY());
int x2 = transform.xToScreen(end.getX());
int y2 = transform.yToScreen(end.getY());
g.drawLine(x1, y1, x2, y2);
}
return new ArrayList<RenderedObject>();
}
@Override
public String getName() {
return "Line of sight rays";
}
}
}
}