diff --git a/.gitignore b/.gitignore index c394076d75a37b52fdbcd46defe55ebacfc03889..7296ae5dcc5c81743786e9ba0c69368f365a9944 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ target/ *.project **.vscode/ *.classpath -**.settings \ No newline at end of file +**.settings +*.iml \ No newline at end of file diff --git a/README.md b/README.md index 846c531f866189e728ea7b4f8e8eb99b5b383b84..832002f8e21f2b596f7f46e25815a2c9000fb1de 100644 --- a/README.md +++ b/README.md @@ -1,6 +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 @@ -29,4 +29,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 de23717827f7ac084aef6c7fb37546708ec6ae17..effeb7eab5cb4cfb0b6381300c46c1f536a25898 100644 --- a/pom.xml +++ b/pom.xml @@ -16,6 +16,13 @@ <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> @@ -76,6 +83,16 @@ <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 index fc487a61550ca4ec5aadb13e965eaae23526d72f..7c7c5860d54548641d1615dd0ad2cc517578ed5d 100644 --- a/src/main/java/ch/hepia/Main.java +++ b/src/main/java/ch/hepia/Main.java @@ -1,13 +1,21 @@ package ch.hepia; import ch.hepia.config.AppConfig; +import ch.hepia.config.AppContext; +import ch.hepia.events.*; +import ch.hepia.models.User; +import ch.hepia.mq.MessageManager; import javafx.application.Application; +import javafx.application.Platform; +import javafx.event.EventHandler; import javafx.fxml.FXMLLoader; import javafx.scene.Parent; import javafx.scene.Scene; import javafx.stage.Stage; +import javafx.stage.WindowEvent; import java.io.IOException; +import java.util.ArrayList; /** @@ -15,11 +23,18 @@ import java.io.IOException; */ public class Main extends Application { + private static AppContext appContext; + /** * Mein starter * @param args The passed arguments */ - public static void main(String[] args) { + 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); } @@ -29,6 +44,27 @@ public class Main extends Application { Scene scene = new Scene(root, AppConfig.APP_WIDTH, AppConfig.APP_HEIGHT); stage.setScene(scene); + stage.setResizable(false); stage.show(); + stage.setOnCloseRequest(new EventHandler<WindowEvent>() { + @Override + public void handle(WindowEvent 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/transport/Connection.java b/src/main/java/ch/hepia/api/transport/Connection.java index 2c0f21247b52f196e33312431198a537adc2fc9b..5b4c9a798efce7f40cd12239a8bcfd91975b95fe 100644 --- a/src/main/java/ch/hepia/api/transport/Connection.java +++ b/src/main/java/ch/hepia/api/transport/Connection.java @@ -9,9 +9,9 @@ import java.util.List; import org.json.JSONArray; import org.json.JSONObject; +import java.io.Serializable; - -public final class Connection { +public class Connection implements Serializable { private Stop from; private Stop to; private Time duration; @@ -21,6 +21,26 @@ public final class Connection { private int capacity2nd; private List<Section> sections; + /** + * Empty Connection + */ + private final static class EmptyConnection extends Connection{ + /** + * Constructor of EmptyConnection + * All values are default ones + * An Empty Connection go from Geneva to Geneva + * @throws ParseException + */ + private 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 @@ -51,10 +71,11 @@ public final class Connection { return new ArrayList<>(sections); } + /** * Builder of Connection object */ - public static class ConnectionBuilder{ + public final static class ConnectionBuilder{ private JSONObject datas; /** @@ -72,27 +93,32 @@ public final class Connection { * @throws ParseException */ public Connection build() throws ParseException { - JSONObject fromJSON = new JSONObject(datas.get("from")); + //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 = new JSONObject(datas.get("to")); + 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.getString("duration").replace('d',':'); + String date = datas.isNull("duration") ? "00:00:00:00" : datas.getString("duration").replace('d',':'); Time duration = new Time(formatter.parse(date).getTime()); - JSONObject serviceJSON = new JSONObject(datas.get("service")); + JSONObject serviceJSON = datas.isNull("service") ? new JSONObject() : new JSONObject(datas.get("service")); Service.ServiceBuilder service = new Service.ServiceBuilder(serviceJSON); - JSONArray productsJSON = datas.getJSONArray("products"); + 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.getJSONArray("sections"); + JSONArray sectionsJSON = datas.isNull("from") ? new JSONArray() : datas.getJSONArray("sections"); List<Section> sections = new ArrayList<>(); sectionsJSON.forEach(o -> { JSONObject k = (JSONObject) o; diff --git a/src/main/java/ch/hepia/api/transport/Coordinates.java b/src/main/java/ch/hepia/api/transport/Coordinates.java index 543f90b7e14c2899553dd42afd828b7da9e000fd..0041489daaf6841d8c8f10d1e1db2b51cfbe825a 100644 --- a/src/main/java/ch/hepia/api/transport/Coordinates.java +++ b/src/main/java/ch/hepia/api/transport/Coordinates.java @@ -1,12 +1,26 @@ package ch.hepia.api.transport; import org.json.JSONObject; +import java.io.Serializable; -public final class Coordinates { - private Type type; +public class Coordinates implements Serializable { + 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{ + /** + * 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) @@ -14,7 +28,7 @@ public final class Coordinates { * @param x coordinate in x * @param y coordinate in y */ - private Coordinates(Type type, double x, double y){ + private Coordinates(String type, double x, double y){ this.type = type; this.x = x; this.y = y; @@ -23,7 +37,7 @@ public final class Coordinates { @Override public String toString(){ StringBuilder string = new StringBuilder(); - return string.append(type.toString()).append("au point (").append(x).append(", ").append(y).append(")").toString(); + return string.append("Coordonnées du type ").append(type).append(" au point (").append(x).append(", ").append(y).append(")").toString(); } /** @@ -45,7 +59,7 @@ public final class Coordinates { /** * Builder of Coordinates object */ - public static class CoordinatesBuilder{ + public final static class CoordinatesBuilder{ private JSONObject datas; private String type; @@ -64,9 +78,13 @@ public final class Coordinates { * @return a new Coordinates object */ public Coordinates build(){ - Type t = Type.valueOf(type.toUpperCase()); - double x = datas.getDouble("x"); - double y = datas.getDouble("y"); + 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(t, x, y); } diff --git a/src/main/java/ch/hepia/api/transport/Journey.java b/src/main/java/ch/hepia/api/transport/Journey.java index d2362fdeec26d37d1834ca992c757382072f491d..1478529f78f60fe393442984c3e31e3a8b5bc0a6 100644 --- a/src/main/java/ch/hepia/api/transport/Journey.java +++ b/src/main/java/ch/hepia/api/transport/Journey.java @@ -6,18 +6,34 @@ import org.json.JSONObject; import java.text.ParseException; import java.util.ArrayList; import java.util.List; +import java.io.Serializable; -public final class Journey { +public class Journey implements Serializable { private String name; private String category; private int categoryCode; - private int number; + 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{ + /** + * 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 @@ -30,7 +46,7 @@ public final class Journey { * @param capacity1st number of seats inst class * @param capacity2nd number of seats in nd class */ - private Journey(String name, String category, int categoryCode, int number, String operator, String to, List<Stop> passList, int capacity1st, int capacity2nd){ + 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; @@ -73,6 +89,13 @@ public final class Journey { 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 @@ -85,7 +108,7 @@ public final class Journey { /** * Builder of Journey object */ - public static class JourneyBuilder{ + public final static class JourneyBuilder{ private JSONObject datas; /** @@ -103,15 +126,19 @@ public final class Journey { * @throws ParseException */ public Journey build() throws ParseException { - String name = datas.getString("name"); - String category = datas.getString("category"); - int categoryCode = datas.getInt("categoryCode"); - int number = datas.getInt("number"); - String operator = datas.getString("operator"); + 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.getString("to"); + String to = datas.isNull("to") ? "" : datas.getString("to"); - JSONArray passListJSON = datas.getJSONArray("passList"); + JSONArray passListJSON = datas.isNull("from") ? new JSONArray() : datas.getJSONArray("passList"); List<Stop> passList = new ArrayList<>(); passListJSON.forEach( o -> { JSONObject k = (JSONObject) o; @@ -123,8 +150,8 @@ public final class Journey { } }); - int capacity1st = datas.getInt("capacity1st"); - int capacity2nd = datas.getInt("capacity2nd"); + 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/Location.java b/src/main/java/ch/hepia/api/transport/Location.java index 1884983b3eff2017d5b1c0297ad1b61086375c20..20125d09f31784a771b1ac62ff99457a6ba3ac5e 100644 --- a/src/main/java/ch/hepia/api/transport/Location.java +++ b/src/main/java/ch/hepia/api/transport/Location.java @@ -1,10 +1,9 @@ package ch.hepia.api.transport; import org.json.JSONObject; +import java.io.Serializable; -import java.util.Optional; - -public final class Location { +public class Location implements Serializable { private int id; private Type type; private String name; @@ -12,6 +11,20 @@ public final class Location { private Coordinates where; private double distance; + /** + * Default Location + */ + private final static class DefaultLocation extends Location{ + /** + * 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()).build(), 0); + } + } + /** * Constructor of the class Location * @param id unique to the Location @@ -38,10 +51,18 @@ public final class Location { return where; } + /** + * Get the name of the location + * @return what + */ + public String getName(){ + return name; + } + /** * Builder of Location objects */ - public static class LocationBuilder{ + public final static class LocationBuilder{ private JSONObject datas; private String type; @@ -60,15 +81,19 @@ public final class Location { * @return a new Location object */ public Location build(){ - int id = datas.getInt("id"); + 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.getString("name"); + String name = datas.isNull("name") ? "" : datas.getString("name"); double score = datas.isNull("score") ? 0 : datas.getDouble("score"); - JSONObject coordinatesJSON = datas.getJSONObject("coordinate"); - Coordinates.CoordinatesBuilder coordinates = new Coordinates.CoordinatesBuilder(coordinatesJSON, type.toString()); + JSONObject coordinatesJSON = datas.isNull("coordinate") ? new JSONObject() : datas.getJSONObject("coordinate"); + Coordinates.CoordinatesBuilder coordinates = new Coordinates.CoordinatesBuilder(coordinatesJSON); - double distance = datas.isNull("score") ? 0 : datas.getDouble("distance"); + 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 index 90ebf678d0fd4e27828c37a8af4929f8d3b106a2..0b8ba5527ee0d825f89c9a2ad494d34454feb9ba 100644 --- a/src/main/java/ch/hepia/api/transport/Prognosis.java +++ b/src/main/java/ch/hepia/api/transport/Prognosis.java @@ -6,14 +6,33 @@ import java.sql.Time; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; +import java.io.Serializable; -public final class Prognosis { +public class Prognosis implements Serializable { private int plateform; private Time departure; private Time arrival; private int capacity1st; private int capacity2nd; + /** + * Empty Prognosis + */ + private final static class EmptyPrognosis extends Prognosis{ + /** + * 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 @@ -34,7 +53,7 @@ public final class Prognosis { /** * Builder of Prognosis Objects */ - public static class PrognosisBuilder{ + public final static class PrognosisBuilder{ private JSONObject datas; /** @@ -52,17 +71,21 @@ public final class Prognosis { * @throws ParseException */ public Prognosis build() throws ParseException { - int plateform = datas.getInt("plateform"); + 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.getString("departure").replace('d',':'); + 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.getString("arrival").replace('d', ':'); + 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.getInt("capacity1st"); - int capacity2nd = datas.getInt("capacity2nd"); + 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 index 8fd4df952cd49f77ecf57459c0f8dd985a61b4f7..72f92d728c85d7f2c7fd7071e1fd47de74e63854 100644 --- a/src/main/java/ch/hepia/api/transport/Section.java +++ b/src/main/java/ch/hepia/api/transport/Section.java @@ -3,13 +3,31 @@ package ch.hepia.api.transport; import org.json.JSONObject; import java.text.ParseException; +import java.io.Serializable; -public final class Section { +public class Section implements Serializable { private Journey journey; private double walktime; private Stop departure; private Stop arrival; + /** + * Empty Section + */ + private final static class EmptySection extends Section{ + /** + * 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 @@ -51,7 +69,7 @@ public final class Section { /** * Builder of Section objects */ - public static class SectionBuilder{ + public final static class SectionBuilder{ private JSONObject datas; /** @@ -69,15 +87,19 @@ public final class Section { * @throws ParseException */ public Section build() throws ParseException { - JSONObject journeyJSON = new JSONObject(datas.get("journey")); + 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 = new JSONObject(datas.get("departure")); + JSONObject departureJSON = datas.isNull("departure") ? new JSONObject() : datas.getJSONObject("departure"); Stop.StopBuilder departure = new Stop.StopBuilder(departureJSON); - JSONObject arrivalJSON = new JSONObject(datas.get("arrival")); + 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 index 4e04c4198f2524d7102d7c5aba877f3a93ca5394..16796b344db5759759a7774140b0fb9fd35fe917 100644 --- a/src/main/java/ch/hepia/api/transport/Service.java +++ b/src/main/java/ch/hepia/api/transport/Service.java @@ -1,12 +1,25 @@ package ch.hepia.api.transport; import org.json.JSONObject; +import java.io.Serializable; - -public final class Service { +public class Service implements Serializable { private String regular; private String irregular; + /** + * Empty Service + */ + private final static class EmptyService extends Service{ + /** + * Constructor of EmptyService + * No informations given + */ + private EmptyService(){ + super("", ""); + } + } + /** * Constructor of Service class * @param regular is the service regular ? @@ -20,7 +33,7 @@ public final class Service { /** * Builder of Service objects */ - public static class ServiceBuilder{ + public final static class ServiceBuilder{ private JSONObject datas; /** @@ -37,8 +50,12 @@ public final class Service { * @return a new Service object */ public Service build(){ - String regular = datas.getString("regular"); - String irregular = datas.getString("irregular"); + 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 index f30c41070d766aec9b9278c27078c854f352b708..b5c2193043b670bf16a5605a7a182acca89c4400 100644 --- a/src/main/java/ch/hepia/api/transport/Stop.java +++ b/src/main/java/ch/hepia/api/transport/Stop.java @@ -6,8 +6,9 @@ import java.sql.Time; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; +import java.io.Serializable; -public final class Stop { +public class Stop implements Serializable { private Location station; private Time arrival; private Time departure; @@ -15,6 +16,25 @@ public final class Stop { private int plateform; private Prognosis prognosis; + /** + * Default Stop in Geneva + */ + private final static class DefaultStop extends Stop{ + /** + * 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 @@ -60,7 +80,7 @@ public final class Stop { /** * Builder of Stop objects */ - public static class StopBuilder{ + public final static class StopBuilder{ private JSONObject datas; /** @@ -78,23 +98,28 @@ public final class Stop { * @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 = (String[]) datas.keySet().toArray(); - String type = keys[0]; - JSONObject locationJSON = new JSONObject(datas.get(type)); + 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("yyyy-MM-dddkk:mm:ss"); + DateFormat formatter = new SimpleDateFormat("kk:mm:ss"); - String arrivalString = datas.getString("arrival"); + 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.getString("departure"); + 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.getInt("delay"); - int plateform = datas.getInt("plateform"); + int delay = datas.isNull("delay") ? 0 : datas.getInt("delay"); + int plateform = datas.isNull("plateform") ? 0 : datas.getInt("plateform"); - JSONObject prognosisJSON = new JSONObject(datas.get("prognosis")); + 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/config/AppConfig.java b/src/main/java/ch/hepia/config/AppConfig.java index 75392f61204b8b92c396f2dba22bcc5574cea242..b52689701cd4ef6e1000e485281587d3f58941e3 100644 --- a/src/main/java/ch/hepia/config/AppConfig.java +++ b/src/main/java/ch/hepia/config/AppConfig.java @@ -1,5 +1,7 @@ package ch.hepia.config; +import javafx.scene.paint.Color; + /** * The variables of the app */ @@ -8,6 +10,27 @@ public final class AppConfig { 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 Integer RABBITMQ_PORT = 5672; + 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 ERROR_API_UNREACHABLE = "Impossible de contacter les services de transport Suisses."; + + /** + * Resources + */ + public static final String CHAT_MESSAGE_ICON = "/img/bubble.png"; + + /** + * 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); + } 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..a06b6bff38fed5300051b7568d63d2579ea6fa09 --- /dev/null +++ b/src/main/java/ch/hepia/events/ChatMessage.java @@ -0,0 +1,22 @@ +package ch.hepia.events; + +import ch.hepia.models.User; +import java.io.Serializable; + +public class ChatMessage implements Serializable { + private User user; + private String chatMessage; + + 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/JoinedJourney.java b/src/main/java/ch/hepia/events/JoinedJourney.java index 5e1cb2e823426db511d4072eb5b6ea6b0124fcdf..aef52676c54626140953e8b5dab95a7f6887585b 100644 --- a/src/main/java/ch/hepia/events/JoinedJourney.java +++ b/src/main/java/ch/hepia/events/JoinedJourney.java @@ -27,6 +27,14 @@ public class JoinedJourney implements Serializable{ this.sections = new ArrayList<>(sections); } + public User getUser() { + return this.user; + } + + public List<Section> getSections() { + return this.sections; + } + @Override public String toString() { // TODO diff --git a/src/main/java/ch/hepia/events/LeftJourney.java b/src/main/java/ch/hepia/events/LeftJourney.java index 11c49eafe3f94421464631575055822f8f4145cc..47964e860ebc6cdbef6433b033eac9c5b35f42c8 100644 --- a/src/main/java/ch/hepia/events/LeftJourney.java +++ b/src/main/java/ch/hepia/events/LeftJourney.java @@ -3,9 +3,9 @@ package ch.hepia.events; import ch.hepia.api.transport.Section; import ch.hepia.models.User; +import java.io.Serializable; import java.util.ArrayList; import java.util.List; -import java.io.Serializable; /** * Represents an event where a user has left a journey. @@ -27,6 +27,14 @@ public class LeftJourney implements Serializable { this.sections = new ArrayList<>(sections); } + public User getUser() { + return this.user; + } + + public List<Section> getSections() { + return this.sections; + } + @Override public String toString() { // TODO diff --git a/src/main/java/ch/hepia/models/User.java b/src/main/java/ch/hepia/models/User.java index 1678ca61dfe1cf9d8c9aad662ab21ea2a46733e8..911acbb3634d32720b44b53b00fba1de14600135 100644 --- a/src/main/java/ch/hepia/models/User.java +++ b/src/main/java/ch/hepia/models/User.java @@ -1,19 +1,66 @@ package ch.hepia.models; - -import java.util.Objects; - +import java.io.Serializable; +import java.util.*; /** * Represents an user. */ -public class User { +public class User implements Serializable { private String username; + private transient List<User> ignoredUserList; 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 username; + 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/DummyClass.java b/src/main/java/ch/hepia/mq/DummyClass.java deleted file mode 100644 index b37bb3270d4677db67ba7854a3262ba25e11a702..0000000000000000000000000000000000000000 --- a/src/main/java/ch/hepia/mq/DummyClass.java +++ /dev/null @@ -1,16 +0,0 @@ -package ch.hepia; -import java.io.Serializable; - -public class DummyClass implements Serializable{ - private static final long serialVersionUID = 0xAEF34565672L; - private String prop0; - public int prop1; - - public DummyClass(){ - this.prop0 = "élè"; - this.prop1 = -12; - } - public String getProp0(){ - return this.prop0; - } -} \ No newline at end of file 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..20b0c81c40891c4d8371070c505cc037f1288b7a --- /dev/null +++ b/src/main/java/ch/hepia/mq/Message.java @@ -0,0 +1,36 @@ +package ch.hepia.mq; +import java.io.Serializable; + +public final class Message implements Serializable { + public static enum Type { + JoinedJourney, LeftJourney, ChatMessage + } + + 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 index 0bc3c17ca7c476a8332e5e3f11692474c339e359..1b57f6ce76a11011c5399b54876fe5f14ea80992 100644 --- a/src/main/java/ch/hepia/mq/MessageManager.java +++ b/src/main/java/ch/hepia/mq/MessageManager.java @@ -7,28 +7,7 @@ import java.util.function.Consumer; import java.util.function.Predicate; public class MessageManager extends MessageQueue { - private enum MessageType { - JoinedJourney, LeftJourney - } - - private final class Message implements Serializable { - private static final long serialVersionUID = 0xAEF34565673L; - private final MessageType type; - private byte[] data; - - public <T extends Serializable> Message(MessageType type, T object) { - this.type = type; - this.data = MessageQueue.serialize(object); - } - public MessageType getMessageType() { - return this.type; - } - - public <T> T getData() { - return MessageQueue.unserialize(this.data); - } - } public MessageManager(String host, String username, String password, String exchange) throws Exception { super(host, username, password, exchange); @@ -37,7 +16,13 @@ public class MessageManager extends MessageQueue { /* * Private functions */ - private <T extends Serializable> void sendEvent(MessageType type, T event) { + + /** + * 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)); @@ -46,11 +31,16 @@ public class MessageManager extends MessageQueue { } } - private <T extends Serializable> void conditionalSubscribe(MessageType type, Consumer<T> eventHandler, + /** + * 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.type == type) { + if (receivedMessage.getMessageType() == type) { T event = receivedMessage.getData(); if (condition.test(event)) { eventHandler.accept(event); @@ -63,28 +53,80 @@ public class MessageManager extends MessageQueue { /* * 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 JoinedJourney events validating the condition + * @param eventHandler JoinedJourney consumer + * @param condition JoinedJourney predicate + */ public void conditionalSubscribeJoinedJourney(Consumer<JoinedJourney> eventHandler, Predicate<JoinedJourney> condition) { - this.conditionalSubscribe(MessageType.JoinedJourney, eventHandler, 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(MessageType.LeftJourney, eventHandler, condition); + this.conditionalSubscribe(Message.Type.LeftJourney, eventHandler, condition); } + /** + * Send a JoinedJourney event + * @param event JoinedJourney event + */ public void sendJoinedJourney(JoinedJourney event) { - this.sendEvent(MessageType.JoinedJourney, event); + this.sendEvent(Message.Type.JoinedJourney, event); } + /** + * Send a LeftJourney event + * @param event LeftJourney event + */ public void sendLeftJourney(LeftJourney event) { - this.sendEvent(MessageType.LeftJourney, event); + this.sendEvent(Message.Type.LeftJourney, event); + } + + /** + * Subscribe to all ChatMessage events + * @param eventHandler ChatMessage consumer + */ + public void subscribeChatMessage(Consumer<ChatMessage> eventHandler) { + this.conditionalSubscribeChatMessage(eventHandler, (event) -> true); + } + + /** + * 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); + } + + /** + * Send a ChatMessage event + * @param event ChatMessage event + */ + public void sendChatMessage(ChatMessage event) { + this.sendEvent(Message.Type.ChatMessage, 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 index e898fb70d28a3ff108c8d1aa950da825575c7cc5..a9df5112fe5473f98124e7076aba41ad0251b4cb 100644 --- a/src/main/java/ch/hepia/mq/MessageQueue.java +++ b/src/main/java/ch/hepia/mq/MessageQueue.java @@ -15,74 +15,98 @@ import com.rabbitmq.client.Connection; import com.rabbitmq.client.DeliverCallback; public abstract class MessageQueue { - private final String exchange; - private final Connection mqConnection; - private final Channel mqChannel; + 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); - 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 -> { + }); + } - 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 -> { - }); - } - - protected void addConsumer(Consumer<byte[]> messageHandler) { - this.consumers.add(messageHandler); - } + /** + * Add a consumer to the queue + * @param messageHandler Message consumer + */ + protected void addConsumer(Consumer<byte[]> messageHandler) { + this.consumers.add(messageHandler); + } - protected void sendBytes(byte[] bytes) throws Exception { - this.mqChannel.basicPublish(this.exchange, "", null, bytes); - } + /** + * 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); + } - public void close() throws Exception { - this.mqChannel.close(); - this.mqConnection.close(); - } - - public static <T extends Serializable> byte[] serialize(T object){ - ObjectOutputStream oos = null; - ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); - try { - oos = new ObjectOutputStream(byteStream); - oos.writeObject(object); - oos.flush(); - return byteStream.toByteArray(); - } catch (final Exception e) { - e.printStackTrace(); - }finally{ - // TODO - System.exit(1); - } - return null; - } + /** + * Terminate the connection with rabbitMQ + */ + public void close() throws Exception { + this.mqChannel.close(); + this.mqConnection.close(); + } - public static <T extends Serializable> T unserialize(byte[] serializedData){ - ByteArrayInputStream inputStream = new ByteArrayInputStream(serializedData); - T result = null; - try { - ObjectInputStream ois = new ObjectInputStream(inputStream); - result = (T) ois.readObject(); - } catch (final Exception e) { - e.printStackTrace(); - }finally{ - // TODO - System.exit(1); - } - return result; + /** + * 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/mq/TesterClass.java b/src/main/java/ch/hepia/mq/TesterClass.java deleted file mode 100644 index defcddcd9b78f3ac68a59a8ad9d0042c5e8b4491..0000000000000000000000000000000000000000 --- a/src/main/java/ch/hepia/mq/TesterClass.java +++ /dev/null @@ -1,14 +0,0 @@ -package ch.hepia.mq; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; -import java.util.Arrays; -import java.util.function.Consumer; -import java.util.*; - -public class TesterClass { - public static void main(String[] argv) throws Exception{ - } -} \ 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 index 1f7f5e083adcfb01c788916ba6909e415519a8ef..9e6abe25f1d0984ba0e5f2ac5dc9c8b6c97ef5ec 100644 --- a/src/main/java/ch/hepia/ui/ConnectionController.java +++ b/src/main/java/ch/hepia/ui/ConnectionController.java @@ -2,6 +2,7 @@ package ch.hepia.ui; import ch.hepia.Main; import ch.hepia.config.AppConfig; +import ch.hepia.models.User; import javafx.animation.FadeTransition; import javafx.event.ActionEvent; import javafx.fxml.FXML; @@ -12,8 +13,10 @@ import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.TextField; +import javafx.scene.input.KeyCode; import javafx.stage.Stage; import javafx.stage.WindowEvent; +import javafx.application.Platform; import javafx.util.Duration; import java.net.URL; @@ -52,7 +55,8 @@ public class ConnectionController implements Initializable { fadeIn.setAutoReverse(false); fadeIn.playFromStart(); } else { - // User user = new User(UsernameSelectionTextField.getText()); + 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); @@ -72,5 +76,7 @@ public class ConnectionController implements Initializable { // 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..bcd829cf834905012e22296a3055c79d1fbc304b --- /dev/null +++ b/src/main/java/ch/hepia/ui/MainWindowController.java @@ -0,0 +1,274 @@ +package ch.hepia.ui; + +import ch.hepia.Main; +import ch.hepia.api.transport.Connection; +import ch.hepia.api.transport.Journey; +import ch.hepia.api.transport.LinkAPI; +import ch.hepia.api.transport.Section; +import ch.hepia.config.AppConfig; +import ch.hepia.config.AppContext; +import ch.hepia.events.ChatMessage; +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 org.json.JSONArray; + +import java.io.IOException; +import java.net.URL; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.List; +import java.util.ResourceBundle; + +import javafx.scene.layout.Pane; + +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 launchItinaryButton; + + @FXML + private Button sendMessageButton; + + @FXML + private TextField messageTextBox; + + @FXML + private Pane chatContainer; + + /** + * Shows a sad message when the API 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 { + JSONArray a = transportApi.getStations(newValue); + ArrayList<String> results = new ArrayList<>(); + for (int i = 0; i < a.length(); i++){ + results.add(a.getJSONObject(i).getString("name")); + } + target.getItems().clear(); + target.getItems().addAll(results); + target.show(); + } catch (IOException e) { + showSadMessage(AppConfig.ERROR_API_UNREACHABLE); + } + } + } + + + /** + * 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(); + //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(User user, 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() >= 8) { + chatContainer.getChildren().remove(0); + } + insertMessageIntoQueue(); + chatContainer.getChildren().add(p); + } + + 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); + } + + /** + * 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(); + } + } + + /** + * Sets the form up + * @param url The JavaFX URL handler + * @param resourceBundle The JavaDX ResB handle + */ + @Override + public void initialize(URL url, ResourceBundle resourceBundle) { + currentJourneyLabel.setText("Vous n'avez prévu aucun voyage pour le moment."); + 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)); + } + }); + + LinkAPI transportApi = new LinkAPI(); + AppContext app = Main.getContext(); + 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); + }); + + launchItinaryButton.setOnAction(event -> { + try { + connectionCanvas.getGraphicsContext2D() + .clearRect(0, 0, connectionCanvas.getWidth(), connectionCanvas.getHeight()); + + JSONArray connections = transportApi.getConnections( + originComboBox.getValue(), destinationComboBox.getValue()); + startStopLabel.setText(originComboBox.getValue() + " - " + destinationComboBox.getValue()); + for (int i = 0; i < connections.length(); i++){ + // Now iterating over connections + Connection c = new Connection.ConnectionBuilder(connections.getJSONObject(i)).build(); + drawConnection(c, 30, 100 + 100 * i); + + } + } catch (IOException | ParseException e) { + showSadMessage(AppConfig.ERROR_API_UNREACHABLE); + } + }); + + sendMessageButton.setOnAction(event -> { + if (app.getUser().isPresent() && !messageTextBox.getText().isEmpty()){ + app.getMessageManager().sendChatMessage(new ChatMessage(app.getUser().get(), messageTextBox.getText())); + messageTextBox.clear(); + } + }); + + app.getMessageManager().conditionalSubscribeChatMessage( + chatMessage -> { + Platform.runLater(() -> { + String message = chatMessage.getUser().toString() + ": " + chatMessage.getMessage(); + drawMessage(chatMessage.getUser(), message, AppConfig.CHAT_MESSAGE_ICON, AppConfig.COLOR_BLUE_10_OPACITY); + }); + }, + chatMessage -> !(app.getUser().get().getIgnoredUserList().contains(chatMessage.getUser())) + ); + } +} 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/MainWindow.fxml b/src/main/resources/fxml/MainWindow.fxml index df15eceb11d4e6656f83d662e0c1f9ec21ad5d2e..35e9be27c159b0fe217872191b50afd54745e551 100644 --- a/src/main/resources/fxml/MainWindow.fxml +++ b/src/main/resources/fxml/MainWindow.fxml @@ -1,5 +1,6 @@ <?xml version="1.0" encoding="UTF-8"?> +<?import javafx.scene.canvas.*?> <?import javafx.scene.text.*?> <?import javafx.scene.paint.*?> <?import javafx.scene.shape.*?> @@ -8,48 +9,59 @@ <?import javafx.scene.layout.*?> <?import javafx.collections.FXCollections?> -<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"> - <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"> +<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="launchItinaryButton" 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> - <ComboBox id="stopComboBox" fx:id="stopComboBox" 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="300.0" startY="1.0"> - <stroke> - <LinearGradient> - <stops> - <Stop color="#35b1ee" /> - <Stop color="#004dff" offset="1.0" /> - </stops> - </LinearGradient> - </stroke> - </Line> - <ComboBox id="directionComboBox" fx:id="directionComboBox" editable="true" layoutX="310.0" layoutY="10.0" prefHeight="27.0" prefWidth="282.0" promptText="Choisissez votre destination..." /> - <Line endY="48.0" layoutX="603.0" layoutY="-1.0" startY="1.0"> - <stroke> - <LinearGradient> - <stops> - <Stop color="#35b1ee" /> - <Stop color="#004dff" offset="1.0" /> - </stops> - </LinearGradient> - </stroke> - </Line> - </children> - </Pane> - </top> - <right> - <Pane prefHeight="517.0" prefWidth="326.0" BorderPane.alignment="CENTER" /> - </right> - <center> - <Pane prefHeight="200.0" prefWidth="200.0" BorderPane.alignment="CENTER"> - <children> - <Label id="StopLabel" fx:id="StopLabel" 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" /> - </children> - </Pane> - </center> + <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 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/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..371cc0a12eae483de2b2ccef1d07fddeda5ff7fd --- /dev/null +++ b/src/test/java/ch/hepia/mq/MessageTest.java @@ -0,0 +1,23 @@ +package ch.hepia.mq; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.awt.TrayIcon.MessageType; + +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