mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 19:45:09 +00:00
553 lines
19 KiB
ObjectPascal
553 lines
19 KiB
ObjectPascal
{
|
||
Copyright (C)
|
||
2026 - Uladzimir Karpenka, EW8BAK
|
||
|
||
This program is free software; you can redistribute it and/or
|
||
modify it under the terms of the GNU General Public License
|
||
as published by the Free Software Foundation; either version 2
|
||
of the License, or (at your option) any later version.
|
||
|
||
This program is distributed in the hope that it will be useful,
|
||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
GNU General Public License for more details.
|
||
|
||
You should have received a copy of the GNU General Public License
|
||
along with this program; if not, write to the Free Software
|
||
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
||
}
|
||
|
||
unit DXSpotStore;
|
||
|
||
{
|
||
DXSpotStore.pas — потокобезопасное хранилище DX-спотов.
|
||
|
||
Единственная точка обмена между сетевым потоком кластера (TDXClusterClient,
|
||
пишет) и UI (оверлей спектра + окно списка, читают). Поэтому здесь НЕТ ни
|
||
Synchronize, ни очередей в главный поток: поток кластера кладёт спот прямо
|
||
сюда под критической секцией, а UI замечает изменение по монотонному
|
||
счётчику Version — ровно так же, как оверлеи замечают смену вида по своим
|
||
ключам кэша. Никаких LCL-зависимостей (юнит компилируется и в headless).
|
||
|
||
Дедуп: спот с тем же позывным в пределах DEDUP_HZ считается тем же самым —
|
||
обновляем частоту/время/комментарий, а не плодим строки. Тот же позывной на
|
||
другом диапазоне — отдельная запись.
|
||
|
||
TTL: споты старше FTTLMinutes выбрасываются при каждой правке/снимке.
|
||
}
|
||
|
||
{$IFDEF FPC}
|
||
{$MODE Delphi}
|
||
{$LONGSTRINGS ON}
|
||
{$ENDIF}
|
||
|
||
interface
|
||
|
||
uses
|
||
SysUtils, Classes, SyncObjs, DateUtils, StrUtils; // StrUtils — PosEx
|
||
|
||
const
|
||
DX_DEDUP_HZ = 5000.0; // тот же позывной в этом окне = тот же спот
|
||
DX_DEFAULT_TTL = 30; // мин
|
||
DX_DEFAULT_MAX = 500; // потолок записей
|
||
|
||
type
|
||
// Мода спота. Определяется по комментарию кластера (FT8/CW/…), при неудаче —
|
||
// остаётся dxmUnknown: гадать по частоте не пытаемся, бэндпланы разные.
|
||
TDXMode = (dxmUnknown, dxmCW, dxmSSB, dxmDigi, dxmFT8, dxmFT4, dxmRTTY,
|
||
dxmPSK, dxmFM, dxmSSTV);
|
||
|
||
TDXModeSet = set of TDXMode;
|
||
|
||
TDXSpot = record
|
||
FreqHz: Double; // частота DX-станции, Гц (кластер отдаёт кГц)
|
||
Call: string; // позывной DX
|
||
Spotter: string; // кто заспотил
|
||
Comment: string; // комментарий кластера (уже без времени)
|
||
TimeUTC: string; // 'HHMM' как пришло от кластера
|
||
Stamp: TDateTime; // локальное время приёма — для TTL и затухания
|
||
Mode: TDXMode;
|
||
ModeGuessed: Boolean; // мода не из комментария, а из бэндплана (см. ниже)
|
||
end;
|
||
|
||
TDXSpotArray = array of TDXSpot;
|
||
|
||
TDXSpotStore = class
|
||
private
|
||
FLock: TCriticalSection;
|
||
FSpots: TDXSpotArray;
|
||
FCount: Integer;
|
||
FVersion: Int64;
|
||
FTTLMinutes: Integer;
|
||
FMaxSpots: Integer;
|
||
procedure PurgeLocked;
|
||
procedure DropOldestLocked;
|
||
function GetTTLMinutes: Integer;
|
||
procedure SetTTLMinutes(V: Integer);
|
||
function GetMaxSpots: Integer;
|
||
procedure SetMaxSpots(V: Integer);
|
||
public
|
||
constructor Create;
|
||
destructor Destroy; override;
|
||
|
||
// Добавить/обновить спот (вызывается из потока кластера).
|
||
procedure Add(const S: TDXSpot);
|
||
// Убрать все споты позывного (SPOT_DELETE в TCI; регистр не важен).
|
||
procedure RemoveCall(const Call: string);
|
||
procedure Clear;
|
||
// Выбросить просроченное прямо сейчас. Нужен тому, кто следит за временем
|
||
// снаружи: сам стор чистится только на Add и снимках, а когда споты никто
|
||
// не берёт и не приходит новых, устаревать им иначе негде.
|
||
procedure Purge;
|
||
|
||
// Снимок всех спотов, отсортированный по частоте.
|
||
function Snapshot(out Arr: TDXSpotArray): Integer;
|
||
// Снимок окна [LoHz..HiHz] с фильтром по моде (Modes = [] — без фильтра)
|
||
// и по возрасту (MaxAgeMin <= 0 — только общий TTL). Для оверлея.
|
||
function SnapshotRange(LoHz, HiHz: Double; Modes: TDXModeSet;
|
||
MaxAgeMin: Integer; out Arr: TDXSpotArray): Integer;
|
||
|
||
// Монотонный счётчик правок — ключ инвалидации кэша оверлея/списка.
|
||
function Version: Int64;
|
||
function Count: Integer;
|
||
|
||
// Читаются сетевым потоком (PurgeLocked/Add), пишутся из UI — только через
|
||
// критическую секцию, как и всё остальное состояние стора.
|
||
property TTLMinutes: Integer read GetTTLMinutes write SetTTLMinutes;
|
||
property MaxSpots: Integer read GetMaxSpots write SetMaxSpots;
|
||
end;
|
||
|
||
// Мода по комментарию кластера ('FT8 -12 dB' → dxmFT8). Пусто/непонятно —
|
||
// dxmUnknown.
|
||
function DXModeFromComment(const Comment: string): TDXMode;
|
||
// Мода по участку бэндплана — запасной путь, когда комментарий молчит (в CW и
|
||
// SSB его сплошь и рядом просто не пишут). dxmUnknown = участок спорный или
|
||
// вне таблицы; см. DX_BAND_PLAN о том, где мы намеренно не гадаем.
|
||
function DXModeFromFreq(FreqHz: Double): TDXMode;
|
||
function DXModeName(M: TDXMode): string;
|
||
|
||
implementation
|
||
|
||
const
|
||
MODE_NAMES: array[TDXMode] of string =
|
||
('', 'CW', 'SSB', 'DIGI', 'FT8', 'FT4', 'RTTY', 'PSK', 'FM', 'SSTV');
|
||
|
||
function DXModeName(M: TDXMode): string;
|
||
begin
|
||
Result := MODE_NAMES[M];
|
||
end;
|
||
|
||
function DXModeFromComment(const Comment: string): TDXMode;
|
||
// Ищем маркер моды как ОТДЕЛЬНОЕ слово: 'CW' внутри 'CWOPS' или позывного
|
||
// модой не является. Порядок проверки — от длинных/специфичных к общим.
|
||
var
|
||
U: string;
|
||
|
||
function HasWord(const W: string): Boolean;
|
||
var P, L, N: Integer; OkL, OkR: Boolean;
|
||
begin
|
||
Result := False;
|
||
L := Length(W); N := Length(U);
|
||
P := Pos(W, U);
|
||
while P > 0 do
|
||
begin
|
||
OkL := (P = 1) or not (U[P-1] in ['A'..'Z', '0'..'9', '/', '-']);
|
||
OkR := (P + L > N) or not (U[P+L] in ['A'..'Z', '0'..'9', '/', '-']);
|
||
if OkL and OkR then Exit(True);
|
||
P := PosEx(W, U, P + 1);
|
||
end;
|
||
end;
|
||
|
||
begin
|
||
Result := dxmUnknown;
|
||
if Comment = '' then Exit;
|
||
U := UpperCase(Comment);
|
||
if HasWord('FT8') then Result := dxmFT8
|
||
else if HasWord('FT4') then Result := dxmFT4
|
||
else if HasWord('RTTY') then Result := dxmRTTY
|
||
else if HasWord('PSK') or HasWord('PSK31') or HasWord('BPSK') then Result := dxmPSK
|
||
else if HasWord('SSTV') then Result := dxmSSTV
|
||
else if HasWord('CW') then Result := dxmCW
|
||
else if HasWord('SSB') or HasWord('USB') or HasWord('LSB') then Result := dxmSSB
|
||
else if HasWord('FM') then Result := dxmFM
|
||
else if HasWord('JT65') or HasWord('JT9') or HasWord('JS8') or
|
||
HasWord('MSK144') or HasWord('OLIVIA') or HasWord('DIGI') then
|
||
Result := dxmDigi;
|
||
end;
|
||
|
||
{ ── Мода по бэндплану ────────────────────────────────────────────────────── }
|
||
|
||
type
|
||
TDXBandSeg = record
|
||
LoKHz, HiKHz: Double; // [Lo; Hi)
|
||
Mode: TDXMode;
|
||
end;
|
||
|
||
const
|
||
// Участки бэндплана для запасного определения моды. Читается СВЕРХУ ВНИЗ,
|
||
// первое попадание выигрывает — поэтому узкие «водопои» цифры стоят раньше
|
||
// широких сегментов, внутрь которых они попадают (FT8 на 7074 живёт посреди
|
||
// телефонного участка R1, FT4 на 21140 — посреди 15-метрового).
|
||
//
|
||
// ★ Границы — по плану IARU Region 1 (наш регион): у R2/R3 они другие, и
|
||
// спорные куски мы НАМЕРЕННО оставляем пустыми, а не гадаем — пропуск честнее
|
||
// ошибки. Отсюда дырки: 1843-1850 (R1 SSB против R2 CW), 7053-7125 у R2 —
|
||
// данные, а не телефон, и т.п. Мода из комментария всегда важнее этой
|
||
// таблицы, сюда попадают только споты, где комментарий промолчал.
|
||
DX_BAND_PLAN: array[0..47] of TDXBandSeg = (
|
||
// ── узкие окна FT8/FT4 (перекрывают широкие сегменты ниже) ──
|
||
(LoKHz: 1840.0; HiKHz: 1843.0; Mode: dxmFT8),
|
||
(LoKHz: 3573.0; HiKHz: 3576.0; Mode: dxmFT8),
|
||
(LoKHz: 7047.0; HiKHz: 7049.0; Mode: dxmFT4),
|
||
(LoKHz: 7074.0; HiKHz: 7078.0; Mode: dxmFT8),
|
||
(LoKHz: 10136.0; HiKHz: 10139.0; Mode: dxmFT8),
|
||
(LoKHz: 10140.0; HiKHz: 10142.0; Mode: dxmFT4),
|
||
(LoKHz: 14074.0; HiKHz: 14078.0; Mode: dxmFT8),
|
||
(LoKHz: 14080.0; HiKHz: 14082.0; Mode: dxmFT4),
|
||
(LoKHz: 18100.0; HiKHz: 18102.0; Mode: dxmFT8),
|
||
(LoKHz: 18104.0; HiKHz: 18106.0; Mode: dxmFT4),
|
||
(LoKHz: 21074.0; HiKHz: 21078.0; Mode: dxmFT8),
|
||
(LoKHz: 21140.0; HiKHz: 21142.0; Mode: dxmFT4),
|
||
(LoKHz: 24915.0; HiKHz: 24917.0; Mode: dxmFT8),
|
||
(LoKHz: 24919.0; HiKHz: 24921.0; Mode: dxmFT4),
|
||
(LoKHz: 28074.0; HiKHz: 28078.0; Mode: dxmFT8),
|
||
(LoKHz: 28180.0; HiKHz: 28182.0; Mode: dxmFT4),
|
||
(LoKHz: 50313.0; HiKHz: 50323.0; Mode: dxmFT8),
|
||
(LoKHz: 144174.0; HiKHz: 144180.0; Mode: dxmFT8),
|
||
(LoKHz: 432174.0; HiKHz: 432180.0; Mode: dxmFT8),
|
||
|
||
// ── КВ, широкие сегменты ──
|
||
(LoKHz: 1800.0; HiKHz: 1838.0; Mode: dxmCW),
|
||
(LoKHz: 1838.0; HiKHz: 1843.0; Mode: dxmDigi),
|
||
(LoKHz: 1850.0; HiKHz: 2000.0; Mode: dxmSSB),
|
||
|
||
(LoKHz: 3500.0; HiKHz: 3570.0; Mode: dxmCW),
|
||
(LoKHz: 3570.0; HiKHz: 3600.0; Mode: dxmDigi),
|
||
(LoKHz: 3600.0; HiKHz: 4000.0; Mode: dxmSSB),
|
||
|
||
(LoKHz: 7000.0; HiKHz: 7040.0; Mode: dxmCW),
|
||
(LoKHz: 7040.0; HiKHz: 7053.0; Mode: dxmDigi),
|
||
(LoKHz: 7053.0; HiKHz: 7300.0; Mode: dxmSSB),
|
||
|
||
(LoKHz: 10100.0; HiKHz: 10130.0; Mode: dxmCW),
|
||
(LoKHz: 10130.0; HiKHz: 10150.0; Mode: dxmDigi),
|
||
|
||
(LoKHz: 14000.0; HiKHz: 14070.0; Mode: dxmCW),
|
||
(LoKHz: 14070.0; HiKHz: 14099.0; Mode: dxmDigi),
|
||
(LoKHz: 14101.0; HiKHz: 14350.0; Mode: dxmSSB),
|
||
|
||
(LoKHz: 18068.0; HiKHz: 18095.0; Mode: dxmCW),
|
||
(LoKHz: 18095.0; HiKHz: 18109.0; Mode: dxmDigi),
|
||
(LoKHz: 18111.0; HiKHz: 18168.0; Mode: dxmSSB),
|
||
|
||
(LoKHz: 21000.0; HiKHz: 21070.0; Mode: dxmCW),
|
||
(LoKHz: 21070.0; HiKHz: 21120.0; Mode: dxmDigi),
|
||
(LoKHz: 21151.0; HiKHz: 21450.0; Mode: dxmSSB),
|
||
|
||
(LoKHz: 24890.0; HiKHz: 24915.0; Mode: dxmCW),
|
||
(LoKHz: 24915.0; HiKHz: 24929.0; Mode: dxmDigi),
|
||
(LoKHz: 24931.0; HiKHz: 24990.0; Mode: dxmSSB),
|
||
|
||
(LoKHz: 28000.0; HiKHz: 28070.0; Mode: dxmCW),
|
||
(LoKHz: 28070.0; HiKHz: 28190.0; Mode: dxmDigi),
|
||
(LoKHz: 28300.0; HiKHz: 29100.0; Mode: dxmSSB),
|
||
(LoKHz: 29100.0; HiKHz: 29700.0; Mode: dxmFM),
|
||
|
||
// ── УКВ (минимум, где регионы согласны) ──
|
||
(LoKHz: 50000.0; HiKHz: 50100.0; Mode: dxmCW),
|
||
(LoKHz: 50100.0; HiKHz: 50500.0; Mode: dxmSSB));
|
||
|
||
function DXModeFromFreq(FreqHz: Double): TDXMode;
|
||
var
|
||
K: Double;
|
||
i: Integer;
|
||
begin
|
||
Result := dxmUnknown;
|
||
if FreqHz <= 0 then Exit;
|
||
|
||
// QO-100 (узкополосный транспондер, DOWNLINK). Отдельно от КВ-таблицы: план
|
||
// AMSAT-DL, границы те же, что рисует BandPlanOverlay. Маяки (…500-505 и
|
||
// …745-755) и «mixed modes» (…850-990) — не гадаем.
|
||
K := FreqHz / 1000.0;
|
||
if (K >= 10489500.0) and (K < 10490000.0) then
|
||
begin
|
||
if (K >= 10489505.0) and (K < 10489540.0) then Result := dxmCW
|
||
else if (K >= 10489540.0) and (K < 10489650.0) then Result := dxmDigi
|
||
else if (K >= 10489650.0) and (K < 10489745.0) then Result := dxmSSB
|
||
else if (K >= 10489755.0) and (K < 10489850.0) then Result := dxmSSB;
|
||
Exit;
|
||
end;
|
||
|
||
for i := Low(DX_BAND_PLAN) to High(DX_BAND_PLAN) do
|
||
if (K >= DX_BAND_PLAN[i].LoKHz) and (K < DX_BAND_PLAN[i].HiKHz) then
|
||
Exit(DX_BAND_PLAN[i].Mode);
|
||
end;
|
||
|
||
{ ── TDXSpotStore ─────────────────────────────────────────────────────────── }
|
||
|
||
constructor TDXSpotStore.Create;
|
||
begin
|
||
inherited Create;
|
||
FLock := TCriticalSection.Create;
|
||
FCount := 0;
|
||
FVersion := 0;
|
||
FTTLMinutes := DX_DEFAULT_TTL;
|
||
FMaxSpots := DX_DEFAULT_MAX;
|
||
SetLength(FSpots, DX_DEFAULT_MAX);
|
||
end;
|
||
|
||
destructor TDXSpotStore.Destroy;
|
||
begin
|
||
FLock.Free;
|
||
inherited Destroy;
|
||
end;
|
||
|
||
procedure TDXSpotStore.PurgeLocked;
|
||
// Выбрасывает споты старше TTL, уплотняя массив на месте.
|
||
var
|
||
i, Dst: Integer;
|
||
Cutoff: TDateTime;
|
||
begin
|
||
if FTTLMinutes <= 0 then Exit;
|
||
Cutoff := IncMinute(Now, -FTTLMinutes);
|
||
Dst := 0;
|
||
for i := 0 to FCount - 1 do
|
||
if FSpots[i].Stamp >= Cutoff then
|
||
begin
|
||
if Dst <> i then FSpots[Dst] := FSpots[i];
|
||
Inc(Dst);
|
||
end;
|
||
if Dst <> FCount then
|
||
begin
|
||
FCount := Dst;
|
||
Inc(FVersion);
|
||
end;
|
||
end;
|
||
|
||
procedure TDXSpotStore.DropOldestLocked;
|
||
var i, Oldest: Integer;
|
||
begin
|
||
if FCount <= 0 then Exit;
|
||
Oldest := 0;
|
||
for i := 1 to FCount - 1 do
|
||
if FSpots[i].Stamp < FSpots[Oldest].Stamp then Oldest := i;
|
||
for i := Oldest to FCount - 2 do FSpots[i] := FSpots[i + 1];
|
||
Dec(FCount);
|
||
end;
|
||
|
||
procedure TDXSpotStore.Add(const S: TDXSpot);
|
||
var
|
||
i: Integer;
|
||
UCall: string;
|
||
begin
|
||
if S.Call = '' then Exit;
|
||
FLock.Enter;
|
||
try
|
||
PurgeLocked;
|
||
UCall := UpperCase(S.Call);
|
||
for i := 0 to FCount - 1 do
|
||
if (UpperCase(FSpots[i].Call) = UCall) and
|
||
(Abs(FSpots[i].FreqHz - S.FreqHz) <= DX_DEDUP_HZ) then
|
||
begin
|
||
FSpots[i] := S; // тот же спот — обновляем целиком
|
||
Inc(FVersion);
|
||
Exit;
|
||
end;
|
||
if Length(FSpots) < FMaxSpots then SetLength(FSpots, FMaxSpots);
|
||
while FCount >= FMaxSpots do DropOldestLocked;
|
||
FSpots[FCount] := S;
|
||
Inc(FCount);
|
||
Inc(FVersion);
|
||
finally
|
||
FLock.Leave;
|
||
end;
|
||
end;
|
||
|
||
procedure TDXSpotStore.Purge;
|
||
begin
|
||
FLock.Enter;
|
||
try
|
||
PurgeLocked; // сам поднимет Version, если что-то выбросил
|
||
finally
|
||
FLock.Leave;
|
||
end;
|
||
end;
|
||
|
||
procedure TDXSpotStore.Clear;
|
||
begin
|
||
FLock.Enter;
|
||
try
|
||
if FCount = 0 then Exit;
|
||
FCount := 0;
|
||
Inc(FVersion);
|
||
finally
|
||
FLock.Leave;
|
||
end;
|
||
end;
|
||
|
||
procedure TDXSpotStore.RemoveCall(const Call: string);
|
||
var i, j: Integer; Up: string;
|
||
begin
|
||
Up := UpperCase(Trim(Call));
|
||
if Up = '' then Exit;
|
||
FLock.Enter;
|
||
try
|
||
i := 0;
|
||
while i < FCount do
|
||
if UpperCase(FSpots[i].Call) = Up then
|
||
begin
|
||
for j := i to FCount - 2 do FSpots[j] := FSpots[j + 1];
|
||
Dec(FCount);
|
||
Inc(FVersion);
|
||
end
|
||
else
|
||
Inc(i);
|
||
finally
|
||
FLock.Leave;
|
||
end;
|
||
end;
|
||
|
||
function CompareSpotFreq(const A, B: TDXSpot): Integer;
|
||
begin
|
||
if A.FreqHz < B.FreqHz then Result := -1
|
||
else if A.FreqHz > B.FreqHz then Result := 1
|
||
else Result := 0;
|
||
end;
|
||
|
||
procedure SortByFreq(var Arr: TDXSpotArray; N: Integer);
|
||
// Вставками: N здесь десятки-сотни и массив почти всегда уже почти
|
||
// отсортирован (споты приходят вперемешку, но окно узкое).
|
||
var i, j: Integer; T: TDXSpot;
|
||
begin
|
||
for i := 1 to N - 1 do
|
||
begin
|
||
T := Arr[i];
|
||
j := i - 1;
|
||
while (j >= 0) and (CompareSpotFreq(Arr[j], T) > 0) do
|
||
begin
|
||
Arr[j + 1] := Arr[j];
|
||
Dec(j);
|
||
end;
|
||
Arr[j + 1] := T;
|
||
end;
|
||
end;
|
||
|
||
function TDXSpotStore.Snapshot(out Arr: TDXSpotArray): Integer;
|
||
var i: Integer;
|
||
begin
|
||
FLock.Enter;
|
||
try
|
||
PurgeLocked;
|
||
SetLength(Arr, FCount);
|
||
for i := 0 to FCount - 1 do Arr[i] := FSpots[i];
|
||
Result := FCount;
|
||
finally
|
||
FLock.Leave;
|
||
end;
|
||
SortByFreq(Arr, Result);
|
||
end;
|
||
|
||
function TDXSpotStore.SnapshotRange(LoHz, HiHz: Double; Modes: TDXModeSet;
|
||
MaxAgeMin: Integer; out Arr: TDXSpotArray): Integer;
|
||
var
|
||
i, N: Integer;
|
||
Cutoff: TDateTime;
|
||
UseAge: Boolean;
|
||
begin
|
||
N := 0;
|
||
UseAge := MaxAgeMin > 0;
|
||
Cutoff := 0;
|
||
if UseAge then Cutoff := IncMinute(Now, -MaxAgeMin);
|
||
FLock.Enter;
|
||
try
|
||
PurgeLocked;
|
||
SetLength(Arr, FCount);
|
||
for i := 0 to FCount - 1 do
|
||
begin
|
||
if (FSpots[i].FreqHz < LoHz) or (FSpots[i].FreqHz > HiHz) then Continue;
|
||
if (Modes <> []) and not (FSpots[i].Mode in Modes) then Continue;
|
||
if UseAge and (FSpots[i].Stamp < Cutoff) then Continue;
|
||
Arr[N] := FSpots[i];
|
||
Inc(N);
|
||
end;
|
||
SetLength(Arr, N);
|
||
Result := N;
|
||
finally
|
||
FLock.Leave;
|
||
end;
|
||
SortByFreq(Arr, Result);
|
||
end;
|
||
|
||
function TDXSpotStore.GetTTLMinutes: Integer;
|
||
begin
|
||
FLock.Enter;
|
||
try
|
||
Result := FTTLMinutes;
|
||
finally
|
||
FLock.Leave;
|
||
end;
|
||
end;
|
||
|
||
procedure TDXSpotStore.SetTTLMinutes(V: Integer);
|
||
begin
|
||
if V < 1 then V := 1;
|
||
FLock.Enter;
|
||
try
|
||
if FTTLMinutes = V then Exit;
|
||
FTTLMinutes := V;
|
||
PurgeLocked; // укоротили TTL — лишнее выбрасываем сразу
|
||
finally
|
||
FLock.Leave;
|
||
end;
|
||
end;
|
||
|
||
function TDXSpotStore.GetMaxSpots: Integer;
|
||
begin
|
||
FLock.Enter;
|
||
try
|
||
Result := FMaxSpots;
|
||
finally
|
||
FLock.Leave;
|
||
end;
|
||
end;
|
||
|
||
procedure TDXSpotStore.SetMaxSpots(V: Integer);
|
||
begin
|
||
if V < 16 then V := 16;
|
||
FLock.Enter;
|
||
try
|
||
if FMaxSpots = V then Exit;
|
||
FMaxSpots := V;
|
||
if Length(FSpots) < FMaxSpots then SetLength(FSpots, FMaxSpots);
|
||
while FCount > FMaxSpots do
|
||
begin
|
||
DropOldestLocked;
|
||
Inc(FVersion);
|
||
end;
|
||
finally
|
||
FLock.Leave;
|
||
end;
|
||
end;
|
||
|
||
function TDXSpotStore.Version: Int64;
|
||
begin
|
||
FLock.Enter;
|
||
try
|
||
Result := FVersion;
|
||
finally
|
||
FLock.Leave;
|
||
end;
|
||
end;
|
||
|
||
function TDXSpotStore.Count: Integer;
|
||
begin
|
||
FLock.Enter;
|
||
try
|
||
Result := FCount;
|
||
finally
|
||
FLock.Leave;
|
||
end;
|
||
end;
|
||
|
||
end.
|