12 Commits
Author SHA1 Message Date
Marc Froehlich 3c7e20a6f8 Reduced opentopo api calls // added 4 char locator new-filter // added new band and tropo filter and priority sorter // decreased map load 2026-06-28 09:24:47 +02:00
Rsclub2_2 a3fbaa57f0 delete not needed files from repo 2026-06-27 20:04:19 +02:00
Marc Froehlich 0bb0b49c40 Fixed worked-23cm-tag when working with dxlog/n1mm logger 2026-06-27 14:31:14 +02:00
Marc Froehlich 1c75fda622 Added filters and sorters for Tropo reachability, AS availability and new big fields / locators. Added reachability band selector for tropo calculation based on a choosen band 2026-06-27 13:35:06 +02:00
Rsclub2_2 23b0fb5a62 Docs added for new Features 2026-06-24 21:56:41 +02:00
Marc Froehlich 3518516e50 changed UI design to save some space. Made the UI more reactive for smaller screens. Maximized resolution at startup to the possible maximum of the current display of the user 2026-06-24 17:34:48 +02:00
Rsclub2_2 d9b0073144 Merge branch 'main' into feature/MapViewAndFilters 2026-06-23 01:10:31 +02:00
Rsclub2_2 6a24c517e7 fix Pipeline Modules n map Linux 2026-06-23 00:33:25 +02:00
Marc Froehlich 7cf0ff2294 debugging map failure 2026-06-22 23:54:06 +02:00
Marc Froehlich af4edca4a7 debugging map failure 2026-06-22 23:31:10 +02:00
Marc Froehlich 4ed8ca6e84 debugging map failure 2026-06-22 22:45:07 +02:00
Marc Froehlich bf6f04c910 Added a map to show where other stn are // refactored message adding to tables for performance, max 30.000 msg now 2026-06-22 22:00:53 +02:00
13 changed files with 268 additions and 837 deletions
+3 -3
View File
@@ -11,18 +11,18 @@ env:
jobs:
compile:
name: Compile (Java 21)
name: Compile (Java 17)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4.1.7
- name: Set up Java 21
- name: Set up Java 17
uses: actions/setup-java@v4.1.0
with:
distribution: temurin
java-version: "21"
java-version: "17"
- name: Ensure mvnw is executable
run: chmod +x mvnw
+2 -37
View File
@@ -116,47 +116,12 @@ Die Datei `de.x08.KST4Contest.flatpakref` aus dem [aktuellen Release](https://gi
flatpak install de.x08.KST4Contest.flatpakref
```
Oder den Remote manuell hinzufügen (empfohlen für Nightly/Beta-Zugriff):
Oder den Remote manuell hinzufügen:
```bash
flatpak remote-add --if-not-exists kst4contest https://praktimarc.github.io/kst4contest/kst4contest.flatpakrepo
flatpak remote-add kst4contest https://praktimarc.github.io/kst4contest/
flatpak install kst4contest de.x08.KST4Contest
```
#### Flatpak-Kanäle (nightly / beta / stable)
Das Flatpak-Repository enthält drei Kanäle, die alle im selben Remote liegen:
| Kanal | Inhalt | Wird gebaut bei |
|---|---|---|
| `stable` | Aktuelle stabile Version | Jedem normalen Release-Tag |
| `beta` | Vorab-Version | Tags mit `beta-` Präfix |
| `nightly` | Entwicklungsstand (main-Branch) | Täglich / jedem Push auf main |
Remote einmalig hinzufügen, dann den gewünschten Kanal installieren:
```bash
flatpak remote-add --if-not-exists kst4contest https://praktimarc.github.io/kst4contest/kst4contest.flatpakrepo
# Stable (Standard)
flatpak install kst4contest de.x08.KST4Contest
# Beta
flatpak install kst4contest de.x08.KST4Contest//beta
# Nightly
flatpak install kst4contest de.x08.KST4Contest//nightly
```
> **Hinweis:** Flatpak erlaubt nur eine installierte Version pro App-ID gleichzeitig. Um den Kanal zu wechseln, zuerst die aktuelle Version deinstallieren:
> ```bash
> flatpak uninstall de.x08.KST4Contest
> flatpak install kst4contest de.x08.KST4Contest//nightly
> ```
Updates erfolgen wie gewohnt mit `flatpak update`.
Die `flatpakref`-Dateien für Beta und Stable liegen jeweils im entsprechenden [GitHub-Release](https://github.com/praktimarc/kst4contest/releases). Das Nightly-`flatpakref` (`de.x08.KST4Contest.nightly.flatpakref`) ist als Artifact im [GitHub Actions](https://github.com/praktimarc/kst4contest/actions)-Tab verfügbar (wird 14 Tage aufbewahrt) der Remote-Weg oben ist für Nightly daher meist einfacher.
### macOS
1. DMG-Datei für die eigene Architektur herunterladen (Apple Silicon oder Intel).
2. DMG-Datei öffnen.
+2 -37
View File
@@ -116,47 +116,12 @@ Download `de.x08.KST4Contest.flatpakref` from the [latest release](https://githu
flatpak install de.x08.KST4Contest.flatpakref
```
Or add the remote manually (recommended for nightly/beta access):
Or add the remote manually:
```bash
flatpak remote-add --if-not-exists kst4contest https://praktimarc.github.io/kst4contest/kst4contest.flatpakrepo
flatpak remote-add kst4contest https://praktimarc.github.io/kst4contest/
flatpak install kst4contest de.x08.KST4Contest
```
#### Flatpak Channels (nightly / beta / stable)
The Flatpak repository provides three channels, all served from the same remote:
| Channel | Content | Built on |
|---|---|---|
| `stable` | Current stable version | Every normal release tag |
| `beta` | Pre-release version | Tags prefixed with `beta-` |
| `nightly` | Development build (main branch) | Every push to main / daily |
Add the remote once, then install the channel you want:
```bash
flatpak remote-add --if-not-exists kst4contest https://praktimarc.github.io/kst4contest/kst4contest.flatpakrepo
# Stable (default)
flatpak install kst4contest de.x08.KST4Contest
# Beta
flatpak install kst4contest de.x08.KST4Contest//beta
# Nightly
flatpak install kst4contest de.x08.KST4Contest//nightly
```
> **Note:** Flatpak only allows one installed version per App-ID at a time. To switch channels, uninstall the current version first:
> ```bash
> flatpak uninstall de.x08.KST4Contest
> flatpak install kst4contest de.x08.KST4Contest//nightly
> ```
Updates work as usual with `flatpak update`.
The `flatpakref` files for beta and stable are attached to their respective [GitHub Releases](https://github.com/praktimarc/kst4contest/releases). The nightly `flatpakref` (`de.x08.KST4Contest.nightly.flatpakref`) is available as an artifact on the [GitHub Actions](https://github.com/praktimarc/kst4contest/actions) tab (kept for 14 days) — for nightly builds, the manual remote approach above is usually more convenient.
### macOS
1. Download the DMG file for your architecture (Apple Silicon or Intel).
2. Open the DMG file.
+27 -12
View File
@@ -24,12 +24,12 @@
<launcher>${project.artifactId}</launcher>
<appName>${project.artifactId}</appName>
<main.class>kst4contest.view.Kst4ContestApplication</main.class>
<java.version>21</java.version>
<java.version>17</java.version>
<required.maven.version>3.6.3</required.maven.version>
<jar.filename>${project.artifactId}-${project.version}</jar.filename>
<!-- Dependency versions -->
<javafx.version>21.0.5</javafx.version>
<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.44</lombok.version>
@@ -50,8 +50,8 @@
<maven.wrapper.plugin>3.2.0</maven.wrapper.plugin>
<moditect.maven.plugin>1.0.0.RC2</moditect.maven.plugin>
<jpackage.maven.plugin>0.1.3</jpackage.maven.plugin>
<maven.pmd.version>3.28.0</maven.pmd.version>
<pmd.version>7.17.0</pmd.version>
<maven.pmd.version>3.21.2</maven.pmd.version>
<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.9.8.2</spotbugs.maven.plugin>
@@ -93,13 +93,6 @@
<version>${javafx.version}</version>
</dependency>
<!-- JLayer: pure-Java MP3 decoder, used instead of JavaFX Media for Flatpak audio compatibility -->
<dependency>
<groupId>javazoom</groupId>
<artifactId>jlayer</artifactId>
<version>1.0.1</version>
</dependency>
<!-- SQLite -->
<dependency>
<groupId>org.xerial</groupId>
@@ -287,7 +280,30 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-pmd-plugin</artifactId>
<version>${maven.pmd.version}</version>
<dependencies>
<dependency>
<groupId>net.sourceforge.pmd</groupId>
<artifactId>pmd-core</artifactId>
<version>${pmd.version}</version>
</dependency>
<dependency>
<groupId>net.sourceforge.pmd</groupId>
<artifactId>pmd-java</artifactId>
<version>${pmd.version}</version>
</dependency>
<dependency>
<groupId>net.sourceforge.pmd</groupId>
<artifactId>pmd-javascript</artifactId>
<version>${pmd.version}</version>
</dependency>
<dependency>
<groupId>net.sourceforge.pmd</groupId>
<artifactId>pmd-jsp</artifactId>
<version>${pmd.version}</version>
</dependency>
</dependencies>
<configuration>
<sourceEncoding>${project.build.sourceEncoding}</sourceEncoding>
<minimumTokens>100</minimumTokens>
<targetJdk>${java.version}</targetJdk>
<linkXRef>false</linkXRef>
@@ -420,7 +436,6 @@
<addmodule>javafx.graphics</addmodule>
<addmodule>javafx.fxml</addmodule>
<addmodule>javafx.web</addmodule>
<addmodule>javafx.media</addmodule>
<addmodule>java.sql</addmodule>
<addmodule>java.net.http</addmodule>
<addmodule>jdk.crypto.ec</addmodule>
@@ -2179,39 +2179,32 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
}
/**
* Returns true when the member's four-character grid square has already been
* worked on any band.
*
* <p>This method intentionally ignores the currently enabled own bands. The grid
* status in the user list is meant to be a simple and robust any-band indicator.</p>
* Returns true when the member's gross field is not worked on at least one enabled
* station band. This is the predicate behind the "new locator" filter.
*
* @param member member to inspect
* @return true if the gross grid square was worked on any band
* @return true if the station is still a new locator on any enabled band
*/
public boolean isGridSquareWorkedAny(ChatMember member) {
public boolean isNewLocatorOnAnyEnabledBand(ChatMember member) {
if (member == null || member.getQra() == null) {
return false;
}
return workedGrossFieldCache.isGrossFieldWorkedAny(member.getQra());
}
EnumSet<Band> enabledBands = reachabilityService == null
? getMyEnabledBandsFromPrefs(chatPreferences)
: reachabilityService.getEnabledStationBands();
/**
* Returns true when the member's four-character grid square has not been worked
* on any band yet.
*
* <p>This is the predicate behind the optional "Only new grids" filter. It no
* longer depends on active station band settings.</p>
*
* @param member member to inspect
* @return true if the gross grid square is still new
*/
public boolean isNewGridSquare(ChatMember member) {
if (member == null || member.getQra() == null) {
if (enabledBands.isEmpty()) {
return false;
}
return !workedGrossFieldCache.isGrossFieldWorkedAny(member.getQra());
for (Band band : enabledBands) {
if (!workedGrossFieldCache.isGrossFieldWorked(band, member.getQra())) {
return true;
}
}
return false;
}
public long getCurrentEpochTime() {
@@ -682,7 +682,8 @@ public class DBController {
String workedBandColumnName = helper_resolveWorkedBandColumnName(chatMemberToStore);
if (workedBandColumnName == null) {
return helper_updateWorkedAnyOnChatMember(chatMemberToStore);
System.out.println("[DBCtrl, Error]: unknown at which band the qso had been!");
return false;
}
// String updateWorkedSql =
@@ -716,47 +717,6 @@ public class DBController {
}
}
/**
* Updates only the global worked-any flag of a stored chatmember row.
*
* <p>This is used when an external logger confirms that a station was worked,
* but the software cannot reliably map the QSO to one of the persisted band
* columns. The UI status "x" is based on this global worked flag, so this method
* makes no-band or unsupported-band log entries persistent as worked-any.</p>
*
* @param chatMemberToStore chatmember that contains the worked call and optional locator
* @return true if an existing database row was updated
* @throws SQLException if the database write fails
*/
private synchronized boolean helper_updateWorkedAnyOnChatMember(ChatMember chatMemberToStore) throws SQLException {
if (chatMemberToStore == null
|| chatMemberToStore.getCallSignRaw() == null
|| chatMemberToStore.getCallSignRaw().isBlank()) {
return false;
}
String updateWorkedAnySql =
"UPDATE ChatMember SET worked = 1, "
+ "qra = CASE WHEN ? IS NOT NULL AND TRIM(?) <> '' AND LOWER(TRIM(?)) <> 'unknown' THEN ? ELSE qra END, "
+ "lastFlagsChangeEpochMs = ? WHERE callsign = ?;";
try (PreparedStatement preparedStatement = connection.prepareStatement(updateWorkedAnySql)) {
String qra = chatMemberToStore.getQra();
preparedStatement.setString(1, qra);
preparedStatement.setString(2, qra);
preparedStatement.setString(3, qra);
preparedStatement.setString(4, qra);
preparedStatement.setLong(5, System.currentTimeMillis());
preparedStatement.setString(6, chatMemberToStore.getCallSignRaw());
int affectedRows = preparedStatement.executeUpdate();
return affectedRows > 0;
}
}
/**
* Updates all not-QRV flags for a chatmember row. The method uses the normalized
* raw callsign and updates the timestamp so that automatic contest cleanup can
@@ -119,31 +119,6 @@ public final class WorkedGrossFieldCache {
.contains(grossField);
}
/**
* Checks whether a locator gross field has already been worked on any band.
*
* <p>The UI grid status and the "Only new grids" filter use this any-band logic.
* This avoids user errors caused by wrong active-band settings and fits single
* band contests such as NAC better.</p>
*
* @param locatorOrGrossField six-character locator or four-character gross field
* @return true if the gross field exists in the cache on any band
*/
public synchronized boolean isGrossFieldWorkedAny(String locatorOrGrossField) {
String grossField = extractGrossField(locatorOrGrossField);
if (grossField == null) {
return false;
}
for (Set<String> workedGrossFields : workedGrossFieldsByBand.values()) {
if (workedGrossFields != null && workedGrossFields.contains(grossField)) {
return true;
}
}
return false;
}
/**
* Tries to normalize a locator. Accepts a plain six-character locator or extracts
* one from exchange strings such as "001JO41HK".
@@ -1,18 +1,13 @@
package kst4contest.utils;
import javazoom.jl.decoder.JavaLayerException;
import javazoom.jl.player.Player;
import javafx.scene.media.Media;
import javafx.scene.media.MediaPlayer;
import kst4contest.ApplicationConstants;
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.File;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* This part of the client drives the sounds. Its a singleton instance. All audio outs are directed to this instance.<br>
@@ -118,16 +113,8 @@ public class PlayAudioUtils {
}
private final Queue<String> musicList = new ConcurrentLinkedQueue<>();
/** True once audio fails; prevents repeated error logs and further play attempts. */
private final AtomicBoolean audioUnavailable = new AtomicBoolean(false);
/** True while the drain loop is running; prevents duplicate concurrent drains. */
private final AtomicBoolean playing = new AtomicBoolean(false);
private final ExecutorService audioThread = Executors.newSingleThreadExecutor(r -> {
Thread t = new Thread(r, "audio-player");
t.setDaemon(true);
return t;
});
private Queue<Media> musicList = new LinkedList<Media>();
private MediaPlayer mediaPlayer ;
/**
* Plays notification sounds out of the windws 95 box by given action character<br/>
@@ -144,31 +131,40 @@ public class PlayAudioUtils {
*/
public void playNoiseLauncher(char actionChar) {
switch (actionChar){
case '-':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/tick.mp3"));
break;
case '!':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/NOISESTARTUP.mp3"));
break;
case 'C':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/NOISECQWINDOW.mp3"));
break;
case 'P':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/NOISEPMWINDOW.mp3"));
break;
case 'E':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/NOISEERROR.mp3"));
break;
case 'N':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/NOISENOTIFY.mp3"));
break;
default:
System.out.println("[KST4ContestApp, warning, letter not defined!]");
// 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();
}
@@ -187,124 +183,125 @@ public class PlayAudioUtils {
for (char letterToPlay: playThisInCW){
switch (letterToPlay){
case 'A':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRA.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRA.mp3")).toURI().toString()));
break;
case 'B':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRB.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRB.mp3")).toURI().toString()));
break;
case 'C':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRC.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRC.mp3")).toURI().toString()));
break;
case 'D':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRD.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRD.mp3")).toURI().toString()));
break;
case 'E':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRE.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRE.mp3")).toURI().toString()));
break;
case 'F':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRF.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRF.mp3")).toURI().toString()));
break;
case 'G':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRG.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRG.mp3")).toURI().toString()));
break;
case 'H':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRH.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRH.mp3")).toURI().toString()));
break;
case 'I':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRI.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRI.mp3")).toURI().toString()));
break;
case 'J':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRJ.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRJ.mp3")).toURI().toString()));
break;
case 'K':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRK.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRK.mp3")).toURI().toString()));
break;
case 'L':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRL.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRL.mp3")).toURI().toString()));
break;
case 'M':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRM.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRM.mp3")).toURI().toString()));
break;
case 'N':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRN.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRN.mp3")).toURI().toString()));
break;
case 'O':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRO.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRO.mp3")).toURI().toString()));
break;
case 'P':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRP.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRP.mp3")).toURI().toString()));
break;
case 'Q':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRQ.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRQ.mp3")).toURI().toString()));
break;
case 'R':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRR.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRR.mp3")).toURI().toString()));
break;
case 'S':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRS.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRS.mp3")).toURI().toString()));
break;
case 'T':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRT.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRT.mp3")).toURI().toString()));
break;
case 'U':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRU.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRU.mp3")).toURI().toString()));
break;
case 'V':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRV.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRV.mp3")).toURI().toString()));
break;
case 'W':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRW.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRW.mp3")).toURI().toString()));
break;
case 'X':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRX.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRX.mp3")).toURI().toString()));
break;
case 'Y':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRY.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRY.mp3")).toURI().toString()));
break;
case 'Z':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRZ.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRZ.mp3")).toURI().toString()));
break;
case '1':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTR1.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTR1.mp3")).toURI().toString()));
break;
case '2':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTR2.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTR2.mp3")).toURI().toString()));
break;
case '3':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTR3.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTR3.mp3")).toURI().toString()));
break;
case '4':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTR4.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTR4.mp3")).toURI().toString()));
break;
case '5':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTR5.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTR5.mp3")).toURI().toString()));
break;
case '6':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTR6.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTR6.mp3")).toURI().toString()));
break;
case '7':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTR7.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTR7.mp3")).toURI().toString()));
break;
case '8':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTR8.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTR8.mp3")).toURI().toString()));
break;
case '9':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTR9.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTR9.mp3")).toURI().toString()));
break;
case '0':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTR0.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTR0.mp3")).toURI().toString()));
break;
case '/':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRSTROKE.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRSTROKE.mp3")).toURI().toString()));
break;
case ' ':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRSPACE.mp3"));
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();
}
@@ -326,166 +323,162 @@ public class PlayAudioUtils {
for (char letterToPlay: spellThisWithVoice){
switch (letterToPlay){
case '!':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEBELL.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEBELL.mp3")).toURI().toString()));
break;
case '?':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEYOUGOTMAIL.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEYOUGOTMAIL.mp3")).toURI().toString()));
break;
case '#':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEHELLO.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEHELLO.mp3")).toURI().toString()));
break;
case '*':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE73.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE73.mp3")).toURI().toString()));
break;
case '$':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICESTROKEPORTABLE.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICESTROKEPORTABLE.mp3")).toURI().toString()));
break;
case 'A':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEA.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEA.mp3")).toURI().toString()));
break;
case 'B':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEB.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEB.mp3")).toURI().toString()));
break;
case 'C':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEC.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEC.mp3")).toURI().toString()));
break;
case 'D':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICED.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICED.mp3")).toURI().toString()));
break;
case 'E':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEE.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEE.mp3")).toURI().toString()));
break;
case 'F':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEF.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEF.mp3")).toURI().toString()));
break;
case 'G':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEG.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEG.mp3")).toURI().toString()));
break;
case 'H':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEH.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEH.mp3")).toURI().toString()));
break;
case 'I':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEI.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEI.mp3")).toURI().toString()));
break;
case 'J':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEJ.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEJ.mp3")).toURI().toString()));
break;
case 'K':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEK.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEK.mp3")).toURI().toString()));
break;
case 'L':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEL.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEL.mp3")).toURI().toString()));
break;
case 'M':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEM.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEM.mp3")).toURI().toString()));
break;
case 'N':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEN.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEN.mp3")).toURI().toString()));
break;
case 'O':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEO.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEO.mp3")).toURI().toString()));
break;
case 'P':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEP.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEP.mp3")).toURI().toString()));
break;
case 'Q':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEQ.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEQ.mp3")).toURI().toString()));
break;
case 'R':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICER.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICER.mp3")).toURI().toString()));
break;
case 'S':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICES.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICES.mp3")).toURI().toString()));
break;
case 'T':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICET.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICET.mp3")).toURI().toString()));
break;
case 'U':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEU.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEU.mp3")).toURI().toString()));
break;
case 'V':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEV.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEV.mp3")).toURI().toString()));
break;
case 'W':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEW.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEW.mp3")).toURI().toString()));
break;
case 'X':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEX.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEX.mp3")).toURI().toString()));
break;
case 'Y':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEY.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEY.mp3")).toURI().toString()));
break;
case 'Z':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEZ.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEZ.mp3")).toURI().toString()));
break;
case '1':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE1.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE1.mp3")).toURI().toString()));
break;
case '2':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE2.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE2.mp3")).toURI().toString()));
break;
case '3':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE3.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE3.mp3")).toURI().toString()));
break;
case '4':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE4.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE4.mp3")).toURI().toString()));
break;
case '5':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE5.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE5.mp3")).toURI().toString()));
break;
case '6':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE6.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE6.mp3")).toURI().toString()));
break;
case '7':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE7.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE7.mp3")).toURI().toString()));
break;
case '8':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE8.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE8.mp3")).toURI().toString()));
break;
case '9':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE9.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE9.mp3")).toURI().toString()));
break;
case '0':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE0.mp3"));
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE0.mp3")).toURI().toString()));
break;
case '/':
musicList.add(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICESTROKE.mp3"));
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() {
if (audioUnavailable.get()) {
musicList.clear();
// System.out.println("Kst4ContestApplication.playMusic");
if(musicList.peek() == null)
{
return;
}
// Only start a drain loop if none is running; the running loop will pick up new items itself.
if (!playing.compareAndSet(false, true)) return;
audioThread.submit(() -> {
String path;
while ((path = musicList.poll()) != null) {
if (audioUnavailable.get()) break;
try (FileInputStream fis = new FileInputStream(path);
BufferedInputStream bis = new BufferedInputStream(fis)) {
new Player(bis).play();
} catch (IOException | JavaLayerException e) {
audioUnavailable.set(true);
musicList.clear();
System.out.println("[KST4ContestApp, warning, audio playback disabled (could not create media player): " + e + "]");
break;
}
}
playing.set(false);
// Items may have been enqueued between the last poll() and playing.set(false);
// restart the drain loop if so.
if (!musicList.isEmpty() && !audioUnavailable.get()) {
mediaPlayer = new MediaPlayer(musicList.poll());
mediaPlayer.setRate(1.0);
mediaPlayer.setOnReady(() -> {
mediaPlayer.play();
mediaPlayer.setOnEndOfMedia(() -> {
// mediaPlayer.dispose();
playMusic();
}
if (musicList.isEmpty()) {
// mediaPlayer.dispose();
}
});
});
}
}
@@ -67,17 +67,12 @@ import kst4contest.utils.ApplicationFileUtils;
import kst4contest.view.map.StationMapBridge;
import kst4contest.view.map.StationMapView;
import kst4contest.view.map.OfflineDemImportService;
import kst4contest.controller.WorkedGrossFieldCache;
public class Kst4ContestApplication extends Application implements StatusUpdateListener {
// private static final Kst4ContestApplication dbcontroller = new DBController();
// Null means Auto: use the lowest session band, or category fallback.
// Enables optional color highlighting of the QRA/grid cell.
// The status text itself remains visible independently of this flag.
private boolean gridSquareHighlightEnabled = false;
private PauseTransition userListRefreshCoalescer;
private String pendingUserListUpdateReason = "";
@@ -1265,75 +1260,6 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
}
});
/**
* Optionally highlights already worked grid squares in the QRA column.
*
* <p>The default table design is preserved as much as possible:
* <ul>
* <li>If grid coloring is disabled, the cell uses the standard JavaFX style.</li>
* <li>If the grid is not worked yet, the cell also uses the standard style.</li>
* <li>Only already worked grids get a subtle background color.</li>
* <li>Selected rows keep the normal table selection style.</li>
* </ul>
*
* <p>No font weight is changed here. The compact worked/grid status such as
* {@code xo} is emphasized only in the worked-any column.</p>
*/
qraCol.setCellFactory(column -> new TableCell<ChatMember, String>() {
@Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText(null);
setTooltip(null);
setStyle("");
return;
}
ChatMember member = getTableRow() == null ? null : getTableRow().getItem();
boolean gridWorked = member != null
&& chatcontroller != null
&& chatcontroller.isGridSquareWorkedAny(member);
String grossField = WorkedGrossFieldCache.extractGrossField(item);
setText(item);
setTooltip(new Tooltip(
"Grid status: "
+ (grossField == null ? "unknown" : grossField)
+ "\nGrid worked any: "
+ (gridWorked ? "yes" : "no")
));
/*
* Important:
* Do not style the cell unless the Grid color button is active AND the
* grid has already been worked. This keeps normal/new grids visually
* identical to the standard table design.
*/
if (!gridSquareHighlightEnabled || !gridWorked) {
setStyle("");
return;
}
/*
* Preserve normal selection appearance. A selected row should look like a
* selected row, not like a custom-colored QRA cell.
*/
if (isSelected() || (getTableRow() != null && getTableRow().isSelected())) {
setStyle("");
return;
}
/*
* Already worked grid:
* Use a subtle background derived from the current theme. No bold text.
*/
setStyle("-fx-background-color: derive(-fx-control-inner-background, -18%);");
}
});
TableColumn<ChatMember, String> qrBCol = new TableColumn<ChatMember, String>("QRB");
qrBCol.setCellValueFactory(new Callback<CellDataFeatures<ChatMember, String>, ObservableValue<String>>() {
@@ -1558,60 +1484,19 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
@Override
public ObservableValue<String> call(CellDataFeatures<ChatMember, String> cellDataFeatures) {
return new SimpleStringProperty(formatWorkedAnyGridStatus(cellDataFeatures.getValue()));
SimpleStringProperty wkd = new SimpleStringProperty();
if (cellDataFeatures.getValue().isWorked()) {
wkd.setValue("X");
} else {
wkd.setValue("");
}
return wkd;
}
});
wkdAny_subcol.prefWidthProperty().bind(tbl_chatMemberTable.widthProperty().divide(14));
/**
* Shows the compact worked/grid status and explains it by tooltip.
*/
wkdAny_subcol.setCellFactory(column -> new TableCell<ChatMember, String>() {
@Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setTooltip(null);
setStyle("");
return;
}
ChatMember member = getTableRow() == null ? null : getTableRow().getItem();
setText(item);
setTooltip(new Tooltip(buildWorkedAnyGridStatusTooltip(member)));
setAlignment(Pos.CENTER);
setStyle("-fx-font-weight: bold;");
}
});
/**
* Shows the compact worked/grid status and explains it by tooltip.
*/
wkdAny_subcol.setCellFactory(column -> new TableCell<ChatMember, String>() {
@Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setTooltip(null);
setStyle("");
return;
}
ChatMember member = getTableRow() == null ? null : getTableRow().getItem();
setText(item);
setTooltip(new Tooltip(buildWorkedAnyGridStatusTooltip(member)));
setAlignment(Pos.CENTER);
setStyle("-fx-font-weight: bold;");
}
});
TableColumn<ChatMember, String> vhfCol_subcol = new TableColumn<ChatMember, String>("144");
vhfCol_subcol
.setCellValueFactory(new Callback<CellDataFeatures<ChatMember, String>, ObservableValue<String>>() {
@@ -5602,79 +5487,29 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
ChatMessage sendMe = new ChatMessage();
/*
* Resolve the selected station safely.
*
* Normal case:
* - selectedCallSignInfoStageChatMember is set by clicking/selecting a station.
*
* Fallbacks:
* - current table selection
* - ScoreService selected member
*
* If no station is selected at all, the message is sent to the main category.
/**
* testing bugfix
*/
ChatMember effectiveSelectedMember = selectedCallSignInfoStageChatMember;
if (effectiveSelectedMember == null
&& tbl_chatMember != null
&& tbl_chatMember.getSelectionModel() != null) {
effectiveSelectedMember = tbl_chatMember.getSelectionModel().getSelectedItem();
}
ChatCategory sendMeInThisCat;
String categoryNumber = selectedCallSignInfoStageChatMember.getChatCategory().getCategoryNumber() +"";
if (effectiveSelectedMember == null
&& chatcontroller != null
&& chatcontroller.getScoreService() != null) {
effectiveSelectedMember = chatcontroller.getScoreService().getSelectedChatMember();
}
if (categoryNumber.equals(chatcontroller.getChatCategoryMain().getCategoryNumber() + "")) {
sendMeInThisCat = chatcontroller.getChatCategoryMain();
/*
* Default decision:
* If no station is selected, use the main category.
*/
ChatCategory sendMeInThisCat = chatcontroller.getChatCategoryMain();
} else if (categoryNumber.equals(chatcontroller.getChatCategorySecondChat().getCategoryNumber() + "")) {
sendMeInThisCat = chatcontroller.getChatCategorySecondChat();
/*
* If a station is selected and its category matches one of our active chat
* categories, send in that category. This keeps the old behaviour, but avoids
* NullPointerExceptions when no station has been selected yet.
*/
if (effectiveSelectedMember != null && effectiveSelectedMember.getChatCategory() != null) {
String categoryNumber = effectiveSelectedMember.getChatCategory().getCategoryNumber() + "";
if (chatcontroller.getChatCategoryMain() != null
&& categoryNumber.equals(chatcontroller.getChatCategoryMain().getCategoryNumber() + "")) {
sendMeInThisCat = chatcontroller.getChatCategoryMain();
} else if (chatcontroller.getChatCategorySecondChat() != null
&& categoryNumber.equals(chatcontroller.getChatCategorySecondChat().getCategoryNumber() + "")) {
sendMeInThisCat = chatcontroller.getChatCategorySecondChat();
} else {
sendMeInThisCat = chatcontroller.getChatCategoryMain(); // Chatcategory default decision
}
}
String selectedMemberDebugText;
if (effectiveSelectedMember == null) {
selectedMemberDebugText = "none, using main category";
} else {
selectedMemberDebugText = effectiveSelectedMember.getCallSignRaw()
+ " / "
+ effectiveSelectedMember.getChatCategory();
sendMeInThisCat = chatcontroller.getChatCategoryMain(); //Chatcategory default decision
}
System.out.println("<<<<<<<<<<<<<<<<<<<<< detected Category for sending message is "
+ sendMeInThisCat
+ " // selected member: "
+ selectedMemberDebugText
+ " evt "
+ event.isConsumed());
System.out.println("<<<<<<<<<<<<<<<<<<<<< detected Category for sending message is " + sendMeInThisCat + " // selected member: " + selectedCallSignInfoStageChatMember.getChatCategory() + " evt " + event.isConsumed() );
/**
* end testing bugfix
*/
sendMe.setChatCategory(sendMeInThisCat); // new in 1.26, answer in channel of the selected member if available
sendMe.setChatCategory(sendMeInThisCat); //new in 1.26, answer in channel of the selected member
sendMe.setMessageText(txt_chatMessageUserInput.getText());
// If operator sends "/cq CALL ..." => arm pending ping metrics for reply-time / no-reply tracking
@@ -5683,9 +5518,11 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
sendMe.setMessageDirectedToServer(false);
chatcontroller.getMessageTXBus().add(sendMe); // move the message to the tx queue
chatcontroller.getMessageTXBus().add(sendMe); //move the message to the tx queue
txt_chatMessageUserInput.clear();
}
});
@@ -6283,14 +6120,13 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
chatMemberTableFilterMaxQrbTF.setFocusTraversable(false);
ToggleButton btnTglNewLocator = new ToggleButton("Only new grids");
ToggleButton btnTglNewLocator = new ToggleButton("New locator");
Predicate<ChatMember> newLocatorPredicate = new Predicate<ChatMember>() {
@Override
public boolean test(ChatMember chatMember) {
return chatcontroller.isNewGridSquare(chatMember);
return chatcontroller.isNewLocatorOnAnyEnabledBand(chatMember);
}
};
btnTglNewLocator.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent actionEvent) {
@@ -6301,26 +6137,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
}
}
});
btnTglNewLocator.setTooltip(new Tooltip("Show only stations whose 4-character grid square has not been worked on any band yet"));
ToggleButton btnTglGridColor = new ToggleButton("Grid color");
/**
* Enables optional QRA-cell coloring for grid status.
*
* <p>This does not filter the list. It only colors the locator field:
* worked grid = darker, new grid = brighter.</p>
*/
btnTglGridColor.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent actionEvent) {
gridSquareHighlightEnabled = btnTglGridColor.isSelected();
tbl_chatMember.refresh();
}
});
btnTglGridColor.setTooltip(new Tooltip("Color the QRA field by grid status without filtering the station list"));
btnTglNewLocator.setTooltip(new Tooltip("Show only stations whose gross locator is still new on at least one active own band"));
ToggleButton btnTglReachableTropo = new ToggleButton("Tropo >=0dB");
Predicate<ChatMember> reachableTropoPredicate = new Predicate<ChatMember>() {
@@ -6990,7 +6807,6 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
chatMemberTableFilterWorkedBandFiltersHbx.getChildren().addAll(
btnTglNewLocator,
btnTglGridColor,
btnTglReachableTropo,
btnTglNewBands,
btnTglAsNext5Min
@@ -9610,75 +9426,6 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
return sb.toString();
}
/**
* Formats the global worked/grid status for the worked-any column.
*
* <p>Status characters follow the user feedback:
* <ul>
* <li>empty = call not worked, grid not worked</li>
* <li>x = call worked</li>
* <li>o = grid worked, call not worked</li>
* <li>xo = call and grid worked</li>
* </ul>
*
* <p>The call status is based on {@link ChatMember#isWorked()}, i.e. worked-any.
* The grid status is based on the four-character grid square worked on any band.</p>
*
* @param member station row
* @return compact status text
*/
private String formatWorkedAnyGridStatus(ChatMember member) {
if (member == null) {
return "";
}
boolean callWorked = member.isWorked();
boolean gridWorked = chatcontroller != null && chatcontroller.isGridSquareWorkedAny(member);
if (callWorked && gridWorked) {
return "xo";
}
if (callWorked) {
return "x";
}
if (gridWorked) {
return "o";
}
return "";
}
/**
* Builds a tooltip for the worked-any/grid-status cell.
*
* @param member station row
* @return tooltip text
*/
private String buildWorkedAnyGridStatusTooltip(ChatMember member) {
if (member == null) {
return "empty = call not worked, grid not worked\n"
+ "x = call worked\n"
+ "o = grid worked\n"
+ "xo = call and grid worked";
}
String grossField = WorkedGrossFieldCache.extractGrossField(member.getQra());
String gridText = grossField == null ? "unknown grid" : "grid " + grossField;
return "Worked status:\n"
+ "empty = call not worked, grid not worked\n"
+ "x = call worked\n"
+ "o = grid worked\n"
+ "xo = call and grid worked\n\n"
+ "This station:\n"
+ "Call worked: " + (member.isWorked() ? "yes" : "no") + "\n"
+ "Grid worked: " + (chatcontroller != null && chatcontroller.isGridSquareWorkedAny(member) ? "yes" : "no")
+ " (" + gridText + ")";
}
// /**
// *
// * resets the style of the not selected direction buttons
@@ -9959,72 +9706,6 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
// }
/**
* Returns the best currently selected ChatMember for actions that depend on a
* selected station.
*
* <p>The normal source is selectedCallSignInfoStageChatMember. As a fallback,
* the current table selection and the ScoreService selection are checked. If
* nothing is selected yet, null is returned. This is valid and must be handled
* by the caller.</p>
*
* @return selected ChatMember or null if no station is selected
*/
private ChatMember getEffectiveSelectedChatMember() {
if (selectedCallSignInfoStageChatMember != null) {
return selectedCallSignInfoStageChatMember;
}
if (tbl_chatMember != null
&& tbl_chatMember.getSelectionModel() != null
&& tbl_chatMember.getSelectionModel().getSelectedItem() != null) {
return tbl_chatMember.getSelectionModel().getSelectedItem();
}
if (chatcontroller != null
&& chatcontroller.getScoreService() != null
&& chatcontroller.getScoreService().getSelectedChatMember() != null) {
return chatcontroller.getScoreService().getSelectedChatMember();
}
return null;
}
/**
* Resolves the chat category for an outgoing operator message.
*
* <p>If a station is selected, the message is sent in the category of that
* station. If no station is selected yet, the message is sent in the main chat
* category. This prevents NullPointerExceptions when the operator writes to the
* chat immediately after joining.</p>
*
* @param selectedMember selected station, may be null
* @return category for the outgoing message, normally main or second category
*/
private ChatCategory resolveOutgoingChatCategory(ChatMember selectedMember) {
ChatCategory mainCategory = chatcontroller.getChatCategoryMain();
ChatCategory secondCategory = chatcontroller.getChatCategorySecondChat();
if (selectedMember == null || selectedMember.getChatCategory() == null) {
return mainCategory;
}
String categoryNumber = selectedMember.getChatCategory().getCategoryNumber() + "";
if (mainCategory != null
&& categoryNumber.equals(mainCategory.getCategoryNumber() + "")) {
return mainCategory;
}
if (secondCategory != null
&& categoryNumber.equals(secondCategory.getCategoryNumber() + "")) {
return secondCategory;
}
return mainCategory;
}
/**
* helper method, in cases of QRG ending qith 0 as double values, it will fill the ending 0 to the qrg string
* @param raw
@@ -214,11 +214,6 @@ public final class MapHtmlResources {
<body class="kst-theme-light">
<div id="map"></div>
<script>
// JavaFX 21 WebView can render Leaflet's CSS translate3d tile positioning incorrectly.
// Disable Leaflet 3D transforms before leaflet.js is loaded to keep tile rendering stable.
window.L_DISABLE_3D = true;
</script>
<script>
""" + leafletJs + """
</script>
@@ -371,31 +366,18 @@ public final class MapHtmlResources {
applyThemeClass();
map = L.map('map', {
zoomControl: true,
zoomAnimation: false,
fadeAnimation: false,
markerZoomAnimation: false,
inertia: false
zoomControl: true
}).setView([51.0, 10.0], 6);
jsLog('Leaflet map initialized');
const tileLayer = L.tileLayer(
L.tileLayer(
'http://127.0.0.1:' + window._kstTileProxyPort + '/tiles/{s}/{z}/{x}/{y}.png',
{
maxZoom: 18,
updateWhenZooming: false,
keepBuffer: 4,
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
}
);
tileLayer.on('tileerror', function (event) {
const source = event && event.tile ? event.tile.src : 'unknown';
jsError('OSM tile load failed: ' + source);
});
tileLayer.addTo(map);
).addTo(map);
map.createPane('beamPane');
map.getPane('beamPane').style.zIndex = 410;
@@ -434,47 +416,22 @@ public final class MapHtmlResources {
}
function invalidateSize() {
if (!map) {
return;
}
const mapElement = document.getElementById('map');
const domWidth = mapElement ? mapElement.clientWidth : -1;
const domHeight = mapElement ? mapElement.clientHeight : -1;
jsLog('invalidateSize dom=' + domWidth + 'x' + domHeight
+ ' leafletBefore=' + map.getSize().x + 'x' + map.getSize().y);
window.requestAnimationFrame(function () {
map.invalidateSize({
animate: false,
pan: false,
debounceMoveend: true
});
window.setTimeout(function () {
map.invalidateSize({
animate: false,
pan: false,
debounceMoveend: true
});
jsLog('invalidateSize leafletAfter=' + map.getSize().x + 'x' + map.getSize().y);
if (invalidateNotifyTimer) {
window.clearTimeout(invalidateNotifyTimer);
}
invalidateNotifyTimer = window.setTimeout(function () {
notifyViewport();
}, 120);
}, 80);
});
}
function resize() {
invalidateSize();
}
if (!map) {
return;
}
jsLog('invalidateSize');
map.invalidateSize(false);
if (invalidateNotifyTimer) {
window.clearTimeout(invalidateNotifyTimer);
}
invalidateNotifyTimer = window.setTimeout(function () {
notifyViewport();
}, 120);
}
function zoomIn() {
if (!map) {
@@ -736,7 +693,6 @@ public final class MapHtmlResources {
return {
init: init,
invalidateSize: invalidateSize,
resize: resize,
zoomIn: zoomIn,
zoomOut: zoomOut,
inspectPoint: inspectPoint,
@@ -49,17 +49,6 @@ public final class StationMapView {
private final Stage stage = new Stage();
private final WebView webView = new WebView();
private final WebEngine webEngine = webView.getEngine();
/**
* Keep a strong Java reference to the bridge object.
*
* JavaFX WebView/JSObject does not guarantee that a Java object passed through
* window.setMember(...) remains strongly reachable from the Java side. Newer
* JavaFX/WebKit/GC combinations can otherwise lose callbacks after a while.
*/
private final JavaMapBridge javaMapBridge = new JavaMapBridge();
private TileProxyServer tileProxyServer;
private Scene scene;
@@ -96,22 +85,6 @@ public final class StationMapView {
private List<MapCallsignRawSnapshot> lastSnapshots = List.of();
private MapCallsignRawSnapshot lastSelectedSnapshot;
private boolean filteredViewActive;
/**
* Last JSON payloads sent into Leaflet. Used to avoid clearing/recreating
* layers every score/table refresh. Rebuilding DOM markers every few seconds
* is visible as flicker in JavaFX 21 WebView.
*/
private String lastRenderedStationsJson = "";
private String lastRenderedBeamJson = "";
private String lastRenderedConnectionJson = "";
private String lastRenderedGridJson = "";
/**
* Prevent periodic refreshes from panning the map back to the selected station.
* Explicit calls to focusCallsignRaw(...) still pan immediately.
*/
private String lastAutoFocusedCallsignRaw = "";
private double homeLatitudeDeg = Double.NaN;
private double homeLongitudeDeg = Double.NaN;
@@ -219,11 +192,8 @@ public final class StationMapView {
return;
}
String normalizedCallsignRaw = callSignRaw.trim().toUpperCase(Locale.ROOT);
lastAutoFocusedCallsignRaw = normalizedCallsignRaw;
executeMapScriptSafely(
"window.kstMapApi.focusCallsignRaw(" + toJsStringLiteral(normalizedCallsignRaw) + ");"
"window.kstMapApi.focusCallsignRaw(" + toJsStringLiteral(callSignRaw.trim().toUpperCase(Locale.ROOT)) + ");"
);
}
@@ -322,8 +292,6 @@ public final class StationMapView {
webView.setMinWidth(0);
webView.setMinHeight(220);
webView.setPrefHeight(420);
webView.setMaxWidth(Double.MAX_VALUE);
webView.setMaxHeight(Double.MAX_VALUE);
VBox.setVgrow(webView, Priority.ALWAYS);
profileSection.setMinWidth(0);
@@ -655,7 +623,7 @@ public final class StationMapView {
webEngine.getLoadWorker().stateProperty().addListener((obs, oldState, newState) -> {
if (newState == javafx.concurrent.Worker.State.SUCCEEDED) {
JSObject window = (JSObject) webEngine.executeScript("window");
window.setMember("javaMapBridge", javaMapBridge);
window.setMember("javaMapBridge", new JavaMapBridge());
executeMapScriptSafely("window.kstMapApi.init();");
@@ -675,13 +643,11 @@ public final class StationMapView {
return;
}
Platform.runLater(() -> Platform.runLater(() ->
executeMapScriptSafely("window.kstMapApi.resize();")
));
Platform.runLater(() ->
Platform.runLater(() ->
executeMapScriptSafely("window.kstMapApi.invalidateSize();")));
}
private void handleWebViewClick(MouseEvent event) {
logWebViewMouseEvent("MOUSE_CLICKED", event);
@@ -944,6 +910,9 @@ public final class StationMapView {
detailPathProfileChart.setObstructionSummary(result.obstructionSummary());
}
private void renderAll() {
if (!mapReady) {
return;
@@ -961,78 +930,49 @@ public final class StationMapView {
renderBeam();
renderConnectionLine();
renderGridIfViewportKnown();
focusSelectedStationOnlyWhenSelectionChanged();
}
private void focusSelectedStationOnlyWhenSelectionChanged() {
if (lastSelectedSnapshot == null || lastSelectedSnapshot.callSignRaw() == null) {
lastAutoFocusedCallsignRaw = "";
return;
if (lastSelectedSnapshot != null) {
focusCallsignRaw(lastSelectedSnapshot.callSignRaw());
}
String selectedCallsignRaw = lastSelectedSnapshot.callSignRaw().trim().toUpperCase(Locale.ROOT);
if (selectedCallsignRaw.isBlank() || selectedCallsignRaw.equals(lastAutoFocusedCallsignRaw)) {
return;
}
focusCallsignRaw(selectedCallsignRaw);
}
private void renderStations() {
String stationsJson = toStationsJson(lastSnapshots);
if (stationsJson.equals(lastRenderedStationsJson)) {
return;
}
lastRenderedStationsJson = stationsJson;
executeMapScriptSafely(
"window.kstMapApi.setStations(" + toJsStringLiteral(stationsJson) + ");"
"window.kstMapApi.setStations(" + toJsStringLiteral(toStationsJson(lastSnapshots)) + ");"
);
}
private void renderBeam() {
String beamJson = "null";
if (!Double.isFinite(homeLatitudeDeg)
|| !Double.isFinite(homeLongitudeDeg)
|| beamWidthDeg <= 0.0
|| maxQrbKm <= 0.0) {
if (Double.isFinite(homeLatitudeDeg)
&& Double.isFinite(homeLongitudeDeg)
&& beamWidthDeg > 0.0
&& maxQrbKm > 0.0) {
List<double[]> sectorPoints = buildBeamPolygon(homeLatitudeDeg, homeLongitudeDeg, antennaAzimuthDeg, beamWidthDeg, maxQrbKm);
beamJson = toPointArrayJson(sectorPoints);
}
if (beamJson.equals(lastRenderedBeamJson)) {
executeMapScriptSafely("window.kstMapApi.setBeam('null');");
return;
}
lastRenderedBeamJson = beamJson;
List<double[]> sectorPoints = buildBeamPolygon(homeLatitudeDeg, homeLongitudeDeg, antennaAzimuthDeg, beamWidthDeg, maxQrbKm);
executeMapScriptSafely(
"window.kstMapApi.setBeam(" + toJsStringLiteral(beamJson) + ");"
"window.kstMapApi.setBeam(" + toJsStringLiteral(toPointArrayJson(sectorPoints)) + ");"
);
}
private void renderConnectionLine() {
String connectionJson = "null";
if (lastSelectedSnapshot == null || !lastSelectedSnapshot.hasUsablePosition()
|| !Double.isFinite(homeLatitudeDeg) || !Double.isFinite(homeLongitudeDeg)) {
if (lastSelectedSnapshot != null && lastSelectedSnapshot.hasUsablePosition()
&& Double.isFinite(homeLatitudeDeg) && Double.isFinite(homeLongitudeDeg)) {
List<double[]> points = List.of(
new double[]{homeLatitudeDeg, homeLongitudeDeg},
new double[]{lastSelectedSnapshot.latitudeDeg(), lastSelectedSnapshot.longitudeDeg()}
);
connectionJson = toPointArrayJson(points);
}
if (connectionJson.equals(lastRenderedConnectionJson)) {
executeMapScriptSafely("window.kstMapApi.setConnection('null');");
return;
}
lastRenderedConnectionJson = connectionJson;
List<double[]> points = List.of(
new double[]{homeLatitudeDeg, homeLongitudeDeg},
new double[]{lastSelectedSnapshot.latitudeDeg(), lastSelectedSnapshot.longitudeDeg()}
);
executeMapScriptSafely(
"window.kstMapApi.setConnection(" + toJsStringLiteral(connectionJson) + ");"
"window.kstMapApi.setConnection(" + toJsStringLiteral(toPointArrayJson(points)) + ");"
);
}
@@ -1071,19 +1011,11 @@ public final class StationMapView {
+ " cellPx=" + String.format(Locale.US, "%.1f/%.1f", renderPlan.estimatedCellWidthPx(), renderPlan.estimatedCellHeightPx())
+ " cells=" + visibleGridCells.size());
String gridJson = toGridJson(visibleGridCells, renderPlan);
if (gridJson.equals(lastRenderedGridJson)) {
return;
}
lastRenderedGridJson = gridJson;
executeMapScriptSafely(
"window.kstMapApi.setGrid(" + toJsStringLiteral(gridJson) + ");"
"window.kstMapApi.setGrid(" + toJsStringLiteral(toGridJson(visibleGridCells, renderPlan)) + ");"
);
}
private List<double[]> buildBeamPolygon(double startLatDeg,
double startLonDeg,
double centerAzimuthDeg,
@@ -19,10 +19,8 @@ import java.util.concurrent.Executors;
*/
final class TileProxyServer {
private static final int CACHE_MAX = 2048;
private static final int CACHE_MAX = 512;
private static final String USER_AGENT = "kst4contest/1.0 amateur-radio-contest-tool";
private static final int TILE_PROXY_THREADS = 12;
private static final int TILE_PROXY_BACKLOG = 128;
private final ServerSocket serverSocket;
private final ExecutorService executor;
@@ -41,9 +39,9 @@ final class TileProxyServer {
}
});
this.serverSocket = new ServerSocket(0, TILE_PROXY_BACKLOG, InetAddress.getByName("127.0.0.1"));
this.serverSocket = new ServerSocket(0, 16, InetAddress.getByName("127.0.0.1"));
this.executor = Executors.newFixedThreadPool(TILE_PROXY_THREADS, r -> {
this.executor = Executors.newFixedThreadPool(4, r -> {
Thread t = new Thread(r, "tile-proxy");
t.setDaemon(true);
return t;
@@ -134,7 +132,6 @@ final class TileProxyServer {
+ "Content-Type: image/png\r\n"
+ "Content-Length: " + tileData.length + "\r\n"
+ "Cache-Control: max-age=86400\r\n"
+ "Access-Control-Allow-Origin: *\r\n"
+ "Connection: close\r\n"
+ "\r\n";
out.write(header.getBytes());
+1 -2
View File
@@ -6,7 +6,6 @@ module praktiKST {
requires java.sql;
requires javafx.media;
requires jdk.jsobject;
requires jlayer;
requires java.net.http;
requires java.desktop;
requires jdk.crypto.ec;
@@ -18,4 +17,4 @@ module praktiKST {
opens kst4contest.view.map to javafx.web;
}
}