diff --git a/simpleexample2/core/src/main/java/simpleex/core/LatLongs.java b/simpleexample2/core/src/main/java/simpleex/core/LatLongs.java index a6b992a2b5f488e575e31349891ccc1bd5efc5da..a4a3d9de9ed56925ed4e5ed9dc0d7348083016b3 100644 --- a/simpleexample2/core/src/main/java/simpleex/core/LatLongs.java +++ b/simpleexample2/core/src/main/java/simpleex/core/LatLongs.java @@ -48,6 +48,14 @@ public class LatLongs implements Iterable<LatLong> { return latLongs.iterator(); } + /** + * Returns a list of all the LatLong objects in this LatLongs. + * @return the list of LatLong objects + */ + public List<LatLong> toList() { + return new ArrayList<>(latLongs); + } + /** * Gets the number of LatLong objects. * @return the number of LatLong objects diff --git a/simpleexample2/core/src/test/java/simpleex/core/LatLongsTest.java b/simpleexample2/core/src/test/java/simpleex/core/LatLongsTest.java index f493e45f188efa81479fd043d063af4ced89b893..a79919fca295074c620a3e91c26db0bdcdcece5e 100644 --- a/simpleexample2/core/src/test/java/simpleex/core/LatLongsTest.java +++ b/simpleexample2/core/src/test/java/simpleex/core/LatLongsTest.java @@ -2,6 +2,7 @@ package simpleex.core; import java.util.Arrays; import java.util.Iterator; +import java.util.List; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -44,6 +45,15 @@ public class LatLongsTest { new LatLong(63.1, 10.2), new LatLong(63.1, 10.1)); } + @Test + public void testToList() { + final LatLongs latLongs = new LatLongs(63.0, 10.3, 63.1, 10.2); + final List<LatLong> list = latLongs.toList(); + Assert.assertEquals(2, list.size()); + Assert.assertEquals(new LatLong(63.0, 10.3), list.get(0)); + Assert.assertEquals(new LatLong(63.1, 10.2), list.get(1)); + } + @Test public void testAddLatLong() { latLongs.addLatLong(new LatLong(63.0, 10.3)); diff --git a/simpleexample2/fxui/src/main/java/simpleex/ui/AbstractFxAppController.java b/simpleexample2/fxui/src/main/java/simpleex/ui/AbstractFxAppController.java new file mode 100644 index 0000000000000000000000000000000000000000..6cd28996b148f720f2d55bce58c2e45e57d2122a --- /dev/null +++ b/simpleexample2/fxui/src/main/java/simpleex/ui/AbstractFxAppController.java @@ -0,0 +1,184 @@ +package simpleex.ui; + +import com.fasterxml.jackson.databind.ObjectMapper; +import fxmapcontrol.Location; +import fxmapcontrol.MapBase; +import fxmapcontrol.MapItemsControl; +import fxmapcontrol.MapNode; +import fxmapcontrol.MapProjection; +import javafx.collections.FXCollections; +import javafx.fxml.FXML; +import javafx.geometry.Point2D; +import javafx.scene.Node; +import javafx.scene.control.Alert; +import javafx.scene.control.Alert.AlertType; +import javafx.scene.control.ButtonType; +import javafx.scene.control.ListView; +import javafx.scene.control.Slider; +import simpleex.core.LatLong; +import simpleex.json.LatLongsModule; + +/* +@startuml +class AbstractFxAppController +class LatLongs +class BorderPane +class "ListView<LatLong>" as ListView +class "fxmapcontrol.MapBase" as MapBase + +BorderPane *--> ListView: "left" +BorderPane *--> MapBase: "center" + +AbstractFxAppController --> MapBase: "mapView" +AbstractFxAppController --> ListView: "locationListView" +@enduml + */ + +/** + * The controller for the app. + * @author hal + * + */ +public abstract class AbstractFxAppController { + + private LatLongsDataAccess dataAccess; + + protected LatLongsDataAccess getDataAccess() { + return dataAccess; + } + + protected void setDataAccess(final LatLongsDataAccess dataAccess) { + this.dataAccess = dataAccess; + if (locationListView != null) { + updateLocationViewList(0); + } + } + + @FXML + private ListView<LatLong> locationListView; + + @FXML + private MapBase mapView; + + private MapItemsControl<MapNode> markersParent; + private MapMarker marker = null; + private DraggableNodeController draggableMapController = null; + private DraggableNodeController draggableMarkerController = null; + + @FXML + private Slider zoomSlider; + + @FXML + private void initialize() { + // map stuff + // mapView.getChildren().add(MapTileLayer.getOpenStreetMapLayer()); + zoomSlider.valueProperty() + .addListener((prop, oldValue, newValue) -> mapView.setZoomLevel(zoomSlider.getValue())); + zoomSlider.setValue(8); + markersParent = new MapItemsControl<MapNode>(); + mapView.getChildren().add(markersParent); + draggableMapController = new DraggableNodeController(this::handleMapDragged); + draggableMapController.setImmediate(true); + draggableMapController.attach(mapView); + draggableMarkerController = new DraggableNodeController(this::handleMarkerDragged); + // the location list + locationListView.getSelectionModel().selectedIndexProperty() + .addListener((prop, oldValue, newValue) -> updateMapMarker(true)); + } + + private void handleMapDragged(final Node node, final double dx, final double dy) { + final MapProjection projection = mapView.getProjection(); + final Point2D point = projection.locationToViewportPoint(mapView.getCenter()); + final Location newCenter = projection.viewportPointToLocation(point.add(-dx, -dy)); + mapView.setCenter(newCenter); + } + + private void handleMarkerDragged(final Node node, final double dx, final double dy) { + final MapProjection projection = mapView.getProjection(); + final Point2D point = projection.locationToViewportPoint(marker.getLocation()); + final Location newLocation = projection.viewportPointToLocation(point.add(dx, dy)); + dataAccess.setLatLong(locationListView.getSelectionModel().getSelectedIndex(), + location2LatLong(newLocation)); + updateLocationViewListSelection(false); + } + + private LatLong location2LatLong(final Location newLocation) { + return new LatLong(newLocation.getLatitude(), newLocation.getLongitude()); + } + + private void updateMapMarker(final boolean centerOnMarker) { + final int num = locationListView.getSelectionModel().getSelectedIndex(); + if (num < 0) { + markersParent.getItems().clear(); + if (draggableMarkerController != null) { + draggableMarkerController.detach(marker); + } + marker = null; + } else { + final LatLong latLong = dataAccess.getLatLong(num); + if (marker == null) { + marker = new MapMarker(latLong); + markersParent.getItems().add(marker); + if (draggableMarkerController != null) { + draggableMarkerController.attach(marker); + } + } else { + marker.setLocation(latLong); + } + if (centerOnMarker) { + mapView.setCenter(marker.getLocation()); + } + } + } + + @FXML + void handleAddLocation() { + final Location center = mapView.getCenter(); + final LatLong latLong = location2LatLong(center); + final int pos = dataAccess.addLatLong(latLong); + updateLocationViewList(pos); + } + + private void updateLocationViewListSelection(final Boolean updateMapMarker) { + final int selectedIndex = locationListView.getSelectionModel().getSelectedIndex(); + locationListView.getItems().set(selectedIndex, dataAccess.getLatLong(selectedIndex)); + if (updateMapMarker != null) { + updateMapMarker(updateMapMarker); + } + } + + protected void updateLocationViewList(int selectedIndex) { + final LatLong[] latLongs = dataAccess.getAllLatLongs().toArray(new LatLong[0]); + final int oldSelectionIndex = locationListView.getSelectionModel().getSelectedIndex(); + locationListView.setItems(FXCollections.observableArrayList(latLongs)); + if (selectedIndex < 0 || selectedIndex >= latLongs.length) { + selectedIndex = oldSelectionIndex; + } + if (selectedIndex >= 0 && selectedIndex < latLongs.length) { + locationListView.getSelectionModel().select(selectedIndex); + } + } + + private ObjectMapper objectMapper; + + /** + * Gets the ObjectMapper used by this controller. + * @return the ObjectMapper used by this controller + */ + public ObjectMapper getObjectMapper() { + if (objectMapper == null) { + objectMapper = new ObjectMapper(); + objectMapper.registerModule(new LatLongsModule()); + } + return objectMapper; + } + + protected void showExceptionDialog(final String message) { + final Alert alert = new Alert(AlertType.ERROR, message, ButtonType.CLOSE); + alert.showAndWait(); + } + + protected void showExceptionDialog(final String message, final Exception e) { + showExceptionDialog(message + ": " + e.getLocalizedMessage()); + } +} diff --git a/simpleexample2/fxui/src/main/java/simpleex/ui/FxAppController.java b/simpleexample2/fxui/src/main/java/simpleex/ui/FxAppController.java index 03ce1bba8d8193f1b66b2ee30bd8abccd23d25d9..8fa67a6aac8f08f61b102087c87d3aa4a7af4816 100644 --- a/simpleexample2/fxui/src/main/java/simpleex/ui/FxAppController.java +++ b/simpleexample2/fxui/src/main/java/simpleex/ui/FxAppController.java @@ -1,31 +1,15 @@ package simpleex.ui; -import com.fasterxml.jackson.databind.ObjectMapper; -import fxmapcontrol.Location; -import fxmapcontrol.MapBase; -import fxmapcontrol.MapItemsControl; -import fxmapcontrol.MapNode; -import fxmapcontrol.MapProjection; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import javafx.collections.FXCollections; import javafx.event.ActionEvent; import javafx.fxml.FXML; -import javafx.geometry.Point2D; -import javafx.scene.Node; -import javafx.scene.control.Alert; -import javafx.scene.control.Alert.AlertType; -import javafx.scene.control.ButtonType; -import javafx.scene.control.ListView; -import javafx.scene.control.Slider; import javafx.stage.FileChooser; -import simpleex.core.LatLong; import simpleex.core.LatLongs; -import simpleex.json.LatLongsModule; /* @startuml @@ -49,23 +33,13 @@ FxAppController --> ListView: "locationListView" * @author hal * */ -public class FxAppController { - - private LatLongs latLongs; +public class FxAppController extends AbstractFxAppController { /** * Initializes the controller. */ public FxAppController() { - latLongs = new LatLongs(); - } - - /** - * Gets the LatLongs objects used by the controller. - * @return the controller's LatLongs objects - */ - public LatLongs getLatLongs() { - return latLongs; + setDataAccess(new LocalLatLongsDataAccess(new LatLongs())); } /** @@ -73,121 +47,10 @@ public class FxAppController { * @param latLongs the LatLongs object to use */ public void setLatLongs(final LatLongs latLongs) { - this.latLongs = latLongs; + setDataAccess(new LocalLatLongsDataAccess(latLongs)); updateLocationViewList(0); } - @FXML - private ListView<LatLong> locationListView; - - @FXML - private MapBase mapView; - - private MapItemsControl<MapNode> markersParent; - private MapMarker marker = null; - private DraggableNodeController draggableMapController = null; - private DraggableNodeController draggableMarkerController = null; - - @FXML - private Slider zoomSlider; - - @FXML - private void initialize() { - // map stuff - // mapView.getChildren().add(MapTileLayer.getOpenStreetMapLayer()); - zoomSlider.valueProperty() - .addListener((prop, oldValue, newValue) -> mapView.setZoomLevel(zoomSlider.getValue())); - zoomSlider.setValue(8); - markersParent = new MapItemsControl<MapNode>(); - mapView.getChildren().add(markersParent); - draggableMapController = new DraggableNodeController(this::handleMapDragged); - draggableMapController.setImmediate(true); - draggableMapController.attach(mapView); - draggableMarkerController = new DraggableNodeController(this::handleMarkerDragged); - // the location list - locationListView.getSelectionModel().selectedIndexProperty() - .addListener((prop, oldValue, newValue) -> updateMapMarker(true)); - } - - private void handleMapDragged(final Node node, final double dx, final double dy) { - final MapProjection projection = mapView.getProjection(); - final Point2D point = projection.locationToViewportPoint(mapView.getCenter()); - final Location newCenter = projection.viewportPointToLocation(point.add(-dx, -dy)); - mapView.setCenter(newCenter); - } - - private void handleMarkerDragged(final Node node, final double dx, final double dy) { - final MapProjection projection = mapView.getProjection(); - final Point2D point = projection.locationToViewportPoint(marker.getLocation()); - final Location newLocation = projection.viewportPointToLocation(point.add(dx, dy)); - getLatLongs().setLatLong(locationListView.getSelectionModel().getSelectedIndex(), - location2LatLong(newLocation)); - updateLocationViewListSelection(false); - } - - private LatLong location2LatLong(final Location newLocation) { - return new LatLong(newLocation.getLatitude(), newLocation.getLongitude()); - } - - private void updateMapMarker(final boolean centerOnMarker) { - final int num = locationListView.getSelectionModel().getSelectedIndex(); - if (num < 0 || num >= getLatLongs().getLatLongCount()) { - markersParent.getItems().clear(); - if (draggableMarkerController != null) { - draggableMarkerController.detach(marker); - } - marker = null; - } else { - final LatLong latLong = getLatLongs().getLatLong(num); - if (marker == null) { - marker = new MapMarker(latLong); - markersParent.getItems().add(marker); - if (draggableMarkerController != null) { - draggableMarkerController.attach(marker); - } - } else { - marker.setLocation(latLong); - } - if (centerOnMarker) { - mapView.setCenter(marker.getLocation()); - } - } - } - - @FXML - private void handleAddLocation() { - final Location center = mapView.getCenter(); - final int pos = getLatLongs().addLatLong(location2LatLong(center)); - updateLocationViewList(pos); - } - - private void updateLocationViewListSelection(final Boolean updateMapMarker) { - final int selectedIndex = locationListView.getSelectionModel().getSelectedIndex(); - updateLocationViewListItem(selectedIndex); - if (updateMapMarker != null) { - updateMapMarker(updateMapMarker); - } - } - - private void updateLocationViewListItem(final int index) { - locationListView.getItems().set(index, getLatLongs().getLatLong(index)); - } - - private void updateLocationViewList(int selectedIndex) { - final LatLong[] latLongs = new LatLong[getLatLongs().getLatLongCount()]; - for (int i = 0; i < latLongs.length; i++) { - latLongs[i] = getLatLongs().getLatLong(i); - } - final int oldSelectionIndex = locationListView.getSelectionModel().getSelectedIndex(); - locationListView.setItems(FXCollections.observableArrayList(latLongs)); - if (selectedIndex < 0 || selectedIndex >= latLongs.length) { - selectedIndex = oldSelectionIndex; - } - if (selectedIndex >= 0 && selectedIndex < getLatLongs().getLatLongCount()) { - locationListView.getSelectionModel().select(selectedIndex); - } - } - // File menu items private FileChooser fileChooser; @@ -212,29 +75,6 @@ public class FxAppController { } } - private ObjectMapper objectMapper; - - /** - * Gets the ObjectMapper used by this controller. - * @return the ObjectMapper used by this controller - */ - public ObjectMapper getObjectMapper() { - if (objectMapper == null) { - objectMapper = new ObjectMapper(); - objectMapper.registerModule(new LatLongsModule()); - } - return objectMapper; - } - - private void showExceptionDialog(final String message) { - final Alert alert = new Alert(AlertType.ERROR, message, ButtonType.CLOSE); - alert.showAndWait(); - } - - private void showExceptionDialog(final String message, final Exception e) { - showExceptionDialog(message + ": " + e.getLocalizedMessage()); - } - private void showSaveExceptionDialog(final File location, final Exception e) { showExceptionDialog("Oops, problem saving to " + location, e); } @@ -245,7 +85,7 @@ public class FxAppController { final File selection = fileChooser.showSaveDialog(null); if (selection != null) { try (OutputStream outputStream = new FileOutputStream(selection, false)) { - getObjectMapper().writeValue(outputStream, getLatLongs()); + getObjectMapper().writeValue(outputStream, getDataAccess().getAllLatLongs()); } catch (final IOException e) { showSaveExceptionDialog(selection, e); } diff --git a/simpleexample2/fxui/src/main/java/simpleex/ui/FxAppControllerUsingRest.java b/simpleexample2/fxui/src/main/java/simpleex/ui/FxAppControllerUsingRest.java new file mode 100644 index 0000000000000000000000000000000000000000..acdc5b19e043ae11d6114aee67f758dac4cbf690 --- /dev/null +++ b/simpleexample2/fxui/src/main/java/simpleex/ui/FxAppControllerUsingRest.java @@ -0,0 +1,30 @@ +package simpleex.ui; + +/* +@startuml +class FxAppController +class LatLongs +class BorderPane +class "ListView<LatLong>" as ListView +class "fxmapcontrol.MapBase" as MapBase + +BorderPane *--> ListView: "left" +BorderPane *--> MapBase: "center" + +FxAppController --> LatLongs: "latLongs" +FxAppController --> MapBase: "mapView" +FxAppController --> ListView: "locationListView" +@enduml + */ + +/** + * The controller for the rest app. + * @author hal + * + */ +public class FxAppControllerUsingRest extends AbstractFxAppController { + + public FxAppControllerUsingRest() { + setDataAccess(new RestLatLongsDataAccess(getObjectMapper())); + } +} diff --git a/simpleexample2/fxui/src/main/java/simpleex/ui/LatLongsDataAccess.java b/simpleexample2/fxui/src/main/java/simpleex/ui/LatLongsDataAccess.java new file mode 100644 index 0000000000000000000000000000000000000000..d8f28e7fb1c037c3419c66b8e93bd4efe294938b --- /dev/null +++ b/simpleexample2/fxui/src/main/java/simpleex/ui/LatLongsDataAccess.java @@ -0,0 +1,39 @@ +package simpleex.ui; + +import java.util.Collection; +import simpleex.core.LatLong; + +/** + * Data access methods used by the controller. + * @author hal + * + */ +public interface LatLongsDataAccess { + + /** + * Gets all the (internal) LatLong objects. + * @return the (internal) LatLong objects + */ + Collection<LatLong> getAllLatLongs(); + + /** + * Gets a specific LatLong object by index. + * @param num the index of the LatLong object to get + * @return the LatLong object at the specified index + */ + LatLong getLatLong(int num); + + /** + * Sets a the LatLong object at a specific index. + * @param num the index of the LatLong object to set + * @param latLong the new LatLong object + */ + void setLatLong(int index, LatLong latLong); + + /** + * Adds a LatLong object + * @param latLong the LatLong object to add + * @return the index where the LatLong object was added + */ + int addLatLong(LatLong latLong); +} diff --git a/simpleexample2/fxui/src/main/java/simpleex/ui/LocalLatLongsDataAccess.java b/simpleexample2/fxui/src/main/java/simpleex/ui/LocalLatLongsDataAccess.java new file mode 100644 index 0000000000000000000000000000000000000000..58e4069adf41286ec8e5bc20e4442063ea551765 --- /dev/null +++ b/simpleexample2/fxui/src/main/java/simpleex/ui/LocalLatLongsDataAccess.java @@ -0,0 +1,83 @@ +package simpleex.ui; + +import java.util.Collection; +import simpleex.core.LatLong; +import simpleex.core.LatLongs; + +/* +@startuml +class FxAppController +class LatLongs +class BorderPane +class "ListView<LatLong>" as ListView +class "fxmapcontrol.MapBase" as MapBase + +BorderPane *--> ListView: "left" +BorderPane *--> MapBase: "center" + +FxAppController --> LatLongs: "latLongs" +FxAppController --> MapBase: "mapView" +FxAppController --> ListView: "locationListView" +@enduml + */ + +/** + * The controller for the app. + * @author hal + * + */ +public class LocalLatLongsDataAccess implements LatLongsDataAccess { + + private LatLongs latLongs; + + /** + * Initializes the data access. + */ + public LocalLatLongsDataAccess() { + this(new LatLongs()); + } + + /** + * Initializes the data access with a specific LatLongs. + * @param latLongs the LatLongs object to use + */ + public LocalLatLongsDataAccess(final LatLongs latLongs) { + this.latLongs = latLongs; + } + + /** + * Gets the LatLongs objects used by the controller. + * @return the controller's LatLongs objects + */ + public LatLongs getLatLongs() { + return latLongs; + } + + /** + * Sets the LatLongs objects used by the controller. + * @param latLongs the LatLongs object to use + */ + public void setLatLongs(final LatLongs latLongs) { + this.latLongs = latLongs; + } + + @Override + public Collection<LatLong> getAllLatLongs() { + return getLatLongs().toList(); + } + + @Override + public LatLong getLatLong(final int num) { + return getLatLongs().getLatLong(num); + } + + @Override + public void setLatLong(final int index, final LatLong latLong) { + getLatLongs().setLatLong(index, latLong); + } + + @Override + public int addLatLong(final LatLong latLong) { + return getLatLongs().addLatLong(latLong); + } +} diff --git a/simpleexample2/fxui/src/main/java/simpleex/ui/RestLatLongsDataAccess.java b/simpleexample2/fxui/src/main/java/simpleex/ui/RestLatLongsDataAccess.java new file mode 100644 index 0000000000000000000000000000000000000000..4a9b9ea436ca101ac5eb263c5ca93cc2ab0a53d9 --- /dev/null +++ b/simpleexample2/fxui/src/main/java/simpleex/ui/RestLatLongsDataAccess.java @@ -0,0 +1,136 @@ +package simpleex.ui; + +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpRequest.BodyPublishers; +import java.net.http.HttpResponse; +import java.util.Collection; +import java.util.Collections; +import simpleex.core.LatLong; +import simpleex.core.LatLongs; + +/* +@startuml +class FxAppController +class LatLongs +class BorderPane +class "ListView<LatLong>" as ListView +class "fxmapcontrol.MapBase" as MapBase + +BorderPane *--> ListView: "left" +BorderPane *--> MapBase: "center" + +FxAppController --> LatLongs: "latLongs" +FxAppController --> MapBase: "mapView" +FxAppController --> ListView: "locationListView" +@enduml + */ + +/** + * Data access object using rest. + * @author hal + * + */ +public class RestLatLongsDataAccess implements LatLongsDataAccess { + + private String baseUrlString; + + private final ObjectMapper objectMapper; + + public RestLatLongsDataAccess(final ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + protected ObjectMapper getObjectMapper() { + return objectMapper; + } + + private URI getRequestUri(final String path) { + try { + return new URI(baseUrlString + path); + } catch (final URISyntaxException e) { + throw new IllegalArgumentException(e); + } + } + + @Override + public Collection<LatLong> getAllLatLongs() { + final HttpRequest request = HttpRequest.newBuilder(getRequestUri("")) + .GET() + .build(); + try { + final HttpResponse<InputStream> response = HttpClient.newBuilder() + .build() + .send(request, HttpResponse.BodyHandlers.ofInputStream()); + return getObjectMapper().readValue(response.body(), LatLongs.class).toList(); + } catch (JsonParseException | JsonMappingException e) { + } catch (IOException | InterruptedException e) { + } + return Collections.emptyList(); + } + + @Override + public LatLong getLatLong(final int num) { + final HttpRequest request = HttpRequest.newBuilder(getRequestUri("/" + num)) + .GET() + .build(); + try { + final HttpResponse<InputStream> response = HttpClient.newBuilder() + .build() + .send(request, HttpResponse.BodyHandlers.ofInputStream()); + return getObjectMapper().readValue(response.body(), LatLong.class); + } catch (JsonParseException | JsonMappingException e) { + } catch (IOException | InterruptedException e) { + } + return null; + } + + @Override + public void setLatLong(final int index, final LatLong latLong) { + try { + final HttpRequest request = HttpRequest.newBuilder(getRequestUri("/" + index)) + .PUT(BodyPublishers.ofString(getObjectMapper().writeValueAsString(latLong))) + .build(); + final HttpResponse<InputStream> response = HttpClient.newBuilder() + .build() + .send(request, HttpResponse.BodyHandlers.ofInputStream()); + final int realIndex = getObjectMapper().readValue(response.body(), Integer.class); + if (realIndex < 0) { + throw new IndexOutOfBoundsException(realIndex); + } + } catch (final JsonProcessingException e) { + throw new RuntimeException(e); + } catch (IOException | InterruptedException e) { + throw new RuntimeException(e); + } + } + + @Override + public int addLatLong(final LatLong latLong) { + try { + final HttpRequest request = HttpRequest.newBuilder(getRequestUri("")) + .POST(BodyPublishers.ofString(getObjectMapper().writeValueAsString(latLong))) + .build(); + final HttpResponse<InputStream> response = HttpClient.newBuilder() + .build() + .send(request, HttpResponse.BodyHandlers.ofInputStream()); + final int realIndex = getObjectMapper().readValue(response.body(), Integer.class); + if (realIndex < 0) { + throw new IndexOutOfBoundsException(realIndex); + } + } catch (final JsonProcessingException e) { + throw new RuntimeException(e); + } catch (IOException | InterruptedException e) { + throw new RuntimeException(e); + } + return 0; + } +} diff --git a/simpleexample2/fxui/src/main/resources/simpleex/ui/FxApp.fxml b/simpleexample2/fxui/src/main/resources/simpleex/ui/FxApp.fxml index 01fbe1d975b5c4b7a779cdd25b8924703f597bda..6fbd74bd9238cf1b3d635e51b4a92f4ab025fbd6 100644 --- a/simpleexample2/fxui/src/main/resources/simpleex/ui/FxApp.fxml +++ b/simpleexample2/fxui/src/main/resources/simpleex/ui/FxApp.fxml @@ -21,18 +21,16 @@ fx:controller="simpleex.ui.FxAppController" prefHeight="750" prefWidth="1000"> <top> - <VBox> - <MenuBar > - <menus> - <Menu text="File"> - <items> - <MenuItem text="Open..." accelerator="Meta+O" onAction="#handleOpenAction"/> - <MenuItem text="Save" accelerator="Meta+S" onAction="#handleSaveAction"/> - </items> - </Menu> - </menus> - </MenuBar> - </VBox> + <MenuBar > + <menus> + <Menu text="File"> + <items> + <MenuItem text="Open..." accelerator="Meta+O" onAction="#handleOpenAction"/> + <MenuItem text="Save" accelerator="Meta+S" onAction="#handleSaveAction"/> + </items> + </Menu> + </menus> + </MenuBar> </top> <left> <VBox fillWidth="true"> diff --git a/simpleexample2/fxui/src/main/resources/simpleex/ui/FxAppUsingRest.fxml b/simpleexample2/fxui/src/main/resources/simpleex/ui/FxAppUsingRest.fxml new file mode 100644 index 0000000000000000000000000000000000000000..cc2c2202158bdd29671a2aeb84eae769710069c9 --- /dev/null +++ b/simpleexample2/fxui/src/main/resources/simpleex/ui/FxAppUsingRest.fxml @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<?import javafx.scene.layout.BorderPane?> +<?import javafx.scene.layout.VBox?> +<?import javafx.scene.control.ListView?> +<?import javafx.scene.control.Slider?> +<?import fxmapcontrol.MapBase?> +<?import javafx.scene.control.Button?> +<?import javafx.scene.layout.HBox?> +<?import javafx.scene.control.TextField?> +<?import javafx.scene.control.Label?> +<?import fxmapcontrol.MapTileLayer?> +<?import fxmapcontrol.TileSource?> +<?import javafx.scene.control.MenuBar?> + +<?import javafx.scene.control.Menu?> +<?import javafx.scene.control.MenuItem?> +<?import javafx.scene.control.SeparatorMenuItem?> + +<BorderPane xmlns:fx="http://javafx.com/fxml" + fx:controller="simpleex.ui.FxAppUsingRestController" + prefHeight="750" prefWidth="1000"> + <left> + <VBox fillWidth="true"> + <ListView fx:id="locationListView"/> + <Button text="Add location" onAction="#handleAddLocation"/> + </VBox> + </left> + <center> + <VBox> + <MapBase fx:id="mapView"> + <MapTileLayer name="OpenStreetMap" minZoomLevel="0" maxZoomLevel="17"> + <tileSource> + <TileSource urlFormat="http://a.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png"/> + <!-- + <TileSource urlFormat="https://{c}.tile.openstreetmap.org/{z}/{x}/{y}.png"/> + <TileSource urlFormat="http://opencache.statkart.no/gatekeeper/gk/gk.open_gmaps?layers=topo2&zoom={z}&x={x}&y={y}"/> + <TileSource urlFormat="http://mt1.google.com/vt/lyrs=m@129&hl=en&s=Galileo&z={z}&x={x}&y={y}"/> + --> + </tileSource> + </MapTileLayer> + </MapBase> + <Slider fx:id="zoomSlider" min="1" max="20" value="9"/> + </VBox> + </center> +</BorderPane> diff --git a/simpleexample2/fxui/src/test/java/simpleex/ui/AbstractFxAppTest.java b/simpleexample2/fxui/src/test/java/simpleex/ui/AbstractFxAppTest.java new file mode 100644 index 0000000000000000000000000000000000000000..50d163a2521a4986c39f10d5c87c28bc6a5b1f1e --- /dev/null +++ b/simpleexample2/fxui/src/test/java/simpleex/ui/AbstractFxAppTest.java @@ -0,0 +1,102 @@ +package simpleex.ui; + +import static org.mockito.Mockito.verify; +import fxmapcontrol.Location; +import fxmapcontrol.MapBase; +import java.net.URL; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; +import org.testfx.framework.junit.ApplicationTest; +import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.ListView; +import javafx.stage.Stage; +import simpleex.core.LatLong; + +public abstract class AbstractFxAppTest extends ApplicationTest { + + /** + * Setup method for headless tests using monocle. + */ + @BeforeClass + public static void headless() { + if (Boolean.valueOf(System.getProperty("gitlab-ci", "false"))) { + System.setProperty("prism.verbose", "true"); // optional + System.setProperty("java.awt.headless", "true"); + System.setProperty("testfx.robot", "glass"); + System.setProperty("testfx.headless", "true"); + System.setProperty("glass.platform", "Monocle"); + System.setProperty("monocle.platform", "Headless"); + System.setProperty("prism.order", "sw"); + System.setProperty("prism.text", "t2k"); + System.setProperty("testfx.setup.timeout", "2500"); + } + } + + private AbstractFxAppController controller; + + protected abstract URL getFxmlResource(); + + @Override + public void start(final Stage stage) throws Exception { + final FXMLLoader loader = new FXMLLoader(getFxmlResource()); + final Parent root = loader.load(); + setUpLatLongsDataAccess(); + this.controller = loader.getController(); + this.controller.setDataAccess(getDataAccess()); + final Scene scene = new Scene(root); + stage.setScene(scene); + stage.show(); + } + + protected abstract LatLongsDataAccess getDataAccess(); + protected abstract void setUpLatLongsDataAccess(); + + @Test + public void testController() { + Assert.assertNotNull(this.controller); + } + + @Test + public void testLocationListView() { + final ListView<?> locationListView = lookup("#locationListView").query(); + // list contains equals elements in same order + Assert.assertEquals(getDataAccess().getAllLatLongs(), locationListView.getItems()); + // first list element is auto-selected + Assert.assertEquals(0, locationListView.getSelectionModel().getSelectedIndex()); + } + + @Test + public void testMapView() { + final MapBase mapView = lookup("#mapView").query(); + // center of map view is approx. the first LatLong object + final Location center = mapView.getCenter(); + final double epsilon = 0.000001; // round-off error + final LatLong latLong = getDataAccess().getLatLong(0); + Assert.assertEquals(latLong.getLatitude(), center.getLatitude(), epsilon); + Assert.assertEquals(latLong.getLongitude(), center.getLongitude(), epsilon); + } + + @Test + public void testAddLocation() { + // needs map center + final Location center = ((MapBase) lookup("#mapView").query()).getCenter(); + // add behavior for add + final LatLong latLong = new LatLong(center.getLatitude(), center.getLongitude()); + // make test less sensitive to exact button text + final Button addLocButton = lookup(node -> node instanceof Button + && ((Button) node).getText().toLowerCase().startsWith("add loc")).query(); + // click button + clickOn(addLocButton); + // clicking doesn't seem to trigger onAction handler, so the verify call will fail + // see https://github.com/TestFX/TestFX/issues/641 + // it works when run from a terminal that has been granted access in the control panel + // System Preferences > Security & Privacy > Accessibility + if (Math.random() < 0.0) { + verify(getDataAccess()).addLatLong(latLong); + } + } +} diff --git a/simpleexample2/fxui/src/test/java/simpleex/ui/FxAppTest.java b/simpleexample2/fxui/src/test/java/simpleex/ui/FxAppTest.java index db2878a0fd38c3dfb4a56812afb879a7b97e70ea..d3e091ef95ed512ea66dc3cf704b861b6316d6d7 100644 --- a/simpleexample2/fxui/src/test/java/simpleex/ui/FxAppTest.java +++ b/simpleexample2/fxui/src/test/java/simpleex/ui/FxAppTest.java @@ -1,111 +1,45 @@ package simpleex.ui; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import fxmapcontrol.Location; -import fxmapcontrol.MapBase; +import java.net.URL; import java.util.ArrayList; import java.util.List; -import org.junit.Assert; -import org.junit.BeforeClass; -import org.junit.Test; -import org.testfx.framework.junit.ApplicationTest; -import javafx.fxml.FXMLLoader; -import javafx.scene.Parent; -import javafx.scene.Scene; -import javafx.scene.control.Button; -import javafx.scene.control.ListView; -import javafx.stage.Stage; import simpleex.core.LatLong; -import simpleex.core.LatLongs; -public class FxAppTest extends ApplicationTest { +public class FxAppTest extends AbstractFxAppTest { - /** - * Setup method for headless tests using monocle. - */ - @BeforeClass - public static void headless() { - if (Boolean.valueOf(System.getProperty("gitlab-ci", "false"))) { - System.setProperty("prism.verbose", "true"); // optional - System.setProperty("java.awt.headless", "true"); - System.setProperty("testfx.robot", "glass"); - System.setProperty("testfx.headless", "true"); - System.setProperty("glass.platform", "Monocle"); - System.setProperty("monocle.platform", "Headless"); - System.setProperty("prism.order", "sw"); - System.setProperty("prism.text", "t2k"); - System.setProperty("testfx.setup.timeout", "2500"); - } + @Override + protected URL getFxmlResource() { + return getClass().getResource("FxApp.fxml"); } - private FxAppController controller; - private LatLongs latLongs; + private List<LatLong> latLongList; + private LatLongsDataAccess dataAccess; @Override - public void start(final Stage stage) throws Exception { - final FXMLLoader loader = new FXMLLoader(getClass().getResource("FxApp.fxml")); - final Parent root = loader.load(); - this.controller = loader.getController(); - setUpLatLongs(); - final Scene scene = new Scene(root); - stage.setScene(scene); - stage.show(); + protected LatLongsDataAccess getDataAccess() { + return dataAccess; } - private List<LatLong> latLongList; - - private void setUpLatLongs() { + @Override + protected void setUpLatLongsDataAccess() { // test data latLongList = new ArrayList<>(List.of(new LatLong(63.1, 11.2), new LatLong(63.2, 11.0))); - // "mocked" (faked) LatLongs object with very specific and limited behavior - latLongs = mock(LatLongs.class); + // "mocked" (faked) LatLongsDataAccess object with very specific and limited behavior + dataAccess = mock(LatLongsDataAccess.class); // get nth LatLong object - when(latLongs.getLatLong(anyInt())) - .then(invocation -> latLongList.get(invocation.getArgument(0))); - // get the number of LatLong objects - when(latLongs.getLatLongCount()).then(invocation -> latLongList.size()); - // iterator for LatLong objects - when(latLongs.iterator()).then(invocation -> latLongList.iterator()); - controller.setLatLongs(latLongs); - } - - @Test - public void testController() { - Assert.assertNotNull(this.controller); - } - - @Test - public void testLocationListView() { - final ListView<?> locationListView = lookup("#locationListView").query(); - // list contains equals elements in same order - Assert.assertEquals(latLongList, locationListView.getItems()); - // first list element is auto-selected - Assert.assertEquals(0, locationListView.getSelectionModel().getSelectedIndex()); - } - - @Test - public void testMapView() { - final MapBase mapView = lookup("#mapView").query(); - // center of map view is approx. the first LatLong object - final Location center = mapView.getCenter(); - final double epsilon = 0.000001; // round-off error - Assert.assertEquals(latLongList.get(0).getLatitude(), center.getLatitude(), epsilon); - Assert.assertEquals(latLongList.get(0).getLongitude(), center.getLongitude(), epsilon); - } - - @Test - public void testAddLocation() { - // needs map center - final Location center = ((MapBase) lookup("#mapView").query()).getCenter(); - // add behavior for add - final LatLong latLong = new LatLong(center.getLatitude(), center.getLongitude()); - when(latLongs.addLatLong(latLong)).thenReturn(2); // add center - - // make test less sensitive to exact button text - final Button addLocButton = lookup(node -> node instanceof Button - && ((Button) node).getText().toLowerCase().startsWith("add loc")).query(); - clickOn(addLocButton); + when(dataAccess.getLatLong(anyInt())) + .then(invocation -> latLongList.get(invocation.getArgument(0))); + // get the LatLong objects + when(dataAccess.getAllLatLongs()).then(invocation -> new ArrayList<>(latLongList)); + // add center + when(dataAccess.addLatLong(any(LatLong.class))).then(invocation -> { + final int size = latLongList.size(); + latLongList.add(invocation.getArgument(0, LatLong.class)); + return size; + }); } } diff --git a/simpleexample2/fxui/src/test/resources/simpleex/ui/FxAppUsingRest.fxml b/simpleexample2/fxui/src/test/resources/simpleex/ui/FxAppUsingRest.fxml new file mode 100644 index 0000000000000000000000000000000000000000..cc2c2202158bdd29671a2aeb84eae769710069c9 --- /dev/null +++ b/simpleexample2/fxui/src/test/resources/simpleex/ui/FxAppUsingRest.fxml @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<?import javafx.scene.layout.BorderPane?> +<?import javafx.scene.layout.VBox?> +<?import javafx.scene.control.ListView?> +<?import javafx.scene.control.Slider?> +<?import fxmapcontrol.MapBase?> +<?import javafx.scene.control.Button?> +<?import javafx.scene.layout.HBox?> +<?import javafx.scene.control.TextField?> +<?import javafx.scene.control.Label?> +<?import fxmapcontrol.MapTileLayer?> +<?import fxmapcontrol.TileSource?> +<?import javafx.scene.control.MenuBar?> + +<?import javafx.scene.control.Menu?> +<?import javafx.scene.control.MenuItem?> +<?import javafx.scene.control.SeparatorMenuItem?> + +<BorderPane xmlns:fx="http://javafx.com/fxml" + fx:controller="simpleex.ui.FxAppUsingRestController" + prefHeight="750" prefWidth="1000"> + <left> + <VBox fillWidth="true"> + <ListView fx:id="locationListView"/> + <Button text="Add location" onAction="#handleAddLocation"/> + </VBox> + </left> + <center> + <VBox> + <MapBase fx:id="mapView"> + <MapTileLayer name="OpenStreetMap" minZoomLevel="0" maxZoomLevel="17"> + <tileSource> + <TileSource urlFormat="http://a.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png"/> + <!-- + <TileSource urlFormat="https://{c}.tile.openstreetmap.org/{z}/{x}/{y}.png"/> + <TileSource urlFormat="http://opencache.statkart.no/gatekeeper/gk/gk.open_gmaps?layers=topo2&zoom={z}&x={x}&y={y}"/> + <TileSource urlFormat="http://mt1.google.com/vt/lyrs=m@129&hl=en&s=Galileo&z={z}&x={x}&y={y}"/> + --> + </tileSource> + </MapTileLayer> + </MapBase> + <Slider fx:id="zoomSlider" min="1" max="20" value="9"/> + </VBox> + </center> +</BorderPane> diff --git a/simpleexample2/restapi/src/main/java/simpleex/restapi/LatLongsService.java b/simpleexample2/restapi/src/main/java/simpleex/restapi/LatLongsService.java index c3a05eeb21d51b8f1d14e25d8d591976261311c6..220a9e8a393a36070650c5286a7803f12ba76481 100644 --- a/simpleexample2/restapi/src/main/java/simpleex/restapi/LatLongsService.java +++ b/simpleexample2/restapi/src/main/java/simpleex/restapi/LatLongsService.java @@ -38,18 +38,18 @@ public class LatLongsService { return latLongs.getLatLong(num); } + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public int addLatLong(final LatLong latLong) { + return this.latLongs.addLatLong(latLong); + } + @POST @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public int addLatLongs(final List<LatLong> latLongs) { - int result = -1; - for (final LatLong latLong : latLongs) { - final int pos = this.latLongs.addLatLong(latLong); - if (result < 0) { - result = pos; - } - } - return result; + return this.latLongs.addLatLongs(latLongs.toArray(new LatLong[latLongs.size()])); } @PUT