Commit e19f8e27 authored by Hallvard Trætteberg's avatar Hallvard Trætteberg

Initial import

parent 70e7abc9
FROM gitpod/workspace-full-vnc
USER gitpod
RUN bash -c ". /home/gitpod/.sdkman/bin/sdkman-init.sh \
&& sdk install java 14.0.2.j9-adpt \
&& sdk default java 14.0.2.j9-adpt"
\ No newline at end of file
image:
file: .gitpod.Dockerfile
tasks:
- init: sdk use java 14.0.2.j9-adpt
command: cd simpleexample
ports:
# used by virtual desktop and vnc, supports JavaFX
- port: 6080
[![Gitpod Ready-to-Code](https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod)](https://gitpod.idi.ntnu.no/#https://gitlab.stud.idi.ntnu.no/it1901/simpleexample)
# simpleexample
An example corresponding to the first stage of the group project
\ No newline at end of file
# ignore maven build directory
target/
# Simpleexample
Dette prosjektet er et eksempel på en nokså minimalistisk trelagsapplikasjon, altså med et domenelag, brukergrensesnitt (UI) og persistens (lagring). Prosjektet inneholder tester for alle lagene, med rimelig god dekningsgrad. Prosjektet er konfigurert med gradle som byggesystem.
## Organisering av koden
Prosjektet er organisert med 2 * 2 = 4 kildekodemapper, kode og ressurser for henholdsvis applikasjonen selv og testene:
- **src/main/java** for koden til applikasjonen
- **src/main/resources** for tilhørende ressurser, f.eks. data-filer og FXML-filer, som brukes av applikasjonen.
- **src/test/java** for testkoden
- **src/test/resources** for tilhørende ressurser, f.eks. data-filer og FXML-filer, som brukes av testene.
Dette er en vanlig organisering for prosjekter som bygges med gradle (og maven).
## Domenelaget
Domenelaget inneholder alle klasser og logikk knyttet til dataene som applikasjonen handler om og håndterer. Dette laget skal være helt uavhengig av brukergrensesnittet eller lagingsteknikken som brukes.
Vår app handler om samlinger av såkalte geo-lokasjoner, altså steder identifisert med lengde- og breddegrader. Domenelaget inneholder klasser for å representere og håndtere slike, og disse finnes i **[simpleex.core](src/main/java/simpleex/core/README.md)**-pakken.
## Brukergrensesnittlaget
Brukergrensesnittlaget inneholder alle klasser og logikk knyttet til visning og handlinger på dataene i domenelaget. Brukergrensesnittet til vår app viser frem en liste av geo-lokasjoner, den som velges vises på et kart. En flytte og legge til geo-lokasjoner. Samlingen med geo-lokasjoner kan lagres og evt. leses inn igjen.
Brukergrensesnittet er laget med JavaFX og FXML og finnes i **[simpleex.ui](src/main/java/simpleex/ui/README.md)**-pakken (JavaFX-koden i **src/main/java** og FXML-filen i **src/main/resources**)
## Persistenslaget
Persistenslaget inneholder alle klasser og logikk for lagring (skriving og lesing) av dataene i domenelaget. Vårt persistenslag implementerer fillagring med JSON-syntaks.
Persistenslaget finnes i **[simpleex.json](src/main/java/simpleex/json/README.md)**-pakken.
## Bygging med gradle
For litt mer komplekse prosjekter, er det lurt å bruke et såkalt byggeverktøy, f.eks. gradle eller maven, for å automatisere oppgaver som kjøring av tester, sjekk av ulike typer kvalitet osv. Vårt prosjekt er konfigurert til å bruke [gradle](https://gradle.org), og følgelig har prosjektet en del filer og mapper spesifikt for det:
- gradle, gradlew og gradlew.bat - mappe og filer for kjøring av gradle, som regel rigget opp en gang for alle ved opprettelse av prosjektet
- settings.gradle - inneholder navnet på prosjektet og evt. navnet på mapper for delprosjekter (ikke brukt her)
- build.gradle - konfigurasjon av gradle-tillegg basert på typen prosjekt
Av disse er det build.gradle som krever mest forklaring, settings.gradle trenger bare å inneholde én linje, som navngir prosjektet: `rootProject.name = 'simpleexample'`. For litt bakgrunn om bygging generelt og gradle spesielt, se [her](ci-gradle.md).
Vårt gradle-bygg er satt opp med tillegg for java-applikasjoner generelt (**application**) og med JavaFX spesielt (**org.openjfx.javafxplugin**). Vi trenger (minst) java-versjon 10 og javafx-version 11.
I tillegg bruker vi diverse kodekvalitetsanalyseverktøy ([jacoco](https://github.com/jacoco/jacoco) med **[jacoco](https://docs.gradle.org/current/userguide/jacoco_plugin.html)**, [PMD](https://pmd.github.io) med **[pmd](https://docs.gradle.org/current/userguide/pmd_plugin.html)**, [spotbugs](https://spotbugs.github.io) med **[com.github.spotbugs](https://spotbugs.readthedocs.io/en/latest/gradle.html)** og [checkstyle](https://checkstyle.sourceforge.io) med **[checkstyle](https://docs.gradle.org/current/userguide/checkstyle_plugin.html)**). Disse er satt opp slik at de ikke stopper bygget om ikke alt er på stell. spotbugs er satt opp til å lage rapport med html i stedet for xml.
De fleste avhengighetene hentes fra de vanlige sentrale repo-ene, med unntak av FxMapControl som er lagt ved som jar-fil i **libs**-mappa.
This diff is collapsed.
This diff is collapsed.
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>it1901</groupId>
<artifactId>simpleexample</artifactId>
<version>0.0.1-SNAPSHOT</version>
<dependencies>
<!-- JavaFX -->
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId>
<version>14.0.1</version>
</dependency>
<!-- Test with JUnit5 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.4.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.4.2</version>
<scope>test</scope>
</dependency>
<!-- Test with TextFX -->
<dependency>
<groupId>org.testfx</groupId>
<artifactId>testfx-core</artifactId>
<version>4.0.16-alpha</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testfx</groupId>
<artifactId>testfx-junit5</artifactId>
<version>4.0.16-alpha</version>
<scope>test</scope>
</dependency>
</dependencies>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<build>
<plugins>
<!-- Compiling code -->
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<encoding>UTF-8</encoding>
<release>14</release>
</configuration>
</plugin>
<!-- Running tests -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M5</version>
<configuration>
<redirectTestOutputToFile>true</redirectTestOutputToFile>
<argLine>--add-exports javafx.graphics/com.sun.javafx.application=ALL-UNNAMED</argLine>
</configuration>
</plugin>
<!-- Running JavaFX code -->
<plugin>
<groupId>org.openjfx</groupId>
<artifactId>javafx-maven-plugin</artifactId>
<version>0.0.4</version>
<configuration>
<mainClass>javafxapp.App</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</project>
package simpleex.core;
/**
* Represents a geo-location as a latitude, longitude pair.
* @author hal
*
*/
public class LatLong {
public static final String SEPARATOR = ",";
private final double latitude;
private final double longitude;
/**
* Initialize a LatLong with provided latitude and longitude.
* @param latitude the latitude
* @param longitude the longitude
*/
public LatLong(final double latitude, final double longitude) {
super();
this.latitude = latitude;
this.longitude = longitude;
}
/**
* Gets the latitude.
* @return the latitude
*/
public double getLatitude() {
return latitude;
}
/**
* Gets the longitude.
* @return the longitude
*/
public double getLongitude() {
return longitude;
}
@Override
public String toString() {
return latitude + SEPARATOR + longitude;
}
@Override
public int hashCode() {
final int prime = 31;
long temp = Double.doubleToLongBits(latitude);
int result = prime + (int) (temp ^ (temp >>> 32));
temp = Double.doubleToLongBits(longitude);
result = prime * result + (int) (temp ^ (temp >>> 32));
return result;
}
@Override
public boolean equals(final Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
final LatLong other = (LatLong) obj;
return (Double.doubleToLongBits(latitude) == Double.doubleToLongBits(other.latitude)
&& Double.doubleToLongBits(longitude) == Double.doubleToLongBits(other.longitude));
}
/**
* Creates a LatLong object from a String.
* The format is &lt;latitude&gt;,&lt;longitude&gt;.
* @param s the String to parse
* @return the new LatLong object
*/
public static LatLong valueOf(final String s) {
return valueOf(s, SEPARATOR);
}
/**
* Creates a LatLong object from a String, using a specific separator.
* The format is &lt;latitude&gt;&lt;separator&gt;&lt;longitude&gt;.
* @param s
* @param sep
* @return
*/
public static LatLong valueOf(final String s, final String sep) {
final int pos = s.indexOf(sep);
if (pos < 0) {
throw new IllegalArgumentException("No '" + sep + "' in " + s);
}
final double lat = Double.valueOf(s.substring(0, pos).trim());
final double lon = Double.valueOf(s.substring(pos + sep.length()).trim());
return new LatLong(lat, lon);
}
/* :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: */
/* :: : */
/* :: This routine calculates the distance between two points (given the : */
/* :: latitude/longitude of those points). It is being used to calculate : */
/* :: the distance between two locations using GeoDataSource (TM) products : */
/* :: : */
/* :: Definitions: : */
/* :: South latitudes are negative, east longitudes are positive : */
/* :: : */
/* :: Passed to function: : */
/* :: lat1, lon1 = Latitude and Longitude of point 1 (in decimal degrees) : */
/* :: lat2, lon2 = Latitude and Longitude of point 2 (in decimal degrees) : */
/* :: Worldwide cities and other features databases with latitude longitude : */
/* :: are available at http://www.geodatasource.com : */
/* :: : */
/* :: For enquiries, please contact sales@geodatasource.com : */
/* :: : */
/* :: Official Web site: http://www.geodatasource.com : */
/* :: : */
/* :: GeoDataSource.com (C) All Rights Reserved 2015 : */
/* :: : */
/* :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: */
public static double distance(final double lat1, final double lon1, final double lat2,
final double lon2) {
if (lon1 == lon2 && lat1 == lat2) {
return 0.0;
}
final double theta = lon1 - lon2;
double dist = Math.sin(deg2rad(lat1)) * Math.sin(deg2rad(lat2))
+ Math.cos(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) * Math.cos(deg2rad(theta));
dist = Math.acos(dist);
// convert to degrees
dist = rad2deg(dist);
dist = dist * 60 * 1.1515;
// convert to meters
dist = dist * 1609.344;
return dist;
}
/**
* Computes the distance in meters between two LatLong objects.
* @param latLong1 the first LatLong
* @param latLong2 the other LatLong
* @return the distance in meters
*/
public static double distance(final LatLong latLong1, final LatLong latLong2) {
return distance(latLong1.latitude, latLong1.longitude, latLong2.latitude, latLong2.longitude);
}
/**
* Computes the distance in meters between this LatLong and an other one.
* @param latLong2 the other LatLong
* @return the distance in meters
*/
public double distance(final LatLong latLong2) {
return distance(latitude, longitude, latLong2.latitude, latLong2.longitude);
}
/* ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: */
/* :: This function converts decimal degrees to radians : */
/* ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: */
private static double deg2rad(final double deg) {
return (deg * Math.PI / 180.0);
}
/* ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: */
/* :: This function converts radians to decimal degrees : */
/* ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: */
private static double rad2deg(final double rad) {
return (rad * 180 / Math.PI);
}
}
package simpleex.core;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
public class LatLongs implements Iterable<LatLong> {
final List<LatLong> latLongs = new ArrayList<>();
public LatLongs() {}
public LatLongs(final double... latLongsArray) {
addLatLongs(latLongsArray);
}
public LatLongs(final LatLong... latLongs) {
addLatLongs(latLongs);
}
public LatLongs(final Collection<LatLong> latLongs) {
addLatLongs(latLongs);
}
@Override
public Iterator<LatLong> iterator() {
return latLongs.iterator();
}
public int getLatLongCount() {
return latLongs.size();
}
public LatLong getLatLong(final int num) {
return latLongs.get(num);
}
public void setLatLong(final int num, final LatLong latLong) {
latLongs.set(num, latLong);
}
public int addLatLong(final LatLong latLong) {
final int pos = latLongs.size();
latLongs.add(latLong);
return pos;
}
public final int addLatLongs(final Collection<LatLong> latLongs) {
final int pos = this.latLongs.size();
this.latLongs.addAll(latLongs);
return pos;
}
public final int addLatLongs(final LatLong... latLongs) {
return addLatLongs(List.of(latLongs));
}
public final int addLatLongs(final double... latLongsArray) {
final Collection<LatLong> latLongs = new ArrayList<>(latLongsArray.length / 2);
for (int i = 0; i < latLongsArray.length; i += 2) {
latLongs.add(new LatLong(latLongsArray[i], latLongsArray[i + 1]));
}
return addLatLongs(latLongs);
}
public LatLong removeLatLong(final int num) {
return latLongs.remove(num);
}
}
# Kildekode for domenelaget
Domenelaget utgjøres av en samling av geo-lokasjoner representert vha. to klasse:
- LatLong - en geo-lokasjon, representert vha. lengde og breddegrad
- LatLongs - en samling LatLong-objekter
```plantuml
class LatLong {
double latitude
double longitude
}
class LatLongs
LatLongs *--> "*" LatLong: "latLongs"
```
package simpleex.json;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.io.IOException;
import simpleex.core.LatLong;
public class LatLongDeserializer extends JsonDeserializer<LatLong> {
private static final int ARRAY_JSON_NODE_SIZE = 2;
@Override
public LatLong deserialize(final JsonParser jsonParser, final DeserializationContext deserContext)
throws IOException, JsonProcessingException {
final JsonNode jsonNode = jsonParser.getCodec().readTree(jsonParser);
return deserialize(jsonNode);
}
public LatLong deserialize(final JsonNode jsonNode) throws JsonProcessingException {
if (jsonNode instanceof ObjectNode) {
final ObjectNode objectNode = (ObjectNode) jsonNode;
final double latitude = objectNode.get(LatLongSerializer.LATITUDE_FIELD_NAME).asDouble();
final double longitude = objectNode.get(LatLongSerializer.LONGITUDE_FIELD_NAME).asDouble();
return new LatLong(latitude, longitude);
} else if (jsonNode instanceof ArrayNode) {
final ArrayNode locationArray = (ArrayNode) jsonNode;
if (locationArray.size() == ARRAY_JSON_NODE_SIZE) {
final double latitude = locationArray.get(0).asDouble();
final double longitude = locationArray.get(1).asDouble();
return new LatLong(latitude, longitude);
}
}
return null;
}
}
package simpleex.json;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import java.io.IOException;
import simpleex.core.LatLong;
public class LatLongSerializer extends JsonSerializer<LatLong> {
public static final String LONGITUDE_FIELD_NAME = "longitude";
public static final String LATITUDE_FIELD_NAME = "latitude";
@Override
public void serialize(final LatLong latLon, final JsonGenerator jsonGen,
final SerializerProvider provider) throws IOException {
jsonGen.writeStartObject();
jsonGen.writeFieldName(LATITUDE_FIELD_NAME);
jsonGen.writeNumber(latLon.getLatitude());
jsonGen.writeFieldName(LONGITUDE_FIELD_NAME);
jsonGen.writeNumber(latLon.getLongitude());
jsonGen.writeEndObject();
}
}
package simpleex.json;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import simpleex.core.LatLong;
import simpleex.core.LatLongs;
public class LatLongsDeserializer extends JsonDeserializer<LatLongs> {
private final LatLongDeserializer latLongDeserializer = new LatLongDeserializer();
@Override
public LatLongs deserialize(final JsonParser jsonParser,
final DeserializationContext deserContext) throws IOException, JsonProcessingException {
final JsonNode jsonNode = jsonParser.getCodec().readTree(jsonParser);
if (jsonNode instanceof ArrayNode) {
final ArrayNode latLongsArray = (ArrayNode) jsonNode;
final Collection<LatLong> latLongs = new ArrayList<>(latLongsArray.size());
for (final JsonNode latLongNode : latLongsArray) {
final LatLong latLong = latLongDeserializer.deserialize(latLongNode);
latLongs.add(latLong);
}
return new LatLongs(latLongs);
}
return null;
}
}
package simpleex.json;
import com.fasterxml.jackson.core.Version;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.module.SimpleDeserializers;
import com.fasterxml.jackson.databind.module.SimpleSerializers;
import simpleex.core.LatLong;
import simpleex.core.LatLongs;
public class LatLongsModule extends Module {
@Override
public String getModuleName() {
return "LatLongsModule";
}
@Override
public Version version() {
return Version.unknownVersion();
}
private final SimpleSerializers serializers = new SimpleSerializers();
private final SimpleDeserializers deserializers = new SimpleDeserializers();
public LatLongsModule() {
serializers.addSerializer(LatLong.class, new LatLongSerializer());
serializers.addSerializer(LatLongs.class, new LatLongsSerializer());
deserializers.addDeserializer(LatLong.class, new LatLongDeserializer());
deserializers.addDeserializer(LatLongs.class, new LatLongsDeserializer());
}
@Override
public void setupModule(final SetupContext context) {
context.addSerializers(serializers);
context.addDeserializers(deserializers);
}
}
package simpleex.json;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import java.io.IOException;
import simpleex.core.LatLong;
import simpleex.core.LatLongs;
public class LatLongsSerializer extends JsonSerializer<LatLongs> {
@Override
public void serialize(final LatLongs latLongs, final JsonGenerator jsonGen,
final SerializerProvider provider) throws IOException {
jsonGen.writeStartArray(latLongs.getLatLongCount());
for (final LatLong latLong : latLongs) {
jsonGen.writeObject(latLong);
}
jsonGen.writeEndArray();
}
}
# Kildekode for persistenslaget
Persistenslaget bruker [Jackson-biblioteket](https://github.com/FasterXML/jackson) for å serialisere objekter til [JSON](https://www.json.org).
For hver [domeneklasse](../core/README.md) finnes tilsvarende klasser for serialisering (konvertering av domeneobjekter til tekststrøm) og deserialisering (parsing av tekststrøm og opprettelse av tilsvarende domeneobjekter). I tillegg finnes det en klasse, **JacksonConfigurator**, som inneholder diverse metoder for å lage og konfigurere **ObjectMapper**- og **SimpleModule**-objektene som trengs for å serialisere og deserialisere objekter.
package simpleex.ui;
import javafx.event.EventHandler;
import javafx.geometry.Point2D;
import javafx.scene.Node;
import javafx.scene.input.MouseEvent;
public class DraggableNodeController {
private final NodeDraggedHandler nodeDraggedHandler;
public DraggableNodeController() {
this(null);
}
public DraggableNodeController(final NodeDraggedHandler nodeDraggedHandler) {
this.nodeDraggedHandler = (nodeDraggedHandler != null ? nodeDraggedHandler : (node, x, y) -> {});
}
private boolean immediate = false;
public void setImmediate(final boolean immediate) {
this.immediate = immediate;
}
private Node currentNode = null;
private Point2D startPoint = null;
private Point2D startTranslate = null;
private final EventHandler<MouseEvent> mousePressedHandler = this::mousePressed;
private final EventHandler<MouseEvent> mouseDraggedHandler = this::mouseDragged;
private final EventHandler<MouseEvent> mouseReleasedHandler = this::mouseReleased;
public void attach(final Node node) {
node.setOnMousePressed(mousePressedHandler);
node.setOnMouseDragged(mouseDraggedHandler);
node.setOnMouseReleased(mouseReleasedHandler);
}
public void detach(final Node node) {
node.setOnMousePressed(null);
node.setOnMouseDragged(null);
node.setOnMouseReleased(null);
}
private void mousePressed(final MouseEvent mouseEvent) {
if (currentNode == null && mouseEvent.getSource() instanceof Node) {
currentNode = (Node) mouseEvent.getSource();
startPoint = new Point2D(mouseEvent.getSceneX(), mouseEvent.getSceneY());
startTranslate = new Point2D(currentNode.getTranslateX(), currentNode.getTranslateY());
mouseEvent.consume();
}
}
private void mouseDragged(final MouseEvent mouseEvent) {
if (currentNode != null && currentNode == mouseEvent.getSource()) {
final double dx = mouseEvent.getSceneX() - startPoint.getX();
final double dy = mouseEvent.getSceneY() - startPoint.getY();
updateNode(dx, dy);
}
}
protected void updateNode(final double dx, final double dy) {
if (immediate) {
nodeDraggedHandler.nodeDragged(currentNode, dx, dy);
startPoint = startPoint.add(dx, dy);
} else if (currentNode != null) {
currentNode.setTranslateX(startTranslate.getX() + dx);
currentNode.setTranslateY(startTranslate.getY() + dy);
}
}
private void mouseReleased(final MouseEvent mouseEvent) {
if (currentNode != null && currentNode == mouseEvent.getSource()) {
final double dx = mouseEvent.getSceneX() - startPoint.getX();
final double dy = mouseEvent.getSceneY() - startPoint.getY();
if (!immediate) {
currentNode.setTranslateX(startTranslate.getX());
currentNode.setTranslateY(startTranslate.getY());
}
final Node node = currentNode;
currentNode = null;
nodeDraggedHandler.nodeDragged(node, dx, dy);
}
}
public interface NodeDraggedHandler {