From ab0874fb141aa2535c31af1b44cddf89efcd130d Mon Sep 17 00:00:00 2001 From: sheirerd Date: Thu, 7 Dec 2023 04:48:34 -0500 Subject: [PATCH] #1725 WIP additional enhancements and resolves missing class from build. --- build.gradle | 6 +- .../dsheirer/audio/AbstractAudioModule.java | 43 ++++--- .../dsheirer/audio/call/CallRepository.java | 25 ++++ .../dsheirer/audio/call/CallViewPanel.java | 121 +++++++++++++++--- .../dsheirer/audio/call/PagingController.java | 5 + .../dsheirer/audio/call/SearchColumn.java | 35 +++++ 6 files changed, 194 insertions(+), 41 deletions(-) create mode 100644 src/main/java/io/github/dsheirer/audio/call/SearchColumn.java diff --git a/build.gradle b/build.gradle index da3f77394..6d253ca9c 100644 --- a/build.gradle +++ b/build.gradle @@ -23,8 +23,8 @@ import java.text.SimpleDateFormat * Instructions for building/compiling the sdrtrunk application. * * Prerequisites: - * Install and configure an OpenJDK version 19+ that includes the JavaFX modules (e.g. Bellsoft Liberica JDK) - * - Optional: install and configure Gradle 7.6+ + * Install and configure an OpenJDK version 20 that includes the JavaFX modules (e.g. Bellsoft Liberica JDK) + * - Optional: install and configure Gradle 8.2+ * * Scenario 1: run the application via gradle command line from the source code root directory: * command: ./gradlew run @@ -236,7 +236,7 @@ def configure(org.beryx.runtime.RuntimeZipTask rt, List jvmArgs) { //jdk.incubator.vector - needed for Project Panama foreign function and vector apis //jdk.accessibility is used with assistive technologies like screen readers //java.management for JVM resource monitoring - rt.extension.addModules('jdk.crypto.ec', 'jdk.incubator.vector', 'jdk.accessibility', 'java.management') + rt.extension.addModules('jdk.crypto.ec', 'jdk.incubator.vector', 'jdk.accessibility', 'java.management', 'java.instrument') //Use auto-detected modules and 'add' any specified modules. rt.extension.additive.set(true) diff --git a/src/main/java/io/github/dsheirer/audio/AbstractAudioModule.java b/src/main/java/io/github/dsheirer/audio/AbstractAudioModule.java index f7c21928c..5c8234ba0 100644 --- a/src/main/java/io/github/dsheirer/audio/AbstractAudioModule.java +++ b/src/main/java/io/github/dsheirer/audio/AbstractAudioModule.java @@ -99,10 +99,6 @@ protected void closeAudioSegment() @Override public void stop() { - if(getAudioSegment().isDuplicate()) - { - mLog.warn("Audio Module [" + getClass() + "] stop() invoked AND DUPLICATE=TRUE is detected"); - } closeAudioSegment(); } @@ -140,26 +136,33 @@ public AudioSegment getAudioSegment() public void addAudio(float[] audioBuffer) { - AudioSegment audioSegment = getAudioSegment(); - - //If the current segment exceeds the max samples length, close it so that a new segment gets generated - //and then link the segments together - if(mAudioSampleCount >= mMaxSegmentAudioSampleLength) + if(audioBuffer != null) { - AudioSegment previous = getAudioSegment(); - closeAudioSegment(); - audioSegment = getAudioSegment(); - audioSegment.linkTo(previous); - } + AudioSegment audioSegment = getAudioSegment(); - try - { - audioSegment.addAudio(audioBuffer); - mAudioSampleCount += audioBuffer.length; + //If the current segment exceeds the max samples length, close it so that a new segment gets generated + //and then link the segments together + if(mAudioSampleCount >= mMaxSegmentAudioSampleLength) + { + AudioSegment previous = getAudioSegment(); + closeAudioSegment(); + audioSegment = getAudioSegment(); + audioSegment.linkTo(previous); + } + + try + { + audioSegment.addAudio(audioBuffer); + mAudioSampleCount += audioBuffer.length; + } + catch(Exception e) + { + closeAudioSegment(); + } } - catch(Exception e) + else { - closeAudioSegment(); + mLog.info("Attempt to add null audio from " + getClass()); } } diff --git a/src/main/java/io/github/dsheirer/audio/call/CallRepository.java b/src/main/java/io/github/dsheirer/audio/call/CallRepository.java index 58795f475..d442e4acd 100644 --- a/src/main/java/io/github/dsheirer/audio/call/CallRepository.java +++ b/src/main/java/io/github/dsheirer/audio/call/CallRepository.java @@ -19,12 +19,37 @@ package io.github.dsheirer.audio.call; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; /** * Calls database repository */ public interface CallRepository extends JpaRepository { + @Query("SELECT t FROM Call t WHERE t.mToId like ?1 OR t.mToAlias like ?1 OR t.mFromId like ?1 OR t.mFromAlias like ?1") + Page findByAnyIdWithPagination(String value, Pageable pageable); + @Query("SELECT t FROM Call t WHERE t.mToId like ?1 OR t.mToAlias like ?1") + Page findByToWithPagination(String value, Pageable pageable); + + @Query("SELECT t FROM Call t WHERE t.mFromId like ?1 OR t.mFromAlias like ?1") + Page findByFromWithPagination(String value, Pageable pageable); + + @Query("SELECT t FROM Call t WHERE t.mCallType like ?1") + Page findByCallTypeWithPagination(String value, Pageable pageable); + + @Query("SELECT t FROM Call t WHERE t.mChannel like ?1") + Page findByChannelWithPagination(String value, Pageable pageable); + + @Query("SELECT t FROM Call t WHERE t.mProtocol like ?1") + Page findByProtocolWithPagination(String value, Pageable pageable); + + @Query("SELECT t FROM Call t WHERE t.mSite like ?1") + Page findBySiteWithPagination(String value, Pageable pageable); + + @Query("SELECT t FROM Call t WHERE t.mSystem like ?1") + Page findBySystemWithPagination(String value, Pageable pageable); } diff --git a/src/main/java/io/github/dsheirer/audio/call/CallViewPanel.java b/src/main/java/io/github/dsheirer/audio/call/CallViewPanel.java index 51367d1f4..e73ab3edb 100644 --- a/src/main/java/io/github/dsheirer/audio/call/CallViewPanel.java +++ b/src/main/java/io/github/dsheirer/audio/call/CallViewPanel.java @@ -37,6 +37,7 @@ import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.control.Alert; +import javafx.scene.control.CheckBox; import javafx.scene.control.ComboBox; import javafx.scene.control.Label; import javafx.scene.control.ProgressIndicator; @@ -87,13 +88,14 @@ public class CallViewPanel extends VBox implements IPageRequestListener private Label mCallsCountLabel; private LongProperty mCallCount = new SimpleLongProperty(); private Sort mSort = DEFAULT_SORT; + private ComboBox mSearchColumnComboBox; + private CheckBox mSearchExactCheckBox; /** * Constructs an instance */ public CallViewPanel() { - setSpacing(5); mCallCount.addListener((observable, oldValue, newValue) -> { getCallsCountLabel().setText("Calls: " + newValue); int pageCount = (int)(mCallCount.get() / getPageSize()); @@ -111,8 +113,13 @@ public CallViewPanel() @PostConstruct public void postConstruct() { + HBox controlBox = new HBox(); + controlBox.setPadding(new Insets(2, 15, 2, 15)); + HBox.setHgrow(getPagingBox(), Priority.ALWAYS); + controlBox.getChildren().addAll(getSearchBox(), getPagingBox()); + VBox.setVgrow(getCallTableView(), Priority.ALWAYS); - getChildren().addAll(getSearchBox(), getCallTableView(), getPagingBox()); + getChildren().addAll(controlBox, getCallTableView()); getPagingController().update(1, 20); @@ -132,6 +139,14 @@ public void preDestroy() mAudioManager.remove(mCallEventListener); } + /** + * Shows first page of results. + */ + private void showFirstPage() + { + showPage(1); + } + /** * Sets the page to view. This implements the IPageRequestListener interface. * @param page number to display. @@ -143,19 +158,52 @@ public void showPage(int page) { Platform.runLater(() -> getPlaceholderProgressIndicator().setVisible(true)); + final String searchTerm = getSearchField().getText(); + final SearchColumn searchColumn = getSearchColumnComboBox().getSelectionModel().getSelectedItem(); + final boolean wildcard = !getSearchExactCheckBox().isSelected(); + //Spin the query off onto a cached thread and then place the results back onto the FX thread. - ThreadPool.CACHED.submit(() -> { + ThreadPool.CACHED.submit(() -> + { + final PageRequest pageRequest = PageRequest.of(page - 1, getPageSize(), mSort); try { - Page callPage = mCallRepository.findAll(PageRequest.of(page - 1, getPageSize(), mSort)); - Platform.runLater(() -> { - mCalls.clear(); - mCalls.addAll(callPage.stream().toList()); - getPlaceholderProgressIndicator().setVisible(false); - getPagingController().update(callPage.getNumber() + 1, callPage.getTotalPages()); - mCallCount.set(callPage.getTotalElements()); - }); + Page callPage = null; + + if(searchTerm != null && !searchTerm.isEmpty() && searchColumn != null) + { + final String wildcardSearch = (wildcard ? "%" : "") + searchTerm + (wildcard ? "%" : ""); + + switch(searchColumn) + { + case ANY_ID -> callPage = mCallRepository.findByAnyIdWithPagination(wildcardSearch, pageRequest); + case CALL_TYPE -> callPage = mCallRepository.findByCallTypeWithPagination(wildcardSearch, pageRequest); + case CHANNEL -> callPage = mCallRepository.findByChannelWithPagination(wildcardSearch, pageRequest); + case FROM -> callPage = mCallRepository.findByFromWithPagination(wildcardSearch, pageRequest); + case PROTOCOL -> callPage = mCallRepository.findByProtocolWithPagination(wildcardSearch, pageRequest); + case SITE -> callPage = mCallRepository.findBySiteWithPagination(wildcardSearch, pageRequest); + case SYSTEM -> callPage = mCallRepository.findBySystemWithPagination(wildcardSearch, pageRequest); + case TO -> callPage = mCallRepository.findByToWithPagination(wildcardSearch, pageRequest); + } + } + else + { + callPage = mCallRepository.findAll(pageRequest); + } + + if(callPage != null) + { + final Page finalCallPage = callPage; + + Platform.runLater(() -> { + mCalls.clear(); + mCalls.addAll(finalCallPage.stream().toList()); + getPlaceholderProgressIndicator().setVisible(false); + getPagingController().update(finalCallPage.getNumber() + 1, finalCallPage.getTotalPages()); + mCallCount.set(finalCallPage.getTotalElements()); + }); + } } catch(Throwable t) { @@ -175,6 +223,10 @@ public void showPage(int page) } }); } + else + { + LOGGER.warn("Invalid page [" + page + "] number request"); + } } /** @@ -207,7 +259,7 @@ private void updateSortOrder() mSort = DEFAULT_SORT; } - showPage(0); + showFirstPage(); } /** @@ -231,6 +283,23 @@ private Label getCallsCountLabel() return mCallsCountLabel; } + /** + * Toggles wildcard searching + * @return true for exact match or false for wildcard/fuzzy matching. + */ + private CheckBox getSearchExactCheckBox() + { + if(mSearchExactCheckBox == null) + { + mSearchExactCheckBox = new CheckBox("Exact Match"); + mSearchExactCheckBox.setTooltip(new Tooltip("Toggle exact matching (checked) or wildcard/fuzzy " + + "matching (unchecked)")); + mSearchExactCheckBox.selectedProperty().addListener((observable, oldValue, newValue) -> showFirstPage()); + } + + return mSearchExactCheckBox; + } + /** * Page size selection combo box. * @@ -243,7 +312,7 @@ private ComboBox getPageSizeComboBox() ObservableList pageSizes = FXCollections.observableArrayList(); pageSizes.addAll(25, 50, 100, 200); mPageSizeComboBox = new ComboBox<>(pageSizes); - mPageSizeComboBox.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> showPage(1)); + mPageSizeComboBox.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> showFirstPage()); mPageSizeComboBox.setTooltip(new Tooltip("Select the page size")); mPageSizeComboBox.getSelectionModel().select(0); } @@ -251,6 +320,23 @@ private ComboBox getPageSizeComboBox() return mPageSizeComboBox; } + /** + * Page size selection combo box. + * + * Note: when the user changes the page size, we always request page 1 again. + */ + private ComboBox getSearchColumnComboBox() + { + if(mSearchColumnComboBox == null) + { + mSearchColumnComboBox = new ComboBox<>(FXCollections.observableArrayList(SearchColumn.values())); + mSearchColumnComboBox.getSelectionModel().select(SearchColumn.ANY_ID); + mSearchColumnComboBox.setTooltip(new Tooltip("Select the field(s) to search against")); + } + + return mSearchColumnComboBox; + } + /** * Paging controls panel */ @@ -259,7 +345,7 @@ private HBox getPagingBox() if(mPagingBox == null) { mPagingBox = new HBox(); - mPagingBox.setAlignment(Pos.CENTER_LEFT); + mPagingBox.setAlignment(Pos.CENTER_RIGHT); mPagingBox.setSpacing(3); Label pageSizeLabel = new Label("Page Size:"); getPagingController().setPadding(new Insets(2, 0, 6, 20)); @@ -442,12 +528,11 @@ private HBox getSearchBox() { mSearchBox = new HBox(); mSearchBox.setAlignment(Pos.CENTER_LEFT); - mSearchBox.setPadding(new Insets(5, 5, 0, 15)); mSearchBox.setSpacing(5); Label searchLabel = new Label("Search:"); searchLabel.setAlignment(Pos.CENTER_RIGHT); - mSearchBox.getChildren().addAll(searchLabel, getSearchField()); + mSearchBox.getChildren().addAll(searchLabel, getSearchField(), getSearchColumnComboBox(), getSearchExactCheckBox()); } return mSearchBox; @@ -458,9 +543,9 @@ private TextField getSearchField() if(mSearchField == null) { mSearchField = TextFields.createClearableTextField(); - mSearchField.setDisable(true); + mSearchField.setTooltip(new Tooltip("Type a value to search against the selected database field(s)")); mSearchField.textProperty().addListener((observable, oldValue, newValue) -> { -// updateListFilters(); + showFirstPage(); }); } diff --git a/src/main/java/io/github/dsheirer/audio/call/PagingController.java b/src/main/java/io/github/dsheirer/audio/call/PagingController.java index fa960bd96..bf006a00d 100644 --- a/src/main/java/io/github/dsheirer/audio/call/PagingController.java +++ b/src/main/java/io/github/dsheirer/audio/call/PagingController.java @@ -148,6 +148,7 @@ private Label getPageLabel() if(mPageLabel == null) { mPageLabel = new Label("Page 1 of 1"); + mPageLabel.setTooltip(new Tooltip("Current Page and Total Page Count")); mPageLabel.setAlignment(Pos.CENTER); mPageLabel.setMaxWidth(Double.MAX_VALUE); mPageLabel.setPadding(new Insets(0, 20, 0, 20)); @@ -164,6 +165,7 @@ private Button getFirstButton() if(mFirstButton == null) { mFirstButton = new Button(); + mFirstButton.setTooltip(new Tooltip("Go To First Page")); IconNode iconNode = new IconNode(FontAwesome.ANGLE_DOUBLE_LEFT); iconNode.setIconSize(20); iconNode.setFill(Color.BLACK); @@ -182,6 +184,7 @@ private Button getLastButton() if(mLastButton == null) { mLastButton = new Button(); + mLastButton.setTooltip(new Tooltip("Go To Last Page")); IconNode iconNode = new IconNode(FontAwesome.ANGLE_DOUBLE_RIGHT); iconNode.setIconSize(20); iconNode.setFill(Color.BLACK); @@ -200,6 +203,7 @@ private Button getNextButton() if(mNextButton == null) { mNextButton = new Button(); + mNextButton.setTooltip(new Tooltip("Go To Next Page")); IconNode iconNode = new IconNode(FontAwesome.ANGLE_RIGHT); iconNode.setIconSize(20); iconNode.setFill(Color.BLACK); @@ -218,6 +222,7 @@ private Button getPreviousButton() if(mPreviousButton == null) { mPreviousButton = new Button(); + mPreviousButton.setTooltip(new Tooltip("Go To Previous Page")); IconNode iconNode = new IconNode(FontAwesome.ANGLE_LEFT); iconNode.setIconSize(20); iconNode.setFill(Color.BLACK); diff --git a/src/main/java/io/github/dsheirer/audio/call/SearchColumn.java b/src/main/java/io/github/dsheirer/audio/call/SearchColumn.java new file mode 100644 index 000000000..c70840d64 --- /dev/null +++ b/src/main/java/io/github/dsheirer/audio/call/SearchColumn.java @@ -0,0 +1,35 @@ +package io.github.dsheirer.audio.call; + +/** + * Search columns + */ +public enum SearchColumn +{ + ANY_ID("Any ID (To / From / Alias)"), + TO("To / Alias"), + FROM("From / Alias"), + CHANNEL("Channel"), + PROTOCOL("Protocol"), + SITE("Site"), + SYSTEM("System"), + CALL_TYPE("Type"); + + private String mLabel; + + /** + * Constructs an instance + * @param label to display + */ + private SearchColumn(String label) + { + mLabel = label; + } + + /** + * Display label + */ + public String toString() + { + return mLabel; + } +}