1
0

Initial implementation

This commit is contained in:
2021-04-15 13:30:43 +03:00
commit 1157182678
42 changed files with 6502 additions and 0 deletions

View File

@ -0,0 +1,21 @@
package simulation;
import java.util.ResourceBundle;
/**
* Convenience class for retrieving translated strings
*
* @author John Ahlroos
*/
public interface I18N {
/**
* Get a translated string
*
* @param key the translation key
* @return the translated message
*/
static String get(String key) {
return ResourceBundle.getBundle("simulation-messages").getString(key);
}
}

View File

@ -0,0 +1,110 @@
package simulation;
import drones.dispatcher.Dispatcher;
import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Help.Visibility;
import picocli.CommandLine.ITypeConverter;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
* Simulates flying drones by using data form the ./data directory
* <p>
* Creates log files to the logs/ directory:
* - dispatcher.log: Logs from the dispatcher controlling the drones
* - drone-<id>.log: Logs from the drones
* <p>
* The output of the simulation that is printed in the console is a report for traffic conditions of the provided
* tube stations. Every line represents the condition of the traffic at a certain point in time and from a certain
* waypoint viewpoint.
*/
@Command(name = "drone-simulator", mixinStandardHelpOptions = true,
version = "1.0", resourceBundle = Simulation.RESOURCE_BUNDLE, showDefaultValues = true)
public class Simulation implements Callable<Integer> {
public static final String RESOURCE_BUNDLE = "simulation-messages";
@Parameters(arity = "1", paramLabel = "DRONES", showDefaultValue = Visibility.NEVER)
private List<Long> droneIds;
@Option(names = {"-d", "--data-dir"}, defaultValue = ".")
private Path dataDir;
@Option(names = {"-t", "--tube-stations"}, defaultValue = "./tube.csv")
private Path tubeStationsFile;
@Option(names = {"-b", "--start-time"}, defaultValue = "2011-03-22 07:47:00", converter = TimeConverter.class)
private LocalDateTime currentTime;
@Option(names = {"-e", "--shut-down-time"}, defaultValue = "2011-03-22 08:10:00", converter = TimeConverter.class)
private LocalDateTime shutDownTime;
@Option(names = {"-p", "--simulation-speed"}, defaultValue = "0.0")
private double simulationSpeed;
@Override
public Integer call() throws Exception {
System.out.println(I18N.get("starting.message"));
System.out.println();
// Use default location for tube file (in the data directory) if not explicitly set
if (!tubeStationsFile.toFile().exists()) {
tubeStationsFile = dataDir.resolve(Path.of("tube.csv"));
}
final var executor = Executors.newFixedThreadPool(droneIds.size() + 1);
try {
// Setup dispatcher
var dispatcher = new Dispatcher(currentTime, shutDownTime, simulationSpeed);
dispatcher.registerTubeStations(Files.newBufferedReader(tubeStationsFile));
// Setup Drones
var drones = new ArrayList<Runnable>();
for (long id : droneIds) {
var droneData = Files.newBufferedReader(dataDir.resolve(Path.of(id + ".csv")));
drones.add(dispatcher.registerDroneWithReader(id, droneData));
}
// Power them on
executor.execute(dispatcher);
drones.forEach(executor::execute);
} finally {
executor.shutdown();
executor.awaitTermination(20, TimeUnit.MINUTES);
}
System.out.println(I18N.get("ended.message"));
return 0;
}
/**
* Main entrypoint of the simulation
*
* @param args program arguments
*/
public static void main(String... args) {
int exitCode = new CommandLine(new Simulation()).execute(args);
System.exit(exitCode);
}
/**
* Set the date formats for input times
*/
static class TimeConverter implements ITypeConverter<LocalDateTime> {
public LocalDateTime convert(String value) throws Exception {
return LocalDateTime.parse(value, Dispatcher.DATE_FORMAT);
}
}
}

View File

@ -0,0 +1,21 @@
package simulation.logging;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.sift.AbstractDiscriminator;
/**
* Provides capability for the logger to log drone logs to different files
*
* @author John Ahlroos
*/
public class DroneLogPerFile extends AbstractDiscriminator<ILoggingEvent> {
@Override
public String getDiscriminatingValue(ILoggingEvent event) {
return event.getLoggerName().split("\\.")[1];
}
@Override
public String getKey() {
return "droneId";
}
}

View File

@ -0,0 +1,277 @@
package simulation.logging;
import ch.qos.logback.classic.spi.LoggingEvent;
import ch.qos.logback.core.AppenderBase;
import drones.drone.Drone;
import drones.geo.Point;
import drones.geo.TubeStation;
import drones.messages.Message.TrafficCondition.Condition;
import simulation.I18N;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.nio.file.Path;
import java.util.List;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;
/**
* Combines logs to form a map of the flow route and tube stations
*
* @author John Ahlroos
*/
public class RouteMapGenerator<E> extends AppenderBase<E> {
private static final int IMAGE_PIXEL_SIZE = 800;
private static final String DRONE_LOGGER = "Drone";
private static final String DISPATCHER_LOGGER = "Dispatcher";
private static final String REPORT_LOGGER = "Report";
private static final String REPORT_FILE_NAME = "traffic-report.png";
private static final Pattern ALL_TUBE_STATIONS_PATTERN = Pattern.compile(
"Registered\sTube\sStation.*(name=(.*),\\slat=([\\d+\\.]+)),\\s(lon=([\\d+\\.-]+))");
private static final Pattern VISITED_TUBE_STATIONS_PATTERN = Pattern.compile(
"(.*) @ (\\d\\d:\\d\\d:\\d\\d): (.*) \\(drone: (\\d*),speed: (\\d*)km/h, distanceToStation: (\\d*)m\\)");
private static final Pattern ROUTE_POINT_PATTERN = Pattern.compile(
"Arrived.*(lat=([\\d+\\.]+)),\\s(lon=([\\d+\\.-]+))");
private double min_lat = Double.MAX_VALUE;
private double min_lon = Double.MAX_VALUE;
private double max_lat = Double.MIN_VALUE;
private double max_lon = Double.MIN_VALUE;
private final Map<Long, List<Point>> points = new ConcurrentHashMap<>();
private final List<TubeStation> stations = new ArrayList<>();
private final Set<TubeStationWithCondition> foundStations = new HashSet<>();
private final List<Long> terminated = new ArrayList<>();
private boolean mapRendered = false;
@Override
protected void append(E event) {
if (simulationTerminated()) {
if (!mapRendered) {
var file = renderMap();
System.out.println();
System.out.println(String.format(I18N.get("map.generation.message"), file.toAbsolutePath()));
System.out.println();
}
return;
}
var loggingEvent = (LoggingEvent) event;
var message = loggingEvent.getFormattedMessage();
var loggerName = loggingEvent.getLoggerName();
if (DISPATCHER_LOGGER.equals(loggerName)) {
collectAllTubeStation(message);
return;
}
if (REPORT_LOGGER.equals(loggerName)) {
collectVisitedTubeStation(message);
return;
}
if (loggerName.startsWith(DRONE_LOGGER)) {
var droneId = Long.parseLong(loggerName.substring(loggerName.indexOf(".") + 1));
registerDroneTerminated(droneId, message);
collectRoutePoint(droneId, message);
return;
}
}
private void collectAllTubeStation(String message) {
var m2 = ALL_TUBE_STATIONS_PATTERN.matcher(message);
while (m2.find()) {
var name = m2.group(2);
var lat = Double.parseDouble(m2.group(3));
var lon = Double.parseDouble(m2.group(5));
var tb = new TubeStation(new Point(lat, lon, null), name);
stations.add(tb);
}
}
private void collectVisitedTubeStation(String message) {
var m = VISITED_TUBE_STATIONS_PATTERN.matcher(message);
while (m.find()) {
var name = m.group(1);
var time = m.group(2);
var condition = m.group(3);
var droneId = Long.parseLong(m.group(4));
var speed = Integer.parseInt(m.group(5));
stations.stream()
.filter(tb -> tb.name().equals(name))
.map(tb -> new TubeStationWithCondition(tb, time, condition, speed, droneId))
.forEach(foundStations::add);
}
}
private void collectRoutePoint(long droneId, String message) {
var m = ROUTE_POINT_PATTERN.matcher(message);
while (m.find()) {
var lat = Double.parseDouble(m.group(2));
min_lat = Math.min(min_lat, lat);
max_lat = Math.max(max_lat, lat);
var lon = Double.parseDouble(m.group(4));
min_lon = Math.min(min_lon, lon);
max_lon = Math.max(max_lon, lon);
points.putIfAbsent(droneId, new ArrayList<>());
points.get(droneId).add(new Point(lat, lon, null));
}
}
private void registerDroneTerminated(long droneId, String message) {
if (message.contains("state=TERMINATED")) {
terminated.add(droneId);
}
}
private boolean simulationTerminated() {
return !points.keySet().isEmpty() && terminated.containsAll(points.keySet());
}
private Path renderMap() {
mapRendered = true;
var colors = new ArrayDeque<>(List.of(Color.BLUE, Color.ORANGE));
var map = new BufferedImage(IMAGE_PIXEL_SIZE, IMAGE_PIXEL_SIZE, BufferedImage.TYPE_INT_ARGB);
var graphics = (Graphics2D) map.getGraphics();
var font1 = new Font("Arial", Font.ITALIC, 18);
var font2 = new Font("Arial", Font.BOLD, 16);
var font3 = new Font("Arial", Font.PLAIN, 14);
// Apply Zoom factor
min_lon -= 0.006;
max_lon += 0.006;
min_lat -= 0.006;
max_lat += 0.006;
renderBackground(graphics);
stations.forEach(tb -> renderStation(tb, graphics));
foundStations.forEach(tb -> renderFoundStation(tb, graphics, font2, font3));
points.forEach((droneId, points) -> renderRoute(droneId, points, graphics, font1, font2, colors));
try {
var file = Path.of(REPORT_FILE_NAME);
ImageIO.write(map, "png", file.toFile());
return file;
} catch (IOException e) {
System.err.println(I18N.get("map.file.generation.failed"));
}
return null;
}
private void renderBackground(Graphics graphics) {
graphics.setColor(Color.LIGHT_GRAY);
graphics.fillRect(0, 0, IMAGE_PIXEL_SIZE - 1, IMAGE_PIXEL_SIZE - 1);
}
private void renderStation(TubeStation tb, Graphics graphics) {
if (foundStations.stream().map(TubeStationWithCondition::tubeStation).anyMatch(tb::equals)) {
return;
}
var x = getXCoordinate(tb.point());
var y = getYCoordinate(tb.point());
graphics.setColor(new Color(170, 170, 170));
graphics.fillRect(x - 5, y - 5, 10, 10);
graphics.drawString(tb.name(), x + 10, y + 5);
}
private void renderFoundStation(TubeStationWithCondition tb, Graphics graphics, Font font2, Font font3) {
var x = getXCoordinate(tb.tubeStation.point());
var y = getYCoordinate(tb.tubeStation.point());
graphics.setColor(switch (Condition.valueOf(tb.condition)) {
case LIGHT -> Color.GREEN;
case MODERATE -> Color.YELLOW;
case HEAVY -> Color.RED;
});
graphics.setFont(font2);
graphics.fillRect(x - 5, y - 5, 10, 10);
graphics.drawString(String.format("%s@%s %s", tb.tubeStation.name(), tb.time, tb.condition), x + 10, y + 5);
graphics.setColor(Color.DARK_GRAY);
graphics.setFont(font3);
graphics.drawString(String.format("(%s@%skm/h)", tb.droneId, tb.speed), x + 10, y + 20);
graphics.setFont(font2);
}
private void renderRoute(long droneId, List<Point> data, Graphics graphics, Font font1, Font font2,
Deque<Color> colors) {
// Convert range to pixels
var range = data.get(0).moveTowards(data.get(1), Drone.TUBE_STATION_RANGE, 1);
var rangeX = Math.abs(getXCoordinate(data.get(0)) - getXCoordinate(range));
var rangeY = Math.abs(getYCoordinate(data.get(0)) - getYCoordinate(range));
var rangeW = Math.max(rangeX, rangeY);
var routeColor = colors.poll();
var prevPoint = data.get(0);
var prevX = getXCoordinate(prevPoint);
var prevY = getYCoordinate(prevPoint);
for (int i = 0; i < data.size(); i++) {
var point = data.get(i);
var x = getXCoordinate(point);
var y = getYCoordinate(point);
var rangeAlpha = (Math.abs(x - prevX + y - prevY) + 255) % 255;
var rangeColor = new Color(0, 30, 254, rangeAlpha);
graphics.setColor(rangeColor);
graphics.fillOval(prevX - rangeW / 2, prevY - rangeW / 2, rangeW, rangeW);
graphics.setColor(routeColor);
graphics.drawLine(prevX, prevY, x, y);
if (i == 0) {
graphics.setFont(font1);
graphics.fillOval(x - 5, y - 5, 10, 10);
graphics.drawString(String.format(I18N.get("map.drone.start"), droneId), x - 20, y - 10);
graphics.setFont(font2);
} else if (i == data.size() - 1) {
graphics.setFont(font1);
graphics.fillOval(x - 5, y - 5, 10, 10);
graphics.drawString(String.format(I18N.get("map.drone.end"), droneId), x - 20, y + 30);
graphics.setFont(font2);
}
prevPoint = point;
prevX = getXCoordinate(prevPoint);
prevY = getYCoordinate(prevPoint);
}
}
private int getXCoordinate(Point p) {
var lonExtent = max_lon - min_lon;
return (int) ((IMAGE_PIXEL_SIZE * (p.longitude() - min_lon)) / lonExtent);
}
private int getYCoordinate(Point p) {
var latExtent = max_lat - min_lat;
return (int) ((IMAGE_PIXEL_SIZE * (p.latitude() - min_lat)) / latExtent);
}
private record TubeStationWithCondition(
TubeStation tubeStation, String time, String condition, int speed, long droneId) {
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
var that = (TubeStationWithCondition) o;
return Objects.equals(tubeStation.name(), that.tubeStation.name());
}
@Override
public int hashCode() {
return Objects.hash(tubeStation.name());
}
}
}

View File

@ -0,0 +1,68 @@
<configuration debug="false" scan="true">
<statusListener class="ch.qos.logback.core.status.NopStatusListener"/>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<withJansi>false</withJansi>
<encoder>
<pattern>%msg%n</pattern>
</encoder>
</appender>
<appender name="REPORT_LOG_FILE" class="ch.qos.logback.core.FileAppender">
<file>logs/report.log</file>
<append>false</append>
<encoder>
<pattern>%msg%n</pattern>
</encoder>
</appender>
<appender name="ROUTE_MAP" class="simulation.logging.RouteMapGenerator">
<encoder>
<pattern>%msg%n</pattern>
</encoder>
</appender>
<appender name="DISPATCHER_LOG" class="ch.qos.logback.core.FileAppender">
<file>logs/dispatcher.log</file>
<append>false</append>
<encoder>
<pattern>Dispatcher - %msg%n</pattern>
</encoder>
</appender>
<appender name="DRONE_LOG" class="ch.qos.logback.classic.sift.SiftingAppender">
<discriminator class="simulation.logging.DroneLogPerFile">
<defaultValue></defaultValue>
</discriminator>
<sift>
<appender name="DRONE_LOG_${contextName}" class="ch.qos.logback.core.FileAppender">
<file>logs/drone-${droneId}.log</file>
<append>false</append>
<encoder>
<pattern>Drone-${droneId} - %msg%n</pattern>
</encoder>
</appender>
</sift>
</appender>
<root level="OFF"/>
<logger name="Dispatcher" level="INFO">
<appender-ref ref="DISPATCHER_LOG"/>
<appender-ref ref="ROUTE_MAP"/>
</logger>
<logger name="Report" level="INFO">
<appender-ref ref="STDOUT"/>
<appender-ref ref="REPORT_LOG_FILE"/>
<appender-ref ref="ROUTE_MAP"/>
</logger>
<logger name="Drone" level="INFO">
<appender-ref ref="DRONE_LOG"/>
<appender-ref ref="ROUTE_MAP"/>
</logger>
</configuration>

View File

@ -0,0 +1,23 @@
# General
usage.headerHeading=A Simulator for simulating drone routes.%n
# Options
d=The path to the drone data
t=The path to the tube stations data
s=At what time should the simulation terminate
p.0=The speed of the simulation time.
p.1=0 (no time simulation) -> 1.0 (full time simulation)
w.0=Should tube conditions only be reported at waypoints
w.1=If false then tube conditions will also be reported along the route to a waypoint when a tube station is within range
# Parameters
DRONES.0=The drones id's to include in the simulation.
DRONES.1=Drone data files must exist for these " +"ids in the --data-dir folder
# General
starting.message=Waiting for traffic reports...
ended.message=Simulation complete.
# Errors
data.file.not.found.error=Data file for drone %d not found in %s
# Route Map
map.drone.start=Start @ drone-%d
map.drone.end=End @ drone-%d
map.file.generation.failed=Image generation failed.
map.generation.message=The route map was rendered to %s