Skip to content

Commit

Permalink
Completed the implementation of shared interaction between data plots…
Browse files Browse the repository at this point in the history
… and the map display.

    Any data point selected on a plot will highlight relevant elements on the map (events/stations) and vice-versa.
    E.g. clicking an event on the Shape tab will highlight all the station records for that event at the presently selected frequency band in the visible plot.
    This eventing is context sensitive to the tab actively displaying map data and will attempt to highlight the most relevant infromation given the particular kind of data you are viewing.

Implemented the multiple-waveform display. This feature now allows for clicking any data point on the map or any plot associated with 2-N records and getting a display with a paginated and sorted list of all relevent data.
A number of significant performance and stability improvements have been implemented throughout the tool. Most visibly the speed of loading data and running the path correction algorithm should be considerably faster.
The map now supports abritrary user-defined lists of WMS 1.3.0 compliant map tile servers/layers for display on the map. This enables custom map overlays and terrain/overhead imagery/survey data to be displayed for the region you want to calibrate a model for.
The map now automatically displays context sensitive information based on which tab you are actively looking at. For example: the Site tab will display relevant stations/events based on which calibration spectra you are looking at.
Added an optional layer to the map to display station to event paths in the Path view.
A small improvement to the auto-picker to use a slightly more robust SNR calculation given input waveforms with sufficient amounts of noise.
Replaced the Plus icon style with a Hexagonal one to improve readability of plots.
The numbers displayed on the Path hovers are now the number of events (N) rather than number of waveforms (N*Stations).
  • Loading branch information
justinbarno committed Jan 25, 2019
1 parent adb88cf commit c11f0b0
Show file tree
Hide file tree
Showing 79 changed files with 2,965 additions and 1,202 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
Expand All @@ -39,6 +40,7 @@
import gov.llnl.gnem.apps.coda.calibration.gui.controllers.CodaParamLoadingController;
import gov.llnl.gnem.apps.coda.calibration.gui.controllers.DataController;
import gov.llnl.gnem.apps.coda.calibration.gui.controllers.EnvelopeLoadingController;
import gov.llnl.gnem.apps.coda.calibration.gui.controllers.MapListeningController;
import gov.llnl.gnem.apps.coda.calibration.gui.controllers.PathController;
import gov.llnl.gnem.apps.coda.calibration.gui.controllers.ReferenceEventLoadingController;
import gov.llnl.gnem.apps.coda.calibration.gui.controllers.ShapeController;
Expand Down Expand Up @@ -98,12 +100,12 @@ public class CodaGuiController {
@FXML
private Tab siteTab;

private Runnable dataRefresh = () -> data.update();
private Runnable paramRefresh = () -> param.update();
private Runnable shapeRefresh = () -> shape.update();
private Runnable pathRefresh = () -> path.update();
private Runnable siteRefresh = () -> site.update();
private Runnable activeTabRefresh = dataRefresh;
private Runnable dataRefresh;
private Runnable paramRefresh;
private Runnable shapeRefresh;
private Runnable pathRefresh;
private Runnable siteRefresh;
private Runnable activeTabRefresh;

@FXML
private Button showMapButton;
Expand Down Expand Up @@ -169,6 +171,13 @@ public CodaGuiController(LeafletMapController mapController, EnvelopeLoadingCont
this.bus = bus;
bus.register(this);

dataRefresh = data.getRefreshFunction();
paramRefresh = param.getRefreshFunction();
shapeRefresh = shape.getRefreshFunction();
pathRefresh = path.getRefreshFunction();
siteRefresh = site.getRefreshFunction();
activeTabRefresh = dataRefresh;

sacDirFileChooser.setTitle("Coda STACK File Directory");
sacFileChooser.getExtensionFilters().add(new ExtensionFilter("Coda STACK Files (.sac,.env)", "*.sac", "*.env"));
sacFileChooser.getExtensionFilters().add(allFilesFilter);
Expand Down Expand Up @@ -199,6 +208,7 @@ private void showMapWindow() {
Platform.runLater(() -> {
if (mapController != null) {
mapController.show();
mapController.fitViewToActiveShapes();
}
});
}
Expand Down Expand Up @@ -303,15 +313,7 @@ public void initialize() {
activeMapIcon = makeMapLabel();
showMapIcon = makeMapLabel();

dataTab.setOnSelectionChanged(e -> {
if (dataTab.isSelected()) {
data.refreshView();
dataTab.setGraphic(activeMapIcon);
activeTabRefresh = dataRefresh;
} else {
dataTab.setGraphic(null);
}
});
addMapEnabledTabListeners(dataTab, data, dataRefresh);

paramTab.setOnSelectionChanged(e -> {
if (paramTab.isSelected()) {
Expand All @@ -320,35 +322,9 @@ public void initialize() {
}
});

shapeTab.setOnSelectionChanged(e -> {
if (shapeTab.isSelected()) {
shape.refreshView();
shapeTab.setGraphic(activeMapIcon);
activeTabRefresh = shapeRefresh;
} else {
shapeTab.setGraphic(null);
}
});

pathTab.setOnSelectionChanged(e -> {
if (pathTab.isSelected()) {
path.refreshView();
pathTab.setGraphic(activeMapIcon);
activeTabRefresh = pathRefresh;
} else {
pathTab.setGraphic(null);
}
});

siteTab.setOnSelectionChanged(e -> {
if (siteTab.isSelected()) {
site.refreshView();
siteTab.setGraphic(activeMapIcon);
activeTabRefresh = siteRefresh;
} else {
siteTab.setGraphic(null);
}
});
addMapEnabledTabListeners(shapeTab, shape, shapeRefresh);
addMapEnabledTabListeners(pathTab, path, pathRefresh);
addMapEnabledTabListeners(siteTab, site, siteRefresh);

rootElement.setOnDragOver(event -> {
if (event.getGestureSource() != rootElement && event.getDragboard().hasFiles()) {
Expand All @@ -375,6 +351,18 @@ public void initialize() {
}
}

private void addMapEnabledTabListeners(Tab tab, MapListeningController controller, Runnable runnable) {
tab.setOnSelectionChanged(e -> {
if (tab.isSelected()) {
controller.refreshView();
tab.setGraphic(activeMapIcon);
activeTabRefresh = runnable;
} else {
tab.setGraphic(null);
}
});
}

@FXML
private void refreshTab(ActionEvent e) {
activeTabRefresh.run();
Expand Down Expand Up @@ -415,7 +403,7 @@ private void listener(CalibrationStatusEvent event) {
@Subscribe
private void listener(EnvelopeLoadCompleteEvent evt) {
if (dataTab.isSelected()) {
data.update();
CompletableFuture.runAsync(dataRefresh);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,15 @@
*/
package gov.llnl.gnem.apps.coda.calibration.gui.controllers;

import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.stream.Collectors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -24,18 +31,22 @@

import com.google.common.eventbus.EventBus;

import gov.llnl.gnem.apps.coda.calibration.gui.plotting.MapPlottingUtilities;
import gov.llnl.gnem.apps.coda.common.gui.data.client.api.WaveformClient;
import gov.llnl.gnem.apps.coda.common.gui.events.WaveformSelectionEvent;
import gov.llnl.gnem.apps.coda.common.gui.util.CellBindingUtils;
import gov.llnl.gnem.apps.coda.common.gui.util.EventStaFreqStringComparator;
import gov.llnl.gnem.apps.coda.common.gui.util.MaybeNumericStringComparator;
import gov.llnl.gnem.apps.coda.common.gui.util.NumberFormatFactory;
import gov.llnl.gnem.apps.coda.common.mapping.api.GeoMap;
import gov.llnl.gnem.apps.coda.common.mapping.api.Icon.IconTypes;
import gov.llnl.gnem.apps.coda.common.mapping.api.IconFactory;
import gov.llnl.gnem.apps.coda.common.mapping.api.Location;
import gov.llnl.gnem.apps.coda.common.mapping.api.Icon;
import gov.llnl.gnem.apps.coda.common.model.domain.Event;
import gov.llnl.gnem.apps.coda.common.model.domain.Station;
import gov.llnl.gnem.apps.coda.common.model.domain.Stream;
import gov.llnl.gnem.apps.coda.common.model.domain.Waveform;
import javafx.beans.binding.Bindings;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
Expand All @@ -45,11 +56,9 @@
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableColumn.CellDataFeatures;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.CheckBoxTableCell;
import javafx.scene.control.cell.PropertyValueFactory;

@Component
public class DataController {
public class DataController implements MapListeningController, RefreshableController {

private static final Logger log = LoggerFactory.getLogger(DataController.class);

Expand All @@ -59,9 +68,6 @@ public class DataController {
@FXML
private TableView<Waveform> tableView;

@FXML
private TableColumn<Waveform, Boolean> selectionCol;

@FXML
private CheckBox selectAllCheckbox;

Expand All @@ -71,86 +77,153 @@ public class DataController {
@FXML
private TableColumn<Waveform, String> eventCol;

@FXML
private TableColumn<Waveform, String> lowFreqCol;

@FXML
private TableColumn<Waveform, String> highFreqCol;

private ObservableList<Waveform> listData = FXCollections.synchronizedObservableList(FXCollections.observableArrayList());

private GeoMap mapImpl;

private IconFactory iconFactory;
private MapPlottingUtilities iconFactory;

private WaveformClient client;

private EventBus bus;

private NumberFormat dfmt2 = NumberFormatFactory.twoDecimalOneLeadingZero();

private EventStaFreqStringComparator eventStaFreqComparator = new EventStaFreqStringComparator();

private ListChangeListener<? super Waveform> tableChangeListener;

private final BiConsumer<Boolean, String> eventSelectionCallback;
private final BiConsumer<Boolean, String> stationSelectionCallback;

@Autowired
public DataController(WaveformClient client, GeoMap mapImpl, IconFactory iconFactory, EventBus bus) {
public DataController(WaveformClient client, GeoMap mapImpl, MapPlottingUtilities iconFactory, EventBus bus) {
super();
this.client = client;
this.mapImpl = mapImpl;
this.iconFactory = iconFactory;
this.bus = bus;
bus.register(this);
tableChangeListener = buildTableListener();

eventSelectionCallback = (selected, eventId) -> {
selectDataByCriteria(bus, selected, (w) -> w.getEvent() != null && w.getEvent().getEventId().equalsIgnoreCase(eventId));
};

stationSelectionCallback = (selected, stationId) -> {
selectDataByCriteria(bus, selected, (w) -> w.getStream() != null && w.getStream().getStation() != null && w.getStream().getStation().getStationName().equalsIgnoreCase(stationId));
};
}

private void selectDataByCriteria(EventBus bus, Boolean selected, Function<Waveform, Boolean> matchCriteria) {
List<Waveform> selection = new ArrayList<>();
List<Integer> selectionIndices = new ArrayList<>();
tableView.getSelectionModel().clearSelection();
if (selected) {
for (int i = 0; i < listData.size(); i++) {
Waveform w = listData.get(i);
if (matchCriteria.apply(w)) {
selection.add(w);
tableView.getSelectionModel().select(i);
}
}
if (!selection.isEmpty()) {
selection.sort(eventStaFreqComparator);
Long[] ids = selection.stream().sequential().map(w -> w.getId()).collect(Collectors.toList()).toArray(new Long[0]);
bus.post(new WaveformSelectionEvent(ids));
}
} else {
selection.addAll(tableView.getSelectionModel().getSelectedItems());
selectionIndices.addAll(tableView.getSelectionModel().getSelectedIndices());
for (int i = 0; i < selection.size(); i++) {
if (matchCriteria.apply(selection.get(i))) {
tableView.getSelectionModel().clearSelection(selectionIndices.get(i));
}
}
}
}

private ListChangeListener<? super Waveform> buildTableListener() {
return (ListChangeListener<Waveform>) change -> {
List<Waveform> selection = new ArrayList<>();
selection.addAll(tableView.getSelectionModel().getSelectedItems());
selection.sort(eventStaFreqComparator);
Long[] ids = selection.stream().sequential().map(w -> w.getId()).collect(Collectors.toList()).toArray(new Long[0]);
bus.post(new WaveformSelectionEvent(ids));
};
}

@FXML
private void reloadTable(ActionEvent e) {
requestData();
CompletableFuture.runAsync(getRefreshFunction());
}

@FXML
public void initialize() {
tableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);

// TODO: Waveform has no "selected" need to change this to look at the
// selection model by WaveformId.
selectionCol.setCellValueFactory(new PropertyValueFactory<Waveform, Boolean>("checkBoxValue"));
selectionCol.setCellFactory(CheckBoxTableCell.forTableColumn(selectionCol));

eventCol.setCellValueFactory(
x -> Bindings.createStringBinding(() -> Optional.ofNullable(x).map(CellDataFeatures::getValue).map(Waveform::getEvent).map(Event::getEventId).orElseGet(String::new)));

eventCol.comparatorProperty().set(new MaybeNumericStringComparator());

CellBindingUtils.attachTextCellFactories(lowFreqCol, Waveform::getLowFrequency, dfmt2);
CellBindingUtils.attachTextCellFactories(highFreqCol, Waveform::getHighFrequency, dfmt2);

stationCol.setCellValueFactory(
x -> Bindings.createStringBinding(
() -> Optional.ofNullable(x).map(CellDataFeatures::getValue).map(Waveform::getStream).map(Stream::getStation).map(Station::getStationName).orElseGet(String::new)));

tableView.getSelectionModel().getSelectedItems().addListener(tableChangeListener);
//Workaround for https://bugs.openjdk.java.net/browse/JDK-8095943, for now we just clear the selection to avoid dumping a stack trace in the logs and mucking up event bubbling
tableView.setOnSort(event -> {
if (tableView.getSelectionModel().getSelectedIndices().size() > 1) {
tableView.getSelectionModel().clearSelection();
}
});
tableView.setItems(listData);
}

@Override
public void refreshView() {
if (listData.isEmpty()) {
requestData();
CompletableFuture.runAsync(getRefreshFunction());
} else {
mapImpl.clearIcons();
listData.forEach(waveform -> genIconsFromData(waveform));
CompletableFuture.runAsync(() -> {
mapImpl.clearIcons();
listData.forEach(waveform -> mapImpl.addIcons(genIconsFromData(waveform)));
});
}
}

public void update() {
requestData();
@Override
public Runnable getRefreshFunction() {
return () -> requestData();
}

private void requestData() {
listData.clear();
mapImpl.clearIcons();
client.getUniqueEventStationMetadataForStacks().filter(Objects::nonNull).doOnComplete(() -> tableView.sort()).subscribe(waveform -> {
client.getUniqueEventStationMetadataForStacks().filter(Objects::nonNull).doOnComplete(() -> {
tableView.sort();
refreshView();
}).subscribe(waveform -> {
listData.add(waveform);
genIconsFromData(waveform);
}, err -> log.trace(err.getMessage(), err));
}, err -> log.error(err.getMessage(), err));
}

protected void genIconsFromData(Waveform waveform) {
mapImpl.addIcon(
iconFactory.newIcon(
IconTypes.TRIANGLE_UP,
new Location(waveform.getStream().getStation().getLatitude(), waveform.getStream().getStation().getLongitude()),
waveform.getStream().getStation().getStationName()));
mapImpl.addIcon(
iconFactory.newIcon(
waveform.getEvent().getEventId(),
IconTypes.CIRCLE,
new Location(waveform.getEvent().getLatitude(), waveform.getEvent().getLongitude()),
waveform.getEvent().getEventId()));
protected List<Icon> genIconsFromData(Waveform waveform) {
List<Icon> icons = new ArrayList<>();
if (waveform != null && waveform.getStream() != null && waveform.getEvent() != null) {
icons.add(iconFactory.createEventIcon(waveform.getEvent()).setIconSelectionCallback(eventSelectionCallback));
icons.add(iconFactory.createStationIcon(waveform.getStream().getStation()).setIconSelectionCallback(stationSelectionCallback));
}
return icons;
}

}
Loading

0 comments on commit c11f0b0

Please sign in to comment.