diff --git a/README.md b/README.md index a15429fb..378d96f4 100644 --- a/README.md +++ b/README.md @@ -51,19 +51,19 @@ mvn clean install #### **As a single runnable JAR** ```shell -java -jar coda-calibration/calibration-standalone/target/calibration-standalone-1.0.18-runnable.jar +java -jar coda-calibration/calibration-standalone/target/calibration-standalone-1.0.19-runnable.jar ``` #### **GUI alone** ```shell -java -jar coda-calibration/calibration-gui/target/calibration-gui-1.0.18-runnable.jar +java -jar coda-calibration/calibration-gui/target/calibration-gui-1.0.19-runnable.jar ``` #### **Calibration REST service alone** ```shell -java -jar coda-calibration/calibration-service/application/target/application-1.0.18-runnable.jar +java -jar coda-calibration/calibration-service/application/target/application-1.0.19-runnable.jar ``` #### A note about HTTPS @@ -188,4 +188,4 @@ The `Coda Calibration Tool` is provided under the [Apache License](LICENSE.txt). by Lawrence Livermore National Laboratory under Contract DE-AC52-07NA27344. ``` -`LLNL-CODE-743439` +`LLNL-CODE-743439, LLNL-CODE-848318` diff --git a/calibration-gui/pom.xml b/calibration-gui/pom.xml index b87cb44d..4a2ac8a0 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.18.3 + 1.0.19 calibration-gui diff --git a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/AppProperties.java b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/AppProperties.java index 52ceba8b..50976962 100644 --- a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/AppProperties.java +++ b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/AppProperties.java @@ -2,11 +2,11 @@ * Copyright (c) 2018, Lawrence Livermore National Security, LLC. Produced at the Lawrence Livermore National Laboratory * CODE-743439. * All rights reserved. -* This file is part of CCT. For details, see https://github.com/LLNL/coda-calibration-tool. -* +* This file is part of CCT. For details, see https://github.com/LLNL/coda-calibration-tool. +* * 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. +* 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 @@ -25,8 +25,7 @@ @Component @ConfigurationProperties("app") public class AppProperties { - - private String baseTitle = "Coda Calibration"; + private String baseTitle = ""; private Integer height = 800; private Integer width = 600; private Boolean debugEnabled = Boolean.FALSE; diff --git a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/CodaGuiController.java b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/CodaGuiController.java index 5a7e3c80..c4659406 100644 --- a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/CodaGuiController.java +++ b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/CodaGuiController.java @@ -1,5 +1,5 @@ /* -* Copyright (c) 2018, Lawrence Livermore National Security, LLC. Produced at the Lawrence Livermore National Laboratory +* Copyright (c) 2023, Lawrence Livermore National Security, LLC. Produced at the Lawrence Livermore National Laboratory * CODE-743439. * All rights reserved. * This file is part of CCT. For details, see https://github.com/LLNL/coda-calibration-tool. @@ -25,7 +25,6 @@ import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; -import java.util.function.Consumer; import javax.annotation.PreDestroy; import javax.net.ssl.HostnameVerifier; @@ -40,15 +39,16 @@ import com.google.common.eventbus.EventBus; import com.google.common.eventbus.Subscribe; +import gov.llnl.gnem.apps.coda.calibration.gui.GuiApplication.ApplicationMode; 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.EventTabController; import gov.llnl.gnem.apps.coda.calibration.gui.controllers.MapListeningController; import gov.llnl.gnem.apps.coda.calibration.gui.controllers.MeasuredMwsController; 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.RefreshableController; -import gov.llnl.gnem.apps.coda.calibration.gui.controllers.ScreenshotEnabledController; import gov.llnl.gnem.apps.coda.calibration.gui.controllers.ShapeController; import gov.llnl.gnem.apps.coda.calibration.gui.controllers.SiteController; import gov.llnl.gnem.apps.coda.calibration.gui.controllers.parameters.ParametersController; @@ -59,19 +59,23 @@ import gov.llnl.gnem.apps.coda.calibration.gui.events.MapIconActivationCallback; import gov.llnl.gnem.apps.coda.calibration.gui.events.MapPolygonChangeHandler; import gov.llnl.gnem.apps.coda.calibration.gui.events.UpdateMapPolygonEvent; +import gov.llnl.gnem.apps.coda.calibration.gui.plotting.CertLeafletMapController; +import gov.llnl.gnem.apps.coda.calibration.gui.plotting.LeafletMapController; import gov.llnl.gnem.apps.coda.calibration.gui.plotting.WaveformGui; import gov.llnl.gnem.apps.coda.calibration.gui.util.CalibrationProgressListener; import gov.llnl.gnem.apps.coda.calibration.gui.util.FileDialogs; import gov.llnl.gnem.apps.coda.calibration.model.messaging.CalibrationStatusEvent; import gov.llnl.gnem.apps.coda.calibration.model.messaging.CalibrationStatusEvent.Status; +import gov.llnl.gnem.apps.coda.calibration.model.messaging.RatioStatusEvent; import gov.llnl.gnem.apps.coda.common.gui.controllers.ProgressGui; import gov.llnl.gnem.apps.coda.common.gui.data.client.api.WaveformClient; import gov.llnl.gnem.apps.coda.common.gui.events.ShowFailureReportEvent; import gov.llnl.gnem.apps.coda.common.gui.util.ProgressMonitor; import gov.llnl.gnem.apps.coda.common.gui.util.SnapshotUtils; -import gov.llnl.gnem.apps.coda.common.mapping.api.GeoMap; import gov.llnl.gnem.apps.coda.common.model.domain.Pair; import gov.llnl.gnem.apps.coda.envelope.gui.EnvelopeGuiController; +import gov.llnl.gnem.apps.coda.spectra.gui.RatioStatusProgressListener; +import gov.llnl.gnem.apps.coda.spectra.gui.SpectraRatioGuiController; import javafx.application.Platform; import javafx.concurrent.Worker; import javafx.event.ActionEvent; @@ -99,15 +103,21 @@ public class CodaGuiController { private static final Logger log = LoggerFactory.getLogger(CodaGuiController.class); + private static final String SCREENSHOT_TITLE = "CERT_Screenshot"; + @FXML private Node rootElement; + private LeafletMapController cctMapController; + private CertLeafletMapController certMapController; private WaveformGui waveformGui; private DataController data; private ParametersController param; private ShapeController shape; private PathController path; private SiteController site; + private EventTabController eventTableController; + private SpectraRatioGuiController spectraGui; private MeasuredMwsController measuredMws; @FXML @@ -131,29 +141,29 @@ public class CodaGuiController { @FXML private Tab measuredMwsTab; - private Runnable activeTabRefresh; - private Consumer activeTabScreenshot; - @FXML private Button showMapButton; @FXML private Button refreshButton; + @FXML + private Button centerMapBtn; + @FXML private Button snapshotButton; @FXML private CheckMenuItem waveformFocus; + private Runnable activeTabRefresh; + private EnvelopeGuiController envelopeGui; private Label activeMapIcon; private Label showMapIcon; - private GeoMap mapController; - private WaveformClient waveformClient; private ParameterClient configClient; @@ -185,7 +195,8 @@ public class CodaGuiController { private ProgressGui loadingGui; - private Map monitors = new HashMap<>(); + private Map calibrationMonitors = new HashMap<>(); + private Map ratioMonitors = new HashMap<>(); private ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor(r -> { Thread thread = new Thread(r); @@ -205,12 +216,14 @@ public class CodaGuiController { private SSLContext sslContext; @Autowired - public CodaGuiController(GeoMap mapController, WaveformClient waveformClient, EnvelopeLoadingController waveformLoadingController, CodaParamLoadingController codaParamLoadingController, - ReferenceEventLoadingController refEventLoadingController, CalibrationClient calibrationClient, ParamExporter paramExporter, WaveformGui waveformGui, DataController data, - ParametersController param, ShapeController shape, PathController path, SiteController site, MeasuredMwsController measuredMws, ParameterClient configClient, - EnvelopeGuiController envelopeGui, HostnameVerifier hostnameVerifier, SSLContext sslContext, Environment env, EventBus bus) { - this.mapController = mapController; + public CodaGuiController(LeafletMapController cctMapController, CertLeafletMapController certMapController, WaveformClient waveformClient, EnvelopeLoadingController waveformLoadingController, + CodaParamLoadingController codaParamLoadingController, ReferenceEventLoadingController refEventLoadingController, CalibrationClient calibrationClient, ParamExporter paramExporter, + WaveformGui waveformGui, DataController data, ParametersController param, ShapeController shape, PathController path, SiteController site, EventTabController certEventTab, + MeasuredMwsController measuredMws, ParameterClient configClient, EnvelopeGuiController envelopeGui, SpectraRatioGuiController spectraGui, HostnameVerifier hostnameVerifier, + SSLContext sslContext, Environment env, EventBus bus) { this.waveformClient = waveformClient; + this.cctMapController = cctMapController; + this.certMapController = certMapController; this.envelopeLoadingController = waveformLoadingController; this.codaParamLoadingController = codaParamLoadingController; this.refEventLoadingController = refEventLoadingController; @@ -222,10 +235,11 @@ public CodaGuiController(GeoMap mapController, WaveformClient waveformClient, En this.shape = shape; this.path = path; this.site = site; + this.eventTableController = certEventTab; this.measuredMws = measuredMws; this.configClient = configClient; this.envelopeGui = envelopeGui; - this.sslContext = sslContext; + this.spectraGui = spectraGui; this.env = env; this.bus = bus; bus.register(this); @@ -258,6 +272,36 @@ public CodaGuiController(GeoMap mapController, WaveformClient waveformClient, En referenceEventFileChooser.getExtensionFilters().add(allFilesFilter); } + @FXML + private void changeAppMode() { + GuiApplication.changeApplicationMode(); + + if (GuiApplication.getStartupMode() == ApplicationMode.CCT) { + addEnabledTabListeners(shapeTab, shape); + addEnabledTabListeners(pathTab, path); + addEnabledTabListeners(siteTab, site); + addEnabledTabListeners(measuredMwsTab, measuredMws); + Platform.runLater(() -> { + showMapButton.setGraphic(activeMapIcon); + }); + } else { + spectraGui.loadEnvelopes(); + siteTab.setOnSelectionChanged(e -> { + if (siteTab.isSelected()) { + activeTabRefresh = ((RefreshableController) eventTableController).getRefreshFunction(); + } else { + siteTab.setGraphic(null); + } + }); + + Platform.runLater(() -> { + mainTabPane.getTabs().remove(shapeTab); + mainTabPane.getTabs().remove(pathTab); + mainTabPane.getTabs().remove(measuredMwsTab); + }); + } + } + @FXML private void openWaveformLoadingWindow() { Optional.ofNullable(sacFileChooser.showOpenMultipleDialog(rootElement.getScene().getWindow())).ifPresent(envelopeLoadingController::loadFiles); @@ -266,9 +310,24 @@ private void openWaveformLoadingWindow() { @FXML private void showMapWindow() { Platform.runLater(() -> { - if (mapController != null) { - mapController.show(); - mapController.fitViewToActiveShapes(); + if (GuiApplication.getStartupMode() == ApplicationMode.CCT) { + if (cctMapController != null) { + cctMapController.show(); + cctMapController.fitViewToActiveShapes(); + } + } else if (certMapController != null) { + certMapController.show(); + certMapController.fitViewToActiveShapes(); + } + }); + } + + @FXML + private void centerMap() { + Platform.runLater(() -> { + if (GuiApplication.getStartupMode() == ApplicationMode.CERT && certMapController != null) { + certMapController.show(); + certMapController.fitViewToActiveShapes(); } }); } @@ -334,8 +393,8 @@ private void openCalibrationDataSavingWindow(ActionEvent e) { Files.move(exportArchive.toPath(), selectedFile.toPath(), StandardCopyOption.REPLACE_EXISTING); } } - } catch (IOException e1) { - FileDialogs.fileIoErrorAlert(e1); + } catch (IOException ex) { + FileDialogs.fileIoErrorAlert(ex); } } } @@ -394,6 +453,8 @@ private void measureMws() { private void clearData() { calibrationClient.clearData().subscribe(val -> { }, err -> log.trace(err.getMessage(), err), () -> data.getRefreshFunction().run()); + spectraGui.loadEnvelopes(); + activeTabRefresh.run(); } @FXML @@ -413,8 +474,11 @@ public void initialize() { waveformFocus.selectedProperty().bindBidirectional(waveformGui.focusProperty()); - mapController.registerEventCallback(new MapIconActivationCallback(waveformClient)); - mapController.registerEventCallback(new MapPolygonChangeHandler(configClient)); + certMapController.registerEventCallback(new MapIconActivationCallback(waveformClient)); + certMapController.registerEventCallback(new MapPolygonChangeHandler(configClient)); + + cctMapController.registerEventCallback(new MapIconActivationCallback(waveformClient)); + cctMapController.registerEventCallback(new MapPolygonChangeHandler(configClient)); activeMapIcon = makeMapLabel(); showMapIcon = makeMapLabel(); @@ -423,21 +487,36 @@ public void initialize() { snapshotButton.setContentDisplay(ContentDisplay.CENTER); addEnabledTabListeners(dataTab, data); - activeTabScreenshot = folder -> SnapshotUtils.writePng(folder, new Pair<>(dataTab.getText(), dataTab.getContent())); + data.setVisible(true); paramTab.setOnSelectionChanged(e -> { if (paramTab.isSelected()) { - mapController.clearIcons(); + cctMapController.clearIcons(); + certMapController.clearIcons(); activeTabRefresh = param.getRefreshFunction(); - activeTabScreenshot = folder -> SnapshotUtils.writePng(folder, new Pair<>(paramTab.getText(), paramTab.getContent())); } }); - addEnabledTabListeners(shapeTab, shape); - addEnabledTabListeners(pathTab, path); - addEnabledTabListeners(siteTab, site); - addEnabledTabListeners(measuredMwsTab, measuredMws); + if (GuiApplication.getStartupMode() == ApplicationMode.CCT) { + addEnabledTabListeners(shapeTab, shape); + addEnabledTabListeners(pathTab, path); + addEnabledTabListeners(siteTab, site); + addEnabledTabListeners(measuredMwsTab, measuredMws); + } else { + siteTab.setOnSelectionChanged(e -> { + if (siteTab.isSelected()) { + activeTabRefresh = ((RefreshableController) eventTableController).getRefreshFunction(); + } else { + siteTab.setGraphic(null); + } + }); + Platform.runLater(() -> { + mainTabPane.getTabs().remove(shapeTab); + mainTabPane.getTabs().remove(pathTab); + mainTabPane.getTabs().remove(measuredMwsTab); + }); + } rootElement.setOnDragOver(event -> { if (event.getGestureSource() != rootElement && event.getDragboard().hasFiles()) { @@ -450,6 +529,17 @@ public void initialize() { boolean success = false; if (event.getGestureSource() != rootElement && event.getDragboard().hasFiles()) { envelopeLoadingController.loadFiles(event.getDragboard().getFiles()); + + if (GuiApplication.getStartupMode() == ApplicationMode.CERT) { + envelopeLoadingController.setCompletionCallback(() -> { + spectraGui.loadEnvelopes(); + if (certMapController != null) { + certMapController.show(); + certMapController.fitViewToActiveShapes(); + } + }); + } + codaParamLoadingController.loadFiles(event.getDragboard().getFiles()); success = true; } @@ -458,7 +548,7 @@ public void initialize() { }); try { - loadingGui = new ProgressGui(); + loadingGui = ProgressGui.getInstance(); } catch (IllegalStateException e) { log.error("Unable to instantiate loading display {}", e.getMessage(), e); } @@ -472,15 +562,13 @@ private void addEnabledTabListeners(Tab tab, MapListeningController controller) controller.refreshView(); tab.setGraphic(activeMapIcon); if (controller instanceof RefreshableController) { - activeTabRefresh = ((RefreshableController) controller).getRefreshFunction(); + activeTabRefresh = () -> { + ((RefreshableController) controller).getRefreshFunction().run(); + ((RefreshableController) eventTableController).getRefreshFunction().run(); + }; } else { activeTabRefresh = () -> controller.refreshView(); } - if (controller instanceof ScreenshotEnabledController) { - activeTabScreenshot = ((ScreenshotEnabledController) controller).getScreenshotFunction(); - } else { - activeTabScreenshot = folder -> SnapshotUtils.writePng(folder, new Pair<>(tab.getText(), tab.getContent())); - } } else { tab.setGraphic(null); controller.setVisible(false); @@ -504,7 +592,7 @@ private void snapshotTab(ActionEvent e) { try { if (folder != null && folder.exists() && folder.isDirectory() && folder.canWrite()) { screenshotFolderChooser.setInitialDirectory(folder); - Platform.runLater(() -> activeTabScreenshot.accept(folder)); + SnapshotUtils.writePng(folder, new Pair<>(SCREENSHOT_TITLE + SnapshotUtils.getTimestampWithLeadingSeparator(), rootElement)); } } catch (SecurityException ex) { log.warn("Exception trying to write screenshots to folder {} : {}", folder, ex.getLocalizedMessage(), ex); @@ -537,32 +625,37 @@ private Label makeSnapshotLabel() { @Subscribe private void listener(UpdateMapPolygonEvent event) { - if (mapController != null && event != null && event.getGeoJSON() != null && !event.getGeoJSON().isEmpty()) { - mapController.setPolygonGeoJSON(event.getGeoJSON()); + if (event != null && event.getGeoJSON() != null && !event.getGeoJSON().isEmpty()) { + if (certMapController != null) { + certMapController.setPolygonGeoJSON(event.getGeoJSON()); + } + if (cctMapController != null) { + cctMapController.setPolygonGeoJSON(event.getGeoJSON()); + } } } //TODO: Move this to a controller @Subscribe private void listener(CalibrationStatusEvent event) { - if (!monitors.containsKey(event.getId()) && event.getStatus() == Status.STARTING) { + if (!calibrationMonitors.containsKey(event.getId()) && event.getStatus() == Status.STARTING) { CalibrationProgressListener eventMonitor = new CalibrationProgressListener(bus, event); ProgressMonitor monitor = new ProgressMonitor("Calibration Progress " + event.getId(), eventMonitor); monitor.addCancelCallback(() -> calibrationClient.cancelCalibration(event.getId()).subscribe()); - monitors.put(event.getId(), monitor); + calibrationMonitors.put(event.getId(), monitor); loadingGui.addProgressMonitor(monitor); loadingGui.show(); } if (event.getStatus() == Status.COMPLETE || event.getStatus() == Status.ERROR) { - final ProgressMonitor monitor = monitors.remove(event.getId()); + final ProgressMonitor monitor = calibrationMonitors.remove(event.getId()); if (monitor != null) { monitor.setProgressStage("Finished"); monitor.clearCancelCallbacks(); service.schedule(() -> loadingGui.removeProgressMonitor(monitor), 15, TimeUnit.MINUTES); } } else { - ProgressMonitor monitor = monitors.get(event.getId()); + ProgressMonitor monitor = calibrationMonitors.get(event.getId()); if (monitor != null) { switch (event.getStatus()) { case PEAK_STARTING: @@ -584,6 +677,37 @@ private void listener(CalibrationStatusEvent event) { } } + @Subscribe + private void listener(RatioStatusEvent event) { + + ProgressMonitor monitor = ratioMonitors.get(event.getId()); + + if (monitor == null && event.getStatus() == RatioStatusEvent.Status.STARTING) { + RatioStatusProgressListener eventMonitor = new RatioStatusProgressListener(bus, event); + ProgressMonitor newMonitor = new ProgressMonitor("Ratio Measurement Progress " + event.getId(), eventMonitor); + loadingGui.addProgressMonitor(newMonitor); + ratioMonitors.put(event.getId(), newMonitor); + service.schedule(() -> loadingGui.removeProgressMonitor(monitor), 15, TimeUnit.MINUTES); + loadingGui.show(); + } + + if (monitor != null && (event.getStatus() == RatioStatusEvent.Status.COMPLETE || event.getStatus() == RatioStatusEvent.Status.ERROR)) { + monitor.setProgressStage("Finished"); + service.schedule(() -> loadingGui.removeProgressMonitor(monitor), 15, TimeUnit.MINUTES); + } else if (monitor != null) { + switch (event.getStatus()) { + case STARTING: + monitor.setProgressStage("Starting..."); + break; + case PROCESSING: + monitor.setProgressStage("Processing..."); + break; + default: + break; + } + } + } + @Subscribe private void listener(CalibrationStageShownEvent evt) { if (!initialized) { diff --git a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/GuiApplication.java b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/GuiApplication.java index f48f7996..66b6134a 100644 --- a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/GuiApplication.java +++ b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/GuiApplication.java @@ -59,16 +59,26 @@ @ComponentScan("gov.llnl.gnem.apps.coda.envelope.model") @ComponentScan("gov.llnl.gnem.apps.coda.envelope.gui") @ComponentScan("gov.llnl.gnem.apps.coda.calibration.gui") +@ComponentScan("gov.llnl.gnem.apps.coda.spectra.gui") public class GuiApplication extends Application { + public enum ApplicationMode { + CERT, CCT + } + + static final String CERT_TITLE = "Coda Envelope Ratio Tool"; + static final String CCT_TITLE = "Coda Calibration Tool"; + private static final Logger log = LoggerFactory.getLogger(GuiApplication.class); - private ConfigurableApplicationContext springContext; + private static ConfigurableApplicationContext springContext; - private Stage primaryStage; + private static Stage primaryStage; private EventBus bus; + private static ApplicationMode startupMode; + @PostConstruct void started() { Locale.setDefault(Locale.ENGLISH); @@ -78,9 +88,10 @@ void started() { public GuiApplication() { } - public GuiApplication(ConfigurableApplicationContext springContext, EventBus bus) { + public GuiApplication(ConfigurableApplicationContext springContext, EventBus bus, ApplicationMode mode) { this.springContext = springContext; this.bus = bus; + GuiApplication.startupMode = mode; } public static void main(String[] args) { @@ -130,13 +141,20 @@ public void start(Stage primaryStage) throws Exception { Class clazz = GuiApplication.class; String className = clazz.getSimpleName() + ".class"; String classPath = clazz.getResource(className).toString(); - String baseTitle = props.getBaseTitle(); + String baseTitle = ""; + + if (GuiApplication.getStartupMode() == ApplicationMode.CCT) { + baseTitle = CCT_TITLE; + } else { + baseTitle = CERT_TITLE; + } + if (classPath.startsWith("jar")) { String manifestPath = classPath.substring(0, classPath.indexOf('!') + 1) + "/META-INF/MANIFEST.MF"; Manifest mf = new Manifest(new URL(manifestPath).openStream()); Attributes atts = mf.getMainAttributes(); // Put this info in the log to help with analysis - log.info( + log.debug( "Version:{} Commit:{} Branch:{} By:{} at {}", atts.getValue("Implementation-Version"), atts.getValue("Implementation-Build"), @@ -147,7 +165,7 @@ public void start(Stage primaryStage) throws Exception { baseTitle += " Built at " + atts.getValue("Build-Timestamp"); } else { // Class not from JAR - log.info("{} not running from a jar.", baseTitle); + log.debug("{} not running from a jar.", baseTitle); } props.setBaseTitle(baseTitle); } catch (IOException e) { @@ -156,7 +174,13 @@ public void start(Stage primaryStage) throws Exception { } Platform.setImplicitExit(true); - FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/fxml/CodaGui.fxml")); + FXMLLoader fxmlLoader = null; + + if (startupMode == ApplicationMode.CERT) { + fxmlLoader = new FXMLLoader(getClass().getResource("/fxml/CertGui.fxml")); + } else { + fxmlLoader = new FXMLLoader(getClass().getResource("/fxml/CodaGui.fxml")); + } fxmlLoader.setControllerFactory(springContext::getBean); try { @@ -174,6 +198,43 @@ public void start(Stage primaryStage) throws Exception { } } + static public void changeApplicationMode() { + + if (GuiApplication.getStartupMode() == ApplicationMode.CCT) { + GuiApplication.startupMode = ApplicationMode.CERT; + } else { + GuiApplication.startupMode = ApplicationMode.CCT; + } + + Platform.runLater(() -> { + primaryStage.close(); + }); + + AppProperties props = springContext.getBean(AppProperties.class); + FXMLLoader fxmlLoader = null; + if (GuiApplication.startupMode == ApplicationMode.CERT) { + props.setBaseTitle(CERT_TITLE); + fxmlLoader = new FXMLLoader(GuiApplication.class.getResource("/fxml/CertGui.fxml")); + } else { + props.setBaseTitle(CCT_TITLE); + fxmlLoader = new FXMLLoader(GuiApplication.class.getResource("/fxml/CodaGui.fxml")); + } + fxmlLoader.setControllerFactory(springContext::getBean); + + try { + Parent root = fxmlLoader.load(); + Platform.runLater(() -> { + primaryStage.setTitle(props.getBaseTitle()); + Scene scene = new Scene(root, props.getHeight(), props.getWidth()); + primaryStage.setScene(scene); + primaryStage.show(); + }); + } catch (IllegalStateException | IOException e) { + log.error("Unable to load main panel FXML file, terminating. {}", e.getMessage(), e); + Platform.exit(); + } + } + @Override public void stop() throws Exception { try { @@ -187,6 +248,14 @@ public void stop() throws Exception { System.exit(0); } + public static ApplicationMode getStartupMode() { + return startupMode; + } + + public static void setStartupMode(ApplicationMode startupMode) { + GuiApplication.startupMode = startupMode; + } + public Stage getPrimaryStage() { return primaryStage; } diff --git a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/WebfluxConfig.java b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/WebfluxConfig.java index e72b7aef..0bd55b36 100644 --- a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/WebfluxConfig.java +++ b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/WebfluxConfig.java @@ -1,68 +1,68 @@ -/* -* Copyright (c) 2019, Lawrence Livermore National Security, LLC. Produced at the Lawrence Livermore National Laboratory -* CODE-743439. -* All rights reserved. -* This file is part of CCT. For details, see https://github.com/LLNL/coda-calibration-tool. -* -* 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; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.codec.json.Jackson2JsonDecoder; -import org.springframework.http.codec.json.Jackson2JsonEncoder; -import org.springframework.web.reactive.function.client.ExchangeStrategies; - -import com.fasterxml.jackson.core.Version; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.module.SimpleAbstractTypeResolver; -import com.fasterxml.jackson.databind.module.SimpleModule; - -import gov.llnl.gnem.apps.coda.calibration.model.domain.SiteFrequencyBandParameters; -import gov.llnl.gnem.apps.coda.calibration.model.domain.SpectraMeasurementMetadata; -import gov.llnl.gnem.apps.coda.calibration.model.domain.SpectraMeasurementMetadataImpl; -import gov.llnl.gnem.apps.coda.calibration.model.domain.WaveformMetadataImpl; -import gov.llnl.gnem.apps.coda.calibration.model.domain.mixins.SharedFrequencyBandParametersJsonMixin; -import gov.llnl.gnem.apps.coda.calibration.model.domain.mixins.SiteFrequencyBandParametersJsonMixin; -import gov.llnl.gnem.apps.coda.common.model.domain.SharedFrequencyBandParameters; -import gov.llnl.gnem.apps.coda.common.model.domain.WaveformMetadata; - -@Configuration -public class WebfluxConfig { - - private final ObjectMapper objectMapper; - - @Autowired - public WebfluxConfig(ObjectMapper objectMapper) { - this.objectMapper = objectMapper; - this.objectMapper.addMixIn(SharedFrequencyBandParameters.class, SharedFrequencyBandParametersJsonMixin.class); - this.objectMapper.addMixIn(SiteFrequencyBandParameters.class, SiteFrequencyBandParametersJsonMixin.class); - - SimpleModule module = new SimpleModule("SpectraMeasurementMapper", Version.unknownVersion()); - SimpleAbstractTypeResolver resolver = new SimpleAbstractTypeResolver(); - resolver.addMapping(SpectraMeasurementMetadata.class, SpectraMeasurementMetadataImpl.class); - resolver.addMapping(WaveformMetadata.class, WaveformMetadataImpl.class); - module.setAbstractTypes(resolver); - this.objectMapper.registerModule(module); - } - - @Bean - public ExchangeStrategies configureJacksonExchangeStrategies() { - return ExchangeStrategies.builder().codecs(clientCodecConfigurer -> { - Jackson2JsonDecoder decoder = new Jackson2JsonDecoder(objectMapper); - decoder.setMaxInMemorySize(-1); - clientCodecConfigurer.customCodecs().registerWithDefaultConfig(decoder); - clientCodecConfigurer.customCodecs().registerWithDefaultConfig(new Jackson2JsonEncoder(objectMapper)); - //Unlimited - clientCodecConfigurer.defaultCodecs().maxInMemorySize(-1); - }).build(); - } -} +/* +* Copyright (c) 2019, Lawrence Livermore National Security, LLC. Produced at the Lawrence Livermore National Laboratory +* CODE-743439. +* All rights reserved. +* This file is part of CCT. For details, see https://github.com/LLNL/coda-calibration-tool. +* +* 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; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.codec.json.Jackson2JsonDecoder; +import org.springframework.http.codec.json.Jackson2JsonEncoder; +import org.springframework.web.reactive.function.client.ExchangeStrategies; + +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleAbstractTypeResolver; +import com.fasterxml.jackson.databind.module.SimpleModule; + +import gov.llnl.gnem.apps.coda.calibration.model.domain.SiteFrequencyBandParameters; +import gov.llnl.gnem.apps.coda.calibration.model.domain.SpectraMeasurementMetadata; +import gov.llnl.gnem.apps.coda.calibration.model.domain.SpectraMeasurementMetadataImpl; +import gov.llnl.gnem.apps.coda.calibration.model.domain.WaveformMetadataImpl; +import gov.llnl.gnem.apps.coda.calibration.model.domain.mixins.SharedFrequencyBandParametersJsonMixin; +import gov.llnl.gnem.apps.coda.calibration.model.domain.mixins.SiteFrequencyBandParametersJsonMixin; +import gov.llnl.gnem.apps.coda.common.model.domain.SharedFrequencyBandParameters; +import gov.llnl.gnem.apps.coda.common.model.domain.WaveformMetadata; + +@Configuration +public class WebfluxConfig { + + private final ObjectMapper objectMapper; + + @Autowired + public WebfluxConfig(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + this.objectMapper.addMixIn(SharedFrequencyBandParameters.class, SharedFrequencyBandParametersJsonMixin.class); + this.objectMapper.addMixIn(SiteFrequencyBandParameters.class, SiteFrequencyBandParametersJsonMixin.class); + + SimpleModule module = new SimpleModule("SpectraMeasurementMapper", Version.unknownVersion()); + SimpleAbstractTypeResolver resolver = new SimpleAbstractTypeResolver(); + resolver.addMapping(SpectraMeasurementMetadata.class, SpectraMeasurementMetadataImpl.class); + resolver.addMapping(WaveformMetadata.class, WaveformMetadataImpl.class); + module.setAbstractTypes(resolver); + this.objectMapper.registerModule(module); + } + + @Bean + public ExchangeStrategies configureJacksonExchangeStrategies() { + return ExchangeStrategies.builder().codecs(clientCodecConfigurer -> { + Jackson2JsonDecoder decoder = new Jackson2JsonDecoder(objectMapper); + decoder.setMaxInMemorySize(-1); + clientCodecConfigurer.customCodecs().registerWithDefaultConfig(decoder); + clientCodecConfigurer.customCodecs().registerWithDefaultConfig(new Jackson2JsonEncoder(objectMapper)); + //Unlimited + clientCodecConfigurer.defaultCodecs().maxInMemorySize(-1); + }).build(); + } +} 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 3de8aeb6..c0d500b0 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 @@ -1,5 +1,5 @@ /* -* Copyright (c) 2021, Lawrence Livermore National Security, LLC. Produced at the Lawrence Livermore National Laboratory +* Copyright (c) 2023, Lawrence Livermore National Security, LLC. Produced at the Lawrence Livermore National Laboratory * CODE-743439. * All rights reserved. * @@ -33,7 +33,6 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Objects; -import java.util.Optional; import java.util.Set; import java.util.TreeMap; import java.util.concurrent.atomic.AtomicReference; @@ -84,7 +83,6 @@ import gov.llnl.gnem.apps.coda.common.model.messaging.WaveformChangeEvent; import gov.llnl.gnem.apps.coda.common.model.util.SPECTRA_TYPES; import javafx.application.Platform; -import javafx.beans.binding.Bindings; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.fxml.FXML; @@ -98,9 +96,7 @@ import javafx.scene.control.MenuItem; import javafx.scene.control.Tab; import javafx.scene.control.TableColumn; -import javafx.scene.control.TableColumn.CellDataFeatures; import javafx.scene.control.TableRow; -import javafx.scene.control.TableView; import javafx.scene.input.MouseEvent; import javafx.scene.layout.StackPane; import javafx.scene.paint.Color; @@ -165,159 +161,6 @@ public abstract class AbstractMeasurementController implements MapListeningContr @FXML protected ComboBox evidCombo; - @FXML - protected TableView eventTable; - - @FXML - protected TableColumn evidCol; - - @FXML - protected TableColumn dateCol; - - @FXML - protected TableColumn mwCol; - - @FXML - protected TableColumn obsEnergyCol; - - @FXML - protected TableColumn totalEnergyCol; - - @FXML - protected TableColumn totalEnergyUq1LowCol; - - @FXML - protected TableColumn totalEnergyUq1HighCol; - - @FXML - protected TableColumn totalEnergyUq2LowCol; - - @FXML - protected TableColumn totalEnergyUq2HighCol; - - @FXML - protected TableColumn totalEnergyMDACCol; - - @FXML - protected TableColumn totalEnergyMDACUq1LowCol; - - @FXML - protected TableColumn totalEnergyMDACUq1HighCol; - - @FXML - protected TableColumn totalEnergyMDACUq2LowCol; - - @FXML - protected TableColumn totalEnergyMDACUq2HighCol; - - @FXML - protected TableColumn energyRatioCol; - - @FXML - protected TableColumn energyStressCol; - - @FXML - protected TableColumn energyStressUq1LowCol; - - @FXML - protected TableColumn energyStressUq1HighCol; - - @FXML - protected TableColumn energyStressUq2LowCol; - - @FXML - protected TableColumn energyStressUq2HighCol; - - @FXML - protected TableColumn stressCol; - - @FXML - protected TableColumn measuredMwCol; - - @FXML - protected TableColumn measuredMeCol; - - @FXML - protected TableColumn measuredStressCol; - - @FXML - protected TableColumn valMwCol; - - @FXML - protected TableColumn valStressCol; - - @FXML - protected TableColumn mistfitCol; - - @FXML - protected TableColumn measuredCornerFreqCol; - - @FXML - protected TableColumn measuredMwUq1LowCol; - - @FXML - protected TableColumn measuredMwUq1HighCol; - - @FXML - protected TableColumn measuredMwUq2LowCol; - - @FXML - protected TableColumn measuredMwUq2HighCol; - - @FXML - protected TableColumn measuredMeUq1LowCol; - - @FXML - protected TableColumn measuredMeUq1HighCol; - - @FXML - protected TableColumn measuredMeUq2LowCol; - - @FXML - protected TableColumn measuredMeUq2HighCol; - - @FXML - protected TableColumn measuredStressUq1LowCol; - - @FXML - protected TableColumn measuredStressUq1HighCol; - - @FXML - protected TableColumn measuredStressUq2LowCol; - - @FXML - protected TableColumn measuredStressUq2HighCol; - - @FXML - protected TableColumn measuredCornerFreqUq1LowCol; - - @FXML - protected TableColumn measuredCornerFreqUq1HighCol; - - @FXML - protected TableColumn measuredCornerFreqUq2LowCol; - - @FXML - protected TableColumn measuredCornerFreqUq2HighCol; - - @FXML - protected TableColumn iterationsCol; - - @FXML - protected TableColumn dataCountCol; - - @FXML - protected TableColumn stationCountCol; - - @FXML - protected TableColumn bandCoverageCol; - - @FXML - protected TableColumn likelyPoorlyConstrainedCol; - - @FXML - protected TableColumn stationCol; - @FXML protected HiddenHeaderTableView> summaryTable; @@ -514,74 +357,6 @@ public void initialize() { } }); - CellBindingUtils.attachTextCellFactoriesString(evidCol, MeasuredMwDetails::getEventId); - evidCol.comparatorProperty().set(new MaybeNumericStringComparator()); - - CellBindingUtils.attachTextCellFactoriesString(dateCol, MeasuredMwDetails::getDatetime); - - CellBindingUtils.attachTextCellFactories(mwCol, MeasuredMwDetails::getRefMw, dfmt4); - CellBindingUtils.attachTextCellFactories(stressCol, MeasuredMwDetails::getRefApparentStressInMpa, dfmt4); - CellBindingUtils.attachTextCellFactories(valMwCol, MeasuredMwDetails::getValMw, dfmt4); - CellBindingUtils.attachTextCellFactories(valStressCol, MeasuredMwDetails::getValApparentStressInMpa, dfmt4); - - CellBindingUtils.attachTextCellFactories(measuredMwCol, MeasuredMwDetails::getMw, dfmt4); - CellBindingUtils.attachTextCellFactories(measuredMwUq1LowCol, MeasuredMwDetails::getMw1Min, dfmt4); - CellBindingUtils.attachTextCellFactories(measuredMwUq1HighCol, MeasuredMwDetails::getMw1Max, dfmt4); - CellBindingUtils.attachTextCellFactories(measuredMwUq2LowCol, MeasuredMwDetails::getMw2Min, dfmt4); - CellBindingUtils.attachTextCellFactories(measuredMwUq2HighCol, MeasuredMwDetails::getMw2Max, dfmt4); - CellBindingUtils.attachTextCellFactories(measuredMeCol, MeasuredMwDetails::getMe, dfmt4); - CellBindingUtils.attachTextCellFactories(measuredMeUq1LowCol, MeasuredMwDetails::getMw1Min, dfmt4); - CellBindingUtils.attachTextCellFactories(measuredMeUq1HighCol, MeasuredMwDetails::getMw1Max, dfmt4); - CellBindingUtils.attachTextCellFactories(measuredMeUq2LowCol, MeasuredMwDetails::getMw2Min, dfmt4); - CellBindingUtils.attachTextCellFactories(measuredMeUq2HighCol, MeasuredMwDetails::getMw2Max, dfmt4); - - CellBindingUtils.attachTextCellFactories(obsEnergyCol, MeasuredMwDetails::getObsEnergy, dfmt4); - CellBindingUtils.attachTextCellFactories(totalEnergyCol, MeasuredMwDetails::getTotalEnergy, dfmt4); - CellBindingUtils.attachTextCellFactories(totalEnergyUq1LowCol, MeasuredMwDetails::getLogTotalEnergy1Min, dfmt4); - CellBindingUtils.attachTextCellFactories(totalEnergyUq1HighCol, MeasuredMwDetails::getLogTotalEnergy1Max, dfmt4); - CellBindingUtils.attachTextCellFactories(totalEnergyUq2LowCol, MeasuredMwDetails::getLogTotalEnergy2Min, dfmt4); - CellBindingUtils.attachTextCellFactories(totalEnergyUq2HighCol, MeasuredMwDetails::getLogTotalEnergy2Max, dfmt4); - - CellBindingUtils.attachTextCellFactories(totalEnergyMDACCol, MeasuredMwDetails::getTotalEnergyMDAC, dfmt4); - CellBindingUtils.attachTextCellFactories(totalEnergyMDACUq1LowCol, MeasuredMwDetails::getLogTotalEnergyMDAC1Min, dfmt4); - CellBindingUtils.attachTextCellFactories(totalEnergyMDACUq1HighCol, MeasuredMwDetails::getLogTotalEnergyMDAC1Max, dfmt4); - CellBindingUtils.attachTextCellFactories(totalEnergyMDACUq2LowCol, MeasuredMwDetails::getLogTotalEnergyMDAC2Min, dfmt4); - CellBindingUtils.attachTextCellFactories(totalEnergyMDACUq2HighCol, MeasuredMwDetails::getLogTotalEnergyMDAC2Max, dfmt4); - - CellBindingUtils.attachTextCellFactories(energyRatioCol, MeasuredMwDetails::getEnergyRatio, dfmt4); - CellBindingUtils.attachTextCellFactories(energyStressCol, MeasuredMwDetails::getEnergyStress, dfmt4); - CellBindingUtils.attachTextCellFactories(energyStressUq1LowCol, MeasuredMwDetails::getObsAppStress1Min, dfmt4); - CellBindingUtils.attachTextCellFactories(energyStressUq1HighCol, MeasuredMwDetails::getObsAppStress1Max, dfmt4); - CellBindingUtils.attachTextCellFactories(energyStressUq2LowCol, MeasuredMwDetails::getObsAppStress2Min, dfmt4); - CellBindingUtils.attachTextCellFactories(energyStressUq2HighCol, MeasuredMwDetails::getObsAppStress2Max, dfmt4); - - CellBindingUtils.attachTextCellFactories(measuredStressCol, MeasuredMwDetails::getApparentStressInMpa, dfmt4); - CellBindingUtils.attachTextCellFactories(measuredStressUq1LowCol, MeasuredMwDetails::getApparentStress1Min, dfmt4); - CellBindingUtils.attachTextCellFactories(measuredStressUq1HighCol, MeasuredMwDetails::getApparentStress1Max, dfmt4); - CellBindingUtils.attachTextCellFactories(measuredStressUq2LowCol, MeasuredMwDetails::getApparentStress2Min, dfmt4); - CellBindingUtils.attachTextCellFactories(measuredStressUq2HighCol, MeasuredMwDetails::getApparentStress2Max, dfmt4); - - CellBindingUtils.attachTextCellFactories(measuredCornerFreqCol, MeasuredMwDetails::getCornerFreq, dfmt4); - CellBindingUtils.attachTextCellFactories(measuredCornerFreqUq1LowCol, MeasuredMwDetails::getCornerFreq1Min, dfmt4); - CellBindingUtils.attachTextCellFactories(measuredCornerFreqUq1HighCol, MeasuredMwDetails::getCornerFreq1Max, dfmt4); - CellBindingUtils.attachTextCellFactories(measuredCornerFreqUq2LowCol, MeasuredMwDetails::getCornerFreq2Min, dfmt4); - CellBindingUtils.attachTextCellFactories(measuredCornerFreqUq2HighCol, MeasuredMwDetails::getCornerFreq2Max, dfmt4); - - CellBindingUtils.attachTextCellFactories(mistfitCol, MeasuredMwDetails::getMisfit, dfmt4); - CellBindingUtils.attachTextCellFactories(bandCoverageCol, MeasuredMwDetails::getBandCoverage, dfmt4); - CellBindingUtils.attachTextCellFactoriesString(likelyPoorlyConstrainedCol, mw -> mw.isLikelyPoorlyConstrained().toString()); - - stationCountCol.setCellValueFactory( - x -> Bindings.createIntegerBinding(() -> Optional.ofNullable(x).map(CellDataFeatures::getValue).map(MeasuredMwDetails::getStationCount).orElseGet(() -> 0)).asObject()); - - iterationsCol.setCellValueFactory( - x -> Bindings.createIntegerBinding(() -> Optional.ofNullable(x).map(CellDataFeatures::getValue).map(MeasuredMwDetails::getIterations).orElseGet(() -> 0)).asObject()); - - dataCountCol.setCellValueFactory( - x -> Bindings.createIntegerBinding(() -> Optional.ofNullable(x).map(CellDataFeatures::getValue).map(MeasuredMwDetails::getDataCount).orElseGet(() -> 0)).asObject()); - - eventTable.setItems(mwParameters); - menu = new ContextMenu(); include = new MenuItem("Include Selected"); menu.getItems().add(include); @@ -615,7 +390,7 @@ private void showWaveformPopup(final Long... ids) { private void plotSpectra() { clearSpectraPlots(); spectraControllers.forEach(spc -> { - spc.getSpectraMeasurementMap().clear(); + spc.getSpectraDataMap().clear(); spc.getSymbolMap().clear(); }); plotPointMap.clear(); @@ -663,7 +438,7 @@ private void plotSpectra() { filteredMeasurements = Collections.emptyList(); } - if (mwDetails.getEventId() != null) { + if (mwDetails != null && mwDetails.getEventId() != null) { final Spectra fitSpectra = fittingSpectra.get(0); summaryValues.add( new Pair<>("Mw (Model Fit)", @@ -765,11 +540,11 @@ private void plotSpectra() { spectraControllers.forEach(spc -> { if (fittingSpectra != null && spc.shouldShowFits()) { - spc.getSpectralPlot().plotXYdata(toPlotPoints(selectedEventMeasurements, spc.getDataFunc()), fittingSpectra); + spc.getSpectralPlot().plotXYdata(toPlotPoints(selectedEventMeasurements, spc.getDataFunc()), fittingSpectra, null); } else { - spc.getSpectralPlot().plotXYdata(toPlotPoints(selectedEventMeasurements, spc.getDataFunc()), null); + spc.getSpectralPlot().plotXYdata(toPlotPoints(selectedEventMeasurements, spc.getDataFunc()), null, null); } - spc.getSpectraMeasurementMap().putAll(mapSpectraToPoint(selectedEventMeasurements, spc.getDataFunc())); + spc.getSpectraDataMap().putAll(mapSpectraToPoint(selectedEventMeasurements, spc.getDataFunc())); spc.showConstraintWarningBanner(showPoorlyConstrainedBanner); }); @@ -1131,7 +906,6 @@ protected void reloadData() { .distinct() .sorted(new MaybeNumericStringComparator()) .collect(Collectors.toList())); - eventTable.sort(); }); final Map> evidStats = new HashMap<>(); @@ -1534,35 +1308,4 @@ protected PropertyChangeListener getPlotpointObserver(final Supplier evs = new ArrayList<>(eventTable.getSelectionModel().getSelectedIndices().size()); - eventTable.getSelectionModel().getSelectedIndices().forEach(i -> evs.add(mwParameters.get(i))); - removeRefEvents(evs); - } - - @FXML - private void toggleValidationEvent() { - final List evs = new ArrayList<>(eventTable.getSelectionModel().getSelectedIndices().size()); - eventTable.getSelectionModel().getSelectedIndices().forEach(i -> evs.add(mwParameters.get(i))); - if (!evs.isEmpty()) { - referenceEventClient.toggleValidationEventsByEventId(evs.stream().map(MeasuredMwDetails::getEventId).distinct().collect(Collectors.toList())) - .doOnComplete(() -> Platform.runLater(this::reloadData)) - .subscribe(); - } - } - - private void removeRefEvents(final List evs) { - if (evs != null && !evs.isEmpty()) { - referenceEventClient.removeReferenceEventsByEventId(evs.stream().map(MeasuredMwDetails::getEventId).distinct().collect(Collectors.toList())) - .doOnSuccess(v -> Platform.runLater(this::reloadData)) - .subscribe(); - } - } } diff --git a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/controllers/BaseSpectraPlotController.java b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/controllers/BaseSpectraPlotController.java new file mode 100644 index 00000000..6d798d70 --- /dev/null +++ b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/controllers/BaseSpectraPlotController.java @@ -0,0 +1,78 @@ +/* +* Copyright (c) 2022, 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.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +import gov.llnl.gnem.apps.coda.calibration.gui.plotting.SpectralPlot; +import javafx.geometry.Point2D; +import llnl.gnem.core.gui.plotting.api.Symbol; + +public class BaseSpectraPlotController { + private final SpectralPlot spectraPlot = new SpectralPlot(); + private boolean isYaxisResizble = false; + private final Function dataFunction; + private boolean shouldShowFits = false; + private final Map spectraDataMap = new HashMap<>(); + + public BaseSpectraPlotController(final Function dataFunction) { + this.dataFunction = dataFunction; + } + + public SpectralPlot getSpectralPlot() { + return spectraPlot; + } + + public Map getSpectraDataMap() { + return spectraDataMap; + } + + public Map> getSymbolMap() { + return spectraPlot.getSymbolMap(); + } + + public void setYAxisResize(final boolean shouldYAxisShrink, final double minY, final double maxY) { + if (isYaxisResizble) { + spectraPlot.setAutoCalculateYaxisRange(shouldYAxisShrink); + if (shouldYAxisShrink) { + spectraPlot.setAllYlimits(minY, maxY); + } else { + spectraPlot.setAllYlimits(); + } + } + } + + public void setYAxisResizable(final boolean isYaxisResizble) { + this.isYaxisResizble = isYaxisResizble; + } + + public Function getDataFunc() { + return dataFunction; + } + + public void setShowCornerFrequencies(final boolean showCornerFrequencies) { + this.spectraPlot.showCornerFrequency(showCornerFrequencies); + } + + public boolean shouldShowFits() { + return shouldShowFits; + } + + public void setShouldShowFits(final boolean shouldShowFits) { + this.shouldShowFits = shouldShowFits; + } +} 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 4e279490..e1d2a4f5 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 @@ -38,14 +38,15 @@ import com.google.common.eventbus.EventBus; import com.google.common.eventbus.Subscribe; +import gov.llnl.gnem.apps.coda.calibration.gui.plotting.LeafletMapController; import gov.llnl.gnem.apps.coda.calibration.gui.plotting.MapPlottingUtilities; +import gov.llnl.gnem.apps.coda.calibration.gui.plotting.CertLeafletMapController; 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.model.domain.Event; import gov.llnl.gnem.apps.coda.common.model.domain.Station; import gov.llnl.gnem.apps.coda.common.model.domain.Stream; @@ -117,7 +118,8 @@ public class DataController implements MapListeningController, RefreshableContro private ObservableList listData = FXCollections.synchronizedObservableList(FXCollections.observableArrayList()); - private GeoMap mapImpl; + private LeafletMapController cctMapImpl; + private CertLeafletMapController certMapImpl; private MapPlottingUtilities iconFactory; @@ -147,9 +149,10 @@ public class DataController implements MapListeningController, RefreshableContro private boolean isVisible = false; @Autowired - public DataController(WaveformClient client, GeoMap mapImpl, MapPlottingUtilities iconFactory, EventBus bus) { + public DataController(WaveformClient client, CertLeafletMapController certMapImpl, LeafletMapController cctMapImpl, MapPlottingUtilities iconFactory, EventBus bus) { this.client = client; - this.mapImpl = mapImpl; + this.cctMapImpl = cctMapImpl; + this.certMapImpl = certMapImpl; this.iconFactory = iconFactory; this.bus = bus; bus.register(this); @@ -317,10 +320,15 @@ public void refreshView() { private void refreshMap() { if (isVisible) { - mapImpl.clearIcons(); + cctMapImpl.clearIcons(); synchronized (listData) { - mapImpl.addIcons(iconFactory.genIconsFromWaveforms(eventSelectionCallback, stationSelectionCallback, listData)); + cctMapImpl.addIcons(iconFactory.genIconsFromWaveforms(eventSelectionCallback, stationSelectionCallback, listData)); } + certMapImpl.clearIcons(); + synchronized (listData) { + certMapImpl.addIcons(iconFactory.genIconsFromWaveforms(eventSelectionCallback, stationSelectionCallback, listData)); + } + } } @@ -333,7 +341,8 @@ private void requestData() { synchronized (listData) { listData.clear(); } - mapImpl.clearIcons(); + cctMapImpl.clearIcons(); + certMapImpl.clearIcons(); client.getUniqueEventStationMetadataForStacks().filter(Objects::nonNull).doOnComplete(() -> { tableView.sort(); refreshView(); 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 index 1b972131..2dc87fdf 100644 --- 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 @@ -148,11 +148,10 @@ public void addFilterToColumn(boolean slider, ToggleGroup toggleGroup, TableColu // Create a label that wraps the checkbox so that the column text // is displayed to the left of the checkbox - Label buttonWrapper = new Label(columnName); + Label buttonWrapper = new Label(); buttonWrapper.setGraphic(filterBtn); buttonWrapper.setContentDisplay(ContentDisplay.RIGHT); column.setGraphic(buttonWrapper); - column.setText(""); // Create handler for when active state changes ChangeListener updateActive = (options, oldValue, newValue) -> { diff --git a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/controllers/EventTabController.java b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/controllers/EventTabController.java new file mode 100644 index 00000000..a6d73896 --- /dev/null +++ b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/controllers/EventTabController.java @@ -0,0 +1,171 @@ +/* +* Copyright (c) 2023, Lawrence Livermore National Security, LLC. Produced at the Lawrence Livermore National Laboratory +* CODE-743439. +* All rights reserved. +* This file is part of CCT. For details, see https://github.com/LLNL/coda-calibration-tool. +* +* 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.File; +import java.lang.reflect.InvocationTargetException; +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import com.google.common.eventbus.EventBus; + +import gov.llnl.gnem.apps.coda.calibration.gui.data.client.api.EventClient; +import gov.llnl.gnem.apps.coda.calibration.gui.data.client.api.ParameterClient; +import gov.llnl.gnem.apps.coda.calibration.gui.data.client.api.SpectraClient; +import gov.llnl.gnem.apps.coda.calibration.gui.data.exporters.ParamExporter; +import gov.llnl.gnem.apps.coda.calibration.gui.plotting.MapPlottingUtilities; +import gov.llnl.gnem.apps.coda.calibration.model.domain.MeasuredMwDetails; +import gov.llnl.gnem.apps.coda.calibration.model.domain.SiteFrequencyBandParameters; +import gov.llnl.gnem.apps.coda.common.gui.data.client.api.WaveformClient; +import gov.llnl.gnem.apps.coda.common.gui.plotting.SymbolStyleMapFactory; +import gov.llnl.gnem.apps.coda.common.gui.util.SnapshotUtils; +import gov.llnl.gnem.apps.coda.common.mapping.api.GeoMap; +import gov.llnl.gnem.apps.coda.common.model.domain.Pair; +import javafx.application.Platform; +import javafx.fxml.FXML; +import javafx.scene.control.Tab; +import llnl.gnem.core.gui.plotting.api.PlotFactory; +import llnl.gnem.core.gui.plotting.api.Symbol; +import reactor.core.scheduler.Schedulers; + +@Component +public class EventTabController extends EventTableController { + + private static final String AVERAGE_LABEL = "Average"; + private static final String DISPLAY_NAME = "Event_Table"; + + /* @FXML + private StackPane sitePane;*/ + + @Autowired + private EventTabController(final SpectraClient spectraClient, final ParameterClient paramClient, final EventClient referenceEventClient, final WaveformClient waveformClient, + final SymbolStyleMapFactory styleFactory, final GeoMap map, final MapPlottingUtilities iconFactory, final ParamExporter paramExporter, final PlotFactory plotFactory, final EventBus bus) { + super(spectraClient, paramClient, referenceEventClient, waveformClient, styleFactory, plotFactory, bus); + } + + @Override + @FXML + public void initialize() { + super.initialize(); + } + + @Override + protected void runGuiUpdate(final Runnable runnable) throws InvocationTargetException, InterruptedException { + Platform.runLater(runnable); + } + + @Override + protected void reloadData() { + super.reloadData(); + List siteTerms = paramClient.getSiteSpecificFrequencyBandParameters().filter(Objects::nonNull).collectList().block(Duration.of(10l, ChronoUnit.SECONDS)); + + Set stationSet = new TreeSet<>(); + List symbols = new ArrayList<>(); + + double minSite = 1E2; + double maxSite = -1E2; + double minCenterFreq = 1E2; + double maxCenterFreq = -1E2; + + Map averageSymbols = new TreeMap<>(); + Map averageSiteTerms = new TreeMap<>(); + + siteTerms.sort((l, r) -> Double.compare(l.getLowFrequency(), r.getLowFrequency())); + for (SiteFrequencyBandParameters val : siteTerms) { + if (val != null && val.getStation() != null) { + final String key = val.getStation().getStationName(); + stationSet.add(key); + double centerFreq = centerFreq(val.getLowFrequency(), val.getHighFrequency()); + //Dyne-cm to nm for plot, in log + double site = val.getSiteTerm() - 7.0; + + if (site < minSite) { + minSite = site; + } + if (site > maxSite) { + maxSite = site; + } + if (centerFreq < minCenterFreq) { + minCenterFreq = centerFreq; + } + if (centerFreq > maxCenterFreq) { + maxCenterFreq = centerFreq; + } + + averageSiteTerms.compute(val.getLowFrequency(), (k, v) -> { + SiteFrequencyBandParameters avg; + if (v == null) { + avg = new SiteFrequencyBandParameters().mergeNonNullOrEmptyFields(val).setSiteTerm(site); + avg.getStation().setStationName(AVERAGE_LABEL); + } else { + avg = v; + } + avg.setSiteTerm((avg.getSiteTerm() + val.getSiteTerm()) / 2.0); + return avg; + }); + + } + } + + maxSite = maxSite + .1; + if (minSite > maxSite) { + minSite = maxSite - .1; + } else { + minSite = minSite - .1; + } + + maxCenterFreq = maxCenterFreq + .1; + if (minCenterFreq > maxCenterFreq) { + minCenterFreq = maxCenterFreq - .1; + } else { + minCenterFreq = minCenterFreq - 1.0; + } + + symbols.addAll(averageSymbols.values()); + siteTerms.addAll(averageSiteTerms.values()); + } + + @Override + protected List getEvents() { + return referenceEventClient.getMeasuredEventDetails() + .filter(ev -> ev.getEventId() != null) + .collect(Collectors.toList()) + .subscribeOn(Schedulers.boundedElastic()) + .block(Duration.ofSeconds(10l)); + } + + public Consumer getScreenshotFunction(Tab tab) { + return folder -> { + final String timestamp = SnapshotUtils.getTimestampWithLeadingSeparator(); + SnapshotUtils.writePng(folder, new Pair<>(getDisplayName(), tab.getContent()), timestamp); + }; + } + + protected String getDisplayName() { + return DISPLAY_NAME; + } +} diff --git a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/controllers/EventTableController.java b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/controllers/EventTableController.java new file mode 100644 index 00000000..f4b7f553 --- /dev/null +++ b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/controllers/EventTableController.java @@ -0,0 +1,717 @@ +/* +* Copyright (c) 2023, 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.lang.reflect.InvocationTargetException; +import java.text.NumberFormat; +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; + +import org.apache.commons.math3.stat.descriptive.SummaryStatistics; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; + +import com.google.common.eventbus.EventBus; +import com.google.common.eventbus.Subscribe; + +import gov.llnl.gnem.apps.coda.calibration.gui.data.client.api.EventClient; +import gov.llnl.gnem.apps.coda.calibration.gui.data.client.api.ParameterClient; +import gov.llnl.gnem.apps.coda.calibration.gui.data.client.api.SpectraClient; +import gov.llnl.gnem.apps.coda.calibration.model.domain.MeasuredMwDetails; +import gov.llnl.gnem.apps.coda.calibration.model.domain.SpectraMeasurement; +import gov.llnl.gnem.apps.coda.common.gui.data.client.api.WaveformClient; +import gov.llnl.gnem.apps.coda.common.gui.plotting.LabeledPlotPoint; +import gov.llnl.gnem.apps.coda.common.gui.plotting.PlotPoint; +import gov.llnl.gnem.apps.coda.common.gui.plotting.SymbolStyleMapFactory; +import gov.llnl.gnem.apps.coda.common.gui.util.CellBindingUtils; +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.model.domain.SharedFrequencyBandParameters; +import gov.llnl.gnem.apps.coda.common.model.domain.Waveform; +import gov.llnl.gnem.apps.coda.common.model.messaging.SpectraMeasurementChangeEvent; +import gov.llnl.gnem.apps.coda.common.model.messaging.WaveformChangeEvent; +import javafx.application.Platform; +import javafx.beans.binding.Bindings; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.ContextMenu; +import javafx.scene.control.Label; +import javafx.scene.control.MenuItem; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableColumn.CellDataFeatures; +import javafx.scene.control.TableView; +import javafx.scene.paint.Color; +import llnl.gnem.core.gui.plotting.api.ColorMaps; +import llnl.gnem.core.gui.plotting.api.Line; +import llnl.gnem.core.gui.plotting.api.LineStyles; +import llnl.gnem.core.gui.plotting.api.PlotFactory; +import llnl.gnem.core.gui.plotting.api.Symbol; +import llnl.gnem.core.gui.plotting.api.SymbolStyles; + +/** + * The EventTableController defines the event table used in both the CERT and + * CCT views. + * + */ +public class EventTableController implements RefreshableController { + + private static final Integer VALIDATION_Z_ORDER = 0; + + private final Logger log = LoggerFactory.getLogger(this.getClass()); + + @FXML + protected TableView eventTable; + + @FXML + protected TableColumn evidCol; + + @FXML + protected TableColumn dateCol; + + @FXML + protected TableColumn mwCol; + + @FXML + protected TableColumn obsEnergyCol; + + @FXML + protected TableColumn totalEnergyCol; + + @FXML + protected TableColumn totalEnergyUq1LowCol; + + @FXML + protected TableColumn totalEnergyUq1HighCol; + + @FXML + protected TableColumn totalEnergyUq2LowCol; + + @FXML + protected TableColumn totalEnergyUq2HighCol; + + @FXML + protected TableColumn totalEnergyMDACCol; + + @FXML + protected TableColumn totalEnergyMDACUq1LowCol; + + @FXML + protected TableColumn totalEnergyMDACUq1HighCol; + + @FXML + protected TableColumn totalEnergyMDACUq2LowCol; + + @FXML + protected TableColumn totalEnergyMDACUq2HighCol; + + @FXML + protected TableColumn energyRatioCol; + + @FXML + protected TableColumn energyStressCol; + + @FXML + protected TableColumn energyStressUq1LowCol; + + @FXML + protected TableColumn energyStressUq1HighCol; + + @FXML + protected TableColumn energyStressUq2LowCol; + + @FXML + protected TableColumn energyStressUq2HighCol; + + @FXML + protected TableColumn stressCol; + + @FXML + protected TableColumn measuredMwCol; + + @FXML + protected TableColumn measuredMeCol; + + @FXML + protected TableColumn measuredStressCol; + + @FXML + protected TableColumn valMwCol; + + @FXML + protected TableColumn valStressCol; + + @FXML + protected TableColumn mistfitCol; + + @FXML + protected TableColumn measuredCornerFreqCol; + + @FXML + protected TableColumn measuredMwUq1LowCol; + + @FXML + protected TableColumn measuredMwUq1HighCol; + + @FXML + protected TableColumn measuredMwUq2LowCol; + + @FXML + protected TableColumn measuredMwUq2HighCol; + + @FXML + protected TableColumn measuredMeUq1LowCol; + + @FXML + protected TableColumn measuredMeUq1HighCol; + + @FXML + protected TableColumn measuredMeUq2LowCol; + + @FXML + protected TableColumn measuredMeUq2HighCol; + + @FXML + protected TableColumn measuredStressUq1LowCol; + + @FXML + protected TableColumn measuredStressUq1HighCol; + + @FXML + protected TableColumn measuredStressUq2LowCol; + + @FXML + protected TableColumn measuredStressUq2HighCol; + + @FXML + protected TableColumn measuredCornerFreqUq1LowCol; + + @FXML + protected TableColumn measuredCornerFreqUq1HighCol; + + @FXML + protected TableColumn measuredCornerFreqUq2LowCol; + + @FXML + protected TableColumn measuredCornerFreqUq2HighCol; + + @FXML + protected TableColumn iterationsCol; + + @FXML + protected TableColumn dataCountCol; + + @FXML + protected TableColumn stationCountCol; + + @FXML + protected TableColumn bandCoverageCol; + + @FXML + protected TableColumn likelyPoorlyConstrainedCol; + + @FXML + protected TableColumn stationCol; + + protected List spectralMeasurements = new ArrayList<>(); + private final ObservableList evids = FXCollections.observableArrayList(); + + protected SpectraClient spectraClient; + protected ParameterClient paramClient; + protected EventClient referenceEventClient; + protected WaveformClient waveformClient; + + protected ObservableList mwParameters = FXCollections.observableArrayList(); + + private final ObservableList stationSymbols = FXCollections.observableArrayList(); + + private final SymbolStyleMapFactory symbolStyleMapFactory; + private Map symbolStyleMap = new HashMap<>(); + + protected ContextMenu menu; + + private final NumberFormat dfmt4 = NumberFormatFactory.fourDecimalOneLeadingZero(); + + protected List spectraControllers = new ArrayList<>(1); + + protected final PlotFactory plotFactory; + private final EventBus bus; + + @Value("${show-energy-uq-summary:false}") + private boolean showEnergyUQ = false; + + // TODO: Break this up into components so this isn't so incredibly huge. + protected EventTableController(final SpectraClient spectraClient, final ParameterClient paramClient, final EventClient referenceEventClient, final WaveformClient waveformClient, + final SymbolStyleMapFactory styleFactory, final PlotFactory plotFactory, final EventBus bus) { + this.spectraClient = spectraClient; + this.paramClient = paramClient; + this.referenceEventClient = referenceEventClient; + this.waveformClient = waveformClient; + this.symbolStyleMapFactory = styleFactory; + this.plotFactory = plotFactory; + this.bus = bus; + + this.bus.register(this); + } + + protected void runGuiUpdate(Runnable runnable) throws InvocationTargetException, InterruptedException { + } + + protected List getEvents() { + return null; + } + + public void initialize() { + + final Label label = new Label("\uE2C4"); + label.getStyleClass().add("material-icons-medium"); + label.setMaxHeight(16); + label.setMinWidth(16); + + CellBindingUtils.attachTextCellFactoriesString(evidCol, MeasuredMwDetails::getEventId); + evidCol.comparatorProperty().set(new MaybeNumericStringComparator()); + + CellBindingUtils.attachTextCellFactoriesString(dateCol, MeasuredMwDetails::getDatetime); + + CellBindingUtils.attachTextCellFactories(mwCol, MeasuredMwDetails::getRefMw, dfmt4); + CellBindingUtils.attachTextCellFactories(stressCol, MeasuredMwDetails::getRefApparentStressInMpa, dfmt4); + CellBindingUtils.attachTextCellFactories(valMwCol, MeasuredMwDetails::getValMw, dfmt4); + CellBindingUtils.attachTextCellFactories(valStressCol, MeasuredMwDetails::getValApparentStressInMpa, dfmt4); + + CellBindingUtils.attachTextCellFactories(measuredMwCol, MeasuredMwDetails::getMw, dfmt4); + CellBindingUtils.attachTextCellFactories(measuredMwUq1LowCol, MeasuredMwDetails::getMw1Min, dfmt4); + CellBindingUtils.attachTextCellFactories(measuredMwUq1HighCol, MeasuredMwDetails::getMw1Max, dfmt4); + CellBindingUtils.attachTextCellFactories(measuredMwUq2LowCol, MeasuredMwDetails::getMw2Min, dfmt4); + CellBindingUtils.attachTextCellFactories(measuredMwUq2HighCol, MeasuredMwDetails::getMw2Max, dfmt4); + CellBindingUtils.attachTextCellFactories(measuredMeCol, MeasuredMwDetails::getMe, dfmt4); + CellBindingUtils.attachTextCellFactories(measuredMeUq1LowCol, MeasuredMwDetails::getMw1Min, dfmt4); + CellBindingUtils.attachTextCellFactories(measuredMeUq1HighCol, MeasuredMwDetails::getMw1Max, dfmt4); + CellBindingUtils.attachTextCellFactories(measuredMeUq2LowCol, MeasuredMwDetails::getMw2Min, dfmt4); + CellBindingUtils.attachTextCellFactories(measuredMeUq2HighCol, MeasuredMwDetails::getMw2Max, dfmt4); + + CellBindingUtils.attachTextCellFactories(obsEnergyCol, MeasuredMwDetails::getObsEnergy, dfmt4); + CellBindingUtils.attachTextCellFactories(totalEnergyCol, MeasuredMwDetails::getTotalEnergy, dfmt4); + CellBindingUtils.attachTextCellFactories(totalEnergyUq1LowCol, MeasuredMwDetails::getLogTotalEnergy1Min, dfmt4); + CellBindingUtils.attachTextCellFactories(totalEnergyUq1HighCol, MeasuredMwDetails::getLogTotalEnergy1Max, dfmt4); + CellBindingUtils.attachTextCellFactories(totalEnergyUq2LowCol, MeasuredMwDetails::getLogTotalEnergy2Min, dfmt4); + CellBindingUtils.attachTextCellFactories(totalEnergyUq2HighCol, MeasuredMwDetails::getLogTotalEnergy2Max, dfmt4); + + CellBindingUtils.attachTextCellFactories(totalEnergyMDACCol, MeasuredMwDetails::getTotalEnergyMDAC, dfmt4); + CellBindingUtils.attachTextCellFactories(totalEnergyMDACUq1LowCol, MeasuredMwDetails::getLogTotalEnergyMDAC1Min, dfmt4); + CellBindingUtils.attachTextCellFactories(totalEnergyMDACUq1HighCol, MeasuredMwDetails::getLogTotalEnergyMDAC1Max, dfmt4); + CellBindingUtils.attachTextCellFactories(totalEnergyMDACUq2LowCol, MeasuredMwDetails::getLogTotalEnergyMDAC2Min, dfmt4); + CellBindingUtils.attachTextCellFactories(totalEnergyMDACUq2HighCol, MeasuredMwDetails::getLogTotalEnergyMDAC2Max, dfmt4); + + CellBindingUtils.attachTextCellFactories(energyRatioCol, MeasuredMwDetails::getEnergyRatio, dfmt4); + CellBindingUtils.attachTextCellFactories(energyStressCol, MeasuredMwDetails::getEnergyStress, dfmt4); + CellBindingUtils.attachTextCellFactories(energyStressUq1LowCol, MeasuredMwDetails::getObsAppStress1Min, dfmt4); + CellBindingUtils.attachTextCellFactories(energyStressUq1HighCol, MeasuredMwDetails::getObsAppStress1Max, dfmt4); + CellBindingUtils.attachTextCellFactories(energyStressUq2LowCol, MeasuredMwDetails::getObsAppStress2Min, dfmt4); + CellBindingUtils.attachTextCellFactories(energyStressUq2HighCol, MeasuredMwDetails::getObsAppStress2Max, dfmt4); + + CellBindingUtils.attachTextCellFactories(measuredStressCol, MeasuredMwDetails::getApparentStressInMpa, dfmt4); + CellBindingUtils.attachTextCellFactories(measuredStressUq1LowCol, MeasuredMwDetails::getApparentStress1Min, dfmt4); + CellBindingUtils.attachTextCellFactories(measuredStressUq1HighCol, MeasuredMwDetails::getApparentStress1Max, dfmt4); + CellBindingUtils.attachTextCellFactories(measuredStressUq2LowCol, MeasuredMwDetails::getApparentStress2Min, dfmt4); + CellBindingUtils.attachTextCellFactories(measuredStressUq2HighCol, MeasuredMwDetails::getApparentStress2Max, dfmt4); + + CellBindingUtils.attachTextCellFactories(measuredCornerFreqCol, MeasuredMwDetails::getCornerFreq, dfmt4); + CellBindingUtils.attachTextCellFactories(measuredCornerFreqUq1LowCol, MeasuredMwDetails::getCornerFreq1Min, dfmt4); + CellBindingUtils.attachTextCellFactories(measuredCornerFreqUq1HighCol, MeasuredMwDetails::getCornerFreq1Max, dfmt4); + CellBindingUtils.attachTextCellFactories(measuredCornerFreqUq2LowCol, MeasuredMwDetails::getCornerFreq2Min, dfmt4); + CellBindingUtils.attachTextCellFactories(measuredCornerFreqUq2HighCol, MeasuredMwDetails::getCornerFreq2Max, dfmt4); + + CellBindingUtils.attachTextCellFactories(mistfitCol, MeasuredMwDetails::getMisfit, dfmt4); + CellBindingUtils.attachTextCellFactories(bandCoverageCol, MeasuredMwDetails::getBandCoverage, dfmt4); + CellBindingUtils.attachTextCellFactoriesString(likelyPoorlyConstrainedCol, mw -> mw.isLikelyPoorlyConstrained().toString()); + + stationCountCol.setCellValueFactory( + x -> Bindings.createIntegerBinding(() -> Optional.ofNullable(x).map(CellDataFeatures::getValue).map(MeasuredMwDetails::getStationCount).orElseGet(() -> 0)).asObject()); + + iterationsCol.setCellValueFactory( + x -> Bindings.createIntegerBinding(() -> Optional.ofNullable(x).map(CellDataFeatures::getValue).map(MeasuredMwDetails::getIterations).orElseGet(() -> 0)).asObject()); + + dataCountCol.setCellValueFactory( + x -> Bindings.createIntegerBinding(() -> Optional.ofNullable(x).map(CellDataFeatures::getValue).map(MeasuredMwDetails::getDataCount).orElseGet(() -> 0)).asObject()); + + eventTable.setItems(mwParameters); + + menu = new ContextMenu(); + MenuItem include = new MenuItem("Include Selected"); + menu.getItems().add(include); + MenuItem exclude = new MenuItem("Exclude Selected"); + menu.getItems().add(exclude); + } + + protected double centerFreq(final Double lowFrequency, final Double highFrequency) { + return lowFrequency + (highFrequency - lowFrequency) / 2.; + } + + @Override + public Runnable getRefreshFunction() { + return this::reloadData; + } + + protected void reloadData() { + try { + final List results = paramClient.getSharedFrequencyBandParameters().filter(Objects::nonNull).collectList().block(Duration.of(10l, ChronoUnit.SECONDS)); + + if (results != null) { + preloadData(); + + runGuiUpdate(() -> { + mwParameters.clear(); + + final List evs = getEvents(); + final List mwPlotSymbols = new ArrayList<>(); + final List stressPlotSymbols = new ArrayList<>(); + + double minMw = 10.0; + double maxMw = 0.0; + double minEnergy = -7; + double maxEnergy = -1; + double minStress = 0.01; + double maxStress = 100.0; + for (final MeasuredMwDetails ev : evs) { + mwParameters.add(ev); + + if (ev.getMw() != null && ev.getMw() != 0.0) { + final Double mw = ev.getMw(); + if (mw < minMw) { + minMw = mw; + } + if (mw > maxMw) { + maxMw = mw; + } + + double m0 = (1.5 * mw) + 9.1; + + if (ev.getTotalEnergy() != null && ev.getTotalEnergy() != 0.0) { + final double energy = ev.getTotalEnergy() - m0; + if (energy < minEnergy) { + minEnergy = energy; + } + if (energy > maxEnergy) { + maxEnergy = energy; + } + Symbol symbol = plotFactory.createSymbol(SymbolStyles.CIRCLE, "Data", m0, energy, Color.BLACK, Color.BLACK, Color.BLACK, ev.getEventId(), false); + symbol.showInLegend(false); + } + + if (ev.getRefMw() != null && ev.getRefMw() != 0.0) { + final Double ref = ev.getRefMw(); + if (ref < minMw) { + minMw = ref; + } + if (ref > maxMw) { + maxMw = ref; + } + + final Symbol mwSym = plotFactory.createSymbol(SymbolStyles.CIRCLE, "Ref.", mw, ref, Color.RED, Color.RED, Color.RED, ev.getEventId(), false); + mwPlotSymbols.add(mwSym); + } + + final Double valMw = ev.getValMw(); + if (valMw != null && valMw != 0.0) { + if (valMw < minMw) { + minMw = valMw; + } + if (valMw > maxMw) { + maxMw = valMw; + } + + final Symbol valSym = plotFactory.createSymbol(SymbolStyles.SQUARE, "Val.", mw, valMw, Color.BLACK, Color.BLACK, Color.BLACK, ev.getEventId(), false); + valSym.setZindex(VALIDATION_Z_ORDER); + mwPlotSymbols.add(valSym); + } + + final Double stress = ev.getApparentStressInMpa(); + Double refStress = ev.getRefApparentStressInMpa(); + + if (stress != null) { + if (stress < minStress) { + minStress = stress; + } + if (stress > maxStress) { + maxStress = stress; + } + + if (refStress == null) { + refStress = 0.0; + } + + if (refStress != null && refStress != 0.0) { + if (refStress < minStress) { + minStress = refStress; + } + if (refStress > maxStress) { + maxStress = refStress; + } + + final Symbol stressSym = plotFactory.createSymbol(SymbolStyles.CIRCLE, "Ref.", stress, refStress, Color.RED, Color.RED, Color.RED, ev.getEventId(), false); + stressPlotSymbols.add(stressSym); + } + + final Double valStress = ev.getValApparentStressInMpa(); + if (valStress != null && valStress != 0.0) { + if (valStress < minStress) { + minStress = valStress; + } + if (valStress > maxStress) { + maxStress = valStress; + } + + final Symbol valSym = plotFactory.createSymbol(SymbolStyles.SQUARE, "Val.", stress, valStress, Color.BLACK, Color.BLACK, Color.BLACK, ev.getEventId(), false); + valSym.setZindex(VALIDATION_Z_ORDER); + stressPlotSymbols.add(valSym); + } + } + } + } + + maxMw = maxMw + Math.abs(maxMw * .1); + if (minMw > maxMw) { + minMw = maxMw - .1; + } else { + minMw = minMw - Math.abs(minMw * .1); + } + + maxEnergy = maxEnergy + Math.abs(maxEnergy * .1); + if (minEnergy > maxEnergy) { + minEnergy = maxEnergy - .1; + } else { + minEnergy = minEnergy - Math.abs(minEnergy * .1); + } + + maxStress = maxStress + Math.abs(maxStress * .1); + if (minStress > maxStress) { + minStress = maxStress - .1; + } else { + minStress = minStress - Math.abs(minStress * .1); + } + + final double[] xy = new double[2]; + xy[0] = minMw; + xy[1] = maxMw; + final Line mwZeroLine = plotFactory.line(xy, xy, Color.LIGHTGRAY, LineStyles.DASH, 2); + mwZeroLine.setName(""); + mwZeroLine.showInLegend(false); + + xy[0] = minStress; + xy[1] = maxStress; + final Line stressZeroLine = plotFactory.line(xy, xy, Color.LIGHTGRAY, LineStyles.DASH, 2); + stressZeroLine.setName(""); + stressZeroLine.showInLegend(false); + }); + + //Wastes a little compute but KISS + Map styleMap = symbolStyleMapFactory.build(spectralMeasurements, specMeas -> specMeas.getWaveform().getStream().getStation().getStationName()); + styleMap.entrySet().forEach(e -> symbolStyleMap.putIfAbsent(e.getKey(), e.getValue())); + + runGuiUpdate(() -> { + stationSymbols.clear(); + stationSymbols.addAll(symbolStyleMap.entrySet().stream().map(e -> new LabeledPlotPoint(e.getKey(), e.getValue())).collect(Collectors.toList())); + + Platform.runLater(() -> { + evids.clear(); + evids.add("All"); + evids.addAll( + spectralMeasurements.stream() + .map(spec -> spec.getWaveform().getEvent().getEventId()) + .distinct() + .sorted(new MaybeNumericStringComparator()) + .collect(Collectors.toList())); + eventTable.sort(); + }); + + final Map> evidStats = new HashMap<>(); + + double minSite = 1E2; + double maxSite = -1E2; + double minCenterFreq = 1E2; + double maxCenterFreq = -1E2; + + for (final SpectraMeasurement meas : spectralMeasurements) { + final String evid = meas.getWaveform().getEvent().getEventId(); + final Double freq = centerFreq(meas.getWaveform()); + evidStats.computeIfAbsent(evid, key -> new HashMap<>()).computeIfAbsent(freq, key -> new SummaryStatistics()).addValue(meas.getPathAndSiteCorrected()); + } + + for (final Map freqStats : evidStats.values()) { + for (final Entry entry : freqStats.entrySet()) { + final double site = entry.getValue().getStandardDeviation(); + if (entry.getValue() != null && entry.getValue().getN() > 1) { + if (site < minSite) { + minSite = site; + } + if (site > maxSite) { + maxSite = site; + } + if (entry.getKey() < minCenterFreq) { + minCenterFreq = entry.getKey(); + } + if (entry.getKey() > maxCenterFreq) { + maxCenterFreq = entry.getKey(); + } + } + } + } + + for (final Map freqStats : evidStats.values()) { + for (final Entry entry : freqStats.entrySet()) { + final double site = entry.getValue().getStandardDeviation(); + if (entry.getValue() != null && entry.getValue().getN() > 1) { + final Symbol sdSym = plotFactory.createSymbol( + SymbolStyles.CIRCLE, + Long.toString(entry.getValue().getN()), + entry.getKey(), + site, + null, + null, + null, + Long.toString(entry.getValue().getN()), + false); + sdSym.setColorationValue((double) entry.getValue().getN()); + sdSym.setColorMap(ColorMaps.VIRIDIS.getColorMap()); + } + } + } + + maxSite = maxSite + .1; + if (minSite > maxSite) { + minSite = maxSite - .1; + } else { + minSite = Math.max(0.0, minSite - .1); + } + + maxCenterFreq = maxCenterFreq + .1; + if (minCenterFreq > maxCenterFreq) { + minCenterFreq = maxCenterFreq - .1; + } else { + minCenterFreq = minCenterFreq - 1.0; + } + }); + } + } catch (final InvocationTargetException ex) { + // nop + } catch (final InterruptedException ex) { + log.warn("Interrupt during re-plotting of controller", ex); + Thread.currentThread().interrupt(); + } + } + + protected void preloadData() { + // Placeholder to allow children to overload any pre-fetching needed before data + // calls + } + + private Double centerFreq(final Waveform waveform) { + return ((waveform.getHighFrequency() - waveform.getLowFrequency()) / 2.0) + waveform.getLowFrequency(); + } + + @Subscribe + private void listener(final WaveformChangeEvent wce) { + final List nonNull = wce.getIds().stream().filter(Objects::nonNull).collect(Collectors.toList()); + synchronized (spectralMeasurements) { + final Map activeMeasurements = spectralMeasurements.stream().collect(Collectors.toMap(x -> x.getWaveform().getId(), Function.identity())); + if (wce.isAddOrUpdate()) { + final List results = waveformClient.getWaveformMetadataFromIds(nonNull).collect(Collectors.toList()).block(Duration.ofSeconds(10l)); + if (results != null) { + results.forEach(md -> { + final SpectraMeasurement measurement = activeMeasurements.get(md.getId()); + if (measurement != null) { + measurement.getWaveform().setActive(md.isActive()); + } + }); + } + } else if (wce.isDelete()) { + nonNull.forEach(id -> { + final SpectraMeasurement measurement = activeMeasurements.remove(id); + if (measurement != null) { + spectralMeasurements.remove(measurement); + } + }); + } + } + } + + @Subscribe + private void listener(final SpectraMeasurementChangeEvent changeEvent) { + final List nonNull = changeEvent.getIds().stream().filter(Objects::nonNull).collect(Collectors.toList()); + synchronized (spectralMeasurements) { + final Map activeMeasurements = spectralMeasurements.stream().collect(Collectors.toMap(x -> x.getWaveform().getId(), Function.identity())); + final List results = spectraClient.getMeasuredSpectraMetadataByIds(nonNull).collect(Collectors.toList()).block(Duration.ofSeconds(10l)); + if (results != null) { + if (changeEvent.isAddOrUpdate()) { + results.forEach(md -> { + final SpectraMeasurement measurement = activeMeasurements.get(md.getWaveform().getId()); + if (measurement != null) { + measurement.setPathAndSiteCorrected(md.getPathAndSiteCorrected()); + measurement.setRawAtMeasurementTime(md.getRawAtMeasurementTime()); + measurement.setRawAtStart(md.getRawAtStart()); + } + }); + } else if (changeEvent.isDelete()) { + results.forEach(md -> { + final SpectraMeasurement measurement = activeMeasurements.remove(md.getWaveform().getId()); + if (measurement != null) { + spectralMeasurements.remove(measurement); + } + }); + } + } + } + } + + @FXML + private void clearRefEvents() { + removeRefEvents(mwParameters); + } + + @FXML + private void removeRefEvents() { + final List evs = new ArrayList<>(eventTable.getSelectionModel().getSelectedIndices().size()); + eventTable.getSelectionModel().getSelectedIndices().forEach(i -> evs.add(mwParameters.get(i))); + removeRefEvents(evs); + } + + @FXML + private void toggleValidationEvent() { + final List evs = new ArrayList<>(eventTable.getSelectionModel().getSelectedIndices().size()); + eventTable.getSelectionModel().getSelectedIndices().forEach(i -> evs.add(mwParameters.get(i))); + if (!evs.isEmpty()) { + referenceEventClient.toggleValidationEventsByEventId(evs.stream().map(MeasuredMwDetails::getEventId).distinct().collect(Collectors.toList())) + .doOnComplete(() -> Platform.runLater(this::reloadData)) + .subscribe(); + } + } + + private void removeRefEvents(final List evs) { + if (evs != null && !evs.isEmpty()) { + referenceEventClient.removeReferenceEventsByEventId(evs.stream().map(MeasuredMwDetails::getEventId).distinct().collect(Collectors.toList())) + .doOnSuccess(v -> Platform.runLater(this::reloadData)) + .subscribe(); + } + } +} diff --git a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/controllers/MeasuredMwsController.java b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/controllers/MeasuredMwsController.java index 1441915a..9740e1f9 100644 --- a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/controllers/MeasuredMwsController.java +++ b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/controllers/MeasuredMwsController.java @@ -64,7 +64,6 @@ import javafx.fxml.FXML; import javafx.geometry.Point2D; import javafx.scene.layout.StackPane; -import javafx.stage.Modality; import llnl.gnem.core.gui.plotting.api.Axis; import llnl.gnem.core.gui.plotting.api.Axis.TickFormat; import llnl.gnem.core.gui.plotting.api.PlotFactory; @@ -85,6 +84,7 @@ public class MeasuredMwsController extends AbstractMeasurementController { private final Map> fitSpectra = new HashMap<>(); private ProgressGui progressGui; + private ProgressMonitor pm; private final ThreadPoolExecutor exec = new ThreadPoolExecutor(1, 1, 0, TimeUnit.SECONDS, new SynchronousQueue<>(), r -> { final Thread thread = new Thread(r); @@ -118,26 +118,16 @@ public void initialize() { spectraPlotPanel = measuredMws; super.initialize(); - final ProgressMonitor pm = new ProgressMonitor("Measuring Mws", new ProgressListener() { - @Override - public double getProgress() { - return -1d; - } - }); - progressGui = new ProgressGui(); - progressGui.addProgressMonitor(pm); - - progressGui.initModality(Modality.NONE); - progressGui.setAlwaysOnTop(true); + progressGui = ProgressGui.getInstance(); final SpectraPlotController spectra = new SpectraPlotController(SpectraMeasurement::getPathAndSiteCorrected); final SpectralPlot plot = spectra.getSpectralPlot(); - plot.getSubplot().addPlotObjectObserver(getPlotpointObserver(spectra::getSpectraMeasurementMap)); + plot.getSubplot().addPlotObjectObserver(getPlotpointObserver(spectra::getSpectraDataMap)); plot.setLabels("Moment Rate Spectra", X_AXIS_LABEL, "log10(N-m)"); plot.getSubplot().setMargin(65, 40, 50, null); final Axis rightAxis = new BasicAxis(Axis.Type.Y_RIGHT, "Mw"); rightAxis.setTickFormat(TickFormat.LOG10_DYNE_CM_TO_MW); - + plot.getSubplot().addAxes(rightAxis); spectra.setShowCornerFrequencies(true); spectra.setYAxisResizable(true); @@ -168,10 +158,18 @@ protected void preloadData() { spectralMeasurements.clear(); fitSpectra.clear(); mwDetails.clear(); - mfs = calibrationClient.makeMwMeasurements(Boolean.TRUE) - .doOnError(err -> log.trace(err.getMessage(), err)) - .doFinally(s -> Platform.runLater(() -> progressGui.hide())) - .block(Duration.of(1000l, ChronoUnit.SECONDS)); + + this.pm = new ProgressMonitor("Measuring Mws", new ProgressListener() { + @Override + public double getProgress() { + return -1d; + } + }); + progressGui.addProgressMonitor(this.pm); + mfs = calibrationClient.makeMwMeasurements(Boolean.TRUE).doOnError(err -> log.trace(err.getMessage(), err)).doFinally(s -> Platform.runLater(() -> { + pm.getProgressBar().setProgress(100.0); + pm.clearCancelCallbacks(); + })).block(Duration.of(1000l, ChronoUnit.SECONDS)); if (mfs != null) { fitSpectra.putAll(mfs.getFitSpectra()); diff --git a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/controllers/PathController.java b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/controllers/PathController.java index 3fe51f9e..078e5fee 100644 --- a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/controllers/PathController.java +++ b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/controllers/PathController.java @@ -475,7 +475,7 @@ public Consumer getScreenshotFunction() { private void plotPaths() { mapImpl.clearIcons(); - if (!measurementsFreqBandMap.isEmpty()) { + if (!measurementsFreqBandMap.isEmpty() && (frequencyBandComboBox.getSelectionModel() != null && frequencyBandComboBox.getSelectionModel().getSelectedItem() != null)) { final List measurements = measurementsFreqBandMap.get(frequencyBandComboBox.getSelectionModel().getSelectedItem()); if (measurements != null) { final Map> stationToEvents = measurements.parallelStream() @@ -514,7 +514,7 @@ private void plotPaths() { } private void plotSd() { - if (!measurementsFreqBandMap.isEmpty()) { + if (frequencyBandComboBox.getSelectionModel() != null && frequencyBandComboBox.getSelectionModel().getSelectedItem() != null && !measurementsFreqBandMap.isEmpty()) { sdSymbolMap.clear(); sdPlot.clear(); Double xmin = null; diff --git a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/controllers/SiteController.java b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/controllers/SiteController.java index e5787fd4..0c2ddd06 100644 --- a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/controllers/SiteController.java +++ b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/controllers/SiteController.java @@ -14,7 +14,11 @@ */ package gov.llnl.gnem.apps.coda.calibration.gui.controllers; +import java.io.File; +import java.io.IOException; import java.lang.reflect.InvocationTargetException; +import java.nio.file.Files; +import java.nio.file.Paths; import java.time.Duration; import java.time.temporal.ChronoUnit; import java.util.ArrayList; @@ -25,8 +29,11 @@ import java.util.TreeMap; import java.util.TreeSet; import java.util.function.BiConsumer; +import java.util.function.Consumer; import java.util.stream.Collectors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -46,7 +53,9 @@ import gov.llnl.gnem.apps.coda.common.gui.plotting.LabeledPlotPoint; import gov.llnl.gnem.apps.coda.common.gui.plotting.PlotPoint; import gov.llnl.gnem.apps.coda.common.gui.plotting.SymbolStyleMapFactory; +import gov.llnl.gnem.apps.coda.common.gui.util.SnapshotUtils; import gov.llnl.gnem.apps.coda.common.mapping.api.GeoMap; +import gov.llnl.gnem.apps.coda.common.model.domain.Pair; import gov.llnl.gnem.apps.coda.common.model.domain.Waveform; import javafx.application.Platform; import javafx.collections.FXCollections; @@ -55,6 +64,7 @@ import javafx.geometry.Point2D; import javafx.scene.control.ComboBox; import javafx.scene.control.SplitPane; +import javafx.scene.control.Tab; import javafx.scene.control.TitledPane; import javafx.scene.layout.StackPane; import javafx.scene.paint.Color; @@ -70,12 +80,17 @@ @Component public class SiteController extends AbstractMeasurementController { + private final Logger log = LoggerFactory.getLogger(this.getClass()); + private static final String AVERAGE_LABEL = "Average"; private static final String X_AXIS_LABEL = "center freq (Hz)"; private static final String DISPLAY_NAME = "Site"; + @FXML + private Tab siteTermsTab; + @FXML private StackPane sitePane; @@ -120,14 +135,14 @@ public void initialize() { final SpectraPlotController raw = new SpectraPlotController(SpectraMeasurement::getRawAtMeasurementTime); SpectralPlot plot = raw.getSpectralPlot(); - plot.getSubplot().addPlotObjectObserver(getPlotpointObserver(raw::getSpectraMeasurementMap)); + plot.getSubplot().addPlotObjectObserver(getPlotpointObserver(raw::getSpectraDataMap)); plot.setLabels("Raw Plot", X_AXIS_LABEL, "log10(non-dim)"); plot.setAutoCalculateYaxisRange(true); rawPlotNode.getChildren().add(plot); final SpectraPlotController site = new SpectraPlotController(SpectraMeasurement::getPathAndSiteCorrected); plot = site.getSpectralPlot(); - plot.getSubplot().addPlotObjectObserver(getPlotpointObserver(site::getSpectraMeasurementMap)); + plot.getSubplot().addPlotObjectObserver(getPlotpointObserver(site::getSpectraDataMap)); plot.setLabels("Moment Rate Spectra", X_AXIS_LABEL, "log10(N-m)"); plot.getSubplot().setMargin(60, 40, 50, null); final Axis rightAxis = new BasicAxis(Axis.Type.Y_RIGHT, "Mw"); @@ -411,4 +426,25 @@ protected List getEvents() { .subscribeOn(Schedulers.boundedElastic()) .block(Duration.ofSeconds(10l)); } + + @Override + public Consumer getScreenshotFunction() { + return folder -> { + if (siteTermsTab.isSelected() && siteTermStationCombo.getValue() != null) { + final String timestamp = SnapshotUtils.getTimestampWithLeadingSeparator(); + SnapshotUtils.writePng(folder, new Pair<>(getDisplayName(), siteTermsTab.getContent()), timestamp); + try { + Files.write(Paths.get(folder + File.separator + getDisplayName() + "_Site-Transfer_" + siteTermStationCombo.getValue() + timestamp + ".svg"), siteTermsPlot.getSVG().getBytes()); + + Files.write( + Paths.get(folder + File.separator + getDisplayName() + "_Relative-Site_" + siteTermStationCombo.getValue() + timestamp + ".svg"), + relativeSiteTermsPlot.getSVG().getBytes()); + } catch (final IOException e) { + log.error("Error attempting to write plots for controller : {}", e.getLocalizedMessage(), e); + } + } else { + super.getScreenshotFunction().accept(folder); + } + }; + } } diff --git a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/controllers/SpectraPlotController.java b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/controllers/SpectraPlotController.java index b54a5370..5b6e7f57 100644 --- a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/controllers/SpectraPlotController.java +++ b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/controllers/SpectraPlotController.java @@ -13,71 +13,17 @@ */ package gov.llnl.gnem.apps.coda.calibration.gui.controllers; -import java.util.HashMap; -import java.util.List; -import java.util.Map; import java.util.function.Function; -import gov.llnl.gnem.apps.coda.calibration.gui.plotting.SpectralPlot; import gov.llnl.gnem.apps.coda.calibration.model.domain.SpectraMeasurement; -import javafx.geometry.Point2D; -import llnl.gnem.core.gui.plotting.api.Symbol; -public class SpectraPlotController { - private final SpectralPlot spectraPlot = new SpectralPlot(); - private boolean isYaxisResizble = false; - private final Function dataFunction; - private boolean shouldShowFits = false; - private final Map spectraMeasurementMap = new HashMap<>(); +public class SpectraPlotController extends BaseSpectraPlotController { public SpectraPlotController(final Function dataFunction) { - this.dataFunction = dataFunction; - } - - public SpectralPlot getSpectralPlot() { - return spectraPlot; - } - - public Map getSpectraMeasurementMap() { - return spectraMeasurementMap; - } - - public Map> getSymbolMap() { - return spectraPlot.getSymbolMap(); - } - - public void setYAxisResize(final boolean shouldYAxisShrink, final double minY, final double maxY) { - if (isYaxisResizble) { - spectraPlot.setAutoCalculateYaxisRange(shouldYAxisShrink); - if (shouldYAxisShrink) { - spectraPlot.setAllYlimits(minY, maxY); - } else { - spectraPlot.setAllYlimits(); - } - } - } - - public void setYAxisResizable(final boolean isYaxisResizble) { - this.isYaxisResizble = isYaxisResizble; - } - - public Function getDataFunc() { - return dataFunction; - } - - public void setShowCornerFrequencies(final boolean showCornerFrequencies) { - this.spectraPlot.showCornerFrequency(showCornerFrequencies); - } - - public boolean shouldShowFits() { - return shouldShowFits; - } - - public void setShouldShowFits(final boolean shouldShowFits) { - this.shouldShowFits = shouldShowFits; + super(dataFunction); } public void showConstraintWarningBanner(boolean visible) { - spectraPlot.showConstraintWarningBanner(visible); + this.getSpectralPlot().showConstraintWarningBanner(visible); } } diff --git a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/controllers/SpectraRatioPlotController.java b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/controllers/SpectraRatioPlotController.java new file mode 100644 index 00000000..d04018e6 --- /dev/null +++ b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/controllers/SpectraRatioPlotController.java @@ -0,0 +1,25 @@ +/* +* 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.util.function.Function; + +import gov.llnl.gnem.apps.coda.spectra.model.domain.util.SpectraRatioPairOperator; + +public class SpectraRatioPlotController extends BaseSpectraPlotController { + + public SpectraRatioPlotController(final Function dataFunction) { + super(dataFunction); + } +} diff --git a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/data/client/EventWebClient.java b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/data/client/EventWebClient.java index c266c9a2..9cb46358 100644 --- a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/data/client/EventWebClient.java +++ b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/data/client/EventWebClient.java @@ -1,145 +1,122 @@ -/* -* Copyright (c) 2021, Lawrence Livermore National Security, LLC. Produced at the Lawrence Livermore National Laboratory -* CODE-743439. -* All rights reserved. -* This file is part of CCT. For details, see https://github.com/LLNL/coda-calibration-tool. -* -* 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.data.client; - -import java.util.List; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.MediaType; -import org.springframework.stereotype.Component; -import org.springframework.web.reactive.function.client.WebClient; - -import com.fasterxml.jackson.core.JsonProcessingException; - -import gov.llnl.gnem.apps.coda.calibration.gui.data.client.api.EventClient; -import gov.llnl.gnem.apps.coda.calibration.model.domain.MeasuredMwDetails; -import gov.llnl.gnem.apps.coda.calibration.model.domain.MeasuredMwParameters; -import gov.llnl.gnem.apps.coda.calibration.model.domain.ReferenceMwParameters; -import gov.llnl.gnem.apps.coda.calibration.model.domain.ValidationMwParameters; -import gov.llnl.gnem.apps.coda.common.model.domain.Event; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -@Component -public class EventWebClient implements EventClient { - - private WebClient client; - - @Autowired - public EventWebClient(WebClient client) { - this.client = client; - } - - @Override - public Flux getReferenceEvents() { - return client.get() - .uri("/reference-events") - .accept(MediaType.APPLICATION_JSON) - .retrieve() - .bodyToFlux(ReferenceMwParameters.class) - .onErrorReturn(new ReferenceMwParameters()); - } - - @Override - public Mono postReferenceEvents(List refEvents) throws JsonProcessingException { - return client.post() - .uri("/reference-events/batch") - .contentType(MediaType.APPLICATION_JSON) - .accept(MediaType.APPLICATION_JSON) - .bodyValue(refEvents) - .retrieve() - .bodyToMono(String.class); - } - - @Override - public Flux getMeasuredEvents() { - return client.get() - .uri("/measured-mws") - .accept(MediaType.APPLICATION_JSON) - .retrieve() - .bodyToFlux(MeasuredMwParameters.class) - .onErrorReturn(new MeasuredMwParameters()); - } - - @Override - public Mono getEvent(String eventId) { - return client.get().uri("/events/" + eventId).accept(MediaType.APPLICATION_JSON).retrieve().bodyToMono(Event.class).onErrorReturn(new Event()); - } - - @Override - public Flux getMeasuredEventDetails() { - return client.get() - .uri("/measured-mws/details") - .accept(MediaType.APPLICATION_JSON) - .retrieve() - .bodyToFlux(MeasuredMwDetails.class) - .onErrorReturn(new MeasuredMwDetails()); - } - - @Override - public Mono removeReferenceEventsByEventId(List evids) { - return client.post() - .uri("/reference-events/delete/batch-by-evids") - .contentType(MediaType.APPLICATION_JSON) - .accept(MediaType.APPLICATION_JSON) - .bodyValue(evids) - .retrieve() - .toBodilessEntity().flatMap(resp -> Mono.empty()); - } - - @Override - public Flux getValidationEvents() { - return client.get() - .uri("/validation-events") - .accept(MediaType.APPLICATION_JSON) - .retrieve() - .bodyToFlux(ValidationMwParameters.class) - .onErrorReturn(new ValidationMwParameters()); - } - - @Override - public Mono postValidationEvents(List events) throws JsonProcessingException { - return client.post() - .uri("/validation-events/batch") - .contentType(MediaType.APPLICATION_JSON) - .accept(MediaType.APPLICATION_JSON) - .bodyValue(events) - .retrieve() - .bodyToMono(String.class); - } - - @Override - public Mono removeValidationEventsByEventId(List evids) { - return client.post() - .uri("/validation-events/delete/batch-by-evids") - .contentType(MediaType.APPLICATION_JSON) - .accept(MediaType.APPLICATION_JSON) - .bodyValue(evids) - .retrieve() - .toBodilessEntity().flatMap(resp -> Mono.empty()); - } - - @Override - public Flux toggleValidationEventsByEventId(List evids) { - return client.post() - .uri("/calibration/toggle-validation/batch-by-evids") - .contentType(MediaType.APPLICATION_JSON) - .accept(MediaType.APPLICATION_JSON) - .bodyValue(evids) - .retrieve() - .bodyToFlux(String.class); - } - -} +/* +* Copyright (c) 2021, Lawrence Livermore National Security, LLC. Produced at the Lawrence Livermore National Laboratory +* CODE-743439. +* All rights reserved. +* This file is part of CCT. For details, see https://github.com/LLNL/coda-calibration-tool. +* +* 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.data.client; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; + +import com.fasterxml.jackson.core.JsonProcessingException; + +import gov.llnl.gnem.apps.coda.calibration.gui.data.client.api.EventClient; +import gov.llnl.gnem.apps.coda.calibration.model.domain.MeasuredMwDetails; +import gov.llnl.gnem.apps.coda.calibration.model.domain.MeasuredMwParameters; +import gov.llnl.gnem.apps.coda.calibration.model.domain.ReferenceMwParameters; +import gov.llnl.gnem.apps.coda.calibration.model.domain.ValidationMwParameters; +import gov.llnl.gnem.apps.coda.common.model.domain.Event; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +@Component +public class EventWebClient implements EventClient { + + private WebClient client; + + @Autowired + public EventWebClient(WebClient client) { + this.client = client; + } + + @Override + public Flux getReferenceEvents() { + return client.get().uri("/reference-events").accept(MediaType.APPLICATION_JSON).retrieve().bodyToFlux(ReferenceMwParameters.class).onErrorReturn(new ReferenceMwParameters()); + } + + @Override + public Mono postReferenceEvents(List refEvents) throws JsonProcessingException { + return client.post().uri("/reference-events/batch").contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON).bodyValue(refEvents).retrieve().bodyToMono(String.class); + } + + @Override + public Flux getMeasuredEvents() { + return client.get().uri("/measured-mws").accept(MediaType.APPLICATION_JSON).retrieve().bodyToFlux(MeasuredMwParameters.class).onErrorReturn(new MeasuredMwParameters()); + } + + @Override + public Mono getEvent(String eventId) { + return client.get().uri("/events/" + eventId).accept(MediaType.APPLICATION_JSON).retrieve().bodyToMono(Event.class).onErrorReturn(new Event()); + } + + @Override + public Flux getMeasuredEventDetails() { + return client.get().uri("/measured-mws/details").accept(MediaType.APPLICATION_JSON).retrieve().bodyToFlux(MeasuredMwDetails.class).onErrorReturn(new MeasuredMwDetails()); + } + + @Override + public Mono removeReferenceEventsByEventId(List evids) { + return client.post() + .uri("/reference-events/delete/batch-by-evids") + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .bodyValue(evids) + .retrieve() + .toBodilessEntity() + .flatMap(resp -> Mono.empty()); + } + + @Override + public Flux getValidationEvents() { + return client.get().uri("/validation-events").accept(MediaType.APPLICATION_JSON).retrieve().bodyToFlux(ValidationMwParameters.class).onErrorReturn(new ValidationMwParameters()); + } + + @Override + public Mono postValidationEvents(List events) throws JsonProcessingException { + return client.post().uri("/validation-events/batch").contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON).bodyValue(events).retrieve().bodyToMono(String.class); + } + + @Override + public Mono removeValidationEventsByEventId(List evids) { + return client.post() + .uri("/validation-events/delete/batch-by-evids") + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .bodyValue(evids) + .retrieve() + .toBodilessEntity() + .flatMap(resp -> Mono.empty()); + } + + @Override + public Flux toggleValidationEventsByEventId(List evids) { + return client.post() + .uri("/calibration/toggle-validation/batch-by-evids") + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .bodyValue(evids) + .retrieve() + .bodyToFlux(String.class); + } + + @Override + public Flux getUniqueEventIds() { + return client.get().uri("/events/unique-ids").accept(MediaType.APPLICATION_JSON).retrieve().bodyToMono(new ParameterizedTypeReference>() { + }).flatMapMany(Flux::fromIterable); + } + +} diff --git a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/data/client/SpectraRatioWebClient.java b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/data/client/SpectraRatioWebClient.java new file mode 100644 index 00000000..540ecf42 --- /dev/null +++ b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/data/client/SpectraRatioWebClient.java @@ -0,0 +1,55 @@ +/* +* Copyright (c) 2021, Lawrence Livermore National Security, LLC. Produced at the Lawrence Livermore National Laboratory +* CODE-743439. +* All rights reserved. +* This file is part of CCT. For details, see https://github.com/LLNL/coda-calibration-tool. +* +* 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.data.client; + +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; + +import gov.llnl.gnem.apps.coda.calibration.gui.data.client.api.SpectraRatioClient; +import gov.llnl.gnem.apps.coda.spectra.model.domain.SpectraRatioMeasurementJob; +import gov.llnl.gnem.apps.coda.spectra.model.domain.messaging.SpectraRatiosReportDTO; +import gov.llnl.gnem.apps.coda.spectra.model.domain.util.SpectraRatiosReportByEventPair; +import reactor.core.publisher.Mono; + +@Component +public class SpectraRatioWebClient implements SpectraRatioClient { + + private static final Logger log = LoggerFactory.getLogger(SpectraRatioWebClient.class); + + private WebClient client; + + @Autowired + public SpectraRatioWebClient(WebClient client) { + this.client = client; + } + + @Override + public Mono makeSpectraRatioMeasurements(boolean autoPickingEnabled, boolean persistResults, List smallEventIds, List largeEventIds) { + return client.post() + .uri("/spectra-ratios/measure-spectra-ratio") + .bodyValue( + new SpectraRatioMeasurementJob().setAutopickingEnabled(autoPickingEnabled).setPersistResults(Boolean.TRUE).setSmallEventIds(smallEventIds).setLargeEventIds(largeEventIds)) + .retrieve() + .bodyToMono(SpectraRatiosReportDTO.class) + .map(SpectraRatiosReportDTO::getReport) + .map(SpectraRatiosReportByEventPair::new); + } + +} diff --git a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/data/client/SpectraWebClient.java b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/data/client/SpectraWebClient.java index 147be931..ee9ed98a 100644 --- a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/data/client/SpectraWebClient.java +++ b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/data/client/SpectraWebClient.java @@ -20,6 +20,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.MediaType; +import org.springframework.http.client.MultipartBodyBuilder; import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.client.WebClient; @@ -97,4 +98,23 @@ public Mono> getFitSpectra(String eventId) { .onErrorReturn(new ArrayList()); } + @Override + public Mono getSpecificSpectra(double moment, double apparentStress, double start, double stop, int count) { + MultipartBodyBuilder mbb = new MultipartBodyBuilder(); + mbb.part("moment", moment, MediaType.APPLICATION_JSON); + mbb.part("apparentStress", apparentStress, MediaType.APPLICATION_JSON); + mbb.part("start", start, MediaType.APPLICATION_JSON); + mbb.part("stop", stop, MediaType.APPLICATION_JSON); + mbb.part("count", count, MediaType.APPLICATION_JSON); + + return client.post() + .uri("/spectra-measurements/compute-spectra") + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .bodyValue(mbb.build()) + .retrieve() + .bodyToMono(Spectra.class) + .onErrorReturn(new Spectra()); + } + } diff --git a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/data/client/api/CalibrationClient.java b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/data/client/api/CalibrationClient.java index ec08ac0d..30ed6e78 100644 --- a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/data/client/api/CalibrationClient.java +++ b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/data/client/api/CalibrationClient.java @@ -22,7 +22,7 @@ public interface CalibrationClient { public Mono runCalibration(Boolean autoPickingEnabled); - + public Mono cancelCalibration(Long id); public Mono makeMwMeasurements(Boolean autoPickingEnabled); diff --git a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/data/client/api/EventClient.java b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/data/client/api/EventClient.java index 4f6d8d0e..0e7a9715 100644 --- a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/data/client/api/EventClient.java +++ b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/data/client/api/EventClient.java @@ -1,50 +1,52 @@ -/* -* Copyright (c) 2020, Lawrence Livermore National Security, LLC. Produced at the Lawrence Livermore National Laboratory -* CODE-743439. -* All rights reserved. -* This file is part of CCT. For details, see https://github.com/LLNL/coda-calibration-tool. -* -* 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.data.client.api; - -import java.util.List; - -import com.fasterxml.jackson.core.JsonProcessingException; - -import gov.llnl.gnem.apps.coda.calibration.model.domain.MeasuredMwDetails; -import gov.llnl.gnem.apps.coda.calibration.model.domain.MeasuredMwParameters; -import gov.llnl.gnem.apps.coda.calibration.model.domain.ReferenceMwParameters; -import gov.llnl.gnem.apps.coda.calibration.model.domain.ValidationMwParameters; -import gov.llnl.gnem.apps.coda.common.model.domain.Event; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -public interface EventClient { - - public Flux getMeasuredEvents(); - - public Flux getMeasuredEventDetails(); - - public Flux getReferenceEvents(); - - public Flux getValidationEvents(); - - public Mono getEvent(String eventId); - - public Mono postReferenceEvents(List refEvents) throws JsonProcessingException; - - public Mono postValidationEvents(List valEvents) throws JsonProcessingException; - - public Mono removeReferenceEventsByEventId(List evids); - - public Mono removeValidationEventsByEventId(List evids); - - public Flux toggleValidationEventsByEventId(List evids); -} +/* +* Copyright (c) 2020, Lawrence Livermore National Security, LLC. Produced at the Lawrence Livermore National Laboratory +* CODE-743439. +* All rights reserved. +* This file is part of CCT. For details, see https://github.com/LLNL/coda-calibration-tool. +* +* 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.data.client.api; + +import java.util.List; + +import com.fasterxml.jackson.core.JsonProcessingException; + +import gov.llnl.gnem.apps.coda.calibration.model.domain.MeasuredMwDetails; +import gov.llnl.gnem.apps.coda.calibration.model.domain.MeasuredMwParameters; +import gov.llnl.gnem.apps.coda.calibration.model.domain.ReferenceMwParameters; +import gov.llnl.gnem.apps.coda.calibration.model.domain.ValidationMwParameters; +import gov.llnl.gnem.apps.coda.common.model.domain.Event; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +public interface EventClient { + + public Flux getMeasuredEvents(); + + public Flux getMeasuredEventDetails(); + + public Flux getReferenceEvents(); + + public Flux getValidationEvents(); + + public Mono getEvent(String eventId); + + public Mono postReferenceEvents(List refEvents) throws JsonProcessingException; + + public Mono postValidationEvents(List valEvents) throws JsonProcessingException; + + public Mono removeReferenceEventsByEventId(List evids); + + public Mono removeValidationEventsByEventId(List evids); + + public Flux toggleValidationEventsByEventId(List evids); + + public Flux getUniqueEventIds(); +} diff --git a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/data/client/api/SpectraClient.java b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/data/client/api/SpectraClient.java index 82ab85c6..5211f403 100644 --- a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/data/client/api/SpectraClient.java +++ b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/data/client/api/SpectraClient.java @@ -35,4 +35,6 @@ public interface SpectraClient { public Mono> getFitSpectra(String eventId); + public Mono getSpecificSpectra(double moment, double apparentStress, double start, double stop, int count); + } diff --git a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/data/client/api/SpectraRatioClient.java b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/data/client/api/SpectraRatioClient.java new file mode 100644 index 00000000..ea9adb4c --- /dev/null +++ b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/data/client/api/SpectraRatioClient.java @@ -0,0 +1,26 @@ +/* +* Copyright (c) 2018, Lawrence Livermore National Security, LLC. Produced at the Lawrence Livermore National Laboratory +* CODE-743439. +* All rights reserved. +* This file is part of CCT. For details, see https://github.com/LLNL/coda-calibration-tool. +* +* 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.data.client.api; + +import java.util.List; + +import gov.llnl.gnem.apps.coda.spectra.model.domain.util.SpectraRatiosReportByEventPair; +import reactor.core.publisher.Mono; + +public interface SpectraRatioClient { + + public Mono makeSpectraRatioMeasurements(boolean autoPickingEnabled, boolean persistResults, List smallEventIds, List largeEventIds); + +} \ No newline at end of file diff --git a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/data/exporters/JsonTempFileWriter.java b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/data/exporters/JsonTempFileWriter.java index bdbe6df4..649d2b0f 100644 --- a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/data/exporters/JsonTempFileWriter.java +++ b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/data/exporters/JsonTempFileWriter.java @@ -40,6 +40,7 @@ import gov.llnl.gnem.apps.coda.calibration.gui.data.exporters.api.MeasuredMwTempFileWriter; import gov.llnl.gnem.apps.coda.calibration.gui.data.exporters.api.ParamTempFileWriter; import gov.llnl.gnem.apps.coda.calibration.gui.data.exporters.api.ReferenceMwTempFileWriter; +import gov.llnl.gnem.apps.coda.calibration.gui.data.exporters.api.SpectraRatioTempFileWriter; import gov.llnl.gnem.apps.coda.calibration.gui.data.exporters.api.SpectraTempFileWriter; import gov.llnl.gnem.apps.coda.calibration.gui.data.exporters.api.ValidationMwTempFileWriter; import gov.llnl.gnem.apps.coda.calibration.model.domain.EventSpectraReport; @@ -61,14 +62,16 @@ import gov.llnl.gnem.apps.coda.common.model.domain.FrequencyBand; import gov.llnl.gnem.apps.coda.common.model.domain.SharedFrequencyBandParameters; import gov.llnl.gnem.apps.coda.common.model.domain.Station; +import gov.llnl.gnem.apps.coda.spectra.model.domain.SpectraRatioPairDetails; @Component -public class JsonTempFileWriter implements SpectraTempFileWriter, ParamTempFileWriter, MeasuredMwTempFileWriter, ReferenceMwTempFileWriter, ValidationMwTempFileWriter { +public class JsonTempFileWriter implements SpectraTempFileWriter, ParamTempFileWriter, MeasuredMwTempFileWriter, ReferenceMwTempFileWriter, ValidationMwTempFileWriter, SpectraRatioTempFileWriter { private static final Logger log = LoggerFactory.getLogger(JsonTempFileWriter.class); private static final String CALIBRATION_JSON_NAME = "Calibration_Parameters.json"; private static final String MW_JSON_NAME = "Measured_Events.json"; + private static final String RATIO_JSON_NAME = "Spectra_Ratio_Pair_Details.json"; private ObjectMapper mapper; @@ -138,6 +141,21 @@ public void writeMeasuredMws(Path folder, String filename, List spectraRatioPairDetails) { + writeSpectraRatioDetails(folder, RATIO_JSON_NAME, spectraRatioPairDetails); + } + + @Override + public void writeSpectraRatioDetails(Path folder, String filename, List spectraRatioPairDetails) { + try { + JsonNode document = createOrGetDocument(folder, filename); + writeSpectraRatioEvents(createOrGetFile(folder, filename), document, spectraRatioPairDetails); + } catch (IOException e) { + log.error(e.getMessage(), e); + } + } + @Override public void writeSpectraValues(Path folder, String filename, List measurements) { try { @@ -197,6 +215,10 @@ private void writeMeasuredEvents(File file, JsonNode document, List spectraRatioPairDetails) throws IOException { + writeArrayNodeToFile(file, document, spectraRatioPairDetails, "spectra-ratio-pair-details"); + } + private void writeArrayNodeToFile(File file, JsonNode document, Collection values, String field) throws IllegalArgumentException, IOException { ArrayNode arrayNode = mapper.createArrayNode(); for (T value : values) { diff --git a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/data/exporters/ParamExporter.java b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/data/exporters/ParamExporter.java index 13e5b1c5..41f7ef80 100644 --- a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/data/exporters/ParamExporter.java +++ b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/data/exporters/ParamExporter.java @@ -1,5 +1,5 @@ /* -* Copyright (c) 2018, Lawrence Livermore National Security, LLC. Produced at the Lawrence Livermore National Laboratory +* Copyright (c) 2023, Lawrence Livermore National Security, LLC. Produced at the Lawrence Livermore National Laboratory * CODE-743439. * All rights reserved. * This file is part of CCT. For details, see https://github.com/LLNL/coda-calibration-tool. @@ -16,25 +16,17 @@ import java.io.File; import java.io.IOException; -import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.time.Duration; import java.util.ArrayList; -import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.TreeMap; import java.util.function.Function; import java.util.stream.Collectors; -import java.util.stream.Stream; -import org.apache.commons.compress.archivers.ArchiveException; -import org.apache.commons.compress.archivers.ArchiveOutputStream; -import org.apache.commons.compress.archivers.ArchiveStreamFactory; -import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; -import org.apache.commons.io.IOUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -56,6 +48,7 @@ import gov.llnl.gnem.apps.coda.calibration.model.domain.SiteFrequencyBandParameters; import gov.llnl.gnem.apps.coda.calibration.model.domain.ValidationMwParameters; import gov.llnl.gnem.apps.coda.calibration.model.domain.VelocityConfiguration; +import gov.llnl.gnem.apps.coda.common.gui.util.CommonGuiUtils; import gov.llnl.gnem.apps.coda.common.model.domain.FrequencyBand; import gov.llnl.gnem.apps.coda.common.model.domain.SharedFrequencyBandParameters; import gov.llnl.gnem.apps.coda.common.model.domain.Station; @@ -79,7 +72,6 @@ public ParamExporter(ParameterClient paramClient, EventClient eventClient, List< List referenceMwWriters, List validationMwWriters, List spectraWriters) { this.paramClient = paramClient; this.paramWriters = paramWriters; - this.eventClient = eventClient; this.mwWriters = mwWriters; this.referenceMwWriters = referenceMwWriters; @@ -151,34 +143,7 @@ public File createExportArchive() throws IOException { } } - File zipDir = File.createTempFile("zip-dir", "tmp"); - zipDir.deleteOnExit(); - - try (Stream fileStream = Files.walk(tmpFolder, 5)) { - List files = fileStream.map(Path::toFile).filter(File::isFile).collect(Collectors.toList()); - try (ArchiveOutputStream os = new ArchiveStreamFactory().createArchiveOutputStream("zip", Files.newOutputStream(zipDir.toPath()))) { - for (File file : files) { - os.putArchiveEntry(new ZipArchiveEntry(file, file.getName())); - try (InputStream fis = Files.newInputStream(file.toPath())) { - IOUtils.copy(fis, os); - } - os.closeArchiveEntry(); - } - os.flush(); - } catch (ArchiveException e) { - throw new IOException(e); - } - try (Stream tmpFileStream = Files.walk(tmpFolder)) { - tmpFileStream.sorted(Comparator.reverseOrder()).forEach(t -> { - try { - Files.deleteIfExists(t); - } catch (IOException e) { - log.trace("Unable to delete temporary file {}", e.getMessage(), e); - } - }); - } - } - return zipDir; + return CommonGuiUtils.zipDirectory(tmpFolder); } public void writeMeasuredMws(Path path, String filename, List mws) { diff --git a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/data/exporters/SpectraRatioExporter.java b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/data/exporters/SpectraRatioExporter.java new file mode 100644 index 00000000..9976e9b8 --- /dev/null +++ b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/data/exporters/SpectraRatioExporter.java @@ -0,0 +1,60 @@ +/* +* Copyright (c) 2023, Lawrence Livermore National Security, LLC. Produced at the Lawrence Livermore National Laboratory +* CODE-743439. +* All rights reserved. +* This file is part of CCT. For details, see https://github.com/LLNL/coda-calibration-tool. +* +* 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.data.exporters; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import gov.llnl.gnem.apps.coda.calibration.gui.data.exporters.api.SpectraRatioTempFileWriter; +import gov.llnl.gnem.apps.coda.common.gui.util.CommonGuiUtils; +import gov.llnl.gnem.apps.coda.spectra.model.domain.SpectraRatioPairDetails; + +@Component +public class SpectraRatioExporter { + private final Logger log = LoggerFactory.getLogger(this.getClass()); + + private List spectraRatioWriters; + + @Autowired + public SpectraRatioExporter(List spectraRatioWriters) { + this.spectraRatioWriters = spectraRatioWriters; + } + + public File createExportArchive(List ratiosByEventPair, Path directory) throws IOException { + + if (spectraRatioWriters != null) { + List spectraRatioDetails = new ArrayList<>(ratiosByEventPair); + for (SpectraRatioTempFileWriter writer : spectraRatioWriters) { + writer.writeSpectraRatioDetails(directory, spectraRatioDetails); + } + } + + return CommonGuiUtils.zipDirectory(directory); + } + + public void writeSpectraRatioPairDetails(Path path, String filename, List spectraRatioDetails) { + for (SpectraRatioTempFileWriter writer : spectraRatioWriters) { + writer.writeSpectraRatioDetails(path, filename, spectraRatioDetails); + } + } +} diff --git a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/data/exporters/api/SpectraRatioTempFileWriter.java b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/data/exporters/api/SpectraRatioTempFileWriter.java new file mode 100644 index 00000000..77caa9fb --- /dev/null +++ b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/data/exporters/api/SpectraRatioTempFileWriter.java @@ -0,0 +1,26 @@ +/* +* Copyright (c) 2023, Lawrence Livermore National Security, LLC. Produced at the Lawrence Livermore National Laboratory +* CODE-743439. +* All rights reserved. +* This file is part of CCT. For details, see https://github.com/LLNL/coda-calibration-tool. +* +* 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.data.exporters.api; + +import java.nio.file.Path; +import java.util.List; + +import gov.llnl.gnem.apps.coda.spectra.model.domain.SpectraRatioPairDetails; + +public interface SpectraRatioTempFileWriter { + public void writeSpectraRatioDetails(Path folder, List spectraRatioPairDetails); + + public void writeSpectraRatioDetails(Path folder, String filename, List spectraRatioPairDetails); +} diff --git a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/plotting/BasicWaveformPlot.java b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/plotting/BasicWaveformPlot.java new file mode 100644 index 00000000..4702fc0c --- /dev/null +++ b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/plotting/BasicWaveformPlot.java @@ -0,0 +1,149 @@ +/* +* Copyright (c) 2021, Lawrence Livermore National Security, LLC. Produced at the Lawrence Livermore National Laboratory +* CODE-743439. +* All rights reserved. +* This file is part of CCT. For details, see https://github.com/LLNL/coda-calibration-tool. +* +* 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.plotting; + +import java.text.NumberFormat; +import java.util.Arrays; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import gov.llnl.gnem.apps.coda.common.gui.util.NumberFormatFactory; +import javafx.scene.paint.Color; +import llnl.gnem.core.gui.plotting.api.Axis; +import llnl.gnem.core.gui.plotting.api.Line; +import llnl.gnem.core.gui.plotting.api.LineStyles; +import llnl.gnem.core.gui.plotting.api.PlotObject; +import llnl.gnem.core.gui.plotting.plotly.BasicAxis; +import llnl.gnem.core.gui.plotting.plotly.BasicLine; +import llnl.gnem.core.gui.plotting.plotly.PlotlyWaveformPlot; +import llnl.gnem.core.util.SeriesMath; +import llnl.gnem.core.util.seriesMathHelpers.MinMax; +import llnl.gnem.core.waveform.seismogram.TimeSeries; + +public class BasicWaveformPlot extends PlotlyWaveformPlot { + + private static final long serialVersionUID = 1L; + + private static final int DEFAULT_LINE_WIDTH = 3; + + private static final double PADDING_FACTOR = 0.1; + + private static final Logger log = LoggerFactory.getLogger(BasicWaveformPlot.class); + + private final NumberFormat dfmt4 = NumberFormatFactory.fourDecimalOneLeadingZero(); + + private final Axis xAxis; + + private final Axis yAxis; + + private MinMax minMax; + + private boolean minMaxSet; + + private enum PLOT_ORDERING { + BACKGROUND(0), NOISE_BOX(1), WAVEFORM(2), NOISE_LINE(3), SHAPE_FIT(4), MODEL_FIT(5), PICKS(6); + + private int zorder; + + private PLOT_ORDERING(final int zorder) { + this.zorder = zorder; + } + + public int getZOrder() { + return zorder; + } + } + + public BasicWaveformPlot(final TimeSeries... seismograms) { + super(seismograms); + xAxis = new BasicAxis(Axis.Type.X, "Time (seconds from origin)"); + yAxis = new BasicAxis(Axis.Type.Y, "log10(amplitude)"); + this.addAxes(xAxis, yAxis); + this.minMax = new MinMax(0, 0); + this.minMaxSet = false; + } + + /*public void fitAxes(final double paddingFactor) { + double xMax = Double.NEGATIVE_INFINITY; + double xMin = Double.POSITIVE_INFINITY; + double yMax = Double.NEGATIVE_INFINITY; + double yMin = Double.POSITIVE_INFINITY; + + for(SeismicSignal sig : getSeismograms()) { + if(sig.getSegmentLength() > xMax) { + xMax = xMax + sig.getSegmentLength() * paddingFactor; + } + if(sig.getMax() > yMax) { + yMax = sig.getMax(); + } + } + }*/ + + private Line createLine(final double timeShift, final double valueShift, final TimeSeries timeSeries, final Color lineColor) { + return createLine(timeShift, valueShift, timeSeries, lineColor, DEFAULT_LINE_WIDTH, LineStyles.SOLID); + } + + private Line createLine(final double timeShift, final double valueShift, final TimeSeries seismogram, final Color lineColor, final int width, final LineStyles style) { + return new BasicLine(seismogram.getIdentifier(), + timeShift + seismogram.getZeroTimeOffsetSeconds(), + seismogram.getDelta(), + SeriesMath.add(seismogram.getData(), valueShift), + lineColor, + style, + width); + } + + public Line addLine(final TimeSeries seismogram, final Color lineColor) { + final Line line = new BasicLine(seismogram.getIdentifier(), + seismogram.getZeroTimeOffsetSeconds(), + seismogram.getDelta(), + seismogram.getData(), + lineColor, + LineStyles.SOLID, + DEFAULT_LINE_WIDTH); + addPlotObject(line, PLOT_ORDERING.WAVEFORM.getZOrder()); + return line; + } + + private Line createFixedLine(final String label, final double value, final int start, final int length, final Color lineColor, final LineStyles lineStyle) { + final float[] data = new float[length]; + Arrays.fill(data, (float) value); + return new BasicLine(label, start, 1.0, data, lineColor, lineStyle, 1); + } + + public static float[] doublesToFloats(final double[] x) { + final float[] xfloats = new float[x.length]; + for (int i = 0; i < x.length; i++) { + xfloats[i] = (float) x[i]; + } + return xfloats; + } + + public Axis getxAxis() { + return xAxis; + } + + public Axis getyAxis() { + return yAxis; + } + + private void addPlotObject(final PlotObject object, final int zOrder) { + if (object != null) { + object.setZindex(zOrder); + } + addPlotObject(object); + } +} diff --git a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/plotting/CertLeafletMapController.java b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/plotting/CertLeafletMapController.java new file mode 100644 index 00000000..615154e2 --- /dev/null +++ b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/plotting/CertLeafletMapController.java @@ -0,0 +1,289 @@ +/* +* Copyright (c) 2018, Lawrence Livermore National Security, LLC. Produced at the Lawrence Livermore National Laboratory +* CODE-743439. +* All rights reserved. +* This file is part of CCT. For details, see https://github.com/LLNL/coda-calibration-tool. +* +* 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.plotting; + +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.time.Instant; +import java.util.Collection; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; + +import javax.imageio.ImageIO; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +import gov.llnl.gnem.apps.coda.common.mapping.MapCallbackEvent; +import gov.llnl.gnem.apps.coda.common.mapping.MapProperties; +import gov.llnl.gnem.apps.coda.common.mapping.WMSLayerDescriptor; +import gov.llnl.gnem.apps.coda.common.mapping.api.GeoMap; +import gov.llnl.gnem.apps.coda.common.mapping.api.GeoShape; +import gov.llnl.gnem.apps.coda.common.mapping.api.Icon; +import javafx.application.Platform; +import javafx.embed.swing.SwingFXUtils; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.scene.SnapshotParameters; +import javafx.scene.control.Button; +import javafx.scene.control.ContentDisplay; +import javafx.scene.control.ScrollPane; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.StackPane; +import javafx.scene.transform.Transform; +import javafx.scene.web.WebView; +import javafx.stage.DirectoryChooser; + +@Component +@Scope(scopeName = ConfigurableBeanFactory.SCOPE_SINGLETON) +public class CertLeafletMapController implements GeoMap { + + private static final Logger log = LoggerFactory.getLogger(CertLeafletMapController.class); + + @FXML + private StackPane mapView; + + @FXML + private ScrollPane viewScroll; + + @FXML + private Button mapSnapshotButton; + + private LeafletMap mapImpl; + + private DirectoryChooser screenshotFolderChooser = new DirectoryChooser(); + + private MapProperties mapProps; + + private CertLeafletMapController(@Autowired(required = false) MapProperties mapProps) { + this.mapProps = mapProps; + this.mapImpl = new LeafletMap(); + } + + @FXML + public void initialize() { + ImageView label = new ImageView(); + label.setImage(new Image(getClass().getResource("/fxml/snapshot-icon.png").toExternalForm())); + label.setFitWidth(16); + label.setFitHeight(16); + mapSnapshotButton.setGraphic(label); + mapSnapshotButton.setContentDisplay(ContentDisplay.CENTER); + + Platform.runLater(() -> { + mapView.setVisible(true); + mapImpl.attach(mapView); + if (mapProps != null) { + mapProps.getLayers().forEach(mapImpl::addLayer); + } + }); + } + + @Override + public void show() { + // No op for now + } + + @Override + public long getIconCount() { + return mapImpl.getIconCount(); + } + + @Override + public void clearIcons() { + mapImpl.clearIcons(); + } + + @Override + public void addLayer(WMSLayerDescriptor layer) { + mapImpl.addLayer(layer); + } + + @Override + public boolean addIcon(Icon icon) { + return mapImpl.addIcon(icon); + } + + @Override + public boolean removeIcon(Icon icon) { + return mapImpl.removeIcon(icon); + } + + @Override + public void addIcons(Collection icons) { + mapImpl.addIcons(icons); + } + + @Override + public void removeIcons(Collection icons) { + mapImpl.removeIcons(icons); + } + + @Override + public void addShape(GeoShape shape) { + mapImpl.addShape(shape); + } + + @Override + public void removeShape(GeoShape shape) { + mapImpl.removeShape(shape); + } + + @Override + public void fitViewToActiveShapes() { + mapImpl.fitViewToActiveShapes(); + } + + @Override + public void registerEventCallback(Consumer callback) { + mapImpl.registerEventCallback(callback); + } + + @Override + public void removeEventCallback(Consumer callback) { + mapImpl.removeEventCallback(callback); + } + + @Override + public String getPolygonGeoJSON() { + return mapImpl.getPolygonGeoJSON(); + } + + @Override + public void setPolygonGeoJSON(String geoJSON) { + Platform.runLater(() -> { + mapImpl.setPolygonGeoJSON(geoJSON); + }); + } + + @Override + public int hashCode() { + return Objects.hash(mapImpl, mapView); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + CertLeafletMapController other = (CertLeafletMapController) obj; + if (!Objects.equals(mapImpl, other.mapImpl)) { + return false; + } + if (!Objects.equals(mapView, other.mapView)) { + return false; + } + return true; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("\"").append(mapView).append("\", \"").append(mapImpl).append('\"'); + return builder.toString(); + } + + @FXML + private void showMapSnapshotDialog(ActionEvent e) { + File folder = screenshotFolderChooser.showDialog(mapView.getScene().getWindow()); + try { + if (folder != null && folder.exists() && folder.isDirectory() && folder.canWrite()) { + screenshotFolderChooser.setInitialDirectory(folder); + exportSnapshot(folder); + } + } catch (SecurityException ex) { + log.warn("Exception trying to write screenshots to folder {} : {}", folder, ex.getLocalizedMessage(), ex); + } + } + + private void exportSnapshot(File folder) { + String mapId = folder.getAbsolutePath() + File.separator + "Map_" + Instant.now().toEpochMilli(); + try { + String svg = mapImpl.getSvgLayer(); + if (svg != null && !svg.trim().isEmpty()) { + Files.write(Paths.get(mapId + ".svg"), svg.getBytes(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + } + } catch (IOException e) { + log.warn("Unable to write map svg due to file exception {}", e.getLocalizedMessage(), e); + } + writePng(mapId + ".png"); + } + + private Image snapshot(final WebView node) { + SnapshotParameters params = new SnapshotParameters(); + params.setTransform(Transform.scale(4.0, 4.0)); + Image snapshot = node.snapshot(params, null); + return snapshot; + } + + private void writePng(BufferedImage image, String filename) { + try { + ImageIO.write(image, "png", new File(filename)); + log.trace("Wrote image to: {}", filename); + } catch (IOException ex) { + log.warn(ex.getMessage(), ex); + } catch (NullPointerException ex) { + log.warn("Null pointer writing image {} to file {} : {}", image, filename, ex.getMessage(), ex); + } + } + + private void writePng(String filename) { + mapImpl.setShowOverlay(false); + CompletableFuture.runAsync(() -> { + //This is really dumb and subject to nasty race conditions but, as of writing (Jan 2020), I can find no good way to get a notification from the JavaFX renderer that it actually executed a render pass. + //There is a pre/post pulse callback on Scene proposed but is not available in Java 8 and we need to maintain compatibility for the moment. + //Thus we have to do this song and dance to have a reasonable assumption a pulse has happened on the FX thread. + //We could try to register and start/stop an AnimationTimer to know for sure but that's considerably more complicated and the fail state for this is "has visible buttons in the screenshot" so eh. + try { + Thread.sleep(200l); + } catch (InterruptedException e) { + //Nop + } + Platform.runLater(() -> { + try { + Boolean visibleLayers = mapImpl.hasVisibleTileLayers(); + if (visibleLayers) { + Image snapshot = snapshot(mapImpl.getWebView()); + CompletableFuture.runAsync(() -> { + if (visibleLayers) { + writePng(SwingFXUtils.fromFXImage(snapshot, null), filename); + } + Platform.runLater(() -> mapImpl.setShowOverlay(true)); + }); + } else { + mapImpl.setShowOverlay(true); + } + } catch (RuntimeException e) { + mapImpl.setShowOverlay(true); + } + }); + }); + } +} diff --git a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/plotting/CodaWaveformPlotManager.java b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/plotting/CodaWaveformPlotManager.java index cff2b0f4..a845d884 100644 --- a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/plotting/CodaWaveformPlotManager.java +++ b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/plotting/CodaWaveformPlotManager.java @@ -170,7 +170,7 @@ public class CodaWaveformPlotManager { final ToggleButton syncZoomMode = new ToggleButton(ZOOM_SYNC_LABEL); final ToggleButton syncZoomMode2 = new ToggleButton(ZOOM_SYNC_LABEL); - private boolean syncZoomModeBoolean = true; + private boolean syncZoomModeBoolean = false; private final EventHandler forwardAction = event -> { if ((currentPage + 1) < totalPages) { @@ -553,11 +553,10 @@ private void setFrequencyDisplayText(Waveform wave) { } } - private void updatePlotAxesInGroup(PlotAxisChange change) { - - if (change.isReset()) { + private void updatePlotAxes(PlotAxisChange change) { + if (selectedSinglePlot != null) { this.setSavedAxisLimits(change.getAxisLimits()); - if (selectedSinglePlot != null) { + if (change.isReset()) { Platform.runLater(() -> { selectedSinglePlot.replot(); }); @@ -567,7 +566,6 @@ private void updatePlotAxesInGroup(PlotAxisChange change) { } if (!orderedWaveformPlots.isEmpty()) { - orderedWaveformPlots.values().forEach(plot -> { if (syncZoomModeBoolean) { if (!change.isReset()) { @@ -591,7 +589,6 @@ private void updatePlotAxesInGroup(PlotAxisChange change) { plot.replot(); }); }); - } } @@ -775,7 +772,7 @@ private CodaWaveformPlot getOrCreatePlot() { plot.setAxisChangeListener(axisChange -> { if (axisChange.getNewValue() instanceof PlotAxisChange) { - updatePlotAxesInGroup((PlotAxisChange) axisChange.getNewValue()); + updatePlotAxes((PlotAxisChange) axisChange.getNewValue()); } }); diff --git a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/plotting/LeafletMapController.java b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/plotting/LeafletMapController.java index 1e100359..7101044e 100644 --- a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/plotting/LeafletMapController.java +++ b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/plotting/LeafletMapController.java @@ -1,309 +1,311 @@ -/* -* Copyright (c) 2018, Lawrence Livermore National Security, LLC. Produced at the Lawrence Livermore National Laboratory -* CODE-743439. -* All rights reserved. -* This file is part of CCT. For details, see https://github.com/LLNL/coda-calibration-tool. -* -* 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.plotting; - -import java.awt.image.BufferedImage; -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.nio.file.StandardOpenOption; -import java.time.Instant; -import java.util.Collection; -import java.util.Objects; -import java.util.concurrent.CompletableFuture; -import java.util.function.Consumer; - -import javax.imageio.ImageIO; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.config.ConfigurableBeanFactory; -import org.springframework.context.annotation.Primary; -import org.springframework.context.annotation.Scope; -import org.springframework.stereotype.Service; - -import gov.llnl.gnem.apps.coda.common.mapping.MapCallbackEvent; -import gov.llnl.gnem.apps.coda.common.mapping.MapProperties; -import gov.llnl.gnem.apps.coda.common.mapping.WMSLayerDescriptor; -import gov.llnl.gnem.apps.coda.common.mapping.api.GeoMap; -import gov.llnl.gnem.apps.coda.common.mapping.api.GeoShape; -import gov.llnl.gnem.apps.coda.common.mapping.api.Icon; -import javafx.application.Platform; -import javafx.embed.swing.SwingFXUtils; -import javafx.event.ActionEvent; -import javafx.fxml.FXML; -import javafx.fxml.FXMLLoader; -import javafx.scene.Parent; -import javafx.scene.Scene; -import javafx.scene.SnapshotParameters; -import javafx.scene.control.Button; -import javafx.scene.control.ContentDisplay; -import javafx.scene.control.ScrollPane; -import javafx.scene.image.Image; -import javafx.scene.image.ImageView; -import javafx.scene.layout.StackPane; -import javafx.scene.transform.Transform; -import javafx.scene.web.WebView; -import javafx.stage.DirectoryChooser; -import javafx.stage.Stage; -import javafx.stage.StageStyle; - -@Service -@Scope(scopeName = ConfigurableBeanFactory.SCOPE_SINGLETON) -@Primary -public class LeafletMapController implements GeoMap { - - private static final Logger log = LoggerFactory.getLogger(LeafletMapController.class); - - @FXML - private StackPane view; - - @FXML - private ScrollPane viewScroll; - - @FXML - private Button snapshotButton; - - private LeafletMap mapImpl; - - private Stage stage; - - private DirectoryChooser screenshotFolderChooser = new DirectoryChooser(); - - private LeafletMapController(@Autowired(required = false) MapProperties mapProps) { - Platform.runLater(() -> { - mapImpl = new LeafletMap(); - FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/fxml/MapView.fxml")); - fxmlLoader.setController(this); - stage = new Stage(StageStyle.DECORATED); - try { - Parent root = fxmlLoader.load(); - Scene scene = new Scene(root); - stage.setScene(scene); - mapImpl.attach(view); - if (mapProps != null) { - mapProps.getLayers().forEach(mapImpl::addLayer); - } - - ImageView label = new ImageView(); - label.setImage(new Image(getClass().getResource("/fxml/snapshot-icon.png").toExternalForm())); - label.setFitWidth(16); - label.setFitHeight(16); - snapshotButton.setGraphic(label); - snapshotButton.setContentDisplay(ContentDisplay.CENTER); - } catch (IOException e) { - throw new IllegalStateException(e); - } - }); - } - - public void hide() { - Platform.runLater(() -> { - stage.hide(); - }); - } - - @Override - public void show() { - Platform.runLater(() -> { - stage.show(); - stage.toFront(); - }); - } - - @Override - public long getIconCount() { - return mapImpl.getIconCount(); - } - - @Override - public void clearIcons() { - mapImpl.clearIcons(); - } - - @Override - public void addLayer(WMSLayerDescriptor layer) { - mapImpl.addLayer(layer); - } - - @Override - public boolean addIcon(Icon icon) { - return mapImpl.addIcon(icon); - } - - @Override - public boolean removeIcon(Icon icon) { - return mapImpl.removeIcon(icon); - } - - @Override - public void addIcons(Collection icons) { - mapImpl.addIcons(icons); - } - - @Override - public void removeIcons(Collection icons) { - mapImpl.removeIcons(icons); - } - - @Override - public void addShape(GeoShape shape) { - mapImpl.addShape(shape); - } - - @Override - public void removeShape(GeoShape shape) { - mapImpl.removeShape(shape); - } - - @Override - public void fitViewToActiveShapes() { - mapImpl.fitViewToActiveShapes(); - } - - @Override - public void registerEventCallback(Consumer callback) { - mapImpl.registerEventCallback(callback); - } - - @Override - public void removeEventCallback(Consumer callback) { - mapImpl.removeEventCallback(callback); - } - - @Override - public String getPolygonGeoJSON() { - return mapImpl.getPolygonGeoJSON(); - } - - @Override - public void setPolygonGeoJSON(String geoJSON) { - Platform.runLater(() -> { - mapImpl.setPolygonGeoJSON(geoJSON); - }); - } - - @Override - public int hashCode() { - return Objects.hash(mapImpl, view); - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (getClass() != obj.getClass()) { - return false; - } - LeafletMapController other = (LeafletMapController) obj; - if (!Objects.equals(mapImpl, other.mapImpl)) { - return false; - } - if (!Objects.equals(view, other.view)) { - return false; - } - return true; - } - - @Override - public String toString() { - StringBuilder builder = new StringBuilder(); - builder.append("\"").append(view).append("\", \"").append(mapImpl).append('\"'); - return builder.toString(); - } - - @FXML - private void showSnapshotDialog(ActionEvent e) { - File folder = screenshotFolderChooser.showDialog(view.getScene().getWindow()); - try { - if (folder != null && folder.exists() && folder.isDirectory() && folder.canWrite()) { - screenshotFolderChooser.setInitialDirectory(folder); - exportSnapshot(folder); - } - } catch (SecurityException ex) { - log.warn("Exception trying to write screenshots to folder {} : {}", folder, ex.getLocalizedMessage(), ex); - } - } - - private void exportSnapshot(File folder) { - String mapId = folder.getAbsolutePath() + File.separator + "Map_" + Instant.now().toEpochMilli(); - try { - String svg = mapImpl.getSvgLayer(); - if (svg != null && !svg.trim().isEmpty()) { - Files.write(Paths.get(mapId + ".svg"), svg.getBytes(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); - } - } catch (IOException e) { - log.warn("Unable to write map svg due to file exception {}", e.getLocalizedMessage(), e); - } - writePng(mapId + ".png"); - } - - private Image snapshot(final WebView node) { - SnapshotParameters params = new SnapshotParameters(); - params.setTransform(Transform.scale(4.0, 4.0)); - Image snapshot = node.snapshot(params, null); - return snapshot; - } - - private void writePng(BufferedImage image, String filename) { - try { - ImageIO.write(image, "png", new File(filename)); - log.trace("Wrote image to: {}", filename); - } catch (IOException ex) { - log.warn(ex.getMessage(), ex); - } catch (NullPointerException ex) { - log.warn("Null pointer writing image {} to file {} : {}", image, filename, ex.getMessage(), ex); - } - } - - private void writePng(String filename) { - mapImpl.setShowOverlay(false); - CompletableFuture.runAsync(() -> { - //This is really dumb and subject to nasty race conditions but, as of writing (Jan 2020), I can find no good way to get a notification from the JavaFX renderer that it actually executed a render pass. - //There is a pre/post pulse callback on Scene proposed but is not available in Java 8 and we need to maintain compatibility for the moment. - //Thus we have to do this song and dance to have a reasonable assumption a pulse has happened on the FX thread. - //We could try to register and start/stop an AnimationTimer to know for sure but that's considerably more complicated and the fail state for this is "has visible buttons in the screenshot" so eh. - try { - Thread.sleep(200l); - } catch (InterruptedException e) { - //Nop - } - Platform.runLater(() -> { - try { - Boolean visibleLayers = mapImpl.hasVisibleTileLayers(); - if (visibleLayers) { - Image snapshot = snapshot(mapImpl.getWebView()); - CompletableFuture.runAsync(() -> { - if (visibleLayers) { - writePng(SwingFXUtils.fromFXImage(snapshot, null), filename); - } - Platform.runLater(() -> mapImpl.setShowOverlay(true)); - }); - } else { - mapImpl.setShowOverlay(true); - } - } catch (RuntimeException e) { - mapImpl.setShowOverlay(true); - } - }); - }); - } -} +/* +* Copyright (c) 2018, Lawrence Livermore National Security, LLC. Produced at the Lawrence Livermore National Laboratory +* CODE-743439. +* All rights reserved. +* This file is part of CCT. For details, see https://github.com/LLNL/coda-calibration-tool. +* +* 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.plotting; + +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.time.Instant; +import java.util.Collection; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; + +import javax.imageio.ImageIO; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.annotation.Primary; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Service; + +import gov.llnl.gnem.apps.coda.common.mapping.MapCallbackEvent; +import gov.llnl.gnem.apps.coda.common.mapping.MapProperties; +import gov.llnl.gnem.apps.coda.common.mapping.WMSLayerDescriptor; +import gov.llnl.gnem.apps.coda.common.mapping.api.GeoMap; +import gov.llnl.gnem.apps.coda.common.mapping.api.GeoShape; +import gov.llnl.gnem.apps.coda.common.mapping.api.Icon; +import javafx.application.Platform; +import javafx.embed.swing.SwingFXUtils; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.scene.SnapshotParameters; +import javafx.scene.control.Button; +import javafx.scene.control.ContentDisplay; +import javafx.scene.control.ScrollPane; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.StackPane; +import javafx.scene.transform.Transform; +import javafx.scene.web.WebView; +import javafx.stage.DirectoryChooser; +import javafx.stage.Stage; +import javafx.stage.StageStyle; + +@Service +@Scope(scopeName = ConfigurableBeanFactory.SCOPE_SINGLETON) +@Primary +public class LeafletMapController implements GeoMap { + + private static final Logger log = LoggerFactory.getLogger(LeafletMapController.class); + + @FXML + private StackPane view; + + @FXML + private ScrollPane viewScroll; + + @FXML + private Button snapshotButton; + + private LeafletMap mapImpl; + + private Stage stage; + + private DirectoryChooser screenshotFolderChooser = new DirectoryChooser(); + + private LeafletMapController(@Autowired(required = false) MapProperties mapProps) { + Platform.runLater(() -> { + mapImpl = new LeafletMap(); + FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/fxml/MapView.fxml")); + fxmlLoader.setController(this); + stage = new Stage(StageStyle.DECORATED); + try { + Parent root = fxmlLoader.load(); + Scene scene = new Scene(root); + stage.setScene(scene); + mapImpl.attach(view); + if (mapProps != null) { + mapProps.getLayers().forEach(mapImpl::addLayer); + } + + ImageView label = new ImageView(); + label.setImage(new Image(getClass().getResource("/fxml/snapshot-icon.png").toExternalForm())); + label.setFitWidth(16); + label.setFitHeight(16); + snapshotButton.setGraphic(label); + snapshotButton.setContentDisplay(ContentDisplay.CENTER); + } catch (IOException e) { + throw new IllegalStateException(e); + } + }); + } + + public void hide() { + Platform.runLater(() -> { + stage.hide(); + }); + } + + @Override + public void show() { + Platform.runLater(() -> { + stage.show(); + stage.toFront(); + }); + } + + @Override + public long getIconCount() { + return mapImpl.getIconCount(); + } + + @Override + public void clearIcons() { + mapImpl.clearIcons(); + } + + @Override + public void addLayer(WMSLayerDescriptor layer) { + mapImpl.addLayer(layer); + } + + @Override + public boolean addIcon(Icon icon) { + return mapImpl.addIcon(icon); + } + + @Override + public boolean removeIcon(Icon icon) { + return mapImpl.removeIcon(icon); + } + + @Override + public void addIcons(Collection icons) { + mapImpl.addIcons(icons); + } + + @Override + public void removeIcons(Collection icons) { + mapImpl.removeIcons(icons); + } + + @Override + public void addShape(GeoShape shape) { + mapImpl.addShape(shape); + } + + @Override + public void removeShape(GeoShape shape) { + mapImpl.removeShape(shape); + } + + @Override + public void fitViewToActiveShapes() { + mapImpl.fitViewToActiveShapes(); + } + + @Override + public void registerEventCallback(Consumer callback) { + mapImpl.registerEventCallback(callback); + } + + @Override + public void removeEventCallback(Consumer callback) { + mapImpl.removeEventCallback(callback); + } + + @Override + public String getPolygonGeoJSON() { + return mapImpl.getPolygonGeoJSON(); + } + + @Override + public void setPolygonGeoJSON(String geoJSON) { + Platform.runLater(() -> { + mapImpl.setPolygonGeoJSON(geoJSON); + }); + } + + @Override + public int hashCode() { + return Objects.hash(mapImpl, view); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + LeafletMapController other = (LeafletMapController) obj; + if (!Objects.equals(mapImpl, other.mapImpl)) { + return false; + } + if (!Objects.equals(view, other.view)) { + return false; + } + return true; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("\"").append(view).append("\", \"").append(mapImpl).append('\"'); + return builder.toString(); + } + + @FXML + private void showSnapshotDialog(ActionEvent e) { + File folder = screenshotFolderChooser.showDialog(view.getScene().getWindow()); + try { + if (folder != null && folder.exists() && folder.isDirectory() && folder.canWrite()) { + screenshotFolderChooser.setInitialDirectory(folder); + exportSnapshot(folder); + } + } catch (SecurityException ex) { + log.warn("Exception trying to write screenshots to folder {} : {}", folder, ex.getLocalizedMessage(), ex); + } + } + + private void exportSnapshot(File folder) { + String mapId = folder.getAbsolutePath() + File.separator + "Map_" + Instant.now().toEpochMilli(); + try { + String svg = mapImpl.getSvgLayer(); + if (svg != null && !svg.trim().isEmpty()) { + Files.write(Paths.get(mapId + ".svg"), svg.getBytes(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + } + } catch (IOException e) { + log.warn("Unable to write map svg due to file exception {}", e.getLocalizedMessage(), e); + } + writePng(mapId + ".png"); + } + + private Image snapshot(final WebView node) { + SnapshotParameters params = new SnapshotParameters(); + params.setTransform(Transform.scale(4.0, 4.0)); + Image snapshot = node.snapshot(params, null); + return snapshot; + } + + private void writePng(BufferedImage image, String filename) { + try { + ImageIO.write(image, "png", new File(filename)); + log.trace("Wrote image to: {}", filename); + } catch (IOException ex) { + log.warn(ex.getMessage(), ex); + } catch (NullPointerException ex) { + log.warn("Null pointer writing image {} to file {} : {}", image, filename, ex.getMessage(), ex); + } + } + + private void writePng(String filename) { + mapImpl.setShowOverlay(false); + CompletableFuture.runAsync(() -> { + //TODO: Look into option to export .ps or equivalent with merged raster and vector layer. + + //This is really dumb and subject to nasty race conditions but, as of writing (Jan 2020), I can find no good way to get a notification from the JavaFX renderer that it actually executed a render pass. + //There is a pre/post pulse callback on Scene proposed but is not available in Java 8 and we need to maintain compatibility for the moment. + //Thus we have to do this song and dance to have a reasonable assumption a pulse has happened on the FX thread. + //We could try to register and start/stop an AnimationTimer to know for sure but that's considerably more complicated and the fail state for this is "has visible buttons in the screenshot" so eh. + try { + Thread.sleep(200l); + } catch (InterruptedException e) { + //Nop + } + Platform.runLater(() -> { + try { + Boolean visibleLayers = mapImpl.hasVisibleTileLayers(); + if (visibleLayers) { + Image snapshot = snapshot(mapImpl.getWebView()); + CompletableFuture.runAsync(() -> { + if (visibleLayers) { + writePng(SwingFXUtils.fromFXImage(snapshot, null), filename); + } + Platform.runLater(() -> mapImpl.setShowOverlay(true)); + }); + } else { + mapImpl.setShowOverlay(true); + } + } catch (RuntimeException e) { + mapImpl.setShowOverlay(true); + } + }); + }); + } +} diff --git a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/plotting/PlotlyPlotFactory.java b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/plotting/PlotlyPlotFactory.java index e78850ce..b0bc90a7 100644 --- a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/plotting/PlotlyPlotFactory.java +++ b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/plotting/PlotlyPlotFactory.java @@ -22,6 +22,7 @@ import llnl.gnem.core.gui.plotting.api.Line; import llnl.gnem.core.gui.plotting.api.LineStyles; import llnl.gnem.core.gui.plotting.api.PlotFactory; +import llnl.gnem.core.gui.plotting.api.Rectangle; import llnl.gnem.core.gui.plotting.api.Symbol; import llnl.gnem.core.gui.plotting.api.SymbolStyles; import llnl.gnem.core.gui.plotting.api.VerticalLine; @@ -30,8 +31,8 @@ import llnl.gnem.core.gui.plotting.plotly.BasicSymbol; import llnl.gnem.core.gui.plotting.plotly.BasicTitle; import llnl.gnem.core.gui.plotting.plotly.PlotlyPlot; -import llnl.gnem.core.gui.plotting.plotly.PlotlyPlotData; -import llnl.gnem.core.gui.plotting.plotly.PlotlyTrace; +import llnl.gnem.core.gui.plotting.plotly.PlotData; +import llnl.gnem.core.gui.plotting.plotly.PlotTrace; @Service public class PlotlyPlotFactory implements PlotFactory { @@ -43,7 +44,7 @@ public BasicPlot basicPlot() { @Override public BasicPlot lineAndMarkerScatterPlot() { - return new PlotlyPlot(false, new PlotlyPlotData(new PlotlyTrace(PlotlyTrace.Style.SCATTER_MARKER_AND_LINE), Color.WHITE, new BasicTitle())); + return new PlotlyPlot(false, new PlotData(new PlotTrace(PlotTrace.Style.SCATTER_MARKER_AND_LINE), Color.WHITE, new BasicTitle())); } @Override @@ -56,11 +57,21 @@ public Line line(final double[] xVals, final double[] yVals, final Color color, return new BasicLine(xVals, yVals, color, style, pxThickness); } + @Override + public Line lineX(String label, double startingX, double xIncrement, float[] xData, Color color, LineStyles style, int pxThickness) { + return new BasicLine(label, startingX, xIncrement, xData, color, style, pxThickness); + } + @Override public VerticalLine verticalLine(final double x, final double yRatio, final String label) { return new VerticalLine(x, yRatio, label); } + @Override + public Rectangle rectangle(final double x1, final double x2, final double yRatio, final String label, final Color color) { + return new Rectangle(x1, x2, yRatio, label, color); + } + @Override public Symbol createSymbol(final SymbolStyles style, final String name, final double x, final double y, final Color color, final Color edgeColor, final Color textColor, final String text, final boolean textVisible) { diff --git a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/plotting/RatioDetailPlot.java b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/plotting/RatioDetailPlot.java new file mode 100644 index 00000000..8e7e8957 --- /dev/null +++ b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/plotting/RatioDetailPlot.java @@ -0,0 +1,725 @@ +/* +* Copyright (c) 2022, Lawrence Livermore National Security, LLC. Produced at the Lawrence Livermore National Laboratory +* CODE-743439. +* All rights reserved. +* This file is part of CCT. For details, see https://github.com/LLNL/coda-calibration-tool. +* +* 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.plotting; +package gov.llnl.gnem.apps.coda.calibration.gui.plotting; + +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.text.NumberFormat; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.stream.IntStream; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import gov.llnl.gnem.apps.coda.common.gui.util.NumberFormatFactory; +import gov.llnl.gnem.apps.coda.common.model.domain.Waveform; +import gov.llnl.gnem.apps.coda.spectra.model.domain.util.SpectraRatioPairOperator; +import javafx.scene.paint.Color; +import llnl.gnem.core.gui.plotting.api.Axis; +import llnl.gnem.core.gui.plotting.api.AxisLimits; +import llnl.gnem.core.gui.plotting.api.Line; +import llnl.gnem.core.gui.plotting.api.LineStyles; +import llnl.gnem.core.gui.plotting.api.PlotObject; +import llnl.gnem.core.gui.plotting.api.Rectangle; +import llnl.gnem.core.gui.plotting.api.VerticalLine; +import llnl.gnem.core.gui.plotting.events.PlotAxisChange; +import llnl.gnem.core.gui.plotting.events.PlotShapeMove; +import llnl.gnem.core.gui.plotting.plotly.BasicAxis; +import llnl.gnem.core.gui.plotting.plotly.BasicLine; +import llnl.gnem.core.gui.plotting.plotly.PlotlyWaveformPlot; +import llnl.gnem.core.util.TimeT; +import llnl.gnem.core.util.seriesMathHelpers.MinMax; +import llnl.gnem.core.waveform.seismogram.TimeSeries; + +public class RatioDetailPlot extends PlotlyWaveformPlot { + + private static final long serialVersionUID = 1L; + + private static final int DEFAULT_LINE_WIDTH = 2; + private static final int DRAGGABLE_LINE_WIDTH = 4; + + private static final Logger log = LoggerFactory.getLogger(RatioDetailPlot.class); + private final NumberFormat dfmt4 = NumberFormatFactory.fourDecimalOneLeadingZero(); + private String plotIdentifier; + + private final Axis xAxis; + + private final Axis yAxis; + + private boolean alignPeaks = true; + + private double diffAvg; + + private final PropertyChangeSupport axisProperty = new PropertyChangeSupport(this); + private final PropertyChangeSupport cutSegmentProperty = new PropertyChangeSupport(this); + + private PropertyChangeListener axisChangeListener = null; + private PropertyChangeListener cutSegmentChangeListener = null; + + private SpectraRatioPairOperator ratioDetails; + private TimeSeries numeratorSeries; + private TimeSeries denominatorSeries; + + private final Color numeratorColor = Color.DARKBLUE; + private final Color numeratorCutColor = Color.BLUE; + private final Color denominatorColor = Color.DARKRED; + private final Color denominatorCutColor = Color.RED; + private final Color diffColor = Color.PURPLE; + private final Color verticalLineColor1 = Color.DARKBLUE; + private final Color verticalLineColor2 = Color.DARKRED; + private final Color ratioValColor = Color.DARKGRAY; + + private final String NUMERATOR_START_CUT_LABEL = "Numer Start Cut"; + private final String DENOMINATOR_START_CUT_LABEL = "Denom Start Cut"; + private final String NUMERATOR_END_CUT_LABEL = "Numer End Cut"; + private final String DENOMINATOR_END_CUT_LABEL = "Denom End Cut"; + private final String DENOMINATOR_CUT_LABEL = "Denominator Cut"; + private final String NUMERATOR_CUT_LABEL = "Numerator Cut"; + + private double peakOffset = 0.0; + + private double plotPadding = 0.05; // Percent of padding for top and bottom in plots + + private enum PLOT_ORDERING { + BACKGROUND(0), NOISE_BOX(1), NUMER_WAVEFORM(2), DENOM_WAVEFORM(3), DIFF_WAVEFORM(4), PICK_LINES(5); + + private int zorder; + + private PLOT_ORDERING(final int zorder) { + this.zorder = zorder; + } + + public int getZOrder() { + return zorder; + } + } + + public RatioDetailPlot() { + xAxis = new BasicAxis(Axis.Type.X, "Time (seconds from origin)"); + yAxis = new BasicAxis(Axis.Type.Y, "log10(amplitude)"); + this.addAxes(xAxis, yAxis); + } + + public RatioDetailPlot(final SpectraRatioPairOperator ratioDetails, boolean alignPeaks) { + xAxis = new BasicAxis(Axis.Type.X, "Time (seconds from origin)"); + yAxis = new BasicAxis(Axis.Type.Y, "log10(amplitude)"); + this.addAxes(xAxis, yAxis); + this.alignPeaks = alignPeaks; + setRatioDetails(ratioDetails); + } + + public void setRatioDetails(final SpectraRatioPairOperator ratioDetails) { + this.ratioDetails = ratioDetails; + Waveform numeratorWave = ratioDetails.getNumerWaveform(); + Waveform denominatorWave = ratioDetails.getDenomWaveform(); + this.numeratorSeries = getTimeSeriesFromWaveform(numeratorWave); + this.denominatorSeries = getTimeSeriesFromWaveform(denominatorWave); + } + + public SpectraRatioPairOperator getRatioDetails() { + return this.ratioDetails; + } + + public void plotRatio() { + if (this.ratioDetails == null) { + return; + } + + clear(); // Clear previous waveforms and picks + this.diffAvg = ratioDetails.getDiffAvg(); + Waveform numeratorWave = ratioDetails.getNumerWaveform(); + Waveform denominatorWave = ratioDetails.getDenomWaveform(); + + if (numeratorWave != null && denominatorWave != null) { + String numEventId = numeratorWave.getEvent().getEventId(); + String denEventId = denominatorWave.getEvent().getEventId(); + String stationName = ratioDetails.getStation().getStationName(); + String frequency = String.valueOf(ratioDetails.getFrequency().getLowFrequency()); + plotIdentifier = String.format("%s_over_%s-%s_%s", numEventId, denEventId, stationName, frequency); + + double offsetAmountToUse = 0.0; // Note this offset is only applied to the denominator wave + peakOffset = ratioDetails.getDenomStartCutSec() - ratioDetails.getNumerStartCutSec(); + + // If alignPeaks is true we need to move the denominator by peakOffset + if (this.alignPeaks) { + offsetAmountToUse = peakOffset; + } + + // Label the peak offset + if (peakOffset != 0.0) { + double numerPeak = numeratorSeries.getMax(); + double denomPeak = denominatorSeries.getMax(); + double peakOffsetLabelY = (numerPeak + denomPeak) / 2; + + if (peakOffset < 0) { + addPlotObject( + createHorizontalSegment( + String.format("True Time Offset: %s s", dfmt4.format(peakOffset)), + peakOffsetLabelY, + ratioDetails.getDenomStartCutSec(), + ratioDetails.getNumerStartCutSec(), + ratioValColor, + LineStyles.DASH_DOT), + PLOT_ORDERING.PICK_LINES.getZOrder()); + } else { + addPlotObject( + createHorizontalSegment( + String.format("True Time Offset: %s s", dfmt4.format(peakOffset)), + peakOffsetLabelY, + ratioDetails.getNumerStartCutSec(), + ratioDetails.getDenomStartCutSec(), + ratioValColor, + LineStyles.DASH_DOT), + PLOT_ORDERING.PICK_LINES.getZOrder()); + } + + if (alignPeaks) { + addPlotObject(new VerticalLine(ratioDetails.getDenomStartCutSec(), 100, "Denominator Actual Start Cut Time", ratioValColor, 1, false, false)); + } + } + + List plotSegments = new ArrayList<>(); + plotSegments.add(numeratorWave.getSegment()); + plotSegments.add(denominatorWave.getSegment()); + plotSegments.add(ratioDetails.getDiffSegment()); + + final MinMax minMax = getMinMaxFromSegments(plotSegments); + double min = minMax.getMin(); + min = min - Math.abs(minMax.getRange() * plotPadding); + double max = minMax.getMax(); + max = max + Math.abs(minMax.getRange() * plotPadding); + + // Adjust zoom level if zoomed + if (this.isZoomed()) { + try { + // Get the min/max y values within the subsection of the xAxis + final double xMin = this.getxAxis().getMin(); + final double xMax = this.getxAxis().getMax(); + final double numerStartTime = Math.abs(ratioDetails.getNumerWaveStartSec()); + final double denomStartTime = Math.abs(ratioDetails.getDenomWaveStartSec()); + + TimeSeries zoomedNumerSection = new TimeSeries(numeratorSeries); + TimeSeries zoomedDenomSection = new TimeSeries(denominatorSeries); + zoomedNumerSection.cut(xMin + numerStartTime, xMax + numerStartTime); + zoomedDenomSection.cut(xMin + denomStartTime, xMax + denomStartTime); + + List zoomedSegments = new ArrayList<>(); + zoomedSegments.add(floatsToDoubles(zoomedNumerSection.getData())); + zoomedSegments.add(floatsToDoubles(zoomedDenomSection.getData())); + zoomedSegments.add(ratioDetails.getDiffSegment()); + + final MinMax yZoomRange = getMinMaxFromSegments(zoomedSegments); + min = yZoomRange.getMin(); + min = min - Math.abs(yZoomRange.getRange() * plotPadding); + max = yZoomRange.getMax(); + max = max + Math.abs(yZoomRange.getRange() * plotPadding); + + this.xAxis.setMin(xMin); + this.xAxis.setMax(xMax); + } catch (Exception e) { + // Don't zoom + this.resetAxisLimits(); + } + } + + this.yAxis.setMin(min); + this.yAxis.setMax(max); + + plotWaveform(numeratorWave, 0.0, "Numerator Wave", PLOT_ORDERING.NUMER_WAVEFORM.getZOrder(), numeratorColor); + plotWaveform(denominatorWave, offsetAmountToUse, "Denominator Wave", PLOT_ORDERING.DENOM_WAVEFORM.getZOrder(), denominatorColor); + + final float[] diffSegment = doublesToFloats(ratioDetails.getDiffSegment()); + final float[] numerSegment = doublesToFloats(ratioDetails.getNumeratorCutSegment()); + final float[] denomSegment = doublesToFloats(ratioDetails.getDenominatorCutSegment()); + + final TimeT numerOriginTime = new TimeT(ratioDetails.getNumeratorEventOriginTime()); + final TimeT denomOriginTime = new TimeT(ratioDetails.getDenominatorEventOriginTime()); + final TimeT numerStartCutTime = new TimeT(ratioDetails.getNumerStartCutSec()).add(numerOriginTime); + final TimeT denomStartCutTime = new TimeT(ratioDetails.getDenomStartCutSec()).add(denomOriginTime); + + plotTimeSeries( + numerSegment, + numeratorWave.getSampleRate(), + numerOriginTime, + numerStartCutTime, + "Numerator Cut", + PLOT_ORDERING.DIFF_WAVEFORM.getZOrder(), + numeratorCutColor, + true, + !this.alignPeaks, + false); + plotTimeSeries( + denomSegment, + denominatorWave.getSampleRate(), + denomOriginTime.add(offsetAmountToUse), + denomStartCutTime, + "Denominator Cut", + PLOT_ORDERING.DIFF_WAVEFORM.getZOrder(), + denominatorCutColor, + false, + !this.alignPeaks, + false); + if (this.alignPeaks) { + plotTimeSeries(diffSegment, numeratorWave.getSampleRate(), numerOriginTime, numerStartCutTime, "Diff Wave", PLOT_ORDERING.DIFF_WAVEFORM.getZOrder(), diffColor, true, true, true); + } + replot(); + } + } + + public void plotDiffRatio() { + if (this.ratioDetails == null) { + return; + } + + this.clearAxes(); + this.showLegend(false); + this.setMargin(5, 5, 5, 5); + Waveform numeratorWave = ratioDetails.getNumerWaveform(); + + if (numeratorWave != null) { + clear(); // Clear previous waveforms and picks + this.diffAvg = ratioDetails.getDiffAvg(); + + List plotSegments = new ArrayList<>(); + plotSegments.add(ratioDetails.getDiffSegment()); + + final MinMax minMax = getMinMaxFromSegments(plotSegments); + double min = minMax.getMin(); + min = min - Math.abs(minMax.getRange()); + double max = minMax.getMax(); + max = max + Math.abs(minMax.getRange()); + + this.yAxis.setMin(min); + this.yAxis.setMax(max); + + final float[] diffSegment = doublesToFloats(ratioDetails.getDiffSegment()); + final TimeT numerOriginTime = new TimeT(ratioDetails.getNumeratorEventOriginTime()); + final TimeT numerStartCutTime = new TimeT(ratioDetails.getNumerStartCutSec()).add(numerOriginTime); + + plotTimeSeries(diffSegment, numeratorWave.getSampleRate(), numerOriginTime, numerStartCutTime, "Diff Wave", PLOT_ORDERING.DIFF_WAVEFORM.getZOrder(), diffColor, true, true, true); + replot(); + } + } + + public void plotWaveform(final Waveform waveform, final double peakOffset, final String waveformName, int zOrder, final Color lineColor) { + if (waveform != null && waveform.hasData() && waveform.getBeginTime() != null) { + final TimeT beginTime = new TimeT(waveform.getBeginTime()); + final TimeT originTime = new TimeT(waveform.getEvent().getOriginTime()).add(peakOffset); + + final float[] waveformSegment = doublesToFloats(waveform.getSegment()); + plotTimeSeries(waveformSegment, waveform.getSampleRate(), originTime, beginTime, waveformName, zOrder, lineColor, false, false, false); + } + } + + private void plotTimeSeries(float[] segment, double sampleRate, TimeT originTime, TimeT beginTime, String identifier, int zOrder, Color lineColor, boolean isNumerator, boolean plotStartEndLines, + boolean plotRatioValue) { + final TimeSeries rawSeries = new TimeSeries(segment, sampleRate, beginTime); + double originTimeZeroOffset = beginTime.subtractD(originTime); + + rawSeries.setIdentifier(identifier); + rawSeries.setZeroTimeOffsetSeconds(originTimeZeroOffset); + addLine(rawSeries, zOrder, lineColor); + + double startTime = beginTime.subtractD(originTime); + double endTime = rawSeries.getLengthInSeconds() + startTime; + + if (plotStartEndLines && peakOffset != 0.0) { + + if (!this.alignPeaks) { + Color rectangleColor = verticalLineColor2; + String title = DENOMINATOR_CUT_LABEL; + if (isNumerator) { + rectangleColor = verticalLineColor1; + title = NUMERATOR_CUT_LABEL; + } + final Rectangle rect = new Rectangle(startTime, + endTime, + DRAGGABLE_LINE_WIDTH, + 100, + title, + rectangleColor.deriveColor(0, 1, 1, 0.9), + rectangleColor.deriveColor(0, 1, 1, 0.05), + true, + false); + addPlotObject(rect); + } else { + Color verticalColor = verticalLineColor2; + String startTitle = DENOMINATOR_START_CUT_LABEL; + String endTitle = DENOMINATOR_END_CUT_LABEL; + if (isNumerator) { + verticalColor = verticalLineColor1; + startTitle = NUMERATOR_START_CUT_LABEL; + endTitle = NUMERATOR_END_CUT_LABEL; + } + final VerticalLine line1 = new VerticalLine(startTime, 100, startTitle, verticalColor.deriveColor(0, 1, 1, 0.8), DRAGGABLE_LINE_WIDTH, true, false); + final VerticalLine line2 = new VerticalLine(endTime, 100, endTitle, verticalColor.deriveColor(0, 1, 1, 0.8), DRAGGABLE_LINE_WIDTH, true, false); + + addPlotObject(line1); + addPlotObject(line2); + } + + } + if (plotRatioValue) { + addPlotObject( + createHorizontalSegment(String.format("Ratio Value: %s", dfmt4.format(diffAvg)), diffAvg, startTime, endTime, ratioValColor, LineStyles.DASH_DOT), + PLOT_ORDERING.PICK_LINES.getZOrder()); + } + } + + public Line addLine(final TimeSeries timeSeries, final int zOrder, final Color lineColor) { + final Line line = new BasicLine(timeSeries.getIdentifier(), + timeSeries.getZeroTimeOffsetSeconds(), + timeSeries.getDelta(), + timeSeries.getData(), + lineColor, + LineStyles.SOLID, + DEFAULT_LINE_WIDTH); + addPlotObject(line, zOrder); + return line; + } + + private TimeSeries getTimeSeriesFromWaveform(Waveform wave) { + final TimeT denomBeginTime = new TimeT(wave.getBeginTime()); + final float[] waveSegment = doublesToFloats(wave.getSegment()); + return new TimeSeries(waveSegment, wave.getSampleRate(), denomBeginTime); + } + + public static float[] doublesToFloats(double[] x) { + float[] xfloats = new float[x.length]; + IntStream.range(0, x.length).parallel().forEach(i -> xfloats[i] = (float) x[i]); + return xfloats; + } + + public static double[] floatsToDoubles(float[] x) { + double[] xdoubles = new double[x.length]; + IntStream.range(0, x.length).parallel().forEach(i -> xdoubles[i] = x[i]); + return xdoubles; + } + + private MinMax getMinMaxFromSegments(List segments) { + Double min = Double.POSITIVE_INFINITY; + Double max = Double.NEGATIVE_INFINITY; + + for (double[] segment : segments) { + for (double value : segment) { + if (value < min) { + min = value; + } + if (value > max) { + max = value; + } + } + } + + return new MinMax(min, max); + } + + /*** + * Adjust the start cut time for both numerator and denominator waves using + * the xValue + * + * @param xValue + * The time in seconds to adjust the cuts by (5.0 would move the + * cuts forward 5 seconds, -3.0 would move back 3 seconds. + */ + private void adjustStartCuts(double xValue) { + + // Calculate the start cut times + double currentTime = ratioDetails.getNumerStartCutSec(); + + double startDiff = currentTime - xValue; + double newNumerStartCutSec = ratioDetails.getNumerStartCutSec() - startDiff; + double newDenomStartCutSec = ratioDetails.getDenomStartCutSec() - startDiff; + + TimeT numerOriginTime = new TimeT(ratioDetails.getNumeratorEventOriginTime()); + TimeT denomOriginTime = new TimeT(ratioDetails.getDenominatorEventOriginTime()); + + // If start cut times are before wave start times, set to start of wave + int numerStartIdx = 0; + int denomStartIdx = 0; + + if (newNumerStartCutSec > ratioDetails.getNumerEndCutSec()) { + newNumerStartCutSec = ratioDetails.getNumerEndCutSec() - 1; + } else if (newNumerStartCutSec < ratioDetails.getNumerWaveStartSec()) { + newNumerStartCutSec = ratioDetails.getNumerWaveStartSec(); + } else { + double newNumerTime = new TimeT(newNumerStartCutSec).add(numerOriginTime).getEpochTime(); + numerStartIdx = numeratorSeries.getIndexForTime(newNumerTime); + } + + if (newDenomStartCutSec > ratioDetails.getDenomEndCutSec()) { + newDenomStartCutSec = ratioDetails.getDenomEndCutSec() - 1; + } else if (newDenomStartCutSec < ratioDetails.getDenomWaveStartSec()) { + newDenomStartCutSec = ratioDetails.getDenomWaveStartSec(); + } else { + double newDenomTime = new TimeT(newDenomStartCutSec).add(denomOriginTime).getEpochTime(); + denomStartIdx = denominatorSeries.getIndexForTime(newDenomTime); + } + + this.ratioDetails.setNumerStartCutSec(newNumerStartCutSec); + this.ratioDetails.setDenomStartCutSec(newDenomStartCutSec); + this.ratioDetails.updateCutTimesAndRecalculateDiff(numerStartIdx, denomStartIdx, ratioDetails.getNumerEndCutIdx(), ratioDetails.getDenomEndCutIdx()); + } + + /*** + * Adjust the end cut time for both numerator and denominator waves using + * the xValue + * + * @param xValue + * The time in seconds to adjust the cuts by (5.0 would move the + * cuts forward 5 seconds, -3.0 would move back 3 seconds. + */ + private void adjustEndCuts(double xValue) { + + // Calculate the end cut times + double currentTime = ratioDetails.getNumerEndCutSec(); + + double endDiff = currentTime - xValue; + double newNumerEndCutSec = ratioDetails.getNumerEndCutSec() - endDiff; + double newDenomEndCutSec = ratioDetails.getDenomEndCutSec() - endDiff; + + TimeT numerOriginTime = new TimeT(ratioDetails.getNumeratorEventOriginTime()); + TimeT denomOriginTime = new TimeT(ratioDetails.getDenominatorEventOriginTime()); + + // If end cut times are after wave end times, set to end of wave + int numerEndIdx = numeratorSeries.getLength() - 1; + int denomEndIdx = denominatorSeries.getLength() - 1; + + if (newNumerEndCutSec > ratioDetails.getNumerWaveEndSec()) { + newNumerEndCutSec = ratioDetails.getNumerWaveEndSec(); + } else if (newNumerEndCutSec < ratioDetails.getNumerStartCutSec()) { + newNumerEndCutSec = ratioDetails.getNumerStartCutSec() + 1; + } else { + double newNumerTime = new TimeT(newNumerEndCutSec).add(numerOriginTime).getEpochTime(); + numerEndIdx = numeratorSeries.getIndexForTime(newNumerTime); + } + + if (newDenomEndCutSec > ratioDetails.getDenomWaveEndSec()) { + newDenomEndCutSec = ratioDetails.getDenomWaveEndSec(); + } else if (newDenomEndCutSec < ratioDetails.getDenomStartCutSec()) { + newDenomEndCutSec = ratioDetails.getDenomStartCutSec() + 1; + } else { + double newDenomTime = new TimeT(newDenomEndCutSec).add(denomOriginTime).getEpochTime(); + denomEndIdx = denominatorSeries.getIndexForTime(newDenomTime); + } + + this.ratioDetails.setNumerEndCutSec(newNumerEndCutSec); + this.ratioDetails.setDenomEndCutSec(newDenomEndCutSec); + this.ratioDetails.updateCutTimesAndRecalculateDiff(ratioDetails.getNumerStartCutIdx(), ratioDetails.getDenomStartCutIdx(), numerEndIdx, denomEndIdx); + } + + /*** + * Shift the start time of the specified cut while keeping the length the + * same. The start time will be adjusted to keep the entire cut within the + * waveform + * + * @param xValue + * The time that the start of the cut should be + * @param shiftNumerator + * If true the numerator cut will be shifted otherwise the + * denominator will be shifted + */ + private void shiftCutByTime(double xValue, boolean shiftNumerator) { + double cutLengthSec; + double minTime; + double maxTime; + + // Calculate time boundary + if (shiftNumerator) { + cutLengthSec = ratioDetails.getNumerEndCutSec() - ratioDetails.getNumerStartCutSec(); + minTime = ratioDetails.getNumerWaveStartSec(); + maxTime = ratioDetails.getNumerWaveEndSec() - cutLengthSec; + } else { + cutLengthSec = ratioDetails.getDenomEndCutSec() - ratioDetails.getDenomStartCutSec(); + minTime = ratioDetails.getDenomWaveStartSec(); + maxTime = ratioDetails.getDenomWaveEndSec() - cutLengthSec; + } + + // Set the new time and make sure it's in-bounds + double newStartTime = xValue; + if (newStartTime < minTime) { + newStartTime = minTime; + } else if (newStartTime > maxTime) { + newStartTime = maxTime; + } + + // Get new end cut time + double newEndTime = newStartTime + cutLengthSec; + + // Update cut using new times + TimeT waveOriginTime = null; + int newStartIdx = 0; + int newEndIdx = 0; + if (shiftNumerator) { + // Shift numerator cut + ratioDetails.setNumerStartCutSec(newStartTime); + ratioDetails.setNumerEndCutSec(newEndTime); + waveOriginTime = new TimeT(ratioDetails.getNumeratorEventOriginTime()); + newStartIdx = numeratorSeries.getIndexForTime(new TimeT(newStartTime).add(waveOriginTime).getEpochTime()); + newEndIdx = numeratorSeries.getIndexForTime(new TimeT(newEndTime).add(waveOriginTime).getEpochTime()); + ratioDetails.updateCutTimesAndRecalculateDiff(newStartIdx, ratioDetails.getDenomStartCutIdx(), newEndIdx, ratioDetails.getDenomEndCutIdx()); + } else { + // Shift denominator cut + ratioDetails.setDenomStartCutSec(newStartTime); + ratioDetails.setDenomEndCutSec(newEndTime); + waveOriginTime = new TimeT(ratioDetails.getDenominatorEventOriginTime()); + newStartIdx = denominatorSeries.getIndexForTime(new TimeT(newStartTime).add(waveOriginTime).getEpochTime()); + newEndIdx = denominatorSeries.getIndexForTime(new TimeT(newEndTime).add(waveOriginTime).getEpochTime()); + ratioDetails.updateCutTimesAndRecalculateDiff(ratioDetails.getNumerStartCutIdx(), newStartIdx, ratioDetails.getNumerEndCutIdx(), newEndIdx); + } + } + + /*** + * Reset the start and end cuts for the ratio plot to be the original + * values. + */ + public void resetCuts() { + TimeT numerOriginTime = new TimeT(ratioDetails.getNumeratorEventOriginTime()); + TimeT denomOriginTime = new TimeT(ratioDetails.getDenominatorEventOriginTime()); + + this.ratioDetails.resetToPeakAndFMarkerCut(); + + int numerStartIdx = numeratorSeries.getIndexForTime(new TimeT(ratioDetails.getNumerStartCutSec()).add(numerOriginTime).getEpochTime()); + int denomStartIdx = denominatorSeries.getIndexForTime(new TimeT(ratioDetails.getDenomStartCutSec()).add(denomOriginTime).getEpochTime()); + int numerEndIdx = numeratorSeries.getIndexForTime(new TimeT(ratioDetails.getNumerEndCutSec()).add(numerOriginTime).getEpochTime()); + int denomEndIdx = denominatorSeries.getIndexForTime(new TimeT(ratioDetails.getDenomEndCutSec()).add(denomOriginTime).getEpochTime()); + + this.ratioDetails.updateCutTimesAndRecalculateDiff(numerStartIdx, denomStartIdx, numerEndIdx, denomEndIdx); + if (this.cutSegmentChangeListener != null) { + CompletableFuture.runAsync(() -> { + cutSegmentProperty.firePropertyChange(new PropertyChangeEvent(this, "segment_change", null, null)); + }); + } + } + + @Override + protected void handlePickMovedState(PlotShapeMove move) { + if (move.getName() != null) { + try { + switch (move.getName()) { + case NUMERATOR_START_CUT_LABEL: + log.trace("Numerator start cut moved."); + adjustStartCuts(move.getX0()); + break; + case NUMERATOR_END_CUT_LABEL: + log.trace("Numerator end cut moved."); + adjustEndCuts(move.getX0()); + break; + case NUMERATOR_CUT_LABEL: + log.trace("Numerator cut moved."); + shiftCutByTime(move.getX0(), true); + break; + case DENOMINATOR_CUT_LABEL: + log.trace("Denominator cut moved."); + shiftCutByTime(move.getX0(), false); + break; + default: + log.trace("No cut moved."); + } + } catch (ClassCastException e) { + log.info("Error updating Waveform, {}", move.getName(), e); + } + } + + if (this.cutSegmentChangeListener != null) { + CompletableFuture.runAsync(() -> { + cutSegmentProperty.firePropertyChange(new PropertyChangeEvent(this, "segment_change", null, move)); + }); + } + } + + public void setCutSegmentChangeListener(PropertyChangeListener cutSegmentChange) { + // Remove existing listener before adding another one + if (this.cutSegmentChangeListener != null) { + this.cutSegmentProperty.removePropertyChangeListener("segment_change", this.cutSegmentChangeListener); + } + + this.cutSegmentChangeListener = cutSegmentChange; + this.cutSegmentProperty.addPropertyChangeListener("segment_change", this.cutSegmentChangeListener); + } + + @Override + protected void handleAxisChange(PlotAxisChange change) { + if (this.axisChangeListener != null) { + CompletableFuture.runAsync(() -> { + axisProperty.firePropertyChange(new PropertyChangeEvent(this, "axis_change", null, change)); + }); + } + + if (change.isReset()) { + this.resetAxisLimits(); + return; + } + + setAxisLimits(change.getAxisLimits().getFirst(), change.getAxisLimits().getSecond()); + } + + public void setAxisChangeListener(PropertyChangeListener axisChange) { + // Remove existing listener before adding another one + if (this.axisChangeListener != null) { + this.axisProperty.removePropertyChangeListener("axis_change", this.axisChangeListener); + } + + this.axisChangeListener = axisChange; + this.axisProperty.addPropertyChangeListener("axis_change", this.axisChangeListener); + } + + public void resetAxisLimits() { + this.setAxisLimits(new AxisLimits(Axis.Type.X, 0.0, 0.0), new AxisLimits(Axis.Type.Y, 0.0, 0.0)); + } + + public boolean isZoomed() { + return this.xAxis.getMin() != this.xAxis.getMax(); + } + + private Line createHorizontalSegment(final String label, final double yValue, final double xStart, final double xEnd, final Color lineColor, final LineStyles lineStyle) { + final float[] data = new float[2]; + data[0] = (float) yValue; + data[1] = (float) yValue; + double length = xEnd - xStart; + return new BasicLine(label, xStart, length, data, lineColor, lineStyle, 2); + } + + public String getPlotIdentifier() { + return plotIdentifier; + } + + public Axis getxAxis() { + return xAxis; + } + + public Axis getyAxis() { + return yAxis; + } + + public boolean isAlignPeaks() { + return alignPeaks; + } + + public void setAlignPeaks(boolean alignPeaks) { + this.alignPeaks = alignPeaks; + } + + private void addPlotObject(final PlotObject object, final int zOrder) { + if (object != null) { + object.setZindex(zOrder); + } + addPlotObject(object); + } +} diff --git a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/plotting/RatioMeasurementSpectraPlotManager.java b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/plotting/RatioMeasurementSpectraPlotManager.java new file mode 100644 index 00000000..91add010 --- /dev/null +++ b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/plotting/RatioMeasurementSpectraPlotManager.java @@ -0,0 +1,1140 @@ +/* +* Copyright (c) 2023, Lawrence Livermore National Security, LLC. Produced at the Lawrence Livermore National Laboratory +* CODE-743439. +* All rights reserved. +* This file is part of CCT. For details, see https://github.com/LLNL/coda-calibration-tool. +* +* 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.plotting; + +import java.beans.PropertyChangeListener; +import java.io.File; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.text.NumberFormat; +import java.time.Duration; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import gov.llnl.gnem.apps.coda.calibration.gui.controllers.SpectraRatioPlotController; +import gov.llnl.gnem.apps.coda.calibration.gui.data.client.api.SpectraClient; +import gov.llnl.gnem.apps.coda.calibration.gui.data.exporters.SpectraRatioExporter; +import gov.llnl.gnem.apps.coda.calibration.gui.util.FileDialogs; +import gov.llnl.gnem.apps.coda.calibration.model.domain.Spectra; +import gov.llnl.gnem.apps.coda.common.gui.plotting.LabeledPlotPoint; +import gov.llnl.gnem.apps.coda.common.gui.plotting.PlotPoint; +import gov.llnl.gnem.apps.coda.common.gui.plotting.SymbolStyleMapFactory; +import gov.llnl.gnem.apps.coda.common.gui.util.NumberFormatFactory; +import gov.llnl.gnem.apps.coda.common.gui.util.SnapshotUtils; +import gov.llnl.gnem.apps.coda.common.mapping.api.Icon; +import gov.llnl.gnem.apps.coda.common.model.domain.FrequencyBand; +import gov.llnl.gnem.apps.coda.common.model.domain.Pair; +import gov.llnl.gnem.apps.coda.common.model.domain.Station; +import gov.llnl.gnem.apps.coda.common.model.domain.Waveform; +import gov.llnl.gnem.apps.coda.common.model.messaging.Result; +import gov.llnl.gnem.apps.coda.common.model.util.SPECTRA_TYPES; +import gov.llnl.gnem.apps.coda.spectra.model.domain.SpectraRatioPairDetails; +import gov.llnl.gnem.apps.coda.spectra.model.domain.SpectraRatioPairInversionResult; +import gov.llnl.gnem.apps.coda.spectra.model.domain.SpectraRatioPairInversionResultJoint; +import gov.llnl.gnem.apps.coda.spectra.model.domain.messaging.EventPair; +import gov.llnl.gnem.apps.coda.spectra.model.domain.util.SpectraRatioPairOperator; +import gov.llnl.gnem.apps.coda.spectra.model.domain.util.SpectraRatiosReportByEventPair; +import javafx.application.Platform; +import javafx.beans.property.Property; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.collections.transformation.SortedList; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.geometry.Point2D; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.ComboBox; +import javafx.scene.control.ContentDisplay; +import javafx.scene.control.Label; +import javafx.scene.control.ListCell; +import javafx.scene.input.KeyEvent; +import javafx.scene.layout.StackPane; +import javafx.scene.paint.Color; +import javafx.scene.text.Font; +import javafx.stage.DirectoryChooser; +import javafx.stage.Stage; +import javafx.stage.StageStyle; +import llnl.gnem.core.gui.plotting.api.Axis; +import llnl.gnem.core.gui.plotting.api.Axis.Type; +import llnl.gnem.core.gui.plotting.api.BasicPlot; +import llnl.gnem.core.gui.plotting.api.ColorMaps; +import llnl.gnem.core.gui.plotting.api.Line; +import llnl.gnem.core.gui.plotting.api.LineStyles; +import llnl.gnem.core.gui.plotting.api.PlotFactory; +import llnl.gnem.core.gui.plotting.api.PlotObject; +import llnl.gnem.core.gui.plotting.api.SymbolStyles; +import llnl.gnem.core.gui.plotting.api.VerticalLine; +import llnl.gnem.core.gui.plotting.events.PlotObjectClick; +import llnl.gnem.core.gui.plotting.plotly.PlotObjectData; +import llnl.gnem.core.gui.plotting.plotly.PlotTrace; +import llnl.gnem.core.util.Geometry.EModel; +import llnl.gnem.core.util.Geometry.GeodeticCoordinate; + +public class RatioMeasurementSpectraPlotManager { + + private static final String CONTOUR_COLOR_MAP = ColorMaps.VIRIDIS.getColorMap(); + private static final String X_AXIS_LABEL = "center freq (Hz)"; + private static final String SPECTRA_RATIO_PREFIX = "Spectra_Ratio_"; + private static final String PAIR_MOMENT_PREFIX = "Pair_Moment_"; + private static final String PAIR_STRESS_PREFIX = "Pair_Stress_"; + private static final String JOINT_MOMENT_PREFIX = "Joint_Moment_"; + private static final String JOINT_STRESS_PREFIX = "Joint_Stress_"; + private final String RATIO_AVG_LABEL = "Ratio Avg"; + private final NumberFormat dfmt4 = NumberFormatFactory.fourDecimalOneLeadingZero(); + private final NumberFormat dfmt2 = NumberFormatFactory.twoDecimalOneLeadingZero(); + private static final Logger log = LoggerFactory.getLogger(RatioMeasurementSpectraPlotManager.class); + + private final DirectoryChooser screenshotFolderChooser = new DirectoryChooser(); + + private SpectraRatiosReportByEventPair ratioMeasurementReport = new SpectraRatiosReportByEventPair(); + private final SymbolStyleMapFactory symbolStyleMapFactory; + private MapPlottingUtilities iconFactory; + private CertLeafletMapController mapImpl; + + private Map symbolMap; + private List spectraRatioPairOperatorList; + private Map symbolStyleMap; + private SpectraClient spectraClient; + + private final Property shouldFocus = new SimpleBooleanProperty(false); + + private SpectraRatioPlotController ratioSpectralPlotController; + + private final ObservableList eventPairList = FXCollections.observableArrayList(); + private final SortedList sortedEventPairList = new SortedList<>(eventPairList); + + private final BiConsumer eventSelectionCallback; + private final BiConsumer stationSelectionCallback; + + @FXML + private StackPane borderPane; + + @FXML + private StackPane spectraRatioPlotNode; + + @FXML + private StackPane pairStressPlotNode; + + @FXML + private StackPane pairMomentPlotNode; + + @FXML + private StackPane jointStressPlotNode; + + @FXML + private StackPane jointMomentPlotNode; + + @FXML + private Button snapshotButton; + + @FXML + private Button downloadButton; + + @FXML + private Button showMapButton; + + @FXML + private ComboBox eventPairComboBox; + + @FXML + private Label topTitleLabel; + + private PlotFactory plotFactory; + + private PlotObjectData jointStressContourPlotData; + private BasicPlot jointStressContourPlot; + + private PlotObjectData jointMomentContourPlotData; + private BasicPlot jointMomentContourPlot; + + private PlotObjectData stressContourPlotData; + private BasicPlot stressContourPlot; + + private PlotObjectData momentContourPlotData; + private BasicPlot momentContourPlot; + + private Axis jointStressYaxis; + private Axis jointStressXaxis; + private PlotObjectData jointStressPointPlotData; + private PlotObjectData bestJointStressPointPlotData; + + private Axis jointMomentXaxis; + private Axis jointMomentYaxis; + private PlotObjectData jointMomentPointPlotData; + private PlotObjectData bestJointMomentPointPlotData; + + private Axis stressYaxis; + private Axis stressXaxis; + private PlotObjectData stressPointPlotData; + private PlotObjectData bestStressPointPlotData; + + private Axis momentXaxis; + private Axis momentYaxis; + private PlotObjectData momentPointPlotData; + private PlotObjectData bestMomentPointPlotData; + + private SpectraRatioExporter spectraRatioExporter; + + public RatioMeasurementSpectraPlotManager(final SymbolStyleMapFactory styleFactory, CertLeafletMapController mapImpl, MapPlottingUtilities iconFactory, SpectraClient spectraClient, + SpectraRatioExporter spectraRatioExporter) { + this.symbolStyleMapFactory = styleFactory; + this.mapImpl = mapImpl; + this.iconFactory = iconFactory; + this.symbolMap = new HashMap<>(); + this.spectraClient = spectraClient; + this.spectraRatioExporter = spectraRatioExporter; + this.ratioSpectralPlotController = new SpectraRatioPlotController(SpectraRatioPairOperator::getDiffAvg); + plotFactory = new PlotlyPlotFactory(); + + eventSelectionCallback = (selected, eventId) -> { + log.debug(eventId); + }; + + stationSelectionCallback = (selected, stationId) -> { + log.debug(stationId); + }; + } + + @FXML + public void initialize() { + final Label label = new Label("\uE3B0"); + label.getStyleClass().add("material-icons-medium"); + label.setMaxHeight(16); + label.setMinWidth(16); + snapshotButton.setGraphic(label); + snapshotButton.setContentDisplay(ContentDisplay.CENTER); + + Label mapLabel = new Label("\uE55B"); + mapLabel.getStyleClass().add("material-icons-medium"); + mapLabel.setMaxHeight(16); + mapLabel.setMinWidth(16); + showMapButton.setGraphic(mapLabel); + + final Label downloadLabel = new Label("\uE884"); + downloadLabel.getStyleClass().add("material-icons-medium"); + downloadLabel.setMaxHeight(16); + downloadLabel.setMinWidth(16); + downloadButton.setGraphic(downloadLabel); + + sortedEventPairList.setComparator((p1, p2) -> { + int compare = 0; + if (Objects.equals(p1, p2)) { + compare = 0; + } else if (p1 == null) { + compare = -1; + } else if (p2 == null) { + compare = 1; + } else { + compare = StringUtils.compare(p1.getY().getEventId(), p2.getY().getEventId()); + if (compare == 0) { + compare = StringUtils.compare(p1.getX().getEventId(), p2.getX().getEventId()); + } + } + return compare; + }); + + eventPairComboBox.setItems(sortedEventPairList); + eventPairComboBox.setCellFactory(lv -> new ListCell() { + @Override + protected void updateItem(EventPair pair, boolean empty) { + super.updateItem(pair, empty); + setText(pair == null ? null : pair.getY().getEventId() + " / " + pair.getX().getEventId()); + } + }); + eventPairComboBox.setButtonCell(new ListCell() { + @Override + protected void updateItem(EventPair pair, boolean empty) { + super.updateItem(pair, empty); + setText(pair == null ? null : pair.getY().getEventId() + " / " + pair.getX().getEventId()); + } + }); + + eventPairComboBox.getSelectionModel().selectedItemProperty().addListener((obs, old, event) -> { + if (event != null) { + plotStationData(event); + } + }); + + screenshotFolderChooser.setTitle("Spectra Plot Screenshot Export Folder"); + spectraRatioPlotNode.setOnKeyReleased(this::triggerKeyEvent); + + ratioSpectralPlotController.setShowCornerFrequencies(true); + ratioSpectralPlotController.setYAxisResizable(true); + ratioSpectralPlotController.setShouldShowFits(true); + + SpectralPlot plot = getRatioSpectraPlot(); + plot.setLabels("Seismic Envelope Ratio Spectra", X_AXIS_LABEL, RATIO_AVG_LABEL); + plot.getSubplot().addPlotObjectObserver(getPlotpointObserver(ratioSpectralPlotController::getSpectraDataMap)); + plot.getSubplot().setMargin(65, 40, 50, null); + plot.getSubplot().attachToDisplayNode(spectraRatioPlotNode); + + jointStressContourPlot = plotFactory.basicPlot(); + jointStressContourPlot.setColorMap(CONTOUR_COLOR_MAP); + Map plotData = jointStressContourPlot.getPlotTypes(); + + jointStressContourPlotData = new PlotObjectData(new PlotTrace(PlotTrace.Style.CONTOUR)); + jointStressPointPlotData = new PlotObjectData(new PlotTrace(PlotTrace.Style.SCATTER_MARKER)); + bestJointStressPointPlotData = new PlotObjectData(new PlotTrace(PlotTrace.Style.SCATTER_MARKER)); + bestJointStressPointPlotData.getTraceStyle().setSeriesName("Estimate"); + bestJointStressPointPlotData.getTraceStyle().setFillColor(Color.WHITE); + bestJointStressPointPlotData.getTraceStyle().setEdgeColor(Color.BLACK); + bestJointStressPointPlotData.getTraceStyle().setzIndex(2); + bestJointStressPointPlotData.getTraceStyle().setPxSize(10); + bestJointStressPointPlotData.getTraceStyle().setStyleName(SymbolStyles.STAR.getStyleName()); + + jointStressContourPlotData.getTraceStyle().setColorMap(CONTOUR_COLOR_MAP); + jointStressPointPlotData.getTraceStyle().setColorMap(CONTOUR_COLOR_MAP); + jointStressPointPlotData.getTraceStyle().setEdgeColor(Color.BLACK); + jointStressPointPlotData.getTraceStyle().setSeriesName("Samples"); + jointStressPointPlotData.getTraceStyle().setzIndex(1); + + plotData.put(jointStressContourPlotData.getTraceStyle().getType().getType(), jointStressContourPlotData); + plotData.put(jointStressPointPlotData.getTraceStyle().getSeriesName(), jointStressPointPlotData); + plotData.put(bestJointStressPointPlotData.getTraceStyle().getSeriesName(), bestJointStressPointPlotData); + + jointStressXaxis = new PlotlyPlotFactory().axis(Type.LOG_X, ""); + jointStressYaxis = new PlotlyPlotFactory().axis(Type.LOG_Y, ""); + jointStressContourPlot.addAxes(jointStressXaxis); + jointStressContourPlot.addAxes(jointStressYaxis); + jointStressContourPlot.attachToDisplayNode(jointStressPlotNode); + + jointMomentContourPlot = plotFactory.basicPlot(); + jointMomentContourPlot.setColorMap(CONTOUR_COLOR_MAP); + plotData = jointMomentContourPlot.getPlotTypes(); + + jointMomentContourPlotData = new PlotObjectData(new PlotTrace(PlotTrace.Style.CONTOUR)); + jointMomentPointPlotData = new PlotObjectData(new PlotTrace(PlotTrace.Style.SCATTER_MARKER)); + bestJointMomentPointPlotData = new PlotObjectData(new PlotTrace(PlotTrace.Style.SCATTER_MARKER)); + bestJointMomentPointPlotData.getTraceStyle().setSeriesName("Estimate"); + bestJointMomentPointPlotData.getTraceStyle().setFillColor(Color.WHITE); + bestJointMomentPointPlotData.getTraceStyle().setEdgeColor(Color.BLACK); + bestJointMomentPointPlotData.getTraceStyle().setzIndex(2); + bestJointMomentPointPlotData.getTraceStyle().setPxSize(10); + bestJointMomentPointPlotData.getTraceStyle().setStyleName(SymbolStyles.STAR.getStyleName()); + + jointMomentContourPlotData.getTraceStyle().setColorMap(CONTOUR_COLOR_MAP); + jointMomentPointPlotData.getTraceStyle().setColorMap(CONTOUR_COLOR_MAP); + jointMomentPointPlotData.getTraceStyle().setEdgeColor(Color.BLACK); + jointMomentPointPlotData.getTraceStyle().setSeriesName("Samples"); + jointMomentPointPlotData.getTraceStyle().setzIndex(1); + + plotData.put(jointMomentContourPlotData.getTraceStyle().getType().getType(), jointMomentContourPlotData); + plotData.put(jointMomentPointPlotData.getTraceStyle().getSeriesName(), jointMomentPointPlotData); + plotData.put(bestJointMomentPointPlotData.getTraceStyle().getSeriesName(), bestJointMomentPointPlotData); + + jointMomentXaxis = new PlotlyPlotFactory().axis(Type.X, ""); + jointMomentYaxis = new PlotlyPlotFactory().axis(Type.Y, ""); + jointMomentContourPlot.addAxes(jointMomentXaxis); + jointMomentContourPlot.addAxes(jointMomentYaxis); + jointMomentContourPlot.attachToDisplayNode(jointMomentPlotNode); + + stressContourPlot = plotFactory.basicPlot(); + stressContourPlot.setColorMap(CONTOUR_COLOR_MAP); + plotData = stressContourPlot.getPlotTypes(); + + stressContourPlotData = new PlotObjectData(new PlotTrace(PlotTrace.Style.CONTOUR)); + stressPointPlotData = new PlotObjectData(new PlotTrace(PlotTrace.Style.SCATTER_MARKER)); + bestStressPointPlotData = new PlotObjectData(new PlotTrace(PlotTrace.Style.SCATTER_MARKER)); + bestStressPointPlotData.getTraceStyle().setSeriesName("Estimate"); + bestStressPointPlotData.getTraceStyle().setFillColor(Color.WHITE); + bestStressPointPlotData.getTraceStyle().setEdgeColor(Color.BLACK); + bestStressPointPlotData.getTraceStyle().setzIndex(2); + bestStressPointPlotData.getTraceStyle().setPxSize(10); + bestStressPointPlotData.getTraceStyle().setStyleName(SymbolStyles.STAR.getStyleName()); + + stressContourPlotData.getTraceStyle().setColorMap(CONTOUR_COLOR_MAP); + stressPointPlotData.getTraceStyle().setColorMap(CONTOUR_COLOR_MAP); + stressPointPlotData.getTraceStyle().setEdgeColor(Color.BLACK); + stressPointPlotData.getTraceStyle().setSeriesName("Samples"); + stressPointPlotData.getTraceStyle().setzIndex(1); + + plotData.put(stressContourPlotData.getTraceStyle().getType().getType(), stressContourPlotData); + plotData.put(stressPointPlotData.getTraceStyle().getSeriesName(), stressPointPlotData); + plotData.put(bestStressPointPlotData.getTraceStyle().getSeriesName(), bestStressPointPlotData); + + stressXaxis = new PlotlyPlotFactory().axis(Type.LOG_X, ""); + stressYaxis = new PlotlyPlotFactory().axis(Type.LOG_Y, ""); + stressContourPlot.addAxes(stressXaxis); + stressContourPlot.addAxes(stressYaxis); + stressContourPlot.attachToDisplayNode(pairStressPlotNode); + + momentContourPlot = plotFactory.basicPlot(); + momentContourPlot.setColorMap(CONTOUR_COLOR_MAP); + plotData = momentContourPlot.getPlotTypes(); + + momentContourPlotData = new PlotObjectData(new PlotTrace(PlotTrace.Style.CONTOUR)); + momentPointPlotData = new PlotObjectData(new PlotTrace(PlotTrace.Style.SCATTER_MARKER)); + bestMomentPointPlotData = new PlotObjectData(new PlotTrace(PlotTrace.Style.SCATTER_MARKER)); + bestMomentPointPlotData.getTraceStyle().setSeriesName("Estimate"); + bestMomentPointPlotData.getTraceStyle().setFillColor(Color.WHITE); + bestMomentPointPlotData.getTraceStyle().setEdgeColor(Color.BLACK); + bestMomentPointPlotData.getTraceStyle().setzIndex(2); + bestMomentPointPlotData.getTraceStyle().setPxSize(10); + bestMomentPointPlotData.getTraceStyle().setStyleName(SymbolStyles.STAR.getStyleName()); + + momentContourPlotData.getTraceStyle().setColorMap(CONTOUR_COLOR_MAP); + momentPointPlotData.getTraceStyle().setColorMap(CONTOUR_COLOR_MAP); + momentPointPlotData.getTraceStyle().setEdgeColor(Color.BLACK); + momentPointPlotData.getTraceStyle().setSeriesName("Samples"); + momentPointPlotData.getTraceStyle().setzIndex(1); + + plotData.put(momentContourPlotData.getTraceStyle().getType().getType(), momentContourPlotData); + plotData.put(momentPointPlotData.getTraceStyle().getSeriesName(), momentPointPlotData); + plotData.put(bestMomentPointPlotData.getTraceStyle().getSeriesName(), bestMomentPointPlotData); + + momentXaxis = new PlotlyPlotFactory().axis(Type.X, ""); + momentYaxis = new PlotlyPlotFactory().axis(Type.Y, ""); + momentContourPlot.addAxes(momentXaxis); + momentContourPlot.addAxes(momentYaxis); + momentContourPlot.attachToDisplayNode(pairMomentPlotNode); + } + + @FXML + private void screenshotPlots(final ActionEvent e) { + Button btn = (Button) e.getSource(); + Parent pane = btn.getParent(); + final File folder = screenshotFolderChooser.showDialog(pane.getScene().getWindow()); + try { + if (folder != null && folder.exists() && folder.isDirectory() && folder.canWrite()) { + screenshotFolderChooser.setInitialDirectory(folder); + Platform.runLater(() -> exportScreenshots(folder)); + } + } catch (final SecurityException ex) { + log.warn("Exception trying to write screenshots to folder {} : {}", folder, ex.getLocalizedMessage(), ex); + } + } + + @FXML + public void showMapWindow(final ActionEvent e) { + SpectraRatioPairDetails ratioDetails = getCurrentFirstRatio(); + if (mapImpl != null && ratioDetails != null) { + List listData = new ArrayList<>(); + listData.add(ratioDetails.getNumerWaveform()); + listData.add(ratioDetails.getDenomWaveform()); + List stationIcons = new ArrayList<>(); + ratioMeasurementReport.getStationsForEventPair(getEventPair()).forEach(station -> { + stationIcons.add(iconFactory.createStationIcon(station).setIconSelectionCallback(stationSelectionCallback)); + }); + + mapImpl.clearIcons(); + mapImpl.addIcons(stationIcons); + mapImpl.addIcons(iconFactory.genIconsFromWaveforms(eventSelectionCallback, null, listData)); + + Platform.runLater(() -> { + mapImpl.show(); + mapImpl.fitViewToActiveShapes(); + }); + } + } + + @FXML + public void downloadPlots(final ActionEvent e) { + //Save all parameters to an archive file and prompt the user about where to save it. + File selectedFile = FileDialogs.openFileSaveDialog("Spectra_Ratio_Data", ".zip", borderPane.getScene().getWindow()); + + if ((selectedFile != null) && FileDialogs.ensureFileIsWritable(selectedFile)) { + List ratiosByEventPair = ratioMeasurementReport.getRatiosList(getEventPair()); + + Platform.runLater(() -> { + try { + File exportArchive; + Path tmpFolder = Files.createTempDirectory(Long.toString(System.currentTimeMillis())); + tmpFolder.toFile().deleteOnExit(); + exportScreenshots(tmpFolder.toFile()); + exportArchive = spectraRatioExporter.createExportArchive(ratiosByEventPair, tmpFolder); + if (exportArchive != null) { + Files.move(exportArchive.toPath(), selectedFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + } + } catch (IOException ex) { + FileDialogs.fileIoErrorAlert(ex); + } + }); + + } + } + + private void plotStationData(EventPair eventPair) { + List ratiosByEventPair = ratioMeasurementReport.getRatiosList(getEventPair()); + symbolStyleMap = symbolStyleMapFactory.build(new ArrayList<>(ratioMeasurementReport.getReport().getData().get(eventPair).keySet()), Station::getStationName); + spectraRatioPairOperatorList = ratiosByEventPair.stream().map(SpectraRatioPairOperator::new).collect(Collectors.toList()); + symbolMap.clear(); + symbolMap.putAll(mapRatioToPoint(spectraRatioPairOperatorList, SpectraRatioPairOperator::getDiffAvg)); + ratioSpectralPlotController.getSpectraDataMap().clear(); + getRatioSpectraPlot().clearPlot(); + + ratioSpectralPlotController.getSpectraDataMap().putAll(symbolMap); + getRatioSpectraPlot().plotXYdata(toPlotPoints(), null, RATIO_AVG_LABEL); + topTitleLabel.setText(getEventPairInfoForTitle()); + + Map inversionData = ratioMeasurementReport.getReport().getInversionEstimates(); + if (inversionData != null) { + plotInversionPairData(inversionData, eventPair); + } + + Map jointInversionData = ratioMeasurementReport.getReport().getJointInversionEstimates(); + if (jointInversionData != null) { + plotJointInversionData(jointInversionData, eventPair); + } + + updateMomentRatioLines(); + + try { + runGuiUpdate(() -> { + getRatioSpectraPlot().getSubplot().fullReplot(); + }); + } catch (InvocationTargetException e) { + log.debug(e.getMessage(), e); + } catch (InterruptedException e) { + log.debug(e.getMessage(), e); + } + } + + private void plotInversionPairData(Map inversionData, EventPair eventPair) { + plotInversionData( + "Pair", + inversionData, + eventPair, + stressContourPlot, + momentContourPlot, + stressContourPlotData, + stressPointPlotData, + bestStressPointPlotData, + momentContourPlotData, + momentPointPlotData, + bestMomentPointPlotData, + stressXaxis, + stressYaxis, + momentXaxis, + momentYaxis, + Color.BLUE); + } + + private void plotJointInversionData(Map jointInversionData, EventPair eventPair) { + SpectraRatioPairInversionResultJoint eventRecord = jointInversionData.get(eventPair); + if (eventRecord != null) { + Map wrappedInversionData = new HashMap<>(1); + wrappedInversionData.put( + eventPair, + new SpectraRatioPairInversionResult().setEventIdA(eventRecord.getEventIdA()) + .setEventIdB(eventRecord.getEventIdB()) + .setMomentEstimateA(eventRecord.getMomentEstimateA()) + .setMomentEstimateB(eventRecord.getMomentEstimateB()) + .setApparentStressEstimateA(eventRecord.getApparentStressEstimateA()) + .setApparentStressEstimateB(eventRecord.getApparentStressEstimateB()) + .setCornerEstimateA(eventRecord.getCornerEstimateA()) + .setCornerEstimateB(eventRecord.getCornerEstimateB()) + .setId(eventRecord.getId()) + .setMisfit(eventRecord.getMisfit()) + .setM0minX(eventRecord.getM0minX()) + .setM0maxX(eventRecord.getM0maxX()) + .setM0minY(eventRecord.getM0minY()) + .setM0maxY(eventRecord.getM0maxY()) + .setAppStressMin(eventRecord.getAppStressMin()) + .setAppStressMax(eventRecord.getAppStressMax()) + .setM0XIndex(eventRecord.getM0XIndex()) + .setM0Xdim(eventRecord.getM0Xdim()) + .setM0YIndex(eventRecord.getM0YIndex()) + .setM0Ydim(eventRecord.getM0Ydim()) + .setM0samples(eventRecord.getM0samples()) + .setStressXIndex(eventRecord.getStressXIndex()) + .setAppStressXdim(eventRecord.getAppStressXdim()) + .setStressYIndex(eventRecord.getStressYIndex()) + .setAppStressYdim(eventRecord.getAppStressYdim()) + .setStressSamples(eventRecord.getStressSamples())); + plotInversionData( + "Joint", + wrappedInversionData, + eventPair, + jointStressContourPlot, + jointMomentContourPlot, + jointStressContourPlotData, + jointStressPointPlotData, + bestJointStressPointPlotData, + jointMomentContourPlotData, + jointMomentPointPlotData, + bestJointMomentPointPlotData, + jointStressXaxis, + jointStressYaxis, + jointMomentXaxis, + jointMomentYaxis, + Color.RED); + } + + } + + private void plotInversionData(String dataLabelPrefix, Map inversionData, EventPair eventPair, BasicPlot stressContourPlot, BasicPlot momentContourPlot, + PlotObjectData stressContourPlotData, PlotObjectData stressPointPlotData, PlotObjectData bestStressPointPlotData, PlotObjectData momentContourPlotData, PlotObjectData momentPointPlotData, + PlotObjectData bestMomentPointPlotData, Axis stressXaxis, Axis stressYaxis, Axis momentXaxis, Axis momentYaxis, Color ratioShapeColor) { + if (eventPair != null) { + try { + //Break the cache + stressContourPlot.clear(); + momentContourPlot.clear(); + //FIXME: Revamp this to not be editing the internal plot data but instead use the + // standard add/remove plot objects + + stressContourPlotData.clear(); + stressPointPlotData.clear(); + bestStressPointPlotData.clear(); + + momentContourPlotData.clear(); + momentPointPlotData.clear(); + bestMomentPointPlotData.clear(); + + SpectraRatioPairInversionResult bestFit = inversionData.get(eventPair); + + bestStressPointPlotData.getXdata().add(Double.valueOf(bestFit.getApparentStressEstimateB())); + bestStressPointPlotData.getYdata().add(Double.valueOf(bestFit.getApparentStressEstimateA())); + bestMomentPointPlotData.getXdata().add(Double.valueOf(bestFit.getMomentEstimateB())); + bestMomentPointPlotData.getYdata().add(Double.valueOf(bestFit.getMomentEstimateA())); + + setCornerFrequencyLines(bestFit.getCornerEstimateA(), bestFit.getCornerEstimateB(), ratioShapeColor); + + Line fitRatioShape = plotMomentRatioShape( + spectraClient.getSpecificSpectra(bestFit.getMomentEstimateA(), bestFit.getApparentStressEstimateA(), 0.001, 30.0, 100).block(Duration.ofMinutes(10l)), + spectraClient.getSpecificSpectra(bestFit.getMomentEstimateB(), bestFit.getApparentStressEstimateB(), 0.001, 30.0, 100).block(Duration.ofMinutes(10l))); + fitRatioShape.setName(dataLabelPrefix + " CERT Mw ratio"); + fitRatioShape.setStyle(LineStyles.SOLID); + fitRatioShape.setFillColor(ratioShapeColor); + + getRatioSpectraPlot().getSubplot().addPlotObject(fitRatioShape); + + double minStressX = bestFit.getAppStressMin(); + double minMomentX = bestFit.getM0minX(); + double maxStressX = bestFit.getAppStressMax(); + double maxMomentX = bestFit.getM0maxX(); + + double minStressY = minStressX; + double minMomentY = bestFit.getM0minY(); + double maxStressY = maxStressX; + double maxMomentY = bestFit.getM0maxY(); + + List stressMisfits = new ArrayList<>(); + List momentMisfits = new ArrayList<>(); + + double logMinStressX = Math.log10(minStressX); + double logMaxStressX = Math.log10(maxStressX); + + double logMinStressY = Math.log10(minStressY); + double logMaxStressY = Math.log10(maxStressY); + + for (int i = 0; i < bestFit.getM0Xdim(); i++) { + double m0x = minMomentX + ((float) i / (float) (bestFit.getM0Xdim() - 1)) * (bestFit.getM0maxX() - bestFit.getM0minX()); + momentContourPlotData.getXdata().add(m0x); + } + for (int i = 0; i < bestFit.getM0Ydim(); i++) { + double m0y = minMomentY + ((float) i / (float) (bestFit.getM0Ydim() - 1)) * (bestFit.getM0maxY() - bestFit.getM0minY()); + momentContourPlotData.getYdata().add(m0y); + momentMisfits.add(new Double[bestFit.getM0Xdim()]); + } + + for (int i = 0; i < bestFit.getM0data().size(); i++) { + //Grab the data and push it into the dense plotting array + momentMisfits.get(bestFit.getM0YIdx().get(i))[bestFit.getM0XIdx().get(i)] = (double) bestFit.getM0data().get(i); + + double misfit = bestFit.getM0data().get(i); + float xCoord = bestFit.getM0minX() + ((float) bestFit.getM0XIdx().get(i) / (float) (bestFit.getM0Xdim() - 1)) * (bestFit.getM0maxX() - bestFit.getM0minX()); + float yCoord = bestFit.getM0minY() + ((float) bestFit.getM0YIdx().get(i) / (float) (bestFit.getM0Ydim() - 1)) * (bestFit.getM0maxY() - bestFit.getM0minY()); + momentPointPlotData.getTextData().add(dfmt4.format(misfit)); + momentPointPlotData.getColorData().add(Double.valueOf(misfit)); + momentPointPlotData.getXdata().add(Double.valueOf(xCoord)); + momentPointPlotData.getYdata().add(Double.valueOf(yCoord)); + } + momentPointPlotData.getTraceStyle().setLegendOnly(true); + + for (int i = 0; i < bestFit.getAppStressXdim(); i++) { + double stressX = logMinStressX + ((float) i / (float) (bestFit.getAppStressXdim() - 1)) * (logMaxStressX - logMinStressX); + stressContourPlotData.getXdata().add(Math.pow(10, stressX)); + } + for (int i = 0; i < bestFit.getAppStressYdim(); i++) { + double stressY = logMinStressY + ((float) i / (float) (bestFit.getAppStressYdim() - 1)) * (logMaxStressY - logMinStressY); + stressContourPlotData.getYdata().add(Math.pow(10, stressY)); + stressMisfits.add(new Double[bestFit.getAppStressXdim()]); + } + + for (int i = 0; i < bestFit.getStressData().size(); i++) { + stressMisfits.get(bestFit.getStressYIdx().get(i))[bestFit.getStressXIdx().get(i)] = (double) bestFit.getStressData().get(i); + + double misfit = bestFit.getStressData().get(i); + Double xCoord = logMinStressX + ((float) bestFit.getStressXIdx().get(i) / (float) (bestFit.getAppStressXdim() - 1)) * (logMaxStressX - logMinStressX); + Double yCoord = logMinStressY + ((float) bestFit.getStressYIdx().get(i) / (float) (bestFit.getAppStressYdim() - 1)) * (logMaxStressY - logMinStressY); + stressPointPlotData.getTextData().add(dfmt4.format(misfit)); + stressPointPlotData.getColorData().add(Double.valueOf(misfit)); + stressPointPlotData.getXdata().add(Math.pow(10, xCoord)); + stressPointPlotData.getYdata().add(Math.pow(10, yCoord)); + } + stressPointPlotData.getTraceStyle().setLegendOnly(true); + + stressXaxis.setMax(logMaxStressX); + stressYaxis.setMax(logMaxStressY); + stressXaxis.setMin(logMinStressX); + stressYaxis.setMin(logMinStressY); + + momentXaxis.setMax(maxMomentX); + momentYaxis.setMax(maxMomentY); + momentXaxis.setMin(minMomentX); + momentYaxis.setMin(minMomentY); + + stressContourPlotData.getZdata().addAll(stressMisfits); + momentContourPlotData.getZdata().addAll(momentMisfits); + + stressXaxis.setText(eventPair.getX().getEventId()); + stressYaxis.setText(eventPair.getY().getEventId()); + + momentXaxis.setText(eventPair.getX().getEventId()); + momentYaxis.setText(eventPair.getY().getEventId()); + + Map plotData = stressContourPlot.getPlotTypes(); + plotData.put(stressContourPlotData.getTraceStyle().getType().getType(), stressContourPlotData); + plotData.put(stressPointPlotData.getTraceStyle().getSeriesName(), stressPointPlotData); + plotData.put(bestStressPointPlotData.getTraceStyle().getSeriesName(), bestStressPointPlotData); + + stressContourPlot.getTitle() + .setText( + String.format( + dataLabelPrefix + " App StressA: %s, App StressB: %s, Err: %s", + dfmt4.format(bestFit.getApparentStressEstimateA()), + dfmt4.format(bestFit.getApparentStressEstimateB()), + dfmt2.format(bestFit.getMisfit()))); + stressContourPlot.replot(); + + plotData = momentContourPlot.getPlotTypes(); + plotData.put(momentContourPlotData.getTraceStyle().getType().getType(), momentContourPlotData); + plotData.put(momentPointPlotData.getTraceStyle().getSeriesName(), momentPointPlotData); + plotData.put(bestMomentPointPlotData.getTraceStyle().getSeriesName(), bestMomentPointPlotData); + + momentContourPlot.getTitle() + .setText( + String.format( + dataLabelPrefix + " log10M0/MwA: %s/%s, log10M0/MwB: %s/%s, Err: %s", + dfmt2.format(bestFit.getMomentEstimateA()), + dfmt2.format((bestFit.getMomentEstimateA() - 9.1) / 1.5), + dfmt2.format(bestFit.getMomentEstimateB()), + dfmt2.format((bestFit.getMomentEstimateB() - 9.1) / 1.5), + dfmt2.format(bestFit.getMisfit()))); + momentContourPlot.replot(); + } catch (Exception e) { + log.error(e.getLocalizedMessage(), e); + } + } + } + + public void setRatioMeasurements(SpectraRatiosReportByEventPair ratiosByEventPair) { + Platform.runLater(() -> { + ratioMeasurementReport = ratiosByEventPair; + eventPairList.clear(); + eventPairList.addAll(ratioMeasurementReport.getEventPairs()); + eventPairComboBox.getSelectionModel().clearSelection(); + if (eventPairList.size() > 0) { + eventPairComboBox.getSelectionModel().select(0); + } + }); + } + + public String getEventPairInfoForTitle() { + StringBuilder sb = new StringBuilder(); + Double eventDistance = getEventPairDistanceKm(); + Double hypocentralEventDistance = getHypocentralEventPairDistanceKm(); + Double eventDepth = getEventPairDepthKm(); + if (eventDistance != null) { + sb.append(String.format("Event Pair Distance: %s Km / %s Km", dfmt2.format(eventDistance.doubleValue()), dfmt2.format(hypocentralEventDistance.doubleValue()))); + } + if (eventDepth != null) { + sb.append(String.format(", Depth Diff: %s Km", dfmt2.format(eventDepth.doubleValue()))); + } + + return sb.toString(); + } + + public Map mapRatioToPoint(final List ratioDetails, final Function func) { + return ratioDetails.stream() + .collect( + Collectors.toMap( + //Dyne-cm to nm for plot, in log + ratio -> new Point2D(centerFreq(ratio.getFrequency().getLowFrequency(), ratio.getFrequency().getHighFrequency()), func.apply(ratio)), + Function.identity(), + (a, b) -> b, + HashMap::new)); + } + + private List toPlotPoints() { + List allPlotPoints = new ArrayList<>(); + + spectraRatioPairOperatorList.forEach(ratioDetails -> { + FrequencyBand freqValue = ratioDetails.getFrequency(); + PlotPoint pp = symbolStyleMap.get(ratioDetails.getDenomWaveform().getStream().getStation().getStationName()); + if (pp != null) { + pp.setX(centerFreq(freqValue.getLowFrequency(), freqValue.getHighFrequency())); + pp.setY(ratioDetails.getDiffAvg()); + + final LabeledPlotPoint point = new LabeledPlotPoint(ratioDetails.getStation().getStationName(), pp); + allPlotPoints.add(point); + } + }); + + return allPlotPoints; + } + + private void updateMomentRatioLines() { + Double momentRatio = null; + + if (ratioMeasurementReport == null || getEventPair() == null) { + momentRatio = null; + } else { + // Get event id's + String largeId = getEventPair().getY().getEventId(); + String smallId = getEventPair().getX().getEventId(); + + // Get the spectra for the event + List largeSpectraSet = new ArrayList<>(spectraClient.getFitSpectra(largeId).block(Duration.ofSeconds(2))); + List smallSpectraSet = new ArrayList<>(spectraClient.getFitSpectra(smallId).block(Duration.ofSeconds(2))); + + Spectra largeSpectra = null; + Spectra smallSpectra = null; + + if (largeSpectraSet.isEmpty()) { + largeSpectraSet = List.of(spectraClient.getReferenceSpectra(largeId).block(Duration.ofSeconds(2))); + } + + if (smallSpectraSet.isEmpty()) { + smallSpectraSet = List.of(spectraClient.getReferenceSpectra(smallId).block(Duration.ofSeconds(2))); + } + + if (!largeSpectraSet.isEmpty() && !smallSpectraSet.isEmpty()) { + double largeMw = 0.0; + for (Spectra spectra : largeSpectraSet) { + if (spectra.getType() == SPECTRA_TYPES.FIT) { + largeMw = spectra.getMw(); + largeSpectra = spectra; + break; + } else if (spectra.getType() == SPECTRA_TYPES.REF) { + largeMw = spectra.getMw(); + largeSpectra = spectra; + } + } + + double smallMw = 0.0; + for (Spectra spectra : smallSpectraSet) { + if (spectra.getType() == SPECTRA_TYPES.FIT) { + smallMw = spectra.getMw(); + smallSpectra = spectra; + break; + } else if (spectra.getType() == SPECTRA_TYPES.REF) { + smallMw = spectra.getMw(); + smallSpectra = spectra; + } + } + + if (largeMw != 0.0 && smallMw != 0.0) { + //Use logM0 ratio (scaled) + //TODO: Revisit on bottom line w.r.t. source scaling? + momentRatio = 1.5 * (largeMw - smallMw); + + Double refRatio = momentRatio / 3.0; + Line refRatioShape = plotMomentRatioShape(largeSpectra, smallSpectra); + refRatioShape.setName("CCT Mw ratio"); + + getRatioSpectraPlot().getSubplot().addPlotObject(refRatioShape); + + setMomentRefRatioLines(momentRatio, refRatio); + } + } + } + } + + private Line plotMomentRatioShape(Spectra largeSpectra, Spectra smallSpectra) { + //TODO: Assumes sorted order and large.length == small.length + if (largeSpectra != null && smallSpectra != null && largeSpectra.getSpectraXY().size() == smallSpectra.getSpectraXY().size()) { + + final double[] ratioDataX = new double[largeSpectra.getSpectraXY().size()]; + final double[] ratioDataY = new double[largeSpectra.getSpectraXY().size()]; + + for (int i = 0; i < largeSpectra.getSpectraXY().size(); i++) { + //Log10 freq, need it in raw + ratioDataX[i] = Math.pow(10, largeSpectra.getSpectraXY().get(i).getX()); + //Log10 spectra + ratioDataY[i] = largeSpectra.getSpectraXY().get(i).getY() - smallSpectra.getSpectraXY().get(i).getY(); + } + + Line ratioShape = plotFactory.line(ratioDataX, ratioDataY, Color.BLUE, LineStyles.DASH, 2); + + return ratioShape; + } + return null; + } + + private void setMomentRefRatioLines(double momentRatio, double refRatio) { + + double minVal = Math.min(0.0, symbolMap.entrySet().stream().mapToDouble(entry -> entry.getKey().getX()).min().getAsDouble()); + double maxVal = Math.max(20.0, symbolMap.entrySet().stream().mapToDouble(entry -> entry.getKey().getX()).max().getAsDouble()); + double length = maxVal - minVal; + + final float[] momentData = new float[2]; + momentData[0] = (float) momentRatio; + momentData[1] = (float) momentRatio; + + PlotObject momentLine = plotFactory.lineX(String.format("CCT LFL: %s", dfmt4.format(momentRatio)), minVal, length, momentData, Color.BLACK, LineStyles.DASH_DOT, 2); + + final float[] refData = new float[2]; + refData[0] = (float) refRatio; + refData[1] = (float) refRatio; + + PlotObject refLine = plotFactory.lineX(String.format("CCT HFL: %s", dfmt4.format(refRatio)), minVal, length, refData, Color.BLACK, LineStyles.DOT, 2); + + getRatioSpectraPlot().getSubplot().addPlotObject(momentLine); + getRatioSpectraPlot().getSubplot().addPlotObject(refLine); + } + + private void setCornerFrequencyLines(double cornerEstimateA, double cornerEstimateB, Color lineColor) { + + VerticalLine lineA = plotFactory.verticalLine(cornerEstimateA, 50.0, dfmt2.format(cornerEstimateA)); + lineA.setFillColor(lineColor); + VerticalLine lineB = plotFactory.verticalLine(cornerEstimateB, 50.0, dfmt2.format(cornerEstimateB)); + lineB.setFillColor(lineColor); + + lineA.setLogScaleX(true); + lineB.setLogScaleX(true); + + getRatioSpectraPlot().getSubplot().addPlotObject(lineA); + getRatioSpectraPlot().getSubplot().addPlotObject(lineB); + } + + protected void runGuiUpdate(final Runnable runnable) throws InvocationTargetException, InterruptedException { + Platform.runLater(runnable); + } + + protected double centerFreq(final Double lowFrequency, final Double highFrequency) { + return lowFrequency + (highFrequency - lowFrequency) / 2.; + } + + public EventPair getEventPair() { + return eventPairComboBox.getSelectionModel().getSelectedItem(); + } + + public SpectralPlot getRatioSpectraPlot() { + return ratioSpectralPlotController.getSpectralPlot(); + } + + public SpectraRatioPairDetails getCurrentFirstRatio() { + if (ratioMeasurementReport != null) { + List ratioReportList = ratioMeasurementReport.getRatiosList(getEventPair()); + if (!ratioReportList.isEmpty()) { + return ratioReportList.get(0); + } + } + return null; + } + + public String getPlotIdentifier() { + StringBuilder sb = new StringBuilder(); + ratioMeasurementReport.getStationsForEventPair(getEventPair()).forEach(station -> { + sb.append(station.getStationName()); + sb.append("_"); + }); + return String.format("large_event_%s_small_event_%s_stations_%s", getEventPair().getY().getEventId(), getEventPair().getX().getEventId(), sb.toString()); + } + + // Gets the epicentral distance EModel WGS84 distance between both events + public Double getEventPairDistanceKm() { + SpectraRatioPairDetails ratioDetails = getCurrentFirstRatio(); + if (ratioDetails != null) { + double numerLatitude = ratioDetails.getNumerWaveform().getEvent().getLatitude(); + double numerLongitude = ratioDetails.getNumerWaveform().getEvent().getLongitude(); + double denomLatitude = ratioDetails.getDenomWaveform().getEvent().getLatitude(); + double denomLongitude = ratioDetails.getDenomWaveform().getEvent().getLongitude(); + + return EModel.getDistanceWGS84(numerLatitude, numerLongitude, denomLatitude, denomLongitude); + } + + return null; + } + + // Gets the hypocentral distance EModel WGS84 distance in Km between both events + public Double getHypocentralEventPairDistanceKm() { + SpectraRatioPairDetails ratioDetails = getCurrentFirstRatio(); + if (ratioDetails != null) { + double numerLatitude = ratioDetails.getNumerWaveform().getEvent().getLatitude(); + double numerLongitude = ratioDetails.getNumerWaveform().getEvent().getLongitude(); + double denomLatitude = ratioDetails.getDenomWaveform().getEvent().getLatitude(); + double denomLongitude = ratioDetails.getDenomWaveform().getEvent().getLongitude(); + double numerDepth = ratioDetails.getNumerWaveform().getEvent().getDepth() / 1000.0; + double denomDepth = ratioDetails.getDenomWaveform().getEvent().getDepth() / 1000.0; + + return EModel.getSeparationMeters(new GeodeticCoordinate(numerLatitude, numerLongitude, numerDepth), new GeodeticCoordinate(denomLatitude, denomLongitude, denomDepth)) / 1000.0; + } + + return null; + } + + // Gets the depth difference between event pair + public Double getEventPairDepthKm() { + SpectraRatioPairDetails ratioDetails = getCurrentFirstRatio(); + if (ratioDetails != null) { + double numerDepth = ratioDetails.getNumerWaveform().getEvent().getDepth(); + double denomDepth = ratioDetails.getDenomWaveform().getEvent().getDepth(); + + return Math.abs(numerDepth - denomDepth) / 1000.0; + } + + return null; + } + + private PropertyChangeListener getPlotpointObserver(final Supplier> ratioDetailsMap) { + return evt -> { + Object po = evt.getNewValue(); + if (po instanceof PlotObjectClick && ((PlotObjectClick) po).getPlotPoints() != null) { + handlePlotObjectClicked((PlotObjectClick) po, point -> ratioDetailsMap.get().get(point)); + } + }; + } + + public Result> getRatioDetailsFromStation(Station station) { + Map ratios = null; + if (station != null) { + Map> eventData = ratioMeasurementReport.getReport().getData().get(getEventPair()); + if (eventData != null && eventData.containsKey(station)) { + ratios = eventData.get(station); + } + } + + return new Result<>(ratios != null, ratios); + } + + public Result getRatioDetailsFromStationAndFreq(Station station, FrequencyBand freqBand) { + SpectraRatioPairDetails ratio = null; + if (station != null && freqBand != null) { + Map> eventData = ratioMeasurementReport.getReport().getData().get(getEventPair()); + if (eventData != null && eventData.containsKey(station)) { + ratio = eventData.get(station).get(freqBand); + } + } + return new Result<>(ratio != null, ratio); + } + + private void createPlotPopup(SpectraRatioPairOperator ratioDetails) { + + RatioMeasurementWaveformPlotManager ratioWaveformPlots = new RatioMeasurementWaveformPlotManager(mapImpl, iconFactory); + ratioWaveformPlots.setParentSpectra(this); + ratioWaveformPlots.setCurrentEvent(ratioMeasurementReport.getStationsForEventPair(getEventPair())); + + final FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/fxml/RatioWaveformGui.fxml")); + fxmlLoader.setController(ratioWaveformPlots); + final Stage stage = new Stage(StageStyle.DECORATED); + Parent root; + Scene scene; + + Font.loadFont(getClass().getResource("/fxml/MaterialIcons-Regular.ttf").toExternalForm(), 18); + try { + root = fxmlLoader.load(); + scene = new Scene(root); + stage.setScene(scene); + + stage.setOnHiding(e -> { + stage.hide(); + }); + + stage.setOnShowing(e -> { + final boolean showing = stage.isShowing(); + stage.show(); + if (!showing || Boolean.TRUE.equals(shouldFocus.getValue())) { + stage.toFront(); + } + }); + + ratioWaveformPlots.setCurrentFreqAndStation(ratioDetails.getFrequency().getLowFrequency(), ratioDetails.getFrequency().getHighFrequency(), ratioDetails.getStation()); + + stage.show(); + } catch (final IOException e) { + throw new IllegalStateException(e); + } + } + + private void handlePlotObjectClicked(final PlotObjectClick poc, final Function measurementFunc) { + + Point2D point = poc.getPlotPoints().get(0); + SpectraRatioPairOperator ratioDetails = measurementFunc.apply(point); + + if (poc.getMouseEvent().isPrimaryButtonDown() && ratioDetails != null) { + try { + runGuiUpdate(() -> { + createPlotPopup(ratioDetails); + }); + } catch (InvocationTargetException e) { + e.printStackTrace(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + + public void triggerKeyEvent(final KeyEvent event) { + log.trace("Key Pressed on Ratio Spectra Plot"); + } + + protected void updatePlotPoint() { + symbolMap = mapRatioToPoint(spectraRatioPairOperatorList, SpectraRatioPairOperator::getDiffAvg); + ratioSpectralPlotController.getSpectraDataMap().clear(); + ratioSpectralPlotController.getSpectraDataMap().putAll(symbolMap); + ratioSpectralPlotController.getSpectralPlot().plotXYdata(toPlotPoints(), null, RATIO_AVG_LABEL); + + updateMomentRatioLines(); + } + + public void exportScreenshots(final File folder) { + String timestamp = SnapshotUtils.getTimestampWithLeadingSeparator(); + SnapshotUtils.writePng(folder, new Pair<>(SPECTRA_RATIO_PREFIX, spectraRatioPlotNode), timestamp); + SnapshotUtils.writePng(folder, new Pair<>(JOINT_MOMENT_PREFIX, jointMomentPlotNode), timestamp); + SnapshotUtils.writePng(folder, new Pair<>(JOINT_STRESS_PREFIX, jointStressPlotNode), timestamp); + SnapshotUtils.writePng(folder, new Pair<>(PAIR_MOMENT_PREFIX, pairMomentPlotNode), timestamp); + SnapshotUtils.writePng(folder, new Pair<>(PAIR_STRESS_PREFIX, pairStressPlotNode), timestamp); + + String plotId = getPlotIdentifier(); + String plotExportSuffix = SnapshotUtils.getTimestampWithLeadingSeparator() + ".svg"; + if (plotId != null && !plotId.isEmpty()) { + plotExportSuffix = plotId + "_" + timestamp + ".svg"; + } + + exportSVG(momentContourPlot, folder + File.separator + PAIR_MOMENT_PREFIX + plotExportSuffix); + exportSVG(stressContourPlot, folder + File.separator + PAIR_STRESS_PREFIX + plotExportSuffix); + exportSVG(ratioSpectralPlotController.getSpectralPlot().getSubplot(), folder + File.separator + SPECTRA_RATIO_PREFIX + plotExportSuffix); + } + + private void exportSVG(BasicPlot plot, String path) { + try { + Files.write(Paths.get(path), plot.getSVG().getBytes()); + } catch (final IOException e) { + log.error("Error attempting to write plots for controller : {}", e.getLocalizedMessage(), e); + } + } +} diff --git a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/plotting/RatioMeasurementWaveformPlotManager.java b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/plotting/RatioMeasurementWaveformPlotManager.java new file mode 100644 index 00000000..2e337f2c --- /dev/null +++ b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/plotting/RatioMeasurementWaveformPlotManager.java @@ -0,0 +1,567 @@ +/* +* Copyright (c) 2021, Lawrence Livermore National Security, LLC. Produced at the Lawrence Livermore National Laboratory +* CODE-743439. +* All rights reserved. +* This file is part of CCT. For details, see https://github.com/LLNL/coda-calibration-tool. +* +* 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.plotting; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.text.NumberFormat; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import gov.llnl.gnem.apps.coda.common.gui.util.NumberFormatFactory; +import gov.llnl.gnem.apps.coda.common.gui.util.SnapshotUtils; +import gov.llnl.gnem.apps.coda.common.model.domain.FrequencyBand; +import gov.llnl.gnem.apps.coda.common.model.domain.Pair; +import gov.llnl.gnem.apps.coda.common.model.domain.Station; +import gov.llnl.gnem.apps.coda.common.model.domain.Waveform; +import gov.llnl.gnem.apps.coda.common.model.messaging.Result; +import gov.llnl.gnem.apps.coda.spectra.model.domain.SpectraRatioPairDetails; +import gov.llnl.gnem.apps.coda.spectra.model.domain.util.SpectraRatioPairOperator; +import javafx.application.Platform; +import javafx.event.ActionEvent; +import javafx.event.EventHandler; +import javafx.fxml.FXML; +import javafx.scene.Parent; +import javafx.scene.control.Button; +import javafx.scene.control.ContentDisplay; +import javafx.scene.control.ContextMenu; +import javafx.scene.control.Label; +import javafx.scene.control.MenuItem; +import javafx.scene.control.SplitPane; +import javafx.scene.control.SplitPane.Divider; +import javafx.scene.control.ToggleButton; +import javafx.scene.control.ToolBar; +import javafx.scene.control.Tooltip; +import javafx.scene.input.InputEvent; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; +import javafx.scene.input.MouseButton; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.Pane; +import javafx.scene.layout.StackPane; +import javafx.scene.text.Font; +import javafx.stage.DirectoryChooser; +import llnl.gnem.core.gui.plotting.api.AxisLimits; +import llnl.gnem.core.gui.plotting.events.PlotAxisChange; +import llnl.gnem.core.util.PairT; + +public class RatioMeasurementWaveformPlotManager { + + private static final String TIME_SECONDS_FROM_ORIGIN = "Time (seconds from origin)"; + private static final String WAVEFORM_PREFIX = "Waveform_"; + private static final String RATIO_WAVEFORM_PREFIX = "Ratio_waveform_"; + private static final Logger log = LoggerFactory.getLogger(RatioMeasurementWaveformPlotManager.class); + private final NumberFormat dfmt4 = NumberFormatFactory.fourDecimalOneLeadingZero(); + private RatioMeasurementSpectraPlotManager parentSpectraPlot; + + private RatioDetailPlot selectedSinglePlot; + private RatioDetailPlot ratioDiffWavePlot; + + private PairT savedAxisLimits = null; + private Label freqBandLabel; + private Label eventStationLabel; + private Map curEventStationRatios; + private List curFrequencies; + private List curEventStations; + private int curFreqIndex = -1; + private int curStationIndex = -1; + private boolean alignPeaksModeBoolean = true; + private boolean ratioWindowModeBoolean = true; + + final Button nextButton = new Button(">"); + final Button prevButton = new Button("<"); + final Button nextStationButton = new Button("↑"); + final Button prevStationButton = new Button("↓"); + final ToggleButton alignPeaksToggle = new ToggleButton("AP"); + final ToggleButton trueTimeToggle = new ToggleButton("TT"); + final ToggleButton ratioWindowToggle = new ToggleButton("RW"); + + @FXML + private StackPane ratioPlotNode; + + @FXML + private BorderPane borderPane; + + @FXML + private ToolBar topToolbar; + + @FXML + private SplitPane ratioSplitPane; + private Divider splitPaneDivider; + + @FXML + private Pane waveformsPane; + + @FXML + private Button snapshotButton; + + @FXML + private Button showRatioOnMapButton; + + @FXML + private Pane ratioWaveformPane; + + // Context menu when plot is right-clicked + private final ContextMenu contextMenu; + private final MenuItem resetToPeak; + + private final DirectoryChooser screenshotFolderChooser = new DirectoryChooser(); + + private CertLeafletMapController mapImpl; + private MapPlottingUtilities iconFactory; + private final BiConsumer eventSelectionCallback; + private final BiConsumer stationSelectionCallback; + + private final EventHandler nextAction = event -> { + if (curEventStationRatios != null && curFrequencies != null && curFreqIndex < curEventStationRatios.size() - 1) { + curFreqIndex += 1; + SpectraRatioPairDetails ratioWave = curEventStationRatios.get(curFrequencies.get(curFreqIndex)); + if (ratioWave != null) { + createPlotRatio(ratioWave, alignPeaksModeBoolean); + } + } + }; + + private final EventHandler prevAction = event -> { + if (curEventStationRatios != null && curFrequencies != null && curFreqIndex > 0) { + curFreqIndex -= 1; + SpectraRatioPairDetails ratioDetails = curEventStationRatios.get(curFrequencies.get(curFreqIndex)); + if (ratioDetails != null) { + createPlotRatio(ratioDetails, alignPeaksModeBoolean); + } + } + }; + + private final EventHandler nextStationAction = event -> { + if (curEventStations != null && curStationIndex < curEventStations.size() - 1) { + curStationIndex += 1; + setCurrentStation(curEventStations.get(curStationIndex)); + } + }; + + private final EventHandler prevStationAction = event -> { + if (curEventStations != null && curStationIndex > 0) { + curStationIndex -= 1; + setCurrentStation(curEventStations.get(curStationIndex)); + } + }; + + private final EventHandler alignPeaksToggleAction = event -> { + alignPeaksModeBoolean = !alignPeaksModeBoolean; + alignPeaksToggle.setSelected(alignPeaksModeBoolean); + trueTimeToggle.setSelected(!alignPeaksModeBoolean); + + if (alignPeaksModeBoolean) { + alignPeaksToggle.setStyle("-fx-background-color: DarkSeaGreen"); + trueTimeToggle.setStyle(null); + } else { + alignPeaksToggle.setStyle(null); + trueTimeToggle.setStyle("-fx-background-color: DarkSeaGreen"); + } + + if (selectedSinglePlot != null) { + Platform.runLater(() -> { + selectedSinglePlot.setAlignPeaks(alignPeaksModeBoolean); + selectedSinglePlot.plotRatio(); + }); + } else { + SpectraRatioPairDetails ratioDetails = curEventStationRatios.get(curFrequencies.get(curFreqIndex)); + if (ratioDetails != null) { + createPlotRatio(ratioDetails, alignPeaksModeBoolean); + } + } + }; + + private final EventHandler showRatioWindowToggleAction = event -> { + ratioWindowModeBoolean = !ratioWindowModeBoolean; + ratioWindowToggle.setSelected(ratioWindowModeBoolean); + + if (ratioWindowModeBoolean) { + ratioWindowToggle.setStyle("-fx-background-color: DarkSeaGreen"); + } else { + ratioWindowToggle.setStyle(null); + } + + showHideRatioWaveform(ratioWindowModeBoolean); + }; + + public RatioMeasurementWaveformPlotManager(CertLeafletMapController mapImpl, MapPlottingUtilities iconFactory) { + this.mapImpl = mapImpl; + this.iconFactory = iconFactory; + freqBandLabel = new Label("Frequency Band"); + eventStationLabel = new Label("Event/Station"); + + contextMenu = new ContextMenu(); + resetToPeak = new MenuItem("Reset Cuts to Default"); + + eventSelectionCallback = (selected, eventId) -> { + log.debug(eventId); + }; + + stationSelectionCallback = (selected, stationId) -> { + log.debug(stationId); + }; + } + + public void showHideRatioWaveform(boolean show) { + if (splitPaneDivider != null) { + final double position = show ? 0.8 : 1.0; + + Platform.runLater(() -> { + splitPaneDivider.setPosition(position); + }); + } + } + + public void setParentSpectra(RatioMeasurementSpectraPlotManager parentSpectra) { + this.parentSpectraPlot = parentSpectra; + } + + public void setCurrentEvent(List stationsForCurrentEvent) { + curEventStations = stationsForCurrentEvent; + curStationIndex = 0; + curFreqIndex = 0; + } + + public void setCurrentStation(Station station) { + Result> result = parentSpectraPlot.getRatioDetailsFromStation(station); + if (result.isSuccess() && result.getResultPayload().isPresent()) { + curEventStationRatios = result.getResultPayload().get(); + curFrequencies = curEventStationRatios.keySet().stream().sorted(this::sortFrequencies).collect(Collectors.toList()); + + curStationIndex = curEventStations.indexOf(station); + if (curStationIndex < 0) { + curStationIndex = 0; + } + + // Ensure frequency index fits new station + if (curFreqIndex > curFrequencies.size()) { + curFreqIndex = curFrequencies.size() - 1; + } + + createPlotRatio(curEventStationRatios.get(curFrequencies.get(curFreqIndex)), alignPeaksModeBoolean); + } + } + + private int sortFrequencies(FrequencyBand f1, FrequencyBand f2) { + int compared = Double.compare(f1.getLowFrequency(), f2.getLowFrequency()); + if (compared == 0) { + compared = Double.compare(f1.getHighFrequency(), f2.getHighFrequency()); + } + return compared; + } + + public void setCurrentFreqAndStation(double lowFreq, double highFreq, Station station) { + Result> result = parentSpectraPlot.getRatioDetailsFromStation(station); + if (result.isSuccess() && result.getResultPayload().isPresent()) { + curEventStationRatios = result.getResultPayload().get(); + curFrequencies = curEventStationRatios.keySet().stream().sorted(this::sortFrequencies).collect(Collectors.toList()); + + curFreqIndex = curFrequencies.indexOf(new FrequencyBand(lowFreq, highFreq)); + if (curFreqIndex < 0) { + curFreqIndex = 0; + } + curStationIndex = curEventStations.indexOf(station); + if (curStationIndex < 0) { + curStationIndex = 0; + } + createPlotRatio(curEventStationRatios.get(curFrequencies.get(curFreqIndex)), alignPeaksModeBoolean); + } + } + + public void setDisplayText(SpectraRatioPairOperator ratioDetails) { + if (ratioDetails != null && curEventStationRatios != null) { + String lowFreq = String.valueOf(ratioDetails.getFrequency().getLowFrequency()); + String highFreq = String.valueOf(ratioDetails.getFrequency().getHighFrequency()); + String numerEvent = ratioDetails.getNumerWaveform().getEvent().getEventId(); + String denomEvent = ratioDetails.getDenomWaveform().getEvent().getEventId(); + String station = ratioDetails.getStation().getStationName(); + String diffAvg = ratioDetails.getDiffAvg() != null ? dfmt4.format(ratioDetails.getDiffAvg()) : "N/A"; + + freqBandLabel.setText(String.format("Frequency Band (%s/%s) - Low %s High: %s", curFreqIndex + 1, curEventStationRatios.size(), lowFreq, highFreq)); + eventStationLabel.setText(String.format("Events: %s/%s - Station %s - Ratio: %s", numerEvent, denomEvent, station, diffAvg)); + } else { + freqBandLabel.setText("No frequency bands to select"); + eventStationLabel.setText(""); + } + } + + public PairT getSavedAxisLimits() { + return savedAxisLimits; + } + + public void setSavedAxisLimits(PairT axisLimits) { + this.savedAxisLimits = axisLimits; + } + + public void exportScreenshots(final File folder) { + String timestamp = SnapshotUtils.getTimestampWithLeadingSeparator(); + SnapshotUtils.writePng(folder, new Pair<>(WAVEFORM_PREFIX, ratioPlotNode), timestamp); + String plotId = selectedSinglePlot.getPlotIdentifier(); + if (plotId != null && !plotId.isEmpty()) { + exportSVG(selectedSinglePlot, folder + File.separator + WAVEFORM_PREFIX + plotId + "_" + timestamp + ".svg"); + exportSVG(ratioDiffWavePlot, folder + File.separator + RATIO_WAVEFORM_PREFIX + plotId + "_" + timestamp + ".svg"); + } else { + exportSVG(selectedSinglePlot, folder + File.separator + WAVEFORM_PREFIX + SnapshotUtils.getTimestampWithLeadingSeparator() + ".svg"); + exportSVG(ratioDiffWavePlot, folder + File.separator + RATIO_WAVEFORM_PREFIX + SnapshotUtils.getTimestampWithLeadingSeparator() + ".svg"); + } + } + + public void exportSVG(RatioDetailPlot plot, String path) { + try { + Files.write(Paths.get(path), plot.getSVG().getBytes()); + } catch (final IOException e) { + log.error("Error attempting to write plots for controller : {}", e.getLocalizedMessage(), e); + } + } + + @FXML + private void screenshotPlots(final ActionEvent e) { + Button btn = (Button) e.getSource(); + Parent pane = btn.getParent(); + final File folder = screenshotFolderChooser.showDialog(pane.getScene().getWindow()); + try { + if (folder != null && folder.exists() && folder.isDirectory() && folder.canWrite()) { + screenshotFolderChooser.setInitialDirectory(folder); + Platform.runLater(() -> exportScreenshots(folder)); + } + } catch (final SecurityException ex) { + log.warn("Exception trying to write screenshots to folder {} : {}", folder, ex.getLocalizedMessage(), ex); + } + } + + @FXML + private void showRatioOnMap(final ActionEvent e) { + + log.trace("Map button pressed!"); + SpectraRatioPairDetails ratioDetails = curEventStationRatios.get(curFrequencies.get(curFreqIndex)); + + if (mapImpl != null && ratioDetails != null) { + List listData = new ArrayList<>(); + listData.add(ratioDetails.getNumerWaveform()); + listData.add(ratioDetails.getDenomWaveform()); + + mapImpl.clearIcons(); + mapImpl.addIcons(iconFactory.genIconsFromWaveforms(eventSelectionCallback, stationSelectionCallback, listData)); + + Platform.runLater(() -> { + mapImpl.show(); + mapImpl.fitViewToActiveShapes(); + }); + } + } + + @FXML + public void initialize() { + topToolbar.getItems().add(prevStationButton); + topToolbar.getItems().add(nextStationButton); + topToolbar.getItems().add(prevButton); + topToolbar.getItems().add(nextButton); + topToolbar.getItems().add(alignPeaksToggle); + topToolbar.getItems().add(trueTimeToggle); + topToolbar.getItems().add(ratioWindowToggle); + topToolbar.getItems().add(freqBandLabel); + topToolbar.getItems().add(eventStationLabel); + + contextMenu.getItems().add(resetToPeak); + + resetToPeak.setOnAction(evt -> { + if (this.selectedSinglePlot != null) { + this.selectedSinglePlot.resetCuts(); + } + }); + + ratioSplitPane.addEventFilter(MouseEvent.MOUSE_CLICKED, t -> { + if (MouseButton.SECONDARY == t.getButton()) { + contextMenu.show(ratioSplitPane, t.getScreenX(), t.getScreenY()); + } else { + contextMenu.hide(); + } + }); + + nextStationButton.addEventHandler(MouseEvent.MOUSE_CLICKED, nextStationAction::handle); + prevStationButton.addEventHandler(MouseEvent.MOUSE_CLICKED, prevStationAction::handle); + nextButton.addEventHandler(MouseEvent.MOUSE_CLICKED, nextAction::handle); + prevButton.addEventHandler(MouseEvent.MOUSE_CLICKED, prevAction::handle); + alignPeaksToggle.addEventHandler(MouseEvent.MOUSE_CLICKED, alignPeaksToggleAction::handle); + trueTimeToggle.addEventHandler(MouseEvent.MOUSE_CLICKED, alignPeaksToggleAction::handle); + ratioWindowToggle.addEventHandler(MouseEvent.MOUSE_CLICKED, showRatioWindowToggleAction::handle); + + final Font sizedFont = Font.font(prevButton.getFont().getFamily(), 12f); + + nextButton.setFont(sizedFont); + prevButton.setFont(sizedFont); + nextStationButton.setFont(sizedFont); + prevStationButton.setFont(sizedFont); + alignPeaksToggle.setFont(sizedFont); + trueTimeToggle.setFont(sizedFont); + ratioWindowToggle.setFont(sizedFont); + + nextButton.setFocusTraversable(false); + prevButton.setFocusTraversable(false); + nextStationButton.setFocusTraversable(false); + prevStationButton.setFocusTraversable(false); + alignPeaksToggle.setFocusTraversable(false); + trueTimeToggle.setFocusTraversable(false); + ratioWindowToggle.setFocusTraversable(false); + + nextButton.setTooltip(new Tooltip("Go to higher frequency band")); + prevButton.setTooltip(new Tooltip("Go to lower frequency band")); + nextStationButton.setTooltip(new Tooltip("Go to next Station")); + prevStationButton.setTooltip(new Tooltip("Go to previous Station")); + alignPeaksToggle.setTooltip(new Tooltip("Toggle on to align waveform peaks")); + trueTimeToggle.setTooltip(new Tooltip("Toggle on to switch to 'True Time' view")); + ratioWindowToggle.setTooltip(new Tooltip("Toggle to show or hide the ratio waveform display at the bottom of plot")); + + alignPeaksToggle.setSelected(alignPeaksModeBoolean); + trueTimeToggle.setSelected(!alignPeaksModeBoolean); + if (alignPeaksModeBoolean) { + alignPeaksToggle.setStyle("-fx-background-color: DarkSeaGreen"); + } else { + trueTimeToggle.setStyle("-fx-background-color: DarkSeaGreen"); + } + if (ratioWindowModeBoolean) { + ratioWindowToggle.setStyle("-fx-background-color: DarkSeaGreen"); + } else { + ratioWindowToggle.setStyle(null); + } + + final Label label = new Label("\uE3B0"); + label.getStyleClass().add("material-icons-medium"); + label.setMaxHeight(16); + label.setMinWidth(16); + snapshotButton.setGraphic(label); + snapshotButton.setContentDisplay(ContentDisplay.CENTER); + + Label mapLabel = new Label("\uE55B"); + mapLabel.getStyleClass().add("material-icons-medium"); + mapLabel.setMaxHeight(16); + mapLabel.setMinWidth(16); + showRatioOnMapButton.setGraphic(mapLabel); + + screenshotFolderChooser.setTitle("Ratio Waveform Screenshot Export Folder"); + ratioSplitPane.setOnKeyReleased(this::triggerKeyEvent); + } + + private void updatePlotAxes(PlotAxisChange change) { + if (selectedSinglePlot != null) { + this.setSavedAxisLimits(change.getAxisLimits()); + if (change.isReset()) { + Platform.runLater(() -> { + selectedSinglePlot.replot(); + }); + } + } + } + + private void updatePlot() { + if (selectedSinglePlot != null) { + if (this.parentSpectraPlot != null) { + this.parentSpectraPlot.updatePlotPoint(); + } + Platform.runLater(() -> { + setDisplayText(selectedSinglePlot.getRatioDetails()); + selectedSinglePlot.plotRatio(); + ratioDiffWavePlot.plotDiffRatio(); + }); + } + } + + private void createPlotRatio(SpectraRatioPairDetails spectraRatioPairDetails, boolean alignPeaks) { + if (spectraRatioPairDetails != null) { + + waveformsPane.getChildren().clear(); + ratioWaveformPane.getChildren().clear(); + + SpectraRatioPairOperator ratio = new SpectraRatioPairOperator(spectraRatioPairDetails); + setDisplayText(ratio); + final RatioDetailPlot plot = new RatioDetailPlot(ratio, alignPeaks); + Double eventDistance = parentSpectraPlot.getEventPairDistanceKm(); + plot.setTitle(String.format("Events Distance: %s Km", dfmt4.format(eventDistance))); + plot.setMargin(null, null, null, null); + plot.getxAxis().setText(TIME_SECONDS_FROM_ORIGIN); + plot.setAlignPeaks(alignPeaks); + plot.attachToDisplayNode(waveformsPane); + waveformsPane.getChildren().add(snapshotButton); // Add snapshot button back + waveformsPane.getChildren().add(showRatioOnMapButton); // Add snapshot button back + selectedSinglePlot = plot; + + final RatioDetailPlot plotDiff = new RatioDetailPlot(ratio, alignPeaks); + + plotDiff.setMargin(null, null, null, null); + plotDiff.getxAxis().setText(TIME_SECONDS_FROM_ORIGIN); + plotDiff.setAlignPeaks(alignPeaks); + plotDiff.attachToDisplayNode(ratioWaveformPane); + + ratioDiffWavePlot = plotDiff; + + if (!ratioSplitPane.getDividers().isEmpty()) { + splitPaneDivider = ratioSplitPane.getDividers().get(0); + } + + plot.setAxisChangeListener(axisChange -> { + if (axisChange.getNewValue() instanceof PlotAxisChange) { + updatePlotAxes((PlotAxisChange) axisChange.getNewValue()); + } + }); + plot.setCutSegmentChangeListener(cutSegmentChange -> { + updatePlot(); + }); + + if (this.getSavedAxisLimits() != null) { + plot.setAxisLimits(this.getSavedAxisLimits().getFirst(), this.getSavedAxisLimits().getSecond()); + } else { + plot.resetAxisLimits(); + } + + showHideRatioWaveform(ratioWindowModeBoolean); + + plot.plotRatio(); + plotDiff.plotDiffRatio(); + } + } + + public void triggerKeyEvent(final KeyEvent event) { + if (event.getCode() == KeyCode.DOWN) { + prevStationButton.requestFocus(); + prevStationAction.handle(event); + } else if (event.getCode() == KeyCode.UP) { + nextStationButton.requestFocus(); + nextStationAction.handle(event); + } else if (event.getCode() == KeyCode.RIGHT) { + nextButton.requestFocus(); + nextAction.handle(event); + } else if (event.getCode() == KeyCode.LEFT) { + prevAction.handle(event); + prevButton.requestFocus(); + } + } + + public void attachToDisplayNode(final Pane parent) { + if (parent != null) { + parent.getChildren().add(borderPane); + } + } + +} diff --git a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/plotting/RatioMeasurementsGui.java b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/plotting/RatioMeasurementsGui.java new file mode 100644 index 00000000..d140efc8 --- /dev/null +++ b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/plotting/RatioMeasurementsGui.java @@ -0,0 +1,120 @@ +/* +* Copyright (c) 2021, Lawrence Livermore National Security, LLC. Produced at the Lawrence Livermore National Laboratory +* CODE-743439. +* All rights reserved. +* This file is part of CCT. For details, see https://github.com/LLNL/coda-calibration-tool. +* +* 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.plotting; + +import java.io.IOException; +import java.util.concurrent.CompletableFuture; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; + +import com.google.common.eventbus.EventBus; +import com.google.common.eventbus.Subscribe; + +import gov.llnl.gnem.apps.coda.calibration.gui.data.client.api.SpectraClient; +import gov.llnl.gnem.apps.coda.calibration.gui.data.client.api.SpectraRatioClient; +import gov.llnl.gnem.apps.coda.calibration.gui.data.exporters.SpectraRatioExporter; +import gov.llnl.gnem.apps.coda.calibration.model.messaging.RatioMeasurementEvent; +import gov.llnl.gnem.apps.coda.common.gui.plotting.SymbolStyleMapFactory; +import javafx.application.Platform; +import javafx.beans.property.Property; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.scene.text.Font; +import javafx.stage.Stage; +import javafx.stage.StageStyle; + +@Controller +public class RatioMeasurementsGui { + + private Parent root; + private Scene scene; + private Stage stage; + + private final SymbolStyleMapFactory symbolStyleFactory; + private RatioMeasurementSpectraPlotManager ratioPlotManager; + private final Property shouldFocus = new SimpleBooleanProperty(false); + + @Autowired + public RatioMeasurementsGui(final EventBus bus, final SymbolStyleMapFactory styleFactory, final CertLeafletMapController mapImpl, final MapPlottingUtilities iconFactory, + SpectraClient spectraClient, SpectraRatioClient spectraRatioClient, SpectraRatioExporter spectraRatioExporter) { + bus.register(this); + this.symbolStyleFactory = styleFactory; + this.ratioPlotManager = new RatioMeasurementSpectraPlotManager(symbolStyleFactory, mapImpl, iconFactory, spectraClient, spectraRatioExporter); + Platform.runLater(() -> { + final FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/fxml/SpectraRatioPlotGui.fxml")); + fxmlLoader.setController(this.ratioPlotManager); + stage = new Stage(StageStyle.DECORATED); + Font.loadFont(getClass().getResource("/fxml/MaterialIcons-Regular.ttf").toExternalForm(), 18); + try { + root = fxmlLoader.load(); + scene = new Scene(root); + stage.setScene(scene); + + stage.setOnHiding(e -> { + hide(); + }); + + stage.setOnShowing(e -> { + show(); + }); + + } catch (final IOException e) { + throw new IllegalStateException(e); + } + }); + } + + @Subscribe + private void listener(final RatioMeasurementEvent event) { + if (ratioPlotManager != null && event != null && event.getRatioMeasurements() != null) { + CompletableFuture.runAsync(() -> { + ratioPlotManager.setRatioMeasurements(event.getRatioMeasurements()); + Platform.runLater(() -> { + this.show(); + }); + }); + } + } + + public void hide() { + Platform.runLater(() -> { + stage.hide(); + }); + } + + public void show() { + final boolean showing = stage.isShowing(); + Platform.runLater(() -> { + stage.show(); + if (!showing || Boolean.TRUE.equals(shouldFocus.getValue())) { + stage.toFront(); + } + }); + } + + public void toFront() { + show(); + Platform.runLater(() -> { + stage.toFront(); + }); + } + + public Property focusProperty() { + return shouldFocus; + } +} diff --git a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/plotting/SpectralPlot.java b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/plotting/SpectralPlot.java index 659cb91b..c1c6e405 100644 --- a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/plotting/SpectralPlot.java +++ b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/calibration/gui/plotting/SpectralPlot.java @@ -1,588 +1,592 @@ -/* -* Copyright (c) 2021, Lawrence Livermore National Security, LLC. Produced at the Lawrence Livermore National Laboratory -* CODE-743439. -* All rights reserved. -* This file is part of CCT. For details, see https://github.com/LLNL/coda-calibration-tool. -* -* 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.plotting; - -import java.io.Serializable; -import java.text.NumberFormat; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -import gov.llnl.gnem.apps.coda.calibration.model.domain.Spectra; -import gov.llnl.gnem.apps.coda.common.gui.plotting.LabeledPlotPoint; -import gov.llnl.gnem.apps.coda.common.gui.plotting.PlotPoint; -import gov.llnl.gnem.apps.coda.common.gui.util.NumberFormatFactory; -import gov.llnl.gnem.apps.coda.common.model.util.SPECTRA_TYPES; -import javafx.geometry.Insets; -import javafx.geometry.Point2D; -import javafx.geometry.Pos; -import javafx.scene.control.Label; -import javafx.scene.layout.Background; -import javafx.scene.layout.BackgroundFill; -import javafx.scene.layout.BorderPane; -import javafx.scene.layout.CornerRadii; -import javafx.scene.layout.HBox; -import javafx.scene.layout.Pane; -import javafx.scene.layout.StackPane; -import javafx.scene.paint.Color; -import llnl.gnem.core.gui.plotting.api.Axis; -import llnl.gnem.core.gui.plotting.api.AxisLimits; -import llnl.gnem.core.gui.plotting.api.BasicPlot; -import llnl.gnem.core.gui.plotting.api.Line; -import llnl.gnem.core.gui.plotting.api.LineStyles; -import llnl.gnem.core.gui.plotting.api.PlotFactory; -import llnl.gnem.core.gui.plotting.api.PlotObject; -import llnl.gnem.core.gui.plotting.api.PlottingUtils; -import llnl.gnem.core.gui.plotting.api.Symbol; -import llnl.gnem.core.gui.plotting.api.SymbolStyles; - -public class SpectralPlot extends Pane implements Serializable { - - private static final String MATERIAL_ICONS_LARGE = "material-icons-large"; - private static final String LIKELY_POORLY_CONSTRAINED = "Likely poorly constrained!"; - - private static final long serialVersionUID = 1L; - - private static final String AVG_MW_LEGEND_LABEL = "Avg"; - - private static final String CODA_MW_LABEL = "Model Fit"; - - private static final double EPSILON = 1E-14; - - private double xmin = 1.0; - private double xmax = -xmin; - private double ymin = xmin; - private double ymax = -xmax; - - private double defaultXMin = 1.0; - private double defaultXMax = 1.0; - private double defaultYMin = 8.0; - private double defaultYMax = 20.0; - - private final NumberFormat dfmt = NumberFormatFactory.twoDecimalOneLeadingZero(); - - private boolean plotCorners = false; - - private BasicPlot plot; - private transient PlotFactory plotFactory; - - private Axis yAxis; - private Axis xAxis; - - private boolean autoCalculateXaxisRange = true; - private boolean autoCalculateYaxisRange = true; - private final Map> symbolMap = new HashMap<>(); - private final transient Object symbolMapLock = new Object(); - - private final transient List selectedData = new ArrayList<>(); - private final transient Object selectedDataLock = new Object(); - - private BorderPane plotContainerPane; - private HBox warningPane; - - public SpectralPlot() { - plotContainerPane = new BorderPane(); - plotContainerPane.prefHeightProperty().bind(this.heightProperty()); - plotContainerPane.prefWidthProperty().bind(this.widthProperty()); - this.getChildren().add(plotContainerPane); - plotFactory = new PlotlyPlotFactory(); - setupPlot(); - } - - private void setupPlot() { - plot = plotFactory.basicPlot(); - - yAxis = plotFactory.axis(Axis.Type.Y, "Y-axis label"); - xAxis = plotFactory.axis(Axis.Type.LOG_X, "X-axis label"); - plot.addAxes(yAxis, xAxis); - - plot.getTitle().setText("Title String"); - plot.getTitle().setFontSize(16); - plot.setSymbolSize(12); - - StackPane wrapper = new StackPane(); - wrapper.prefWidth(Double.MAX_VALUE); - wrapper.prefHeight(Double.MAX_VALUE); - plot.attachToDisplayNode(wrapper); - plot.setMargin(30, 40, 50, null); - - plotContainerPane.setCenter(wrapper); - plotContainerPane.setPickOnBounds(false); - - warningPane = new HBox(); - warningPane.setBackground(new Background(new BackgroundFill(Color.RED, CornerRadii.EMPTY, Insets.EMPTY))); - Label warningIcon = new Label("\uE000"); - warningIcon.getStyleClass().add(MATERIAL_ICONS_LARGE); - warningIcon.setPrefSize(32.0, 32.0); - warningIcon.setAlignment(Pos.CENTER); - - Label warningText = new Label(LIKELY_POORLY_CONSTRAINED); - warningText.setStyle("-fx-font-weight:bold; -fx-font-size: 28px;"); - - warningPane.getChildren().add(warningIcon); - warningPane.getChildren().add(warningText); - } - - public void clearPlot() { - xmin = 0.0; - xmax = 0.0; - ymin = 0.0; - ymax = 0.0; - } - - /** - * @param points - *

- * List of {X, Y, {@link SymbolStyles}} values. Presently this is - * used mostly as { log10(centerFrequency), log10(amplitiude), - * {@link SymbolStyles} } - *

- */ - protected void plotXYdata(final List points) { - if (points != null) { - symbolMap.clear(); - selectedData.clear(); - points.stream().filter(p -> rescalePlot(p.getX(), p.getY())).map(v -> { - String label; - if (v instanceof LabeledPlotPoint) { - label = ((LabeledPlotPoint) v).getLabel(); - } else { - label = ""; - } - return plotFactory.createSymbol(v.getStyle(), label, v.getX(), v.getY(), v.getColor(), Color.BLACK, v.getColor(), label, true); - }).forEach(symbol -> { - synchronized (symbolMapLock) { - //Dyne-cm to Newton-meters - symbolMap.computeIfAbsent(new Point2D(symbol.getX(), symbol.getY() - 7.0), v -> new ArrayList<>()).add(symbol); - } - plot.addPlotObject(symbol); - }); - } - } - - /** - * @param plots - *

- * List of {X, Y, {@link SymbolStyles}} values. Presently this is - * used mostly as { log10(centerFrequency), log10(amplitiude), - * {@link SymbolStyles} } - *

- * @param spectra - *

- * Spectra containing the amp/freq/mw information for a - * calibration spectra - *

- */ - public void plotXYdata(final List plots, final List spectra) { - plot.clear(); - - if (plots.size() > 1) { - // get the average for each freq and plot it - connect the points - final List averages = createAveragePlot(sortPointsByX(plots)); - - final double[] x = new double[averages.size()]; - final double[] y = new double[averages.size()]; - for (int i = 0; i < averages.size(); i++) { - final PlotPoint point = averages.get(i); - x[i] = point.getX(); - y[i] = point.getY(); - } - - final Line line = plotFactory.line(x, y, Color.BLACK, LineStyles.SOLID, 2); - line.setName(AVG_MW_LEGEND_LABEL); - plot.addPlotObject(line); - } - - if (spectra != null) { - for (final Spectra spec : spectra) { - if (plotCorners && spec.getCornerFrequency() != null) { - plotCornerFrequency(spec.getCornerFrequency()); - } - plotSpectraObject(plot, spec); - } - } - - plotXYdata(plots); - - refreshPlotAxes(); - - plot.replot(); - } - - private void plotCornerFrequency(final double cornerFreq) { - plot.addPlotObject(plotFactory.verticalLine(cornerFreq, 50, "~Fc (" + dfmt.format(cornerFreq) + ")").setDraggable(false).setLogScaleX(true).setFillColor(Color.BLACK)); - } - - private void plotSpectraObject(final BasicPlot jsubplot, final Spectra spectra) { - if (spectra != null && !spectra.getSpectraXY().isEmpty()) { - //Dyne-cm to nm for plot, in log - final List netMwValues = spectra.getSpectraXY().stream().map(d -> new PlotPoint(Math.pow(10, d.getX()), d.getY() - 7.0, null, null, null)).collect(Collectors.toList()); - final double[] x = new double[netMwValues.size()]; - final double[] y = new double[netMwValues.size()]; - for (int i = 0; i < netMwValues.size(); i++) { - final PlotPoint point = netMwValues.get(i); - x[i] = point.getX(); - y[i] = point.getY(); - } - - Line line = null; - switch (spectra.getType()) { - case REF: - line = plotFactory.line(x, y, Color.BLACK, LineStyles.DASH, 2); - break; - case VAL: - line = plotFactory.line(x, y, Color.BLUE, LineStyles.DASH_DOT, 2); - break; - case UQ1: - if (y.length > 0) { - line = plotFactory.line(x, y, Color.LIGHTGRAY, LineStyles.DASH, 2); - line.setLegendGrouping("UQ1"); - line.setName(dfmt.format(y[0])); - line.showInLegend(false); - - jsubplot.addPlotObject(PlottingUtils.legendOnlyLine("UQ1", plotFactory, Color.LIGHTGRAY, LineStyles.DASH)); - } - break; - case UQ2: - if (y.length > 0) { - line = plotFactory.line(x, y, Color.LIGHTGRAY, LineStyles.DOT, 2); - line.setLegendGrouping("UQ2"); - line.setName(dfmt.format(y[0])); - line.showInLegend(false); - - jsubplot.addPlotObject(PlottingUtils.legendOnlyLine("UQ2", plotFactory, Color.LIGHTGRAY, LineStyles.DASH)); - } - break; - case FIT: - line = plotFactory.line(x, y, Color.RED, LineStyles.DASH, 2); - - break; - default: - break; - } - if (line != null) { - if (SPECTRA_TYPES.UQ1 != spectra.getType() && SPECTRA_TYPES.UQ2 != spectra.getType()) { - String spectraName; - if (SPECTRA_TYPES.FIT == spectra.getType()) { - spectraName = CODA_MW_LABEL; - } else { - spectraName = spectra.getType().name(); - } - - line.setName(spectraName); - } - - jsubplot.addPlotObject(line); - } - } - } - - private List createAveragePlot(final List allOrderedPoints) { - final List xyvector = new ArrayList<>(); - final List amplitudes = new ArrayList<>(); - double xTest = allOrderedPoints.get(0).getX(); - - for (final PlotPoint point : allOrderedPoints) { - if (Math.abs(Math.abs(point.getX()) - Math.abs(xTest)) < EPSILON) { - amplitudes.add(point.getY()); - } else { - Double sum = 0d; - for (final Double vals : amplitudes) { - sum += vals; - } - final Double amplitude = sum / amplitudes.size(); - final PlotPoint xypoint = new PlotPoint(xTest, amplitude, null, null, null); - xyvector.add(xypoint); - - xTest = point.getX(); - amplitudes.clear(); - amplitudes.add(point.getY()); - } - } - // don't forget to add the last point - if (!amplitudes.isEmpty()) { - Double sum = 0d; - for (final Double vals : amplitudes) { - sum += vals; - } - final Double amplitude = sum / amplitudes.size(); - final PlotPoint xypoint = new PlotPoint(xTest, amplitude, null, null, null); - xyvector.add(xypoint); - } - - return xyvector; - } - - private List sortPointsByX(final List inPlots) { - final List plots = new ArrayList<>(inPlots); - final int smallestIndex = getSmallestX(plots); - final List orderedList = new ArrayList<>(plots.size()); - orderedList.add(plots.remove(smallestIndex)); - while (!plots.isEmpty()) { - // Find the index of the closest point (using another method) - final int nearestIndex = findNearestIndex(orderedList.get(orderedList.size() - 1), plots); - // Remove from the unorderedList and add to the ordered one - orderedList.add(plots.remove(nearestIndex)); - } - - return orderedList; - } - - private int getSmallestX(final List plots) { - int smallest = -1; - double test = Double.MAX_VALUE; - for (int i = 0; i < plots.size(); i++) { - if (test > plots.get(i).getX()) { - test = plots.get(i).getX(); - smallest = i; - } - } - return smallest; - } - - private int findNearestIndex(final PlotPoint thisPoint, final List listToSearch) { - double nearestDistSquared = Double.POSITIVE_INFINITY; - int nearestIndex = -1; - for (int i = 0; i < listToSearch.size(); i++) { - final PlotPoint point2 = listToSearch.get(i); - final double distsq = (thisPoint.getX() - point2.getX()) * (thisPoint.getX() - point2.getX()); - if (distsq < nearestDistSquared) { - nearestDistSquared = distsq; - nearestIndex = i; - } - } - return nearestIndex; - } - - /** - * Ensure that the plot includes the minimum and maximum x and y points - * - * @param x - * @param y - * @return - */ - public boolean rescalePlot(final double x, final double y) { - if (x < xmin) { - xmin = x; - } - if (x > xmax) { - xmax = x; - } - if (y < ymin) { - ymin = y; - } - if (y > ymax) { - ymax = y; - } - return true; - } - - public void setAllXlimits(final double xmin, final double xmax) { - plot.setAxisLimits(new AxisLimits(Axis.Type.LOG_X, xmin, xmax)); - this.xmin = xmin; - this.xmax = xmax; - plot.replot(); - } - - public void setAllXlimits() { - plot.setAxisLimits(new AxisLimits(Axis.Type.LOG_X, defaultXMin, defaultXMax)); - this.xmin = defaultXMin; - this.xmax = defaultXMax; - plot.replot(); - } - - public void setAllYlimits(final double ymin, final double ymax) { - plot.setAxisLimits(new AxisLimits(Axis.Type.Y, ymin, ymax)); - this.ymin = ymin; - this.ymax = ymax; - plot.replot(); - } - - public void setAllYlimits() { - plot.setAxisLimits(new AxisLimits(Axis.Type.Y, defaultYMin, defaultYMax)); - this.ymin = defaultYMin; - this.ymax = defaultYMax; - plot.replot(); - } - - public void refreshPlotAxes() { - double xMin; - double xMax; - double yMin; - double yMax; - - if (autoCalculateYaxisRange) { - yMin = ymin - Math.abs(ymin * .3); - yMax = ymax + Math.abs(ymax * .3); - } else { - yMin = defaultYMin; - yMax = defaultYMax; - } - - if (autoCalculateXaxisRange) { - xMin = xmin - Math.abs(xmin * .1); - xMax = xmax + Math.abs(xmax * .1); - } else { - xMin = defaultXMin; - xMax = defaultXMax; - } - plot.setAxisLimits(new AxisLimits(Axis.Type.LOG_X, Math.log10(xMin), Math.log10(xMax)), new AxisLimits(Axis.Type.Y, yMin, yMax)); - plot.replot(); - } - - /** Set the Title, X and Y axis labels simultaneously */ - public void setLabels(final String title, final String xlabel, final String ylabel) { - plot.getTitle().setText(title); - xAxis.setText(xlabel); - yAxis.setText(ylabel); - } - - public BasicPlot getSubplot() { - return plot; - } - - public double getDefaultYMin() { - return defaultYMin; - } - - public void setDefaultYMin(final double defaultYMin) { - this.defaultYMin = defaultYMin; - } - - public double getDefaultYMax() { - return defaultYMax; - } - - public void setDefaultYMax(final double defaultYMax) { - this.defaultYMax = defaultYMax; - } - - public double getDefaultXMin() { - return defaultXMin; - } - - public void setDefaultXMin(final double defaultXMin) { - this.defaultXMin = defaultXMin; - } - - public double getDefaultXMax() { - return defaultXMax; - } - - public void setDefaultXMax(final double defaultXMax) { - this.defaultXMax = defaultXMax; - } - - public void setAutoCalculateXaxisRange(final boolean autoCalculateXaxisRange) { - this.autoCalculateXaxisRange = autoCalculateXaxisRange; - } - - public void setAutoCalculateYaxisRange(final boolean autoCalculateYaxisRange) { - this.autoCalculateYaxisRange = autoCalculateYaxisRange; - } - - public Map> getSymbolMap() { - return symbolMap; - } - - public void selectPoint(Point2D xyPoint) { - List symbols = symbolMap.get(xyPoint); - if (symbols != null) { - symbols.forEach(symbol -> { - plot.removePlotObject(symbol); - symbol.setEdgeColor(symbol.getFillColor()); - symbol.setFillColor(Color.YELLOW); - symbol.setZindex(1000); - plot.addPlotObject(symbol); - synchronized (selectedDataLock) { - selectedData.add(symbol); - } - }); - } - } - - public void deselectPoint(Point2D xyPoint) { - List symbols = symbolMap.get(xyPoint); - if (symbols != null) { - symbols.forEach(symbol -> { - // proxy for "are you active" - if (symbol.getZindex() != null) { - setStandardSymbolStyle(symbol); - synchronized (selectedDataLock) { - selectedData.remove(symbol); - } - } - }); - } - } - - public List getSelectedPoints() { - return selectedData.stream().map(sym -> new Point2D(sym.getX(), sym.getY())).collect(Collectors.toList()); - } - - public void deselectAllPoints() { - synchronized (selectedDataLock) { - selectedData.forEach(this::setStandardSymbolStyle); - selectedData.clear(); - } - } - - private void setStandardSymbolStyle(PlotObject symbol) { - plot.removePlotObject(symbol); - symbol.setFillColor(symbol.getEdgeColor()); - symbol.setEdgeColor(Color.BLACK); - symbol.setZindex(null); - plot.addPlotObject(symbol); - } - - public void setPointsActive(List points, boolean active) { - for (Point2D xyPoint : points) { - List symbols = symbolMap.get(xyPoint); - if (symbols != null) { - symbols.forEach(symbol -> { - plot.removePlotObject(symbol); - if (active) { - symbol.setFillColor(symbol.getEdgeColor()); - symbol.setEdgeColor(Color.BLACK); - } else { - symbol.setEdgeColor(symbol.getFillColor()); - symbol.setFillColor(Color.GRAY); - } - plot.addPlotObject(symbol); - }); - } - } - plot.replot(); - } - - public void showCornerFrequency(final boolean showCornerFreq) { - this.plotCorners = showCornerFreq; - } - - public String getTitle() { - return plot.getTitle().getText(); - } - - public void showConstraintWarningBanner(boolean visible) { - if (visible) { - plotContainerPane.setBottom(warningPane); - } else { - plotContainerPane.setBottom(null); - } - } - -} +/* +* Copyright (c) 2021, Lawrence Livermore National Security, LLC. Produced at the Lawrence Livermore National Laboratory +* CODE-743439. +* All rights reserved. +* This file is part of CCT. For details, see https://github.com/LLNL/coda-calibration-tool. +* +* 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.plotting; + +import java.io.Serializable; +import java.text.NumberFormat; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import gov.llnl.gnem.apps.coda.calibration.model.domain.Spectra; +import gov.llnl.gnem.apps.coda.common.gui.plotting.LabeledPlotPoint; +import gov.llnl.gnem.apps.coda.common.gui.plotting.PlotPoint; +import gov.llnl.gnem.apps.coda.common.gui.util.NumberFormatFactory; +import gov.llnl.gnem.apps.coda.common.model.util.SPECTRA_TYPES; +import javafx.geometry.Insets; +import javafx.geometry.Point2D; +import javafx.geometry.Pos; +import javafx.scene.control.Label; +import javafx.scene.layout.Background; +import javafx.scene.layout.BackgroundFill; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.CornerRadii; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Pane; +import javafx.scene.layout.StackPane; +import javafx.scene.paint.Color; +import llnl.gnem.core.gui.plotting.api.Axis; +import llnl.gnem.core.gui.plotting.api.AxisLimits; +import llnl.gnem.core.gui.plotting.api.BasicPlot; +import llnl.gnem.core.gui.plotting.api.Line; +import llnl.gnem.core.gui.plotting.api.LineStyles; +import llnl.gnem.core.gui.plotting.api.PlotFactory; +import llnl.gnem.core.gui.plotting.api.PlotObject; +import llnl.gnem.core.gui.plotting.api.PlottingUtils; +import llnl.gnem.core.gui.plotting.api.Symbol; +import llnl.gnem.core.gui.plotting.api.SymbolStyles; + +public class SpectralPlot extends Pane implements Serializable { + + private static final String MATERIAL_ICONS_LARGE = "material-icons-large"; + private static final String LIKELY_POORLY_CONSTRAINED = "Likely poorly constrained!"; + + private static final long serialVersionUID = 1L; + + private static final String AVG_MW_LEGEND_LABEL = "Avg"; + + private static final String CODA_MW_LABEL = "Model Fit"; + + private static final double EPSILON = 1E-14; + + private double xmin = 1.0; + private double xmax = -xmin; + private double ymin = xmin; + private double ymax = -xmax; + + private double defaultXMin = 1.0; + private double defaultXMax = 1.0; + private double defaultYMin = 8.0; + private double defaultYMax = 20.0; + + private final NumberFormat dfmt = NumberFormatFactory.twoDecimalOneLeadingZero(); + + private boolean plotCorners = false; + + private BasicPlot plot; + private transient PlotFactory plotFactory; + + private Axis yAxis; + private Axis xAxis; + + private boolean autoCalculateXaxisRange = true; + private boolean autoCalculateYaxisRange = true; + private final Map> symbolMap = new HashMap<>(); + private final transient Object symbolMapLock = new Object(); + + private final transient List selectedData = new ArrayList<>(); + private final transient Object selectedDataLock = new Object(); + + private BorderPane plotContainerPane; + private HBox warningPane; + + public SpectralPlot() { + plotContainerPane = new BorderPane(); + plotContainerPane.prefHeightProperty().bind(this.heightProperty()); + plotContainerPane.prefWidthProperty().bind(this.widthProperty()); + this.getChildren().add(plotContainerPane); + plotFactory = new PlotlyPlotFactory(); + setupPlot(); + } + + private void setupPlot() { + plot = plotFactory.basicPlot(); + + yAxis = plotFactory.axis(Axis.Type.Y, "Y-axis label"); + xAxis = plotFactory.axis(Axis.Type.LOG_X, "X-axis label"); + plot.addAxes(yAxis, xAxis); + + plot.getTitle().setText("Title String"); + plot.getTitle().setFontSize(16); + plot.setSymbolSize(12); + + StackPane wrapper = new StackPane(); + wrapper.prefWidth(Double.MAX_VALUE); + wrapper.prefHeight(Double.MAX_VALUE); + plot.attachToDisplayNode(wrapper); + plot.setMargin(30, 40, 50, null); + + plotContainerPane.setCenter(wrapper); + plotContainerPane.setPickOnBounds(false); + + warningPane = new HBox(); + warningPane.setBackground(new Background(new BackgroundFill(Color.RED, CornerRadii.EMPTY, Insets.EMPTY))); + Label warningIcon = new Label("\uE000"); + warningIcon.getStyleClass().add(MATERIAL_ICONS_LARGE); + warningIcon.setPrefSize(32.0, 32.0); + warningIcon.setAlignment(Pos.CENTER); + + Label warningText = new Label(LIKELY_POORLY_CONSTRAINED); + warningText.setStyle("-fx-font-weight:bold; -fx-font-size: 28px;"); + + warningPane.getChildren().add(warningIcon); + warningPane.getChildren().add(warningText); + } + + public void clearPlot() { + xmin = 0.0; + xmax = 0.0; + ymin = 0.0; + ymax = 0.0; + } + + /** + * @param points + *

+ * List of {X, Y, {@link SymbolStyles}} values. Presently this is + * used mostly as { log10(centerFrequency), log10(amplitiude), + * {@link SymbolStyles} } + *

+ */ + protected void plotXYdata(final List points) { + if (points != null) { + symbolMap.clear(); + selectedData.clear(); + points.stream().filter(p -> rescalePlot(p.getX(), p.getY())).map(v -> { + String label; + if (v instanceof LabeledPlotPoint) { + label = ((LabeledPlotPoint) v).getLabel(); + } else { + label = ""; + } + return plotFactory.createSymbol(v.getStyle(), label, v.getX(), v.getY(), v.getColor(), Color.BLACK, v.getColor(), label, true); + }).forEach(symbol -> { + synchronized (symbolMapLock) { + //Dyne-cm to Newton-meters + symbolMap.computeIfAbsent(new Point2D(symbol.getX(), symbol.getY() - 7.0), v -> new ArrayList<>()).add(symbol); + } + plot.addPlotObject(symbol); + }); + } + } + + /** + * @param plots + *

+ * List of {X, Y, {@link SymbolStyles}} values. Presently this is + * used mostly as { log10(centerFrequency), log10(amplitiude), + * {@link SymbolStyles} } + *

+ * @param spectra + *

+ * Spectra containing the amp/freq/mw information for a + * calibration spectra + *

+ */ + public void plotXYdata(final List plots, final List spectra, final String label) { + plot.clear(); + + if (plots.size() > 1) { + // get the average for each freq and plot it - connect the points + final List averages = createAveragePlot(sortPointsByX(plots)); + + final double[] x = new double[averages.size()]; + final double[] y = new double[averages.size()]; + for (int i = 0; i < averages.size(); i++) { + final PlotPoint point = averages.get(i); + x[i] = point.getX(); + y[i] = point.getY(); + } + + final Line line = plotFactory.line(x, y, Color.BLACK, LineStyles.SOLID, 2); + String lineLabel = AVG_MW_LEGEND_LABEL; + if (label != null) { + lineLabel = label; + } + line.setName(lineLabel); + plot.addPlotObject(line); + } + + if (spectra != null) { + for (final Spectra spec : spectra) { + if (plotCorners && spec.getCornerFrequency() != null) { + plotCornerFrequency(spec.getCornerFrequency()); + } + plotSpectraObject(plot, spec); + } + } + + plotXYdata(plots); + + refreshPlotAxes(); + + plot.replot(); + } + + private void plotCornerFrequency(final double cornerFreq) { + plot.addPlotObject(plotFactory.verticalLine(cornerFreq, 50, "~Fc (" + dfmt.format(cornerFreq) + ")").setDraggable(false).setLogScaleX(true).setFillColor(Color.BLACK)); + } + + private void plotSpectraObject(final BasicPlot jsubplot, final Spectra spectra) { + if (spectra != null && !spectra.getSpectraXY().isEmpty()) { + //Dyne-cm to nm for plot, in log + final List netMwValues = spectra.getSpectraXY().stream().map(d -> new PlotPoint(Math.pow(10, d.getX()), d.getY() - 7.0, null, null, null)).collect(Collectors.toList()); + final double[] x = new double[netMwValues.size()]; + final double[] y = new double[netMwValues.size()]; + for (int i = 0; i < netMwValues.size(); i++) { + final PlotPoint point = netMwValues.get(i); + x[i] = point.getX(); + y[i] = point.getY(); + } + + Line line = null; + switch (spectra.getType()) { + case REF: + line = plotFactory.line(x, y, Color.BLACK, LineStyles.DASH, 2); + break; + case VAL: + line = plotFactory.line(x, y, Color.BLUE, LineStyles.DASH_DOT, 2); + break; + case UQ1: + if (y.length > 0) { + line = plotFactory.line(x, y, Color.LIGHTGRAY, LineStyles.DASH, 2); + line.setLegendGrouping("UQ1"); + line.setName(dfmt.format(y[0])); + line.showInLegend(false); + + jsubplot.addPlotObject(PlottingUtils.legendOnlyLine("UQ1", plotFactory, Color.LIGHTGRAY, LineStyles.DASH)); + } + break; + case UQ2: + if (y.length > 0) { + line = plotFactory.line(x, y, Color.LIGHTGRAY, LineStyles.DOT, 2); + line.setLegendGrouping("UQ2"); + line.setName(dfmt.format(y[0])); + line.showInLegend(false); + + jsubplot.addPlotObject(PlottingUtils.legendOnlyLine("UQ2", plotFactory, Color.LIGHTGRAY, LineStyles.DOT)); + } + break; + case FIT: + line = plotFactory.line(x, y, Color.RED, LineStyles.DASH, 2); + + break; + default: + break; + } + if (line != null) { + if (SPECTRA_TYPES.UQ1 != spectra.getType() && SPECTRA_TYPES.UQ2 != spectra.getType()) { + String spectraName; + if (SPECTRA_TYPES.FIT == spectra.getType()) { + spectraName = CODA_MW_LABEL; + } else { + spectraName = spectra.getType().name(); + } + + line.setName(spectraName); + } + + jsubplot.addPlotObject(line); + } + } + } + + private List createAveragePlot(final List allOrderedPoints) { + final List xyvector = new ArrayList<>(); + final List amplitudes = new ArrayList<>(); + double xTest = allOrderedPoints.get(0).getX(); + + for (final PlotPoint point : allOrderedPoints) { + if (Math.abs(Math.abs(point.getX()) - Math.abs(xTest)) < EPSILON) { + amplitudes.add(point.getY()); + } else { + Double sum = 0d; + for (final Double vals : amplitudes) { + sum += vals; + } + final Double amplitude = sum / amplitudes.size(); + final PlotPoint xypoint = new PlotPoint(xTest, amplitude, null, null, null); + xyvector.add(xypoint); + + xTest = point.getX(); + amplitudes.clear(); + amplitudes.add(point.getY()); + } + } + // don't forget to add the last point + if (!amplitudes.isEmpty()) { + Double sum = 0d; + for (final Double vals : amplitudes) { + sum += vals; + } + final Double amplitude = sum / amplitudes.size(); + final PlotPoint xypoint = new PlotPoint(xTest, amplitude, null, null, null); + xyvector.add(xypoint); + } + + return xyvector; + } + + private List sortPointsByX(final List inPlots) { + final List plots = new ArrayList<>(inPlots); + final int smallestIndex = getSmallestX(plots); + final List orderedList = new ArrayList<>(plots.size()); + orderedList.add(plots.remove(smallestIndex)); + while (!plots.isEmpty()) { + // Find the index of the closest point (using another method) + final int nearestIndex = findNearestIndex(orderedList.get(orderedList.size() - 1), plots); + // Remove from the unorderedList and add to the ordered one + orderedList.add(plots.remove(nearestIndex)); + } + + return orderedList; + } + + private int getSmallestX(final List plots) { + int smallest = -1; + double test = Double.MAX_VALUE; + for (int i = 0; i < plots.size(); i++) { + if (test > plots.get(i).getX()) { + test = plots.get(i).getX(); + smallest = i; + } + } + return smallest; + } + + private int findNearestIndex(final PlotPoint thisPoint, final List listToSearch) { + double nearestDistSquared = Double.POSITIVE_INFINITY; + int nearestIndex = -1; + for (int i = 0; i < listToSearch.size(); i++) { + final PlotPoint point2 = listToSearch.get(i); + final double distsq = (thisPoint.getX() - point2.getX()) * (thisPoint.getX() - point2.getX()); + if (distsq < nearestDistSquared) { + nearestDistSquared = distsq; + nearestIndex = i; + } + } + return nearestIndex; + } + + /** + * Ensure that the plot includes the minimum and maximum x and y points + * + * @param x + * @param y + * @return + */ + public boolean rescalePlot(final double x, final double y) { + if (x < xmin) { + xmin = x; + } + if (x > xmax) { + xmax = x; + } + if (y < ymin) { + ymin = y; + } + if (y > ymax) { + ymax = y; + } + return true; + } + + public void setAllXlimits(final double xmin, final double xmax) { + plot.setAxisLimits(new AxisLimits(Axis.Type.LOG_X, xmin, xmax)); + this.xmin = xmin; + this.xmax = xmax; + plot.replot(); + } + + public void setAllXlimits() { + plot.setAxisLimits(new AxisLimits(Axis.Type.LOG_X, defaultXMin, defaultXMax)); + this.xmin = defaultXMin; + this.xmax = defaultXMax; + plot.replot(); + } + + public void setAllYlimits(final double ymin, final double ymax) { + plot.setAxisLimits(new AxisLimits(Axis.Type.Y, ymin, ymax)); + this.ymin = ymin; + this.ymax = ymax; + plot.replot(); + } + + public void setAllYlimits() { + plot.setAxisLimits(new AxisLimits(Axis.Type.Y, defaultYMin, defaultYMax)); + this.ymin = defaultYMin; + this.ymax = defaultYMax; + plot.replot(); + } + + public void refreshPlotAxes() { + double xMin; + double xMax; + double yMin; + double yMax; + + if (autoCalculateYaxisRange) { + yMin = ymin - Math.abs(ymin * .3); + yMax = ymax + Math.abs(ymax * .3); + } else { + yMin = defaultYMin; + yMax = defaultYMax; + } + + if (autoCalculateXaxisRange) { + xMin = xmin - Math.abs(xmin * .1); + xMax = xmax + Math.abs(xmax * .1); + } else { + xMin = defaultXMin; + xMax = defaultXMax; + } + plot.setAxisLimits(new AxisLimits(Axis.Type.LOG_X, Math.log10(xMin), Math.log10(xMax)), new AxisLimits(Axis.Type.Y, yMin, yMax)); + plot.replot(); + } + + /** Set the Title, X and Y axis labels simultaneously */ + public void setLabels(final String title, final String xlabel, final String ylabel) { + plot.getTitle().setText(title); + xAxis.setText(xlabel); + yAxis.setText(ylabel); + } + + public BasicPlot getSubplot() { + return plot; + } + + public double getDefaultYMin() { + return defaultYMin; + } + + public void setDefaultYMin(final double defaultYMin) { + this.defaultYMin = defaultYMin; + } + + public double getDefaultYMax() { + return defaultYMax; + } + + public void setDefaultYMax(final double defaultYMax) { + this.defaultYMax = defaultYMax; + } + + public double getDefaultXMin() { + return defaultXMin; + } + + public void setDefaultXMin(final double defaultXMin) { + this.defaultXMin = defaultXMin; + } + + public double getDefaultXMax() { + return defaultXMax; + } + + public void setDefaultXMax(final double defaultXMax) { + this.defaultXMax = defaultXMax; + } + + public void setAutoCalculateXaxisRange(final boolean autoCalculateXaxisRange) { + this.autoCalculateXaxisRange = autoCalculateXaxisRange; + } + + public void setAutoCalculateYaxisRange(final boolean autoCalculateYaxisRange) { + this.autoCalculateYaxisRange = autoCalculateYaxisRange; + } + + public Map> getSymbolMap() { + return symbolMap; + } + + public void selectPoint(Point2D xyPoint) { + List symbols = symbolMap.get(xyPoint); + if (symbols != null) { + symbols.forEach(symbol -> { + plot.removePlotObject(symbol); + symbol.setEdgeColor(symbol.getFillColor()); + symbol.setFillColor(Color.YELLOW); + symbol.setZindex(1000); + plot.addPlotObject(symbol); + synchronized (selectedDataLock) { + selectedData.add(symbol); + } + }); + } + } + + public void deselectPoint(Point2D xyPoint) { + List symbols = symbolMap.get(xyPoint); + if (symbols != null) { + symbols.forEach(symbol -> { + // proxy for "are you active" + if (symbol.getZindex() != null) { + setStandardSymbolStyle(symbol); + synchronized (selectedDataLock) { + selectedData.remove(symbol); + } + } + }); + } + } + + public List getSelectedPoints() { + return selectedData.stream().map(sym -> new Point2D(sym.getX(), sym.getY())).collect(Collectors.toList()); + } + + public void deselectAllPoints() { + synchronized (selectedDataLock) { + selectedData.forEach(this::setStandardSymbolStyle); + selectedData.clear(); + } + } + + private void setStandardSymbolStyle(PlotObject symbol) { + plot.removePlotObject(symbol); + symbol.setFillColor(symbol.getEdgeColor()); + symbol.setEdgeColor(Color.BLACK); + symbol.setZindex(null); + plot.addPlotObject(symbol); + } + + public void setPointsActive(List points, boolean active) { + for (Point2D xyPoint : points) { + List symbols = symbolMap.get(xyPoint); + if (symbols != null) { + symbols.forEach(symbol -> { + plot.removePlotObject(symbol); + if (active) { + symbol.setFillColor(symbol.getEdgeColor()); + symbol.setEdgeColor(Color.BLACK); + } else { + symbol.setEdgeColor(symbol.getFillColor()); + symbol.setFillColor(Color.GRAY); + } + plot.addPlotObject(symbol); + }); + } + } + plot.replot(); + } + + public void showCornerFrequency(final boolean showCornerFreq) { + this.plotCorners = showCornerFreq; + } + + public String getTitle() { + return plot.getTitle().getText(); + } + + public void showConstraintWarningBanner(boolean visible) { + if (visible) { + plotContainerPane.setBottom(warningPane); + } else { + plotContainerPane.setBottom(null); + } + } + +} diff --git a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/common/gui/controllers/AbstractSeismogramSaveLoadController.java b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/common/gui/controllers/AbstractSeismogramSaveLoadController.java index ec613c13..89282f93 100644 --- a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/common/gui/controllers/AbstractSeismogramSaveLoadController.java +++ b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/common/gui/controllers/AbstractSeismogramSaveLoadController.java @@ -95,7 +95,7 @@ public void saveToDirectory(File exportDirectory) { ProgressMonitor processingMonitor = new ProgressMonitor("Exporting", new ProgressEventProgressListener(bus, processingProgressEvent)); try { - ProgressGui progressGui = new ProgressGui(); + ProgressGui progressGui = ProgressGui.getInstance(); progressGui.show(); progressGui.addProgressMonitor(processingMonitor); fileProcessingProgress.setTotal(1l); @@ -147,7 +147,7 @@ public void loadFiles(List inputFiles, Runnable completionCallback, Progre Platform.runLater(() -> processingFailedMonitor.getProgressBar().getStyleClass().add("red-bar")); try { - ProgressGui progressGui = new ProgressGui(); + ProgressGui progressGui = ProgressGui.getInstance(); progressGui.show(); progressGui.addProgressMonitor(processingMonitor); progressGui.addProgressMonitor(processingFailedMonitor); diff --git a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/common/gui/controllers/ProgressGui.java b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/common/gui/controllers/ProgressGui.java index d0728657..2870cbc0 100644 --- a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/common/gui/controllers/ProgressGui.java +++ b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/common/gui/controllers/ProgressGui.java @@ -27,6 +27,7 @@ import javafx.scene.Node; import javafx.scene.Parent; import javafx.scene.Scene; +import javafx.scene.control.Button; import javafx.scene.control.TableCell; import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; @@ -49,10 +50,15 @@ public class ProgressGui { @FXML private TableColumn taskColumn; + @FXML + private Button clearAllBtn; + private Stage stage; private ObservableList monitors = FXCollections.observableArrayList(); - public ProgressGui() { + private static ProgressGui progressGui; + + private ProgressGui() { Platform.runLater(() -> { FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/fxml/ProgressDisplay.fxml")); fxmlLoader.setController(this); @@ -99,6 +105,22 @@ protected void updateItem(Node item, boolean empty) { }); } + @FXML + public void clearAllProgressMonitors() { + if (progressGui != null && progressGui.monitors != null) { + progressGui.monitors.clear(); + } + } + + public static ProgressGui getInstance() { + if (progressGui == null) { + progressGui = new ProgressGui(); + progressGui.setAlwaysOnTop(true); + } + + return progressGui; + } + public void addProgressMonitor(ProgressMonitor monitor) { monitors.add(monitor); } diff --git a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/common/gui/data/client/WaveformWebClient.java b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/common/gui/data/client/WaveformWebClient.java index 8f92b29a..4b1c9720 100644 --- a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/common/gui/data/client/WaveformWebClient.java +++ b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/common/gui/data/client/WaveformWebClient.java @@ -1,231 +1,231 @@ -/* -* Copyright (c) 2018, Lawrence Livermore National Security, LLC. Produced at the Lawrence Livermore National Laboratory -* CODE-743439. -* All rights reserved. -* This file is part of CCT. For details, see https://github.com/LLNL/coda-calibration-tool. -* -* 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.common.gui.data.client; - -import java.util.Collection; -import java.util.List; -import java.util.Objects; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.MediaType; -import org.springframework.http.client.MultipartBodyBuilder; -import org.springframework.stereotype.Component; -import org.springframework.web.reactive.function.client.WebClient; - -import com.fasterxml.jackson.core.JsonProcessingException; - -import gov.llnl.gnem.apps.coda.common.gui.data.client.api.WaveformClient; -import gov.llnl.gnem.apps.coda.common.model.domain.SyntheticCoda; -import gov.llnl.gnem.apps.coda.common.model.domain.Waveform; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -@Component -public class WaveformWebClient implements WaveformClient { - - private static final Logger log = LoggerFactory.getLogger(WaveformWebClient.class); - - private WebClient client; - - @Autowired - public WaveformWebClient(WebClient client) { - this.client = client; - } - - @Override - public Mono getWaveformFromId(Long id) { - return client.get().uri("/single-waveform/{id}", id).accept(MediaType.APPLICATION_JSON).retrieve().bodyToMono(Waveform.class); - } - - @Override - public Mono getSyntheticFromWaveformId(Long id) { - return client.get().uri("/synthetics/single/{id}", id).accept(MediaType.APPLICATION_JSON).retrieve().bodyToMono(SyntheticCoda.class); - } - - @Override - public Mono postWaveform(Waveform segment) throws JsonProcessingException { - return client.post().uri("/single-waveform").contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON).bodyValue(segment).retrieve().bodyToMono(Waveform.class); - } - - @Override - public Flux postWaveforms(Long sessionId, List segments) { - return client.post().uri("/waveforms/batch/" + sessionId).contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON).bodyValue(segments).retrieve().bodyToFlux(String.class); - } - - @Override - public Flux getAllStacks() { - return client.get() - .uri("/waveforms/query/stacks") - .accept(MediaType.APPLICATION_JSON) - .retrieve() - .bodyToFlux(Waveform.class) - .doOnError(e -> log.error(e.getMessage(), e)) - .onErrorReturn(new Waveform()); - } - - @Override - public Flux getAllActiveStacks() { - return client.get() - .uri("/waveforms/query/active-stacks") - .accept(MediaType.APPLICATION_JSON) - .retrieve() - .bodyToFlux(Waveform.class) - .doOnError(e -> log.error(e.getMessage(), e)) - .onErrorReturn(new Waveform()); - } - - @Override - public Flux getSharedEventStationWaveformsById(Long id) { - return client.get() - .uri("/waveforms/query/shared-event-station-by-id/{id}", id) - .accept(MediaType.APPLICATION_JSON) - .retrieve() - .bodyToFlux(Waveform.class) - .filter(Objects::nonNull) - .onErrorReturn(new Waveform()); - } - - @Override - public Flux getActiveSharedEventStationWaveformsById(Long id) { - return client.get() - .uri("/waveforms/query/active-shared-event-station-by-id/{id}", id) - .accept(MediaType.APPLICATION_JSON) - .retrieve() - .bodyToFlux(Waveform.class) - .filter(Objects::nonNull) - .onErrorReturn(new Waveform()); - } - - @Override - public Flux getUniqueEventStationMetadataForStacks() { - return client.get() - .uri("/waveforms/query/unique-by-event-station") - .accept(MediaType.APPLICATION_JSON) - .retrieve() - .bodyToFlux(Waveform.class) - .filter(w -> w != null && w.getId() != null) - .onErrorReturn(new Waveform()); - } - - @Override - public Flux getWaveformsFromIds(Collection ids) { - return client.get() - .uri("/waveforms/batch/{ids}", ids.toString().replaceAll("\\[|\\]", "")) - .accept(MediaType.APPLICATION_JSON) - .retrieve() - .bodyToFlux(Waveform.class) - .filter(Objects::nonNull) - .onErrorReturn(new Waveform()); - } - - @Override - public Flux getWaveformMetadataFromIds(List ids) { - return client.get() - .uri("/waveforms/metadata/batch/{ids}", ids.toString().replaceAll("\\[|\\]", "")) - .accept(MediaType.APPLICATION_JSON) - .retrieve() - .bodyToFlux(Waveform.class) - .filter(Objects::nonNull) - .onErrorReturn(new Waveform()); - } - - @Override - public Flux getSyntheticsFromWaveformIds(Collection ids) { - return client.get() - .uri("/synthetics/batch/{ids}", ids.toString().replaceAll("\\[|\\]", "")) - .accept(MediaType.APPLICATION_JSON) - .retrieve() - .bodyToFlux(SyntheticCoda.class) - .filter(Objects::nonNull) - .onErrorReturn(new SyntheticCoda()); - } - - @Override - public Flux setWaveformsActiveByIds(List selectedWaveforms, boolean active) { - return client.post() - .uri("/waveforms/set-active/batch/" + active) - .contentType(MediaType.APPLICATION_JSON) - .accept(MediaType.APPLICATION_JSON) - .bodyValue(selectedWaveforms) - .retrieve() - .bodyToFlux(String.class); - } - - @Override - public Flux setWaveformsActiveByEventId(String id, boolean active) { - return client.post() - .uri("/waveforms/set-active/by-event-id/" + active) - .contentType(MediaType.APPLICATION_JSON) - .accept(MediaType.APPLICATION_JSON) - .bodyValue(id) - .retrieve() - .bodyToFlux(String.class); - } - - @Override - public Flux setWaveformsActiveByStationName(String name, boolean active) { - return client.post() - .uri("/waveforms/set-active/by-station-name/" + active) - .contentType(MediaType.APPLICATION_JSON) - .accept(MediaType.APPLICATION_JSON) - .bodyValue(name) - .retrieve() - .bodyToFlux(String.class); - } - - @Override - public Flux setWaveformsActiveByStationNameAndEventId(String name, String id, boolean active) { - - MultipartBodyBuilder mbb = new MultipartBodyBuilder(); - mbb.part("stationName", name, MediaType.APPLICATION_JSON); - mbb.part("eventId", id, MediaType.APPLICATION_JSON); - - return client.post() - .uri("/waveforms/set-active/by-station-name-and-event-id/" + active) - .contentType(MediaType.APPLICATION_JSON) - .accept(MediaType.APPLICATION_JSON) - .bodyValue(mbb.build()) - .retrieve() - .bodyToFlux(String.class); - } - - @Override - public Mono clearAutoPicks() { - return client.get().uri("/waveform-picks/clear-autopicks").accept(MediaType.APPLICATION_JSON).retrieve().bodyToMono(String.class); - } - - @Override - public Flux setWaveformsActiveOutsidePolygon(boolean active) { - return client.post() - .uri("/geometry/set-active/waveforms-outside-polygon/" + active) - .contentType(MediaType.APPLICATION_JSON) - .accept(MediaType.APPLICATION_JSON) - .retrieve() - .bodyToFlux(String.class); - } - - @Override - public Flux setWaveformsActiveInsidePolygon(boolean active) { - return client.post() - .uri("/geometry/set-active/waveforms-inside-polygon/" + active) - .contentType(MediaType.APPLICATION_JSON) - .accept(MediaType.APPLICATION_JSON) - .retrieve() - .bodyToFlux(String.class); - } - -} +/* +* Copyright (c) 2018, Lawrence Livermore National Security, LLC. Produced at the Lawrence Livermore National Laboratory +* CODE-743439. +* All rights reserved. +* This file is part of CCT. For details, see https://github.com/LLNL/coda-calibration-tool. +* +* 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.common.gui.data.client; + +import java.util.Collection; +import java.util.List; +import java.util.Objects; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.http.client.MultipartBodyBuilder; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; + +import com.fasterxml.jackson.core.JsonProcessingException; + +import gov.llnl.gnem.apps.coda.common.gui.data.client.api.WaveformClient; +import gov.llnl.gnem.apps.coda.common.model.domain.SyntheticCoda; +import gov.llnl.gnem.apps.coda.common.model.domain.Waveform; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +@Component +public class WaveformWebClient implements WaveformClient { + + private static final Logger log = LoggerFactory.getLogger(WaveformWebClient.class); + + private WebClient client; + + @Autowired + public WaveformWebClient(WebClient client) { + this.client = client; + } + + @Override + public Mono getWaveformFromId(Long id) { + return client.get().uri("/single-waveform/{id}", id).accept(MediaType.APPLICATION_JSON).retrieve().bodyToMono(Waveform.class); + } + + @Override + public Mono getSyntheticFromWaveformId(Long id) { + return client.get().uri("/synthetics/single/{id}", id).accept(MediaType.APPLICATION_JSON).retrieve().bodyToMono(SyntheticCoda.class); + } + + @Override + public Mono postWaveform(Waveform segment) throws JsonProcessingException { + return client.post().uri("/single-waveform").contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON).bodyValue(segment).retrieve().bodyToMono(Waveform.class); + } + + @Override + public Flux postWaveforms(Long sessionId, List segments) { + return client.post().uri("/waveforms/batch/" + sessionId).contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON).bodyValue(segments).retrieve().bodyToFlux(String.class); + } + + @Override + public Flux getAllStacks() { + return client.get() + .uri("/waveforms/query/stacks") + .accept(MediaType.APPLICATION_JSON) + .retrieve() + .bodyToFlux(Waveform.class) + .doOnError(e -> log.debug(e.getMessage(), e)) + .onErrorReturn(new Waveform()); + } + + @Override + public Flux getAllActiveStacks() { + return client.get() + .uri("/waveforms/query/active-stacks") + .accept(MediaType.APPLICATION_JSON) + .retrieve() + .bodyToFlux(Waveform.class) + .doOnError(e -> log.debug(e.getMessage(), e)) + .onErrorReturn(new Waveform()); + } + + @Override + public Flux getSharedEventStationWaveformsById(Long id) { + return client.get() + .uri("/waveforms/query/shared-event-station-by-id/{id}", id) + .accept(MediaType.APPLICATION_JSON) + .retrieve() + .bodyToFlux(Waveform.class) + .filter(Objects::nonNull) + .onErrorReturn(new Waveform()); + } + + @Override + public Flux getActiveSharedEventStationWaveformsById(Long id) { + return client.get() + .uri("/waveforms/query/active-shared-event-station-by-id/{id}", id) + .accept(MediaType.APPLICATION_JSON) + .retrieve() + .bodyToFlux(Waveform.class) + .filter(Objects::nonNull) + .onErrorReturn(new Waveform()); + } + + @Override + public Flux getUniqueEventStationMetadataForStacks() { + return client.get() + .uri("/waveforms/query/unique-by-event-station") + .accept(MediaType.APPLICATION_JSON) + .retrieve() + .bodyToFlux(Waveform.class) + .filter(w -> w != null && w.getId() != null) + .onErrorReturn(new Waveform()); + } + + @Override + public Flux getWaveformsFromIds(Collection ids) { + return client.get() + .uri("/waveforms/batch/{ids}", ids.toString().replaceAll("\\[|\\]", "")) + .accept(MediaType.APPLICATION_JSON) + .retrieve() + .bodyToFlux(Waveform.class) + .filter(Objects::nonNull) + .onErrorReturn(new Waveform()); + } + + @Override + public Flux getWaveformMetadataFromIds(List ids) { + return client.get() + .uri("/waveforms/metadata/batch/{ids}", ids.toString().replaceAll("\\[|\\]", "")) + .accept(MediaType.APPLICATION_JSON) + .retrieve() + .bodyToFlux(Waveform.class) + .filter(Objects::nonNull) + .onErrorReturn(new Waveform()); + } + + @Override + public Flux getSyntheticsFromWaveformIds(Collection ids) { + return client.get() + .uri("/synthetics/batch/{ids}", ids.toString().replaceAll("\\[|\\]", "")) + .accept(MediaType.APPLICATION_JSON) + .retrieve() + .bodyToFlux(SyntheticCoda.class) + .filter(Objects::nonNull) + .onErrorReturn(new SyntheticCoda()); + } + + @Override + public Flux setWaveformsActiveByIds(List selectedWaveforms, boolean active) { + return client.post() + .uri("/waveforms/set-active/batch/" + active) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .bodyValue(selectedWaveforms) + .retrieve() + .bodyToFlux(String.class); + } + + @Override + public Flux setWaveformsActiveByEventId(String id, boolean active) { + return client.post() + .uri("/waveforms/set-active/by-event-id/" + active) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .bodyValue(id) + .retrieve() + .bodyToFlux(String.class); + } + + @Override + public Flux setWaveformsActiveByStationName(String name, boolean active) { + return client.post() + .uri("/waveforms/set-active/by-station-name/" + active) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .bodyValue(name) + .retrieve() + .bodyToFlux(String.class); + } + + @Override + public Flux setWaveformsActiveByStationNameAndEventId(String name, String id, boolean active) { + + MultipartBodyBuilder mbb = new MultipartBodyBuilder(); + mbb.part("stationName", name, MediaType.APPLICATION_JSON); + mbb.part("eventId", id, MediaType.APPLICATION_JSON); + + return client.post() + .uri("/waveforms/set-active/by-station-name-and-event-id/" + active) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .bodyValue(mbb.build()) + .retrieve() + .bodyToFlux(String.class); + } + + @Override + public Mono clearAutoPicks() { + return client.get().uri("/waveform-picks/clear-autopicks").accept(MediaType.APPLICATION_JSON).retrieve().bodyToMono(String.class); + } + + @Override + public Flux setWaveformsActiveOutsidePolygon(boolean active) { + return client.post() + .uri("/geometry/set-active/waveforms-outside-polygon/" + active) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .retrieve() + .bodyToFlux(String.class); + } + + @Override + public Flux setWaveformsActiveInsidePolygon(boolean active) { + return client.post() + .uri("/geometry/set-active/waveforms-inside-polygon/" + active) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .retrieve() + .bodyToFlux(String.class); + } + +} diff --git a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/common/gui/util/CommonGuiUtils.java b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/common/gui/util/CommonGuiUtils.java index 68c2862f..cec50327 100644 --- a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/common/gui/util/CommonGuiUtils.java +++ b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/common/gui/util/CommonGuiUtils.java @@ -19,12 +19,26 @@ import java.awt.PointerInfo; import java.awt.Rectangle; import java.awt.Toolkit; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; import javax.swing.SwingUtilities; +import org.apache.commons.compress.archivers.ArchiveException; +import org.apache.commons.compress.archivers.ArchiveOutputStream; +import org.apache.commons.compress.archivers.ArchiveStreamFactory; +import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; +import org.apache.commons.io.IOUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -55,7 +69,7 @@ public static void setIcon(Class clazz, String iconPath) { //Java < 9 macos support Class util = Class.forName("com.apple.eawt.Application"); try { - Method getApplication = util.getMethod("getApplication", new Class[0]); + Method getApplication = util.getMethod("getApplication"); Object application = getApplication.invoke(util); Method setDockIconImage = util.getMethod("setDockIconImage", java.awt.Image.class); setDockIconImage.invoke(application, image); @@ -100,4 +114,37 @@ public static Point getScaledMouseLocation(Scene scene, PointerInfo pi) { } return point; } + + public static File zipDirectory(Path zipFolder) throws IOException { + File zipDir; + zipDir = File.createTempFile("zip-dir", "tmp"); + + zipDir.deleteOnExit(); + + try (Stream fileStream = Files.walk(zipFolder, 5)) { + List files = fileStream.map(Path::toFile).filter(File::isFile).collect(Collectors.toList()); + try (ArchiveOutputStream os = new ArchiveStreamFactory().createArchiveOutputStream("zip", Files.newOutputStream(zipDir.toPath()))) { + for (File file : files) { + os.putArchiveEntry(new ZipArchiveEntry(file, file.getName())); + try (InputStream fis = Files.newInputStream(file.toPath())) { + IOUtils.copy(fis, os); + } + os.closeArchiveEntry(); + } + os.flush(); + } catch (ArchiveException e) { + throw new IOException(e); + } + try (Stream tmpFileStream = Files.walk(zipFolder)) { + tmpFileStream.sorted(Comparator.reverseOrder()).forEach(t -> { + try { + Files.deleteIfExists(t); + } catch (IOException e) { + log.trace("Unable to delete temporary file {}", e.getMessage(), e); + } + }); + } + } + return zipDir; + } } diff --git a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/envelope/gui/controllers/WaveformLoadingController.java b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/envelope/gui/controllers/WaveformLoadingController.java index 15ce8401..07d5ac67 100644 --- a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/envelope/gui/controllers/WaveformLoadingController.java +++ b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/envelope/gui/controllers/WaveformLoadingController.java @@ -1,339 +1,338 @@ -/* -* Copyright (c) 2018, Lawrence Livermore National Security, LLC. Produced at the Lawrence Livermore National Laboratory -* CODE-743439. -* All rights reserved. -* This file is part of CCT. For details, see https://github.com/LLNL/coda-calibration-tool. -* -* 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.envelope.gui.controllers; - -import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.SortedMap; -import java.util.TreeMap; -import java.util.concurrent.atomic.AtomicLong; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import javax.annotation.PostConstruct; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; - -import com.google.common.eventbus.EventBus; - -import gov.llnl.gnem.apps.coda.common.gui.controllers.AbstractSeismogramSaveLoadController; -import gov.llnl.gnem.apps.coda.common.gui.converters.api.CodaFilenameParser; -import gov.llnl.gnem.apps.coda.common.gui.converters.api.FileToWaveformConverter; -import gov.llnl.gnem.apps.coda.common.gui.converters.api.StackInfo; -import gov.llnl.gnem.apps.coda.common.gui.converters.sac.SacExporter; -import gov.llnl.gnem.apps.coda.common.gui.converters.sac.SacLoader; -import gov.llnl.gnem.apps.coda.common.gui.util.ProgressEventProgressListener; -import gov.llnl.gnem.apps.coda.common.gui.util.ProgressMonitor; -import gov.llnl.gnem.apps.coda.common.model.domain.Waveform; -import gov.llnl.gnem.apps.coda.common.model.messaging.Progress; -import gov.llnl.gnem.apps.coda.common.model.messaging.ProgressEvent; -import gov.llnl.gnem.apps.coda.common.model.messaging.Result; -import gov.llnl.gnem.apps.coda.envelope.gui.data.api.EnvelopeClient; -import llnl.gnem.core.io.SAC.SACHeader; -import llnl.gnem.core.util.TimeT; -import llnl.gnem.core.waveform.seismogram.TimeSeries; - -@Component -public class WaveformLoadingController extends AbstractSeismogramSaveLoadController { - - private static final Logger log = LoggerFactory.getLogger(WaveformLoadingController.class); - - private static final String SEP = "_"; - - private Path exportPath = Paths.get("./envelopes/"); - - private SacLoader sacLoader; - - private CodaFilenameParser filenameParser; - - private ProgressMonitor progressMonitor; - - private ProgressEvent progressEvent; - - private Progress progress; - - @Value(value = "${envelope-app.max-batching:50}") - private int batchSize; - - @Autowired - public WaveformLoadingController(List fileConverters, EnvelopeClient client, EnvelopeParamsController params, EventBus bus, SacExporter sacExporter, SacLoader sacLoader, - CodaFilenameParser filenameParser) { - super(fileConverters, bus, log, sacExporter, () -> client.getAllEnvelopes(), null); - this.sacLoader = sacLoader; - this.filenameParser = filenameParser; - this.loadClient = (id, waveforms) -> client.postEnvelopes(id, waveforms).doOnNext(w -> { - this.sacExporter.writeWaveformToDirectory(getExportPath(w).toFile(), w); - }); - this.setCompletionCallback(() -> { - stackEnvelopes(createEnvelopeMapping(getSacFiles(getExportPath()))); - }); - } - - @PostConstruct - private void setup() { - if (batchSize > 0) { - this.setMaxBatching(batchSize); - } else { - log.warn("Invalid batch size {} defined. Defaulting to {} instead.", batchSize, maxBatching); - } - } - - private List getSacFiles(Path path) { - List files = new ArrayList<>(); - if (path != null) { - try (Stream fs = Files.walk(path)) { - files = fs.map(p -> p.toFile()).filter(f -> f.isFile() && f.getName().toLowerCase(Locale.ENGLISH).endsWith(".env")).collect(Collectors.toList()); - } catch (IOException e) { - log.warn(e.getMessage(), e); - } - } - return files; - } - - @Override - public void loadFiles(List inputFiles) { - try { - progress = new Progress(-1l, 0l); - progressEvent = new ProgressEvent(idCounter.getAndIncrement(), progress); - progressMonitor = new ProgressMonitor("Saving Event-Sta-Freq pairs", new ProgressEventProgressListener(bus, progressEvent)); - Files.createDirectories(getExportPath()); - super.loadFiles(inputFiles, this.getCompletionCallback(), progressMonitor); - } catch (IOException ex) { - // TODO: bus.post(new DisplayableExceptionEvent("Unable to create directory for envelopes.", ex)); - log.error(ex.getMessage(), ex); - } - } - - public SortedMap> createEnvelopeMapping(List files) { - TreeMap> evidStaFreqMap = new TreeMap<>(); - for (int ii = 0; ii < files.size(); ii++) { - try { - File file = files.get(ii); - Result res = filenameParser.parse(file.getName().toUpperCase(Locale.ENGLISH)); - Optional payload = res.getResultPayload(); - if (res.isSuccess() && payload.isPresent()) { - StackInfo info = payload.get(); - try (SACHeader header = new SACHeader(file)) { - if (isValidStationName(header)) { - String evid = null; - if (header.kevnm != null && !header.kevnm.trim().isEmpty() && header.kevnm.trim().matches("[0-9]*")) { - evid = header.kevnm.trim(); - } else { - if (header.nevid != 0) { - evid = Integer.toString(header.nevid).trim(); - } else { - evid = sacLoader.getOrCreateEvid(header).trim(); - } - } - if (evid != null) { - String evidStaFreq = evid + SEP + header.kstnm.trim() + SEP + info.getLowFrequency() + SEP + info.getHighFrequency(); - evidStaFreqMap.putIfAbsent(evidStaFreq, new ArrayList<>()); - evidStaFreqMap.get(evidStaFreq).add(file); - } else { - log.warn("No valid evid for {}", file.getName()); - } - } else { - log.warn("No valid station for {}", file.getName()); - } - } - } - } catch (IOException e) { - log.warn(e.getMessage(), e); - } - } - return evidStaFreqMap; - } - - private boolean isValidStationName(SACHeader header) { - return header.kstnm != null && !SACHeader.STRINGDEFAULT.equalsIgnoreCase(header.kstnm) && !"".equalsIgnoreCase(header.kstnm) && !"0".equalsIgnoreCase(header.kstnm); - } - - public void stackEnvelopes(SortedMap> evidStaFreqMap) { - final AtomicLong count = new AtomicLong(0); - progress.setTotal((long) evidStaFreqMap.size()); - progress.setCurrent(count.get()); - progressEvent.setProgress(progress); - bus.post(progressEvent); - - evidStaFreqMap.entrySet().parallelStream().forEach(entry -> { - if (entry.getValue().size() > 1) { - List files = entry.getValue(); - Map> waveformsByFreqAndSta = new HashMap<>(); - - for (int i = 0; i < files.size(); i++) { - Result res = filenameParser.parse(files.get(i).getName().toUpperCase(Locale.ENGLISH)); - if (res != null && res.isSuccess() && res.getResultPayload().isPresent()) { - StackInfo stackInfo = res.getResultPayload().get(); - Result result = sacLoader.convertSacFileToWaveform(files.get(i)); - - if (result.isSuccess() && result.getResultPayload().isPresent()) { - Waveform rawWaveform = result.getResultPayload().get(); - rawWaveform.setLowFrequency(stackInfo.getLowFrequency()); - rawWaveform.setHighFrequency(stackInfo.getHighFrequency()); - if (rawWaveform != null - && rawWaveform.hasData() - && rawWaveform.getSegmentLength() > 0 - && rawWaveform.getStream() != null - && rawWaveform.getStream().getStation() != null - && rawWaveform.getStream().getChannelName() != null) { - if (!gov.llnl.gnem.apps.coda.common.model.domain.Stream.TYPE_STACK.equalsIgnoreCase(rawWaveform.getStream().getChannelName())) { - waveformsByFreqAndSta.computeIfAbsent(entry.getKey() + " " + rawWaveform.getStream().getStation().hashCode(), k -> new ArrayList<>()).add(rawWaveform); - } - } else { - log.warn("No data or bad station specification for waveform {}.", rawWaveform); - } - } else { - log.warn("Unable to read envelope file {}. {}", files.get(i), result.getErrors()); - } - } else { - log.warn("Unable to parse envelope filename for frequency band {}. {}", files.get(i), res.getErrors()); - } - } - - List stackedWaveforms = waveformsByFreqAndSta.entrySet().stream().map(e -> stackEnvelopes(e.getValue())).filter(Objects::nonNull).collect(Collectors.toList()); - - // TODO: Export envelopes and stacks to separate dirs - for (Waveform stackedWaveform : stackedWaveforms) { - File stackFolder = getExportPath(stackedWaveform).toFile(); - sacExporter.writeWaveformToDirectory(stackFolder, stackedWaveform); - } - } - - long currentCount = count.getAndIncrement(); - if (currentCount % this.getMaxBatching() == 0) { - progress.setCurrent(currentCount); - progressEvent.setProgress(progress); - bus.post(progressEvent); - } - }); - - progress.setCurrent(progress.getTotal()); - progressEvent.setProgress(progress); - bus.post(progressEvent); - } - - private Waveform stackEnvelopes(List waves) { - // FIXME: Duplicate of the one in service. - // Need a common-utils because this pulls in stuff from Externals for TimeSeries etc so I can't cheat and slam it into the common model. - Waveform base = null; - if (waves != null && waves.size() > 1) { - try { - base = waves.get(0); - TimeSeries seis = convertToTimeSeries(base); - - for (int i = 1; i < waves.size(); i++) { - TimeSeries seis2 = convertToTimeSeries(waves.get(i)); - seis = seis.add(seis2); - } - seis.MultiplyScalar(1d / waves.size()); - - float[] seisData = seis.getData(); - double[] data = new double[seisData.length]; - for (int j = 0; j < data.length; ++j) { - data[j] = seisData[j]; - } - base.setSegment(data); - if (!base.hasData() || base.getSegmentLength() == 0) { - return null; - } - - base.setSampleRate(seis.getSamprate()); - base.setBeginTime(seis.getTime().getDate()); - base.setEndTime(seis.getEndtime().getDate()); - if (base.getStream() != null) { - base.getStream().setChannelName(gov.llnl.gnem.apps.coda.common.model.domain.Stream.TYPE_STACK); - } - } catch (Exception e) { - log.info(e.getMessage(), e); - } - } else { - log.info("Waveform with only one channel found for list {}, skipping stacking", waves); - } - return base; - } - - private TimeSeries convertToTimeSeries(Waveform base) { - double[] segment = base.getSegment(); - float[] fData = new float[segment.length]; - for (int j = 0; j < fData.length; ++j) { - fData[j] = (float) segment[j]; - } - TimeSeries seis = new TimeSeries(fData, base.getSampleRate(), new TimeT(base.getBeginTime())); - return seis; - } - - public Path getExportPath() { - return exportPath; - } - - public Path getExportPath(Waveform w) { - - Path path = exportPath; - if (exportPath == null) { - throw new IllegalStateException("Unable to export waveform, export path was null"); - } - - if (w == null) { - throw new IllegalStateException("Unable to export waveform, waveform was null"); - } - - String station = Optional.ofNullable(w.getStream()).map(stream -> stream.getStation()).map(sta -> sta.getStationName()).orElse(""); - TimeT time = w.getEvent() != null ? new TimeT(w.getEvent().getOriginTime()) : new TimeT(w.getBeginTime()); - if (time != null) { - String evid = sacLoader.getOrCreateEvid(w); - // evid - int year = time.getYear(); - int month = time.getMonth(); - - String newPath = exportPath.toAbsolutePath().toString() + File.separator + year + File.separator + String.format("%02d", month) + File.separator + evid + File.separator + station; - - File result = new File(newPath); - if (!result.exists()) { - boolean wasCreated = result.mkdirs(); - if (!wasCreated) { - throw new IllegalStateException("Could not create directory: " + newPath); - } - } - path = result.toPath(); - } - return path; - } - - public void setExportPath(File exportDirectory) { - if (exportDirectory != null) { - Path path; - if (exportDirectory.isFile() && exportDirectory.getParentFile() != null) { - path = exportDirectory.getParentFile().toPath(); - } else { - path = exportDirectory.toPath(); - } - this.exportPath = path; - } - } -} +/* +* Copyright (c) 2018, Lawrence Livermore National Security, LLC. Produced at the Lawrence Livermore National Laboratory +* CODE-743439. +* All rights reserved. +* This file is part of CCT. For details, see https://github.com/LLNL/coda-calibration-tool. +* +* 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.envelope.gui.controllers; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.annotation.PostConstruct; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import com.google.common.eventbus.EventBus; + +import gov.llnl.gnem.apps.coda.common.gui.controllers.AbstractSeismogramSaveLoadController; +import gov.llnl.gnem.apps.coda.common.gui.converters.api.CodaFilenameParser; +import gov.llnl.gnem.apps.coda.common.gui.converters.api.FileToWaveformConverter; +import gov.llnl.gnem.apps.coda.common.gui.converters.api.StackInfo; +import gov.llnl.gnem.apps.coda.common.gui.converters.sac.SacExporter; +import gov.llnl.gnem.apps.coda.common.gui.converters.sac.SacLoader; +import gov.llnl.gnem.apps.coda.common.gui.util.ProgressEventProgressListener; +import gov.llnl.gnem.apps.coda.common.gui.util.ProgressMonitor; +import gov.llnl.gnem.apps.coda.common.model.domain.Station; +import gov.llnl.gnem.apps.coda.common.model.domain.Waveform; +import gov.llnl.gnem.apps.coda.common.model.messaging.Progress; +import gov.llnl.gnem.apps.coda.common.model.messaging.ProgressEvent; +import gov.llnl.gnem.apps.coda.common.model.messaging.Result; +import gov.llnl.gnem.apps.coda.envelope.gui.data.api.EnvelopeClient; +import llnl.gnem.core.io.SAC.SACHeader; +import llnl.gnem.core.util.TimeT; +import llnl.gnem.core.waveform.seismogram.TimeSeries; + +@Component +public class WaveformLoadingController extends AbstractSeismogramSaveLoadController { + + private static final Logger log = LoggerFactory.getLogger(WaveformLoadingController.class); + + private static final String SEP = "_"; + + private Path exportPath = Paths.get("./envelopes/"); + + private SacLoader sacLoader; + + private CodaFilenameParser filenameParser; + + private ProgressMonitor progressMonitor; + + private ProgressEvent progressEvent; + + private Progress progress; + + @Value(value = "${envelope-app.max-batching:50}") + private int batchSize; + + @Autowired + public WaveformLoadingController(List fileConverters, EnvelopeClient client, EnvelopeParamsController params, EventBus bus, SacExporter sacExporter, SacLoader sacLoader, + CodaFilenameParser filenameParser) { + super(fileConverters, bus, log, sacExporter, () -> client.getAllEnvelopes(), null); + this.sacLoader = sacLoader; + this.filenameParser = filenameParser; + this.loadClient = (id, waveforms) -> client.postEnvelopes(id, waveforms).doOnNext(w -> { + this.sacExporter.writeWaveformToDirectory(getExportPath(w).toFile(), w); + }); + this.setCompletionCallback(() -> { + stackEnvelopes(createEnvelopeMapping(getSacFiles(getExportPath()))); + }); + } + + @PostConstruct + private void setup() { + if (batchSize > 0) { + this.setMaxBatching(batchSize); + } else { + log.warn("Invalid batch size {} defined. Defaulting to {} instead.", batchSize, maxBatching); + } + } + + private List getSacFiles(Path path) { + List files = new ArrayList<>(); + if (path != null) { + try (Stream fs = Files.walk(path)) { + files = fs.map(Path::toFile).filter(f -> f.isFile() && f.getName().toLowerCase(Locale.ENGLISH).endsWith(".env")).collect(Collectors.toList()); + } catch (IOException e) { + log.warn(e.getMessage(), e); + } + } + return files; + } + + @Override + public void loadFiles(List inputFiles) { + try { + progress = new Progress(-1l, 0l); + progressEvent = new ProgressEvent(idCounter.getAndIncrement(), progress); + progressMonitor = new ProgressMonitor("Saving Event-Sta-Freq pairs", new ProgressEventProgressListener(bus, progressEvent)); + Files.createDirectories(getExportPath()); + super.loadFiles(inputFiles, this.getCompletionCallback(), progressMonitor); + } catch (IOException ex) { + // TODO: bus.post(new DisplayableExceptionEvent("Unable to create directory for envelopes.", ex)); + log.error(ex.getMessage(), ex); + } + } + + public SortedMap> createEnvelopeMapping(List files) { + TreeMap> evidStaFreqMap = new TreeMap<>(); + for (int ii = 0; ii < files.size(); ii++) { + try { + File file = files.get(ii); + Result res = filenameParser.parse(file.getName().toUpperCase(Locale.ENGLISH)); + Optional payload = res.getResultPayload(); + if (res.isSuccess() && payload.isPresent()) { + StackInfo info = payload.get(); + try (SACHeader header = new SACHeader(file)) { + if (isValidStationName(header)) { + String evid = null; + if (header.kevnm != null && !header.kevnm.trim().isEmpty() && header.kevnm.trim().matches("[0-9]*")) { + evid = header.kevnm.trim(); + } else if (header.nevid != 0) { + evid = Integer.toString(header.nevid).trim(); + } else { + evid = sacLoader.getOrCreateEvid(header).trim(); + } + if (evid != null) { + String evidStaFreq = evid + SEP + header.kstnm.trim() + SEP + info.getLowFrequency() + SEP + info.getHighFrequency(); + evidStaFreqMap.putIfAbsent(evidStaFreq, new ArrayList<>()); + evidStaFreqMap.get(evidStaFreq).add(file); + } else { + log.warn("No valid evid for {}", file.getName()); + } + } else { + log.warn("No valid station for {}", file.getName()); + } + } + } + } catch (IOException e) { + log.warn(e.getMessage(), e); + } + } + return evidStaFreqMap; + } + + private boolean isValidStationName(SACHeader header) { + return header.kstnm != null && !SACHeader.STRINGDEFAULT.equalsIgnoreCase(header.kstnm) && !"".equalsIgnoreCase(header.kstnm) && !"0".equalsIgnoreCase(header.kstnm); + } + + public void stackEnvelopes(SortedMap> evidStaFreqMap) { + final AtomicLong count = new AtomicLong(0); + progress.setTotal((long) evidStaFreqMap.size()); + progress.setCurrent(count.get()); + progressEvent.setProgress(progress); + bus.post(progressEvent); + + evidStaFreqMap.entrySet().parallelStream().forEach(entry -> { + if (entry.getValue().size() > 1) { + List files = entry.getValue(); + Map> waveformsByFreqAndSta = new HashMap<>(); + + for (int i = 0; i < files.size(); i++) { + Result res = filenameParser.parse(files.get(i).getName().toUpperCase(Locale.ENGLISH)); + if (res != null && res.isSuccess() && res.getResultPayload().isPresent()) { + StackInfo stackInfo = res.getResultPayload().get(); + Result result = sacLoader.convertSacFileToWaveform(files.get(i)); + + if (result.isSuccess() && result.getResultPayload().isPresent()) { + Waveform rawWaveform = result.getResultPayload().get(); + rawWaveform.setLowFrequency(stackInfo.getLowFrequency()); + rawWaveform.setHighFrequency(stackInfo.getHighFrequency()); + if (rawWaveform != null + && rawWaveform.hasData() + && rawWaveform.getSegmentLength() > 0 + && rawWaveform.getStream() != null + && rawWaveform.getStream().getStation() != null + && rawWaveform.getStream().getChannelName() != null) { + if (!gov.llnl.gnem.apps.coda.common.model.domain.Stream.TYPE_STACK.equalsIgnoreCase(rawWaveform.getStream().getChannelName())) { + waveformsByFreqAndSta.computeIfAbsent(entry.getKey() + " " + rawWaveform.getStream().getStation().hashCode(), k -> new ArrayList<>()).add(rawWaveform); + } + } else { + log.warn("No data or bad station specification for waveform {}.", rawWaveform); + } + } else { + log.warn("Unable to read envelope file {}. {}", files.get(i), result.getErrors()); + } + } else { + log.warn("Unable to parse envelope filename for frequency band {}. {}", files.get(i), res.getErrors()); + } + } + + List stackedWaveforms = waveformsByFreqAndSta.entrySet().stream().map(e -> stackEnvelopes(e.getValue())).filter(Objects::nonNull).collect(Collectors.toList()); + + // TODO: Export envelopes and stacks to separate dirs + for (Waveform stackedWaveform : stackedWaveforms) { + File stackFolder = getExportPath(stackedWaveform).toFile(); + sacExporter.writeWaveformToDirectory(stackFolder, stackedWaveform); + } + } + + long currentCount = count.getAndIncrement(); + if (currentCount % this.getMaxBatching() == 0) { + progress.setCurrent(currentCount); + progressEvent.setProgress(progress); + bus.post(progressEvent); + } + }); + + progress.setCurrent(progress.getTotal()); + progressEvent.setProgress(progress); + bus.post(progressEvent); + } + + private Waveform stackEnvelopes(List waves) { + // FIXME: Duplicate of the one in service. + // Need a common-utils because this pulls in stuff from Externals for TimeSeries etc so I can't cheat and slam it into the common model. + Waveform base = null; + if (waves != null && waves.size() > 1) { + try { + base = waves.get(0); + TimeSeries seis = convertToTimeSeries(base); + + for (int i = 1; i < waves.size(); i++) { + TimeSeries seis2 = convertToTimeSeries(waves.get(i)); + seis = seis.add(seis2); + } + seis.MultiplyScalar(1d / waves.size()); + + float[] seisData = seis.getData(); + double[] data = new double[seisData.length]; + for (int j = 0; j < data.length; ++j) { + data[j] = seisData[j]; + } + base.setSegment(data); + if (!base.hasData() || base.getSegmentLength() == 0) { + return null; + } + + base.setSampleRate(seis.getSamprate()); + base.setBeginTime(seis.getTime().getDate()); + base.setEndTime(seis.getEndtime().getDate()); + if (base.getStream() != null) { + base.getStream().setChannelName(gov.llnl.gnem.apps.coda.common.model.domain.Stream.TYPE_STACK); + } + } catch (Exception e) { + log.info(e.getMessage(), e); + } + } else { + log.info("Waveform with only one channel found for list {}, skipping stacking", waves); + } + return base; + } + + private TimeSeries convertToTimeSeries(Waveform base) { + double[] segment = base.getSegment(); + float[] fData = new float[segment.length]; + for (int j = 0; j < fData.length; ++j) { + fData[j] = (float) segment[j]; + } + TimeSeries seis = new TimeSeries(fData, base.getSampleRate(), new TimeT(base.getBeginTime())); + return seis; + } + + public Path getExportPath() { + return exportPath; + } + + public Path getExportPath(Waveform w) { + + Path path = exportPath; + if (exportPath == null) { + throw new IllegalStateException("Unable to export waveform, export path was null"); + } + + if (w == null) { + throw new IllegalStateException("Unable to export waveform, waveform was null"); + } + + String station = Optional.ofNullable(w.getStream()).map(gov.llnl.gnem.apps.coda.common.model.domain.Stream::getStation).map(Station::getStationName).orElse(""); + TimeT time = w.getEvent() != null ? new TimeT(w.getEvent().getOriginTime()) : new TimeT(w.getBeginTime()); + if (time != null) { + String evid = sacLoader.getOrCreateEvid(w); + // evid + int year = time.getYear(); + int month = time.getMonth(); + + String newPath = exportPath.toAbsolutePath().toString() + File.separator + year + File.separator + String.format("%02d", month) + File.separator + evid + File.separator + station; + + File result = new File(newPath); + if (!result.exists()) { + boolean wasCreated = result.mkdirs(); + if (!wasCreated) { + throw new IllegalStateException("Could not create directory: " + newPath); + } + } + path = result.toPath(); + } + return path; + } + + public void setExportPath(File exportDirectory) { + if (exportDirectory != null) { + Path path; + if (exportDirectory.isFile() && exportDirectory.getParentFile() != null) { + path = exportDirectory.getParentFile().toPath(); + } else { + path = exportDirectory.toPath(); + } + this.exportPath = path; + } + } +} diff --git a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/spectra/gui/RatioStatusProgressListener.java b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/spectra/gui/RatioStatusProgressListener.java new file mode 100644 index 00000000..3be2cda5 --- /dev/null +++ b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/spectra/gui/RatioStatusProgressListener.java @@ -0,0 +1,51 @@ +/* +* Copyright (c) 2023, Lawrence Livermore National Security, LLC. Produced at the Lawrence Livermore National Laboratory +* CODE-743439. +* All rights reserved. +* This file is part of CCT. For details, see https://github.com/LLNL/coda-calibration-tool. +* +* 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.spectra.gui; + +import com.google.common.eventbus.EventBus; +import com.google.common.eventbus.Subscribe; + +import gov.llnl.gnem.apps.coda.calibration.model.messaging.RatioStatusEvent; +import gov.llnl.gnem.apps.coda.common.gui.util.ProgressListener; +import gov.llnl.gnem.apps.coda.common.model.messaging.Progress; + +public class RatioStatusProgressListener extends ProgressListener { + + private RatioStatusEvent cachedEvent; + private Progress progress = new Progress((long) RatioStatusEvent.Status.COMPLETE.ordinal(), 0l); + + public RatioStatusProgressListener(EventBus bus, RatioStatusEvent event) { + this.cachedEvent = event; + bus.register(this); + } + + @Subscribe + private void listener(RatioStatusEvent event) { + if (cachedEvent != null && cachedEvent.getId().equals(event.getId())) { + cachedEvent = event; + if (cachedEvent.getStatus() == RatioStatusEvent.Status.COMPLETE || cachedEvent.getStatus() == RatioStatusEvent.Status.ERROR) { + progress.setCurrent((long) RatioStatusEvent.Status.COMPLETE.ordinal()); + } else { + progress.setCurrent((long) cachedEvent.getStatus().ordinal()); + } + changeSupport.firePropertyChange(this.getClass().getName(), null, progress); + } + } + + @Override + public double getProgress() { + return progress.getProgress(); + } +} \ No newline at end of file diff --git a/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/spectra/gui/SpectraRatioGuiController.java b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/spectra/gui/SpectraRatioGuiController.java new file mode 100644 index 00000000..a2831236 --- /dev/null +++ b/calibration-gui/src/main/java/gov/llnl/gnem/apps/coda/spectra/gui/SpectraRatioGuiController.java @@ -0,0 +1,336 @@ +/* +* Copyright (c) 2023, Lawrence Livermore National Security, LLC. Produced at the Lawrence Livermore National Laboratory +* CODE-743439. +* All rights reserved. +* This file is part of CCT. For details, see https://github.com/LLNL/coda-calibration-tool. +* +* 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.spectra.gui; + +import java.io.IOException; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import javax.annotation.PreDestroy; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.stereotype.Component; + +import com.google.common.eventbus.EventBus; +import com.google.common.eventbus.Subscribe; + +import gov.llnl.gnem.apps.coda.calibration.gui.controllers.RefreshableController; +import gov.llnl.gnem.apps.coda.calibration.gui.data.client.api.EventClient; +import gov.llnl.gnem.apps.coda.calibration.gui.data.client.api.SpectraClient; +import gov.llnl.gnem.apps.coda.calibration.gui.data.client.api.SpectraRatioClient; +import gov.llnl.gnem.apps.coda.calibration.model.domain.Spectra; +import gov.llnl.gnem.apps.coda.calibration.model.messaging.RatioMeasurementEvent; +import gov.llnl.gnem.apps.coda.calibration.model.messaging.RatioStatusEvent; +import gov.llnl.gnem.apps.coda.calibration.model.messaging.RatioStatusEvent.Status; +import gov.llnl.gnem.apps.coda.common.gui.events.ShowFailureReportEvent; +import gov.llnl.gnem.apps.coda.common.gui.util.MaybeNumericStringComparator; +import gov.llnl.gnem.apps.coda.common.model.messaging.Result; +import gov.llnl.gnem.apps.coda.common.model.util.SPECTRA_TYPES; +import gov.llnl.gnem.apps.coda.spectra.model.domain.SpectraEvent; +import javafx.application.Platform; +import javafx.beans.binding.Bindings; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.Alert; +import javafx.scene.control.Button; +import javafx.scene.control.CheckBox; +import javafx.scene.control.MenuItem; +import javafx.scene.control.SelectionMode; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableColumn.CellDataFeatures; +import javafx.scene.control.TableView; +import javafx.stage.Stage; + +@Component +public class SpectraRatioGuiController implements RefreshableController { + + private static final Logger log = LoggerFactory.getLogger(SpectraRatioGuiController.class); + + @FXML + private TableView tableView; + + @FXML + private TableColumn eventCol; + + @FXML + private TableColumn fitMwCol; + + @FXML + private TableColumn refMwCol; + + @FXML + private TableColumn dateCol; + + @FXML + private TableColumn numCol; + + @FXML + private TableColumn denCol; + + @FXML + private MenuItem setRowsAsNumeratorBtn; + + @FXML + private MenuItem setRowsAsDenominatorBtn; + + @FXML + private MenuItem deselectRowsBtn; + + @FXML + private Button calcRatioBtn; + + private Alert alertPopup; + + private ObservableList listData = FXCollections.synchronizedObservableList(FXCollections.observableArrayList()); + + private EventClient eventClient; + private SpectraClient spectraClient; + private SpectraRatioClient spectraRatioClient; + + private EventBus bus; + private boolean calculationProcessing = false; + private long currentRatioStatusEventId = 0l; + + private ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor(r -> { + Thread thread = new Thread(r); + thread.setName("Ratio-Scheduled"); + thread.setDaemon(true); + return thread; + }); + + private ScheduledExecutorService guiService = Executors.newSingleThreadScheduledExecutor(r -> { + Thread thread = new Thread(r); + thread.setName("Ratio-Gui-Service"); + thread.setDaemon(true); + return thread; + }); + + private Stage stage; + + public SpectraRatioGuiController(EventClient eventClient, SpectraClient spectraClient, SpectraRatioClient spectraRatioClient, EventBus bus, ConfigurableApplicationContext springContext) + throws IOException { + this.eventClient = eventClient; + this.spectraClient = spectraClient; + this.spectraRatioClient = spectraRatioClient; + this.bus = bus; + } + + @FXML + private void openFailureReportDisplay() { + bus.post(new ShowFailureReportEvent()); + } + + private Double getEventFitMw(SpectraEvent event) { + if (event == null) { + return null; + } + + List eventSpectra = new ArrayList<>(spectraClient.getFitSpectra(event.getEventID()).block(Duration.ofSeconds(2))); + + if (eventSpectra.isEmpty()) { + return null; + } + + double eventMw = 0.0; + for (Spectra spectra : eventSpectra) { + if (spectra.getType() == SPECTRA_TYPES.FIT) { + eventMw = spectra.getMw(); + break; + } + } + return eventMw; + } + + private Double getEventRefMw(SpectraEvent event) { + if (event == null) { + return null; + } + + Spectra eventSpectra = spectraClient.getReferenceSpectra(event.getEventID()).block(Duration.ofSeconds(2)); + + if (eventSpectra == null || eventSpectra.getMw() < 0.0) { + return null; + } + + return eventSpectra.getMw(); + } + + public void loadEnvelopes() { + requestData(); + } + + @FXML + public void initialize() { + bus.register(this); + this.alertPopup = new Alert(Alert.AlertType.INFORMATION); + alertPopup.setTitle("Notice"); + alertPopup.setHeaderText(null); + alertPopup.setContentText("You need to select at least 1 numerator event and 1 denominator event to calculate ratios."); + + tableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); + eventCol.setCellValueFactory(x -> Bindings.createStringBinding(() -> Optional.ofNullable(x).map(CellDataFeatures::getValue).map(SpectraEvent::getEventID).orElseGet(String::new))); + eventCol.comparatorProperty().set(new MaybeNumericStringComparator()); + + fitMwCol.setCellValueFactory(x -> Bindings.createObjectBinding(() -> Optional.ofNullable(x).map(CellDataFeatures::getValue).map(this::getEventFitMw).orElseGet(null))); + refMwCol.setCellValueFactory(x -> Bindings.createObjectBinding(() -> Optional.ofNullable(x).map(CellDataFeatures::getValue).map(this::getEventRefMw).orElseGet(null))); + + dateCol.setCellValueFactory( + x -> Bindings.createStringBinding( + () -> Optional.ofNullable(x).map(CellDataFeatures::getValue).map(event -> eventClient.getEvent(event.getEventID()).block().getOriginTime().toString()).orElseGet(String::new))); + + numCol.setCellValueFactory(x -> Bindings.createObjectBinding(() -> Optional.ofNullable(x).map(CellDataFeatures::getValue).map(event -> { + CheckBox box = new CheckBox(); + box.setSelected(event.isNumerator()); + box.selectedProperty().addListener((obs, o, n) -> { + if (n != null && !o.equals(n)) { + event.setNumerator(n); + tableView.refresh(); + } + }); + return box; + }).orElseGet(CheckBox::new))); + numCol.comparatorProperty().set((c1, c2) -> Boolean.compare(c1.isSelected(), c2.isSelected())); + + denCol.setCellValueFactory(x -> Bindings.createObjectBinding(() -> Optional.ofNullable(x).map(CellDataFeatures::getValue).map(event -> { + CheckBox box = new CheckBox(); + box.setSelected(event.isDenominator()); + box.selectedProperty().addListener((obs, o, n) -> { + if (n != null && !o.equals(n)) { + event.setDenominator(n); + tableView.refresh(); + } + }); + return box; + }).orElseGet(CheckBox::new))); + denCol.comparatorProperty().set((c1, c2) -> Boolean.compare(c1.isSelected(), c2.isSelected())); + + tableView.setItems(listData); + } + + @PreDestroy + private void cleanUp() { + service.shutdownNow(); + } + + public void toFront() { + stage.show(); + stage.toFront(); + } + + @FXML + private void setRowsAsNumerator() { + if (tableView != null) { + tableView.getSelectionModel().getSelectedItems().forEach(selectedEvent -> { + CheckBox box = numCol.getCellData(selectedEvent); + if (box != null) { + box.setSelected(true); + } + }); + } + } + + @FXML + private void setRowsAsDenominator() { + if (tableView != null) { + tableView.getSelectionModel().getSelectedItems().forEach(selectedEvent -> { + CheckBox box = denCol.getCellData(selectedEvent); + if (box != null) { + box.setSelected(true); + } + }); + } + } + + @FXML + private void deselectRows() { + if (tableView != null) { + tableView.getSelectionModel().getSelectedItems().forEach(selectedEvent -> { + CheckBox numBox = numCol.getCellData(selectedEvent); + if (numBox != null && numBox.isSelected()) { + numBox.setSelected(false); + } + CheckBox denBox = denCol.getCellData(selectedEvent); + if (denBox != null && denBox.isSelected()) { + denBox.setSelected(false); + } + }); + } + } + + @FXML + private void calculateSpectraRatio() { + + // Prevent multiple clicks of the calculate button causing ratios to be calculated too many times + if (calculationProcessing) { + return; + } + + List smallEventIds = this.listData.stream().filter(SpectraEvent::isDenominator).map(SpectraEvent::getEventID).collect(Collectors.toList()); + + List largeEventIds = this.listData.stream().filter(SpectraEvent::isNumerator).map(SpectraEvent::getEventID).collect(Collectors.toList()); + + if (smallEventIds.size() > 0 && largeEventIds.size() > 0) { + + calculationProcessing = true; + Platform.runLater(() -> calcRatioBtn.setDisable(true)); + + spectraRatioClient.makeSpectraRatioMeasurements(true, true, smallEventIds, largeEventIds).doOnError(err -> { + bus.post(new RatioStatusEvent(currentRatioStatusEventId, Status.ERROR)); + log.error(err.getMessage()); + }).subscribe(mwRatioReportByEventPair -> bus.post(new RatioMeasurementEvent(currentRatioStatusEventId, new Result<>(true, mwRatioReportByEventPair)))); + + // Small delay so plots are loaded before sending completion status + guiService.schedule(() -> { + calculationProcessing = false; + Platform.runLater(() -> calcRatioBtn.setDisable(false)); + bus.post(new RatioStatusEvent(currentRatioStatusEventId, Status.COMPLETE)); + }, 1, TimeUnit.SECONDS); + log.trace("Received measured spectra ratio."); + } else { + alertPopup.show(); + } + } + + @Subscribe + private void listener(final RatioStatusEvent event) { + // Capturing the event id of incoming ratio status event + // so we can use the id to update it's completion status later + if ((event != null) && (event.getStatus() == Status.STARTING)) { + currentRatioStatusEventId = event.getId(); + } + } + + private void requestData() { + List uniqueEvents = eventClient.getUniqueEventIds().map(SpectraEvent::new).collectList().block(Duration.ofMinutes(10l)); + synchronized (listData) { + listData.clear(); + listData.addAll(uniqueEvents); + } + } + + @Override + public Runnable getRefreshFunction() { + return this::requestData; + } +} diff --git a/calibration-gui/src/main/resources/application.properties b/calibration-gui/src/main/resources/application.properties index d4972e39..6a13bb29 100644 --- a/calibration-gui/src/main/resources/application.properties +++ b/calibration-gui/src/main/resources/application.properties @@ -8,5 +8,5 @@ webclient.basePath=127.0.0.1:53921 webclient.subscriptions=/topic/status-events,/topic/calibration-events app.height=1200 app.width=800 -app.baseTitle=Coda Calibration +app.baseTitle=Seismic Envelope Ratio spring.codec.max-in-memory-size=-1 \ No newline at end of file diff --git a/calibration-gui/src/main/resources/fxml/CertGui.fxml b/calibration-gui/src/main/resources/fxml/CertGui.fxml new file mode 100644 index 00000000..80f97f8e --- /dev/null +++ b/calibration-gui/src/main/resources/fxml/CertGui.fxml @@ -0,0 +1,198 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
diff --git a/calibration-gui/src/main/resources/fxml/CertMapView.fxml b/calibration-gui/src/main/resources/fxml/CertMapView.fxml new file mode 100644 index 00000000..fb0cb26a --- /dev/null +++ b/calibration-gui/src/main/resources/fxml/CertMapView.fxml @@ -0,0 +1,30 @@ + + + + + + + + + + + + +
+ + + + + +
+
+ +
+
diff --git a/calibration-gui/src/main/resources/fxml/CodaGui.fxml b/calibration-gui/src/main/resources/fxml/CodaGui.fxml index 04b3145f..aabccaa5 100644 --- a/calibration-gui/src/main/resources/fxml/CodaGui.fxml +++ b/calibration-gui/src/main/resources/fxml/CodaGui.fxml @@ -17,7 +17,7 @@ - + @@ -80,6 +80,7 @@ + diff --git a/calibration-gui/src/main/resources/fxml/Data.fxml b/calibration-gui/src/main/resources/fxml/Data.fxml index 769fc435..6e8a80de 100644 --- a/calibration-gui/src/main/resources/fxml/Data.fxml +++ b/calibration-gui/src/main/resources/fxml/Data.fxml @@ -6,7 +6,7 @@ - +
diff --git a/calibration-gui/src/main/resources/fxml/EventsTableGui.fxml b/calibration-gui/src/main/resources/fxml/EventsTableGui.fxml new file mode 100644 index 00000000..4221c324 --- /dev/null +++ b/calibration-gui/src/main/resources/fxml/EventsTableGui.fxml @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/calibration-gui/src/main/resources/fxml/MeasuredMws.fxml b/calibration-gui/src/main/resources/fxml/MeasuredMws.fxml index 00efe158..409e8157 100644 --- a/calibration-gui/src/main/resources/fxml/MeasuredMws.fxml +++ b/calibration-gui/src/main/resources/fxml/MeasuredMws.fxml @@ -4,9 +4,7 @@ - - @@ -19,7 +17,7 @@ - + @@ -167,101 +165,9 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + diff --git a/calibration-gui/src/main/resources/fxml/ProgressDisplay.fxml b/calibration-gui/src/main/resources/fxml/ProgressDisplay.fxml index 7700da15..c6a07881 100644 --- a/calibration-gui/src/main/resources/fxml/ProgressDisplay.fxml +++ b/calibration-gui/src/main/resources/fxml/ProgressDisplay.fxml @@ -1,5 +1,6 @@ + @@ -7,8 +8,7 @@ - - + @@ -16,18 +16,16 @@ - + - - - + + + + +
+ + + +
+
+
+ + + + diff --git a/calibration-gui/src/main/resources/fxml/Site.fxml b/calibration-gui/src/main/resources/fxml/Site.fxml index 19a682b2..00346cf5 100644 --- a/calibration-gui/src/main/resources/fxml/Site.fxml +++ b/calibration-gui/src/main/resources/fxml/Site.fxml @@ -4,9 +4,7 @@ - - @@ -21,7 +19,7 @@ - + @@ -186,101 +184,13 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + @@ -288,7 +198,7 @@ - + diff --git a/calibration-gui/src/main/resources/fxml/SpectraRatioGui.fxml b/calibration-gui/src/main/resources/fxml/SpectraRatioGui.fxml new file mode 100644 index 00000000..59b28290 --- /dev/null +++ b/calibration-gui/src/main/resources/fxml/SpectraRatioGui.fxml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/calibration-gui/src/main/resources/fxml/SpectraRatioPlotGui.fxml b/calibration-gui/src/main/resources/fxml/SpectraRatioPlotGui.fxml new file mode 100644 index 00000000..4027ed9f --- /dev/null +++ b/calibration-gui/src/main/resources/fxml/SpectraRatioPlotGui.fxml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/calibration-gui/src/main/resources/fxml/WaveformGui.fxml b/calibration-gui/src/main/resources/fxml/WaveformGui.fxml index 185e0104..cd4d78fa 100644 --- a/calibration-gui/src/main/resources/fxml/WaveformGui.fxml +++ b/calibration-gui/src/main/resources/fxml/WaveformGui.fxml @@ -8,7 +8,7 @@ - + @@ -16,12 +16,12 @@ - + - + - -