2022-04-07 18:43:21 +02:00

737 lines
24 KiB
Java

/*
* 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> 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<Summit> 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<UserParameter, Label> labels = new EnumMap<UserParameter, Label>(
UserParameter.class);
Map<UserParameter, Node> fields = new EnumMap<UserParameter, Node>(
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<Integer> stringConverter = new FixedPointStringConverter(
parameter.scale());
TextFormatter<Integer> 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<Integer> field = new ChoiceBox<Integer>(
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<GeoPoint> pts = new ArrayList<GeoPoint>();
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<String> textProperty = new SimpleObjectProperty<String>();
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<String> 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;
}
}
}