/* * Author: Cedric Holzl - Mohamed Khadri * Sciper: 257844 - 261203 */ package ch.epfl.alpano.gui; import static ch.epfl.alpano.Math2.sq; import static ch.epfl.alpano.PanoramaComputer.NATURAL_VARIATION; import static ch.epfl.alpano.summit.GazetteerParser.readSummitsFrom; import static java.awt.Desktop.getDesktop; import static java.lang.Math.abs; import static java.lang.Math.atan; import static java.lang.Math.toDegrees; import java.io.File; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Collections; import java.util.EnumMap; import java.util.List; import java.util.Locale; import java.util.Map; import javax.imageio.ImageIO; import ch.epfl.alpano.Azimuth; import ch.epfl.alpano.GeoPoint; import ch.epfl.alpano.dem.CompositElevationProfile; import ch.epfl.alpano.summit.Summit; import javafx.application.Application; import javafx.application.Platform; import javafx.beans.binding.Bindings; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.collections.FXCollections; import javafx.embed.swing.SwingFXUtils; import javafx.geometry.HPos; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Node; import javafx.scene.Scene; import javafx.scene.canvas.Canvas; import javafx.scene.canvas.GraphicsContext; import javafx.scene.control.Button; import javafx.scene.control.ChoiceBox; import javafx.scene.control.Label; import javafx.scene.control.Menu; import javafx.scene.control.MenuBar; import javafx.scene.control.MenuItem; import javafx.scene.control.RadioMenuItem; import javafx.scene.control.ScrollPane; import javafx.scene.control.TextArea; import javafx.scene.control.TextField; import javafx.scene.control.TextFormatter; import javafx.scene.control.ToggleGroup; import javafx.scene.image.ImageView; import javafx.scene.input.MouseEvent; import javafx.scene.layout.BorderPane; import javafx.scene.layout.ColumnConstraints; import javafx.scene.layout.GridPane; import javafx.scene.layout.Pane; import javafx.scene.layout.StackPane; import javafx.scene.text.Text; import javafx.stage.Stage; import javafx.util.StringConverter; /** * Main class used for GUI Controls the program through events */ public class Alpano extends Application { private static final String ALPANO_WINDOW_TITLE = "Alpano"; private static final PanoramaUserParameters DEFAULT_PARAMS = PredefinedPanoramas.JURA_ALPS.get(); private static final String SUMMIT_FILENAME = "alps.txt"; private static final List SUMMIT_LIST; private static final PanoramaComputerBean pcbean; private static final PanoramaParametersBean ppbean; private static final MapViewBean mvbean; private static final CursorInfo cursor = new CursorInfo(); static final String REPAINT_NOTICE_TEXT = "CLICK HERE TO REPAINT PANORAMA"; static final String UPDATE_NOTICE_TEXT = "CLICK HERE TO UPDATE PANORAMA"; static { List slist; try { slist = readSummitsFrom(new File(SUMMIT_FILENAME)); } catch (IOException e) { slist = Collections.emptyList(); // No summits found in file, empty list } SUMMIT_LIST = slist; pcbean = new PanoramaComputerBean(SUMMIT_LIST); ppbean = new PanoramaParametersBean(DEFAULT_PARAMS); mvbean = new MapViewBean(pcbean); } /** * Main Fuction ran at the start of the program * @param args : program arguments */ public static void main(String[] args) { Application.launch(args); } @Override public void start(Stage primaryStage) throws Exception { BorderPane root = new BorderPane(); root.setCenter(makePanoPane()); root.setBottom(makeParamsGrid()); root.setTop(makeMenu()); Scene scene = new Scene(root, 1100, 700); primaryStage.setTitle(ALPANO_WINDOW_TITLE); primaryStage.setScene(scene); primaryStage.show(); primaryStage.setOnHidden(e -> { try { Thread.sleep(1000); } catch (InterruptedException e1) { } finally { Platform.exit(); } }); } /** * Builds a stack pane containing the panogroup and all the notices * @return a stack pane for the panorama group and notices */ private Node makePanoPane() { ScrollPane panoScrollPane = new ScrollPane(makePanoGroup()); return new StackPane(panoScrollPane, makePainterNotice(), makeDownloadNotice(), makeUpdateNotice()); } /** * Builds a grid of nodes and binds the input fields to the computable * variables in PanoramaParameterBean * * @return node grid displaying the parameters */ private Node makeParamsGrid() { GridPane grid = new GridPane(); Map labels = new EnumMap( UserParameter.class); Map fields = new EnumMap( UserParameter.class); int nbParams = 0; for (UserParameter parameter : UserParameter.values()) { Label label = new Label(parameter.label()); labels.put(parameter, label); GridPane.setHalignment(label, HPos.RIGHT); fields.put(parameter, makeField(parameter)); nbParams++; if (nbParams % 3 == 0) { int nbRow = nbParams / 3 - 1; grid.addRow(nbRow, labels.get(UserParameter.values()[3 * nbRow]), fields.get(UserParameter.values()[3 * nbRow]), labels.get(UserParameter.values()[3 * nbRow + 1]), fields.get(UserParameter.values()[3 * nbRow + 1]), labels.get(UserParameter.values()[3 * nbRow + 2]), fields.get(UserParameter.values()[3 * nbRow + 2])); } } grid.addColumn(6, makeCursorInfo()); ColumnConstraints colL = new ColumnConstraints(); colL.setPrefWidth(150); ColumnConstraints colT = new ColumnConstraints(); colT.setPrefWidth(80); ColumnConstraints colA = new ColumnConstraints(); colA.setPrefWidth(300); grid.getColumnConstraints().setAll(colL, colT, colL, colT, colL, colT, colA); return grid; } /** * Makes a field for the params grid * @param parameter : parameter of the field * @return the field */ private static Node makeField(UserParameter parameter) { if (!parameter.equals(UserParameter.SUPER_SAMPLING_EXPONENT)) { return makeTextField(parameter); } else { return makeSuperSamplingField(parameter); } } /** * Makes a text field for the param * @param parameter : parameter to use * @return a text field */ private static Node makeTextField(UserParameter parameter) { TextField field = new TextField(); StringConverter stringConverter = new FixedPointStringConverter( parameter.scale()); TextFormatter formatter = new TextFormatter<>(stringConverter); field.setTextFormatter(formatter); field.setAlignment(Pos.CENTER_RIGHT); formatter.valueProperty() .bindBidirectional(ppbean.getProperty(parameter)); return field; } /** * Makes a Choice box for the supersampling parameter * @param parameter : parameter * @return a choice box field for the supersampling parameter */ private static Node makeSuperSamplingField(UserParameter parameter) { ChoiceBox field = new ChoiceBox( FXCollections.observableArrayList(0, 1, 2)); field.setConverter(new LabeledListStringConverter("none", "2x", "4x")); field.valueProperty().bindBidirectional(ppbean.getProperty(parameter)); return field; } /** * Makes a cursor info text area * @return a text area holding the cursor info */ private static Node makeCursorInfo() { TextArea cursorInfo = new TextArea(); cursorInfo.setWrapText(true); cursorInfo.textProperty().bind(cursor.textProperty()); cursorInfo.setMinWidth(100); cursorInfo.setPrefRowCount(2); cursorInfo.setEditable(false); GridPane.setMargin(cursorInfo, new Insets(0, 0, 0, 20)); GridPane.setRowSpan(cursorInfo, 3); return cursorInfo; } /** * Makes a pane holding the update notice * @return a pane when clicked updates the pcbean */ private static Node makeUpdateNotice() { Label updateText = new Label(UPDATE_NOTICE_TEXT); StackPane updateNotice = new StackPane(updateText); updateNotice.setOpacity(0.9); updateNotice.setStyle("-fx-background-color: #FFFFFF;"); updateNotice.visibleProperty().bind(ppbean.ParametersProperty() .isNotEqualTo(pcbean.parametersProperty())); updateNotice.setOnMouseClicked((MouseEvent event) -> { pcbean.setPainter(ppbean.painterProperty().get()); pcbean.setParameters(ppbean.ParametersProperty().get()); }); return updateNotice; } /** * Makes a pane holding the repaint notice * @return a pane when clicked repaints the panorama */ private static Node makePainterNotice() { Label updateText = new Label(REPAINT_NOTICE_TEXT); StackPane updateNotice = new StackPane(updateText); updateNotice.setOpacity(0.9); updateNotice.setStyle("-fx-background-color: #FFFFFF;"); updateNotice.visibleProperty() .bind(ppbean.painterProperty() .isNotEqualTo(pcbean.painterProperty()) .and(ppbean.ParametersProperty() .isEqualTo(pcbean.parametersProperty()))); updateNotice.setOnMouseClicked((MouseEvent event) -> { pcbean.setPainter(ppbean.painterProperty().get()); }); return updateNotice; } /** * Makes a pane holding the download notice * @return a pane dispkaying the MapzenManager download info */ private static Node makeDownloadNotice() { Label dlText = new Label(); StackPane dlNotice = new StackPane(dlText); dlNotice.setOpacity(0.9); dlNotice.setStyle("-fx-background-color: #FFFFFF;"); dlNotice.visibleProperty().bind(pcbean.downloadProperty()); dlText.textProperty().bind(pcbean.downloadTextProperty()); dlText.setAlignment(Pos.CENTER); return dlNotice; } /** * Makes a ImageView holding the image of the panorama * @return an ImageView with bound to the panorama image */ private static Node makePanoView() { ImageView panoView = new ImageView(); panoView.imageProperty().bind(pcbean.imageProperty()); panoView.fitWidthProperty().bind(ppbean.WidthProperty()); panoView.setPreserveRatio(true); panoView.setSmooth(true); return panoView; } /** * Makes a pane holding the labels of the panorama * @return a pane holding the labels */ private static Node makeLabelsPane() { Pane labelsPane = new Pane(); labelsPane.prefWidthProperty().bind(ppbean.WidthProperty()); labelsPane.prefHeightProperty().bind(ppbean.HeightProperty()); Bindings.bindContent(labelsPane.getChildren(), pcbean.getLabels()); return labelsPane; } /** * Makes an ImageView holding the map image * @return ImageView holding the map image */ private static ImageView makeMapView() { ImageView panoView = new ImageView(); panoView.imageProperty().bind(mvbean.mapView()); panoView.setFitWidth(720); panoView.setPreserveRatio(true); panoView.setSmooth(true); return panoView; } /** * Builds the trekking stage * @param ps : stage * @return the trekking stage */ private static Stage makeTrekkingStage(Stage ps) { BorderPane p = new BorderPane(); ///////////////////////////////// makeMapPan ///////////////////////////////// makeMapGroup ImageView im = makeMapView(); Pane lines = new Pane(); lines.setPrefWidth(720); lines.setPrefHeight(720); StackPane panoGroup = new StackPane(im, lines); Bindings.bindContent(lines.getChildren(), mvbean.tl().get()); panoGroup.setOnMouseClicked((MouseEvent e) -> { switch(e.getClickCount()){ case 1: mvbean.addTs(e.getX(),e.getY()); break; case 2: mvbean.zoom(e.getButton()); break; } }); ///////////////////////////////// Pane panoScrollPane = new Pane(panoGroup); StackPane mapPane = new StackPane(panoScrollPane); //////////////////////////////// Button compute = new Button("Compute"); compute.setOnAction(e -> makeElevationProfileStage().show()); Button clear = new Button("clear"); p.setCenter(mapPane); Button save = new Button("Save"); GridPane grid = new GridPane(); grid.add(compute, 0, 0); grid.add(clear, (int) compute.getScaleX(), 0); grid.add(save, 50, 0); Button path = new Button("path"); path.setOnAction(e -> mvbean.buildEndToEndPath()); grid.add(path, 100, 0); p.setBottom(grid); Stage s = new Stage(); Scene scene = new Scene(p); s.setScene(scene); clear.setOnAction(e -> mvbean.clearTs()); return s; } /** * Builds a view of the composite elevation profile * @return a ImageView holding the elevation profile */ private static Node makeElevationView() { ImageView elevationView = new ImageView(); ArrayList pts = new ArrayList(); pts.addAll(mvbean.cts().get()); pts.addAll(mvbean.ts().get()); elevationView.imageProperty() .set(ElevationProfileRenderer.renderElevation( new CompositElevationProfile(pcbean.cDem().get(),pts))); // MapViewRenderer.trekkingProfile(pcbean.cDem()))); // elevationView.setFitWidth(700); elevationView.setPreserveRatio(true); elevationView.setSmooth(true); return elevationView; } /** * Builds a label holding the elevation profile max height data * @return a label with the max height */ private static Node makeElevationProfileData() { Label t = new Label(); Long d = Math.round(ElevationProfileRenderer.currentMaxHeight.get()); t.setText(d.toString() + " m"); return t; } /** * Builds a pane holding the bottom information for the elevation profile * @return a pane for the bottom of the elevation profile */ private static Node makeElevationBottomPane() { Text t0 = new Text(-100, 0, "0"); int d = (int)Math.round( ElevationProfileRenderer.currentElevationProfileLength.get()); GridPane pp = new GridPane(); StackPane p = new StackPane(); p.setPrefWidth(ElevationProfileRenderer.WIDTH); // p.setPrefHeight(ElevationProfileRenderer.HEIGHT); Text t1 = new Text(p.widthProperty().get() - 20, 0, new FixedPointStringConverter(3).toString(d)+ " km"); pp.add(t0, 0, 0); pp.add(t1, 1, 0); ColumnConstraints colN = new ColumnConstraints(); colN.setMinWidth(700); ColumnConstraints colL = new ColumnConstraints(); colL.setPrefWidth(700); pp.getColumnConstraints().setAll(colN, colL); p.getChildren().add(pp); return p; } /** * Builds a pane holding the elevation profile group * @return a stack pane with the elevation group */ private static Node makeElevationPane() { Pane scrollPane = new Pane(makeElevationGroup()); return new StackPane(scrollPane); }; /** * Builds a window holding the elevation profile stuff * @return a stage for the elevation profile */ private static Stage makeElevationProfileStage() { BorderPane p = new BorderPane(); p.setCenter(makeElevationPane()); p.setLeft(makeElevationProfileData()); p.setBottom(makeElevationBottomPane()); Stage s = new Stage(); Scene scene = new Scene(p); s.setScene(scene); return s; } /** * Builds a pane holding the elevation profile group * @return a stack pane for the elevation profile */ private static Node makeElevationGroup() { StackPane panoGroup = new StackPane(makeElevationView()); return panoGroup; } /** * Builds a pane holding the panorama group * @return a stack pane for the panorama group */ private static Node makePanoGroup() { StackPane panoGroup = new StackPane(makePanoView(), makeLabelsPane()); panoGroup.setOnMouseMoved((MouseEvent e) -> { int x = (int) e.getX(); int y = (int) e.getY(); cursor.update(x, y); }); panoGroup.setOnMouseClicked((MouseEvent e) -> { String qy = String.format((Locale) null, "mlat=%.4f&mlon=%.4f", toDegrees(cursor.lat()), toDegrees(cursor.lon()));// ; String fg = String.format((Locale) null, "map=15/%.4f/%.4f", toDegrees(cursor.lat()), toDegrees(cursor.lon())); URI osmURI; try { osmURI = new URI("http", "www.openstreetmap.org", "/", qy, fg); getDesktop().browse(osmURI); } catch (IOException e1) { // Do nothing } catch (URISyntaxException e1) { // Do Nothing } }); return panoGroup; } /** * Menu Button used for the trecking profile * @return a button opening the mapview window */ private static Menu trekkingProfile() { Label settingsL = new Label("Trekking Profile"); settingsL.setOnMouseClicked(e -> { makeTrekkingStage(null).show(); }); Menu settings = new Menu(); settings.setGraphic(settingsL); settings.setDisable(true); pcbean.cDem().addListener(e -> settings.setDisable(false)); return settings; } /** * Builds the menu top bar * @return the menu bar */ private static MenuBar makeMenu() { MenuBar menu = new MenuBar(); menu.getMenus().addAll(saveMenu(), predefinedPanoramaMenu(), painterMenu(), trekkingProfile(), quitMenu()); return menu; } /** * Button that saves the current panorama without labels * @return a button that saves the panorama */ private static Menu saveMenu() { Label saveL = new Label("Save"); saveL.setOnMouseClicked(e -> { if (pcbean.getImage() != null) { String name = Integer .toString(pcbean.getParameters().hashCode()) + ".png"; try { ImageIO.write( SwingFXUtils.fromFXImage(pcbean.getImage(), null), "png", new File(name)); } catch (IOException e1) { e1.printStackTrace(); } } }); Menu save = new Menu(); save.setGraphic(saveL); return save; } /** * Menu holding all the predefined Panoramas * @return a menu with all predefined panoramas */ private static Menu predefinedPanoramaMenu() { Menu ppMenu = new Menu("Predefined Panorama"); for (PredefinedPanoramas pp : PredefinedPanoramas.values()) { MenuItem ppItem = new MenuItem(pp.toString()); ppItem.setOnAction(e -> { ppbean.updateProperties(pp.get()); }); ppMenu.getItems().add(ppItem); } return ppMenu; } /** * Menu holding all custom painters * @return a menu with all custom painters */ private static Menu painterMenu() { Menu cpMenu = new Menu("Painter"); Menu gradItem = new Menu("Gradient Painter"); cpMenu.getItems().add(gradItem); ToggleGroup toggleGroup = new ToggleGroup(); for (CustomPainters cp : CustomPainters.values()) { RadioMenuItem cpItem = new RadioMenuItem(cp.displayText()); cpItem.setToggleGroup(toggleGroup); cpItem.setOnAction(e -> { ppbean.setPainter(cp); }); if (pcbean.painterProperty().equals(cp)) cpItem.setSelected(true); if (cp.isGradient()) { Canvas cv = new Canvas(20, 20); GraphicsContext gc = cv.getGraphicsContext2D(); gc.setFill(cp.gradient().get()); gc.fillRect(0, 0, 20, 20); cv.prefHeight(20); cv.setRotate(180); cpItem.setGraphic(cv); gradItem.getItems().add(cpItem); } else { cpMenu.getItems().add(cpItem); } } return cpMenu; } /** * Quit button that closes the program * @return a quit button */ private static Menu quitMenu() { Label quitL = new Label("Quit"); quitL.setOnMouseClicked(e -> Platform.exit()); Menu quit = new Menu(); quit.setGraphic(quitL); return quit; } /** * Class used to manage the cursor on the panorama pane */ private static final class CursorInfo { private ObjectProperty textProperty = new SimpleObjectProperty(); private double lon, lat, dist = Double.POSITIVE_INFINITY; /** * Updates the x,y position of the cursor * @param x * @param y */ public void update(int x, int y) { int ss = pcbean.getParameters().superSamplingExponent(); x *= Math.pow(2, ss); y *= Math.pow(2, ss); if (isValid(x, y)) { lat = pcbean.getPanorama().latitudeAt(x, y); lon = pcbean.getPanorama().longitudeAt(x, y); dist = pcbean.getPanorama().distanceAt(x, y); double alt = pcbean.getPanorama().elevationAt(x, y); GeoPoint point = new GeoPoint(lon, lat); double azi = pcbean.getParameters().panoramaDisplayParameters() .observerPosition().azimuthTo(point); double slope = (alt - pcbean.getParameters().panoramaDisplayParameters() .observerElevation() - (sq(dist) * NATURAL_VARIATION)) / dist; double ele = toDegrees(atan(slope)); String slat = lat < 0 ? "S" : "N"; String slon = lon < 0 ? "W" : "E"; textProperty.set(String.format((Locale) null, "Position : %.4f°%s %.4f°%s \nDistance : %.1f km \nAltitude : %d m \nAzimut : %.1f° (%s) Elévation : %.1f°", abs(toDegrees(lat)), slat, abs(toDegrees(lon)), slon, (dist / 1_000.0), (int) alt, toDegrees(azi), Azimuth.toOctantString(azi, "N", "S", "E", "W"), ele)); } } /** * * @return the textProperty object to display the cursor information */ public ObjectProperty textProperty() { return textProperty; } /** * Returns the longitude at the cursors position * * @return longitude at cursors last valid position */ public double lon() { return lon; } /** * Returns the latitude at the cursors position * * @return latitude at cursors last valid position */ public double lat() { return lat; } /** * Returns if the x,y index is valid (not too far away) * * @param x * : x index * @param y * : y index * @return if the position at x,y is valid */ public boolean isValid(int x, int y) { return pcbean.getPanorama() != null && pcbean.getPanorama().distanceAt(x, y, (float) Double.POSITIVE_INFINITY) < Double.POSITIVE_INFINITY; } } }