mirror of
https://github.com/praktimarc/kst4contest.git
synced 2026-04-03 14:35:40 +02:00
Compare commits
19 Commits
bugfix_nov
...
1f3aa031c3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f3aa031c3 | ||
| 6be44bbea2 | |||
| 6b311c3907 | |||
|
|
7f9b1bfc4d | ||
|
|
ee5ee535bb | ||
|
|
5cca2923c2 | ||
|
|
1663b0fd7f | ||
|
|
3e8783d7cd | ||
|
|
8bea4111f0 | ||
|
|
136cf08f08 | ||
|
|
eb04ad3f33 | ||
|
|
51712a1f85 | ||
|
|
4a605f54ba | ||
|
|
037dc8a05b | ||
|
|
476b4a7dd1 | ||
|
|
bd687dc50f | ||
|
|
7bce7be2ba | ||
|
|
3286a34a08 | ||
|
|
c2086a73b0 |
145
.github/workflows/nightly-artifacts.yml
vendored
Normal file
145
.github/workflows/nightly-artifacts.yml
vendored
Normal file
@@ -0,0 +1,145 @@
|
||||
name: Nightly Runtime Artifacts
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
schedule:
|
||||
- cron: "20 2 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-windows-zip:
|
||||
name: Build Windows ZIP Artifact
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Extract version from pom.xml
|
||||
shell: pwsh
|
||||
run: |
|
||||
$xml = [xml](Get-Content pom.xml)
|
||||
$version = $xml.project.version
|
||||
Add-Content -Path $env:GITHUB_ENV -Value "VERSION=$version"
|
||||
|
||||
- name: Set up Java 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "17"
|
||||
|
||||
- name: Install WiX Toolset
|
||||
shell: pwsh
|
||||
run: choco install wixtoolset --no-progress -y
|
||||
|
||||
- name: Build JAR and copy runtime dependencies
|
||||
shell: pwsh
|
||||
run: |
|
||||
.\mvnw.cmd -B -DskipTests package dependency:copy-dependencies -DincludeScope=runtime -DoutputDirectory=target/dist-libs
|
||||
$jar = Get-ChildItem -Path target -Filter 'praktiKST-*.jar' | Sort-Object LastWriteTime -Descending | Select-Object -First 1
|
||||
if (-not $jar) {
|
||||
throw "No project JAR produced"
|
||||
}
|
||||
Copy-Item $jar.FullName target/dist-libs/app.jar
|
||||
|
||||
- name: Build app-image with jpackage
|
||||
shell: pwsh
|
||||
run: |
|
||||
New-Item -ItemType Directory -Force -Path dist | Out-Null
|
||||
jpackage --type app-image --name praktiKST --input target/dist-libs --main-jar app.jar --main-class kst4contest.view.Kst4ContestApplication --module-path target/dist-libs --add-modules javafx.controls,javafx.graphics,javafx.fxml,javafx.web,javafx.media,java.sql --dest dist
|
||||
|
||||
- name: Create Windows ZIP
|
||||
shell: pwsh
|
||||
run: |
|
||||
if (-not (Test-Path dist/praktiKST)) {
|
||||
throw "No Windows app-image produced by jpackage"
|
||||
}
|
||||
Compress-Archive -Path dist/praktiKST -DestinationPath dist/praktiKST-${{ env.VERSION }}-windows-x64.zip -Force
|
||||
|
||||
- name: Upload Windows ZIP artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: nightly-windows-zip
|
||||
path: dist/praktiKST-${{ env.VERSION }}-windows-x64.zip
|
||||
retention-days: 14
|
||||
|
||||
build-linux-appimage:
|
||||
name: Build Linux AppImage Artifact
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Extract version from pom.xml
|
||||
run: |
|
||||
VERSION=$(grep -m1 '<version>' pom.xml | sed 's/.*<version>\(.*\)<\/version>.*/\1/')
|
||||
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
||||
|
||||
- name: Set up Java 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "17"
|
||||
|
||||
- name: Ensure mvnw is executable
|
||||
run: chmod +x mvnw
|
||||
|
||||
- name: Build JAR and copy runtime dependencies
|
||||
run: |
|
||||
./mvnw -B -DskipTests package dependency:copy-dependencies -DincludeScope=runtime -DoutputDirectory=target/dist-libs
|
||||
cp "$(ls -t target/praktiKST-*.jar | head -n 1)" target/dist-libs/app.jar
|
||||
|
||||
- name: Build app-image with jpackage
|
||||
run: |
|
||||
mkdir -p dist
|
||||
jpackage \
|
||||
--type app-image \
|
||||
--name praktiKST \
|
||||
--input target/dist-libs \
|
||||
--main-jar app.jar \
|
||||
--main-class kst4contest.view.Kst4ContestApplication \
|
||||
--module-path target/dist-libs \
|
||||
--add-modules javafx.controls,javafx.graphics,javafx.fxml,javafx.web,javafx.media,java.sql \
|
||||
--dest dist
|
||||
|
||||
- name: Create AppDir metadata
|
||||
run: |
|
||||
rm -rf target/praktiKST.AppDir
|
||||
cp -a dist/praktiKST target/praktiKST.AppDir
|
||||
|
||||
cat > target/praktiKST.AppDir/AppRun << 'EOF'
|
||||
#!/bin/sh
|
||||
HERE="$(dirname "$(readlink -f "$0")")"
|
||||
exec "$HERE/bin/praktiKST" "$@"
|
||||
EOF
|
||||
chmod +x target/praktiKST.AppDir/AppRun
|
||||
|
||||
cat > target/praktiKST.AppDir/praktiKST.desktop << 'EOF'
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=praktiKST
|
||||
Exec=praktiKST
|
||||
Icon=praktiKST
|
||||
Categories=Network;HamRadio;
|
||||
Terminal=false
|
||||
EOF
|
||||
|
||||
if [ -f target/praktiKST.AppDir/lib/praktiKST.png ]; then
|
||||
cp target/praktiKST.AppDir/lib/praktiKST.png target/praktiKST.AppDir/praktiKST.png
|
||||
fi
|
||||
|
||||
- name: Build AppImage
|
||||
run: |
|
||||
wget -q -O target/appimagetool.AppImage https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage
|
||||
chmod +x target/appimagetool.AppImage
|
||||
APPIMAGE_EXTRACT_AND_RUN=1 ARCH=x86_64 target/appimagetool.AppImage target/praktiKST.AppDir dist/praktiKST-${{ env.VERSION }}-linux-x86_64.AppImage
|
||||
|
||||
- name: Upload Linux AppImage artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: nightly-linux-appimage
|
||||
path: dist/praktiKST-${{ env.VERSION }}-linux-x86_64.AppImage
|
||||
retention-days: 14
|
||||
28
.github/workflows/pr-compile-check.yml
vendored
Normal file
28
.github/workflows/pr-compile-check.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
name: PR Compile Check
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
compile:
|
||||
name: Compile (Java 17)
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Java 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "17"
|
||||
|
||||
- name: Ensure mvnw is executable
|
||||
run: chmod +x mvnw
|
||||
|
||||
- name: Compile
|
||||
run: ./mvnw -B -DskipTests compile
|
||||
164
.github/workflows/tagged-release.yml
vendored
Normal file
164
.github/workflows/tagged-release.yml
vendored
Normal file
@@ -0,0 +1,164 @@
|
||||
name: Tagged Release Build
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build-windows-zip:
|
||||
name: Build Windows ZIP
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Java 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "17"
|
||||
|
||||
- name: Install WiX Toolset
|
||||
shell: pwsh
|
||||
run: choco install wixtoolset --no-progress -y
|
||||
|
||||
- name: Build JAR and copy runtime dependencies
|
||||
shell: pwsh
|
||||
run: |
|
||||
.\mvnw.cmd -B -DskipTests package dependency:copy-dependencies -DincludeScope=runtime -DoutputDirectory=target/dist-libs
|
||||
$jar = Get-ChildItem -Path target -Filter 'praktiKST-*.jar' | Sort-Object LastWriteTime -Descending | Select-Object -First 1
|
||||
if (-not $jar) {
|
||||
throw "No project JAR produced"
|
||||
}
|
||||
Copy-Item $jar.FullName target/dist-libs/app.jar
|
||||
|
||||
- name: Build app-image with jpackage
|
||||
shell: pwsh
|
||||
run: |
|
||||
New-Item -ItemType Directory -Force -Path dist | Out-Null
|
||||
jpackage --type app-image --name praktiKST --input target/dist-libs --main-jar app.jar --main-class kst4contest.view.Kst4ContestApplication --module-path target/dist-libs --add-modules javafx.controls,javafx.graphics,javafx.fxml,javafx.web,javafx.media,java.sql --dest dist
|
||||
|
||||
- name: Create Windows ZIP
|
||||
shell: pwsh
|
||||
run: |
|
||||
if (-not (Test-Path dist/praktiKST)) {
|
||||
throw "No Windows app-image produced by jpackage"
|
||||
}
|
||||
Compress-Archive -Path dist/praktiKST -DestinationPath dist/praktiKST-${{ github.ref_name }}-windows-x64.zip -Force
|
||||
|
||||
- name: Upload Windows artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: windows-zip
|
||||
path: dist/praktiKST-${{ github.ref_name }}-windows-x64.zip
|
||||
|
||||
build-linux-appimage:
|
||||
name: Build Linux AppImage
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Java 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "17"
|
||||
|
||||
- name: Ensure mvnw is executable
|
||||
run: chmod +x mvnw
|
||||
|
||||
- name: Build JAR and copy runtime dependencies
|
||||
run: |
|
||||
./mvnw -B -DskipTests package dependency:copy-dependencies -DincludeScope=runtime -DoutputDirectory=target/dist-libs
|
||||
cp "$(ls -t target/praktiKST-*.jar | head -n 1)" target/dist-libs/app.jar
|
||||
|
||||
- name: Build app-image with jpackage
|
||||
run: |
|
||||
mkdir -p dist
|
||||
jpackage \
|
||||
--type app-image \
|
||||
--name praktiKST \
|
||||
--input target/dist-libs \
|
||||
--main-jar app.jar \
|
||||
--main-class kst4contest.view.Kst4ContestApplication \
|
||||
--module-path target/dist-libs \
|
||||
--add-modules javafx.controls,javafx.graphics,javafx.fxml,javafx.web,javafx.media,java.sql \
|
||||
--dest dist
|
||||
|
||||
- name: Create AppDir metadata
|
||||
run: |
|
||||
rm -rf target/praktiKST.AppDir
|
||||
cp -a dist/praktiKST target/praktiKST.AppDir
|
||||
|
||||
cat > target/praktiKST.AppDir/AppRun << 'EOF'
|
||||
#!/bin/sh
|
||||
HERE="$(dirname "$(readlink -f "$0")")"
|
||||
exec "$HERE/bin/praktiKST" "$@"
|
||||
EOF
|
||||
chmod +x target/praktiKST.AppDir/AppRun
|
||||
|
||||
cat > target/praktiKST.AppDir/praktiKST.desktop << 'EOF'
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=praktiKST
|
||||
Exec=praktiKST
|
||||
Icon=praktiKST
|
||||
Categories=Network;HamRadio;
|
||||
Terminal=false
|
||||
EOF
|
||||
|
||||
if [ -f target/praktiKST.AppDir/lib/praktiKST.png ]; then
|
||||
cp target/praktiKST.AppDir/lib/praktiKST.png target/praktiKST.AppDir/praktiKST.png
|
||||
fi
|
||||
|
||||
- name: Build AppImage
|
||||
run: |
|
||||
wget -q -O target/appimagetool.AppImage https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage
|
||||
chmod +x target/appimagetool.AppImage
|
||||
APPIMAGE_EXTRACT_AND_RUN=1 ARCH=x86_64 target/appimagetool.AppImage target/praktiKST.AppDir dist/praktiKST-${{ github.ref_name }}-linux-x86_64.AppImage
|
||||
|
||||
- name: Upload Linux artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: linux-appimage
|
||||
path: dist/praktiKST-${{ github.ref_name }}-linux-x86_64.AppImage
|
||||
|
||||
release-tag:
|
||||
name: Publish Tagged Release
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build-windows-zip
|
||||
- build-linux-appimage
|
||||
|
||||
steps:
|
||||
- name: Download Windows artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: windows-zip
|
||||
path: release-assets/windows
|
||||
|
||||
- name: Download Linux artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: linux-appimage
|
||||
path: release-assets/linux
|
||||
|
||||
- name: Create tagged release
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag: ${{ github.ref_name }}
|
||||
name: Release ${{ github.ref_name }}
|
||||
allowUpdates: false
|
||||
replacesArtifacts: false
|
||||
makeLatest: true
|
||||
generateReleaseNotes: true
|
||||
artifacts: release-assets/windows/praktiKST-${{ github.ref_name }}-windows-x64.zip,release-assets/linux/praktiKST-${{ github.ref_name }}-linux-x86_64.AppImage
|
||||
16
.gitignore
vendored
16
.gitignore
vendored
@@ -16,3 +16,19 @@ target
|
||||
debug.out
|
||||
.DS_Store
|
||||
|
||||
#Logfiles
|
||||
SimpleLogFile.txt
|
||||
udpReaderBackup.txt
|
||||
|
||||
#tempfiles
|
||||
.idea/
|
||||
out/
|
||||
|
||||
#targetfiles - mvn wrapper
|
||||
target/
|
||||
|
||||
#builds
|
||||
build/
|
||||
|
||||
#zip files for local backups
|
||||
*.zip
|
||||
@@ -1 +1,2 @@
|
||||
do5sa
|
||||
dr2x
|
||||
oe3cin
|
||||
15832
bugsept24.txt
Normal file
15832
bugsept24.txt
Normal file
File diff suppressed because it is too large
Load Diff
3
jdk.cmd
Normal file
3
jdk.cmd
Normal file
@@ -0,0 +1,3 @@
|
||||
@echo off
|
||||
SET JAVA_HOME=C:\Program Files\Java\jdk-17
|
||||
SET PATH=%JAVA_HOME%\bin,%PATH%
|
||||
8
pom.xml
8
pom.xml
@@ -6,7 +6,7 @@
|
||||
|
||||
<groupId>de.x08</groupId>
|
||||
<artifactId>praktiKST</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<version>1.40.0-nightly</version>
|
||||
|
||||
<name>praktiKST</name>
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<javafx.version>19.0.2.1</javafx.version>
|
||||
<jetbrains.annotations.version>24.0.1</jetbrains.annotations.version>
|
||||
<junit.version>5.10.1</junit.version>
|
||||
<lombok.version>1.18.30</lombok.version>
|
||||
<lombok.version>1.18.44</lombok.version>
|
||||
<mockito.version>5.7.0</mockito.version>
|
||||
<sqlite.version>3.43.2.2</sqlite.version>
|
||||
|
||||
@@ -54,8 +54,8 @@
|
||||
<pmd.version>6.55.0</pmd.version>
|
||||
<codehaus.version.plugin>2.16.1</codehaus.version.plugin>
|
||||
<javafx.maven.plugin>0.0.8</javafx.maven.plugin>
|
||||
<spotbugs.maven.plugin>4.8.1.0</spotbugs.maven.plugin>
|
||||
<spotbugs.version>4.8.1</spotbugs.version>
|
||||
<spotbugs.maven.plugin>4.9.8.2</spotbugs.maven.plugin>
|
||||
<spotbugs.version>4.9.8</spotbugs.version>
|
||||
|
||||
<!-- other properties -->
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
|
||||
@@ -1,8 +1,54 @@
|
||||
package kst4contest;
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
public class ApplicationConstants {
|
||||
|
||||
/**
|
||||
* default constructor generates runtime id
|
||||
*/
|
||||
ApplicationConstants() {
|
||||
sessionRuntimeUniqueId = generateRuntimeId();
|
||||
};
|
||||
|
||||
public static int sessionRuntimeUniqueId = generateRuntimeId();
|
||||
/**
|
||||
* Name of the Application.
|
||||
*/
|
||||
public static final String APPLICATION_NAME = "praktiKST";
|
||||
|
||||
/**
|
||||
* Name of file to store preferences in.
|
||||
*/
|
||||
public static final double APPLICATION_CURRENTVERSIONNUMBER = 1.41;
|
||||
|
||||
public static final String VERSIONINFOURLFORUPDATES_KST4CONTEST = "https://do5amf.funkerportal.de/kst4ContestVersionInfo.xml";
|
||||
public static final String VERSIONINFDOWNLOADEDLOCALFILE = "kst4ContestVersionInfo.xml";
|
||||
|
||||
public static final String STYLECSSFILE_DEFAULT_DAYLIGHT = "KST4ContestDefaultDay.css";
|
||||
public static final String STYLECSSFILE_DEFAULT_EVENING = "KST4ContestDefaultEvening.css";
|
||||
|
||||
public static final String DISCSTRING_DISCONNECT_AND_CLOSE = "CLOSEALL";
|
||||
public static final String DISCSTRING_DISCONNECT_DUE_PAWWORDERROR = "JUSTDSICCAUSEPWWRONG";
|
||||
public static final String DISCSTRING_DISCONNECTONLY = "ONLYDISCONNECT";
|
||||
|
||||
// public static final String DISCONNECT_RDR_POISONPILL = "POISONPILL_KILLTHREAD: " + sessionRuntimeUniqueId; //whereever a (blocking) udp or tcp reader in an infinite loop gets this message, it will break this loop
|
||||
|
||||
public static final String DISCONNECT_RDR_POISONPILL = "UNKNOWN: KST4C KILL POISONPILL_KILLTHREAD=: " + sessionRuntimeUniqueId; //whereever a (blocking) udp or tcp reader in an infinite loop gets this message, it will break this loop
|
||||
|
||||
public static final String AUTOANSWER_PREFIX = "[KST4C Automsg] "; // hard-coded marker (user can't remove it)
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* generates a unique runtime id per session. Its used to feed the poison pill in order to kill only this one and
|
||||
* only instance if the program and not multiple instances
|
||||
* @return
|
||||
*/
|
||||
public static int generateRuntimeId() {
|
||||
|
||||
Random ran = new Random();
|
||||
|
||||
return ran.nextInt(6) + 100;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import java.net.UnknownHostException;
|
||||
import java.util.TimerTask;
|
||||
|
||||
import javafx.collections.ObservableList;
|
||||
import kst4contest.locatorUtils.Location;
|
||||
import kst4contest.model.ChatMember;
|
||||
|
||||
|
||||
@@ -28,22 +29,45 @@ public class AirScoutPeriodicalAPReflectionInquirerTask extends TimerTask {
|
||||
public void run() {
|
||||
|
||||
Thread.currentThread().setName("AirscoutPeriodicalReflectionInquirierTask");
|
||||
|
||||
|
||||
String KSTClientsNameForQuery = this.client.getChatPreferences().getAirScout_asClientNameString();
|
||||
String ASServerNameStringForAnswer = this.client.getChatPreferences().getAirScout_asServerNameString();
|
||||
|
||||
//TODO: Manage prefixes kst and as via preferences file and instance
|
||||
//TODO: Check if locator is changeable via the preferences object, need to be correct if it changes
|
||||
DatagramSocket dsocket;
|
||||
|
||||
String prefix_asSetpath ="ASSETPATH: \"KST\" \"AS\" ";
|
||||
String prefix_asWatchList = "ASWATCHLIST: \"KST\" \"AS\" ";
|
||||
String bandString = "1440000";
|
||||
String myCallAndMyLocString = this.client.getChatPreferences().getLoginCallSign() + "," + this.client.getChatPreferences().getLoginLocator();
|
||||
// String prefix_asSetpath ="ASSETPATH: \"KST\" \"AS\" "; //working original
|
||||
// String prefix_asWatchList = "ASWATCHLIST: \"KST\" \"AS\" "; //working original
|
||||
|
||||
String prefix_asSetpath ="ASSETPATH: \"" + this.client.getChatPreferences().getAirScout_asClientNameString() + "\" \"" + this.client.getChatPreferences().getAirScout_asServerNameString() + "\" ";
|
||||
String prefix_asWatchList = "ASWATCHLIST: \""+ this.client.getChatPreferences().getAirScout_asClientNameString()+ "\" \"" + this.client.getChatPreferences().getAirScout_asServerNameString() + "\" ";
|
||||
|
||||
String bandString = "1440000"; //TODO: this must variable in case of higher bands! ... default: 1440000
|
||||
// String myCallAndMyLocString = this.client.getChatPreferences().getStn_loginCallSign() + "," + this.client.getChatPreferences().getStn_loginLocatorMainCat(); //before fix 1.266
|
||||
|
||||
|
||||
String ownCallSign = this.client.getChatPreferences().getStn_loginCallSign();
|
||||
try {
|
||||
if (this.client.getChatPreferences().getStn_loginCallSign().contains("-")) {
|
||||
ownCallSign = this.client.getChatPreferences().getStn_loginCallSign().split("-")[0];
|
||||
} else {
|
||||
ownCallSign = this.client.getChatPreferences().getStn_loginCallSign();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
System.out.println("[ASPERIODICAL, Error]: " + e.getMessage());
|
||||
}
|
||||
String myCallAndMyLocString = ownCallSign + "," + this.client.getChatPreferences().getStn_loginLocatorMainCat(); //bugfix, Airscout do not process 9A1W-2 but 9A1W like formatted calls
|
||||
|
||||
|
||||
String suffix = ""; //"FOREIGNCALL,FOREIGNLOC " -- dont forget the space at the end!!!
|
||||
String asWatchListString = prefix_asWatchList + bandString + "," + myCallAndMyLocString;
|
||||
String asWatchListStringSuffix = asWatchListString;
|
||||
|
||||
String host = "255.255.255.255";
|
||||
int port = 9872;
|
||||
// int port = 9872;
|
||||
|
||||
int port = client.getChatPreferences().getAirScout_asCommunicationPort();
|
||||
// byte[] message = "ASSETPATH: \"KST\" \"AS\" 1440000,DO5AMF,JN49GL,OK1MZM,JN89IW ".getBytes(); Original, ging
|
||||
InetAddress address;
|
||||
|
||||
@@ -60,113 +84,41 @@ public class AirScoutPeriodicalAPReflectionInquirerTask extends TimerTask {
|
||||
praktiKSTActiveUserList.toArray(ary_threadSafeChatMemberArray);
|
||||
|
||||
for (ChatMember i : ary_threadSafeChatMemberArray) {
|
||||
|
||||
suffix = i.getCallSign() + "," + i.getQra() + " ";
|
||||
//
|
||||
String queryStringToAirScout = "";
|
||||
|
||||
queryStringToAirScout += prefix_asSetpath + bandString + "," + myCallAndMyLocString + "," + suffix;
|
||||
|
||||
byte[] queryStringToAirScoutMSG = queryStringToAirScout.getBytes();
|
||||
|
||||
try {
|
||||
address = InetAddress.getByName("255.255.255.255");
|
||||
DatagramPacket packet = new DatagramPacket(queryStringToAirScoutMSG, queryStringToAirScoutMSG.length, address, port);
|
||||
dsocket = new DatagramSocket();
|
||||
dsocket.setBroadcast(true);
|
||||
dsocket.send(packet);
|
||||
dsocket.close();
|
||||
} catch (UnknownHostException e1) {
|
||||
// TODO Auto-generated catch block
|
||||
e1.printStackTrace();
|
||||
} catch (NoRouteToHostException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
catch (SocketException e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
} catch (IOException e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
}
|
||||
// System.out.println("[ASUDPTask, info:] sent query " + queryStringToAirScout);
|
||||
if (i.getQrb() < this.client.getChatPreferences().getStn_maxQRBDefault())
|
||||
//Here: check if maximum distance to the chatmember is reached, only ask AS if distance is lower!
|
||||
//this counts for AS request and Aswatchlist
|
||||
{
|
||||
suffix = i.getCallSign() + "," + i.getQra() + " ";
|
||||
|
||||
asWatchListStringSuffix += "," + i.getCallSign() + "," + i.getQra();
|
||||
|
||||
String queryStringToAirScout = "";
|
||||
|
||||
queryStringToAirScout += prefix_asSetpath + bandString + "," + myCallAndMyLocString + "," + suffix;
|
||||
|
||||
byte[] queryStringToAirScoutMSG = queryStringToAirScout.getBytes();
|
||||
|
||||
try {
|
||||
address = InetAddress.getByName("255.255.255.255");
|
||||
DatagramPacket packet = new DatagramPacket(queryStringToAirScoutMSG, queryStringToAirScoutMSG.length, address, port);
|
||||
dsocket = new DatagramSocket();
|
||||
dsocket.setBroadcast(true);
|
||||
dsocket.send(packet);
|
||||
dsocket.close();
|
||||
} catch (UnknownHostException e1) {
|
||||
e1.printStackTrace();
|
||||
} catch (NoRouteToHostException e) {
|
||||
e.printStackTrace();
|
||||
} catch (SocketException e) {
|
||||
e.printStackTrace();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
// System.out.println("[ASUDPTask, info:] sent query " + queryStringToAirScout);
|
||||
|
||||
asWatchListStringSuffix += "," + i.getCallSign() + "," + i.getQra();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// for (Iterator iterator = praktiKSTActiveUserList.iterator(); iterator.hasNext();) {
|
||||
// ChatMember chatMember = (ChatMember) iterator.next();
|
||||
//
|
||||
// suffix = chatMember.getCallSign() + "," + chatMember.getQra() + " ";
|
||||
//
|
||||
// String queryStringToAirScout = "";
|
||||
//
|
||||
// queryStringToAirScout += prefix_asSetpath + bandString + "," + myCallAndMyLocString + "," + suffix;
|
||||
//
|
||||
// byte[] queryStringToAirScoutMSG = queryStringToAirScout.getBytes();
|
||||
//
|
||||
// try {
|
||||
// address = InetAddress.getByName("255.255.255.255");
|
||||
// DatagramPacket packet = new DatagramPacket(queryStringToAirScoutMSG, queryStringToAirScoutMSG.length, address, port);
|
||||
// dsocket = new DatagramSocket();
|
||||
// dsocket.setBroadcast(true);
|
||||
// dsocket.send(packet);
|
||||
// dsocket.close();
|
||||
// } catch (UnknownHostException e1) {
|
||||
// // TODO Auto-generated catch block
|
||||
// e1.printStackTrace();
|
||||
// } catch (SocketException e) {
|
||||
// // TODO Auto-generated catch block
|
||||
// e.printStackTrace();
|
||||
// } catch (IOException e) {
|
||||
// // TODO Auto-generated catch block
|
||||
// e.printStackTrace();
|
||||
// }
|
||||
//
|
||||
//// System.out.println("[ASUDPTask, info:] sent query " + queryStringToAirScout);
|
||||
//
|
||||
// asWatchListStringSuffix += "," + chatMember.getCallSign() + "," + chatMember.getQra();
|
||||
//
|
||||
// }}
|
||||
|
||||
|
||||
// for (Iterator iterator = praktiKSTActiveUserList.iterator(); iterator.hasNext();) {
|
||||
// ChatMember chatMember = (ChatMember) iterator.next();
|
||||
//
|
||||
// suffix = chatMember.getCallSign() + "," + chatMember.getQra() + " ";
|
||||
//
|
||||
// String queryStringToAirScout = "";
|
||||
//
|
||||
// queryStringToAirScout += prefix_asSetpath + bandString + "," + myCallAndMyLocString + "," + suffix;
|
||||
//
|
||||
// byte[] queryStringToAirScoutMSG = queryStringToAirScout.getBytes();
|
||||
//
|
||||
// try {
|
||||
// address = InetAddress.getByName("255.255.255.255");
|
||||
// DatagramPacket packet = new DatagramPacket(queryStringToAirScoutMSG, queryStringToAirScoutMSG.length, address, port);
|
||||
// dsocket = new DatagramSocket();
|
||||
// dsocket.setBroadcast(true);
|
||||
// dsocket.send(packet);
|
||||
// dsocket.close();
|
||||
// } catch (UnknownHostException e1) {
|
||||
// // TODO Auto-generated catch block
|
||||
// e1.printStackTrace();
|
||||
// } catch (SocketException e) {
|
||||
// // TODO Auto-generated catch block
|
||||
// e.printStackTrace();
|
||||
// } catch (IOException e) {
|
||||
// // TODO Auto-generated catch block
|
||||
// e.printStackTrace();
|
||||
// }
|
||||
//
|
||||
//// System.out.println("[ASUDPTask, info:] sent query " + queryStringToAirScout);
|
||||
//
|
||||
// asWatchListStringSuffix += "," + chatMember.getCallSign() + "," + chatMember.getQra();
|
||||
//
|
||||
// }
|
||||
|
||||
/**
|
||||
* As next we will set the ASWatchlist. All stations in chat will be watched by airscout causing following code.\n\n
|
||||
* ASWATCHLIST: "KST" "AS" 4320000,DO5AMF,JN49GL,DF9QX,JO42HD,DG2KBC,JN58MI,DJ0PY,JO32MF,DL1YDI,JO42FA,DL6BF,JO32QI,F1NZC,JN15MR,F4TXU,JN23CX,F5GHP,IN96LE,F6HTJ,JN12KQ,G0GGG,IO81VE,G0JCC,IO82MA,G0JDL,JO02SI,G0MBL,JO01QH,G4AEP,IO91MB,G4CLA,IO92JL,G4DCV,IO91OF,G4LOH,IO70JC,G4MKF,IO91HJ,G4TRA,IO81WN,G8GXP,IO93FQ,G8VHI,IO92FM,GW0RHC,IO71UN,HA4ND,JN97MJ,I5/HB9SJV/P,JN52JS,IW2DAL,JN45NN,OK1FPR,JO80CE,OK6M,JN99CR,OV3T,JO46CM,OZ2M,JO65FR,PA0V,JO33II,PA2RU,JO32LT,PA3DOL,JO22MT,PA9R,JO22JK,PE1EVX,JO22MP,S51AT,JN75GW,SM7KOJ,JO66ND,SP9TTG,JO90KW<4B>
|
||||
@@ -183,14 +135,7 @@ public class AirScoutPeriodicalAPReflectionInquirerTask extends TimerTask {
|
||||
dsocket.setBroadcast(true);
|
||||
dsocket.send(packet);
|
||||
dsocket.close();
|
||||
} catch (UnknownHostException e1) {
|
||||
// TODO Auto-generated catch block
|
||||
e1.printStackTrace();
|
||||
} catch (SocketException e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
} catch (IOException e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package kst4contest.controller;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.TimerTask;
|
||||
|
||||
import kst4contest.model.ChatMessage;
|
||||
import kst4contest.model.ThreadStateMessage;
|
||||
|
||||
/**
|
||||
* This class is for sending beacons intervalled to the public chat. Gets all
|
||||
@@ -20,43 +22,99 @@ import kst4contest.model.ChatMessage;
|
||||
public class BeaconTask extends TimerTask {
|
||||
|
||||
private ChatController chatController;
|
||||
private ThreadStatusCallback callBackToController;
|
||||
private String ThreadNickName = "MyBeacon";
|
||||
|
||||
public BeaconTask(ChatController client) {
|
||||
|
||||
public BeaconTask(ChatController client, ThreadStatusCallback callback) {
|
||||
this.callBackToController = callback;
|
||||
this.chatController = client;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Thread.currentThread().setName("BeaconTask");
|
||||
|
||||
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "initialized", false);
|
||||
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
|
||||
|
||||
Thread.currentThread().setName("BeaconTask");
|
||||
|
||||
|
||||
ChatMessage beaconMSG = new ChatMessage();
|
||||
|
||||
String replaceVariables = this.chatController.getChatPreferences().getBcn_beaconText();
|
||||
// replaceVariables = bcn_beaconText;
|
||||
|
||||
replaceVariables = replaceVariables.replaceAll("MYQRG", this.chatController.getChatPreferences().getMYQRG().getValue());
|
||||
replaceVariables = replaceVariables.replaceAll("MYCALL", this.chatController.getChatPreferences().getLoginCallSign());
|
||||
replaceVariables = replaceVariables.replaceAll("MYLOCATOR", this.chatController.getChatPreferences().getLoginLocator());
|
||||
String replaceVariables = this.chatController.getChatPreferences().getBcn_beaconTextMainCat();
|
||||
|
||||
replaceVariables = replaceVariables.replaceAll("MYQRG", this.chatController.getChatPreferences().getMYQRGFirstCat().getValue());
|
||||
replaceVariables = replaceVariables.replaceAll("MYCALL", this.chatController.getChatPreferences().getStn_loginCallSign());
|
||||
replaceVariables = replaceVariables.replaceAll("MYLOCATOR", this.chatController.getChatPreferences().getStn_loginLocatorMainCat());
|
||||
replaceVariables = replaceVariables.replaceAll("MYQTF", this.chatController.getChatPreferences().getActualQTF().getValue() + "");
|
||||
replaceVariables = replaceVariables.replaceAll("SECONDQRG", this.chatController.getChatPreferences().getActualQTF().getValue() + "");
|
||||
|
||||
|
||||
beaconMSG.setMessageText(
|
||||
"MSG|" + this.chatController.getChatPreferences().getLoginChatCategory().getCategoryNumber() + "|0|" + replaceVariables + "|0|");
|
||||
"MSG|" + this.chatController.getChatPreferences().getLoginChatCategoryMain().getCategoryNumber() + "|0|" + replaceVariables + "|0|");
|
||||
beaconMSG.setMessageDirectedToServer(true);
|
||||
|
||||
// System.out.println("########### " + replaceVariables);
|
||||
|
||||
if (this.chatController.getChatPreferences().isBcn_beaconsEnabled() ) {
|
||||
|
||||
|
||||
|
||||
|
||||
ChatMessage beaconMSG2 = new ChatMessage();
|
||||
|
||||
String replaceVariables2 = this.chatController.getChatPreferences().getBcn_beaconTextSecondCat();
|
||||
|
||||
replaceVariables2 = replaceVariables2.replaceAll("MYQRG", this.chatController.getChatPreferences().getMYQRGFirstCat().getValue());
|
||||
replaceVariables2 = replaceVariables2.replaceAll("MYCALL", this.chatController.getChatPreferences().getStn_loginCallSign());
|
||||
replaceVariables2 = replaceVariables2.replaceAll("MYLOCATOR", this.chatController.getChatPreferences().getStn_loginLocatorMainCat());
|
||||
replaceVariables2 = replaceVariables2.replaceAll("MYQTF", this.chatController.getChatPreferences().getActualQTF().getValue() + "");
|
||||
replaceVariables2 = replaceVariables2.replaceAll("SECONDQRG", this.chatController.getChatPreferences().getMYQRGSecondCat().getValue() + "");
|
||||
|
||||
|
||||
beaconMSG2.setMessageText(
|
||||
"MSG|" + this.chatController.getChatPreferences().getLoginChatCategorySecond().getCategoryNumber() + "|0|" + replaceVariables + "|0|");
|
||||
beaconMSG2.setMessageDirectedToServer(true);
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* beacon 1st Chatcategory
|
||||
*/
|
||||
if (this.chatController.getChatPreferences().isBcn_beaconsEnabledMainCat() ) {
|
||||
|
||||
System.out.println(new Utils4KST().time_generateCurrentMMDDhhmmTimeString()
|
||||
+ " [BeaconTask, Info]: Sending CQ: " + beaconMSG.getMessageText());
|
||||
this.chatController.getMessageTXBus().add(beaconMSG);
|
||||
|
||||
threadStateMessage = new ThreadStateMessage(this.ThreadNickName + " 1", true, "on", false);
|
||||
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
|
||||
|
||||
} else {
|
||||
//do nothing, CQ is disabled
|
||||
threadStateMessage = new ThreadStateMessage(this.ThreadNickName + " 1", false, "off", false);
|
||||
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* beacon 2nd Chatcategory
|
||||
*/
|
||||
if (this.chatController.getChatPreferences().isLoginToSecondChatEnabled()) { //only send if 2nd cat enabled
|
||||
|
||||
if (this.chatController.getChatPreferences().isBcn_beaconsEnabledSecondCat()) {
|
||||
|
||||
beaconMSG2.setMessageText(
|
||||
"MSG|" + this.chatController.getChatPreferences().getLoginChatCategorySecond().getCategoryNumber() + "|0|" + replaceVariables2 + "|0|");
|
||||
beaconMSG2.setMessageDirectedToServer(true);
|
||||
|
||||
System.out.println(new Utils4KST().time_generateCurrentMMDDhhmmTimeString()
|
||||
+ " [BeaconTask, Info]: Sending CQ 2nd Cat: " + beaconMSG2.getMessageText());
|
||||
this.chatController.getMessageTXBus().add(beaconMSG2);
|
||||
|
||||
threadStateMessage = new ThreadStateMessage(this.ThreadNickName + " 2", true, "on", false);
|
||||
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
|
||||
|
||||
} else {
|
||||
threadStateMessage = new ThreadStateMessage(this.ThreadNickName + " 2", false, "off", false);
|
||||
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
196
src/main/java/kst4contest/controller/DXClusterController.java
Normal file
196
src/main/java/kst4contest/controller/DXClusterController.java
Normal file
@@ -0,0 +1,196 @@
|
||||
//package kst4contest.controller;
|
||||
|
||||
|
||||
//
|
||||
//import kst4contest.model.ChatMember;
|
||||
//import kst4contest.model.ChatMessage;
|
||||
//
|
||||
//import java.io.*;
|
||||
//import java.net.ServerSocket;
|
||||
//import java.net.Socket;
|
||||
//import java.nio.channels.ServerSocketChannel;
|
||||
//import java.nio.channels.SocketChannel;
|
||||
//import java.time.Instant;
|
||||
//
|
||||
///**
|
||||
// * This thread is responsible for providing DXCluster messages for a connected log program.
|
||||
// *
|
||||
// *
|
||||
// */
|
||||
////public class DXClusterController extends Thread {
|
||||
// PrintWriter outTelnet;
|
||||
// BufferedReader inTelnet;
|
||||
// private Socket socket;
|
||||
// private ChatController client;
|
||||
//// private OutputStream output;
|
||||
//// private InputStream input;
|
||||
//
|
||||
// private ChatMessage messageTextRaw;
|
||||
//
|
||||
// private static final int PORT = 23;
|
||||
// private static final String USERNAME = "user";
|
||||
// private static final String PASSWORD = "pass";
|
||||
// private Socket clientSocket;
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//// public DXClusterController(Socket clientSocket, ChatController client) throws InterruptedException {
|
||||
////
|
||||
//// this.client = client;
|
||||
////
|
||||
//// try {
|
||||
//// outTelnet = new PrintWriter(clientSocket.getOutputStream(), true);
|
||||
//// inTelnet = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
|
||||
//// } catch (IOException e) {
|
||||
//// throw new RuntimeException(e);
|
||||
//// }
|
||||
//// System.out.println("defcons");
|
||||
//// this.clientSocket = clientSocket;
|
||||
////
|
||||
//// }
|
||||
//
|
||||
// public DXClusterController(Socket clientSocket, ChatController chatController) {
|
||||
//
|
||||
// try {
|
||||
// socket = clientSocket;
|
||||
// } catch (Exception e) {
|
||||
// throw new RuntimeException(e);
|
||||
// }
|
||||
// this.client = chatController;
|
||||
//
|
||||
// try {
|
||||
// outTelnet = new PrintWriter(socket.getOutputStream(), true);
|
||||
// inTelnet = new BufferedReader(new InputStreamReader(socket.getInputStream()));
|
||||
// } catch (IOException e) {
|
||||
// throw new RuntimeException(e);
|
||||
// }
|
||||
// System.out.println("[DXCCtrl, info:] DXCluster Controller created!");
|
||||
// this.clientSocket = socket;
|
||||
// }
|
||||
//
|
||||
// public DXClusterController(Socket clientSocket, ObjectOutputStream objectout, ChatController chatController) {
|
||||
//
|
||||
// try {
|
||||
// socket = clientSocket;
|
||||
// } catch (Exception e) {
|
||||
// throw new RuntimeException(e);
|
||||
// }
|
||||
// this.client = chatController;
|
||||
//
|
||||
// try {
|
||||
// outTelnet = new PrintWriter(socket.getOutputStream(), true);
|
||||
// inTelnet = new BufferedReader(new InputStreamReader(socket.getInputStream()));
|
||||
// } catch (IOException e) {
|
||||
// throw new RuntimeException(e);
|
||||
// }
|
||||
// System.out.println("[DXCCtrl, info:] DXCluster Controller created!");
|
||||
// this.clientSocket = socket;
|
||||
// }
|
||||
//
|
||||
//// public DXClusterController(ServerSocket clientSocket, ChatController client) throws InterruptedException {
|
||||
//// //TODO: GOT FROM https://stackoverflow.com/questions/15541804/creating-the-serversocket-in-a-separate-thread
|
||||
//// try {
|
||||
//// socket = clientSocket.accept(2);
|
||||
//// } catch (IOException e) {
|
||||
//// throw new RuntimeException(e);
|
||||
//// }
|
||||
//// this.client = client;
|
||||
////
|
||||
//// try {
|
||||
//// outTelnet = new PrintWriter(socket.getOutputStream(), true);
|
||||
//// inTelnet = new BufferedReader(new InputStreamReader(socket.getInputStream()));
|
||||
//// } catch (IOException e) {
|
||||
//// throw new RuntimeException(e);
|
||||
//// }
|
||||
//// System.out.println("defcons");
|
||||
//// this.clientSocket = socket;
|
||||
////
|
||||
//// }
|
||||
//
|
||||
//// public DXClusterController(ServerSocketChannel serverSocketChannel, ChatController client) throws InterruptedException {
|
||||
////
|
||||
//// this.client = client;
|
||||
////
|
||||
////// clientSocketChannel.ac
|
||||
////
|
||||
//// try {
|
||||
////
|
||||
//// serverSocketChannel.accept();
|
||||
//// serverSocketChannel.rea
|
||||
////
|
||||
//// outTelnet = new PrintWriter(clientSocket.getOutputStream(), true);
|
||||
//// inTelnet = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
|
||||
//// } catch (IOException e) {
|
||||
//// throw new RuntimeException(e);
|
||||
//// }
|
||||
//// System.out.println("defcons");
|
||||
//// this.clientSocket = clientSocket;
|
||||
////
|
||||
//// }
|
||||
//
|
||||
// public boolean terminateConnection() throws IOException {
|
||||
//
|
||||
//// this.output.close();
|
||||
// this.socket.close();
|
||||
//
|
||||
// return true;
|
||||
// }
|
||||
//
|
||||
// public void sendLocalClusterMessage() {
|
||||
//
|
||||
// }
|
||||
//
|
||||
// public void run() {
|
||||
//// try (
|
||||
//// PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
|
||||
//// BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream())))
|
||||
//// {
|
||||
//
|
||||
//// out.println("Welcome to the Telnet Server");
|
||||
// outTelnet.print("login: ");
|
||||
// outTelnet.flush();
|
||||
// try {
|
||||
// String user = inTelnet.readLine();
|
||||
// } catch (IOException ex) {
|
||||
// throw new RuntimeException(ex);
|
||||
// }
|
||||
//// finally {
|
||||
//// try {
|
||||
//// clientSocket.close();
|
||||
//// } catch (Exception e) {
|
||||
//// System.out.println("Error closing client socket: " + e.getMessage());
|
||||
//// }
|
||||
//// }
|
||||
//
|
||||
//
|
||||
//// for (int i = 0; i < 10; i++) {
|
||||
////
|
||||
//// outTelnet.println("DX de DM5M: 144222.0 DO5AMF JN49FL 2250Z\n");
|
||||
//// }
|
||||
//
|
||||
//
|
||||
//
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * Sends a DX cluster message to the connected log programs via telnet, returns true if sent
|
||||
// *
|
||||
// * @param aChatMember
|
||||
// * @return
|
||||
// */
|
||||
// public boolean propagateSingleDXClusterEntry(ChatMember aChatMember) {
|
||||
//
|
||||
// String singleDXClusterMessage = "DX de ";
|
||||
//
|
||||
// singleDXClusterMessage += client.getChatPreferences().getLoginCallSign() + " ";
|
||||
// singleDXClusterMessage += aChatMember.getFrequency().getValue() + " ";
|
||||
// singleDXClusterMessage += aChatMember.getCallSign().toUpperCase() + " ";
|
||||
// singleDXClusterMessage += aChatMember.getQra().toUpperCase() + " ";
|
||||
// singleDXClusterMessage += new Utils4KST().time_generateCurrenthhmmZTimeStringForClusterMessage() + "\n";
|
||||
//
|
||||
// outTelnet.println(singleDXClusterMessage);
|
||||
// outTelnet.flush();
|
||||
// return true;
|
||||
// }
|
||||
//}
|
||||
@@ -0,0 +1,237 @@
|
||||
package kst4contest.controller;
|
||||
|
||||
import kst4contest.model.ChatMember;
|
||||
import kst4contest.model.ChatPreferences;
|
||||
import kst4contest.model.ThreadStateMessage;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
public class DXClusterThreadPooledServer implements Runnable{
|
||||
|
||||
private List<Socket> clientSockets = Collections.synchronizedList(new ArrayList<>()); //list of all connected clients
|
||||
|
||||
private ThreadStatusCallback callBackToController;
|
||||
private String ThreadNickName = "DXCluster-Server";
|
||||
ChatController chatController = null;
|
||||
protected int serverPort = 8080;
|
||||
protected ServerSocket serverSocket = null;
|
||||
protected boolean isStopped = false;
|
||||
protected Thread runningThread= null;
|
||||
protected ExecutorService threadPool =
|
||||
Executors.newFixedThreadPool(10);
|
||||
Socket clientSocket;
|
||||
|
||||
public DXClusterThreadPooledServer(int port, ChatController chatController, ThreadStatusCallback callback){
|
||||
this.serverPort = port;
|
||||
this.chatController = chatController;
|
||||
this.callBackToController = callback;
|
||||
}
|
||||
|
||||
public void run(){
|
||||
|
||||
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "initialized", false);
|
||||
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
|
||||
|
||||
synchronized(this){
|
||||
this.runningThread = Thread.currentThread();
|
||||
runningThread.setName("DXCluster-thread-pooled-server");
|
||||
}
|
||||
openServerSocket();
|
||||
while(! isStopped()){
|
||||
clientSocket = null;
|
||||
try {
|
||||
clientSocket = this.serverSocket.accept();
|
||||
|
||||
synchronized(clientSockets) {
|
||||
clientSockets.add(clientSocket); // add dx cluster client to the "clients list" for broadcasting
|
||||
}
|
||||
|
||||
} catch (IOException e) {
|
||||
if(isStopped()) {
|
||||
System.out.println("Server Stopped.") ;
|
||||
break;
|
||||
}
|
||||
throw new RuntimeException(
|
||||
"Error accepting client connection", e);
|
||||
}
|
||||
|
||||
DXClusterServerWorkerRunnable worker = new DXClusterServerWorkerRunnable(clientSocket, "Thread Pooled DXCluster Server ", chatController, clientSockets, chatController);
|
||||
|
||||
this.threadPool.execute(worker);
|
||||
|
||||
}
|
||||
this.threadPool.shutdown();
|
||||
System.out.println("Server Stopped.") ;
|
||||
}
|
||||
|
||||
private synchronized boolean isStopped() {
|
||||
return this.isStopped;
|
||||
}
|
||||
|
||||
public synchronized void stop(){
|
||||
this.isStopped = true;
|
||||
try {
|
||||
this.serverSocket.close();
|
||||
synchronized(clientSockets) {
|
||||
for (Socket socket : clientSockets) {
|
||||
socket.close(); // close all client connections
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("DXCCSERVER Error closing server", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void openServerSocket() {
|
||||
try {
|
||||
this.serverSocket = new ServerSocket(this.serverPort);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("DXCCSERVER Cannot open port ", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a DX cluster message to ALL connected log programs via telnet, returns true if sent
|
||||
*
|
||||
* @param aChatMember
|
||||
* @return boolean true if message had been sent
|
||||
*/
|
||||
public boolean broadcastSingleDXClusterEntryToLoggers(ChatMember aChatMember) {
|
||||
synchronized(clientSockets) {
|
||||
|
||||
System.out.println("DXClusterSrvr: broadcasting message to clients: " + clientSockets.size());
|
||||
|
||||
try {
|
||||
|
||||
System.out.println("-------------> ORIGINALEE VAL: " + aChatMember.getFrequency().getValue());
|
||||
System.out.println("-------------> NORMALIZED VAL: " + Utils4KST.normalizeFrequencyString(aChatMember.getFrequency().getValue(), chatController.getChatPreferences().getNotify_optionalFrequencyPrefix()) + " ");
|
||||
} catch (Exception e) {
|
||||
System.out.println("DXCThPooledServer: Error accessing value in chatmember object: " + e.getMessage());
|
||||
// e.printStackTrace();
|
||||
}
|
||||
|
||||
for (Socket socket : clientSockets) {
|
||||
|
||||
try {
|
||||
|
||||
OutputStream output = socket.getOutputStream();
|
||||
|
||||
String singleDXClusterMessage = "DX de ";
|
||||
|
||||
// singleDXClusterMessage += chatController.getChatPreferences().getLoginCallSign() + ": ";
|
||||
|
||||
|
||||
|
||||
|
||||
singleDXClusterMessage += this.chatController.getChatPreferences().getNotify_DXCSrv_SpottersCallSign().getValue() + ": ";
|
||||
singleDXClusterMessage += Utils4KST.normalizeFrequencyString(aChatMember.getFrequency().getValue(), chatController.getChatPreferences().getNotify_optionalFrequencyPrefix()) + " ";
|
||||
singleDXClusterMessage += aChatMember.getCallSign().toUpperCase() + " "; //we need such an amount of spaces for n1mm to work, otherwise bullshit happens
|
||||
singleDXClusterMessage += aChatMember.getQra().toUpperCase() + " ";
|
||||
singleDXClusterMessage += new Utils4KST().time_generateCurrenthhmmZTimeStringForClusterMessage() + ((char)7) + ((char)7) + "\r\n";
|
||||
|
||||
// singleDXClusterMessage += chatController.getChatPreferences().getLoginCallSign() + ": ";
|
||||
// singleDXClusterMessage += Utils4KST.normalizeFrequencyString(aChatMember.getFrequency().getValue(), chatController.getChatPreferences().getNotify_optionalFrequencyPrefix()) + " ";
|
||||
// singleDXClusterMessage += aChatMember.getCallSign().toUpperCase() + " ";
|
||||
// singleDXClusterMessage += aChatMember.getQra().toUpperCase() + " ";
|
||||
// singleDXClusterMessage += new Utils4KST().time_generateCurrenthhmmZTimeStringForClusterMessage() + ((char)7) + ((char)7) + "\r\n";
|
||||
|
||||
output.write((singleDXClusterMessage).getBytes());
|
||||
|
||||
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "Last msg to " + clientSockets.size() + " Cluster Clients:\n" + singleDXClusterMessage, false);
|
||||
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
|
||||
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
System.out.println("[DXClusterSrvr, Error:] broadcasting DXC-message to clients went wrong!");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true; //if message had been sent, return true for "ok"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class DXClusterServerWorkerRunnable implements Runnable{
|
||||
|
||||
protected Socket clientSocket = null;
|
||||
protected String serverText = null;
|
||||
private ChatController client = null;
|
||||
private List<Socket> dxClusterClientSocketsConnectedList;
|
||||
private ThreadStatusCallback callBackToController;
|
||||
private String ThreadNickName = "DXCluster-Server";
|
||||
|
||||
public DXClusterServerWorkerRunnable(Socket clientSocket, String serverText, ChatController chatController, List<Socket> clientSockets, ThreadStatusCallback callback) {
|
||||
this.clientSocket = clientSocket;
|
||||
this.serverText = serverText;
|
||||
this.client = chatController;
|
||||
this.dxClusterClientSocketsConnectedList = clientSockets;
|
||||
this.callBackToController = callback;
|
||||
|
||||
}
|
||||
|
||||
public void run() {
|
||||
try {
|
||||
OutputStream output = clientSocket.getOutputStream();
|
||||
dxClusterClientSocketsConnectedList.add(clientSocket);
|
||||
|
||||
Timer dXCkeepAliveTimer = new Timer();
|
||||
dXCkeepAliveTimer.schedule(new TimerTask() {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
StringBuilder connectedClients = new StringBuilder(); //only for statistics
|
||||
|
||||
for (Socket socket : dxClusterClientSocketsConnectedList) {
|
||||
|
||||
connectedClients.append(socket.getInetAddress()).append("\n");
|
||||
|
||||
try {
|
||||
OutputStream output = socket.getOutputStream();
|
||||
output.write(("\r\n").getBytes());
|
||||
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
System.out.println("[DXClusterSrvr, Error:] broadcasting DXC-message to clients went wrong!");
|
||||
dXCkeepAliveTimer.purge();
|
||||
|
||||
try {
|
||||
socket.close();
|
||||
} catch (IOException ex) {
|
||||
ex.printStackTrace();
|
||||
}
|
||||
finally {
|
||||
this.cancel();
|
||||
}
|
||||
dxClusterClientSocketsConnectedList.remove(socket); //if socket is closed by client, remove it from the broadcast list and close it
|
||||
}
|
||||
}
|
||||
|
||||
// ThreadStateMessage threadStateMessage = new ThreadStateMessage(ThreadNickName, true, "Connected clients: " + connectedClients.toString(), false);
|
||||
// callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
|
||||
|
||||
}
|
||||
}, 30000, 30000);
|
||||
|
||||
|
||||
output.write(("login: ").getBytes()); //say hello to the client, it will answer with a callsign
|
||||
System.out.println("[DXClusterThreadPooledServer, Info:] New cluster client connected! "); //TODO: maybe integrate non blocking reader for client identification
|
||||
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
synchronized(dxClusterClientSocketsConnectedList) {
|
||||
dxClusterClientSocketsConnectedList.remove(clientSocket); // Entferne den Client nach Verarbeitung
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package kst4contest.controller;
|
||||
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import kst4contest.model.ChatMember;
|
||||
import kst4contest.model.ChatPreferences;
|
||||
|
||||
public class DXClusterThreadPooledServerTest {
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
ChatController client = new ChatController();
|
||||
ChatPreferences testPreferences = new ChatPreferences();
|
||||
testPreferences.setStn_loginCallSign("DM5M");
|
||||
|
||||
client.setChatPreferences(testPreferences);
|
||||
DXClusterThreadPooledServer dxClusterServer = new DXClusterThreadPooledServer(8000, client, client);
|
||||
|
||||
new Thread(dxClusterServer).start();
|
||||
|
||||
|
||||
try {
|
||||
Thread.sleep(10 * 1000);
|
||||
System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>ready.....go!");
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
ChatMember test = new ChatMember();
|
||||
test.setCallSign("DL5ASG");
|
||||
test.setQra("JO51HK");
|
||||
test.setFrequency(new SimpleStringProperty("144776.0"));
|
||||
|
||||
dxClusterServer.broadcastSingleDXClusterEntryToLoggers(test);
|
||||
|
||||
|
||||
// try {
|
||||
// Thread.sleep(20 * 3333);
|
||||
// } catch (InterruptedException e) {
|
||||
// e.printStackTrace();
|
||||
// }
|
||||
// System.out.println("Stopping Server");
|
||||
// server.stop();
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,7 @@ public class InputReaderThread extends Thread {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
ownMSG.setMessageText("MSG|" + this.client.getCategory().getCategoryNumber() + "|0|" + sendThisMessage23001 + "|0|");
|
||||
ownMSG.setMessageText("MSG|" + this.client.getChatCategoryMain().getCategoryNumber() + "|0|" + sendThisMessage23001 + "|0|");
|
||||
|
||||
// System.out.println("inreader " + ownMSG.getMessage() + client.getMessageTXBus().size());
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
237
src/main/java/kst4contest/controller/PstRotatorClient.java
Normal file
237
src/main/java/kst4contest/controller/PstRotatorClient.java
Normal file
@@ -0,0 +1,237 @@
|
||||
package kst4contest.controller;
|
||||
|
||||
import kst4contest.controller.interfaces.PstRotatorEventListener;
|
||||
import kst4contest.model.ThreadStateMessage;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
public class PstRotatorClient implements Runnable {
|
||||
|
||||
private ThreadStatusCallback callBackToController;
|
||||
private String ThreadNickName = "PSTRotator";
|
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(PstRotatorClient.class.getName());
|
||||
private static final int BUFFER_SIZE = 1024;
|
||||
|
||||
// Konfiguration
|
||||
private final String host;
|
||||
private final int remotePort; // Port, auf dem PSTRotator hört (z.B. 12060)
|
||||
private final int localPort; // Port, auf dem wir hören (z.B. 12061)
|
||||
|
||||
private DatagramSocket socket;
|
||||
private volatile boolean running = false;
|
||||
private PstRotatorEventListener listener;
|
||||
|
||||
// Executor für Polling (Status-Abfrage)
|
||||
private ScheduledExecutorService poller;
|
||||
|
||||
/**
|
||||
* Konstruktor
|
||||
* @param host IP Adresse von PSTRotator (meist "127.0.0.1")
|
||||
* @param remotePort Der Port, der in PSTRotator eingestellt ist (User-Wunsch: 12060)
|
||||
* @param listener Callback für den Chatcontroller
|
||||
*/
|
||||
public PstRotatorClient(String host, int remotePort, PstRotatorEventListener listener, ThreadStatusCallback callBack) {
|
||||
this.callBackToController = callBack;
|
||||
this.host = host;
|
||||
this.remotePort = remotePort;
|
||||
// Laut Manual antwortet PSTRotator oft auf Port+1.
|
||||
// Wir binden uns also standardmäßig auf remotePort + 1.
|
||||
this.localPort = remotePort + 1;
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* alternative constructor for seting the remote port explicitely
|
||||
*/
|
||||
public PstRotatorClient(String host, int remotePort, int localPort, PstRotatorEventListener listener) {
|
||||
this.host = host;
|
||||
this.remotePort = remotePort;
|
||||
this.localPort = localPort;
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Startet den Empfangs-Thread und das Polling
|
||||
*/
|
||||
public void start() {
|
||||
try {
|
||||
|
||||
// Socket binden
|
||||
// socket = new DatagramSocket(null);
|
||||
// socket.setReuseAddress(true);
|
||||
// socket = new DatagramSocket(localPort);
|
||||
//
|
||||
socket = new DatagramSocket(null);
|
||||
socket.setReuseAddress(true);
|
||||
socket.bind(new InetSocketAddress(localPort)); //bind to port
|
||||
|
||||
running = true;
|
||||
|
||||
// 1. Empfangs-Thread starten (dieses Runnable)
|
||||
Thread thread = new Thread(this, "PSTRotator-Listener-" + remotePort);
|
||||
thread.start();
|
||||
|
||||
// 2. Polling starten (z.B. alle 2 Sekunden Status abfragen)
|
||||
poller = Executors.newSingleThreadScheduledExecutor();
|
||||
poller.scheduleAtFixedRate(this::pollStatus, 1, 2, TimeUnit.SECONDS);
|
||||
|
||||
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, running, "initialized", false);
|
||||
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
|
||||
LOGGER.info("PstRotatorClient started. Remote: " + remotePort + ", Local: " + localPort);
|
||||
|
||||
} catch (SocketException e) {
|
||||
LOGGER.log(Level.SEVERE, "Fehler beim Öffnen des UDP Sockets", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stopping threads and closing sockets of pstRotator communicator
|
||||
*/
|
||||
public void stop() {
|
||||
running = false;
|
||||
if (poller != null && !poller.isShutdown()) {
|
||||
poller.shutdownNow();
|
||||
}
|
||||
if (socket != null && !socket.isClosed()) {
|
||||
socket.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main loop in thread which listens fpr PSTrotator packets
|
||||
*/
|
||||
@Override
|
||||
public void run() {
|
||||
byte[] buffer = new byte[BUFFER_SIZE];
|
||||
|
||||
while (running && !socket.isClosed()) {
|
||||
try {
|
||||
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
|
||||
socket.receive(packet); // Blockiert bis Daten kommen
|
||||
|
||||
String received = new String(packet.getData(), 0, packet.getLength(), StandardCharsets.US_ASCII).trim();
|
||||
|
||||
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "received line\n" + received, false);
|
||||
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
|
||||
|
||||
parseResponse(received);
|
||||
|
||||
} catch (IOException e) {
|
||||
if (running) {
|
||||
LOGGER.log(Level.WARNING, "Fehler beim Empfangen des Pakets", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* parses a pst rotatpr message to fit the PST listener interface
|
||||
* @param msg
|
||||
*/
|
||||
|
||||
private void parseResponse(String msg) {
|
||||
// Debug
|
||||
if (listener != null) listener.onMessageReceived(msg);
|
||||
|
||||
// Example answer: "AZ:145.0<CR>", "EL:010.0<CR>", "MODE:1<CR>"
|
||||
msg = msg.replace("<CR>", "").trim();
|
||||
|
||||
try {
|
||||
if (msg.startsWith("AZ:")) {
|
||||
String val = msg.substring(3);
|
||||
if (listener != null) listener.onAzimuthUpdate(Double.parseDouble(val));
|
||||
}
|
||||
else if (msg.startsWith("EL:")) {
|
||||
String val = msg.substring(3);
|
||||
if (listener != null) listener.onElevationUpdate(Double.parseDouble(val));
|
||||
}
|
||||
else if (msg.startsWith("MODE:")) {
|
||||
// MODE:1 = Tracking, MODE:0 = Manual
|
||||
String val = msg.substring(5);
|
||||
boolean tracking = "1".equals(val);
|
||||
if (listener != null) listener.onModeUpdate(tracking);
|
||||
}
|
||||
else if (msg.startsWith("OK:")) {
|
||||
// Bestätigung von Befehlen, z.B. OK:STOP:1
|
||||
LOGGER.fine("Befehl bestätigt: " + msg);
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
LOGGER.warning("Konnte Wert nicht parsen: " + msg);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Sende Methoden (API für den Chatcontroller) ---
|
||||
|
||||
private void sendUdp(String message) {
|
||||
if (socket == null || socket.isClosed()) return;
|
||||
|
||||
try {
|
||||
byte[] data = message.getBytes(StandardCharsets.US_ASCII);
|
||||
InetAddress address = InetAddress.getByName(host);
|
||||
DatagramPacket packet = new DatagramPacket(data, data.length, address, remotePort);
|
||||
socket.send(packet);
|
||||
} catch (IOException e) {
|
||||
LOGGER.log(Level.SEVERE, "Fehler beim Senden an PstRotator", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sendet den generischen XML Befehl.
|
||||
* Bsp: <PST><AZIMUTH>85</AZIMUTH></PST>
|
||||
*/
|
||||
private void sendCommand(String tag, String value) {
|
||||
String xml = String.format("<PST><%s>%s</%s></PST>", tag, value, tag);
|
||||
System.out.println("PSTRotatorClient: sent: " + xml);
|
||||
sendUdp(xml);
|
||||
}
|
||||
|
||||
// Öffentliche Steuermethoden
|
||||
|
||||
public void setAzimuth(double degrees) {
|
||||
// Formatierung ohne unnötige Nachkommastellen, falls nötig
|
||||
sendCommand("AZIMUTH", String.valueOf((int) degrees));
|
||||
}
|
||||
|
||||
public void setElevation(double degrees) {
|
||||
sendCommand("ELEVATION", String.valueOf(degrees));
|
||||
}
|
||||
|
||||
public void stopRotor() {
|
||||
sendCommand("STOP", "1");
|
||||
}
|
||||
|
||||
public void park() {
|
||||
sendCommand("PARK", "1");
|
||||
}
|
||||
|
||||
public void setTrackingMode(boolean enable) {
|
||||
sendCommand("TRACK", enable ? "1" : "0");
|
||||
}
|
||||
|
||||
/**
|
||||
* Method for polling rotators status via PSTRotator software. Asks only for AZ value!<br/>
|
||||
* Scheduled in a fixed time by executor
|
||||
*/
|
||||
public void pollStatus() {
|
||||
// PSTRotator Dokumentation:
|
||||
// <PST>AZ?</PST>
|
||||
// <PST>EL?</PST>
|
||||
// <PST>MODE?</PST>
|
||||
|
||||
// Man kann mehrere Befehle in einem Paket senden
|
||||
String query = "<PST><AZ?></AZ?><EL?></EL?><MODE?></MODE?></PST>";
|
||||
// HINWEIS: Laut Doku ist die Syntax für Abfragen etwas anders: <PST>AZ?</PST>
|
||||
// Daher bauen wir den String manuell, da sendCommand Tags schließt.
|
||||
|
||||
sendUdp("<PST>AZ?</PST>");
|
||||
// sendUdp("<PST>EL?</PST>");
|
||||
sendUdp("<PST>MODE?</PST>");
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package kst4contest.controller;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import kst4contest.model.ChatMessage;
|
||||
|
||||
@@ -39,7 +40,7 @@ public class ReadThread extends Thread {
|
||||
|
||||
try {
|
||||
input = socket.getInputStream();
|
||||
reader = new BufferedReader(new InputStreamReader(input));
|
||||
reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
|
||||
|
||||
} catch (IOException ex) {
|
||||
System.out.println("Error getting input stream: " + ex.getMessage());
|
||||
@@ -91,48 +92,7 @@ public class ReadThread extends Thread {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
// try {
|
||||
// sleep(3000);
|
||||
// } catch (InterruptedException e) {
|
||||
// // TODO Auto-generated catch block
|
||||
// e.printStackTrace();
|
||||
// }
|
||||
|
||||
// try {
|
||||
// System.out.println("RDTH: try new socket");
|
||||
// this.client.getSocket().close();
|
||||
// this.client.getSocket().close();
|
||||
// this.client.setSocket(new Socket(this.client.getHostname(), this.client.getPort()));
|
||||
// socket.connect(new InetSocketAddress(this.client.getHostname(), this.client.getPort()));
|
||||
// System.out.println("[Readthread, Warning:] new socket connected? -> " + socket.isConnected());
|
||||
|
||||
// input = socket.getInputStream();
|
||||
// reader = new BufferedReader(new InputStreamReader(input));
|
||||
//
|
||||
// this.sleep(5000);
|
||||
// } catch (IOException | InterruptedException e2) {
|
||||
// // TODO Auto-generated catch block
|
||||
// System.out.println("fucktah");
|
||||
// e2.printStackTrace();
|
||||
// }
|
||||
// try {
|
||||
// sleep(2000);
|
||||
// } catch (InterruptedException e1) {
|
||||
// // TODO Auto-generated catch block
|
||||
// e1.printStackTrace();
|
||||
// }
|
||||
// try {
|
||||
// this.client.getSocket().close();
|
||||
// this.client.setSocket(new Socket(this.client.getHostname(), this.client.getPort()));
|
||||
// } catch (UnknownHostException e) {
|
||||
// // TODO Auto-generated catch block
|
||||
// e.printStackTrace();
|
||||
// } catch (IOException e) {
|
||||
// // TODO Auto-generated catch block
|
||||
// e.printStackTrace();
|
||||
// }
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
388
src/main/java/kst4contest/controller/ReadUDPByWintestThread.java
Normal file
388
src/main/java/kst4contest/controller/ReadUDPByWintestThread.java
Normal file
@@ -0,0 +1,388 @@
|
||||
package kst4contest.controller;
|
||||
|
||||
import kst4contest.ApplicationConstants;
|
||||
import kst4contest.model.ChatMember;
|
||||
import kst4contest.model.ThreadStateMessage;
|
||||
import kst4contest.view.GuiUtils;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.net.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class ReadUDPByWintestThread extends Thread {
|
||||
|
||||
private static final Pattern STATUS_TOKEN_PATTERN = Pattern.compile("\"([^\"]*)\"|(\\S+)");
|
||||
|
||||
private DatagramSocket socket;
|
||||
private ChatController client;
|
||||
|
||||
private volatile boolean running = true;
|
||||
|
||||
private int PORT = 9871; //default
|
||||
|
||||
private static final int BUFFER_SIZE = 4096;
|
||||
|
||||
private final Map<Integer, String> receivedQsos = new ConcurrentHashMap<>();
|
||||
private long lastPacketTime = 0;
|
||||
|
||||
private String myStation = "DO5AMF";
|
||||
|
||||
private String targetStation = "";
|
||||
private String stationID = "";
|
||||
private int lastKnownQso = 0;
|
||||
|
||||
private ThreadStatusCallback callBackToController;
|
||||
private String ThreadNickName = "Wintest-msg";
|
||||
|
||||
|
||||
public ReadUDPByWintestThread(ChatController client, ThreadStatusCallback callback) {
|
||||
|
||||
this.callBackToController = callback;
|
||||
this.client = client;
|
||||
this.myStation = client.getChatPreferences().getStn_loginCallSignRaw(); //callsign of the logging stn
|
||||
this.PORT = client.getChatPreferences().getLogsynch_wintestNetworkPort();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void interrupt() {
|
||||
running = false;
|
||||
if (socket != null && !socket.isClosed()) socket.close();
|
||||
super.interrupt();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "initialized", false);
|
||||
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
|
||||
Thread.currentThread().setName("ReadUDPByWintestThread");
|
||||
|
||||
byte[] buffer = new byte[BUFFER_SIZE];
|
||||
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
|
||||
|
||||
try {
|
||||
socket = new DatagramSocket(null); //first init with null, then make ready for reuse
|
||||
socket.setReuseAddress(true);
|
||||
// socket = new DatagramSocket(PORT);
|
||||
socket.bind(new InetSocketAddress(client.getChatPreferences().getLogsynch_wintestNetworkPort()));
|
||||
socket.setSoTimeout(3000);
|
||||
System.out.println("[WinTest UDP listener] started at port: " + PORT);
|
||||
} catch (SocketException e) {
|
||||
e.printStackTrace();
|
||||
return;
|
||||
}
|
||||
|
||||
while (running) {
|
||||
try {
|
||||
socket.receive(packet);
|
||||
String msg = new String(packet.getData(), 0, packet.getLength(), StandardCharsets.US_ASCII).trim();
|
||||
processWinTestMessage(msg);
|
||||
} catch (SocketTimeoutException e) {
|
||||
// checkForMissingQsos();
|
||||
} catch (IOException e) {
|
||||
//TODO: here is something to catch
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void processWinTestMessage(String msg) {
|
||||
// System.out.println("Wintest-Message received: " + msg);
|
||||
|
||||
lastPacketTime = System.currentTimeMillis();
|
||||
|
||||
if (msg.startsWith("HELLO:")) { //Client Signon of wintest
|
||||
parseHello(msg);
|
||||
try {
|
||||
// send_needqso();
|
||||
}catch (Exception e) {
|
||||
System.out.println("Error: ");
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
|
||||
} else if (msg.startsWith("ADDQSO:")) { //adding qso to wintest log
|
||||
try {
|
||||
|
||||
parseAddQso(msg);
|
||||
} catch (Exception e) {
|
||||
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "Parsing ERROR: " + Arrays.toString(e.getStackTrace()), true);
|
||||
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
|
||||
}
|
||||
|
||||
} else if (msg.startsWith("STATUS")) {
|
||||
parseStatus(msg);
|
||||
|
||||
} else if (msg.startsWith("IHAVE:")) { //periodical message of wintest, which qsos are in the log
|
||||
// parseIHave(msg); //TODO
|
||||
}
|
||||
|
||||
else if (msg.contains(ApplicationConstants.DISCONNECT_RDR_POISONPILL)) {
|
||||
System.out.println("ReadUdpByWintest, Info: got poison, now dieing....");
|
||||
socket.close();
|
||||
running = false;
|
||||
|
||||
}
|
||||
|
||||
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "message received\n" + msg, false);
|
||||
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* parsing of the hello message of wintest:
|
||||
* "HELLO: "STN1" "" 6667 130 "SLAVE" 1 0 1762201985"
|
||||
* @param msg
|
||||
*/
|
||||
private void parseHello(String msg) {
|
||||
try {
|
||||
String[] tokens = msg.split("\"");
|
||||
if (tokens.length >= 2) {
|
||||
targetStation = tokens[1];
|
||||
System.out.println("[WinTest rcv: found logger instance: " + targetStation);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
System.out.println("[WinTest] ERROR on HELLO-Parsing: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private byte util_calculateChecksum(byte[] bytes) {
|
||||
int sum = 0;
|
||||
for (byte b : bytes) sum += b;
|
||||
return (byte) ((sum | 0x80) & 0xFF);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse Win-Test STATUS packets and update own QRG from WT station.
|
||||
*
|
||||
* Parsing model (tokenized with quotes preserved):
|
||||
* parts[0] = "STATUS"
|
||||
* parts[1] = station name (example: "STN1")
|
||||
* parts[5] = val2 (used to derive mode: 1 => SSB, else CW)
|
||||
* parts[7] = frequency in 0.1 kHz units (example: 1443210 => 144321.0)
|
||||
*/
|
||||
private void parseStatus(String msg) {
|
||||
try {
|
||||
ArrayList<String> parts = new ArrayList<>();
|
||||
Matcher matcher = STATUS_TOKEN_PATTERN.matcher(msg);
|
||||
while (matcher.find()) {
|
||||
if (matcher.group(1) != null) {
|
||||
parts.add(matcher.group(1));
|
||||
} else {
|
||||
parts.add(matcher.group(2));
|
||||
}
|
||||
}
|
||||
|
||||
if (parts.size() < 8) {
|
||||
System.out.println("[WinTest] STATUS too short: " + msg);
|
||||
return;
|
||||
}
|
||||
|
||||
String stn = parts.get(1);
|
||||
String stationFilter = client.getChatPreferences().getLogsynch_wintestNetworkStationNameOfWintestClient1();
|
||||
if (stationFilter != null && !stationFilter.isBlank() && !stn.equalsIgnoreCase(stationFilter)) {
|
||||
return;
|
||||
}
|
||||
|
||||
String val2 = parts.get(5);
|
||||
String freqRaw = parts.get(7);
|
||||
double freqFloat = Integer.parseInt(freqRaw) / 10.0;
|
||||
|
||||
String mode;
|
||||
if ("1".equals(val2)) {
|
||||
mode = freqFloat > 10000.0 ? "usb" : "lsb";
|
||||
} else {
|
||||
mode = "cw";
|
||||
}
|
||||
|
||||
String formattedQRG = String.format(Locale.US, "%.1f", freqFloat);
|
||||
this.client.getChatPreferences().getMYQRGFirstCat().set(formattedQRG);
|
||||
|
||||
System.out.println("[WinTest STATUS] stn=" + stn + ", mode=" + mode + ", qrg=" + formattedQRG);
|
||||
} catch (Exception e) {
|
||||
System.out.println("[WinTest] STATUS parsing error: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// private void send_needqso() throws IOException {
|
||||
// String payload = String.format("NEEDQSO:\"%s\" \"%s\" \"%s\" %d %d?\0",
|
||||
// "DO5AMF", "STN1", stationID, 1, 9999);
|
||||
// InetAddress broadcast = InetAddress.getByName("255.255.255.255");
|
||||
// byte[] bytes = payload.getBytes(StandardCharsets.US_ASCII);
|
||||
// bytes[bytes.length - 2] = util_calculateChecksum((bytes));
|
||||
// socket.send(new DatagramPacket(bytes, bytes.length, broadcast, 9871));
|
||||
// }
|
||||
|
||||
// private void send_hello() throws IOException {
|
||||
// String payload = String.format("HELLO:\"%s\" \"%s\" \"%s\" %d %d?\0",
|
||||
// "DO5AMF", "", stationID, "SLAVE", 1, 14);
|
||||
// InetAddress broadcast = InetAddress.getByName("255.255.255.255");
|
||||
// byte[] bytes = payload.getBytes(StandardCharsets.US_ASCII);
|
||||
// bytes[bytes.length - 2] = util_calculateChecksum((bytes));
|
||||
// socket.send(new DatagramPacket(bytes, bytes.length, broadcast, 9871));
|
||||
// }
|
||||
|
||||
/**
|
||||
* Catches add-qso messages of wintest if a new qso gets into the log<br/>
|
||||
*
|
||||
* String is like this:<br/><br/>
|
||||
*ADDQSO: "STN1" "" "STN1" 1762202297 1440000 0 12 0 0 0 2 2 "DM2RN" "599" "599001" "JO51UM" "" "" 0 "" "" "" 44510
|
||||
*
|
||||
* ^^^^sentby<br/>
|
||||
* ^^^^^^^^^^time<br/>
|
||||
* ^^^^^^qrg<br/>
|
||||
* ^^band<br/>
|
||||
* ^^^^^callsign logged<br/>
|
||||
* stn-id ^^^^
|
||||
* @param msg
|
||||
*/
|
||||
private void parseAddQso(String msg) {
|
||||
|
||||
|
||||
ChatMember modifyThat = null;
|
||||
|
||||
try {
|
||||
// int qsoNumber = extractQsoNumber(msg);
|
||||
// receivedQsos.put(qsoNumber, msg);
|
||||
// lastKnownQso = Math.max(lastKnownQso, qsoNumber);
|
||||
String callSignCatched = msg.split("\"") [7];
|
||||
|
||||
ChatMember workedCall = new ChatMember();
|
||||
workedCall.setCallSign(callSignCatched);
|
||||
workedCall.setWorked(true); //its worked at this place, for sure!
|
||||
|
||||
ArrayList<Integer> markTheseChattersAsWorked = client.checkListForChatMemberIndexesByCallSign(workedCall);
|
||||
|
||||
String bandId;
|
||||
bandId = msg.split("\"")[6].split(" ")[4].trim();
|
||||
|
||||
switch (bandId) {
|
||||
case "10" -> workedCall.setWorked50(true);
|
||||
case "11" -> workedCall.setWorked70(true);
|
||||
case "12" -> workedCall.setWorked144(true);
|
||||
case "14" -> workedCall.setWorked432(true);
|
||||
case "16" -> workedCall.setWorked1240(true);
|
||||
case "17" -> workedCall.setWorked2300(true);
|
||||
case "18" -> workedCall.setWorked3400(true);
|
||||
case "19" -> workedCall.setWorked5600(true);
|
||||
case "20" -> workedCall.setWorked10G(true);
|
||||
case "21" -> workedCall.setWorked24G(true);
|
||||
case "22" -> workedCall.setWorked47G(true);
|
||||
case "23" -> workedCall.setWorked76G(true);
|
||||
default -> System.out.println("[WinTestUDPRcvr: warning] Unbekannte Band-ID: " + bandId);
|
||||
}
|
||||
|
||||
if (!markTheseChattersAsWorked.isEmpty()) {
|
||||
//Worked call is part of the current chatmember list
|
||||
|
||||
for (int index : markTheseChattersAsWorked) {
|
||||
//iterate through the logged in chatmembers callsigns and set the worked markers
|
||||
modifyThat = client.getLst_chatMemberList().get(index);
|
||||
|
||||
modifyThat.setWorked(true); //worked its for sure
|
||||
|
||||
if (workedCall.isWorked50()) {
|
||||
modifyThat.setWorked50(true);
|
||||
} else if (workedCall.isWorked70()) {
|
||||
modifyThat.setWorked70(true);
|
||||
} else if (workedCall.isWorked144()) {
|
||||
modifyThat.setWorked144(true);
|
||||
} else if (workedCall.isWorked432()) {
|
||||
modifyThat.setWorked432(true);
|
||||
} else if (workedCall.isWorked1240()) {
|
||||
modifyThat.setWorked1240(true);
|
||||
} else if (workedCall.isWorked2300()) {
|
||||
modifyThat.setWorked2300(true);
|
||||
} else if (workedCall.isWorked3400()) {
|
||||
modifyThat.setWorked3400(true);
|
||||
} else if (workedCall.isWorked5600()) {
|
||||
modifyThat.setWorked5600(true);
|
||||
} else if (workedCall.isWorked10G()) {
|
||||
modifyThat.setWorked10G(true);
|
||||
} else if (workedCall.isWorked24G()) {
|
||||
modifyThat.setWorked24G(true);
|
||||
} else if (workedCall.isWorked47G()) {
|
||||
modifyThat.setWorked47G(true);
|
||||
} else if (workedCall.isWorked76G()) {
|
||||
modifyThat.setWorked76G(true);
|
||||
} else {
|
||||
System.out.println("[WinTestUDPRcvr: warning] found no new worked-flag for this band: " + workedCall.getCallSignRaw() + bandId);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
GuiUtils.triggerGUIFilteredChatMemberListChange(client); //not clean at all
|
||||
|
||||
// trigger band-upgrade hint after log entry (Win-Test)
|
||||
try {
|
||||
client.onExternalLogEntryReceived(workedCall.getCallSignRaw());
|
||||
} catch (Exception e) {
|
||||
System.out.println("[WinTestUDPRcvr, warning]: band-upgrade hint failed: " + e.getMessage());
|
||||
}
|
||||
|
||||
} catch (Exception IllegalStateException) {
|
||||
//do nothing, as it works...
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
boolean isInChat = this.client.getDbHandler().updateWkdInfoOnChatMember(workedCall);
|
||||
// This will update the worked info on a worked chatmember. DBHandler will
|
||||
// check, if an entry at the db had been modified. If not, then the worked
|
||||
// station had not been stored. DBHandler will store the information then.
|
||||
if (!isInChat) {
|
||||
|
||||
workedCall.setName("unknown");
|
||||
workedCall.setQra("unknown");
|
||||
workedCall.setLastActivity(new Utils4KST().time_generateActualTimeInDateFormat());
|
||||
this.client.getDbHandler().storeChatMember(workedCall);
|
||||
}
|
||||
|
||||
File logUDPMessageToThisFile = new File(this.client.getChatPreferences()
|
||||
.getLogSynch_storeWorkedCallSignsFileNameUDPMessageBackup());
|
||||
|
||||
FileWriter fileWriterPersistUDPToFile = null;
|
||||
BufferedWriter bufwrtrRawMSGOut;
|
||||
|
||||
try {
|
||||
fileWriterPersistUDPToFile = new FileWriter(logUDPMessageToThisFile, true);
|
||||
|
||||
} catch (IOException e1) {
|
||||
e1.printStackTrace();
|
||||
}
|
||||
|
||||
bufwrtrRawMSGOut = new BufferedWriter(fileWriterPersistUDPToFile);
|
||||
|
||||
if (modifyThat != null) {
|
||||
bufwrtrRawMSGOut.write("\n" + modifyThat.toString());
|
||||
bufwrtrRawMSGOut.flush();
|
||||
bufwrtrRawMSGOut.close();
|
||||
|
||||
} else {
|
||||
bufwrtrRawMSGOut.write("\n" + workedCall.toString());
|
||||
bufwrtrRawMSGOut.flush();
|
||||
bufwrtrRawMSGOut.close();
|
||||
|
||||
}
|
||||
|
||||
|
||||
System.out.println("[WinTest, Info: Marking Chatmember as worked: " + workedCall.toString());
|
||||
|
||||
// markChatMemberAsWorked(call, band); //TODO
|
||||
|
||||
} catch (Exception e) {
|
||||
System.out.println("[WinTest] Fehler beim ADDQSO-Parsing: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package kst4contest.controller;
|
||||
|
||||
import kst4contest.model.ChatPreferences;
|
||||
|
||||
public class ReadUDPByWintestThreadTest {
|
||||
|
||||
public static void main(String[] args) {
|
||||
ChatController ctrl1 = new ChatController();
|
||||
|
||||
ChatPreferences prefs = new ChatPreferences();
|
||||
ctrl1.setChatPreferences(prefs);
|
||||
|
||||
// ReadUDPByWintestThread test = new ReadUDPByWintestThread(ctrl1);
|
||||
|
||||
|
||||
// test.run();
|
||||
}
|
||||
}
|
||||
@@ -2,13 +2,16 @@ package kst4contest.controller;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import kst4contest.ApplicationConstants;
|
||||
import kst4contest.model.AirPlane;
|
||||
import kst4contest.model.AirPlaneReflectionInfo;
|
||||
import kst4contest.model.ChatMember;
|
||||
import kst4contest.model.ThreadStateMessage;
|
||||
|
||||
/**
|
||||
* This thread is responsible for reading server's input and printing it to the
|
||||
@@ -23,13 +26,16 @@ public class ReadUDPbyAirScoutMessageThread extends Thread {
|
||||
private ChatController client;
|
||||
private int localPort;
|
||||
private String ASIdentificator, ChatClientIdentificator;
|
||||
|
||||
public ReadUDPbyAirScoutMessageThread(int localPort) {
|
||||
this.localPort = localPort;
|
||||
}
|
||||
private ThreadStatusCallback callBackToController;
|
||||
private String ThreadNickName = "AirScout msg";
|
||||
// public ReadUDPbyAirScoutMessageThread(int localPort) {
|
||||
// this.localPort = localPort;
|
||||
// }
|
||||
|
||||
public ReadUDPbyAirScoutMessageThread(int localPort, ChatController client, String ASIdentificator,
|
||||
String ChatClientIdentificator) {
|
||||
String ChatClientIdentificator, ThreadStatusCallback callback) {
|
||||
|
||||
this.callBackToController = callback;
|
||||
this.localPort = localPort;
|
||||
this.client = client;
|
||||
this.ASIdentificator = ASIdentificator;
|
||||
@@ -38,6 +44,7 @@ public class ReadUDPbyAirScoutMessageThread extends Thread {
|
||||
|
||||
@Override
|
||||
public void interrupt() {
|
||||
System.out.println("ReadUDP");
|
||||
super.interrupt();
|
||||
try {
|
||||
if (this.socket != null) {
|
||||
@@ -50,7 +57,13 @@ public class ReadUDPbyAirScoutMessageThread extends Thread {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void callThreadStateToUi (ThreadStateMessage threadStateMessage) {
|
||||
if (callBackToController != null) {
|
||||
//update the visual control of running thread
|
||||
callBackToController.onThreadStatus("AirScout", threadStateMessage);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void run() {
|
||||
Thread.currentThread().setName("ReadUDPByAirScoutThread");
|
||||
@@ -89,6 +102,12 @@ public class ReadUDPbyAirScoutMessageThread extends Thread {
|
||||
}
|
||||
|
||||
socket.receive(packet);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
} catch (SocketTimeoutException e2) {
|
||||
// this will catch the repeating Sockettimeoutexception...nothing to do
|
||||
// e2.printStackTrace();
|
||||
@@ -104,29 +123,44 @@ public class ReadUDPbyAirScoutMessageThread extends Thread {
|
||||
String received = new String(packet.getData(), packet.getOffset(), packet.getLength());
|
||||
received = received.trim();
|
||||
|
||||
if (received.contains(ApplicationConstants.DISCONNECT_RDR_POISONPILL)) {
|
||||
System.out.println("ReadUdpByASMsgTh, Info: got poison, now dieing....");
|
||||
try {
|
||||
terminateConnection();
|
||||
} catch (Exception e) {
|
||||
System.out.println("ASUDPRDR: catched error " + e.getMessage());
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
if (received.contains("ASSETPATH") || received.contains("ASWATCHLIST")) {
|
||||
// do nothing, that is your own message
|
||||
} else if (received.contains("ASNEAREST:")) { //answer by airscout
|
||||
processASUDPMessage(received);
|
||||
|
||||
// System.out.println("[ReadUSPASTh, info:] received AS String " + received);
|
||||
// processASUDPMessage(received); //TODO: 2025-11-Zeile deaktiviert. Fand hier Doppelberechnung statt?!
|
||||
|
||||
AirPlaneReflectionInfo apReflectInfoForChatMember;
|
||||
|
||||
apReflectInfoForChatMember = processASUDPMessage(received);
|
||||
if (this.client.getLst_chatMemberList().size() != 0) {
|
||||
if (!this.client.getLst_chatMemberList().isEmpty()) {
|
||||
|
||||
try {
|
||||
|
||||
// if (this.client.checkListForChatMemberIndexByCallSign(apReflectInfoForChatMember.getReceiver()) != -1) {
|
||||
// this.client.getLst_chatMemberList()
|
||||
// .get(this.client.checkListForChatMemberIndexByCallSign(
|
||||
// apReflectInfoForChatMember.getReceiver()))
|
||||
// .setAirPlaneReflectInfo(apReflectInfoForChatMember); // TODO: here we set the ap info at
|
||||
// // the central instance of
|
||||
// // chatmember list .... -1 is a
|
||||
// // problem!
|
||||
|
||||
ArrayList<Integer> addApInfoToThese = this.client.checkListForChatMemberIndexesByCallSign(apReflectInfoForChatMember.getReceiver());
|
||||
addApInfoToThese.forEach((integerIndex) -> {this.client.getLst_chatMemberList().get(integerIndex).setAirPlaneReflectInfo(apReflectInfoForChatMember); });
|
||||
|
||||
// AirScout availability strongly affects priority => request recompute the score of the chatmember
|
||||
this.client.getScoreService().requestRecompute("airscout-update");
|
||||
|
||||
this.client.getLst_chatMemberList()
|
||||
.get(this.client.checkListForChatMemberIndexByCallSign(
|
||||
apReflectInfoForChatMember.getReceiver()))
|
||||
.setAirPlaneReflectInfo(apReflectInfoForChatMember); // TODO: here we set the ap info at
|
||||
// the central instance of
|
||||
// chatmember list .... -1 is a
|
||||
// problem!
|
||||
/**
|
||||
* CK| MSGBUS BGFX Listactualizer Exception in thread "Thread-10"
|
||||
* java.util.ConcurrentModificationException at
|
||||
@@ -137,6 +171,7 @@ public class ReadUDPbyAirScoutMessageThread extends Thread {
|
||||
* kst4contest.controller.ReadUDPbyAirScoutMessageThread.run(ReadUDPbyAirScoutMessageThread.java:93)
|
||||
*
|
||||
*/
|
||||
// System.out.println("[ReadUdpByASth, AP-Info catched: ] " + apReflectInfoForChatMember.toString());
|
||||
// }
|
||||
} catch (Exception e) {
|
||||
|
||||
@@ -146,6 +181,13 @@ public class ReadUDPbyAirScoutMessageThread extends Thread {
|
||||
// TODO: handle exception
|
||||
}
|
||||
|
||||
// String[] newState = new String[3];
|
||||
// newState[0] = "On";
|
||||
// newState[1] = "received line";
|
||||
// newState[2] = apReflectInfoForChatMember.toString();
|
||||
// callThreadStateToUi(newState);
|
||||
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "received line\n" + apReflectInfoForChatMember.toString(), false);
|
||||
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -261,9 +303,13 @@ public class ReadUDPbyAirScoutMessageThread extends Thread {
|
||||
return apInfo;
|
||||
}
|
||||
|
||||
public boolean terminateConnection() throws IOException {
|
||||
public boolean terminateConnection() {
|
||||
|
||||
this.socket.close();
|
||||
try {
|
||||
this.socket.close();
|
||||
} catch (Exception e) {
|
||||
System.out.println("udpbyas: catched " + e.getMessage());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -3,12 +3,17 @@ package kst4contest.controller;
|
||||
import java.io.*;
|
||||
import java.net.*;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
|
||||
import javax.xml.XMLConstants;
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
|
||||
import kst4contest.ApplicationConstants;
|
||||
import kst4contest.model.ThreadStateMessage;
|
||||
import kst4contest.view.GuiUtils;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.Node;
|
||||
@@ -29,13 +34,19 @@ public class ReadUDPbyUCXMessageThread extends Thread {
|
||||
private BufferedReader reader;
|
||||
private Socket socket;
|
||||
private ChatController client;
|
||||
private int udpPortNr = 12060;
|
||||
private ThreadStatusCallback callBackToController;
|
||||
private String ThreadNickName = "UDP-Log msg";
|
||||
|
||||
public ReadUDPbyUCXMessageThread(int localPort) {
|
||||
// public ReadUDPbyUCXMessageThread(int localPort , ThreadStatusCallback callback) {
|
||||
//
|
||||
//// this.callBackToController = callback;
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
public ReadUDPbyUCXMessageThread(int localPort, ChatController client) {
|
||||
this.client = client;
|
||||
public ReadUDPbyUCXMessageThread(int localPort, ChatController client, ThreadStatusCallback callback) {
|
||||
this.udpPortNr = localPort;
|
||||
this.client = client;
|
||||
this.callBackToController = callback;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -43,27 +54,35 @@ public class ReadUDPbyUCXMessageThread extends Thread {
|
||||
super.interrupt();
|
||||
try {
|
||||
if (this.socket != null) {
|
||||
|
||||
this.socket.close();
|
||||
System.out.println(">>>>>>>>>>>>>>ReadUdpbyUCS: closing socket");
|
||||
terminateConnection();
|
||||
// callBackToController.onThreadStatus("UDPReceiver", new String[]);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
System.out.println("UCXUDPRDR: catched error " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public void run() {
|
||||
|
||||
System.out.println("ReadUDPByUCXLogThread: started Thread for UCXLog getUDP");
|
||||
Thread.currentThread().setName("ReadUDPByUCXLogThread");
|
||||
|
||||
|
||||
|
||||
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "initialized", false);
|
||||
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
|
||||
|
||||
DatagramSocket socket = null;
|
||||
boolean running;
|
||||
|
||||
boolean running;
|
||||
byte[] buf = new byte[1777];
|
||||
DatagramPacket packet = new DatagramPacket(buf, buf.length);
|
||||
|
||||
try {
|
||||
socket = new DatagramSocket(12060);
|
||||
socket.setSoTimeout(11000); //TODO try for end properly
|
||||
// socket = new DatagramSocket(12060);
|
||||
socket = new DatagramSocket(udpPortNr);
|
||||
socket.setSoTimeout(2000); //TODO try for end properly
|
||||
}
|
||||
|
||||
catch (SocketException e) {
|
||||
@@ -75,22 +94,45 @@ public class ReadUDPbyUCXMessageThread extends Thread {
|
||||
|
||||
boolean timeOutIndicator = false;
|
||||
|
||||
if (this.client.isDisconnectionPerformedByUser()) {
|
||||
break;//TODO: what if it´s not the finally closage but a band channel change?
|
||||
}
|
||||
// packet = new DatagramPacket(buf, buf.length); //TODO: Changed that due to memory leak, check if all works (seems like that)
|
||||
// DatagramPacket packet = new DatagramPacket(SRPDefinitions.BYTE_BUFFER_MAX_LENGTH); //TODO: Changed that due to memory leak, check if all works (seems like that)
|
||||
try {
|
||||
socket.receive(packet);
|
||||
|
||||
} catch (SocketTimeoutException e2) {
|
||||
|
||||
|
||||
timeOutIndicator = true;
|
||||
// this will catch the repeating Sockettimeoutexception...nothing to do
|
||||
// e2.printStackTrace();
|
||||
}
|
||||
}
|
||||
catch (IOException e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
} catch (NullPointerException nE) {
|
||||
// TODO Auto-generated catch block
|
||||
nE.printStackTrace();
|
||||
System.out.println("ReadUdpByUCXTH: Socket not ready");
|
||||
|
||||
try {
|
||||
socket = new DatagramSocket(client.getChatPreferences().getLogsynch_ucxUDPWkdCallListenerPort());
|
||||
socket.setSoTimeout(2000);
|
||||
} catch (SocketException e) {
|
||||
System.out.println("[ReadUDPByUCSMsgTH, Error]: socket in use or something:");
|
||||
e.printStackTrace();
|
||||
|
||||
try {
|
||||
socket = new DatagramSocket(null);
|
||||
socket.setReuseAddress(true);
|
||||
socket.bind(new InetSocketAddress(client.getChatPreferences().getLogsynch_ucxUDPWkdCallListenerPort()));
|
||||
socket.receive(packet);
|
||||
socket.setSoTimeout(3000);
|
||||
} catch (Exception ex) {
|
||||
System.out.println("ReadUDPByUCXMsgTh: Could not solve that. Program Restart needed.");
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
InetAddress address = packet.getAddress();
|
||||
@@ -99,9 +141,26 @@ public class ReadUDPbyUCXMessageThread extends Thread {
|
||||
String received = new String(packet.getData(), packet.getOffset(), packet.getLength());
|
||||
received = received.trim();
|
||||
|
||||
// System.out.println("recvudpucx");
|
||||
|
||||
|
||||
// System.out.println("<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<recv " + received);
|
||||
|
||||
if (received.contains(ApplicationConstants.DISCONNECT_RDR_POISONPILL)) {
|
||||
System.out.println("ReadUdpByUCX, Info: got poison, now dieing....");
|
||||
socket.close();
|
||||
timeOutIndicator = true;
|
||||
|
||||
// threadStatusMessage = new String[2];
|
||||
// threadStatusMessage[0] = "stopped";
|
||||
// threadStatusMessage[1] = "by poisonpill message (disconnect on purpose)";
|
||||
threadStateMessage = new ThreadStateMessage(this.ThreadNickName, false, "stopped by Poisonpill", false);
|
||||
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
|
||||
break;
|
||||
}
|
||||
|
||||
if (this.client.isDisconnectionPerformedByUser()) {
|
||||
break;//TODO: what if it´s not the finally closage but a band channel change?
|
||||
}
|
||||
|
||||
if (!timeOutIndicator) {
|
||||
processUCXUDPMessage(received);
|
||||
} else {
|
||||
@@ -122,7 +181,17 @@ public class ReadUDPbyUCXMessageThread extends Thread {
|
||||
|
||||
ChatMember modifyThat = null;
|
||||
|
||||
// System.out.println(udpMsg);
|
||||
// System.out.println("ReadUDPByUCX, message catched: " + udpMsg);
|
||||
|
||||
// String[] threadStatusMessage = new String[2];
|
||||
// threadStatusMessage = new String[3];
|
||||
// threadStatusMessage[0] = "on";
|
||||
// threadStatusMessage[1] = "received message:";
|
||||
// threadStatusMessage[2] = udpMsg;
|
||||
|
||||
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "received Message\n" + udpMsg, false);
|
||||
|
||||
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
|
||||
|
||||
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
|
||||
try {
|
||||
@@ -155,8 +224,12 @@ public class ReadUDPbyUCXMessageThread extends Thread {
|
||||
// call = call.toLowerCase();
|
||||
String band = element.getElementsByTagName("band").item(0).getTextContent();
|
||||
|
||||
String points = element.getElementsByTagName("points").item(0).getTextContent();
|
||||
|
||||
System.out.println("[Readudp, info ]: received Current Element :" + node.getNodeName()
|
||||
+ "call: " + call + " / " + band);
|
||||
+ "call: " + call + " / " + band + " ----> " + points + " POINTS");
|
||||
|
||||
// client.getChatPreferences().setBcn_contestScoreSum(Long.parseLong(points));
|
||||
|
||||
ChatMember workedCall = new ChatMember();
|
||||
workedCall.setCallSign(call);
|
||||
@@ -195,6 +268,44 @@ public class ReadUDPbyUCXMessageThread extends Thread {
|
||||
|
||||
case "10G": {
|
||||
workedCall.setWorked10G(true);
|
||||
break;
|
||||
}
|
||||
|
||||
/**
|
||||
* cases hotfix for MINOS logger, which tells band like "2m", not "144"
|
||||
*/
|
||||
case "2m": {
|
||||
workedCall.setWorked144(true);
|
||||
break;
|
||||
}
|
||||
|
||||
case "70cm": {
|
||||
workedCall.setWorked432(true);
|
||||
break;
|
||||
}
|
||||
|
||||
case "23cm": {
|
||||
workedCall.setWorked1240(true);
|
||||
break;
|
||||
}
|
||||
|
||||
case "13cm": {
|
||||
workedCall.setWorked2300(true);
|
||||
break;
|
||||
}
|
||||
|
||||
case "9cm": {
|
||||
workedCall.setWorked3400(true);
|
||||
break;
|
||||
}
|
||||
|
||||
case "6cm": {
|
||||
workedCall.setWorked5600(true);
|
||||
break;
|
||||
}
|
||||
|
||||
case "3cm": {
|
||||
workedCall.setWorked10G(true);
|
||||
|
||||
}
|
||||
|
||||
@@ -216,68 +327,135 @@ public class ReadUDPbyUCXMessageThread extends Thread {
|
||||
|
||||
// modifyThat = (ChatMember) client.getMap_ucxLogInfoWorkedCalls().get(call);
|
||||
|
||||
int indexOfChatMemberInTable = -1;
|
||||
indexOfChatMemberInTable = client.checkListForChatMemberIndexByCallSign(workedCall);
|
||||
// asd //TODO: Check if callsign and callsignraw is similar, then mark first and further via new checklistforchatmembermultiplemethod with array of indize
|
||||
|
||||
if (indexOfChatMemberInTable == -1) {
|
||||
// do nothing
|
||||
ArrayList<Integer> markTheseChattersAsWorked = client.checkListForChatMemberIndexesByCallSign(workedCall);
|
||||
|
||||
if (markTheseChattersAsWorked.isEmpty()) {
|
||||
//Worked call is not part of the chatmember list
|
||||
} else {
|
||||
modifyThat = client.getLst_chatMemberList().get(indexOfChatMemberInTable);
|
||||
// modifyThat.setWorked(true);
|
||||
|
||||
client.getLst_chatMemberList()
|
||||
.get(client.checkListForChatMemberIndexByCallSign(modifyThat)).setWorked(true);
|
||||
for (int index : markTheseChattersAsWorked) {
|
||||
modifyThat = client.getLst_chatMemberList().get(index);
|
||||
|
||||
if (workedCall.isWorked144()) {
|
||||
modifyThat.setWorked144(true);
|
||||
client.getLst_chatMemberList()
|
||||
.get(client.checkListForChatMemberIndexByCallSign(modifyThat))
|
||||
.setWorked144(true);
|
||||
modifyThat.setWorked(true);
|
||||
|
||||
} else if (workedCall.isWorked432()) {
|
||||
modifyThat.setWorked432(true);
|
||||
client.getLst_chatMemberList()
|
||||
.get(client.checkListForChatMemberIndexByCallSign(modifyThat))
|
||||
.setWorked432(true);
|
||||
if (workedCall.isWorked144()) {
|
||||
modifyThat.setWorked144(true);
|
||||
|
||||
} else if (workedCall.isWorked1240()) {
|
||||
modifyThat.setWorked1240(true);
|
||||
client.getLst_chatMemberList()
|
||||
.get(client.checkListForChatMemberIndexByCallSign(modifyThat))
|
||||
.setWorked1240(true);
|
||||
} else if (workedCall.isWorked432()) {
|
||||
modifyThat.setWorked432(true);
|
||||
|
||||
} else if (workedCall.isWorked2300()) {
|
||||
modifyThat.setWorked2300(true);
|
||||
client.getLst_chatMemberList()
|
||||
.get(client.checkListForChatMemberIndexByCallSign(modifyThat))
|
||||
.setWorked2300(true);
|
||||
} else if (workedCall.isWorked1240()) {
|
||||
modifyThat.setWorked1240(true);
|
||||
|
||||
} else if (workedCall.isWorked3400()) {
|
||||
modifyThat.setWorked3400(true);
|
||||
client.getLst_chatMemberList()
|
||||
.get(client.checkListForChatMemberIndexByCallSign(modifyThat))
|
||||
.setWorked3400(true);
|
||||
} else if (workedCall.isWorked2300()) {
|
||||
modifyThat.setWorked2300(true);
|
||||
|
||||
} else if (workedCall.isWorked5600()) {
|
||||
modifyThat.setWorked5600(true);
|
||||
client.getLst_chatMemberList()
|
||||
.get(client.checkListForChatMemberIndexByCallSign(modifyThat))
|
||||
.setWorked5600(true);
|
||||
} else if (workedCall.isWorked3400()) {
|
||||
modifyThat.setWorked3400(true);
|
||||
|
||||
} else if (workedCall.isWorked10G()) {
|
||||
modifyThat.setWorked10G(true);
|
||||
client.getLst_chatMemberList()
|
||||
.get(client.checkListForChatMemberIndexByCallSign(modifyThat))
|
||||
.setWorked10G(true);
|
||||
} else if (workedCall.isWorked5600()) {
|
||||
modifyThat.setWorked5600(true);
|
||||
|
||||
} else if (workedCall.isWorked10G()) {
|
||||
modifyThat.setWorked10G(true);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
GuiUtils.triggerGUIFilteredChatMemberListChange(this.client);
|
||||
// BEGIN PATCH: trigger band-upgrade hint after log entry (UCXLog)
|
||||
try {
|
||||
client.onExternalLogEntryReceived(workedCall.getCallSignRaw());
|
||||
} catch (Exception e) {
|
||||
System.out.println("[UCXUDPRcvr, warning]: band-upgrade hint failed: " + e.getMessage());
|
||||
}
|
||||
|
||||
|
||||
} catch (Exception IllegalStateException) {
|
||||
//do nothing, as it works...
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* old mechanic to markup worked stations in the chatmember table
|
||||
*/
|
||||
// int indexOfChatMemberInTable = -1; //chatmember not in table
|
||||
// indexOfChatMemberInTable = client.checkListForChatMemberIndexByCallSign(workedCall);
|
||||
//
|
||||
// if (indexOfChatMemberInTable == -1) {
|
||||
// // do nothing
|
||||
// } else {
|
||||
// modifyThat = client.getLst_chatMemberList().get(indexOfChatMemberInTable);
|
||||
//
|
||||
// client.getLst_chatMemberList()
|
||||
// .get(client.checkListForChatMemberIndexByCallSign(modifyThat)).setWorked(true);
|
||||
//
|
||||
// if (workedCall.isWorked144()) {
|
||||
// modifyThat.setWorked144(true);
|
||||
// client.getLst_chatMemberList()
|
||||
// .get(client.checkListForChatMemberIndexByCallSign(modifyThat))
|
||||
// .setWorked144(true);
|
||||
//
|
||||
// } else if (workedCall.isWorked432()) {
|
||||
// modifyThat.setWorked432(true);
|
||||
// client.getLst_chatMemberList()
|
||||
// .get(client.checkListForChatMemberIndexByCallSign(modifyThat))
|
||||
// .setWorked432(true);
|
||||
//
|
||||
// } else if (workedCall.isWorked1240()) {
|
||||
// modifyThat.setWorked1240(true);
|
||||
// client.getLst_chatMemberList()
|
||||
// .get(client.checkListForChatMemberIndexByCallSign(modifyThat))
|
||||
// .setWorked1240(true);
|
||||
//
|
||||
// } else if (workedCall.isWorked2300()) {
|
||||
// modifyThat.setWorked2300(true);
|
||||
// client.getLst_chatMemberList()
|
||||
// .get(client.checkListForChatMemberIndexByCallSign(modifyThat))
|
||||
// .setWorked2300(true);
|
||||
//
|
||||
// } else if (workedCall.isWorked3400()) {
|
||||
// modifyThat.setWorked3400(true);
|
||||
// client.getLst_chatMemberList()
|
||||
// .get(client.checkListForChatMemberIndexByCallSign(modifyThat))
|
||||
// .setWorked3400(true);
|
||||
//
|
||||
// } else if (workedCall.isWorked5600()) {
|
||||
// modifyThat.setWorked5600(true);
|
||||
// client.getLst_chatMemberList()
|
||||
// .get(client.checkListForChatMemberIndexByCallSign(modifyThat))
|
||||
// .setWorked5600(true);
|
||||
//
|
||||
// } else if (workedCall.isWorked10G()) {
|
||||
// modifyThat.setWorked10G(true);
|
||||
// client.getLst_chatMemberList()
|
||||
// .get(client.checkListForChatMemberIndexByCallSign(modifyThat))
|
||||
// .setWorked10G(true);
|
||||
// }
|
||||
/**
|
||||
* //TODO: following line is a quick fix to making disappear worked chatmembers of the list
|
||||
* Thats uncomfortable due to this also causes selection changes,
|
||||
* Better way is to change all worked and qrv values to observables and then trigger the underlying
|
||||
* list to fire an invalidationevent. Really Todo!
|
||||
*/
|
||||
// try{
|
||||
//
|
||||
// GuiUtils.triggerGUIFilteredChatMemberListChange(client); //not clean at all
|
||||
// } catch (Exception IllegalStateException) {
|
||||
// //do nothing, as it works...
|
||||
// }
|
||||
// }
|
||||
/**
|
||||
* end -> old mechanic to markup worked stations in the chatmember table
|
||||
*/
|
||||
}
|
||||
|
||||
boolean isInChat = this.client.getDbHandler().updateWkdInfoOnChatMember(workedCall);
|
||||
// This will update the worked info on a worked chatmember. DBHandler will
|
||||
// check, if an entry at the db had been modified. If not, then the worked
|
||||
// station had not been stored. DBHandler will store the informations then.
|
||||
// station had not been stored. DBHandler will store the information then.
|
||||
if (!isInChat) {
|
||||
|
||||
workedCall.setName("unknown");
|
||||
@@ -297,7 +475,6 @@ public class ReadUDPbyUCXMessageThread extends Thread {
|
||||
fileWriterPersistUDPToFile = new FileWriter(logUDPMessageToThisFile, true);
|
||||
|
||||
} catch (IOException e1) {
|
||||
// TODO Auto-generated catch block
|
||||
e1.printStackTrace();
|
||||
}
|
||||
|
||||
@@ -373,9 +550,9 @@ public class ReadUDPbyUCXMessageThread extends Thread {
|
||||
// System.out.println("Radio Mode: " + mode);
|
||||
// System.out.println("[ReadUDPFromUCX, Info:] Setted QRG pref to: \"" + qrg + "\"" );
|
||||
|
||||
this.client.getChatPreferences().getMYQRG().set(formattedQRG);
|
||||
this.client.getChatPreferences().getMYQRGFirstCat().set(formattedQRG);
|
||||
|
||||
System.out.println("[ReadUDPbyUCXTh: ] Radioinfo processed: " + formattedQRG);
|
||||
// System.out.println("[ReadUDPbyUCXTh: ] Radioinfo processed: " + formattedQRG);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -385,9 +562,27 @@ public class ReadUDPbyUCXMessageThread extends Thread {
|
||||
e.printStackTrace();
|
||||
System.out.println(e.getCause());
|
||||
System.out.println(e.getMessage());
|
||||
|
||||
// threadStatusMessage = new String[2];
|
||||
// threadStatusMessage[0] = "STOPPED";
|
||||
// threadStatusMessage[1] = Arrays.toString(e.getStackTrace());
|
||||
threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "CRASHED" + udpMsg, true);
|
||||
threadStateMessage.setCriticalStateFurtherInfo(Arrays.toString(e.getStackTrace()));
|
||||
|
||||
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
|
||||
|
||||
} catch (SQLException e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "CRASHED" + udpMsg, true);
|
||||
threadStateMessage.setCriticalStateFurtherInfo(Arrays.toString(e.getStackTrace()));
|
||||
|
||||
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
|
||||
|
||||
// threadStatusMessage = new String[2];
|
||||
// threadStatusMessage[0] = "STOPPED";
|
||||
// threadStatusMessage[1] = Arrays.toString(e.getStackTrace());
|
||||
// callBackToController.onThreadStatus(ThreadNickName,threadStatusMessage);
|
||||
}
|
||||
|
||||
// System.out.println("[ReadUDPbyUCXTh: ] worked size = " + this.client.getMap_ucxLogInfoWorkedCalls().size());
|
||||
@@ -397,6 +592,14 @@ public class ReadUDPbyUCXMessageThread extends Thread {
|
||||
}
|
||||
|
||||
public boolean terminateConnection() throws IOException {
|
||||
// String[] threadStatusMessage = new String[2];
|
||||
// threadStatusMessage = new String[2];
|
||||
// threadStatusMessage[0] = "STOPPED";
|
||||
// threadStatusMessage[1] = "Connection terminated for purpose.";
|
||||
// callBackToController.onThreadStatus(ThreadNickName,threadStatusMessage);
|
||||
|
||||
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, false, "terminated", false);
|
||||
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
|
||||
|
||||
this.socket.close();
|
||||
|
||||
|
||||
309
src/main/java/kst4contest/controller/ScoreService.java
Normal file
309
src/main/java/kst4contest/controller/ScoreService.java
Normal file
@@ -0,0 +1,309 @@
|
||||
package kst4contest.controller;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.LongProperty;
|
||||
import javafx.beans.property.ReadOnlyDoubleProperty;
|
||||
import javafx.beans.property.ReadOnlyDoubleWrapper;
|
||||
import javafx.beans.property.SimpleLongProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import kst4contest.logic.PriorityCalculator;
|
||||
import kst4contest.model.ChatCategory;
|
||||
import kst4contest.model.ChatMember;
|
||||
import kst4contest.model.ChatPreferences;
|
||||
import kst4contest.model.ContestSked;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
/**
|
||||
* Calculates priority scores off the JavaFX thread and publishes a small UI model.
|
||||
*
|
||||
* Design goals:
|
||||
* - No per-member Platform.runLater flooding.
|
||||
* - Score is computed once per callsignRaw (e.g. "SM6VTZ"), even if it exists in multiple chat categories.
|
||||
* - A routing hint (preferred ChatCategory) is kept using "last inbound category" if available.
|
||||
*/
|
||||
public final class ScoreService {
|
||||
|
||||
public static final int DEFAULT_TOP_N = 15; //how many top places we have?
|
||||
|
||||
/** Force a refresh at least every X ms (some scoring inputs are time dependent). */
|
||||
private static final long MAX_SNAPSHOT_AGE_MS = 10_000L;
|
||||
|
||||
private final ChatController controller;
|
||||
private final PriorityCalculator priorityCalculator;
|
||||
|
||||
private final AtomicBoolean recomputeRequested = new AtomicBoolean(true);
|
||||
private final AtomicReference<ScoreSnapshot> latestSnapshot = new AtomicReference<>(ScoreSnapshot.empty());
|
||||
|
||||
// UI outputs
|
||||
private final ObservableList<TopCandidate> topCandidatesFx = FXCollections.observableArrayList();
|
||||
private final ReadOnlyDoubleWrapper selectedCallPriorityScore = new ReadOnlyDoubleWrapper(Double.NaN);
|
||||
private final LongProperty uiPulse = new SimpleLongProperty(0);
|
||||
|
||||
private volatile String selectedCallSignRaw;
|
||||
private volatile long lastComputedEpochMs = 0L;
|
||||
private final int topN;
|
||||
|
||||
private final ObjectProperty<ChatMember> selectedChatMember = new SimpleObjectProperty<>(null);
|
||||
|
||||
|
||||
public ScoreService(ChatController controller, PriorityCalculator priorityCalculator, int topN) {
|
||||
this.controller = Objects.requireNonNull(controller, "controller");
|
||||
this.priorityCalculator = Objects.requireNonNull(priorityCalculator, "priorityCalculator");
|
||||
this.topN = topN > 0 ? topN : DEFAULT_TOP_N;
|
||||
}
|
||||
|
||||
public ObservableList<TopCandidate> getTopCandidatesFx() {
|
||||
return topCandidatesFx;
|
||||
}
|
||||
|
||||
public ReadOnlyDoubleProperty selectedCallPriorityScoreProperty() {
|
||||
return selectedCallPriorityScore.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
/**
|
||||
* A lightweight UI invalidation signal that increments after every published snapshot.
|
||||
* Consumers can refresh small panels (timeline/toplist), but should avoid refreshing huge tables.
|
||||
*/
|
||||
public LongProperty uiPulseProperty() {
|
||||
return uiPulse;
|
||||
}
|
||||
|
||||
public ScoreSnapshot getLatestSnapshot() {
|
||||
return latestSnapshot.get();
|
||||
}
|
||||
|
||||
/** Coalesced recompute request (safe to call frequently from other threads). */
|
||||
public void requestRecompute(String reason) {
|
||||
recomputeRequested.set(true);
|
||||
}
|
||||
|
||||
/** Called by UI when selection changes. */
|
||||
public void setSelectedChatMember(ChatMember member) {
|
||||
|
||||
// keep a central selection for UI actions (FurtherInfo buttons, timeline clicks, etc.)
|
||||
if (Platform.isFxApplicationThread()) {
|
||||
selectedChatMember.set(member);
|
||||
} else {
|
||||
Platform.runLater(() -> selectedChatMember.set(member));
|
||||
}
|
||||
|
||||
selectedCallSignRaw = member == null ? null : normalizeCallRaw(member.getCallSignRaw());
|
||||
|
||||
// Update score immediately from the latest snapshot
|
||||
if (Platform.isFxApplicationThread()) {
|
||||
updateSelectedScoreFromSnapshot(latestSnapshot.get());
|
||||
} else {
|
||||
Platform.runLater(() -> updateSelectedScoreFromSnapshot(latestSnapshot.get()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called periodically by the scheduler thread.
|
||||
* Recomputes only if explicitly requested or if the snapshot is too old.
|
||||
*/
|
||||
public void tick() {
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
boolean shouldRecompute = recomputeRequested.getAndSet(false) || (now - lastComputedEpochMs) > MAX_SNAPSHOT_AGE_MS;
|
||||
if (!shouldRecompute) return;
|
||||
|
||||
try {
|
||||
|
||||
// Apply "no reply" strikes (operator pinged via /cq but no inbound line arrived)
|
||||
controller.getStationMetricsService().evaluateNoReplyTimeouts(now, controller.getChatPreferences());
|
||||
|
||||
recompute(now);
|
||||
} catch (Exception e) {
|
||||
System.err.println("[ScoreService] CRITICAL error while recomputing scores");
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private void recompute(long nowEpochMs) {
|
||||
|
||||
// Keep sked list clean (must happen on FX thread)
|
||||
controller.requestRemoveExpiredSkeds(nowEpochMs);
|
||||
|
||||
final List<ChatMember> members = controller.snapshotChatMembers();
|
||||
final List<ContestSked> activeSkeds = controller.snapshotActiveSkeds();
|
||||
final ChatPreferences prefs = controller.getChatPreferences();
|
||||
final Map<String, ChatCategory> lastInbound = controller.snapshotLastInboundCategoryMap();
|
||||
|
||||
StationMetricsService.Snapshot metricsSnapshot =
|
||||
controller.getStationMetricsService().snapshot(nowEpochMs, prefs);
|
||||
|
||||
// 1) Choose one representative per callsignRaw
|
||||
Map<String, ChatMember> representativeByCallRaw = chooseRepresentativeMembers(members, lastInbound);
|
||||
|
||||
// 2) Compute score once per callsignRaw
|
||||
Map<String, Double> scoreByCallRaw = new HashMap<>(representativeByCallRaw.size());
|
||||
Map<String, ChatCategory> preferredCategoryByCallRaw = new HashMap<>(representativeByCallRaw.size());
|
||||
List<TopCandidate> topAll = new ArrayList<>(representativeByCallRaw.size());
|
||||
|
||||
for (Map.Entry<String, ChatMember> e : representativeByCallRaw.entrySet()) {
|
||||
String callRaw = e.getKey();
|
||||
ChatMember representative = e.getValue();
|
||||
if (representative == null) continue;
|
||||
|
||||
double score = priorityCalculator.calculatePriority(
|
||||
representative,
|
||||
prefs,
|
||||
activeSkeds,
|
||||
metricsSnapshot,
|
||||
nowEpochMs
|
||||
);
|
||||
|
||||
scoreByCallRaw.put(callRaw, score);
|
||||
preferredCategoryByCallRaw.put(callRaw, representative.getChatCategory());
|
||||
|
||||
topAll.add(new TopCandidate(callRaw, representative.getCallSign(), representative.getChatCategory(), score));
|
||||
}
|
||||
|
||||
// 3) Build Top-N
|
||||
topAll.sort(Comparator.comparingDouble(TopCandidate::getScore).reversed());
|
||||
List<TopCandidate> topNList = topAll.size() <= topN ? topAll : new ArrayList<>(topAll.subList(0, topN));
|
||||
|
||||
ScoreSnapshot snap = new ScoreSnapshot(
|
||||
nowEpochMs,
|
||||
Collections.unmodifiableMap(scoreByCallRaw),
|
||||
Collections.unmodifiableMap(preferredCategoryByCallRaw),
|
||||
Collections.unmodifiableList(topNList)
|
||||
);
|
||||
|
||||
latestSnapshot.set(snap);
|
||||
lastComputedEpochMs = nowEpochMs;
|
||||
|
||||
// 4) Publish to UI in ONE batched runLater
|
||||
Platform.runLater(() -> {
|
||||
topCandidatesFx.setAll(snap.getTopCandidates());
|
||||
updateSelectedScoreFromSnapshot(snap);
|
||||
uiPulse.set(uiPulse.get() + 1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Picks one ChatMember object per callsignRaw.
|
||||
* Preference order:
|
||||
* 1) Variant in last inbound chat category (stable reply routing)
|
||||
* 2) Most recently active variant (fallback)
|
||||
*/
|
||||
private Map<String, ChatMember> chooseRepresentativeMembers(
|
||||
List<ChatMember> members,
|
||||
Map<String, ChatCategory> lastInboundCategoryByCallRaw
|
||||
) {
|
||||
Map<String, List<ChatMember>> byCallRaw = new HashMap<>();
|
||||
|
||||
for (ChatMember m : members) {
|
||||
if (m == null) continue;
|
||||
String callRaw = normalizeCallRaw(m.getCallSignRaw());
|
||||
if (callRaw == null || callRaw.isEmpty()) continue;
|
||||
byCallRaw.computeIfAbsent(callRaw, k -> new ArrayList<>()).add(m);
|
||||
}
|
||||
|
||||
Map<String, ChatMember> representative = new HashMap<>(byCallRaw.size());
|
||||
|
||||
for (Map.Entry<String, List<ChatMember>> entry : byCallRaw.entrySet()) {
|
||||
String callRaw = entry.getKey();
|
||||
List<ChatMember> variants = entry.getValue();
|
||||
|
||||
ChatCategory preferredCat = lastInboundCategoryByCallRaw.get(callRaw);
|
||||
ChatMember chosen = null;
|
||||
|
||||
if (preferredCat != null) {
|
||||
for (ChatMember v : variants) {
|
||||
if (v != null && v.getChatCategory() == preferredCat) {
|
||||
chosen = v;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (chosen == null) {
|
||||
chosen = variants.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.max(Comparator.comparingLong(ChatMember::getActivityTimeLastInEpoch))
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
if (chosen != null) representative.put(callRaw, chosen);
|
||||
}
|
||||
|
||||
return representative;
|
||||
}
|
||||
|
||||
private void updateSelectedScoreFromSnapshot(ScoreSnapshot snap) {
|
||||
if (snap == null || selectedCallSignRaw == null) {
|
||||
selectedCallPriorityScore.set(Double.NaN);
|
||||
return;
|
||||
}
|
||||
Double v = snap.getScoreByCallSignRaw().get(selectedCallSignRaw);
|
||||
selectedCallPriorityScore.set(v == null ? Double.NaN : v);
|
||||
}
|
||||
|
||||
private static String normalizeCallRaw(String callRaw) {
|
||||
if (callRaw == null) return null;
|
||||
return callRaw.trim().toUpperCase();
|
||||
}
|
||||
|
||||
// ------------------------- DTOs -------------------------
|
||||
|
||||
public static final class TopCandidate {
|
||||
private final String callSignRaw;
|
||||
private final String displayCallSign;
|
||||
private final ChatCategory preferredChatCategory;
|
||||
private final double score;
|
||||
|
||||
public TopCandidate(String callSignRaw, String displayCallSign, ChatCategory preferredChatCategory, double score) {
|
||||
this.callSignRaw = callSignRaw;
|
||||
this.displayCallSign = displayCallSign;
|
||||
this.preferredChatCategory = preferredChatCategory;
|
||||
this.score = score;
|
||||
}
|
||||
|
||||
public String getCallSignRaw() { return callSignRaw; }
|
||||
public String getDisplayCallSign() { return displayCallSign; }
|
||||
public ChatCategory getPreferredChatCategory() { return preferredChatCategory; }
|
||||
public double getScore() { return score; }
|
||||
}
|
||||
|
||||
public static final class ScoreSnapshot {
|
||||
private final long computedAtEpochMs;
|
||||
private final Map<String, Double> scoreByCallSignRaw;
|
||||
private final Map<String, ChatCategory> preferredCategoryByCallSignRaw;
|
||||
private final List<TopCandidate> topCandidates;
|
||||
|
||||
public ScoreSnapshot(long computedAtEpochMs,
|
||||
Map<String, Double> scoreByCallSignRaw,
|
||||
Map<String, ChatCategory> preferredCategoryByCallSignRaw,
|
||||
List<TopCandidate> topCandidates) {
|
||||
this.computedAtEpochMs = computedAtEpochMs;
|
||||
this.scoreByCallSignRaw = scoreByCallSignRaw;
|
||||
this.preferredCategoryByCallSignRaw = preferredCategoryByCallSignRaw;
|
||||
this.topCandidates = topCandidates;
|
||||
}
|
||||
|
||||
public static ScoreSnapshot empty() {
|
||||
return new ScoreSnapshot(System.currentTimeMillis(), Collections.emptyMap(), Collections.emptyMap(), Collections.emptyList());
|
||||
}
|
||||
|
||||
public long getComputedAtEpochMs() { return computedAtEpochMs; }
|
||||
public Map<String, Double> getScoreByCallSignRaw() { return scoreByCallSignRaw; }
|
||||
public Map<String, ChatCategory> getPreferredCategoryByCallSignRaw() { return preferredCategoryByCallSignRaw; }
|
||||
public List<TopCandidate> getTopCandidates() { return topCandidates; }
|
||||
}
|
||||
|
||||
public ReadOnlyObjectProperty<ChatMember> selectedChatMemberProperty() {
|
||||
return selectedChatMember;
|
||||
}
|
||||
|
||||
public ChatMember getSelectedChatMember() {
|
||||
return selectedChatMember.get();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package kst4contest.controller;
|
||||
|
||||
import kst4contest.model.ChatMessage;
|
||||
|
||||
import java.util.TimerTask;
|
||||
|
||||
/**
|
||||
* This class is updateing the scoreboard at https://slovhf.net/claimed/. Gets scores of all bands out of the
|
||||
* preferences which is updated via ReadUdpByUCXLog Thread.
|
||||
*
|
||||
* api description: https://slovhf.net/claimed-score-api/
|
||||
*
|
||||
* <br/><br/>
|
||||
* The task will be runned out of the singleton ChatController instance in an
|
||||
* intervall as specified by the Chatpreferences-instance (typically as
|
||||
* configured in the xml file.
|
||||
*
|
||||
*
|
||||
* @author prakt
|
||||
*
|
||||
*/
|
||||
public class ScoreboardUpdateTask extends TimerTask {
|
||||
|
||||
private ChatController chatController;
|
||||
|
||||
public ScoreboardUpdateTask(ChatController client) {
|
||||
|
||||
this.chatController = client;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Thread.currentThread().setName("BeaconTask");
|
||||
|
||||
ChatMessage beaconMSG = new ChatMessage();
|
||||
|
||||
String replaceVariables = this.chatController.getChatPreferences().getBcn_beaconTextMainCat();
|
||||
// replaceVariables = bcn_beaconText;
|
||||
|
||||
replaceVariables = replaceVariables.replaceAll("MYQRG", this.chatController.getChatPreferences().getMYQRGFirstCat().getValue());
|
||||
replaceVariables = replaceVariables.replaceAll("MYCALL", this.chatController.getChatPreferences().getStn_loginCallSign());
|
||||
replaceVariables = replaceVariables.replaceAll("MYLOCATOR", this.chatController.getChatPreferences().getStn_loginLocatorMainCat());
|
||||
replaceVariables = replaceVariables.replaceAll("MYQTF", this.chatController.getChatPreferences().getActualQTF().getValue() + "");
|
||||
|
||||
|
||||
beaconMSG.setMessageText(
|
||||
"MSG|" + this.chatController.getChatPreferences().getLoginChatCategoryMain().getCategoryNumber() + "|0|" + replaceVariables + "|0|");
|
||||
beaconMSG.setMessageDirectedToServer(true);
|
||||
|
||||
// System.out.println("########### " + replaceVariables);
|
||||
|
||||
if (this.chatController.getChatPreferences().isBcn_beaconsEnabledMainCat() ) {
|
||||
|
||||
System.out.println(new Utils4KST().time_generateCurrentMMDDhhmmTimeString()
|
||||
+ " [BeaconTask, Info]: Sending CQ: " + beaconMSG.getMessageText());
|
||||
this.chatController.getMessageTXBus().add(beaconMSG);
|
||||
} else {
|
||||
//do nothing, CQ is disabled
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
124
src/main/java/kst4contest/controller/SkedReminderService.java
Normal file
124
src/main/java/kst4contest/controller/SkedReminderService.java
Normal file
@@ -0,0 +1,124 @@
|
||||
package kst4contest.controller;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import kst4contest.model.ChatCategory;
|
||||
import kst4contest.model.ThreadStateMessage;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.*;
|
||||
|
||||
/**
|
||||
* Schedules PM reminders for a specific sked time.
|
||||
*
|
||||
* Requirements:
|
||||
* - Reminder goes out as PM to the station (via "/cq CALL ...").
|
||||
* - Reminders are armed manually from FurtherInfo.
|
||||
*/
|
||||
public final class SkedReminderService {
|
||||
|
||||
private final ChatController controller;
|
||||
|
||||
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
|
||||
Thread t = new Thread(r);
|
||||
t.setDaemon(true);
|
||||
t.setName("SkedReminderService");
|
||||
return t;
|
||||
});
|
||||
|
||||
private final ConcurrentHashMap<String, List<ScheduledFuture<?>>> scheduledByCallRaw = new ConcurrentHashMap<>();
|
||||
|
||||
public SkedReminderService(ChatController controller) {
|
||||
this.controller = controller;
|
||||
}
|
||||
|
||||
/**
|
||||
* Arms reminders for one sked. Existing reminders for this call are cancelled.
|
||||
*
|
||||
* @param callSignRaw target call
|
||||
* @param preferredCategory where to send (if null, controller resolves via lastInbound category)
|
||||
* @param skedTimeEpochMs sked time
|
||||
* @param offsetsMinutes e.g. [5,2,1] => reminders 5,2,1 minutes before
|
||||
*/
|
||||
public void armReminders(String callSignRaw,
|
||||
ChatCategory preferredCategory,
|
||||
long skedTimeEpochMs,
|
||||
List<Integer> offsetsMinutes) {
|
||||
|
||||
String callRaw = normalize(callSignRaw);
|
||||
if (callRaw == null || callRaw.isBlank()) return;
|
||||
|
||||
cancelReminders(callRaw);
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
List<Integer> offsets = (offsetsMinutes == null) ? List.of() : offsetsMinutes;
|
||||
|
||||
List<ScheduledFuture<?>> futures = new ArrayList<>();
|
||||
for (Integer offMin : offsets) {
|
||||
if (offMin == null) continue;
|
||||
|
||||
long fireAt = skedTimeEpochMs - (offMin * 60_000L);
|
||||
long delayMs = fireAt - now;
|
||||
if (delayMs <= 0) continue;
|
||||
|
||||
ScheduledFuture<?> f = scheduler.schedule(
|
||||
() -> fireReminder(callRaw, preferredCategory, offMin),
|
||||
delayMs,
|
||||
TimeUnit.MILLISECONDS
|
||||
);
|
||||
futures.add(f);
|
||||
}
|
||||
|
||||
scheduledByCallRaw.put(callRaw, futures);
|
||||
|
||||
controller.onThreadStatus("SkedReminderService",
|
||||
new ThreadStateMessage("SkedReminder", true,
|
||||
"Armed for " + callRaw + " (" + offsets + " min before)", false));
|
||||
}
|
||||
|
||||
public void cancelReminders(String callSignRaw) {
|
||||
String callRaw = normalize(callSignRaw);
|
||||
if (callRaw == null || callRaw.isBlank()) return;
|
||||
|
||||
List<ScheduledFuture<?>> futures = scheduledByCallRaw.remove(callRaw);
|
||||
if (futures != null) {
|
||||
for (ScheduledFuture<?> f : futures) {
|
||||
if (f != null) f.cancel(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void fireReminder(String callRaw, ChatCategory preferredCategory, int minutesBefore) {
|
||||
try {
|
||||
controller.queuePrivateCqMessage(callRaw, preferredCategory, "[KST4C Autoreminder] sked in " + minutesBefore + " min");
|
||||
controller.fireUiReminderEvent(callRaw, minutesBefore); //triggers some blingbling in the UI
|
||||
|
||||
///Local acoustic hint (reuse existing project audio utilities, no AWT, no extra JavaFX modules)
|
||||
|
||||
try {
|
||||
if (controller.getChatPreferences().isNotify_playSimpleSounds()) {
|
||||
controller.getPlayAudioUtils().playNoiseLauncher('!'); // choose a suitable char you already use
|
||||
}
|
||||
// Optional: voice/cw hint (short, not too intrusive)
|
||||
// controller.getPlayAudioUtils().playCWLauncher(" SKED " + minutesBefore);
|
||||
} catch (Exception ignore) {
|
||||
// never block reminder sending because of audio issues
|
||||
}
|
||||
|
||||
controller.onThreadStatus("SkedReminderService",
|
||||
new ThreadStateMessage("SkedReminder", true,
|
||||
"PM reminder sent to " + callRaw + " (" + minutesBefore + " min)", false));
|
||||
} catch (Exception e) {
|
||||
controller.onThreadStatus("SkedReminderService",
|
||||
new ThreadStateMessage("SkedReminder", false,
|
||||
"ERROR sending reminder to " + callRaw + ": " + e.getMessage(), true));
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private static String normalize(String s) {
|
||||
if (s == null) return null;
|
||||
return s.trim().toUpperCase();
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,9 @@
|
||||
*/
|
||||
package kst4contest.controller;
|
||||
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.value.ObservableStringValue;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
@@ -19,7 +22,9 @@ public class StartChat {
|
||||
public static void main(String[] args) throws InterruptedException, IOException {
|
||||
|
||||
System.out.println("[Startchat:] Starting new Chat instance");
|
||||
|
||||
|
||||
// ObservableStringValue messageBus = new SimpleStringProperty("");
|
||||
|
||||
ChatController client = new ChatController();
|
||||
client.execute();
|
||||
|
||||
|
||||
268
src/main/java/kst4contest/controller/StationMetricsService.java
Normal file
268
src/main/java/kst4contest/controller/StationMetricsService.java
Normal file
@@ -0,0 +1,268 @@
|
||||
package kst4contest.controller;
|
||||
|
||||
import kst4contest.logic.SignalDetector;
|
||||
import kst4contest.model.ChatPreferences;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Deque;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Thread-safe metrics store keyed by normalized callsignRaw (e.g. "SM6VTZ").
|
||||
*
|
||||
* Purpose:
|
||||
* - Provide inputs for scoring (momentum, reply time, no-reply strikes, manual sked-fail, positive signals).
|
||||
* - Decouple MessageBus / TX from ScoreService (only data flows, no UI calls here).
|
||||
*/
|
||||
public final class StationMetricsService {
|
||||
|
||||
/** /cq <CALL> ... */
|
||||
private static final Pattern OUTBOUND_CQ_PATTERN = Pattern.compile("(?i)^\\s*/cq\\s+([A-Z0-9/]+)\\b.*");
|
||||
|
||||
/** Rolling window timestamps for momentum scoring. */
|
||||
private static final int MAX_STORED_INBOUND_TIMESTAMPS = 32;
|
||||
|
||||
private final ConcurrentHashMap<String, StationMetrics> byCallRaw = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* Called when the operator sends a message.
|
||||
* If it is a "/cq CALL ..." message, this arms a pending ping for response-time / no-reply tracking.
|
||||
*/
|
||||
public Optional<String> tryRecordOutboundCq(String messageText, long nowEpochMs) {
|
||||
if (messageText == null) return Optional.empty();
|
||||
|
||||
Matcher m = OUTBOUND_CQ_PATTERN.matcher(messageText.trim());
|
||||
if (!m.matches()) return Optional.empty();
|
||||
|
||||
String callRaw = normalizeCallRaw(m.group(1));
|
||||
if (callRaw == null || callRaw.isBlank()) return Optional.empty();
|
||||
|
||||
StationMetrics metrics = byCallRaw.computeIfAbsent(callRaw, k -> new StationMetrics());
|
||||
synchronized (metrics) {
|
||||
metrics.pendingCqSentAtEpochMs = nowEpochMs;
|
||||
metrics.lastOutboundCqEpochMs = nowEpochMs;
|
||||
}
|
||||
return Optional.of(callRaw);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called for EVERY inbound line from a station (CH or PM).
|
||||
* "Any line counts as activity"
|
||||
*/
|
||||
public void onInboundMessage(String senderCallSignRaw,
|
||||
long nowEpochMs,
|
||||
String messageText,
|
||||
ChatPreferences prefs,
|
||||
String ownCallSignRaw) {
|
||||
|
||||
String callRaw = normalizeCallRaw(senderCallSignRaw);
|
||||
if (callRaw == null || callRaw.isBlank()) return;
|
||||
|
||||
// ignore own echoed messages
|
||||
if (ownCallSignRaw != null && callRaw.equalsIgnoreCase(normalizeCallRaw(ownCallSignRaw))) return;
|
||||
|
||||
StationMetrics metrics = byCallRaw.computeIfAbsent(callRaw, k -> new StationMetrics());
|
||||
synchronized (metrics) {
|
||||
metrics.lastInboundEpochMs = nowEpochMs;
|
||||
|
||||
// rolling timestamps (momentum)
|
||||
metrics.recentInboundEpochMs.addLast(nowEpochMs);
|
||||
while (metrics.recentInboundEpochMs.size() > MAX_STORED_INBOUND_TIMESTAMPS) {
|
||||
metrics.recentInboundEpochMs.removeFirst();
|
||||
}
|
||||
|
||||
// positive signal detection (extendable by prefs)
|
||||
if (messageText != null && prefs != null) {
|
||||
if (SignalDetector.containsPositiveSignal(messageText, prefs.getNotify_positiveSignalsPatterns())) {
|
||||
metrics.lastPositiveSignalEpochMs = nowEpochMs;
|
||||
}
|
||||
}
|
||||
|
||||
// response time measurement: any inbound line ends a pending ping
|
||||
if (metrics.pendingCqSentAtEpochMs > 0) {
|
||||
long rttMs = Math.max(0, nowEpochMs - metrics.pendingCqSentAtEpochMs);
|
||||
metrics.pendingCqSentAtEpochMs = 0;
|
||||
|
||||
// EWMA for response time (stable, no spikes)
|
||||
final double alpha = 0.25;
|
||||
if (metrics.avgResponseTimeMs <= 0) {
|
||||
metrics.avgResponseTimeMs = rttMs;
|
||||
} else {
|
||||
metrics.avgResponseTimeMs = alpha * rttMs + (1.0 - alpha) * metrics.avgResponseTimeMs;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called periodically (e.g. from ScoreService.tick()).
|
||||
* Applies a "no reply" strike if the pending ping is older than prefs timeout.
|
||||
*/
|
||||
public void evaluateNoReplyTimeouts(long nowEpochMs, ChatPreferences prefs) {
|
||||
if (prefs == null) return;
|
||||
long timeoutMs = Math.max(1, prefs.getNotify_noReplyPenaltyMinutes()) * 60_000L;
|
||||
|
||||
for (Map.Entry<String, StationMetrics> e : byCallRaw.entrySet()) {
|
||||
StationMetrics metrics = e.getValue();
|
||||
if (metrics == null) continue;
|
||||
|
||||
synchronized (metrics) {
|
||||
if (metrics.pendingCqSentAtEpochMs <= 0) continue;
|
||||
|
||||
long age = nowEpochMs - metrics.pendingCqSentAtEpochMs;
|
||||
if (age >= timeoutMs) {
|
||||
metrics.pendingCqSentAtEpochMs = 0;
|
||||
metrics.noReplyStrikes++;
|
||||
metrics.lastNoReplyStrikeEpochMs = nowEpochMs;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Manual sked fail: permanent until reset. */
|
||||
public void markManualSkedFail(String callSignRaw) {
|
||||
String callRaw = normalizeCallRaw(callSignRaw);
|
||||
if (callRaw == null || callRaw.isBlank()) return;
|
||||
|
||||
StationMetrics metrics = byCallRaw.computeIfAbsent(callRaw, k -> new StationMetrics());
|
||||
synchronized (metrics) {
|
||||
metrics.manualSkedFailed = true;
|
||||
metrics.manualSkedFailCount++;
|
||||
}
|
||||
}
|
||||
|
||||
public void resetManualSkedFail(String callSignRaw) {
|
||||
String callRaw = normalizeCallRaw(callSignRaw);
|
||||
if (callRaw == null || callRaw.isBlank()) return;
|
||||
|
||||
StationMetrics metrics = byCallRaw.computeIfAbsent(callRaw, k -> new StationMetrics());
|
||||
synchronized (metrics) {
|
||||
metrics.manualSkedFailed = false;
|
||||
metrics.manualSkedFailCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isManualSkedFailed(String callSignRaw) {
|
||||
String callRaw = normalizeCallRaw(callSignRaw);
|
||||
if (callRaw == null || callRaw.isBlank()) return false;
|
||||
|
||||
StationMetrics metrics = byCallRaw.get(callRaw);
|
||||
if (metrics == null) return false;
|
||||
|
||||
synchronized (metrics) {
|
||||
return metrics.manualSkedFailed;
|
||||
}
|
||||
}
|
||||
|
||||
/** Immutable snapshot for scoring */
|
||||
public Snapshot snapshot(long nowEpochMs, ChatPreferences prefs) {
|
||||
long momentumWindowMs = (prefs != null ? prefs.getNotify_momentumWindowSeconds() : 180) * 1000L;
|
||||
|
||||
Snapshot snap = new Snapshot(nowEpochMs, momentumWindowMs);
|
||||
for (Map.Entry<String, StationMetrics> e : byCallRaw.entrySet()) {
|
||||
String callRaw = e.getKey();
|
||||
StationMetrics m = e.getValue();
|
||||
if (m == null) continue;
|
||||
|
||||
synchronized (m) {
|
||||
snap.byCallRaw.put(callRaw, new Snapshot.Metrics(
|
||||
m.lastInboundEpochMs,
|
||||
countRecent(m.recentInboundEpochMs, nowEpochMs, momentumWindowMs),
|
||||
m.avgResponseTimeMs,
|
||||
m.noReplyStrikes,
|
||||
m.manualSkedFailed,
|
||||
m.manualSkedFailCount,
|
||||
m.lastPositiveSignalEpochMs
|
||||
));
|
||||
}
|
||||
}
|
||||
return snap;
|
||||
}
|
||||
|
||||
private static int countRecent(Deque<Long> timestamps, long nowEpochMs, long windowMs) {
|
||||
if (timestamps == null || timestamps.isEmpty()) return 0;
|
||||
int cnt = 0;
|
||||
for (Long t : timestamps) {
|
||||
if (t == null) continue;
|
||||
if (nowEpochMs - t <= windowMs) cnt++;
|
||||
}
|
||||
return cnt;
|
||||
}
|
||||
|
||||
private static String normalizeCallRaw(String s) {
|
||||
if (s == null) return null;
|
||||
return s.trim().toUpperCase();
|
||||
}
|
||||
|
||||
private static final class StationMetrics {
|
||||
long lastInboundEpochMs;
|
||||
long lastOutboundCqEpochMs;
|
||||
|
||||
long pendingCqSentAtEpochMs; // 0 = none
|
||||
int noReplyStrikes;
|
||||
long lastNoReplyStrikeEpochMs;
|
||||
|
||||
double avgResponseTimeMs; // EWMA
|
||||
final Deque<Long> recentInboundEpochMs = new ArrayDeque<>();
|
||||
|
||||
long lastPositiveSignalEpochMs;
|
||||
|
||||
boolean manualSkedFailed;
|
||||
int manualSkedFailCount;
|
||||
}
|
||||
|
||||
public static final class Snapshot {
|
||||
private final long snapshotEpochMs;
|
||||
private final long momentumWindowMs;
|
||||
private final ConcurrentHashMap<String, Metrics> byCallRaw = new ConcurrentHashMap<>();
|
||||
|
||||
private Snapshot(long snapshotEpochMs, long momentumWindowMs) {
|
||||
this.snapshotEpochMs = snapshotEpochMs;
|
||||
this.momentumWindowMs = momentumWindowMs;
|
||||
}
|
||||
|
||||
public Metrics get(String callSignRaw) {
|
||||
if (callSignRaw == null) return null;
|
||||
return byCallRaw.get(normalizeCallRaw(callSignRaw));
|
||||
}
|
||||
|
||||
public long getSnapshotEpochMs() {
|
||||
return snapshotEpochMs;
|
||||
}
|
||||
|
||||
public long getMomentumWindowMs() {
|
||||
return momentumWindowMs;
|
||||
}
|
||||
|
||||
public static final class Metrics {
|
||||
public final long lastInboundEpochMs;
|
||||
public final int inboundCountInWindow;
|
||||
public final double avgResponseTimeMs;
|
||||
public final int noReplyStrikes;
|
||||
public final boolean manualSkedFailed;
|
||||
public final int manualSkedFailCount;
|
||||
public final long lastPositiveSignalEpochMs;
|
||||
|
||||
public Metrics(long lastInboundEpochMs,
|
||||
int inboundCountInWindow,
|
||||
double avgResponseTimeMs,
|
||||
int noReplyStrikes,
|
||||
boolean manualSkedFailed,
|
||||
int manualSkedFailCount,
|
||||
long lastPositiveSignalEpochMs) {
|
||||
|
||||
this.lastInboundEpochMs = lastInboundEpochMs;
|
||||
this.inboundCountInWindow = inboundCountInWindow;
|
||||
this.avgResponseTimeMs = avgResponseTimeMs;
|
||||
this.noReplyStrikes = noReplyStrikes;
|
||||
this.manualSkedFailed = manualSkedFailed;
|
||||
this.manualSkedFailCount = manualSkedFailCount;
|
||||
this.lastPositiveSignalEpochMs = lastPositiveSignalEpochMs;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package kst4contest.controller;
|
||||
|
||||
import kst4contest.model.ThreadStateMessage;
|
||||
|
||||
public interface StatusUpdateListener {
|
||||
|
||||
/**
|
||||
* Thread (key) will send update status (value) to the view via this interface.
|
||||
*
|
||||
*/
|
||||
void onThreadStatusChanged(String key, ThreadStateMessage threadStateMessage);
|
||||
|
||||
|
||||
/**
|
||||
* Called on change if the userlist to update the UI (sort the chatmembers list)
|
||||
*/
|
||||
void onUserListUpdated(String reason);
|
||||
// new: userlist-update
|
||||
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package kst4contest.controller;
|
||||
|
||||
import kst4contest.model.ThreadStateMessage;
|
||||
|
||||
public interface ThreadStatusCallback {
|
||||
void onThreadStatus(String threadName, ThreadStateMessage threadStateMessage);
|
||||
|
||||
}
|
||||
@@ -15,7 +15,10 @@ import kst4contest.model.ChatMember;
|
||||
public class UCXLogFileToHashsetParser {
|
||||
|
||||
public BufferedReader fileReader;
|
||||
private final String PTRN_CallSign = "(([a-zA-Z]{1,2}[\\d{1}]?\\/)?(\\d{1}[a-zA-Z][\\d{1}][a-zA-Z]{1,3})((\\/p)|(\\/\\d))?)|(([a-zA-Z0-9]{1,2}[\\d{1}]?\\/)?(([a-zA-Z]{1,2}(\\d{1}[a-zA-Z]{1,4})))((\\/p)|(\\/\\d))?)";
|
||||
// private final String PTRN_CallSign = "(([a-zA-Z]{1,2}[\\d{1}]?\\/)?(\\d{1}[a-zA-Z][\\d{1}][a-zA-Z]{1,3})((\\/p)|(\\/\\d))?)|(([a-zA-Z0-9]{1,2}[\\d{1}]?\\/)?(([a-zA-Z]{1,2}(\\d{1}[a-zA-Z]{1,4})))((\\/p)|(\\/\\d))?)"; //OLD, S51AR for example will not work
|
||||
private final String PTRN_CallSign = "(([a-zA-Z]{1,2}[\\d]{1}?\\/)?(\\d{1}[a-zA-Z][\\d]{1}[a-zA-Z]{1,3})((\\/p)|(\\/\\d))?)|(([a-zA-Z0-9]{1,2}[\\d]{1}?\\/)?(([a-zA-Z]{1,2}(\\d{1}[a-zA-Z]{1,4})))((\\/p)|(\\/\\d))?)|([A-Z]\\d{2}[A-Z]{1,3})";
|
||||
|
||||
|
||||
|
||||
public UCXLogFileToHashsetParser(String filePathAndName) {
|
||||
|
||||
@@ -37,7 +40,7 @@ public class UCXLogFileToHashsetParser {
|
||||
*/
|
||||
private ChatMember checkIfLineInhibitsCallSign(String line) {
|
||||
|
||||
Pattern pattern = Pattern.compile(PTRN_CallSign); // TODO: PTRN should depend to category-selection of own stn
|
||||
Pattern pattern = Pattern.compile(PTRN_CallSign);
|
||||
Matcher matcher = pattern.matcher(line);
|
||||
|
||||
String matchedString = "";
|
||||
|
||||
237
src/main/java/kst4contest/controller/UpdateChecker.java
Normal file
237
src/main/java/kst4contest/controller/UpdateChecker.java
Normal file
@@ -0,0 +1,237 @@
|
||||
package kst4contest.controller;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
import kst4contest.ApplicationConstants;
|
||||
|
||||
import kst4contest.model.UpdateInformation;
|
||||
import kst4contest.utils.ApplicationFileUtils;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
|
||||
import javax.xml.XMLConstants;
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import java.io.File;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class UpdateChecker {
|
||||
|
||||
public static void main(String[] args) {
|
||||
// new UpdateChecker(null).parseUpdateXMLFile();
|
||||
|
||||
if (new UpdateChecker(null).downloadLatestVersionInfoXML()) {
|
||||
// ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME,ApplicationConstants.VERSIONINFDOWNLOADEDLOCALFILE,ApplicationConstants.VERSIONINFDOWNLOADEDLOCALFILE);
|
||||
}
|
||||
new UpdateChecker(null).parseUpdateXMLFile();
|
||||
}
|
||||
|
||||
public UpdateChecker(ChatController chatController) {
|
||||
|
||||
|
||||
System.out.println("[Updatechecker: checking for updates...]");
|
||||
// double currentVersionNumber = ApplicationConstants.APPLICATION_CURRENTVERSIONNUMBER;
|
||||
|
||||
}
|
||||
|
||||
String versionInfoDownloadedFromServerFileName = ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, ApplicationConstants.VERSIONINFDOWNLOADEDLOCALFILE);
|
||||
String versionInfoXMLURLAtServer = ApplicationConstants.VERSIONINFOURLFORUPDATES_KST4CONTEST;
|
||||
// double currentVersion = ApplicationConstants.APPLICATION_CURRENTVERSIONNUMBER;
|
||||
//DOWNLOAD from URL, then parse, then do anything with it...
|
||||
|
||||
/**
|
||||
* Downloads the versioninfo-xml-file from a webserver to local. Returns true if download was successful, else false
|
||||
*
|
||||
* @return true if successful
|
||||
*/
|
||||
public boolean downloadLatestVersionInfoXML() {
|
||||
|
||||
try {
|
||||
|
||||
InputStream in = new URL(versionInfoXMLURLAtServer).openStream();
|
||||
Files.copy(in, Paths.get(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/"+ApplicationConstants.VERSIONINFDOWNLOADEDLOCALFILE)), StandardCopyOption.REPLACE_EXISTING);
|
||||
|
||||
in.close();
|
||||
|
||||
// System.out.println(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/"+ApplicationConstants.VERSIONINFDOWNLOADEDLOCALFILE));
|
||||
// ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME,ApplicationFileUtils.get,ApplicationConstants.VERSIONINFDOWNLOADEDLOCALFILE);
|
||||
|
||||
} catch (Exception e) {
|
||||
System.out.println("ERROR DOWNLOADING!" + e);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public UpdateInformation parseUpdateXMLFile() {
|
||||
|
||||
UpdateInformation updateInfos = new UpdateInformation();
|
||||
|
||||
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME,"/"+ApplicationConstants.VERSIONINFDOWNLOADEDLOCALFILE,ApplicationConstants.VERSIONINFDOWNLOADEDLOCALFILE);
|
||||
|
||||
// System.out.println("[Updatecker, Info]: restoring prefs from file " + versionInfoDownloadedFromServerFileName);
|
||||
|
||||
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
|
||||
try {
|
||||
dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
|
||||
} catch (ParserConfigurationException e1) {
|
||||
// TODO Auto-generated catch block
|
||||
e1.printStackTrace();
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
File xmlConfigFile = new File(versionInfoDownloadedFromServerFileName);
|
||||
|
||||
DocumentBuilder db = dbf.newDocumentBuilder();
|
||||
Document doc = db.parse(xmlConfigFile);
|
||||
|
||||
/**
|
||||
* latestVersion on server
|
||||
*/
|
||||
NodeList list = doc.getElementsByTagName("latestVersion");
|
||||
if (list.getLength() != 0) {
|
||||
|
||||
for (int temp = 0; temp < list.getLength(); temp++) {
|
||||
|
||||
Node node = list.item(temp);
|
||||
|
||||
if (node.getNodeType() == Node.ELEMENT_NODE) {
|
||||
|
||||
Element element = (Element) node;
|
||||
updateInfos.setLatestVersionNumberOnServer(Double.parseDouble(element.getElementsByTagName("versionNumber").item(0).getTextContent()));
|
||||
updateInfos.setAdminMessage(element.getElementsByTagName("adminMessage").item(0).getTextContent());
|
||||
updateInfos.setMajorChanges(element.getElementsByTagName("majorChanges").item(0)
|
||||
.getTextContent());
|
||||
updateInfos.setLatestVersionPathOnWebserver(element.getElementsByTagName("latestVersionPathOnWebserver").item(0).getTextContent());
|
||||
// System.out.println(updateInfos.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Section changeLog
|
||||
*/
|
||||
|
||||
list = doc.getElementsByTagName("changeLog");
|
||||
ArrayList<String[]> changeLogArrayList = new ArrayList<String[]>();
|
||||
|
||||
if (list.getLength() != 0) {
|
||||
|
||||
for (int temp = 0; temp < list.getLength(); temp++) {
|
||||
|
||||
Node node = list.item(temp);
|
||||
|
||||
Element element = (Element) node;
|
||||
int childNodeCounter = 0; //need an extra counter due to childnodes are counted...no idea, how
|
||||
String[] aChangeLogEntry = new String[7];
|
||||
aChangeLogEntry[0] = "";
|
||||
aChangeLogEntry[1] = "Date: ";
|
||||
aChangeLogEntry[2] = "Desc: ";
|
||||
aChangeLogEntry[3] = "Added: ";
|
||||
aChangeLogEntry[4] = "Changed: ";
|
||||
aChangeLogEntry[5] = "Fixed: ";
|
||||
aChangeLogEntry[6] = "Removed: ";
|
||||
|
||||
for (int i = 0; i < element.getChildNodes().getLength(); i++) {
|
||||
|
||||
if (element.getChildNodes().item(i).getNodeType() == Node.ELEMENT_NODE) {
|
||||
// System.out.println(element.getChildNodes().item(i).getTextContent() + " <<<<<<<<<<<<<<<<<< " + i + " / " + childNodeCounter);
|
||||
// System.out.println(element.getChildNodes().item(i).getNodeName());
|
||||
aChangeLogEntry[childNodeCounter] = aChangeLogEntry[childNodeCounter] + element.getChildNodes().item(i).getTextContent();
|
||||
childNodeCounter++;
|
||||
}
|
||||
}
|
||||
changeLogArrayList.add(aChangeLogEntry);
|
||||
}
|
||||
updateInfos.setChangeLog(changeLogArrayList);
|
||||
}
|
||||
|
||||
/**
|
||||
* Section Buglist
|
||||
*/
|
||||
|
||||
list = doc.getElementsByTagName("bug");
|
||||
ArrayList<String[]> bugFixArrayList = new ArrayList<String[]>();
|
||||
|
||||
if (list.getLength() != 0) {
|
||||
|
||||
for (int temp = 0; temp < list.getLength(); temp++) {
|
||||
|
||||
Node node = list.item(temp);
|
||||
|
||||
Element element = (Element) node;
|
||||
int childNodeCounter = 0; //need an extra counter due to childnodes are counted...no idea, how
|
||||
String[] aChangeLogEntry = new String[3];
|
||||
aChangeLogEntry[0] = "";
|
||||
aChangeLogEntry[1] = "State: ";
|
||||
|
||||
|
||||
for (int i = 0; i < element.getChildNodes().getLength(); i++) {
|
||||
|
||||
if (element.getChildNodes().item(i).getNodeType() == Node.ELEMENT_NODE) {
|
||||
// System.out.println(element.getChildNodes().item(i).getTextContent() + " <<<<<<<<<<<<<<<<<< " + i + " / " + childNodeCounter);
|
||||
// System.out.println(element.getChildNodes().item(i).getNodeName());
|
||||
aChangeLogEntry[childNodeCounter] = aChangeLogEntry[childNodeCounter] + element.getChildNodes().item(i).getTextContent();
|
||||
childNodeCounter++;
|
||||
}
|
||||
}
|
||||
bugFixArrayList.add(aChangeLogEntry);
|
||||
}
|
||||
updateInfos.setBugList(bugFixArrayList);
|
||||
}
|
||||
|
||||
|
||||
} catch (Exception e) {
|
||||
System.out.println(e.getMessage());
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
String[] testEntry = new String[7];
|
||||
testEntry[0] = "0.99";
|
||||
testEntry[1] = "2022-09";
|
||||
testEntry[2] = "researched the Chatprotocol";
|
||||
testEntry[3] = "addednothing";
|
||||
testEntry[4] = "changedsome";
|
||||
testEntry[5] = "fixedxed";
|
||||
testEntry[6] = "removedYourMom";
|
||||
|
||||
String[] testEntry2 = new String[7];
|
||||
testEntry2[0] = "0.29";
|
||||
testEntry2[1] = "2033-09";
|
||||
testEntry2[2] = "tested";
|
||||
testEntry2[3] = "addednotashing";
|
||||
testEntry2[4] = "changeasdsome";
|
||||
testEntry2[5] = "fixedxeds";
|
||||
testEntry2[6] = "removedYosssurMom";
|
||||
|
||||
// changeLogArrayList.add(testEntry);
|
||||
// changeLogArrayList.add(testEntry2);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
return updateInfos;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
String toString = "";
|
||||
|
||||
// toString += this.currentVersion;
|
||||
|
||||
return toString;
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import java.util.TimerTask;
|
||||
import javafx.collections.ObservableList;
|
||||
import kst4contest.model.ChatMember;
|
||||
import kst4contest.model.ClusterMessage;
|
||||
import kst4contest.view.GuiUtils;
|
||||
|
||||
public class UserActualizationTask extends TimerTask {
|
||||
|
||||
@@ -50,21 +51,10 @@ public class UserActualizationTask extends TimerTask {
|
||||
UCXLogFileToHashsetParser getWorkedCallsignsOfUCXLogFile = new UCXLogFileToHashsetParser(
|
||||
this.client.getChatPreferences().getLogsynch_fileBasedWkdCallInterpreterFileNameReadOnly());
|
||||
|
||||
// UCXLogFileToHashsetParser getWorkedCallsignsOfUDPBackupFile = new UCXLogFileToHashsetParser(
|
||||
// this.client.getChatPreferences().getLogSynch_storeWorkedCallSignsFileNameUDPMessageBackup());
|
||||
|
||||
try {
|
||||
fetchedWorkedSet = getWorkedCallsignsOfUCXLogFile.parse();
|
||||
// fetchedWorkedSetUdpBckup = getWorkedCallsignsOfUDPBackupFile.parse();
|
||||
|
||||
// for (HashMap.Entry entry : fetchedWorkedSet.entrySet()) {
|
||||
// String key = (String) entry.getKey();
|
||||
// Object value = entry.getValue();
|
||||
// System.out.println("key " + key);
|
||||
// }
|
||||
|
||||
System.out.println("USERACT: fetchedWorkedSet size: " + fetchedWorkedSet.size());
|
||||
// System.out.println("USERACT: fetchedWorkedSetudpbckup size: " + fetchedWorkedSetUdpBckup.size());
|
||||
|
||||
} catch (IOException e) {
|
||||
// TODO Auto-generated catch block
|
||||
@@ -89,6 +79,7 @@ public class UserActualizationTask extends TimerTask {
|
||||
// chatMember.setWorked(true);
|
||||
// System.out.println("[USERACT, info:] marking Chatuser " + chatMember.getCallSign() + " as worked, based on UDPLsnBackup-Logfile.");
|
||||
// }
|
||||
// GuiUtils.triggerGUIFilteredChatMemberListChange(this.client); //todo: quick and dirty gui fix
|
||||
}
|
||||
|
||||
ObservableList<ClusterMessage> praktiKSTClusterList = this.client.getLst_clusterMemberList();
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package kst4contest.controller;
|
||||
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.OffsetDateTime;
|
||||
@@ -7,6 +9,8 @@ import java.time.ZoneOffset;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Date;
|
||||
import java.util.TimeZone;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class Utils4KST {
|
||||
|
||||
@@ -20,6 +24,14 @@ public class Utils4KST {
|
||||
return millisecondsSinceEpoch;
|
||||
}
|
||||
|
||||
public String time_generateCurrenthhmmZTimeStringForClusterMessage() {
|
||||
|
||||
OffsetDateTime currentTimeInUtc = OffsetDateTime.now(ZoneOffset.UTC);
|
||||
System.out.println("Utils generated current time " + currentTimeInUtc + " --> " + currentTimeInUtc.format(DateTimeFormatter.ofPattern("HHmm"))+"Z");
|
||||
return currentTimeInUtc.format(DateTimeFormatter.ofPattern("HHmm"))+"Z";
|
||||
|
||||
}
|
||||
|
||||
public String time_generateCurrentMMDDhhmmTimeString() {
|
||||
|
||||
OffsetDateTime currentTimeInUtc = OffsetDateTime.now(ZoneOffset.UTC);
|
||||
@@ -40,7 +52,8 @@ public class Utils4KST {
|
||||
// Instant instant = Instant.ofEpochSecond(epoch);
|
||||
|
||||
Date date = new Date(epoch * 1000L);
|
||||
DateFormat format = new SimpleDateFormat("dd.MM HH:mm:ss");
|
||||
// DateFormat format = new SimpleDateFormat("dd.MM HH:mm:ss"); //old value which is too long
|
||||
DateFormat format = new SimpleDateFormat("H:mm:ss");
|
||||
format.setTimeZone(TimeZone.getTimeZone("Etc/UTC"));
|
||||
String formatted = format.format(date);
|
||||
|
||||
@@ -49,6 +62,21 @@ public class Utils4KST {
|
||||
return formatted;
|
||||
|
||||
}
|
||||
|
||||
public static long time_getSecondsBetweenEpochAndNow(String epoch1) {
|
||||
|
||||
long epoch1Long = Long.parseLong(epoch1);
|
||||
long epoch2Long = new Utils4KST().time_generateCurrentEpochTime();
|
||||
// Instant instant = Instant.ofEpochSecond(epoch);
|
||||
|
||||
Date date = new Date(epoch1Long * 1000L);
|
||||
Date date2 = new Date(epoch2Long * 1000L);
|
||||
|
||||
long seconds = Math.abs(date.getTime()-date2.getTime())/1000;
|
||||
|
||||
return seconds;
|
||||
|
||||
}
|
||||
|
||||
public Date time_generateActualTimeInDateFormat() {
|
||||
Date date = new Date(time_generateCurrentEpochTime() * 1000L);
|
||||
@@ -56,4 +84,119 @@ public class Utils4KST {
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This method tests a regexp-pattern against a given string
|
||||
*
|
||||
* @param testString: check if this string matches a given pattern
|
||||
* @param regExPattern: pattern which should be checked
|
||||
* @return true if match, else false
|
||||
*/
|
||||
private static boolean testPattern(String testString, String regExPattern) {
|
||||
|
||||
Pattern pattern = Pattern.compile(regExPattern);
|
||||
Matcher matcher = pattern.matcher(testString);
|
||||
|
||||
return matcher.find();
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a chatmembers frequency-string for cluster usage<br/>
|
||||
* <b>returns a frequency String in KHz like = "144300" or "144300.0" to match DXC protocol needs</b>
|
||||
*
|
||||
* @param optionalPrefix: if there is a value like ".300", it have to be decided, wich ".300": 144.300, 432.300, 1296.300 .... prefix means for example "144."
|
||||
*/
|
||||
public static String normalizeFrequencyString(String qrgString, SimpleStringProperty optionalPrefix) {
|
||||
|
||||
// final String PTRN_QRG_CAT2 = "(([0-9]{3,4}[\\.|,| ]?[0-9]{3})([\\.|,][\\d]{1,2})?)|(([a-zA-Z][0-4]{1}[\\d]{2}\\b)([\\.|,][\\d]{1,2}\\b)?)|((\\b[0-4]{1}[\\d]{2}\\b)([\\.|,][\\d]{1,2}\\b)?)";
|
||||
|
||||
try {
|
||||
qrgString = qrgString.replace(" ","");
|
||||
} catch (Exception e) {
|
||||
System.out.println("UTILS: QRG NULL, nothing to convert");
|
||||
// e.printStackTrace();
|
||||
}
|
||||
|
||||
|
||||
final String PTRN_QRG_CAT2_wholeQRGMHz4Digits = "(([0-9]{4}[\\.|,| ]?[0-9]{3})([\\.|,][\\d]{1,2})?)"; //1296.300.3 etc
|
||||
final String PTRN_QRG_CAT2_wholeQRGMHz3Digits = "(([0-9]{3}[\\.|,| ]?[0-9]{3})([\\.][\\d]{1,2})?)"; //144.300.3 etc
|
||||
final String PTRN_QRG_CAT2_QRGwithoutPrefix = "((\\b[0-4]{1}[\\d]{2}\\b)([\\.|,][\\d]{1,2}\\b)?)"; //144.300.3 etc
|
||||
|
||||
String stringAggregation = "";
|
||||
|
||||
if (testPattern(qrgString, PTRN_QRG_CAT2_wholeQRGMHz4Digits)) {//case 1296.200 or 1296.200.2 etc.
|
||||
stringAggregation = qrgString;
|
||||
|
||||
stringAggregation = stringAggregation.replace(".","");
|
||||
stringAggregation = stringAggregation.replace(",","");
|
||||
stringAggregation = stringAggregation.replace(" ", "");
|
||||
|
||||
if (stringAggregation.length() == 8) {
|
||||
String stringAggregationNew = stringAggregation.substring(0, stringAggregation.length()-1) + "." + stringAggregation.substring(stringAggregation.length()-1, stringAggregation.length());
|
||||
stringAggregation = stringAggregationNew + ".0";
|
||||
return stringAggregation;
|
||||
|
||||
} else if (stringAggregation.length() == 9) {
|
||||
String stringAggregationNew = stringAggregation.substring(0, stringAggregation.length()-2) + "." + stringAggregation.substring(stringAggregation.length()-2, stringAggregation.length());
|
||||
stringAggregation = stringAggregationNew;
|
||||
return stringAggregation;
|
||||
}
|
||||
|
||||
} else
|
||||
|
||||
if (testPattern(qrgString, PTRN_QRG_CAT2_wholeQRGMHz3Digits)) { //case 144.300 or 144.300.2
|
||||
stringAggregation = qrgString;
|
||||
|
||||
stringAggregation = stringAggregation.replace(".","");
|
||||
stringAggregation = stringAggregation.replace(",","");
|
||||
stringAggregation = stringAggregation.replace(" ", "");
|
||||
|
||||
if (stringAggregation.length() == 6) {
|
||||
stringAggregation = stringAggregation + ".0";
|
||||
return stringAggregation;
|
||||
}
|
||||
if (stringAggregation.length() == 7) {
|
||||
String stringAggregationNew = stringAggregation.substring(0, stringAggregation.length()-1) + "." + stringAggregation.substring(stringAggregation.length()-1, stringAggregation.length());
|
||||
stringAggregation = stringAggregationNew + ".0";
|
||||
return stringAggregation;
|
||||
|
||||
} else if (stringAggregation.length() == 8) {
|
||||
String stringAggregationNew = stringAggregation.substring(0, stringAggregation.length()-2) + "." + stringAggregation.substring(stringAggregation.length()-2, stringAggregation.length());
|
||||
stringAggregation = stringAggregationNew;
|
||||
return stringAggregation;
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
if (testPattern(qrgString, PTRN_QRG_CAT2_QRGwithoutPrefix)) { //case ".050 or .300 or something like that"
|
||||
stringAggregation = qrgString;
|
||||
|
||||
stringAggregation = stringAggregation.replace(".", "");
|
||||
stringAggregation = stringAggregation.replace(",", "");
|
||||
stringAggregation = stringAggregation.replace(" ", "");
|
||||
|
||||
if (stringAggregation.length() == 3) { // like 050 or 300
|
||||
String stringAggregationNew = optionalPrefix.getValue() + stringAggregation;
|
||||
stringAggregation = stringAggregationNew + ".0";
|
||||
return stringAggregation;
|
||||
|
||||
} else if (stringAggregation.length() == 4) { //like 050.2 --> 0502
|
||||
|
||||
stringAggregation = optionalPrefix.getValue() + stringAggregation;
|
||||
String stringAggregationNew = stringAggregation.substring(0, stringAggregation.length() - 1) + "." + stringAggregation.substring(stringAggregation.length() - 1, stringAggregation.length());
|
||||
stringAggregation = stringAggregationNew;
|
||||
return stringAggregation;
|
||||
|
||||
} else if (stringAggregation.length() == 5) { //like 050.20 --> 05020
|
||||
|
||||
stringAggregation = optionalPrefix.getValue() + stringAggregation;
|
||||
String stringAggregationNew = stringAggregation.substring(0, stringAggregation.length() - 2) + "." + stringAggregation.substring(stringAggregation.length() - 2, stringAggregation.length());
|
||||
stringAggregation = stringAggregationNew;
|
||||
return stringAggregation;
|
||||
}
|
||||
}
|
||||
|
||||
return stringAggregation; //if nothing else helps
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,7 +2,10 @@ package kst4contest.controller;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.*;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import kst4contest.ApplicationConstants;
|
||||
import kst4contest.model.ChatMessage;
|
||||
|
||||
/**
|
||||
@@ -17,7 +20,7 @@ public class WriteThread extends Thread {
|
||||
private ChatController client;
|
||||
private OutputStream output;
|
||||
|
||||
private ChatMessage messageTextRaw;
|
||||
private ChatMessage messageToBeSend;
|
||||
|
||||
public WriteThread(Socket socket, ChatController client) throws InterruptedException {
|
||||
this.socket = socket;
|
||||
@@ -25,7 +28,9 @@ public class WriteThread extends Thread {
|
||||
|
||||
try {
|
||||
output = socket.getOutputStream();
|
||||
writer = new PrintWriter(output, true);
|
||||
|
||||
writer = new PrintWriter(output, true, StandardCharsets.UTF_8);
|
||||
|
||||
} catch (IOException ex) {
|
||||
System.out.println("Error getting output stream: " + ex.getMessage());
|
||||
ex.printStackTrace();
|
||||
@@ -34,7 +39,8 @@ public class WriteThread extends Thread {
|
||||
|
||||
/**
|
||||
* This method is used to send a message to the server, raw formatted. E.g. for
|
||||
* the keepalive message.
|
||||
* the keepalive message. This method sends only in the main message-Category. To send it in a category
|
||||
* "defined by Chatmessage", use txByRxmsgCatOrigin(Chatmessage "toBeSend")
|
||||
*
|
||||
* @param messageToServer
|
||||
* @throws InterruptedException
|
||||
@@ -48,6 +54,33 @@ public class WriteThread extends Thread {
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This method is used to send a message directly to a receiver in a special chatcategory. The receivers category
|
||||
* will be read out of the Chatmessage.getChatCategory method. <b> The message text will be modified to fit kst
|
||||
* messageformat</b>
|
||||
*
|
||||
* @param messageToServer
|
||||
* @throws InterruptedException
|
||||
*/
|
||||
public void txByRxmsgCatOrigin(ChatMessage messageToServer) throws InterruptedException {
|
||||
|
||||
// writer.println(messageToServer.getMessage()); //kst4contest.test 4 23001
|
||||
// writer.flush(); //kst4contest.test 4 23001
|
||||
|
||||
String originalMessageText = messageToServer.getMessageText() + "";
|
||||
|
||||
String newMessageText = "";
|
||||
|
||||
newMessageText = ("MSG|" + messageToServer.getChatCategory().getCategoryNumber()
|
||||
+ "|0|" + originalMessageText + "|0|"); //original before 1.26
|
||||
|
||||
|
||||
System.out.println(newMessageText + "< sended to the writer (DIRECTED REPLY)");
|
||||
writer.println(newMessageText);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* This method gets a textmessage to the chat and adds some characters to hit
|
||||
* the neccessarry format to send a message in the on4kst chat either to another
|
||||
@@ -59,29 +92,28 @@ public class WriteThread extends Thread {
|
||||
public void txKSTFormatted(ChatMessage messageToServer) throws InterruptedException {
|
||||
|
||||
// writer.println(messageToServer.getMessageText());
|
||||
messageTextRaw = messageToServer;
|
||||
messageToBeSend = messageToServer;
|
||||
|
||||
try {
|
||||
|
||||
messageTextRaw = client.getMessageTXBus().take();
|
||||
messageToBeSend = client.getMessageTXBus().take();
|
||||
// this.client.getmesetChatsetServerready(true);
|
||||
|
||||
} catch (InterruptedException e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
String messageLine = messageTextRaw.getMessageText();
|
||||
String messageLine = messageToBeSend.getMessageText();
|
||||
|
||||
if (messageTextRaw.isMessageDirectedToServer()) {
|
||||
if (messageToBeSend.isMessageDirectedToServer()) {
|
||||
/**
|
||||
* We have to check if we only commands the server (keepalive) or want do talk
|
||||
* to the community
|
||||
*/
|
||||
|
||||
try {
|
||||
tx(messageTextRaw);
|
||||
System.out.println("BUS: tx: " + messageTextRaw.getMessageText());
|
||||
tx(messageToBeSend);
|
||||
System.out.println("BUS: tx: " + messageToBeSend.getMessageText());
|
||||
|
||||
} catch (InterruptedException e) {
|
||||
// TODO Auto-generated catch block
|
||||
@@ -95,8 +127,8 @@ public class WriteThread extends Thread {
|
||||
// ownMSG.setMessageText(
|
||||
// "MSG|" + this.client.getCategory().getCategoryNumber() + "|0|" + messageLine + "|0|");
|
||||
|
||||
ownMSG.setMessageText("MSG|" + this.client.getChatPreferences().getLoginChatCategory().getCategoryNumber()
|
||||
+ "|0|" + messageLine + "|0|");
|
||||
ownMSG.setMessageText("MSG|" + this.client.getChatPreferences().getLoginChatCategoryMain().getCategoryNumber()
|
||||
+ "|0|" + messageLine + "|0|"); //original before 1.26
|
||||
|
||||
try {
|
||||
tx(ownMSG);
|
||||
@@ -108,7 +140,7 @@ public class WriteThread extends Thread {
|
||||
}
|
||||
}
|
||||
|
||||
if (messageTextRaw.equals("/QUIT")) {
|
||||
if (messageToBeSend.equals("/QUIT")) {
|
||||
try {
|
||||
this.client.getReadThread().terminateConnection();
|
||||
this.client.getReadThread().interrupt();
|
||||
@@ -117,7 +149,6 @@ public class WriteThread extends Thread {
|
||||
this.interrupt();
|
||||
|
||||
} catch (IOException e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
@@ -137,59 +168,62 @@ public class WriteThread extends Thread {
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
messageTextRaw = client.getMessageTXBus().take();
|
||||
messageToBeSend = client.getMessageTXBus().take();
|
||||
|
||||
if (messageTextRaw.getMessageText().equals("POISONPILL_KILLTHREAD")
|
||||
&& messageTextRaw.getMessageSenderName().equals("POISONPILL_KILLTHREAD")) {
|
||||
if (messageToBeSend.getMessageText().equals(ApplicationConstants.DISCONNECT_RDR_POISONPILL)
|
||||
&& messageToBeSend.getMessageSenderName().equals(ApplicationConstants.DISCONNECT_RDR_POISONPILL)) {
|
||||
client.getMessageRXBus().clear();
|
||||
this.interrupt();
|
||||
break;
|
||||
} else {
|
||||
String messageLine = messageTextRaw.getMessageText();
|
||||
String messageLine = messageToBeSend.getMessageText();
|
||||
|
||||
if (messageTextRaw.isMessageDirectedToServer()) {
|
||||
if (messageToBeSend.isMessageDirectedToServer()) {
|
||||
/**
|
||||
* We have to check if we only commands the server (keepalive) or want do talk
|
||||
* to the community
|
||||
*/
|
||||
|
||||
try {
|
||||
tx(messageTextRaw);
|
||||
System.out.println("BUS: tx: " + messageTextRaw.getMessageText());
|
||||
tx(messageToBeSend);
|
||||
System.out.println("BUS: tx: " + messageToBeSend.getMessageText());
|
||||
|
||||
} catch (InterruptedException e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
} else {
|
||||
} else { //message is not directed to the server, it´s directed to all or to a station
|
||||
|
||||
ChatMessage ownMSG = new ChatMessage();
|
||||
if (messageToBeSend.getChatCategory() == this.client.getChatCategoryMain() || messageToBeSend.getChatCategory() == this.client.getChatCategorySecondChat()) {
|
||||
|
||||
// ownMSG.setMessageText(
|
||||
// "MSG|" + this.client.getCategory().getCategoryNumber() + "|0|" + messageLine + "|0|");
|
||||
txByRxmsgCatOrigin(messageToBeSend);
|
||||
|
||||
ownMSG.setMessageText(
|
||||
"MSG|" + this.client.getChatPreferences().getLoginChatCategory().getCategoryNumber() + "|0|"
|
||||
+ messageLine + "|0|");
|
||||
} else { //default bhv if destination cat is not detectable
|
||||
|
||||
try {
|
||||
tx(ownMSG);
|
||||
System.out.println("BUS: tx: " + ownMSG.getMessageText());
|
||||
|
||||
} catch (InterruptedException e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
ChatMessage ownMSG = new ChatMessage();
|
||||
|
||||
ownMSG.setMessageText(
|
||||
"MSG|" + this.client.getChatPreferences().getLoginChatCategoryMain().getCategoryNumber() + "|0|"
|
||||
+ messageLine + "|0|");
|
||||
|
||||
try {
|
||||
tx(ownMSG);
|
||||
System.out.println("WT: tx (raw): " + ownMSG.getMessageText());
|
||||
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
System.out.println("WritheTh: got message out of the queue: " + messageTextRaw.getMessageText());
|
||||
System.out.println("WritheTh: got message out of the queue: " + messageToBeSend.getMessageText());
|
||||
|
||||
// this.client.getmesetChatsetServerready(true);
|
||||
|
||||
} catch (InterruptedException e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
client.getMessageTXBus().clear();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package kst4contest.controller.interfaces;
|
||||
|
||||
public interface PstRotatorEventListener {
|
||||
|
||||
void onAzimuthUpdate(double azimuth);
|
||||
void onElevationUpdate(double elevation);
|
||||
void onModeUpdate(boolean isTracking); // true = Tracking, false = Manual
|
||||
void onMessageReceived(String rawMessage); // Debugging usage
|
||||
// void setRotorPosition(double azimuth);
|
||||
|
||||
}
|
||||
|
||||
|
||||
110
src/main/java/kst4contest/locatorUtils/DirectionUtils.java
Normal file
110
src/main/java/kst4contest/locatorUtils/DirectionUtils.java
Normal file
@@ -0,0 +1,110 @@
|
||||
package kst4contest.locatorUtils;
|
||||
|
||||
public class DirectionUtils {
|
||||
|
||||
/**
|
||||
* Checks wheter a sked-sender writes to a sked-receiver and is in my direction due he beams to this receiver
|
||||
*
|
||||
* @param myLocator
|
||||
* @param locatorOfSkedSender
|
||||
* @param locatorOfSekdReceiver
|
||||
* @param maxRangeKm
|
||||
* @param hisAntennaBeamWidth
|
||||
* @return
|
||||
*/
|
||||
public static boolean isInAngleAndRange(String myLocator, String locatorOfSkedSender, String locatorOfSekdReceiver, double maxRangeKm, double hisAntennaBeamWidth) {
|
||||
|
||||
Location myLocation = new Location(myLocator);
|
||||
Location skedSenderLocation = new Location(locatorOfSkedSender);
|
||||
Location skedReceiverLocation = new Location(locatorOfSekdReceiver);
|
||||
|
||||
double distanceFromMeToLocSender = new Location(myLocator).getDistanceKm(new Location(locatorOfSkedSender));
|
||||
|
||||
// Check if distance exceeds my setted maximum range
|
||||
if (distanceFromMeToLocSender > maxRangeKm) {
|
||||
System.out.println("too far, " + distanceFromMeToLocSender + " km");
|
||||
return false;
|
||||
}
|
||||
|
||||
//check bearing of sender to receiver
|
||||
|
||||
double bearingOfSekdSenderToSkedReceiver = skedSenderLocation.getBearing(skedReceiverLocation);
|
||||
// System.out.println("skedTX -> skedTX deg: " + bearingOfSekdSenderToSkedReceiver);
|
||||
|
||||
double bearingOfSekdSenderToMe = skedSenderLocation.getBearing(myLocation);
|
||||
// System.out.println("skedTX -> me deg: " + bearingOfSekdSenderToMe);
|
||||
|
||||
/**
|
||||
* simple mech works
|
||||
*/
|
||||
// if (bearingOfSekdSenderToMe >= bearingOfSekdSenderToSkedReceiver) {
|
||||
// if (bearingOfSekdSenderToMe-bearingOfSekdSenderToSkedReceiver <= hisAntennaBeamWidth/2){
|
||||
// System.out.println(bearingOfSekdSenderToMe-bearingOfSekdSenderToSkedReceiver + " <= " + hisAntennaBeamWidth);
|
||||
// return true;
|
||||
// }
|
||||
// } else if ((bearingOfSekdSenderToMe <= bearingOfSekdSenderToSkedReceiver)) {
|
||||
// if (bearingOfSekdSenderToSkedReceiver-bearingOfSekdSenderToMe <= hisAntennaBeamWidth/2){
|
||||
// return true;
|
||||
// }
|
||||
// } else return false;
|
||||
/**
|
||||
* simple mech end
|
||||
*/
|
||||
|
||||
if (DirectionUtils.isAngleInRange(bearingOfSekdSenderToSkedReceiver, bearingOfSekdSenderToMe, hisAntennaBeamWidth)) {
|
||||
//I may should get "/2" because of 50% of the 3dB opening angle if txer is directed to sender exactly
|
||||
// System.out.println("------------> isinangleandrange!");
|
||||
return true;
|
||||
} else {
|
||||
// System.out.println("not in angle and reach");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Tests, if the angle (from me to) other station is in the range of the
|
||||
* angle (qtf) in degrees where my antenna points to.
|
||||
*
|
||||
* @param toForeignAngle [degrees]
|
||||
* @param mySelectedQTFAngle [degrees]
|
||||
* @param antennaBeamwidth [degrees]
|
||||
* @return
|
||||
*/
|
||||
public static boolean isAngleInRange(double toForeignAngle,
|
||||
double mySelectedQTFAngle, double antennaBeamwidth) {
|
||||
|
||||
double beamwidth = antennaBeamwidth / 2; // half left, half right
|
||||
|
||||
double startAngle = mySelectedQTFAngle - beamwidth;
|
||||
double endAngle = mySelectedQTFAngle + beamwidth;
|
||||
|
||||
// Normalize angles to be between 0 and 360 degrees
|
||||
toForeignAngle = normalizeAngle(toForeignAngle);
|
||||
startAngle = normalizeAngle(startAngle);
|
||||
endAngle = normalizeAngle(endAngle);
|
||||
|
||||
// Check if the range wraps around 360 degrees
|
||||
if (startAngle <= endAngle) {
|
||||
return toForeignAngle >= startAngle && toForeignAngle <= endAngle;
|
||||
} else {
|
||||
// Range wraps around 360 degrees, so check if angle is within the
|
||||
// range or outside the range
|
||||
return toForeignAngle >= startAngle || toForeignAngle <= endAngle;
|
||||
}
|
||||
}
|
||||
|
||||
private static double normalizeAngle(double angle) {
|
||||
if (angle < 0) {
|
||||
angle += 360;
|
||||
}
|
||||
if (angle >= 360) {
|
||||
angle -= 360;
|
||||
}
|
||||
return angle;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -1,5 +1,10 @@
|
||||
package kst4contest.locatorUtils;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.NumberFormat;
|
||||
import java.text.ParseException;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Location class with methods allowing conversion to and from Maidenhead
|
||||
* locator (grid squares) based off of
|
||||
@@ -203,6 +208,27 @@ public class Location {
|
||||
return getDistanceKm(this, loc2);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param locator1 6 letter location string
|
||||
* @param locator2 6 letter location string
|
||||
* @return great circle distance in kilometers
|
||||
*/
|
||||
public double getDistanceKmByTwoLocatorStrings(String locator1,String locator2 ) {
|
||||
Location loc1 = new Location(locator1);
|
||||
Location loc2 = new Location(locator2);
|
||||
|
||||
Locale locale = new Locale("en", "UK");
|
||||
String pattern = "###.##";
|
||||
|
||||
DecimalFormat decimalFormat = (DecimalFormat)
|
||||
NumberFormat.getNumberInstance(locale);
|
||||
decimalFormat.applyPattern(pattern);
|
||||
|
||||
String format = decimalFormat.format(loc1.getDistanceKm(loc2));
|
||||
|
||||
return Double.parseDouble(format);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param loc2
|
||||
* second location
|
||||
@@ -278,6 +304,19 @@ public class Location {
|
||||
return getBearing(this, loc2);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @return bearing in degrees
|
||||
*/
|
||||
public double getBearingOfTwoLocatorStrings(String locator1, String locator2) {
|
||||
|
||||
Location loc1 = new Location(locator1);
|
||||
Location loc2 = new Location(locator2);
|
||||
|
||||
return getBearing(loc1, loc2);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param loc1
|
||||
* source location
|
||||
@@ -300,6 +339,21 @@ public class Location {
|
||||
- Math.sin(loc1.getLatitude().getRadians())
|
||||
* Math.cos(loc2.getLatitude().getRadians())
|
||||
* Math.cos(dLon);
|
||||
return (Angle.radiansToDegrees(Math.atan2(y, x)) + 360) % 360;
|
||||
|
||||
double bearing = (Angle.radiansToDegrees(Math.atan2(y, x)) + 360) % 360;
|
||||
|
||||
// return bearing;
|
||||
|
||||
|
||||
Locale locale = new Locale("en", "UK");
|
||||
String pattern = "###.##";
|
||||
|
||||
DecimalFormat decimalFormat = (DecimalFormat)
|
||||
NumberFormat.getNumberInstance(locale);
|
||||
decimalFormat.applyPattern(pattern);
|
||||
|
||||
String format = decimalFormat.format(bearing);
|
||||
|
||||
return Double.parseDouble(format);
|
||||
}
|
||||
}
|
||||
|
||||
43
src/main/java/kst4contest/locatorUtils/TestLocatorUtils.java
Normal file
43
src/main/java/kst4contest/locatorUtils/TestLocatorUtils.java
Normal file
@@ -0,0 +1,43 @@
|
||||
package kst4contest.locatorUtils;
|
||||
|
||||
public class TestLocatorUtils {
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
|
||||
// isInAngle(myLocation, location1, location2);
|
||||
System.out.println(isInAngleAndRange("JN49FL", "jo43xm", "jo30sa", 900, 50));
|
||||
System.out.println(isInAngleAndRange("JN49FL", "jo51ij", "jn39oc", 900, 50));
|
||||
System.out.println(isInAngleAndRange("JN49FL", "jn39oc", "jo51ij", 1100, 50));
|
||||
}
|
||||
|
||||
public static boolean isInAngleAndRange(String myLocator, String locatorOfSkedSender, String locatorOfSekdReceiver, double maxRangeKm, double hisAntennaBeamWidth) {
|
||||
|
||||
Location myLocation = new Location(myLocator);
|
||||
Location skedSenderLocation = new Location(locatorOfSkedSender);
|
||||
Location skedReceiverLocation = new Location(locatorOfSekdReceiver);
|
||||
|
||||
double distanceFromMeToLocSender = new Location(myLocator).getDistanceKm(new Location(locatorOfSkedSender));
|
||||
|
||||
// Check if distance exceeds my setted maximum range
|
||||
if (distanceFromMeToLocSender > maxRangeKm) {
|
||||
System.out.println("too far, " + distanceFromMeToLocSender + " km");
|
||||
return false;
|
||||
}
|
||||
|
||||
//check bearing of sender to receiver
|
||||
|
||||
double bearingOfSekdSenderToSkedReceiver = skedSenderLocation.getBearing(skedReceiverLocation);
|
||||
System.out.println("skedTX -> skedRX deg: " + bearingOfSekdSenderToSkedReceiver);
|
||||
|
||||
double bearingOfSekdSenderToMe = skedSenderLocation.getBearing(myLocation);
|
||||
System.out.println("skedTX -> me deg: " + bearingOfSekdSenderToMe);
|
||||
|
||||
if (DirectionUtils.isAngleInRange(bearingOfSekdSenderToSkedReceiver, bearingOfSekdSenderToMe, hisAntennaBeamWidth)) {
|
||||
//may I should get "/2" because of 50% of the 3dB opening angle if txer is directed to sender exactly
|
||||
return true;
|
||||
} else return false;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
292
src/main/java/kst4contest/logic/PriorityCalculator.java
Normal file
292
src/main/java/kst4contest/logic/PriorityCalculator.java
Normal file
@@ -0,0 +1,292 @@
|
||||
package kst4contest.logic;
|
||||
|
||||
import kst4contest.controller.StationMetricsService;
|
||||
import kst4contest.model.*;
|
||||
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Priority score calculation (off FX-thread).
|
||||
*
|
||||
* Notes:
|
||||
* - Score is computed once per callsignRaw by ScoreService.
|
||||
* - This calculator MUST be pure (no UI calls) and fast.
|
||||
*/
|
||||
public class PriorityCalculator {
|
||||
|
||||
/** Max age for "known active bands" (derived from chat history). */
|
||||
private static final long RX_BANDS_MAX_AGE_MS = 30L * 60L * 1000L; // 30 minutes
|
||||
|
||||
public double calculatePriority(ChatMember member,
|
||||
ChatPreferences prefs,
|
||||
List<ContestSked> activeSkeds,
|
||||
StationMetricsService.Snapshot metricsSnapshot,
|
||||
long nowEpochMs) {
|
||||
|
||||
if (member == null || prefs == null) return 0.0;
|
||||
|
||||
final String callRaw = normalize(member.getCallSignRaw());
|
||||
if (callRaw == null || callRaw.isBlank()) return 0.0;
|
||||
|
||||
// --------------------------------------------------------------------
|
||||
// 1) HARD FILTER: reachable hardware + "already worked on all possible bands"
|
||||
// --------------------------------------------------------------------
|
||||
// --------------------------------------------------------------------
|
||||
// 1) HARD FILTER: reachable hardware + "already worked on all possible bands"
|
||||
// --------------------------------------------------------------------
|
||||
EnumSet<Band> myEnabledBands = getMyEnabledBands(prefs);
|
||||
|
||||
// "worked" for scoring is derived ONLY from per-band flags (worked144/432/...)
|
||||
// IMPORTANT: ChatMember.worked is UI-only and NOT used in scoring.
|
||||
EnumSet<Band> workedBandsForScoring = getWorkedBands(member);
|
||||
|
||||
// Remaining bands that are:
|
||||
// - recently offered by the station (from knownActiveBands history)
|
||||
// - enabled at our station
|
||||
// - NOT worked yet (per-band flags)
|
||||
// If we do not know offered bands (history empty), this remains empty.
|
||||
EnumSet<Band> unworkedPossible = EnumSet.noneOf(Band.class);
|
||||
|
||||
EnumSet<Band> stationOfferedBands = getStationOfferedBandsFromHistory(member, nowEpochMs);
|
||||
EnumSet<Band> possibleBands = stationOfferedBands.isEmpty()
|
||||
? EnumSet.noneOf(Band.class) // unknown => don't hard-filter
|
||||
: EnumSet.copyOf(stationOfferedBands);
|
||||
|
||||
if (!possibleBands.isEmpty()) {
|
||||
possibleBands.retainAll(myEnabledBands);
|
||||
if (possibleBands.isEmpty()) {
|
||||
// We know their bands, but none of them are enabled at our station.
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
unworkedPossible = EnumSet.copyOf(possibleBands);
|
||||
unworkedPossible.removeAll(workedBandsForScoring);
|
||||
|
||||
// If already worked on all possible bands => no priority on them anymore (contest logic).
|
||||
if (unworkedPossible.isEmpty()) {
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------
|
||||
// 2) BASE SCORE
|
||||
// --------------------------------------------------------------------
|
||||
double score = 100.0;
|
||||
|
||||
// if (!member.isWorked()) {
|
||||
// score += 200.0;
|
||||
// }
|
||||
|
||||
//"worked" for scoring is derived ONLY from per-band flags (worked144/432/...)
|
||||
// EnumSet<Band> workedBandsForScoring = getWorkedBands(member);
|
||||
|
||||
if (workedBandsForScoring.isEmpty()) {
|
||||
score += 200.0; // never worked on any supported band -> higher priority
|
||||
} else {
|
||||
score -= 150.0; // already worked on at least one band -> lower base priority
|
||||
}
|
||||
|
||||
|
||||
// Multi-band bonus: if they offer >1 possible band and we worked at least one, prefer them
|
||||
if (!possibleBands.isEmpty()) {
|
||||
int bandCount = possibleBands.size();
|
||||
score += (bandCount - 1) * 80.0;
|
||||
}
|
||||
|
||||
// Optional: band-upgrade visibility boost
|
||||
// If the station is already worked on at least one band, but is still QRV on other unworked enabled band(s),
|
||||
// we can optionally add a boost so it remains visible in the list.
|
||||
if (prefs.isNotify_bandUpgradePriorityBoostEnabled()
|
||||
&& !workedBandsForScoring.isEmpty()
|
||||
&& !unworkedPossible.isEmpty()) {
|
||||
score += 180.0; // tuned visibility boost
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------
|
||||
// 3) DISTANCE ("Goldilocks Zone")
|
||||
// --------------------------------------------------------------------
|
||||
double distKm = member.getQrb() == null ? 0.0 : member.getQrb();
|
||||
|
||||
if (distKm > 0) {
|
||||
if (distKm < 200) {
|
||||
score *= 0.7;
|
||||
} else if (distKm > prefs.getStn_maxQRBDefault()) {
|
||||
score *= 0.3;
|
||||
} else {
|
||||
score *= 1.15;
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------
|
||||
// 4) AIRSCOUT BOOST
|
||||
// --------------------------------------------------------------------
|
||||
AirPlaneReflectionInfo apInfo = member.getAirPlaneReflectInfo();
|
||||
if (apInfo != null && apInfo.getAirPlanesReachableCntr() > 0) {
|
||||
score += 200;
|
||||
|
||||
int nextMinutes = findNextAirplaneArrivingMinutes(apInfo);
|
||||
if (nextMinutes == 0) score += 120;
|
||||
else if (nextMinutes == 1) score += 60;
|
||||
else if (nextMinutes == 2) score += 30;
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------
|
||||
// 5) BOOST IDEA #1: Beam direction match (within beamwidth)
|
||||
// --------------------------------------------------------------------
|
||||
if (member.getQTFdirection() != null) {
|
||||
double myAz = prefs.getActualQTF().getValue();
|
||||
double targetAz = member.getQTFdirection();
|
||||
double diff = minimalAngleDiffDeg(myAz, targetAz);
|
||||
|
||||
double halfBeam = Math.max(1.0, prefs.getStn_antennaBeamWidthDeg()) / 2.0;
|
||||
if (diff <= halfBeam) {
|
||||
double centerFactor = 1.0 - (diff / halfBeam); // 1.0 center -> 0.0 edge
|
||||
score += 80.0 + (120.0 * centerFactor);
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------
|
||||
// 6) BOOST IDEA #3: Conversation momentum (recent inbound burst)
|
||||
// --------------------------------------------------------------------
|
||||
if (metricsSnapshot != null) {
|
||||
StationMetricsService.Snapshot.Metrics mx = metricsSnapshot.get(callRaw);
|
||||
if (mx != null) {
|
||||
long ageMs = mx.lastInboundEpochMs > 0 ? (nowEpochMs - mx.lastInboundEpochMs) : Long.MAX_VALUE;
|
||||
|
||||
// "Active now" bonus
|
||||
if (ageMs < 60_000) score += 120;
|
||||
else if (ageMs < 3 * 60_000) score += 60;
|
||||
|
||||
// Momentum bonus: multiple lines in the configured window
|
||||
int cnt = mx.inboundCountInWindow;
|
||||
if (cnt >= 6) score += 160;
|
||||
else if (cnt >= 4) score += 110;
|
||||
else if (cnt >= 2) score += 60;
|
||||
|
||||
// Positive signal (configurable)
|
||||
if (mx.lastPositiveSignalEpochMs > 0 && (nowEpochMs - mx.lastPositiveSignalEpochMs) < 5 * 60_000) {
|
||||
score += 120;
|
||||
}
|
||||
|
||||
// Reply time: prefer fast responders
|
||||
if (mx.avgResponseTimeMs > 0) {
|
||||
if (mx.avgResponseTimeMs < 60_000) score += 80;
|
||||
else if (mx.avgResponseTimeMs < 3 * 60_000) score += 40;
|
||||
}
|
||||
|
||||
// No-reply penalty (automatic failed attempt)
|
||||
if (mx.noReplyStrikes > 0) {
|
||||
score /= (1.0 + (mx.noReplyStrikes * 0.6));
|
||||
}
|
||||
|
||||
// Manual sked fail (path likely bad) => strong, permanent penalty until reset
|
||||
if (mx.manualSkedFailed) {
|
||||
score *= 0.15;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------
|
||||
// 7) BOOST IDEA #4: Sked commitment ramp-up
|
||||
// --------------------------------------------------------------------
|
||||
if (activeSkeds != null && !activeSkeds.isEmpty()) {
|
||||
for (ContestSked sked : activeSkeds) {
|
||||
if (sked == null) continue;
|
||||
|
||||
if (!callRaw.equals(normalize(sked.getTargetCallsign()))) continue;
|
||||
|
||||
long seconds = sked.getTimeUntilSkedSeconds();
|
||||
|
||||
// Imminent sked: absolute priority (T-3min..T+1min)
|
||||
if (seconds < 180 && seconds > -60) {
|
||||
score += 5000;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ramp: 0..15 minutes before => up to +1200
|
||||
if (seconds >= 0 && seconds <= 15 * 60) {
|
||||
double t = (15 * 60 - seconds) / (15.0 * 60.0); // 0.0..1.0
|
||||
score += 300 + (900 * t);
|
||||
} else if (seconds > 15 * 60) {
|
||||
score += 40;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------
|
||||
// 8) Legacy penalty: failed attempts in ChatMember
|
||||
// --------------------------------------------------------------------
|
||||
if (member.getFailedQSOAttempts() > 0) {
|
||||
score = score / (member.getFailedQSOAttempts() + 1);
|
||||
}
|
||||
|
||||
return Math.max(0.0, score);
|
||||
}
|
||||
|
||||
private static EnumSet<Band> getMyEnabledBands(ChatPreferences prefs) {
|
||||
EnumSet<Band> out = EnumSet.noneOf(Band.class);
|
||||
if (prefs.isStn_bandActive144()) out.add(Band.B_144);
|
||||
if (prefs.isStn_bandActive432()) out.add(Band.B_432);
|
||||
if (prefs.isStn_bandActive1240()) out.add(Band.B_1296);
|
||||
if (prefs.isStn_bandActive2300()) out.add(Band.B_2320);
|
||||
if (prefs.isStn_bandActive3400()) out.add(Band.B_3400);
|
||||
if (prefs.isStn_bandActive5600()) out.add(Band.B_5760);
|
||||
if (prefs.isStn_bandActive10G()) out.add(Band.B_10G);
|
||||
return out;
|
||||
}
|
||||
|
||||
private static EnumSet<Band> getStationOfferedBandsFromHistory(ChatMember member, long nowEpochMs) {
|
||||
EnumSet<Band> out = EnumSet.noneOf(Band.class);
|
||||
Map<Band, ChatMember.ActiveFrequencyInfo> map = member.getKnownActiveBands();
|
||||
if (map == null || map.isEmpty()) return out;
|
||||
|
||||
for (Map.Entry<Band, ChatMember.ActiveFrequencyInfo> e : map.entrySet()) {
|
||||
if (e == null || e.getKey() == null || e.getValue() == null) continue;
|
||||
long age = nowEpochMs - e.getValue().timestampEpoch;
|
||||
if (age <= RX_BANDS_MAX_AGE_MS) {
|
||||
out.add(e.getKey());
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private static EnumSet<Band> getWorkedBands(ChatMember member) {
|
||||
EnumSet<Band> out = EnumSet.noneOf(Band.class);
|
||||
if (member.isWorked144()) out.add(Band.B_144);
|
||||
if (member.isWorked432()) out.add(Band.B_432);
|
||||
if (member.isWorked1240()) out.add(Band.B_1296);
|
||||
if (member.isWorked2300()) out.add(Band.B_2320);
|
||||
if (member.isWorked3400()) out.add(Band.B_3400);
|
||||
if (member.isWorked5600()) out.add(Band.B_5760);
|
||||
if (member.isWorked10G()) out.add(Band.B_10G);
|
||||
if (member.isWorked24G()) out.add(Band.B_24G);
|
||||
return out;
|
||||
}
|
||||
|
||||
private static int findNextAirplaneArrivingMinutes(AirPlaneReflectionInfo apInfo) {
|
||||
try {
|
||||
if (apInfo.getRisingAirplanes() == null || apInfo.getRisingAirplanes().isEmpty()) return -1;
|
||||
|
||||
int min = Integer.MAX_VALUE;
|
||||
for (AirPlane ap : apInfo.getRisingAirplanes()) {
|
||||
if (ap == null) continue;
|
||||
min = Math.min(min, ap.getArrivingDurationMinutes());
|
||||
}
|
||||
return min == Integer.MAX_VALUE ? -1 : min;
|
||||
} catch (Exception ignore) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
private static double minimalAngleDiffDeg(double a, double b) {
|
||||
double diff = Math.abs((a - b) % 360.0);
|
||||
return diff > 180.0 ? 360.0 - diff : diff;
|
||||
}
|
||||
|
||||
private static String normalize(String s) {
|
||||
if (s == null) return null;
|
||||
return s.trim().toUpperCase();
|
||||
}
|
||||
}
|
||||
51
src/main/java/kst4contest/logic/SignalDetector.java
Normal file
51
src/main/java/kst4contest/logic/SignalDetector.java
Normal file
@@ -0,0 +1,51 @@
|
||||
package kst4contest.logic;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Lightweight positive-signal detector.
|
||||
*
|
||||
* Patterns are configured via a single preference string, delimited by ';' or newlines.
|
||||
* Examples: "QRV;READY;RX OK;RGR;TNX;TU;HRD"
|
||||
*/
|
||||
public final class SignalDetector {
|
||||
|
||||
private static final AtomicReference<String> lastPatterns = new AtomicReference<>("");
|
||||
private static final AtomicReference<List<Pattern>> cached = new AtomicReference<>(List.of());
|
||||
|
||||
private SignalDetector() {}
|
||||
|
||||
public static boolean containsPositiveSignal(String messageText, String patternsDelimited) {
|
||||
if (messageText == null || messageText.isBlank()) return false;
|
||||
List<Pattern> patterns = compileIfChanged(patternsDelimited);
|
||||
|
||||
String txt = messageText.toUpperCase();
|
||||
for (Pattern p : patterns) {
|
||||
if (p.matcher(txt).find()) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static List<Pattern> compileIfChanged(String patternsDelimited) {
|
||||
String p = patternsDelimited == null ? "" : patternsDelimited.trim();
|
||||
String prev = lastPatterns.get();
|
||||
if (p.equals(prev)) return cached.get();
|
||||
|
||||
List<Pattern> out = new ArrayList<>();
|
||||
for (String token : p.split("[;\\n\\r]+")) {
|
||||
String t = token.trim();
|
||||
if (t.isEmpty()) continue;
|
||||
|
||||
// plain substring match, but regex-safe
|
||||
String regex = Pattern.quote(t.toUpperCase());
|
||||
out.add(Pattern.compile(regex));
|
||||
}
|
||||
|
||||
lastPatterns.set(p);
|
||||
cached.set(List.copyOf(out));
|
||||
return out;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,25 @@ package kst4contest.model;
|
||||
public class AirPlane {
|
||||
|
||||
String apCallSign, apSizeCategory;
|
||||
String potencialDescriptionAsWord;
|
||||
|
||||
public String getPotencialDescriptionAsWord() {
|
||||
if (this.getPotential() <=50) {
|
||||
return "small AP";
|
||||
} else if (this.getPotential() <=75 && this.getPotential() > 50) {
|
||||
return "big AP";
|
||||
} else if (this.getPotential() > 75) {
|
||||
return "very big AP";
|
||||
}
|
||||
|
||||
|
||||
return potencialDescriptionAsWord;
|
||||
}
|
||||
|
||||
public void setPotencialDescriptionAsWord(String potencialDescriptionAsWord) {
|
||||
this.potencialDescriptionAsWord = potencialDescriptionAsWord;
|
||||
}
|
||||
|
||||
int distanceKm, potential, arrivingDurationMinutes;
|
||||
public String getApCallSign() {
|
||||
return apCallSign;
|
||||
|
||||
49
src/main/java/kst4contest/model/Band.java
Normal file
49
src/main/java/kst4contest/model/Band.java
Normal file
@@ -0,0 +1,49 @@
|
||||
package kst4contest.model;
|
||||
|
||||
/**
|
||||
* Represents Amateur Radio Bands and their physical limits.
|
||||
* Used for plausibility checks in the Smart Parser.
|
||||
*/
|
||||
public enum Band {
|
||||
B_144(144.000, 146.000, "144"),
|
||||
B_432(432.000, 434.000, "432"),
|
||||
B_1296(1296.000, 1298.000, "1296"),
|
||||
B_2320(2320.000, 2322.000, "2320"),
|
||||
B_3400(3400.000, 3410.000, "3400"),
|
||||
B_5760(5760.000, 5762.000, "5760"),
|
||||
B_10G(10368.000, 10370.000, "10368"),
|
||||
B_24G(24048.000, 24050.000, "24048");
|
||||
// more space for future usage
|
||||
|
||||
private final double minFreq;
|
||||
private final double maxFreq;
|
||||
private final String prefix; // Default prefix for "short value" parsing (e.g., .210)
|
||||
|
||||
Band(double min, double max, String prefix) {
|
||||
this.minFreq = min;
|
||||
this.maxFreq = max;
|
||||
this.prefix = prefix;
|
||||
}
|
||||
|
||||
public String getPrefix() {
|
||||
return prefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a specific frequency falls within this band's limits.
|
||||
*/
|
||||
public boolean isPlausible(double freq) {
|
||||
return freq >= minFreq && freq <= maxFreq;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to find the matching Band enum for a given frequency.
|
||||
* Returns null if no band matches.
|
||||
*/
|
||||
public static Band fromFrequency(double freq) {
|
||||
for (Band b : values()) {
|
||||
if (b.isPlausible(freq)) return b;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -53,28 +53,17 @@ public class ChatCategory {
|
||||
|
||||
public ChatCategory(int setThiscategoryNumber) {
|
||||
this.categoryNumber = setThiscategoryNumber;
|
||||
setCategoryNumber(setThiscategoryNumber);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public int getCategoryNumber() {
|
||||
return categoryNumber;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public void setCategoryNumber(int categoryNumber) {
|
||||
this.categoryNumber = categoryNumber;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Returns an Array of int with possible frequency prefixes, due to in the chat
|
||||
* normally the following format is used (not ever): <br/>
|
||||
|
||||
@@ -1,31 +1,47 @@
|
||||
package kst4contest.model;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
|
||||
public class ChatMember {
|
||||
|
||||
|
||||
long lastFlagsChangeEpochMs; // timestamp of the last worked/not-QRV flag change in the internal DB
|
||||
|
||||
// private final BooleanProperty workedInfoChangeFireListEventTrigger = new SimpleBooleanProperty();
|
||||
AirPlaneReflectionInfo airPlaneReflectInfo;
|
||||
String callSign;
|
||||
String qra;
|
||||
String name;
|
||||
String callSignRaw; //without -2 or -70 etc.
|
||||
|
||||
|
||||
|
||||
boolean isInAngleAndRange; //if he tries a sked in my dir, he is in range, will process that in the messages
|
||||
|
||||
// String frequency; // last known qrg of the station
|
||||
|
||||
StringProperty frequency = new SimpleStringProperty();
|
||||
|
||||
String password; // only used by own instance of the chatmember instance to login to the chat
|
||||
ChatCategory chatCategory; // only used by own instance of the chatmember instance to login to the chat
|
||||
ChatCategory chatCategory; //Source category
|
||||
// ChatCategory chatCategory;//only used by own instance of the chatmember instance to login to the chat
|
||||
|
||||
long activityCounter; // time of last activity in epochtimesec
|
||||
long activityTimeLastInEpoch; // time of last activity in epochtimesec
|
||||
Date lastActivity; // time of last activity in epochtimesec
|
||||
Date lastActualizationTimeOfThisMember; // time of last state change if that member
|
||||
int qrb;
|
||||
Double qrb;
|
||||
int state;
|
||||
|
||||
int QTFdirection; // antenna direction in deg
|
||||
Double QTFdirection; // antenna direction in deg
|
||||
int[] workedCategories; // Chatcategory where the station is in the log, see kst4contest.model.ChatCategory
|
||||
|
||||
boolean worked; // true if the callsign is logged already - for temporary worked processing
|
||||
@@ -36,6 +52,62 @@ public class ChatMember {
|
||||
boolean worked3400;
|
||||
boolean worked5600;
|
||||
boolean worked10G;
|
||||
boolean Worked50;
|
||||
boolean Worked70;
|
||||
boolean Worked24G;
|
||||
boolean Worked47G;
|
||||
boolean Worked76G;
|
||||
|
||||
|
||||
/**
|
||||
* Chatmember is qrv at all band except we initialize anything other, depending to user entry
|
||||
*/
|
||||
boolean qrv144 = true;
|
||||
boolean qrv432 = true;
|
||||
boolean qrv1240 = true;
|
||||
boolean qrv2300 = true;
|
||||
boolean qrv3400 = true;
|
||||
boolean qrv5600 = true;
|
||||
boolean qrv10G = true;
|
||||
boolean qrvAny = true;
|
||||
|
||||
// Stores the last known frequency per band (Context History)
|
||||
private final Map<Band, ActiveFrequencyInfo> knownActiveBands = new ConcurrentHashMap<>();
|
||||
|
||||
|
||||
// --- INNER CLASS FOR QRG HISTORY ---
|
||||
public class ActiveFrequencyInfo {
|
||||
public double frequency;
|
||||
public long timestampEpoch;
|
||||
|
||||
public ActiveFrequencyInfo(double freq) {
|
||||
this.frequency = freq;
|
||||
this.timestampEpoch = System.currentTimeMillis();
|
||||
}
|
||||
}
|
||||
|
||||
// Counter for failed calls (Penalty Logic)
|
||||
private int failedQSOAttempts = 0;
|
||||
|
||||
// Calculated Score for sorting the user list
|
||||
private double currentPriorityScore = 0.0;
|
||||
|
||||
|
||||
public long getLastFlagsChangeEpochMs() {
|
||||
return lastFlagsChangeEpochMs;
|
||||
}
|
||||
|
||||
public void setLastFlagsChangeEpochMs(long lastFlagsChangeEpochMs) {
|
||||
this.lastFlagsChangeEpochMs = lastFlagsChangeEpochMs;
|
||||
}
|
||||
|
||||
public boolean isInAngleAndRange() {
|
||||
return isInAngleAndRange;
|
||||
}
|
||||
|
||||
public void setInAngleAndRange(boolean inAngleAndRange) {
|
||||
isInAngleAndRange = inAngleAndRange;
|
||||
}
|
||||
|
||||
public AirPlaneReflectionInfo getAirPlaneReflectInfo() {
|
||||
return airPlaneReflectInfo;
|
||||
@@ -117,6 +189,70 @@ public class ChatMember {
|
||||
worked10G = worked10g;
|
||||
}
|
||||
|
||||
public boolean isQrv144() {
|
||||
return qrv144;
|
||||
}
|
||||
|
||||
public void setQrv144(boolean qrv144) {
|
||||
this.qrv144 = qrv144;
|
||||
}
|
||||
|
||||
public boolean isQrv432() {
|
||||
return qrv432;
|
||||
}
|
||||
|
||||
public void setQrv432(boolean qrv432) {
|
||||
this.qrv432 = qrv432;
|
||||
}
|
||||
|
||||
public boolean isQrv1240() {
|
||||
return qrv1240;
|
||||
}
|
||||
|
||||
public void setQrv1240(boolean qrv1240) {
|
||||
this.qrv1240 = qrv1240;
|
||||
}
|
||||
|
||||
public boolean isQrv2300() {
|
||||
return qrv2300;
|
||||
}
|
||||
|
||||
public void setQrv2300(boolean qrv2300) {
|
||||
this.qrv2300 = qrv2300;
|
||||
}
|
||||
|
||||
public boolean isQrv3400() {
|
||||
return qrv3400;
|
||||
}
|
||||
|
||||
public void setQrv3400(boolean qrv3400) {
|
||||
this.qrv3400 = qrv3400;
|
||||
}
|
||||
|
||||
public boolean isQrv5600() {
|
||||
return qrv5600;
|
||||
}
|
||||
|
||||
public void setQrv5600(boolean qrv5600) {
|
||||
this.qrv5600 = qrv5600;
|
||||
}
|
||||
|
||||
public boolean isQrv10G() {
|
||||
return qrv10G;
|
||||
}
|
||||
|
||||
public void setQrv10G(boolean qrv10G) {
|
||||
this.qrv10G = qrv10G;
|
||||
}
|
||||
|
||||
public boolean isQrvAny() {
|
||||
return qrvAny;
|
||||
}
|
||||
|
||||
public void setQrvAny(boolean qrvAny) {
|
||||
this.qrvAny = qrvAny;
|
||||
}
|
||||
|
||||
public int[] getWorkedCategories() {
|
||||
return workedCategories;
|
||||
}
|
||||
@@ -125,8 +261,8 @@ public class ChatMember {
|
||||
this.workedCategories = workedCategories;
|
||||
}
|
||||
|
||||
public void setActivityCounter(long activityCounter) {
|
||||
this.activityCounter = activityCounter;
|
||||
public void setActivityTimeLastInEpoch(long activityTimeLastInEpoch) {
|
||||
this.activityTimeLastInEpoch = activityTimeLastInEpoch;
|
||||
}
|
||||
|
||||
public int getState() {
|
||||
@@ -153,34 +289,149 @@ public class ChatMember {
|
||||
this.lastActualizationTimeOfThisMember = lastActualizationTimeOfThisMember;
|
||||
}
|
||||
|
||||
public int getQrb() {
|
||||
public Double getQrb() {
|
||||
return qrb;
|
||||
}
|
||||
|
||||
public void setQrb(int qrb) {
|
||||
public void setQrb(Double qrb) {
|
||||
this.qrb = qrb;
|
||||
}
|
||||
|
||||
public int getQTFdirection() {
|
||||
public Double getQTFdirection() {
|
||||
return QTFdirection;
|
||||
}
|
||||
|
||||
public void setQTFdirection(int qTFdirection) {
|
||||
public void setQTFdirection(Double qTFdirection) {
|
||||
QTFdirection = qTFdirection;
|
||||
}
|
||||
|
||||
// public int getWorkedCategory() {
|
||||
// return workedCategory;
|
||||
// }
|
||||
// public void setWorkedCategory(int workedCategory) {
|
||||
// this.workedCategory = workedCategory;
|
||||
// }
|
||||
public String getCallSign() {
|
||||
return callSign;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the original callsign and derives the normalized base callsign which is
|
||||
* used as the database key. Prefixes like EA5/ and suffixes like /P or -70 are
|
||||
* ignored for the raw-key handling.
|
||||
*
|
||||
* @param callSign callsign as received from chat or database
|
||||
*/
|
||||
public void setCallSign(String callSign) {
|
||||
this.callSign = callSign;
|
||||
|
||||
if (callSign == null) {
|
||||
this.callSign = null;
|
||||
this.callSignRaw = null;
|
||||
return;
|
||||
}
|
||||
|
||||
this.callSign = callSign.trim().toUpperCase(Locale.ROOT);
|
||||
this.callSignRaw = normalizeCallSignToBaseCallSign(this.callSign);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a callsign to the base callsign which is used as the unique key in
|
||||
* the internal database. The method removes KST suffixes like "-2", portable
|
||||
* suffixes like "/P" and prefix additions like "EA5/".
|
||||
*
|
||||
* @param callSign callsign to normalize
|
||||
* @return normalized base callsign in upper case
|
||||
*/
|
||||
public static String normalizeCallSignToBaseCallSign(String callSign) {
|
||||
|
||||
if (callSign == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String normalizedCallSign = callSign.trim().toUpperCase(Locale.ROOT);
|
||||
|
||||
if (normalizedCallSign.isBlank()) {
|
||||
return normalizedCallSign;
|
||||
}
|
||||
|
||||
String callSignWithoutDashSuffix = normalizedCallSign.split("-", 2)[0].trim();
|
||||
|
||||
if (!callSignWithoutDashSuffix.contains("/")) {
|
||||
return callSignWithoutDashSuffix;
|
||||
}
|
||||
|
||||
String[] callSignParts = callSignWithoutDashSuffix.split("/");
|
||||
String bestMatchingCallsignPart = helper_selectBestCallsignPart(callSignParts);
|
||||
|
||||
if (bestMatchingCallsignPart == null || bestMatchingCallsignPart.isBlank()) {
|
||||
return callSignWithoutDashSuffix;
|
||||
}
|
||||
|
||||
return bestMatchingCallsignPart;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects the most plausible base callsign segment from a slash-separated
|
||||
* callsign. In strings like "EA5/G8MBI/P" the segment "G8MBI" is preferred over
|
||||
* prefix or portable markers.
|
||||
*
|
||||
* @param callSignParts slash-separated callsign parts
|
||||
* @return best matching base callsign segment
|
||||
*/
|
||||
private static String helper_selectBestCallsignPart(String[] callSignParts) {
|
||||
|
||||
String bestLikelyBaseCallsignPart = null;
|
||||
int bestLikelyBaseCallsignLength = -1;
|
||||
String bestFallbackCallsignPart = null;
|
||||
int bestFallbackCallsignLength = -1;
|
||||
|
||||
for (String rawCallsignPart : callSignParts) {
|
||||
|
||||
String currentCallsignPart = rawCallsignPart == null ? "" : rawCallsignPart.trim().toUpperCase(Locale.ROOT);
|
||||
|
||||
if (currentCallsignPart.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (currentCallsignPart.length() > bestFallbackCallsignLength) {
|
||||
bestFallbackCallsignPart = currentCallsignPart;
|
||||
bestFallbackCallsignLength = currentCallsignPart.length();
|
||||
}
|
||||
|
||||
if (helper_isLikelyBaseCallsignSegment(currentCallsignPart)
|
||||
&& currentCallsignPart.length() > bestLikelyBaseCallsignLength) {
|
||||
bestLikelyBaseCallsignPart = currentCallsignPart;
|
||||
bestLikelyBaseCallsignLength = currentCallsignPart.length();
|
||||
}
|
||||
}
|
||||
|
||||
if (bestLikelyBaseCallsignPart != null) {
|
||||
return bestLikelyBaseCallsignPart;
|
||||
}
|
||||
|
||||
return bestFallbackCallsignPart;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a slash-separated segment looks like a real base callsign. A
|
||||
* normal amateur-radio callsign typically contains letters and digits and is
|
||||
* longer than one-character postfix markers.
|
||||
*
|
||||
* @param callsignSegment segment to inspect
|
||||
* @return true if the segment looks like a base callsign
|
||||
*/
|
||||
private static boolean helper_isLikelyBaseCallsignSegment(String callsignSegment) {
|
||||
|
||||
boolean containsLetter = false;
|
||||
boolean containsDigit = false;
|
||||
|
||||
for (int currentIndex = 0; currentIndex < callsignSegment.length(); currentIndex++) {
|
||||
char currentCharacter = callsignSegment.charAt(currentIndex);
|
||||
|
||||
if (Character.isLetter(currentCharacter)) {
|
||||
containsLetter = true;
|
||||
}
|
||||
|
||||
if (Character.isDigit(currentCharacter)) {
|
||||
containsDigit = true;
|
||||
}
|
||||
}
|
||||
|
||||
return containsLetter && containsDigit && callsignSegment.length() >= 3;
|
||||
}
|
||||
|
||||
public String getQra() {
|
||||
@@ -210,22 +461,83 @@ public class ChatMember {
|
||||
this.frequency = frequency;
|
||||
}
|
||||
|
||||
public long getActivityCounter() {
|
||||
return activityCounter;
|
||||
public long getActivityTimeLastInEpoch() {
|
||||
return activityTimeLastInEpoch;
|
||||
}
|
||||
|
||||
public void setActivityCounter(int activityCounter) {
|
||||
this.activityCounter = activityCounter;
|
||||
this.activityTimeLastInEpoch = activityCounter;
|
||||
}
|
||||
|
||||
public boolean isWorked() {
|
||||
return worked;
|
||||
}
|
||||
|
||||
public void setWorked(boolean worked) {
|
||||
public boolean isWorked50() {
|
||||
return Worked50;
|
||||
}
|
||||
|
||||
public void setWorked50(boolean worked50) {
|
||||
Worked50 = worked50;
|
||||
}
|
||||
|
||||
public boolean isWorked70() {
|
||||
return Worked70;
|
||||
}
|
||||
|
||||
public void setWorked70(boolean worked70) {
|
||||
Worked70 = worked70;
|
||||
}
|
||||
|
||||
public boolean isWorked24G() {
|
||||
return Worked24G;
|
||||
}
|
||||
|
||||
public void setWorked24G(boolean worked24G) {
|
||||
Worked24G = worked24G;
|
||||
}
|
||||
|
||||
public boolean isWorked47G() {
|
||||
return Worked47G;
|
||||
}
|
||||
|
||||
public void setWorked47G(boolean worked47G) {
|
||||
Worked47G = worked47G;
|
||||
}
|
||||
|
||||
public boolean isWorked76G() {
|
||||
return Worked76G;
|
||||
}
|
||||
|
||||
public void setWorked76G(boolean worked76G) {
|
||||
Worked76G = worked76G;
|
||||
}
|
||||
|
||||
public void setWorked(boolean worked) {
|
||||
this.worked = worked;
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return String (callsign) without -2 or -70 etc.
|
||||
*/
|
||||
public String getCallSignRaw() {
|
||||
|
||||
|
||||
return callSignRaw;
|
||||
// String raw = "";
|
||||
//
|
||||
// try {
|
||||
// return this.getCallSign().split("-")[0]; //e.g. OK2M-70, returns only ok2m
|
||||
// } catch (Exception e) {
|
||||
// return getCallSign();
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets all worked information of this object to false. Scope: GUI, Reset Button
|
||||
* for worked info, called by appcontroller
|
||||
@@ -234,21 +546,44 @@ public class ChatMember {
|
||||
|
||||
this.setWorked(false);
|
||||
this.setWorked144(false);
|
||||
this.setWorked50(false);
|
||||
this.setWorked70(false);
|
||||
this.setWorked432(false);
|
||||
this.setWorked1240(false);
|
||||
this.setWorked2300(false);
|
||||
this.setWorked3400(false);
|
||||
this.setWorked5600(false);
|
||||
this.setWorked10G(false);
|
||||
this.setWorked24G(false);
|
||||
this.setWorked47G(false);
|
||||
this.setWorked76G(false);
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets all worked information of this object to false. Scope: GUI, Reset Button
|
||||
* for worked info, called by appcontroller
|
||||
*/
|
||||
public void resetQRVInformationAtAllBands() {
|
||||
|
||||
this.setQrvAny(true);
|
||||
this.setQrv144(true);
|
||||
this.setQrv432(true);
|
||||
this.setQrv1240(true);
|
||||
this.setQrv2300(true);
|
||||
this.setQrv3400(true);
|
||||
this.setQrv5600(true);
|
||||
this.setQrv10G(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
String chatMemberSerialization = "";
|
||||
|
||||
chatMemberSerialization += callSign + ";" + name + ";" + qra + ";" + frequency + ";" + worked + ";" + worked144
|
||||
+ ";" + worked432 + ";" + worked1240 + ";" + worked2300 + ";" + worked3400 + ";" + worked5600 + ";"
|
||||
+ worked10G;
|
||||
chatMemberSerialization += callSign + ";" + name + ";" + qra + ";" + frequency + "; wkd " + worked + "; wkd144 " + worked144
|
||||
+ "; wkd432" + worked432 + "; wkd1240" + worked1240 + "; wkd2300" + worked2300 + "; wkd3400" + worked3400 + "; wkd5600" + worked5600 + "; wkd10G"
|
||||
+ worked10G + " ; " + chatCategory;
|
||||
|
||||
return chatMemberSerialization;
|
||||
}
|
||||
@@ -267,4 +602,56 @@ public class ChatMember {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new recognized frequency by band to the internal band/qrg map
|
||||
* @param band
|
||||
* @param freq
|
||||
*/
|
||||
public void addKnownFrequency(Band band, double freq) {
|
||||
this.knownActiveBands.put(band, new ActiveFrequencyInfo(freq));
|
||||
}
|
||||
|
||||
/**
|
||||
* represents a map of bands which are known of this chatmember
|
||||
*
|
||||
* @return Band
|
||||
*/
|
||||
public Map<Band, ActiveFrequencyInfo> getKnownActiveBands() {
|
||||
return knownActiveBands;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* If a sked fails and the user tells this to the client, this counter will be increased to give the station a
|
||||
* lower score
|
||||
*/
|
||||
public void incrementFailedAttempts() {
|
||||
this.failedQSOAttempts++;
|
||||
}
|
||||
|
||||
public void resetFailedAttempts() {
|
||||
this.failedQSOAttempts = 0;
|
||||
}
|
||||
|
||||
public int getFailedQSOAttempts() {
|
||||
return failedQSOAttempts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the working-priority score of a chatmember for the "Todo-List"
|
||||
* @param score
|
||||
*/
|
||||
public void setCurrentPriorityScore(double score) {
|
||||
this.currentPriorityScore = score;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the working-priority score of a chatmember for the "Todo-List"
|
||||
*
|
||||
*/
|
||||
public double getCurrentPriorityScore() {
|
||||
return currentPriorityScore;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
52
src/main/java/kst4contest/model/ContestSked.java
Normal file
52
src/main/java/kst4contest/model/ContestSked.java
Normal file
@@ -0,0 +1,52 @@
|
||||
package kst4contest.model;
|
||||
|
||||
/**
|
||||
* Represents a scheduled event or an AirScout opportunity in the future.
|
||||
* Used for the Timeline View and Priority Calculation.
|
||||
*/
|
||||
public class ContestSked {
|
||||
|
||||
private String targetCallsign;
|
||||
private double targetAzimuth; // Required for Antenna-Visuals
|
||||
private long skedTimeEpoch; // The peak time (e.g., AP)
|
||||
private Band band;
|
||||
// Opportunity potential (0..100). -1 means "unknown".
|
||||
int opportunityPotentialPercent = -1;
|
||||
|
||||
// Status flags to prevent spamming alarms
|
||||
private boolean warning3MinSent = false;
|
||||
private boolean warningNowSent = false;
|
||||
|
||||
public ContestSked(String call, double azimuth, long time, Band b) {
|
||||
this.targetCallsign = call;
|
||||
this.targetAzimuth = azimuth;
|
||||
this.skedTimeEpoch = time;
|
||||
this.band = b;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the seconds remaining until the event.
|
||||
* Negative values mean the event is in the past.
|
||||
*/
|
||||
public long getTimeUntilSkedSeconds() {
|
||||
return (skedTimeEpoch - System.currentTimeMillis()) / 1000;
|
||||
}
|
||||
|
||||
// Getters and Setters...
|
||||
public String getTargetCallsign() { return targetCallsign; }
|
||||
public double getTargetAzimuth() { return targetAzimuth; }
|
||||
public long getSkedTimeEpoch() { return skedTimeEpoch; }
|
||||
public Band getBand() { return band; }
|
||||
public boolean isWarning3MinSent() { return warning3MinSent; }
|
||||
public void setWarning3MinSent(boolean b) { this.warning3MinSent = b; }
|
||||
public boolean isWarningNowSent() { return warningNowSent; }
|
||||
public void setWarningNowSent(boolean b) { this.warningNowSent = b; }
|
||||
|
||||
public int getOpportunityPotentialPercent() {
|
||||
return opportunityPotentialPercent;
|
||||
}
|
||||
|
||||
public void setOpportunityPotentialPercent(int opportunityPotentialPercent) {
|
||||
this.opportunityPotentialPercent = opportunityPotentialPercent;
|
||||
}
|
||||
}
|
||||
103
src/main/java/kst4contest/model/ThreadStateMessage.java
Normal file
103
src/main/java/kst4contest/model/ThreadStateMessage.java
Normal file
@@ -0,0 +1,103 @@
|
||||
package kst4contest.model;
|
||||
|
||||
/**
|
||||
* Object for the description of the activity of a Thread to show these information in a View.
|
||||
* <br/><br/>
|
||||
* If state is critical, there could be used a further information field for the stacktrace
|
||||
*/
|
||||
public class ThreadStateMessage {
|
||||
String threadNickName;
|
||||
String threadDescription;
|
||||
boolean running;
|
||||
String runningInformationTextDescription;
|
||||
String runningInformation;
|
||||
|
||||
boolean criticalState;
|
||||
String criticalStateFurtherInfo;
|
||||
|
||||
|
||||
|
||||
public ThreadStateMessage(String threadNickName, boolean running, String runningInformation, boolean criticalState) {
|
||||
|
||||
this.threadNickName = threadNickName;
|
||||
this.running = running;
|
||||
this.criticalState = criticalState;
|
||||
this.runningInformation = runningInformation;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* This triggers the message for "Sked armed"
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public String getRunningInformationTextDescription() {
|
||||
|
||||
// If a custom description was set (e.g. for UI indicator buttons), prefer it.
|
||||
if (runningInformationTextDescription != null && !runningInformationTextDescription.isBlank()) {
|
||||
return runningInformationTextDescription;
|
||||
}
|
||||
|
||||
// Fallback (legacy behavior)
|
||||
if (isRunning()) {
|
||||
return "on";
|
||||
} else if (!isRunning() && isCriticalState()) {
|
||||
return "FAILED";
|
||||
} else {
|
||||
return "off";
|
||||
}
|
||||
}
|
||||
|
||||
public void setRunningInformationTextDescription(String runningInformationTextDescription) {
|
||||
this.runningInformationTextDescription = runningInformationTextDescription;
|
||||
}
|
||||
|
||||
public String getThreadNickName() {
|
||||
return threadNickName;
|
||||
}
|
||||
|
||||
public void setThreadNickName(String threadNickName) {
|
||||
this.threadNickName = threadNickName;
|
||||
}
|
||||
|
||||
public String getThreadDescription() {
|
||||
return threadDescription;
|
||||
}
|
||||
|
||||
public void setThreadDescription(String threadDescription) {
|
||||
this.threadDescription = threadDescription;
|
||||
}
|
||||
|
||||
public boolean isRunning() {
|
||||
return running;
|
||||
}
|
||||
|
||||
public void setRunning(boolean running) {
|
||||
this.running = running;
|
||||
}
|
||||
|
||||
public String getRunningInformation() {
|
||||
return runningInformation;
|
||||
}
|
||||
|
||||
public void setRunningInformation(String runningInformation) {
|
||||
this.runningInformation = runningInformation;
|
||||
}
|
||||
|
||||
public boolean isCriticalState() {
|
||||
return criticalState;
|
||||
}
|
||||
|
||||
public void setCriticalState(boolean criticalState) {
|
||||
this.criticalState = criticalState;
|
||||
}
|
||||
|
||||
public String getCriticalStateFurtherInfo() {
|
||||
return criticalStateFurtherInfo;
|
||||
}
|
||||
|
||||
public void setCriticalStateFurtherInfo(String criticalStateFurtherInfo) {
|
||||
this.criticalStateFurtherInfo = criticalStateFurtherInfo;
|
||||
}
|
||||
}
|
||||
|
||||
87
src/main/java/kst4contest/model/UpdateInformation.java
Normal file
87
src/main/java/kst4contest/model/UpdateInformation.java
Normal file
@@ -0,0 +1,87 @@
|
||||
package kst4contest.model;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class UpdateInformation {
|
||||
double latestVersionNumberOnServer = 1.26; //dummy value to prevent nullpointerexc
|
||||
String adminMessage ="";
|
||||
String majorChanges ="";
|
||||
String latestVersionPathOnWebserver="";
|
||||
ArrayList<String> needUpdateResourcesSinceLastVersion = new ArrayList<String>();
|
||||
ArrayList<String[]> featureRequest = new ArrayList<String[]>();
|
||||
ArrayList<String[]> bugRequests = new ArrayList<String[]>();
|
||||
ArrayList<String[]> changeLog = new ArrayList<String[]>();
|
||||
ArrayList<String[]> bugList = new ArrayList<String[]>();
|
||||
|
||||
public ArrayList<String[]> getBugList() {
|
||||
return bugList;
|
||||
}
|
||||
|
||||
public void setBugList(ArrayList<String[]> bugList) {
|
||||
this.bugList = bugList;
|
||||
}
|
||||
|
||||
public ArrayList<String[]> getChangeLog() {
|
||||
return changeLog;
|
||||
}
|
||||
|
||||
public void setChangeLog(ArrayList<String[]> changeLog) {
|
||||
this.changeLog = changeLog;
|
||||
}
|
||||
|
||||
public double getLatestVersionNumberOnServer() {
|
||||
return latestVersionNumberOnServer;
|
||||
}
|
||||
|
||||
public void setLatestVersionNumberOnServer(double latestVersionNumberOnServer) {
|
||||
this.latestVersionNumberOnServer = latestVersionNumberOnServer;
|
||||
}
|
||||
|
||||
public String getAdminMessage() {
|
||||
return adminMessage;
|
||||
}
|
||||
|
||||
public void setAdminMessage(String adminMessage) {
|
||||
this.adminMessage = adminMessage;
|
||||
}
|
||||
|
||||
public String getMajorChanges() {
|
||||
return majorChanges;
|
||||
}
|
||||
|
||||
public void setMajorChanges(String majorChanges) {
|
||||
this.majorChanges = majorChanges;
|
||||
}
|
||||
|
||||
public String getLatestVersionPathOnWebserver() {
|
||||
return latestVersionPathOnWebserver;
|
||||
}
|
||||
|
||||
public void setLatestVersionPathOnWebserver(String latestVersionPathOnWebserver) {
|
||||
this.latestVersionPathOnWebserver = latestVersionPathOnWebserver;
|
||||
}
|
||||
|
||||
public ArrayList<String> getNeedUpdateResourcesSinceLastVersion() {
|
||||
return needUpdateResourcesSinceLastVersion;
|
||||
}
|
||||
|
||||
public void setNeedUpdateResourcesSinceLastVersion(ArrayList<String> needUpdateResourcesSinceLastVersion) {
|
||||
this.needUpdateResourcesSinceLastVersion = needUpdateResourcesSinceLastVersion;
|
||||
}
|
||||
|
||||
public ArrayList<String[]> getFeatureRequest() {
|
||||
return featureRequest;
|
||||
}
|
||||
|
||||
public void setFeatureRequest(ArrayList<String[]> featureRequest) {
|
||||
this.featureRequest = featureRequest;
|
||||
}
|
||||
|
||||
public ArrayList<String[]> getBugRequests() {
|
||||
return bugRequests;
|
||||
}
|
||||
|
||||
public void setBugRequests(ArrayList<String[]> bugRequests) {
|
||||
this.bugRequests = bugRequests;
|
||||
}
|
||||
}
|
||||
256
src/main/java/kst4contest/test/MockKstServer.java
Normal file
256
src/main/java/kst4contest/test/MockKstServer.java
Normal file
@@ -0,0 +1,256 @@
|
||||
package kst4contest.test;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.*;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
public class MockKstServer {
|
||||
|
||||
private static final int PORT = 23001;
|
||||
private static final String CHAT_ID = "2"; // 2 = 144/432 MHz
|
||||
|
||||
// Thread-sichere Liste aller verbundenen Clients (OutputStreams)
|
||||
private final List<PrintWriter> clients = new CopyOnWriteArrayList<>();
|
||||
|
||||
// Permanente User (Ihre Test-Callsigns)
|
||||
private final Map<String, User> onlineUsers = new HashMap<>();
|
||||
// Historien müssen synchronisiert werden
|
||||
private final List<String> historyChat = Collections.synchronizedList(new ArrayList<>());
|
||||
private final List<String> historyDx = Collections.synchronizedList(new ArrayList<>());
|
||||
|
||||
private boolean running = false;
|
||||
private ServerSocket serverSocket;
|
||||
|
||||
public MockKstServer() {
|
||||
// Initiale Permanente User
|
||||
addUser("DK5EW", "Erwin", "JN47NX");
|
||||
addUser("DL1TEST", "TestOp", "JO50XX");
|
||||
addUser("ON4KST", "Alain", "JO20HI");
|
||||
addUser("PA9R-2", "2", "JO20HI");
|
||||
addUser("PA9R-70", "70", "JO20HI");
|
||||
addUser("PA9R", "general", "JO20HI");
|
||||
}
|
||||
|
||||
// Startet den Server im Hintergrund (Non-Blocking)
|
||||
public void start() {
|
||||
if (running) return;
|
||||
running = true;
|
||||
|
||||
new Thread(() -> {
|
||||
try {
|
||||
serverSocket = new ServerSocket(PORT);
|
||||
System.out.println("[Server] ON4KST Simulation gestartet auf Port " + PORT);
|
||||
|
||||
// Startet den Simulator für Zufallstraffic
|
||||
new Thread(this::simulationLoop).start();
|
||||
|
||||
while (running) {
|
||||
Socket clientSocket = serverSocket.accept();
|
||||
System.out.println("[Server] Neuer Client verbunden: " + clientSocket.getInetAddress());
|
||||
new Thread(new ClientHandler(clientSocket)).start();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
if (running) e.printStackTrace();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
running = false;
|
||||
try {
|
||||
if (serverSocket != null) serverSocket.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private void addUser(String call, String name, String loc) {
|
||||
onlineUsers.put(call, new User(call, name, loc));
|
||||
}
|
||||
|
||||
private void removeUser(String call) {
|
||||
onlineUsers.remove(call);
|
||||
}
|
||||
|
||||
// Sendet Nachricht an ALLE verbundenen Clients (inkl. Sender)
|
||||
private void broadcast(String message) {
|
||||
if (!message.endsWith("\r\n")) message += "\r\n";
|
||||
String finalMsg = message;
|
||||
|
||||
for (PrintWriter writer : clients) {
|
||||
try {
|
||||
writer.print(finalMsg);
|
||||
writer.flush(); // WICHTIG: Sofort senden!
|
||||
} catch (Exception e) {
|
||||
// Client wohl weg, wird beim nächsten Schreibversuch oder im Handler entfernt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Innere Logik: Client Handler ---
|
||||
private class ClientHandler implements Runnable {
|
||||
private Socket socket;
|
||||
private PrintWriter out;
|
||||
private BufferedReader in;
|
||||
private String myCall = "MYCLIENT"; // Default, wird bei LOGIN überschrieben
|
||||
|
||||
public ClientHandler(Socket socket) {
|
||||
this.socket = socket;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
// ISO-8859-1 ist Standard für KST/Telnet Cluster
|
||||
in = new BufferedReader(new InputStreamReader(socket.getInputStream(), "ISO-8859-1"));
|
||||
out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), "ISO-8859-1"), true);
|
||||
|
||||
clients.add(out);
|
||||
|
||||
String line;
|
||||
boolean loginComplete = false;
|
||||
|
||||
while ((line = in.readLine()) != null) {
|
||||
// System.out.println("[RECV] " + line); // Debugging aktivieren falls nötig
|
||||
String[] parts = line.split("\\|");
|
||||
String cmd = parts[0];
|
||||
|
||||
if (cmd.equals("LOGIN") || cmd.equals("LOGINC")) {
|
||||
// Protokoll: LOGIN|callsign|password|... [cite: 21]
|
||||
if (parts.length > 1) myCall = parts[1];
|
||||
|
||||
// 1. Login Bestätigung
|
||||
// Format: LOGSTAT|100|chat id|client software version|session key|config|dx option|
|
||||
send("LOGSTAT|100|" + CHAT_ID + "|JavaSim|KEY123|Config|3|");
|
||||
|
||||
// Bei LOGIN senden wir die Daten sofort
|
||||
// Bei LOGINC warten wir eigentlich auf SDONE, senden hier aber vereinfacht direkt
|
||||
if (cmd.equals("LOGIN")) {
|
||||
sendInitialData();
|
||||
loginComplete = true;
|
||||
}
|
||||
}
|
||||
else if (cmd.equals("SDONE")) {
|
||||
// Abschluss der Settings (bei LOGINC) [cite: 34]
|
||||
sendInitialData();
|
||||
loginComplete = true;
|
||||
}
|
||||
else if (cmd.equals("MSG")) {
|
||||
// MSG|chat id|destination|command|0| [cite: 42]
|
||||
if (parts.length >= 4) {
|
||||
String text = parts[3];
|
||||
// Nachricht sofort als CH Frame an alle verteilen (Echo)
|
||||
handleChatMessage(myCall, "Me", text);
|
||||
}
|
||||
}
|
||||
else if (cmd.equals("CK")) {
|
||||
// Keepalive [cite: 20]
|
||||
// Server muss nicht zwingend antworten, aber Connection bleibt offen
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// System.out.println("Client getrennt");
|
||||
} finally {
|
||||
clients.remove(out);
|
||||
try { socket.close(); } catch (IOException e) {}
|
||||
}
|
||||
}
|
||||
|
||||
private void send(String msg) {
|
||||
if (!msg.endsWith("\r\n")) msg += "\r\n";
|
||||
out.print(msg);
|
||||
out.flush();
|
||||
}
|
||||
|
||||
private void sendInitialData() {
|
||||
// 1. User Liste UA0 [cite: 14]
|
||||
for (User u : onlineUsers.values()) {
|
||||
send("UA0|" + CHAT_ID + "|" + u.call + "|" + u.name + "|" + u.loc + "|0|");
|
||||
}
|
||||
// 2. Chat History CR [cite: 7]
|
||||
synchronized(historyChat) {
|
||||
for (String h : historyChat) send(h);
|
||||
}
|
||||
// 3. DX History DL [cite: 10]
|
||||
synchronized(historyDx) {
|
||||
for (String d : historyDx) send(d);
|
||||
}
|
||||
// 4. Ende User Liste UE [cite: 15]
|
||||
send("UE|" + CHAT_ID + "|" + onlineUsers.size() + "|");
|
||||
}
|
||||
}
|
||||
|
||||
// --- Hilfsmethoden für Traffic ---
|
||||
|
||||
private void handleChatMessage(String call, String name, String text) {
|
||||
// CH|chat id|date|callsign|firstname|destination|msg|highlight|
|
||||
String date = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
|
||||
String frame = String.format("CH|%s|%s|%s|%s|0|%s|0|", CHAT_ID, date, call, name, text);
|
||||
|
||||
synchronized(historyChat) {
|
||||
historyChat.add(frame);
|
||||
if (historyChat.size() > 50) historyChat.remove(0);
|
||||
}
|
||||
broadcast(frame);
|
||||
}
|
||||
|
||||
private void handleDxSpot(String spotter, String dx, String freq) {
|
||||
// DL|Unix time|dx utc|spotter|qrg|dx|info|spotter locator|dx locator| [cite: 10]
|
||||
long unixTime = System.currentTimeMillis() / 1000;
|
||||
String utc = new SimpleDateFormat("HHmm").format(new Date());
|
||||
// Simple Dummy Locators
|
||||
String frame = String.format("DL|%d|%s|%s|%s|%s|Simulated|JO00|JO99|",
|
||||
unixTime, utc, spotter, freq, dx);
|
||||
|
||||
synchronized(historyDx) {
|
||||
historyDx.add(frame);
|
||||
if (historyDx.size() > 20) historyDx.remove(0);
|
||||
}
|
||||
broadcast(frame);
|
||||
}
|
||||
|
||||
private void simulationLoop() {
|
||||
String[] randomCalls = {"PA0GUS", "F6APE", "OH8K", "OZ2M", "G4CBW"};
|
||||
String[] msgs = {"CQ 144.300", "Tnx for QSO", "Any sked?", "QRV 432.200"};
|
||||
Random rand = new Random();
|
||||
|
||||
while (running) {
|
||||
try {
|
||||
Thread.sleep(3000 + rand.nextInt(5000)); // 3-8 Sek Pause
|
||||
|
||||
int action = rand.nextInt(10);
|
||||
String call = randomCalls[rand.nextInt(randomCalls.length)];
|
||||
|
||||
if (action < 4) { // 40% Chat
|
||||
handleChatMessage(call, "SimOp", msgs[rand.nextInt(msgs.length)]);
|
||||
} else if (action < 7) { // 30% DX Spot
|
||||
handleDxSpot(call, randomCalls[rand.nextInt(randomCalls.length)], "144." + rand.nextInt(400));
|
||||
} else if (action == 8) { // Login Simulation UA5
|
||||
if (!onlineUsers.containsKey(call)) {
|
||||
addUser(call, "SimOp", "JO11");
|
||||
broadcast("UA5|" + CHAT_ID + "|" + call + "|SimOp|JO11|2|");
|
||||
}
|
||||
} else if (action == 9) { // Logout Simulation UR6
|
||||
if (onlineUsers.containsKey(call) && !call.equals("DK5EW")) { // DK5EW nicht kicken
|
||||
removeUser(call);
|
||||
broadcast("UR6|" + CHAT_ID + "|" + call + "|");
|
||||
}
|
||||
}
|
||||
|
||||
// Ping ab und zu
|
||||
if (rand.nextInt(5) == 0) broadcast("CK|");
|
||||
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Kleine Datenklasse
|
||||
private static class User {
|
||||
String call, name, loc;
|
||||
User(String c, String n, String l) { this.call=c; this.name=n; this.loc=l; }
|
||||
}
|
||||
}
|
||||
131
src/main/java/kst4contest/test/PatternMatcherTest.java
Normal file
131
src/main/java/kst4contest/test/PatternMatcherTest.java
Normal file
@@ -0,0 +1,131 @@
|
||||
package kst4contest.test;
|
||||
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import kst4contest.controller.Utils4KST;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class PatternMatcherTest {
|
||||
|
||||
/**
|
||||
* Tests if pattern matches with the given String.
|
||||
*
|
||||
* @param testString
|
||||
* @param regExPattern
|
||||
* @return true if match, else false
|
||||
*/
|
||||
|
||||
private static boolean testPattern(String testString, String regExPattern) {
|
||||
|
||||
Pattern pattern = Pattern.compile(regExPattern);
|
||||
Matcher matcher = pattern.matcher(testString);
|
||||
|
||||
return matcher.find();
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a chatmembers frequency-string for cluster usage<br/>
|
||||
* <b>returns a frequency String in KHz like = "144300" or "144300.0" to match DXC protocol needs</b>
|
||||
*
|
||||
* @param optionalPrefix: if there is a value like ".300", it have to be decided, wich ".300": 144.300, 432.300, 1296.300 .... prefix means for example "144."
|
||||
*/
|
||||
private static String normalizeFrequencyString(String qrgString, String optionalPrefix) {
|
||||
|
||||
// final String PTRN_QRG_CAT2 = "(([0-9]{3,4}[\\.|,| ]?[0-9]{3})([\\.|,][\\d]{1,2})?)|(([a-zA-Z][0-4]{1}[\\d]{2}\\b)([\\.|,][\\d]{1,2}\\b)?)|((\\b[0-4]{1}[\\d]{2}\\b)([\\.|,][\\d]{1,2}\\b)?)";
|
||||
|
||||
final String PTRN_QRG_CAT2_wholeQRGMHz4Digits = "(([0-9]{4}[\\.|,| ]?[0-9]{3})([\\.|,][\\d]{1,2})?)"; //1296.300.3 etc
|
||||
final String PTRN_QRG_CAT2_wholeQRGMHz3Digits = "(([0-9]{3}[\\.|,| ]?[0-9]{3})([\\.][\\d]{1,2})?)"; //144.300.3 etc
|
||||
final String PTRN_QRG_CAT2_QRGwithoutPrefix = "((\\b[0-4]{1}[\\d]{2}\\b)([\\.|,][\\d]{1,2}\\b)?)"; //144.300.3 etc
|
||||
String predefinedPrefixInMHz = optionalPrefix;
|
||||
|
||||
String stringAggregation = "";
|
||||
|
||||
if (testPattern(qrgString, PTRN_QRG_CAT2_wholeQRGMHz4Digits)) {
|
||||
System.out.print("yep: ");
|
||||
stringAggregation = qrgString;
|
||||
|
||||
stringAggregation = stringAggregation.replace(".","");
|
||||
stringAggregation = stringAggregation.replace(",","");
|
||||
if (stringAggregation.length() == 8) {
|
||||
String stringAggregationNew = stringAggregation.substring(0, stringAggregation.length()-1) + "." + stringAggregation.substring(stringAggregation.length()-1, stringAggregation.length());
|
||||
stringAggregation = stringAggregationNew;
|
||||
} else if (stringAggregation.length() == 9) {
|
||||
String stringAggregationNew = stringAggregation.substring(0, stringAggregation.length()-2) + "." + stringAggregation.substring(stringAggregation.length()-2, stringAggregation.length());
|
||||
stringAggregation = stringAggregationNew;
|
||||
}
|
||||
|
||||
} else
|
||||
|
||||
if (testPattern(qrgString, PTRN_QRG_CAT2_wholeQRGMHz3Digits)) {
|
||||
System.out.print("yep: ");
|
||||
stringAggregation = qrgString;
|
||||
|
||||
stringAggregation = stringAggregation.replace(".","");
|
||||
stringAggregation = stringAggregation.replace(",","");
|
||||
if (stringAggregation.length() == 7) {
|
||||
String stringAggregationNew = stringAggregation.substring(0, stringAggregation.length()-1) + "." + stringAggregation.substring(stringAggregation.length()-1, stringAggregation.length());
|
||||
stringAggregation = stringAggregationNew;
|
||||
} else if (stringAggregation.length() == 8) {
|
||||
String stringAggregationNew = stringAggregation.substring(0, stringAggregation.length()-2) + "." + stringAggregation.substring(stringAggregation.length()-2, stringAggregation.length());
|
||||
stringAggregation = stringAggregationNew;
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
if (testPattern(qrgString, PTRN_QRG_CAT2_QRGwithoutPrefix)) { //case ".050 or .300 or something like that"
|
||||
System.out.print("yep: ");
|
||||
stringAggregation = qrgString;
|
||||
|
||||
stringAggregation = stringAggregation.replace(".", "");
|
||||
stringAggregation = stringAggregation.replace(",", "");
|
||||
if (stringAggregation.length() == 3) { // like 050 or 300
|
||||
String stringAggregationNew = optionalPrefix + stringAggregation;
|
||||
stringAggregation = stringAggregationNew;
|
||||
return stringAggregation;
|
||||
|
||||
} else if (stringAggregation.length() == 4) { //like 050.2 --> 0502
|
||||
|
||||
stringAggregation = optionalPrefix + stringAggregation;
|
||||
String stringAggregationNew = stringAggregation.substring(0, stringAggregation.length() - 1) + "." + stringAggregation.substring(stringAggregation.length() - 1, stringAggregation.length());
|
||||
stringAggregation = stringAggregationNew;
|
||||
return stringAggregation;
|
||||
|
||||
} else if (stringAggregation.length() == 5) { //like 050.20 --> 05020
|
||||
|
||||
stringAggregation = optionalPrefix + stringAggregation;
|
||||
String stringAggregationNew = stringAggregation.substring(0, stringAggregation.length() - 2) + "." + stringAggregation.substring(stringAggregation.length() - 2, stringAggregation.length());
|
||||
stringAggregation = stringAggregationNew;
|
||||
return stringAggregation;
|
||||
}
|
||||
}
|
||||
|
||||
return qrgString;
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
int i = 0;
|
||||
|
||||
System.out.println(i++ + ": " + Utils4KST.normalizeFrequencyString("144.775", new SimpleStringProperty("144")));
|
||||
System.out.println(i++ + ": " + Utils4KST.normalizeFrequencyString("144.300.2", new SimpleStringProperty("144")));
|
||||
System.out.println(i++ + ": " + Utils4KST.normalizeFrequencyString("144,300.2", new SimpleStringProperty("144")));
|
||||
System.out.println(i++ + ": " + Utils4KST.normalizeFrequencyString("144300.2", new SimpleStringProperty("144")));
|
||||
System.out.println(i++ + ": " + Utils4KST.normalizeFrequencyString("144300,2", new SimpleStringProperty("144")));
|
||||
System.out.println(i++ + ": " + Utils4KST.normalizeFrequencyString("144.300", new SimpleStringProperty("144")));
|
||||
System.out.println(i++ + ": " + Utils4KST.normalizeFrequencyString("144.300.20", new SimpleStringProperty("144")));
|
||||
System.out.println(i++ + ": " + Utils4KST.normalizeFrequencyString("300", new SimpleStringProperty("144")));
|
||||
System.out.println(i++ + ": " + Utils4KST.normalizeFrequencyString(".300", new SimpleStringProperty("144")));
|
||||
System.out.println(i++ + ": " + Utils4KST.normalizeFrequencyString(".300.2", new SimpleStringProperty("144")));
|
||||
System.out.println(i++ + ": " + Utils4KST.normalizeFrequencyString(".300.20", new SimpleStringProperty("144")));
|
||||
System.out.println(i++ + ": " + Utils4KST.normalizeFrequencyString("1296.300", new SimpleStringProperty("144")));
|
||||
System.out.println(i++ + ": " + Utils4KST.normalizeFrequencyString("1296,300", new SimpleStringProperty("144")));
|
||||
System.out.println(i++ + ": " + Utils4KST.normalizeFrequencyString("1296.300.2", new SimpleStringProperty("144")));
|
||||
System.out.println(i++ + ": " + Utils4KST.normalizeFrequencyString("1296.300.20", new SimpleStringProperty("144")));
|
||||
System.out.println(i++ + ": " + Utils4KST.normalizeFrequencyString("1296,300,2", new SimpleStringProperty("144")));
|
||||
System.out.println(i++ + ": " + Utils4KST.normalizeFrequencyString("1296,300,20", new SimpleStringProperty("144")));
|
||||
System.out.println(i++ + ": " + Utils4KST.normalizeFrequencyString("1296.300,2", new SimpleStringProperty("144")));
|
||||
System.out.println(i++ + ": " + Utils4KST.normalizeFrequencyString("1296,300.2", new SimpleStringProperty("144")));
|
||||
System.out.println(i++ + ": " + Utils4KST.normalizeFrequencyString("q305", new SimpleStringProperty("144")));
|
||||
}
|
||||
}
|
||||
484
src/main/java/kst4contest/utils/PlayAudioUtils.java
Normal file
484
src/main/java/kst4contest/utils/PlayAudioUtils.java
Normal file
@@ -0,0 +1,484 @@
|
||||
package kst4contest.utils;
|
||||
|
||||
import javafx.scene.media.Media;
|
||||
import javafx.scene.media.MediaPlayer;
|
||||
import kst4contest.ApplicationConstants;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Queue;
|
||||
|
||||
/**
|
||||
* This part of the client drives the sounds. Its a singleton instance. All audio outs are directed to this instance.<br>
|
||||
* <br>
|
||||
* */
|
||||
public class PlayAudioUtils {
|
||||
|
||||
/**
|
||||
* Default constructor initializes the sound files and copies it to the project home folder
|
||||
*/
|
||||
public PlayAudioUtils() {
|
||||
|
||||
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/NOISESTARTUP.mp3", "NOISESTARTUP.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/NOISECQWINDOW.mp3", "NOISECQWINDOW.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/NOISEPMWINDOW.mp3", "NOISEPMWINDOW.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/NOISEERROR.mp3", "NOISEERROR.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/NOISENOTIFY.mp3", "NOISENOTIFY.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/tick.mp3", "tick.mp3");
|
||||
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTRA.mp3", "LTTRA.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTRB.mp3", "LTTRB.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTRC.mp3", "LTTRC.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTRD.mp3", "LTTRD.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTRE.mp3", "LTTRE.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTRF.mp3", "LTTRF.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTRG.mp3", "LTTRG.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTRH.mp3", "LTTRH.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTRI.mp3", "LTTRI.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTRJ.mp3", "LTTRJ.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTRK.mp3", "LTTRK.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTRL.mp3", "LTTRL.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTRM.mp3", "LTTRM.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTRN.mp3", "LTTRN.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTRO.mp3", "LTTRO.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTRP.mp3", "LTTRP.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTRQ.mp3", "LTTRQ.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTRR.mp3", "LTTRR.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTRS.mp3", "LTTRS.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTRT.mp3", "LTTRT.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTRU.mp3", "LTTRU.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTRV.mp3", "LTTRV.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTRW.mp3", "LTTRW.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTRX.mp3", "LTTRX.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTRY.mp3", "LTTRY.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTRZ.mp3", "LTTRZ.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTR0.mp3", "LTTR0.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTR1.mp3", "LTTR1.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTR2.mp3", "LTTR2.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTR3.mp3", "LTTR3.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTR4.mp3", "LTTR4.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTR5.mp3", "LTTR5.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTR6.mp3", "LTTR6.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTR7.mp3", "LTTR7.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTR8.mp3", "LTTR8.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTR9.mp3", "LTTR9.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTRSTROKE.mp3", "LTTRSTROKE.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTRSPACE.mp3", "LTTRSPACE.mp3");
|
||||
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICEA.mp3", "VOICEA.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICEB.mp3", "VOICEB.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICEC.mp3", "VOICEC.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICED.mp3", "VOICED.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICEE.mp3", "VOICEE.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICEF.mp3", "VOICEF.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICEG.mp3", "VOICEG.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICEH.mp3", "VOICEH.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICEI.mp3", "VOICEI.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICEJ.mp3", "VOICEJ.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICEK.mp3", "VOICEK.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICEL.mp3", "VOICEL.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICEM.mp3", "VOICEM.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICEN.mp3", "VOICEN.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICEO.mp3", "VOICEO.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICEP.mp3", "VOICEP.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICEQ.mp3", "VOICEQ.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICER.mp3", "VOICER.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICES.mp3", "VOICES.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICET.mp3", "VOICET.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICEU.mp3", "VOICEU.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICEV.mp3", "VOICEV.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICEW.mp3", "VOICEW.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICEX.mp3", "VOICEX.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICEY.mp3", "VOICEY.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICEZ.mp3", "VOICEZ.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICE0.mp3", "VOICE0.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICE1.mp3", "VOICE1.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICE2.mp3", "VOICE2.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICE3.mp3", "VOICE3.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICE4.mp3", "VOICE4.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICE5.mp3", "VOICE5.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICE6.mp3", "VOICE6.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICE7.mp3", "VOICE7.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICE8.mp3", "VOICE8.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICE9.mp3", "VOICE9.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICESTROKE.mp3", "VOICESTROKE.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICEBELL.mp3", "VOICEBELL.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICEYOUGOTMAIL.mp3", "VOICEYOUGOTMAIL.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICEHELLO.mp3", "VOICEHELLO.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICE73.mp3", "VOICE73.mp3");
|
||||
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICESTROKEPORTABLE.mp3", "VOICESTROKEPORTABLE.mp3");
|
||||
|
||||
|
||||
}
|
||||
|
||||
private Queue<Media> musicList = new LinkedList<Media>();
|
||||
private MediaPlayer mediaPlayer ;
|
||||
|
||||
/**
|
||||
* Plays notification sounds out of the windws 95 box by given action character<br/>
|
||||
*<br/>
|
||||
*
|
||||
* case '!': Startup<br/>
|
||||
* case '-': tick<br/>
|
||||
* case 'C': CQ Window new entry<br/>
|
||||
* case 'P': PM Window new entry<br/>
|
||||
* case 'E': Error occured<br/>
|
||||
* case 'N': other notification sounds<br/>
|
||||
*
|
||||
* @param actionChar
|
||||
*/
|
||||
public void playNoiseLauncher(char actionChar) {
|
||||
|
||||
|
||||
|
||||
// ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/NOISESTARTUP.mp3");
|
||||
|
||||
|
||||
switch (actionChar){
|
||||
case '-':
|
||||
musicList.add(new Media(new File (ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/tick.mp3")).toURI().toString()));
|
||||
break;
|
||||
case '!':
|
||||
musicList.add(new Media(new File (ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/NOISESTARTUP.mp3")).toURI().toString()));
|
||||
break;
|
||||
case 'C':
|
||||
musicList.add(new Media(new File (ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/NOISECQWINDOW.mp3")).toURI().toString()));
|
||||
break;
|
||||
case 'P':
|
||||
musicList.add(new Media(new File (ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/NOISEPMWINDOW.mp3")).toURI().toString()));
|
||||
break;
|
||||
case 'E':
|
||||
musicList.add(new Media(new File (ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/NOISEERROR.mp3")).toURI().toString()));
|
||||
break;
|
||||
case 'N':
|
||||
musicList.add(new Media(new File (ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/NOISENOTIFY.mp3")).toURI().toString()));
|
||||
break;
|
||||
// case 'M':
|
||||
// musicList.add(new Media(new File ("VOICE.mp3").toURI().toString()));
|
||||
// break;
|
||||
|
||||
default:
|
||||
System.out.println("[KST4ContestApp, warning, letter not defined!]");
|
||||
|
||||
}
|
||||
playMusic();
|
||||
// mediaPlayer.dispose();
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Plays all chars of a given String-parameter as CW Sound out of the speaker.
|
||||
* As a workaround for delay problems at the beginning of playing, there are added 2x pause chars to the string.
|
||||
*
|
||||
* @param playThisChars
|
||||
*/
|
||||
public void playCWLauncher(String playThisChars) {
|
||||
|
||||
char[] playThisInCW = playThisChars.toUpperCase().toCharArray();
|
||||
|
||||
|
||||
for (char letterToPlay: playThisInCW){
|
||||
switch (letterToPlay){
|
||||
case 'A':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRA.mp3")).toURI().toString()));
|
||||
break;
|
||||
case 'B':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRB.mp3")).toURI().toString()));
|
||||
break;
|
||||
case 'C':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRC.mp3")).toURI().toString()));
|
||||
break;
|
||||
case 'D':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRD.mp3")).toURI().toString()));
|
||||
break;
|
||||
case 'E':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRE.mp3")).toURI().toString()));
|
||||
break;
|
||||
case 'F':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRF.mp3")).toURI().toString()));
|
||||
break;
|
||||
case 'G':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRG.mp3")).toURI().toString()));
|
||||
break;
|
||||
case 'H':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRH.mp3")).toURI().toString()));
|
||||
break;
|
||||
case 'I':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRI.mp3")).toURI().toString()));
|
||||
break;
|
||||
case 'J':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRJ.mp3")).toURI().toString()));
|
||||
break;
|
||||
case 'K':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRK.mp3")).toURI().toString()));
|
||||
break;
|
||||
case 'L':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRL.mp3")).toURI().toString()));
|
||||
break;
|
||||
case 'M':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRM.mp3")).toURI().toString()));
|
||||
break;
|
||||
case 'N':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRN.mp3")).toURI().toString()));
|
||||
break;
|
||||
case 'O':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRO.mp3")).toURI().toString()));
|
||||
break;
|
||||
case 'P':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRP.mp3")).toURI().toString()));
|
||||
break;
|
||||
case 'Q':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRQ.mp3")).toURI().toString()));
|
||||
break;
|
||||
case 'R':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRR.mp3")).toURI().toString()));
|
||||
break;
|
||||
case 'S':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRS.mp3")).toURI().toString()));
|
||||
break;
|
||||
case 'T':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRT.mp3")).toURI().toString()));
|
||||
break;
|
||||
case 'U':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRU.mp3")).toURI().toString()));
|
||||
break;
|
||||
case 'V':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRV.mp3")).toURI().toString()));
|
||||
break;
|
||||
case 'W':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRW.mp3")).toURI().toString()));
|
||||
break;
|
||||
case 'X':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRX.mp3")).toURI().toString()));
|
||||
break;
|
||||
case 'Y':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRY.mp3")).toURI().toString()));
|
||||
break;
|
||||
case 'Z':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRZ.mp3")).toURI().toString()));
|
||||
break;
|
||||
case '1':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTR1.mp3")).toURI().toString()));
|
||||
break;
|
||||
case '2':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTR2.mp3")).toURI().toString()));
|
||||
break;
|
||||
case '3':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTR3.mp3")).toURI().toString()));
|
||||
break;
|
||||
case '4':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTR4.mp3")).toURI().toString()));
|
||||
break;
|
||||
case '5':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTR5.mp3")).toURI().toString()));
|
||||
break;
|
||||
case '6':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTR6.mp3")).toURI().toString()));
|
||||
break;
|
||||
case '7':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTR7.mp3")).toURI().toString()));
|
||||
break;
|
||||
case '8':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTR8.mp3")).toURI().toString()));
|
||||
break;
|
||||
case '9':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTR9.mp3")).toURI().toString()));
|
||||
break;
|
||||
case '0':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTR0.mp3")).toURI().toString()));
|
||||
break;
|
||||
case '/':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRSTROKE.mp3")).toURI().toString()));
|
||||
break;
|
||||
case ' ':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRSPACE.mp3")).toURI().toString()));
|
||||
break;
|
||||
default:
|
||||
System.out.println("[KST4ContestApp, warning, letter not defined:] cwLetters = " + Arrays.toString(playThisInCW));
|
||||
}
|
||||
}
|
||||
playMusic();
|
||||
// mediaPlayer.dispose();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Plays a voice file for each char in the string (only EN alphabetic and numbers) except some specials: <br/><br/>
|
||||
* <b>Note that the audio settings (ChatPreferences) must be switched on in order to make the sounds playing.</b><br/><br/>
|
||||
* case '!': BELL<br/>
|
||||
* case '?': YOUGOTMAIL<br/>
|
||||
* case '#': HELLO<br/>
|
||||
* case '*': 73 bye<br/>
|
||||
* case '$': STROKEPORTABLE<br/>
|
||||
* @param playThisChars
|
||||
*/
|
||||
public void playVoiceLauncher(String playThisChars) {
|
||||
|
||||
char[] spellThisWithVoice = playThisChars.toUpperCase().toCharArray();
|
||||
|
||||
for (char letterToPlay: spellThisWithVoice){
|
||||
switch (letterToPlay){
|
||||
case '!':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEBELL.mp3")).toURI().toString()));
|
||||
break;
|
||||
case '?':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEYOUGOTMAIL.mp3")).toURI().toString()));
|
||||
break;
|
||||
case '#':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEHELLO.mp3")).toURI().toString()));
|
||||
break;
|
||||
case '*':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE73.mp3")).toURI().toString()));
|
||||
break;
|
||||
case '$':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICESTROKEPORTABLE.mp3")).toURI().toString()));
|
||||
break;
|
||||
case 'A':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEA.mp3")).toURI().toString()));
|
||||
break;
|
||||
case 'B':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEB.mp3")).toURI().toString()));
|
||||
break;
|
||||
case 'C':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEC.mp3")).toURI().toString()));
|
||||
break;
|
||||
case 'D':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICED.mp3")).toURI().toString()));
|
||||
break;
|
||||
case 'E':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEE.mp3")).toURI().toString()));
|
||||
break;
|
||||
case 'F':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEF.mp3")).toURI().toString()));
|
||||
break;
|
||||
case 'G':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEG.mp3")).toURI().toString()));
|
||||
break;
|
||||
case 'H':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEH.mp3")).toURI().toString()));
|
||||
break;
|
||||
case 'I':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEI.mp3")).toURI().toString()));
|
||||
break;
|
||||
case 'J':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEJ.mp3")).toURI().toString()));
|
||||
break;
|
||||
case 'K':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEK.mp3")).toURI().toString()));
|
||||
break;
|
||||
case 'L':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEL.mp3")).toURI().toString()));
|
||||
break;
|
||||
case 'M':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEM.mp3")).toURI().toString()));
|
||||
break;
|
||||
case 'N':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEN.mp3")).toURI().toString()));
|
||||
break;
|
||||
case 'O':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEO.mp3")).toURI().toString()));
|
||||
break;
|
||||
case 'P':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEP.mp3")).toURI().toString()));
|
||||
break;
|
||||
case 'Q':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEQ.mp3")).toURI().toString()));
|
||||
break;
|
||||
case 'R':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICER.mp3")).toURI().toString()));
|
||||
break;
|
||||
case 'S':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICES.mp3")).toURI().toString()));
|
||||
break;
|
||||
case 'T':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICET.mp3")).toURI().toString()));
|
||||
break;
|
||||
case 'U':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEU.mp3")).toURI().toString()));
|
||||
break;
|
||||
case 'V':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEV.mp3")).toURI().toString()));
|
||||
break;
|
||||
case 'W':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEW.mp3")).toURI().toString()));
|
||||
break;
|
||||
case 'X':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEX.mp3")).toURI().toString()));
|
||||
break;
|
||||
case 'Y':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEY.mp3")).toURI().toString()));
|
||||
break;
|
||||
case 'Z':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEZ.mp3")).toURI().toString()));
|
||||
break;
|
||||
case '1':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE1.mp3")).toURI().toString()));
|
||||
break;
|
||||
case '2':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE2.mp3")).toURI().toString()));
|
||||
break;
|
||||
case '3':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE3.mp3")).toURI().toString()));
|
||||
break;
|
||||
case '4':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE4.mp3")).toURI().toString()));
|
||||
break;
|
||||
case '5':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE5.mp3")).toURI().toString()));
|
||||
break;
|
||||
case '6':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE6.mp3")).toURI().toString()));
|
||||
break;
|
||||
case '7':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE7.mp3")).toURI().toString()));
|
||||
break;
|
||||
case '8':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE8.mp3")).toURI().toString()));
|
||||
break;
|
||||
case '9':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE9.mp3")).toURI().toString()));
|
||||
break;
|
||||
case '0':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE0.mp3")).toURI().toString()));
|
||||
break;
|
||||
case '/':
|
||||
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICESTROKE.mp3")).toURI().toString()));
|
||||
break;
|
||||
// case ' ':
|
||||
// musicList.add(new Media(new File ("VOICESPACE.mp3").toURI().toString()));
|
||||
// break;
|
||||
default:
|
||||
System.out.println("[KST4ContestApp, warning, letter not defined:] cwLetters = " + Arrays.toString(spellThisWithVoice));
|
||||
}
|
||||
}
|
||||
playMusic();
|
||||
// mediaPlayer.dispose();
|
||||
|
||||
}
|
||||
private void playMusic() {
|
||||
|
||||
// System.out.println("Kst4ContestApplication.playMusic");
|
||||
if(musicList.peek() == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
mediaPlayer = new MediaPlayer(musicList.poll());
|
||||
mediaPlayer.setRate(1.0);
|
||||
|
||||
mediaPlayer.setOnReady(() -> {
|
||||
mediaPlayer.play();
|
||||
mediaPlayer.setOnEndOfMedia(() -> {
|
||||
// mediaPlayer.dispose();
|
||||
playMusic();
|
||||
if (musicList.isEmpty()) {
|
||||
// mediaPlayer.dispose();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
19
src/main/java/kst4contest/utils/TestAudioPlayerUtils.java
Normal file
19
src/main/java/kst4contest/utils/TestAudioPlayerUtils.java
Normal file
@@ -0,0 +1,19 @@
|
||||
package kst4contest.utils;
|
||||
|
||||
import javafx.application.Application;
|
||||
import javafx.stage.Stage;
|
||||
import kst4contest.utils.PlayAudioUtils;
|
||||
|
||||
public class TestAudioPlayerUtils extends Application {
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(Stage stage) throws Exception {
|
||||
|
||||
PlayAudioUtils testAudio = new PlayAudioUtils();
|
||||
testAudio.playCWLauncher("DO5AMF");
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,15 @@
|
||||
package kst4contest.view;
|
||||
|
||||
import kst4contest.controller.ChatController;
|
||||
import kst4contest.model.ChatMember;
|
||||
|
||||
import java.util.function.Predicate;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class GuiUtils {
|
||||
|
||||
private static final String PTRN_CALLSIGNSYNTAX = "^(?:[A-Z]{1,2}[0-9]|[0-9][A-Z])[0-9A-Z]{1,3}$";
|
||||
/**
|
||||
* Checks wheter the input value of the String is numeric or not, true if yes
|
||||
* TODO: Move to a utils class for checking input values by user...
|
||||
@@ -11,5 +19,58 @@ public class GuiUtils {
|
||||
static boolean isNumeric(String str){
|
||||
return str != null && str.matches("[0-9.]+");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks wheter the given String has a HAM radio callsign syntax or not
|
||||
* @param maybeCallSignValue
|
||||
* @return true if yes
|
||||
*/
|
||||
static boolean isCallSignSyntax(String maybeCallSignValue) {
|
||||
|
||||
Pattern pattern = Pattern.compile(PTRN_CALLSIGNSYNTAX, Pattern.CASE_INSENSITIVE);
|
||||
Matcher matcher = pattern.matcher(maybeCallSignValue);
|
||||
|
||||
try {
|
||||
if (matcher.find()) {
|
||||
return true;
|
||||
}
|
||||
else return false;
|
||||
|
||||
} catch (Exception exc) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static void triggerGUIFilteredChatMemberListChange(ChatController chatController) {
|
||||
|
||||
if (javafx.application.Platform.isFxApplicationThread()) {
|
||||
triggerUpdate(chatController);
|
||||
} else{
|
||||
javafx.application.Platform.runLater(() -> triggerUpdate(chatController));
|
||||
}
|
||||
}
|
||||
|
||||
private static void triggerUpdate(ChatController chatController) {
|
||||
{
|
||||
//trick to trigger gui changes on property changes of obects
|
||||
|
||||
Predicate<ChatMember> dummyPredicate = new Predicate<ChatMember>() {
|
||||
@Override
|
||||
public boolean test(ChatMember chatMember) {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* //TODO: following 2 lines are a quick fix to making disappear worked chatmembers of the list
|
||||
* Thats uncomfortable due to this also causes selection changes,
|
||||
* Better way is to change all worked and qrv values to observables and then trigger the underlying
|
||||
* list to fire an invalidationevent. Really Todo!
|
||||
*/
|
||||
chatController.getLst_chatMemberListFilterPredicates().add(dummyPredicate);
|
||||
chatController.getLst_chatMemberListFilterPredicates().remove(dummyPredicate);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
369
src/main/java/kst4contest/view/TimelineView.java
Normal file
369
src/main/java/kst4contest/view/TimelineView.java
Normal file
@@ -0,0 +1,369 @@
|
||||
package kst4contest.view;
|
||||
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.shape.Circle;
|
||||
import javafx.scene.shape.Line;
|
||||
import javafx.scene.shape.Polygon;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.effect.DropShadow;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Group;
|
||||
import javafx.scene.text.Font;
|
||||
import kst4contest.model.ContestSked;
|
||||
import kst4contest.model.ChatCategory;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* A custom UI Component that visualizes future events (Skeds/AP).
|
||||
* It changes opacity based on the current antenna direction.
|
||||
*
|
||||
* Extended:
|
||||
* - Can also render "priority candidates" (ScoreService top list) with time base = next AirScout airplane minute.
|
||||
* - Clicking a candidate triggers a callback (selection + /cq preparation happens in Kst4ContestApplication).
|
||||
*/
|
||||
public class TimelineView extends Pane {
|
||||
|
||||
private double currentAntennaAzimuth = 0;
|
||||
private double beamWidth = 50.0; // TODO: from Prefs (later)
|
||||
private final long PREVIEW_TIME_MS = 30 * 60 * 1000; // 30 Minutes Preview
|
||||
double margin = 30; // enough space for the callsign label
|
||||
|
||||
private Function<ContestSked, String> skedTooltipExtraTextProvider; //used for further info in sked tooltip
|
||||
|
||||
|
||||
private Consumer<CandidateEvent> onCandidateClicked;
|
||||
|
||||
public TimelineView() {
|
||||
this.setPrefHeight(40);
|
||||
this.setStyle("-fx-background-color: #2b2b2b;");
|
||||
// weitere init defaults, falls du welche hattest
|
||||
}
|
||||
|
||||
/**
|
||||
* Backward compatibility: if some code still calls the old ctor.
|
||||
* Potential/tooltip are not view-wide properties; they belong to markers/events.
|
||||
*/
|
||||
@Deprecated
|
||||
public TimelineView(int opportunityPotentialPercent, String tooltipText) {
|
||||
this(); // ignore args on purpose
|
||||
}
|
||||
|
||||
//
|
||||
// public int getOpportunityPotentialPercent() {
|
||||
// return opportunityPotentialPercent;
|
||||
// }
|
||||
|
||||
public void setCurrentAntennaAzimuth(double az) {
|
||||
this.currentAntennaAzimuth = az;
|
||||
}
|
||||
|
||||
public long getPreviewTimeMs() {
|
||||
return PREVIEW_TIME_MS;
|
||||
}
|
||||
|
||||
public void setOnCandidateClicked(Consumer<CandidateEvent> handler) {
|
||||
this.onCandidateClicked = handler;
|
||||
}
|
||||
|
||||
/** Backward compatible call (Skeds only) */
|
||||
public void updateVisuals(List<ContestSked> skeds) {
|
||||
updateVisuals(skeds, Collections.emptyList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Redraws the timeline based on the list of active skeds AND priority candidates.
|
||||
*/
|
||||
public void updateVisuals(List<ContestSked> skeds, List<CandidateEvent> candidates) {
|
||||
this.getChildren().clear();
|
||||
|
||||
double width = this.getWidth();
|
||||
if (width <= 5) {
|
||||
// Layout not ready yet; will be updated by caller later (uiPulse/list change)
|
||||
return;
|
||||
}
|
||||
|
||||
// Draw Axis
|
||||
double axisY = 30;
|
||||
Line axis = new Line(0, axisY, width, axisY);
|
||||
axis.setStroke(Color.GRAY);
|
||||
this.getChildren().add(axis);
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
// 1) Draw Priority Candidates (upper lanes)
|
||||
for (CandidateEvent ev : candidates) {
|
||||
long timeDiff = ev.getTimeUntilMs();
|
||||
if (timeDiff < 0 || timeDiff > PREVIEW_TIME_MS) continue;
|
||||
|
||||
double percent = (double) timeDiff / PREVIEW_TIME_MS;
|
||||
double xPos = percent * width;
|
||||
xPos = Math.max(margin, Math.min(this.getWidth() - margin, xPos)); //starting point of the diagram
|
||||
|
||||
|
||||
Node marker = createCandidateMarker(ev);
|
||||
|
||||
applyAntennaEffect(marker, ev.getTargetAzimuth());
|
||||
|
||||
// Upper lanes so they don't overlap skeds
|
||||
double laneBaseY = 2;
|
||||
double laneOffsetY = 14.0 * ev.getLaneIndex();
|
||||
|
||||
marker.setLayoutX(xPos);
|
||||
marker.setLayoutY(laneBaseY + laneOffsetY);
|
||||
|
||||
this.getChildren().add(marker);
|
||||
}
|
||||
|
||||
// 2) Draw Skeds (lower lane)
|
||||
for (ContestSked sked : skeds) {
|
||||
long timeDiff = sked.getSkedTimeEpoch() - now;
|
||||
|
||||
// Only draw if within the next 30 mins
|
||||
if (timeDiff >= 0 && timeDiff <= PREVIEW_TIME_MS) {
|
||||
|
||||
double percent = (double) timeDiff / PREVIEW_TIME_MS;
|
||||
double xPos = percent * width;
|
||||
xPos = clamp(xPos, 10, width - 10);
|
||||
|
||||
Node marker = createSkedMarker(sked);
|
||||
|
||||
applyAntennaEffect(marker, sked.getTargetAzimuth());
|
||||
|
||||
marker.setLayoutX(xPos);
|
||||
marker.setLayoutY(axisY - 18); // below candidate lanes, near axis
|
||||
this.getChildren().add(marker);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private double clamp(double v, double min, double max) {
|
||||
return Math.max(min, Math.min(max, v));
|
||||
}
|
||||
|
||||
/**
|
||||
* Logic:
|
||||
* If Antenna is ON TARGET -> Bright & Glowing.
|
||||
* If Antenna is OFF TARGET -> Transparent (Ghost).
|
||||
*/
|
||||
private void applyAntennaEffect(Node marker, double targetAz) {
|
||||
|
||||
// invalid azimuth -> keep readable
|
||||
if (!Double.isFinite(targetAz) || targetAz < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
double delta = Math.abs(currentAntennaAzimuth - targetAz);
|
||||
if (delta > 180) delta = 360 - delta;
|
||||
|
||||
final boolean onTarget = delta <= (beamWidth / 2.0);
|
||||
final boolean inBeam = delta <= beamWidth;
|
||||
|
||||
// Rule: only fade when we are clearly NOT pointing there
|
||||
final double iconOpacity = inBeam ? 1.0 : 0.30;
|
||||
|
||||
if (marker instanceof Group g) {
|
||||
// Never fade the whole group -> text stays readable
|
||||
g.setOpacity(1.0);
|
||||
|
||||
for (Node child : g.getChildren()) {
|
||||
if (child instanceof Label) {
|
||||
child.setOpacity(1.0);
|
||||
} else {
|
||||
child.setOpacity(iconOpacity);
|
||||
}
|
||||
}
|
||||
|
||||
// Add glow only if well centered (optional)
|
||||
if (onTarget) {
|
||||
g.setEffect(new DropShadow(10, Color.LIMEGREEN));
|
||||
g.setScaleX(1.10);
|
||||
g.setScaleY(1.10);
|
||||
} else {
|
||||
g.setEffect(null);
|
||||
g.setScaleX(1.0);
|
||||
g.setScaleY(1.0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// fallback
|
||||
marker.setOpacity(iconOpacity);
|
||||
marker.setEffect(onTarget ? new DropShadow(10, Color.LIMEGREEN) : null);
|
||||
}
|
||||
|
||||
public void setSkedTooltipExtraTextProvider(Function<ContestSked, String> provider) {
|
||||
this.skedTooltipExtraTextProvider = provider;
|
||||
}
|
||||
|
||||
|
||||
/** Existing marker for Skeds (diamond + label) */
|
||||
private Node createSkedMarker(ContestSked sked) {
|
||||
Polygon diamond = new Polygon(0.0, 0.0, 6.0, 6.0, 0.0, 12.0, -6.0, 6.0);
|
||||
|
||||
diamond.setFill(colorForPotential(sked.getOpportunityPotentialPercent()));
|
||||
|
||||
String baseToolTipFallBack = sked.getTargetCallsign() + " (" + sked.getBand() + ")\nAz: " + sked.getTargetAzimuth();
|
||||
|
||||
if (skedTooltipExtraTextProvider != null) {
|
||||
String extra = skedTooltipExtraTextProvider.apply(sked);
|
||||
if (extra != null && !extra.isBlank()) {
|
||||
baseToolTipFallBack += "\n" + extra;
|
||||
}
|
||||
}
|
||||
|
||||
Tooltip t = new Tooltip(baseToolTipFallBack);
|
||||
Tooltip.install(diamond, t);
|
||||
|
||||
Label lbl = new Label("SKED: " + sked.getTargetCallsign());
|
||||
// lbl.setFont(new Font(9));
|
||||
// lbl.setTextFill(Color.WHITE);
|
||||
lbl.setLayoutY(14);
|
||||
lbl.setLayoutX(-10);
|
||||
|
||||
lbl.setStyle(
|
||||
"-fx-text-fill: white;" +
|
||||
"-fx-font-weight: bold;" +
|
||||
"-fx-background-color: rgba(0,0,0,0.65);" +
|
||||
"-fx-background-radius: 6;" +
|
||||
"-fx-padding: 1 4 1 4;"
|
||||
);
|
||||
lbl.setEffect(new DropShadow(2, Color.BLACK));
|
||||
|
||||
|
||||
return new Group(diamond, lbl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Give me a color for a given potencial of a AP reflection
|
||||
*
|
||||
* @param p AS potencial
|
||||
* @return
|
||||
*/
|
||||
private Color colorForPotential(int p) {
|
||||
if (p >= 95) return Color.MAGENTA; // ~100%
|
||||
if (p >= 75) return Color.RED;
|
||||
if (p >= 50) return Color.YELLOW;
|
||||
return Color.DEEPSKYBLUE; // low potential
|
||||
}
|
||||
|
||||
/** New marker for Priority Candidates (triangle + label) */
|
||||
private Node createCandidateMarker(CandidateEvent ev) {
|
||||
|
||||
// Color derived from airplane potential (not urgency)
|
||||
Color markerColor = colorForPotential(ev.getOpportunityPotentialPercent());
|
||||
|
||||
// small triangle marker (points downwards)
|
||||
Polygon tri = new Polygon(-6.0, 0.0, 6.0, 0.0, 0.0, 10.0);
|
||||
tri.setFill(markerColor);
|
||||
|
||||
// Optional: small dot behind triangle (makes it easier to see)
|
||||
Circle dot = new Circle(4, markerColor);
|
||||
dot.setLayoutY(5); // center behind triangle
|
||||
|
||||
Label lbl = new Label(ev.getDisplayCallSign());
|
||||
lbl.setFont(new Font(9));
|
||||
lbl.setTextFill(Color.WHITE);
|
||||
lbl.setLayoutY(10);
|
||||
lbl.setLayoutX(-12);
|
||||
|
||||
lbl.setStyle(
|
||||
"-fx-text-fill: white;" +
|
||||
"-fx-font-weight: bold;" +
|
||||
"-fx-background-color: rgba(0,0,0,0.65);" +
|
||||
"-fx-background-radius: 6;" +
|
||||
"-fx-padding: 1 4 1 4;"
|
||||
);
|
||||
lbl.setEffect(new DropShadow(8, Color.BLACK));
|
||||
|
||||
// IMPORTANT: include dot + triangle + label in the Group
|
||||
Group g = new Group(dot, tri, lbl);
|
||||
|
||||
if (ev.getTooltipText() != null && !ev.getTooltipText().isBlank()) {
|
||||
Tooltip.install(g, new Tooltip(ev.getTooltipText()));
|
||||
}
|
||||
|
||||
g.setOnMouseClicked(e -> {
|
||||
if (onCandidateClicked != null) {
|
||||
onCandidateClicked.accept(ev);
|
||||
}
|
||||
});
|
||||
|
||||
return g;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Data object rendered by the Timeline ("priority candidate").
|
||||
* Created in Kst4ContestApplication from ScoreService TopCandidates + AirScout next-AP minute.
|
||||
*/
|
||||
public static class CandidateEvent {
|
||||
private final String callSignRaw;
|
||||
private final String displayCallSign;
|
||||
private final ChatCategory preferredChatCategory;
|
||||
|
||||
private final long timeUntilMs;
|
||||
private final int minuteBucket;
|
||||
private final int laneIndex;
|
||||
|
||||
private final double targetAzimuth;
|
||||
private final double score;
|
||||
|
||||
private final String tooltipText;
|
||||
|
||||
private final int opportunityPotentialPercent;
|
||||
|
||||
|
||||
public CandidateEvent(
|
||||
String callSignRaw,
|
||||
String displayCallSign,
|
||||
ChatCategory preferredChatCategory,
|
||||
long timeUntilMs,
|
||||
int minuteBucket,
|
||||
int laneIndex,
|
||||
double targetAzimuth,
|
||||
double score,
|
||||
int opportunityPotentialPercent,
|
||||
String tooltipText
|
||||
) {
|
||||
this.callSignRaw = callSignRaw;
|
||||
this.displayCallSign = displayCallSign;
|
||||
this.preferredChatCategory = preferredChatCategory;
|
||||
this.timeUntilMs = timeUntilMs;
|
||||
this.minuteBucket = minuteBucket;
|
||||
this.laneIndex = laneIndex;
|
||||
this.targetAzimuth = targetAzimuth;
|
||||
this.score = score;
|
||||
this.opportunityPotentialPercent = opportunityPotentialPercent;
|
||||
this.tooltipText = tooltipText;
|
||||
}
|
||||
|
||||
public String getCallSignRaw() { return callSignRaw; }
|
||||
public String getDisplayCallSign() { return displayCallSign; }
|
||||
public ChatCategory getPreferredChatCategory() { return preferredChatCategory; }
|
||||
|
||||
public long getTimeUntilMs() { return timeUntilMs; }
|
||||
public int getMinuteBucket() { return minuteBucket; }
|
||||
public int getLaneIndex() { return laneIndex; }
|
||||
|
||||
public double getTargetAzimuth() { return targetAzimuth; }
|
||||
public double getScore() { return score; }
|
||||
|
||||
public String getTooltipText() { return tooltipText; }
|
||||
public int getOpportunityPotentialPercent() { return opportunityPotentialPercent; }
|
||||
|
||||
}
|
||||
|
||||
public void setBeamWidthDeg(double beamWidthDeg) {
|
||||
if (beamWidthDeg > 0 && Double.isFinite(beamWidthDeg)) {
|
||||
this.beamWidth = beamWidthDeg;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
11
src/main/java/module-info.java
Normal file
11
src/main/java/module-info.java
Normal file
@@ -0,0 +1,11 @@
|
||||
module praktiKST {
|
||||
requires javafx.controls;
|
||||
requires jdk.xml.dom;
|
||||
requires java.sql;
|
||||
requires javafx.media;
|
||||
exports kst4contest.controller.interfaces;
|
||||
exports kst4contest.controller;
|
||||
exports kst4contest.locatorUtils;
|
||||
exports kst4contest.model;
|
||||
exports kst4contest.view;
|
||||
}
|
||||
201
src/main/resources/KST4ContestDefaultDay.css
Normal file
201
src/main/resources/KST4ContestDefaultDay.css
Normal file
@@ -0,0 +1,201 @@
|
||||
.button:pressed {
|
||||
-fx-border-color: #ff0000;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
-fx-border-color: #ff7777;
|
||||
}
|
||||
|
||||
.btn-showstate-enabled-default {
|
||||
/*-fx-background-color:linear-gradient(#f0ff35, #b8ee36),*/
|
||||
/*radial-gradient(center 50% -40%, radius 200%, #b8ee36 45%, #80c800 50%);*/
|
||||
-fx-background-radius: 6, 5;
|
||||
-fx-background-insets: 0, 1;
|
||||
-fx-effect: dropshadow( three-pass-box , rgba(0,0,0,0.4) , 5, 0.0 , 0 , 1 );
|
||||
-fx-text-fill: black;
|
||||
}
|
||||
.btn-showstate-enabled-default:hover {
|
||||
/*-fx-background-color:linear-gradient(#f0ff35, #b8ee36),*/
|
||||
/*radial-gradient(center 50% -40%, radius 200%, #b8ee36 45%, #80c800 50%);*/
|
||||
-fx-background-radius: 6, 5;
|
||||
-fx-background-insets: 0, 1;
|
||||
-fx-effect: dropshadow( three-pass-box , rgba(0,0,0,0.4) , 5, 0.0 , 0 , 1 );
|
||||
-fx-text-fill: black;
|
||||
}
|
||||
|
||||
.btn-showstate-enabled-furtherInfo {
|
||||
/*-fx-background-color:linear-gradient(#f0ff35, #b8ee36),*/
|
||||
/* radial-gradient(center 50% -40%, radius 200%, #b8ee36 45%, #80c800 50%);*/
|
||||
-fx-background-radius: 6, 5;
|
||||
-fx-background-insets: 0, 1;
|
||||
-fx-effect: dropshadow( three-pass-box , rgba(0,0,0,0.4) , 5, 0.0 , 0 , 1 );
|
||||
-fx-text-fill: green;
|
||||
}
|
||||
|
||||
.btn-showstate-enabled-furtherInfo:hover {
|
||||
/*-fx-background-color:linear-gradient(#f0ff35, #b8ee36),*/
|
||||
/* radial-gradient(center 50% -40%, radius 200%, #b8ee36 45%, #80c800 50%);*/
|
||||
-fx-background-radius: 6, 5;
|
||||
-fx-background-insets: 0, 1;
|
||||
-fx-effect: dropshadow( three-pass-box , rgba(0,0,0,0.4) , 5, 0.0 , 0 , 1 );
|
||||
-fx-text-fill: green;
|
||||
}
|
||||
|
||||
.btn-showstate-disabled {
|
||||
-fx-background-color:linear-gradient(#f0ff35, #111111),
|
||||
radial-gradient(center 50% -40%, radius 200%, #b8ee36 45%, #80c800 50%);
|
||||
-fx-background-radius: 6, 5;
|
||||
-fx-background-insets: 0, 1;
|
||||
-fx-effect: dropshadow( three-pass-box , rgba(0,0,0,0.4) , 5, 0.0 , 0 , 1 );
|
||||
-fx-text-fill: red;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.toggle-button:selected {
|
||||
-fx-background-color:linear-gradient(#f0ff35, #a9ff00),
|
||||
radial-gradient(center 50% -40%, radius 200%, #b8ee36 45%, #80c800 50%);
|
||||
-fx-background-radius: 6, 5;
|
||||
-fx-background-insets: 0, 1;
|
||||
-fx-effect: dropshadow( three-pass-box , rgba(0,0,0,0.4) , 5, 0.0 , 0 , 1 );
|
||||
-fx-text-fill: #395306;
|
||||
}
|
||||
|
||||
.text-field {
|
||||
-fx-prompt-text-fill: black;
|
||||
}
|
||||
|
||||
.text-field .text {
|
||||
-fx-fill: linear-gradient(from 0% 0% to 100% 200%, green 0%, lightgreen 100%);
|
||||
-fx-stroke: green;
|
||||
-fx-stroke-width: 0.2;
|
||||
-fx-font-size: 25px;
|
||||
}
|
||||
|
||||
.text-input-MYQRG1 {
|
||||
-fx-text-fill: linear-gradient(from 0% 0% to 100% 200%, orange 0%, red 100%);
|
||||
-fx-font-weight: 300;
|
||||
-fx-padding: 1,1,1,1;
|
||||
}
|
||||
|
||||
.button{
|
||||
-fx-focus-traversable: false;
|
||||
}
|
||||
|
||||
.button:hover{
|
||||
-fx-text-fill: linear-gradient(from 0% 0% to 100% 200%, blue 0%, red 100%);
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
-fx-background-color:linear-gradient(#f0ff35, #a9ff00),
|
||||
radial-gradient(center 50% -40%, radius 200%, lightblue 45%, orange 50%);
|
||||
-fx-background-radius: 6, 5;
|
||||
-fx-background-insets: 0, 1;
|
||||
-fx-effect: dropshadow( three-pass-box , rgba(0,0,0,0.4) , 5, 0.0 , 0 , 1 );
|
||||
-fx-text-fill: #395306;
|
||||
}
|
||||
|
||||
.buttonMyQrg1 {
|
||||
-fx-background-color: linear-gradient(from 0% 0% to 100% 200%, #00ffff 0%, #ff99ff 100%);
|
||||
-fx-background-radius: 6, 5;
|
||||
-fx-background-insets: 0, 1;
|
||||
-fx-effect: dropshadow( three-pass-box , rgba(0,0,0,0.4) , 5, 0.0 , 0 , 1 );
|
||||
-fx-text-fill: #395306;
|
||||
}
|
||||
|
||||
.toggle-button:selected {
|
||||
-fx-background-color: linear-gradient(from 0% 0% to 100% 200%, #00ffff 0%, #ff99ff 100%);
|
||||
-fx-background-radius: 6, 5;
|
||||
-fx-background-insets: 0, 1;
|
||||
-fx-effect: dropshadow( three-pass-box , rgba(0,0,0,0.4) , 5, 0.0 , 0 , 1 );
|
||||
-fx-text-fill: #395306;
|
||||
}
|
||||
|
||||
.table-view .column-header .text {
|
||||
-fx-fill: linear-gradient(from 0% 0% to 100% 200%, repeat, black 0%, red 50%);
|
||||
-fx-stroke: black;
|
||||
-fx-stroke-width: 0.3;
|
||||
}
|
||||
|
||||
.table-view .column-header .label{
|
||||
-fx-alignment: CENTER_LEFT;
|
||||
-fx-font-weight: none;
|
||||
}
|
||||
|
||||
.table-row-cell > .defaultText-column {
|
||||
-fx-text-fill: black;
|
||||
-fx-background-insets: 0 0 1 0px;
|
||||
-fx-selection-bar-non-focused: derive(-fx-base, 50%);
|
||||
|
||||
}
|
||||
|
||||
.table-row-cell > .messageToMe-column {
|
||||
-fx-text-fill: green;
|
||||
-fx-background-insets: 0 0 1 0px;
|
||||
-fx-selection-bar-non-focused: derive(-fx-base, 50%);
|
||||
}
|
||||
|
||||
.messageHighlightOwn-column { /*PM own message*/
|
||||
-fx-background-color: #00ffff;
|
||||
-fx-background-insets: 0 0 1 0px;
|
||||
-fx-text-fill: black;
|
||||
-fx-selection-bar-non-focused: derive(-fx-base, 50%);
|
||||
}
|
||||
|
||||
.messageHighlight30-column { /*PM for 30 sec: works*/
|
||||
-fx-text-fill: black;
|
||||
-fx-background-color: #33cc33;
|
||||
-fx-selection-bar-non-focused: derive(-fx-base, 50%);
|
||||
}
|
||||
.messageHighlight60-column { /*PM for 30 sec: works*/
|
||||
-fx-text-fill: black;
|
||||
-fx-background-color: #40bf40;
|
||||
-fx-selection-bar-non-focused: derive(-fx-base, 50%);
|
||||
}
|
||||
|
||||
.messageHighlight90-column { /*PM for 30 sec: works*/
|
||||
-fx-text-fill: black;
|
||||
-fx-background-color: #4db34d;
|
||||
-fx-selection-bar-non-focused: derive(-fx-base, 50%);
|
||||
}
|
||||
|
||||
.messageHighlight120-column { /*PM for 30 sec: works*/
|
||||
-fx-text-fill: black;
|
||||
-fx-background-color: #59a659;
|
||||
-fx-selection-bar-non-focused: derive(-fx-base, 50%);
|
||||
}
|
||||
.messageHighlight180-column { /*PM for 30 sec: works*/
|
||||
-fx-text-fill: black;
|
||||
-fx-background-color: #669966;
|
||||
-fx-selection-bar-non-focused: derive(-fx-base, 50%);
|
||||
}
|
||||
.messageHighlight300-column { /*PM for 30 sec: works*/
|
||||
-fx-text-fill: black;
|
||||
-fx-background-color: #738c73;
|
||||
-fx-selection-bar-non-focused: derive(-fx-base, 50%);
|
||||
}
|
||||
|
||||
|
||||
.table-cell-bold {
|
||||
-fx-font-weight: bold;
|
||||
}
|
||||
|
||||
.table-cell-inAngleAndRange {
|
||||
-fx-text-fill: green;
|
||||
-fx-font-weight: bold;
|
||||
}
|
||||
|
||||
.table-cell-100PercentAP { /*GEHT*/
|
||||
-fx-text-fill: linear-gradient(from 0% 0% to 100% 200%, #f98aff 0%, #f98aff 100%); /*purple*/;
|
||||
-fx-font-weight: bold;
|
||||
}
|
||||
|
||||
.table-cell-75PercentAP { /*GEHT*/
|
||||
-fx-text-fill: #fa6666;
|
||||
-fx-font-weight: bold;
|
||||
}
|
||||
|
||||
.table-cell-50PercentAP {
|
||||
-fx-text-fill: #fa9f66;
|
||||
-fx-font-weight: bold;
|
||||
}
|
||||
243
src/main/resources/KST4ContestDefaultEvening.css
Normal file
243
src/main/resources/KST4ContestDefaultEvening.css
Normal file
@@ -0,0 +1,243 @@
|
||||
.root {
|
||||
-fx-accent: #1e74c6;
|
||||
-fx-focus-color: -fx-accent;
|
||||
-fx-base: #373e43;
|
||||
-fx-control-inner-background: derive(-fx-base, 35%);
|
||||
-fx-control-inner-background-alt: -fx-control-inner-background ;
|
||||
}
|
||||
|
||||
.label{
|
||||
-fx-text-fill: lightgray;
|
||||
}
|
||||
|
||||
.label-callSignChatCatDescriptor {
|
||||
-fx-font-family: "Arial";
|
||||
-fx-font-size: 18px;
|
||||
-fx-text-fill: linear-gradient(from 0% 0% to 100% 200%, green 0%, lightgreen 100%);
|
||||
-fx-alignment: center;
|
||||
}
|
||||
|
||||
.text-field {
|
||||
-fx-prompt-text-fill: gray;
|
||||
}
|
||||
|
||||
.text-field .text {
|
||||
-fx-fill: linear-gradient(from 0% 0% to 100% 200%, green 0%, lightgreen 100%);
|
||||
-fx-stroke: green;
|
||||
-fx-stroke-width: 0.2;
|
||||
-fx-font-size: 25px;
|
||||
}
|
||||
|
||||
.text-input-MYQRG1 {
|
||||
-fx-text-fill: linear-gradient(from 0% 0% to 100% 200%, #f98aff 0%, #f98aff 100%); /*purple*/
|
||||
-fx-font-weight: 300;
|
||||
-fx-padding: 1,1,1,1;
|
||||
}
|
||||
|
||||
|
||||
.titulo{
|
||||
-fx-font-weight: bold;
|
||||
-fx-font-size: 18px;
|
||||
}
|
||||
|
||||
.button{
|
||||
-fx-focus-traversable: false;
|
||||
}
|
||||
|
||||
.button:hover{
|
||||
-fx-text-fill: white;
|
||||
-fx-border-color: #ff7777;
|
||||
}
|
||||
|
||||
.separator *.line {
|
||||
-fx-background-color: #3C3C3C;
|
||||
-fx-border-style: solid;
|
||||
-fx-border-width: 1px;
|
||||
}
|
||||
|
||||
.scroll-bar{
|
||||
-fx-background-color: derive(-fx-base,45%)
|
||||
}
|
||||
|
||||
.button:default {
|
||||
-fx-base: -fx-accent ;
|
||||
-fx-font-weight: bold;
|
||||
}
|
||||
|
||||
.button:pressed {
|
||||
-fx-border-color: #ff0000;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
-fx-background-color:linear-gradient(#f0ff35, #a9ff00),
|
||||
radial-gradient(center 50% -40%, radius 200%, #b8ee36 45%, #80c800 50%);
|
||||
-fx-background-radius: 6, 5;
|
||||
-fx-background-insets: 0, 1;
|
||||
-fx-effect: dropshadow( three-pass-box , rgba(0,0,0,0.4) , 5, 0.0 , 0 , 1 );
|
||||
-fx-text-fill: #395306;
|
||||
}
|
||||
|
||||
.buttonMyQrg1 {
|
||||
-fx-background-color: linear-gradient(from 0% 0% to 100% 200%, green 0%, lightgreen 100%);
|
||||
-fx-background-radius: 6, 5;
|
||||
-fx-background-insets: 0, 1;
|
||||
-fx-effect: dropshadow( three-pass-box , rgba(0,0,0,0.4) , 5, 0.0 , 0 , 1 );
|
||||
-fx-text-fill: #395306;
|
||||
}
|
||||
|
||||
.btn-showstate-enabled-default {
|
||||
-fx-base: #373e43;
|
||||
-fx-text-fill: lightgray;
|
||||
-fx-background-radius: 6, 5;
|
||||
-fx-background-insets: 0, 1;
|
||||
-fx-effect: dropshadow( three-pass-box , rgba(0,0,0,0.4) , 5, 0.0 , 0 , 1 );
|
||||
}
|
||||
|
||||
.btn-showstate-enabled-default:hover {
|
||||
-fx-base: #373e43;
|
||||
-fx-text-fill: black;
|
||||
-fx-background-radius: 6, 5;
|
||||
-fx-background-insets: 0, 1;
|
||||
-fx-effect: dropshadow( three-pass-box , rgba(0,0,0,0.4) , 5, 0.0 , 0 , 1 );
|
||||
}
|
||||
|
||||
.btn-showstate-enabled-furtherInfo {
|
||||
-fx-base: #373e43;
|
||||
-fx-text-fill: linear-gradient(from 0% 0% to 100% 200%, green 0%, lightgreen 100%);
|
||||
-fx-background-radius: 6, 5;
|
||||
|
||||
-fx-background-insets: 0, 1;
|
||||
|
||||
-fx-effect: dropshadow( three-pass-box , rgba(0,0,0,0.4) , 5, 0.0 , 0 , 1 );
|
||||
}
|
||||
|
||||
.btn-showstate-enabled-furtherInfo:hover {
|
||||
-fx-base: #373e43;
|
||||
-fx-text-fill: linear-gradient(from 0% 0% to 100% 200%, green 0%, lightgreen 100%);
|
||||
-fx-background-radius: 6, 5;
|
||||
|
||||
-fx-background-insets: 0, 1;
|
||||
|
||||
-fx-effect: dropshadow( three-pass-box , rgba(0,0,0,0.4) , 5, 0.0 , 0 , 1 );
|
||||
}
|
||||
|
||||
.btn-showstate-disabled {
|
||||
-fx-base: #373e43 ;
|
||||
-fx-font-weight: bold;
|
||||
-fx-text-fill: red;
|
||||
-fx-background-radius: 6, 5;
|
||||
|
||||
-fx-background-insets: 0, 1;
|
||||
|
||||
-fx-effect: dropshadow( three-pass-box , rgba(0,0,0,0.4) , 5, 0.0 , 0 , 1 );
|
||||
}
|
||||
|
||||
|
||||
|
||||
.toggle-button:selected {
|
||||
-fx-background-color:linear-gradient(#f0ff35, #a9ff00),
|
||||
radial-gradient(center 50% -40%, radius 200%, #b8ee36 45%, #80c800 50%);
|
||||
-fx-background-radius: 6, 5;
|
||||
-fx-background-insets: 0, 1;
|
||||
-fx-effect: dropshadow( three-pass-box , rgba(0,0,0,0.4) , 5, 0.0 , 0 , 1 );
|
||||
-fx-text-fill: #395306;
|
||||
}
|
||||
|
||||
|
||||
.table-view .column-header {
|
||||
-fx-background-color: linear-gradient(to right, #373838, #373838);
|
||||
}
|
||||
|
||||
.table-view .column-header .text {
|
||||
-fx-fill: linear-gradient(from 0% 0% to 100% 200%, repeat, green 0%, lightgreen 50%);
|
||||
-fx-stroke: green;
|
||||
-fx-stroke-width: 0.2;
|
||||
}
|
||||
|
||||
.table-view{
|
||||
/*-fx-background-color: derive(-fx-base, 10%);*/
|
||||
-fx-selection-bar-non-focused: derive(-fx-base, 50%);
|
||||
}
|
||||
|
||||
.table-view .column-header .label{
|
||||
-fx-alignment: CENTER_LEFT;
|
||||
-fx-font-weight: none;
|
||||
}
|
||||
|
||||
.table-row-cell > .defaultText-column {
|
||||
-fx-text-fill: white;
|
||||
-fx-background-insets: 0 0 1 0px;
|
||||
-fx-selection-bar-non-focused: derive(-fx-base, 50%);
|
||||
|
||||
}
|
||||
|
||||
.table-row-cell > .messageToMe-column {
|
||||
-fx-text-fill: lightgreen;
|
||||
-fx-background-insets: 0 0 1 0px;
|
||||
-fx-selection-bar-non-focused: derive(-fx-base, 50%);
|
||||
}
|
||||
|
||||
.messageHighlightOwn-column { /*PM own message*/
|
||||
-fx-background-color: #4674b9;
|
||||
-fx-background-insets: 0 0 1 0px;
|
||||
-fx-text-fill: white;
|
||||
-fx-selection-bar-non-focused: derive(-fx-base, 50%);
|
||||
}
|
||||
|
||||
.messageHighlight30-column { /*PM for 30 sec: works*/
|
||||
-fx-text-fill: white;
|
||||
-fx-background-color: #33cc33;
|
||||
-fx-selection-bar-non-focused: derive(-fx-base, 50%);
|
||||
}
|
||||
.messageHighlight60-column { /*PM for 30 sec: works*/
|
||||
-fx-text-fill: white;
|
||||
-fx-background-color: #40bf40;
|
||||
-fx-selection-bar-non-focused: derive(-fx-base, 50%);
|
||||
}
|
||||
|
||||
.messageHighlight90-column { /*PM for 30 sec: works*/
|
||||
-fx-text-fill: white;
|
||||
-fx-background-color: #4db34d;
|
||||
-fx-selection-bar-non-focused: derive(-fx-base, 50%);
|
||||
}
|
||||
|
||||
.messageHighlight120-column { /*PM for 30 sec: works*/
|
||||
-fx-text-fill: white;
|
||||
-fx-background-color: #59a659;
|
||||
-fx-selection-bar-non-focused: derive(-fx-base, 50%);
|
||||
}
|
||||
.messageHighlight180-column { /*PM for 30 sec: works*/
|
||||
-fx-text-fill: white;
|
||||
-fx-background-color: #669966;
|
||||
-fx-selection-bar-non-focused: derive(-fx-base, 50%);
|
||||
}
|
||||
.messageHighlight300-column { /*PM for 30 sec: works*/
|
||||
-fx-text-fill: white;
|
||||
-fx-background-color: #738c73;
|
||||
-fx-selection-bar-non-focused: derive(-fx-base, 50%);
|
||||
}
|
||||
|
||||
|
||||
.table-cell-bold {
|
||||
-fx-font-weight: bold;
|
||||
}
|
||||
|
||||
.table-cell-inAngleAndRange {
|
||||
-fx-text-fill: lightgreen;
|
||||
-fx-font-weight: bold;
|
||||
}
|
||||
|
||||
.table-cell-100PercentAP { /*GEHT*/
|
||||
-fx-text-fill: linear-gradient(from 0% 0% to 100% 200%, #f98aff 0%, #f98aff 100%); /*purple*/;
|
||||
-fx-font-weight: bold;
|
||||
}
|
||||
|
||||
.table-cell-75PercentAP { /*GEHT*/
|
||||
-fx-text-fill: #fa6666;
|
||||
-fx-font-weight: bold;
|
||||
}
|
||||
|
||||
.table-cell-50PercentAP {
|
||||
-fx-text-fill: #fa9f66;
|
||||
-fx-font-weight: bold;
|
||||
}
|
||||
BIN
src/main/resources/LTTR0.mp3
Normal file
BIN
src/main/resources/LTTR0.mp3
Normal file
Binary file not shown.
BIN
src/main/resources/LTTR1.mp3
Normal file
BIN
src/main/resources/LTTR1.mp3
Normal file
Binary file not shown.
BIN
src/main/resources/LTTR2.mp3
Normal file
BIN
src/main/resources/LTTR2.mp3
Normal file
Binary file not shown.
BIN
src/main/resources/LTTR3.mp3
Normal file
BIN
src/main/resources/LTTR3.mp3
Normal file
Binary file not shown.
BIN
src/main/resources/LTTR4.mp3
Normal file
BIN
src/main/resources/LTTR4.mp3
Normal file
Binary file not shown.
BIN
src/main/resources/LTTR5.mp3
Normal file
BIN
src/main/resources/LTTR5.mp3
Normal file
Binary file not shown.
BIN
src/main/resources/LTTR6.mp3
Normal file
BIN
src/main/resources/LTTR6.mp3
Normal file
Binary file not shown.
BIN
src/main/resources/LTTR7.mp3
Normal file
BIN
src/main/resources/LTTR7.mp3
Normal file
Binary file not shown.
BIN
src/main/resources/LTTR8.mp3
Normal file
BIN
src/main/resources/LTTR8.mp3
Normal file
Binary file not shown.
BIN
src/main/resources/LTTR9.mp3
Normal file
BIN
src/main/resources/LTTR9.mp3
Normal file
Binary file not shown.
BIN
src/main/resources/LTTRA.mp3
Normal file
BIN
src/main/resources/LTTRA.mp3
Normal file
Binary file not shown.
BIN
src/main/resources/LTTRB.mp3
Normal file
BIN
src/main/resources/LTTRB.mp3
Normal file
Binary file not shown.
BIN
src/main/resources/LTTRC.mp3
Normal file
BIN
src/main/resources/LTTRC.mp3
Normal file
Binary file not shown.
BIN
src/main/resources/LTTRD.mp3
Normal file
BIN
src/main/resources/LTTRD.mp3
Normal file
Binary file not shown.
BIN
src/main/resources/LTTRE.mp3
Normal file
BIN
src/main/resources/LTTRE.mp3
Normal file
Binary file not shown.
BIN
src/main/resources/LTTRF.mp3
Normal file
BIN
src/main/resources/LTTRF.mp3
Normal file
Binary file not shown.
BIN
src/main/resources/LTTRG.mp3
Normal file
BIN
src/main/resources/LTTRG.mp3
Normal file
Binary file not shown.
BIN
src/main/resources/LTTRH.mp3
Normal file
BIN
src/main/resources/LTTRH.mp3
Normal file
Binary file not shown.
BIN
src/main/resources/LTTRI.mp3
Normal file
BIN
src/main/resources/LTTRI.mp3
Normal file
Binary file not shown.
BIN
src/main/resources/LTTRJ.mp3
Normal file
BIN
src/main/resources/LTTRJ.mp3
Normal file
Binary file not shown.
BIN
src/main/resources/LTTRK.mp3
Normal file
BIN
src/main/resources/LTTRK.mp3
Normal file
Binary file not shown.
BIN
src/main/resources/LTTRL.mp3
Normal file
BIN
src/main/resources/LTTRL.mp3
Normal file
Binary file not shown.
BIN
src/main/resources/LTTRM.mp3
Normal file
BIN
src/main/resources/LTTRM.mp3
Normal file
Binary file not shown.
BIN
src/main/resources/LTTRMINUS.mp3
Normal file
BIN
src/main/resources/LTTRMINUS.mp3
Normal file
Binary file not shown.
BIN
src/main/resources/LTTRN.mp3
Normal file
BIN
src/main/resources/LTTRN.mp3
Normal file
Binary file not shown.
BIN
src/main/resources/LTTRO.mp3
Normal file
BIN
src/main/resources/LTTRO.mp3
Normal file
Binary file not shown.
BIN
src/main/resources/LTTRP.mp3
Normal file
BIN
src/main/resources/LTTRP.mp3
Normal file
Binary file not shown.
BIN
src/main/resources/LTTRPOINT.mp3
Normal file
BIN
src/main/resources/LTTRPOINT.mp3
Normal file
Binary file not shown.
BIN
src/main/resources/LTTRQ.mp3
Normal file
BIN
src/main/resources/LTTRQ.mp3
Normal file
Binary file not shown.
BIN
src/main/resources/LTTRQUESTMARK.mp3
Normal file
BIN
src/main/resources/LTTRQUESTMARK.mp3
Normal file
Binary file not shown.
BIN
src/main/resources/LTTRR.mp3
Normal file
BIN
src/main/resources/LTTRR.mp3
Normal file
Binary file not shown.
BIN
src/main/resources/LTTRS.mp3
Normal file
BIN
src/main/resources/LTTRS.mp3
Normal file
Binary file not shown.
BIN
src/main/resources/LTTRSPACE.mp3
Normal file
BIN
src/main/resources/LTTRSPACE.mp3
Normal file
Binary file not shown.
BIN
src/main/resources/LTTRSTROKE.mp3
Normal file
BIN
src/main/resources/LTTRSTROKE.mp3
Normal file
Binary file not shown.
BIN
src/main/resources/LTTRT.mp3
Normal file
BIN
src/main/resources/LTTRT.mp3
Normal file
Binary file not shown.
BIN
src/main/resources/LTTRU.mp3
Normal file
BIN
src/main/resources/LTTRU.mp3
Normal file
Binary file not shown.
BIN
src/main/resources/LTTRV.mp3
Normal file
BIN
src/main/resources/LTTRV.mp3
Normal file
Binary file not shown.
BIN
src/main/resources/LTTRW.mp3
Normal file
BIN
src/main/resources/LTTRW.mp3
Normal file
Binary file not shown.
BIN
src/main/resources/LTTRX.mp3
Normal file
BIN
src/main/resources/LTTRX.mp3
Normal file
Binary file not shown.
BIN
src/main/resources/LTTRY.mp3
Normal file
BIN
src/main/resources/LTTRY.mp3
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user