From ced79fefd2c604baaa94d89e512bca0749304252 Mon Sep 17 00:00:00 2001 From: Philipp Wagner Date: Mon, 30 Mar 2026 21:54:31 +0200 Subject: [PATCH] WIP: fix #30 with limit to 10000 Messages with dequeList --- .../controller/ChatController.java | 11 +- .../MessageBusManagementThread.java | 18 +- .../utils/BoundedDequeObservableList.java | 168 ++++++++++++++++++ 3 files changed, 184 insertions(+), 13 deletions(-) create mode 100644 src/main/java/kst4contest/utils/BoundedDequeObservableList.java diff --git a/src/main/java/kst4contest/controller/ChatController.java b/src/main/java/kst4contest/controller/ChatController.java index 68a65d3..f08fe51 100644 --- a/src/main/java/kst4contest/controller/ChatController.java +++ b/src/main/java/kst4contest/controller/ChatController.java @@ -23,6 +23,7 @@ import kst4contest.locatorUtils.DirectionUtils; import kst4contest.logic.PriorityCalculator; import kst4contest.model.*; import kst4contest.test.MockKstServer; +import kst4contest.utils.BoundedDequeObservableList; import kst4contest.utils.PlayAudioUtils; import kst4contest.view.Kst4ContestApplication; @@ -1007,7 +1008,8 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList // ******All abstract types below here are used by the messageprocessor! // *************** - private ObservableList lst_globalChatMessageList = FXCollections.observableArrayList(); //All chatmessages will be put in there, later create filtered message lists + private static final int MAX_CHAT_MESSAGES = 10000; + private final BoundedDequeObservableList lst_globalChatMessageList = new BoundedDequeObservableList<>(MAX_CHAT_MESSAGES); //All chatmessages will be put in there, later create filtered message lists // private ObservableList lst_toAllMessageList = FXCollections.observableArrayList(); // directed to all // (beacon) private FilteredList lst_toAllMessageList = new FilteredList<>(lst_globalChatMessageList); // directed to all @@ -1152,13 +1154,14 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList this.lst_selectedCallSignInfofilteredMessageList = lst_selectedCallSignInfofilteredMessageList; } + public void addChatMessage(ChatMessage message) { + lst_globalChatMessageList.addFirst(message); + } + public ObservableList getLst_globalChatMessageList() { return lst_globalChatMessageList; } - public void setLst_globalChatMessageList(ObservableList lst_globalChatMessageList) { - this.lst_globalChatMessageList = lst_globalChatMessageList; - } public String getHostname() { return hostname; diff --git a/src/main/java/kst4contest/controller/MessageBusManagementThread.java b/src/main/java/kst4contest/controller/MessageBusManagementThread.java index 87356b6..2173e55 100644 --- a/src/main/java/kst4contest/controller/MessageBusManagementThread.java +++ b/src/main/java/kst4contest/controller/MessageBusManagementThread.java @@ -772,7 +772,7 @@ public class MessageBusManagementThread extends Thread { dummy.setCallSign("ALL"); newMessageArrived.setReceiver(dummy); - this.client.getLst_globalChatMessageList().add(0, newMessageArrived); // sdtout to all message-List + this.client.addChatMessage(newMessageArrived); // sdtout to all message-List } else { //message is directed to another chatmember, process as such! @@ -817,7 +817,7 @@ public class MessageBusManagementThread extends Thread { if (newMessageArrived.getReceiver().getCallSign() .equals(this.client.getChatPreferences().getStn_loginCallSign())) { - this.client.getLst_globalChatMessageList().add(0, newMessageArrived); + this.client.addChatMessage(newMessageArrived); if (this.client.getChatPreferences().isNotify_playSimpleSounds()) { this.client.getPlayAudioUtils().playNoiseLauncher('P'); @@ -960,7 +960,7 @@ public class MessageBusManagementThread extends Thread { String originalMessage = newMessageArrived.getMessageText(); newMessageArrived .setMessageText("(>" + newMessageArrived.getReceiver().getCallSign() + ")" + originalMessage); - this.client.getLst_globalChatMessageList().add(0,newMessageArrived); + this.client.addChatMessage(newMessageArrived); // if you sent the message to another station, it will be sorted in to // the "to me message list" with modified messagetext, added rxers callsign @@ -1024,7 +1024,7 @@ public class MessageBusManagementThread extends Thread { newMessageArrived.getSender().setInAngleAndRange(false); } - this.client.getLst_globalChatMessageList().add(0, newMessageArrived); + this.client.addChatMessage(newMessageArrived); // System.out.println("MSGBS bgfx: tx call = " + newMessageArrived.getSender().getCallSign() + " / rx call = " + newMessageArrived.getReceiver().getCallSign()); } } catch (NullPointerException referenceDeletedByUserLeftChatDuringMessageprocessing) { @@ -1364,7 +1364,7 @@ public class MessageBusManagementThread extends Thread { dummy.setCallSign("ALL"); newMessageArrived.setReceiver(dummy); - this.client.getLst_globalChatMessageList().add(0, newMessageArrived); // sdtout to all message-List + this.client.addChatMessage(newMessageArrived); // sdtout to all message-List } else { //message is directed to another chatmember, process as such! @@ -1408,7 +1408,7 @@ public class MessageBusManagementThread extends Thread { if (newMessageArrived.getReceiver().getCallSign() .equals(this.client.getChatPreferences().getStn_loginCallSign())) { - this.client.getLst_globalChatMessageList().add(0, newMessageArrived); + this.client.addChatMessage(newMessageArrived); System.out.println("Historic message directed to me: " + newMessageArrived.getReceiver().getCallSign() + "."); @@ -1421,7 +1421,7 @@ public class MessageBusManagementThread extends Thread { String originalMessage = newMessageArrived.getMessageText(); newMessageArrived .setMessageText("(>" + newMessageArrived.getReceiver().getCallSign() + ")" + originalMessage); - this.client.getLst_globalChatMessageList().add(0,newMessageArrived); + this.client.addChatMessage(newMessageArrived); // if you sent the message to another station, it will be sorted in to // the "to me message list" with modified messagetext, added rxers callsign @@ -1441,7 +1441,7 @@ public class MessageBusManagementThread extends Thread { newMessageArrived.getSender().setInAngleAndRange(false); } - this.client.getLst_globalChatMessageList().add(0, newMessageArrived); + this.client.addChatMessage(newMessageArrived); // System.out.println("MSGBS bgfx: tx call = " + newMessageArrived.getSender().getCallSign() + " / rx call = " + newMessageArrived.getReceiver().getCallSign()); } } catch (NullPointerException referenceDeletedByUserLeftChatDuringMessageprocessing) { @@ -1514,7 +1514,7 @@ public class MessageBusManagementThread extends Thread { for (int i = 0; i < 10; i++) { - client.getLst_globalChatMessageList().add(pwErrorMsg); + client.addChatMessage(pwErrorMsg); // client.getLst_toMeMessageList().add(pwErrorMsg); // client.getLst_toAllMessageList().add(pwErrorMsg); } diff --git a/src/main/java/kst4contest/utils/BoundedDequeObservableList.java b/src/main/java/kst4contest/utils/BoundedDequeObservableList.java new file mode 100644 index 0000000..5db1a6e --- /dev/null +++ b/src/main/java/kst4contest/utils/BoundedDequeObservableList.java @@ -0,0 +1,168 @@ +package kst4contest.utils; + +import javafx.collections.ObservableListBase; + +import java.util.Arrays; + +/** + * A bounded ObservableList backed by a circular buffer (ring buffer). + *

+ * Provides O(1) {@link #addFirst} and {@link #addLast} as well as O(1) + * random access via {@link #get}. When the list reaches {@code maxCapacity}, + * adding a new element at the front automatically evicts the oldest element + * at the back — and vice versa. + *

+ * This is a drop-in replacement for {@code FXCollections.observableArrayList()} + * wherever elements are prepended frequently, e.g. chat message lists. + */ +public class BoundedDequeObservableList extends ObservableListBase { + + private final int maxCapacity; + private final Object[] elements; + private int head = 0; + private int size = 0; + + public BoundedDequeObservableList(int maxCapacity) { + if (maxCapacity <= 0) throw new IllegalArgumentException("maxCapacity must be > 0"); + this.maxCapacity = maxCapacity; + this.elements = new Object[maxCapacity]; + } + + // ── read access ────────────────────────────────────────────────────────── + + @Override + public int size() { + return size; + } + + @Override + @SuppressWarnings("unchecked") + public E get(int index) { + checkIndex(index); + return (E) elements[physicalIndex(index)]; + } + + // ── O(1) deque operations ───────────────────────────────────────────────── + + /** + * Inserts {@code element} at index 0 (newest-first order). + * If the list is already at capacity the oldest element (last index) is + * removed first — both changes are reported as a single compound change. + */ + public void addFirst(E element) { + beginChange(); + if (size == maxCapacity) { + // evict last element + int lastPhysical = physicalIndex(size - 1); + @SuppressWarnings("unchecked") + E evicted = (E) elements[lastPhysical]; + elements[lastPhysical] = null; + size--; + nextRemove(size, evicted); // index after decrement == old last index + } + head = (head - 1 + maxCapacity) % maxCapacity; + elements[head] = element; + size++; + nextAdd(0, 1); + endChange(); + } + + /** + * Appends {@code element} at the last index (oldest-first order). + * If the list is already at capacity the newest element (index 0) is + * removed first. + */ + public void addLast(E element) { + beginChange(); + if (size == maxCapacity) { + // evict first element + @SuppressWarnings("unchecked") + E evicted = (E) elements[head]; + elements[head] = null; + head = (head + 1) % maxCapacity; + size--; + nextRemove(0, evicted); + } + elements[physicalIndex(size)] = element; + size++; + nextAdd(size - 1, size); + endChange(); + } + + // ── standard List mutation (O(n) — use addFirst/addLast for hot path) ───── + + @Override + public void add(int index, E element) { + if (index == 0) { + addFirst(element); + return; + } + if (index == size) { + addLast(element); + return; + } + checkIndexForAdd(index); + beginChange(); + if (size == maxCapacity) { + int lastPhysical = physicalIndex(size - 1); + @SuppressWarnings("unchecked") + E evicted = (E) elements[lastPhysical]; + elements[lastPhysical] = null; + size--; + nextRemove(size, evicted); + } + // shift elements [index .. size-1] one position towards the end + for (int i = size; i > index; i--) { + elements[physicalIndex(i)] = elements[physicalIndex(i - 1)]; + } + elements[physicalIndex(index)] = element; + size++; + nextAdd(index, index + 1); + endChange(); + } + + @Override + public E remove(int index) { + checkIndex(index); + beginChange(); + @SuppressWarnings("unchecked") + E removed = (E) elements[physicalIndex(index)]; + // shift elements [index+1 .. size-1] one position towards the front + for (int i = index; i < size - 1; i++) { + elements[physicalIndex(i)] = elements[physicalIndex(i + 1)]; + } + elements[physicalIndex(size - 1)] = null; + size--; + nextRemove(index, removed); + endChange(); + return removed; + } + + @Override + public E set(int index, E element) { + checkIndex(index); + beginChange(); + @SuppressWarnings("unchecked") + E old = (E) elements[physicalIndex(index)]; + elements[physicalIndex(index)] = element; + nextSet(index, old); + endChange(); + return old; + } + + // ── helpers ─────────────────────────────────────────────────────────────── + + private int physicalIndex(int virtualIndex) { + return (head + virtualIndex) % maxCapacity; + } + + private void checkIndex(int index) { + if (index < 0 || index >= size) + throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size); + } + + private void checkIndexForAdd(int index) { + if (index < 0 || index > size) + throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size); + } +}