mirror of
https://github.com/praktimarc/kst4contest.git
synced 2026-09-12 04:05:34 +02:00
* Fixed Map: leaflet rendering in Java 21 Webview (disabling 3D in leaflet) * Added grid square coloring function and fixed bug of message categorizing after entering the chat and not selected a chatmember --------- Authored-by: Marc Froehlich <praktimarc@gmail.com>
2746 lines
90 KiB
Java
2746 lines
90 KiB
Java
package kst4contest.controller;
|
|
|
|
import java.net.*;
|
|
import java.nio.ByteBuffer;
|
|
import java.nio.channels.Selector;
|
|
import java.sql.SQLException;
|
|
import java.time.OffsetDateTime;
|
|
import java.time.ZoneOffset;
|
|
import java.util.*;
|
|
import java.util.concurrent.Executors;
|
|
import java.util.concurrent.LinkedBlockingQueue;
|
|
|
|
import javafx.application.Platform;
|
|
import javafx.beans.property.*;
|
|
import javafx.collections.FXCollections;
|
|
import javafx.collections.ListChangeListener;
|
|
import javafx.collections.ObservableList;
|
|
import javafx.collections.transformation.FilteredList;
|
|
import javafx.collections.transformation.SortedList;
|
|
import kst4contest.ApplicationConstants;
|
|
import kst4contest.controller.interfaces.PstRotatorEventListener;
|
|
import kst4contest.locatorUtils.DirectionUtils;
|
|
import kst4contest.logic.PriorityCalculator;
|
|
import kst4contest.model.*;
|
|
import kst4contest.test.MockKstServer;
|
|
import kst4contest.utils.PlayAudioUtils;
|
|
import kst4contest.view.Kst4ContestApplication;
|
|
|
|
import java.io.*;
|
|
import java.util.concurrent.ScheduledExecutorService;
|
|
import java.util.concurrent.TimeUnit;
|
|
import java.util.function.Consumer;
|
|
import java.util.function.Predicate;
|
|
|
|
|
|
|
|
/**
|
|
*
|
|
* Central Chat kst4contest.controller. Instantiate only one time per category of kst Chat.
|
|
* Provides complex data types for communication with the gui and drives the
|
|
* threads of telnet tx, telnet rx and message processing. <br/>
|
|
* <b>SINGLETON</b>
|
|
*
|
|
*/
|
|
public class ChatController implements ThreadStatusCallback, PstRotatorEventListener {
|
|
|
|
/**
|
|
* Chat selection ? 50/70 MHz..............1 144/432 MHz............2
|
|
* Microwave..............3 EME/JT65...............4 Low Band...............5 50
|
|
* MHz IARU Region 3...6 50 MHz IARU Region 2...7 144/432 MHz IARU R 2...8
|
|
* 144/432 MHz IARU R 3...9 kHz (2000-630m).......10 Warc (30,17,12m)......11 28
|
|
* MHz................12 Your choice :
|
|
*
|
|
*/
|
|
|
|
private static final boolean DEBUG_BAND_UPGRADE_HINT = true; //for new band hint
|
|
|
|
|
|
private PstRotatorClient rotatorClient;
|
|
private Consumer<Double> viewRotorCallback;
|
|
|
|
private Kst4ContestApplication view; //effectively final, for recoupling of the controller to the view
|
|
|
|
private StatusUpdateListener statusListener; //update info interface for the threads
|
|
|
|
|
|
public void setView(Kst4ContestApplication view) {
|
|
this.view = view;
|
|
}
|
|
|
|
private UpdateInformation updateInformation;
|
|
private ChatPreferences chatPreferences;
|
|
|
|
private ChatCategory chatCategoryMain;
|
|
private ChatCategory chatCategorySecondChat;
|
|
boolean connectedAndLoggedIn;
|
|
boolean connectedAndNOTLoggedIn;
|
|
boolean disconnected;
|
|
boolean disconnectionPerformedByUser = false;
|
|
|
|
|
|
public boolean isDisconnectionPerformedByUser() {
|
|
return disconnectionPerformedByUser;
|
|
}
|
|
|
|
public void setDisconnectionPerformedByUser(boolean disconnectionPerformedByUser) {
|
|
this.disconnectionPerformedByUser = disconnectionPerformedByUser;
|
|
}
|
|
|
|
|
|
public ChatCategory getChatCategorySecondChat() {
|
|
return chatCategorySecondChat;
|
|
}
|
|
|
|
public void setChatCategorySecondChat(ChatCategory chatCategorySecondChat) {
|
|
this.chatCategorySecondChat = chatCategorySecondChat;
|
|
}
|
|
|
|
public UpdateInformation getUpdateInformation() {
|
|
return updateInformation;
|
|
}
|
|
|
|
public void setUpdateInformation(UpdateInformation updateInformation) {
|
|
this.updateInformation = updateInformation;
|
|
}
|
|
|
|
public String getChatState() {
|
|
return chatState;
|
|
}
|
|
|
|
public void setChatState(String chatState) {
|
|
this.chatState = chatState;
|
|
}
|
|
|
|
public boolean isConnectedAndLoggedIn() {
|
|
return connectedAndLoggedIn;
|
|
}
|
|
|
|
public void setConnectedAndLoggedIn(boolean connectedAndLoggedIn) {
|
|
this.connectedAndLoggedIn = connectedAndLoggedIn;
|
|
}
|
|
|
|
public boolean isConnectedAndNOTLoggedIn() {
|
|
return connectedAndNOTLoggedIn;
|
|
}
|
|
|
|
public void setConnectedAndNOTLoggedIn(boolean connectedAndNOTLoggedIn) {
|
|
this.connectedAndNOTLoggedIn = connectedAndNOTLoggedIn;
|
|
}
|
|
|
|
public boolean isDisconnected() {
|
|
return disconnected;
|
|
}
|
|
|
|
public void setDisconnected(boolean disconnected) {
|
|
this.disconnected = disconnected;
|
|
}
|
|
|
|
|
|
public StatusUpdateListener getStatusListener() {
|
|
return statusListener;
|
|
}
|
|
|
|
public void setStatusListener(StatusUpdateListener statusListener) {
|
|
this.statusListener = statusListener;
|
|
}
|
|
|
|
@Override
|
|
public void onThreadStatus(String threadName, ThreadStateMessage threadStateMessage) {
|
|
// Weiterleiten an die View
|
|
if (statusListener != null) {
|
|
statusListener.onThreadStatusChanged(threadName, threadStateMessage);
|
|
} else System.out.println("ERRRRRRRRRRRRRRRRRRRRRRRRRRRÖRRRRRRRRRRRRRRRRRRR");
|
|
}
|
|
|
|
|
|
/********************************************************************************
|
|
* PSTRotator controlling
|
|
*******************************************************************************/
|
|
|
|
|
|
|
|
|
|
public void initRotor() {
|
|
// Beispiel: PSTRotator läuft lokal auf Port 12060
|
|
// Der Client wird automatisch auf 12061 hören.
|
|
rotatorClient = new PstRotatorClient("127.0.0.1", 12000, this, this); //TODO: IP anpassen, Port auch aus den prefs holen, default 12000
|
|
|
|
// Startet den Thread und das Polling
|
|
rotatorClient.start();
|
|
}
|
|
|
|
/**
|
|
* sets rotator to "AZ DEGREE" by button click <br/><br/>
|
|
* <b>Note that there is a workaround for spid rotators: <br/>
|
|
* The AZ will be set, after 'time' secs it will be controlled if the rotator started, If not, the rotator will<br/>
|
|
* be homed to 0 deg for very shord period, then the AZ value will be set again.
|
|
* </b>
|
|
* @param azimuth
|
|
*/
|
|
public void rotateTo(double azimuth) {
|
|
|
|
double beforeRotateAzWas = chatPreferences.getActualQTF().getValue();
|
|
|
|
if (rotatorClient != null) {
|
|
rotatorClient.setTrackingMode(false);
|
|
System.out.println("Chatcontroller, Info: turning ant to " + azimuth + " by user request");
|
|
rotatorClient.setAzimuth(azimuth);
|
|
|
|
Object lockDelay = new Object();
|
|
synchronized (lockDelay) {
|
|
try{
|
|
|
|
TimeUnit.SECONDS.sleep(2);; //wait 2s, then check if rotator does anything due SPID
|
|
// sometimes does simply not accept a rotating value for first try!
|
|
} catch (InterruptedException e) {
|
|
|
|
}
|
|
}
|
|
|
|
if (chatPreferences.getActualQTF().getValue() == beforeRotateAzWas) {
|
|
rotatorClient.setAzimuth(0); //do some reset
|
|
rotatorClient.setAzimuth(azimuth); //then rotate
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called when an external logger (Win-Test or UCXLog interface) reports that a QSO was logged.
|
|
*
|
|
* Process goal:
|
|
* 1) Detect whether the logged station is *still active (QRV)* on at least one *other* band
|
|
* that is enabled for "my station" (stn_bandActive[Band]) AND not worked yet (worked144/432/...).
|
|
* 2) If yes: trigger an on-screen hint (blinking status button) and play the existing sked-notification sound.
|
|
* 3) Request a score recompute so the station can become visible again (optional boost is applied in PriorityCalculator).
|
|
*
|
|
* IMPORTANT:
|
|
* - We do NOT use ChatMember.worked (UI-only filter flag) for scoring decisions.
|
|
* - We only use per-band worked flags (worked144, worked432, ...).
|
|
* - "QRV on band" is derived from recent entries in ChatMember.knownActiveBands.
|
|
*/
|
|
public void onExternalLogEntryReceived(String callSignRaw) {
|
|
|
|
if (callSignRaw == null || callSignRaw.isBlank()) return;
|
|
if (chatPreferences == null) return;
|
|
if (!chatPreferences.isNotify_bandUpgradeHintOnLogEnabled()) return;
|
|
|
|
final String callRaw = normalizeCallRaw(callSignRaw);
|
|
|
|
if (DEBUG_BAND_UPGRADE_HINT) {
|
|
System.out.println("[BandUpgradeHint] LOG received for call=" + callRaw);
|
|
}
|
|
|
|
// 1) Determine which bands I am active on (configured at startup via stn_bandActive* flags)
|
|
EnumSet<Band> myEnabledBands = getMyEnabledBandsFromPrefs(chatPreferences);
|
|
if (myEnabledBands.isEmpty()) return;
|
|
|
|
// 2) Determine which bands the station was recently seen active on (from Smart Frequency Extraction history)
|
|
final long now = System.currentTimeMillis();
|
|
final long maxAgeMs = TimeUnit.MINUTES.toMillis(30); // keep consistent with "recent activity" semantics
|
|
EnumSet<Band> stationOfferedBands = collectStationOfferedBandsFromHistory(callRaw, now, maxAgeMs);
|
|
if (stationOfferedBands.isEmpty()) return;
|
|
|
|
// 3) Keep only bands that I can actually work
|
|
stationOfferedBands.retainAll(myEnabledBands);
|
|
if (stationOfferedBands.isEmpty()) return;
|
|
|
|
// 4) Determine already worked bands (per-band flags only)
|
|
EnumSet<Band> workedBands = collectWorkedBands(callRaw);
|
|
|
|
// 5) Remaining bands = offered ∩ enabled - worked
|
|
EnumSet<Band> remainingBands = EnumSet.copyOf(stationOfferedBands);
|
|
remainingBands.removeAll(workedBands);
|
|
if (remainingBands.isEmpty()) return;
|
|
|
|
if (DEBUG_BAND_UPGRADE_HINT) {
|
|
System.out.println("[BandUpgradeHint] call=" + callRaw
|
|
+ " enabled=" + formatBandsHuman(myEnabledBands)
|
|
+ " offered=" + formatBandsHuman(stationOfferedBands)
|
|
+ " worked=" + (workedBands.isEmpty() ? "-" : formatBandsHuman(workedBands))
|
|
+ " remaining=" + formatBandsHuman(remainingBands));
|
|
}
|
|
|
|
// 6) Build UI text (button + tooltip)
|
|
String remainingHuman = formatBandsHuman(remainingBands);
|
|
String shortText = "BAND+ " + callRaw + " " + remainingHuman;
|
|
|
|
String tooltip = "Logged " + callRaw + ", but station is still QRV on additional band(s): "
|
|
+ remainingHuman
|
|
+ "\n(Enabled: " + formatBandsHuman(myEnabledBands)
|
|
+ " | Worked: " + (workedBands.isEmpty() ? "-" : formatBandsHuman(workedBands)) + ")";
|
|
|
|
ThreadStateMessage msg = new ThreadStateMessage("BandUpgradeHint", true, tooltip, false);
|
|
msg.setRunningInformationTextDescription(shortText);
|
|
|
|
// 7) Trigger status update -> View will blink a dedicated indicator button
|
|
onThreadStatus("BandUpgradeHint", msg);
|
|
|
|
// 8) Sound (re-use existing sked notification sound) - respects global simple-sound flag
|
|
if (chatPreferences.isNotify_playSimpleSounds()) {
|
|
try {
|
|
getPlayAudioUtils().playNoiseLauncher('!'); // same as SkedReminderService
|
|
} catch (Exception e) {
|
|
System.out.println("[ChatController, warning]: failed to play band-upgrade hint sound: " + e.getMessage());
|
|
}
|
|
}
|
|
|
|
// 9) Make sure score reacts quickly (boost is applied in PriorityCalculator if enabled)
|
|
if (getScoreService() != null) {
|
|
getScoreService().requestRecompute("BandUpgradeHint");
|
|
}
|
|
}
|
|
|
|
/** Normalize callsign raw to a stable key for comparisons. */
|
|
private static String normalizeCallRaw(String callRaw) {
|
|
return callRaw.trim().toUpperCase(Locale.ROOT);
|
|
}
|
|
|
|
/** Helper: create enabled-band set from preferences. */
|
|
private static EnumSet<Band> getMyEnabledBandsFromPrefs(ChatPreferences prefs) {
|
|
EnumSet<Band> s = EnumSet.noneOf(Band.class);
|
|
if (prefs.isStn_bandActive144()) s.add(Band.B_144);
|
|
if (prefs.isStn_bandActive432()) s.add(Band.B_432);
|
|
if (prefs.isStn_bandActive1240()) s.add(Band.B_1296);
|
|
if (prefs.isStn_bandActive2300()) s.add(Band.B_2320);
|
|
if (prefs.isStn_bandActive3400()) s.add(Band.B_3400);
|
|
if (prefs.isStn_bandActive5600()) s.add(Band.B_5760);
|
|
if (prefs.isStn_bandActive10G()) s.add(Band.B_10G);
|
|
return s;
|
|
}
|
|
|
|
/**
|
|
* Helper: union of all recently detected "QRV on band" entries across *all* ChatMember instances
|
|
* having the same callSignRaw (because a callsign may exist multiple times with different categories).
|
|
*/
|
|
private EnumSet<Band> collectStationOfferedBandsFromHistory(String callRaw, long nowMs, long maxAgeMs) {
|
|
|
|
EnumSet<Band> offered = EnumSet.noneOf(Band.class);
|
|
|
|
synchronized (getLst_chatMemberList()) {
|
|
for (ChatMember cm : getLst_chatMemberList()) {
|
|
if (cm == null || cm.getCallSignRaw() == null) continue;
|
|
if (!normalizeCallRaw(cm.getCallSignRaw()).equals(callRaw)) continue;
|
|
|
|
Map<Band, ChatMember.ActiveFrequencyInfo> map = cm.getKnownActiveBands();
|
|
if (map == null || map.isEmpty()) continue;
|
|
|
|
for (Map.Entry<Band, ChatMember.ActiveFrequencyInfo> e : map.entrySet()) {
|
|
if (e.getKey() == null || e.getValue() == null) continue;
|
|
|
|
long age = nowMs - e.getValue().timestampEpoch;
|
|
if (age >= 0 && age <= maxAgeMs) {
|
|
offered.add(e.getKey());
|
|
}
|
|
|
|
if (DEBUG_BAND_UPGRADE_HINT) {
|
|
System.out.println("[BandUpgradeHint] history call=" + callRaw
|
|
+ " band=" + e.getKey()
|
|
+ " freq=" + e.getValue().frequency
|
|
+ " ageMs=" + age);
|
|
}
|
|
|
|
|
|
}
|
|
}
|
|
}
|
|
return offered;
|
|
}
|
|
|
|
/**
|
|
* Helper: union of per-band worked flags across all ChatMember instances for the same call.
|
|
* IMPORTANT: ChatMember.worked is UI-only and NOT used here.
|
|
*/
|
|
private EnumSet<Band> collectWorkedBands(String callRaw) {
|
|
|
|
EnumSet<Band> worked = EnumSet.noneOf(Band.class);
|
|
|
|
synchronized (getLst_chatMemberList()) {
|
|
for (ChatMember cm : getLst_chatMemberList()) {
|
|
if (cm == null || cm.getCallSignRaw() == null) continue;
|
|
if (!normalizeCallRaw(cm.getCallSignRaw()).equals(callRaw)) continue;
|
|
|
|
if (cm.isWorked144()) worked.add(Band.B_144);
|
|
if (cm.isWorked432()) worked.add(Band.B_432);
|
|
if (cm.isWorked1240()) worked.add(Band.B_1296);
|
|
if (cm.isWorked2300()) worked.add(Band.B_2320);
|
|
if (cm.isWorked3400()) worked.add(Band.B_3400);
|
|
if (cm.isWorked5600()) worked.add(Band.B_5760);
|
|
if (cm.isWorked10G()) worked.add(Band.B_10G);
|
|
if (cm.isWorked24G()) worked.add(Band.B_24G); // optional, only if your Band enum supports it
|
|
}
|
|
}
|
|
return worked;
|
|
}
|
|
|
|
private static String formatBandsHuman(EnumSet<Band> bands) {
|
|
if (bands == null || bands.isEmpty()) return "-";
|
|
return bands.stream().map(ChatController::bandToHumanLabel).sorted().reduce((a, b) -> a + ", " + b).orElse("-");
|
|
}
|
|
|
|
private static String bandToHumanLabel(Band b) {
|
|
if (b == null) return "?";
|
|
return switch (b) {
|
|
case B_144 -> "2m";
|
|
case B_432 -> "70cm";
|
|
case B_1296 -> "23cm";
|
|
case B_2320 -> "13cm";
|
|
case B_3400 -> "9cm";
|
|
case B_5760 -> "6cm";
|
|
case B_10G -> "3cm";
|
|
case B_24G -> "1.2cm";
|
|
default -> b.name();
|
|
};
|
|
}
|
|
|
|
|
|
|
|
public void stopRotator() {
|
|
if (rotatorClient != null) {
|
|
rotatorClient.stop();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onAzimuthUpdate(double azimuth) {
|
|
// We are in the rotor client thread. JavaFX properties must be updated on the FX thread.
|
|
Runnable fxUpdate = () -> chatPreferences.getActualQTF().setValue(azimuth);
|
|
|
|
if (Platform.isFxApplicationThread()) {
|
|
fxUpdate.run();
|
|
} else {
|
|
Platform.runLater(fxUpdate);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onElevationUpdate(double elevation) {
|
|
// System.out.println("Neue Elevation: " + elevation);
|
|
//not used in first version
|
|
}
|
|
|
|
@Override
|
|
public void onModeUpdate(boolean isTracking) {
|
|
// System.out.println("Modus: " + (isTracking ? "Tracking" : "Manuell"));
|
|
//not used in first version
|
|
}
|
|
|
|
@Override
|
|
public void onMessageReceived(String raw) {
|
|
// Logging
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
* Helping
|
|
* @param targetCallSignRaw
|
|
* @param preferredCategory
|
|
* @param messageAfterCq
|
|
*/
|
|
public void queuePrivateCqMessage(String targetCallSignRaw, ChatCategory preferredCategory, String messageAfterCq) {
|
|
if (targetCallSignRaw == null || targetCallSignRaw.isBlank()) return;
|
|
|
|
ChatCategory categoryToUse = preferredCategory;
|
|
if (categoryToUse == null) {
|
|
ChatCategory last = lastInboundCategoryByCallSignRaw.get(targetCallSignRaw.trim().toUpperCase());
|
|
categoryToUse = (last != null) ? last : chatCategoryMain;
|
|
}
|
|
|
|
String text = "/cq " + targetCallSignRaw.trim().toUpperCase() + " " + (messageAfterCq == null ? "" : messageAfterCq);
|
|
|
|
ChatMessage msg = new ChatMessage();
|
|
msg.setChatCategory(categoryToUse);
|
|
msg.setMessageText(text);
|
|
msg.setMessageDirectedToServer(false);
|
|
|
|
messageTXBus.add(msg);
|
|
|
|
// Metrics: treat this as an outbound ping
|
|
stationMetricsService.tryRecordOutboundCq(text, System.currentTimeMillis());
|
|
|
|
// Scoring should react quickly to outbound actions
|
|
if (scoreService != null) {
|
|
scoreService.requestRecompute("outbound-cq");
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
*
|
|
* @param remoteChatMember with callsign of the foreign station
|
|
*/
|
|
|
|
public void airScout_SendAsShowPathPacket(ChatMember remoteChatMember) {
|
|
|
|
DatagramSocket dsocket;
|
|
|
|
String prefix_asSetpath ="ASSHOWPATH: \""+ this.getChatPreferences().getAirScout_asClientNameString()+ "\" \"" + this.getChatPreferences().getAirScout_asServerNameString() + "\" ";
|
|
|
|
// String prefix_asSetpath ="ASSHOWPATH: \"KST\" \"AS\" "; Old hard coded
|
|
String bandString = "1440000";
|
|
// String myCallAndMyLocString = chatPreferences.getStn_loginCallSign() + "," + chatPreferences.getStn_loginLocatorMainCat(); // original b4 bugfix 1266
|
|
String remoteCallAndLocString = remoteChatMember.getCallSign() +"," + remoteChatMember.getQra();
|
|
|
|
String ownCallSign="";
|
|
try {
|
|
if (chatPreferences.getStn_loginCallSign().contains("-")) {
|
|
ownCallSign = chatPreferences.getStn_loginCallSign().split("-")[0];
|
|
} else {
|
|
ownCallSign = chatPreferences.getStn_loginCallSign();
|
|
}
|
|
} catch (Exception e) {
|
|
System.out.println("[ASPERIODICAL, Error]: " + e.getMessage());
|
|
}
|
|
|
|
String myCallAndMyLocString = ownCallSign + "," + chatPreferences.getStn_loginLocatorMainCat(); // original b4 bugfix 1266
|
|
|
|
String host = "255.255.255.255";
|
|
// int port = 9872;
|
|
int port = chatPreferences.getAirScout_asCommunicationPort();
|
|
// System.out.println("<<<<<<<<<<<<<<<<<<<<ASPERI: " + port);
|
|
|
|
// byte[] message = "ASSETPATH: \"KST\" \"AS\" 1440000,DO5AMF,JN49GL,OK1MZM,JN89IW ".getBytes(); Original, ging
|
|
InetAddress address;
|
|
|
|
String queryStringToAirScout = "";
|
|
|
|
queryStringToAirScout += prefix_asSetpath + bandString + "," + myCallAndMyLocString + "," + remoteCallAndLocString+ "Å";
|
|
|
|
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();
|
|
}
|
|
|
|
|
|
}
|
|
|
|
/**
|
|
* starts the calculation scheduler for scores / priorities of skeds to be made
|
|
*/
|
|
private void startScoreScheduler() {
|
|
|
|
if (scoreScheduler != null && !scoreScheduler.isShutdown()) return;
|
|
|
|
scoreScheduler = Executors.newSingleThreadScheduledExecutor(r -> {
|
|
Thread t = new Thread(r);
|
|
t.setName("ScoreServiceScheduler");
|
|
t.setDaemon(true);
|
|
return t;
|
|
});
|
|
|
|
scoreScheduler.scheduleAtFixedRate(() -> {
|
|
try {
|
|
scoreService.tick();
|
|
} catch (Exception e) {
|
|
System.err.println("[ChatController] CRITICAL ERROR in ScoreService tick:");
|
|
e.printStackTrace();
|
|
}
|
|
}, 1, 3, TimeUnit.SECONDS);
|
|
|
|
scoreService.requestRecompute("startup");
|
|
System.out.println("[ChatController] ScoreService scheduler started.");
|
|
}
|
|
|
|
private void stopScoreScheduler() {
|
|
if (scoreScheduler != null) scoreScheduler.shutdownNow();
|
|
scoreScheduler = null;
|
|
}
|
|
|
|
|
|
/**
|
|
* Handles the disconnect of either the chat (Case DISCONNECTONLY) or the
|
|
* complete application life including all threads (case CLOSEALL)<br/><br/>
|
|
* Look in ApplicationConstants for the DISCSTRINGS
|
|
*
|
|
* @param action: "CLOSEALL" or "DISCONNECTONLYCHAT", on application close event
|
|
* (Settings Window closed), Disconnect on Disconnect-Button
|
|
* clicked (new connection may follow)
|
|
*/
|
|
public void disconnect(String action) {
|
|
|
|
// stopContextLoop(); //stops thread for calculating sked priorities
|
|
|
|
stopScoreScheduler();
|
|
|
|
this.dxClusterServer.stop();
|
|
|
|
this.setDisconnectionPerformedByUser(true);
|
|
|
|
try {
|
|
/**
|
|
* Kill UCX and Wintest packetreader by sending poison pill to the reader threads
|
|
*/
|
|
DatagramSocket dsocket;
|
|
|
|
String host = "255.255.255.255";
|
|
int port = chatPreferences.getLogsynch_ucxUDPWkdCallListenerPort();
|
|
InetAddress address;
|
|
|
|
address = InetAddress.getByName("255.255.255.255");
|
|
DatagramPacket packet = new DatagramPacket(ApplicationConstants.DISCONNECT_RDR_POISONPILL.getBytes(), ApplicationConstants.DISCONNECT_RDR_POISONPILL.length(), address, port);
|
|
DatagramPacket killWintestReaderPacket = new DatagramPacket(ApplicationConstants.DISCONNECT_RDR_POISONPILL.getBytes(), ApplicationConstants.DISCONNECT_RDR_POISONPILL.length(), address, chatPreferences.getLogsynch_wintestNetworkPort());
|
|
|
|
dsocket = new DatagramSocket();
|
|
dsocket.setBroadcast(true);
|
|
dsocket.send(packet);
|
|
dsocket.close();
|
|
|
|
dsocket = new DatagramSocket();
|
|
dsocket.setBroadcast(true);
|
|
dsocket.send(killWintestReaderPacket);
|
|
dsocket.close();
|
|
|
|
readUDPbyUCXThread.interrupt();
|
|
stopWintestUdpListener();
|
|
|
|
|
|
} catch (Exception error) {
|
|
System.out.println("Chatcrontroller, ERROR: unable to send poison pill to ucxThread");
|
|
}
|
|
|
|
try {
|
|
/**
|
|
* Kill AS packetreader by sending poison pill to the reader thread
|
|
*/
|
|
DatagramSocket dsocket;
|
|
|
|
String host = "255.255.255.255";
|
|
int port = chatPreferences.getAirScout_asCommunicationPort();
|
|
InetAddress address;
|
|
|
|
address = InetAddress.getByName("255.255.255.255");
|
|
DatagramPacket packet = new DatagramPacket(ApplicationConstants.DISCONNECT_RDR_POISONPILL.getBytes(), ApplicationConstants.DISCONNECT_RDR_POISONPILL.length(), address, port);
|
|
dsocket = new DatagramSocket();
|
|
dsocket.setBroadcast(true);
|
|
dsocket.send(packet);
|
|
dsocket.close();
|
|
} catch (Exception error) {
|
|
System.out.println("Chatcrontroller, ERROR: unable to send poison pill to ucxThread");
|
|
}
|
|
|
|
|
|
if (action.equals(ApplicationConstants.DISCSTRING_DISCONNECT_AND_CLOSE)) {
|
|
|
|
// rotatorClient.
|
|
|
|
this.lst_chatMemberList.clear();;
|
|
this.lst_clusterMemberList.clear();
|
|
|
|
this.setDisconnected(true);
|
|
this.setConnectedAndLoggedIn(false);
|
|
this.setConnectedAndNOTLoggedIn(false);
|
|
// disconnect telnet and kill all sockets and connections
|
|
|
|
keepAliveTimer.cancel();
|
|
keepAliveTimer.purge();
|
|
|
|
ChatMessage killThreadPoisonPillMsg = new ChatMessage();
|
|
killThreadPoisonPillMsg.setMessageText(ApplicationConstants.DISCONNECT_RDR_POISONPILL);
|
|
killThreadPoisonPillMsg.setMessageSenderName(ApplicationConstants.DISCONNECT_RDR_POISONPILL);
|
|
|
|
messageRXBus.clear();
|
|
messageTXBus.clear();
|
|
messageRXBus.add(killThreadPoisonPillMsg); //kills messageprocessor
|
|
messageTXBus.add(killThreadPoisonPillMsg); //kills writethread
|
|
|
|
// writeThread.interrupt();
|
|
// readThread.interrupt();
|
|
|
|
beaconTimer.purge();
|
|
beaconTimer.cancel();
|
|
ASQueryTimer.purge();
|
|
ASQueryTimer.cancel();
|
|
socketCheckTimer.purge();
|
|
socketCheckTimer.cancel();
|
|
|
|
userActualizationtimer.purge();
|
|
userActualizationtimer.cancel();
|
|
|
|
userActualizationtimer.purge();
|
|
userActualizationtimer.cancel();
|
|
|
|
// consoleReader.interrupt();
|
|
messageProcessor.interrupt();
|
|
|
|
readUDPbyUCXThread.interrupt();
|
|
stopWintestUdpListener();
|
|
|
|
|
|
|
|
airScoutUDPReaderThread.interrupt();
|
|
|
|
dbHandler.closeDBConnection();
|
|
|
|
dxClusterServer.stop();
|
|
|
|
rotatorClient.stopRotor();
|
|
rotatorClient.stop();
|
|
|
|
try {
|
|
|
|
if (socket != null) {
|
|
socket.close();
|
|
}
|
|
|
|
} catch (IOException e) {
|
|
// TODO Auto-generated catch block
|
|
e.printStackTrace();
|
|
} catch (Exception e2) {
|
|
// TODO Auto-generated catch block
|
|
e2.printStackTrace();
|
|
}
|
|
} else if (action.equals(ApplicationConstants.DISCSTRING_DISCONNECTONLY)){
|
|
|
|
this.lst_chatMemberList.clear();;
|
|
this.lst_clusterMemberList.clear();
|
|
|
|
|
|
this.setDisconnected(true);
|
|
this.setConnectedAndLoggedIn(false);
|
|
this.setConnectedAndNOTLoggedIn(false);
|
|
// disconnect telnet and kill all sockets and connections
|
|
|
|
keepAliveTimer.cancel();
|
|
keepAliveTimer.purge();
|
|
|
|
ChatMessage killThreadPoisonPillMsg = new ChatMessage();
|
|
killThreadPoisonPillMsg.setMessageText(ApplicationConstants.DISCONNECT_RDR_POISONPILL);
|
|
killThreadPoisonPillMsg.setMessageSenderName(ApplicationConstants.DISCONNECT_RDR_POISONPILL);
|
|
|
|
messageRXBus.clear();
|
|
messageTXBus.clear();
|
|
messageRXBus.add(killThreadPoisonPillMsg); //kills messageprocessor
|
|
messageTXBus.add(killThreadPoisonPillMsg); //kills writethread
|
|
|
|
writeThread.interrupt();
|
|
readThread.interrupt();
|
|
|
|
beaconTimer.purge();
|
|
beaconTimer.cancel();
|
|
ASQueryTimer.purge();
|
|
ASQueryTimer.cancel();
|
|
socketCheckTimer.purge();
|
|
socketCheckTimer.cancel();
|
|
|
|
userActualizationtimer.purge();
|
|
userActualizationtimer.cancel();
|
|
|
|
// consoleReader.interrupt();
|
|
// messageProcessor.interrupt();
|
|
|
|
readUDPbyUCXThread.interrupt(); //need poisonpill?
|
|
stopWintestUdpListener();
|
|
|
|
airScoutUDPReaderThread.interrupt(); //need poisonpill?
|
|
|
|
// dbHandler.closeDBConnection();
|
|
// this.dbHandler = null;
|
|
|
|
|
|
try {
|
|
|
|
if (socket != null) {
|
|
socket.close();
|
|
}
|
|
|
|
} catch (IOException e) {
|
|
// TODO Auto-generated catch block
|
|
e.printStackTrace();
|
|
} catch (Exception e2) {
|
|
// TODO Auto-generated catch block
|
|
e2.printStackTrace();
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// private ObservableList<ContestSked> activeSkeds = FXCollections.observableArrayList();
|
|
// public ObservableList<ContestSked> getActiveSkeds() {
|
|
// return activeSkeds;
|
|
// }
|
|
|
|
// SIGNAL: Ein Property, das wir hochzählen, um der GUI zu sagen "Daten haben sich geändert"
|
|
// private LongProperty uiRefreshSignal = new SimpleLongProperty(0);
|
|
// public LongProperty uiRefreshSignalProperty() {
|
|
// return uiRefreshSignal;
|
|
// }
|
|
|
|
// public void addSked(ContestSked sked) {
|
|
// Platform.runLater(() -> {
|
|
// this.activeSkeds.add(sked);
|
|
// runContextLoopCycle(); // Trigger sofort
|
|
// });
|
|
// }
|
|
|
|
// private PriorityCalculator priorityCalculator = new PriorityCalculator();
|
|
// private ScheduledExecutorService contextLoopService;
|
|
|
|
|
|
private ObservableList<ContestSked> activeSkeds =
|
|
FXCollections.synchronizedObservableList(FXCollections.observableArrayList());
|
|
|
|
public ObservableList<ContestSked> getActiveSkeds() {
|
|
return activeSkeds;
|
|
}
|
|
|
|
/**
|
|
* Priority score pipeline (replaces the former 1-second ContextLoop).
|
|
*/
|
|
private final Map<String, ChatCategory> lastInboundCategoryByCallSignRaw =
|
|
new java.util.concurrent.ConcurrentHashMap<>();
|
|
|
|
/** Tracks the last time WE sent a message containing a QRG to a specific callsign (UPPERCASE).
|
|
* Compared against knownActiveBands.timestampEpoch to decide whose QRG to use in a SKED. */
|
|
private final Map<String, Long> lastSentQRGToCallsign =
|
|
new java.util.concurrent.ConcurrentHashMap<>();
|
|
|
|
/** Call this whenever we send a PM to {@code receiverCallsign} that contains our QRG. */
|
|
public void recordOutboundQRG(String receiverCallsign) {
|
|
if (receiverCallsign == null) return;
|
|
lastSentQRGToCallsign.put(receiverCallsign.trim().toUpperCase(), System.currentTimeMillis());
|
|
System.out.println("[ChatController] Recorded outbound QRG to: " + receiverCallsign);
|
|
}
|
|
|
|
/** Returns epoch-ms of when we last sent our QRG to this callsign, or 0 if never. */
|
|
public long getLastSentQRGTimestamp(String callsign) {
|
|
if (callsign == null) return 0L;
|
|
return lastSentQRGToCallsign.getOrDefault(callsign.trim().toUpperCase(), 0L);
|
|
}
|
|
|
|
private final ScoreService scoreService = new ScoreService(this, new PriorityCalculator(), 15);
|
|
private ScheduledExecutorService scoreScheduler;
|
|
private final StationMetricsService stationMetricsService = new StationMetricsService();
|
|
private final SkedReminderService skedReminderService = new SkedReminderService(this);
|
|
|
|
|
|
public ScoreService getScoreService() {
|
|
return scoreService;
|
|
}
|
|
|
|
public void addSked(ContestSked sked) {
|
|
Platform.runLater(() -> {
|
|
this.activeSkeds.add(sked);
|
|
scoreService.requestRecompute("sked-added");
|
|
});
|
|
|
|
// Push sked to Win-Test via UDP if enabled
|
|
if (chatPreferences.isLogsynch_wintestNetworkListenerEnabled()) {
|
|
pushSkedToWinTest(sked);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Pushes a sked to Win-Test via UDP broadcast (LOCKSKED / ADDSKED / UNLOCKSKED).
|
|
* Runs on a background thread to avoid blocking the UI.
|
|
*/
|
|
private void pushSkedToWinTest(ContestSked sked) {
|
|
new Thread(() -> {
|
|
try {
|
|
InetAddress broadcastAddr = InetAddress.getByName(
|
|
chatPreferences.getLogsynch_wintestNetworkBroadcastAddress());
|
|
int port = chatPreferences.getLogsynch_wintestNetworkPort();
|
|
String stationName = chatPreferences.getLogsynch_wintestNetworkStationNameOfKST();
|
|
|
|
WinTestSkedSender sender = new WinTestSkedSender(stationName, broadcastAddr, port, this);
|
|
|
|
// Frequency resolution:
|
|
// Compare WHO sent a QRG most recently in the PM conversation:
|
|
// - OM sent their QRG last → use OM's Last Known QRG (ChatMember.frequency)
|
|
// - WE sent our QRG last → use our own Win-Test QRG (MYQRG)
|
|
// Fallback chain if no timestamps exist: OM's Last Known QRG → hardcoded default
|
|
double freqKHz = -1.0;
|
|
final long SKED_FREQ_MAX_AGE_MS = 60 * 60 * 1000L; // 60 minutes
|
|
|
|
ChatMember targetMember = resolveSkedTargetMember(sked.getTargetCallsign());
|
|
|
|
// Collect timestamps: when did the OM last mention their QRG? When did WE last send ours?
|
|
long omLastQRGTimestamp = 0L;
|
|
double omLastQRGMhz = 0.0;
|
|
if (targetMember != null && sked.getBand() != null) {
|
|
ChatMember.ActiveFrequencyInfo fi = targetMember.getKnownActiveBands().get(sked.getBand());
|
|
if (fi != null && fi.frequency > 0
|
|
&& (System.currentTimeMillis() - fi.timestampEpoch) <= SKED_FREQ_MAX_AGE_MS) {
|
|
omLastQRGTimestamp = fi.timestampEpoch;
|
|
omLastQRGMhz = fi.frequency;
|
|
}
|
|
}
|
|
long ourLastQRGTimestamp = getLastSentQRGTimestamp(sked.getTargetCallsign());
|
|
|
|
// Decision: who was more recent?
|
|
if (omLastQRGTimestamp > 0 && omLastQRGTimestamp >= ourLastQRGTimestamp) {
|
|
// OM mentioned their QRG MORE RECENTLY (or at same time) → use their QRG
|
|
freqKHz = omLastQRGMhz * 1000.0;
|
|
System.out.println("[ChatController] SKED freq: OM sent last → "
|
|
+ omLastQRGMhz + " MHz → " + freqKHz + " kHz");
|
|
|
|
} else if (ourLastQRGTimestamp > 0) {
|
|
// WE sent our QRG more recently → use our Win-Test QRG
|
|
try {
|
|
String qrgStr = chatPreferences.getMYQRGFirstCat().get();
|
|
if (qrgStr != null && !qrgStr.isBlank()) {
|
|
String cleaned = qrgStr.trim().replace(".", "");
|
|
double parsed = Double.parseDouble(cleaned) / 100.0;
|
|
if (parsed > 50000) {
|
|
freqKHz = parsed;
|
|
System.out.println("[ChatController] SKED freq: WE sent last → "
|
|
+ freqKHz + " kHz (raw: " + qrgStr + ")");
|
|
}
|
|
}
|
|
} catch (NumberFormatException ignored) { }
|
|
}
|
|
|
|
// Fallback A: OM's Last Known QRG from KST field (if no PM QRG exchange found at all)
|
|
if (freqKHz < 0 && targetMember != null) {
|
|
try {
|
|
String memberQrg = targetMember.getFrequency().get();
|
|
if (memberQrg != null && !memberQrg.isBlank()) {
|
|
double mhz = Double.parseDouble(memberQrg.trim());
|
|
freqKHz = mhz * 1000.0;
|
|
System.out.println("[ChatController] SKED freq: fallback Last Known QRG → "
|
|
+ mhz + " MHz → " + freqKHz + " kHz");
|
|
}
|
|
} catch (NumberFormatException ignored) { }
|
|
}
|
|
|
|
// Fallback B: hardcoded default
|
|
if (freqKHz < 0) {
|
|
freqKHz = 144300.0;
|
|
}
|
|
|
|
// Build notes string with target locator/azimuth info like reference: [JO02OB - 279°]
|
|
String targetLocator = resolveSkedTargetLocator(sked.getTargetCallsign());
|
|
String notes = "sked via KST4Contest";
|
|
if (targetLocator != null && !targetLocator.isBlank() && sked.getTargetAzimuth() > 0) {
|
|
notes = String.format("[%s - %.0f°] %s", targetLocator, sked.getTargetAzimuth(), notes);
|
|
} else if (targetLocator != null && !targetLocator.isBlank()) {
|
|
notes = String.format("[%s] %s", targetLocator, notes);
|
|
} else if (sked.getTargetAzimuth() > 0) {
|
|
notes = String.format("[%.0f°] %s", sked.getTargetAzimuth(), notes);
|
|
}
|
|
|
|
// Determine mode: -1 = auto-detect, 0 = CW, 1 = SSB
|
|
String modeStr = chatPreferences.getLogsynch_wintestSkedMode();
|
|
int modeOverride = -1; // AUTO
|
|
if ("CW".equalsIgnoreCase(modeStr)) modeOverride = 0;
|
|
else if ("SSB".equalsIgnoreCase(modeStr)) modeOverride = 1;
|
|
|
|
sender.pushSkedToWinTest(sked, freqKHz, notes, modeOverride);
|
|
} catch (Exception e) {
|
|
System.out.println("[ChatController] Error pushing sked to Win-Test: " + e.getMessage());
|
|
e.printStackTrace();
|
|
}
|
|
}, "WinTestSkedPush").start();
|
|
}
|
|
|
|
private ChatMember resolveSkedTargetMember(String targetCallsignRaw) {
|
|
if (targetCallsignRaw == null || targetCallsignRaw.isBlank()) {
|
|
return null;
|
|
}
|
|
String normalizedTargetCall = normalizeCallRaw(targetCallsignRaw);
|
|
synchronized (getLst_chatMemberList()) {
|
|
for (ChatMember member : getLst_chatMemberList()) {
|
|
if (member == null || member.getCallSignRaw() == null) continue;
|
|
if (normalizeCallRaw(member.getCallSignRaw()).equals(normalizedTargetCall)) {
|
|
return member;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private String resolveSkedTargetLocator(String targetCallsignRaw) {
|
|
if (targetCallsignRaw == null || targetCallsignRaw.isBlank()) {
|
|
return null;
|
|
}
|
|
|
|
String normalizedTargetCall = normalizeCallRaw(targetCallsignRaw);
|
|
synchronized (getLst_chatMemberList()) {
|
|
for (ChatMember member : getLst_chatMemberList()) {
|
|
if (member == null || member.getCallSignRaw() == null) continue;
|
|
if (!normalizeCallRaw(member.getCallSignRaw()).equals(normalizedTargetCall)) continue;
|
|
|
|
String locator = member.getQra();
|
|
if (locator != null && !locator.isBlank()) {
|
|
return locator.trim().toUpperCase(Locale.ROOT);
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public StationMetricsService getStationMetricsService() {
|
|
return stationMetricsService;
|
|
}
|
|
|
|
public SkedReminderService getSkedReminderService() {
|
|
return skedReminderService;
|
|
}
|
|
|
|
/**
|
|
* saves the last recognized chat category for a chatmember, for example when we seen a message
|
|
* @param callSignRaw
|
|
* @param category
|
|
*/
|
|
public void rememberLastInboundCategory(String callSignRaw, ChatCategory category) {
|
|
if (callSignRaw == null || category == null) return;
|
|
lastInboundCategoryByCallSignRaw.put(callSignRaw.trim().toUpperCase(), category);
|
|
}
|
|
|
|
public Map<String, ChatCategory> snapshotLastInboundCategoryMap() {
|
|
return new HashMap<>(lastInboundCategoryByCallSignRaw);
|
|
}
|
|
|
|
public List<ChatMember> snapshotChatMembers() {
|
|
synchronized (getLst_chatMemberList()) {
|
|
return new ArrayList<>(getLst_chatMemberList());
|
|
}
|
|
}
|
|
|
|
public List<ContestSked> snapshotActiveSkeds() {
|
|
synchronized (activeSkeds) {
|
|
return new ArrayList<>(activeSkeds);
|
|
}
|
|
}
|
|
|
|
public void requestRemoveExpiredSkeds(long nowEpochMs) {
|
|
Platform.runLater(() -> {
|
|
synchronized (activeSkeds) {
|
|
activeSkeds.removeIf(sked -> (nowEpochMs - sked.getSkedTimeEpoch()) > 300_000);
|
|
}
|
|
});
|
|
}
|
|
|
|
private String userName;
|
|
private String password;
|
|
private String showedName;
|
|
private String qra;
|
|
|
|
private String chatState;
|
|
|
|
// private String hostname = "109.90.0.130";
|
|
private String hostname;
|
|
// private String praktiKSTVersion = "praktiKST 1.0";
|
|
private String praktiKSTVersionInfo = "2022-10 - 2022-12\ndeveloped by DO5AMF, Marc\nContact: praktimarc@gmail.com\nDonations via paypal are welcome";
|
|
|
|
private int port = 23001; // kst4contest.test 4 23001 //TODO: auslagern in Chatprefs
|
|
private ReadUDPbyUCXMessageThread readUDPbyUCXThread;
|
|
private ReadUDPByWintestThread readUDPByWintestThread;
|
|
private WriteThread writeThread;
|
|
private ReadThread readThread;
|
|
private InputReaderThread consoleReader;
|
|
private ChatMember ownChatMemberObject; // Todo: set at startup
|
|
private ChatController chatController;
|
|
private MessageBusManagementThread messageProcessor;
|
|
private ReadUDPbyAirScoutMessageThread airScoutUDPReaderThread;
|
|
private DXClusterThreadPooledServer dxClusterServer;
|
|
|
|
private PlayAudioUtils playAudioUtils = new PlayAudioUtils();
|
|
|
|
public PlayAudioUtils getPlayAudioUtils() {
|
|
return playAudioUtils;
|
|
}
|
|
|
|
|
|
private TimerTask userActualizationTask;
|
|
|
|
private TimerTask keepAliveMessageSenderTask;
|
|
|
|
private LinkedBlockingQueue<ChatMessage> messageRXBus; // Queue in which all Chatmessages are buffered, sources are
|
|
// read- and write-thread
|
|
private LinkedBlockingQueue<ChatMessage> messageTXBus; // Queue in which all Chatmessages are buffered, sources are
|
|
// read- and write-thread
|
|
private String observedSendThisMessageString;
|
|
|
|
private DBController dbHandler;
|
|
|
|
private ReachabilityService reachabilityService;
|
|
private final WorkedGrossFieldCache workedGrossFieldCache = new WorkedGrossFieldCache();
|
|
|
|
|
|
private Socket socket;
|
|
private ServerSocket cluster_telnetServerSocket; // socket that accepts telnet client connects (cluster client)
|
|
// private ServerSocketChannel cluster_telnetServerSocketChannel;
|
|
|
|
|
|
private Timer userActualizationtimer;
|
|
|
|
private Timer keepAliveTimer;
|
|
|
|
private Timer beaconTimer;
|
|
|
|
private Timer ASQueryTimer;
|
|
|
|
private Timer socketCheckTimer;
|
|
|
|
// ******All abstract types below here are used by the messageprocessor!
|
|
// ***************
|
|
|
|
private ObservableList<ChatMessage> lst_globalChatMessageList = FXCollections.observableArrayList(); //All chatmessages will be put in there, later create filtered message lists
|
|
// private ObservableList<ChatMessage> lst_toAllMessageList = FXCollections.observableArrayList(); // directed to all
|
|
// (beacon)
|
|
private FilteredList<ChatMessage> lst_toAllMessageList = new FilteredList<>(lst_globalChatMessageList); // directed to all
|
|
|
|
// private ObservableList<ChatMessage> lst_toMeMessageList = FXCollections.observableArrayList(); // directed to my
|
|
// call
|
|
private FilteredList<ChatMessage> lst_toMeMessageList = new FilteredList<>(lst_globalChatMessageList);
|
|
|
|
private FilteredList<ChatMessage> lst_selectedCallSignInfofilteredMessageList = new FilteredList<>(lst_globalChatMessageList); // directed to all
|
|
|
|
// private ObservableList<ChatMessage> lst_toOtherMessageList = FXCollections.observableArrayList(); // directed to a
|
|
// call but not
|
|
// mine
|
|
private FilteredList<ChatMessage> lst_toOtherMessageList = new FilteredList<>(lst_globalChatMessageList);
|
|
|
|
private ObservableList<String> lstNotify_QSOSniffer_sniffedCallSignList = FXCollections.observableArrayList();
|
|
/**
|
|
* we do some trick here with the chatmemberlist to not make it neccessary to change all boolean properties if the
|
|
* chatmember object to observables. We trigger the list for changes on an object which we change whenever a list
|
|
* update will be neccessary to process (important for correct lifetime filtering!)
|
|
*/
|
|
// private ObservableList<ChatMember> chatMemberList = FXCollections.observableArrayList(workedInfoChange -> new Observable[] {workedInfoChange.workedInfoChangeFireListEventTriggerProperty()}); // List of active stations
|
|
// in chat
|
|
private ObservableList<ChatMember> chatMemberList = FXCollections.observableArrayList(); // List of active stations
|
|
|
|
|
|
private ObservableList<ChatMember> lst_chatMemberList = FXCollections.synchronizedObservableList(chatMemberList); // List
|
|
// of active stn in chat
|
|
private FilteredList<ChatMember> lst_chatMemberListFiltered = new FilteredList<ChatMember>(chatMemberList);
|
|
private SortedList<ChatMember> lst_chatMemberSortedFilteredList = new SortedList<ChatMember>(lst_chatMemberListFiltered);
|
|
private ObservableList<Predicate<ChatMember>> lst_chatMemberListFilterPredicates = FXCollections.observableArrayList();
|
|
private ObservableList<ClusterMessage> lst_clusterMemberList = FXCollections.observableArrayList();
|
|
|
|
|
|
/*
|
|
* Message table update buffers.
|
|
*
|
|
* Do not write directly to lst_globalChatMessageList from worker threads.
|
|
* Use publishChatMessage(...) instead.
|
|
*
|
|
* The actual ObservableList mutation is batched and executed on the JavaFX
|
|
* application thread. The visible list order remains newest-first.
|
|
*/
|
|
private final Object pendingChatMessagesLock = new Object();
|
|
private final List<ChatMessage> pendingChatMessages = new ArrayList<>();
|
|
private boolean chatMessageFlushScheduled = false;
|
|
|
|
/*
|
|
* Same idea for DXCluster messages.
|
|
*/
|
|
private final Object pendingClusterMessagesLock = new Object();
|
|
private final List<ClusterMessage> pendingClusterMessages = new ArrayList<>();
|
|
private boolean clusterMessageFlushScheduled = false;
|
|
|
|
private ObservableList<ChatMember> lst_DBBasedWkdCallSignList = FXCollections.observableArrayList();
|
|
|
|
// private HashMap<String, ChatMember> map_ucxLogInfoWorkedCalls = new HashMap<String, ChatMember>(); //Destination of ucx-log worked-messages
|
|
|
|
// ******************************************************************************************************************************************
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
* checks if the callsign-String of a given chatmember instance and a given list
|
|
* instance is in the list (multiple entries are possible to find by this method! <br/>
|
|
* If yes, returns an Array of int with the list indexes <b>if not, returns empty array</b>
|
|
* <br/>
|
|
* <br/>Also gives back indexes for callsign-70 or callsign-2 etc.<br/>
|
|
*
|
|
* @param lookForThis
|
|
* @return int[]
|
|
*/
|
|
public ArrayList<Integer> checkListForChatMemberIndexesByCallSign(ChatMember lookForThis) {
|
|
|
|
ArrayList<Integer> resultingIndexes = new ArrayList<Integer>();
|
|
|
|
if (lookForThis == null) {
|
|
|
|
// System.out.println("[ChatCtrl] ERROR: null Value for Chatmember detected! Member cannot be in the list!");
|
|
return resultingIndexes;
|
|
|
|
} else if (lookForThis.getCallSignRaw() == null) {
|
|
System.out.println("[ChatCtrl] ERROR: null Value in Callsign detected! Member cannot be in the list!");
|
|
return resultingIndexes;
|
|
}
|
|
|
|
for (Iterator iterator = lst_chatMemberList.iterator(); iterator.hasNext();) {
|
|
ChatMember chatMember = (ChatMember) iterator.next();
|
|
if (chatMember.getCallSignRaw().equals(lookForThis.getCallSignRaw())) { //Change for stations with -2 or -70 in logincallsign
|
|
// System.out
|
|
// .println("chtctrlr: Found raw " + chatMember.getCallSignRaw() + " // " + lookForThis.getCallSign());
|
|
|
|
resultingIndexes.add(lst_chatMemberList.indexOf(chatMember));
|
|
|
|
} else {
|
|
|
|
}
|
|
}
|
|
return resultingIndexes;
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* Notifies the UI that station-list derived values have changed.
|
|
*
|
|
* <p>This method may be called from parser threads, UDP listener threads and
|
|
* reachability worker threads. Therefore the listener is always invoked on the
|
|
* JavaFX application thread.</p>
|
|
*
|
|
* @param reason short debug reason for the UI log
|
|
*/
|
|
public void fireUserListUpdate(String reason) {
|
|
if (statusListener == null) {
|
|
return;
|
|
}
|
|
|
|
if (Platform.isFxApplicationThread()) {
|
|
statusListener.onUserListUpdated(reason);
|
|
} else {
|
|
Platform.runLater(() -> statusListener.onUserListUpdated(reason));
|
|
}
|
|
}
|
|
|
|
// /**
|
|
// * checks if the callsign-String of a given chatmember instance and a given list
|
|
// * instance is in the list. If yes, returns the index in the List, <b>if not,
|
|
// * returns -1.</b>
|
|
// *
|
|
// * @param lookForThis
|
|
// * @return Integer (index), -1 for not found
|
|
// */
|
|
// public int checkListForChatMemberIndexByCallSign(ChatMember lookForThis) {
|
|
//
|
|
// if (lookForThis == null) {
|
|
//
|
|
//// System.out.println("[ChatCtrl] ERROR: null Value for Chatmember detected! Member cannot be in the list!");
|
|
// return -1;
|
|
// } else if (lookForThis.getCallSign() == null) {
|
|
// System.out.println("[ChatCtrl] ERROR: null Value in Callsign detected! Member cannot be in the list!");
|
|
// return -1;
|
|
// }
|
|
//
|
|
// for (Iterator iterator = lst_chatMemberList.iterator(); iterator.hasNext();) {
|
|
// ChatMember chatMember = (ChatMember) iterator.next();
|
|
//// if (chatMember.getCallSign().equals(lookForThis.getCallSign())) {
|
|
// if (chatMember.getCallSignRaw().equals(lookForThis.getCallSignRaw())) { //TODO: Change for stations with -2 or -70 in logincallsign
|
|
//// System.out
|
|
//// .println("chtctrlr: Found raw " + chatMember.getCallSignRaw() + " // " + lookForThis.getCallSign());
|
|
//
|
|
// return lst_chatMemberList.indexOf(chatMember);
|
|
// } else {
|
|
//
|
|
// }
|
|
// }
|
|
/**
|
|
* At this point we know, the callsign is not active in the chat.
|
|
*/
|
|
// System.out
|
|
// .println("[ChatCtrl, ERROR:] ChecklistForChatMemberIndexByCallsign: " + lookForThis.getCallSign() + "\n" + "List: ");
|
|
// for (Iterator iterator = lst_chatMemberList.iterator(); iterator.hasNext();) {
|
|
// ChatMember chatMember = (ChatMember) iterator.next();
|
|
// System.out.println(lst_chatMemberList.indexOf(lookForThis) + ": " + chatMember.getCallSign());
|
|
// }
|
|
|
|
// return -1;
|
|
|
|
// }
|
|
|
|
public FilteredList<ChatMessage> getLst_selectedCallSignInfofilteredMessageList() {
|
|
return lst_selectedCallSignInfofilteredMessageList;
|
|
}
|
|
|
|
public void setLst_selectedCallSignInfofilteredMessageList(FilteredList<ChatMessage> lst_selectedCallSignInfofilteredMessageList) {
|
|
this.lst_selectedCallSignInfofilteredMessageList = lst_selectedCallSignInfofilteredMessageList;
|
|
}
|
|
|
|
public ObservableList<ChatMessage> getLst_globalChatMessageList() {
|
|
return lst_globalChatMessageList;
|
|
}
|
|
|
|
/**
|
|
* Adds a chat message to the UI message store.
|
|
*
|
|
* Important:
|
|
* - This method may be called from worker threads.
|
|
* - The ObservableList is modified only on the JavaFX application thread.
|
|
* - The backing list remains newest-first.
|
|
* - Old messages are trimmed to avoid unlimited memory growth.
|
|
*/
|
|
public void publishChatMessage(ChatMessage message) {
|
|
if (message == null) {
|
|
return;
|
|
}
|
|
|
|
synchronized (pendingChatMessagesLock) {
|
|
pendingChatMessages.add(message);
|
|
|
|
if (chatMessageFlushScheduled) {
|
|
return;
|
|
}
|
|
|
|
chatMessageFlushScheduled = true;
|
|
}
|
|
|
|
Platform.runLater(this::flushPendingChatMessagesToUi);
|
|
}
|
|
|
|
private void flushPendingChatMessagesToUi() {
|
|
List<ChatMessage> batch;
|
|
|
|
synchronized (pendingChatMessagesLock) {
|
|
batch = new ArrayList<>(pendingChatMessages);
|
|
pendingChatMessages.clear();
|
|
chatMessageFlushScheduled = false;
|
|
}
|
|
|
|
if (batch.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
/*
|
|
* pendingChatMessages is collected in arrival order:
|
|
* old -> new
|
|
*
|
|
* lst_globalChatMessageList must stay newest-first:
|
|
* new -> old
|
|
*/
|
|
Collections.reverse(batch);
|
|
|
|
lst_globalChatMessageList.addAll(0, batch);
|
|
|
|
trimGlobalChatMessageListIfNeeded();
|
|
}
|
|
|
|
private void trimGlobalChatMessageListIfNeeded() {
|
|
int maxSize = ApplicationConstants.CHAT_MESSAGE_STORE_MAX_SIZE;
|
|
int trimToSize = ApplicationConstants.CHAT_MESSAGE_STORE_TRIM_TO_SIZE;
|
|
|
|
if (maxSize <= 0 || trimToSize <= 0 || trimToSize >= maxSize) {
|
|
return;
|
|
}
|
|
|
|
int currentSize = lst_globalChatMessageList.size();
|
|
|
|
if (currentSize <= maxSize) {
|
|
return;
|
|
}
|
|
|
|
/*
|
|
* List order is newest-first.
|
|
* Therefore old messages are at the end of the list.
|
|
*/
|
|
lst_globalChatMessageList.remove(trimToSize, currentSize);
|
|
}
|
|
|
|
/**
|
|
* Adds a DXCluster message to the UI cluster message store.
|
|
*
|
|
* Same policy as for chat messages:
|
|
* - batched UI update
|
|
* - JavaFX thread only for ObservableList mutation
|
|
* - newest-first visible order
|
|
* - bounded list size
|
|
*/
|
|
public void publishClusterMessage(ClusterMessage message) {
|
|
if (message == null) {
|
|
return;
|
|
}
|
|
|
|
synchronized (pendingClusterMessagesLock) {
|
|
pendingClusterMessages.add(message);
|
|
|
|
if (clusterMessageFlushScheduled) {
|
|
return;
|
|
}
|
|
|
|
clusterMessageFlushScheduled = true;
|
|
}
|
|
|
|
Platform.runLater(this::flushPendingClusterMessagesToUi);
|
|
}
|
|
|
|
private void flushPendingClusterMessagesToUi() {
|
|
List<ClusterMessage> batch;
|
|
|
|
synchronized (pendingClusterMessagesLock) {
|
|
batch = new ArrayList<>(pendingClusterMessages);
|
|
pendingClusterMessages.clear();
|
|
clusterMessageFlushScheduled = false;
|
|
}
|
|
|
|
if (batch.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
Collections.reverse(batch);
|
|
|
|
lst_clusterMemberList.addAll(0, batch);
|
|
|
|
trimClusterMessageListIfNeeded();
|
|
}
|
|
|
|
private void trimClusterMessageListIfNeeded() {
|
|
int maxSize = ApplicationConstants.CLUSTER_MESSAGE_STORE_MAX_SIZE;
|
|
int trimToSize = ApplicationConstants.CLUSTER_MESSAGE_STORE_TRIM_TO_SIZE;
|
|
|
|
if (maxSize <= 0 || trimToSize <= 0 || trimToSize >= maxSize) {
|
|
return;
|
|
}
|
|
|
|
int currentSize = lst_clusterMemberList.size();
|
|
|
|
if (currentSize <= maxSize) {
|
|
return;
|
|
}
|
|
|
|
lst_clusterMemberList.remove(trimToSize, currentSize);
|
|
}
|
|
|
|
public void setLst_globalChatMessageList(ObservableList<ChatMessage> lst_globalChatMessageList) {
|
|
this.lst_globalChatMessageList = lst_globalChatMessageList;
|
|
}
|
|
|
|
public String getHostname() {
|
|
return hostname;
|
|
}
|
|
|
|
public void setHostname(String hostname) {
|
|
this.hostname = hostname;
|
|
}
|
|
|
|
public int getPort() {
|
|
return port;
|
|
}
|
|
|
|
public void setPort(int port) {
|
|
this.port = port;
|
|
}
|
|
|
|
public Socket getSocket() {
|
|
return socket;
|
|
}
|
|
|
|
public void setSocket(Socket socket) {
|
|
this.socket = socket;
|
|
}
|
|
|
|
public void setMessageTXBus(LinkedBlockingQueue<ChatMessage> messageTXBus) {
|
|
this.messageTXBus = messageTXBus;
|
|
}
|
|
|
|
// public String getPraktiKSTVersion() {
|
|
// return praktiKSTVersion;
|
|
// }
|
|
|
|
// public void setPraktiKSTVersion(String praktiKSTVersion) {
|
|
// this.praktiKSTVersion = praktiKSTVersion;
|
|
// }
|
|
|
|
public String getPraktiKSTVersionInfo() {
|
|
return praktiKSTVersionInfo;
|
|
}
|
|
|
|
public void setPraktiKSTVersionInfo(String praktiKSTVersionInfo) {
|
|
this.praktiKSTVersionInfo = praktiKSTVersionInfo;
|
|
}
|
|
|
|
// public HashMap getMap_ucxLogInfoWorkedCalls() {
|
|
// return map_ucxLogInfoWorkedCalls;
|
|
// }
|
|
|
|
// public void setMap_ucxLogInfoWorkedCalls(HashMap map_ucxLogInfoWorkedCalls) {
|
|
// this.map_ucxLogInfoWorkedCalls = map_ucxLogInfoWorkedCalls;
|
|
// }
|
|
|
|
public ObservableList<ChatMember> getLst_chatMemberList() {
|
|
return lst_chatMemberList;
|
|
}
|
|
|
|
public ObservableList<ChatMember> getLst_DBBasedWkdCallSignList() {
|
|
return lst_DBBasedWkdCallSignList;
|
|
}
|
|
|
|
public void setLst_DBBasedWkdCallSignList(ObservableList<ChatMember> lst_DBBasedWkdCallSignList) {
|
|
this.lst_DBBasedWkdCallSignList = lst_DBBasedWkdCallSignList;
|
|
}
|
|
|
|
public void setLst_chatMemberList(ObservableList<ChatMember> lst_chatMemberList) {
|
|
this.lst_chatMemberList = lst_chatMemberList;
|
|
}
|
|
|
|
public FilteredList<ChatMember> getLst_chatMemberListFiltered() {
|
|
return lst_chatMemberListFiltered;
|
|
}
|
|
|
|
public SortedList<ChatMember> getLst_chatMemberSortedFilteredList() {
|
|
return lst_chatMemberSortedFilteredList;
|
|
}
|
|
|
|
public ObservableList<Predicate<ChatMember>> getLst_chatMemberListFilterPredicates() {
|
|
return lst_chatMemberListFilterPredicates;
|
|
}
|
|
|
|
public void setLst_chatMemberListFilterPredicates(ObservableList<Predicate<ChatMember>> lst_chatMemberListFilterPredicates) {
|
|
this.lst_chatMemberListFilterPredicates = lst_chatMemberListFilterPredicates;
|
|
}
|
|
|
|
public ObservableList<ClusterMessage> getLst_clusterMemberList() {
|
|
return lst_clusterMemberList;
|
|
}
|
|
|
|
public void setLst_clusterMemberList(ObservableList<ClusterMessage> lst_clusterMemberList) {
|
|
this.lst_clusterMemberList = lst_clusterMemberList;
|
|
}
|
|
|
|
public ObservableList<ChatMessage> getLst_toAllMessageList() {
|
|
return lst_toAllMessageList;
|
|
}
|
|
|
|
// public void setLst_toAllMessageList(ObservableList<ChatMessage> lst_toAllMessageList) {
|
|
// this.lst_toAllMessageList = lst_toAllMessageList;
|
|
// }
|
|
public void setLst_toAllMessageList(FilteredList<ChatMessage> lst_toAllMessageList) {
|
|
this.lst_toAllMessageList = lst_toAllMessageList;
|
|
}
|
|
|
|
public ObservableList<ChatMessage> getLst_toMeMessageList() {
|
|
return lst_toMeMessageList;
|
|
}
|
|
|
|
// public void setLst_toMeMessageList(ObservableList<ChatMessage> lst_toMeMessageList) {
|
|
// this.lst_toMeMessageList = lst_toMeMessageList;
|
|
// }
|
|
public void setLst_toMeMessageList(FilteredList<ChatMessage> lst_toMeMessageList) {
|
|
this.lst_toMeMessageList = lst_toMeMessageList;
|
|
}
|
|
|
|
public ObservableList<ChatMessage> getLst_toOtherMessageList() {
|
|
return lst_toOtherMessageList;
|
|
}
|
|
|
|
// public void setLst_toOtherMessageList(ObservableList<ChatMessage> lst_toOtherMessageList) {
|
|
// this.lst_toOtherMessageList = lst_toOtherMessageList;
|
|
// }
|
|
|
|
public void setLst_toOtherMessageList(FilteredList<ChatMessage> lst_toOtherMessageList) {
|
|
this.lst_toOtherMessageList = lst_toOtherMessageList;
|
|
}
|
|
|
|
public LinkedBlockingQueue<ChatMessage> getMessageTXBus() {
|
|
return messageTXBus;
|
|
}
|
|
|
|
public ChatController() {
|
|
|
|
super();
|
|
chatCategoryMain = new ChatCategory(2);//Todo: selectable chatcategory, switched by user
|
|
chatCategorySecondChat = new ChatCategory(3); //Todo: selectable chatcategory, switched by user
|
|
|
|
ownChatMemberObject = new ChatMember();
|
|
ownChatMemberObject.setCallSign(userName);
|
|
ownChatMemberObject.setName(showedName);
|
|
ownChatMemberObject.setQra(qra);
|
|
|
|
// this.category = ChatCategory.VUHF;
|
|
this.userName = ownChatMemberObject.getName();
|
|
// this.hostname = "www.on4kst.org";
|
|
this.port = port;
|
|
}
|
|
|
|
/**
|
|
* This constructor is used by the Main()-Class of the praktiKST javaFX-gui.
|
|
*
|
|
*
|
|
* @param setOwnChatMemberObject
|
|
*/
|
|
public ChatController(ChatMember setOwnChatMemberObject,StatusUpdateListener listener) {
|
|
super();
|
|
|
|
chatPreferences = new ChatPreferences();
|
|
chatPreferences.readPreferencesFromXmlFile();
|
|
// this.statusListener = listener;
|
|
|
|
String dnsFromPrefs = chatPreferences.getStn_on4kstServersDns();
|
|
if (dnsFromPrefs != null && !dnsFromPrefs.isEmpty()) {
|
|
this.hostname = dnsFromPrefs;
|
|
} else {
|
|
this.hostname = "109.90.0.130";
|
|
}
|
|
|
|
UpdateChecker checkForUpdates = new UpdateChecker(this);
|
|
|
|
if (checkForUpdates.downloadLatestVersionInfoXML()) {
|
|
updateInformation = checkForUpdates.parseUpdateXMLFile();
|
|
};
|
|
|
|
initLst_toMeMessageList();
|
|
|
|
|
|
|
|
lst_toAllMessageList.setPredicate(new Predicate<ChatMessage>() {
|
|
@Override
|
|
public boolean test(ChatMessage chatMessage) {
|
|
|
|
try {
|
|
if (chatMessage.getReceiver().getCallSign().equals("ALL")) { //TODO: ALL have to be an application-constant
|
|
return true;
|
|
} else return false;
|
|
|
|
}
|
|
catch (Exception nullPointerExc) {
|
|
nullPointerExc.printStackTrace();
|
|
System.out.println("ChatController, ERROR: maybe the receiver was null, mostly like a cq message!");
|
|
return true;
|
|
}
|
|
|
|
}
|
|
});
|
|
|
|
lst_toOtherMessageList.setPredicate(new Predicate<ChatMessage>() {
|
|
@Override
|
|
public boolean test(ChatMessage chatMessage) {
|
|
try {
|
|
if ((!chatMessage.getSender().getCallSign().equals(getChatPreferences().getStn_loginCallSign())) &&
|
|
(!chatMessage.getReceiver().getCallSign().equals(getChatPreferences().getStn_loginCallSign())) && (!chatMessage.getReceiver().getCallSign().equals("ALL")) )
|
|
//RX not own callsign and TX not own callsign and callsign is not "ALL" (that means, directed to public)
|
|
{
|
|
return true;
|
|
} else return false;
|
|
|
|
} catch (Exception nullPointerExc) {
|
|
// nullPointerExc.printStackTrace();
|
|
System.out.println("ChatController, <<<catched ERROR>>>: maybe the receiver was null!");
|
|
return false;
|
|
}
|
|
}
|
|
});
|
|
|
|
dbHandler = new DBController();
|
|
reachabilityService = new ReachabilityService(this);
|
|
rebuildWorkedGrossFieldCacheFromDatabase();
|
|
|
|
// chatPreferences = new ChatPreferences();
|
|
// chatPreferences.readPreferencesFromXmlFile(); // set the praktikst Prefs by file or default if file is corrupted
|
|
|
|
chatCategoryMain = chatPreferences.getLoginChatCategoryMain();
|
|
chatCategorySecondChat = chatPreferences.getLoginChatCategorySecond();
|
|
this.userName = chatPreferences.getStn_loginCallSign();
|
|
this.password = chatPreferences.getStn_loginPassword();
|
|
// category = setCategory;
|
|
ownChatMemberObject = setOwnChatMemberObject;
|
|
|
|
// this.userName = ownChatMemberObject.getName();
|
|
// this.password = ownChatMemberObject.getPassword();
|
|
this.hostname = this.getChatPreferences().getStn_on4kstServersDns(); //default: www.on4kst.org
|
|
|
|
}
|
|
|
|
|
|
private void initLst_toMeMessageList() {
|
|
// ObservableList<String> sniffedList = chatPreferences.getLstNotify_QSOSniffer_sniffedCallSignList();
|
|
|
|
Predicate<ChatMessage> chatFilterPredicate = chatMessage -> {
|
|
// Sicherheits-Checks gegen NullPointer (statt try-catch)
|
|
if (chatMessage == null || chatMessage.getSender() == null || chatMessage.getReceiver() == null) {
|
|
return false;
|
|
}
|
|
|
|
String myCallSign = getChatPreferences().getStn_loginCallSign();
|
|
String senderCall = chatMessage.getSender().getCallSign();
|
|
String receiverCall = chatMessage.getReceiver().getCallSign();
|
|
String msgText = chatMessage.getMessageText();
|
|
|
|
// --- NEUE LOGIK: Sniffer Liste prüfen ---
|
|
// Wenn Absender ODER Empfänger in der Beobachtungsliste stehen -> Anzeigen
|
|
// if ((lstNotify_QSOSniffer_sniffedCallSignList.contains(senderCall) ||
|
|
// lstNotify_QSOSniffer_sniffedCallSignList.contains(receiverCall)) &&
|
|
// (!receiverCall.equals(this.getChatPreferences().getStn_loginCallSignRaw()))) {
|
|
//
|
|
// msgText = ("Sniffed: " + "(" + senderCall + " > ") + receiverCall +") " + msgText;
|
|
// chatMessage.setMessageText(msgText);
|
|
// return true;
|
|
// }
|
|
|
|
if (isSniffedMessage(chatMessage)) {
|
|
return true;
|
|
}
|
|
|
|
// --- BESTEHENDE LOGIK ---
|
|
|
|
// 1. Nachrichten direkt an dich
|
|
if (receiverCall.equals(myCallSign)) {
|
|
return true;
|
|
}
|
|
|
|
// 2. Deine eigenen Nachrichten (außer an ALL)
|
|
if (senderCall.equals(myCallSign) && !receiverCall.equals("ALL")) {
|
|
return true;
|
|
}
|
|
|
|
// 3. Mentions im Text (jemand schreibt über dich)
|
|
// Nur prüfen, wenn Text nicht null ist und du nicht selbst der Absender bist
|
|
if (msgText != null && !senderCall.equals(myCallSign)) {
|
|
// containsIgnoreCase Logik (etwas robuster als deine Variante)
|
|
if (msgText.toLowerCase().contains(myCallSign.toLowerCase())) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
lstNotify_QSOSniffer_sniffedCallSignList.addListener((ListChangeListener<String>) c -> {
|
|
|
|
// System.out.println(c.toString());
|
|
|
|
// Wir zwingen die FilteredList zum Neuscannen, indem wir das Prädikat neu setzen.
|
|
lst_toMeMessageList.setPredicate(null); // kurz resetten (manchmal nötig in älteren JavaFX Versionen)
|
|
lst_toMeMessageList.setPredicate(chatFilterPredicate);
|
|
|
|
});
|
|
|
|
lstNotify_QSOSniffer_sniffedCallSignList.add("DF0GEB");
|
|
|
|
|
|
lst_toMeMessageList.setPredicate(chatFilterPredicate); //sniffed callsign filter predicate is here!
|
|
|
|
// lst_toMeMessageList.setPredicate(new Predicate<ChatMessage>() {
|
|
// @Override
|
|
// public boolean test(ChatMessage chatMessage) {
|
|
//
|
|
// try {
|
|
//
|
|
// if (chatMessage.getReceiver().getCallSign().equals(getChatPreferences().getStn_loginCallSign())) {
|
|
// return true; //messages addressed to you
|
|
// }
|
|
// if ((chatMessage.getSender().getCallSign().equals(getChatPreferences().getStn_loginCallSign())) && (!chatMessage.getReceiver().getCallSign().equals("ALL"))){
|
|
// return true; //your own echo except texts to all (CQ)
|
|
// }
|
|
//
|
|
// String ignoreCaseString = chatMessage.getMessageText();
|
|
//
|
|
// if ((chatMessage.getMessageText().contains(chatPreferences.getStn_loginCallSign().toLowerCase()) || (chatMessage.getMessageText().contains(chatPreferences.getStn_loginCallSign().toUpperCase())))
|
|
// && (!chatMessage.getSender().getCallSign().equals(getChatPreferences().getStn_loginCallSign()))) {
|
|
// return true; //if someone writes about you, you will get the mail, too, except you are the sender...!
|
|
// }
|
|
//
|
|
// else {
|
|
// return false;
|
|
// }
|
|
// }
|
|
// catch (Exception nullPointerExc) {
|
|
// nullPointerExc.printStackTrace();
|
|
// System.out.println("ChatController, <<<catched ERROR>>>: maybe the receiver was null, message received b4 user entered chatmessage...!" + nullPointerExc.getMessage());
|
|
// return false;
|
|
// }
|
|
// }
|
|
// });
|
|
|
|
|
|
|
|
}
|
|
|
|
/**
|
|
* starts wintest udp listener thread
|
|
*/
|
|
public synchronized void startWintestUdpListener() {
|
|
if (readUDPByWintestThread != null && readUDPByWintestThread.isAlive()) {
|
|
return;
|
|
}
|
|
|
|
readUDPByWintestThread = new ReadUDPByWintestThread(this, this);
|
|
readUDPByWintestThread.setName("readUDPByWintestThread");
|
|
readUDPByWintestThread.start();
|
|
|
|
System.out.println("[ChatController] Win-Test UDP listener started.");
|
|
}
|
|
|
|
/**
|
|
* stops wintest udp listener thread
|
|
*/
|
|
public synchronized void stopWintestUdpListener() {
|
|
if (readUDPByWintestThread == null) return;
|
|
|
|
try {
|
|
readUDPByWintestThread.interrupt();
|
|
} catch (Exception ignored) { }
|
|
|
|
readUDPByWintestThread = null;
|
|
System.out.println("[ChatController] Win-Test UDP listener stopped.");
|
|
}
|
|
|
|
/**
|
|
* restarts wintest udp listener thread
|
|
*/
|
|
public synchronized void restartWintestUdpListenerIfEnabled() {
|
|
stopWintestUdpListener();
|
|
if (chatPreferences.isLogsynch_wintestNetworkListenerEnabled()) {
|
|
startWintestUdpListener();
|
|
}
|
|
}
|
|
|
|
|
|
|
|
public ObservableList<String> getLstNotify_QSOSniffer_sniffedCallSignList() {
|
|
return lstNotify_QSOSniffer_sniffedCallSignList;
|
|
}
|
|
|
|
public void setLstNotify_QSOSniffer_sniffedCallSignList(ObservableList<String> lstNotify_QSOSniffer_sniffedCallSignList) {
|
|
this.lstNotify_QSOSniffer_sniffedCallSignList = lstNotify_QSOSniffer_sniffedCallSignList;
|
|
}
|
|
|
|
public ChatPreferences getChatPreferences() {
|
|
return chatPreferences;
|
|
}
|
|
|
|
public void setChatPreferences(ChatPreferences chatPreferences) {
|
|
this.chatPreferences = chatPreferences;
|
|
}
|
|
|
|
public ChatMember getownChatMemberObject() {
|
|
return ownChatMemberObject;
|
|
}
|
|
|
|
public void setOwnCall(ChatMember ownCall) {
|
|
this.ownChatMemberObject = ownCall;
|
|
}
|
|
|
|
public LinkedBlockingQueue<ChatMessage> getMessageRXBus() {
|
|
return messageRXBus;
|
|
}
|
|
|
|
public void setMessageRXBus(LinkedBlockingQueue<ChatMessage> messageBus) {
|
|
this.messageRXBus = messageBus;
|
|
}
|
|
|
|
public WriteThread getWriteThread() {
|
|
return writeThread;
|
|
}
|
|
|
|
|
|
public void setWriteThread(WriteThread writeThread) {
|
|
this.writeThread = writeThread;
|
|
}
|
|
|
|
public ReadThread getReadThread() {
|
|
return readThread;
|
|
}
|
|
|
|
public void setReadThread(ReadThread readThread) {
|
|
this.readThread = readThread;
|
|
}
|
|
|
|
public ChatCategory getChatCategoryMain() {
|
|
return chatCategoryMain;
|
|
}
|
|
|
|
public void setChatCategoryMain(ChatCategory chatCategoryMain) {
|
|
this.chatCategoryMain = chatCategoryMain;
|
|
}
|
|
|
|
public DXClusterThreadPooledServer getDxClusterServer() {
|
|
return dxClusterServer;
|
|
}
|
|
|
|
// public void setChatMemberTable(Hashtable<String, ChatMember> chatMemberTable) {
|
|
// this.chatMemberTable = chatMemberTable;
|
|
// }
|
|
//
|
|
// public void setChatMemberTable(HashMap<String, ChatMember> chatMemberTable) {
|
|
// this.chatMemberTable = chatMemberTable;
|
|
// }
|
|
|
|
public DBController getDbHandler() {
|
|
return dbHandler;
|
|
}
|
|
|
|
public void setDbHandler(DBController dbHandler) {
|
|
this.dbHandler = dbHandler;
|
|
}
|
|
|
|
/**
|
|
* execute is the main entry point where the application starts.
|
|
* @throws InterruptedException
|
|
* @throws IOException
|
|
*/
|
|
public void execute() throws InterruptedException, IOException {
|
|
|
|
|
|
chatController = this;
|
|
|
|
// ApplicationConstants constants = new ApplicationConstants();
|
|
|
|
// This block constructs a sample message
|
|
// ChatMessage Test = new ChatMessage();
|
|
// Test.setMessage("kst4contest.test");
|
|
// Test.setMessageDirectedToCommunity(false);
|
|
// Test.setMessageGeneratedTime(new Utils4KST().time_convertEpochToReadable("1664669836"));
|
|
// Test.setMessageSenderName("marc");
|
|
// Test.setMessageText("test2");
|
|
// Test.setSender(ownChatMemberObject);
|
|
// getLst_toAllMessageList().add(Test);
|
|
|
|
try {
|
|
setDisconnectionPerformedByUser(false);
|
|
|
|
startScoreScheduler();
|
|
//runs sked priority thread
|
|
|
|
messageRXBus = new LinkedBlockingQueue<ChatMessage>();
|
|
messageTXBus = new LinkedBlockingQueue<ChatMessage>();
|
|
|
|
// socket = new Socket(hostname, port);//socket for the on4kst chat server
|
|
socket = new Socket(chatController.chatPreferences.getStn_on4kstServersDns(), port);//socket for the on4kst chat server
|
|
System.out.println("Connected to the chat server: " + socket.isConnected());
|
|
|
|
ByteBuffer buffer = ByteBuffer.allocate(1024);
|
|
Selector selector = Selector.open();
|
|
|
|
// consoleReader = new InputReaderThread(this);
|
|
// consoleReader.start();
|
|
|
|
readThread = new ReadThread(socket, this);
|
|
readThread.setName("ReadThread-telnetreader");
|
|
readThread.start();
|
|
|
|
writeThread = new WriteThread(socket, this);
|
|
writeThread.setName("Writethread-telnetwriter");
|
|
writeThread.start();
|
|
|
|
readUDPbyUCXThread = new ReadUDPbyUCXMessageThread(chatPreferences.getLogsynch_ucxUDPWkdCallListenerPort(), this, this);
|
|
readUDPbyUCXThread.setName("readUDPbyUCXThread");
|
|
readUDPbyUCXThread.start();
|
|
|
|
if (chatPreferences.isLogsynch_wintestNetworkListenerEnabled()) {
|
|
startWintestUdpListener();
|
|
} else {
|
|
System.out.println("[ChatController] Win-Test listener disabled by preference -> not starting.");
|
|
}
|
|
|
|
messageProcessor = new MessageBusManagementThread(this, this);
|
|
messageProcessor.setName("messagebusManagementThread");
|
|
messageProcessor.start();
|
|
|
|
airScoutUDPReaderThread = new ReadUDPbyAirScoutMessageThread(chatPreferences.getAirScout_asCommunicationPort(), this, this.getChatPreferences().getAirScout_asServerNameString(), this.getChatPreferences().getAirScout_asServerNameString(), this); //working original
|
|
airScoutUDPReaderThread.setName("airscoutudpreaderThread");
|
|
airScoutUDPReaderThread.start();
|
|
|
|
userActualizationtimer = new Timer();
|
|
userActualizationtimer.schedule(new UserActualizationTask(this), 4000, 60000);// TODO: Temporary userlistoutput known qrgs
|
|
|
|
keepAliveTimer = new Timer();
|
|
keepAliveTimer.schedule(new keepAliveMessageSenderTask(this), 4000, 60000);//
|
|
|
|
if (chatPreferences.isStn_pstRotatorEnabled()) {
|
|
initRotor();
|
|
} else {
|
|
System.out.println("[ChatController, info]: PSTRotator disabled by user preference -> not starting rotator client.");
|
|
}
|
|
|
|
/**
|
|
* DX cluster service running config
|
|
*/
|
|
dxClusterServer = new DXClusterThreadPooledServer(this.getChatPreferences().getNotify_dxclusterServerPort(), this, this);
|
|
new Thread(dxClusterServer).start();
|
|
|
|
|
|
this.setConnectedAndLoggedIn(true);
|
|
|
|
/**
|
|
* The CQ-beacon-Task will be executed every time but checks for itself whether
|
|
* CQ messages are enabled or not
|
|
*/
|
|
// Timer beaconTimer;
|
|
beaconTimer = new Timer();
|
|
beaconTimer.schedule(new BeaconTask(this, this), 10000,
|
|
this.getChatPreferences().getBcn_beaconIntervalInMinutesMainCat() * 60000);
|
|
// 60000 * intervalInMinutes = IntervalInMillis
|
|
|
|
/**
|
|
* The AS querier task will be executed every time but checks for itself whether
|
|
* AS usage is enabled or not
|
|
*/
|
|
// Timer ASQueryTimer;
|
|
ASQueryTimer = new Timer();
|
|
ASQueryTimer.schedule(new AirScoutPeriodicalAPReflectionInquirerTask(this), 10000, 60000);
|
|
// 60000 * intervalInMinutes = IntervalInMillis
|
|
|
|
/**
|
|
* Check if socket works
|
|
*/
|
|
// Timer socketCheckTimer;
|
|
socketCheckTimer = new Timer();
|
|
socketCheckTimer.schedule(new TimerTask() {
|
|
|
|
@Override
|
|
public void run() {
|
|
// System.out.println("[Chatcontroller, info: ] periodical socketcheck");
|
|
|
|
Thread.currentThread().setName("SocketcheckTimer");
|
|
|
|
if (!socket.isConnected() || socket.isClosed()) {
|
|
try {
|
|
messageRXBus.clear();
|
|
messageTXBus.clear();
|
|
socket.close();
|
|
|
|
chatController.setConnectedAndLoggedIn(false);
|
|
chatController.getLst_chatMemberList().clear();
|
|
|
|
System.out.println("[Chatcontroller, Warning: ] Socket closed or disconnected");
|
|
|
|
ChatMessage killThreadPoisonPillMsg = new ChatMessage();
|
|
killThreadPoisonPillMsg.setMessageText(ApplicationConstants.DISCONNECT_RDR_POISONPILL);
|
|
killThreadPoisonPillMsg.setMessageSenderName(ApplicationConstants.DISCONNECT_RDR_POISONPILL);
|
|
|
|
ChatMessage killThreadPoisonPillMsg2 = new ChatMessage();
|
|
killThreadPoisonPillMsg2.setMessageText(ApplicationConstants.DISCONNECT_RDR_POISONPILL);
|
|
killThreadPoisonPillMsg2.setMessageSenderName(ApplicationConstants.DISCONNECT_RDR_POISONPILL);
|
|
|
|
|
|
messageRXBus.add(killThreadPoisonPillMsg);
|
|
|
|
messageTXBus.add(killThreadPoisonPillMsg2);
|
|
chatController.getReadThread().interrupt();
|
|
|
|
|
|
} catch (Exception e) {
|
|
// TODO Auto-generated catch block
|
|
e.printStackTrace();
|
|
}
|
|
|
|
/**
|
|
* The next block will attempt to reinit the chatclient after accidental
|
|
* disconnection
|
|
*/
|
|
try {
|
|
|
|
if (!disconnectionPerformedByUser) {
|
|
|
|
messageRXBus.clear();
|
|
messageTXBus.clear();
|
|
|
|
socket = new Socket(hostname, port);
|
|
|
|
// readThread.interrupt();
|
|
|
|
chatController.setReadThread(new ReadThread(socket, chatController));
|
|
chatController.readThread.start();
|
|
|
|
chatController.setWriteThread(new WriteThread(socket, chatController));
|
|
chatController.writeThread.start();
|
|
|
|
messageProcessor = new MessageBusManagementThread(chatController, chatController);
|
|
messageProcessor.start();
|
|
|
|
System.out.println("[Chatcontroller, info: initialized new socket, is connected? ] "
|
|
+ socket.isConnected() + " " + socket.isClosed());
|
|
|
|
initialize23001();
|
|
|
|
Timer waitABit = new Timer();
|
|
socketCheckTimer.schedule(new TimerTask() {
|
|
|
|
@Override
|
|
public void run() {
|
|
Thread.currentThread().setName("waiting");
|
|
|
|
if (socket.isConnected()) {
|
|
chatController.setConnectedAndLoggedIn(true);
|
|
}
|
|
|
|
//just take a breath
|
|
}
|
|
}, 5000);
|
|
|
|
|
|
// keepAliveTimer.notify();
|
|
|
|
}
|
|
|
|
} catch (Exception e) {
|
|
// TODO Auto-generated catch block
|
|
e.printStackTrace();
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
}, 10000, 10000);
|
|
|
|
} catch (UnknownHostException ex) {
|
|
System.out.println("Server not found: " + ex.getMessage());
|
|
} catch (IOException ex) {
|
|
System.out.println("I/O Error: " + ex.getMessage());
|
|
}
|
|
|
|
while (readThread == null) {
|
|
// do nothing, wait!
|
|
System.out.println("Reader not ready.");
|
|
}
|
|
|
|
// initialize();//kst4contest.test 4 23001
|
|
initialize23001(); // init Chatcontroller for using port 23001
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns the background reachability service used by the station table.
|
|
*
|
|
* @return reachability service
|
|
*/
|
|
public ReachabilityService getReachabilityService() {
|
|
return reachabilityService;
|
|
}
|
|
|
|
/**
|
|
* Returns the runtime gross-field cache used by the new-locator filter.
|
|
*
|
|
* @return worked gross-field cache
|
|
*/
|
|
public WorkedGrossFieldCache getWorkedGrossFieldCache() {
|
|
return workedGrossFieldCache;
|
|
}
|
|
|
|
/**
|
|
* Rebuilds the gross-field cache from persistent SQLite data and enriches it with
|
|
* legacy ChatMember worked/qra rows where possible.
|
|
*/
|
|
public void rebuildWorkedGrossFieldCacheFromDatabase() {
|
|
if (dbHandler == null) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
workedGrossFieldCache.rebuildFromDatabaseSnapshot(dbHandler.fetchWorkedGrossFieldsFromDB());
|
|
workedGrossFieldCache.addWorkedBandsFromStoredChatMembers(dbHandler.fetchChatMemberWkdDataFromDB().values());
|
|
} catch (Exception exception) {
|
|
System.out.println("[ChatController, warning]: could not rebuild worked gross-field cache: "
|
|
+ exception.getMessage());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Persists and caches one worked gross field after a logger QSO packet.
|
|
*
|
|
* @param band worked band
|
|
* @param locator6 six-character Maidenhead locator
|
|
* @param workedCall worked station
|
|
* @param source logger/source name
|
|
*/
|
|
public void registerWorkedGrossField(Band band, String locator6, ChatMember workedCall, String source) {
|
|
if (band == null || locator6 == null) {
|
|
return;
|
|
}
|
|
|
|
String normalizedLocator6 = WorkedGrossFieldCache.extractLocator6(locator6);
|
|
if (normalizedLocator6 == null) {
|
|
return;
|
|
}
|
|
|
|
String callSignRaw = workedCall == null ? null : workedCall.getCallSignRaw();
|
|
|
|
try {
|
|
dbHandler.upsertWorkedGrossField(band, normalizedLocator6, callSignRaw, source);
|
|
} catch (Exception exception) {
|
|
System.out.println("[ChatController, warning]: could not persist worked gross field: "
|
|
+ exception.getMessage());
|
|
}
|
|
|
|
workedGrossFieldCache.addWorked(band, normalizedLocator6);
|
|
fireUserListUpdate("Worked gross field updated");
|
|
}
|
|
|
|
/**
|
|
* 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>
|
|
*
|
|
* @param member member to inspect
|
|
* @return true if the gross grid square was worked on any band
|
|
*/
|
|
public boolean isGridSquareWorkedAny(ChatMember member) {
|
|
if (member == null || member.getQra() == null) {
|
|
return false;
|
|
}
|
|
|
|
return workedGrossFieldCache.isGrossFieldWorkedAny(member.getQra());
|
|
}
|
|
|
|
/**
|
|
* 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) {
|
|
return false;
|
|
}
|
|
|
|
return !workedGrossFieldCache.isGrossFieldWorkedAny(member.getQra());
|
|
}
|
|
|
|
public long getCurrentEpochTime() {
|
|
|
|
OffsetDateTime currentTimeInUtc = OffsetDateTime.now(ZoneOffset.UTC);
|
|
|
|
// System.out.println(currentTimeInUtc.format(DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm X")));
|
|
|
|
long millisecondsSinceEpoch = currentTimeInUtc.toInstant().toEpochMilli() / 1000;
|
|
// System.out.println(millisecondsSinceEpoch);
|
|
return millisecondsSinceEpoch;
|
|
}
|
|
|
|
/**
|
|
* Setting the initial parameters at the chat via port 23001 <br/>
|
|
* <br/>
|
|
* <b>Login parameter format is like that: </b><br/>
|
|
* LOGINC|do5amf|password|2|kst4contest1251|25|0|1|1663879818|0| <br/>
|
|
* SDONE|2| <br/>
|
|
*
|
|
* @throws InterruptedException
|
|
* @throws IOException
|
|
*/
|
|
public void initialize23001() throws InterruptedException, IOException {
|
|
|
|
messageTXBus.clear();
|
|
|
|
ChatMessage message = new ChatMessage();
|
|
|
|
new Timer().schedule(new TimerTask() {
|
|
|
|
@Override
|
|
public void run() {
|
|
|
|
Thread.currentThread().setName("LoginStringTimer");
|
|
|
|
//this is the original loginC without history abonnement
|
|
// String loginString = "";
|
|
// loginString = "LOGINC|" + chatPreferences.getStn_loginCallSign() + "|" + chatPreferences.getStn_loginPassword()
|
|
// + "|" + chatPreferences.getLoginChatCategoryMain().getCategoryNumber() + "|praktiKST v" + ApplicationConstants.APPLICATION_CURRENTVERSIONNUMBER
|
|
// + "|25|0|1|" + getCurrentEpochTime() + "|0|";
|
|
|
|
String loginString = "";
|
|
loginString = "LOGINC|" + chatPreferences.getStn_loginCallSign() + "|" + chatPreferences.getStn_loginPassword()
|
|
+ "|" + chatPreferences.getLoginChatCategoryMain().getCategoryNumber() + "|praktiKST v" + ApplicationConstants.APPLICATION_CURRENTVERSIONNUMBER
|
|
+ "|25|0|1|" + "0" + "|0|";
|
|
|
|
// System.out.println(loginString);
|
|
ChatMessage message = new ChatMessage();
|
|
message.setMessageText(loginString);
|
|
message.setMessageDirectedToServer(true);
|
|
getMessageTXBus().add(message);
|
|
|
|
}
|
|
}, 2000);
|
|
|
|
/**
|
|
* Entering second chat
|
|
*
|
|
* ACHAT|chat id|past messages number|past dx/map number|users list/update flags|last Unix timestamp for messages|last Unix timestamp for dx/map|
|
|
*/
|
|
|
|
if (this.chatController.getChatPreferences().isLoginToSecondChatEnabled()) { //only login to second if wished
|
|
|
|
new Timer().schedule(new TimerTask() {
|
|
|
|
@Override
|
|
public void run() { //test second chat
|
|
|
|
Thread.currentThread().setName("LoginStringTimerSecond");
|
|
|
|
String loginString = "";
|
|
loginString = "ACHAT|" + chatController.getChatPreferences().getLoginChatCategorySecond().getCategoryNumber() + "|" + "25"
|
|
+ "|" + "10" + "|2|" + getCurrentEpochTime() + "|" + getCurrentEpochTime();
|
|
|
|
// System.out.println(loginString);
|
|
ChatMessage message = new ChatMessage();
|
|
message.setMessageText(loginString);
|
|
message.setMessageDirectedToServer(true);
|
|
getMessageTXBus().add(message);
|
|
|
|
}
|
|
}, 5000);
|
|
}
|
|
/**
|
|
* end testing second chat
|
|
*
|
|
*/
|
|
|
|
new Timer().schedule(new TimerTask() {
|
|
|
|
@Override
|
|
public void run() {
|
|
|
|
Thread.currentThread().setName("SDONEStringTimer");
|
|
ChatMessage message = new ChatMessage();
|
|
message.setMessageText("SDONE|" + chatPreferences.getLoginChatCategoryMain().getCategoryNumber() + "|\r");
|
|
message.setMessageDirectedToServer(true);
|
|
getMessageTXBus().add(message);
|
|
|
|
}
|
|
}, 3000);
|
|
|
|
new Timer().schedule(new TimerTask() {
|
|
|
|
@Override
|
|
public void run() {
|
|
Thread.currentThread().setName("SETLOCTIMER");
|
|
ChatMessage message = new ChatMessage();
|
|
message.setMessageText("MSG|" + chatPreferences.getLoginChatCategoryMain().getCategoryNumber()
|
|
+ "|0|/SETLOC " + chatPreferences.getStn_loginLocatorMainCat() + "|0|\r");
|
|
message.setMessageDirectedToServer(true);
|
|
getMessageTXBus().add(message);
|
|
}
|
|
}, 4000);
|
|
|
|
new Timer().schedule(new TimerTask() {
|
|
|
|
@Override
|
|
public void run() {
|
|
Thread.currentThread().setName("SETNAMETIMER");
|
|
ChatMessage message = new ChatMessage();
|
|
message.setMessageText("MSG|" + chatPreferences.getLoginChatCategoryMain().getCategoryNumber()
|
|
+ "|0|/SETNAME " + chatPreferences.getStn_loginNameMainCat() + "|0|\r");
|
|
message.setMessageDirectedToServer(true);
|
|
getMessageTXBus().add(message);
|
|
}
|
|
}, 5000);
|
|
|
|
new Timer().schedule(new TimerTask() {
|
|
|
|
@Override
|
|
public void run() {
|
|
Thread.currentThread().setName("SETHereTimerMain");
|
|
ChatMessage message = new ChatMessage();
|
|
message.setMessageText("MSG|" + chatPreferences.getLoginChatCategoryMain().getCategoryNumber()
|
|
+ "|0|/BACK" + "|0|\r");
|
|
message.setMessageDirectedToServer(true);
|
|
getMessageTXBus().add(message);
|
|
}
|
|
}, 6500);
|
|
|
|
|
|
if (chatPreferences.isLoginToSecondChatEnabled()) { //only if second category had been enabled
|
|
|
|
new Timer().schedule(new TimerTask() {
|
|
|
|
@Override
|
|
public void run() {
|
|
Thread.currentThread().setName("SETNAMETIMER2nd");
|
|
ChatMessage message = new ChatMessage();
|
|
message.setMessageText("MSG|" + chatPreferences.getLoginChatCategorySecond().getCategoryNumber()
|
|
+ "|0|/SETNAME " + chatPreferences.getStn_loginNameSecondCat() + "|0|\r");
|
|
message.setMessageDirectedToServer(true);
|
|
getMessageTXBus().add(message);
|
|
}
|
|
}, 5500);
|
|
|
|
new Timer().schedule(new TimerTask() {
|
|
|
|
@Override
|
|
public void run() {
|
|
Thread.currentThread().setName("SETHereTimerSecond");
|
|
ChatMessage message = new ChatMessage();
|
|
message.setMessageText("MSG|" + chatPreferences.getLoginChatCategorySecond().getCategoryNumber()
|
|
+ "|0|/BACK" + "|0|\r");
|
|
message.setMessageDirectedToServer(true);
|
|
getMessageTXBus().add(message);
|
|
}
|
|
}, 7000);
|
|
|
|
}
|
|
|
|
new Timer().schedule(new TimerTask() {
|
|
|
|
@Override
|
|
public void run() {
|
|
Thread.currentThread().setName("fetchWorkedFromDBTimer");
|
|
refreshWorkedStateAndDatabaseListFromDatabase();
|
|
}
|
|
}, 10000);
|
|
|
|
// new Timer().schedule(new TimerTask() {
|
|
// HashMap<String, ChatMember> getWorkedDataFromDb;
|
|
//
|
|
// @Override
|
|
// public void run() {
|
|
//
|
|
// Thread.currentThread().setName("fetchWorkedFromDBTimer");
|
|
//
|
|
// try {
|
|
// getWorkedDataFromDb = dbHandler.fetchChatMemberWkdDataFromDB();
|
|
// } catch (SQLException e) {
|
|
// System.out.println("[Chatctrl, Error: ] got no worked data from DB due to communication error");
|
|
// }
|
|
//
|
|
// for (Iterator iterator = getLst_chatMemberList().iterator(); iterator.hasNext();) {
|
|
//
|
|
//
|
|
// ChatMember chatMember = (ChatMember) iterator.next();
|
|
// System.out.println("[Chatctrl]: Marking ChatMembers wkd information: "
|
|
// + getWorkedDataFromDb.get(chatMember.getCallSign()).getCallSign());
|
|
// chatMember.setWorked(getWorkedDataFromDb.get(chatMember.getCallSign()).isWorked());
|
|
// chatMember.setWorked144(getWorkedDataFromDb.get(chatMember.getCallSignRaw()).isWorked144());
|
|
// ;
|
|
// chatMember.setWorked432(getWorkedDataFromDb.get(chatMember.getCallSignRaw()).isWorked432());
|
|
// ;
|
|
// chatMember.setWorked1240(getWorkedDataFromDb.get(chatMember.getCallSignRaw()).isWorked1240());
|
|
// ;
|
|
// chatMember.setWorked2300(getWorkedDataFromDb.get(chatMember.getCallSignRaw()).isWorked2300());
|
|
// ;
|
|
// chatMember.setWorked3400(getWorkedDataFromDb.get(chatMember.getCallSignRaw()).isWorked3400());
|
|
// ;
|
|
// chatMember.setWorked5600(getWorkedDataFromDb.get(chatMember.getCallSignRaw()).isWorked5600());
|
|
// ;
|
|
// chatMember.setWorked10G(getWorkedDataFromDb.get(chatMember.getCallSignRaw()).isWorked10G());
|
|
// /**
|
|
// * v1.2 since here
|
|
// * TODO: Change that, this ins not generative
|
|
// */
|
|
//
|
|
// chatMember.setQrv144(getWorkedDataFromDb.get(chatMember.getCallSignRaw()).isQrv144());
|
|
// ;
|
|
// chatMember.setQrv432(getWorkedDataFromDb.get(chatMember.getCallSignRaw()).isQrv432());
|
|
// ;
|
|
// chatMember.setQrv1240(getWorkedDataFromDb.get(chatMember.getCallSignRaw()).isQrv1240());
|
|
// ;
|
|
// chatMember.setQrv2300(getWorkedDataFromDb.get(chatMember.getCallSignRaw()).isQrv2300());
|
|
// ;
|
|
// chatMember.setQrv3400(getWorkedDataFromDb.get(chatMember.getCallSignRaw()).isQrv3400());
|
|
// ;
|
|
// chatMember.setQrv5600(getWorkedDataFromDb.get(chatMember.getCallSignRaw()).isQrv5600());
|
|
// ;
|
|
// chatMember.setQrv10G(getWorkedDataFromDb.get(chatMember.getCallSignRaw()).isQrv10G());
|
|
// ;
|
|
// }
|
|
//
|
|
// /**
|
|
// *
|
|
// * This creates the list of the worked stations which had to be displayed in the
|
|
// * settings menu. TODO: May make this List editable
|
|
// *
|
|
// */
|
|
//
|
|
// getWorkedDataFromDb.forEach((key, value) -> {
|
|
//
|
|
// chatController.getLst_DBBasedWkdCallSignList().add(value);
|
|
//
|
|
//// System.out.println("Key=" + key + ", Value=" + value);
|
|
// });
|
|
// }
|
|
// }, 10000);
|
|
|
|
// message = new ChatMessage();
|
|
// message.setMessageText("MSG|2|0|/SETNAME " + ownChatMemberObject.getName() + "|0|\r");
|
|
// message.setMessageDirectedToServer(true);
|
|
// this.getMessageTXBus().add(message);
|
|
|
|
// message.setMessageText(ownCall.getCallSign());
|
|
// this.getMessageTXBus().add(message);
|
|
|
|
}
|
|
|
|
/**
|
|
* Reloads the worked/not-QRV state from the internal database and applies the
|
|
* result both to the active chatmember list and to the database table list in the
|
|
* settings dialog. UI-bound list modifications are executed on the JavaFX thread.
|
|
*/
|
|
public void refreshWorkedStateAndDatabaseListFromDatabase() {
|
|
|
|
HashMap<String, ChatMember> workedDataFromDatabase;
|
|
|
|
try {
|
|
workedDataFromDatabase = dbHandler.fetchChatMemberWkdDataFromDB();
|
|
} catch (SQLException e) {
|
|
System.out.println("[Chatctrl, Error: ] got no worked data from DB due to communication error");
|
|
e.printStackTrace();
|
|
return;
|
|
}
|
|
|
|
rebuildWorkedGrossFieldCacheFromDatabase();
|
|
|
|
HashMap<String, ChatMember> finalWorkedDataFromDatabase = workedDataFromDatabase;
|
|
|
|
Platform.runLater(() -> {
|
|
helper_applyWorkedAndQrvStateFromDatabase(finalWorkedDataFromDatabase);
|
|
getLst_DBBasedWkdCallSignList().setAll(finalWorkedDataFromDatabase.values());
|
|
fireUserListUpdate("Worked database state refreshed");
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Applies the worked and not-QRV state from the database snapshot to all active
|
|
* chatmember objects that are currently visible in the live chat list.
|
|
*
|
|
* @param workedDataFromDatabase map keyed by normalized raw callsign
|
|
*/
|
|
private void helper_applyWorkedAndQrvStateFromDatabase(HashMap<String, ChatMember> workedDataFromDatabase) {
|
|
|
|
for (Iterator iterator = getLst_chatMemberList().iterator(); iterator.hasNext();) {
|
|
|
|
ChatMember activeChatMember = (ChatMember) iterator.next();
|
|
ChatMember storedChatMemberState = workedDataFromDatabase.get(activeChatMember.getCallSignRaw());
|
|
|
|
if (storedChatMemberState == null) {
|
|
continue;
|
|
}
|
|
|
|
activeChatMember.setWorked(storedChatMemberState.isWorked());
|
|
activeChatMember.setWorked144(storedChatMemberState.isWorked144());
|
|
activeChatMember.setWorked432(storedChatMemberState.isWorked432());
|
|
activeChatMember.setWorked1240(storedChatMemberState.isWorked1240());
|
|
activeChatMember.setWorked2300(storedChatMemberState.isWorked2300());
|
|
activeChatMember.setWorked3400(storedChatMemberState.isWorked3400());
|
|
activeChatMember.setWorked5600(storedChatMemberState.isWorked5600());
|
|
activeChatMember.setWorked10G(storedChatMemberState.isWorked10G());
|
|
activeChatMember.setQrv144(storedChatMemberState.isQrv144());
|
|
activeChatMember.setQrv432(storedChatMemberState.isQrv432());
|
|
activeChatMember.setQrv1240(storedChatMemberState.isQrv1240());
|
|
activeChatMember.setQrv2300(storedChatMemberState.isQrv2300());
|
|
activeChatMember.setQrv3400(storedChatMemberState.isQrv3400());
|
|
activeChatMember.setQrv5600(storedChatMemberState.isQrv5600());
|
|
activeChatMember.setQrv10G(storedChatMemberState.isQrv10G());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resets all worked flags in the live GUI chatmember list.
|
|
*/
|
|
public void resetWorkedInfoInGuiLists() {
|
|
|
|
this.chatController.getLst_chatMemberList().forEach(
|
|
chatMember -> chatMember.resetWorkedInformationAtAllBands());
|
|
}
|
|
|
|
/**
|
|
* Resets all not-QRV flags in the live GUI chatmember list.
|
|
*/
|
|
public void resetQRVInfoInGuiLists() {
|
|
|
|
this.chatController.getLst_chatMemberList().forEach(
|
|
chatMember -> chatMember.resetQRVInformationAtAllBands());
|
|
}
|
|
|
|
/**
|
|
* Resets both worked and not-QRV flags in the live GUI chatmember list.
|
|
*/
|
|
public void resetWorkedAndQrvInfoInGuiLists() {
|
|
resetWorkedInfoInGuiLists();
|
|
resetQRVInfoInGuiLists();
|
|
}
|
|
|
|
|
|
|
|
|
|
// public void resetWorkedInfoInGuiLists() {
|
|
//
|
|
// this.chatController.getLst_chatMemberList().forEach(
|
|
// chatMember -> chatMember.resetWorkedInformationAtAllBands());
|
|
//
|
|
// }
|
|
//
|
|
// public void resetQRVInfoInGuiLists() {
|
|
//
|
|
// this.chatController.getLst_chatMemberList().forEach(
|
|
// chatMember -> chatMember.resetQRVInformationAtAllBands());
|
|
//
|
|
// }
|
|
|
|
/**
|
|
* Setting the initial parameters at the chat
|
|
*
|
|
* @throws InterruptedException
|
|
* @throws IOException
|
|
*/
|
|
public void initialize23000() throws InterruptedException, IOException {
|
|
|
|
ChatMessage message = new ChatMessage();
|
|
|
|
// message.setDirectedToServer(true);
|
|
message.setMessageText(ownChatMemberObject.getCallSign());
|
|
|
|
this.getMessageTXBus().add(message);
|
|
|
|
message = new ChatMessage();
|
|
// message.setDirectedToServer(true);
|
|
message.setMessageText(password);
|
|
this.getMessageTXBus().add(message);
|
|
//
|
|
message = new ChatMessage();
|
|
// message.setDirectedToServer(true);
|
|
message.setMessageText(chatCategoryMain + "");
|
|
this.getMessageTXBus().add(message);
|
|
//
|
|
message = new ChatMessage();
|
|
// message.setDirectedToServer(true);
|
|
message.setMessageText("/set qra " + ownChatMemberObject.getQra());
|
|
this.getMessageTXBus().add(message);
|
|
//
|
|
message = new ChatMessage();
|
|
// message.setDirectedToServer(true);
|
|
message.setMessageText("/set name " + ownChatMemberObject.getName());
|
|
this.getMessageTXBus().add(message);
|
|
//
|
|
message = new ChatMessage();
|
|
// message.setDirectedToServer(true);
|
|
message.setMessageText("/set here");
|
|
this.getMessageTXBus().add(message);
|
|
|
|
// message = new ChatMessage();
|
|
// message.setDirectedToServer(true);
|
|
// message.setMessageText("/show user");
|
|
// this.getMessageTXBus().add(message);
|
|
// will done by another Thread
|
|
}
|
|
|
|
public static final class UiReminderEvent {
|
|
private final String callSignRaw;
|
|
private final int minutesBefore;
|
|
private final long epochMs;
|
|
|
|
public UiReminderEvent(String callSignRaw, int minutesBefore, long epochMs) {
|
|
this.callSignRaw = callSignRaw;
|
|
this.minutesBefore = minutesBefore;
|
|
this.epochMs = epochMs;
|
|
}
|
|
|
|
public String getCallSignRaw() { return callSignRaw; }
|
|
public int getMinutesBefore() { return minutesBefore; }
|
|
public long getEpochMs() { return epochMs; }
|
|
}
|
|
|
|
public void fireUiReminderEvent(String callSignRaw, int minutesBefore) {
|
|
final String raw = callSignRaw == null ? null : callSignRaw.trim().toUpperCase();
|
|
final long now = System.currentTimeMillis();
|
|
|
|
// Ensure property updates happen on FX thread
|
|
if (Platform.isFxApplicationThread()) {
|
|
lastUiReminderEvent.set(new UiReminderEvent(raw, minutesBefore, now));
|
|
} else {
|
|
Platform.runLater(() -> lastUiReminderEvent.set(new UiReminderEvent(raw, minutesBefore, now)));
|
|
}
|
|
}
|
|
|
|
|
|
private final ObjectProperty<UiReminderEvent> lastUiReminderEvent = new SimpleObjectProperty<>(null);
|
|
|
|
public ReadOnlyObjectProperty<UiReminderEvent> lastUiReminderEventProperty() {
|
|
return lastUiReminderEvent;
|
|
}
|
|
|
|
/**
|
|
* Helper method to check if a chatmember is in my beam(range)
|
|
*
|
|
* @param member
|
|
* @return
|
|
*/
|
|
public boolean isChatMemberInMyBeam(ChatMember member) {
|
|
if (member == null || member.getQTFdirection() == null) return false;
|
|
|
|
double targetAz = member.getQTFdirection();
|
|
double myAz = getChatPreferences().getActualQTF().get();
|
|
double beamWidth = getChatPreferences().getStn_antennaBeamWidthDeg();
|
|
|
|
return DirectionUtils.isAngleInRange(targetAz, myAz, beamWidth);
|
|
}
|
|
|
|
/**
|
|
* decides if a message in the in-queue is directed to me or if its directed to another station and sniffed
|
|
* @param chatMessage
|
|
* @return
|
|
*/
|
|
public boolean isSniffedMessage(ChatMessage chatMessage) {
|
|
if (chatMessage == null || chatMessage.getSender() == null || chatMessage.getReceiver() == null) {
|
|
return false;
|
|
}
|
|
|
|
String senderCall = chatMessage.getSender().getCallSign();
|
|
String receiverCall = chatMessage.getReceiver().getCallSign();
|
|
|
|
if (senderCall == null || receiverCall == null) {
|
|
return false;
|
|
}
|
|
|
|
if (lstNotify_QSOSniffer_sniffedCallSignList == null || lstNotify_QSOSniffer_sniffedCallSignList.isEmpty()) {
|
|
return false;
|
|
}
|
|
|
|
boolean observedCall =
|
|
lstNotify_QSOSniffer_sniffedCallSignList.contains(senderCall)
|
|
|| lstNotify_QSOSniffer_sniffedCallSignList.contains(receiverCall);
|
|
|
|
if (!observedCall) {
|
|
return false;
|
|
}
|
|
|
|
String myCall = getChatPreferences() != null ? getChatPreferences().getStn_loginCallSign() : null;
|
|
String myRawCall = getChatPreferences() != null ? getChatPreferences().getStn_loginCallSignRaw() : null;
|
|
|
|
/*
|
|
* Sniffed messages should appear in the private table only if they are not
|
|
* already direct messages to my own callsign.
|
|
*/
|
|
return !receiverCall.equals(myCall) && !receiverCall.equals(myRawCall);
|
|
}
|
|
|
|
/**
|
|
* changes the chatmessage if it had been a sniffed one and not directed to me. Only for marking.
|
|
* @param chatMessage
|
|
* @return
|
|
*/
|
|
public String formatChatMessageTextForDisplay(ChatMessage chatMessage) {
|
|
if (chatMessage == null) {
|
|
return "";
|
|
}
|
|
|
|
String msgText = chatMessage.getMessageText();
|
|
|
|
if (msgText == null) {
|
|
msgText = "";
|
|
}
|
|
|
|
if (!isSniffedMessage(chatMessage)) {
|
|
return msgText;
|
|
}
|
|
|
|
String senderCall = chatMessage.getSender() != null ? chatMessage.getSender().getCallSign() : "";
|
|
String receiverCall = chatMessage.getReceiver() != null ? chatMessage.getReceiver().getCallSign() : "";
|
|
|
|
return "Sniffed: (" + senderCall + " > " + receiverCall + ") " + msgText;
|
|
}
|
|
} |