diff --git a/.gitignore b/.gitignore index bf70086c415482dea564c87fd3dc6e96e48133dc..7296ae5dcc5c81743786e9ba0c69368f365a9944 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,9 @@ *.class *.jar target/ -**/*.DS_Store \ No newline at end of file +**/*.DS_Store +*.project +**.vscode/ +*.classpath +**.settings +*.iml \ No newline at end of file diff --git a/README.md b/README.md index 846c531f866189e728ea7b4f8e8eb99b5b383b84..85398b35c9cc9060b2718436ce44dc646844b4fc 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ # Travail Pratique 4 Un travail de Alexis Durgnat, Michael El Kharroubi, Quentin Leblanc et Théo Pirkl - ## Description Ce travail pratique consiste à proposer une application de réseau social permettant de savoir qui prend quel transport public. Certaines intégrations y seront ajoutées. @@ -9,12 +8,31 @@ quel transport public. Certaines intégrations y seront ajoutées. Voici un mockup de l'application :  +## Compilation +L'application peut être compilée avec +``` +mvn compile exec:java +``` +Les tests sont exécutés avec +``` +mvn test +``` + +## Utilisation +Lors du lancement de l'application, un champs demande le nom d'utilisateur. Après avoir entré celui-ci, l'application principale s'ouvre. +Il est possible de chercher une correspondance grâce à la barre supérieure. La première combobox est le départ, la seconde la destinations. +Après avoir sélectionné le point de départ et d'arriver, un clic sur "Itinéraires" permet d'afficher les prochaines correspondances disponibles. + +La barre latérale droite contient les notifications et messages des utilisateurs connectés. Il est possible d'envoyer un message à tout le monde +grâce au champ textuel en dessous. + ## Technique Nous implémentons : * L'API OpenData Transport [(disponible ici)](https://transport.opendata.ch) * L'API Prévisions-Météo.ch [(disponible ici)](https://www.prevision-meteo.ch/uploads/pdf/recuperation-donnees-meteo.pdf) -* Une interface graphique avec JavaFX, liée à un WebEngine -* D'autres trucs (?) +* Une interface graphique avec JavaFX +* Un chat :cat: +* Quelques surprises... :wink: ## Intégration Merci de suivre au possible le système suivant : @@ -29,4 +47,4 @@ Schéma des branches (exemple) : Lors du merge : * gui —> devel et opendata-transport —> devel -* devel -> master (une fois que ca fonctionne) \ No newline at end of file +* devel -> master (une fois que ca fonctionne) diff --git a/pom.xml b/pom.xml index 481cc24e6673f76f36b780c683ce38ddad706f75..effeb7eab5cb4cfb0b6381300c46c1f536a25898 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,87 @@ <properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> </properties> + <dependencies> + <!-- JUnit --> + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-engine</artifactId> + <version>5.2.0</version> + <scope>test</scope> + </dependency> + <!-- Java FX --> + <dependency> + <groupId>org.openjfx</groupId> + <artifactId>javafx-controls</artifactId> + <version>11</version> + </dependency> + <dependency> + <groupId>org.openjfx</groupId> + <artifactId>javafx-fxml</artifactId> + <version>11</version> + </dependency> + <dependency> + <groupId>org.json</groupId> + <artifactId>json</artifactId> + <version>20180813</version> + </dependency> + <!-- Rabbit MQ --> + <dependency> + <groupId>com.rabbitmq</groupId> + <artifactId>amqp-client</artifactId> + <version>5.5.2</version> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-api</artifactId> + <version>1.7.5</version> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-log4j12</artifactId> + <version>1.7.5</version> + </dependency> + </dependencies> + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-compiler-plugin</artifactId> + <version>3.8.0</version> + <configuration> + <release>11</release> + </configuration> + </plugin> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>exec-maven-plugin</artifactId> + <version>1.6.0</version> + <executions> + <execution> + <goals> + <goal>java</goal> + </goals> + </execution> + </executions> + <configuration> + <mainClass>ch.hepia.Main</mainClass> + </configuration> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-surefire-plugin</artifactId> + <version>2.22.0</version> + <configuration> + <excludes> + <exclude>some test to exclude</exclude> + </excludes> + </configuration> + </plugin> + </plugins> + </build> </project> \ No newline at end of file diff --git a/src/main/java/ch/hepia/Main.java b/src/main/java/ch/hepia/Main.java new file mode 100644 index 0000000000000000000000000000000000000000..7efc6c084d0c9cf6eed99ac2af37113aa4d210d5 --- /dev/null +++ b/src/main/java/ch/hepia/Main.java @@ -0,0 +1,68 @@ +package ch.hepia; + +import ch.hepia.config.AppConfig; +import ch.hepia.config.AppContext; +import ch.hepia.mq.MessageManager; +import javafx.application.Application; +import javafx.application.Platform; +import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.stage.Stage; + +import java.io.IOException; + + +/** + * Starts up the app. + */ +public class Main extends Application { + + private static AppContext appContext; + + /** + * Main starter + * @param args The passed arguments + */ + public static void main(String[] args) throws Exception { + MessageManager m = new MessageManager( + AppConfig.RABBITMQ_HOSTNAME, AppConfig.RABBITMQ_USERNAME, AppConfig.RABBITMQ_PASSWORD, + AppConfig.RABBITMQ_EXCHANGE + ); + appContext = new AppContext(m); + launch(args); + } + + /** + * Starts up the JavaFX app. + * @param stage The stage to start + * @throws IOException When goofed up with JavaFX + */ + @Override + public void start(Stage stage) throws IOException { + Parent root = FXMLLoader.load(Main.class.getResource("/fxml/ConnectionWindow.fxml")); + Scene scene = new Scene(root, AppConfig.APP_WIDTH, AppConfig.APP_HEIGHT); + + stage.setScene(scene); + stage.setResizable(false); + stage.show(); + stage.setTitle(AppConfig.APP_NAME); + stage.setOnCloseRequest(t -> { + Platform.exit(); + try { + appContext.getMessageManager().close(); + } catch (Exception e){ + System.exit(1); + } + System.exit(0); + }); + } + + /** + * Gets the app context of the app + * @return the app context of the app + */ + public static AppContext getContext(){ + return appContext; + } +} diff --git a/src/main/java/ch/hepia/api/HTTPUtils.java b/src/main/java/ch/hepia/api/HTTPUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..f0ce17e29536cfbe7c8e0ba59f1ca7fda476972c --- /dev/null +++ b/src/main/java/ch/hepia/api/HTTPUtils.java @@ -0,0 +1,32 @@ +package ch.hepia.api; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; + +public class HTTPUtils { + /** + * Fetch datas with an http GET request on the form of a string given the url path + * @param path + * @return content + * @throws IOException + */ + public static String getContent(String path) throws IOException { + URL url = new URL(path); + HttpURLConnection http = (HttpURLConnection) url.openConnection(); + http.setRequestMethod("GET"); + http.connect(); + + BufferedReader reader = new BufferedReader(new InputStreamReader(http.getInputStream())); + String line; + StringBuilder content = new StringBuilder(); + while((line = reader.readLine()) != null){ + content.append(line); + } + reader.close(); + http.disconnect(); + return content.toString(); + } +} diff --git a/src/main/java/ch/hepia/api/transport/Connection.java b/src/main/java/ch/hepia/api/transport/Connection.java new file mode 100644 index 0000000000000000000000000000000000000000..4001bae6ef102a788c1a224cf3f7f293227df225 --- /dev/null +++ b/src/main/java/ch/hepia/api/transport/Connection.java @@ -0,0 +1,160 @@ +package ch.hepia.api.transport; + +import java.sql.Time; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.List; + +import org.json.JSONArray; +import org.json.JSONObject; +import java.io.Serializable; + +public class Connection implements Serializable { + private static final long serialVersionUID = 0xAEF55565673L; + private Stop from; + private Stop to; + private Time duration; + private Service service; + private List<String> products; + private int capacity1st; + private int capacity2nd; + private List<Section> sections; + + /** + * Empty Connection + */ + + public final static class EmptyConnection extends Connection{ + private static final long serialVersionUID = 0xAEF55565672L; + + /** + * Constructor of EmptyConnection All values are default ones An Empty + * Connection go from Geneva to Geneva + * + * @throws ParseException + */ + public EmptyConnection() throws ParseException { + super(new Stop.StopBuilder(new JSONObject()).build(), + new Stop.StopBuilder(new JSONObject()).build(), + new Time(new SimpleDateFormat("dd:kk:mm:ss").parse("00:00:00:00").getTime()), + new Service.ServiceBuilder(new JSONObject()).build(), new ArrayList<>(), 0, 0, new ArrayList<>()); + } + } + + /** + * Constructor of Connection Object + * + * @param from stop object + * @param to stop object + * @param duration time of the connection + * @param service how regular is the connection + * @param products list the products you have to purchase for this connection + * @param capacity1st number of seats in 1st class + * @param capacity2nd number of seats in 2nd class + * @param sections Where the connection will go through + */ + private Connection(Stop from, Stop to, Time duration, Service service, List<String> products, int capacity1st, + int capacity2nd, List<Section> sections) { + this.from = from; + this.to = to; + this.duration = duration; + this.service = service; + this.products = new ArrayList<>(products); + this.capacity1st = capacity1st; + this.capacity2nd = capacity2nd; + this.sections = new ArrayList<>(sections); + } + + public Section getInCommonSection(Connection connection) throws ParseException { + for (Section section : this.sections) { + if (connection.sections.contains(section)) { + return section; + } + } + return Section.empty(); + } + + /** + * Get all the sections of the connection + * + * @return A list of Section objects + */ + public List<Section> getSections() { + return new ArrayList<>(sections); + } + + public Stop getFrom() { + return this.from; + } + + public Stop getTo() { + return this.to; + } + + /** + * Builder of Connection object + */ + public final static class ConnectionBuilder { + private JSONObject datas; + + /** + * Constructor of the class ConnectionBuilder Stock Datas from a JSON object + * given + * + * @param datas contains datas to construct a Connection object + */ + public ConnectionBuilder(JSONObject datas) { + this.datas = datas; + } + + /** + * Build a Connection object from the datas obtained + * + * @return a new Connection object + * @throws ParseException + */ + public Connection build() throws ParseException { + // if the datas retrieved contain nothing, we have to prevent errors + if (datas.isEmpty()) { + return new EmptyConnection(); + } + + JSONObject fromJSON = datas.isNull("from") ? new JSONObject() : datas.getJSONObject("from"); + Stop.StopBuilder from = new Stop.StopBuilder(fromJSON); + + JSONObject toJSON = datas.isNull("to") ? new JSONObject() : datas.getJSONObject("to"); + Stop.StopBuilder to = new Stop.StopBuilder(toJSON); + + DateFormat formatter = new SimpleDateFormat("dd:kk:mm:ss"); + String date = datas.isNull("duration") ? "00:00:00:00" : datas.getString("duration").replace('d', ':'); + Time duration = new Time(formatter.parse(date).getTime()); + + JSONObject serviceJSON = datas.isNull("service") ? new JSONObject() : new JSONObject(datas.get("service")); + Service.ServiceBuilder service = new Service.ServiceBuilder(serviceJSON); + + JSONArray productsJSON = datas.isNull("from") ? new JSONArray() : datas.getJSONArray("products"); + List<String> products = new ArrayList<>(); + productsJSON.forEach(k -> products.add(k.toString())); + + int capacity1st = datas.isNull("capacity1st") ? 0 : datas.getInt("capacity1st"); + int capacity2nd = datas.isNull("capacity2nd") ? 0 : datas.getInt("capacity2nd"); + + JSONArray sectionsJSON = datas.isNull("from") ? new JSONArray() : datas.getJSONArray("sections"); + List<Section> sections = new ArrayList<>(); + sectionsJSON.forEach(o -> { + JSONObject k = (JSONObject) o; + Section.SectionBuilder section = new Section.SectionBuilder(k); + try { + sections.add(section.build()); + } catch (ParseException e) { + e.printStackTrace(); + } + }); + + return new Connection(from.build(), to.build(), duration, service.build(), products, capacity1st, + capacity2nd, sections); + } + } +} diff --git a/src/main/java/ch/hepia/api/transport/Coordinates.java b/src/main/java/ch/hepia/api/transport/Coordinates.java new file mode 100644 index 0000000000000000000000000000000000000000..63884acef945614c3e5a307f8bad74f91d129042 --- /dev/null +++ b/src/main/java/ch/hepia/api/transport/Coordinates.java @@ -0,0 +1,108 @@ +package ch.hepia.api.transport; + +import org.json.JSONObject; +import java.io.Serializable; + +public class Coordinates implements Serializable { + private static final long serialVersionUID = 0xFFF34565673L; + private String type; + private double x; + private double y; + + /** + * Default coordinates containing default values (Geneva is the default value) + * WGS84 is the default type value + */ + private final static class DefaultCoordinates extends Coordinates{ + private static final long serialVersionUID = 0xFFF34565670L; + /** + * Constructor of DefaultCoordinates, located in Geneva + */ + private DefaultCoordinates(){ + super("WGS84", 46.210237, 6.142422); + } + } + + /** + * Constructor of the class Coordinates + * A coordinates is represented by a type of place and a point (x, y) + * @param type type of place we are a these coordinates + * @param x coordinate in x + * @param y coordinate in y + */ + private Coordinates(String type, double x, double y){ + this.type = type; + this.x = x; + this.y = y; + } + + @Override + public String toString(){ + StringBuilder string = new StringBuilder(); + return string.append("Coordonnées du type ").append(type).append(" au point (").append(x).append(", ").append(y).append(")").toString(); + } + + /** + * Get the x coordinate of the place (x, y) + * @return x + */ + public double getX(){ + return x; + } + + /** + * Get the y coordinate of the place (x, y) + * @return y + */ + public double getY(){ + return y; + } + + @Override + public boolean equals(Object o){ + if (this == o) { + return true; + } + if (o == null || o.getClass() != this.getClass()) { + return false; + } + Coordinates c = (Coordinates) o; + return c.type.equals(this.type) && + Math.abs(c.x - this.x) < Math.pow(10, -6) && + Math.abs(c.y - this.y) < Math.pow(10, -6); + } + + /** + * Builder of Coordinates object + */ + public final static class CoordinatesBuilder{ + private JSONObject datas; + private String type; + + /** + * Constructor of the class CoordinatesBuilder + * Stock Datas from a JSON object given + * @param datas contains datas to construct a Coordinates object + */ + public CoordinatesBuilder(JSONObject datas, String type){ + this.datas = datas; + this.type = type; + } + + /** + * Build a Coordinates object from the datas obtained + * @return a new Coordinates object + */ + public Coordinates build(){ + if(datas.isEmpty()){ + return new DefaultCoordinates(); + } + + String type = datas.getString("type"); + double x = datas.isNull("x") ? 0 : datas.getDouble("x"); + double y = datas.isNull("y") ? 0 : datas.getDouble("y"); + + return new Coordinates(type, x, y); + } + } +} diff --git a/src/main/java/ch/hepia/api/transport/Journey.java b/src/main/java/ch/hepia/api/transport/Journey.java new file mode 100644 index 0000000000000000000000000000000000000000..0f1208126fc5f13b24b9b5c5183736f1fbf3ba24 --- /dev/null +++ b/src/main/java/ch/hepia/api/transport/Journey.java @@ -0,0 +1,173 @@ +package ch.hepia.api.transport; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.text.ParseException; +import java.util.ArrayList; +import java.util.List; +import java.io.Serializable; + +public class Journey implements Serializable { + private static final long serialVersionUID = 0xCBF34565673L; + private String name; + private String category; + private int categoryCode; + private String number; + private String operator; + private String to; + private List<Stop> passList; + private int capacity1st; + private int capacity2nd; + + /** + * Empty Journey + */ + public final static class EmptyJourney extends Journey{ + private static final long serialVersionUID = 0xCBF34565670L; + /** + * Constructor of EmptyJourney + * No Information and no Stops in the Journey + * @throws ParseException + */ + private EmptyJourney() throws ParseException { + super("", "", 0, "", "", "", + new ArrayList<>(), 0, 0); + } + } + + /** + * Constructor of the Journey Object + * @param name + * @param category what transport category the journey is + * @param categoryCode + * @param number + * @param operator which company operates the journey + * @param to destination of the journey + * @param passList Where the journey will pass through + * @param capacity1st number of seats inst class + * @param capacity2nd number of seats in nd class + */ + private Journey(String name, String category, int categoryCode, String number, String operator, String to, List<Stop> passList, int capacity1st, int capacity2nd){ + this.name = name; + this.category = category; + this.categoryCode = categoryCode; + this.number = number; + this.operator = operator; + this.to = to; + this.passList = new ArrayList<>(passList); + this.capacity1st = capacity1st; + this.capacity2nd = capacity2nd; + } + + /** + * Get the name of the journey + * @return name + */ + public String getName(){ + return name; + } + + /** + * Get the Category of the transport of the Journey + * @return category + */ + public String getCategory(){ + return category; + } + + /** + * Get the categoryCode affiliated with the Journey + * @return categoryCode + */ + public int getCategoryCode(){ + return categoryCode; + } + + /** + * Get where the Journey is headed to + * @return to + */ + public String getTo(){ + return to; + } + /** + * Get the transport number + * @return number + */ + public String getNumber(){ + return number; + } + + /** + * Get all the Stops where the journey will pass through + * @return passList + */ + public List<Stop> getPassList(){ + return new ArrayList<>(passList); + } + + @Override + public boolean equals(Object o){ + if (this == o) { + return true; + } + if (o == null || o.getClass() != this.getClass()) { + return false; + } + Journey j = (Journey) o; + return j.number.equals(this.number) && j.name.equals(this.name); + } + + /** + * Builder of Journey object + */ + public final static class JourneyBuilder{ + private JSONObject datas; + + /** + * Constructor of the class JourneyBuilder + * Stock Datas from a JSON object given + * @param datas contains datas to construct a Journey object + */ + public JourneyBuilder(JSONObject datas){ + this.datas = datas; + } + + /** + * Build a new Journey object from the datas obtained + * @return a new Journey Object + * @throws ParseException + */ + public Journey build() throws ParseException { + if(datas.isEmpty()){ + return new EmptyJourney(); + } + + String name = datas.isNull("name") ? "" : datas.getString("name"); + String category = datas.isNull("category") ? "" : datas.getString("category"); + int categoryCode = datas.isNull("categoryCode") ? 0 : datas.getInt("categoryCode"); + String number = datas.isNull("number") ? "" : datas.getString("number"); + String operator = datas.isNull("operator") ? "" : datas.getString("operator"); + + String to = datas.isNull("to") ? "" : datas.getString("to"); + + JSONArray passListJSON = datas.isNull("from") ? new JSONArray() : datas.getJSONArray("passList"); + List<Stop> passList = new ArrayList<>(); + passListJSON.forEach( o -> { + JSONObject k = (JSONObject) o; + Stop.StopBuilder stop = new Stop.StopBuilder(k); + try { + passList.add(stop.build()); + } catch (ParseException e) { + e.printStackTrace(); + } + }); + + int capacity1st = datas.isNull("capacity1st") ? 0 : datas.getInt("capacity1st"); + int capacity2nd = datas.isNull("capacity2nd") ? 0 : datas.getInt("capacity2nd"); + + return new Journey(name, category, categoryCode, number, operator, to, passList, capacity1st, capacity2nd); + } + } +} diff --git a/src/main/java/ch/hepia/api/transport/LinkAPI.java b/src/main/java/ch/hepia/api/transport/LinkAPI.java new file mode 100644 index 0000000000000000000000000000000000000000..780348437601453d6387de5f627ddf0bb5891512 --- /dev/null +++ b/src/main/java/ch/hepia/api/transport/LinkAPI.java @@ -0,0 +1,98 @@ +package ch.hepia.api.transport; + +import ch.hepia.api.HTTPUtils; +import org.json.JSONArray; +import org.json.JSONObject; + +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +public class LinkAPI { + private String baseURL = "http://transport.opendata.ch/v1/"; + + /** + * Get the stations containing the string query in their names + * This return a List of Station type Location Objects + * @param query + * @return a Lis<Location> + * @throws IOException + */ + public List<Location> getStations(String query) throws IOException { + String path = baseURL + "locations?query=" + URLEncoder.encode(query, StandardCharsets.UTF_8); + String content = HTTPUtils.getContent(path); + + JSONArray stations = new JSONObject(content).getJSONArray("stations"); + List<Location> stationsList = new ArrayList<>(); + stations.forEach(o -> { + JSONObject k = (JSONObject) o; + Location l = new Location.LocationBuilder(k, "station").build(); + stationsList.add(l); + }); + + return stationsList; + } + + /** + * Get all the connections from an end point to another + * Return a List of Connection Objects + * @param from The origin + * @param to The destination + * @return a List<Connection> + * @throws IOException + */ + public List<Connection> getConnections(String from, String to) throws IOException { + String path = baseURL + "connections?from=" + + URLEncoder.encode(from, StandardCharsets.UTF_8) + "&to=" + + URLEncoder.encode(to, StandardCharsets.UTF_8); + String content = HTTPUtils.getContent(path); + + JSONArray connections = new JSONObject(content).getJSONArray("connections"); + List<Connection> connectionsList = new ArrayList<>(); + connections.forEach(o->{ + JSONObject k = (JSONObject) o; + try { + Connection c = new Connection.ConnectionBuilder(k).build(); + connectionsList.add(c); + } catch(Exception e){ + e.printStackTrace(); + } + }); + + return connectionsList; + } + + /** + * Get the Stationboard of the station given + * The StationBoard is a List of Journey Objects + * The limit is the maximum number of stations the Stationboard should fetch + * @param station The station to get + * @param limit The limit of transportations + * @return a List<Journey> + * @throws IOException + */ + public List<Journey> getStationBoard(String station, int limit) throws IOException { + String path = baseURL + "stationboard?station=" + URLEncoder.encode(station, StandardCharsets.UTF_8); + if(limit != 0){ + path += "&limit=" + limit; + } + String content = HTTPUtils.getContent(path); + + JSONArray journeys = new JSONObject(content).getJSONArray("stationboard"); + List<Journey> journeysList = new ArrayList<>(); + journeys.forEach(o->{ + JSONObject k = (JSONObject) o; + try { + Journey j = new Journey.JourneyBuilder(k).build(); + journeysList.add(j); + } catch(Exception e){ + e.printStackTrace(); + } + }); + + return journeysList; + } + +} diff --git a/src/main/java/ch/hepia/api/transport/Location.java b/src/main/java/ch/hepia/api/transport/Location.java new file mode 100644 index 0000000000000000000000000000000000000000..ae33c639daff7f8ece62458d4939e3156e58f8d7 --- /dev/null +++ b/src/main/java/ch/hepia/api/transport/Location.java @@ -0,0 +1,118 @@ +package ch.hepia.api.transport; + +import org.json.JSONObject; +import java.io.Serializable; + +public class Location implements Serializable { + private static final long serialVersionUID = 0xAEF84565673L; + private int id; + private Type type; + private String name; + private double score; + private Coordinates where; + private double distance; + + /** + * Default Location + */ + private final static class DefaultLocation extends Location{ + private static final long serialVersionUID = 0xAEF84565670L; + /** + * Constructor of DefaultLocation, centered in Geneva + * Type is REFINE, because it's a default choice + */ + private DefaultLocation(){ + //here building new empty coordinates will return a default coordinates corresponding to Geneva + super(8501008, Type.REFINE, "Genève", 0, new Coordinates.CoordinatesBuilder(new JSONObject(), "refine").build(), 0); + } + } + + /** + * Constructor of the class Location + * @param id unique to the Location + * @param type type of place the Location is + * @param name + * @param score accuracy of the result (may not be used) + * @param where Localisation of the Location + * @param distance distance to the Location if there has been Coordinates (may not be used) + */ + private Location(int id, Type type, String name, double score, Coordinates where, double distance){ + this.id = id; + this.type = type; + this.name = name; + this.score = score; + this.where = where; + this.distance = distance; + } + + /** + * Get the coordinates of the Location + * @return where + */ + public Coordinates getCoordinates(){ + return where; + } + + /** + * Get the name of the location + * @return what + */ + public String getName(){ + return name; + } + + @Override + public boolean equals(Object o){ + if (this == o) { + return true; + } + if (o == null || o.getClass() != this.getClass()) { + return false; + } + Location l = (Location) o; + return l.type.equals(this.type) && + l.id == this.id && + l.name.equals(this.name) && + l.where.equals(this.where); + } + + /** + * Builder of Location objects + */ + public final static class LocationBuilder{ + private JSONObject datas; + private String type; + + /** + * Constructor of the class LocationBuilder + * Stock Datas from a JSON object given + * @param datas contains datas to construct a Locatiop object + */ + public LocationBuilder(JSONObject datas, String type){ + this.datas = datas; + this.type = type; + } + + /** + * Build a Location object with the datas obtained + * @return a new Location object + */ + public Location build(){ + if(datas.isEmpty()){ + return new DefaultLocation(); + } + + int id = datas.isNull("id") ? 0 : datas.getInt("id"); + Type type = Type.valueOf(this.type.toUpperCase()); + String name = datas.isNull("name") ? "" : datas.getString("name"); + double score = datas.isNull("score") ? 0 : datas.getDouble("score"); + + JSONObject coordinatesJSON = datas.isNull("coordinate") ? new JSONObject() : datas.getJSONObject("coordinate"); + Coordinates.CoordinatesBuilder coordinates = new Coordinates.CoordinatesBuilder(coordinatesJSON, this.type); + + double distance = datas.isNull("distance") ? 0 : datas.getDouble("distance"); + + return new Location(id, type, name, score, coordinates.build(), distance); + } + } +} diff --git a/src/main/java/ch/hepia/api/transport/Prognosis.java b/src/main/java/ch/hepia/api/transport/Prognosis.java new file mode 100644 index 0000000000000000000000000000000000000000..58754adea2afaba99393e65b0328af0d59e4980e --- /dev/null +++ b/src/main/java/ch/hepia/api/transport/Prognosis.java @@ -0,0 +1,95 @@ +package ch.hepia.api.transport; + +import org.json.JSONObject; + +import java.sql.Time; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.io.Serializable; + +public class Prognosis implements Serializable { + private static final long serialVersionUID = 0xAE034565673L; + private int plateform; + private Time departure; + private Time arrival; + private int capacity1st; + private int capacity2nd; + + /** + * Empty Prognosis + */ + private final static class EmptyPrognosis extends Prognosis{ + private static final long serialVersionUID = 0xAE034565670L; + /** + * Constructor of EmptyPrognosis + * No departure and no Arrival Time + * No information on the capacity of 1st and 2nd class nor on the plateform + * @throws ParseException + */ + private EmptyPrognosis() throws ParseException { + super(0, + new Time(new SimpleDateFormat("dd:kk:mm:ss").parse("00:00:00:00").getTime()), + new Time(new SimpleDateFormat("dd:kk:mm:ss").parse("00:00:00:00").getTime()), + 0, 0); + } + } + + /** + * Constructor of the Prognosis class + * A Prognosis contains information on the status of a Stop Object + * @param plateform plateform where the transport should arrive and leave + * @param departure time of departure + * @param arrival time of arrival + * @param capacity1st number of seats in 1st class + * @param capacity2nd number of seats in 2nd class + */ + private Prognosis(int plateform, Time departure, Time arrival, int capacity1st, int capacity2nd){ + this.plateform = plateform; + this.departure = departure; + this.arrival = arrival; + this.capacity1st = capacity1st; + this.capacity2nd = capacity2nd; + } + + /** + * Builder of Prognosis Objects + */ + public final static class PrognosisBuilder{ + private JSONObject datas; + + /** + * Constructor of the class PrognosisBuilder + * Stock Datas from a JSON object given + * @param datas contains datas to construct a Prognosis object + */ + public PrognosisBuilder(JSONObject datas){ + this.datas = datas; + } + + /** + * Build a Prognosis object from the datas obtained + * @return a new Prognosis object + * @throws ParseException + */ + public Prognosis build() throws ParseException { + if(datas.isEmpty()){ + return new EmptyPrognosis(); + } + + int plateform = datas.isNull("plateform") ? 0 : datas.getInt("plateform"); + + DateFormat formatter = new SimpleDateFormat("dd:kk:mm:ss"); + String depatureString = datas.isNull("departure") ? "00:00:00:00" : datas.getString("departure").replace('d',':'); + Time departure = new Time(formatter.parse(depatureString).getTime()); + + String arrivalString = datas.isNull("arrival") ? "00:00:00:00" : datas.getString("arrival").replace('d', ':'); + Time arrival = new Time(formatter.parse(arrivalString).getTime()); + + int capacity1st = datas.isNull("capacity1st") ? 0 : datas.getInt("capacity1st"); + int capacity2nd = datas.isNull("capacity2nd") ? 0 : datas.getInt("capacity2nd"); + + return new Prognosis(plateform, departure, arrival, capacity1st, capacity2nd); + } + } +} diff --git a/src/main/java/ch/hepia/api/transport/Section.java b/src/main/java/ch/hepia/api/transport/Section.java new file mode 100644 index 0000000000000000000000000000000000000000..8214ea5ebf3781a1d8ad3c25e760ee7b8ef85811 --- /dev/null +++ b/src/main/java/ch/hepia/api/transport/Section.java @@ -0,0 +1,126 @@ +package ch.hepia.api.transport; + +import org.json.JSONObject; + +import java.text.ParseException; +import java.io.Serializable; + +public class Section implements Serializable { + private static final long serialVersionUID = 0xAEF37565673L; + private Journey journey; + private double walktime; + private Stop departure; + private Stop arrival; + + /** + * Empty Section + */ + private final static class EmptySection extends Section{ + private static final long serialVersionUID = 0xAEF37565670L; + /** + * Constructor of EmptySection + * An empty Section is a Journey From Geneva to Geneva + * @throws ParseException + */ + private EmptySection() throws ParseException { + super(new Journey.JourneyBuilder(new JSONObject()).build(), 0, + new Stop.StopBuilder(new JSONObject()).build(), + new Stop.StopBuilder(new JSONObject()).build()); + } + } + + + /** + * Constructor of the class Section + * @param journey what Journey this section is part of + * @param walkTime time to get to the Section by walk + * @param departure where the Section will depart from + * @param arrival where the Section will arrive to + */ + private Section(Journey journey, double walkTime, Stop departure, Stop arrival){ + this.journey = journey; + this.walktime = walkTime; + this.departure = departure; + this.arrival = arrival; + } + + public static Section empty() throws ParseException{ + return new EmptySection(); + } + + @Override + public boolean equals(Object o){ + if (this == o) { + return true; + } + if (o == null || o.getClass() != this.getClass()) { + return false; + } + Section s = (Section) o; + return s.journey.equals(this.journey); + } + + /** + * Get the journey this Section is part of + * @return journey + */ + public Journey getJourney(){ + return journey; + } + + /** + * Get the Stop where the Section will depart from + * @return departure + */ + public Stop getDeparture(){ + return departure; + } + + /** + * get the Stop where the Section is headed to + * @return arrival + */ + public Stop getArrival(){ + return arrival; + } + + /** + * Builder of Section objects + */ + public final static class SectionBuilder{ + private JSONObject datas; + + /** + * Constructor of the class SectionBuilder + * Stock Datas from a JSON object given + * @param datas contains datas to construct a Section object + */ + public SectionBuilder(JSONObject datas){ + this.datas = datas; + } + + /** + * Build a Section object from the datas obtained + * @return a new Section object + * @throws ParseException + */ + public Section build() throws ParseException { + if(datas.isEmpty()){ + return new EmptySection(); + } + + JSONObject journeyJSON = datas.isNull("journey") ? new JSONObject() : datas.getJSONObject("journey"); + Journey.JourneyBuilder journey = new Journey.JourneyBuilder(journeyJSON); + + double walktime = datas.isNull("walktime") ? 0 : datas.getDouble("walktime"); + + JSONObject departureJSON = datas.isNull("departure") ? new JSONObject() : datas.getJSONObject("departure"); + Stop.StopBuilder departure = new Stop.StopBuilder(departureJSON); + + JSONObject arrivalJSON = datas.isNull("arrival") ? new JSONObject() : datas.getJSONObject("arrival"); + Stop.StopBuilder arrival = new Stop.StopBuilder(arrivalJSON); + + return new Section(journey.build(), walktime, departure.build(), arrival.build()); + } + } +} diff --git a/src/main/java/ch/hepia/api/transport/Service.java b/src/main/java/ch/hepia/api/transport/Service.java new file mode 100644 index 0000000000000000000000000000000000000000..cf68f9a9fbf873a05c0eb5a03f2aa7be99935e58 --- /dev/null +++ b/src/main/java/ch/hepia/api/transport/Service.java @@ -0,0 +1,65 @@ +package ch.hepia.api.transport; + +import org.json.JSONObject; +import java.io.Serializable; + +public class Service implements Serializable { + private static final long serialVersionUID = 0xACF34565673L; + private String regular; + private String irregular; + + /** + * Empty Service + */ + private final static class EmptyService extends Service{ + private static final long serialVersionUID = 0xACF34565670L; + /** + * Constructor of EmptyService + * No informations given + */ + private EmptyService(){ + super("", ""); + } + } + + /** + * Constructor of Service class + * @param regular is the service regular ? + * @param irregular is the service disturbed ? + */ + private Service(String regular, String irregular){ + this.regular = regular; + this.irregular = irregular; + } + + /** + * Builder of Service objects + */ + public final static class ServiceBuilder{ + private JSONObject datas; + + /** + * Constructor of the class ServiceBuilder + * Stock Datas from a JSON object given + * @param datas contains datas to construct a Service object + */ + public ServiceBuilder(JSONObject datas){ + this.datas = datas; + } + + /** + * Build a Service object from the datas obtained + * @return a new Service object + */ + public Service build(){ + if(datas.isEmpty()){ + return new EmptyService(); + } + + String regular = datas.isNull("regular") ? "" : datas.getString("regular"); + String irregular = datas.isNull("irregular") ? "" : datas.getString("irregular"); + + return new Service(regular, irregular); + } + } +} diff --git a/src/main/java/ch/hepia/api/transport/Stop.java b/src/main/java/ch/hepia/api/transport/Stop.java new file mode 100644 index 0000000000000000000000000000000000000000..910a87672e33de3ebad98a0c31d93039a439dac4 --- /dev/null +++ b/src/main/java/ch/hepia/api/transport/Stop.java @@ -0,0 +1,146 @@ +package ch.hepia.api.transport; + +import org.json.JSONObject; + +import java.sql.Time; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.io.Serializable; + +public class Stop implements Serializable { + private static final long serialVersionUID = 0xABF34565673L; + private Location station; + private Time arrival; + private Time departure; + private int delay; + private int plateform; + private Prognosis prognosis; + + /** + * Default Stop in Geneva + */ + private final static class DefaultStop extends Stop{ + private static final long serialVersionUID = 0xABF34565670L; + /** + * Constructor of DefaultStop + * DefaultStop is located in Geneva + * No Time of departure nor Arrival, no Prognosis, no plateform and no delay + * @throws ParseException + */ + private DefaultStop() throws ParseException { + super(new Location.LocationBuilder(new JSONObject(), "refine").build(), + new Time(new SimpleDateFormat("dd:kk:mm:ss").parse("00:00:00:00").getTime()), + new Time(new SimpleDateFormat("dd:kk:mm:ss").parse("00:00:00:00").getTime()), + 0, 0, + new Prognosis.PrognosisBuilder(new JSONObject()).build()); + } + } + + /** + * Constructor of the Stop class + * @param station Location where the Stop is + * @param arrival time of arrival at this Stop + * @param departure time of departure from this Stop + * @param delay duration time of unpredictated delay + * @param plateform where the Stop will arrive to and depart from + * @param prognosis complementary informations (See also {@link Prognosis}) + */ + private Stop(Location station, Time arrival, Time departure, int delay, int plateform, Prognosis prognosis){ + this.station = station; + this.arrival = arrival; + this.departure = departure; + this.delay = delay; + this.plateform = plateform; + this.prognosis = prognosis; + } + + /** + * Get the Location of the Stop + * @return station + */ + public Location getLocation(){ + return station; + } + + /** + * Get the departure time of the transport from the Stop + * @return departure + */ + public Time getDepartureTime(){ + return departure; + } + + /** + * Get the Arrival time of the transport to the Stop + * @return arrival + */ + public Time getArrivalTime(){ + return arrival; + } + + @Override + public boolean equals(Object o){ + if (this == o) { + return true; + } + if (o == null || o.getClass() != this.getClass()) { + return false; + } + Stop s = (Stop) o; + return s.arrival.equals(this.arrival) && + s.departure.equals(this.departure) && + s.plateform == this.plateform && + s.station.equals(this.station) && + s.delay == this.delay; + } + + /** + * Builder of Stop objects + */ + public final static class StopBuilder{ + private JSONObject datas; + + /** + * Constructor of the class StopBuilder + * Stock Datas from a JSON object given + * @param datas contains datas to construct a Stop object + */ + public StopBuilder(JSONObject datas){ + this.datas = datas; + } + + /** + * Build a Stop object from the datas obtained + * @return a new Stop object + * @throws ParseException + */ + public Stop build() throws ParseException { + if(datas.isEmpty()){ + return new DefaultStop(); + } + + //getting the first key to know the type of the Location + String[] keys = new String[10]; + datas.keySet().toArray(keys); + String type = keys[4]; + JSONObject locationJSON = datas.isNull("location") ? new JSONObject() : datas.getJSONObject("location"); + Location.LocationBuilder location = new Location.LocationBuilder(locationJSON, type); + + DateFormat formatter = new SimpleDateFormat("kk:mm:ss"); + + String arrivalString = datas.isNull("arrival") ? "00:00:00" : datas.getString("arrival").substring(11, datas.getString("arrival").indexOf("+")); + Time arrival = new Time(formatter.parse(arrivalString).getTime()); + String depatureString = datas.isNull("departure") ? "00:00:00" : datas.getString("departure").substring(11, datas.getString("departure").indexOf("+")); + Time departure = new Time(formatter.parse(depatureString).getTime()); + + int delay = datas.isNull("delay") ? 0 : datas.getInt("delay"); + int plateform = datas.isNull("plateform") ? 0 : datas.getInt("plateform"); + + JSONObject prognosisJSON = datas.isNull("prognosis") ? new JSONObject() : new JSONObject(datas.get("prognosis")); + Prognosis.PrognosisBuilder prognosis = new Prognosis.PrognosisBuilder(prognosisJSON); + + return new Stop(location.build(), arrival, departure, delay, plateform, prognosis.build()); + } + } +} diff --git a/src/main/java/ch/hepia/api/transport/Type.java b/src/main/java/ch/hepia/api/transport/Type.java new file mode 100644 index 0000000000000000000000000000000000000000..c3e6db8e20f4bf491d01cb7692091fa4681c68ee --- /dev/null +++ b/src/main/java/ch/hepia/api/transport/Type.java @@ -0,0 +1,5 @@ +package ch.hepia.api.transport; + +public enum Type { + STATION, POI, ADDRESS, REFINE +} diff --git a/src/main/java/ch/hepia/api/weather/CantFetchWeatherException.java b/src/main/java/ch/hepia/api/weather/CantFetchWeatherException.java new file mode 100644 index 0000000000000000000000000000000000000000..d838c2715d3357e9e7abf88839207ada6c3ae1d1 --- /dev/null +++ b/src/main/java/ch/hepia/api/weather/CantFetchWeatherException.java @@ -0,0 +1,7 @@ +package ch.hepia.api.weather; + +import java.io.IOException; + +public class CantFetchWeatherException extends IOException { + private static final long serialVersionUID = 0xDBF34565673L; +} diff --git a/src/main/java/ch/hepia/api/weather/CantReadWeatherException.java b/src/main/java/ch/hepia/api/weather/CantReadWeatherException.java new file mode 100644 index 0000000000000000000000000000000000000000..0cc3b94a726b873776eabe8093f2f1100b3fb063 --- /dev/null +++ b/src/main/java/ch/hepia/api/weather/CantReadWeatherException.java @@ -0,0 +1,7 @@ +package ch.hepia.api.weather; + +import java.io.IOException; + +public class CantReadWeatherException extends IOException { + private static final long serialVersionUID = 0xEBF34565673L; +} diff --git a/src/main/java/ch/hepia/api/weather/Meteo.java b/src/main/java/ch/hepia/api/weather/Meteo.java new file mode 100644 index 0000000000000000000000000000000000000000..32258236835d81681048bb42babd28f1f1eb0c52 --- /dev/null +++ b/src/main/java/ch/hepia/api/weather/Meteo.java @@ -0,0 +1,115 @@ +package ch.hepia.api.weather; + +import ch.hepia.config.AppConfig; +import org.json.JSONObject; + +public class Meteo { + private double temperature; + private String conditions; + + /** + * Constructor of Meteo class to obtain basic information on the weather + * @param temperature La température actuelle + * @param conditions + */ + private Meteo(double temperature, String conditions){ + this.temperature = temperature; + this.conditions = conditions; + } + + /** + * Get the temperature of the Meteo Object + * @return temperature + */ + public double getTemperature(){ return temperature; } + + /** + * Get the weather conditions of the Meteo Object + * @return conditions + */ + public String getConditions(){ return conditions; } + + /** + * Converts a meteo condition into one of our icons + * @return The path to our icon + * @apiNote Aussi appellé le switch des enfers. + * @apiNote Retourne sunny si ne trouve rien (politique du verre à moitié plein) + */ + public String getConditionsIcon(){ + switch (this.conditions) { + case "Stratus": + case "Nuit nuageuse": + case "Fortement nuageux": + case "Développement nuageux": + case "Nuit avec développement nuageux": + return AppConfig.WEATHER_ICON_CLOUDY; + case "Ciel voilé": + case "Nuit légèrement voilée": + case "Faibles passages nuageux": + case "Brouillard": + case "Stratus se dissipant": + case "Nuit claire et stratus": + case "Faiblement nuageux": + return AppConfig.WEATHER_ICON_FOGGY; + case "Averses de pluie faible": + case "Nuit avec averses": + case "Averses de pluie modérée": + case "Averses de pluie forte": + case "Couvert avec averses": + case "Pluie faible": + case "Pluie forte": + case "Pluie modérée": + return AppConfig.WEATHER_ICON_RAINY; + case "Averses de neige faible": + case "Nuit avec averses de neige faible": + case "Neige faible": + case "Neige modérée": + case "Neige forte": + case "Pluie et neige mêlée faible": + case "Pluie et neige mêlée modérée": + case "Pluie et neige mêlée forte": + return AppConfig.WEATHER_ICON_SNOWY; + case "Faiblement orageux": + case "Nuit faiblement orageuse": + case "Orage modéré": + case "Fortement orageux": + return AppConfig.WEATHER_ICON_STORMY; + case "Ensoleillé": + case "Nuit claire": + case "Nuit bien dégagée": + case "Eclaircies": + default: + return AppConfig.WEATHER_ICON_SUNNY; + } + } + + /** + * Builder of Meteo Object + */ + public static class MeteoBuilder{ + private JSONObject datas; + + /** + * Constructor of the class MeteoBuilder + * Stock Datas from a JSON object given + * @param datas contains datas to construct a Meteo object + */ + public MeteoBuilder(JSONObject datas){ + this.datas = datas; + } + + /** + * Build a Meteo object from the datas obtained + * @return a new Meteo object + * @throws CantReadWeatherException + */ + public Meteo build() throws CantReadWeatherException { + double temperature = datas.isNull("tmp") ? -2000 : datas.getDouble("tmp"); + String conditions = datas.getString("condition"); + if(temperature != -2000 && conditions != null){ + return new Meteo(temperature, conditions); + } + throw new CantReadWeatherException(); + } + } +} diff --git a/src/main/java/ch/hepia/api/weather/WeatherAPI.java b/src/main/java/ch/hepia/api/weather/WeatherAPI.java new file mode 100644 index 0000000000000000000000000000000000000000..cfc6bd35f4140c07db51f9e7e876f5aca51fdfd7 --- /dev/null +++ b/src/main/java/ch/hepia/api/weather/WeatherAPI.java @@ -0,0 +1,52 @@ +package ch.hepia.api.weather; + +import ch.hepia.api.HTTPUtils; +import ch.hepia.api.transport.Coordinates; +import org.json.JSONObject; + +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + +public class WeatherAPI { + private String baseURL = "https://www.prevision-meteo.ch/services/json/"; + + /** + * Processes the data received by the API + * @param path The URL to process + * @return The processed data + * @throws IOException When the processed data cannot be processed. + */ + private Meteo processData(String path) throws IOException { + String content = HTTPUtils.getContent(path); + + JSONObject informations = new JSONObject(content); + if(informations.isNull("errors")) { + return new Meteo.MeteoBuilder(informations.getJSONObject("current_condition")).build(); + } + throw new CantFetchWeatherException(); + } + + /** + * Get the informations on the weather in the location searched + * @param city where we want to display the weather from + * @return a Meteo Object + * @throws IOException + */ + public Meteo getWeatherFrom(String city) throws IOException { + String path = baseURL + URLEncoder.encode(city, StandardCharsets.UTF_8); + return processData(path); + } + + /** + * Get the informations on the weather in the location searched + * @param coordinates where we want to display the weather from + * @return a Meteo Object + * @throws IOException + */ + public Meteo getWeatherFrom(Coordinates coordinates) throws IOException { + String path = baseURL + "lat="+coordinates.getX()+"lng="+coordinates.getY(); + return processData(path); + } + +} diff --git a/src/main/java/ch/hepia/config/AppConfig.java b/src/main/java/ch/hepia/config/AppConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..56bc22c8f721de82cf45e514499b8a23766aab2f --- /dev/null +++ b/src/main/java/ch/hepia/config/AppConfig.java @@ -0,0 +1,67 @@ +package ch.hepia.config; + +import javafx.scene.paint.Color; + +import java.util.List; + +/** + * The variables of the app + */ +public final class AppConfig { + public static final String APP_NAME = "TransportWave"; + public static final Integer APP_WIDTH = 1000; + public static final Integer APP_HEIGHT = 565; + + public static final Integer APP_MAIN_VIEW_WIDTH = 622; + + public static final String RABBITMQ_HOSTNAME = "redgrave.science"; + public static final String RABBITMQ_USERNAME = "frog"; + public static final String RABBITMQ_PASSWORD = "poney1234"; + public static final String RABBITMQ_EXCHANGE = "broadcaster"; + public static final Integer RABBITMQ_PORT = 5672; // The MQ uses the default port, so this value is not used. + + public static final String WEATHER_DESTINATION = "Météo à destination"; + + public static final String ERROR_API_WEATHER = "Impossible de contacter les services météo."; + public static final String ERROR_API_UNREACHABLE = "Impossible de contacter les services de transport Suisses."; + public static final String ERROR_API_MQ = "Une erreur s'est produite lors de la publication de cet évènement."; + public static final String DEFAULT_JOURNEY_TEXT = "Vous n'avez prévu aucun voyage pour le moment."; + + public static final String COMMON_ITINERARY_TEXT = "Vous allez croiser %s à %s ! Pensez à vous saluer !"; + + public static List<String> CHAT_COMMANDS = List.of( + "help", + "block", + "unblock", + "blacklist" + ); + + /** + * Resources + */ + public static final String CHAT_TRAIN_ICON = "/img/train.png"; + public static final String CHAT_TRAIN_ICON_SELF = "/img/train_self.png"; + public static final String CHAT_MESSAGE_ICON = "/img/bubble.png"; + public static final String CHAT_MESSAGE_ICON_SELF = "/img/bubble_self.png"; + public static final String HELP_MESSAGE_ICON = "/img/help.png"; + public static final String WEATHER_ICON_CLOUDY = "/img/cloudy.png"; + public static final String WEATHER_ICON_FOGGY = "/img/foggy.png"; + public static final String WEATHER_ICON_RAINY = "/img/rainy.png"; + public static final String WEATHER_ICON_SNOWY = "/img/snowy.png"; + public static final String WEATHER_ICON_STORMY = "/img/stormy.png"; + public static final String WEATHER_ICON_SUNNY = "/img/sunny.png"; + public static final String JOURNEY_ICON_COMMON_ITINERARY = "/img/friends.png"; + public static final String EASTER_EGG_23DBM = "/img/javafx_res1"; + public static final String EASTER_EGG_CAFE = "/img/javafx_res2"; + public static final String EASTER_EGG_EFFET_DE_BORD = "/img/javafx_res3"; + public static final String EASTER_EGG_SCRUM = "/img/javafx_res4"; + + /** + * Style + */ + public static final Color COLOR_BLUE_10_OPACITY = Color.color(0.207, 0.694, 0.933, 0.1); + public static final Color COLOR_BLUE_50_OPACITY = Color.color(0.207, 0.694, 0.933, 0.5); + public static final Color COLOR_BLUE_100_OPACITY = Color.color(0.207, 0.694, 0.933); + + public static final Color COLOR_GREEN_20_OPACITY = Color.color(1.0, 0.9, 0.5, 0.2); +} diff --git a/src/main/java/ch/hepia/config/AppContext.java b/src/main/java/ch/hepia/config/AppContext.java new file mode 100644 index 0000000000000000000000000000000000000000..f1db3272275c6b9bf947cdac9ae1f52cb8e800a1 --- /dev/null +++ b/src/main/java/ch/hepia/config/AppContext.java @@ -0,0 +1,47 @@ +package ch.hepia.config; + +import ch.hepia.models.User; +import ch.hepia.mq.MessageManager; + +import java.util.Optional; + +/** + * Represents the current config of the app. + */ +public class AppContext { + private MessageManager messageManager; + private Optional<User> user; + + /** + * Main constructor + * @param messageManager the message manager instance + */ + public AppContext(MessageManager messageManager){ + this.messageManager = messageManager; + this.user = Optional.empty(); + } + + /** + * Gets the message manager + * @return The message manager + */ + public MessageManager getMessageManager() { + return messageManager; + } + + /** + * Gets the user + * @return The current user + */ + public Optional<User> getUser() { + return user; + } + + /** + * Sets the user + * @param user the user + */ + public void setUser(User user) { + this.user = Optional.of(user); + } +} diff --git a/src/main/java/ch/hepia/events/ChatMessage.java b/src/main/java/ch/hepia/events/ChatMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..8fd7251a180c88081caa8b14d785453d3345594a --- /dev/null +++ b/src/main/java/ch/hepia/events/ChatMessage.java @@ -0,0 +1,32 @@ +package ch.hepia.events; + +import ch.hepia.models.User; + +import java.io.Serializable; + +/** + * Implement a chat message event + */ +public class ChatMessage implements Event, Serializable { + private static final long serialVersionUID = 0xAEF34565631L; + private User user; + private String chatMessage; + + /** + * Main constructor + * @param user The user + * @param chatMessage The message + */ + public ChatMessage(User user, String chatMessage) { + this.user = user; + this.chatMessage = chatMessage; + } + + public User getUser() { + return this.user; + } + + public String getMessage() { + return this.chatMessage; + } +} \ No newline at end of file diff --git a/src/main/java/ch/hepia/events/Event.java b/src/main/java/ch/hepia/events/Event.java new file mode 100644 index 0000000000000000000000000000000000000000..8504935660b525040bac4cf5d05fbad904a2a146 --- /dev/null +++ b/src/main/java/ch/hepia/events/Event.java @@ -0,0 +1,14 @@ +package ch.hepia.events; + +import ch.hepia.models.User; + +/** + * Represents an event send into the MQ. + */ +public interface Event { + /** + * Gets the user + * @return the user + */ + User getUser(); +} diff --git a/src/main/java/ch/hepia/events/JoinedJourney.java b/src/main/java/ch/hepia/events/JoinedJourney.java new file mode 100644 index 0000000000000000000000000000000000000000..251ab55e9c3539888d4e66eb75c79783ac418f12 --- /dev/null +++ b/src/main/java/ch/hepia/events/JoinedJourney.java @@ -0,0 +1,60 @@ +package ch.hepia.events; + +import ch.hepia.api.transport.Connection; +import ch.hepia.models.User; + +import java.io.Serializable; +import java.util.Objects; + +/** + * Represents an event where a user has selected (and joined) a journey. A + * journey is composed of several sections (Genève-Vauderens : Genève-Morges is + * the first section, Morges-Vauderens is the second.) + */ +public class JoinedJourney implements Event, Serializable { + private static final long serialVersionUID = 0xAEF34565673L; + private User user; + private Connection connection; + private String weatherToDestination; + + /** + * Main constructor + * + * @param user The user triggering the event + * @param connection The sections of the journey + */ + public JoinedJourney(User user, Connection connection, String weatherToDestination) { + this.user = user; + this.connection = connection; + this.weatherToDestination = weatherToDestination; + } + + public User getUser() { + return this.user; + } + + public Connection getConnection() { + return this.connection; + } + + public String getWeatherToDestination(){ + return weatherToDestination; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || obj.getClass() != this.getClass()) { + return false; + } + JoinedJourney jj = (JoinedJourney) obj; + return jj.connection.equals(this.connection) && jj.user.equals(this.user); + } + + @Override + public int hashCode() { + return Objects.hash(this.user, this.connection); + } +} diff --git a/src/main/java/ch/hepia/events/LeftJourney.java b/src/main/java/ch/hepia/events/LeftJourney.java new file mode 100644 index 0000000000000000000000000000000000000000..ee30b6185995a2992ddacab5d16bf4303270b6eb --- /dev/null +++ b/src/main/java/ch/hepia/events/LeftJourney.java @@ -0,0 +1,41 @@ +package ch.hepia.events; + +import ch.hepia.api.transport.Connection; +import ch.hepia.models.User; + +import java.io.Serializable; + +/** + * Represents an event where a user has left a journey. + * A journey is composed of several sections + * (Genève-Vauderens : Genève-Morges is the first section, Morges-Vauderens is the second.) + */ +public class LeftJourney implements Event, Serializable { + private static final long serialVersionUID = 0xAEF34565674L; + private User user; + private Connection connection; + private String weatherToDestination; + + /** + * Main constructor + * @param user The user triggering the event + * @param connection The sections of the journey + */ + public LeftJourney(User user, Connection connection, String weatherToDestination){ + this.user = user; + this.connection = connection; + this.weatherToDestination = weatherToDestination; + } + + public User getUser() { + return this.user; + } + + public Connection getConnection() { + return this.connection; + } + + public String getWeatherToDestination(){ + return weatherToDestination; + } +} diff --git a/src/main/java/ch/hepia/events/Meeting.java b/src/main/java/ch/hepia/events/Meeting.java new file mode 100644 index 0000000000000000000000000000000000000000..9d56620c096fed346a9ec74da3b6c9600f4b9b45 --- /dev/null +++ b/src/main/java/ch/hepia/events/Meeting.java @@ -0,0 +1,52 @@ +package ch.hepia.events; + +import ch.hepia.api.transport.Section; +import ch.hepia.models.User; + +import java.io.Serializable; + +/** + * Implements a meeting event, that is when two users meet somewhere. + */ +public class Meeting implements Serializable, Event { + private static final long serialVersionUID = 0xAEF34565679L; + private User user; // + private User partner; + private Section section; + + + /** + * Main constructor + * @param userOne The first user to meet the other + * @param userTwo The other user to meet the first one + * @param section The section where both meet each other + */ + public Meeting(User userOne, User userTwo, Section section){ + this.user = userOne; + this.partner = userTwo; + this.section = section; + } + + + @Override + public User getUser() { + return user; + } + + /** + * Gets the user two + * @return The second user + */ + public User getPartner(){ + return partner; + } + + /** + * Gets the section where the two users meet + * @return The section where the two users meet + */ + public Section getSection(){ + return section; + } + +} diff --git a/src/main/java/ch/hepia/models/User.java b/src/main/java/ch/hepia/models/User.java new file mode 100644 index 0000000000000000000000000000000000000000..8f70f2ce79f740be850df054f5641414aadee3ad --- /dev/null +++ b/src/main/java/ch/hepia/models/User.java @@ -0,0 +1,72 @@ +package ch.hepia.models; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +/** + * Represents an user. + */ +public class User implements Serializable { + private static final long serialVersionUID = 0xAEF34564613L; + private String username; + private transient List<User> ignoredUserList; + + /** + * Main constructor + * @param username the username + */ + public User(String username){ + this.username = username; + this.ignoredUserList = new ArrayList<>(); + } + + /** + * Add a user to the ignored list + * @param user User to add + */ + public void addIgnoredUser(User user) { + this.ignoredUserList.add(user); + } + + /** + * Remove a user to the ignored list + * @param user User to remove + */ + public void removeIgnoredUser(User user) { + this.ignoredUserList.remove(user); + } + + /** + * Get the ignored list + * @return The Ingored user list to remove + */ + public List<User> getIgnoredUserList(){ + return this.ignoredUserList; + } + + /** + * Get the name + * @return The username + */ + public String getName(){ + return this.username; + } + + @Override + public String toString(){ + return this.username; + } + + @Override + public boolean equals(Object o) { + if((o == null) || (this.getClass() != o.getClass())){ + return false; + } else { + User u = (User)o; + if (this.username.equals(u.username)){ + return true; + } + } + return false; + } +} diff --git a/src/main/java/ch/hepia/mq/Message.java b/src/main/java/ch/hepia/mq/Message.java new file mode 100644 index 0000000000000000000000000000000000000000..a2a23c37e44ed0c85a1f8b133de8f4231ff028d3 --- /dev/null +++ b/src/main/java/ch/hepia/mq/Message.java @@ -0,0 +1,39 @@ +package ch.hepia.mq; +import java.io.Serializable; + +/** + * Represents a message sent to the message queue. + */ +public final class Message implements Serializable { + public static enum Type { + JoinedJourney, LeftJourney, ChatMessage, Meeting + } + + private static final long serialVersionUID = 0xAEF34565673L; + private final Type type; + private byte[] data; + + /** + * Standard constructor + * @param type The type of the message + * @param object Payload to serialize + */ + public <T extends Serializable> Message(Type type, T object) { + this.type = type; + this.data = MessageQueue.serialize(object); + } + /** + * Get the message type + * @return The type of the message + */ + public Type getMessageType() { + return this.type; + } + /** + * Get the payload of the message + * @return The payload of the message + */ + public <T> T getData() { + return MessageQueue.unserialize(this.data); + } +} diff --git a/src/main/java/ch/hepia/mq/MessageManager.java b/src/main/java/ch/hepia/mq/MessageManager.java new file mode 100644 index 0000000000000000000000000000000000000000..541cc700ee22ae8ca631af8c4ac566b679e0b8ce --- /dev/null +++ b/src/main/java/ch/hepia/mq/MessageManager.java @@ -0,0 +1,155 @@ +package ch.hepia.mq; + +import ch.hepia.events.ChatMessage; +import ch.hepia.events.JoinedJourney; +import ch.hepia.events.LeftJourney; +import ch.hepia.events.Meeting; + +import java.io.Serializable; +import java.util.function.Consumer; +import java.util.function.Predicate; + +/** + * Represents the wrapper for the message queue. + */ +public class MessageManager extends MessageQueue { + + + public MessageManager(String host, String username, String password, String exchange) throws Exception { + super(host, username, password, exchange); + } + + /* + * Private functions + */ + + /** + * Send an event to the queue + * @param type The type of the message + * @param event The event + */ + private <T extends Serializable> void sendEvent(Message.Type type, T event) { + Message m = new Message(type, event); + try { + this.sendBytes(MessageQueue.serialize(m)); + } catch (final Exception e) { + e.printStackTrace(); + } + } + + /** + * Add a consumer for a specific type of message + * @param type The type of the message + * @param eventHandler Consumer to add + */ + private <T extends Serializable> void conditionalSubscribe(Message.Type type, Consumer<T> eventHandler, + Predicate<T> condition) { + Consumer<byte[]> consumer = (bytes) -> { + Message receivedMessage = MessageQueue.unserialize(bytes); + if (receivedMessage.getMessageType() == type) { + T event = receivedMessage.getData(); + if (condition.test(event)) { + eventHandler.accept(event); + } + } + }; + this.addConsumer(consumer); + } + + /* + * Public functions + */ + + /** + * Subscribe to all JoinedJourney events + * @param eventHandler JoinedJourney consumer + */ + public void subscribeJoinedJourney(Consumer<JoinedJourney> eventHandler) { + this.conditionalSubscribeJoinedJourney(eventHandler, (event) -> true); + } + + /** + * Subscribe to all LeftJourney events + * @param eventHandler LeftJourney consumer + */ + public void subscribeLeftJourney(Consumer<LeftJourney> eventHandler) { + this.conditionalSubscribeLeftJourney(eventHandler, (event) -> true); + } + + /** + * Subscribe to all ChatMessage events + * @param eventHandler ChatMessage consumer + */ + public void subscribeChatMessage(Consumer<ChatMessage> eventHandler) { + this.conditionalSubscribeChatMessage(eventHandler, (event) -> true); + } + + /** + * Subscribe to JoinedJourney events validating the condition + * @param eventHandler JoinedJourney consumer + * @param condition JoinedJourney predicate + */ + public void conditionalSubscribeJoinedJourney(Consumer<JoinedJourney> eventHandler, + Predicate<JoinedJourney> condition) { + this.conditionalSubscribe(Message.Type.JoinedJourney, eventHandler, condition); + } + + /** + * Subscribe to LeftJourney events validating the condition + * @param eventHandler LeftJourney consumer + * @param condition LeftJourney predicate + */ + public void conditionalSubscribeLeftJourney(Consumer<LeftJourney> eventHandler, Predicate<LeftJourney> condition) { + this.conditionalSubscribe(Message.Type.LeftJourney, eventHandler, condition); + } + + /** + * Subscribe to ChatMessage events validating the condition + * @param eventHandler ChatMessage consumer + * @param condition ChatMessage predicate + */ + public void conditionalSubscribeChatMessage(Consumer<ChatMessage> eventHandler, Predicate<ChatMessage> condition) { + this.conditionalSubscribe(Message.Type.ChatMessage, eventHandler, condition); + } + + /** + * Subscribe to Meetings events + * @param eventHandler Meeting consumer + * @param condition Meeting predicate + */ + public void conditionalSubscribeMeeting(Consumer<Meeting> eventHandler, Predicate<Meeting> condition) { + this.conditionalSubscribe(Message.Type.Meeting, eventHandler, condition); + } + + /** + * Send a JoinedJourney event + * @param event JoinedJourney event + */ + public void sendJoinedJourney(JoinedJourney event) { + this.sendEvent(Message.Type.JoinedJourney, event); + } + + /** + * Send a LeftJourney event + * @param event LeftJourney event + */ + public void sendLeftJourney(LeftJourney event) { + this.sendEvent(Message.Type.LeftJourney, event); + } + + /** + * Send a ChatMessage event + * @param event ChatMessage event + */ + public void sendChatMessage(ChatMessage event) { + this.sendEvent(Message.Type.ChatMessage, event); + } + + /** + * Send a Meeting event + * @param event Meeting event + */ + public void sendMeeting(Meeting event) { + this.sendEvent(Message.Type.Meeting, event); + } +} \ No newline at end of file diff --git a/src/main/java/ch/hepia/mq/MessageQueue.java b/src/main/java/ch/hepia/mq/MessageQueue.java new file mode 100644 index 0000000000000000000000000000000000000000..c6823d7c892282fecc1447d684039e9057a02a3e --- /dev/null +++ b/src/main/java/ch/hepia/mq/MessageQueue.java @@ -0,0 +1,111 @@ +package ch.hepia.mq; + +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; +import com.rabbitmq.client.DeliverCallback; + +import java.io.*; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +/** + * Implementation of the message queue. + */ +public abstract class MessageQueue { + private final String exchange; + private final Connection mqConnection; + private final Channel mqChannel; + private final List<Consumer<byte[]>> consumers; + + /** + * Construct a temporary RabbitMQ queue, bind it to an exchange + * on the server + * @param host Hostname of the rabbitmq server + * @param username RabbitMQ user + * @param password RabbitMQ password + * @param exchange Exchange to bind + */ + public MessageQueue(String host, String username, String password, String exchange) throws Exception { + this.consumers = new ArrayList<>(); + this.exchange = exchange; + ConnectionFactory queueConnector = new ConnectionFactory(); + queueConnector.setHost(host); + queueConnector.setUsername(username); + queueConnector.setPassword(password); + + this.mqConnection = queueConnector.newConnection(); + this.mqChannel = this.mqConnection.createChannel(); + String queueName = this.mqChannel.queueDeclare().getQueue(); + this.mqChannel.queueBind(queueName, this.exchange, ""); + DeliverCallback deliverCallback = (consumerTag, delivery) -> { + for (Consumer<byte[]> consumer : this.consumers) { + consumer.accept(delivery.getBody()); + } + }; + this.mqChannel.basicConsume(queueName, true, deliverCallback, consumerTag -> { + }); + } + + /** + * Add a consumer to the queue + * @param messageHandler Message consumer + */ + protected void addConsumer(Consumer<byte[]> messageHandler) { + this.consumers.add(messageHandler); + } + + /** + * Send a byte array in the queue. + * @param bytes The byte array + */ + protected void sendBytes(byte[] bytes) throws Exception { + this.mqChannel.basicPublish(this.exchange, "", null, bytes); + } + + /** + * Terminate the connection with rabbitMQ + */ + public void close() throws Exception { + this.mqChannel.close(); + this.mqConnection.close(); + } + + /** + * Serialize an object, return a byte array. + * @param object Object to serialize + */ + public static <T extends Serializable> byte[] serialize(T object){ + try ( + ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(byteStream) + ) { + oos.writeObject(object); + oos.flush(); + return byteStream.toByteArray(); + } catch (final Exception e) { + e.printStackTrace(); + System.exit(1); + } + return null; + } + + /** + * Deserialize a byte array + * @param serializedData serialized object + */ + public static <T extends Serializable> T unserialize(byte[] serializedData){ + T result = null; + try ( + ByteArrayInputStream inputStream = new ByteArrayInputStream(serializedData); + ObjectInputStream ois = new ObjectInputStream(inputStream) + ) { + result = (T) ois.readObject(); + } catch (final Exception e) { + e.printStackTrace(); + System.exit(1); + } + return result; + } +} \ No newline at end of file diff --git a/src/main/java/ch/hepia/ui/ConnectionController.java b/src/main/java/ch/hepia/ui/ConnectionController.java new file mode 100644 index 0000000000000000000000000000000000000000..9c86154d3b1df55d5eb326da8ddefd64f59bfabd --- /dev/null +++ b/src/main/java/ch/hepia/ui/ConnectionController.java @@ -0,0 +1,84 @@ +package ch.hepia.ui; + +import ch.hepia.Main; +import ch.hepia.config.AppConfig; +import ch.hepia.models.User; +import javafx.animation.FadeTransition; +import javafx.application.Platform; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.fxml.Initializable; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.TextField; +import javafx.stage.Stage; +import javafx.util.Duration; + +import java.net.URL; +import java.util.ResourceBundle; + +/** + * Implements the login screen + */ +public class ConnectionController implements Initializable { + + @FXML + private Label appNameLabel; + + @FXML + private Label appConnectionStatusLabel; + + @FXML + private TextField usernameSelectionTextField; + + @FXML + private Label errorLabel; + + @FXML + private Button handleConfirmButton; + + @FXML + public void handleConfirmButton(ActionEvent event) throws Exception { + handleConfirmButton.setDisable(true); + String oldText = handleConfirmButton.getText(); + handleConfirmButton.setText("Veuillez patienter..."); + if (usernameSelectionTextField.getText().isEmpty()){ + FadeTransition fadeIn = new FadeTransition(Duration.millis(3000)); + errorLabel.setText("Veuillez rentrer un nom d'utilisateur."); + errorLabel.setVisible(true); + fadeIn.setNode(errorLabel); + fadeIn.setFromValue(1.0); + fadeIn.setToValue(0.0); + fadeIn.setCycleCount(1); + fadeIn.setAutoReverse(false); + fadeIn.playFromStart(); + } else { + User user = new User(usernameSelectionTextField.getText()); + Main.getContext().setUser(user); + Stage stage = (Stage) handleConfirmButton.getScene().getWindow(); + Parent root = FXMLLoader.load(Main.class.getResource("/fxml/MainWindow.fxml")); + Scene scene = new Scene(root, AppConfig.APP_WIDTH, AppConfig.APP_HEIGHT); + + stage.close(); + stage.setTitle(AppConfig.APP_NAME); + stage.setScene(scene); + stage.show(); + + + } + handleConfirmButton.setText(oldText); + handleConfirmButton.setDisable(false); + } + + @Override + public void initialize(URL location, ResourceBundle resources) { + // Faire les initialisations avant affichage ici. + appNameLabel.setText(AppConfig.APP_NAME); + appConnectionStatusLabel.setText("Merci de rentrer votre nom. Il permettra de vous identifier."); + Platform.runLater(() -> usernameSelectionTextField.requestFocus()); + UiUtils.buttonWhenEnter(usernameSelectionTextField, handleConfirmButton); + } +} diff --git a/src/main/java/ch/hepia/ui/MainWindowController.java b/src/main/java/ch/hepia/ui/MainWindowController.java new file mode 100644 index 0000000000000000000000000000000000000000..579bd51f070c506c72a9a6b246b669df44b75b43 --- /dev/null +++ b/src/main/java/ch/hepia/ui/MainWindowController.java @@ -0,0 +1,612 @@ +package ch.hepia.ui; + +import ch.hepia.Main; +import ch.hepia.api.transport.*; +import ch.hepia.api.weather.WeatherAPI; +import ch.hepia.config.AppConfig; +import ch.hepia.config.AppContext; +import ch.hepia.events.*; +import ch.hepia.models.User; +import javafx.animation.TranslateTransition; +import javafx.application.Platform; +import javafx.fxml.FXML; +import javafx.fxml.Initializable; +import javafx.geometry.Insets; +import javafx.scene.canvas.Canvas; +import javafx.scene.canvas.GraphicsContext; +import javafx.scene.control.*; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.*; +import javafx.scene.paint.Color; +import javafx.scene.paint.CycleMethod; +import javafx.scene.paint.LinearGradient; +import javafx.scene.paint.Stop; +import javafx.util.Duration; + +import javax.sound.sampled.AudioInputStream; +import javax.sound.sampled.AudioSystem; +import javax.sound.sampled.Clip; +import java.io.IOException; +import java.net.URL; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.ResourceBundle; +import java.util.function.Predicate; + +/** + * Implements all of the logic, UI and events for the main window. + * It is heavy but it's the price for a nice UI + */ +public class MainWindowController implements Initializable { + + @FXML + private Label currentJourneyLabel; + + @FXML + private Label startStopLabel; + + @FXML + private ComboBox<String> originComboBox; + + @FXML + private ComboBox<String> destinationComboBox; + + @FXML + private Canvas connectionCanvas; + + @FXML + private Button searchOriginButton; + + @FXML + private Button searchDestinationButton; + + @FXML + private Button launchItineraryButton; + + @FXML + private Button sendMessageButton; + + @FXML + private TextField messageTextBox; + + @FXML + private Pane chatContainer; + + @FXML + private Pane connectionContainer; + + // OWN ATTRIBUTES + + private List<Connection> displayedConnections; + + private Connection currentJourney; + + /** + * Shows a sad message when the API/app crashes. + */ + private void showSadMessage(String text){ + UiUtils.dialog(Alert.AlertType.ERROR, "Erreur", + "Une erreur est survenue.", text); + } + + /** + * Searches stops that matches the query. + * @param newValue The stop name query. + * @param transportApi The API to connect to + * @param target The combo box where to put the results. + */ + private void searchStops(String newValue, LinkAPI transportApi, ComboBox<String> target){ + if (!newValue.isEmpty() && newValue.length() > 3 && !target.getItems().contains(newValue)){ + try { + List<Location> locations = transportApi.getStations(newValue); + ArrayList<String> results = new ArrayList<>(); + locations.forEach((location) -> results.add(location.getName())); + target.getItems().clear(); + target.getItems().addAll(results); + target.show(); + } catch (IOException e) { + showSadMessage(AppConfig.ERROR_API_UNREACHABLE); + } + } + } + + private void drawWeather(Connection connection) { + Coordinates coords = connection.getTo().getLocation().getCoordinates(); + WeatherAPI api = new WeatherAPI(); + try { + String wth = api.getWeatherFrom(coords).getConditionsIcon(); + Image weatherImg = new Image(Main.class.getResourceAsStream(wth)); + ImageView image = new ImageView(weatherImg); + image.setTranslateX(580); + image.setTranslateY(30); + connectionContainer.getChildren().add(image); + GraphicsContext gcx = connectionCanvas.getGraphicsContext2D(); + gcx.setFill(Color.BLACK); + gcx.fillText(AppConfig.WEATHER_DESTINATION, 450, 10); + } catch (IOException e) { + e.printStackTrace(); + showSadMessage(AppConfig.ERROR_API_WEATHER); + } + } + + + /** + * Draws a connection between two places. Basically lines. + * @param connection The connection to draw + * @param x Where to draw + * @param y Where to draw but vertically + */ + private void drawConnection(Connection connection, int x, int y){ + List<Section> sections = connection.getSections(); + GraphicsContext gcx = connectionCanvas.getGraphicsContext2D(); + drawWeather(connection); + //Draw starting station + gcx.setFill(Color.RED); + gcx.fillText(sections.get(0).getDeparture().getLocation().getName(), x, y - 20); + gcx.setFill(Color.BLACK); + gcx.fillText(sections.get(0).getDeparture().getDepartureTime().toString(), x, y - 40); + gcx.setFill(Color.RED); + gcx.fillOval(x - 5, y - 5, 10, 10); + gcx.strokeLine(x, y, x + 622 / (sections.size() + 1), y); + for (int i = 0; i < sections.size(); i++){ + //Draw each step + drawConnectionStep(sections.get(i), sections.size(), x, y, i); + } + } + /** + * Draws a connection step between two places. + * @param section The section to draw + * @param size Number of section in the connection + * @param x Where to draw + * @param y Where to draw but vertically + * @param cnt Section position in connection list. + */ + private void drawConnectionStep(Section section, int size, int x, int y, int cnt) { + GraphicsContext gcx = connectionCanvas.getGraphicsContext2D(); + int xSize = (AppConfig.APP_MAIN_VIEW_WIDTH / (size + 1)); + gcx.strokeLine( x + xSize * (cnt + 1), y, AppConfig.APP_MAIN_VIEW_WIDTH / (size + 1), y); + gcx.setFill(Color.RED); + gcx.fillOval(x + xSize * (cnt + 1) - 5, y - 5, 10, 10); + gcx.fillText(section.getArrival().getLocation().getName(), x + xSize * (cnt + 1), y - 20); + gcx.setFill(Color.BLACK); + gcx.fillText(section.getArrival().getArrivalTime().toString(), x + xSize * (cnt + 1), y - 40); + Journey jrn = section.getJourney(); + String transportType = "MARCHE"; //Draw the type of transport used on previous step + if (!(jrn instanceof Journey.EmptyJourney)) { + transportType = jrn.getCategory() + ", " + jrn.getNumber(); + } + gcx.setFill(Color.BLUE); + gcx.fillText(transportType,x + xSize * (cnt), y - 55); + gcx.setFill(Color.RED); + } + + /** + * Draws a newly received message + * @param message The message to draw + */ + private void drawMessage(String message, String image, Color color){ + Pane p = new Pane(); + setChatPanelStyle(p, color); + Image lblImg = new Image(Main.class.getResourceAsStream(image)); + Label msg = new Label(); + msg.setWrapText(true); + msg.setStyle("-fx-padding: 8px"); + msg.setText(message); + msg.setMaxWidth(310); + msg.setGraphic(new ImageView(lblImg)); + p.getChildren().add(msg); + if (chatContainer.getChildren().size() >= 7) { + chatContainer.getChildren().remove(0); + } + insertMessageIntoQueue(); + chatContainer.getChildren().add(p); + } + + /** + * Style a chat panel before pushing + * @param p Panel to style + * @param color Background color of the gradient + */ + private void setChatPanelStyle(Pane p, Color color) { + p.setBackground( + new Background( + new BackgroundFill( + new LinearGradient(0, 0, 0, 1, true, + CycleMethod.NO_CYCLE, + new Stop(1, color), + new Stop(0, Color.WHITE) + ), + new CornerRadii(5), + Insets.EMPTY))); + p.setBorder(new Border(new BorderStroke( + Color.color(0.6, 0.6, 0.6), BorderStrokeStyle.SOLID, new CornerRadii(5), + BorderWidths.DEFAULT))); + p.setPrefWidth(312); + p.setMaxWidth(320); + } + + /** + * Style a connection panel + * @param p Panel to style + */ + private void setConnectionPanelStyle(Pane p, Color color) { + p.setBackground( + new Background( + new BackgroundFill( + new LinearGradient(0, 0, 0, 1, true, + CycleMethod.NO_CYCLE, + new Stop(1, color), + new Stop(0, Color.TRANSPARENT) + ), + new CornerRadii(5), + Insets.EMPTY))); + p.setBorder(new Border(new BorderStroke(Color.color(0.6, 0.6, 0.6), BorderStrokeStyle.SOLID, + new CornerRadii(5), BorderWidths.DEFAULT))); + p.setPrefWidth(AppConfig.APP_MAIN_VIEW_WIDTH + 10); + p.setPrefHeight(90); + } + + /** + * Wrapper for local chat message + * @param message Message to print + */ + private void drawHelpMessage(String message) { + drawMessage(message, AppConfig.HELP_MESSAGE_ICON, Color.color(1, 0.9, 0.5, 0.3)); + } + + /** + * Print available commands in the chat + */ + private void drawCommands(){ + StringBuilder buffer = new StringBuilder("/"+AppConfig.CHAT_COMMANDS.get(0)); + for(int i = 1; i < AppConfig.CHAT_COMMANDS.size(); i++){ + buffer.append(", /").append(AppConfig.CHAT_COMMANDS.get(i)); + } + drawHelpMessage(buffer.toString()); + } + + /** + * Moves the older messages downwards + */ + private void insertMessageIntoQueue(){ + for (int i = 0; i < chatContainer.getChildren().size(); i++){ + Pane sp = (Pane) chatContainer.getChildren().get(i); + TranslateTransition t = new TranslateTransition(Duration.seconds(0.25), sp); + + t.setToY((chatContainer.getChildren().size() - i) * (sp.getHeight() + 4)); + t.play(); + } + } + + /** + * Leaves a journey and broadcast it + * @param app The app context + */ + private void leaveJourney(AppContext app, String weatherIcon){ + app.getMessageManager().sendLeftJourney(new LeftJourney(app.getUser().get(), currentJourney, weatherIcon)); + currentJourneyLabel.setText(AppConfig.DEFAULT_JOURNEY_TEXT); + try { + currentJourney = new Connection.EmptyConnection(); + } catch (ParseException e){ + showSadMessage("Quelque chose s'est mal passé en voulant quitter votre trajet."); + e.printStackTrace(); + } + currentJourneyLabel.setUnderline(false); + } + + /** + * Handle current connection UI reactivity events + * @param app The app context + * @param api The Weather API + * @throws IOException Whenever the Weather API goofs up + */ + private void setupCurrentConnection(AppContext app, WeatherAPI api) throws IOException { + String wtcd = api.getWeatherFrom(currentJourney.getTo().getLocation().getCoordinates()).getConditionsIcon(); + currentJourneyLabel.setText("Vous voyagez de " + currentJourney.getFrom().getLocation().getName() + " vers " + + currentJourney.getTo().getLocation().getName() + ". Cliquez ici pour le quitter."); + currentJourneyLabel.setUnderline(true); + currentJourneyLabel.setOnMouseClicked(event -> { + Alert alertQuit = new Alert(Alert.AlertType.CONFIRMATION); + alertQuit.setTitle("Quitter le trajet"); + alertQuit.setHeaderText("Quitter le trajet"); + alertQuit.setContentText("Souhaitez-vous quitter ce trajet ?"); + ButtonType ouiQuit = new ButtonType("Oui"); + ButtonType nonQuit = new ButtonType("Non"); + alertQuit.getButtonTypes().setAll(ouiQuit, nonQuit); + + Optional<ButtonType> resultQuit = alertQuit.showAndWait(); + if (resultQuit.get().equals(ouiQuit)){ + leaveJourney(app, wtcd); + } + }); + } + + /** + * Create a new journey and broadcast it + * Set the journey label and the current journey + * @param app App context + * @param pnl Panel containing the journey information + */ + private void createJourney(AppContext app, Pane pnl) throws IOException, ParseException { + if (currentJourney instanceof Connection.EmptyConnection){ + Alert alert = new Alert(Alert.AlertType.CONFIRMATION); + alert.setTitle("Valider le trajet"); + alert.setHeaderText("Valider le trajet"); + alert.setContentText("Souhaitez-vous sélectionner ce trajet ?"); + ButtonType oui = new ButtonType("Oui"); + ButtonType non = new ButtonType("Non"); + alert.getButtonTypes().setAll(oui, non); + + Optional<ButtonType> result = alert.showAndWait(); + if (result.get().equals(oui)){ + Integer pos = Integer.parseInt(pnl.getId()); + WeatherAPI api = new WeatherAPI(); + Connection connection = displayedConnections.get(pos); + String wtd = api.getWeatherFrom(connection.getTo().getLocation().getCoordinates()).getConditionsIcon(); + + JoinedJourney joinedJourney = new JoinedJourney(app.getUser().get(), connection, wtd); + Platform.runLater(() -> { + app.getMessageManager().sendJoinedJourney(joinedJourney); + }); + currentJourney = displayedConnections.get(pos); + setupCurrentConnection(app, api); + } + } else { + UiUtils.dialog(Alert.AlertType.WARNING, "Note", "Vous avez un trajet en cours !", + "Veuillez s'il vous plait quitter le trajet actuel avant d'en choisir un autre."); + } + } + + + /** + * Parse the line and search for chat command. + * @param cmd The line to parse. + */ + private void parseChatCommand(String cmd){ + String[] command = cmd.split(" ", 2); + Color c = Color.color(1, 0.9, 0.5, 0.3); + if (AppConfig.CHAT_COMMANDS.contains(command[0])){ + switch (command[0]) { + case "help": + drawCommands(); + break; + case "block": + if (command.length > 1) { + Main.getContext().getUser().get().addIgnoredUser(new User(command[1])); + drawMessage(command[1] + " bloqué.", AppConfig.HELP_MESSAGE_ICON, c); + } + break; + case "blacklist": + List<User> list = Main.getContext().getUser().get().getIgnoredUserList(); + if (list.size() > 0) { + StringBuilder buffer = new StringBuilder(list.get(0).getName()); + for (int i = 1; i < list.size(); i++){ + buffer.append(", ").append(list.get(i).getName()); + } + drawMessage("Bloqués : " + buffer.toString(), + AppConfig.HELP_MESSAGE_ICON, c); + } + break; + case "unblock": + List<User> ulist = Main.getContext().getUser().get().getIgnoredUserList(); + if (ulist.remove(new User(command[1]))){ + drawMessage(command[1] + " débloqué.", AppConfig.HELP_MESSAGE_ICON, c); + } + break; + } + } else { + drawCommands(); + } + } + + /** + * "refreshes the screen" (right) + * @param message The message to check + * @return True if show, false if not + * Test d'obfuscation de code lol + */ + private boolean refreshScreen(String message) { + if (message.toLowerCase().contains("23 dbm") || message.toLowerCase().contains("23dbm")){ + Color c = Color.color(1, 0.0, 0.0, 0.5); + drawMessage("Halte ! Pas plus de 23 dBm !", AppConfig.EASTER_EGG_23DBM, c); + return true; + } + if (message.toLowerCase().contains("yaka")){ + Color c = Color.color(0.96,0.317,0.592,0.5); + drawMessage("Pause café !", AppConfig.EASTER_EGG_CAFE, c); + return true; + } + if (message.toLowerCase().contains("effet de bord")){ + Color c = Color.color(1, 0.5, 0.1, 1); + drawMessage("Au lieu de faire des EDB, tu ferais mieux de réviser ta contravariance !", + AppConfig.EASTER_EGG_EFFET_DE_BORD, c); + return true; + } + // La pièce de résistance. + if (message.toLowerCase().contains("scrum")){ + try { + Color c = Color.color(0, 1, 0, 1); + drawMessage("KANBAN !!", AppConfig.EASTER_EGG_SCRUM, c); + AudioInputStream audioIn = AudioSystem.getAudioInputStream( + MainWindowController.class.getResource("/img/javafx_res5").toURI().toURL()); + Clip clip = AudioSystem.getClip(); + clip.open(audioIn); + clip.start(); + return true; + } catch (Exception e){ + e.printStackTrace(); + return false; + } + } + return false; + } + + /** + * Initializes the event handler for every textbox/combofield that has a "press enter" behaviour. + * @param app The app context + * @param transportApi The transport API + */ + private void initializeTextFieldWithEnterBehaviour(AppContext app, LinkAPI transportApi){ + UiUtils.buttonWhenEnter(originComboBox, searchOriginButton); + UiUtils.buttonWhenEnter(destinationComboBox, searchDestinationButton); + UiUtils.buttonWhenEnter(messageTextBox, sendMessageButton); + + searchOriginButton.setOnAction(event -> { + originComboBox.setValue(originComboBox.getEditor().getText()); + searchStops(originComboBox.getValue(), transportApi, originComboBox); + }); + + searchDestinationButton.setOnAction(event -> { + destinationComboBox.setValue(destinationComboBox.getEditor().getText()); + searchStops(destinationComboBox.getValue(), transportApi, destinationComboBox); + }); + + sendMessageButton.setOnAction(event -> { + if (app.getUser().isPresent() && !messageTextBox.getText().isEmpty()){ + if ( messageTextBox.getText().charAt(0) != '/') { + if(!refreshScreen(messageTextBox.getText())){ + app.getMessageManager().sendChatMessage(new ChatMessage(app.getUser().get(), messageTextBox.getText())); + } + } else { + parseChatCommand(messageTextBox.getText().substring(1)); + } + messageTextBox.clear(); + } + }); + } + + /** + * Handle event subscriptions. + * @param app The app context + */ + private void subscribeToEvents(AppContext app){ + Predicate<Event> userFilter = event -> !(app.getUser().get().getIgnoredUserList().contains(event.getUser())); + + // Subscribe to chat message + app.getMessageManager().conditionalSubscribeChatMessage(chatMessage -> Platform.runLater(() -> { + User sender = chatMessage.getUser(); + String message = sender.getName() + ": " + chatMessage.getMessage(); + if (sender.equals(app.getUser().get())) { + drawMessage(message, AppConfig.CHAT_MESSAGE_ICON_SELF, AppConfig.COLOR_BLUE_10_OPACITY); + } else { + drawMessage(message, AppConfig.CHAT_MESSAGE_ICON, AppConfig.COLOR_BLUE_10_OPACITY); + } + }), userFilter::test); + + // Subscribe to joined journey + app.getMessageManager().conditionalSubscribeJoinedJourney(joinedJourney -> Platform.runLater(() -> { + User sender = joinedJourney.getUser(); + String message = sender.getName() + " voyage vers " + joinedJourney.getConnection().getTo().getLocation().getName() + "."; + if (sender.equals(app.getUser().get())) { + drawMessage(message, AppConfig.CHAT_TRAIN_ICON_SELF, AppConfig.COLOR_BLUE_10_OPACITY); + } else { + drawMessage(message, joinedJourney.getWeatherToDestination(), AppConfig.COLOR_BLUE_10_OPACITY); + try { + Section commonSection = joinedJourney.getConnection().getInCommonSection(currentJourney); + if (!(commonSection.equals(Section.empty()))){ + app.getMessageManager().sendMeeting(new Meeting(sender, app.getUser().get(), commonSection)); + } + } catch (ParseException e) { + e.printStackTrace(); + } + } + }), userFilter::test); + + // Subscribe to left journey + app.getMessageManager().conditionalSubscribeLeftJourney(leftJourney -> Platform.runLater(() -> { + User sender = leftJourney.getUser(); + String message = sender.getName() + " est arrivé à " + leftJourney.getConnection().getTo().getLocation().getName() + "."; + if (sender.equals(app.getUser().get())) { + drawMessage(message, AppConfig.CHAT_TRAIN_ICON, AppConfig.COLOR_BLUE_10_OPACITY); + } else { + drawMessage(message, leftJourney.getWeatherToDestination(), AppConfig.COLOR_BLUE_10_OPACITY); + } + }), userFilter::test); + + // Subscribe to meeting + app.getMessageManager().conditionalSubscribeMeeting(meeting -> Platform.runLater(() -> { + drawMessage( + String.format( + AppConfig.COMMON_ITINERARY_TEXT, + app.getUser().get().equals(meeting.getUser()) ? meeting.getPartner().getName() : meeting.getUser().getName(), + meeting.getSection().getDeparture().getLocation().getName() + ), + AppConfig.JOURNEY_ICON_COMMON_ITINERARY, + AppConfig.COLOR_BLUE_50_OPACITY + ); + }), event -> (event.getUser().equals(app.getUser().get()) || event.getPartner().equals(app.getUser().get()))); + } + + /** + * Sets the form up + * @param url The JavaFX URL handler + * @param resourceBundle The JavaDX ResB handle + */ + @Override + public void initialize(URL url, ResourceBundle resourceBundle) { + LinkAPI transportApi = new LinkAPI(); + AppContext app = Main.getContext(); + + currentJourneyLabel.setText(AppConfig.DEFAULT_JOURNEY_TEXT); + startStopLabel.setText(""); // No text should be visible when no journey has been selected. + messageTextBox.textProperty().addListener((ov, oldValue, newValue) -> { + if (messageTextBox.getText().length() > 60) { + messageTextBox.setText(messageTextBox.getText().substring(0, 60)); + } + }); + + initializeTextFieldWithEnterBehaviour(app, transportApi); + try { + currentJourney = new Connection.EmptyConnection(); + } catch (ParseException e){ + showSadMessage("Impossible de démarrer proprement l'application."); + e.printStackTrace(); + } + + launchItineraryButton.setOnAction(event -> { + try { + connectionCanvas.getGraphicsContext2D() + .clearRect(0, 0, connectionCanvas.getWidth(), connectionCanvas.getHeight()); + connectionContainer.getChildren().removeIf(c -> (c instanceof Pane) || (c instanceof ImageView)); + displayedConnections = transportApi.getConnections( + originComboBox.getValue(), destinationComboBox.getValue()); + startStopLabel.setText(originComboBox.getValue() + " - " + destinationComboBox.getValue()); + + for (int i = 0; i < displayedConnections.size(); i++){ + // Now iterating over connections + drawConnection(displayedConnections.get(i), 30, 100 + 100 * i); + Pane pane = new Pane(); + setConnectionPanelStyle(pane, AppConfig.COLOR_BLUE_10_OPACITY); + pane.setTranslateX(20); + pane.setTranslateY(85 + 100 * i); + pane.setId(Integer.toString(i)); + Platform.runLater(() -> { + connectionContainer.getChildren().add(pane); + pane.setOnMouseClicked(e -> { + Pane pnl = (Pane) e.getSource(); + try { + createJourney(app, pnl); + } catch (Exception ex){ + showSadMessage(AppConfig.ERROR_API_MQ); + ex.printStackTrace(); + } + }); + }); + } + } catch (IOException e) { + showSadMessage(AppConfig.ERROR_API_UNREACHABLE); + } + }); + + subscribeToEvents(app); + + // Login messages: + drawCommands(); + Platform.runLater(() -> { + drawHelpMessage("Bonjour " + app.getUser().get().getName() + " ! Les commandes à dispositions sont :"); + }); + } +} diff --git a/src/main/java/ch/hepia/ui/UiUtils.java b/src/main/java/ch/hepia/ui/UiUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..040ca7672dd3753cc6739f2deed31704e936b434 --- /dev/null +++ b/src/main/java/ch/hepia/ui/UiUtils.java @@ -0,0 +1,39 @@ +package ch.hepia.ui; + +import javafx.scene.Node; +import javafx.scene.control.Alert; +import javafx.scene.control.Button; +import javafx.scene.input.KeyCode; + +/** + * Utilities for reusing UI code. + */ +public class UiUtils { + /** + * Shows a dialog box. + * @param type The dialog box type + * @param title The dialog box title + * @param header The dialog box header + * @param content The dialog box content + */ + public static void dialog(Alert.AlertType type, String title, String header, String content){ + Alert alert = new Alert(type); + alert.setTitle(title); + alert.setHeaderText(header); + alert.setContentText(content); + alert.show(); + } + + /** + * Fires a button when enter is pressed + * @param target The node on where to add the event to + * @param button The button to fire + */ + public static void buttonWhenEnter(Node target, Button button){ + target.setOnKeyPressed(event -> { + if (event.getCode().equals(KeyCode.ENTER)){ + button.fire(); + } + }); + } +} diff --git a/src/main/resources/fxml/ConnectionWindow.fxml b/src/main/resources/fxml/ConnectionWindow.fxml new file mode 100644 index 0000000000000000000000000000000000000000..829ded0106f334353d3209aff341ac7942291971 --- /dev/null +++ b/src/main/resources/fxml/ConnectionWindow.fxml @@ -0,0 +1,50 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<?import javafx.geometry.Insets?> +<?import javafx.scene.control.*?> +<?import javafx.scene.layout.*?> +<?import javafx.scene.text.Font?> +<BorderPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="565.0" prefWidth="1000.0" style="-fx-background-color: #bababa;" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="ch.hepia.ui.ConnectionController"> + <top> + <Label id="AppNameLabel" fx:id="appNameLabel" alignment="CENTER" contentDisplay="CENTER" prefHeight="80.0" prefWidth="1000.0" style="-fx-background-color: #35B1EE;" text="AppNameLabel" textAlignment="CENTER" textFill="WHITE" BorderPane.alignment="CENTER"> + <font> + <Font size="35.0" /> + </font> + </Label> + </top> + <center> + <GridPane BorderPane.alignment="CENTER"> + <columnConstraints> + <ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" /> + <ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" /> + <ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" /> + </columnConstraints> + <rowConstraints> + <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" /> + <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" /> + <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" /> + </rowConstraints> + <children> + <Label id="AppConnectionStatusLabel" fx:id="appConnectionStatusLabel" alignment="CENTER" text="AppConnectionStatusLabel" textAlignment="CENTER" textFill="WHITE" wrapText="true" GridPane.columnIndex="1" GridPane.halignment="CENTER" GridPane.rowIndex="1" GridPane.valignment="BASELINE"> + <GridPane.margin> + <Insets top="10.0" /> + </GridPane.margin></Label> + <Button mnemonicParsing="false" fx:id="handleConfirmButton" onAction="#handleConfirmButton" text="Se connecter" GridPane.columnIndex="1" GridPane.halignment="RIGHT" GridPane.rowIndex="1"> + <GridPane.margin> + <Insets right="10.0" /> + </GridPane.margin> + </Button> + <TextField fx:id="usernameSelectionTextField" maxWidth="205.0" GridPane.columnIndex="1" GridPane.rowIndex="1"> + <GridPane.margin> + <Insets left="10.0" /> + </GridPane.margin> + </TextField> + <Label id="AppConnectionStatusLabel" fx:id="errorLabel" alignment="CENTER" text="ErrorLabel" textAlignment="CENTER" textFill="RED" visible="false" wrapText="true" GridPane.columnIndex="1" GridPane.halignment="CENTER" GridPane.rowIndex="1" GridPane.valignment="BOTTOM"> + <GridPane.margin> + <Insets bottom="40.0" /> + </GridPane.margin> + </Label> + </children> + </GridPane> + </center> +</BorderPane> diff --git a/src/main/resources/fxml/MainWindow.fxml b/src/main/resources/fxml/MainWindow.fxml new file mode 100644 index 0000000000000000000000000000000000000000..51f520a6b6813acd41db75cf9bfa0bda747c15a5 --- /dev/null +++ b/src/main/resources/fxml/MainWindow.fxml @@ -0,0 +1,64 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<?import javafx.scene.canvas.Canvas?> +<?import javafx.scene.control.*?> +<?import javafx.scene.layout.*?> +<?import javafx.scene.paint.*?> +<?import javafx.scene.shape.Line?> +<?import javafx.scene.text.Font?> +<BorderPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="565.0" prefWidth="1000.0" style="-fx-background-color: #fafafa;" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="ch.hepia.ui.MainWindowController"> + <top> + <Pane maxHeight="48.0" maxWidth="1000.0" minHeight="48.0" minWidth="32.0" prefHeight="48.0" prefWidth="32.0" style="-fx-background-color: #35B1EE;" BorderPane.alignment="CENTER"> + <children> + <ComboBox id="originComboBox" fx:id="originComboBox" editable="true" layoutX="12.0" layoutY="10.0" prefHeight="27.0" prefWidth="275.0" promptText="Tapez le nom d'un arrêt..." /> + <Line endY="48.0" layoutX="411.0" layoutY="-1.0" startY="1.0"> + <stroke> + <LinearGradient> + <stops> + <Stop color="#35b1ee" /> + <Stop color="#004dff" offset="1.0" /> + </stops> + </LinearGradient> + </stroke> + </Line> + <ComboBox id="destinationComboBox" fx:id="destinationComboBox" editable="true" layoutX="421.0" layoutY="9.0" prefHeight="27.0" prefWidth="282.0" promptText="Choisissez votre destination..." /> + <Line endY="48.0" layoutX="827.0" layoutY="-1.0" startY="1.0"> + <stroke> + <LinearGradient> + <stops> + <Stop color="#35b1ee" /> + <Stop color="#004dff" offset="1.0" /> + </stops> + </LinearGradient> + </stroke> + </Line> + <Button id="searchOriginButton" fx:id="searchOriginButton" layoutX="293.0" layoutY="10.0" mnemonicParsing="false" prefHeight="27.0" prefWidth="107.0" text="Chercher" /> + <Button fx:id="searchDestinationButton" layoutX="711.0" layoutY="9.0" mnemonicParsing="false" prefHeight="27.0" prefWidth="107.0" text="Chercher" /> + <Button id="launchItinaryButton" fx:id="launchItineraryButton" layoutX="836.0" layoutY="9.0" mnemonicParsing="false" prefHeight="27.0" prefWidth="156.0" text="Itinéraire..." /> + </children> + </Pane> + </top> + <right> + <Pane prefHeight="517.0" prefWidth="326.0" BorderPane.alignment="CENTER"> + <children> + <Line endX="326.0" endY="475.0" startY="475.0" stroke="#0000008c" /> + <TextField id="messageTextBox" fx:id="messageTextBox" layoutX="6.0" layoutY="483.0" prefHeight="27.0" prefWidth="206.0" /> + <Button id="sendMessageButton" fx:id="sendMessageButton" layoutX="219.0" layoutY="483.0" mnemonicParsing="false" prefHeight="27.0" prefWidth="101.0" text="Envoyer" /> + <Pane fx:id="chatContainer" layoutX="7.0" layoutY="6.0" prefHeight="465.0" prefWidth="315.0" /> + </children></Pane> + </right> + <center> + <Pane id="connectionContainer" fx:id="connectionContainer" prefHeight="200.0" prefWidth="200.0" BorderPane.alignment="CENTER"> + <children> + <Label id="startStopLabel" fx:id="startStopLabel" layoutX="14.0" layoutY="14.0" text="Arrêt : CHANGEZ-MOI" textFill="#4c7ba8"> + <font> + <Font size="28.0" /> + </font> + </Label> + <Line endX="100.0" layoutX="115.0" layoutY="49.0" startX="-100.0" stroke="#4c7ba8" /> + <Canvas id="connectionCanvas" fx:id="connectionCanvas" height="451.0" layoutX="15.0" layoutY="59.0" width="652.0" /> + <Label id="currentJourneyLabel" fx:id="currentJourneyLabel" layoutX="15.0" layoutY="59.0" text="CHANGEZ-MOI" textFill="#2600ff" /> + </children> + </Pane> + </center> +</BorderPane> diff --git a/src/main/resources/img/bubble.png b/src/main/resources/img/bubble.png new file mode 100644 index 0000000000000000000000000000000000000000..98fcb6b83c43d21ba51d30df9f40b1b90c43769c Binary files /dev/null and b/src/main/resources/img/bubble.png differ diff --git a/src/main/resources/img/bubble_self.png b/src/main/resources/img/bubble_self.png new file mode 100644 index 0000000000000000000000000000000000000000..3f82566859fd2d56154b276532a5745d7c19ceb5 Binary files /dev/null and b/src/main/resources/img/bubble_self.png differ diff --git a/src/main/resources/img/close.png b/src/main/resources/img/close.png new file mode 100644 index 0000000000000000000000000000000000000000..3378727278afcac737f2b9ca973ea6a92cb95585 Binary files /dev/null and b/src/main/resources/img/close.png differ diff --git a/src/main/resources/img/cloudy.png b/src/main/resources/img/cloudy.png new file mode 100644 index 0000000000000000000000000000000000000000..24498e12032bc1fff4654e249139b5839622fdc9 Binary files /dev/null and b/src/main/resources/img/cloudy.png differ diff --git a/src/main/resources/img/foggy.png b/src/main/resources/img/foggy.png new file mode 100644 index 0000000000000000000000000000000000000000..7112ec41354e99c58d5af07b34b893d3b5972584 Binary files /dev/null and b/src/main/resources/img/foggy.png differ diff --git a/src/main/resources/img/friends.png b/src/main/resources/img/friends.png new file mode 100644 index 0000000000000000000000000000000000000000..306c38c7aa49ca2d8e416c4bc4fa1914b978f8f0 Binary files /dev/null and b/src/main/resources/img/friends.png differ diff --git a/src/main/resources/img/help.png b/src/main/resources/img/help.png new file mode 100644 index 0000000000000000000000000000000000000000..fae26f1a071fe0c3c9889a3571c43736361cf6d5 Binary files /dev/null and b/src/main/resources/img/help.png differ diff --git a/src/main/resources/img/javafx_res1 b/src/main/resources/img/javafx_res1 new file mode 100644 index 0000000000000000000000000000000000000000..04b4f68601638e582dc865e82f5197d92c57d633 Binary files /dev/null and b/src/main/resources/img/javafx_res1 differ diff --git a/src/main/resources/img/javafx_res2 b/src/main/resources/img/javafx_res2 new file mode 100644 index 0000000000000000000000000000000000000000..c225ccd8657f60b5c85a6be8d04c57ef6af798d8 Binary files /dev/null and b/src/main/resources/img/javafx_res2 differ diff --git a/src/main/resources/img/javafx_res3 b/src/main/resources/img/javafx_res3 new file mode 100644 index 0000000000000000000000000000000000000000..3730e32ea20345efeff7342047c264521001263b Binary files /dev/null and b/src/main/resources/img/javafx_res3 differ diff --git a/src/main/resources/img/javafx_res4 b/src/main/resources/img/javafx_res4 new file mode 100644 index 0000000000000000000000000000000000000000..a81a4c206be0bc657c48ca3fe51caee3555025be Binary files /dev/null and b/src/main/resources/img/javafx_res4 differ diff --git a/src/main/resources/img/javafx_res5 b/src/main/resources/img/javafx_res5 new file mode 100644 index 0000000000000000000000000000000000000000..52494628c9f017a801fd2c555039a31a9d2ab6c7 Binary files /dev/null and b/src/main/resources/img/javafx_res5 differ diff --git a/src/main/resources/img/rainy.png b/src/main/resources/img/rainy.png new file mode 100644 index 0000000000000000000000000000000000000000..615dd7437d7e6fffdd5c5bf5a1d581b4cf2d3487 Binary files /dev/null and b/src/main/resources/img/rainy.png differ diff --git a/src/main/resources/img/snowy.png b/src/main/resources/img/snowy.png new file mode 100644 index 0000000000000000000000000000000000000000..96656484a45f68f1cdf04eb87b5adac59e58fd9b Binary files /dev/null and b/src/main/resources/img/snowy.png differ diff --git a/src/main/resources/img/stormy.png b/src/main/resources/img/stormy.png new file mode 100644 index 0000000000000000000000000000000000000000..c7c10c2719e4a9bcf5b0ed59a799f19854878f1a Binary files /dev/null and b/src/main/resources/img/stormy.png differ diff --git a/src/main/resources/img/sunny.png b/src/main/resources/img/sunny.png new file mode 100644 index 0000000000000000000000000000000000000000..9204c0d8db49095a084880515a471c8215b192b0 Binary files /dev/null and b/src/main/resources/img/sunny.png differ diff --git a/src/main/resources/img/train.png b/src/main/resources/img/train.png new file mode 100644 index 0000000000000000000000000000000000000000..be0ae7ee7831acb6ec8ceb5bddfc1f2e7a2cb3f7 Binary files /dev/null and b/src/main/resources/img/train.png differ diff --git a/src/main/resources/img/train_self.png b/src/main/resources/img/train_self.png new file mode 100644 index 0000000000000000000000000000000000000000..d1b2660f370bb3589771dd8d51353f98a1f67a86 Binary files /dev/null and b/src/main/resources/img/train_self.png differ diff --git a/src/main/resources/log4j.properties b/src/main/resources/log4j.properties new file mode 100644 index 0000000000000000000000000000000000000000..393e0877ec1c2207f866bcf94740760170893486 --- /dev/null +++ b/src/main/resources/log4j.properties @@ -0,0 +1,8 @@ +# Root logger option +log4j.rootLogger=INFO, stdout + +# Direct log messages to stdout +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.Target=System.out +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n \ No newline at end of file diff --git a/src/test/java/ch/hepia/models/UserTest.java b/src/test/java/ch/hepia/models/UserTest.java new file mode 100644 index 0000000000000000000000000000000000000000..b06742b3b3dc7d7f185d2115414f134f23c2b3fc --- /dev/null +++ b/src/test/java/ch/hepia/models/UserTest.java @@ -0,0 +1,13 @@ +package ch.hepia.models; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +class UserTest { + @Test + void constructorTest() { + User u = new User("Hubert-Stanislas"); + assertEquals(u, new User("Hubert-Stanislas")); + } +} \ No newline at end of file diff --git a/src/test/java/ch/hepia/mq/MessageTest.java b/src/test/java/ch/hepia/mq/MessageTest.java new file mode 100644 index 0000000000000000000000000000000000000000..16073e6c17c54f24adf3b3812a67428978dc0261 --- /dev/null +++ b/src/test/java/ch/hepia/mq/MessageTest.java @@ -0,0 +1,21 @@ +package ch.hepia.mq; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +import ch.hepia.mq.Message.Type; +import ch.hepia.events.ChatMessage; +import ch.hepia.models.*; +class UserTest { + User u = new User("Test"); + ChatMessage cm = new ChatMessage(u, "Bonjour"); + Message m = new Message(Type.ChatMessage, cm); + + @Test + void ChatMessageTest() { + ChatMessage msg = m.getData(); + assertEquals(u, msg.getUser()); + assertEquals("Bonjour", msg.getMessage()); + } +} \ No newline at end of file