mirror of
https://github.com/praktimarc/kst4contest.git
synced 2026-07-13 16:16:44 +02:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6d3351175f | |||
| 674c5a62e3 | |||
| 750d0a3be1 | |||
| dc703fca9e | |||
| 77c9d34d64 | |||
| efd6ed6299 | |||
| de8e8ba471 | |||
| eaf611b203 | |||
| e339b6fccf | |||
| bd9b42d4a5 | |||
| 275ce48769 | |||
|
d201997587
|
@@ -22,7 +22,7 @@ public class ApplicationConstants {
|
||||
*/
|
||||
public static final double APPLICATION_CURRENTVERSIONNUMBER = 1.42;
|
||||
|
||||
public static final String VERSIONINFOURLFORUPDATES_KST4CONTEST = "https://do5amf.funkerportal.de/kst4ContestVersionInfo.xml";
|
||||
public static final String VERSIONINFOURLFORUPDATES_KST4CONTEST = "https://kst4contest.hamradioonline.de/kst4ContestVersionInfo.xml";
|
||||
public static final String VERSIONINFDOWNLOADEDLOCALFILE = "kst4ContestVersionInfo.xml";
|
||||
|
||||
public static final String STYLECSSFILE_DEFAULT_DAYLIGHT = "KST4ContestDefaultDay.css";
|
||||
|
||||
@@ -24,6 +24,7 @@ import kst4contest.logic.PriorityCalculator;
|
||||
import kst4contest.model.*;
|
||||
import kst4contest.test.MockKstServer;
|
||||
import kst4contest.utils.PlayAudioUtils;
|
||||
import kst4contest.locatorUtils.Location;
|
||||
import kst4contest.view.Kst4ContestApplication;
|
||||
|
||||
import java.io.*;
|
||||
@@ -34,6 +35,7 @@ import java.util.function.Predicate;
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* Central Chat kst4contest.controller. Instantiate only one time per category of kst Chat.
|
||||
@@ -318,33 +320,29 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
|
||||
|
||||
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;
|
||||
for (ChatMember cm : findActiveChatMembersByRawCall(callRaw)) {
|
||||
if (cm == null || cm.getCallSignRaw() == null) continue;
|
||||
|
||||
Map<Band, ChatMember.ActiveFrequencyInfo> map = cm.getKnownActiveBands();
|
||||
if (map == null || map.isEmpty()) 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);
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -356,21 +354,19 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
|
||||
|
||||
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;
|
||||
for (ChatMember cm : findActiveChatMembersByRawCall(callRaw)) {
|
||||
if (cm == null || cm.getCallSignRaw() == null) 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
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
return worked;
|
||||
}
|
||||
|
||||
@@ -645,7 +641,8 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
|
||||
|
||||
// rotatorClient.
|
||||
|
||||
this.lst_chatMemberList.clear();;
|
||||
// this.lst_chatMemberList.clear();;
|
||||
this.clearActiveChatMembers();
|
||||
this.lst_clusterMemberList.clear();
|
||||
|
||||
this.setDisconnected(true);
|
||||
@@ -713,7 +710,7 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
|
||||
}
|
||||
} else if (action.equals(ApplicationConstants.DISCSTRING_DISCONNECTONLY)){
|
||||
|
||||
this.lst_chatMemberList.clear();;
|
||||
this.clearActiveChatMembers();
|
||||
this.lst_clusterMemberList.clear();
|
||||
|
||||
|
||||
@@ -959,16 +956,9 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
|
||||
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;
|
||||
|
||||
List<ChatMember> matchingMembers = findActiveChatMembersByRawCall(targetCallsignRaw);
|
||||
return matchingMembers.isEmpty() ? null : matchingMembers.get(0);
|
||||
}
|
||||
|
||||
private String resolveSkedTargetLocator(String targetCallsignRaw) {
|
||||
@@ -976,22 +966,43 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
|
||||
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);
|
||||
}
|
||||
for (ChatMember member : findActiveChatMembersByRawCall(targetCallsignRaw)) {
|
||||
String locator = member.getQra();
|
||||
if (locator != null && !locator.isBlank()) {
|
||||
return locator.trim().toUpperCase(Locale.ROOT);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// private ChatMember resolveSkedTargetMember(String targetCallsignRaw) {
|
||||
// if (targetCallsignRaw == null || targetCallsignRaw.isBlank()) {
|
||||
// return null;
|
||||
// }
|
||||
//
|
||||
// List<ChatMember> matchingMembers = findActiveChatMembersByRawCall(targetCallsignRaw);
|
||||
// return matchingMembers.isEmpty() ? null : matchingMembers.get(0);
|
||||
//
|
||||
// }
|
||||
//
|
||||
// private String resolveSkedTargetLocator(String targetCallsignRaw) {
|
||||
// if (targetCallsignRaw == null || targetCallsignRaw.isBlank()) {
|
||||
// return null;
|
||||
// }
|
||||
//
|
||||
// String normalizedTargetCall = normalizeCallRaw(targetCallsignRaw);
|
||||
//
|
||||
// for (ChatMember member : findActiveChatMembersByRawCall(targetCallsignRaw)) {
|
||||
// String locator = member.getQra();
|
||||
// if (locator != null && !locator.isBlank()) {
|
||||
// return locator.trim().toUpperCase(Locale.ROOT);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return null;
|
||||
// }
|
||||
|
||||
public StationMetricsService getStationMetricsService() {
|
||||
return stationMetricsService;
|
||||
}
|
||||
@@ -1014,10 +1025,14 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
|
||||
return new HashMap<>(lastInboundCategoryByCallSignRaw);
|
||||
}
|
||||
|
||||
// public List<ChatMember> snapshotChatMembers() {
|
||||
// synchronized (getLst_chatMemberList()) {
|
||||
// return new ArrayList<>(getLst_chatMemberList());
|
||||
// }
|
||||
// }
|
||||
|
||||
public List<ChatMember> snapshotChatMembers() {
|
||||
synchronized (getLst_chatMemberList()) {
|
||||
return new ArrayList<>(getLst_chatMemberList());
|
||||
}
|
||||
return new ArrayList<>(activeChatMembersByCallAndCategory.values());
|
||||
}
|
||||
|
||||
public List<ContestSked> snapshotActiveSkeds() {
|
||||
@@ -1126,13 +1141,27 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
|
||||
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<ChatMember> lst_chatMemberList = FXCollections.synchronizedObservableList(chatMemberList); // List
|
||||
private ObservableList<Predicate<ChatMember>> lst_chatMemberListFilterPredicates = FXCollections.observableArrayList();
|
||||
private ObservableList<ClusterMessage> lst_clusterMemberList = FXCollections.observableArrayList();
|
||||
|
||||
/*
|
||||
* Thread-safe active-member model.
|
||||
*
|
||||
* MessageBusManagementThread must not use the JavaFX TableView backing list as
|
||||
* its primary data model. Otherwise every add/remove fires FilteredList,
|
||||
* SortedList and TableView selection listeners on the MessageBus thread and JavaFX
|
||||
* can throw "Not on FX application thread" or internal listener NPEs.
|
||||
*
|
||||
* This map is the worker-thread safe source of truth. The ObservableList below
|
||||
* remains a UI mirror and is mutated only on the JavaFX application thread.
|
||||
*/
|
||||
private final java.util.concurrent.ConcurrentMap<String, ChatMember> activeChatMembersByCallAndCategory =
|
||||
new java.util.concurrent.ConcurrentHashMap<>();
|
||||
|
||||
/*
|
||||
* Message table update buffers.
|
||||
@@ -1161,6 +1190,254 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
|
||||
// ******************************************************************************************************************************************
|
||||
|
||||
|
||||
/**
|
||||
* Executes UI-bound work on the JavaFX application thread.
|
||||
*
|
||||
* Worker threads may call this safely. If the caller is already on the FX
|
||||
* thread, the action is executed immediately to preserve ordering.
|
||||
*/
|
||||
private void runOnFxThread(Runnable action) {
|
||||
if (action == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Platform.isFxApplicationThread()) {
|
||||
action.run();
|
||||
} else {
|
||||
Platform.runLater(action);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the active-member key. The raw/base callsign alone is not enough
|
||||
* because the same station can be logged into multiple ON4KST categories at
|
||||
* the same time. Therefore the category number is part of the key.
|
||||
*/
|
||||
private String buildActiveChatMemberKey(String callSign, ChatCategory category) {
|
||||
String normalizedCallsign = ChatMember.normalizeCallSignToBaseCallSign(callSign);
|
||||
if (normalizedCallsign == null || normalizedCallsign.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int categoryNumber = category == null ? -1 : category.getCategoryNumber();
|
||||
return normalizedCallsign.trim().toUpperCase(Locale.ROOT) + "|" + categoryNumber;
|
||||
}
|
||||
|
||||
private String buildActiveChatMemberKey(ChatMember member) {
|
||||
if (member == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String callSign = member.getCallSignRaw() != null ? member.getCallSignRaw() : member.getCallSign();
|
||||
return buildActiveChatMemberKey(callSign, member.getChatCategory());
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds or replaces an active chat member in the worker-thread model and mirrors
|
||||
* that change to the JavaFX list. This is the only supported path for ON4KST
|
||||
* user-enter events.
|
||||
*/
|
||||
public void addOrUpdateActiveChatMember(ChatMember member) {
|
||||
String key = buildActiveChatMemberKey(member);
|
||||
if (key == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
activeChatMembersByCallAndCategory.put(key, member);
|
||||
|
||||
runOnFxThread(() -> {
|
||||
int existingIndex = findChatMemberIndexInUiListByKey(key);
|
||||
if (existingIndex >= 0) {
|
||||
lst_chatMemberList.set(existingIndex, member);
|
||||
} else {
|
||||
lst_chatMemberList.add(member);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an active chat member from the worker-thread model and from the UI
|
||||
* mirror. Removal from the ObservableList is always performed on the FX thread
|
||||
* because the list is bound to FilteredList/SortedList/TableView.
|
||||
*/
|
||||
public boolean removeActiveChatMember(ChatMember member) {
|
||||
String key = buildActiveChatMemberKey(member);
|
||||
if (key == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ChatMember removedMember = activeChatMembersByCallAndCategory.remove(key);
|
||||
runOnFxThread(() -> lst_chatMemberList.removeIf(currentMember -> key.equals(buildActiveChatMemberKey(currentMember))));
|
||||
return removedMember != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the active-member model and the JavaFX UI mirror. Use this instead of
|
||||
* getLst_chatMemberList().clear() from timers or worker threads.
|
||||
*/
|
||||
public void clearActiveChatMembers() {
|
||||
activeChatMembersByCallAndCategory.clear();
|
||||
runOnFxThread(() -> lst_chatMemberList.clear());
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a member from the thread-safe active model. This avoids reading the
|
||||
* TableView backing list from MessageBusManagementThread.
|
||||
*/
|
||||
public ChatMember findActiveChatMember(ChatMember lookForThis) {
|
||||
String key = buildActiveChatMemberKey(lookForThis);
|
||||
return key == null ? null : activeChatMembersByCallAndCategory.get(key);
|
||||
}
|
||||
|
||||
public ChatMember findActiveChatMember(String callSign, ChatCategory category) {
|
||||
String key = buildActiveChatMemberKey(callSign, category);
|
||||
return key == null ? null : activeChatMembersByCallAndCategory.get(key);
|
||||
}
|
||||
|
||||
public int getActiveChatMemberCount() {
|
||||
return activeChatMembersByCallAndCategory.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all active category variants of a callsign. This is used when a QRG or
|
||||
* band hint should be propagated from one category instance to the same station
|
||||
* in another category.
|
||||
*/
|
||||
public List<ChatMember> findActiveChatMembersByRawCall(String callSignRaw) {
|
||||
String normalizedCallsign = ChatMember.normalizeCallSignToBaseCallSign(callSignRaw);
|
||||
if (normalizedCallsign == null || normalizedCallsign.isBlank()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
String normalizedKeyPart = normalizedCallsign.trim().toUpperCase(Locale.ROOT);
|
||||
List<ChatMember> matchingMembers = new ArrayList<>();
|
||||
|
||||
for (ChatMember member : activeChatMembersByCallAndCategory.values()) {
|
||||
if (member == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String memberCall = member.getCallSignRaw() != null ? member.getCallSignRaw() : member.getCallSign();
|
||||
String normalizedMemberCall = ChatMember.normalizeCallSignToBaseCallSign(memberCall);
|
||||
if (normalizedMemberCall != null && normalizedMemberCall.equalsIgnoreCase(normalizedKeyPart)) {
|
||||
matchingMembers.add(member);
|
||||
}
|
||||
}
|
||||
|
||||
return matchingMembers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates locator and derived direction values in the active model. The UI list
|
||||
* contains the same member object, but the user-list refresh is still triggered
|
||||
* on the JavaFX thread so TableView/map derived displays can repaint safely.
|
||||
*/
|
||||
public boolean updateActiveChatMemberLocator(ChatMember lookupMember, String newLocator) {
|
||||
ChatMember activeMember = findActiveChatMember(lookupMember);
|
||||
if (activeMember == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
activeMember.setQra(newLocator);
|
||||
activeMember.setQrb(new Location().getDistanceKmByTwoLocatorStrings(chatPreferences.getStn_loginLocatorMainCat(), newLocator));
|
||||
activeMember.setQTFdirection(new Location(chatPreferences.getStn_loginLocatorMainCat()).getBearing(new Location(newLocator)));
|
||||
fireUserListUpdate("Locator changed");
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean updateActiveChatMemberState(ChatMember lookupMember, int newState) {
|
||||
ChatMember activeMember = findActiveChatMember(lookupMember);
|
||||
if (activeMember == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
activeMember.setState(newState);
|
||||
fireUserListUpdate("User state changed");
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a UM3-style user-info update only to an already active chat member.
|
||||
*
|
||||
* ON4KST may send UM3 messages for stations that are not logged into the
|
||||
* current channel, for example when a user changes locator/profile data on the
|
||||
* website. Such stations must not be added to the visible chat member list,
|
||||
* because they cannot be addressed in the channel. Their full information will
|
||||
* be delivered again with UA0/UA5/UA2 when they actually join.
|
||||
*
|
||||
* @return true if an active member was found and updated, false if the UM3
|
||||
* information was intentionally ignored.
|
||||
*/
|
||||
public boolean updateActiveChatMemberInfoIfPresent(ChatMember updatedMember) {
|
||||
String key = buildActiveChatMemberKey(updatedMember);
|
||||
if (key == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ChatMember activeMember = activeChatMembersByCallAndCategory.get(key);
|
||||
if (activeMember == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
activeMember.setName(updatedMember.getName());
|
||||
activeMember.setQra(updatedMember.getQra());
|
||||
activeMember.setState(updatedMember.getState());
|
||||
activeMember.setLastActivity(updatedMember.getLastActivity());
|
||||
activeMember.setActivityTimeLastInEpoch(updatedMember.getActivityTimeLastInEpoch());
|
||||
activeMember.setQrb(updatedMember.getQrb());
|
||||
activeMember.setQTFdirection(updatedMember.getQTFdirection());
|
||||
fireUserListUpdate("User info updated");
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Backward-compatible wrapper for older call sites. Despite the historic name,
|
||||
* this method no longer adds unknown UM3 users to the active member model.
|
||||
*/
|
||||
public boolean updateOrAddActiveChatMemberInfo(ChatMember updatedMember) {
|
||||
return updateActiveChatMemberInfoIfPresent(updatedMember);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores a newly detected QRG/band on all active category variants of a station.
|
||||
* The old StringProperty is still set for compatibility with existing TableView
|
||||
* columns and DXCluster code.
|
||||
*/
|
||||
public void applyDetectedFrequencyToActiveMembers(ChatMember sender, Band detectedBand, double detectedFrequencyMhz) {
|
||||
if (sender == null || detectedBand == null || detectedFrequencyMhz <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
String displayFrequency = String.valueOf(detectedFrequencyMhz);
|
||||
String rawCall = sender.getCallSignRaw() != null ? sender.getCallSignRaw() : sender.getCallSign();
|
||||
|
||||
for (ChatMember member : findActiveChatMembersByRawCall(rawCall)) {
|
||||
if (member == null) {
|
||||
continue;
|
||||
}
|
||||
member.addKnownFrequency(detectedBand, detectedFrequencyMhz);
|
||||
member.setFrequency(new SimpleStringProperty(displayFrequency));
|
||||
}
|
||||
|
||||
// Also cover dummy senders that are not in the active model, e.g. [n/a] senders.
|
||||
sender.addKnownFrequency(detectedBand, detectedFrequencyMhz);
|
||||
sender.setFrequency(new SimpleStringProperty(displayFrequency));
|
||||
fireUserListUpdate("Frequency detected");
|
||||
}
|
||||
|
||||
private int findChatMemberIndexInUiListByKey(String key) {
|
||||
if (key == null) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (int i = 0; i < lst_chatMemberList.size(); i++) {
|
||||
if (key.equals(buildActiveChatMemberKey(lst_chatMemberList.get(i)))) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -2011,7 +2288,7 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
|
||||
socket.close();
|
||||
|
||||
chatController.setConnectedAndLoggedIn(false);
|
||||
chatController.getLst_chatMemberList().clear();
|
||||
chatController.clearActiveChatMembers();
|
||||
|
||||
System.out.println("[Chatcontroller, Warning: ] Socket closed or disconnected");
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5016,7 +5016,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
help2.setOnAction(new EventHandler<ActionEvent>() {
|
||||
public void handle(ActionEvent event) {
|
||||
|
||||
getHostServices().showDocument("https://www.paypal.com/paypalme/do5amf");
|
||||
getHostServices().showDocument("https://ko-fi.com/praktimarc");
|
||||
|
||||
}
|
||||
});
|
||||
@@ -5970,9 +5970,19 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
txt_myQTF.getStyleClass().add("text-input");
|
||||
txt_myQTF.getStyleClass().add("text-input-MYQRG1");
|
||||
|
||||
txt_myQTF.textProperty().bind(Bindings.createStringBinding(
|
||||
() -> Double.toString(chatcontroller.getChatPreferences().getActualQTF().get()),
|
||||
chatcontroller.getChatPreferences().getActualQTF()));
|
||||
// Rotator sync on/off determines whether this field is bound (read-only, driven
|
||||
// by PSTRotator) or free for manual entry, mirroring the MYQRG sync behaviour above.
|
||||
if (chatcontroller.getChatPreferences().isStn_pstRotatorEnabled()) {
|
||||
txt_myQTF.textProperty().bind(Bindings.createStringBinding(
|
||||
() -> Double.toString(chatcontroller.getChatPreferences().getActualQTF().get()),
|
||||
chatcontroller.getChatPreferences().getActualQTF()));
|
||||
txt_myQTF.setTooltip(new Tooltip("This is your current QTF, read out at PSTRotator"));
|
||||
txt_myQTF.setFocusTraversable(false);
|
||||
} else {
|
||||
txt_myQTF.setText(Double.toString(chatcontroller.getChatPreferences().getActualQTF().get()));
|
||||
txt_myQTF.setTooltip(new Tooltip("Enter your antenna heading (QTF) by hand - no rotator sync active"));
|
||||
txt_myQTF.setFocusTraversable(true);
|
||||
}
|
||||
|
||||
txt_myQTF.focusedProperty().addListener(new ChangeListener<Boolean>() {
|
||||
@Override
|
||||
@@ -5982,13 +5992,16 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
// System.out.println("Textfield on focus");
|
||||
// Do nothing until field loses focus, user will enter his frequency
|
||||
} else {
|
||||
if (txt_myQTF.textProperty().isBound()) {
|
||||
return; // rotator sync active, field is driven by PSTRotator
|
||||
}
|
||||
try {
|
||||
System.out.println(
|
||||
"[Main.java, Info]: Set the MYQTF property by hand to: " + txt_myQTF.getText());
|
||||
chatcontroller.getChatPreferences().getActualQTF().set(Integer.parseInt(txt_myQTF.getText()));}
|
||||
chatcontroller.getChatPreferences().getActualQTF().set(Double.parseDouble(txt_myQTF.getText()));}
|
||||
catch (Exception exception) {
|
||||
System.out.println("bullshit entered in myqtf");
|
||||
txt_myQTF.setText("0");
|
||||
txt_myQTF.setText("0.0");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5997,8 +6010,6 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
txt_myQTF.setPrefSize(40, 0);
|
||||
// txt_ownqrg.setMinSize(40, 0);
|
||||
txt_myQTF.setAlignment(Pos.BASELINE_RIGHT);
|
||||
txt_myQTF.setTooltip(new Tooltip("This is your current QTF, read out at PSTRotator"));
|
||||
txt_myQTF.setFocusTraversable(false);
|
||||
|
||||
SplitPane mainWindowLeftSplitPane = new SplitPane();
|
||||
mainWindowLeftSplitPane.setOrientation(Orientation.HORIZONTAL);
|
||||
@@ -8637,9 +8648,21 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
chkBx_station_pstRotatorEnabled.setTooltip(new Tooltip(
|
||||
"If disabled: no PSTRotator connection is started and antenna direction is ignored in priority scoring."
|
||||
));
|
||||
chkBx_station_pstRotatorEnabled.selectedProperty().addListener((obs, oldV, newV) ->
|
||||
chatcontroller.getChatPreferences().setStn_pstRotatorEnabled(newV)
|
||||
);
|
||||
chkBx_station_pstRotatorEnabled.selectedProperty().addListener((obs, oldV, newV) -> {
|
||||
chatcontroller.getChatPreferences().setStn_pstRotatorEnabled(newV);
|
||||
if (newV) {
|
||||
txt_myQTF.textProperty().bind(Bindings.createStringBinding(
|
||||
() -> Double.toString(chatcontroller.getChatPreferences().getActualQTF().get()),
|
||||
chatcontroller.getChatPreferences().getActualQTF()));
|
||||
txt_myQTF.setTooltip(new Tooltip("This is your current QTF, read out at PSTRotator"));
|
||||
txt_myQTF.setFocusTraversable(false);
|
||||
} else {
|
||||
txt_myQTF.textProperty().unbind();
|
||||
txt_myQTF.setText(Double.toString(chatcontroller.getChatPreferences().getActualQTF().get()));
|
||||
txt_myQTF.setTooltip(new Tooltip("Enter your antenna heading (QTF) by hand - no rotator sync active"));
|
||||
txt_myQTF.setFocusTraversable(true);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
grdPnlStation.add(lblCallSign, 0, 0);
|
||||
|
||||
@@ -13,8 +13,12 @@ import java.nio.charset.StandardCharsets;
|
||||
* - inspectPoint(x,y) returns what is under the cursor
|
||||
* - zoomIn()/zoomOut() are callable from Java
|
||||
* - grid / beam / connection use non-interactive panes
|
||||
* - JavaScript logs are forwarded to Java through javaMapBridge
|
||||
* - JavaScript errors are forwarded to Java through javaMapBridge
|
||||
* - setTheme(light|dark) aligns the map with the JavaFX application theme
|
||||
*
|
||||
* Important:
|
||||
* This version intentionally uses integer Leaflet zoom levels again.
|
||||
* Fractional zoom in JavaFX WebView caused unreliable marker positioning.
|
||||
*/
|
||||
public final class MapHtmlResources {
|
||||
|
||||
@@ -184,6 +188,111 @@ public final class MapHtmlResources {
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.station-cluster-wrapper {
|
||||
background: transparent;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.station-cluster-root {
|
||||
position: relative;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
pointer-events: auto;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/*
|
||||
* Cluster design aligned with normal KST4Contest station markers:
|
||||
* - dark center like station-dot
|
||||
* - blue border for normal clustered stations
|
||||
* - yellow border when the cluster contains worked stations
|
||||
* - orange hover border similar to selected station state
|
||||
*/
|
||||
.station-cluster-bubble {
|
||||
position: absolute;
|
||||
left: -16px;
|
||||
top: -16px;
|
||||
min-width: 32px;
|
||||
height: 32px;
|
||||
padding: 0 7px;
|
||||
border-radius: 18px;
|
||||
background: #1d1d1d;
|
||||
color: #f4f7fa;
|
||||
border: 2px solid #4da6ff;
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(0, 0, 0, 0.35),
|
||||
0 2px 7px rgba(0, 0, 0, 0.42);
|
||||
box-sizing: border-box;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
line-height: 28px;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.station-cluster-bubble.medium {
|
||||
left: -18px;
|
||||
top: -18px;
|
||||
min-width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
.station-cluster-bubble.large {
|
||||
left: -21px;
|
||||
top: -21px;
|
||||
min-width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 23px;
|
||||
font-size: 15px;
|
||||
line-height: 38px;
|
||||
}
|
||||
|
||||
.station-cluster-bubble.worked {
|
||||
border-color: #ffd24d;
|
||||
}
|
||||
|
||||
.station-cluster-bubble.warning {
|
||||
border-color: #00ff66;
|
||||
color: #00ff66;
|
||||
}
|
||||
|
||||
.station-cluster-root:hover .station-cluster-bubble {
|
||||
border-color: #ff9900;
|
||||
box-shadow:
|
||||
0 0 0 2px rgba(255, 153, 0, 0.28),
|
||||
0 2px 9px rgba(0, 0, 0, 0.48);
|
||||
}
|
||||
|
||||
body.kst-theme-dark .station-cluster-bubble {
|
||||
background: #202428;
|
||||
color: #f1f3f5;
|
||||
border-color: #4da6ff;
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(255, 255, 255, 0.12),
|
||||
0 2px 8px rgba(0, 0, 0, 0.58);
|
||||
}
|
||||
|
||||
body.kst-theme-dark .station-cluster-bubble.worked {
|
||||
border-color: #ffd24d;
|
||||
}
|
||||
|
||||
body.kst-theme-dark .station-cluster-bubble.warning {
|
||||
border-color: #00ff66;
|
||||
color: #00ff66;
|
||||
}
|
||||
|
||||
body.kst-theme-dark .station-cluster-root:hover .station-cluster-bubble {
|
||||
border-color: #ff9900;
|
||||
box-shadow:
|
||||
0 0 0 2px rgba(255, 153, 0, 0.32),
|
||||
0 2px 10px rgba(0, 0, 0, 0.65);
|
||||
}
|
||||
|
||||
.maidenhead-grid-label-wrapper {
|
||||
background: transparent;
|
||||
border: none;
|
||||
@@ -225,9 +334,6 @@ public final class MapHtmlResources {
|
||||
<script>window._kstTileProxyPort=__TILE_PROXY_PORT__;</script>
|
||||
|
||||
<script>
|
||||
|
||||
|
||||
|
||||
window.kstMapApi = (function () {
|
||||
let map;
|
||||
let stationLayer;
|
||||
@@ -235,11 +341,77 @@ public final class MapHtmlResources {
|
||||
let beamLayer;
|
||||
let connectionLayer;
|
||||
let profileHoverMarker;
|
||||
|
||||
/*
|
||||
* Currently visible individual station markers.
|
||||
*
|
||||
* A callsign is present here only when it is rendered individually.
|
||||
* If the station is inside a cluster, stationsByCallsignRaw is used
|
||||
* for focus/zoom operations.
|
||||
*/
|
||||
let markersByCallsignRaw = {};
|
||||
|
||||
/*
|
||||
* All station data as received from Java.
|
||||
*/
|
||||
let stationData = [];
|
||||
let stationsByCallsignRaw = {};
|
||||
|
||||
let clustersById = {};
|
||||
let clusterSequence = 0;
|
||||
|
||||
let activeTheme = 'light';
|
||||
let invalidateNotifyTimer = 0;
|
||||
|
||||
/*
|
||||
* Integer zoom only.
|
||||
*
|
||||
* Fractional zoom is intentionally disabled because JavaFX WebView
|
||||
* and Leaflet marker positioning were unreliable at intermediate
|
||||
* zoom levels.
|
||||
*/
|
||||
const KST_ZOOM_STEP = 1;
|
||||
const KST_MIN_ZOOM = 3;
|
||||
const KST_MAX_ZOOM = 18;
|
||||
|
||||
/*
|
||||
* Enables verbose JavaScript map logging.
|
||||
*
|
||||
* Keep this false for normal operation. jsError() still reports
|
||||
* real errors.
|
||||
*/
|
||||
const KST_MAP_DEBUG = false;
|
||||
|
||||
/*
|
||||
* Cluster settings.
|
||||
*
|
||||
* With integer zoom, clustering is disabled at zoom >= 8.
|
||||
* At zoom 7 only very close stations are grouped.
|
||||
*/
|
||||
const KST_CLUSTER_DISABLE_ZOOM = 8;
|
||||
|
||||
/*
|
||||
* Do not cluster pairs immediately. Two nearby stations are still
|
||||
* readable and should remain individually clickable.
|
||||
*/
|
||||
const KST_CLUSTER_MIN_STATIONS = 3;
|
||||
|
||||
/*
|
||||
* Cluster cell sizes in screen pixels.
|
||||
*
|
||||
* Smaller cells make clustering less aggressive. Stations must be
|
||||
* closer together on screen before they are grouped.
|
||||
*/
|
||||
const KST_CLUSTER_CELL_SIZE_HIGH_ZOOM = 55;
|
||||
const KST_CLUSTER_CELL_SIZE_MEDIUM_ZOOM = 70;
|
||||
const KST_CLUSTER_CELL_SIZE_LOW_ZOOM = 95;
|
||||
const KST_CLUSTER_CELL_SIZE_VERY_LOW_ZOOM = 125;
|
||||
|
||||
function jsLog(message) {
|
||||
if (!KST_MAP_DEBUG) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (window.javaMapBridge) {
|
||||
window.javaMapBridge.onJsLog(String(message));
|
||||
@@ -358,6 +530,331 @@ public final class MapHtmlResources {
|
||||
+ '</div>';
|
||||
}
|
||||
|
||||
function getStationCallsignKey(station) {
|
||||
if (!station || station.callSignRaw === null || station.callSignRaw === undefined) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return String(station.callSignRaw);
|
||||
}
|
||||
|
||||
function isStationPositionValid(station) {
|
||||
if (!station) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isFinite(station.latitudeDeg) && isFinite(station.longitudeDeg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true for stations that must not be hidden inside a cluster.
|
||||
*
|
||||
* These stations remain individually visible even at low zoom levels
|
||||
* because they are operationally important during contest operation.
|
||||
*/
|
||||
function shouldRenderStationIndividually(station) {
|
||||
if (!station) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (station.selected) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (station.warningToMyDirection) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the cluster grid size in screen pixels for the current zoom level.
|
||||
*
|
||||
* The cluster calculation is screen-based, not locator-based. This makes
|
||||
* the decision match the real visual problem: too many labels in the same
|
||||
* screen area.
|
||||
*/
|
||||
function getClusterCellSizePx() {
|
||||
if (!map) {
|
||||
return KST_CLUSTER_CELL_SIZE_MEDIUM_ZOOM;
|
||||
}
|
||||
|
||||
const zoom = Number(map.getZoom());
|
||||
|
||||
if (zoom >= 7) {
|
||||
return KST_CLUSTER_CELL_SIZE_HIGH_ZOOM;
|
||||
}
|
||||
|
||||
if (zoom >= 6) {
|
||||
return KST_CLUSTER_CELL_SIZE_MEDIUM_ZOOM;
|
||||
}
|
||||
|
||||
if (zoom >= 5) {
|
||||
return KST_CLUSTER_CELL_SIZE_LOW_ZOOM;
|
||||
}
|
||||
|
||||
return KST_CLUSTER_CELL_SIZE_VERY_LOW_ZOOM;
|
||||
}
|
||||
|
||||
function buildClusterMarkerHtml(clusterId, clusterStations) {
|
||||
const count = clusterStations ? clusterStations.length : 0;
|
||||
|
||||
let bubbleClasses = 'station-cluster-bubble';
|
||||
|
||||
if (count >= 20) {
|
||||
bubbleClasses += ' large';
|
||||
} else if (count >= 8) {
|
||||
bubbleClasses += ' medium';
|
||||
}
|
||||
|
||||
const containsWorkedStation = clusterStations
|
||||
&& clusterStations.some(station => station && station.worked);
|
||||
|
||||
const containsWarningStation = clusterStations
|
||||
&& clusterStations.some(station => station && station.warningToMyDirection);
|
||||
|
||||
if (containsWarningStation) {
|
||||
bubbleClasses += ' warning';
|
||||
} else if (containsWorkedStation) {
|
||||
bubbleClasses += ' worked';
|
||||
}
|
||||
|
||||
return '<div class="station-cluster-root" data-cluster-id="' + escapeHtml(clusterId) + '">'
|
||||
+ '<div class="' + bubbleClasses + '">' + count + '</div>'
|
||||
+ '</div>';
|
||||
}
|
||||
|
||||
function buildClusterTooltipHtml(clusterStations) {
|
||||
if (!clusterStations || clusterStations.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const callsigns = clusterStations
|
||||
.map(station => station.markerLabel || station.callSignRaw || station.callSign || '')
|
||||
.filter(value => value !== null && value !== undefined && String(value).trim() !== '')
|
||||
.map(value => String(value).trim())
|
||||
.sort();
|
||||
|
||||
const maxPreviewCount = 20;
|
||||
const preview = callsigns
|
||||
.slice(0, maxPreviewCount)
|
||||
.map(value => escapeHtml(value))
|
||||
.join('<br>');
|
||||
|
||||
const remainingCount = Math.max(0, callsigns.length - maxPreviewCount);
|
||||
|
||||
let html = '<b>' + clusterStations.length + ' stations</b>';
|
||||
|
||||
if (preview) {
|
||||
html += '<br>' + preview;
|
||||
}
|
||||
|
||||
if (remainingCount > 0) {
|
||||
html += '<br>+' + remainingCount + ' more';
|
||||
}
|
||||
|
||||
html += '<br><i>Click to zoom in</i>';
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function addStationMarker(station) {
|
||||
if (!stationLayer || !isStationPositionValid(station)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const marker = L.marker(
|
||||
[station.latitudeDeg, station.longitudeDeg],
|
||||
{
|
||||
interactive: true,
|
||||
keyboard: false,
|
||||
icon: L.divIcon({
|
||||
className: 'station-marker-wrapper',
|
||||
html: buildStationMarkerHtml(station),
|
||||
iconSize: [1, 1],
|
||||
iconAnchor: [0, 0]
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
marker.addTo(stationLayer);
|
||||
|
||||
const callSignKey = getStationCallsignKey(station);
|
||||
if (callSignKey) {
|
||||
markersByCallsignRaw[callSignKey] = marker;
|
||||
}
|
||||
}
|
||||
|
||||
function addClusterMarker(clusterStations) {
|
||||
if (!stationLayer || !clusterStations || clusterStations.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let latSum = 0.0;
|
||||
let lonSum = 0.0;
|
||||
let validCount = 0;
|
||||
|
||||
clusterStations.forEach(station => {
|
||||
if (isStationPositionValid(station)) {
|
||||
latSum += Number(station.latitudeDeg);
|
||||
lonSum += Number(station.longitudeDeg);
|
||||
validCount++;
|
||||
}
|
||||
});
|
||||
|
||||
if (validCount === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const centerLat = latSum / validCount;
|
||||
const centerLon = lonSum / validCount;
|
||||
|
||||
const clusterId = 'cluster-' + (++clusterSequence);
|
||||
clustersById[clusterId] = clusterStations;
|
||||
|
||||
const clusterMarker = L.marker(
|
||||
[centerLat, centerLon],
|
||||
{
|
||||
interactive: true,
|
||||
keyboard: false,
|
||||
icon: L.divIcon({
|
||||
className: 'station-cluster-wrapper',
|
||||
html: buildClusterMarkerHtml(clusterId, clusterStations),
|
||||
iconSize: [1, 1],
|
||||
iconAnchor: [0, 0]
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
clusterMarker.on('click', function () {
|
||||
zoomToCluster(clusterStations);
|
||||
});
|
||||
|
||||
clusterMarker.bindTooltip(buildClusterTooltipHtml(clusterStations), {
|
||||
direction: 'top',
|
||||
sticky: true,
|
||||
opacity: 0.95
|
||||
});
|
||||
|
||||
clusterMarker.addTo(stationLayer);
|
||||
}
|
||||
|
||||
function renderAllStationsIndividually() {
|
||||
stationData.forEach(station => {
|
||||
addStationMarker(station);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders stations with simple screen-grid clustering.
|
||||
*
|
||||
* Important stations such as the currently selected station and warning
|
||||
* stations are rendered individually before clustering. They remain
|
||||
* visible even at low zoom levels.
|
||||
*/
|
||||
function renderClusteredStations() {
|
||||
const clusterCellSizePx = getClusterCellSizePx();
|
||||
const buckets = {};
|
||||
|
||||
stationData.forEach(station => {
|
||||
if (!isStationPositionValid(station)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldRenderStationIndividually(station)) {
|
||||
addStationMarker(station);
|
||||
return;
|
||||
}
|
||||
|
||||
const point = map.latLngToContainerPoint([station.latitudeDeg, station.longitudeDeg]);
|
||||
const cellX = Math.floor(point.x / clusterCellSizePx);
|
||||
const cellY = Math.floor(point.y / clusterCellSizePx);
|
||||
const key = cellX + ':' + cellY;
|
||||
|
||||
if (!buckets[key]) {
|
||||
buckets[key] = [];
|
||||
}
|
||||
|
||||
buckets[key].push(station);
|
||||
});
|
||||
|
||||
Object.keys(buckets).forEach(key => {
|
||||
const bucketStations = buckets[key];
|
||||
|
||||
if (bucketStations.length >= KST_CLUSTER_MIN_STATIONS) {
|
||||
addClusterMarker(bucketStations);
|
||||
} else {
|
||||
bucketStations.forEach(station => addStationMarker(station));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-renders station markers for the current zoom and viewport.
|
||||
*
|
||||
* This is called when stations arrive from Java and after zoom/move
|
||||
* events, because screen-grid clusters depend on the current viewport.
|
||||
*/
|
||||
function renderStationMarkers() {
|
||||
if (!map || !stationLayer) {
|
||||
return;
|
||||
}
|
||||
|
||||
stationLayer.clearLayers();
|
||||
markersByCallsignRaw = {};
|
||||
clustersById = {};
|
||||
clusterSequence = 0;
|
||||
|
||||
if (!stationData || stationData.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Number(map.getZoom()) >= KST_CLUSTER_DISABLE_ZOOM) {
|
||||
renderAllStationsIndividually();
|
||||
} else {
|
||||
renderClusteredStations();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Zooms into a cluster.
|
||||
*
|
||||
* A cluster click does not select one station. It moves the map towards
|
||||
* the cluster center and increases zoom until individual stations become
|
||||
* visible.
|
||||
*/
|
||||
function zoomToCluster(clusterStations) {
|
||||
if (!map || !clusterStations || clusterStations.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const latLngs = [];
|
||||
|
||||
clusterStations.forEach(station => {
|
||||
if (isStationPositionValid(station)) {
|
||||
latLngs.push(L.latLng(station.latitudeDeg, station.longitudeDeg));
|
||||
}
|
||||
});
|
||||
|
||||
if (latLngs.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bounds = L.latLngBounds(latLngs);
|
||||
const currentZoom = Number(map.getZoom());
|
||||
|
||||
const targetZoom = clampZoomToLeafletLimits(
|
||||
Math.min(KST_CLUSTER_DISABLE_ZOOM, currentZoom + 1)
|
||||
);
|
||||
|
||||
map.setView(bounds.getCenter(), targetZoom, {
|
||||
animate: false
|
||||
});
|
||||
|
||||
notifyViewport();
|
||||
}
|
||||
|
||||
function init() {
|
||||
if (map) {
|
||||
return true;
|
||||
@@ -372,6 +869,17 @@ public final class MapHtmlResources {
|
||||
|
||||
map = L.map('map', {
|
||||
zoomControl: true,
|
||||
minZoom: KST_MIN_ZOOM,
|
||||
maxZoom: KST_MAX_ZOOM,
|
||||
|
||||
/*
|
||||
* Integer zoom only.
|
||||
* This keeps station positions stable in JavaFX WebView.
|
||||
*/
|
||||
zoomSnap: 1,
|
||||
zoomDelta: 1,
|
||||
wheelPxPerZoomLevel: 120,
|
||||
|
||||
zoomAnimation: false,
|
||||
fadeAnimation: false,
|
||||
markerZoomAnimation: false,
|
||||
@@ -383,7 +891,8 @@ public final class MapHtmlResources {
|
||||
const tileLayer = L.tileLayer(
|
||||
'http://127.0.0.1:' + window._kstTileProxyPort + '/tiles/{s}/{z}/{x}/{y}.png',
|
||||
{
|
||||
maxZoom: 18,
|
||||
minZoom: KST_MIN_ZOOM,
|
||||
maxZoom: KST_MAX_ZOOM,
|
||||
updateWhenZooming: false,
|
||||
keepBuffer: 4,
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
||||
@@ -419,12 +928,12 @@ public final class MapHtmlResources {
|
||||
connectionLayer = L.layerGroup().addTo(map);
|
||||
|
||||
map.on('zoomend', function () {
|
||||
jsLog('zoomend -> ' + map.getZoom());
|
||||
renderStationMarkers();
|
||||
notifyViewport();
|
||||
});
|
||||
|
||||
map.on('moveend', function () {
|
||||
jsLog('moveend');
|
||||
renderStationMarkers();
|
||||
notifyViewport();
|
||||
});
|
||||
|
||||
@@ -476,20 +985,39 @@ public final class MapHtmlResources {
|
||||
invalidateSize();
|
||||
}
|
||||
|
||||
function zoomIn() {
|
||||
/**
|
||||
* Keeps the zoom value inside the explicit KST zoom limits.
|
||||
*/
|
||||
function clampZoomToLeafletLimits(zoom) {
|
||||
return Math.max(KST_MIN_ZOOM, Math.min(KST_MAX_ZOOM, zoom));
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the zoom in stable integer steps.
|
||||
*/
|
||||
function changeZoomByKstStep(delta) {
|
||||
if (!map) {
|
||||
return;
|
||||
}
|
||||
|
||||
map.setZoom(map.getZoom() + 1, { animate: false });
|
||||
const currentZoom = Number(map.getZoom());
|
||||
const nextZoom = clampZoomToLeafletLimits(currentZoom + delta);
|
||||
|
||||
if (nextZoom === currentZoom) {
|
||||
return;
|
||||
}
|
||||
|
||||
map.setZoom(nextZoom, {
|
||||
animate: false
|
||||
});
|
||||
}
|
||||
|
||||
function zoomIn() {
|
||||
changeZoomByKstStep(KST_ZOOM_STEP);
|
||||
}
|
||||
|
||||
function zoomOut() {
|
||||
if (!map) {
|
||||
return;
|
||||
}
|
||||
|
||||
map.setZoom(map.getZoom() - 1, { animate: false });
|
||||
changeZoomByKstStep(-KST_ZOOM_STEP);
|
||||
}
|
||||
|
||||
function getViewportState() {
|
||||
@@ -522,6 +1050,12 @@ public final class MapHtmlResources {
|
||||
return 'station|' + callSignRaw + '|' + el.tagName + '|' + (el.className || '') + '|' + (el.textContent || '').trim();
|
||||
}
|
||||
|
||||
const clusterRoot = el.closest('.station-cluster-root');
|
||||
if (clusterRoot) {
|
||||
const clusterId = clusterRoot.getAttribute('data-cluster-id') || '';
|
||||
return 'cluster|' + clusterId + '|' + el.tagName + '|' + (el.className || '') + '|' + (el.textContent || '').trim();
|
||||
}
|
||||
|
||||
const zoomInButton = el.closest('.leaflet-control-zoom-in');
|
||||
if (zoomInButton) {
|
||||
return 'zoomIn||' + el.tagName + '|' + (el.className || '') + '|' + (el.textContent || '').trim();
|
||||
@@ -542,8 +1076,8 @@ public final class MapHtmlResources {
|
||||
if (!init()) {
|
||||
return;
|
||||
}
|
||||
jsLog('setHome lat=' + lat + ' lon=' + lon + ' zoom=' + zoom);
|
||||
map.setView([lat, lon], zoom);
|
||||
|
||||
map.setView([lat, lon], clampZoomToLeafletLimits(Math.round(Number(zoom))));
|
||||
}
|
||||
|
||||
function setStations(stationsJson) {
|
||||
@@ -551,30 +1085,17 @@ public final class MapHtmlResources {
|
||||
return;
|
||||
}
|
||||
|
||||
stationLayer.clearLayers();
|
||||
markersByCallsignRaw = {};
|
||||
stationData = JSON.parse(stationsJson);
|
||||
stationsByCallsignRaw = {};
|
||||
|
||||
const stations = JSON.parse(stationsJson);
|
||||
jsLog('setStations count=' + stations.length);
|
||||
|
||||
stations.forEach(station => {
|
||||
const marker = L.marker(
|
||||
[station.latitudeDeg, station.longitudeDeg],
|
||||
{
|
||||
interactive: true,
|
||||
keyboard: false,
|
||||
icon: L.divIcon({
|
||||
className: 'station-marker-wrapper',
|
||||
html: buildStationMarkerHtml(station),
|
||||
iconSize: [1, 1],
|
||||
iconAnchor: [0, 0]
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
marker.addTo(stationLayer);
|
||||
markersByCallsignRaw[station.callSignRaw] = marker;
|
||||
stationData.forEach(station => {
|
||||
const callSignKey = getStationCallsignKey(station);
|
||||
if (callSignKey) {
|
||||
stationsByCallsignRaw[callSignKey] = station;
|
||||
}
|
||||
});
|
||||
|
||||
renderStationMarkers();
|
||||
}
|
||||
|
||||
function setBeam(beamJson) {
|
||||
@@ -592,8 +1113,6 @@ public final class MapHtmlResources {
|
||||
return;
|
||||
}
|
||||
|
||||
jsLog('setBeam points=' + points.length);
|
||||
|
||||
const latLngs = points.map(point => [point.lat, point.lon]);
|
||||
|
||||
L.polygon(latLngs, {
|
||||
@@ -621,8 +1140,6 @@ public final class MapHtmlResources {
|
||||
return;
|
||||
}
|
||||
|
||||
jsLog('setConnection');
|
||||
|
||||
L.polyline(points.map(point => [point.lat, point.lon]), {
|
||||
pane: 'connectionPane',
|
||||
color: connectionColor(),
|
||||
@@ -671,7 +1188,6 @@ public final class MapHtmlResources {
|
||||
gridLayer.clearLayers();
|
||||
|
||||
const cells = JSON.parse(gridJson);
|
||||
jsLog('setGrid cells=' + cells.length);
|
||||
|
||||
cells.forEach(cell => {
|
||||
const rectangle = L.rectangle(
|
||||
@@ -712,48 +1228,60 @@ public final class MapHtmlResources {
|
||||
}
|
||||
|
||||
function focusCallsignRaw(callSignRaw) {
|
||||
if (!map || !callSignRaw) {
|
||||
return;
|
||||
}
|
||||
|
||||
const marker = markersByCallsignRaw[callSignRaw];
|
||||
if (!marker) {
|
||||
jsLog('focusCallsignRaw skipped for ' + callSignRaw);
|
||||
return;
|
||||
}
|
||||
|
||||
jsLog('focusCallsignRaw ' + callSignRaw);
|
||||
|
||||
map.panTo(marker.getLatLng(), {
|
||||
animate: false
|
||||
});
|
||||
|
||||
notifyViewport();
|
||||
}
|
||||
|
||||
|
||||
if (!map || !callSignRaw) {
|
||||
return;
|
||||
}
|
||||
|
||||
const marker = markersByCallsignRaw[callSignRaw];
|
||||
if (marker) {
|
||||
map.panTo(marker.getLatLng(), {
|
||||
animate: false
|
||||
});
|
||||
|
||||
notifyViewport();
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* If the station is currently hidden inside a cluster, there is no
|
||||
* individual marker. Use the raw station data and zoom in far enough
|
||||
* so clustering is disabled and the station becomes visible.
|
||||
*/
|
||||
const station = stationsByCallsignRaw[callSignRaw];
|
||||
if (station && isStationPositionValid(station)) {
|
||||
const focusZoom = clampZoomToLeafletLimits(
|
||||
Math.max(Number(map.getZoom()), KST_CLUSTER_DISABLE_ZOOM)
|
||||
);
|
||||
|
||||
map.setView([station.latitudeDeg, station.longitudeDeg], focusZoom, {
|
||||
animate: false
|
||||
});
|
||||
|
||||
notifyViewport();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
init: init,
|
||||
invalidateSize: invalidateSize,
|
||||
resize: resize,
|
||||
zoomIn: zoomIn,
|
||||
zoomOut: zoomOut,
|
||||
inspectPoint: inspectPoint,
|
||||
getViewportState: getViewportState,
|
||||
setHome: setHome,
|
||||
setStations: setStations,
|
||||
setBeam: setBeam,
|
||||
setConnection: setConnection,
|
||||
setProfileHoverPoint: setProfileHoverPoint,
|
||||
setGrid: setGrid,
|
||||
focusCallsignRaw: focusCallsignRaw,
|
||||
setTheme: setTheme
|
||||
};
|
||||
init: init,
|
||||
invalidateSize: invalidateSize,
|
||||
resize: resize,
|
||||
zoomIn: zoomIn,
|
||||
zoomOut: zoomOut,
|
||||
inspectPoint: inspectPoint,
|
||||
getViewportState: getViewportState,
|
||||
setHome: setHome,
|
||||
setStations: setStations,
|
||||
setBeam: setBeam,
|
||||
setConnection: setConnection,
|
||||
setProfileHoverPoint: setProfileHoverPoint,
|
||||
setGrid: setGrid,
|
||||
focusCallsignRaw: focusCallsignRaw,
|
||||
setTheme: setTheme
|
||||
};
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
""".replace("__TILE_PROXY_PORT__", String.valueOf(tileProxyPort));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,14 @@ import javafx.scene.layout.ColumnConstraints;
|
||||
*/
|
||||
public final class StationMapView {
|
||||
|
||||
/**
|
||||
* Enables verbose map debug output.
|
||||
*
|
||||
* Keep this false for normal operation because scroll and viewport logs are very
|
||||
* noisy during map interaction.
|
||||
*/
|
||||
private static final boolean MAP_DEBUG_LOGGING = false;
|
||||
|
||||
private final PathProfileChart detailPathProfileChart = new PathProfileChart();
|
||||
private final Label detailPathModeValue = new Label("-");
|
||||
|
||||
@@ -287,12 +295,17 @@ public final class StationMapView {
|
||||
webView.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> handleWebViewClick(event));
|
||||
|
||||
webView.addEventHandler(ScrollEvent.SCROLL, event -> {
|
||||
System.out.println("[StationMap FX] SCROLL x=" + (int) event.getX()
|
||||
+ " y=" + (int) event.getY()
|
||||
+ " deltaY=" + event.getDeltaY());
|
||||
|
||||
if (MAP_DEBUG_LOGGING) {
|
||||
System.out.println("[StationMap FX] SCROLL x=" + (int) event.getX()
|
||||
+ " y=" + (int) event.getY()
|
||||
+ " deltaY=" + event.getDeltaY());
|
||||
}
|
||||
|
||||
InteractiveTarget target = inspectInteractiveTarget(event.getX(), event.getY());
|
||||
System.out.println("[StationMap FX] inspect scroll -> " + target);
|
||||
if (MAP_DEBUG_LOGGING) {
|
||||
System.out.println("[StationMap FX] inspect scroll -> " + target);
|
||||
}
|
||||
|
||||
if (event.getDeltaY() > 0) {
|
||||
executeMapScriptSafely("window.kstMapApi.zoomIn();");
|
||||
|
||||
@@ -650,6 +650,8 @@ svg.leaflet-image-layer.leaflet-interactive path {
|
||||
border-right-color: #fff;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Printing */
|
||||
|
||||
@media print {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<meta name="twitter:title" content="About KST4Contest">
|
||||
<meta name="twitter:description" content="About KST4Contest and its contest-oriented ON4KST workflow.">
|
||||
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783552468141">
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||
|
||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<meta name="twitter:title" content="Contact | KST4Contest">
|
||||
<meta name="twitter:description" content="Contact options for KST4Contest.">
|
||||
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783552468141">
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||
|
||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<meta name="twitter:title" content="Download KST4Contest">
|
||||
<meta name="twitter:description" content="Download KST4Contest releases for Windows, Linux and macOS.">
|
||||
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783552468141">
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||
|
||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<meta name="twitter:title" content="KST4Contest FAQ">
|
||||
<meta name="twitter:description" content="Frequently asked questions about KST4Contest, ON4KST contest operation, AirScout integration and VHF/UHF/SHF contest workflow.">
|
||||
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783552468141">
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||
|
||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<meta name="twitter:title" content="AirScout Integration">
|
||||
<meta name="twitter:description" content="AirScout information helps operators evaluate AP windows, aircraft scatter timing and candidate stations during VHF/UHF/SHF contests.">
|
||||
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783552468141">
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||
|
||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<meta name="twitter:title" content="Dual Chat Categories">
|
||||
<meta name="twitter:description" content="Dual category support gives contest operators better overview when working across multiple ON4KST chat rooms.">
|
||||
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783552468141">
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||
|
||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<meta name="twitter:title" content="DXCluster Server">
|
||||
<meta name="twitter:description" content="KST4Contest includes DXCluster functionality to support situational awareness during contest operation.">
|
||||
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783552468141">
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||
|
||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<meta name="twitter:title" content="KST4Contest Features">
|
||||
<meta name="twitter:description" content="Contest-optimized ON4KST features for VHF, UHF and SHF operation.">
|
||||
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783552468141">
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||
|
||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<meta name="twitter:title" content="Log Synchronization">
|
||||
<meta name="twitter:description" content="Log synchronization helps KST4Contest understand worked stations, active bands and contest context.">
|
||||
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783552468141">
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||
|
||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<meta name="twitter:title" content="Macros and Variables">
|
||||
<meta name="twitter:description" content="Macros reduce repetitive typing and help operators respond quickly during active ON4KST contest sessions.">
|
||||
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783552468141">
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||
|
||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<meta name="twitter:title" content="Priority Score System">
|
||||
<meta name="twitter:description" content="KST4Contest ranks stations by activity, direction, sked context, band information, QRG hints and operator workflow signals.">
|
||||
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783552468141">
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||
|
||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<meta name="twitter:title" content="Sked Reminder">
|
||||
<meta name="twitter:description" content="KST4Contest keeps skeds in focus while the operator handles chat traffic, logging, bands and aircraft scatter timing.">
|
||||
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783552468141">
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||
|
||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<meta name="twitter:title" content="Timeline View">
|
||||
<meta name="twitter:description" content="The timeline view helps operators understand short propagation windows and candidate timing during active contest operation.">
|
||||
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783552468141">
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||
|
||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<meta name="twitter:title" content="KST4Contest – Contest-Optimized ON4KST Chat Client">
|
||||
<meta name="twitter:description" content="KST4Contest is a modern ON4KST chat client built for VHF, UHF and SHF contest operation.">
|
||||
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783552468141">
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||
|
||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||
|
||||
@@ -0,0 +1,537 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<praktiKST>
|
||||
<latestVersion>
|
||||
<versionNumber>1.411</versionNumber>
|
||||
<adminMessage></adminMessage>
|
||||
<majorChanges>- Score of Top-Priority Candidates in the user section
|
||||
- Broadcast Messages (To ALL Station in Contest) in seperate Window</majorChanges>
|
||||
<latestVersionPathOnWebserver>https://github.com/praktimarc/kst4contest/releases/download/v1.41.1/praktiKST-v1.41.1-windows-x64.zip</latestVersionPathOnWebserver>
|
||||
</latestVersion>
|
||||
<needUpdateSinceLastVersion>
|
||||
<filename>nothing</filename>
|
||||
</needUpdateSinceLastVersion>
|
||||
<roadmap>
|
||||
<featurerequest>
|
||||
<featureDescription>Cyclic update of own name in chat to actual QRG</featureDescription>
|
||||
<requestDate>2024-02</requestDate>
|
||||
<state>open</state>
|
||||
</featurerequest>
|
||||
<featurerequest>
|
||||
<featureDescription>Acoustic warnings</featureDescription>
|
||||
<requestDate>2023-12</requestDate>
|
||||
<state>added</state>
|
||||
</featurerequest>
|
||||
<featurerequest>
|
||||
<featureDescription>Airscout support</featureDescription>
|
||||
<requestDate>2023-03</requestDate>
|
||||
<state>added</state>
|
||||
</featurerequest>
|
||||
<featurerequest>
|
||||
<featureDescription>cyclic beacon messages in public chat</featureDescription>
|
||||
<requestDate>2023-03</requestDate>
|
||||
<state>added</state>
|
||||
</featurerequest>
|
||||
</roadmap>
|
||||
<bugsReported>
|
||||
<bug>
|
||||
<bugdescription>
|
||||
stations like ol7c-7 and ol7c-2 will not be marked as worked if only "ol7c"
|
||||
is in the log. Need to interprete that. May ignore suffix starting with "-"</bugdescription>
|
||||
<state>solved</state>
|
||||
</bug>
|
||||
<bug>
|
||||
<bugdescription>
|
||||
worked callsigns table is updated only once after connected to chat
|
||||
Should be available directly after program started.
|
||||
But Reset works all time</bugdescription>
|
||||
<state>open</state>
|
||||
</bug>
|
||||
<bug>
|
||||
<bugdescription>feature request: language-specific strings, maybe based to an xml file
|
||||
</bugdescription>
|
||||
<state>open</state>
|
||||
</bug>
|
||||
<bug>
|
||||
<bugdescription>feature request: Macros
|
||||
</bugdescription>
|
||||
<state>added</state>
|
||||
</bug>
|
||||
<bug>
|
||||
<bugdescription>feature request:
|
||||
Automatic elimination of calls already connected from the log
|
||||
</bugdescription>
|
||||
<state>added</state>
|
||||
</bug>
|
||||
<bug>
|
||||
<bugdescription>feature request:
|
||||
Limitation of users beyond a certain QRB
|
||||
</bugdescription>
|
||||
<state>added</state>
|
||||
</bug>
|
||||
<bug>
|
||||
<bugdescription>feature request:
|
||||
User division for QTF perhaps settable based on the antenna lobe
|
||||
</bugdescription>
|
||||
<state>added</state>
|
||||
</bug>
|
||||
<bug>
|
||||
<bugdescription>feature request:
|
||||
Notification of new incoming users</bugdescription>
|
||||
<state>added</state>
|
||||
</bug>
|
||||
<bug>
|
||||
<bugdescription>feature request:
|
||||
Evidence of a call which was written but not responded to.
|
||||
</bugdescription>
|
||||
<state>added</state>
|
||||
</bug>
|
||||
<bug>
|
||||
<bugdescription>feature request:
|
||||
direction based reachable-warnings:
|
||||
if the chatclient detects a realised qso between two stations which both must have
|
||||
directed their beams to my station, there should be an instant warning to me for
|
||||
working at least one of the stations at the detected qrg</bugdescription>
|
||||
<state>open</state>
|
||||
</bug>
|
||||
<bug>
|
||||
<bugdescription>feature request:
|
||||
implement ASShowpath message to airacout;
|
||||
by doubleclicking a callsign in the chatmember list, the path and plane state should be
|
||||
provided by airscout </bugdescription>
|
||||
<state>open</state>
|
||||
</bug>
|
||||
<bug>
|
||||
<bugdescription>feature request:
|
||||
make chatlines copyable to the clipboard...</bugdescription>
|
||||
<state>open</state>
|
||||
</bug>
|
||||
<bug>
|
||||
<bugdescription>feature request:
|
||||
adding an activity detection for sensing if another chatmembers alive-state</bugdescription>
|
||||
<state>added</state>
|
||||
</bug>
|
||||
<bug>
|
||||
<bugdescription>On sql error close the connection to make a clean exit</bugdescription>
|
||||
<state>open</state>
|
||||
</bug>
|
||||
<bug>
|
||||
<bugdescription>SETNAME /MYQRG in options menu doesnt work</bugdescription>
|
||||
<state>solved</state>
|
||||
</bug>
|
||||
<bug>
|
||||
<bugdescription>Disconnect button does not work (DL5ZK)</bugdescription>
|
||||
<state>solved</state>
|
||||
</bug>
|
||||
<bug>
|
||||
<bugdescription>Password-wrong error not displayed (DO5SA)</bugdescription>
|
||||
<state>solved</state>
|
||||
</bug>
|
||||
<bug>
|
||||
<bugdescription>/CQ isnt written on own PMs</bugdescription>
|
||||
<state>solved</state>
|
||||
</bug>
|
||||
<bug>
|
||||
<bugdescription>rightclick-textsnippets arent available in cq-table</bugdescription>
|
||||
<state>solved</state>
|
||||
</bug>
|
||||
</bugsReported>
|
||||
<changeLog>
|
||||
<changedVersionNumber>1.411</changedVersionNumber>
|
||||
<date>2026-07-08</date>
|
||||
<description>Release v1.41.1</description>
|
||||
<added></added>
|
||||
<changed>Hotfix of Bug #56</changed>
|
||||
<fixed></fixed>
|
||||
<removed></removed>
|
||||
</changeLog>
|
||||
<changeLog>
|
||||
<changedVersionNumber>1.41</changedVersionNumber>
|
||||
<date>2026-07-01</date>
|
||||
<description>Release v1.41.0</description>
|
||||
<added>- Score of Top-Priority Candidates in the user section
|
||||
- Broadcast Messages (To ALL Station in Contest) in seperate Window</added>
|
||||
<changed>Migration to Github
|
||||
- Band and Frequency Detection per Chatmember — KST4Contest identifies which bands or frequencies a station appears active or reachable on based on chat data. This information is integrated into the display and prioritization.
|
||||
- Band Alert — If a logged station could be of interest on other locally active bands, KST14Contest can alert the user.
|
||||
- Sked Reminder ALERT — New reminder function for planned Skeds to ensure that agreed contacts are not forgotten during contest operation.
|
||||
- Skedfail Button — Failed Skeds can be marked; this allows the station to be handled differently in terms of score and workflow.
|
||||
- Win-Test SKED Push via UDP — Skeds can be passed to Win-Test via a LOCKSKED/ADDSKED/UNLOCKSKED sequence. This feature is configurable in the preferences and disabled by default.
|
||||
- Win-Test Broadcast Defaults and SKed Workflow — Improved broadcast addresses, frequency formats, mode selection, and notes including locator/azimuth.
|
||||
- PSTRotator Interface — KST4Contest received an interface to PSTRoter to better integrate antenna direction and rotator workflow.
|
||||
- QSO /-Callsign Sniffer — Callsigns or QSO-relevant information can be detected and utilized for further workflow.
|
||||
- Chat History at Startup — Historical chat data can be loaded upon startup, allowing the client to quickly gain relevant context for scoring, activity, and station identification.
|
||||
- Station Map / Map View — New map view with station markers, your own location, locator/Maidenhead reference, connection line, and antenna/beam representation.
|
||||
- Local Leaflet /-TileProxy Backend — The map function utilizes embedded Leaflet resources and a local TileProxy approach, making the map more portable and independent.
|
||||
- Maidenhead /-Grid Overlay — The map can display locator/grid information; recently added Grid-Square coloring.
|
||||
- Path /-Terrain /-Horizon Profile — Map documentation describes a path profile with terrain, Fresnel, and horizon information for the selected link.
|
||||
- Tropo /-AS Reachability Filters — New filters and sorting were added for Tropospheric reachability, AirScoop availability, large locator fields, and priority.
|
||||
- Reachability Band Selector — Added a band selection for range/Troposphere calculations.
|
||||
- 4-Char Locator Filter — Locator evaluations can be reduced to 4-character fields; this improves overview and performance.
|
||||
- KST Testserver Support — A KST server test server was added into the Source.
|
||||
@praktimarc and @Rsclub22</changed>
|
||||
<fixed>- [BUG]
|
||||
- [BUG] Programmstart kann ohne selektieren keine Nachrichten senden
|
||||
- KST4Contest crashes after to many messages or to much RAM Usage
|
||||
- Scaling Problem at 1280x800 screen resolution</fixed>
|
||||
<removed></removed>
|
||||
</changeLog>
|
||||
<changeLog>
|
||||
<changedVersionNumber>1.40</changedVersionNumber>
|
||||
<date>2026-02-16</date>
|
||||
<description>Much changes and fixes</description>
|
||||
<added>
|
||||
- win-test support (not deactivable yet, beta)
|
||||
- QSO sniffer which filters changable callsigns lists messages to your pm window
|
||||
- Chatmember Score system. Each chatmember gets a score depending to antenna direction, activity time,
|
||||
activity msgcount, positive signals, active bands, active frequencies, his sked direction (degrees),
|
||||
self handed rate, sked-made-rate and some others
|
||||
- Chatmember Band and frequency recognition: now we list all active bands of each other chatmember
|
||||
- Band alert for logged stations
|
||||
- Sked reminder ALERT with automatic messages in 2+1 / 5+2+1 / 10+5+2+1 minutes
|
||||
- PSTRotator interface
|
||||
- AirPlane Timeline
|
||||
- Skedfail button in furtherinfo panel
|
||||
- Wintest logsynch interface can now be activated and deactivated in the preferences
|
||||
- load chat history on startup
|
||||
</added>
|
||||
<changed>
|
||||
- added save option: ServerDNS / Port
|
||||
- added save option: activate/deactivate pstRotator interface
|
||||
- added save option: activate/deactivate wintest interface
|
||||
- added save option: callsign sniffer
|
||||
- added save option: boolean GUI_darkModeActiveByDefault = false
|
||||
- added AP notes at the internal DX cluster spots
|
||||
- Generic Autoanswer fires maximum once in 45 seconds per rx callsign
|
||||
</changed>
|
||||
<fixed>
|
||||
- Userlist sort automatically on new member signon
|
||||
- Posonpill-messages now kills only exactly one Client instance (bugreport 9A2HM)
|
||||
- Crash of wtkst by disconnection of kst4Contest
|
||||
- wtKST crashes when kst4contest is running (many reporters)
|
||||
- wintest: logged per band excludes pre worked band (bugreport SM6VTZ)
|
||||
- PSTRotator SOCKET Close on DISCONNECT BUTTON works now
|
||||
- AirScout Map had not been updated (bugreport SM0RJV)
|
||||
- QTFDefault had been not saved correctly (bugreport SM0RJV)
|
||||
- Show path in AS button worked only with fixed server Strings (bugreport 9A2HM)
|
||||
- Version number had been displayed wrong
|
||||
- suffixes like /p, -2, etc. problem now fixed everywhere
|
||||
- Dark mode: passing for qrg fields fix
|
||||
</fixed>
|
||||
<removed>
|
||||
- No more date display. Not needed. Saving space, only time is important
|
||||
</removed>
|
||||
</changeLog>
|
||||
<changeLog>
|
||||
<changedVersionNumber>1.31</changedVersionNumber>
|
||||
<date>2025-12-13</date>
|
||||
<description>Much changes and a hotfix</description>
|
||||
<added>
|
||||
- win-test support (not deactivable yet, beta)
|
||||
- pstRotator support (not deactivable yet, beta)
|
||||
- airScout fix (calculations depends to cpu load now)
|
||||
- QSO sniffer
|
||||
</added>
|
||||
<changed>
|
||||
- DNS from www.on4kst.info to www.on4kst.org (hotfix, now configurable in preferences)
|
||||
</changed>
|
||||
<fixed>
|
||||
- Endless loop in error case lets freeze the client
|
||||
- PSTRotator SOCKET Close on DISCONNECT BUTTON
|
||||
- Dark mode: passing for qrg fields fix
|
||||
</fixed>
|
||||
<removed></removed>
|
||||
</changeLog>
|
||||
<changeLog>
|
||||
<changedVersionNumber>1.266</changedVersionNumber>
|
||||
<date>2025-10-03</date>
|
||||
<description>Airscout interface did not work with multi-signons</description>
|
||||
<added>nothing</added>
|
||||
<changed>nothing</changed>
|
||||
<fixed>
|
||||
Fixed by using raw callsign (without -2 suffix etc.) when communicating with AirScout.
|
||||
Thank you very much for testing this, Kreso 9A2HM!
|
||||
</fixed>
|
||||
<removed></removed>
|
||||
</changeLog>
|
||||
<changeLog>
|
||||
<changedVersionNumber>1.265</changedVersionNumber>
|
||||
<date>2025-09-28</date>
|
||||
<description>Direction buttons stay now colored, if activated</description>
|
||||
<added>nothing</added>
|
||||
<changed>nothing</changed>
|
||||
<fixed>The buttons stay now colored, if activated</fixed>
|
||||
<removed></removed>
|
||||
</changeLog>
|
||||
<changeLog>
|
||||
<changedVersionNumber>1.264</changedVersionNumber>
|
||||
<date>2025-08-02</date>
|
||||
<description>Changed the callsign recognition pattern for Simplelogfile</description>
|
||||
<added>nothing</added>
|
||||
<changed>nothing</changed>
|
||||
<fixed>
|
||||
Callsigns like S53CC, S51A etc. are now correctly marked as worked in SimpleLogFile.txt.
|
||||
(bugreport Boris, S53CC)
|
||||
</fixed>
|
||||
<removed></removed>
|
||||
</changeLog>
|
||||
<changeLog>
|
||||
<changedVersionNumber>1.263</changedVersionNumber>
|
||||
<date>2025-06-08</date>
|
||||
<description>Airscout communication and Loginname</description>
|
||||
<added>nothing</added>
|
||||
<changed>
|
||||
Only entries with QRB lower than max-QRB are sent to AirScout in 60s intervals (was 12s for all).
|
||||
</changed>
|
||||
<fixed>
|
||||
- AS communication message count hugely reduced
|
||||
- Name in chat is now saveable
|
||||
- Fixed AS client name (was hard-wired to "KST")
|
||||
73 / DO5AMF
|
||||
</fixed>
|
||||
<removed></removed>
|
||||
</changeLog>
|
||||
<changeLog>
|
||||
<changedVersionNumber>1.262</changedVersionNumber>
|
||||
<date>2025-05-21</date>
|
||||
<description>Freezes caused by getting messages before user login should be fixed now</description>
|
||||
<added>nothing</added>
|
||||
<changed>nothing</changed>
|
||||
<fixed>
|
||||
ON4KST delivers messages of stations not yet logged in. That caused a freeze at the message
|
||||
processing engine which is now fixed.
|
||||
</fixed>
|
||||
<removed></removed>
|
||||
</changeLog>
|
||||
<changeLog>
|
||||
<changedVersionNumber>1.26</changedVersionNumber>
|
||||
<date>2025-05</date>
|
||||
<description>Login to multiple Channels via single signon / spend some colors</description>
|
||||
<added>
|
||||
1. UI: Dark mode. Switch in "Window -> use dark mode"
|
||||
2. Usage of two Chatcategories at the same time
|
||||
3. opposite station multi-callsign login-tagging
|
||||
73 / DO5AMF
|
||||
</added>
|
||||
<changed>
|
||||
- Coloring mechanic of the software. Modify colors via css by yourself...
|
||||
</changed>
|
||||
<fixed>
|
||||
- Station tagging fixed completely
|
||||
</fixed>
|
||||
<removed></removed>
|
||||
</changeLog>
|
||||
<changeLog>
|
||||
<changedVersionNumber>1.251</changedVersionNumber>
|
||||
<date>2025-02</date>
|
||||
<description>BUGFIX of 1.25, tnx Steve Clements!</description>
|
||||
<added>
|
||||
- Steve spotted a problem in udp broadcast spot info reading, its now fixed!
|
||||
73 / DO5AMF
|
||||
</added>
|
||||
<changed></changed>
|
||||
<fixed>- Station tagging</fixed>
|
||||
<removed></removed>
|
||||
</changeLog>
|
||||
<changeLog>
|
||||
<changedVersionNumber>1.25</changedVersionNumber>
|
||||
<date>2025-02</date>
|
||||
<description>Wishlist-time</description>
|
||||
<added>
|
||||
- New configuration Tab: Messagehandling (auto-answering, CQ qrg auto-answer)
|
||||
- Coloured lines: new pm rows appear in red, fade rainbow-like to white over 30s (tnx Gianluca)
|
||||
73 / DO5AMF
|
||||
</added>
|
||||
<changed></changed>
|
||||
<fixed>
|
||||
- Users with suffixes like "-2 and -70" are now correctly marked as worked
|
||||
</fixed>
|
||||
<removed></removed>
|
||||
</changeLog>
|
||||
<changeLog>
|
||||
<changedVersionNumber>1.24</changedVersionNumber>
|
||||
<date>2024-11</date>
|
||||
<description>Wishlist-time</description>
|
||||
<added>
|
||||
- Button to show qrz.com profile of a selected station
|
||||
- Button to show qrzcq.com profile of a selected station
|
||||
</added>
|
||||
<changed></changed>
|
||||
<fixed></fixed>
|
||||
<removed></removed>
|
||||
</changeLog>
|
||||
<changeLog>
|
||||
<changedVersionNumber>1.23</changedVersionNumber>
|
||||
<date>2024-10</date>
|
||||
<description>DXCluster Server is now implemented</description>
|
||||
<added>
|
||||
- DXCluster Server (tnx OMAAO): KST4Contest now provides a DXCluster server to feed
|
||||
your log client with station-reachable warnings.
|
||||
</added>
|
||||
<changed></changed>
|
||||
<fixed></fixed>
|
||||
<removed></removed>
|
||||
</changeLog>
|
||||
<changeLog>
|
||||
<changedVersionNumber>1.22</changedVersionNumber>
|
||||
<date>2024-05</date>
|
||||
<description>Increase usability, fixed AS button</description>
|
||||
<added>- Variables (tnx OMAAO): MYLOCATORSHORT, MYQRGSHORT, QRZNAME</added>
|
||||
<changed>- Sendtext-field focus on callsign click</changed>
|
||||
<fixed>
|
||||
- Worked-station-filter (tnx Gianluca): filter is now live
|
||||
- Chatters list sorting by QRB (tnx Alessandro): fixed to numeric sort
|
||||
- Airscout showpath button now maximizes AS and shows the path
|
||||
</fixed>
|
||||
<removed></removed>
|
||||
</changeLog>
|
||||
<changeLog>
|
||||
<changedVersionNumber>1.21</changedVersionNumber>
|
||||
<date>2024-04</date>
|
||||
<description>Increase usability</description>
|
||||
<added></added>
|
||||
<changed>
|
||||
- Window sizes and divider positions are now stored and restored on next startup
|
||||
- Filters section is now a flowpane for lower resolutions
|
||||
</changed>
|
||||
<fixed></fixed>
|
||||
<removed></removed>
|
||||
</changeLog>
|
||||
<changeLog>
|
||||
<changedVersionNumber>1.2</changedVersionNumber>
|
||||
<date>2024-04</date>
|
||||
<description>Increase usability</description>
|
||||
<added>
|
||||
- Selectable bands
|
||||
- Unworkable tags for each callsign per band
|
||||
- QTF-Arrow on the "show path in AS" button
|
||||
</added>
|
||||
<changed></changed>
|
||||
<fixed></fixed>
|
||||
<removed></removed>
|
||||
</changeLog>
|
||||
<changeLog>
|
||||
<changedVersionNumber>1.1</changedVersionNumber>
|
||||
<date>2024-03</date>
|
||||
<description>Increased workflow</description>
|
||||
<added>
|
||||
- Reachable warning function
|
||||
- Variables FIRSTAP and SECONDAP for predefined texts
|
||||
- Marking new connected stations (bold) and away-state (brackets)
|
||||
- Activity-timer since last message of a chatter
|
||||
</added>
|
||||
<changed>- QRB/QTF info in pm-window</changed>
|
||||
<fixed>
|
||||
- Behaviour of messagefilter-radiobutton corrected (tnx Gian)
|
||||
- Error sending macros on selected stations in pm window (tnx Gian)
|
||||
</fixed>
|
||||
<removed>- boot sound</removed>
|
||||
</changeLog>
|
||||
<changeLog>
|
||||
<changedVersionNumber>1.0</changedVersionNumber>
|
||||
<date>2024-02</date>
|
||||
<description>First stable release</description>
|
||||
<added>
|
||||
- audio notification support
|
||||
- update-available warning
|
||||
- changelog displaying
|
||||
- known-bugs displaying
|
||||
- message filters for selected station
|
||||
- stationlist-filters (show by qtf / textstring, hide by qrb, qtf, workedstate)
|
||||
- 10 macros (textsnippets available by hitting strg+numberkey)
|
||||
</added>
|
||||
<changed></changed>
|
||||
<fixed></fixed>
|
||||
<removed></removed>
|
||||
</changeLog>
|
||||
<changeLog>
|
||||
<changedVersionNumber>0.9</changedVersionNumber>
|
||||
<date>2024-02</date>
|
||||
<description>Now the chat is disconnectable without closing it</description>
|
||||
<added></added>
|
||||
<changed></changed>
|
||||
<fixed>disconnect-button, Textsnippets in CQ Window, "/CQ" at click on own sendtexts,
|
||||
setname "qrg" works now</fixed>
|
||||
<removed></removed>
|
||||
</changeLog>
|
||||
<changeLog>
|
||||
<changedVersionNumber>0.8</changedVersionNumber>
|
||||
<date>2023-10</date>
|
||||
<description>savesettings function and disconnect function added. MYCALL-Highlighting fixed</description>
|
||||
<added>savesettings function, disconnect function</added>
|
||||
<changed></changed>
|
||||
<fixed>MYCALL-Highlighting</fixed>
|
||||
<removed></removed>
|
||||
</changeLog>
|
||||
<changeLog>
|
||||
<changedVersionNumber>0.7</changedVersionNumber>
|
||||
<date>2023-07</date>
|
||||
<description>Internal database for worked stations, shortcut and textsnippet editor</description>
|
||||
<added>database, shortcut-editor, textsnipped-editor</added>
|
||||
<changed></changed>
|
||||
<fixed></fixed>
|
||||
<removed></removed>
|
||||
</changeLog>
|
||||
<changeLog>
|
||||
<changedVersionNumber>0.6</changedVersionNumber>
|
||||
<date>2023-07</date>
|
||||
<description>Added periodically sent beacon function</description>
|
||||
<added>beacon to public channel</added>
|
||||
<changed></changed>
|
||||
<fixed></fixed>
|
||||
<removed></removed>
|
||||
</changeLog>
|
||||
<changeLog>
|
||||
<changedVersionNumber>0.5</changedVersionNumber>
|
||||
<date>2023-02</date>
|
||||
<description>Airscout interface, UDP interfaces for worked-station data and TRX info, shortcuts</description>
|
||||
<added>Airscout-interface, packetreceiver for ucxlog, n1mm, qartest</added>
|
||||
<changed></changed>
|
||||
<fixed></fixed>
|
||||
<removed></removed>
|
||||
</changeLog>
|
||||
<changeLog>
|
||||
<changedVersionNumber>0.4</changedVersionNumber>
|
||||
<date>2022-12</date>
|
||||
<description>Worked info by logfile via universal pattern matching logfile interpreter</description>
|
||||
<added>simple-logfile</added>
|
||||
<changed></changed>
|
||||
<fixed></fixed>
|
||||
<removed></removed>
|
||||
</changeLog>
|
||||
<changeLog>
|
||||
<changedVersionNumber>0.3</changedVersionNumber>
|
||||
<date>2022-10</date>
|
||||
<description>Added a GUI, chat was text based only</description>
|
||||
<added>Graphical user interface</added>
|
||||
<changed></changed>
|
||||
<fixed></fixed>
|
||||
<removed></removed>
|
||||
</changeLog>
|
||||
<changeLog>
|
||||
<changedVersionNumber>0.2</changedVersionNumber>
|
||||
<date>2022-10</date>
|
||||
<description>Pattern matching system for frequency information, frequency collection per chatmember</description>
|
||||
<added>frequency-extraction for every callsign</added>
|
||||
<changed></changed>
|
||||
<fixed></fixed>
|
||||
<removed></removed>
|
||||
</changeLog>
|
||||
<changeLog>
|
||||
<changedVersionNumber>0.15</changedVersionNumber>
|
||||
<date>2022-09</date>
|
||||
<description>Researched the Chatprotocol and developed the base communication with the chatserver</description>
|
||||
<added></added>
|
||||
<changed></changed>
|
||||
<fixed></fixed>
|
||||
<removed></removed>
|
||||
</changeLog>
|
||||
<changeLog>
|
||||
<changedVersionNumber>0.1</changedVersionNumber>
|
||||
<date>2022-09</date>
|
||||
<description>Primary architecture of the chat, message bus system for handling the message queues</description>
|
||||
<added></added>
|
||||
<changed></changed>
|
||||
<fixed></fixed>
|
||||
<removed></removed>
|
||||
</changeLog>
|
||||
</praktiKST>
|
||||
@@ -17,7 +17,7 @@
|
||||
<meta name="twitter:title" content="Legal Notice | KST4Contest">
|
||||
<meta name="twitter:description" content="Legal notice for the KST4Contest website.">
|
||||
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783552468141">
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||
|
||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
|
||||
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
|
||||
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783552468141">
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||
|
||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
|
||||
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
|
||||
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783552468141">
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||
|
||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
|
||||
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
|
||||
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783552468141">
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||
|
||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
|
||||
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
|
||||
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783552468141">
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||
|
||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
|
||||
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
|
||||
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783552468141">
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||
|
||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
|
||||
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
|
||||
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783552468141">
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||
|
||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<meta name="twitter:title" content="KST4Contest Deutsches Handbuch">
|
||||
<meta name="twitter:description" content="Deutsches Benutzerhandbuch für KST4Contest.">
|
||||
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783552468141">
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||
|
||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
|
||||
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
|
||||
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783552468141">
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||
|
||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
|
||||
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
|
||||
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783552468141">
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||
|
||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
|
||||
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
|
||||
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783552468141">
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||
|
||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
|
||||
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
|
||||
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783552468141">
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||
|
||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
|
||||
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
|
||||
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783552468141">
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||
|
||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
|
||||
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
|
||||
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783552468141">
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||
|
||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
|
||||
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
|
||||
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783552468141">
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||
|
||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
|
||||
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
|
||||
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783552468141">
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||
|
||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
|
||||
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
|
||||
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783552468141">
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||
|
||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
|
||||
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
|
||||
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783552468141">
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||
|
||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<meta name="twitter:title" content="KST4Contest English Manual">
|
||||
<meta name="twitter:description" content="English user manual for KST4Contest.">
|
||||
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783552468141">
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||
|
||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
|
||||
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
|
||||
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783552468141">
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||
|
||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
|
||||
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
|
||||
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783552468141">
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||
|
||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
|
||||
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
|
||||
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783552468141">
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||
|
||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
|
||||
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
|
||||
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783552468141">
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||
|
||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<meta name="twitter:title" content="KST4Contest Manual">
|
||||
<meta name="twitter:description" content="Online manual for KST4Contest, the contest-optimized ON4KST chat client.">
|
||||
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783552468141">
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||
|
||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<meta name="twitter:title" content="Hotfix Version 1.41.1">
|
||||
<meta name="twitter:description" content="Contest-optimized ON4KST chat client for VHF, UHF and SHF ham radio contests.">
|
||||
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783552468141">
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||
|
||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<meta name="twitter:title" content="KST4Contest Website Launch">
|
||||
<meta name="twitter:description" content="Contest-optimized ON4KST chat client for VHF, UHF and SHF ham radio contests.">
|
||||
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783552468141">
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||
|
||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<meta name="twitter:title" content="KST4Contest News">
|
||||
<meta name="twitter:description" content="News, release updates and development notes for KST4Contest.">
|
||||
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783552468141">
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||
|
||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<meta name="twitter:title" content="Privacy Policy | KST4Contest">
|
||||
<meta name="twitter:description" content="Privacy policy for the KST4Contest website.">
|
||||
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783552468141">
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||
|
||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<meta name="twitter:title" content="KST4Contest Roadmap">
|
||||
<meta name="twitter:description" content="Planned enhancements for upcoming KST4Contest versions, generated from GitHub milestones and issues.">
|
||||
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783552468141">
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||
|
||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||
@@ -89,8 +89,8 @@
|
||||
<a href="https://github.com/praktimarc/kst4contest/issues/52">[FEATURE] center horizontal slider</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="https://github.com/praktimarc/kst4contest/issues/49">[FEATURE] QTF degree, manual change</a>
|
||||
<li class="roadmap-done">
|
||||
<a href="https://github.com/praktimarc/kst4contest/issues/49">[FEATURE] QTF degree, manual change</a> ✓
|
||||
</li>
|
||||
|
||||
<li class="roadmap-done">
|
||||
@@ -148,6 +148,10 @@
|
||||
|
||||
<ul>
|
||||
|
||||
<li>
|
||||
<a href="https://github.com/praktimarc/kst4contest/issues/59">[FEATURE] window control</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a href="https://github.com/praktimarc/kst4contest/issues/55">[FEATURE] Reset-Button for the map</a>
|
||||
</li>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<meta name="twitter:title" content="KST4Contest Screenshots">
|
||||
<meta name="twitter:description" content="Screenshots of KST4Contest, the contest-optimized ON4KST chat client for VHF, UHF and SHF operators.">
|
||||
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783552468141">
|
||||
<link rel="stylesheet" href="/assets/css/main.css?v=1783632276803">
|
||||
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
|
||||
|
||||
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
|
||||
|
||||
+22
-22
@@ -10,90 +10,90 @@
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://kst4contest.hamradioonline.de/about/</loc>
|
||||
<lastmod>2026-07-08</lastmod>
|
||||
<lastmod>2026-07-09</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://kst4contest.hamradioonline.de/contact/</loc>
|
||||
<lastmod>2026-07-08</lastmod>
|
||||
<lastmod>2026-07-09</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://kst4contest.hamradioonline.de/download/</loc>
|
||||
<lastmod>2026-07-08</lastmod>
|
||||
<lastmod>2026-07-09</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://kst4contest.hamradioonline.de/faq/</loc>
|
||||
<lastmod>2026-07-08</lastmod>
|
||||
<lastmod>2026-07-09</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://kst4contest.hamradioonline.de/features/airscout/</loc>
|
||||
<lastmod>2026-07-08</lastmod>
|
||||
<lastmod>2026-07-09</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://kst4contest.hamradioonline.de/features/dual-chat/</loc>
|
||||
<lastmod>2026-07-08</lastmod>
|
||||
<lastmod>2026-07-09</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://kst4contest.hamradioonline.de/features/dx-cluster/</loc>
|
||||
<lastmod>2026-07-08</lastmod>
|
||||
<lastmod>2026-07-09</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://kst4contest.hamradioonline.de/features/</loc>
|
||||
<lastmod>2026-07-08</lastmod>
|
||||
<lastmod>2026-07-09</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://kst4contest.hamradioonline.de/features/log-sync/</loc>
|
||||
<lastmod>2026-07-08</lastmod>
|
||||
<lastmod>2026-07-09</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://kst4contest.hamradioonline.de/features/macros/</loc>
|
||||
<lastmod>2026-07-08</lastmod>
|
||||
<lastmod>2026-07-09</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://kst4contest.hamradioonline.de/features/priority-score/</loc>
|
||||
<lastmod>2026-07-08</lastmod>
|
||||
<lastmod>2026-07-09</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://kst4contest.hamradioonline.de/features/sked-reminder/</loc>
|
||||
<lastmod>2026-07-08</lastmod>
|
||||
<lastmod>2026-07-09</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://kst4contest.hamradioonline.de/features/timeline/</loc>
|
||||
<lastmod>2026-07-08</lastmod>
|
||||
<lastmod>2026-07-09</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://kst4contest.hamradioonline.de/</loc>
|
||||
<lastmod>2026-07-08</lastmod>
|
||||
<lastmod>2026-07-09</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://kst4contest.hamradioonline.de/legal-notice/</loc>
|
||||
<lastmod>2026-07-08</lastmod>
|
||||
<lastmod>2026-07-09</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://kst4contest.hamradioonline.de/manual/de/airscout-integration/</loc>
|
||||
<lastmod>2026-07-08</lastmod>
|
||||
<lastmod>2026-07-09</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://kst4contest.hamradioonline.de/manual/de/</loc>
|
||||
<lastmod>2026-07-08</lastmod>
|
||||
<lastmod>2026-07-09</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://kst4contest.hamradioonline.de/manual/en/</loc>
|
||||
<lastmod>2026-07-08</lastmod>
|
||||
<lastmod>2026-07-09</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://kst4contest.hamradioonline.de/manual/</loc>
|
||||
<lastmod>2026-07-08</lastmod>
|
||||
<lastmod>2026-07-09</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://kst4contest.hamradioonline.de/privacy/</loc>
|
||||
<lastmod>2026-07-08</lastmod>
|
||||
<lastmod>2026-07-09</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://kst4contest.hamradioonline.de/roadmap/</loc>
|
||||
<lastmod>2026-07-08</lastmod>
|
||||
<lastmod>2026-07-09</lastmod>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://kst4contest.hamradioonline.de/screenshots/</loc>
|
||||
<lastmod>2026-07-08</lastmod>
|
||||
<lastmod>2026-07-09</lastmod>
|
||||
</url>
|
||||
</urlset>
|
||||
@@ -6,5 +6,7 @@ module.exports = [
|
||||
{ title: "News", url: "/news/" },
|
||||
{ title: "Roadmap", url: "/roadmap/" },
|
||||
{ title: "About", url: "/about/" },
|
||||
{ title: "FAQ", url: "/faq/" }
|
||||
{ title: "FAQ", url: "/faq/" },
|
||||
{ title: "Support", url: "/support/"
|
||||
}
|
||||
];
|
||||
@@ -0,0 +1,382 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Historical changelog entries for releases not published as GitHub Releases.
|
||||
The CI pipeline prepends new entries from GitHub Releases on top of these. -->
|
||||
<history>
|
||||
<changeLog>
|
||||
<changedVersionNumber>1.41</changedVersionNumber>
|
||||
<date>2026-07-01</date>
|
||||
<description>Map view and path calculations</description>
|
||||
<added>
|
||||
Band and Frequency Detection per Chatmember, Band alert, Win-Test SKED Push via UDP,
|
||||
Win-Test Broadcast Defaults and SKed Workflow, Chat History at Startup, Station Map / Map View
|
||||
Local Leaflet /-TileProxy Backend, Maidenhead /-Grid Overlay, Path calculation, path profile,
|
||||
Reachability filters, NAC filters (grid sqaure marking)
|
||||
</added>
|
||||
<changed>
|
||||
button housing, it works now on smaller screens / plus: detecting maximum screen size
|
||||
</changed>
|
||||
<fixed>
|
||||
9A2HM reported that 23cm qsos with N1MM are not marked correctly, ist fixed
|
||||
</fixed>
|
||||
<removed></removed>
|
||||
</changeLog>
|
||||
<changeLog>
|
||||
<changedVersionNumber>1.40</changedVersionNumber>
|
||||
<date>2026-02-16</date>
|
||||
<description>Much changes and fixes</description>
|
||||
<added>
|
||||
- win-test support (not deactivable yet, beta)
|
||||
- QSO sniffer which filters changable callsigns lists messages to your pm window
|
||||
- Chatmember Score system. Each chatmember gets a score depending to antenna direction, activity time,
|
||||
activity msgcount, positive signals, active bands, active frequencies, his sked direction (degrees),
|
||||
self handed rate, sked-made-rate and some others
|
||||
- Chatmember Band and frequency recognition: now we list all active bands of each other chatmember
|
||||
- Band alert for logged stations
|
||||
- Sked reminder ALERT with automatic messages in 2+1 / 5+2+1 / 10+5+2+1 minutes
|
||||
- PSTRotator interface
|
||||
- AirPlane Timeline
|
||||
- Skedfail button in furtherinfo panel
|
||||
- Wintest logsynch interface can now be activated and deactivated in the preferences
|
||||
- load chat history on startup
|
||||
</added>
|
||||
<changed>
|
||||
- added save option: ServerDNS / Port
|
||||
- added save option: activate/deactivate pstRotator interface
|
||||
- added save option: activate/deactivate wintest interface
|
||||
- added save option: callsign sniffer
|
||||
- added save option: boolean GUI_darkModeActiveByDefault = false
|
||||
- added AP notes at the internal DX cluster spots
|
||||
- Generic Autoanswer fires maximum once in 45 seconds per rx callsign
|
||||
</changed>
|
||||
<fixed>
|
||||
- Userlist sort automatically on new member signon
|
||||
- Posonpill-messages now kills only exactly one Client instance (bugreport 9A2HM)
|
||||
- Crash of wtkst by disconnection of kst4Contest
|
||||
- wtKST crashes when kst4contest is running (many reporters)
|
||||
- wintest: logged per band excludes pre worked band (bugreport SM6VTZ)
|
||||
- PSTRotator SOCKET Close on DISCONNECT BUTTON works now
|
||||
- AirScout Map had not been updated (bugreport SM0RJV)
|
||||
- QTFDefault had been not saved correctly (bugreport SM0RJV)
|
||||
- Show path in AS button worked only with fixed server Strings (bugreport 9A2HM)
|
||||
- Version number had been displayed wrong
|
||||
- suffixes like /p, -2, etc. problem now fixed everywhere
|
||||
- Dark mode: passing for qrg fields fix
|
||||
</fixed>
|
||||
<removed>
|
||||
- No more date display. Not needed. Saving space, only time is important
|
||||
</removed>
|
||||
</changeLog>
|
||||
<changeLog>
|
||||
<changedVersionNumber>1.31</changedVersionNumber>
|
||||
<date>2025-12-13</date>
|
||||
<description>Much changes and a hotfix</description>
|
||||
<added>
|
||||
- win-test support (not deactivable yet, beta)
|
||||
- pstRotator support (not deactivable yet, beta)
|
||||
- airScout fix (calculations depends to cpu load now)
|
||||
- QSO sniffer
|
||||
</added>
|
||||
<changed>
|
||||
- DNS from www.on4kst.info to www.on4kst.org (hotfix, now configurable in preferences)
|
||||
</changed>
|
||||
<fixed>
|
||||
- Endless loop in error case lets freeze the client
|
||||
- PSTRotator SOCKET Close on DISCONNECT BUTTON
|
||||
- Dark mode: passing for qrg fields fix
|
||||
</fixed>
|
||||
<removed></removed>
|
||||
</changeLog>
|
||||
<changeLog>
|
||||
<changedVersionNumber>1.266</changedVersionNumber>
|
||||
<date>2025-10-03</date>
|
||||
<description>Airscout interface did not work with multi-signons</description>
|
||||
<added>nothing</added>
|
||||
<changed>nothing</changed>
|
||||
<fixed>
|
||||
Fixed by using raw callsign (without -2 suffix etc.) when communicating with AirScout.
|
||||
Thank you very much for testing this, Kreso 9A2HM!
|
||||
</fixed>
|
||||
<removed></removed>
|
||||
</changeLog>
|
||||
<changeLog>
|
||||
<changedVersionNumber>1.265</changedVersionNumber>
|
||||
<date>2025-09-28</date>
|
||||
<description>Direction buttons stay now colored, if activated</description>
|
||||
<added>nothing</added>
|
||||
<changed>nothing</changed>
|
||||
<fixed>The buttons stay now colored, if activated</fixed>
|
||||
<removed></removed>
|
||||
</changeLog>
|
||||
<changeLog>
|
||||
<changedVersionNumber>1.264</changedVersionNumber>
|
||||
<date>2025-08-02</date>
|
||||
<description>Changed the callsign recognition pattern for Simplelogfile</description>
|
||||
<added>nothing</added>
|
||||
<changed>nothing</changed>
|
||||
<fixed>
|
||||
Callsigns like S53CC, S51A etc. are now correctly marked as worked in SimpleLogFile.txt.
|
||||
(bugreport Boris, S53CC)
|
||||
</fixed>
|
||||
<removed></removed>
|
||||
</changeLog>
|
||||
<changeLog>
|
||||
<changedVersionNumber>1.263</changedVersionNumber>
|
||||
<date>2025-06-08</date>
|
||||
<description>Airscout communication and Loginname</description>
|
||||
<added>nothing</added>
|
||||
<changed>
|
||||
Only entries with QRB lower than max-QRB are sent to AirScout in 60s intervals (was 12s for all).
|
||||
</changed>
|
||||
<fixed>
|
||||
- AS communication message count hugely reduced
|
||||
- Name in chat is now saveable
|
||||
- Fixed AS client name (was hard-wired to "KST")
|
||||
73 / DO5AMF
|
||||
</fixed>
|
||||
<removed></removed>
|
||||
</changeLog>
|
||||
<changeLog>
|
||||
<changedVersionNumber>1.262</changedVersionNumber>
|
||||
<date>2025-05-21</date>
|
||||
<description>Freezes caused by getting messages before user login should be fixed now</description>
|
||||
<added>nothing</added>
|
||||
<changed>nothing</changed>
|
||||
<fixed>
|
||||
ON4KST delivers messages of stations not yet logged in. That caused a freeze at the message
|
||||
processing engine which is now fixed.
|
||||
</fixed>
|
||||
<removed></removed>
|
||||
</changeLog>
|
||||
<changeLog>
|
||||
<changedVersionNumber>1.26</changedVersionNumber>
|
||||
<date>2025-05</date>
|
||||
<description>Login to multiple Channels via single signon / spend some colors</description>
|
||||
<added>
|
||||
1. UI: Dark mode. Switch in "Window -> use dark mode"
|
||||
2. Usage of two Chatcategories at the same time
|
||||
3. opposite station multi-callsign login-tagging
|
||||
73 / DO5AMF
|
||||
</added>
|
||||
<changed>
|
||||
- Coloring mechanic of the software. Modify colors via css by yourself...
|
||||
</changed>
|
||||
<fixed>
|
||||
- Station tagging fixed completely
|
||||
</fixed>
|
||||
<removed></removed>
|
||||
</changeLog>
|
||||
<changeLog>
|
||||
<changedVersionNumber>1.251</changedVersionNumber>
|
||||
<date>2025-02</date>
|
||||
<description>BUGFIX of 1.25, tnx Steve Clements!</description>
|
||||
<added>
|
||||
- Steve spotted a problem in udp broadcast spot info reading, its now fixed!
|
||||
73 / DO5AMF
|
||||
</added>
|
||||
<changed></changed>
|
||||
<fixed>- Station tagging</fixed>
|
||||
<removed></removed>
|
||||
</changeLog>
|
||||
<changeLog>
|
||||
<changedVersionNumber>1.25</changedVersionNumber>
|
||||
<date>2025-02</date>
|
||||
<description>Wishlist-time</description>
|
||||
<added>
|
||||
- New configuration Tab: Messagehandling (auto-answering, CQ qrg auto-answer)
|
||||
- Coloured lines: new pm rows appear in red, fade rainbow-like to white over 30s (tnx Gianluca)
|
||||
73 / DO5AMF
|
||||
</added>
|
||||
<changed></changed>
|
||||
<fixed>
|
||||
- Users with suffixes like "-2 and -70" are now correctly marked as worked
|
||||
</fixed>
|
||||
<removed></removed>
|
||||
</changeLog>
|
||||
<changeLog>
|
||||
<changedVersionNumber>1.24</changedVersionNumber>
|
||||
<date>2024-11</date>
|
||||
<description>Wishlist-time</description>
|
||||
<added>
|
||||
- Button to show qrz.com profile of a selected station
|
||||
- Button to show qrzcq.com profile of a selected station
|
||||
</added>
|
||||
<changed></changed>
|
||||
<fixed></fixed>
|
||||
<removed></removed>
|
||||
</changeLog>
|
||||
<changeLog>
|
||||
<changedVersionNumber>1.23</changedVersionNumber>
|
||||
<date>2024-10</date>
|
||||
<description>DXCluster Server is now implemented</description>
|
||||
<added>
|
||||
- DXCluster Server (tnx OMAAO): KST4Contest now provides a DXCluster server to feed
|
||||
your log client with station-reachable warnings.
|
||||
</added>
|
||||
<changed></changed>
|
||||
<fixed></fixed>
|
||||
<removed></removed>
|
||||
</changeLog>
|
||||
<changeLog>
|
||||
<changedVersionNumber>1.22</changedVersionNumber>
|
||||
<date>2024-05</date>
|
||||
<description>Increase usability, fixed AS button</description>
|
||||
<added>- Variables (tnx OMAAO): MYLOCATORSHORT, MYQRGSHORT, QRZNAME</added>
|
||||
<changed>- Sendtext-field focus on callsign click</changed>
|
||||
<fixed>
|
||||
- Worked-station-filter (tnx Gianluca): filter is now live
|
||||
- Chatters list sorting by QRB (tnx Alessandro): fixed to numeric sort
|
||||
- Airscout showpath button now maximizes AS and shows the path
|
||||
</fixed>
|
||||
<removed></removed>
|
||||
</changeLog>
|
||||
<changeLog>
|
||||
<changedVersionNumber>1.21</changedVersionNumber>
|
||||
<date>2024-04</date>
|
||||
<description>Increase usability</description>
|
||||
<added></added>
|
||||
<changed>
|
||||
- Window sizes and divider positions are now stored and restored on next startup
|
||||
- Filters section is now a flowpane for lower resolutions
|
||||
</changed>
|
||||
<fixed></fixed>
|
||||
<removed></removed>
|
||||
</changeLog>
|
||||
<changeLog>
|
||||
<changedVersionNumber>1.2</changedVersionNumber>
|
||||
<date>2024-04</date>
|
||||
<description>Increase usability</description>
|
||||
<added>
|
||||
- Selectable bands
|
||||
- Unworkable tags for each callsign per band
|
||||
- QTF-Arrow on the "show path in AS" button
|
||||
</added>
|
||||
<changed></changed>
|
||||
<fixed></fixed>
|
||||
<removed></removed>
|
||||
</changeLog>
|
||||
<changeLog>
|
||||
<changedVersionNumber>1.1</changedVersionNumber>
|
||||
<date>2024-03</date>
|
||||
<description>Increased workflow</description>
|
||||
<added>
|
||||
- Reachable warning function
|
||||
- Variables FIRSTAP and SECONDAP for predefined texts
|
||||
- Marking new connected stations (bold) and away-state (brackets)
|
||||
- Activity-timer since last message of a chatter
|
||||
</added>
|
||||
<changed>- QRB/QTF info in pm-window</changed>
|
||||
<fixed>
|
||||
- Behaviour of messagefilter-radiobutton corrected (tnx Gian)
|
||||
- Error sending macros on selected stations in pm window (tnx Gian)
|
||||
</fixed>
|
||||
<removed>- boot sound</removed>
|
||||
</changeLog>
|
||||
<changeLog>
|
||||
<changedVersionNumber>1.0</changedVersionNumber>
|
||||
<date>2024-02</date>
|
||||
<description>First stable release</description>
|
||||
<added>
|
||||
- audio notification support
|
||||
- update-available warning
|
||||
- changelog displaying
|
||||
- known-bugs displaying
|
||||
- message filters for selected station
|
||||
- stationlist-filters (show by qtf / textstring, hide by qrb, qtf, workedstate)
|
||||
- 10 macros (textsnippets available by hitting strg+numberkey)
|
||||
</added>
|
||||
<changed></changed>
|
||||
<fixed></fixed>
|
||||
<removed></removed>
|
||||
</changeLog>
|
||||
<changeLog>
|
||||
<changedVersionNumber>0.9</changedVersionNumber>
|
||||
<date>2024-02</date>
|
||||
<description>Now the chat is disconnectable without closing it</description>
|
||||
<added></added>
|
||||
<changed></changed>
|
||||
<fixed>disconnect-button, Textsnippets in CQ Window, "/CQ" at click on own sendtexts,
|
||||
setname "qrg" works now</fixed>
|
||||
<removed></removed>
|
||||
</changeLog>
|
||||
<changeLog>
|
||||
<changedVersionNumber>0.8</changedVersionNumber>
|
||||
<date>2023-10</date>
|
||||
<description>savesettings function and disconnect function added. MYCALL-Highlighting fixed</description>
|
||||
<added>savesettings function, disconnect function</added>
|
||||
<changed></changed>
|
||||
<fixed>MYCALL-Highlighting</fixed>
|
||||
<removed></removed>
|
||||
</changeLog>
|
||||
<changeLog>
|
||||
<changedVersionNumber>0.7</changedVersionNumber>
|
||||
<date>2023-07</date>
|
||||
<description>Internal database for worked stations, shortcut and textsnippet editor</description>
|
||||
<added>database, shortcut-editor, textsnipped-editor</added>
|
||||
<changed></changed>
|
||||
<fixed></fixed>
|
||||
<removed></removed>
|
||||
</changeLog>
|
||||
<changeLog>
|
||||
<changedVersionNumber>0.6</changedVersionNumber>
|
||||
<date>2023-07</date>
|
||||
<description>Added periodically sent beacon function</description>
|
||||
<added>beacon to public channel</added>
|
||||
<changed></changed>
|
||||
<fixed></fixed>
|
||||
<removed></removed>
|
||||
</changeLog>
|
||||
<changeLog>
|
||||
<changedVersionNumber>0.5</changedVersionNumber>
|
||||
<date>2023-02</date>
|
||||
<description>Airscout interface, UDP interfaces for worked-station data and TRX info, shortcuts</description>
|
||||
<added>Airscout-interface, packetreceiver for ucxlog, n1mm, qartest</added>
|
||||
<changed></changed>
|
||||
<fixed></fixed>
|
||||
<removed></removed>
|
||||
</changeLog>
|
||||
<changeLog>
|
||||
<changedVersionNumber>0.4</changedVersionNumber>
|
||||
<date>2022-12</date>
|
||||
<description>Worked info by logfile via universal pattern matching logfile interpreter</description>
|
||||
<added>simple-logfile</added>
|
||||
<changed></changed>
|
||||
<fixed></fixed>
|
||||
<removed></removed>
|
||||
</changeLog>
|
||||
<changeLog>
|
||||
<changedVersionNumber>0.3</changedVersionNumber>
|
||||
<date>2022-10</date>
|
||||
<description>Added a GUI, chat was text based only</description>
|
||||
<added>Graphical user interface</added>
|
||||
<changed></changed>
|
||||
<fixed></fixed>
|
||||
<removed></removed>
|
||||
</changeLog>
|
||||
<changeLog>
|
||||
<changedVersionNumber>0.2</changedVersionNumber>
|
||||
<date>2022-10</date>
|
||||
<description>Pattern matching system for frequency information, frequency collection per chatmember</description>
|
||||
<added>frequency-extraction for every callsign</added>
|
||||
<changed></changed>
|
||||
<fixed></fixed>
|
||||
<removed></removed>
|
||||
</changeLog>
|
||||
<changeLog>
|
||||
<changedVersionNumber>0.15</changedVersionNumber>
|
||||
<date>2022-09</date>
|
||||
<description>Researched the Chatprotocol and developed the base communication with the chatserver</description>
|
||||
<added></added>
|
||||
<changed></changed>
|
||||
<fixed></fixed>
|
||||
<removed></removed>
|
||||
</changeLog>
|
||||
<changeLog>
|
||||
<changedVersionNumber>0.1</changedVersionNumber>
|
||||
<date>2022-09</date>
|
||||
<description>Primary architecture of the chat, message bus system for handling the message queues</description>
|
||||
<added></added>
|
||||
<changed></changed>
|
||||
<fixed></fixed>
|
||||
<removed></removed>
|
||||
</changeLog>
|
||||
</history>
|
||||
@@ -0,0 +1,131 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Static roadmap/bugsReported sections carried over from the previously
|
||||
hand-maintained kst4ContestVersionInfo.xml on do5amf.funkerportal.de.
|
||||
UpdateChecker.java does not parse these today; kept for parity/history. -->
|
||||
<legacy>
|
||||
<roadmap>
|
||||
<featurerequest>
|
||||
<featureDescription>Cyclic update of own name in chat to actual QRG</featureDescription>
|
||||
<requestDate>2024-02</requestDate>
|
||||
<state>open</state>
|
||||
</featurerequest>
|
||||
<featurerequest>
|
||||
<featureDescription>Acoustic warnings</featureDescription>
|
||||
<requestDate>2023-12</requestDate>
|
||||
<state>added</state>
|
||||
</featurerequest>
|
||||
<featurerequest>
|
||||
<featureDescription>Airscout support</featureDescription>
|
||||
<requestDate>2023-03</requestDate>
|
||||
<state>added</state>
|
||||
</featurerequest>
|
||||
<featurerequest>
|
||||
<featureDescription>cyclic beacon messages in public chat</featureDescription>
|
||||
<requestDate>2023-03</requestDate>
|
||||
<state>added</state>
|
||||
</featurerequest>
|
||||
</roadmap>
|
||||
<bugsReported>
|
||||
<bug>
|
||||
<bugdescription>
|
||||
stations like ol7c-7 and ol7c-2 will not be marked as worked if only "ol7c"
|
||||
is in the log. Need to interprete that. May ignore suffix starting with "-"</bugdescription>
|
||||
<state>solved</state>
|
||||
</bug>
|
||||
<bug>
|
||||
<bugdescription>
|
||||
worked callsigns table is updated only once after connected to chat
|
||||
Should be available directly after program started.
|
||||
But Reset works all time</bugdescription>
|
||||
<state>open</state>
|
||||
</bug>
|
||||
<bug>
|
||||
<bugdescription>feature request: language-specific strings, maybe based to an xml file
|
||||
</bugdescription>
|
||||
<state>open</state>
|
||||
</bug>
|
||||
<bug>
|
||||
<bugdescription>feature request: Macros
|
||||
</bugdescription>
|
||||
<state>added</state>
|
||||
</bug>
|
||||
<bug>
|
||||
<bugdescription>feature request:
|
||||
Automatic elimination of calls already connected from the log
|
||||
</bugdescription>
|
||||
<state>added</state>
|
||||
</bug>
|
||||
<bug>
|
||||
<bugdescription>feature request:
|
||||
Limitation of users beyond a certain QRB
|
||||
</bugdescription>
|
||||
<state>added</state>
|
||||
</bug>
|
||||
<bug>
|
||||
<bugdescription>feature request:
|
||||
User division for QTF perhaps settable based on the antenna lobe
|
||||
</bugdescription>
|
||||
<state>added</state>
|
||||
</bug>
|
||||
<bug>
|
||||
<bugdescription>feature request:
|
||||
Notification of new incoming users</bugdescription>
|
||||
<state>added</state>
|
||||
</bug>
|
||||
<bug>
|
||||
<bugdescription>feature request:
|
||||
Evidence of a call which was written but not responded to.
|
||||
</bugdescription>
|
||||
<state>added</state>
|
||||
</bug>
|
||||
<bug>
|
||||
<bugdescription>feature request:
|
||||
direction based reachable-warnings:
|
||||
if the chatclient detects a realised qso between two stations which both must have
|
||||
directed their beams to my station, there should be an instant warning to me for
|
||||
working at least one of the stations at the detected qrg</bugdescription>
|
||||
<state>open</state>
|
||||
</bug>
|
||||
<bug>
|
||||
<bugdescription>feature request:
|
||||
implement ASShowpath message to airacout;
|
||||
by doubleclicking a callsign in the chatmember list, the path and plane state should be
|
||||
provided by airscout </bugdescription>
|
||||
<state>open</state>
|
||||
</bug>
|
||||
<bug>
|
||||
<bugdescription>feature request:
|
||||
make chatlines copyable to the clipboard...</bugdescription>
|
||||
<state>open</state>
|
||||
</bug>
|
||||
<bug>
|
||||
<bugdescription>feature request:
|
||||
adding an activity detection for sensing if another chatmembers alive-state</bugdescription>
|
||||
<state>added</state>
|
||||
</bug>
|
||||
<bug>
|
||||
<bugdescription>On sql error close the connection to make a clean exit</bugdescription>
|
||||
<state>open</state>
|
||||
</bug>
|
||||
<bug>
|
||||
<bugdescription>SETNAME /MYQRG in options menu doesnt work</bugdescription>
|
||||
<state>solved</state>
|
||||
</bug>
|
||||
<bug>
|
||||
<bugdescription>Disconnect button does not work (DL5ZK)</bugdescription>
|
||||
<state>solved</state>
|
||||
</bug>
|
||||
<bug>
|
||||
<bugdescription>Password-wrong error not displayed (DO5SA)</bugdescription>
|
||||
<state>solved</state>
|
||||
</bug>
|
||||
<bug>
|
||||
<bugdescription>/CQ isnt written on own PMs</bugdescription>
|
||||
<state>solved</state>
|
||||
</bug>
|
||||
<bug>
|
||||
<bugdescription>rightclick-textsnippets arent available in cq-table</bugdescription>
|
||||
<state>solved</state>
|
||||
</bug>
|
||||
</bugsReported>
|
||||
</legacy>
|
||||
@@ -0,0 +1,183 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const REPO = "praktimarc/kst4contest";
|
||||
const API = `https://api.github.com/repos/${REPO}`;
|
||||
const LABEL_MAP = { enhancement: "added", bug: "fixed" };
|
||||
|
||||
async function githubGet(urlPath) {
|
||||
const headers = { Accept: "application/vnd.github+json" };
|
||||
if (process.env.GITHUB_TOKEN) {
|
||||
headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
|
||||
}
|
||||
const res = await fetch(`${API}${urlPath}`, { headers });
|
||||
if (!res.ok) {
|
||||
throw new Error(`GitHub API ${urlPath} failed: ${res.status}`);
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
// UpdateChecker.java parses <versionNumber> with Double.parseDouble() and
|
||||
// compares it against ApplicationConstants.APPLICATION_CURRENTVERSIONNUMBER,
|
||||
// which encodes patch releases by appending the patch digit(s) instead of a
|
||||
// third dot (e.g. tag v1.41.1 -> app version 1.411, v1.41.0 -> 1.41). See
|
||||
// commit 38ef50c ("...set to 1.41.1 for upcoming hotfix" -> 1.411).
|
||||
function toAppVersionNumber(tag) {
|
||||
const cleaned = tag.replace(/^v/, "");
|
||||
const [major, minor, ...rest] = cleaned.split(".");
|
||||
if (rest.length === 0) return cleaned;
|
||||
const patch = rest.join("");
|
||||
return /^0*$/.test(patch) ? `${major}.${minor}` : `${major}.${minor}${patch}`;
|
||||
}
|
||||
|
||||
function escapeXml(value) {
|
||||
return String(value ?? "")
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">");
|
||||
}
|
||||
|
||||
// Strips GitHub's auto-generated release-notes markdown noise (headings,
|
||||
// "Full Changelog" links, PR author attributions) down to plain text.
|
||||
function cleanReleaseBody(body) {
|
||||
const lines = [];
|
||||
for (let line of (body || "").split("\n")) {
|
||||
if (/^#{1,3}\s/.test(line)) continue;
|
||||
if (line.trim().startsWith("**Full Changelog**")) continue;
|
||||
line = line.replace(/\s+by @\S+ in https:\/\/\S+/, "");
|
||||
line = line.replace(/^\* /, "- ");
|
||||
if (line.trim()) lines.push(line);
|
||||
}
|
||||
return lines.join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
||||
}
|
||||
|
||||
// Groups closed issues (excluding PRs) in the (since, until] window by
|
||||
// label into "added" (enhancement) / "fixed" (bug) bullet lists.
|
||||
async function getIssuesForRelease(since, until) {
|
||||
const buckets = { added: [], fixed: [] };
|
||||
let page = 1;
|
||||
for (;;) {
|
||||
const items = await githubGet(`/issues?state=closed&per_page=100&page=${page}`);
|
||||
if (!items.length) break;
|
||||
for (const issue of items) {
|
||||
if (issue.pull_request) continue;
|
||||
const closed = issue.closed_at || "";
|
||||
if (!(closed > since && closed <= until)) continue;
|
||||
const labels = issue.labels.map((l) => l.name);
|
||||
const bucket = Object.keys(LABEL_MAP).find((l) => labels.includes(l));
|
||||
if (bucket) buckets[LABEL_MAP[bucket]].push(`- ${issue.title}`);
|
||||
}
|
||||
if (items.length < 100) break;
|
||||
page++;
|
||||
}
|
||||
return { added: buckets.added.join("\n"), fixed: buckets.fixed.join("\n") };
|
||||
}
|
||||
|
||||
// version-history.xml holds hand-written <changeLog> entries for releases
|
||||
// published before GitHub Releases existed; re-emitted verbatim here.
|
||||
function loadHistoryEntries() {
|
||||
const historyPath = path.join(__dirname, "version-history.xml");
|
||||
const xml = fs.readFileSync(historyPath, "utf-8");
|
||||
return [...xml.matchAll(/<changeLog>[\s\S]*?<\/changeLog>/g)].map((m) => m[0]);
|
||||
}
|
||||
|
||||
function historyEntryVersion(entryXml) {
|
||||
const m = entryXml.match(/<changedVersionNumber>([\s\S]*?)<\/changedVersionNumber>/);
|
||||
return m ? m[1].trim() : null;
|
||||
}
|
||||
|
||||
// Static <roadmap>/<bugsReported> sections carried over from the previous
|
||||
// hand-maintained XML on do5amf.funkerportal.de. UpdateChecker.java doesn't
|
||||
// parse these, kept only so the migration doesn't drop existing content.
|
||||
function loadLegacySections() {
|
||||
const legacyPath = path.join(__dirname, "version-legacy-sections.xml");
|
||||
const xml = fs.readFileSync(legacyPath, "utf-8");
|
||||
const roadmap = xml.match(/<roadmap>[\s\S]*?<\/roadmap>/);
|
||||
const bugsReported = xml.match(/<bugsReported>[\s\S]*?<\/bugsReported>/);
|
||||
return [roadmap ? roadmap[0] : null, bugsReported ? bugsReported[0] : null].filter(Boolean);
|
||||
}
|
||||
|
||||
// Builds kst4ContestVersionInfo.xml (the update-checker feed read by
|
||||
// UpdateChecker.java) from GitHub releases + closed issues, falling back to
|
||||
// version-history.xml for releases that predate GitHub Releases.
|
||||
module.exports = async function () {
|
||||
const fallback = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n<praktiKST></praktiKST>';
|
||||
|
||||
try {
|
||||
const historyEntries = loadHistoryEntries();
|
||||
|
||||
const rawReleases = await githubGet("/releases?per_page=100");
|
||||
const releases = rawReleases
|
||||
.map((r) => ({
|
||||
tagName: r.tag_name,
|
||||
publishedAt: r.published_at || "",
|
||||
name: r.name || r.tag_name,
|
||||
body: r.body || "",
|
||||
isPrerelease: r.prerelease
|
||||
}))
|
||||
.sort((a, b) => (a.publishedAt < b.publishedAt ? 1 : -1));
|
||||
|
||||
const stableReleases = releases.filter((r) => !r.isPrerelease);
|
||||
const stable = stableReleases[0];
|
||||
const ghVersions = new Set(stableReleases.map((r) => toAppVersionNumber(r.tagName)));
|
||||
|
||||
const parts = [];
|
||||
parts.push('<?xml version="1.0" encoding="UTF-8" standalone="no"?>');
|
||||
parts.push("<praktiKST>");
|
||||
|
||||
const latestTag = stable ? stable.tagName : null;
|
||||
const winFilename = latestTag ? `praktiKST-${latestTag}-windows-x64.zip` : "";
|
||||
|
||||
parts.push(" <latestVersion>");
|
||||
if (stable) {
|
||||
const latestIssues = await getIssuesForRelease("1970-01-01T00:00:00Z", stable.publishedAt);
|
||||
parts.push(` <versionNumber>${escapeXml(toAppVersionNumber(latestTag))}</versionNumber>`);
|
||||
parts.push(" <adminMessage></adminMessage>");
|
||||
parts.push(` <majorChanges>${escapeXml(latestIssues.added.slice(0, 300))}</majorChanges>`);
|
||||
parts.push(
|
||||
` <latestVersionPathOnWebserver>https://github.com/${REPO}/releases/download/${latestTag}/${winFilename}</latestVersionPathOnWebserver>`
|
||||
);
|
||||
}
|
||||
parts.push(" </latestVersion>");
|
||||
|
||||
// UpdateChecker.parseUpdateXMLFile() never actually reads this field
|
||||
// (setNeedUpdateResourcesSinceLastVersion is never called); the
|
||||
// previous server always hardcoded "nothing" here, so we match that.
|
||||
parts.push(" <needUpdateSinceLastVersion>");
|
||||
parts.push(" <filename>nothing</filename>");
|
||||
parts.push(" </needUpdateSinceLastVersion>");
|
||||
|
||||
for (const section of loadLegacySections()) {
|
||||
parts.push(section);
|
||||
}
|
||||
|
||||
for (let i = 0; i < stableReleases.length; i++) {
|
||||
const rel = stableReleases[i];
|
||||
const until = rel.publishedAt;
|
||||
const since = stableReleases[i + 1] ? stableReleases[i + 1].publishedAt : "1970-01-01T00:00:00Z";
|
||||
const issues = await getIssuesForRelease(since, until);
|
||||
|
||||
parts.push(" <changeLog>");
|
||||
parts.push(` <changedVersionNumber>${escapeXml(toAppVersionNumber(rel.tagName))}</changedVersionNumber>`);
|
||||
parts.push(` <date>${escapeXml(until.slice(0, 10))}</date>`);
|
||||
parts.push(` <description>${escapeXml(rel.name)}</description>`);
|
||||
parts.push(` <added>${escapeXml(issues.added)}</added>`);
|
||||
parts.push(` <changed>${escapeXml(cleanReleaseBody(rel.body))}</changed>`);
|
||||
parts.push(` <fixed>${escapeXml(issues.fixed)}</fixed>`);
|
||||
parts.push(" <removed></removed>");
|
||||
parts.push(" </changeLog>");
|
||||
}
|
||||
|
||||
for (const entry of historyEntries) {
|
||||
const version = historyEntryVersion(entry);
|
||||
if (version && ghVersions.has(version)) continue;
|
||||
parts.push(entry);
|
||||
}
|
||||
|
||||
parts.push("</praktiKST>");
|
||||
return parts.join("\n");
|
||||
} catch (err) {
|
||||
console.warn(`[versionInfo] Could not generate version info XML, using empty fallback: ${err.message}`);
|
||||
return fallback;
|
||||
}
|
||||
};
|
||||
@@ -71,6 +71,7 @@
|
||||
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
|
||||
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
|
||||
<p><a href="/contact/">Contact</a></p>
|
||||
<p><a href="/support/">❤️ Support KST4Contest</a></p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -80,5 +81,10 @@
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
{% if heroFx %}
|
||||
<script src="/assets/js/hero-radio-fx.js?v={{ build.version }}" defer></script>
|
||||
{% endif %}
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -26,5 +26,20 @@ description: About KST4Contest and its contest-oriented ON4KST workflow.
|
||||
The project is open source and focused on practical contest station workflows.
|
||||
Features are designed around real operating pressure, not theoretical UI concepts.
|
||||
</p>
|
||||
|
||||
<h2>Community supported</h2>
|
||||
|
||||
<p>
|
||||
|
||||
KST4Contest is developed independently.
|
||||
|
||||
Community support helps finance hosting,
|
||||
online services,
|
||||
testing across multiple operating systems
|
||||
and future contest-oriented features.
|
||||
|
||||
</p>
|
||||
|
||||
</article>
|
||||
|
||||
</section>
|
||||
@@ -7,10 +7,22 @@
|
||||
--text: #f8fafc;
|
||||
--muted: #aab6ca;
|
||||
--soft: #64748b;
|
||||
--accent: #38bdf8;
|
||||
--accent2: #a855f7;
|
||||
--accent3: #22c55e;
|
||||
/*--accent: #38bdf8;*/
|
||||
/*--accent2: #a855f7;*/
|
||||
/*--accent3: #22c55e;*/
|
||||
--shadow: 0 24px 80px rgba(0, 0, 0, 0.45);
|
||||
|
||||
--accent: #54d63d;
|
||||
--accent2: #8aef64;
|
||||
--accent3: #31b54a;
|
||||
|
||||
--fx-plane: #79ff73;
|
||||
--fx-plane-muted: rgba(121, 255, 115, 0.42);
|
||||
--fx-beam: #52ff65;
|
||||
--fx-beam-glow: rgba(82, 255, 101, 0.28);
|
||||
--fx-reflection: #d2ffc8;
|
||||
--fx-trail: rgba(121, 255, 115, 0.17);
|
||||
--fx-grid: rgba(121, 255, 115, 0.055);
|
||||
}
|
||||
|
||||
* {
|
||||
@@ -25,13 +37,39 @@ html {
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
|
||||
background:
|
||||
radial-gradient(circle at 14% 12%, rgba(168, 85, 247, 0.30), transparent 28%),
|
||||
radial-gradient(circle at 82% 16%, rgba(56, 189, 248, 0.24), transparent 30%),
|
||||
radial-gradient(circle at 50% 85%, rgba(34, 197, 94, 0.11), transparent 35%),
|
||||
linear-gradient(180deg, #050816 0%, #07111f 100%);
|
||||
radial-gradient(
|
||||
circle at 14% 12%,
|
||||
rgba(52, 165, 57, 0.20),
|
||||
transparent 30%
|
||||
),
|
||||
radial-gradient(
|
||||
circle at 82% 16%,
|
||||
rgba(95, 218, 82, 0.14),
|
||||
transparent 32%
|
||||
),
|
||||
radial-gradient(
|
||||
circle at 50% 85%,
|
||||
rgba(32, 126, 55, 0.12),
|
||||
transparent 36%
|
||||
),
|
||||
linear-gradient(
|
||||
180deg,
|
||||
#070b09 0%,
|
||||
#0b1410 100%
|
||||
);
|
||||
|
||||
color: var(--text);
|
||||
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
font-family:
|
||||
Inter,
|
||||
ui-sans-serif,
|
||||
system-ui,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
"Segoe UI",
|
||||
sans-serif;
|
||||
|
||||
line-height: 1.65;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
@@ -561,4 +599,156 @@ a:hover {
|
||||
.footer-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Interactive radio / aircraft-scatter hero layer
|
||||
*/
|
||||
|
||||
.hero-radio-fx {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
opacity: 0.76;
|
||||
}
|
||||
|
||||
.hero-fx-vignette {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
|
||||
background:
|
||||
linear-gradient(
|
||||
90deg,
|
||||
rgba(7, 11, 9, 0.92) 0%,
|
||||
rgba(7, 11, 9, 0.68) 34%,
|
||||
rgba(7, 11, 9, 0.16) 64%,
|
||||
rgba(7, 11, 9, 0.42) 100%
|
||||
),
|
||||
linear-gradient(
|
||||
180deg,
|
||||
rgba(7, 11, 9, 0.10),
|
||||
rgba(7, 11, 9, 0.58)
|
||||
);
|
||||
}
|
||||
|
||||
.hero-split > *:not(.hero-radio-fx):not(.hero-fx-vignette) {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/*
|
||||
* Operator-console badge styling
|
||||
*/
|
||||
|
||||
.badge,
|
||||
.eyebrow {
|
||||
color: #caffc0;
|
||||
background: rgba(52, 165, 57, 0.12);
|
||||
border-color: rgba(105, 232, 88, 0.32);
|
||||
box-shadow: 0 0 22px rgba(82, 255, 101, 0.08);
|
||||
}
|
||||
|
||||
/*
|
||||
* Light-mode identity:
|
||||
* white/grey UI with red contest accents
|
||||
*/
|
||||
|
||||
html[data-theme="light"] {
|
||||
--bg: #f4f4f2;
|
||||
--bg2: #ffffff;
|
||||
--panel: rgba(255, 255, 255, 0.88);
|
||||
--panel2: rgba(242, 242, 239, 0.92);
|
||||
--border: rgba(91, 91, 86, 0.20);
|
||||
--text: #252522;
|
||||
--muted: #656560;
|
||||
--soft: #888881;
|
||||
|
||||
--accent: #d33732;
|
||||
--accent2: #f25a51;
|
||||
--accent3: #ae2727;
|
||||
|
||||
--fx-plane: #df3732;
|
||||
--fx-plane-muted: rgba(211, 55, 50, 0.38);
|
||||
--fx-beam: #ef4039;
|
||||
--fx-beam-glow: rgba(239, 64, 57, 0.22);
|
||||
--fx-reflection: #ff8d84;
|
||||
--fx-trail: rgba(211, 55, 50, 0.15);
|
||||
--fx-grid: rgba(185, 47, 43, 0.045);
|
||||
}
|
||||
|
||||
html[data-theme="light"] body {
|
||||
background:
|
||||
radial-gradient(
|
||||
circle at 14% 12%,
|
||||
rgba(211, 55, 50, 0.12),
|
||||
transparent 30%
|
||||
),
|
||||
radial-gradient(
|
||||
circle at 82% 16%,
|
||||
rgba(242, 90, 81, 0.09),
|
||||
transparent 32%
|
||||
),
|
||||
linear-gradient(
|
||||
180deg,
|
||||
#ffffff 0%,
|
||||
#f0f0ed 100%
|
||||
);
|
||||
}
|
||||
|
||||
html[data-theme="light"] .hero-fx-vignette {
|
||||
background:
|
||||
linear-gradient(
|
||||
90deg,
|
||||
rgba(255, 255, 255, 0.95) 0%,
|
||||
rgba(255, 255, 255, 0.76) 34%,
|
||||
rgba(255, 255, 255, 0.18) 66%,
|
||||
rgba(255, 255, 255, 0.48) 100%
|
||||
),
|
||||
linear-gradient(
|
||||
180deg,
|
||||
rgba(255, 255, 255, 0.06),
|
||||
rgba(240, 240, 237, 0.62)
|
||||
);
|
||||
}
|
||||
|
||||
html[data-theme="light"] .badge,
|
||||
html[data-theme="light"] .eyebrow {
|
||||
color: #962a27;
|
||||
background: rgba(211, 55, 50, 0.08);
|
||||
border-color: rgba(211, 55, 50, 0.24);
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.hero-radio-fx {
|
||||
opacity: 0.48;
|
||||
}
|
||||
|
||||
.hero-fx-vignette {
|
||||
background:
|
||||
linear-gradient(
|
||||
180deg,
|
||||
rgba(7, 11, 9, 0.82),
|
||||
rgba(7, 11, 9, 0.34)
|
||||
);
|
||||
}
|
||||
|
||||
html[data-theme="light"] .hero-fx-vignette {
|
||||
background:
|
||||
linear-gradient(
|
||||
180deg,
|
||||
rgba(255, 255, 255, 0.90),
|
||||
rgba(255, 255, 255, 0.42)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.hero-radio-fx {
|
||||
opacity: 0.28;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,710 @@
|
||||
(() => {
|
||||
"use strict";
|
||||
|
||||
const canvas = document.querySelector("[data-hero-radio-fx]");
|
||||
|
||||
if (!(canvas instanceof HTMLCanvasElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const context = canvas.getContext("2d", {
|
||||
alpha: true,
|
||||
desynchronized: true
|
||||
});
|
||||
|
||||
if (!context) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hero = canvas.closest(".hero");
|
||||
|
||||
if (!(hero instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const reducedMotion = window.matchMedia(
|
||||
"(prefers-reduced-motion: reduce)"
|
||||
);
|
||||
|
||||
const coarsePointer = window.matchMedia(
|
||||
"(pointer: coarse)"
|
||||
);
|
||||
|
||||
const state = {
|
||||
width: 0,
|
||||
height: 0,
|
||||
dpr: 1,
|
||||
running: false,
|
||||
visible: true,
|
||||
lastFrame: 0,
|
||||
spawnAccumulator: 0,
|
||||
beamAccumulator: 0,
|
||||
animationFrameId: 0,
|
||||
pointer: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
active: false
|
||||
},
|
||||
planes: [],
|
||||
beams: [],
|
||||
reflections: []
|
||||
};
|
||||
|
||||
const clamp = (value, minimum, maximum) =>
|
||||
Math.max(minimum, Math.min(maximum, value));
|
||||
|
||||
const randomBetween = (minimum, maximum) =>
|
||||
minimum + Math.random() * (maximum - minimum);
|
||||
|
||||
function getCssColor(name, fallback) {
|
||||
const value = getComputedStyle(document.documentElement)
|
||||
.getPropertyValue(name)
|
||||
.trim();
|
||||
|
||||
return value || fallback;
|
||||
}
|
||||
|
||||
function getPalette() {
|
||||
return {
|
||||
plane: getCssColor("--fx-plane", "#79ff73"),
|
||||
planeMuted: getCssColor("--fx-plane-muted", "rgba(121,255,115,.42)"),
|
||||
beam: getCssColor("--fx-beam", "#52ff65"),
|
||||
beamGlow: getCssColor("--fx-beam-glow", "rgba(82,255,101,.28)"),
|
||||
reflection: getCssColor("--fx-reflection", "#c8ffbf"),
|
||||
trail: getCssColor("--fx-trail", "rgba(121,255,115,.16)"),
|
||||
grid: getCssColor("--fx-grid", "rgba(121,255,115,.055)")
|
||||
};
|
||||
}
|
||||
|
||||
function resizeCanvas() {
|
||||
const bounds = hero.getBoundingClientRect();
|
||||
|
||||
state.width = Math.max(1, Math.round(bounds.width));
|
||||
state.height = Math.max(1, Math.round(bounds.height));
|
||||
state.dpr = Math.min(window.devicePixelRatio || 1, 1.5);
|
||||
|
||||
canvas.width = Math.round(state.width * state.dpr);
|
||||
canvas.height = Math.round(state.height * state.dpr);
|
||||
|
||||
canvas.style.width = `${state.width}px`;
|
||||
canvas.style.height = `${state.height}px`;
|
||||
|
||||
context.setTransform(
|
||||
state.dpr,
|
||||
0,
|
||||
0,
|
||||
state.dpr,
|
||||
0,
|
||||
0
|
||||
);
|
||||
|
||||
if (!state.pointer.active) {
|
||||
state.pointer.x = state.width * 0.54;
|
||||
state.pointer.y = state.height * 0.42;
|
||||
}
|
||||
}
|
||||
|
||||
function getPlaneLimit() {
|
||||
if (coarsePointer.matches) {
|
||||
return 5;
|
||||
}
|
||||
|
||||
if (state.width < 900) {
|
||||
return 7;
|
||||
}
|
||||
|
||||
return 12;
|
||||
}
|
||||
|
||||
function createPlane() {
|
||||
const margin = 55;
|
||||
const edge = Math.floor(Math.random() * 4);
|
||||
|
||||
let x;
|
||||
let y;
|
||||
|
||||
switch (edge) {
|
||||
case 0:
|
||||
x = randomBetween(0, state.width);
|
||||
y = -margin;
|
||||
break;
|
||||
|
||||
case 1:
|
||||
x = state.width + margin;
|
||||
y = randomBetween(0, state.height);
|
||||
break;
|
||||
|
||||
case 2:
|
||||
x = randomBetween(0, state.width);
|
||||
y = state.height + margin;
|
||||
break;
|
||||
|
||||
default:
|
||||
x = -margin;
|
||||
y = randomBetween(0, state.height);
|
||||
break;
|
||||
}
|
||||
|
||||
const targetX = state.pointer.active
|
||||
? state.pointer.x
|
||||
: state.width * randomBetween(0.38, 0.72);
|
||||
|
||||
const targetY = state.pointer.active
|
||||
? state.pointer.y
|
||||
: state.height * randomBetween(0.2, 0.72);
|
||||
|
||||
const angle = Math.atan2(targetY - y, targetX - x);
|
||||
const speed = randomBetween(18, 34);
|
||||
|
||||
return {
|
||||
x,
|
||||
y,
|
||||
previousX: x,
|
||||
previousY: y,
|
||||
velocityX: Math.cos(angle) * speed,
|
||||
velocityY: Math.sin(angle) * speed,
|
||||
angle,
|
||||
size: randomBetween(5.5, 9),
|
||||
age: 0,
|
||||
maximumAge: randomBetween(12, 20),
|
||||
steering: randomBetween(0.65, 1.2),
|
||||
wobble: randomBetween(0, Math.PI * 2),
|
||||
wobbleSpeed: randomBetween(0.7, 1.4),
|
||||
hit: 0
|
||||
};
|
||||
}
|
||||
|
||||
function updatePlane(plane, deltaSeconds) {
|
||||
plane.age += deltaSeconds;
|
||||
plane.wobble += deltaSeconds * plane.wobbleSpeed;
|
||||
|
||||
plane.previousX = plane.x;
|
||||
plane.previousY = plane.y;
|
||||
|
||||
const targetX = state.pointer.active
|
||||
? state.pointer.x
|
||||
: state.width * 0.56;
|
||||
|
||||
const targetY = state.pointer.active
|
||||
? state.pointer.y
|
||||
: state.height * 0.44;
|
||||
|
||||
const desiredAngle = Math.atan2(
|
||||
targetY - plane.y,
|
||||
targetX - plane.x
|
||||
);
|
||||
|
||||
let angleDifference = desiredAngle - plane.angle;
|
||||
|
||||
while (angleDifference > Math.PI) {
|
||||
angleDifference -= Math.PI * 2;
|
||||
}
|
||||
|
||||
while (angleDifference < -Math.PI) {
|
||||
angleDifference += Math.PI * 2;
|
||||
}
|
||||
|
||||
plane.angle += angleDifference * plane.steering * deltaSeconds;
|
||||
|
||||
const speed = Math.hypot(
|
||||
plane.velocityX,
|
||||
plane.velocityY
|
||||
);
|
||||
|
||||
const wobble = Math.sin(plane.wobble) * 0.14;
|
||||
|
||||
plane.velocityX =
|
||||
Math.cos(plane.angle + wobble) * speed;
|
||||
|
||||
plane.velocityY =
|
||||
Math.sin(plane.angle + wobble) * speed;
|
||||
|
||||
plane.x += plane.velocityX * deltaSeconds;
|
||||
plane.y += plane.velocityY * deltaSeconds;
|
||||
|
||||
plane.hit = Math.max(0, plane.hit - deltaSeconds * 1.8);
|
||||
}
|
||||
|
||||
function isPlaneExpired(plane) {
|
||||
const margin = 180;
|
||||
|
||||
return (
|
||||
plane.age > plane.maximumAge ||
|
||||
plane.x < -margin ||
|
||||
plane.x > state.width + margin ||
|
||||
plane.y < -margin ||
|
||||
plane.y > state.height + margin
|
||||
);
|
||||
}
|
||||
|
||||
function chooseBeamTarget() {
|
||||
if (state.planes.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const targetX = state.pointer.active
|
||||
? state.pointer.x
|
||||
: state.width * 0.56;
|
||||
|
||||
const targetY = state.pointer.active
|
||||
? state.pointer.y
|
||||
: state.height * 0.44;
|
||||
|
||||
const candidates = state.planes
|
||||
.map((plane) => ({
|
||||
plane,
|
||||
distance: Math.hypot(
|
||||
plane.x - targetX,
|
||||
plane.y - targetY
|
||||
)
|
||||
}))
|
||||
.filter((entry) => entry.distance < 260)
|
||||
.sort((a, b) => a.distance - b.distance);
|
||||
|
||||
if (candidates.length > 0) {
|
||||
return candidates[0].plane;
|
||||
}
|
||||
|
||||
return state.planes[
|
||||
Math.floor(Math.random() * state.planes.length)
|
||||
];
|
||||
}
|
||||
|
||||
function createBeam(target) {
|
||||
const leftStation = {
|
||||
x: state.width * 0.12,
|
||||
y: state.height * 0.82
|
||||
};
|
||||
|
||||
const rightStation = {
|
||||
x: state.width * 0.88,
|
||||
y: state.height * 0.78
|
||||
};
|
||||
|
||||
const source =
|
||||
Math.abs(target.x - leftStation.x) <
|
||||
Math.abs(target.x - rightStation.x)
|
||||
? leftStation
|
||||
: rightStation;
|
||||
|
||||
state.beams.push({
|
||||
startX: source.x,
|
||||
startY: source.y,
|
||||
endX: target.x,
|
||||
endY: target.y,
|
||||
age: 0,
|
||||
maximumAge: 0.62
|
||||
});
|
||||
|
||||
target.hit = 1;
|
||||
|
||||
state.reflections.push({
|
||||
x: target.x,
|
||||
y: target.y,
|
||||
age: 0,
|
||||
maximumAge: 0.9,
|
||||
radius: randomBetween(8, 15)
|
||||
});
|
||||
}
|
||||
|
||||
function updateEffects(deltaSeconds) {
|
||||
for (const beam of state.beams) {
|
||||
beam.age += deltaSeconds;
|
||||
}
|
||||
|
||||
state.beams = state.beams.filter(
|
||||
(beam) => beam.age < beam.maximumAge
|
||||
);
|
||||
|
||||
for (const reflection of state.reflections) {
|
||||
reflection.age += deltaSeconds;
|
||||
reflection.radius += deltaSeconds * 30;
|
||||
}
|
||||
|
||||
state.reflections = state.reflections.filter(
|
||||
(reflection) =>
|
||||
reflection.age < reflection.maximumAge
|
||||
);
|
||||
}
|
||||
|
||||
function drawGrid(palette) {
|
||||
context.save();
|
||||
|
||||
context.strokeStyle = palette.grid;
|
||||
context.lineWidth = 1;
|
||||
|
||||
const spacing = 54;
|
||||
|
||||
for (
|
||||
let x = spacing;
|
||||
x < state.width;
|
||||
x += spacing
|
||||
) {
|
||||
context.beginPath();
|
||||
context.moveTo(x, 0);
|
||||
context.lineTo(x, state.height);
|
||||
context.stroke();
|
||||
}
|
||||
|
||||
for (
|
||||
let y = spacing;
|
||||
y < state.height;
|
||||
y += spacing
|
||||
) {
|
||||
context.beginPath();
|
||||
context.moveTo(0, y);
|
||||
context.lineTo(state.width, y);
|
||||
context.stroke();
|
||||
}
|
||||
|
||||
context.restore();
|
||||
}
|
||||
|
||||
function drawRadarTarget(palette) {
|
||||
const x = state.pointer.active
|
||||
? state.pointer.x
|
||||
: state.width * 0.56;
|
||||
|
||||
const y = state.pointer.active
|
||||
? state.pointer.y
|
||||
: state.height * 0.44;
|
||||
|
||||
context.save();
|
||||
|
||||
context.strokeStyle = palette.planeMuted;
|
||||
context.lineWidth = 1;
|
||||
|
||||
for (const radius of [24, 48, 82]) {
|
||||
context.globalAlpha = 0.24 - radius / 500;
|
||||
|
||||
context.beginPath();
|
||||
context.arc(x, y, radius, 0, Math.PI * 2);
|
||||
context.stroke();
|
||||
}
|
||||
|
||||
context.globalAlpha = 0.2;
|
||||
|
||||
context.beginPath();
|
||||
context.moveTo(x - 100, y);
|
||||
context.lineTo(x + 100, y);
|
||||
context.moveTo(x, y - 100);
|
||||
context.lineTo(x, y + 100);
|
||||
context.stroke();
|
||||
|
||||
context.restore();
|
||||
}
|
||||
|
||||
function drawPlane(plane, palette) {
|
||||
context.save();
|
||||
|
||||
context.translate(plane.x, plane.y);
|
||||
context.rotate(plane.angle);
|
||||
|
||||
const alpha = clamp(
|
||||
Math.min(
|
||||
plane.age * 1.8,
|
||||
(plane.maximumAge - plane.age) * 1.8
|
||||
),
|
||||
0,
|
||||
1
|
||||
);
|
||||
|
||||
context.globalAlpha = alpha;
|
||||
|
||||
context.strokeStyle = palette.trail;
|
||||
context.lineWidth = 1;
|
||||
|
||||
context.beginPath();
|
||||
context.moveTo(-plane.size * 4.5, 0);
|
||||
context.lineTo(-plane.size * 1.4, 0);
|
||||
context.stroke();
|
||||
|
||||
context.fillStyle =
|
||||
plane.hit > 0
|
||||
? palette.reflection
|
||||
: palette.plane;
|
||||
|
||||
context.shadowColor =
|
||||
plane.hit > 0
|
||||
? palette.reflection
|
||||
: palette.beamGlow;
|
||||
|
||||
context.shadowBlur =
|
||||
plane.hit > 0
|
||||
? 16
|
||||
: 5;
|
||||
|
||||
const size = plane.size;
|
||||
|
||||
context.beginPath();
|
||||
|
||||
context.moveTo(size * 1.65, 0);
|
||||
context.lineTo(-size * 0.7, size * 0.34);
|
||||
context.lineTo(-size * 0.15, size * 1.05);
|
||||
context.lineTo(-size * 0.65, size * 1.08);
|
||||
context.lineTo(-size * 1.35, size * 0.3);
|
||||
context.lineTo(-size * 1.7, size * 0.28);
|
||||
context.lineTo(-size * 1.12, 0);
|
||||
context.lineTo(-size * 1.7, -size * 0.28);
|
||||
context.lineTo(-size * 1.35, -size * 0.3);
|
||||
context.lineTo(-size * 0.65, -size * 1.08);
|
||||
context.lineTo(-size * 0.15, -size * 1.05);
|
||||
context.lineTo(-size * 0.7, -size * 0.34);
|
||||
|
||||
context.closePath();
|
||||
context.fill();
|
||||
|
||||
context.restore();
|
||||
}
|
||||
|
||||
function drawBeam(beam, palette) {
|
||||
const progress = beam.age / beam.maximumAge;
|
||||
const alpha = Math.sin(progress * Math.PI);
|
||||
|
||||
context.save();
|
||||
|
||||
context.globalAlpha = alpha;
|
||||
context.strokeStyle = palette.beamGlow;
|
||||
context.lineWidth = 6;
|
||||
context.shadowColor = palette.beam;
|
||||
context.shadowBlur = 15;
|
||||
|
||||
context.beginPath();
|
||||
context.moveTo(beam.startX, beam.startY);
|
||||
context.lineTo(beam.endX, beam.endY);
|
||||
context.stroke();
|
||||
|
||||
context.strokeStyle = palette.beam;
|
||||
context.lineWidth = 1.4;
|
||||
context.shadowBlur = 5;
|
||||
|
||||
context.beginPath();
|
||||
context.moveTo(beam.startX, beam.startY);
|
||||
context.lineTo(beam.endX, beam.endY);
|
||||
context.stroke();
|
||||
|
||||
context.restore();
|
||||
}
|
||||
|
||||
function drawReflection(reflection, palette) {
|
||||
const progress =
|
||||
reflection.age / reflection.maximumAge;
|
||||
|
||||
context.save();
|
||||
|
||||
context.globalAlpha = 1 - progress;
|
||||
context.strokeStyle = palette.reflection;
|
||||
context.lineWidth = 1.5;
|
||||
context.shadowColor = palette.beam;
|
||||
context.shadowBlur = 12;
|
||||
|
||||
context.beginPath();
|
||||
context.arc(
|
||||
reflection.x,
|
||||
reflection.y,
|
||||
reflection.radius,
|
||||
0,
|
||||
Math.PI * 2
|
||||
);
|
||||
context.stroke();
|
||||
|
||||
context.restore();
|
||||
}
|
||||
|
||||
function drawStaticScene() {
|
||||
const palette = getPalette();
|
||||
|
||||
context.clearRect(
|
||||
0,
|
||||
0,
|
||||
state.width,
|
||||
state.height
|
||||
);
|
||||
|
||||
drawGrid(palette);
|
||||
drawRadarTarget(palette);
|
||||
}
|
||||
|
||||
function drawScene() {
|
||||
const palette = getPalette();
|
||||
|
||||
context.clearRect(
|
||||
0,
|
||||
0,
|
||||
state.width,
|
||||
state.height
|
||||
);
|
||||
|
||||
drawGrid(palette);
|
||||
drawRadarTarget(palette);
|
||||
|
||||
for (const plane of state.planes) {
|
||||
drawPlane(plane, palette);
|
||||
}
|
||||
|
||||
for (const beam of state.beams) {
|
||||
drawBeam(beam, palette);
|
||||
}
|
||||
|
||||
for (const reflection of state.reflections) {
|
||||
drawReflection(reflection, palette);
|
||||
}
|
||||
}
|
||||
|
||||
function animate(timestamp) {
|
||||
if (!state.running) {
|
||||
return;
|
||||
}
|
||||
|
||||
const deltaSeconds = clamp(
|
||||
(timestamp - state.lastFrame) / 1000 || 0,
|
||||
0,
|
||||
0.05
|
||||
);
|
||||
|
||||
state.lastFrame = timestamp;
|
||||
|
||||
if (!state.visible || document.hidden) {
|
||||
state.animationFrameId =
|
||||
requestAnimationFrame(animate);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
state.spawnAccumulator += deltaSeconds;
|
||||
state.beamAccumulator += deltaSeconds;
|
||||
|
||||
const planeLimit = getPlaneLimit();
|
||||
const spawnInterval =
|
||||
coarsePointer.matches ? 2.2 : 1.15;
|
||||
|
||||
if (
|
||||
state.spawnAccumulator >= spawnInterval &&
|
||||
state.planes.length < planeLimit
|
||||
) {
|
||||
state.spawnAccumulator = 0;
|
||||
state.planes.push(createPlane());
|
||||
}
|
||||
|
||||
const beamInterval =
|
||||
coarsePointer.matches ? 3.3 : 1.8;
|
||||
|
||||
if (
|
||||
state.beamAccumulator >= beamInterval &&
|
||||
state.planes.length > 0
|
||||
) {
|
||||
state.beamAccumulator = 0;
|
||||
|
||||
const target = chooseBeamTarget();
|
||||
|
||||
if (target) {
|
||||
createBeam(target);
|
||||
}
|
||||
}
|
||||
|
||||
for (const plane of state.planes) {
|
||||
updatePlane(plane, deltaSeconds);
|
||||
}
|
||||
|
||||
state.planes = state.planes.filter(
|
||||
(plane) => !isPlaneExpired(plane)
|
||||
);
|
||||
|
||||
updateEffects(deltaSeconds);
|
||||
drawScene();
|
||||
|
||||
state.animationFrameId =
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
|
||||
function start() {
|
||||
cancelAnimationFrame(state.animationFrameId);
|
||||
|
||||
resizeCanvas();
|
||||
|
||||
if (reducedMotion.matches) {
|
||||
state.running = false;
|
||||
drawStaticScene();
|
||||
return;
|
||||
}
|
||||
|
||||
state.running = true;
|
||||
state.lastFrame = performance.now();
|
||||
|
||||
if (state.planes.length === 0) {
|
||||
const initialPlanes =
|
||||
coarsePointer.matches ? 2 : 5;
|
||||
|
||||
for (let index = 0; index < initialPlanes; index++) {
|
||||
const plane = createPlane();
|
||||
plane.age = Math.random() * 2;
|
||||
state.planes.push(plane);
|
||||
}
|
||||
}
|
||||
|
||||
state.animationFrameId =
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
|
||||
function updatePointer(event) {
|
||||
const bounds = hero.getBoundingClientRect();
|
||||
|
||||
state.pointer.x = clamp(
|
||||
event.clientX - bounds.left,
|
||||
0,
|
||||
bounds.width
|
||||
);
|
||||
|
||||
state.pointer.y = clamp(
|
||||
event.clientY - bounds.top,
|
||||
0,
|
||||
bounds.height
|
||||
);
|
||||
|
||||
state.pointer.active = true;
|
||||
}
|
||||
|
||||
hero.addEventListener("pointermove", updatePointer, {
|
||||
passive: true
|
||||
});
|
||||
|
||||
hero.addEventListener("pointerleave", () => {
|
||||
state.pointer.active = false;
|
||||
});
|
||||
|
||||
window.addEventListener("resize", resizeCanvas, {
|
||||
passive: true
|
||||
});
|
||||
|
||||
document.addEventListener("visibilitychange", () => {
|
||||
state.lastFrame = performance.now();
|
||||
});
|
||||
|
||||
reducedMotion.addEventListener("change", start);
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
resizeCanvas();
|
||||
|
||||
if (reducedMotion.matches) {
|
||||
drawStaticScene();
|
||||
}
|
||||
});
|
||||
|
||||
resizeObserver.observe(hero);
|
||||
|
||||
const intersectionObserver = new IntersectionObserver(
|
||||
(entries) => {
|
||||
state.visible =
|
||||
entries[0]?.isIntersecting ?? true;
|
||||
|
||||
state.lastFrame = performance.now();
|
||||
},
|
||||
{
|
||||
threshold: 0.05
|
||||
}
|
||||
);
|
||||
|
||||
intersectionObserver.observe(hero);
|
||||
|
||||
start();
|
||||
})();
|
||||
@@ -56,4 +56,32 @@ description: Download KST4Contest releases for Windows, Linux and macOS.
|
||||
<a class="button secondary" href="/faq/">FAQ</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
|
||||
<div class="card content-card">
|
||||
|
||||
<h2>Enjoying KST4Contest?</h2>
|
||||
|
||||
<p>
|
||||
|
||||
If KST4Contest makes contesting easier for you,
|
||||
please consider supporting future development.
|
||||
|
||||
</p>
|
||||
|
||||
<div class="actions">
|
||||
|
||||
<a class="button"
|
||||
href="/support/">
|
||||
|
||||
❤️ Support Development
|
||||
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</section>
|
||||
@@ -2,9 +2,18 @@
|
||||
layout: base.njk
|
||||
title: KST4Contest – Contest-Optimized ON4KST Chat Client
|
||||
description: KST4Contest is a modern ON4KST chat client built for VHF, UHF and SHF contest operation.
|
||||
heroFx: true
|
||||
---
|
||||
|
||||
<section class="hero hero-split">
|
||||
|
||||
<canvas
|
||||
class="hero-radio-fx"
|
||||
data-hero-radio-fx
|
||||
aria-hidden="true">
|
||||
</canvas>
|
||||
|
||||
<div class="hero-fx-vignette" aria-hidden="true"></div>
|
||||
<div>
|
||||
<p class="badge">The contest-optimized ON4KST client</p>
|
||||
<h1>KST4Contest</h1>
|
||||
@@ -73,6 +82,7 @@ description: KST4Contest is a modern ON4KST chat client built for VHF, UHF and S
|
||||
<div class="actions">
|
||||
<a class="button" href="/download/">Download KST4Contest</a>
|
||||
<a class="button secondary" href="/features/">Explore features</a>
|
||||
<a class="button ghost" href="/support/">❤️ Support Development</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -44,6 +44,22 @@ description: Privacy policy for the KST4Contest website.
|
||||
GitHub is responsible for its own data processing.
|
||||
</p>
|
||||
|
||||
<h2>
|
||||
|
||||
Support via Ko-fi
|
||||
|
||||
</h2>
|
||||
|
||||
<p>
|
||||
|
||||
This website links to Ko-fi for voluntary financial support.
|
||||
|
||||
Payments are processed entirely by Ko-fi and its payment providers.
|
||||
|
||||
KST4Contest itself never receives or processes payment information.
|
||||
|
||||
</p>
|
||||
|
||||
<h2>Your rights</h2>
|
||||
<p>
|
||||
You may have the right to access, rectification, erasure, restriction of processing,
|
||||
|
||||
@@ -48,3 +48,46 @@ description: Planned enhancements for upcoming KST4Contest versions, generated f
|
||||
</article>
|
||||
{% endfor %}
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
|
||||
<div class="cta-panel">
|
||||
|
||||
<p class="eyebrow">
|
||||
|
||||
Community
|
||||
|
||||
</p>
|
||||
|
||||
<h2>
|
||||
|
||||
Development is driven by the community.
|
||||
|
||||
</h2>
|
||||
|
||||
<p>
|
||||
|
||||
Many roadmap items require
|
||||
time,
|
||||
online services
|
||||
and infrastructure.
|
||||
|
||||
Community support helps accelerate development.
|
||||
|
||||
</p>
|
||||
|
||||
<div class="actions">
|
||||
|
||||
<a class="button"
|
||||
|
||||
href="/support/">
|
||||
|
||||
❤️ Support Development
|
||||
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</section>
|
||||
@@ -0,0 +1,176 @@
|
||||
---
|
||||
layout: base.njk
|
||||
title: Support KST4Contest
|
||||
description: Help support the ongoing development of KST4Contest.
|
||||
---
|
||||
|
||||
<section class="hero">
|
||||
<p class="badge">Support Development</p>
|
||||
|
||||
<h1>Help shape the future of KST4Contest.</h1>
|
||||
|
||||
<p class="lead">
|
||||
KST4Contest is an independent open-source project developed in my free time.
|
||||
Your support helps cover infrastructure costs and enables new contest-oriented
|
||||
features that would otherwise be difficult to implement.
|
||||
</p>
|
||||
|
||||
<div class="actions">
|
||||
<a class="button"
|
||||
href="https://ko-fi.com/praktimarc"
|
||||
target="_blank"
|
||||
rel="noopener">
|
||||
❤️ Support on Ko-fi
|
||||
</a>
|
||||
|
||||
<a class="button secondary"
|
||||
href="https://github.com/praktimarc/kst4contest">
|
||||
View Source Code
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
<section class="section">
|
||||
|
||||
<div class="section-heading">
|
||||
<p class="eyebrow">What previous support has enabled</p>
|
||||
|
||||
<h2>Real improvements funded by the community.</h2>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
|
||||
<article class="card">
|
||||
|
||||
<h3>🛰 PSTRotator Integration</h3>
|
||||
|
||||
<p>
|
||||
|
||||
Support helped justify the investment into rotor hardware and the required
|
||||
PSTRotator license. Today KST4Contest can automatically control antennas
|
||||
during contest operation.
|
||||
|
||||
</p>
|
||||
|
||||
</article>
|
||||
|
||||
<article class="card">
|
||||
|
||||
<h3>🗺 Interactive Map</h3>
|
||||
|
||||
<p>
|
||||
|
||||
The interactive station map required commercial map API usage.
|
||||
Community support helped cover those API costs and made the feature possible.
|
||||
|
||||
</p>
|
||||
|
||||
</article>
|
||||
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
|
||||
<section class="section">
|
||||
|
||||
<div class="section-heading">
|
||||
|
||||
<p class="eyebrow">Future goals</p>
|
||||
|
||||
<h2>What future support will fund</h2>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
|
||||
<article class="card">
|
||||
|
||||
<h3>☁ Map API</h3>
|
||||
|
||||
<p>
|
||||
|
||||
Keeping the online map service available requires ongoing API usage.
|
||||
|
||||
</p>
|
||||
|
||||
</article>
|
||||
|
||||
<article class="card">
|
||||
|
||||
<h3>📡 Tropo Server</h3>
|
||||
|
||||
<p>
|
||||
|
||||
Future propagation prediction services and server-side calculations
|
||||
will require dedicated hosting infrastructure.
|
||||
|
||||
</p>
|
||||
|
||||
</article>
|
||||
|
||||
<article class="card">
|
||||
|
||||
<h3>🔌 New Integrations</h3>
|
||||
|
||||
<p>
|
||||
|
||||
Development of additional interfaces, logging software integrations,
|
||||
contest tools and automation features.
|
||||
|
||||
</p>
|
||||
|
||||
</article>
|
||||
|
||||
<article class="card">
|
||||
|
||||
<h3>🚀 Faster Development</h3>
|
||||
|
||||
<p>
|
||||
|
||||
More development time means more contest-oriented features,
|
||||
better testing and better documentation.
|
||||
|
||||
</p>
|
||||
|
||||
</article>
|
||||
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
|
||||
<section class="section">
|
||||
|
||||
<div class="cta-panel">
|
||||
|
||||
<p class="eyebrow">Every contribution helps</p>
|
||||
|
||||
<h2>Whether it's one coffee or ongoing support.</h2>
|
||||
|
||||
<p>
|
||||
|
||||
Your support directly improves KST4Contest.
|
||||
|
||||
Unlike commercial software, every new feature is driven by real contest
|
||||
experience and community feedback.
|
||||
|
||||
</p>
|
||||
|
||||
<div class="actions">
|
||||
|
||||
<a class="button"
|
||||
href="https://ko-fi.com/praktimarc"
|
||||
target="_blank"
|
||||
rel="noopener">
|
||||
|
||||
❤️ Support KST4Contest
|
||||
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</section>
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
permalink: /kst4ContestVersionInfo.xml
|
||||
eleventyExcludeFromCollections: true
|
||||
---
|
||||
{{ versionInfo | safe }}
|
||||
Reference in New Issue
Block a user