Changed sked opportunity highlighting mechanism to more precision, updated the manual

This commit is contained in:
Marc Froehlich
2026-07-26 23:56:32 +02:00
parent 182de0526b
commit 344ca547fd
10 changed files with 225 additions and 134 deletions
@@ -776,6 +776,19 @@ public class MessageBusManagementThread extends Thread {
newMessageArrived));
}
/*
* Detect and store the sender's QRG before any directional-opportunity
* or DX-Cluster processing. A frequency first mentioned in this directed
* message is then immediately available for the resulting spot.
*
* The parser needs only the message text, sender and preferences. The
* receiver does not have to be resolved yet.
*/
smartFrequencyExtraction(
newMessageArrived,
this.client.getChatPreferences());
// TODO: Next: get frequency infos out of name?
if (splittedMessageLine[7].equals("0")) {
// message is not directed to anyone, move it to the cq messages.
@@ -1058,9 +1071,6 @@ public class MessageBusManagementThread extends Thread {
exceptionOccured.printStackTrace();
}
// --- Band/QRG recognition (fills ChatMember.knownActiveBands) ---
smartFrequencyExtraction(newMessageArrived, this.client.getChatPreferences());
// TODO: Next: get frequency infos out of name?
} else
@@ -26,9 +26,16 @@ public class ChatMember {
String name;
String callSignRaw; //without -2 or -70 etc.
/**
* A directional opportunity inferred from a directed chat message remains
* relevant for five minutes. The timestamp is used instead of a permanent
* boolean so an old antenna-direction assumption cannot remain visible
* indefinitely.
*/
static final long DIRECTION_OPPORTUNITY_VALIDITY_MILLIS = 5L * 60L * 1000L;
private volatile long directionOpportunityValidUntilEpochMs;
boolean isInAngleAndRange; //if he tries a sked in my dir, he is in range, will process that in the messages
// String frequency; // last known qrg of the station
@@ -108,12 +115,37 @@ public class ChatMember {
this.lastFlagsChangeEpochMs = lastFlagsChangeEpochMs;
}
/**
* Returns whether the most recently inferred directional opportunity is
* still valid.
*
* @return {@code true} until the five-minute validity period has expired
*/
public boolean isInAngleAndRange() {
return isInAngleAndRange;
return isInAngleAndRangeAt(System.currentTimeMillis());
}
/**
* Time-aware variant used by the public getter and by unit tests.
*
* @param nowEpochMs time against which the validity is checked
* @return {@code true} while the stored validity timestamp is still in the future
*/
boolean isInAngleAndRangeAt(long nowEpochMs) {
return directionOpportunityValidUntilEpochMs > nowEpochMs;
}
/**
* Starts a new five-minute validity period or removes the current
* directional opportunity immediately.
*
* @param inAngleAndRange {@code true} for a newly detected opportunity;
* {@code false} to clear it
*/
public void setInAngleAndRange(boolean inAngleAndRange) {
isInAngleAndRange = inAngleAndRange;
directionOpportunityValidUntilEpochMs = inAngleAndRange
? System.currentTimeMillis() + DIRECTION_OPPORTUNITY_VALIDITY_MILLIS
: 0L;
}
public AirPlaneReflectionInfo getAirPlaneReflectInfo() {
@@ -0,0 +1,34 @@
package kst4contest.model;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class ChatMemberDirectionOpportunityTest {
@Test
void directionalOpportunityExpiresAfterConfiguredValidityPeriod() {
ChatMember member = new ChatMember();
member.setInAngleAndRange(true);
long afterActivationEpochMs = System.currentTimeMillis();
assertTrue(member.isInAngleAndRangeAt(afterActivationEpochMs));
assertFalse(member.isInAngleAndRangeAt(
afterActivationEpochMs
+ ChatMember.DIRECTION_OPPORTUNITY_VALIDITY_MILLIS
+ 1L));
}
@Test
void directionalOpportunityCanBeClearedImmediately() {
ChatMember member = new ChatMember();
member.setInAngleAndRange(true);
assertTrue(member.isInAngleAndRange());
member.setInAngleAndRange(false);
assertFalse(member.isInAngleAndRange());
}
}
@@ -1277,115 +1277,65 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
tbl_chatMemberTable.setTooltip(new Tooltip(
"Stations available \n\nUse right click to a station to select predefined texts\nor hit <strg> + <1> ... <9> to write textsnippet to selected station\n\nHit <enter> to send"));
TableColumn<ChatMember, String> callSignCol = new TableColumn<ChatMember, String>("Callsign");
callSignCol.setCellValueFactory(new Callback<CellDataFeatures<ChatMember, String>, ObservableValue<String>>() {
TableColumn<ChatMember, String> callSignCol =
new TableColumn<ChatMember, String>("Callsign");
@Override
public ObservableValue<String> call(CellDataFeatures<ChatMember, String> cellDataFeatures) {
SimpleStringProperty callsgn = new SimpleStringProperty();
callSignCol.setCellValueFactory(cellDataFeatures -> {
ChatMember member = cellDataFeatures.getValue();
ChatMember member = cellDataFeatures.getValue();
String baseCallsign;
if (member.getState() == 1) {
baseCallsign = "(" + member.getCallSign() + ")"; //away user
} else {
baseCallsign = member.getCallSign();
}
callsgn.setValue(baseCallsign);
// System.out.println(member.getCallSign() + " / " + member.getState()+ " <<<<<<<<<<<<<<<<<< state ");
return callsgn;
if (member == null || member.getCallSign() == null) {
return new SimpleStringProperty("");
}
String displayedCallsign = member.getState() == 1
? "(" + member.getCallSign() + ")"
: member.getCallSign();
return new SimpleStringProperty(displayedCallsign);
});
callSignCol.setCellFactory(new Callback<TableColumn<ChatMember, String>, TableCell<ChatMember, String>>() {
public TableCell call(TableColumn param) {
callSignCol.setCellFactory(column -> new TableCell<ChatMember, String>() {
return new TableCell<ChatMember, String>() {
@Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
/*
* JavaFX reuses TableCell instances while the user scrolls, sorts or
* filters the table. Every visual state must therefore be reset before
* the cell is populated with another ChatMember.
*/
getStyleClass().remove("table-cell-inAngleAndRange");
setGraphic(null);
@Override
public void updateItem(String item, boolean empty) {
if (empty || item == null) {
setText(null);
setStyle("");
return;
}
super.updateItem(item, empty);
setText(item);
int currentIndex = indexProperty().getValue() < 0 ? 0 : indexProperty().getValue();
// System.out.println(">>>>>>>>>>>>>>>> INDEXPROPERTY = " + indexProperty().getValue() + " " + getIndex() + " / " + item);
ChatMember chatMember = getTableRow() == null
? null
: getTableRow().getItem();
if (chatMember == null) {
setStyle("");
return;
}
if (item != null) {
boolean useBoldFont = chatMember.getState() == 2
|| chatMember.getState() == 3;
ChatMember chatMember = (ChatMember) param.getTableView().getItems().get(currentIndex);
// System.out.println(chatMember.getCallSign() + " / " + chatMember.getState() + " <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<state ");
if (chatMember.isInAngleAndRange()) {
getStyleClass().add("table-cell-inAngleAndRange");
useBoldFont = true;
}
// System.out.println(this.getStyleClass() + "<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<");
// this.getStyleClass().clear(); //clear css reference, then recoloring
// this.getStyleClass().add("table-cell"); //set old reference
// System.out.println("KST4Contestapp ************************************** members state is: " + chatMember.getState());
if (chatMember.getState() == 3 ) { //login in last 5 min
// System.out.println("####################################### fATfatFett");
this.setStyle("-fx-font-weight: bold");
// this.getStyleClass().clear();
// this.getStyleClass().add("table-cell-bold"); //add new special colored css reference
} else if (chatMember.getState() == 0 ) { //here
// this.getStyleClass().clear();
// this.getStyleClass().add("table-cell");
this.setStyle("-fx-font-weight: normal");
} else if (chatMember.getState() == 2 ) { //here and relogin
// this.getStyleClass().clear();
// this.getStyleClass().add("table-cell-bold");
this.setStyle("-fx-font-weight: bold");
} else if (chatMember.getState() == 1 ) { //away
// this.getStyleClass().clear();
// this.getStyleClass().add("table-cell");
this.setStyle("-fx-font-weight: normal");
}
if (chatMember.isInAngleAndRange()) {
// this.getStyleClass().add("table-cell-inAngleAndRange");
//// getStyleClass().add("");
this.setTextFill(Color.LIGHTGREEN);
this.setStyle("-fx-font-weight: bold");
}
// else if (chatMember.getState() != 3){ //TODO: this double handling should be improved as may there can be new markers. Neccessarry to reset the colour to black
// this.setTextFill(Color.BLACK);
// this.setStyle("-fx-font-weight: normal");
// } else {
// this.setTextFill(Color.BLACK);
// }
// if ((Utils4KST.time_getSecondsBetweenEpochAndNow(chatMember.getActivityTimeLastInEpoch()+"") /60%60) < 2) {
// this.setTextFill(Color.ORANGE);
// }
}
// if (!isEmpty()) {
// this.setTextFill(Color.BLACK);
// // Get fancy and change color based on data
//
// if (item.contains("5")) {
// this.setTextFill(Color.BLUEVIOLET);
// } else if (item.contains("7") ) {
// this.setTextFill(Color.RED);
// } else if (item.contains("0") ) {
// this.setTextFill(Color.ORANGE);
// }
////
setText(item);
// }
}
};
setStyle(useBoldFont
? "-fx-font-weight: bold;"
: "-fx-font-weight: normal;");
}
});
@@ -7470,7 +7420,23 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
// chatMemberTableFilterQTFHBox.setSpacing(5);
chatMemberTableFilterQTFHBox.getChildren().addAll(chatMemberTableFilterQtfTF, new Label("deg +/- " + chatcontroller.getChatPreferences().getStn_antennaBeamWidthDeg() + ""), qtfNorth, qtfNorthEast, qtfEast, qtfSouthEast, qtfSouth, qtfSouthWest, qtfWest, qtfNorthWest);
Label lblQtfHalfBeamwidth = new Label("± BW/2");
lblQtfHalfBeamwidth.setTooltip(new Tooltip(
"The QTF filter uses half of the configured total antenna beamwidth "
+ "on each side of the selected direction."));
chatMemberTableFilterQTFHBox.getChildren().addAll(
chatMemberTableFilterQtfTF,
lblQtfHalfBeamwidth,
qtfNorth,
qtfNorthEast,
qtfEast,
qtfSouthEast,
qtfSouth,
qtfSouthWest,
qtfWest,
qtfNorthWest);
chatMemberTableFilterQTFAndQRBHbox.getChildren().add(chatMemberTableFilterQTFHBox);
chatMemberTableFilterVBoxForAllFilters.getChildren().add(chatMemberTableFilterQTFAndQRBHbox);
@@ -8474,7 +8440,12 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
TextField txtFldstn_antennaBeamWidthDeg = new TextField(this.chatcontroller.getChatPreferences().getStn_antennaBeamWidthDeg() + "");
txtFldstn_antennaBeamWidthDeg.setFocusTraversable(false);
txtFldstn_antennaBeamWidthDeg.setTooltip(new Tooltip("Your antenna beamwidth in DEG\n\nEnter correct values here due it´s used for path suggestions!!!"));
txtFldstn_antennaBeamWidthDeg.setTooltip(new Tooltip(
"Your antenna beamwidth in degrees.\n\n"
+ "KST4Contest also uses this value as an assumed beamwidth "
+ "for other stations when it derives directional opportunities "
+ "from directed chat messages."));
txtFldstn_antennaBeamWidthDeg.textProperty().addListener(new ChangeListener<String>() {
@Override