diff --git a/corefx/src/main/java/com/core/Controller.java b/corefx/src/main/java/com/core/Controller.java index 6fb1155a..63a4d8dc 100644 --- a/corefx/src/main/java/com/core/Controller.java +++ b/corefx/src/main/java/com/core/Controller.java @@ -77,6 +77,8 @@ public class Controller implements Initializable { private MobilityDialog mobilityDialog = new MobilityDialog(this); private ChartDialog chartDialog = new ChartDialog(this); private NodeTypesDialog nodeTypesDialog = new NodeTypesDialog(this); + private BackgroundDialog backgroundDialog = new BackgroundDialog(this); + private LocationDialog locationDialog = new LocationDialog(this); public Controller() { // load configuration @@ -140,6 +142,8 @@ public class Controller implements Initializable { configDialog.setOwner(window); mobilityDialog.setOwner(window); nodeTypesDialog.setOwner(window); + backgroundDialog.setOwner(window); + locationDialog.setOwner(window); } @FXML @@ -147,6 +151,16 @@ public class Controller implements Initializable { nodeTypesDialog.showDialog(); } + @FXML + private void onOptionsMenuBackground(ActionEvent event) { + backgroundDialog.showDialog(); + } + + @FXML + private void onOptionsMenuLocation(ActionEvent event) { + locationDialog.showDialog(); + } + @FXML private void onHelpMenuWebsite(ActionEvent event) { application.getHostServices().showDocument("https://github.com/coreemu/core"); diff --git a/corefx/src/main/java/com/core/client/ICoreClient.java b/corefx/src/main/java/com/core/client/ICoreClient.java index 4bfb0321..94cdc907 100644 --- a/corefx/src/main/java/com/core/client/ICoreClient.java +++ b/corefx/src/main/java/com/core/client/ICoreClient.java @@ -79,4 +79,8 @@ public interface ICoreClient { MobilityConfig getMobilityConfig(CoreNode node) throws IOException; boolean mobilityAction(CoreNode node, String action) throws IOException; + + LocationConfig getLocationConfig() throws IOException; + + boolean setLocationConfig(LocationConfig config) throws IOException; } diff --git a/corefx/src/main/java/com/core/client/rest/CoreRestClient.java b/corefx/src/main/java/com/core/client/rest/CoreRestClient.java index 898532ca..fe929e1e 100644 --- a/corefx/src/main/java/com/core/client/rest/CoreRestClient.java +++ b/corefx/src/main/java/com/core/client/rest/CoreRestClient.java @@ -309,6 +309,18 @@ public class CoreRestClient implements ICoreClient { return WebUtils.putJson(url, config); } + @Override + public LocationConfig getLocationConfig() throws IOException { + String url = getUrl(String.format("sessions/%s/location", sessionId)); + return WebUtils.getJson(url, LocationConfig.class); + } + + @Override + public boolean setLocationConfig(LocationConfig config) throws IOException { + String url = getUrl(String.format("sessions/%s/location", sessionId)); + return WebUtils.putJson(url, config); + } + @Override public boolean createNode(CoreNode node) throws IOException { String url = getUrl(String.format("sessions/%s/nodes", sessionId)); diff --git a/corefx/src/main/java/com/core/data/Location.java b/corefx/src/main/java/com/core/data/Location.java new file mode 100644 index 00000000..f5c399c4 --- /dev/null +++ b/corefx/src/main/java/com/core/data/Location.java @@ -0,0 +1,10 @@ +package com.core.data; + +import lombok.Data; + +@Data +public class Location { + private Double latitude = 0.0; + private Double longitude = 0.0; + private Double altitude = 0.0; +} diff --git a/corefx/src/main/java/com/core/data/LocationConfig.java b/corefx/src/main/java/com/core/data/LocationConfig.java new file mode 100644 index 00000000..10ba11ac --- /dev/null +++ b/corefx/src/main/java/com/core/data/LocationConfig.java @@ -0,0 +1,10 @@ +package com.core.data; + +import lombok.Data; + +@Data +public class LocationConfig { + private Position position = new Position(); + private Location location = new Location(); + private Double scale; +} diff --git a/corefx/src/main/java/com/core/graph/BackgroundPaintable.java b/corefx/src/main/java/com/core/graph/BackgroundPaintable.java new file mode 100644 index 00000000..b0cfe6a3 --- /dev/null +++ b/corefx/src/main/java/com/core/graph/BackgroundPaintable.java @@ -0,0 +1,51 @@ +package com.core.graph; + +import edu.uci.ics.jung.visualization.Layer; +import edu.uci.ics.jung.visualization.VisualizationViewer; + +import javax.imageio.ImageIO; +import javax.swing.*; +import java.awt.*; +import java.awt.geom.AffineTransform; +import java.io.IOException; +import java.nio.file.Paths; + +public class BackgroundPaintable implements VisualizationViewer.Paintable { + private final ImageIcon imageIcon; + private final VisualizationViewer vv; + private final String imagePath; + + public BackgroundPaintable(String imagePath, VisualizationViewer vv) throws IOException { + this.imagePath = imagePath; + Image image = ImageIO.read(Paths.get(imagePath).toFile()); + imageIcon = new ImageIcon(image); + this.vv = vv; + } + + public String getImage() { + return imagePath; + } + + @Override + public void paint(Graphics g) { + Graphics2D g2d = (Graphics2D) g; + AffineTransform oldXform = g2d.getTransform(); + AffineTransform lat = + vv.getRenderContext().getMultiLayerTransformer().getTransformer(Layer.LAYOUT).getTransform(); + AffineTransform vat = + vv.getRenderContext().getMultiLayerTransformer().getTransformer(Layer.VIEW).getTransform(); + AffineTransform at = new AffineTransform(); + at.concatenate(g2d.getTransform()); + at.concatenate(vat); + at.concatenate(lat); + g2d.setTransform(at); + g.drawImage(imageIcon.getImage(), 0, 0, + imageIcon.getIconWidth(), imageIcon.getIconHeight(), vv); + g2d.setTransform(oldXform); + } + + @Override + public boolean useTransform() { + return false; + } +} diff --git a/corefx/src/main/java/com/core/graph/NetworkGraph.java b/corefx/src/main/java/com/core/graph/NetworkGraph.java index ffa8fe59..9741f5bd 100644 --- a/corefx/src/main/java/com/core/graph/NetworkGraph.java +++ b/corefx/src/main/java/com/core/graph/NetworkGraph.java @@ -55,6 +55,7 @@ public class NetworkGraph { private Supplier linkFactory = () -> new CoreLink(linkId++); private CorePopupGraphMousePlugin customPopupPlugin; private CoreAnnotatingGraphMousePlugin customAnnotatingPlugin; + private BackgroundPaintable backgroundPaintable; public NetworkGraph(Controller controller) { this.controller = controller; @@ -164,6 +165,24 @@ public class NetworkGraph { }); } + public void setBackground(String imagePath) { + try { + backgroundPaintable = new BackgroundPaintable<>(imagePath, graphViewer); + graphViewer.addPreRenderPaintable(backgroundPaintable); + graphViewer.repaint(); + } catch (IOException ex) { + logger.error("error setting background", ex); + } + } + + public void removeBackground() { + if (backgroundPaintable != null) { + graphViewer.removePreRenderPaintable(backgroundPaintable); + graphViewer.repaint(); + backgroundPaintable = null; + } + } + public void setMode(ModalGraphMouse.Mode mode) { graphMouse.setMode(mode); } diff --git a/corefx/src/main/java/com/core/ui/BackgroundDialog.java b/corefx/src/main/java/com/core/ui/BackgroundDialog.java new file mode 100644 index 00000000..1cf73590 --- /dev/null +++ b/corefx/src/main/java/com/core/ui/BackgroundDialog.java @@ -0,0 +1,86 @@ +package com.core.ui; + +import com.core.Controller; +import com.core.data.CoreLink; +import com.core.data.CoreNode; +import com.core.graph.BackgroundPaintable; +import com.jfoenix.controls.JFXButton; +import com.jfoenix.controls.JFXTextField; +import javafx.fxml.FXML; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.HBox; +import javafx.stage.FileChooser; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.File; +import java.nio.file.Paths; + +public class BackgroundDialog extends StageDialog { + private static final Logger logger = LogManager.getLogger(); + + @FXML + private ImageView imageView; + + @FXML + private JFXTextField fileTextField; + + @FXML + private JFXButton fileButton; + + private JFXButton saveButton; + private JFXButton clearButton; + + + public BackgroundDialog(Controller controller) { + super(controller, "/fxml/background_dialog.fxml"); + setTitle("Background Configuration"); + saveButton = createButton("Save"); + saveButton.setOnAction(event -> { + controller.getNetworkGraph().setBackground(fileTextField.getText()); + close(); + }); + clearButton = createButton("Clear"); + clearButton.setOnAction(event -> { + controller.getNetworkGraph().removeBackground(); + close(); + }); + addCancelButton(); + + HBox parent = (HBox) imageView.getParent(); + imageView.fitHeightProperty().bind(parent.heightProperty()); + + fileButton.setOnAction(event -> { + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle("Select Background"); + fileChooser.setInitialDirectory(new File(System.getProperty("user.home"))); + fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("PNG", "*.png")); + File file = fileChooser.showOpenDialog(getStage()); + if (file != null) { + String uri = file.toURI().toString(); + imageView.setImage(new Image(uri)); + fileTextField.setText(file.getPath()); + saveButton.setDisable(false); + } + }); + } + + public void showDialog() { + BackgroundPaintable backgroundPaintable = getController().getNetworkGraph() + .getBackgroundPaintable(); + saveButton.setDisable(true); + fileTextField.setText(null); + imageView.setImage(null); + if (backgroundPaintable == null) { + clearButton.setDisable(true); + } else { + String imagePath = backgroundPaintable.getImage(); + fileTextField.setText(imagePath); + imageView.setImage(new Image(Paths.get(imagePath).toUri().toString())); + clearButton.setDisable(false); + } + + show(); + } +} diff --git a/corefx/src/main/java/com/core/ui/LocationDialog.java b/corefx/src/main/java/com/core/ui/LocationDialog.java new file mode 100644 index 00000000..c989ed80 --- /dev/null +++ b/corefx/src/main/java/com/core/ui/LocationDialog.java @@ -0,0 +1,85 @@ +package com.core.ui; + +import com.core.Controller; +import com.core.data.LocationConfig; +import com.jfoenix.controls.JFXButton; +import com.jfoenix.controls.JFXTextField; +import com.jfoenix.validation.DoubleValidator; +import javafx.fxml.FXML; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.IOException; + +public class LocationDialog extends StageDialog { + private static final Logger logger = LogManager.getLogger(); + + @FXML + private JFXTextField scaleTextField; + + @FXML + private JFXTextField xTextField; + + @FXML + private JFXTextField yTextField; + + @FXML + private JFXTextField latTextField; + + @FXML + private JFXTextField lonTextField; + + @FXML + private JFXTextField altTextField; + + private JFXButton saveButton; + + public LocationDialog(Controller controller) { + super(controller, "/fxml/location_dialog.fxml"); + setTitle("Location Configuration"); + saveButton = createButton("Save"); + saveButton.setOnAction(event -> { + boolean result = scaleTextField.validate(); + if (!result) { + return; + } + + LocationConfig config = new LocationConfig(); + config.setScale(getDouble(scaleTextField)); + config.getPosition().setX(getDouble(xTextField)); + config.getPosition().setY(getDouble(yTextField)); + config.getLocation().setLatitude(getDouble(latTextField)); + config.getLocation().setLongitude(getDouble(lonTextField)); + config.getLocation().setAltitude(getDouble(altTextField)); + try { + getCoreClient().setLocationConfig(config); + close(); + } catch (IOException ex) { + Toast.error("error setting location config", ex); + } + }); + addCancelButton(); + + DoubleValidator validator = new DoubleValidator(); + scaleTextField.getValidators().add(validator); + } + + public Double getDouble(JFXTextField textField) { + return Double.parseDouble(textField.getText()); + } + + public void showDialog() { + try { + LocationConfig config = getCoreClient().getLocationConfig(); + scaleTextField.setText(config.getScale().toString()); + xTextField.setText(config.getPosition().getX().toString()); + yTextField.setText(config.getPosition().getY().toString()); + latTextField.setText(config.getLocation().getLatitude().toString()); + lonTextField.setText(config.getLocation().getLongitude().toString()); + altTextField.setText(config.getLocation().getAltitude().toString()); + show(); + } catch (IOException ex) { + Toast.error("error getting location config", ex); + } + } +} diff --git a/corefx/src/main/resources/fxml/background_dialog.fxml b/corefx/src/main/resources/fxml/background_dialog.fxml new file mode 100644 index 00000000..09cfdb25 --- /dev/null +++ b/corefx/src/main/resources/fxml/background_dialog.fxml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + diff --git a/corefx/src/main/resources/fxml/location_dialog.fxml b/corefx/src/main/resources/fxml/location_dialog.fxml new file mode 100644 index 00000000..53fee417 --- /dev/null +++ b/corefx/src/main/resources/fxml/location_dialog.fxml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + diff --git a/corefx/src/main/resources/fxml/main.fxml b/corefx/src/main/resources/fxml/main.fxml index a2acf9b0..1f3b7012 100644 --- a/corefx/src/main/resources/fxml/main.fxml +++ b/corefx/src/main/resources/fxml/main.fxml @@ -32,7 +32,9 @@ - + + + diff --git a/webapp/session_routes.py b/webapp/session_routes.py index ec3bb17f..bd1969bc 100644 --- a/webapp/session_routes.py +++ b/webapp/session_routes.py @@ -101,6 +101,38 @@ def set_session_options(session_id): return jsonify() +@api.route("/sessions//location", methods=["PUT"]) +def set_session_location(session_id): + session = core_utils.get_session(coreemu, session_id) + data = request.get_json() or {} + position = data["position"] + location = data["location"] + session.location.refxyz = (position["x"], position["y"], position["z"]) + session.location.setrefgeo(location["latitude"], location["longitude"], location["altitude"]) + session.location.refscale = data["scale"] + return jsonify() + + +@api.route("/sessions//location") +def get_session_location(session_id): + session = core_utils.get_session(coreemu, session_id) + x, y, z = session.location.refxyz + lat, lon, alt = session.location.refgeo + return jsonify({ + "position": { + "x": x, + "y": y, + "z": z, + }, + "location": { + "latitude": lat, + "longitude": lon, + "altitude": alt + }, + "scale": session.location.refscale + }) + + @api.route("/sessions/") def get_session(session_id): session = core_utils.get_session(coreemu, session_id)