diff --git a/README.md b/README.md
index 4d04a723..c7b8d873 100644
--- a/README.md
+++ b/README.md
@@ -42,18 +42,18 @@ We don't presently deploy versioned artifacts into a public repository like the
#### **As a single runnable JAR**
```shell
-java -jar coda-calibration/calibration-standalone/target/calibration-standalone-1.0.11-runnable.jar
+java -jar coda-calibration/calibration-standalone/target/calibration-standalone-1.0.13-runnable.jar
```
#### **GUI alone**
```shell
-java -jar coda-calibration/calibration-gui/target/calibration-gui-1.0.11-runnable.jar
+java -jar coda-calibration/calibration-gui/target/calibration-gui-1.0.13-runnable.jar
```
#### **Calibration REST service alone**
```shell
-java -jar coda-calibration/calibration-service/application/target/application-1.0.11-runnable.jar
+java -jar coda-calibration/calibration-service/application/target/application-1.0.13-runnable.jar
```
#### A note about HTTPS
diff --git a/calibration-gui/pom.xml b/calibration-gui/pom.xml
index 091d4a65..aa67f4c4 100644
--- a/calibration-gui/pom.xml
+++ b/calibration-gui/pom.xml
@@ -5,7 +5,7 @@
gov.llnl.gnem.apps.coda.calibration
coda-calibration
- 1.0.12
+ 1.0.13
calibration-gui
diff --git a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/controllers/AbstractMeasurementController.java b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/controllers/AbstractMeasurementController.java
index 129f0924..5ddd16a4 100644
--- a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/controllers/AbstractMeasurementController.java
+++ b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/controllers/AbstractMeasurementController.java
@@ -142,6 +142,21 @@ public abstract class AbstractMeasurementController implements MapListeningContr
@FXML
protected TableColumn mwCol;
+ @FXML
+ protected TableColumn obsEnergyCol;
+
+ @FXML
+ protected TableColumn totalEnergyCol;
+
+ @FXML
+ protected TableColumn totalEnergyMDACCol;
+
+ @FXML
+ protected TableColumn energyRatioCol;
+
+ @FXML
+ protected TableColumn energyStressCol;
+
@FXML
protected TableColumn stressCol;
@@ -196,6 +211,12 @@ public abstract class AbstractMeasurementController implements MapListeningContr
@FXML
protected TextField eventLoc;
+ @FXML
+ protected TextField obsTotalEnergy;
+
+ @FXML
+ protected TextField obsEnergy;
+
protected List spectralMeasurements = new ArrayList<>();
private final ObservableList evids = FXCollections.observableArrayList();
@@ -334,8 +355,14 @@ public void initialize() {
CellBindingUtils.attachTextCellFactories(valMwCol, MeasuredMwDetails::getValMw, dfmt4);
CellBindingUtils.attachTextCellFactories(valStressCol, MeasuredMwDetails::getValApparentStressInMpa, dfmt4);
-
CellBindingUtils.attachTextCellFactories(measuredMwCol, MeasuredMwDetails::getMw, dfmt4);
+
+ CellBindingUtils.attachTextCellFactories(obsEnergyCol, MeasuredMwDetails::getObsEnergy, dfmt4);
+ CellBindingUtils.attachTextCellFactories(totalEnergyCol, MeasuredMwDetails::getTotalEnergy, dfmt4);
+ CellBindingUtils.attachTextCellFactories(totalEnergyMDACCol, MeasuredMwDetails::getTotalEnergyMDAC, dfmt4);
+ CellBindingUtils.attachTextCellFactories(energyRatioCol, MeasuredMwDetails::getEnergyRatio, dfmt4);
+ CellBindingUtils.attachTextCellFactories(energyStressCol, MeasuredMwDetails::getEnergyStress, dfmt4);
+
CellBindingUtils.attachTextCellFactories(measuredStressCol, MeasuredMwDetails::getApparentStressInMpa, dfmt4);
CellBindingUtils.attachTextCellFactories(measuredCornerFreqCol, MeasuredMwDetails::getCornerFreq, dfmt4);
@@ -401,20 +428,27 @@ private void plotSpectra() {
fittingSpectra.add(validationSpectra);
if (filteredMeasurements != null && !filteredMeasurements.isEmpty() && filteredMeasurements.get(0).getWaveform() != null) {
final Event event = filteredMeasurements.get(0).getWaveform().getEvent();
+ final Spectra fitSpectra = fittingSpectra.get(0);
eventTime.setText(
"Date: "
+ DateTimeFormatter.ISO_INSTANT.format(event.getOriginTime().toInstant())
+ " Julian Day: "
+ TimeT.jdateToTimeT(TimeT.EpochToJdate(event.getOriginTime().toInstant().getEpochSecond())).getJDay());
eventLoc.setText("Lat: " + dfmt4.format(event.getLatitude()) + " Lon: " + dfmt4.format(event.getLongitude()) + " Depth: " + dfmt2.format(event.getDepth()));
+ obsEnergy.setText("Observed Energy: " + dfmt4.format(fitSpectra.getObsEnergy()) + " J MDAC Energy: " + dfmt4.format(fitSpectra.getlogTotalEnergyMDAC()) + " J");
+ obsTotalEnergy.setText("Observed Total Energy: " + dfmt4.format(fitSpectra.getLogTotalEnergy()) + " J @ " + dfmt4.format(fitSpectra.getObsAppStress()) + " MPa");
eventTime.setVisible(true);
eventLoc.setVisible(true);
+ obsEnergy.setVisible(true);
+ obsTotalEnergy.setVisible(true);
} else {
filteredMeasurements = Collections.emptyList();
}
} else {
eventTime.setVisible(false);
eventLoc.setVisible(false);
+ obsEnergy.setVisible(false);
+ obsTotalEnergy.setVisible(false);
filteredMeasurements = spectralMeasurements;
fittingSpectra = null;
}
@@ -911,7 +945,8 @@ protected void handlePlotObjectClicked(final PlotObjectClick poc, final Function
List points = poc.getPlotPoints();
Set waveforms = new HashSet<>();
- //FIXME: This entire scheme is tremendously inefficient and needs a rework at some point.
+ // FIXME: This entire scheme is tremendously inefficient and needs a rework at
+ // some point.
for (SpectraPlotController spc : spectraControllers) {
spc.getSpectralPlot().deselectAllPoints();
}
diff --git a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/controllers/AutoCompleteCombo.java b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/controllers/AutoCompleteCombo.java
new file mode 100644
index 00000000..2ecd86f9
--- /dev/null
+++ b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/controllers/AutoCompleteCombo.java
@@ -0,0 +1,80 @@
+/*
+* Copyright (c) 2021, Lawrence Livermore National Security, LLC. Produced at the Lawrence Livermore National Laboratory
+* CODE-743439.
+* All rights reserved.
+*
+* Licensed under the Apache License, Version 2.0 (the “Licensee”); you may not use this file except in compliance with the License. You may obtain a copy of the License at:
+* http://www.apache.org/licenses/LICENSE-2.0
+* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and limitations under the license.
+*
+* This work was performed under the auspices of the U.S. Department of Energy
+* by Lawrence Livermore National Laboratory under Contract DE-AC52-07NA27344.
+*/
+package gov.llnl.gnem.apps.coda.calibration.gui.controllers;
+
+import org.apache.commons.lang3.StringUtils;
+
+import javafx.collections.ObservableList;
+import javafx.scene.control.ComboBox;
+import javafx.scene.control.TextField;
+import javafx.scene.input.KeyEvent;
+import javafx.scene.input.MouseButton;
+
+public class AutoCompleteCombo extends ComboBox {
+
+ private ObservableList originalItems;
+
+ public AutoCompleteCombo(ObservableList items) {
+ this.originalItems = items;
+ this.setEditable(true);
+ this.getEditor().setOnKeyTyped(e -> handle(e));
+ this.getEditor().setOnMouseClicked(event -> {
+ if (event.getButton().equals(MouseButton.PRIMARY) && (event.getClickCount() == 2)) {
+ return;
+ }
+ this.show();
+ });
+ this.setItems(originalItems);
+ };
+
+ public void setOriginalItems(ObservableList items) {
+ originalItems = items;
+ }
+
+ private void handle(KeyEvent event) {
+ TextField field = this.getEditor();
+ if (field == null) {
+ return;
+ }
+
+ final String text = field.getText();
+ ObservableList filtered = filteredItems(text);
+
+ if (this.getSelectionModel().getSelectedItem() != null) {
+ this.getSelectionModel().clearSelection();
+ field.setText(text.trim());
+ field.end();
+ }
+
+ this.setItems(filtered);
+ this.show();
+ }
+
+ private ObservableList filteredItems(String text) {
+ if (StringUtils.isBlank(text)) {
+ return originalItems;
+ }
+ ObservableList dropDownItems = originalItems;
+ if (!originalItems.isEmpty()) {
+ dropDownItems = originalItems.filtered(data -> {
+ if (data != null && !StringUtils.isBlank(data.toString())) {
+ return data.toString().toLowerCase().contains(text.toLowerCase());
+ }
+ return false;
+ });
+ }
+
+ return dropDownItems;
+ }
+}
\ No newline at end of file
diff --git a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/controllers/DataController.java b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/controllers/DataController.java
index f3eb4fee..5445aa65 100644
--- a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/controllers/DataController.java
+++ b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/controllers/DataController.java
@@ -29,7 +29,6 @@
import java.util.stream.Collectors;
import javax.annotation.PreDestroy;
-
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -73,362 +72,374 @@
@Component
public class DataController implements MapListeningController, RefreshableController {
- private static final Logger log = LoggerFactory.getLogger(DataController.class);
+ private static final Logger log = LoggerFactory.getLogger(DataController.class);
+
+ @FXML
+ private MenuItem importWaveforms;
- @FXML
- private MenuItem importWaveforms;
+ @FXML
+ private ScrollPane scrollPane;
- @FXML
- private ScrollPane scrollPane;
-
- @FXML
- private TableView tableView;
-
- @FXML
- private CheckBox selectAllCheckbox;
-
- @FXML
- private TableColumn usedCol;
-
- @FXML
- private TableColumn stationCol;
-
- @FXML
- private TableColumn networkCol;
-
- @FXML
- private TableColumn eventCol;
-
- @FXML
- private TableColumn lowFreqCol;
-
- @FXML
- private TableColumn highFreqCol;
-
- @FXML
- private TableColumn depthCol;
-
- private ObservableList listData = FXCollections.synchronizedObservableList(FXCollections.observableArrayList());
-
- private GeoMap mapImpl;
-
- 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 eventSelectionCallback;
- private final BiConsumer stationSelectionCallback;
- private ScheduledExecutorService scheduled = Executors.newSingleThreadScheduledExecutor(r -> {
- Thread thread = new Thread(r);
- thread.setName("Data-Scheduled");
- thread.setDaemon(true);
- return thread;
- });
-
- private List dataUpdateList = Collections.synchronizedList(new ArrayList<>());
- private List dataDeleteList = Collections.synchronizedList(new ArrayList<>());
- private List updatedData = Collections.synchronizedList(new ArrayList<>());
-
- private boolean isVisible = false;
-
- @Autowired
- 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();
- scheduled.scheduleWithFixedDelay(() -> updateData(), 1000l, 1000l, TimeUnit.MILLISECONDS);
-
- 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 matchCriteria) {
- List selection = new ArrayList<>();
- List selectionIndices = new ArrayList<>();
- tableView.getSelectionModel().clearSelection();
- if (selected) {
- tableView.getSelectionModel().getSelectedItems().removeListener(tableChangeListener);
- synchronized (listData) {
- for (int i = 0; i < listData.size(); i++) {
- Waveform w = listData.get(i);
- if (matchCriteria.apply(w)) {
- selection.add(w);
- tableView.getSelectionModel().select(i);
- }
- }
- }
- tableView.getSelectionModel().getSelectedItems().addListener(tableChangeListener);
- 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) change -> {
- List selection = new ArrayList<>();
- selection.addAll(tableView.getSelectionModel().getSelectedItems());
- selection.sort(eventStaFreqComparator);
- Long[] ids = getIds(selection).toArray(new Long[0]);
- bus.post(new WaveformSelectionEvent(ids));
- };
- }
-
- private List getIds(List selection) {
- return selection.stream().sequential().map(w -> w.getId()).collect(Collectors.toList());
- }
-
- @FXML
- private void reloadTable(ActionEvent e) {
- CompletableFuture.runAsync(getRefreshFunction());
- }
-
- @FXML
- public void initialize() {
- tableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
- 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);
-
- depthCol.setCellValueFactory(x -> Bindings.createStringBinding(() -> Optional.ofNullable(x)
- .map(CellDataFeatures::getValue)
- .map(Waveform::getEvent)
- .map(ev -> dfmt2.format(ev.getDepth()))
- .orElseGet(String::new)));
- depthCol.comparatorProperty().set(new MaybeNumericStringComparator());
-
- usedCol.setCellValueFactory(x -> Bindings.createObjectBinding(() -> Optional.ofNullable(x).map(CellDataFeatures::getValue).map(waveform -> {
- CheckBox box = new CheckBox();
- box.setSelected(waveform.isActive());
- if (!waveform.isActive()) {
- box.setStyle("-fx-background-color: red");
- } else {
- box.setStyle("");
- }
- box.selectedProperty().addListener((obs, o, n) -> {
- if (n != null && !o.equals(n)) {
- client.setWaveformsActiveByIds(Collections.singletonList(waveform.getId()), n).subscribe();
- }
- });
- return box;
- }).orElseGet(CheckBox::new)));
- usedCol.comparatorProperty().set((c1, c2) -> Boolean.compare(c1.isSelected(), c2.isSelected()));
-
- stationCol.setCellValueFactory(x -> Bindings.createStringBinding(() -> Optional.ofNullable(x)
- .map(CellDataFeatures::getValue)
- .map(Waveform::getStream)
- .map(Stream::getStation)
- .map(Station::getStationName)
- .orElseGet(String::new)));
-
- networkCol.setCellValueFactory(x -> Bindings.createStringBinding(() -> Optional.ofNullable(x)
- .map(CellDataFeatures::getValue)
- .map(Waveform::getStream)
- .map(Stream::getStation)
- .map(Station::getNetworkName)
- .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();
- }
- });
-
- ContextMenu menu = new ContextMenu();
- MenuItem include = new MenuItem("Include Selected");
- include.setOnAction(evt -> includeWaveforms());
- menu.getItems().add(include);
- MenuItem exclude = new MenuItem("Exclude Selected");
- exclude.setOnAction(evt -> excludeWaveforms());
- menu.getItems().add(exclude);
-
- tableView.addEventHandler(MouseEvent.MOUSE_CLICKED, new EventHandler() {
- @Override
- public void handle(MouseEvent t) {
- if (MouseButton.SECONDARY == t.getButton()) {
- menu.show(tableView, t.getScreenX(), t.getScreenY());
- } else {
- menu.hide();
- }
- }
- });
-
- tableView.setItems(listData);
- }
-
- @Override
- public void refreshView() {
- CompletableFuture.runAsync(() -> {
- synchronized (listData) {
- if (!listData.isEmpty()) {
- refreshMap();
- }
- }
- Platform.runLater(() -> {
- if (tableView != null) {
- tableView.refresh();
- }
- });
- });
- }
-
- private void refreshMap() {
- if (isVisible) {
- mapImpl.clearIcons();
- synchronized (listData) {
- mapImpl.addIcons(iconFactory.genIconsFromWaveforms(eventSelectionCallback, stationSelectionCallback, listData));
- }
- }
- }
-
- @Override
- public Runnable getRefreshFunction() {
- return () -> requestData();
- }
-
- private void requestData() {
- synchronized (listData) {
- listData.clear();
- }
- mapImpl.clearIcons();
- client.getUniqueEventStationMetadataForStacks().filter(Objects::nonNull).doOnComplete(() -> {
- tableView.sort();
- refreshView();
- }).subscribe(waveform -> {
- synchronized (listData) {
- listData.add(waveform);
- }
- }, err -> log.error(err.getMessage(), err));
- }
-
- private void requestUpdates() {
- List updates = new ArrayList<>();
- List deletes = new ArrayList<>();
- synchronized (dataUpdateList) {
- updates.addAll(dataUpdateList);
- dataUpdateList.clear();
- deletes.addAll(dataDeleteList);
- dataDeleteList.clear();
- }
-
- if (!deletes.isEmpty()) {
- synchronized (listData) {
- deletes.forEach(id -> {
- synchronized (listData) {
- int idx = -1;
- for (int i = 0; i < listData.size(); i++) {
- if (listData.get(i).getId().equals(id)) {
- idx = i;
- break;
- }
- }
- if (idx >= 0) {
- listData.remove(idx);
- }
- }
- });
- }
- }
-
- client.getWaveformMetadataFromIds(updates).filter(Objects::nonNull).subscribe(waveform -> updatedData.add(waveform), err -> log.error(err.getMessage(), err));
- }
-
- private void updateData() {
- List updates = new ArrayList<>();
- synchronized (updatedData) {
- updates.addAll(updatedData);
- updatedData.clear();
- }
- if (!updates.isEmpty()) {
- synchronized (listData) {
- tableView.getSelectionModel().getSelectedItems().removeListener(tableChangeListener);
- updates.forEach(waveform -> {
- int idx = -1;
- for (int i = 0; i < listData.size(); i++) {
- if (listData.get(i).getId().equals(waveform.getId())) {
- idx = i;
- break;
- }
- }
- if (idx >= 0) {
- listData.set(idx, waveform);
- } else {
- listData.add(waveform);
- }
- });
- tableView.getSelectionModel().getSelectedItems().addListener(tableChangeListener);
- }
- refreshMap();
- }
- }
-
- private void excludeWaveforms() {
- client.setWaveformsActiveByIds(getSelectedWaveforms(), false).subscribe();
- }
-
- private void includeWaveforms() {
- client.setWaveformsActiveByIds(getSelectedWaveforms(), true).subscribe();
- }
-
- private List getSelectedWaveforms() {
- return getIds(tableView.getSelectionModel().getSelectedItems());
- }
-
- @Subscribe
- private void listener(WaveformChangeEvent wce) {
- List nonNull = wce.getIds().stream().filter(Objects::nonNull).collect(Collectors.toList());
- synchronized (dataUpdateList) {
- if (wce.isAddOrUpdate()) {
- dataUpdateList.addAll(nonNull);
- } else if (wce.isDelete()) {
- dataDeleteList.addAll(nonNull);
- }
- }
- requestUpdates();
- }
-
- @PreDestroy
- private void cleanup() {
- scheduled.shutdownNow();
- }
-
- @Override
- public void setVisible(boolean visible) {
- isVisible = visible;
- }
+ @FXML
+ private TableView tableView;
+
+ @FXML
+ private CheckBox selectAllCheckbox;
+
+ @FXML
+ private TableColumn usedCol;
+
+ @FXML
+ private TableColumn stationCol;
+
+ @FXML
+ private TableColumn networkCol;
+
+ @FXML
+ private TableColumn eventCol;
+
+ @FXML
+ private TableColumn lowFreqCol;
+
+ @FXML
+ private TableColumn highFreqCol;
+
+ @FXML
+ private TableColumn depthCol;
+
+ private ObservableList listData = FXCollections
+ .synchronizedObservableList(FXCollections.observableArrayList());
+
+ private GeoMap mapImpl;
+
+ 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 eventSelectionCallback;
+ private final BiConsumer stationSelectionCallback;
+ private ScheduledExecutorService scheduled = Executors.newSingleThreadScheduledExecutor(r -> {
+ Thread thread = new Thread(r);
+ thread.setName("Data-Scheduled");
+ thread.setDaemon(true);
+ return thread;
+ });
+
+ private List dataUpdateList = Collections.synchronizedList(new ArrayList<>());
+ private List dataDeleteList = Collections.synchronizedList(new ArrayList<>());
+ private List updatedData = Collections.synchronizedList(new ArrayList<>());
+
+ private boolean isVisible = false;
+
+ @Autowired
+ 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();
+ scheduled.scheduleWithFixedDelay(() -> updateData(), 1000l, 1000l, TimeUnit.MILLISECONDS);
+
+ 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 matchCriteria) {
+ List selection = new ArrayList<>();
+ List selectionIndices = new ArrayList<>();
+ tableView.getSelectionModel().clearSelection();
+ if (selected) {
+ tableView.getSelectionModel().getSelectedItems().removeListener(tableChangeListener);
+ synchronized (listData) {
+ for (int i = 0; i < listData.size(); i++) {
+ Waveform w = listData.get(i);
+ if (matchCriteria.apply(w)) {
+ selection.add(w);
+ tableView.getSelectionModel().select(i);
+ }
+ }
+ }
+ tableView.getSelectionModel().getSelectedItems().addListener(tableChangeListener);
+ 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) change -> {
+ List selection = new ArrayList<>();
+ selection.addAll(tableView.getSelectionModel().getSelectedItems());
+ selection.sort(eventStaFreqComparator);
+ Long[] ids = getIds(selection).toArray(new Long[0]);
+ bus.post(new WaveformSelectionEvent(ids));
+ };
+ }
+
+ private List getIds(List selection) {
+ return selection.stream().sequential().map(w -> w.getId()).collect(Collectors.toList());
+ }
+
+ @FXML
+ private void reloadTable(ActionEvent e) {
+ CompletableFuture.runAsync(getRefreshFunction());
+ }
+
+ @FXML
+ public void initialize() {
+ tableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
+ 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);
+
+ depthCol.setCellValueFactory(
+ x -> Bindings.createStringBinding(() -> Optional.ofNullable(x).map(CellDataFeatures::getValue)
+ .map(Waveform::getEvent).map(ev -> dfmt2.format(ev.getDepth())).orElseGet(String::new)));
+ depthCol.comparatorProperty().set(new MaybeNumericStringComparator());
+
+ usedCol.setCellValueFactory(x -> Bindings
+ .createObjectBinding(() -> Optional.ofNullable(x).map(CellDataFeatures::getValue).map(waveform -> {
+ CheckBox box = new CheckBox();
+ box.setSelected(waveform.isActive());
+ if (!waveform.isActive()) {
+ box.setStyle("-fx-background-color: red");
+ } else {
+ box.setStyle("");
+ }
+ box.selectedProperty().addListener((obs, o, n) -> {
+ if (n != null && !o.equals(n)) {
+ client.setWaveformsActiveByIds(Collections.singletonList(waveform.getId()), n).subscribe();
+ }
+ });
+ return box;
+ }).orElseGet(CheckBox::new)));
+ usedCol.comparatorProperty().set((c1, c2) -> Boolean.compare(c1.isSelected(), c2.isSelected()));
+
+ stationCol.setCellValueFactory(x -> Bindings.createStringBinding(
+ () -> Optional.ofNullable(x).map(CellDataFeatures::getValue).map(Waveform::getStream)
+ .map(Stream::getStation).map(Station::getStationName).orElseGet(String::new)));
+
+ networkCol.setCellValueFactory(x -> Bindings.createStringBinding(
+ () -> Optional.ofNullable(x).map(CellDataFeatures::getValue).map(Waveform::getStream)
+ .map(Stream::getStation).map(Station::getNetworkName).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();
+ }
+ });
+
+ ContextMenu menu = new ContextMenu();
+ MenuItem include = new MenuItem("Include Selected");
+ include.setOnAction(evt -> includeWaveforms());
+ menu.getItems().add(include);
+ MenuItem exclude = new MenuItem("Exclude Selected");
+ exclude.setOnAction(evt -> excludeWaveforms());
+ menu.getItems().add(exclude);
+
+ tableView.addEventHandler(MouseEvent.MOUSE_CLICKED, new EventHandler() {
+ @Override
+ public void handle(MouseEvent t) {
+
+ if (MouseButton.SECONDARY == t.getButton()) {
+ menu.show(tableView, t.getScreenX(), t.getScreenY());
+ } else {
+ menu.hide();
+ }
+ }
+ });
+ tableView.setItems(listData);
+
+ DataFilterController filterController = new DataFilterController<>(tableView);
+
+ filterController.addFilterToColumn(eventCol, (data, item) -> data.getEvent().getEventId().equals(item));
+ filterController.addFilterToColumn(depthCol,
+ (data, item) -> dfmt2.format(data.getEvent().getDepth()).equals(item));
+ filterController.addFilterToColumn(lowFreqCol, (data, item) -> data.getLowFrequency().toString().equals(item));
+ filterController.addFilterToColumn(highFreqCol,
+ (data, item) -> data.getHighFrequency().toString().equals(item));
+ filterController.addFilterToColumn(stationCol,
+ (data, item) -> data.getStream().getStation().getStationName().equals(item));
+ filterController.addFilterToColumn(networkCol,
+ (data, item) -> data.getStream().getStation().getNetworkName().equals(item));
+ }
+
+ @Override
+ public void refreshView() {
+ CompletableFuture.runAsync(() -> {
+ synchronized (listData) {
+ if (!listData.isEmpty()) {
+ refreshMap();
+ }
+ }
+ Platform.runLater(() -> {
+ if (tableView != null) {
+ tableView.refresh();
+ }
+ });
+ });
+ }
+
+ private void refreshMap() {
+ if (isVisible) {
+ mapImpl.clearIcons();
+ synchronized (listData) {
+ mapImpl.addIcons(
+ iconFactory.genIconsFromWaveforms(eventSelectionCallback, stationSelectionCallback, listData));
+ }
+ }
+ }
+
+ @Override
+ public Runnable getRefreshFunction() {
+ return () -> requestData();
+ }
+
+ private void requestData() {
+ synchronized (listData) {
+ listData.clear();
+ }
+ mapImpl.clearIcons();
+ client.getUniqueEventStationMetadataForStacks().filter(Objects::nonNull).doOnComplete(() -> {
+ tableView.sort();
+ refreshView();
+ }).subscribe(waveform -> {
+ synchronized (listData) {
+ listData.add(waveform);
+ }
+ }, err -> log.error(err.getMessage(), err));
+ }
+
+ private void requestUpdates() {
+ List updates = new ArrayList<>();
+ List deletes = new ArrayList<>();
+ synchronized (dataUpdateList) {
+ updates.addAll(dataUpdateList);
+ dataUpdateList.clear();
+ deletes.addAll(dataDeleteList);
+ dataDeleteList.clear();
+ }
+
+ if (!deletes.isEmpty()) {
+ synchronized (listData) {
+ deletes.forEach(id -> {
+ synchronized (listData) {
+ int idx = -1;
+ for (int i = 0; i < listData.size(); i++) {
+ if (listData.get(i).getId().equals(id)) {
+ idx = i;
+ break;
+ }
+ }
+ if (idx >= 0) {
+ listData.remove(idx);
+ }
+ }
+ });
+ }
+ }
+
+ client.getWaveformMetadataFromIds(updates).filter(Objects::nonNull)
+ .subscribe(waveform -> updatedData.add(waveform), err -> log.error(err.getMessage(), err));
+ }
+
+ private void updateData() {
+ List updates = new ArrayList<>();
+ synchronized (updatedData) {
+ updates.addAll(updatedData);
+ updatedData.clear();
+ }
+ if (!updates.isEmpty()) {
+ synchronized (listData) {
+ tableView.getSelectionModel().getSelectedItems().removeListener(tableChangeListener);
+ updates.forEach(waveform -> {
+ int idx = -1;
+ for (int i = 0; i < listData.size(); i++) {
+ if (listData.get(i).getId().equals(waveform.getId())) {
+ idx = i;
+ break;
+ }
+ }
+ if (idx >= 0) {
+ listData.set(idx, waveform);
+ } else {
+ listData.add(waveform);
+ }
+ });
+ tableView.getSelectionModel().getSelectedItems().addListener(tableChangeListener);
+ }
+ refreshMap();
+ }
+ }
+
+ private void excludeWaveforms() {
+ client.setWaveformsActiveByIds(getSelectedWaveforms(), false).subscribe();
+ }
+
+ private void includeWaveforms() {
+ client.setWaveformsActiveByIds(getSelectedWaveforms(), true).subscribe();
+ }
+
+ private List getSelectedWaveforms() {
+ return getIds(tableView.getSelectionModel().getSelectedItems());
+ }
+
+ @Subscribe
+ private void listener(WaveformChangeEvent wce) {
+ List nonNull = wce.getIds().stream().filter(Objects::nonNull).collect(Collectors.toList());
+ synchronized (dataUpdateList) {
+ if (wce.isAddOrUpdate()) {
+ dataUpdateList.addAll(nonNull);
+ } else if (wce.isDelete()) {
+ dataDeleteList.addAll(nonNull);
+ }
+ }
+ requestUpdates();
+ }
+
+ @PreDestroy
+ private void cleanup() {
+ scheduled.shutdownNow();
+ }
+
+ @Override
+ public void setVisible(boolean visible) {
+ isVisible = visible;
+ }
}
diff --git a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/controllers/DataFilterController.java b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/controllers/DataFilterController.java
new file mode 100644
index 00000000..eda9188d
--- /dev/null
+++ b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/controllers/DataFilterController.java
@@ -0,0 +1,153 @@
+/*
+* Copyright (c) 2021, Lawrence Livermore National Security, LLC. Produced at the Lawrence Livermore National Laboratory
+* CODE-743439.
+* All rights reserved.
+*
+* Licensed under the Apache License, Version 2.0 (the “Licensee”); you may not use this file except in compliance with the License. You may obtain a copy of the License at:
+* http://www.apache.org/licenses/LICENSE-2.0
+* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and limitations under the license.
+*
+* This work was performed under the auspices of the U.S. Department of Energy
+* by Lawrence Livermore National Laboratory under Contract DE-AC52-07NA27344.
+*/
+package gov.llnl.gnem.apps.coda.calibration.gui.controllers;
+
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.function.Predicate;
+
+import javafx.collections.FXCollections;
+import javafx.collections.ListChangeListener;
+import javafx.collections.ListChangeListener.Change;
+import javafx.collections.ObservableList;
+import javafx.scene.control.Button;
+import javafx.scene.control.ContentDisplay;
+import javafx.scene.control.Label;
+import javafx.scene.control.TableColumn;
+import javafx.scene.control.TableView;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+
+class DataFilterController {
+ TableView tableView;
+ // The list of unfiltered table items
+ ObservableList items;
+ FilterDialogController filterDialog;
+ PredicateBuilder predicateBuilder;
+ // The filter buttons attached to columns
+ private List